精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

C++秋招高頻面試題匯總:C++多態(tài)、malloc函數(shù)、內(nèi)存管理、并發(fā)編程、數(shù)據(jù)庫

開發(fā) 前端
內(nèi)存管理向來是 C++ 開發(fā)的重難點,內(nèi)存泄漏、懸空指針等問題時刻考驗開發(fā)者功底。智能指針如何借助 RAII 機(jī)制實現(xiàn)自動內(nèi)存管理,堆和棧內(nèi)存的區(qū)別及使用場景,都可能在面試中被問到。并發(fā)編程隨著多核處理器普及愈發(fā)重要。

在競爭激烈的 C++ 秋招戰(zhàn)場上,扎實掌握核心知識、精準(zhǔn)應(yīng)對面試提問是脫穎而出的關(guān)鍵。今天,為大家精心匯總 C++ 秋招中的高頻面試題,聚焦多態(tài)、malloc、內(nèi)存管理、并發(fā)編程、數(shù)據(jù)庫這些重點領(lǐng)域。多態(tài)作為 C++ 面向?qū)ο缶幊痰暮诵奶匦裕尨a更具靈活性與擴(kuò)展性,面試官常圍繞其原理、實現(xiàn)方式深入考查。比如,動態(tài)多態(tài)如何借助虛函數(shù)與運行時綁定來達(dá)成,不同類型的多態(tài)(像函數(shù)重載體現(xiàn)的靜態(tài)多態(tài))在實際場景中怎樣運用。malloc 作為 C 語言就有的動態(tài)內(nèi)存分配函數(shù),在 C++ 開發(fā)里也常用到。理解它與 C++ 專屬的 new 操作符的差異,掌握其內(nèi)存分配機(jī)制、失敗處理方式,是應(yīng)對面試的必備技能。

內(nèi)存管理向來是 C++ 開發(fā)的重難點,內(nèi)存泄漏、懸空指針等問題時刻考驗開發(fā)者功底。智能指針如何借助 RAII 機(jī)制實現(xiàn)自動內(nèi)存管理,堆和棧內(nèi)存的區(qū)別及使用場景,都可能在面試中被問到。并發(fā)編程隨著多核處理器普及愈發(fā)重要。線程與進(jìn)程的區(qū)別、線程同步方式(如互斥鎖、條件變量的運用)、死鎖的成因與預(yù)防,都是面試官愛考察的方向。數(shù)據(jù)庫方面,無論是 MySQL 等關(guān)系型數(shù)據(jù)庫的索引原理、事務(wù)特性,還是 Redis 這類非關(guān)系型數(shù)據(jù)庫的應(yīng)用場景、數(shù)據(jù)結(jié)構(gòu),都可能出現(xiàn)在面試題中。接下來,就讓我們深入剖析這些高頻面試題,為你的秋招之路添磚加瓦,助力你斬獲心儀 offer !

Part1.C++多態(tài):讓代碼 “七十二變”

1.1 C++多態(tài)

多態(tài),是面向?qū)ο缶幊讨械年P(guān)鍵概念,簡單來說,就是 “同一接口,不同實現(xiàn)” ,一個事物可以有多種形態(tài)。就好比生活中的交通工具,都有 “行駛” 這個接口,但汽車、自行車、飛機(jī)的行駛方式截然不同。在編程世界里,多態(tài)允許不同類的對象對同一消息做出不同響應(yīng)。

假設(shè)我們有一個圖形繪制程序,定義一個基類Shape,其中包含一個draw方法用于繪制圖形。然后派生出Circle(圓形)和Rectangle(矩形)類,它們都重寫了draw方法,以各自的方式實現(xiàn)圖形繪制。當(dāng)我們使用Shape類型的變量來引用Circle或Rectangle對象時,調(diào)用draw方法就會根據(jù)實際對象的類型,繪制出圓形或矩形。這就是多態(tài)的魅力,它讓代碼更加靈活和可擴(kuò)展,無需大量的條件判斷語句。

在 C++ 中,多態(tài)通過虛函數(shù)和動態(tài)綁定來實現(xiàn);Java 則依靠方法重寫和運行時動態(tài)綁定機(jī)制達(dá)成多態(tài)效果。多態(tài)在框架設(shè)計、插件系統(tǒng)、策略模式等場景中廣泛應(yīng)用,是構(gòu)建靈活可擴(kuò)展系統(tǒng)的重要基石。

具體來說,多態(tài)的實現(xiàn)涉及以下幾個關(guān)鍵點:

  • 虛函數(shù):在基類中使用virtual關(guān)鍵字聲明的函數(shù)稱為虛函數(shù)。這些函數(shù)可以在派生類中被重寫。
  • 虛函數(shù)表(vtable):編譯器為包含虛函數(shù)的類生成一個虛函數(shù)表,表中存儲了該類所有虛函數(shù)的地址。
  • 動態(tài)綁定:在運行時,根據(jù)對象的實際類型從虛函數(shù)表中查找并調(diào)用相應(yīng)的函數(shù)。
  • 動態(tài)聯(lián)編:通過基類指針或引用調(diào)用虛函數(shù)時,編譯時不確定具體調(diào)用哪個實現(xiàn),而是在運行時根據(jù)對象的實際類型確定。

通過這種方式,多態(tài)允許我們編寫與特定實現(xiàn)無關(guān)的代碼,提高了代碼的靈活性和可維護(hù)性。同時,這也帶來了額外的空間和時間開銷,因為每個有虛函數(shù)的對象都需要額外的空間來存儲vptr,并且在運行時需要進(jìn)行虛函數(shù)表的查找。盡管如此,多態(tài)帶來的好處通常超過了這些額外的開銷,使得它在許多情況下成為首選的設(shè)計模式。

1.2 多態(tài)如何解決代碼復(fù)用難題

在軟件開發(fā)中,代碼復(fù)用是提高開發(fā)效率、降低維護(hù)成本的關(guān)鍵。然而,在沒有多態(tài)的情況下,實現(xiàn)代碼復(fù)用往往面臨諸多挑戰(zhàn)。比如,我們要開發(fā)一個圖形繪制系統(tǒng),其中包含圓形、矩形和三角形等多種圖形。如果不使用多態(tài),那么為了繪制這些不同的圖形,我們可能需要編寫大量重復(fù)的代碼。

class Circle {
public:
    void drawCircle() {
        // 繪制圓形的具體代碼
        std::cout << "Drawing a circle" << std::endl;
    }
};

class Rectangle {
public:
    void drawRectangle() {
        // 繪制矩形的具體代碼
        std::cout << "Drawing a rectangle" << std::endl;
    }
};

class Triangle {
public:
    void drawTriangle() {
        // 繪制三角形的具體代碼
        std::cout << "Drawing a triangle" << std::endl;
    }
};

int main() {
    Circle circle;
    Rectangle rectangle;
    Triangle triangle;

    circle.drawCircle();
    rectangle.drawRectangle();
    triangle.drawTriangle();

    return 0;
}

在這段代碼中,每個圖形類都有自己獨立的繪制函數(shù),當(dāng)我們需要繪制不同的圖形時,需要分別調(diào)用不同的函數(shù)。如果后續(xù)要添加新的圖形,比如梯形,就需要再次編寫新的繪制函數(shù),并且在使用時也需要額外添加調(diào)用邏輯,代碼的擴(kuò)展性和復(fù)用性都很差 。

而當(dāng)我們引入多態(tài)后,情況就大不相同了。我們可以定義一個基類,比如Shape,在其中聲明一個虛函數(shù)draw,然后讓各個圖形類繼承自Shape類,并重寫draw函數(shù)。

class Shape {
public:
    virtual void draw() = 0; // 純虛函數(shù),使Shape成為抽象類
};

class Circle : public Shape {
public:
    void draw() override {
        // 繪制圓形的具體代碼
        std::cout << "Drawing a circle" << std::endl;
    }
};

class Rectangle : public Shape {
public:
    void draw() override {
        // 繪制矩形的具體代碼
        std::cout << "Drawing a rectangle" << std::endl;
    }
};

class Triangle : public Shape {
public:
    void draw() override {
        // 繪制三角形的具體代碼
        std::cout << "Drawing a triangle" << std::endl;
    }
};

void drawShapes(Shape* shapes[], int count) {
    for (int i = 0; i < count; ++i) {
        shapes[i]->draw();
    }
}

int main() {
    Circle circle;
    Rectangle rectangle;
    Triangle triangle;

    Shape* shapes[] = {&circle, &rectangle, &triangle};
    int count = sizeof(shapes) / sizeof(shapes[0]);

    drawShapes(shapes, count);

    return 0;
}

在這個改進(jìn)后的代碼中,drawShapes函數(shù)可以接受一個Shape類型的指針數(shù)組,無論數(shù)組中的元素是指向Circle、Rectangle還是Triangle對象,都可以通過調(diào)用draw函數(shù)來實現(xiàn)正確的繪制。這樣,當(dāng)我們需要添加新的圖形時,只需要創(chuàng)建一個新的派生類并重寫draw函數(shù),而drawShapes函數(shù)的代碼無需修改,大大提高了代碼的復(fù)用性和可擴(kuò)展性。

1.3 多態(tài)的實現(xiàn)原理

C++實現(xiàn)多態(tài)的主要方式有:

(1)重載(Overloading):通過函數(shù)名相同但參數(shù)不同的多個函數(shù)實現(xiàn)不同行為。在編譯時通過參數(shù)類型決定調(diào)用哪個函數(shù)。

void add(int a, int b) { ... } 
void add(double a, double b) { ... }

(2)重寫(Overriding):通過繼承讓派生類重新實現(xiàn)基類的虛函數(shù)。在運行時通過指針/引用的實際類型調(diào)用對應(yīng)的函數(shù)。

class Base {
public:
    virtual void func() { ... }
};

class Derived extends Base {
public:
    virtual void func() { ... } 
}; 

Base* b = new Derived();
b->func(); // Calls Derived::func()

(3)編譯時多態(tài):通過模板和泛型實現(xiàn)針對不同類型具有不同實現(xiàn)的函數(shù)。在編譯時通過傳入類型決定具體實現(xiàn)。

template <typename T>
void func(T t) { ... }

func(1);   // Calls func<int> 
func(3.2); // Calls func<double>

(4)條件編譯:通過#ifdef/#elif等預(yù)處理命令針對不同條件編譯不同的代碼實現(xiàn)產(chǎn)生不同行為的程序。編譯時通過定義的宏決定具體實現(xiàn)

#ifdef _WIN32 
    void func() { ... }   // Windows version
#elif __linux__
    void func() { ... }   // Linux version   
#endif

綜上,C++通過重載、重寫、模板、條件編譯等手段實現(xiàn)多態(tài)。其中,重寫基于繼承和虛函數(shù)實現(xiàn)真正的運行時多態(tài),增強了程序的靈活性和可擴(kuò)展性。

一個接口,多種方法:

  • 用virtual關(guān)鍵字申明的函數(shù)叫做虛函數(shù),虛函數(shù)肯定是類的成員函數(shù)。
  • 存在虛函數(shù)的類都有一個一維的虛函數(shù)表叫做虛表。當(dāng)類中聲明虛函數(shù)時,編譯器會在類中生成一個虛函數(shù)表。
  • 類的對象有一個指向虛表開始的虛指針。虛表是和類對應(yīng)的,虛表指針是和對象對應(yīng)的。
  • 虛函數(shù)表是一個存儲類成員函數(shù)指針的數(shù)據(jù)結(jié)構(gòu)。
  • 虛函數(shù)表是由編譯器自動生成與維護(hù)的。
  • virtual成員函數(shù)會被編譯器放入虛函數(shù)表中。
  • 當(dāng)存在虛函數(shù)時,每個對象中都有一個指向虛函數(shù)的指針(C++編譯器給父類對象,子類對象提前布局vptr指針),當(dāng)進(jìn)行test(parent *base)函數(shù)的時候,C++編譯器不需要區(qū)分子類或者父類對象,只需要在base指針中,找到vptr指針即可)。
  • vptr一般作為類對象的第一個成員。

Part2.malloc:動態(tài)內(nèi)存分配的魔法棒

2.1 malloc技術(shù)

在 C 和 C++ 編程中,malloc(memory allocation,動態(tài)內(nèi)存分配)是一個非常重要的函數(shù),它能在程序運行時根據(jù)需要動態(tài)分配內(nèi)存。想象一下,你正在搭建一座積木城堡,一開始不知道需要多少積木,malloc就像是一個神奇的袋子,能隨時根據(jù)你的需求提供積木(內(nèi)存)。

malloc函數(shù)的原型是void* malloc(size_t size),它接受一個參數(shù)size,表示需要分配的內(nèi)存字節(jié)數(shù)。返回值是一個void*類型的指針,指向分配的內(nèi)存塊起始地址,如果分配失敗則返回NULL 。例如,我們要動態(tài)分配一個能存儲 10 個整數(shù)的數(shù)組,可以這樣寫:

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

int main() {
    int *arr;
    // 分配10個整數(shù)大小的內(nèi)存空間
    arr = (int*)malloc(10 * sizeof(int)); 
    if (arr == NULL) {
        printf("內(nèi)存分配失敗\n");
        return 1;
    }
    // 使用數(shù)組
    for (int i = 0; i < 10; i++) {
        arr[i] = i;
    }
    // 輸出數(shù)組元素
    for (int i = 0; i < 10; i++) {
        printf("%d ", arr[i]);
    }
    // 釋放內(nèi)存
    free(arr); 
    return 0;
}

在使用malloc時,有幾個關(guān)鍵的注意事項。首先,一定要檢查返回值是否為NULL,以確保內(nèi)存分配成功,否則對空指針進(jìn)行操作會導(dǎo)致程序崩潰。其次,當(dāng)不再需要分配的內(nèi)存時,必須使用free函數(shù)釋放內(nèi)存,否則會造成內(nèi)存泄漏,就像你借了圖書館的書卻不歸還,導(dǎo)致后續(xù)其他人無法借閱(內(nèi)存無法被重新利用)。并且,free只能釋放由malloc、calloc、realloc分配的內(nèi)存,不能釋放其他類型的內(nèi)存。同時,釋放內(nèi)存后,應(yīng)將指針置為NULL,防止成為野指針,避免不小心再次訪問已釋放的內(nèi)存區(qū)域。

與其他內(nèi)存分配方式相比,malloc屬于動態(tài)內(nèi)存分配,與靜態(tài)內(nèi)存分配(如在函數(shù)內(nèi)部定義的局部變量)不同,靜態(tài)內(nèi)存的生命周期在函數(shù)結(jié)束時就結(jié)束了,而動態(tài)分配的內(nèi)存只要不調(diào)用free,就會一直存在,這使得它在需要靈活管理內(nèi)存的場景中非常有用。和 C++ 中的new操作符相比,malloc只是分配內(nèi)存,不會調(diào)用對象的構(gòu)造函數(shù)進(jìn)行初始化,而new會調(diào)用構(gòu)造函數(shù);malloc分配失敗返回NULL,new則是拋出異常。

2.2 Malloc函數(shù)

(1)數(shù)據(jù)結(jié)構(gòu)

首先我們要確定所采用的數(shù)據(jù)結(jié)構(gòu)。一個簡單可行方案是將堆內(nèi)存空間以塊的形式組織起來,每個塊由meta區(qū)和數(shù)據(jù)區(qū)組成,meta區(qū)記錄數(shù)據(jù)塊的元信息(數(shù)據(jù)區(qū)大小、空閑標(biāo)志位、指針等等),數(shù)據(jù)區(qū)是真實分配的內(nèi)存區(qū)域,并且數(shù)據(jù)區(qū)的第一個字節(jié)地址即為malloc返回的地址,可以使用如下結(jié)構(gòu)體定義一個block

typedef struct s_block *t_block;
struck s_block{
    size_t size;//數(shù)據(jù)區(qū)大小
    t_block next;//指向下個塊的指針
    int free;//是否是空閑塊
    int padding;//填充4字節(jié),保證meta塊長度為8的倍數(shù)
    char data[1];//這是一個虛擬字段,表示數(shù)據(jù)塊的第一個字節(jié),長度不應(yīng)計入meta
};
(2)尋找合適的block

現(xiàn)在考慮如何在block鏈中查找合適的block。一般來說有兩種查找算法:First fit:從頭開始,使用第一個數(shù)據(jù)區(qū)大小大于要求size的塊所謂此次分配的塊 Best fit:從頭開始,遍歷所有塊,使用數(shù)據(jù)區(qū)大小大于size且差值最小的塊作為此次分配的塊 兩種方式各有千秋,best fit有較高的內(nèi)存使用率(payload較高),而first fit具有較高的運行效率。這里我們采用first fit算法

t_block find_block(t_block *last,size_t size){
    t_block b = first_block;
    while(b&&b->size>=size)
    {
        *last = b;
        b = b->next;
    }
    return b;
}

find_block從first_block開始,查找第一個符合要求的block并返回block起始地址,如果找不到這返回NULL,這里在遍歷時會更新一個叫l(wèi)ast的指針,這個指針始終指向當(dāng)前遍歷的block.這是為了如果找不到合適的block而開辟新block使用的。

(3)開辟新的block

如果現(xiàn)有block都不能滿足size的要求,則需要在鏈表最后開辟一個新的block。這里關(guān)鍵是如何只使用sbrk創(chuàng)建一個struct:

#define BLOCK_SIZE 24

t_block extend_heap{
    t_block b;
    b = sbrk(0);
        if(sbrk(BLOCK_SIZE+s)==(void*)-1)
        return NULL;
        b->size = s;
        b->next - NULL;
        if(last)
        last->next = b;
        b->free = 0;
        return b;
};
(4)分裂block

First fit有一個比較致命的缺點,就是可能會讓更小的size占據(jù)很大的一塊block,此時,為了提高payload,應(yīng)該在剩余數(shù)據(jù)區(qū)足夠大的情況下,將其分裂為一個新的block:

void split_block(t_block b,size_t s)
{
    t_block new;
    new = b->data;
    new->size = b->size-s-BLOCK_SIZE;
    new->next = b->next;
    new ->free = 1;
    b->size = s;
    b->next = new;
}

2.3 Malloc函數(shù)的實現(xiàn)原理

(1)空閑鏈表機(jī)制

在malloc函數(shù)背后,有著依靠空閑鏈表來管理內(nèi)存的一套機(jī)制。當(dāng)我們調(diào)用malloc函數(shù)時,它會沿著空閑鏈表去查找滿足用戶請求大小的內(nèi)存塊。比如說,鏈表上有多個不同大小的空閑內(nèi)存塊,它就會依次遍歷這些塊來找到合適的那一個。

找到合適的內(nèi)存塊后,如果這個內(nèi)存塊比用戶請求的大小要大,那么就會按需將其分割成兩部分,一部分的大小剛好與用戶請求的大小相等,這部分就會分配給用戶使用,而剩下的那部分則會被放回空閑鏈表中,等待后續(xù)其他的內(nèi)存分配請求再進(jìn)行分配。例如,空閑鏈表中有一個 50 字節(jié)的空閑塊,而用戶請求分配 20 字節(jié)的內(nèi)存,這時malloc就會把這個 50 字節(jié)的塊分成 20 字節(jié)(分配給用戶)和 30 字節(jié)(放回空閑鏈表)兩塊。

而當(dāng)我們使用free函數(shù)釋放內(nèi)存時,相應(yīng)被釋放的內(nèi)存塊又會被重新連接到空閑鏈上。這樣,整個空閑鏈表就處于一個動態(tài)變化的過程,不斷地有內(nèi)存塊被分配出去,也不斷地有釋放的內(nèi)存塊回歸鏈表,以實現(xiàn)內(nèi)存的循環(huán)利用,避免浪費。不過,隨著程序不斷地分配和釋放內(nèi)存,空閑鏈有可能會被切成很多的小內(nèi)存片段,要是后續(xù)用戶申請一個較大的內(nèi)存片段時,空閑鏈上可能暫時沒有可以滿足要求的片段了,這時malloc函數(shù)可能就需要進(jìn)行一些整理操作,比如對這些小的空閑塊嘗試合并等,以便能滿足較大內(nèi)存請求的情況。

(2)虛擬內(nèi)存地址和物理內(nèi)存地址

為了簡單,現(xiàn)代操作系統(tǒng)在處理物理內(nèi)存地址時,普遍采用虛擬內(nèi)存地址技術(shù)。即在匯編程序?qū)用妫?dāng)涉及內(nèi)存地址時,都是使用的虛擬內(nèi)存地址。采用這種技術(shù)時,每個進(jìn)程仿佛自己獨享一片2N字節(jié)的內(nèi)存,其中N是機(jī)器位數(shù)。例如在64位CPU和64位操作系統(tǒng)下每個進(jìn)程的虛擬地址空間為264Byte。

這種虛擬地址空間的作用主要是簡化程序的編寫及方便操作系統(tǒng)對進(jìn)程間內(nèi)存的隔離管理,真實中的進(jìn)程不太可能如此大的空間,實際能用到的空間大小取決于物理內(nèi)存的大小。由于在機(jī)器語言層面都是采用虛擬地址,當(dāng)實際的機(jī)器碼程序涉及到內(nèi)存操作時,需要根據(jù)當(dāng)前進(jìn)程運行的實際上下文將虛擬地址轉(zhuǎn)化為物理內(nèi)存地址,才能實現(xiàn)對內(nèi)存數(shù)據(jù)的操作。這個轉(zhuǎn)換一般由一個叫MMU的硬件完成。

(3)頁與地址構(gòu)成

在現(xiàn)代操作系統(tǒng)中,不論是虛擬內(nèi)存還是物理內(nèi)存,都不是以字節(jié)為單位進(jìn)行管理的,而是以頁為單位。一個內(nèi)存頁是一段固定大小的連續(xù)的連續(xù)內(nèi)存地址的總稱,具體到Linux中,典型的內(nèi)存頁大小為4096 Byte。所以內(nèi)存地址可以分為頁號和頁內(nèi)偏移量。下面以64位機(jī)器,4G物理內(nèi)存,4K頁大小為例,虛擬內(nèi)存地址和物理內(nèi)存地址的組成如下:

圖片圖片

上面是虛擬內(nèi)存地址,下面是物理內(nèi)存地址。由于頁大小都是4k,所以頁內(nèi)偏移都是用低12位表示,而剩下的高地址表示頁號 MMU映射單位并不是字節(jié),而是頁,這個映射通過差一個常駐內(nèi)存的數(shù)據(jù)結(jié)構(gòu)頁表來實現(xiàn)。現(xiàn)在計算機(jī)具體的內(nèi)存地址映射比較復(fù)雜,為了加快速度會引入一系列緩存和優(yōu)化,例如TLB等機(jī)制,下面給出一個經(jīng)過簡化的內(nèi)存地址翻譯示意圖:

圖片圖片

(4)內(nèi)存頁與磁盤頁

我們知道一般將內(nèi)存看做磁盤的緩存,有時MMU在工作時,會發(fā)現(xiàn)頁表表名某個內(nèi)存頁不在物理內(nèi)存頁不在物理內(nèi)存中,此時會觸發(fā)一個缺頁異常,此時系統(tǒng)會到磁盤中相應(yīng)的地方將磁盤頁載入到內(nèi)存中,然后重新執(zhí)行由于缺頁而失敗的機(jī)器指令。關(guān)于這部分,因為可以看做對malloc實現(xiàn)是透明的,所以不再詳述。

真實地址翻譯流程:

圖片圖片

(5)Linux進(jìn)程級內(nèi)存管理

內(nèi)存排布:明白了虛擬內(nèi)存和物理內(nèi)存的關(guān)系及相關(guān)的映射機(jī)制,下面看一下具體在一個進(jìn)程內(nèi)是如何排布內(nèi)存的。以Linux 64位系統(tǒng)為例。理論上,64bit內(nèi)存地址空間為0x0000000000000000-0xFFFFFFFFFFFFFFF,這是個相當(dāng)龐大的空間,Linux實際上只用了其中一小部分。具體分布如圖所示:

圖片圖片

對用戶來說主要關(guān)心的是User Space。將User Space放大后,可以看到里面主要分成如下幾段:

  • Code:這是整個用戶空間的最低地址部分,存放的是指令(也就是程序所編譯成的可執(zhí)行機(jī)器碼) Data:這里存放的是初始化過的全局變量
  • BSS:這里存放的是未初始化的全局變量
  • Heap:堆,這是我們本文主要關(guān)注的地方,堆自底向上由低地址向高地址增長
  • Mapping Area:這里是與mmap系統(tǒng)調(diào)用相關(guān)區(qū)域。大多數(shù)實際的malloc實現(xiàn)會考慮通過mmap分配較大塊的內(nèi)存空間,本文不考慮這種情況,這個區(qū)域由高地址像低地址增長Stack:棧區(qū)域,自高地址像低地址增長 。
  • Heap內(nèi)存模型:一般來說,malloc所申請的內(nèi)存主要從Heap區(qū)域分配,來看看Heap的結(jié)構(gòu)是怎樣的。

圖片圖片

Linux維護(hù)一個break指針,這個指針執(zhí)行堆空間的某個地址,從堆開始到break之間的地址空間為映射好的,可以供進(jìn)程訪問,而從break往上,是未映射的地址空間,如果訪問這段空間則程序會報錯。

2.4 malloc函數(shù)的使用注意事項

⑴內(nèi)存分配成功判斷

在使用 malloc 函數(shù)時,一定要進(jìn)行內(nèi)存分配成功與否的判斷哦。因為 malloc 函數(shù)在執(zhí)行后,有可能會由于系統(tǒng)內(nèi)存不足等原因,導(dǎo)致無法按照要求分配出相應(yīng)的內(nèi)存空間。此時,它會返回一個 NULL 指針來表示分配失敗。

例如,我們寫這樣一段代碼:

int *p = (int *)malloc(1000000000 * sizeof(int));  // 嘗試分配非常大的內(nèi)存空間,可能超出系統(tǒng)可分配范圍
if (p == NULL) {
    printf("內(nèi)存分配失敗,無法繼續(xù)執(zhí)行后續(xù)操作!\n");
    // 在這里可以添加一些應(yīng)對分配失敗的處理邏輯,比如返回錯誤碼、進(jìn)行相應(yīng)提示等
    return -1;
}
// 如果分配成功,就可以繼續(xù)使用這塊內(nèi)存,例如進(jìn)行賦值等操作
*p = 10;

像這樣通過檢查返回的指針是否為 NULL,就能知道內(nèi)存分配是不是成功啦。要是忽略了這個判斷,后續(xù)還繼續(xù)去使用這個可能為 NULL 的指針,就很容易引發(fā)程序崩潰,比如出現(xiàn)段錯誤等情況呢,所以這一步的判斷千萬不能省略哦。

⑵內(nèi)存釋放操作

使用完 malloc 函數(shù)分配的內(nèi)存后,及時用 free 函數(shù)進(jìn)行釋放是非常重要的操作呀。因為如果一直不釋放這些申請來的內(nèi)存,它們就會始終被占用,久而久之,就容易造成內(nèi)存泄漏問題。內(nèi)存泄漏積累起來,會不斷消耗系統(tǒng)的可用內(nèi)存資源,可能使得程序運行越來越卡頓,嚴(yán)重的話甚至?xí)?dǎo)致整個系統(tǒng)癱瘓呢。

free 函數(shù)的使用方式很簡單,它的原型是 void free(void *FirstByte),參數(shù)就是之前 malloc 分配內(nèi)存時返回的那個指針。舉個簡單的例子來說明一下:

char *str = (char *)malloc(50 * sizeof(char));  // 分配可以存放50個字符的內(nèi)存空間
if (str!= NULL) {
    strcpy(str, "Hello World");  // 使用分配的內(nèi)存空間存放字符串
    // 一些其他對這塊內(nèi)存的操作......
    free(str);  // 使用完后,用free函數(shù)釋放內(nèi)存
}

這樣就把通過 malloc 申請的內(nèi)存歸還給系統(tǒng)了,讓系統(tǒng)可以把這些內(nèi)存再次分配給其他需要的部分使用哦。

⑶避免野指針問題

當(dāng)我們使用 free 函數(shù)釋放了 malloc 分配的內(nèi)存后,還有一個需要特別注意的點,那就是要避免野指針的出現(xiàn)哦。野指針就是指向了一塊已經(jīng)被釋放的內(nèi)存區(qū)域或者是指向不確定內(nèi)存位置的指針啦。

比如說,有這樣一段代碼:

int *ptr = (int *)malloc(10 * sizeof(int));
if (ptr!= NULL) {
    *ptr = 10;
    free(ptr);
    // 這里如果沒有將ptr置為NULL,ptr就變成了野指針
    if (ptr!= NULL) {  // 這個判斷其實是無效的哦,可能會意外進(jìn)入
        *ptr = 20;  // 此時再去對這塊已經(jīng)釋放的內(nèi)存區(qū)域進(jìn)行寫操作,就是錯誤的,可能導(dǎo)致程序出現(xiàn)未定義行為,比如崩潰或者數(shù)據(jù)錯亂等情況
    }
}

所以呀,為了避免這種情況發(fā)生,在釋放內(nèi)存后,正確的做法是把對應(yīng)的指針置為 NULL,像這樣修改一下上面的代碼:

int *ptr = (int *)malloc(10 * sizeof(int));
if (ptr!= NULL) {
    *ptr = 10;
    free(ptr);
    ptr = NULL;  // 釋放后將指針置為NULL,避免成為野指針
}

這樣就能有效地防止意外訪問已經(jīng)釋放的內(nèi)存區(qū)域,減少程序出現(xiàn)錯誤的風(fēng)險啦。

Part3.內(nèi)存管理:守護(hù)程序的 “內(nèi)存管家”

3.1 Linux內(nèi)存管理

內(nèi)存管理,是計算機(jī)系統(tǒng)中極為關(guān)鍵的一環(huán),就像一個精密工廠的物料管理員,負(fù)責(zé)著內(nèi)存資源的分配、回收和優(yōu)化,確保程序能夠高效穩(wěn)定地運行。在多任務(wù)環(huán)境下,多個程序同時運行,內(nèi)存管理要合理地為每個程序分配內(nèi)存空間,避免沖突和資源浪費。

內(nèi)存分配方式主要有靜態(tài)分配和動態(tài)分配。靜態(tài)分配在程序編譯時就確定了內(nèi)存大小和位置,比如全局變量和靜態(tài)變量,它們在程序運行期間一直占據(jù)固定內(nèi)存,優(yōu)點是簡單高效,缺點是缺乏靈活性 。動態(tài)分配則是在程序運行時根據(jù)需要申請內(nèi)存,如 C 語言中的malloc、C++ 的new以及 Java 中的new操作,它賦予了程序根據(jù)實際情況靈活使用內(nèi)存的能力,但也帶來了管理的復(fù)雜性,如內(nèi)存泄漏和內(nèi)存碎片等問題。

內(nèi)存回收同樣至關(guān)重要。在 C 和 C++ 中,使用free(對應(yīng)malloc)和delete(對應(yīng)new)手動釋放不再使用的內(nèi)存;Java、Python 等語言則引入了垃圾回收機(jī)制(Garbage Collection,GC),自動檢測和回收不再被引用的對象所占用的內(nèi)存,大大減輕了開發(fā)者的負(fù)擔(dān),不過垃圾回收也會帶來一定的性能開銷,并且可能導(dǎo)致程序出現(xiàn)短暫停頓。

內(nèi)存優(yōu)化是提升系統(tǒng)性能的關(guān)鍵。優(yōu)化方法包括減少內(nèi)存碎片,內(nèi)存碎片是由于頻繁的內(nèi)存分配和釋放導(dǎo)致的內(nèi)存不連續(xù)現(xiàn)象,會降低內(nèi)存利用率,可通過采用合適的內(nèi)存分配算法(如伙伴系統(tǒng)算法、 slab 分配器等)來緩解;合理使用緩存,緩存利用局部性原理,將常用數(shù)據(jù)存儲在高速緩存中,減少對主內(nèi)存的訪問次數(shù),從而提高數(shù)據(jù)訪問速度;還有優(yōu)化數(shù)據(jù)結(jié)構(gòu),選擇合適的數(shù)據(jù)結(jié)構(gòu)可以減少內(nèi)存占用,例如在需要頻繁插入和刪除元素的場景中,使用鏈表比數(shù)組更節(jié)省內(nèi)存。

內(nèi)存管理不當(dāng)會引發(fā)一系列問題,如內(nèi)存泄漏,內(nèi)存泄漏指程序中已分配的內(nèi)存由于某種原因未被釋放,隨著程序運行,泄漏的內(nèi)存越來越多,最終導(dǎo)致系統(tǒng)內(nèi)存耗盡,程序崩潰。再比如空指針引用,當(dāng)訪問一個空指針指向的內(nèi)存時,會導(dǎo)致程序異常終止,這通常是由于內(nèi)存分配失敗或內(nèi)存釋放后未將指針置為NULL造成的。

3.2 物理內(nèi)存與虛擬內(nèi)存

(1)物理內(nèi)存的管理

物理內(nèi)存是計算機(jī)系統(tǒng)中實際存在的內(nèi)存,它由計算機(jī)硬件直接管理,通常由 DRAM 芯片組成,是計算機(jī)系統(tǒng)中最快的存儲器 ,其大小通常是固定的,取決于計算機(jī)硬件的配置。在 Linux 內(nèi)核中,物理內(nèi)存被劃分為一個個固定大小的頁框(Page Frame),每個頁框通常為 4KB(也有其他大小,如 2MB、1GB 的大頁)。內(nèi)核通過頁框號(Page Frame Number,PFN)來管理這些頁框,就像給圖書館的每一本書都編上了唯一的編號,方便查找和管理。

物理內(nèi)存的作用至關(guān)重要,它是程序運行和數(shù)據(jù)存儲的直接場所。當(dāng)程序運行時,其代碼和數(shù)據(jù)會被加載到物理內(nèi)存中,CPU 直接從物理內(nèi)存中讀取指令和數(shù)據(jù)進(jìn)行處理。比如,當(dāng)我們啟動一個文本編輯器時,編輯器的程序代碼會被加載到物理內(nèi)存的某個區(qū)域,我們在編輯器中輸入的文本數(shù)據(jù)也會存儲在物理內(nèi)存中,這樣 CPU 才能快速地對這些數(shù)據(jù)進(jìn)行處理,實現(xiàn)文本的編輯、保存等操作。

(2)虛擬內(nèi)存的奧秘

虛擬內(nèi)存,簡單來說,是一種內(nèi)存管理技術(shù),它為每個進(jìn)程提供了一個獨立的、連續(xù)的地址空間,讓進(jìn)程誤以為自己擁有一塊完整且足夠大的內(nèi)存空間 ,而無需關(guān)心實際物理內(nèi)存的具體布局和大小限制。這就好比你擁有一個超大的虛擬倉庫,你可以隨意規(guī)劃貨物的擺放位置,而不用擔(dān)心倉庫空間不夠。

虛擬內(nèi)存的主要作用之一是實現(xiàn)內(nèi)存地址轉(zhuǎn)換。在 Linux 系統(tǒng)中,每個進(jìn)程都有自己的虛擬地址空間,這個空間通過頁表(Page Table)與物理內(nèi)存進(jìn)行映射。頁表就像是一本地址翻譯字典,負(fù)責(zé)將進(jìn)程使用的虛擬地址翻譯成實際的物理地址。

當(dāng)程序運行時,它所訪問的內(nèi)存地址都是虛擬地址。例如,當(dāng)程序需要讀取某個變量的值時,它會給出一個虛擬地址。CPU 首先會根據(jù)這個虛擬地址中的頁號(Page Number)在頁表中查找對應(yīng)的物理頁框號(Page Frame Number)。如果頁表中存在這個映射關(guān)系(即頁表項有效),CPU 就可以通過物理頁框號和虛擬地址中的頁內(nèi)偏移(Offset)計算出實際的物理地址,從而訪問到物理內(nèi)存中的數(shù)據(jù)。

但如果頁表中沒有找到對應(yīng)的映射關(guān)系(即發(fā)生缺頁異常,Page Fault),系統(tǒng)會認(rèn)為這個虛擬頁還沒有被加載到物理內(nèi)存中。此時,操作系統(tǒng)會介入,從磁盤的交換區(qū)(Swap Area)或者文件系統(tǒng)中找到對應(yīng)的物理頁,并將其加載到物理內(nèi)存中,同時更新頁表,建立虛擬地址與物理地址的映射關(guān)系。之后,程序就可以通過新建立的映射關(guān)系訪問到數(shù)據(jù)了。

為了更直觀地理解,我們可以把虛擬內(nèi)存想象成一個圖書館的目錄系統(tǒng)。每個進(jìn)程就像是一個讀者,擁有自己的目錄(虛擬地址空間)。當(dāng)讀者想要查找某本書(訪問數(shù)據(jù))時,會先在自己的目錄中找到對應(yīng)的條目(虛擬地址),然后通過這個條目去書架(物理內(nèi)存)上找到實際的書。如果書架上沒有這本書(缺頁異常),圖書館管理員(操作系統(tǒng))就會從倉庫(磁盤)中把書取出來放到書架上,并更新目錄(頁表),以便下次讀者能更快地找到這本書。

例如:對于程序計數(shù)器位數(shù)為32位的處理器來說,他的地址發(fā)生器所能發(fā)出的地址數(shù)目為2^32=4G個,于是這個處理器所能訪問的最大內(nèi)存空間就是4G。在計算機(jī)技術(shù)中,這個值就叫做處理器的尋址空間或?qū)ぶ纺芰Α?/span>

照理說,為了充分利用處理器的尋址空間,就應(yīng)按照處理器的最大尋址來為其分配系統(tǒng)的內(nèi)存。如果處理器具有32位程序計數(shù)器,那么就應(yīng)該按照下圖的方式,為其配備4G的內(nèi)存:

圖片圖片

這樣,處理器所發(fā)出的每一個地址都會有一個真實的物理存儲單元與之對應(yīng);同時,每一個物理存儲單元都有唯一的地址與之對應(yīng)。這顯然是一種最理想的情況。

3.3 分頁:虛擬內(nèi)存的基石

在 Linux 的世界里,內(nèi)存分頁機(jī)制就像是一位有條不紊的大管家,精心管理著系統(tǒng)的內(nèi)存資源。簡單來說,內(nèi)存分頁機(jī)制就是把物理內(nèi)存和虛擬內(nèi)存分割成固定大小的小塊,這些小塊被稱作 “頁” ,每個頁的大小一般為 4KB 或者 8KB。就好比你有一個巨大的倉庫(內(nèi)存),為了更好地管理里面的貨物(數(shù)據(jù)),你把倉庫劃分成了一個個大小相同的小隔間(頁)。

圖片圖片

(1)什么是分頁機(jī)制

分頁機(jī)制是 80x86 內(nèi)存管理機(jī)制的第二部分。它在分段機(jī)制的基礎(chǔ)上完成虛擬地址到物理地址的轉(zhuǎn)換過程。分段機(jī)制把邏輯地址轉(zhuǎn)換成線性地址,而分頁機(jī)制則把線性地址轉(zhuǎn)換成物理地址。分頁機(jī)制可用于任何一種分段模型。處理器分頁機(jī)制會把線性地址空間劃分成頁面,然后這些線性地址空間頁面被映射到物理地址空間的頁面上。分頁機(jī)制的幾種頁面級保護(hù)措施,可和分段機(jī)制保護(hù)措施或用或替代分段機(jī)制的保護(hù)措施。

(2)分頁機(jī)制如何啟用

在我們進(jìn)行程序開發(fā)的時候,一般情況下,是不需要管理內(nèi)存的,也不需要操心內(nèi)存夠不夠用,其實,這就是分頁機(jī)制給我們帶來的好處。它是實現(xiàn)虛擬存儲的關(guān)鍵,位于線性地址與物理地址之間,在使用這種內(nèi)存分頁管理方法時,每個執(zhí)行中的進(jìn)程(任務(wù))可以使用比實際內(nèi)存容量大得多的連續(xù)地址空間。而且當(dāng)系統(tǒng)內(nèi)存實際上被分成很多凌亂的塊時,它可以建立一個大而連續(xù)的內(nèi)存空間的映象,好讓程序不用操心和管理這些分散的內(nèi)存塊。

分頁機(jī)制增強了分段機(jī)制的性能。頁地址變換是建立在段變換基礎(chǔ)之上的。因為,段管理機(jī)制對于Intel處理器來說是最基本的,任何時候都無法關(guān)閉。所以即使啟用了頁管理功能,分段機(jī)制依然是起作用的,段部件也依然工作。

分頁只能在保護(hù)模式(CR0.PE = 1)下使用。在保護(hù)模式下,是否開啟分頁,由 CR0. PG 位(位 31)決定:

  • 當(dāng) CR0.PG = 0 時,未開啟分頁,線性地址等同于物理地址;
  • 當(dāng) CR0.PG = 1 時,開啟分頁。
(3)分頁機(jī)制線性地址到物理地址轉(zhuǎn)換過程

80x86使用 4K 字節(jié)固定大小的頁面,每個頁面均是 4KB,并且對其于 4K 地址邊界處。這表示分頁機(jī)制把 2^32字節(jié)(4GB)的線性地址空間劃分成 2^20(1M = 1048576)個頁面。分頁機(jī)制通過把線性地址空間中的頁面重新定位到物理地址空間中進(jìn)行操作。由于 4K 大小的頁面作為一個單元進(jìn)行映射,并且對其于 4K 邊界,因此線性地址的低 12 位可做為頁內(nèi)偏移地量直接作為物理地址的低 12 位。分頁機(jī)制執(zhí)行的重定向功能可以看作是把線性地址的高 20 位轉(zhuǎn)換到對應(yīng)物理地址的高 20 位。

線性到物理地址轉(zhuǎn)換功能,被擴(kuò)展成允許一個線性地址被標(biāo)注為無效的,而非要讓其產(chǎn)生一個物理地址。以下兩種情況一個頁面可以被標(biāo)注為無效的:

  • 1. 操作系統(tǒng)不支持的線性地址。
  • 2. 對應(yīng)的虛擬內(nèi)存系統(tǒng)中的頁面在磁盤上而非在物理內(nèi)存中。

在第一中情況下,產(chǎn)生無效地址的程序必須被終止,在第二種情況下,該無效地址實際上是請求 操作系統(tǒng)虛擬內(nèi)存管理器 把對應(yīng)的頁面從磁盤加載到物理內(nèi)存中,以供程序訪問。因為無效頁面通常與虛擬存儲系統(tǒng)相關(guān),因此它們被稱為不存在頁面,由頁表中稱為存在的屬性來確定。

當(dāng)使用分頁時,處理器會把線性地址空間劃分成固定大小的頁面(4KB),這些頁面可以映射到物理內(nèi)存中或磁盤存儲空間中,當(dāng)一個程序引用內(nèi)存中的邏輯地址時,處理器會把該邏輯地址轉(zhuǎn)換成一個線性地址,然后使用分頁機(jī)制把該線性地址轉(zhuǎn)換成對應(yīng)的物理地址。

如果包含線性地址的頁面不在當(dāng)前物理內(nèi)存中,處理器就會產(chǎn)生一個頁錯誤異常。頁錯誤異常處理程序就會讓操作系統(tǒng)從磁盤中把相應(yīng)頁面加載到物理內(nèi)存中(操作過程中可能會把物理內(nèi)存中不同的頁面寫到磁盤上)。當(dāng)頁面加載到物理內(nèi)存之后,從異常處理過程的返回操作會使異常的指令被重新執(zhí)行。處理器把用于線性地址轉(zhuǎn)換成物理地址和用于產(chǎn)生頁錯誤的信息包含在存儲與內(nèi)存中的頁目錄與頁表中。

(4)分頁機(jī)制與分段機(jī)制的不同

分頁與分段的最大的不同之處在于分頁使用了固定長度的頁面。段的長度通常與存放在其中的代碼或數(shù)據(jù)結(jié)構(gòu)有相同的長度。與段不同,頁面有固定的長度。如果僅使用分段地址轉(zhuǎn)換,那么存儲在物理內(nèi)存中的一個數(shù)據(jù)結(jié)構(gòu)將包含其所有的部分。如果使用了分頁,那么一個數(shù)據(jù)結(jié)構(gòu)就可以一部分存儲與物理內(nèi)存中,而另一部分保存在磁盤中。

為了減少地址轉(zhuǎn)換所要求的總線周期數(shù)量,最近訪問的頁目錄和頁表會被存放在處理器的一個叫做轉(zhuǎn)換查找緩沖區(qū)(TLB)的緩沖器件中。TLB 可以滿足大多數(shù)讀頁目錄和頁表的請求而無需使用總線周期。只有當(dāng) TLB 中不包含所要求的頁表項是才會出現(xiàn)使用額外的總線周期從內(nèi)存讀取頁表項。通常在一個頁表項很長時間沒有訪問過時才會出現(xiàn)這種情況。

3.4 交換空間:內(nèi)存不足時的后盾

首先呢,提一個概念,交換空間(swap space),這個大家應(yīng)該不陌生,在重裝系統(tǒng)的時候,會讓你選擇磁盤分區(qū),就比如說一個硬盤分幾個部分去管理。其中就會分一部分磁盤空間用作交換,叫做swap space。其實就是一段臨時存儲空間,內(nèi)存不夠用的時候就用它了,雖然它也在磁盤中,但省去了很多的查找時間啊。當(dāng)發(fā)生進(jìn)程切換的時候,內(nèi)存與交換空間就要發(fā)生數(shù)據(jù)交換一滿足需求。所以啊,進(jìn)程的切換消耗是很大的,這也說明了為什么自旋鎖比信號量效率高的原因。

那么我們的程序里申請的內(nèi)存的時候,linux內(nèi)核其實只分配一個虛擬內(nèi)存( 線性地址),并沒有分配實際的物理內(nèi)存。只有當(dāng)程序真正使用這塊內(nèi)存時,才會分配物理內(nèi)存。這就叫做延遲分配和請頁機(jī)制。釋放內(nèi)存時,先釋放線性區(qū)對應(yīng)的物理內(nèi)存,然后釋放線性區(qū);"請頁機(jī)制"將物理內(nèi)存的分配延后了,這樣是充分利用了程序的局部性原來,節(jié)約內(nèi)存空間,提高系統(tǒng)吞吐;就是說一個函數(shù)可能只在物理內(nèi)存中呆了一會,用完了就被清除出去了,雖然在虛擬地址空間還在。(不過虛擬地址空間不是事實上的存儲,所以只能說這個函數(shù)占據(jù)了一段虛擬地址空間,當(dāng)你訪問這段地址時,就會產(chǎn)生缺頁處理,從交換區(qū)把對應(yīng)的代碼搬到物理內(nèi)存上來)

Swap 交換機(jī)制是 Linux 虛擬內(nèi)存管理的另一個重要組成部分。簡單來說,Swap 是磁盤上的一塊區(qū)域,當(dāng)物理內(nèi)存不足時,系統(tǒng)會將一部分暫時不用的內(nèi)存頁面(Page)交換到 Swap 空間中,騰出物理內(nèi)存給更需要的進(jìn)程使用 。當(dāng)被交換出去的頁面再次被訪問時,系統(tǒng)會將其從 Swap 空間換回到物理內(nèi)存中。

Swap 交換機(jī)制的工作原理涉及到內(nèi)存回收和頁面置換算法。當(dāng)系統(tǒng)內(nèi)存緊張時,內(nèi)核會啟動內(nèi)存回收機(jī)制,掃描內(nèi)存中的頁面,選擇一些不常用或最近最少使用的頁面進(jìn)行回收。如果這些頁面是匿名頁面(沒有關(guān)聯(lián)到文件的內(nèi)存頁面,如進(jìn)程的堆和棧空間),就會被交換到 Swap 空間中;如果是文件映射頁面(關(guān)聯(lián)到文件的內(nèi)存頁面,如共享庫、文件緩存等),則會根據(jù)情況進(jìn)行處理,臟頁面(被修改過的頁面)會被寫回文件,干凈頁面(未被修改過的頁面)可以直接釋放。

Swap 交換機(jī)制對系統(tǒng)性能有著重要的影響。當(dāng) Swap 使用頻繁時,說明物理內(nèi)存不足,系統(tǒng)需要頻繁地在物理內(nèi)存和 Swap 空間之間交換頁面,這會導(dǎo)致磁盤 I/O 增加,系統(tǒng)性能下降 。因為磁盤的讀寫速度遠(yuǎn)遠(yuǎn)低于內(nèi)存,過多的 Swap 操作會使系統(tǒng)變得遲緩。因此,在實際應(yīng)用中,需要合理配置 Swap 空間的大小,并密切關(guān)注系統(tǒng)的內(nèi)存使用情況,避免 Swap 過度使用。

例如,可以通過調(diào)整/proc/sys/vm/swappiness參數(shù)來控制系統(tǒng)對 Swap 的使用傾向,swappiness的值范圍是 0 - 100,表示系統(tǒng)將內(nèi)存頁面交換到 Swap 空間的傾向程度,值越大表示越傾向于使用 Swap 。一般來說,對于內(nèi)存充足的系統(tǒng),可以將swappiness設(shè)置為較低的值,如 10 或 20,以減少不必要的 Swap 操作;對于內(nèi)存緊張的系統(tǒng),可以適當(dāng)提高swappiness的值,但也要注意不要過高,以免嚴(yán)重影響性能。

3.5 內(nèi)存映射:高效 I/O 的秘訣

內(nèi)存映射,英文名為 Memory - mapped I/O,從字面意思理解,就是將磁盤文件的數(shù)據(jù)映射到內(nèi)存中。在 Linux 系統(tǒng)中,這一機(jī)制允許進(jìn)程把一個文件或者設(shè)備的數(shù)據(jù)關(guān)聯(lián)到內(nèi)存地址空間,使得進(jìn)程能夠像訪問內(nèi)存一樣對文件進(jìn)行操作 。

舉個簡單的例子,假設(shè)有一個文本文件,通常我們讀取它時,會使用read函數(shù),數(shù)據(jù)從磁盤先讀取到內(nèi)核緩沖區(qū),再拷貝到用戶空間。而內(nèi)存映射則直接在進(jìn)程的虛擬地址空間中為這個文件創(chuàng)建一個映射區(qū)域,進(jìn)程可以直接通過指針訪問這個映射區(qū)域,就好像文件數(shù)據(jù)已經(jīng)在內(nèi)存中一樣,大大簡化了文件操作的流程 。

內(nèi)存映射的工作原理涉及到虛擬內(nèi)存、頁表以及文件系統(tǒng)等多個方面的知識。當(dāng)進(jìn)程調(diào)用mmap函數(shù)進(jìn)行內(nèi)存映射時,大致會經(jīng)歷以下幾個關(guān)鍵步驟 :

  1. 虛擬內(nèi)存區(qū)域創(chuàng)建:系統(tǒng)首先在進(jìn)程的虛擬地址空間中尋找一段滿足要求的連續(xù)空閑虛擬地址,然后為這段虛擬地址分配一個vm_area_struct結(jié)構(gòu),這個結(jié)構(gòu)用于描述虛擬內(nèi)存區(qū)域的各種屬性,如起始地址、結(jié)束地址、權(quán)限等,并將其插入到進(jìn)程的虛擬地址區(qū)域鏈表或樹中 。就好比在一片空地上,規(guī)劃出一塊特定大小和用途的區(qū)域,并做好標(biāo)記。
  2. 地址映射建立:通過待映射的文件指針,找到對應(yīng)的文件描述符,進(jìn)而鏈接到內(nèi)核 “已打開文件集” 中該文件的文件結(jié)構(gòu)體。再通過這個文件結(jié)構(gòu)體,調(diào)用內(nèi)核函數(shù)mmap,定位到文件磁盤物理地址,然后通過remap_pfn_range函數(shù)建立頁表,實現(xiàn)文件物理地址和進(jìn)程虛擬地址的一一映射關(guān)系 。這一步就像是在規(guī)劃好的區(qū)域和實際的文件存儲位置之間建立起一條通道,讓數(shù)據(jù)能夠順利流通。不過,此時只是建立了地址映射,真正的數(shù)據(jù)還沒有拷貝到內(nèi)存中 。
  3. 數(shù)據(jù)加載(缺頁異常處理):當(dāng)進(jìn)程首次訪問映射區(qū)域中的數(shù)據(jù)時,由于數(shù)據(jù)還未在物理內(nèi)存中,會觸發(fā)缺頁異常。內(nèi)核會捕獲這個異常,然后在交換緩存空間(swap cache)中尋找需要訪問的內(nèi)存頁,如果沒有找到,則調(diào)用nopage函數(shù)把所缺的頁從磁盤裝入到主存中 。這個過程就像是當(dāng)你需要使用某個物品,但它不在身邊,你就需要去存放它的地方把它取回來。之后,進(jìn)程就可以對這片主存進(jìn)行正常的讀或?qū)懖僮鳎绻麑懖僮鞲淖兞藬?shù)據(jù)內(nèi)容,系統(tǒng)會在一定時間后自動將臟頁面回寫臟頁面到對應(yīng)磁盤地址,完成寫入到文件的過程 。當(dāng)然,也可以調(diào)用msync函數(shù)來強制同步,讓數(shù)據(jù)立即保存到文件里 。

mmap內(nèi)存映射的實現(xiàn)過程,總的來說可以分為三個階段:

①進(jìn)程啟動映射過程,并在虛擬地址空間中為映射創(chuàng)建虛擬映射區(qū)域

  • 進(jìn)程在用戶空間調(diào)用庫函數(shù)mmap,原型:void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
  • 在當(dāng)前進(jìn)程的虛擬地址空間中,尋找一段空閑的滿足要求的連續(xù)的虛擬地址
  • 為此虛擬區(qū)分配一個vm_area_struct結(jié)構(gòu),接著對這個結(jié)構(gòu)的各個域進(jìn)行了初始化
  • 將新建的虛擬區(qū)結(jié)構(gòu)(vm_area_struct)插入進(jìn)程的虛擬地址區(qū)域鏈表或樹中

②調(diào)用內(nèi)核空間的系統(tǒng)調(diào)用函數(shù)mmap(不同于用戶空間函數(shù)),實現(xiàn)文件物理地址和進(jìn)程虛擬地址的一一映射關(guān)系

  • 為映射分配了新的虛擬地址區(qū)域后,通過待映射的文件指針,在文件描述符表中找到對應(yīng)的文件描述符,通過文件描述符,鏈接到內(nèi)核“已打開文件集”中該文件的文件結(jié)構(gòu)體(struct file),每個文件結(jié)構(gòu)體維護(hù)著和這個已打開文件相關(guān)各項信息。
  • 通過該文件的文件結(jié)構(gòu)體,鏈接到file_operations模塊,調(diào)用內(nèi)核函數(shù)mmap,其原型為:int mmap(struct file *filp, struct vm_area_struct *vma),不同于用戶空間庫函數(shù)。
  • 內(nèi)核mmap函數(shù)通過虛擬文件系統(tǒng)inode模塊定位到文件磁盤物理地址。
  • 通過remap_pfn_range函數(shù)建立頁表,即實現(xiàn)了文件地址和虛擬地址區(qū)域的映射關(guān)系。此時,這片虛擬地址并沒有任何數(shù)據(jù)關(guān)聯(lián)到主存中。

③進(jìn)程發(fā)起對這片映射空間的訪問,引發(fā)缺頁異常,實現(xiàn)文件內(nèi)容到物理內(nèi)存(主存)的拷貝

注:前兩個階段僅在于創(chuàng)建虛擬區(qū)間并完成地址映射,但是并沒有將任何文件數(shù)據(jù)的拷貝至主存。真正的文件讀取是當(dāng)進(jìn)程發(fā)起讀或?qū)懖僮鲿r。

  • 進(jìn)程的讀或?qū)懖僮髟L問虛擬地址空間這一段映射地址,通過查詢頁表,發(fā)現(xiàn)這一段地址并不在物理頁面上。因為目前只建立了地址映射,真正的硬盤數(shù)據(jù)還沒有拷貝到內(nèi)存中,因此引發(fā)缺頁異常。
  • 缺頁異常進(jìn)行一系列判斷,確定無非法操作后,內(nèi)核發(fā)起請求調(diào)頁過程。
  • 調(diào)頁過程先在交換緩存空間(swap cache)中尋找需要訪問的內(nèi)存頁,如果沒有則調(diào)用nopage函數(shù)把所缺的頁從磁盤裝入到主存中。
  • 之后進(jìn)程即可對這片主存進(jìn)行讀或者寫的操作,如果寫操作改變了其內(nèi)容,一定時間后系統(tǒng)會自動回寫臟頁面到對應(yīng)磁盤地址,也即完成了寫入到文件的過程。
  • 注:修改過的臟頁面并不會立即更新回文件中,而是有一段時間的延遲,可以調(diào)用msync()來強制同步, 這樣所寫的內(nèi)容就能立即保存到文件里了。

Part4.并發(fā)編程:開啟程序高效運行的大門

4.1 并發(fā)編程技術(shù)

隨著計算機(jī)硬件技術(shù)的飛速發(fā)展,多核處理器已經(jīng)成為主流,并發(fā)編程也因此變得愈發(fā)重要。并發(fā)編程,簡單來說,就是讓程序中的多個任務(wù)在同一時間段內(nèi)執(zhí)行,從而充分利用多核 CPU 的資源,顯著提升程序的執(zhí)行效率和響應(yīng)速度。

在日常生活中,并發(fā)編程的應(yīng)用場景隨處可見。比如在 Web 服務(wù)器中,當(dāng)多個用戶同時訪問網(wǎng)站時,服務(wù)器需要并發(fā)處理這些請求,快速響應(yīng)每個用戶,而不是讓用戶排隊等待;在視頻編輯軟件里,一邊加載視頻素材,一邊進(jìn)行視頻渲染,多個任務(wù)并發(fā)進(jìn)行,節(jié)省用戶的時間。

并發(fā)編程帶來強大功能的同時,也伴隨著諸多挑戰(zhàn)。其中,數(shù)據(jù)競爭是一個常見問題,當(dāng)多個線程同時訪問和修改共享數(shù)據(jù)時,就可能出現(xiàn)數(shù)據(jù)不一致的情況。例如,兩個線程同時讀取一個變量的值,然后各自進(jìn)行加 1 操作,最后再寫回變量,由于線程執(zhí)行順序的不確定性,最終變量的值可能并不是預(yù)期的增加了 2 。死鎖也是一個棘手的問題,當(dāng)兩個或多個線程相互等待對方釋放資源時,就會陷入死鎖狀態(tài),程序無法繼續(xù)執(zhí)行。比如線程 A 持有資源 1,等待資源 2,而線程 B 持有資源 2,等待資源 1,此時就會發(fā)生死鎖。

為了解決這些問題,人們想出了許多有效的方法。對于數(shù)據(jù)競爭,可以使用鎖機(jī)制,如互斥鎖(Mutex),它就像一把鑰匙,同一時間只有一個線程能拿到這把鑰匙,進(jìn)入臨界區(qū)訪問共享數(shù)據(jù),從而保證數(shù)據(jù)的一致性。讀寫鎖(Read - Write Lock)也是一種常用的鎖,它允許多個線程同時進(jìn)行讀操作,但寫操作時會獨占資源,適用于讀多寫少的場景。還可以采用原子操作,像 Java 中的Atomic類,它提供了一系列原子操作方法,如AtomicInteger的incrementAndGet方法,能保證對整數(shù)的自增操作是原子的,不會受到其他線程的干擾。對于死鎖問題,避免死鎖的策略包括按照固定順序獲取鎖,避免嵌套鎖;設(shè)置鎖的超時時間,當(dāng)?shù)却i超時后,線程放棄等待,避免無限期等待;使用資源分配圖算法檢測死鎖,一旦發(fā)現(xiàn)死鎖,及時采取措施解除死鎖。

在并發(fā)編程領(lǐng)域,有許多常用的編程模型和工具。線程池是一種廣泛使用的工具,它預(yù)先創(chuàng)建好一定數(shù)量的線程,當(dāng)有任務(wù)到來時,直接從線程池中獲取線程執(zhí)行任務(wù),任務(wù)完成后,線程又回到線程池中等待下一個任務(wù),這樣可以避免頻繁創(chuàng)建和銷毀線程帶來的開銷,提高線程的復(fù)用性和執(zhí)行效率。例如,在 Java 中,可以使用ThreadPoolExecutor類來創(chuàng)建和管理線程池,通過設(shè)置核心線程數(shù)、最大線程數(shù)、任務(wù)隊列等參數(shù),靈活控制線程池的行為。還有并發(fā)集合類,如 Java 中的ConcurrentHashMap,它是線程安全的哈希表,在多線程環(huán)境下能高效地進(jìn)行數(shù)據(jù)存儲和讀取操作,比傳統(tǒng)的HashMap更適合并發(fā)場景 。消息隊列也是一種重要的并發(fā)編程模型,它通過異步消息傳遞的方式,解耦生產(chǎn)者和消費者,實現(xiàn)任務(wù)的異步處理和并發(fā)執(zhí)行,常見的消息隊列有 Kafka、RabbitMQ 等。

4.2 什么是無鎖編程

無鎖編程,即不使用鎖的情況下實現(xiàn)多線程之間的變量同步,也就是在沒有線程被阻塞的情況下實現(xiàn)變量的同步,所以也叫非阻塞同步(Non-blocking Synchronization),實現(xiàn)非阻塞同步的方案稱為“無鎖編程算法”。

為什么要非阻塞同步,使用lock實現(xiàn)線程同步有非常多缺點:

  • 產(chǎn)生競爭時,線程被阻塞等待,無法做到線程實時響應(yīng)
  • dead lock
  • live lock
  • 優(yōu)先級反轉(zhuǎn)
  • 使用不當(dāng),造成性能下降

假設(shè)在不使用 lock 的情況下,實現(xiàn)變量同步,那就會避免非常多問題。盡管眼下來看,無鎖編程并不能替代 lock。

實現(xiàn)級別

非同步阻塞的實現(xiàn)分為三個級別:wait-free/lock-free/obstruction-free

(1)wait-free

  • 最理想的模式,整個操作保證每一個線程在有限步驟下完畢
  • 保證系統(tǒng)級吞吐(system-wide throughput)以及無線程饑餓
  • 截止2011年,沒有多少詳細(xì)的實現(xiàn)。即使實現(xiàn)了,也須要依賴于詳細(xì)CPU

(2)lock-free

  • 同意個別線程饑餓,但保證系統(tǒng)級吞吐。
  • 確保至少有一個線程可以繼續(xù)運行。
  • wait-free的算法必然也是lock-free的。
(3)obstruction-free

在不論什么時間點,一個線程被隔離為一個事務(wù)進(jìn)行運行(其它線程suspended),而且在有限步驟內(nèi)完畢。在運行過程中,一旦發(fā)現(xiàn)數(shù)據(jù)被改動(採用時間戳、版本),則回滾,也叫做樂觀鎖,即樂觀并發(fā)控制(OOC)。

事務(wù)的過程是:

  • 讀取,并寫時間戳
  • 準(zhǔn)備寫入,版本號校驗
  • 檢驗通過則寫入,檢驗不通過,則回滾

lock-free必然是obstruction-free的。

4.3 為什么要無鎖?

首先是性能考慮。通信項目一般對性能有極致的追求,這是我們使用無鎖的重要原因。當(dāng)然,無鎖算法如果實現(xiàn)的不好,性能可能還不如使用鎖,所以我們選擇比較擅長的數(shù)據(jù)結(jié)構(gòu)和算法進(jìn)行l(wèi)ock-free實現(xiàn),比如Queue,對于比較復(fù)雜的數(shù)據(jù)結(jié)構(gòu)和算法我們通過lock來控制,比如Map(雖然我們實現(xiàn)了無鎖Hash,但是大小是限定的,而Map是大小不限定的),對于性能數(shù)據(jù),后續(xù)文章會給出無鎖和有鎖的對比。

次要是避免鎖的使用引起的錯誤和問題:

  • 死鎖(dead lock):兩個以上線程互相等待
  • 鎖護(hù)送(lock convoy):多個同優(yōu)先級的線程反復(fù)競爭同一個鎖,搶占鎖失敗后強制上下文切換,引起性能下降
  • 優(yōu)先級反轉(zhuǎn)(priority inversion):低優(yōu)先級線程擁有鎖時被中優(yōu)先級的線程搶占,而高優(yōu)先級的線程因為申請不到鎖被阻塞。

4.4 如何無鎖?

在現(xiàn)代的 CPU 處理器上,很多操作已經(jīng)被設(shè)計為原子的,比如對齊讀(Aligned Read)和對齊寫(Aligned Write)等。Read-Modify-Write(RMW)操作的設(shè)計讓執(zhí)行更復(fù)雜的事務(wù)操作變成了原子操作,當(dāng)有多個寫入者想對相同的內(nèi)存進(jìn)行修改時,保證一次只執(zhí)行一個操作。

RMW 操作在不同的 CPU 家族中是通過不同的方式來支持的:

  • x86/64 和 Itanium 架構(gòu)通過 Compare-And-Swap (CAS) 方式來實現(xiàn)
  • PowerPC、MIPS 和 ARM 架構(gòu)通過 Load-Link/Store-Conditional (LL/SC) 方式來實現(xiàn)

在x64下進(jìn)行實踐的,用的是CAS操作,CAS操作是lock-free技術(shù)的基礎(chǔ),我們可以用下面的代碼來描述:

template <class T>
bool CAS(T* addr, T expected, T value)
{
  if (*addr == expected)
  {
     *addr = value;
     return true;
  }
  return false;
}

在GCC中,CAS操作如下所示:

bool __sync_bool_compare_and_swap (type *ptr, type oldval type newval, ...)
type __sync_val_compare_and_swap (type *ptr, type oldval type newval, ...)

這兩個函數(shù)提供原子的比較和交換,如果*ptr == oldval,就將newval寫入*ptr,第一個函數(shù)在相等并寫入的情況下返回true,第二個函數(shù)的內(nèi)置行為和第一個函數(shù)相同,只是它返回操作之前的值。

后面的可擴(kuò)展參數(shù)(...)用來指出哪些變量需要memory barrier,因為目前gcc實現(xiàn)的是full barrier,所以可以略掉這個參數(shù),除過CAS操作,GCC還提供了其他一些原子操作,可以在無鎖算法中靈活使用:

type __sync_fetch_and_add (type *ptr, type value, ...)
type __sync_fetch_and_sub (type *ptr, type value, ...)
type __sync_fetch_and_or (type *ptr, type value, ...)
type __sync_fetch_and_and (type *ptr, type value, ...)
type __sync_fetch_and_xor (type *ptr, type value, ...)
type __sync_fetch_and_nand (type *ptr, type value, ...)

type __sync_add_and_fetch (type *ptr, type value, ...)
type __sync_sub_and_fetch (type *ptr, type value, ...)
type __sync_or_and_fetch (type *ptr, type value, ...)
type __sync_and_and_fetch (type *ptr, type value, ...)
type __sync_xor_and_fetch (type *ptr, type value, ...)
type __sync_nand_and_fetch (type *ptr, type value, ...)

_sync_*系列的built-in函數(shù),用于提供加減和邏輯運算的原子操作。這兩組函數(shù)的區(qū)別在于第一組返回更新前的值,第二組返回更新后的值。

無鎖算法感觸最深的是復(fù)雜度的分解,比如多線程對于一個雙向鏈表的插入或刪除操作,如何能一步一步分解成一個一個串聯(lián)的原子操作,并能保證事務(wù)內(nèi)存的一致性。

Part5.數(shù)據(jù)庫:數(shù)據(jù)世界的堅固堡壘

簡單來說,數(shù)據(jù)庫就是按照一定結(jié)構(gòu)組織并長期存儲在計算機(jī)內(nèi)、可共享的數(shù)據(jù)集合 ,它就像是一個巨大的倉庫,有條不紊地存放著各種數(shù)據(jù)。

數(shù)據(jù)庫管理系統(tǒng)(DBMS)則是操作和管理數(shù)據(jù)庫的軟件,是數(shù)據(jù)庫的核心樞紐,像常見的 MySQL、Oracle、SQL Server 等都是廣泛使用的數(shù)據(jù)庫管理系統(tǒng),它們?yōu)橛脩艉统绦騿T提供了創(chuàng)建、檢索、更新和管理數(shù)據(jù)的便捷方式,還具備數(shù)據(jù)的安全性保障、一致性維護(hù)、并發(fā)控制以及恢復(fù)機(jī)制等重要功能。

數(shù)據(jù)庫的類型豐富多樣,其中關(guān)系型數(shù)據(jù)庫基于關(guān)系模型設(shè)計,以表格形式存儲數(shù)據(jù),通過行和列來組織數(shù)據(jù),每個表格代表一種實體類型,每一行是一個實例,每一列對應(yīng)實例的一個屬性,并且各表之間通過鍵建立關(guān)聯(lián)關(guān)系,它支持 SQL 語言進(jìn)行強大的數(shù)據(jù)查詢和操作,能保證數(shù)據(jù)的一致性和完整性,適用于對數(shù)據(jù)一致性要求高、事務(wù)處理復(fù)雜的場景,如銀行系統(tǒng)、電商訂單管理系統(tǒng)等 。常見的關(guān)系型數(shù)據(jù)庫有 MySQL、PostgreSQL、Oracle、Microsoft SQL Server 等。

隨著大數(shù)據(jù)時代的來臨,數(shù)據(jù)量呈爆炸式增長,數(shù)據(jù)類型也愈發(fā)復(fù)雜多樣,NoSQL 數(shù)據(jù)庫應(yīng)運而生。它打破了傳統(tǒng)關(guān)系型數(shù)據(jù)庫的固定表結(jié)構(gòu)束縛,更適合處理大規(guī)模分布式環(huán)境下的非結(jié)構(gòu)化數(shù)據(jù),具有靈活的數(shù)據(jù)模型、高可擴(kuò)展性和高性能等特點 。像 MongoDB 屬于文檔型 NoSQL 數(shù)據(jù)庫,它以文檔(類似 JSON 格式)的形式存儲數(shù)據(jù),每個文檔可以有不同的結(jié)構(gòu),非常適合存儲和處理內(nèi)容管理系統(tǒng)、日志記錄系統(tǒng)等場景下的數(shù)據(jù);Redis 是鍵值型 NoSQL 數(shù)據(jù)庫,它將數(shù)據(jù)存儲為鍵值對,讀寫速度極快,常被用作緩存、消息隊列等,在高并發(fā)讀寫的場景中表現(xiàn)出色。

在數(shù)據(jù)庫設(shè)計階段,需求分析至關(guān)重要,需要深入了解業(yè)務(wù)需求,明確數(shù)據(jù)的存儲、查詢和處理要求。概念設(shè)計時,通常使用實體 - 關(guān)系(ER)模型來描述數(shù)據(jù)之間的關(guān)系,將現(xiàn)實世界中的實體、屬性以及它們之間的聯(lián)系抽象為 ER 圖,這就像是搭建房子前的設(shè)計藍(lán)圖,為后續(xù)的設(shè)計奠定基礎(chǔ) 。邏輯設(shè)計則是將 ER 模型轉(zhuǎn)換為具體的數(shù)據(jù)庫模型,如關(guān)系模型,確定數(shù)據(jù)庫的表結(jié)構(gòu)、字段類型、主鍵、外鍵等。物理設(shè)計階段要考慮數(shù)據(jù)庫在物理存儲介質(zhì)上的存儲結(jié)構(gòu)和訪問方法,選擇合適的存儲設(shè)備、索引策略等,以提高數(shù)據(jù)庫的性能。

優(yōu)化數(shù)據(jù)庫性能的方法有很多,比如合理設(shè)計索引,索引能加速數(shù)據(jù)檢索,就像書的目錄,幫助快速定位到所需數(shù)據(jù),但過多的索引會增加存儲開銷和數(shù)據(jù)更新的時間,因此要根據(jù)實際查詢需求創(chuàng)建合適的索引;優(yōu)化 SQL 語句也十分關(guān)鍵,編寫高效的 SQL 查詢,避免全表掃描、減少子查詢的使用、合理使用 JOIN 操作等,可以顯著提升查詢效率;數(shù)據(jù)庫配置優(yōu)化同樣不可忽視,根據(jù)服務(wù)器硬件資源和業(yè)務(wù)負(fù)載,調(diào)整數(shù)據(jù)庫的參數(shù)配置,如緩沖池大小、連接數(shù)、日志設(shè)置等,能讓數(shù)據(jù)庫更好地適應(yīng)實際運行環(huán)境 。

數(shù)據(jù)庫在我們的生活中無處不在,電商平臺利用數(shù)據(jù)庫存儲商品信息、用戶訂單,方便用戶購物和商家管理;社交網(wǎng)絡(luò)依靠數(shù)據(jù)庫保存用戶資料、動態(tài)和好友關(guān)系,讓人們能夠便捷地交流互動;企業(yè)的財務(wù)管理系統(tǒng)通過數(shù)據(jù)庫存儲財務(wù)數(shù)據(jù),支持財務(wù)分析和決策制定。可以說,數(shù)據(jù)庫已經(jīng)成為現(xiàn)代信息系統(tǒng)的基石,支撐著各個領(lǐng)域的信息化運作,其重要性不言而喻。

責(zé)任編輯:武曉燕 來源: 深度Linxu
相關(guān)推薦

2025-05-23 08:15:00

C++constexpr字面類型

2025-08-07 09:42:15

2021-10-27 11:00:30

C++語言面試

2011-07-15 00:47:13

C++多態(tài)

2025-05-20 08:10:00

函數(shù)函數(shù)類型函數(shù)指針類型

2025-05-20 10:00:00

C++命名空間別名代碼

2019-09-29 00:25:11

CC++內(nèi)存泄漏

2025-06-09 07:55:00

C++引用語言

2011-04-11 09:47:50

C++內(nèi)存管理

2025-05-26 03:20:00

2011-03-29 14:31:41

CC++

2025-05-27 10:15:00

void*函數(shù)開發(fā)

2025-04-30 10:10:00

在 C++C++11Lambda

2024-01-23 10:13:57

C++虛函數(shù)

2010-01-28 16:16:32

C++多態(tài)性

2024-04-22 13:22:00

虛函數(shù)象編程C++

2010-02-03 10:50:33

C++多態(tài)

2011-07-10 15:26:54

C++

2025-05-06 08:20:00

互斥鎖C++編程

2011-05-30 15:29:32

C++
點贊
收藏

51CTO技術(shù)棧公眾號

欧美调教网站| 中文日产幕无线码一区二区| 国产精品一区二区久激情瑜伽| 久久香蕉频线观| 911亚洲精选| 免费亚洲电影| 中文字幕综合网| 精品国产免费人成电影在线观...| 久久久精品福利| 日韩精品一卡| 亚洲国产精品va在线看黑人动漫| 欧美日韩亚洲第一| 国产在线高清视频| 26uuu另类欧美| 成人免费在线视频网站| 成年人视频在线免费看| 亚洲精品va| 亚洲欧美激情视频| 丰满饥渴老女人hd| 91九色综合| 午夜精品视频一区| 99re99热| 国产精品一区在线看| 高清国产一区二区三区| 国产精品你懂得| 在线看成人av| 欧美午夜不卡| 最近中文字幕mv在线一区二区三区四区| 18禁一区二区三区| 日韩三区四区| 欧美影视一区在线| 成熟丰满熟妇高潮xxxxx视频| 国精产品一区| 国产精品美女久久久久久久网站| 精品在线视频一区二区三区| 国产视频在线观看视频| 强制捆绑调教一区二区| 欧美亚洲第一页| 国产香蕉在线视频| 欧美日韩午夜| 另类图片亚洲另类| 天天爽天天爽天天爽| 欧美日韩有码| 亚洲精品自拍偷拍| 亚洲制服丝袜在线播放| 综合伊人久久| 日韩欧美国产1| 捷克做爰xxxⅹ性视频| 欧美aaaaaa| 欧美三级中文字| 中文字幕第80页| 成人小电影网站| 精品国产乱码久久久久久虫虫漫画 | 亚洲欧洲日产国产网站| 你懂的在线观看网站| 91精品短视频| 欧美精品一区二区三区蜜桃| 成年人看片网站| 538任你躁精品视频网免费| 欧美一级精品在线| 老司机av网站| 这里视频有精品| 精品日韩欧美在线| 国产精品麻豆入口| 亚洲美女久久| 国产午夜精品全部视频播放| 夫妇交换中文字幕| 久久久国产精品| 超碰日本道色综合久久综合| 91插插插插插插| 激情久久久久| 91chinesevideo永久地址| 欧美a∨亚洲欧美亚洲| 国产精品久久国产愉拍| 国产99视频精品免视看7| 久久精品视频2| 久久精品国产亚洲a| 91精品视频播放| 亚洲免费视频网| 2023国产一二三区日本精品2022| 欧美欧美一区二区| 在线观看黄av| 亚洲国产cao| 国产精彩免费视频| 999精品视频在线观看| 日韩美女一区二区三区四区| 黄色片视频免费观看| 精品国产91| 久久精品视频va| 久久精品国产亚洲av高清色欲| 亚洲综合国产| 国产日本欧美视频| 欧美熟妇交换久久久久久分类| 26uuu欧美| 日日噜噜噜夜夜爽爽| av手机在线观看| 日韩欧美一区视频| 亚洲在线观看网站| 欧美调教网站| 美日韩精品免费观看视频| 国产精品乱子伦| 精品在线免费视频| 蜜桃视频在线观看91| 免费在线观看av网站| 欧美日韩国产中文字幕| 激情五月婷婷基地| 亚洲精品亚洲人成在线| 欧美不卡视频一区发布| 国产91精品看黄网站在线观看| 精品一区二区久久| 日本一区二区精品| bl视频在线免费观看| 欧美视频一区二区三区在线观看| 李丽珍裸体午夜理伦片| 色999日韩| 欧美在线视频观看| 精品久久人妻av中文字幕| 国产日产欧产精品推荐色 | 国产成人精品免费视频大全最热| 国产在线一二| 天天操天天色综合| 95视频在线观看| 一区二区三区午夜视频| 国产成人福利视频| 天天摸夜夜添狠狠添婷婷| 亚洲欧美偷拍三级| 午夜久久久精品| 免费看成人哺乳视频网站| 久久久久久久久久久成人| 97人妻人人澡人人爽人人精品| 久久久久国产免费免费| 九九爱精品视频| 天堂av一区| 欧美成人激情在线| 国产精品伦一区二区三区| 国产午夜亚洲精品理论片色戒| 免费在线观看亚洲视频| 国产精品45p| 欧美国产日韩免费| 99精品国产99久久久久久97| 国产精品欧美久久久久一区二区| 成人久久久久久久久| 日韩精品免费一区二区夜夜嗨| 欧美激情a∨在线视频播放| 国产欧美一区二区三区视频在线观看| 国产欧美日韩精品在线| 欧美精品无码一区二区三区| 色综合久久中文| 91av在线国产| 男生女生差差差的视频在线观看| 色综合夜色一区| 成年人网站免费在线观看| 亚洲欧美日韩国产一区| 欧美欧美一区二区| 日韩在线观看不卡| 色哟哟入口国产精品| 97人人爽人人爽人人爽| 亚洲女爱视频在线| 午夜性福利视频| 亚洲网站视频| 精品国产二区在线| 不卡av影片| 国产一区二区三区在线视频| 无码人妻久久一区二区三区| 亚洲国产精品激情在线观看| 亚洲综合欧美激情| 91成人观看| 国产成人精品日本亚洲11| 欧美aa在线| 亚洲图片欧美午夜| 97人妻精品一区二区三区软件 | 国产亚洲欧美日韩精品| 中文无码av一区二区三区| 国产精品电影院| 一级黄色免费毛片| 日韩午夜免费| 欧美日韩一区二区视频在线 | 污影院在线观看| 精品福利二区三区| 成人av网站在线播放| 中文字幕亚洲不卡| 91精品又粗又猛又爽| 奶水喷射视频一区| ijzzijzzij亚洲大全| 99re8这里有精品热视频免费| 欧美一级高清免费播放| 岛国在线视频免费看| 欧美一区二区黄| 欧美国产成人精品一区二区三区| 国产欧美精品在线观看| 在线观看视频在线观看| 美女诱惑黄网站一区| 欧美日韩在线免费观看视频| 欧美天堂社区| 亚洲一区二区在线| 韩国久久久久久| 久久的精品视频| 日韩资源在线| 欧美一区在线视频| 国产精品第5页| 亚洲美女视频一区| 黄色片网站免费| 国产成人精品三级| 日韩av片网站| 99热精品在线| 懂色av一区二区三区四区五区| 久久精品凹凸全集| 亚洲精品女av网站| 色婷婷综合久久久中字幕精品久久 | 亚洲精品色图| 正在播放91九色| 国产99久久| 国产欧美一区二区三区另类精品 | 亚洲日产国产精品| 蜜桃视频成人在线观看| 欧美热在线视频精品999| 国产99在线免费| 激情视频亚洲| 国产精品一区二区三区久久久 | 福利成人在线观看| 亚洲国产高清福利视频| 国产欧美日韩成人| 精品视频免费看| 成人免费毛片男人用品| 亚洲成人激情av| 69av视频在线| 亚洲欧洲精品天堂一级| 亚洲天堂最新地址| 久久亚洲影视婷婷| 中文在线永久免费观看| 国产大片一区二区| 午夜激情视频网| 蜜桃免费网站一区二区三区| 国产淫片av片久久久久久| 在线国产精品一区| 国产精品久久国产| 欧美日韩网址| 日韩成人三级视频| 狠狠爱综合网| 蜜臀精品一区二区| 欧美色一级片| 亚洲色婷婷久久精品av蜜桃| 99精品美女| 亚洲最新在线| 99久久婷婷| 国产卡一卡二在线| 欧美在线黄色| 粉嫩av一区二区三区天美传媒 | 538国产精品一区二区免费视频| 欧美理论电影| 欧美精品九九久久| 51漫画成人app入口| 久久免费精品日本久久中文字幕| 四虎亚洲成人| 久久久免费精品视频| 欧美三级网站| 日本中文字幕成人| 欧美暴力调教| 国产日韩欧美视频| 精品中文字幕一区二区三区| 亚洲淫片在线视频| 中文字幕一区二区三区中文字幕| 国产精品久久久久久久免费大片| 国产精品对白| 蜜桃成人免费视频| 欧美熟乱15p| 免费在线观看污污视频| 自产国语精品视频| av无码久久久久久不卡网站| 日韩一区二区免费看| 日韩精品一区二区三区不卡| 老司机免费视频一区二区| 99999精品| 成人免费视频免费观看| 国产美女喷水视频| 国产精品家庭影院| 免费一级黄色大片| 色综合一个色综合| 一炮成瘾1v1高h| 亚洲第一视频网| 成人精品一区| 欧美多人爱爱视频网站| 亚洲天堂手机| 成人亚洲激情网| 欧美精品国产白浆久久久久| 日韩av在线电影观看| 国产精品久久久久无码av| 无码av天堂一区二区三区| 午夜在线播放视频欧美| 在线观看国产福利| yourporn久久国产精品| 国产探花视频在线播放| 一区二区欧美在线观看| 无码人妻丰满熟妇精品区| 91精品国产免费| 青青久在线视频免费观看| 久久久av一区| 成人欧美一区二区三区的电影| 91精品视频在线免费观看| 日韩欧美国产大片| 99热都是精品| 日韩高清一区在线| 天天躁日日躁狠狠躁av麻豆男男| 欧美韩国日本不卡| 亚欧视频在线观看| 91精品在线麻豆| 国产中文在线| 韩剧1988免费观看全集| 在线观看欧美| 日韩中文字幕av在线| 在线观看视频免费一区二区三区| 亚洲 国产 图片| 国产日韩av一区| 国产又黄又粗又爽| 精品国产乱码91久久久久久网站| 午夜小视频在线| 青青草成人在线| 国产精品15p| 国产精品videossex国产高清 | 国产精品无码毛片| 亚洲女同女同女同女同女同69| 久草视频在线免费| 精品亚洲一区二区三区在线观看| 色婷婷在线播放| 91影视免费在线观看| 爽成人777777婷婷| 亚洲免费av一区二区三区| 91小视频在线免费看| 国产一级特黄视频| 91精品国模一区二区三区| 成年网站在线| 国产精品久久久久久超碰| 九九亚洲精品| aa免费在线观看| 91麻豆精东视频| 久久久久99精品成人片三人毛片| 日韩精品一区二| 手机在线免费av| 亚洲xxxxx电影| 99久久婷婷这里只有精品 | 成人av在线电影| 久久精品第一页| 欧美变态tickle挠乳网站| 伊人影院蕉久影院在线播放| 91久久夜色精品国产网站| 四季av在线一区二区三区| 五月激情婷婷在线| 国产精品区一区二区三| 一级久久久久久久| 久久精品小视频| 日韩中文字幕无砖| 999在线观看视频| 久久综合丝袜日本网| 日韩在线播放中文字幕| 亚洲人成电影在线播放| 四虎成人在线| 亚洲自拍偷拍二区| 国产福利一区在线| 日韩三级一区二区三区| 亚洲精品美女在线观看| 亚洲欧美se| 亚洲成人午夜在线| 国内精品视频666| 看片网站在线观看| 日韩av最新在线| av激情成人网| 中国成人亚色综合网站| 国产成人在线视频网址| 日本在线免费观看| 亚洲人在线视频| 涩涩涩久久久成人精品| www.激情网| 91丨九色丨国产丨porny| 区一区二在线观看| 播播国产欧美激情| 99久热这里只有精品视频免费观看| 人妻av中文系列| 国产无人区一区二区三区| 国产又粗又长又大视频| 欧美极品欧美精品欧美视频| 欧美成人一区在线观看| 精品999在线| 一区二区三区中文免费| 色中色在线视频| 91免费看片网站| 一本久道久久久| 很污很黄的网站| 337p日本欧洲亚洲大胆精品| 日本另类视频| 六月婷婷在线视频| 国产精品午夜春色av| 黄片毛片在线看| 国产精品精品一区二区三区午夜版 | 成人在线免费在线观看| 国产精品午夜免费| 香蕉视频国产在线| 91免费看网站| 免费欧美日韩国产三级电影| 精品无码一区二区三区电影桃花| 亚洲欧美国产高清va在线播|