C++入門基礎
1. C++的第一個程序
C++繼承C語言許多大多數的語法,所以以C語言實現的hello world也可以運行,C++中需要把文件定義為.cpp,vs編譯器看是.cpp就會調用C++編譯器編譯,linux下要用g++編譯,不再是gcc。
// test.cpp
#include <stdio.h>int main()
{printf("hello world\n");return 0;
}
當然C++有一套自己的輸入輸出,嚴格說C++版本的hello world應該是這樣寫的。
// test.cpp
// 這里的std cout等我們都看不懂,沒關系,下面我們會依次講解
#include <iostream>
using namespace std;int main()
{cout << "hello world\n" << endl;return 0;
}
因為C++是在C語言基礎上進行完善和在發展,所以二者的結構是十分相似的。比較上述兩個程序,將下面的C++程序與熟悉的C語言程序類比可知:
iostream
是程序的頭文件,根據釋義可知其包含輸入輸出函數的頭文件。cout
是輸出函數。
然而其中using namespace std;
可能無法理解,若要理解這條語句需要繼續學習命名空間的知識。
2. 命名空間
2.1 namespace的價值
在下面的程序中,程序中包含了 <stdlib.h>
頭文件,該頭文件中含有 rand
函數,如果再用 rand
作為變量名定義變量,就會造成重定義。
#include <stdio.h>
#include <stdlib.h>// 其中定義了函數rand(),若不包含此頭文件,則程序可以正常編譯運行int rand = 10;int main()
{printf("%d\n", rand);return 0;
}
// 編譯報錯: error C2365: “rand”: 重定義;以前的定義是“函數”
在C/C++中,變量、函數和面向對象的類都是大量存在的,在編寫大型項目的時候這些變量、函數和類的名稱都在全局作用域內沖突,可能會導致很多命名沖突。使用命名空間的目的是對標準符的名稱進行本地化,以避免命名沖突或者符號污染,其中定義命名空間的關鍵字是 namespace。
2.2 命名空間的定義
定義命名空間,需要使用到namespace關鍵字,后面跟命名空間的名字,然后接一對{}
即可,{}
中即為命名空間內的變量/函數/類等。
- 命名空間中可以定義變量/函數/類等。
- namespace只能定義在全局,當然還可以嵌套定義。
- namespace后面的空間名不受限制,可以隨機取,一般取項目名稱作為空間名。
命名空間的定義:
//定義命名空間
namespace N1
{//定義變量int rand = 100;//定義函數int Add(int a, int b){return a + b;}//定義類型(結構體)typedef struct SLNode{int data;SLNode* next;}SLNode;
}
命名空間的嵌套定義:
//定義命名空間
namespace N1//定義一個名為N1的命名空間
{//定義變量int a = 100;namespace N2 //嵌套定義另一個名為N2的命名空間{int b = 200;}
}//嵌套定義的訪問
int main()
{printf("%d\n", N1::N2::b);
}
多文件命名空間的定義
- 項目工程中多個文件中定義的同名namespace會認為是一個namespace,編譯器最后會將其成員合成在同一個命名空間中,不會沖突。
- 所以不能在相同名稱的命名空間中定義兩個相同名稱的成員。
- 注意:一個命名空間就定義了一個新的作用域,命名空間中所有內容都局限于該命名空間中。
2.2.1 命名空間的本質解釋
2.2.1.1 namespace
- namespace本質是定義一個區域,這個區域跟區域各自獨立,不同的區域可以定義不同變量。
- 上面那個C語言的程序之所以會報錯是因為主函數中定義的變量
rand
和stdilb.h
這個頭文件中的rand
函數都是被定義在全局域中,所以會產生命名沖突。在使用了namespace
使用了命名空間這個概念之后就相當于形成了一個新的域,此時的rand
因為在不同的域中則下面程序中rand不在沖突了。 - 既然已經創建了兩個域,那么如何分別調用這不同域中的數據呢?這里就需要使用域作用限定符
::
,未加::
默認訪問全局域,加上了::
則默認訪問此域中的信息。
代碼示例:
#include<stdio.h>
#include<stdlib.h>namespace N2
{int rand = 100;
}int main()
{// 這里默認是訪問的是全局的rand函數指針printf("%p\n", rand);//這里指定訪問N2命名空間中的rand//::域作用限定符printf("%d\n", N2::rand);return 0;
}
運行結果:
這樣即可分別調用不同域中的同一個名稱為rand
的信息了。
2.2.1.2 域
-
C++中域有函數局部域、全局域、命名空間域、類域;區域影響的是編譯時查找一個變量/函數/類型出現的位置(聲明或定義),所有有了域隔離,名字沖突就解決了。
-
局部域和全局域除了不會影響編譯查找邏輯,是會影響變量的生命周期的,命名空間域和類域不影響變量生命周期。
- 在namespace中的定義的變量,其生命周期都是全局的,命名空間域只是起到隔離的作用,沒有影響變量生命周期。
- 局部域中的變量只能在當前局部內訪問。
-
不同的域中可以用同名變量,同一個域中不可以用同名變量。
示例代碼:
#include <stdio.h>//全局域
int x = 0;//局部域
void func()
{int x = 1;
}
//命名空間域
namespace N1
{int x = 2;
}int main()
{//局部域int x = 3;//打印局部域--打印3printf("%d\n", x);//打印命名空間域--打印2printf("%d\n", N1::x);//打印全局域--打印0printf("%d\n", ::x);
}
//這里的打印是在main函數中進行的,所以打印的就是當前局部域中的變量x,而不是func這個局部域中的局部變量
2.2.2 C++標準庫
- C++標準庫都放在一個叫std(standard)的命名空間中。
這也就解釋了開頭using namespace std;
中namespace std;
的含義,其表示要調用C++標準庫中的定義的變量和函數。
2.3 命名空間的使用
編譯查找一個變量的聲明/定義時,默認只會在局部或全局查找,不會主動到命名空間里面去查找。所以下面程序會編譯報錯。
#include <stdio.h>
namespace N1 {int a = 0;int b = 1;
}int main()
{// 編譯報錯: error C2065: “a”: 未聲明的標識符printf("%d\n", a);return 0;
}
可以使用命名空間中的變量/函數,有三種方式:
- **法一:**指定命名空間訪問:項目中推薦這樣方式。
- **法二:**using 將命名空間中的某些成員展開,項目中經常訪問的不在沖突的成員推薦這樣方式。
- 法三:展開命名空間中全部成員,項目不推薦,沖突風險很大,日常小練習程序為了方便推薦使用。
//法一:指定命名空間訪問
int main()
{printf("%d\n", N1::a);return 0;
}//法二:using 將命名空間中某個成員展開
using N::b;
int main()
{printf("%d\n", N1::a);printf("%d\n", b);return 0;
}//法三:展開命名空間中全部成員
using namespace N1
int main()
{ptintf("%d\n", a);printf("%d\n", b);return 0;
}
? 這里再次回歸到上面那個第一個C++代碼中,就可以看懂這句using namespace std;
了,他表示的是利用展開命名空間全部成員的方式展開std(C++標準庫)
。
3. C++ 輸入與輸出
? 這里再次引入,第一個C++代碼:
#include <iostream>
using namespace std;int main()
{cout << "hello world\n" << endl;return 0;
}
? 在C語言中有標準輸入輸出函數scanf
和printf
,而在C++中有**cin
標準輸入和cout
標準輸出**。在C語言中使用scanf
和printf
函數,需要包含頭文件stdio.h
。在C++中使用cin
和cout
,需要包含頭文件iostream
以及std
標準命名空間(如果不寫則需要完整表示std::cout
或者std::cin
)。
- 是 Input Output Stream 的縮寫,是標準的輸入、輸出流庫,定義了標準的輸入、輸出對象。
3.1 輸入輸出函數
3.1.1 cin
函數和 cout
函數
-
std::cin 是 istream 類的對象,它主要面向窄字符(narrow characters of type char) 的標準輸入流。
-
std::cout 是 ostream 類的對象,它主要面向窄字符的標準輸出流。
補充知識:
cin
和cout
中的c
是什么意思?c的含義是窄字符,其本質思想是將內存中的各種數據類型或者原反補碼等數據都轉化成字符流。cin就相當于將輸入的字符流解析成內存中的各種數據,cout就相當于將內存中的各種數據轉化成字符流輸出。
只有在內存中才會有整型、浮點型各種類型和原反補碼等概念,因為CPU需要對這些二進制數據進行一系列運算;但是在其他環境中比如文件、網絡、終端控制臺中只有字符的概念。所以當內存中的一個整型數據想要在控制臺或者文件中來回傳遞都需要先經過字符流進行轉化。
3.1.2 endl
函數
-
std::endl 是一個函數,流輸出時,相當于插入一個換行字符加刷新區。
-
endl其實是end line的縮寫。
3.2 運算符
<<
是流插入運算符- 這個符號用于數據的輸出,可以想象運算符右邊的數據流進cout,然后輸出。
>>
是流提取運算符- 這個符號用于數據的輸入,可以想象運算符右邊的數據流入cin,然后輸入。
- 這兩個運算符是對C語言中的進行復用,C語言還用這兩個運算符做位運算符左右移/右移。
3.3 輸入輸出示例
? 使用C++輸入輸出更方便,不需要像printf/scanf輸入輸出時那樣,需要手動指定格式,C++的輸入輸出可以自動識別變量類型(本質是通過函數模板重載實現的),這個以后會學到,其實最重要的是C++的流能夠更好地支持自定義類型對象的輸入輸出。
#include<iostream>
#include<stdio.h>int main()
{//打印字符串std::cout << "hello world\n";//打印整型int i = 10;std::cout << i << '\n' << "\n";//打印浮點型double d = 1.1;std::cout << d << std::endl;//輸入一個整型,一個浮點型并打印出來//C++和C語言可以混合使用并不干擾std::cin >> i >> d;scanf("%d%lf", &i, &d);std::cout << i << " " << d << std::endl;printf("%d %.2lf", i, d);
}
詳細講解
- 利用語句
std::cout
進行輸出或者std::cin
進行數據輸入的時候,不再需要像C語言中的printf一樣先輸入其類型的占位符再輸出,cout不需要指定其輸出內容是什么類型可以自動識別變量的類型。 cout
和cin
支持連續的字符流的輸出和插入,就像第8行代碼,可以在i
輸出之后繼續在后面執行換行的命令,直接對流進行插入即可。- 最推薦的輸出換行方式就是利用函數
endl
進行換行操作,因為在不同的操作系統下可能有不同的換行符,但是使用這個函數只要程序使用C++的代碼編寫都可以執行換行的命令。 - 在C++代碼中是可以進行和C語言進行混合使用的,并且有些目標的實現利用C語言的函數實現得更加簡單。如這里對于浮點數小數位數的控制則推薦使用
printf
,C++內置的控制函數過于復雜。 - 這里沒有包含
<stdio.h>
,也可以使用printf
和scanf
,在包含頭文件時,vs系列編譯器是這樣子的,其他編譯器可能會報錯。
3.4 補充知識
- IO流涉及類和對象,運算符重載,繼承等很多面向對象的知識,這些知識還辦法進行闡釋,所以這里只能簡單認識一下C++ IO流的用法,后面會有專門的一個部分來細講IO流庫。
cout/cin/end
等都屬于C++標準庫,C++標準庫都放在一個叫std(standard)的命名空間中,所以要通過命名空間的使用方式去調用它們。
- 因為C++中的
cout
和cin
的效率不高,至于為什么效率會不高也會在后面對IO流做細致講解的時候有介紹,如果IO需求較高則采用下面的方式。
#include <iostream>
using namespace std;int main()
{// 在io需求比較高的地方,如部分大量輸入的競賽題中,加上以下3行代碼// 可以提高C++IO效率ios_base::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr);return 0;
}
4. 缺省參數
4.1 缺省參數的概念
? 缺省參數是聲明或定義函數時為函數的參數指定一個缺省值。在調用該函數時,如果沒有指定實參,則采用該形參的缺省值,否則使用指定的實參,缺省參數為全缺省和半缺省參數。(有些地方把缺省參數也叫默認參數)
代碼示例:
#include <iostream>
#include <cassert>
using namespace std;void Func(int a = 0)
{cout << a << endl;
}int main()
{Func(); // 沒有傳參數時,使用參數的默認值Func(10); // 傳參數時,使用指定的實參return 0;
}
運行結果:
0
10
4.2 缺省參數的分類
4.2.1 全缺省參數
全缺省就是全部形參給缺省值。
void Func(int a = 10, int b = 20, int c = 30)
{cout << "a = " << a << endl;cout << "b = " << b << endl;cout << "c = " << c << endl;
}
4.2.2 半缺省參數
半缺省就是部分形參給缺省值。
C++規定半缺省參數必須從左至右次序連續缺省,不能間隔跳躍缺省參數。
void Func(int a, int b = 10, int c = 20)
{cout << "a = " << a << endl;cout << "b = " << b << endl;cout << "c = " << c << endl;
}
4.2.3 注意事項
- 帶缺省參數的函數調用,C++規定必須從左到右依次給實參,不能跳躍給實參。
- 函數聲明和定義分離時,缺省參數不能在函數聲明和定義中同時出現,規定必須函數聲明給缺省值。
缺省參數在實際代碼中的應用:
// Stack.h
#include <iostream>
#include <cassert>
using namespace std;typedef int STDataType;
typedef struct Stack
{STDataType* a;int top;int capacity;
}ST;void STInit(ST* ps, int n = 4);//半缺省,給的默認空間為4個類型大小的空間// Stack.cpp
#include "Stack.h"void STInit(ST* ps, int n)
{assert(ps && n > 0);ps->a = (STDataType*)malloc(n * sizeof(STDataType));ps->top = 0;ps->capacity = n;
}// test.cpp
#include "Stack.h"
int main()
{ST s1;STInit(&s1);//可以在初始化的時候,不指定空間大小,因為缺省參數,會自動把空間設置為4// 如果確定知道要插入1000個數據,初始化時把容量設置大,避免擴容,影響效率ST s2;STInit(&s2, 1000);//由此可以看出缺省參數十分靈活、好用return 0;
}
5. 函數重載
5.1 函數重載的概念
C++支持在同一作用域中出現同名函數,但是要求這些同名函數的形參不同,可以是參數個數不同或者參數類型不同。這樣C++函數調用表現出了多態行為,使用更靈活。C語言是不支持同一作用域中出現同名函數的。
#include <iostream>
using namespace std;// 1、參數類型不同
int Add(int left, int right)
{cout << "int Add(int left, int right)" << endl;return left + right;
}double Add(double left, double right)
{cout <<"double Add(int left, int right)" << endl;return left + right;
}//2、參數個數不同
void f()
{cout << "f()" << endl;
}void f(int a)
{cout << "f(int a)" << endl;
}//3、參數順序不同(本質還是類型不同)
void f(int a, char b)
{cout << "f(int a, char b)" << endl;
}void f(char b, int a)
{cout << "f(char b, int a)" << endl;
}int main()
{Add(10, 20);//打印int Add(int left, int right),并計算10+20的結果返回Add(10.1, 20.2);//打印double Add(int left, int right)并計算10.1+20.2的結果返回f();//打印f()f(10);//打印f(int a)f(10, 'a');//打印f(int a, char b)f('a', 10);//打印f(char b, int a)return 0;
}
補充知識:
- 返回值不同不能作為重載條件,因為調用時也無法區分。(因為參數不同可以區分函數,但是返回值不同無法區分函數)
void f1()
{}int f1()
{return 0;
}
- 構成重載的函數也有可能報錯,下面代碼兩個重載函數語法正確,但是會發生調用不明確的問題。
//下面兩個函數構成重載(參數不同)
//f() 但是調用時,會報錯,存在歧義,編譯器不知道調用哪個
void f1()
{cout << "f()" << endl;
}void f1(int a = 10)
{cout << "f(int a)" << endl;
}
6. 引用
6.1 引用的概念和定義
引用不是新定義一個變量,而是給已存在變量取了一個別名,編譯器不會為引用變量開辟內存空間,它和它引用的變量共享同一塊內存空間。例如:水滸傳中李逵,宋江叫"鐵牛",江湖上人稱"黑旋風"; 林沖,外號豹子頭;
語法:類型& 引用別名 = 引用對象;
C++中為了避免引用太多的運算符,會復用C語言的一些符號,比如前面的<<和>>,這里引用也和取址使用了同一個符號&,注意用法角度區分就可以。
這里和C語言中的typedef
不一樣,typedef
是給類型取別名,這里的引用是給變量取別名。
#include <iostream>
using namespace std;int main()
{int a = 0;// 引用:b和c是a的別名int& b = a;int& c = a;// 也可以給別名b取別名,d相當于a的別名int& d = b;++d;// 這里取地址看到是一樣的,d++他們都++cout << &a << endl;cout << &b << endl;cout << &c << endl;cout << &d << endl;return 0;
}
實際的底層情況如下:也就是一塊空間有多個名字。
6.2 引用的特性
-
引用在定義時必須初始化
int a = 10; int& b = a;//引用在定義時必須初始化
-
一個變量可以有多個引用
int a = 10; int& b = a; int& c = a; //給別名起別名 int& d = b;
-
引用一旦引用一個實體,就不可以再引用其他實體
這個特性也也就決定了別名是沒有辦法替代指針的。
在鏈表這一個數據結構中,數據與數據之間利用指針相互連接,當刪除中間的一個數據的時候,需要將其前一個數據的指針地址由原來的指向此時刪除的數據改為指向現在刪除的這個數據的后一個數據,這樣的行為引用是沒有辦法做到的,因為引用的實體沒有辦法改變,也就無法實現將鏈表的刪除操作,所以其無法替代指針。
int a = 10; int& b = a; int c = 20; b = c; //想法:讓b轉而引用c,但是實際操作的是把c中的20賦值給了a
6.3 引用的使用
引用在實踐中主要是用于引用傳參和引用返回值中減少拷貝(利用別名達到不開辟新空間的目的)提高效率和改變引用對象時改變被引用對象。
6.3.1 引用傳參
示例一:
引用傳參跟指針傳參功能是類似的,引用傳參更方便一些。
- 上面代碼體現了引用特性的功能2:改變引用對象時改變被引用對象。
- 這里使用引用的方式,將a作為rx的引用,e作為ry的引用;同樣可以起到傳址調用的效果。
- 其實本質是讓形參作為實參的別名,讓形參的改變也會影響實參。
示例二:
struct A
{int arr[1000];
};void func(A aa)
{}int main()
{A aa1;func(aa1);return 0;
}
上述代碼中利用別名代替形參的方式,避免了再次創建四千個字節的情況,避免了空間的浪費。
6.3.2 引用作返回值
引用返回值的場景相對比較復雜,在這里只簡單講了一下場景,還有一些內容后續類和對象章節中會繼續深入講解。
#include<iostream>
int& Add(int a, int b)
{static int c = a + b;return c;
}int main()
{int a = 10, b = 20;Add(a, b)++;std::cout << Add(a, b) << std::endl;return 0;
}//運行結果:31
上述代碼就是引用作為返回值的應用,函數Add
的返回值是c
的別名,并在主函數中對對其++,最后打印結果,運行成功。
這里使用引用作為返回值是因為,函數的返回值的本質和形參的本質一樣也是將返回值的數值拷貝到一塊臨時空間,所以如果這里的返回值單純的使用int
表示,編譯器則會報錯:
注意:也并不是所有函數都可以使用別名作為返回值
函數返回的數據不能是函數內部創建的普通局部變量,因為在函數內部定義的普通的局部變量會隨著函數調用的結束而被銷毀。函數返回的數據必須是被static修飾或者是動態開辟的或者是全局變量等不會隨著函數調用的結束而被銷毀的數據,才可以使用引用將其返回。
補充:越界不一定報錯
-
越界讀一定不報錯
-
越界寫不一定報錯
這是因為編譯器對于越界的檢查是抽查,在這里會在數組后面的幾個字節中固定寫入某個值,在程序運行中會去檢查這幾個字節位置的數據有沒有被更改,如果有則報錯,說明一定越界了。但是有時修改數據的位置可能不在抽查位置,這個時候程序就可以正常運行,不會報錯。
6.4 const引用
6.4.1 權限
- 可以引用一個
const
對象,但是必須用const
引用。const
引用也可以引用普通對象,因為對象的訪問權限在引用過程中可以縮小或者平移,但是不能放大。
//const中的權限問題
int main()
{//訪問權限的放大const int a = 10;int& ra = a;// 編譯報錯: error C2440: “初始化”: 無法從“const int”轉換為“int &”// 權限可以縮小int b = 1;const int& rb = b;rb++;b++;//正常編譯運行//訪問權限的平移const int a = 10;const int& ra = a;//正常編譯運行//const的使用const int& ra = a;ra++;//const修飾的ra,所以就不能對ra進行修改// 編譯報錯: error C3892: “ra”: 不能給常量賦值//空間訪問權限和空間拷貝的辨析const int x = 0;int y = x;//這里的y僅僅是對x中的值進行拷貝,不涉及權限問題return 0;
}
//指針中的權限問題
int main()
{// 權限不能放大const int a = 10;const int* p1 = &a;int* p2 = p1;//報錯// 權限可以縮小int b = 20;int* p3 = &b;const int* p4 = p3;//正常編譯運行// 不存在權限放大,因為const修飾的是p5本身不是指向的內容int* const p5 = &b;int* p6 = p5;return 0;
}
6.4.2 臨時對象的常性
- 需要注意的是類似
int& rb = a*3; double d = 12.34; int& rd = d;
這樣一些場景下,表達式a*3
的結果保存在一個臨時對象中,int& rd = d
也是類似,在類型轉換中會產生臨時對象儲存中間值,也就是此時rb
和rd
引用的都是臨時對象,而C++規定臨時對象具有常性(相當于被const修飾),所以這里就觸發了引用限制,必須要用常引用才可以。 - 所調用臨時對象就是編譯器需要一個空間暫時存儲表達式的求值結果時臨時創建的一個未命名的對象,C++中把這個未命名對象做臨時對象。
- 臨時對象一般用于存放表達式的結果或者是類型轉換時的中間值。
#include <iostream>
using namespace std;int main()
{int a = 10;int& rb = a * 3;// 報錯,原因是這里的a*3被保存在臨時對象中,相當于這里的指向a*3的這塊空間被const修飾了,現在用"int&"去修飾本質也是權限的放大//應該改為const int& rb = a * 3;即可double d = 12.34;int& rd = d;// 編譯報錯: “初始化”: 無法從“double”轉換為“int &”//在類型轉換的時候也會產生臨時空間存在d,臨時空間因為有常性,所以就相當于指向d的這塊空間被const修飾,所以現在用"int&"去修飾本質也是權限的放大//改為const int& rd = d;即可return 0;
}
6.4.3 const引用的使用場景
void f1(const int& rx)
{}int main()
{int a = 10;double b = 12.34;f1(a);f1(a * 3);f1(d);
}
在函數f1的形參使用const
進行修飾,可以在調用此函數的對于形參的填寫形式更加寬泛,其實本質都是因為加上了const
權限變得更小了,所以正常的各種參數都可以作為形參傳過去。
使用const
可以引用const對象、普通對象和臨時對象,引用對象十分寬泛。
**注意:(非常重要!!!)**當然其實上述代碼,將形參改為int rx
也可以,也是可以正常傳參的,雖然傳入的是實參的臨時拷貝,這樣其實也是可以的。這是因為現在使用的數據類型都是一些簡單數據類型類似int
、float
等,但是如果這里的數據類型是A(一個極大地數據類型)
,這里使用傳值傳參就會拷貝這個極大的數據,這樣的代價就會很大,所以還是推薦引用傳參,使用引用傳參能接收更多類型的對象,并且要保證引用內容不被更改,所以使用const int& XX
的方式作為函數的形參。這是后期C++學習常見的形參格式。
6.5 指針和引用的關系
C++中指針和引用就像兩個性格迥異的親兄弟,指針是哥哥,引用是弟弟,在實踐中它們相輔相成,功能能有重疊性,但是各有自己的特點,互相不可替代。
-
語法概念上引用是一個變量的取別名不開辟新空間,指針是存儲一個變量地址,要開空間。
-
從底層匯編語言來看,引用也是利用指針實現的,也需要開辟空間。
-
-
引用在定義時必須初始化,指針建議初始化,也不是必須的。
-
引用在初始化時引用一個對象后,就不能再引用其他對象;而指針可以不斷地改變指向對象。
-
引用可以直接訪問指向對象,指針需要解引用才能訪問指向對象。
-
sizeof中含義不同,引用結果為引用類型的大小,但指針始終是地址空間所占字節個數(32位平臺下占4個字節,64位下是8個字節)
-
指針很容易出現空指針和野指針的問題,引用很少出現,引用使用起來相對更安全一些。
7. 內聯函數
7.1 內聯函數的定義
用inline修飾的函數叫做內聯函數,編譯時C++編譯器會在調用的地方展開內聯函數,這樣調用內聯函數就不需要建立棧幀了,就可以提高效率。
#include <iostream>
using namespace std;inline int Add(int x, int y)
{int ret = x + y;ret += 1;ret += 1;ret += 1;return ret;
}int main()
{// 可以通過匯編程序是否展開// 有call Add語句就沒有展開,沒有就是展開了int ret = Add(1, 2);return 0;
}
下圖左是以上代碼的匯編代碼,下圖右是函數Add加上inline后的匯編代碼:
7.2 inline與宏函數
7.2.1 回憶宏函數
C語言實現宏函數也會在預處理時替換展開,但是宏函數實現很容易出錯的,而且不方便調試。
// 正確的宏實現
#define ADD(a, b) ((a) + (b))
// 為什么不能分號?
// 為什么要加外面的括號?
// 為什么要加里面的括號?
//重點:宏函數雖然坑很多,但是因為替換機制讓其調用的時候不同開辟棧幀,提高程序運行效率int main()
{int ret = ADD(1, 2);//不加;的原因cout << ADD(1, 2) << endl;//加外面括號的原因cout << ADD(1, 2) * 5 << endl;//加里面括號的原因int x = 1, y = 2;//位運算符優先級較低,會先執行+-操作ADD(x & y, x | y); // -> (x & y + x | y)return 0;
}
7.2.2 inline
為了彌補C語言中宏函數的各種坑,C++設計了inline的目的是替代C的宏函數。
7.2.2.1 inline的底層邏輯
inline對于編譯器而言只是一個建議,也就是說,加了inline編譯器也可以選擇在調用的地方不展開,不同編譯器關于inline什么情況展開各不相同,因為C++標準沒有規定這個。inline適用于頻繁調用的小函數,對于遞歸函數,代碼相對多一些的函數,加上inline也會被編譯器忽略。
-
vs編譯器debug版本下面默認是不展開inline的,這樣方便調試,debug版本想展開需要設置一下以下兩個地方。
下圖右邊是未展開內聯函數,在底層匯編中還是用了指令call
創建了函數棧幀。左圖這是已經展開了內聯函數,可以看到并沒有調用指令call
而是直接執行ADD函數的邏輯。
在這里插入圖片描述
但是當內聯函數行數過長(這里判斷過長函數一般是由編譯器決定的,比如這里的VS一般是10行以上)編譯器就不會將內聯函數展開。
補充:為什么將inline設置為對編譯器的建議,而不是將決定權交給程序員?
eg:現在有一個100行指令的ADD函數,在1000個位置調用這個函數。
比較:所需要的指令(這里的指令不是內存空間,不要弄混淆)
inline展開,占多少指令:10000*100
inline不展開,占多少指令:10000*1+100(call調用指令每個函數占一次)
要想理解其底層原理,需要對程序運行的本質有一個了解。
在計算機中編寫的所有程序都是一個個文件,在這些文件編譯之后會形成一個.exe(以windows為例)
的可執行文件,這個文件中就是實際的指令,計算機會生成一個進程去分配內存去將這個可執行文件的指令加載到內存中。
有了以上鋪墊和上面的比較可以看出當內聯函數過長,會導致其所占的指令會很多,也就導致指令膨脹,就會導致可執行文件變大,所以將其加載到進程中所占據的內存就會變大,就會造成很多影響。
inline設計的本質思路是一種以空間換時間的做法,省去了調用函數的額外開銷。而將是否將內聯函數展開的決定權交給編譯器,這樣的設計本質是一種防御策略,害怕遇到那些不靠譜的程序員。
7.2.2.2 inline的注意事項
- inline不建議聲明和定義分別到兩個文件,分離會導致鏈接錯誤。因為inline被展開,就沒有函數地址,鏈接時會出報錯。
- 在使用inline定義內聯函數的時候不需要聲明,直接定義到頭文件中即可。
//F.h
#include <iostream>
using namespace std;inline void f(int i);// F.cpp
#include "F.h"
void f(int i)
{cout << i << endl;
}// main.cpp
#include "F.h"
int main()
{// 鏈接錯誤:無法解析的外部符號 "void __cdecl f(int)" (?f@YAXH@Z)f(10);return 0;
}
8. nullptr
NULL 實際上是一個宏,在傳統的 C 頭文件(stddef.h)中,可以看到如下代碼:
#ifndef NULL#ifdef __cplusplus#define NULL 0#else#define NULL ((void*)0)#endif
#endif
C++中 NULL 可能被定義為整數 0,或者 C 中被定義為無類型指針 (void*)
的常量。不論取何種定義,在使用空值的指針時,都不可避免的會遇到一些麻煩。
#include <iostream>
using namespace std;void f(int x)
{cout << "f(int x)" << endl;
}void f(int* ptr)
{cout << "f(int* ptr)" << endl;
}int main()
{f(0);// 本想通過f(NULL)調用指針版本的f(int*)函數,但是由于NULL被定義成0,調用了f(int x)f(NULL);f((int*)NULL);return 0;
}
本想通過 f(NULL)
調用指針版本的 f(int*)
函數,但是由于 NULL 被定義成 0,調用了 f(int x)
,因此與程序的初衷相悖。f((void*)NULL)
調用會報錯。
C++11中引入了 nullptr
,nullptr
是一個特殊的關鍵字,nullptr
是一種特殊類型的字面量,它可以轉換成任何其他類型的指針類型。使用 nullptr
定義空指針可以避免類型轉換的問題,因為 nullptr
只能被隱式地轉換為指針類型,而不能被轉換為整型類型。
#include <iostream>
using namespace std;void f(int x)
{cout << "f(int x)" << endl;
}void f(int* ptr)
{cout << "f(int* ptr)" << endl;
}int main()
{f(0);f(nullptr);//正常運行不會報錯return 0;
}