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

2019-07-02

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

#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

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

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

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,警告我们返回了一个函数内局部变量的地址。

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

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

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

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

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

#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 参数表示显示所有段的内容,得到如下结果:

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..........<...
 0040 00000000 30000000 00410e10 8602430d  ....0....A....C.
 0050 066b0c07 08000000                    .k......        

这里我们关注:Contents of section .rodata:这一块内容,.rodata,也就是只读数据段(ro:read only)。对照左右两侧,可以看到这一块的内容存放的正是helloworld;

至此,这个问题就结束了,字符串常量根本就不存在函数栈帧中,也就不存在函数返回会被回收这种说法了。

再进一步做一个实验:

#include <stdio.h>
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,然后查看他们的地址,运行结果:

$ ./a.out
a的地址是:0x7ffe8c6e8f98
b的地址是:0x7ffe8c6e8f97
c的地址是:0x7ffe8c6e8f9c
d的地址是:0x4006d4

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


参考资料:《程序员的自我修养——链接、装载与库》