2-Linux C語言指針與內存-學習筆記

Linux C語言指針與內存

前面咱們對於:linux

  • c語言的基本用法
  • makeFile文件的使用
  • main函數的詳解
  • 標準輸入輸出流以及錯誤流管道

工具與原理

mark

指針與內存都是c語言中的要點與難點程序員

  • 指針
  • 數組
  • 字符串
  • 堆內存與棧內存的差別
  • gdb內存調試工具

mark

gdb是linux中的調試工具,可讓咱們直接查看內存中的數據。編程

咱們能夠看到cpu到底作了什麼事,而內存中又發生了什麼變化數組

C語言中指針的基本用法(初識指針)

main0.c:安全

#include <stdio.h>

void change(int a, int b)
{
    int tmp =a;
    a=b;
    b=tmp;
}

int main()
{
    int a=5;
    int b=3;
    change(a,b);
    printf("num a =%d\n num b =%d\n",a,b);
    return 0;
}

上述代碼沒法實現a,b數值的交換。函數

mark

改成指針類型實現代碼以下:工具

main1.c:性能

#include <stdio.h>
void change(int *a, int *b)
{
    int tmp =*a;
    *a=*b;
    *b=tmp;
}

int main()
{
    int a=5;
    int b=3;
    change(&a,&b);
    printf("num a =%d\nnum b =%d\n",a,b);
    return 0;
}

爲原來的變量值加上*, change函數改成傳入&a &b
3和5能夠成功的交換。優化

mark

int* aint *a都是能夠的,被稱爲指針。& 取地址符。操作系統

咱們要引入工具來分析

  • 須要將實參的地址傳到子函數才能改變實參! 如change(&a,&b)

C語言中int未初始化時,初值爲隨機

int變量未初始化的默認初值,和變量的類型有關

  • 局部變量,在未初始化狀況下,初值爲隨機值。C規範對該初值並無作規定,具體實現由編譯器決定。如VC/VS等編譯器,會將初始值值爲0xCCCCCCCC,而GCC等編譯器則是不可預知的隨機值。
  • 靜態局部變量,即帶static修飾的局部變量。全局變量和靜態全局變量,即定義在函數外,不屬於任何一個函數的變量。這幾種默認初值爲0.

gdb工具的使用

經過gdb工具分析原理,分析結果

安裝gdb工具:

sudo apt install gdb
gdb -v

gdb能夠單步調試,打斷點,查看內存中變量。可是即便生成了可調試版本,仍是須要源代碼.c

  • gcc -g main0.c -o main0_debug.out生成可調試版本。
  • gdb ./main0_debug.out

  • l 全稱list:查看源代碼
  • 回車:繼續執行上條指令(此時的上條指令爲l)
  • break 行數:設置斷點
  • start :單步調試
  • p a 全稱print:查看a在內存中的狀況
  • n:執行到下一條語句

mark

$1 $2 只代表是第幾個變量。正在顯示的這行是待執行。

咱們想看change函數裏面是啥?而不是直接執行完函數。

  • s:進入函數內部

mark

能夠看到只是把數字傳進去了。

mark

  • bt:查看函數堆棧(能夠看到main函數和change函數)
  • f 1:切換到1號函數(棧頂的是咱們當前所在函數)
  • q:退出調試

形參與實參,函數默認傳入變量其實只是將數值傳入,而函數內部的局部變量不會改變全局中的數值。change中的形參a,b只是個代號而已。

使用gdb調試帶指針的版本

mark

此時傳遞的是地址。正好相差四個字節。

下節課會介紹計算機內存的分配,什麼是堆內存,什麼是棧內存,內存地址,指針變量的實質是什麼東西。

  • p *a

int *a時, p a打印出的是a的內存地址, p *a打印的是這個地址裏對應的值.
P &a顯示a的內存地址空間

mark

  • P &functionname: p + &函數名, 顯示函數程序在代碼段的內存地址
*a    取a這個地址的內容
&a    取a這個變量的地址

由於不知道一個指針指向的數據有多大, 因此須要在聲明一個指針變量的時候須要明確的類型。

不能交換數值的解析:只是傳值,只是change的局部變量,是實參的備份。

能夠交換數值的解析加:變量加個指針,change傳入取地址符,實現交換功能。

計算機中數據表示方法:

計算機內存中最小的單位叫作字節(Byte)

mark

一個字節是八個二進制位

爲何是二進制呢?

由於咱們的計算機是電子計算機,電流只有兩個狀態: 高電位(亮) 低電位(不亮)

mark

人類習慣於十進制數字,能夠將二進制與十進制進行轉換。(十個手指頭)

十進制滿十進一,二進制滿二進一

二進制寫起來太長了,爲了方便咱們顯示。

0x表示十六進制(滿16進1 ABCDEF)

1個16進制的數字,就能夠表示4位二進制數字

  • 計算用二進制
  • 顯示用十進制
  • 編程用16進制

內存管理

  • 內存是什麼?

計算機系統中內存是由操做系統來統一管理的,一個字節有八個bit,也就是八個二進制位。

mark

無論插幾個內存條,都會把內存當作一個總體來計算內存大小。但是內存也不是你想插多少就插多少的。

mark

32位的操做系統最大隻能使用4G的內存

  • 那麼問題來了,爲何32位操做系統只能使用4G內存呢?

由於32位的硬件平臺上,cpu的地址總線是32位,也就是操做系統的尋址空間是32位。

32位指的是: 給內存編號只能編到32個二進制位(這個編號就相似於咱們街道的門牌號碼)

好比一個小區只有八棟樓,那麼這個編號就不能超過8.

cpu的地址總線有多少根,那麼編號也就只能有多少個組合。

由於地址總線能夠存在多種狀態。

mark

32根地址總線就有2的32次方個狀態

其中的一個編號就能夠表明一個(內存的最小存儲單位)字節。

因此一共能夠存儲2的32次方個字節。

  • 那麼問題來了2的32次方個字節等於多少呢?

mark

mark

1024個字節等於1KB 1024個KB等於1MB,1024個MB等於1GB

內存分配:

1byte = 8bit(1字節 = 8進制位)

4G內存遠遠不夠用。(64位操做系統出現)

mark

GB T PB EB

mark

操做系統會對全部內存進行編號。每一個號碼錶示一個惟一的字節存放地址,一個字節能夠存放8個二進制位的數據。

因此64位操做系統內存地址編號

mark

mark

一共64個零到64個一

mark

左側即是咱們的計算機中內存的編號示意圖,從16位的0到16位的16個f。右側則是咱們每一個編號對應的內存,每一個字節(byte)能夠保存8個bit(狀態位)

這些內存全都要交給操做系統來管理。由於咱們的一個計算機中可能同時要運行多個程序。
多個程序由不一樣的人或團隊來開發,若是要由程序員來進行內存直接的管理是不太合理的。

多個程序對同一個內存地址來進行操做的話,到底分給哪一個程序呢?這會引發衝突。

內存的佔用不肯定, 不須要程序員來本身管理內存

mark

mark

mark

應用程序是由操做系統來調用的

main()函數就是全部函數的入口,操做系統知道入口後就能執行代碼了,程序就能夠被調用了。

mark

操做系統: 除了能給內存作編號之外,還能夠給內存作必定的規劃

好比在64位操做系統中: 程序員可使用的內存只要有前面的48位就能夠了。

也就是0X7fffffffffffffff(0x7fffffffffff = 01111111111111111111111111111111111111111111111)如下的。

而以上的內存空間是給操做系統內核使用的。

  • 用戶內存和操做系統內存隔離開的好處:

操做系統的內存不會被大量佔用,避免機器卡住,卡死,死機等狀態。

可經過操做系統把應用程序關閉,使得操做系統更安全。

mark

做爲用戶程序的內存空間又能夠進行分段,從高到低又劃分爲:、

  • 系統內核(亂入,屬用戶程序內存以外)
  • 棧(暫時存儲首先執行的程序狀態)
  • 自由可分配內存(可動態分配內存)
  • 數據段(聲明一些全局變量或者聲明一些常量)
  • 代碼段(程序源代碼編譯後存放在此)

咱們寫的c語言代碼,編寫的函數在編譯後存到磁盤,運行程序時,就把源代碼編譯後的二進制數據加載到內存中。將源代碼編譯以後的二進制就會被存放在代碼段

聲明的全局變量或常量放置在數據段。

mark

數據段的內存地址編號一般會大於代碼段。

mark

高位內存空間分配給操做系統內核使用,低位內存空間分配給用戶程序使用。

每次調用新的函數,就將新的函數壓入棧區,正在調用的函數將位於棧頂。

64位系統中 只有前48位是給程序員使用的。 0x7fffffffffffffff ~ 0x0

劇透: 下一節中看在應用程序中棧,堆,數據段,代碼段的做用。

變量與指針的本質

簡單的例子:

#include <stdio.h>

int global = 0;

int rect(int a,int b)
{
    static int count=0;
    count++;
    global++;
    int s=a*b;
    return s;
}

int quadrate(int a)
{
    static int count=0;
    count++;
    global++;
    int s = rect(a,a);
    return s;
}

int main()
{
    int a=3;
    int b=4;
    int *pa =&a;
    int *pb =&b;
    int *pglobal =&global;
    int (*pquadrate)(int a)= &quadrate;
    int s = quadrate(a);
    printf("%d\n",s);  
}

rect求長方形面積,quadrate求正方形面積(內部實際調用了求長方形面積)。
爲了方面咱們查看內存的狀況,添加了個業務邏輯無關的一些變量。

int global = 0;

全局變量global

static int count=0;
count++;
global++;

函數內的靜態變量: count,每一個函數調用內部都讓count和global加加。

main函數中聲明瞭一系列的指針。

int *pa =&a;
int *pb =&b;
int *pglobal =&global;
int (*pquadrate)(int a)= &quadrate;
gcc -g main.c -o main.out  //加-g生成的main.out才能夠用gdb進行調試
gdb ./main.out   //調試

gdb調試命令:

  • l(list) 列出代碼
  • start 開始調試
  • n 單步調試
  • s 進入函數內部
  • p 變量名 輸出變量的值
  • bt 查看棧標號
  • f 棧標號 切換棧
  • q 退出gdb
  • 回車 重複執行上一次的命令

之因此能夠調試代碼?是機器碼被加載進了咱們的內存(代碼段,它位於整個內存空間的最低位)

mark

每一行都是咱們的一條指令,被存放在代碼段。可是c語言的語法是不容許咱們直接操做代碼段的。

除了代碼編譯後會存在代碼段之外,還有一個地方保存咱們當前程序運行的狀態,好比當前在調用哪一個函數,當前調用的函數運行到多少行?而且這個函數中有哪些變量,這些變量的值是什麼

就像是一張照相機拍攝的快照,記錄當前的狀態信息。這些信息會被記錄到棧內存中。

mark

能夠打印出當前的變量值,由於這個信息被記錄在了棧內存當中。

mark

由於尚未運行int *pa = &a; 內存中的pa值爲空。

mark

a裏面是3,b裏面是4,都被棧內存記錄下來了。

mark

變量的本質是什麼?

  • 變量名只是一個代號(一個標識符)
  • 變量的本質就是內存

指針的本質?

mark

mark

指針pa也是一個變量,它也有本身的內存地址(0x7fffffffdcc8)。而這個內存地址中保存的數據是內存地址(0x7fffffffdcc0)。

C語言中全部的變量都有類型。

mark

  • 指針保存的就是內存的地址。
  • 變量: a = 第五個櫃子第二個抽屜。

mark

操做系統對於內存的管理

咱們能夠看到操做系統是如何管理內存的,以及gcc這類編譯器對於咱們源代碼所作的優化。

代碼段在整個內存地址中編號最小。

mark

能夠看出rect的編號小於quadrate的。代碼段中保存咱們編譯以後的機器碼。計算機在執行的時候rect函數先被加載進去。quadrate函數後被加載進去。先加載進去的內存地址就更小一些。

由於這兩個函數是順序執行的,多以使用大的減少的就是rect在內存中佔用的大小。

mark

數據段: 全局變量 & 常量都在咱們的數據段當中。

mark

能夠看到數據段的地址是要比代碼段大的。

一個函數能夠被屢次調用,main函數能夠被操做系統屢次調用。

mark

如咱們多開qq

mark

咱們連續聲明瞭兩個變量,爲什麼兩個變量ab的地址不連續呢?

由於這裏的地址指的是內存的首地址,如int佔四個字節。那麼dcbc dcbd dcde dcbf都是屬於變量a的內存空間。那麼下一個內存地址的首地址就是dcc0了。

咱們的b的首地址是dcc0,它也是int類型四個字節,爲啥下一個變量pa的地址不是dcc4而是dcc8呢?

這裏就涉及到咱們編譯器的優化了,它爲了讓cpu操做指令更快,提高程序的執行效率會對咱們的源代碼作必定的優化。編譯以後的指令存儲有可能和咱們編寫代碼的順序不同。

在代碼中咱們還聲明瞭另外一個整數類型變量s

int s = quadrate(a);

mark

能夠看到一樣爲整數類型的變量s的地址與前兩個a,b是連續的。

gcc編譯器的優化,若是咱們的函數中聲明瞭若干個整型變量,若干個指針類型變量,若干個浮點型變量,它會把咱們的同一類型的變量聲明放到一塊兒。接下來才聲明指針變量。

這樣的好處: 講到數組,指針計算的時候,解釋它的好處。

32位系統指針佔用4個字節, 也就是32個bit, 64位系統佔用64個bit, 也就是8字節。

mark

能夠看到指針pa的內存佔用從dcc8 到 dcd0(共8個字節)pb從dcd0到dcd8,(共八個字節),無論指針指向什麼,它自己內存中存放的都是內存地址,佔八個字節。

int (*pquadrate)(int a)= &quadrate;

mark

由dcd8 加上8個字節。來到了dce0

代碼段中內存地址愈來愈大,先聲明的函數地址小,後聲明的函數地址大。
棧先聲明的地址大,後聲明的地址小,與代碼段數據段相反。

下一節中: main函數調用正方形,正方形調用長方形。搞清棧內存如何分配的,再搞清靜態變量,局部變量都是怎麼存放的,理解函數的返回值return。

函數棧以及數據段內存

進行再一次的調試。

mark

運行到函數quadrate時,將a=3傳入。

mark

能夠看到棧中最下面的內存地址是最早分配的,若是從內存地址的大小來體現的話,main函數的內存地址大小第比較大的。

mark

能夠看到最早調用的main函數在最下面,而後是第二個調用的quadrate函數,最上面永遠是當前執行的函數。

棧的特色: 先進後出。 最後進的是rect函數,最早出去的也應該是rect函數。

mark

能夠看到越到棧頂的函數,兩個s(第一個s是存放棧頂的rect函數返回值的,第二個s是存放quadrate函數返回值的)

0x7fffffffdc74 0x7fffffffdc9c 棧頂的更小一點,也能夠體現出越晚進來的,地址越小。

mark

能夠看到更早進來的main函數中s地址更大。所以棧中是越早進來越在棧底,地址越大。

mark

能夠看出咱們在rect中的靜態變量count,和咱們在quadrate中的靜態變量count是連續存儲的。

static int count=0;

mark

能夠看出函數內的兩個靜態局部變量count是獨立的,連續存儲的。而兩個函數中的global變量都是指向同一個地址的。

觀察大小,咱們局部靜態變量count的地址值和去全局變量的地址值都很小。所以說明他們並不存放在棧中。(棧的地址很大)

咱們的靜態變量,常量,包括全局變量,默認都存儲在數據段中。因爲靜態變量時屬於某個函數特有的,因此靜態變量也是屬於某個函數特定的,是獨立的。全局變量是全部函數公用的,可是因爲他們都在數據段中,即便一個函數被屢次調用,靜態變量指向的仍是數據段中的一個固定地址。不一樣函數裏的count是不一樣的count,可是同一個函數無論調用多少次,這個count都指向同一塊內存。

數據段(data segment)一般是指用來存放程序中已初始化的全局變量的一塊內存區域。數據段屬於靜態內存分配。

編譯器優化代碼,把聲明時不在一塊兒的同一類型變量,放到一塊兒(某種程度上修改了源碼)

如聲明

int a; float b; int c;

編譯後變量a的地址和c的地址是連在一塊兒的.CPU在編譯的時候對棧內變量的存儲地址進行優化,他會將類型相同的變量在連續地址中儲存。

地址分配: 代碼段,數據段是從下往上分配(先低地址,後高地址)。棧是從上往下分配(先高地址,後低地址)

函數中靜態變量,局部變量區別:

局部變量(相對數據段而言的高地址)中,而靜態變量數據段(低地址)中.
因此在屢次調用函數時,靜態變量不會被從新初始化.或者這麼說,靜態變量的生存週期和數據段相同,局部變量生存時間受調用函數時,所屬函數進棧出棧的影響而會從新初始化.

全局變量和靜態變量都在數據段中,但靜態變量是某個函數特有的.

下面來探究函數指針是怎麼一回事?

指針能夠指向一個變量,吧變量的值取出來。函數指針?

(函數指針與指針指向的數據訪問)

修改咱們的源代碼(上面我用的是修改過的,可是不影響上面概念的理解)

int s = quadrate(a);

修改成:

// int s = quadrate(a);
int s = (*pquadrate)(a);

函數指針在調用的時候傳入a的值進來。

gcc -g main.c -o main.out  //加-g生成的main.out才能夠用gdb進行調試
gdb ./main.out   //調試
int (*pquadrate)(int a)= &quadrate;

這一行是一個函數指針

mark

這裏依然能夠進入函數內部,運行代碼時函數指針也能夠調用函數內容。這種作法常常用於寫程序時作回調函數使用。

  • p &a 是將變量a的內存地址找出來。&符號時取地址符

mark

  • 若是已經知道一個地址如何取裏面的數據?

mark

  • p *&a: 取變量a所在地址的值 (先進行&運算,&a至關於取變量a的地址,在執行*運算,*&p至關於取變量a所在地址的值)

  • p &*a:取變量a的地址 (先進行*運算,*a至關於變量a的值,再進行&運算,&*p就至關於取變量a的地址)

quadrate 自己是一個函數指針,*quadrate取出了指向的函數內容(一組指令構成),
(*quadrate)(3)就表示調用函數,並傳入參數3

mark

  • p pa pa是指向pa的地址。
  • p *pa 表明取出0x7fffffffdcbc這個地址存放的值,pa指向的是一個棧的地址,若是是棧內存地址確定是要訪問數據,棧,堆,數據段內存都認爲是取數據。代碼段是找到一個代碼塊。

mark

  • p &pa 指找到pa變量自己的地址。

下面: 數組,動態堆內存建立,指針運算。

數組申明的內存排列

示例代碼:

#include <stdio.h>
int main()
{
    int a =3;
    int b =2;
    int array[3];
    array[0] =1;
    array[1] =10;
    array[2] =100;
    int *p=&a;
    int i;
    for (i = 0; i < 6; i++)
    {
        printf("*p=%d\n",*p);
        p++;
    }
    printf("-------------------------------------\n");
    p =&a;
    for (i = 0; i < 6; i++) 
    {
        printf("p[%d]=%d\n",i,p[i] );
    }
    return 0;
}

爲了說明問題,全部的數據類型通通是整型,除了指針p之外。c語言的數組類型是比較原始的,在函數內聲明,所以也在棧內存當中。指針p指向a的地址。

p是一個指針,指針的加加操做。咱們類比一下,整數類型的加加,3++,下次打印就會變成4。

p[i] 指針的括號取值,與數組取值有些相似。

gcc -g main.c -o main.out  //加-g生成的main.out才能夠用gdb進行調試
./main.out        //觀察結果
gdb ./main.out   //調試

mark

for循環括號里加不加int,內存中還有區別的。

mark

指針p指向的值等於3,1,2

int類型的內存地址和數組內存地址不連續,而是差了16位。

for (i = 0; i < 6; i++)
        {
                printf("*p=%d\n",*p );
                if(i == 2){
                   p=p+4;
                }
                else{
                   p++;
                }
        }

將第一個for循環中的代碼改成如上面所示。

mark

for(i = 0; i < 6; i++)
        {
                if(i > 2){
                printf("p[%d]=%d\n",i+3,p[i+3] );
                }else{
                printf("p[%d]=%d\n",i,p[i] );
                }
        }

mark

mark

此時咱們打印a的地址,打印p的地址是同樣的。由於咱們把a的地址賦值給了p

每一個整型數字佔四個字節。

(gdb) p *p
$3 = 3
(gdb) p *&a
$4 = 3
(gdb) p *0x7fffffffdcc4
$5 = 3
(gdb) p * 0x7fffffffdcc4
$6 = 3

能夠看到四種等價的操做。都是*加上地址,能夠直接打印出內存中的數據值。

mark

先聲明瞭a,再聲明瞭b。可是咱們a的下一個內存地址中存放的卻不是b。c8地址中存放的是0

gcc編譯器有自動優化功能會把全部的同一類型的變量放到一塊兒來聲明。由於咱們還聲明過一個i變量,i也是整型的。
會把i也和a,b放在一塊兒,具體哪一個變量在前,哪一個變量在後。

一般狀況先寫的會在前面,這裏由於咱們i在很下面聲明的,中間又隔了一個指針p

mark

0的值就等於i的值,兩個指向同一個地址。

main函數執行的棧中,最低的地址放的a的值,接下來是i的值,i的值以後應該推測是b的值。

0x7fffffffdcc8 + 4
0x7fffffffdccc

mark

能夠看出地址順序依次增大: a i b

一直p p p的輸出很麻煩,如何方便的輸出?

x/3d 0x7fffffffdcc4

x表示要輸出內存中的值,/表示要輸出幾個值,輸出3個值。按照什麼類型來輸出。d按照十進制進行輸出。從哪一個地址開始顯示呢?

mark

咱們還能夠指定顯示變量要有多大長度,默認是4個字節。

mark

取九塊內存地址的內容,能夠看到整數類型的數字和數組的存儲中間相差三個內存空間(地址上從頭到頭,相差16個字節)

那些隨機值是不可控的,程序中使用到未初始化的值,會對軟件形成異常。

由於c語言不作指針的安全檢查,它會操做這個地址的值等,未初始化,有多是其餘程序使用過的值。

棧內存中, 連續的地址空間來存放整型變量和咱們的數組元素。

能夠看出數組是按順序放置元素的。

指針運算。

mark

能夠看到指針往下移動了4格,但是指針怎麼知道要加四格呢?

由於程序員在聲明指針類型的時候是整型,int佔四個字節。因此p++的時候會一次移動4個。

這是指針的偏移運算。指針的偏移運行效率高,性能好。

p +=3;

把指針往下移三格(整數類型指針)移動12個字節

*p =101;

將p指針所指向的值修改成101

p =&a;

讓p再次指向a的地址,不影響咱們下面的打印。

mark

能夠看到本來p指向a,而後往下移動三格(忽略整型與數組中間三塊內存,四個地址差)

第一次,從a移動到i;第二次,從i移動到b;第三次,從b移動到數組第一個元素。

p[3] //等價於p +=3,也就是把p往下移動三格
*p = 101
//上面兩行合二爲一的想法是錯的,由於只有下面這行才能起到理想目的。
p[3] = 101
int *p=&a;
p[2];
*p = 66;

P[4]不是p往下面移動了4個位置,而是從p開始的地址日後移動4個位置取值,p指向的地址仍是不變的這時候就不用跟採用p++時,再將指針歸位了。

int array[2];
int *pa =array;
pa[0]=1;
pa[1]=10;
pa[2]=100;

若是說數組自己也是一種指針類型的話,裏面就是地址。把地址賦給地址變量就不須要加取地址符了。

任何須要用數組操做的地方,均可以用指針來代替。由於咱們的指針變量本質上是內存地址,數組也是地址。

反過來就不行了,指針能作的,數組不必定能作。

int array[2];
array+=2;

上面的代碼就是錯誤的。

數組其實就是個指針常量,指針是指針變量,常量是不可更改的。array永遠都指向的是同一個地址,固然地址裏面的內容是能夠改變的。

下節課: 一種特殊的數組,字符數組

字符數組和指針字符串

小示例代碼:

#include <stdio.h>

int main()
{
    char str[]="hello";
    char *str2="world";
    char str3[10];
    printf("input the value\n");
    scanf("%s",str3);
    printf("str is %s\n",str); 
    printf("str2 is %s\n",str2);
    printf("str3 is %s\n",str3);
}

聲明瞭一個字符數組並賦值hello,又聲明瞭一個字符指針,還聲明瞭一個長度爲10的字符數組,並未初始化。

經過scanf將輸入的字符串寫入str3中。

而後進行打印。

gcc -g main.c -o main.out

生成可調試代碼。

mark

gdb main.out //開始調試

mark

打印str和str2的時候,均可以打印出內存中的字符串來。

str直接打印出裏面的值,由於str2指明的是指針類型,會等於一個地址0x5555555548b4

地址是一個很小的地址(相對於0x7),是在代碼段中的地址,是源代碼編譯就編譯進去的。 而咱們的str2只是指向這個地址而已。

mark

能夠看出str2這個指針對應的是整個數組的首地址而已。首地址對應的內存中存儲着首字母w

119是w對應的ASCII碼。第6個值,是由於上面o的那次,已經將指針後移了一位,++後移第二位。

mark

指針忘記歸位,致使str2只剩下三個字母。

字符類型的指針和字符數組也是能夠混用的。

mark

通常的咱們經過scanf輸入,是要輸入&a,也就是要加取地址符的。

聲明str3的時候,它是一個字符數組,數組就是內存地址。str3就能夠直接傳進去,不須要取地址符。

scanf("%s",str);

將輸入存放至str中。

mark

能夠看到由於數組的本質是指針常量,str指向的地址,被mtianyan2str填充。而str3的指針還指向原來的位置,因此形成str3的內容爲str的後半部分。

str在建立時有五個字母+一個null結束符,可是它的數組長度是6。

mark

因此計算str3時須要減去6個字母才能夠獲得。

char str4[]={'h','e','l','l','o'};
int len = sizeof(str4) / sizeof(char);

而採用這種單字母初始化方法,數組長度與字符個數一致。

mark

咱們嘗試向str2中寫入東西。

mark

能夠看到往str2寫數據,會出現段錯誤(核心已轉儲的錯誤)

c語言的字符串是一個字符數組,以/0結尾。有五個字符就有六個長度

mark

gdb的x命令,能夠打印地址中的值

  • x/個數 地址
  • x/6cb 地址: 打印該地址後的6個字符,c:字符形式打印,b:打印的單位按byte

mark

scanf能夠將輸入存入str或str3,可是不能存入str2

堆和棧內存裏才能夠寫入(預留空間纔可寫入),而str2是編譯以後,加載到內存的一個代碼段變量,不容許寫入。操做系統對內存作安全管理。

mark

咱們聲明一個函數把一個函數定義好了,函數所在的棧的內存就分配好了。

而咱們使用malloc函數,它會爲咱們分配堆內存。

字符數組的深刻理解

示例代碼2:

#include <stdio.h>

int main()
{
    char str[]="hello";
    char *str2="world";
    char str3[10];
    printf("input the value\n");
    str[3]='\0';
    scanf("%s",str3);
    printf("str is %s\n",str); 
    printf("str2 is %s\n",str2);
    printf("str3 is %s\n",str3);
}
gcc -g main2.c -o main2.out

mark

只會打印出hel,由於prinf打印以\0爲結束。

char str1[] = "hello";
char str3[5];

能夠認爲str1的6個字節結束以後,就是str3指向的地址。

若是經過scanf輸入到str1的值超過長度6,由於沒有\0,還會繼續往裏寫。都會寫入str中。

  • 當str1聲明時,長度爲五個字母加上\0爲6,str3也是數組類型,與str1都在同一個內存空間中,而且在str1以後聲明, 它們的地址應該是連續的。

  • 當scanf輸入到str1的值爲 HelloWorld時,str1以前的大小裝不下這麼多字符,就會使用後面的內存地址,str3 本是空的並無賦值,可是因爲str1的內存溢出,裝到了str3中,因此str3也是有值的.

總之,數組內存溢出是件危險的事

string類型輸出遇到\0結束
char類型輸出遇到\0繼續輸出

#include <stdio.h>

int main()
{
    char str[]="hello";
    char *str2="world";
    char str3[10];
    printf("input the value\n");
    str[3] = '\0';
    scanf("%s",str3);
    int i=0;
    for(i=0;i<6;i++)
    {
      printf("%c\n",str[i]);
    }
    printf("str is %s\n",str); 
    printf("str2 is %s\n",str2);
    printf("str3 is %s\n",str3);
}

mark

相關文章
相關標籤/搜索