wuhuaji | blog

C拾遗:字符串常量在内存中如何存储?

2019/07/02 Share

碰到这样一个问题,代码简化之后如下:

1
2
3
4
5
6
7
8
9
#include <stdio.h>
char *get_string(){
char *string = "hello world";
return string;
}
int main(int argc, char *argv[]){
char *result = get_string();
printf("%s\n",result); //会输出:hello world
}

代码很简单,在 main 中调用 get_string 函数,这个函数返回一个指向”hello world”字符串的指针,打印出这个字符串,能输出helloworld

这里就有一个问题,推理如下:

  • 每一个函数调用,就会为这个函数分配栈内存,所谓栈帧,这块内存用于存放函数内的局部变量、实参等,函数返回这块内存是会就会被回收。
  • get_string函数调用时,就会在其栈帧中分配一块内存用于存放”hello world”,然后把这块内存的首地址赋值给string.
  • get_string 函数返回string, 实际上就是将存放”helloworld”的这块内存地址返回赋值给main中的result变量。
  • 这时虽然result指向原先”hello world”的内存地址,但get_string的栈帧已经被回收了,helloworld都不存在了,怎么还能打印出helloworld呢?

真是百思不得其解,为了验证上面的推理没有错,稍微改动一下代码,做一下实验:

1
2
3
4
5
6
7
8
9
int *get_int(){
int a = 100;
return &a;
}

int main(int argc, char *argv[]){
int *result = get_int();
printf("%d\n",*result);
}

这里我把char换成了int来实验。声明了一个int a = 100;然会返回a的地址。果不其然编译时就收获了一个warning,警告我们返回了一个函数内局部变量的地址。

1
2
warning: function returns address of local variable [-Wreturn-local-addr]
return &a;

忽略这个警告运行:收获了一个segmentation falut。说明前面的推理是没问题的,函数栈已经被回收了,再试图去访问里面的内存,当然会报错了。

1
2
$ ./a.out
[1] 60602 segmentation fault (core dumped) ./a.out

查阅资料而得:“hello world”不是普通变量,而是字符串常量,字符串常量不是存在函数栈帧中,而是存在内存中的:只读数据段

再做一个实验:
还是最开始的代码:

1
2
3
4
5
6
7
8
9
#include <stdio.h>
char *get_string(){
char *string = "hello world";
return string;
}
int main(int argc, char *argv[]){
char *result = get_string();
printf("%s\n",result); //会输出:hello world
}

这里我们对其汇编,也就是执行:gcc -c test.c,让其生成目标文件,也就是未进行链接步骤。这里会生成目标文件:test.o
我们利用 objdump工具查看目标文件里的内容:objdump -s test.o, -s 参数表示显示所有段的内容,得到如下结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
test.o:     file format elf64-x86-64

Contents of section .text:
0000 554889e5 48c745f8 00000000 488b45f8 UH..H.E.....H.E.
0010 5dc35548 89e54883 ec20897d ec488975 ].UH..H.. .}.H.u
0020 e0b80000 0000e800 00000048 8945f848 ...........H.E.H
0030 8b45f848 89c7e800 000000b8 00000000 .E.H............
0040 c9c3 ..
Contents of section .rodata:
0000 68656c6c 6f20776f 726c6400 hello world.
Contents of section .comment:
0000 00474343 3a202855 62756e74 7520352e .GCC: (Ubuntu 5.
0010 342e302d 36756275 6e747531 7e31362e 4.0-6ubuntu1~16.
0020 30342e31 31292035 2e342e30 20323031 04.11) 5.4.0 201
0030 36303630 3900 60609.
Contents of section .eh_frame:
0000 14000000 00000000 017a5200 01781001 .........zR..x..
0010 1b0c0708 90010000 1c000000 1c000000 ................
0020 00000000 12000000 00410e10 8602430d .........A....C.
0030 064d0c07 08000000 1c000000 3c000000 .M..........<... ....0....a....c. .k...... of section .rodata: only>
int main(int argc, char *argv[]){
int a = 100;
char b = 'j';
float c = 3.14;
char *d = "helloworld\n";

printf("a的地址是:%p\n",&a);
printf("b的地址是:%p\n",&b);
printf("c的地址是:%p\n",&c);
printf("d的地址是:%p\n",d);
}

这里在main中申明几个普通变量:a、b、c和指向字符串常量的d,然后查看他们的地址,运行结果:

1
2
3
4
5
$ ./a.out
a的地址是:0x7ffe8c6e8f98
b的地址是:0x7ffe8c6e8f97
c的地址是:0x7ffe8c6e8f9c
d的地址是:0x4006d4

可以看到a,b,c地址是紧邻着的,而d的地址则完全不同,也佐证了字符串常量不存于函数栈内存中。


参考资料:《程序员的自我修养——链接、装载与库》</…></stdio.h></stdio.h>

CATALOG