我们先看第一段代码
1 |
void foo()
{ |
2 |
char *a= "abcdefg" ; |
3 |
a[2]= 'x' ; |
4 |
} |
5 |
6 |
int main()
{ |
7 |
foo(); |
8 |
return 0; |
9 |
} |
用gcc编译执行后,报“段错误”(Segment fault)。如果你在windows下执行的话,那就会弹出一个非法操作的对话框了。
第二段代码
1 |
void foo()
{ |
2 |
char a[]= "abcdefg" ; |
3 |
a[2]= 'x' ; |
4 |
} |
5 |
6 |
int main()
{ |
7 |
foo(); |
8 |
return 0; |
9 |
} |
用gcc编译执行后一切正常。
那么char *a="abcdefg"和char a[]="abcdefg"究竟有啥区别呢?为啥第一段代码会出段错误呢。
我们把第一段代码反汇编来看一下(foo函数),反汇编的命令是objdump -d exec_file_name
我们可以看到,对于char *a="abcdefg",代码中会把地址0x8048480赋值给a,然后给a[2]赋值。那么这个0x8048480地址,我们猜一下也知道,肯定是"abcdefg"所在的位置了。那段错误是如何产生的呢?
原因在于,"abcdefg"被编译器放到了.rodata段,这个段的属性是readonly的,因此当我们要给a[2]赋值时,就会看到段错误了。
我们使用objdump -s exec_file_name命令可以看到可执行文件中的所有段,下面是.rodata的内容,可以看到"abcdefg"就在里面(0x8048478表示这一行的地址,61正好在0x8048478+8=0x8048480位置)。
Contents of section .rodata:
那么对于char a[]="abcdefg"又是什么情况呢?
我们也来反汇编一下。
这里有两个地址0x8048500和0x8048504,如果看不懂汇编也关系,事实上这几行的意思是从这两个地址把数据拷贝给数组a。显然a的内存空间在栈上,那么这当然是可以写的啦,所以就不会出错了。也就是说,对于char a[]="abcdefg",编译器会生成一段代码,把数据从.rodata这个只读的段,拷贝到a的数组中。这里附上.rodata的内容,大家可以算算地址。
还有点东西,如果你把char a[]="abcdefg"改成char a[]="testtest",反汇编出来的又会是不一样的结果。
这里直接将两个常量0x74736574赋值给数组a了,0x74736574翻译成ascii正好是test(注意字节序,这里是little endian)。
其实可能还有其他的初始化数组a的情况,也不必去细究细节。
关键是要明白一点,写char *a=“abcdefg”的话,a指针指向的事实上是只读的区域,因此,更准确的写法是const char *a="abcdefg"。
而char a[]=“abcdefg”,编译器会生成一段初始化数组的代码,但是数组a的内容是可以修改的。如果不需要修改数组a的内容,写成const char *a="abcdefg"会有效率上的提高。
最后感慨一下,C/C++是一个大坑,没有多年的编程积累和对操作系统及编译器等底层的了解,是很难真正掌握的。