1. 程式人生 > 其它 >【春招預熱】C++回爐重造

【春招預熱】C++回爐重造

本文主要參照[https://m.nowcoder.com/tutorial/93/a34ed23d58b84da3a707c70371f59c21]進行梳理,進行一定補充,更正了一些錯誤,刪除部分失效內容。

C++基礎知識

C and C++

匯入C函式的關鍵字 - extern "C"

指示編譯器這一段程式碼將按照c來編譯

編譯區別:C++編譯時會包含引數型別,而C不支援過載,因此編譯後代碼不會包含函式型別,而僅有函式名

extern "C" int strcmp(const char* c1, const char* c2);

static變數初始化

C:初始化發生在編譯階段

C++:首次使用時在進行構造

static

儲存在靜態儲存區。多次訪問函式會得到之前的值

全域性靜態變數作用在全域性域和檔案域,程式結束後會後記憶體。

全域性變數作用在全域性域,分配在靜態資料區。

區域性變臉作用在區域性作用域,分配在棧上,出了生存週期就會回收記憶體。

行內函數和巨集函式inline define

巨集函式在預編譯階段做程式碼替換,不檢查引數型別,本質上並不是函式

行內函數在編譯階段做程式碼插入,檢查返回型別和返回值,本質上是函式

inline 普通函式在呼叫時需要定址,inline可以減少這個開銷。inline不允許呼叫自己,不允許迴圈和switch,否則編譯時當作一般函式

const define

const是常量,單獨存放在常量記憶體區,define不需要存放的空間

define在預編譯階段替換,const在編譯階段生效

const有型別,define沒有

const int a;// 常量,a不變
const int* a;// a指向的地址的值不變。*a
int const* a;//同上
int *const a;//a指向的地址不變,a不變
const int *const a;//*a不變,a也不變

i++和++i

先賦值,後增加

++i效率更高,i++不能做左值

new和malloc

new是運算子,可以過載,會呼叫建構函式,分配失敗丟擲異常

malloc是c庫的函式,如果分配失敗返回null,返回的是指標,需要強制型別轉換,需要指定空間大小

???https://m.nowcoder.com/tutorial/93/a34ed23d58b84da3a707c70371f59c21)malloc採用記憶體池的管理方式,減少記憶體碎片。

野指標

指標指向的位置是不可知的。釋放記憶體後不及時置空,依然指向該記憶體。可能出現非法訪問。

char *p = (char*)malloc(sizeof(char)*100);
strcpy(p, "1234");
free(p);//記憶體被釋放,但指標依然指向原本的地址
//assert(p != NULL);
if(p!=NULL){//判斷失效,沒有預防
    strcpy(p,"1234");
}

ptr為nullptr時,可以呼叫成員函式嗎?可以。因為編譯時物件綁定了函式地址,但是涉及到this時會執行錯誤。

fish* pFish = nullptr;
pFish->print();//ok
pFish->add1();//this = nullptr,執行出錯

函式指標

是指向函式的指標變數,函式指標的值即為函式入口地址

可以用於回撥,如sort()函式,允許傳入自定義的比較函式,這裡使用的就是函式指標。回撥:我們可以呼叫別人的API,而別人的庫中呼叫我寫的函式即為回撥。

引用傳遞和值傳遞

值傳遞,形參是拷貝,對形參的改變不影響原來的變數實參

引用傳遞,傳遞的是原變數的別名,對形參的更改就是對實參的更改。形參作為區域性變數在棧上有空間,但是存的是實參的地址。對形參的操作會通過間接定址訪問主調函式中的實參變數。

C++記憶體

堆疊

  • 堆是陣列結構,棧是棧結構
  • 棧由系統分配,儲存區域性變數,引數,堆一般由程式設計師來分配釋放
  • 棧一級快取,堆二級快取,棧比較快

簡述C++記憶體管理

記憶體分配方式

C++記憶體分為五個區,堆,棧,區域性/靜態區,自由儲存區,常量區

自由儲存區:malloc分配,free釋放

記憶體 -> 分配 -> 初始化 (避免越界)------->釋放

對應:未分配使用,分配未初始化,越界,忘記釋放,釋放了繼續用

NULL,下標不越界,申請釋放配對,防止記憶體洩漏,free後NUL防止野指標,使用只能指標

記憶體洩漏

  • malloc不free,new不delete
  • 子類繼承父類,父類析構是虛擬函式(???
  • Windows控制代碼沒有釋放(???

程式section/記憶體模型

從高地址到低地址:kernel,環境變數,命令列引數,棧,共享空間,堆,.bss,.data,.text。受保護的地址

.bss 執行前清零,儲存未初始化和初始化為0的一塊記憶體區域

.data 初始化的全域性變數,程式會進行初始化

.text 程式碼段,只讀

位元組對齊

struct,union,class需要對齊,size是是最寬變數的整數倍不足要補齊,struct中struct,子struct要從最寬的開始放

保證存取效率,如果不齊的話一個變數要讀多次,組合成需要的資料

程式啟動過程

https://m.nowcoder.com/tutorial/93/8f38bec08f974de192275e5366d8ae24

程序分配,虛擬記憶體對映

匯入符號表,動態連結庫

初始化全域性變數

進入程式入口函式

面向物件

多型

靜態(編譯時)多型

在編譯階段即可確定下來,主要通過過載:函式過載、運算子過載

動態(執行時)多型

程式執行時才可確定

繼承、虛擬函式。公有繼承,將後代類物件賦值給祖先類

動態聯編:目標物件的型別在執行時確定,只有採用指向基類物件的指標或者引用來第呼叫虛擬函式時,才會按動態聯編的方式呼叫

重寫 過載

子類可以重新定義父類中已經存在的函式,返回型別,引數列表,函式名均一致,父類中被重寫的函式由virtual修飾。

不同的引數的同名函式

實現

  • 過載:命名傾軋計數,在編譯階段完成,加上引數型別的首字母用於區分
  • 重寫:根據物件的實際型別來呼叫不同類的函式,使用虛擬函式表

虛擬函式

虛擬函式:virtual說明,在派生類中重新定義

virtual <int><testf>(){}

純虛擬函式:基類中只宣告,沒有具體實現,派生類中必須重定義該函式

virtual <int><testf>() = 0;

虛擬函式表 類物件A的指標包含一個指向該類虛擬函式表的指標,而虛擬函式表指向該類的虛擬函式,因此每個類的物件都會呼叫自己類的虛擬函式

靜態成員函式、行內函數、友元函式和建構函式不能被說明為虛擬函式,解構函式可以

為什麼

  1. 不行。虛擬函式需要虛表,而虛表儲存在物件的空間中,呼叫建構函式前,物件沒有例項化,還沒有虛表
  2. 沒有意義。建構函式在物件建立時被呼叫,並不會存在一個父類指標呼叫子類建構函式的情況
  3. 可以保證釋放基類指標時釋放子類空間,不會記憶體洩漏

深拷貝和淺拷貝

淺拷貝,只賦值值,兩個物件可能指向同一塊記憶體,只是不同的別名

深拷貝,申請相同的空間,再賦值,這樣就是兩塊不同的地址,之間的值相同。

移動建構函式

轉移所有權,原物件將丟失其內容。當使用一個無名物件來對一個新物件構造初始化時,移動拷貝構造被呼叫。

struct testmove{
    int* p;
    testmove(int x){
        p = new int;
        *p = x;
    }
    testmove(const testmove& copy){
        p = new int;
        *p = *copy.p;
    }
    testmove(testmove&& right):p(right.p){
        cout<<"move construct"<<endl;
        right.p = nullptr;
    }
    testmove add(testmove x){
        *x.p = *x.p+1;
        return x;
    }
};
int main() {
    testmove t5(0);
    cout<<(*t5.p)<<endl;
    testmove t6(t5.add(t5));//add返回右值,移動構造
    cout<<(*t6.p)<<endl;
    testmove t7(move(t5));//move變為右值,移動構造
    cout<<(*t7.p)<<endl;
}
/*
output:
0
move construct
1
move construct
0
*/

含有引用成員

需要提供引用成員的建構函式,且需要用初始化列表來初始化

struct testref{
    int &r;
    testref(int &a):r(a){
    }
};
int main() {
    int b = 5;
    int &a = b;
    testref r(a);
    cout<<r.r<<endl;
}

常函式

在函式名後面加const,表示它不會對(非靜態的)資料成員作修改

struct testconst {
    int a;
    static int x;
    void add(int num) const {
        //a += num;//error: 表示式必須是可修改的左值
        x += num;
    }
    void sub(int num){
        x -= num;
    }
};
int testconst::x = 0;
int main() {
    testconst t;
    t.add(3);
    cout<<testconst::x<<endl;
    const testconst t2{2};
    t2.add(3);//常變數可以呼叫常函式
    //t2.sub(3);//error 物件含有與成員 函式 "testconst::sub" 不相容的型別限定符
    cout<<testconst::x<<endl;
}

虛繼承

  • 多重繼承時,變數會拷貝。\(A \leftarrow B, A \leftarrow C, B C \leftarrow D\)
  • 解決二義性。

實現 virtual base pointer 虛基類指標, 4位元組,指向虛基類表,記錄子類和虛基類的偏移,這樣就找到了虛基類成員。

#include <iostream>
using namespace std;

class A  //大小為4
{
   public:
    int a;
};
class B : virtual public A  //大小為16,變數a,b共8位元組,虛基類表指標8
{
   public:
    int b;
};
class C : virtual public A  //與B一樣16
{
   public:
    int c;
    virtual void add() { c = 10; }
};
class D
    : public B,
      public C  // 24,變數a,b,c,d共16,B的虛基類指標8,C的虛基類指標8,通過控制變數,猜測還加上了虛擬函式表??
{
   public:
    int d;
    virtual void add() { c = 10; }
};

int main() {
    A a;
    B b;
    C c;
    D d;
    cout << sizeof(a) << endl;
    cout << sizeof(b) << endl;
    cout << sizeof(c) << endl;
    cout << sizeof(d) << endl;

    return 0;
}

類模板 模板類

類模板是一個模板,不是一個實在的類,定義中用到通用型別引數。

模板類是實在的類定義,是類模板的例項化。模板類中的引數被實際型別替代。

STL

說說STL

廣義上說,STL包含:演算法,容器,迭代器。演算法和容器可以通過迭代器無縫連線

詳細來說:包含容器,介面卡,迭代器,仿函式,演算法,空間介面卡

迭代器返回的是引用

push_back()呼叫建構函式和拷貝建構函式,push_emplace()只調用建構函式

新特性

說一說C++11的新特性

**auto關鍵字 ** 編譯器自動推斷型別

decltype 求表示式型別

auto要求變數必須初始化,而decltype根據表示式推匯出變數型別,與=右邊的值無關

int a = 0;
decltype(a) b = 3.3;
decltype(a) c;
cout<<b<<endl;// 輸出3

**新增三種智慧指標 ** shared_ptr 使用引用計數,引用計數為0時才釋放記憶體

move和右值引用

c++98中就有引用,但是一般只允許引用左值。或者用常量左值來引用右值。這樣的話右值不能修改,沒有意義。c++11提出右值引用,&& 與左值相同,右值引用也必須立即初始化。

int && a = 10;
a = 100;
cout<<a<<endl;// 輸出100

move將某個左值轉化為右值

空指標 nullptr 是右值常量,專門用於初始化空型別指標。nullptr_t是c++11新增的型別,而nullptr是該型別的一個例項物件。nullptr可以隱式轉換為任意型別的指標。

在c++中,NULL即為0 #define NULL 0 ,因為c++不能將 void* 隱式轉換為其他型別指標,過載整形時會出現二義性,NULL其實是 int ,而不是空指標。

lambda表示式

正則表示式

雜湊表無序容器

統一的初始化方法

初始化列表,(大括號)c++11允許變數名後直接跟上初始化列表,來進行對物件的初始化。

int a{3}; pair<int,int> p{2,3};

成員變數預設初始化

構建類不需要用建構函式。

class A{
  	int a = 3;  
};

基於範圍的for迴圈

vector<int> vec;
for(int x:vec){
    
}

智慧指標

簡單、安全地管理動態記憶體。智慧指標是具有指標行為的物件。定義在memory標頭檔案中

c++11 擯棄了auto_ptr

會出現引用的一個物件被刪除多次

shared_ptr

允許多指標指向同一物件,共享所有權,當最後一個智慧指標銷燬時,物件銷燬

unique_ptr

獨佔指向的物件,互斥所有權,只有一個指標可以指向物件。使用一般的拷貝語義不可以用賦值,但是使用move()能夠將一個unique_ptr賦給另一個(所有權轉移)

  • move()會返回一個物件,使用了移動構造和右值引用

  • 可以delete[] new [] ,也就是可用於陣列,而auto_ptr是不可以的

weak_ptr

弱引用,指向shared_ptr管理的物件。weak_ptr只提供一種訪問手段,它的構造和析構不會引起引用計數的變化,和shared_ptr之間可以相互轉化,使用lock函式可以獲得shared_ptr。

weak_ptr用來解決shared_ptr互相引用產生的死鎖問題。

#include <iostream>
#include <memory>
using namespace std;

class B;
class A{
public:
    shared_ptr<B> _pb;
    ~A(){
        cout<<"A 2"<<endl;
    }
};
class B{
public:
    shared_ptr<A> _pa;
    ~B(){
        cout<<"B 2"<<endl;
    }
};

int main() {
    shared_ptr<A> pa(new A());
    shared_ptr<B> pb(new B());
    pb->_pa = pa;
    pa->_pb = pb;
    cout<<pb->_pa.use_count()<<endl;
    cout<<pa->_pb.use_count()<<endl;

    return 0;
}

由於pa,pb相互引用,要跳出函式時,兩者的引用計數還是1,導致解構函式沒有呼叫,資源沒有釋放。改用weak_ptr即可

class A{
public:
    weak_ptr<B> _pb;
    ~A(){
        cout<<"A 2"<<endl;
    }
};

不可以通過弱指標直接訪問物件的方法,需要使用.lock()轉化為shared_ptr