C语言基本功修炼秘籍第3节,指针与字符串的关系,不能向指针直接拷贝数据的,因为指针没有“地盘”存放数据

指针是C语言基础语法的一部分,所以每一个合格的C语言程序员都应该掌握。但是不可否认的是,对于很多初学者来说,C语言的指针语法的确比较难理解。

虽然指针的概念一句话就可以描述,但是在实际的C语言程序开发中,指针的应用却是非常广泛的,这一点看过我之前文章的读者应该是比较清楚的。所以,本节将从两段简单的C语言代码出发,尝试讨论一下C语言初学者常会感到费解的指针应用。

请看下面这段C语言代码

这段代码非常简单,无非就是定义了一个 char 型指针 p,并且令其指向“hello world”,以及调用 strcpy() 函数将“hello world”拷贝给 p。那么这段C语言代码有什么问题吗?

# include <string.h>

int main()
{
    char *p = NULL;
    /** 下面两句代码有什么问题? */
    p = "hello world";
    strcpy(p, "hello world");

    return 0;
}


动手欲强的读者,应该已经尝试编译这段C语言代码了。如果使用的编译器是 gcc,会发现即使添加 -Wall 选项,编译器也不会发出任何警报的:

# gcc t.c -Wall
# ls
a.out  t.c

可见,这段C语言代码是没有语法错误的,gcc 编译器能够生成可执行文件 a.out 。但是,当尝试执行 a.out 时,发现C语言程序出现了“Segmentation fault”。

# ./a.out 
Segmentation fault

这是怎么回事呢?真正可能导致错误的只有第 7,第 8 两行代码,读者可以自行使用 gdb 工具,或者添加打印语句定位错误,应该能够发现第 7 行的赋值语句没有问题,导致段错误的是 strcpy() 函数,为什么呢?在语句:

p = "hello world";

中,字符串“hello world”是常量,它的地址在编译阶段就被确定下来,并且存储在常量段里。因此实际上,此处的“hello world”在程序内存中,是有“自己的地盘”的,这里的赋值语句,仅仅是告诉 p 它的“地盘”在哪里,之后可以使用 p 访问它而已。

对于 strcpy(p, "hello world"); 语句,strcpy() 会尝试将“hello world”放入 p 的“地盘”,但是 p 只是一个指针,它并没有“自己的地盘”。不过 strcpy() 可不管这些,它会尝试将数据全部塞入 p 指向的地址段。在本例中,p 指向的地址段存放的是常量“hello world”,常量是只读的,所以 strcpy() 实际上是在尝试往只读内存区域写入数据,操作系统当然不允许,只能报错处理了。

了解了这一点,再看下面就不难了

编写 myprint() 函数,接收两个参数,分别是字符串数目,和字符串组,相关C语言代码如下,请看:

void myprint(int argc, char *argv[])
{
    int i;
    for(i=0; i<argc; i++){
        printf("argv[%d]: %s\n", i, argv[i]);
    }
}

myprint() 函数的参数与标准 main() 函数的原型是一致的,编写 main() 函数调用 myprint() 函数,相关C语言代码如下,请看:

int main(int argc, char *argv[])
{
    myprint(argc, argv);
    return 0:
}


编译这段C语言代码,可得到可执行文件,在执行时指定参数,得到如下输出:

# gcc t.c
# ./a.out 
argv[0]: ./a.out
# ./a.out hello world
argv[0]: ./a.out
argv[1]: hello
argv[2]: world

现在以两种方式存储字符串组,并分别调用 myprint() 函数,相关C语言代码如下,请看:

char *strs1[2];
char strs2[2][128];

strs1[0] = "1 hello";
strs1[1] = "world";

strcpy(strs2[0], "2 hello");
strcpy(strs2[1], "world");

myprint(2, strs1);
myprint(2, strs2);


这段C语言代码有什么问题呢?如果读者尝试使用 gcc 编译这段C语言程序,应该能够发现是有警告的:

提示 myprint(2, strs2); 中 str2 与函数参数类型不符。要是不管这个警告,直接执行程序,则会得到如下输出:

# ./a.out 
argv[0]: 1 hello
argv[1]: world
Segmentation fault

显然,C语言程序在执行 myprint(2, strs2); 时出现了段错误。根据编译器的提示信息可以知道原因:myprint() 函数在处理 strs2 时,会在内部将其转换为 char ** 型。对于 myprint() 函数来说:

void myprint(int argc, char *argv[]);

形参 argv 接收到的参数实际上是 strs2 的地址。接着 myprint() 函数会从 strs2 的地址处取数据使用,这显然是不合理的。

而 myprint(2, strs1); 则是正常的。这是因为 strs1 是一个指针数组,自然允许被 myprint() 使用。不过,strs1 作为指针数组,它的每一个元素实际上就是指针而已,让其指向常量数组自然没有什么问题,但是若是希望在程序运行过程中,动态的向其拷贝字符串,必定会引发段错误,这一点在上一个例子中已经解释清楚。

所以,为了既方便 myprint() 函数使用,又方便在C语言程序运行过程中动态拷贝,可以将 str1 和 strs2 结合起来使用:strs2 有属于自己的“地盘”,因此可以动态拷贝,而 strs1 本质上是指针,便于 myprint() 使用。例如:

char *strs1[2];
char strs2[2][128];

strs1[0] = strs2[0];
strs1[1] = strs2[1];
/** 动态拷贝 */
strcpy(strs2[0], "2 hello");
strcpy(strs2[1], "world");

myprint(2, strs1);


编译并执行这段C语言代码,可以得到如下输出:

# gcc t.c -g
# ./a.out 
argv[0]: 2 hello
argv[1]: world

小结

本节通过两段简单的C语言代码实例,讨论了指针与字符串的关系。可见,指针本身的作用只是为了索引数据,C语言程序在处理指针时,实际上处理的是指针指向的数据。我们并不能直接向指针拷贝数据,因为指针本身是没有自己的“地盘”的。

阅读更多:   C语言
添加新评论

icon_redface.gificon_idea.gificon_cool.gif2016kuk.gificon_mrgreen.gif2016shuai.gif2016tp.gif2016db.gif2016ch.gificon_razz.gif2016zj.gificon_sad.gificon_cry.gif2016zhh.gificon_question.gif2016jk.gif2016bs.gificon_lol.gif2016qiao.gificon_surprised.gif2016fendou.gif2016ll.gif