【春招預熱】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的指標包含一個指向該類虛擬函式表的指標,而虛擬函式表指向該類的虛擬函式,因此每個類的物件都會呼叫自己類的虛擬函式
靜態成員函式、行內函數、友元函式和建構函式不能被說明為虛擬函式,解構函式可以
為什麼
- 不行。虛擬函式需要虛表,而虛表儲存在物件的空間中,呼叫建構函式前,物件沒有例項化,還沒有虛表
- 沒有意義。建構函式在物件建立時被呼叫,並不會存在一個父類指標呼叫子類建構函式的情況
- 可以保證釋放基類指標時釋放子類空間,不會記憶體洩漏
深拷貝和淺拷貝
淺拷貝,只賦值值,兩個物件可能指向同一塊記憶體,只是不同的別名
深拷貝,申請相同的空間,再賦值,這樣就是兩塊不同的地址,之間的值相同。
移動建構函式
轉移所有權,原物件將丟失其內容。當使用一個無名物件來對一個新物件構造初始化時,移動拷貝構造被呼叫。
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