11

C语言基本功修炼秘籍第3节,指针与字符串的关系,不能向指针直接拷贝数据的,因为指针...

 3 years ago
source link: https://blog.popkx.com/c%E8%AF%AD%E8%A8%80%E5%9F%BA%E6%9C%AC%E5%8A%9F%E4%BF%AE%E7%82%BC%E7%A7%98%E7%B1%8D%E7%AC%AC3%E8%8A%82-%E6%8C%87%E9%92%88%E4%B8%8E%E5%AD%97%E7%AC%A6%E4%B8%B2%E7%9A%84%E5%85%B3%E7%B3%BB/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

指针是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;
}
529c717df75d06cd171dac8e96869a9c.png

动手欲强的读者,应该已经尝试编译这段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:
}
50d8a1e75f3c0d081c77cca160c4273b.png

编译这段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);
c84d390b38c7420de530b7476971e5b1.png

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

提示 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);
2bc47d3df99280ebe01377ce424f1e03.png

编译并执行这段C语言代码,可以得到如下输出:
# gcc t.c -g
# ./a.out 
argv[0]: 2 hello
argv[1]: world

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK