字符指針與字符數組真正的區別


          
轉載地址: http://blog.csdn.net/on_1y/article/details/13030439

字符指針與字符數組真正的區別


問題緣起

先看一個示例

示例1

#include <stdio.h>

int main() {
    char *p = "hello";
    char q[] = "hello";

    printf ("p: %s\n", p);
    printf ("q: %s\n", q);

    return 0;
}

上面的例子會給出這樣的輸出

p: hello
q: hello

這樣看,char *p 和 char q[] 好像沒什麼區別, 那麼我們再看一個例子

示例2

#include <stdio.h>

int main() {
    char *p = "hello";
    char q[] = "hello";

    p[0] = 's';
    q[0] = 's';

    return 0;
}

如果你在Linux下,運行時可能得到這樣的結果

Segmentation fault (core dumped)

這時候你看到了區別,出現了段錯誤, 你一定想明白,到底發生了什麼, 經過幾個小實驗,你可能會發現使用 char *p 定義時,p指向的數據是無法改變的。 然後你Google, 別人可能會告訴你

  • char 指針指向的數據不能修改
  • char 指針指向的數據沒有分配
  • ...

你聽了還是一頭霧水,不能修改是什麼意思,沒有分配?沒有分配爲什麼能輸出?

作爲一個好奇心很重的人,你是絕對不能容忍這些問題的困擾的,且聽我慢慢道來


深入理解

首先,你的程序代碼和程序數據都在內存裏放着,也就是說 p 指向的 hello 和 q 指向的 hello, 都是內存中的數據。從兩個示例的比較中,你發現,同樣是內存中的數據,都可以讀,但是有的可以寫, 有的不能寫。從這可以看出,問題的關鍵在於,內存是如何組織的。

寫好的程序代碼放在硬盤上。程序代碼的運行需要CPU一條指令一條指令地執行。

在硬盤上讀東西是慢的,CPU是快的,所以有了內存。

因此程序代碼如果要運行,需要先載入內存。這時候,問題又出現了,系統中同時有許多程序要運行, 你想要這塊內存,我也想要這塊內存,那這塊內存給誰呢? 何況,寫程序的時候,我是不知道哪塊內存 被佔用,哪塊沒有被佔用的。總不能每次我想放數據,都檢查一下吧。那程序員的負擔也太大了。

一件事如果大家都需要,肯定會出現專門做這件事的人。

於是,操作系統接管了內存。程序A說,我要12號內存單元。程序B說,我要12號內存單元。 操作系統表示很爲難。不能都給,要不就衝突了,也不能不給,內存還有好大地方呢。

操作系統是聰明的,聰明人是會抽象的。

所謂抽象,就是看不到具體的東西了,只能看到上層的東西。當程序A和程序B都請求12號內存單元時, 操作系統把3號內存單元給了A,5號內存單元給了B。但是爲了讓程序中對內存的訪問保持一致性, 並不讓程序知道給他們的不是12號內存單元,否則程序中凡是和12號內存單元相關的,都要作修改, 又變成了程序自己維護內存。操作系統爲每個程序維護一個映射表。在映射表中, 對於程序A來說,12號內存單元對應3號內存單元,對於程序B來說12號內存單元對應5號內存單元。 這時候程序看到的12號內存單元和操作系統實際給出的3,5號內存單元,就變成了兩種不同的事物。 12號內存單元只是程序看到的,3,5號是真實的內存單元。我們把前者稱爲虛擬內存單元,後者指爲物理 內存單元。

有了虛擬內存的概念後,程序就無法無天了,全部的內存我都可以用,想訪問哪塊訪問哪塊,至於 實際上真正訪問的是內存哪個位置,我可不關心,那是操作系統的事,我只要把一個虛擬內存號告訴 操作系統就可以了。所以,從程序看來,他擁有整個內存空間。

嚴格來說,程序這個詞是不準確的, 程序一般就是指的代碼本身。但是代碼一旦運行起來, 和這段代碼相關的東西就太多了,比如指令,數據,映射表,用到的內存。另一方面, 系統中有多個程序在執行,有時候程序A執行,有時候程序B執行,操作系統從A切換到B時, 肯定要記下來A執行到哪裏了,這也和程序相關。所以這時候,我們又抽象出一個概念,叫進程。 這時候,程序就表示硬盤上那塊代碼,進程表示正在運行的程序,進程不僅包含代碼, 還包含一些運行時相關的東西。

現在,當你啓動一個程序時,操作系統會先創建一個進程,爲這個進程建立一個私有的虛擬的內存空間, 把程序代碼加載進來。進程代表一個運行中的程序,程序在運行時要使用內存,並且使用內存的方式多種多樣, 程序有有些數據放在內存中是不變的,有些是一開始就分配好的,還有一些會根據需要分配。所以, 我們需要對進程的虛擬內存空間進行良好的組織,以便操作系統和程序配合,高效地完成任務。

下圖是一個Linux進程的虛擬內存空間


所有的Linux進程的虛擬內存空間都是以這種方式組織的,只不過不同進程因爲映射表不同,所以 同一虛擬地址對應不同的物理地址。如果進程需要共享一塊內存區,只需要在映射表中把同一虛擬內存 地址映射到相同物理地址就可以了,比如上圖中的Kernel virutal memory區域,這個區域是操作系統 內核的代碼,所有進程都需要共享,所以操作系統就可以把所有進程的這一區域映射到相同物理地址處。

上圖的下半部分是Process virtual memory,代碼進程使用的虛擬內存空間,可以看出他們被分成了 幾個塊,這些塊代表了程序使用內存的不同方式。我們先來看一段代碼,並結合上圖說明一下程序使用 內存的不同方式。

示例3

#include <stdio.h>
#include <stdlib.h>

int main() {
    char *p = "hello";
    char q[] = "world";
    char *r = (char *)malloc(sizeof(char)*6);

    p[0] = 's';
    q[0] = 's';
    r[0] = 's';

    printf ("p is:%s",p);
    printf ("q is:%s",q);
    printf ("r is:%s",r);

    return 0;
}

我們先用gcc將這段代碼編程成彙編語言

gcc -S tchar.c -o tchar.s

示例3彙編版本(含註釋, 只含關鍵代碼)

.file   "tcharp.c"
    .section    .rodata
.LC0:
    .string "hello"
.LC1:
    .string "p is:%s"
.LC2:
    .string "q is:%s"
.LC3:
    .string "r is:%s"
    .text
    .globl  main
    .type   main, @function
main:
# char *p = "hello"
    movl    $.LC0, 28(%esp) 

# char q[] = "world"
    movl    $1819438967, 38(%esp) 
    movw    $100, 42(%esp)

# char *r = (char *)malloc(sizeof(char)*6)
    movl    $6, (%esp)
    call    malloc
    movl    %eax, 32(%esp)

# p[0] = 's'
    movl    28(%esp), %eax
    movb    $115, (%eax)
# q[0] = 's'
    movb    $115, 38(%esp)
# r[0] = 's'
    movl    32(%esp), %eax
    movb    $115, (%eax)

    movl    $.LC1, %eax
    movl    28(%esp), %edx # save p
    movl    %edx, 4(%esp)
    movl    %eax, (%esp)
    call    printf

    movl    $.LC2, %eax
    leal    38(%esp), %edx # save q
    movl    %edx, 4(%esp)
    movl    %eax, (%esp)
    call    printf

    movl    $.LC3, %eax
    movl    32(%esp), %edx # save r
    movl    %edx, 4(%esp)
    movl    %eax, (%esp)
    call    printf

從上述彙編代碼可以看出p,q,r三種使用內存的方式。從初始化上看, p指向的"hello",初始化時,直接指向了一個固定的位置,這意味着代碼執行的時候, 這個位置已經有數據了。q指向的"world",初始化是由代碼完成的,你把"world"經ASCII碼轉化成數字形式, 對比一下就會發現,那兩個數字,1819438967,100,對應的就是"world"。而r的初始化,是調用malloc得到的。

從這段彙編代碼。我們從直覺上會感覺到這三種使用內存方式的不同,接下來,我們再來看一下Linux運行時存儲器映像。


.text 段放着已經編譯的程序機器代碼。
.rodata 段放着只讀數據,printf函數參數中的字符串,p指向的"hello", 都在這存着。正因爲這個段是隻讀的,所以不能修改,代碼

p[0] = 's'

執行時就會出現段錯誤。
.data 段放着已經初始化的全局變量,.bss 段變着沒有初始化的全局變量。
再往上是 Run-time heap, 我們用malloc分配的內存空間都在這一段。
接着是User Stack,程序中的局部變量都在這一段,我們q指向的"world"就存儲在這裏。 從圖中也可以看到,%esp指向棧頂,再回頭看一下彙編代碼,你可能就明白之前相對於(%esp)地址所做的操作意味着什麼。

這裏特別要區分 地址與數據 。
p,q,r是局部變量,它們的值都是地址,這個地址作爲局部變量的值,在User Stack裏存儲。
p表示的地址指向數據"hello",這是不可變量,在.rodata段中存儲。q表示的地址指向的數據"world",作爲局部變量的數據,在User Stack段存儲。 r表示的地址指向的數據,在Run-time heap中存儲。

爲了驗證我們的想法,我們做一個實驗,把p,q,r三者地址打印出來, 再把三者指向的數據的地址打印出來。 然後查看內存分配。

示例4

#include <stdio.h>
#include <stdlib.h>

int main() {
    char *p = "hello";
    char q[] = "world";
    char *r = (char *)malloc(sizeof(char)*6);
    int n;

    printf ("addr of p:%p\n", &p);
    printf ("addr of q:%p\n", &q);
    printf ("addr of r:%p\n", &r);

    printf ("addr of p's data:%p\n", p);
    printf ("addr of q's data:%p\n", q);
    printf ("addr of r's data:%p\n", r);

    scanf ("%d\n", &n);

    return 0;
}

爲了便於觀察,我們引入scanf,同時放在後臺運行,這樣只要我們不輸入數據, 進程就不會終止,我們就可以觀察它。運行它,

$ ./tcharp  &
[2] 3461
addr of p:0xbfaa91d8
addr of q:0xbfaa91e6
addr of r:0xbfaa91dc
addr of p's data:0x8048670
addr of q's data:0xbfaa91e6
addr of r's data:0x87ad008

從這裏,我們可以看出:p,q,r本身的值,以及q指向的數據,存儲的位置離的很近,我們猜測, 所以0xbfXXXXXX這一塊應該是User Stack區域,0x8048XXX這一塊是.rodata區域, 0x87XXXXX這一塊是Run-time heap區域。

接下來,我們使用 readelf 命令,得到各個區域的實際位置,進一步明確我們的猜想。

$ readelf -a tcharp > tcharp_elf.txt

從tcharp_elf.txt中截取關鍵數據, 得到

  [11] .init             PROGBITS        08048318 000318 00002e 00  AX  0   0  4
  [12] .plt              PROGBITS        08048350 000350 000060 04  AX  0   0 16
  [13] .text             PROGBITS        080483b0 0003b0 00023c 00  AX  0   0 16
  [14] .fini             PROGBITS        080485ec 0005ec 00001a 00  AX  0   0  4
  [15] .rodata           PROGBITS        08048608 000608 000077 00   A  0   0  4
  [16] .eh_frame_hdr     PROGBITS        08048680 000680 000034 00   A  0   0  4
  [17] .eh_frame         PROGBITS        080486b4 0006b4 0000c4 00   A  0   0  4
  [18] .ctors            PROGBITS        08049f14 000f14 000008 00  WA  0   0  4
  [19] .dtors            PROGBITS        08049f1c 000f1c 000008 00  WA  0   0  4
  [20] .jcr              PROGBITS        08049f24 000f24 000004 00  WA  0   0  4
  [21] .dynamic          DYNAMIC         08049f28 000f28 0000c8 08  WA  6   0  4
  [22] .got              PROGBITS        08049ff0 000ff0 000004 04  WA  0   0  4
  [23] .got.plt          PROGBITS        08049ff4 000ff4 000020 04  WA  0   0  4
  [24] .data             PROGBITS        0804a014 001014 000008 00  WA  0   0  4
  [25] .bss              NOBITS          0804a01c 00101c 000008 00  WA  0   0  4
  [26] .comment          PROGBITS        00000000 00101c 00002a 01  MS  0   0  1
  [27] .shstrtab         STRTAB          00000000 001046 0000fc 00      0   0  1
  [28] .symtab           SYMTAB          00000000 0015f4 000430 10     29  45  4
  [29] .strtab           STRTAB          00000000 001a24 00022c 00      0   0  1

從這裏,我們可以驗證對 .rodata 段的猜測,p指向的 "hello", 確實是存儲在這一段。

然後,我們查看其它段的位置

$ cat /proc/3461/maps 
08048000-08049000 r-xp 00000000 08:0a 4981409    /home/zhaoxk/test/tcharp
08049000-0804a000 r--p 00000000 08:0a 4981409    /home/zhaoxk/test/tcharp
0804a000-0804b000 rw-p 00001000 08:0a 4981409    /home/zhaoxk/test/tcharp
087ad000-087ce000 rw-p 00000000 00:00 0          [heap]
b75fc000-b75fd000 rw-p 00000000 00:00 0 
b75fd000-b77a1000 r-xp 00000000 08:07 412678     /lib/i386-linux-gnu/libc-2.15.so
b77a1000-b77a3000 r--p 001a4000 08:07 412678     /lib/i386-linux-gnu/libc-2.15.so
b77a3000-b77a4000 rw-p 001a6000 08:07 412678     /lib/i386-linux-gnu/libc-2.15.so
b77a4000-b77a7000 rw-p 00000000 00:00 0 
b77c1000-b77c5000 rw-p 00000000 00:00 0 
b77c5000-b77c6000 r-xp 00000000 00:00 0          [vdso]
b77c6000-b77e6000 r-xp 00000000 08:07 403838     /lib/i386-linux-gnu/ld-2.15.so
b77e6000-b77e7000 r--p 0001f000 08:07 403838     /lib/i386-linux-gnu/ld-2.15.so
b77e7000-b77e8000 rw-p 00020000 08:07 403838     /lib/i386-linux-gnu/ld-2.15.so
bfa8b000-bfaac000 rw-p 00000000 00:00 0          [stack]

看到stack和heap段的位置了吧,再一次印證了我們的想法。

好了,我們的探索到這裏就結束了。


文後的話

從上面的過程可以看出,要想真正理解C語言,你需要了解彙編,需要了解操作系統, 而Linux提供了一系列工具,方便你探索整個系統的運行機制。如果你也想了解它, 請開始使用它。
還是那句話。

既然看起來不錯,爲什麼不試試呢?


文中圖片均來自《深入理解計算機系統》一書。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章