1. 程式人生 > >linux高階程式設計day01 筆記 (轉)

linux高階程式設計day01 筆記 (轉)

1.malloc怎麼分配空間

    malloc與new的關係

    看完下面的2再回答這個問題。

2. linux對記憶體的結構描述

    a)         /proc/${pid}/         存放程序執行時候所有的資訊。程式一結束,該目錄就刪掉了。

    b)        任何一個程式的記憶體空間其實分成4個基本部分。

                        i.              程式碼區

                        ii.              全域性棧區

                       iii.              堆

                       iv.              區域性棧

小實驗: 執行一個只包含while(1);的程式,然後另起一個終端,cd /proc下面的對應程序的pid目錄,cat maps,檢視到執行程序的記憶體空間分配情況。

程序檢視: ps aue

    c)         理解程式的變數與記憶體空間的關係

小實驗:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int add(int a, int b)
{
    return a+b;
}
int a1 = 1;
static int a2 = 2;
const int a3 = 3;
main()
{
    int
 b1 = 4;
    static b2 = 5;
    const b3 = 6;
    
    int *p1 = malloc(4);
    
    printf("a1:%p\n", &a1);
    printf("a2:%p\n", &a2);
    printf("a3:%p\n", &a3);
    printf("b1:%p\n", &b1);
    printf("b2:%p\n", &b2);
    printf("b3:%p\n", &b3);
    printf("p1:%p\n", p1);
    printf("main:%p\n", main);
    printf("add:%p\n", add);
    
    printf("%d\n", getpid());
    while
(1);
}

把列印結果與/proc下對應目錄中的maps檔案比較。

(程式碼區一般是ox8048000開頭的區域。 )

可以看到 a3全域性常量在程式碼區(字面值神馬的也是放在程式碼區)。 b3區域性常量放在區域性棧區。

a1, a2, b2 則是放在全域性棧區。

main, add 在程式碼區。

b1, b3在區域性棧區。

p1 在堆

小實驗:

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

main()
{
    int a1 = 10;
    int a2 = 20;
    int a3 = 30;
    
    int *p1 = malloc(4);
    int *p2 = malloc(4);
    int *p3 = malloc(4);
    
    printf("%p\n", &a1);
    printf("%p\n", &a2);
    printf("%p\n", &a3);
    printf("%p\n", p1);
    printf("%p\n", p2);
    printf("%p\n", p3);
    printf("%p\n", &a1);
    printf("%p\n", &a1);
    printf("%p\n", &a1);
    
    printf("%d\n", getpid());
    while(1);
}複製程式碼

執行結果如下:

可以看到,a1, a2, a3的地址降序排列,相差4個位元組。(棧  分配記憶體是直接壓到棧頂)

p1, p2, p3的地址升序排列,相差16個位元組。(堆)

小結:

     (1)記憶體分四個區。
     (2)各種變數對應存放區。
     (3)堆疊是一種管理記憶體的資料結構。

檢視程式的記憶體地址。

回到問題1.

看一個小實驗:

 #include <stdio.h>

#include <stdlib.h>

int main()
{
    int *p1 = malloc(4);
    int *p2 = malloc(4);
    int *p3 = malloc(4);
    
    *p1 = 1;
    *(p1+1) = 2;
    *(p1+2) = 3;    
    *(p1+3) = 4;
    *(p1+4) = 5;
    *(p1+5) = 6;
    *(p1+6) = 7;
    *(p1+7) = 8;
    *(p1+8) = 9;
    
    printf("%d\n", *p2);
    return 0;
}

執行結果是5.

如果在程式中加一句話後:

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

int main()
{
    int *p1 = malloc(4);
    int *p2 = malloc(4);
    int *p3 = malloc(4);
    
    *p1 = 1;
    *(p1+1) = 2;
    *(p1+2) = 3;    
    *(p1+3) = 4;
    *(p1+4) = 5;
    *(p1+5) = 6;
    *(p1+6) = 7;
    *(p1+7) = 8;
    *(p1+8) = 9;
    
    free(p1);    //比上面的程式多了這句話    printf("%d\n", *p2);
    return 0;
}

則會發生錯誤。

p1指向int型,應該只佔用4個位元組。可是實際上卻佔了16個位元組,因為P1其實是連結串列裡的一個節點,多的12個位元組其實是儲存的一些指向下一個節點,或者別的一些資訊。我們在用*(p1+1) = 2;   *(p1+2) = 3;          *(p1+3) = 4;破壞這些資訊的時候,不會報錯,但是在使用這個節點(free(p))時,則會報錯了。

3. 理解malloc的工作原理

malloc使用一個數據結構(連結串列)來維護分配的空間。連結串列的構成:分配的空間、上一個空間的地址、下一個空間的地址、以及本空間的資訊等。對malloc分配的空間不要越界訪問,因為容易破壞後臺的連結串列維護結構,導致malloc/free/calloc/realloc不正常工作。

4. C++的new與malloc的關係

小實驗:


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

int main()
{
    int *p1 = (int*)malloc(4);
    int *p2 = new int;
    int *p3 = (int *)malloc(4);
    int *p4 = new int;
    int *p5 = new int;
    int *p6 = new int;
    
    printf("%p\n", p1);
    printf("%p\n", p2);
    printf("%p\n", p3);
    printf("%p\n", p4);
    printf("%p\n", p5);
    printf("%p\n", p6);
    return 0;
}

執行結果:

結論:new的實現使用的是malloc來實現的。

區別:new使用malloc後,還要初始化空間。基本型別,直接初始化成預設值。 UDT型別呼叫指定的構造器

推論:delete也是呼叫free實現。

區別:delete會呼叫指定的析構器,然後再呼叫free()。

new與new[]的區別:new只調用一個構造器初始化。new[]迴圈對每個區域呼叫構造器。

delete與delete[]的區別:delete只調用一次解構函式,而delete則把陣列中的每個物件的解構函式都呼叫一遍。

malloc      new

realloc      new() //定位分配

calloc       new[]

free          delete

5. 函式呼叫棧空間分配與釋放

5.1 總結:

  1. 函式執行的時候有自己的臨時棧。(C++中的成員函式有物件棧空間和函式棧空間兩個空間)
  2. 函式的引數就在臨時棧中。如果函式傳遞實參,則用來初始化臨時引數變數。
  3. 通過暫存器返回值(使用返回值返回資料)
  4. 通過引數返回值(引數必須是指標。指標指向的區域必須事先分配)
  5. 如果引數返回指標,引數就是雙指標。

5.2 __stdcall, __cdecl __fastcall的問題(瞭解,應付面試即可)

#include <stdio.h>
int _attribute_((stdcall)) add(int *a, int *b)
{
    return *a+*b;
}

int main()
{
    int a1 = 20;
    int b2 = 30;
    int r = add(&a, &b);
    
    printf("%d\n", r);
}
  1. 這三個屬性決定函式引數壓棧順序。都是從右到左。
  2. 決定函式棧清空的方式。是呼叫者清空還是被呼叫者清空
  3. 決定了函式的名字轉換方式。(編譯的時候,會把函式重新命名。)

6. far near huge指標的問題(linux中不考慮這個問題。window中屬於遺留問題。windows程式設計統一採用far指標)

    near  16

    far    32

    huge 綜合

  Note: C與C++明顯的不同表現在 引用, 模板, 異常以及面向物件

              函式引數傳值和傳指標其實是一樣的,只是一個是把值拷貝過去,一個是把地址拷貝過去。

7.虛擬記憶體

小實驗:寫一個程式,定義一個整型指標,賦值為999,打印出它的地址,同時while(1)讓它一直執行著。再寫一個程式,定義一個整形指標,直接指向剛才打印出來的地址,然後列印這個指標指向的整數,為打印出999嗎?  (不會,段錯誤)

問題:

為什麼一個程式不能訪問另外一個程式的地址指向的空間?

理解:

  1. 每個程式的開始地址一般都是0x80084000。
  2. 由1可以看出程式中使用的地址不是實體地址,而是邏輯地址(虛擬記憶體)。邏輯地址僅僅是個編號,使用int 4位元組整數表示。(4位元組所能表示的最大整數是2的32次方=4294967296=4G)。所以每個程式提供了4G的訪問能力

問題:

       邏輯地址和實體地址怎麼關聯?(記憶體對映)

背景:

       虛擬記憶體的提出:禁止使用者直接訪問物理儲存地址。有助於系統的穩定。

結論:

       虛擬地址與實體地址在對映的時候有一個基本單位4k(16進位制的1000,稱為記憶體頁)。

       段錯誤:無效訪問。虛擬地址與實體地址沒有對映。

       沒有段錯誤不一定是合法訪問。

       合法訪問:比如malloc分配的空間之外的空間(malloc後就映射了)可以訪問但是訪問非法。int *p1 = malloc(4);   int *(p1+12) = 233;  第二句不會報段錯誤,但是是非法訪問。

8.虛擬記憶體的分配

       棧:編譯器自動生成程式碼維護

       堆:地址是否對映?對映的空間是否被管理?

  1. brk/sbrk記憶體對映函式

  補充:幫助文件:man 節 關鍵字 

  節:1-8    1: Linux系統(shell)指令  (ls等)

                2: 系統函式  (brk等)

                3: 標準C函式的幫助文件  (fopen等)

                7: 系統的程式設計幫助  (tcp, icmp等)

       分配釋放記憶體

       int brk(void *addr);  //分配空間,釋放空間

       void *sbrk(int size);  //返回指定大小的空間的地址

       應用:

  1. 使用sbrk分配記憶體空間    int *p = sbrk(4);  //分配4位元組整數
  2. 使用sbrk得到沒有對映的虛擬地址  int *p1 = sbrk(0);  //返回沒有對映的虛擬地址的首地址,不能給*p1賦值,會出現段錯誤。因為還沒有對映。
  3. 使用brk分配空間
  4. 使用brk釋放空間

理解:

       sbrk(int size)

       sbrk與brk後臺系統維護一個指標。指標預設是null。

       呼叫sbrk,判定指標是否是0(第一次呼叫),如果是:得到大塊空閒地址的首地址來初始化該指標。返回該指標給指標變數賦值,同時把指標指向+size的地方。如果是否:返回指標,並且將指標位置+size。

#include <stdio.h>
#include <unistd.h>

int main()
{
    int *p = sbrk(0);  //返回空閒地址,並修改指標為+size(這裡是0,),注意這個指標不是*p,而是sbrk指向記憶體裡的指標。
//這裡是0,並且是首次呼叫,所以記憶體並沒有對映。如果括號裡是4或者4的倍數,則會返回指標的同時做對映,並把sbrk的指標指向+4的位
//置以便供下一次呼叫的時候返回地址。並不是括號裡是4就只對映4個位元組的地址,而是對映一頁的記憶體。這是為了效率的考慮。好比吃饅頭,
//不是吃一個做一個,而是要吃了,做一屜,慢慢吃。所以 *(p+10)= 20; 是可以訪問的(p最多隻能加到1023,不然仍然會段錯誤)。
//但是是非法訪問。    
    printf("%d\n", *p);
}
#include <stdio.h>
#include <unistd.h>

int main()
{
    int *p1 = sbrk(4);  //返回空閒地址,並修改指標為+size    int *p2 = sbrk(0);
    
    printf("%p\n", p1);
    printf("%p\n", p2);  //通過上面的程式分析,這裡列印的
//是p1加上4個位元組後的地址。int *p2 = sbrk(200);這句話括號裡即使
//是200,p2也是p1加4個位元組,因為sbrk是先返回當前的地址,再加括
//號裡的size。如果括號裡是負數,則表示釋放空間。    
    while(1);
}


下面再看brk(void *p)函式:


#include <stdio.h>
#include <unistd.h>

int main()
{
    int *p = sbrk(0);
    brk(p+1);  //將sbrk裡面的指標向後移動4個位元組,發現沒
//有對映,就會自動對映區域。所以後面的*p就可以訪問了。    *p = 800;
    brk(p);   //將指標再移回去,相當於釋放記憶體空間,即取
//消之前的對映。後面再訪問就會出錯了。    *p = 29;  //段錯誤。    
    while(1);
}


應用案例:

       寫一個程式查詢1-10000之間的所有的素數,並且存放到緩衝,然後列印。

       分析:1-10000如果用陣列的話,不太現實,有大部分空間都用不上。C++的話可以用連結串列實現,但是連結串列的開銷比較大。用malloc和new都不太好。所以,緩衝的實現使用sbrk/brk。

       流程:

              判斷是否是素數(isPrime)

              是,分配空間存放

              否,繼續下步

 #include <stdio.h>

#include <unistd.h>

int isPrime(int a)
{
    int i = 0;
    for(i = 2; i < a; i++)
    {
        if(a%i == 0)
            {
                return 1;
            }
    }
    return 0;
}

int main()
{
    int i = 2; //迴圈變數    int *r;
    int *p;  //p一直指向頁首    r = sbrk(0);
    p = r;
    for(; i<10000; i++)
    {
        if(isPrime(i))
            {
                brk(r+1);
                *r = i;
                r = sbrk(0);
            }
    }
    
    i = 0;
    r = p;
    while(r != sbrk(0))
    {
        printf("%d\n", *r);
        r++;
    }
    brk(p);  //釋放空間}

總結:

       new   //C++裡面用得比較多

       malloc  //C裡面用得比較多,一定要制定空間大小

       brk/sbrk  //資料比較簡單,量比較大的時候用效率比較高

異常處理

       int brk(void *)  //返回int值

       void *sbrk(int)  //返回指標

       如果成功,brk返回0, sbrk返回指標

       如果失敗, brk返回-1, sbrk返回(void *)-1


#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>

int main()
{
    void *p = sbrk(1000000000*3);
    if(p == (void *)-1)
        {
            printf("error!");
            perror("Hello"); //打印出錯誤資訊            printf("%m");  //打印出memory error            printf("%s", strerror(errno));
        }
}


以下是一些比較常用的函式:

       字串函式string.h      cstring

       記憶體管理函式malloc    memset   mamcmp memcpy…bzero

       錯誤處理函式

       時間函式

       型別轉換函式

作業:

       找出列印1-10000之間的所有孿生素數。