關鍵字的作用
靜态變量的初始化
int main() {
int initNum = 3;
for (int i=1; i<=5; i++) {
static int n1 = initNum;
n1++;
printf("%d\n", n1);
}
return 0;
}
/*
4
5
6
7
8
*/
int main() {
int initNum = 3;
for (int i=1; i<=5; i++) {
static int n1 = initNum;
{
int *p = &n1;
p++;
*p = 0;
}
n1++;
printf("%d\n", n1);
}
return 0;
}
/*
4
4
4
4
4
*/
複制
函數指針
在編譯過程中,每一個函數都有一個入口位址,而函數指針就是指向該入口位址的指針。
#include<iostream>
using namespace std;
void fun1(int x) {
cout << x << endl;
}
void fun2(int x) {
cout << x+x <<endl;
}
int main() {
void (*pf)(int);
pf = fun1;
pf(222);
pf = fun2;
pf(222);
}
複制
多态性和虛函數(virtual)
- 靜态多态主要表現為重載,在編譯時就确定了。
- 動态多态的基礎是虛函數機制,虛函數的作用是實作動态多态,在運作期間動态綁定,決定了派生類調用哪個函數。
#include<iostream>
using namespace std;
class Shape {
public:
void show() { // 未定義為虛函數
cout << "Shape::show()" << endl;
}
void virtual show() { // 定義為虛函數
cout << "Shape::show()" << endl;
}
};
class Line : public Shape {
public:
void show() {
cout << "Line::show()" << endl;
}
};
class Point : public Shape {
public:
void show() {
cout << "Point::show()" << endl;
}
};
int main() {
Shape *pt;
pt = new Line();
pt->show();
pt = new Point();
pt->show();
return 0;
}
/*
未定義為虛函數時輸出結果
Shape::show()
Shape::show()
定義為虛函數時輸出結果
Line::show()
Point::show()
*/
複制
純虛函數
有些情況下,基類生成的對象是不合理的,比如動物可以派生出獅子、孔雀等,這些派生類顯然存在着較大的差異。那麼可以讓基類定義一個函數,并不給出具體的操作内容,讓派生類在繼承的時候在給出具體的操作,這樣的函數被稱為純虛函數。含有純虛函數的類成為抽象類,抽象類不能聲明對象,隻能用于其他類的繼承。
純虛函數的定義方法為:
void ReturnType Function() = 0;
複制
子類可以不重寫虛函數,但一定要重寫純虛函數。
靜态函數和虛函數
靜态函數在編譯時就确定了調用它的時機,而虛函數在運作時動态綁定,虛函數由于用到了虛函數表和虛函數虛函數指針,會增加記憶體使用。
構造函數和析構函數
- 構造函數在每次建立對象的時候調用,函數名稱和類名相同,無傳回類型,構造函數可以為類初始化某些成員。
- 析構函數在每次删除對象的時候調用,函數名稱和類名相同,但在前面加了一個
\sim
符号,同樣無傳回類型。若對象在調用過程中用
new
動态配置設定了記憶體,可以在析構函數中寫
delete
語句統一釋放記憶體。
- 如果使用者沒有寫析構函數,編譯系統會自動生成預設析構函數。
- 假設存在繼承:孫類繼承父類,父類繼承爺類
- 孫類構造過程:爺類 -> 父類 -> 孫類
- 孫類析構過程:孫類 -> 父類 -> 爺類
析構函數和虛函數
- 可能作為繼承父類的析構函數需要設定成虛函數,這樣可以保證當一個基類指針指向其子類對象并釋放基類指針的時候,可以及時釋放掉子類的空間。
- 虛函數需要額外的虛函數表和虛函數指針,占用額外的記憶體,是以不會作為繼承父類的析構函數不用設定成虛函數,否則會浪費記憶體。
C
++預設的析構函數不是虛函數,隻要當其作為父類的時候,才會設定為虛函數。
重載、重寫(覆寫)、隐藏
可以通過 attribute 關鍵字,聲明 constructor 和 destructor 來實作。
#include<iostream>
using namespace std;
__attribute((constructor)) void before_main() {
cout << __FUNCTION__ << endl;
}
__attribute((destructor)) void after_main() {
cout << __FUNCTION__ << endl;
}
int main() {
cout << __FUNCTION__ << endl;
return 0;
}
複制
虛函數表
在有虛函數的類中,存在一個虛函數指針,該指針指向一張虛函數表,當子類繼承基類的時候,也會繼承其虛函數表。當子類重寫基類中的虛函數時,會将虛函數表中的位址替換成重寫的函數位址。
char* 和 char[] 的差別
char *s1 = "abc";
char s2[] = "abc"
複制
C
int x = 10;
const int *a = &x;
int* const b = &x;
複制
為什麼函數參數入棧順序從右到左
為了支援 不定長參數函數
int add(int num, ...) {
va_list valist;
int sum = 0;
int i;
va_start(valist, num);
for (i = 0; i < num; i++) {
sum += va_arg(valist, int);
}
va_end(valist);
return sum;
}
複制
C98
C98
enum color{red, green, blue, yellow};
enum color2{red, green}; // ERROR,因為 red 和 green 已經在 color 中定義過了
auto x = red; //OK,因為 red 沒有限定作用域
auto y = color::red; //OK
複制
C11
enum struct color{red, green, blue, yellow};
enum struct color2{red, green}; // OK,red 和 green 在不同作用域内
auto x = red; // ERROR,red 沒有指定作用域
auto y = color::red;
複制
強類型轉換的優點在于
- 限定作用域的枚舉類型将名字空間污染降低
- 限定作用域的枚舉類型是強類型的,無法通過隐式轉換到其他類型,而不限定的枚舉類型可以自動轉換為整形
宏定義和枚舉的差別
- 枚舉是一種實體,占記憶體。宏定義是一種表達式,不占記憶體。
- 枚舉在編譯階段進行處理,宏定義在與編譯階段就完成了文本替換。
空類
隐式類型轉換
- 表達式中,低精度類型向高精度類型發生轉換。
- 條件語句中,非布爾類型向布爾類型發生轉換。
- 初始化語句中,初始值向變量類型發生轉換。
- 指派語句中,右側運算對象向左側運算對象發生轉換。
- 可以用 單個形參 來調用的構造函數定義了從 形參類型 到 該類類型 的一個隐式轉換。注意 單個形參 并不是隻有一個形參,可以有多個形參,但其他形參要有預設實參。
#include<iostream>
using namespace std;
class Node {
public :
string s1;
int a;
Node(string s, int val=0) : s1(s), a(val) {
}
bool ok(Node other) const {
return s1 == other.s1;
}
};
int main() {
Node x("xxxxx");
cout << x.ok(string("xxxxx")) << endl; // 隐式
cout << x.ok(Node("xxxxx")) << endl; // 顯式
return 0;
}
複制
extern "C"
函數調用過程
每一個函數調用都配置設定一個函數棧,先将傳回位址入棧,在将目前函數的棧指針入棧,然後在棧内執行函數。
C
- 函數傳回值時,生成一個臨時變量,傳回該臨時變量,然後在調用處把該臨時變量指派給左側變量。
- 函數傳回引用時,傳回和接收應是 int& 類型,不能傳回局部變量的引用。不生成臨時變量,直接傳回該引用。
#include<iostream>
using namespace std;
int x, y;
int get1() {
cout << "get1 中 x 的位址" << &x << endl;
return x;
}
int& get2() {
cout << "get2 中 y 的位址" << &y << endl;
return y;
}
int main() {
int x = get1();
cout << "main 中 x 的位址" << &x << endl;
int& y = get2();
cout << "main 中 y 的位址" << &y << endl;
return 0;
}
/*
get1 中 x 的位址0x4c600c
main 中 x 的位址0x6efef8
get2 中 y 的位址0x4c6010
main 中 y 的位址0x4c6010
*/
複制
不能,會造成無限循環。
#include<iostream>
using namespace std;
class Node {
public:
int x;
Node(int a) : x(a) {
}
Node(Node& a){ // 方法1,正确
x = a.x;
}
Node(Node a) { // 方法2,錯誤,無法編譯通過
x = a.x;
}
};
int main() {
Node x(10);
Node y(x);
return 0;
}
複制
動态記憶體配置設定
int *p = (int*)malloc(sizeof(int)*100);
free(p);
int *a = new int;
delete a;
int *q = new int[100];
delete[] q;
複制
A* a = new A; a->i = 10; 在記憶體配置設定上發生了什麼?
是标準函數庫 | 是 ++ 運算符 |
從堆配置設定記憶體 | 從自由存儲區配置設定記憶體 |
需要顯式指出配置設定記憶體大小 | 編譯器自行計算 |
不會調用構造/析構函數 | 會調用構造/析構函數 |
傳回無類型指針 () | 傳回有類型指針 |
不可調用 | 可以基于 |
不可被重載 | 可以被重載 |
構造函數分為初始化和計算兩個階段,第一階段對應初始化清單,第二階段對應函數主體,引用必須在第一階段完成。
#include <iostream>
using namespace std;
class Node {
public:
int& a;
int b, c, d;
Node(int &x, int y, int z, int k) : a(x), b(y), c(z), d(k) {
}
};
int main() {
int t = 1;
Node x(t, 2, 3, 4), y(t, 2, 3, 4);
x.a++;
cout << y.a << endl;
return 0;
}
複制
const int x = 10;
const int &y = x;
const int &z = 20;
複制
#include<bits/stdc++.h>
using namespace std;
void func(int& x) {
cout << "左值引用" << endl;
}
void func(int&& x) {
cout << "右值引用" << endl;
}
void func(const int& x) {
cout << "const 左值引用" << endl;
}
void func(const int&& x) {
cout << "const 右值引用" << endl;
}
template<typename T> void fun(T&& x) {
func(forward<T>(x));
}
int main() {
fun(10);
int x = 0;
fun(x);
fun(move(x));
const int y = 0;
fun(y);
fun(move(y));
return 0;
}
/*
右值引用
左值引用
右值引用
const 左值引用
const 右值引用
*/
複制
通過紅黑樹實作 | 通過 表實作 |
操作複雜度 級别 | 操作複雜度常數級别 |
内部有序 | 内部無序 |
适用于對順序有要求的場景 | 适用于頻繁查找的場景 |
vector | list | |
---|---|---|
類型 | 動态數組 | 動态連結清單 |
底層實作 | 數組實作 | 雙向連結清單實作 |
通路 | 支援随機通路, | 不支援随機通路, |
插入 | 在末尾 ,在中間 | 很快, |
删除 | 在末尾 ,在中間 | 很快, |
記憶體來源 | 從堆區配置設定空間 | 從堆區配置設定空間 |
記憶體使用 | 是順序記憶體 | 不是順序記憶體 |
記憶體配置設定 | 一次性配置設定好,不夠時擴容 | 每次插入節點都需要進行記憶體申請 |
性能 | 通路性能好,插入删除性能差 | 插入删除性能好,通路性能差 |
适用場景 | 經常随機通路,不在乎插入和删除效率 | 經常插入删除,不在乎通路效率 |
#include <bits/stdc++.h>
using namespace std;
int main() {
vector<int> g;
for(int i=1; i<=10; i++)
g.push_back(i);
cout << "capacity = " << g.capacity() << endl;
vector<int>().swap(g);
cout << "capacity = " << g.capacity() << endl;
return 0;
}
複制
#include <bits/stdc++.h>
using namespace std;
int main() {
{
set<int> st = {1, 2, 3, 4, 5, 6};
for(set<int>::iterator iter=st.begin(); iter!=st.end(); ) {
if(*iter == 3) {
st.erase(iter++); // 傳給 erase 的是 iter 的一個副本
} else {
iter++;
}
}
}
{
vector<int> g = {1, 2, 3, 4, 5, 6};
for(vector<int>::iterator iter=g.begin(); iter!=g.end(); ) {
if(*iter == 3) {
iter = g.erase(iter);
} else {
iter++;
}
}
}
return 0;
}
複制
#include <bits/stdc++.h>
using namespace std;
void print() {
cout << endl;
}
template<class T, class... Args>
void print(T num, Args... rest) {
cout << num << " ";
print(rest...);
}
int main() {
print(1, 2, 3, 4, 5);
return 0;
}
複制
// head.h 内容
struct Node {
int a, b;
};
// main.h 内容
Node node
/// main.cpp 錯誤寫法
#include "main.h"
#include "head.h"
int main() {
return 0;
}
// main.cpp 正确寫法
#include "head.h"
#include "main.h"
int main() {
return 0;
}
複制
使用尖括号和雙引号的差別在于:編譯器預處理階段尋找頭檔案的路徑順序不一樣。
- 使用雙引号的查找順序
- 目前頭檔案目錄
- 編譯器設定的頭檔案路徑
- 系統變量指定的頭檔案路徑
- 使用尖括号的查找順序
- 編譯器設定的頭檔案路徑
- 系統變量指定的頭檔案路徑
記憶體洩漏
記憶體溢出
程式申請記憶體的時候,超出了系統實際配置設定給你的空間,此時系統無法完成滿足你的需求,就會發生記憶體溢出。
記憶體溢出的情況:
- 記憶體中加載的資料量過于龐大。
- 代碼中存在死循環或者遞歸過深導緻棧溢出。
- 記憶體洩漏導緻記憶體溢出。
段錯誤
#include<bits/stdc++.h>
using namespace std;
class A;
class B;
class A {
public:
A() {
cout << "A Created" << endl;
}
~A() {
cout << "A Destroyed" << endl;
}
shared_ptr<B> ptr;
};
class B {
public:
B() {
cout << "B Created" << endl;
}
~B() {
cout << "B Destroyed" << endl;
}
shared_ptr<A> ptr;
};
int main() {
shared_ptr<A> pt1(new A());
shared_ptr<B> pt2(new B());
pt1->ptr = pt2;
pt2->ptr = pt1;
cout << "use of pt1: " << pt1.use_count() << endl;
cout << "use of pt2: " << pt2.use_count() << endl;
return 0;
}
/*
A Created
B Created
use of pt1: 2
use of pt2: 2
*/
複制
#include<bits/stdc++.h>
using namespace std;
class A;
class B;
class A {
public:
A() {
cout << "A Created" << endl;
}
~A() {
cout << "A Destroyed" << endl;
}
weak_ptr<B> ptr;
};
class B {
public:
B() {
cout << "B Created" << endl;
}
~B() {
cout << "B Destroyed" << endl;
}
weak_ptr<A> ptr;
};
int main() {
shared_ptr<A> pt1(new A());
shared_ptr<B> pt2(new B());
pt1->ptr = pt2;
pt2->ptr = pt1;
cout << "use of pt1: " << pt1.use_count() << endl;
cout << "use of pt2: " << pt2.use_count() << endl;
return 0;
}
/*
A Created
B Created
use of pt1: 1
use of pt2: 1
B Destroyed
A Destroye
*/
複制
因為存在這種情況:申請的空間在函數結束後忘記釋放,造成記憶體洩漏。使用智能指針可以很大程度的避免這個問題,因為智能指針是一個類,超出類的作用範圍後,類會調用析構函數釋放資源,是以智能指針的作用原理就是在函數結束後自動釋放記憶體空間。