本篇博客給大家帶來的是一些C++基礎知識!包含函數棧幀的詳解!
🐟🐟文章專欄:C++
🚀🚀若有問題評論區下討論,我會及時回答
??歡迎大家點贊、收藏、分享!
今日思想:微事不通,粗事不能者,必勞;大事不得,小事不為者,必貧——劉向!
一、C++簡介
? ? ? ? C++是在C語言的基礎上完善C語言的不足(表達能力、可維護性、可擴展性)而發展而來的一門語言,它是由Bjarne Stroustrup(本賈尼 斯特勞斯特盧普)命名并完善。
? ? ? ? C++和C語言的區別:
????????C語言:面向過程。C++:面向對象和泛型編程。
? ? ? ? C++:由于C++是在C語言的基礎上完善而來的一門語言,它比C語言更加便捷,很多人學習了這門語言之后都不想寫C語言了。
二、C++的重要性
? ? ? ? 論述一門編程語言的重要性我們可以看它在各種語言的排名、能干些什么(僅個人觀點)。
? ? ? ? 1、2025年5月TIOBE編程語言排行榜:
? ? ? ? 從上圖我們可以看出在近幾年C++的排名是在前5名的,這表明在各行各業C++起著至關重要的作用。
? ? ? ? 2、C++作用
? ? ? ? 1)大型系統軟件開發。瀏覽器、操作系統、編譯器等的開發。
? ? ? ? 2)音視頻處理。FFmpeg、WebRTC等開發最主要的技術棧就是C++。
? ? ? ? 3)PC客戶端開發。開發Windows上的桌面軟件。
? ? ? ? 4)服務端開發。游戲服務、流媒體服務、量化高頻交易服務等的開發。
? ? ? ? 5)游戲引擎開發。開發游戲的。
? ? ? ? 6)嵌入式開發。把具有技術能力的主控板嵌入到機器裝置或者電子裝置的內部,通過軟件來控制這些裝置。如:智能手環、攝像頭、掃地機器人等。
? ? ? ? 7)機器學習引擎。底層用C++來實現,上層用python封裝起來。
? ? ? ? 8)測試開發測試。根據產品來設計測試用例,然后手動的方式進行測試。
三、C++推薦書籍
? ? ? ? 1)C++Primer:經典主講C++語言語法的書籍。
? ? ? ? 2)STL源碼剖析:從底層實現的角度結合STL源碼來剖析STL的實現。
? ? ? ? 3 ) Effctive C++:主講55個高效使用C++的條款。
四、C++第一個程序
? ? ? ? 我們之前一開始學C語言第一次編寫的代碼:
//C語言版
int main()
{printf("hello world\n");return 0;
}
? ? ? ? 由于C++是在C語言的基礎上完善的,它兼容C,那么我們可以在后綴為.cpp文件上實現C語言的代碼。
? ? ? ? 那么我們怎么用C++來實現呢??
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<iostream>//初學階段一旦我們使用流插入(cin)和流提取(cout)就要包含這個文件
using namespace std;//這里看不懂,往后學再回來看
//C++版int main()
{cout << "hello world" << endl;return 0;
}
? ? ? ? 他們的區別是什么??
? ? ? ? C++不用手寫數據類型,而C語言要。
例如:
//C語言版
int main()
{char arr[] = "hello wolde";printf("%s\n",arr);return 0;
}
//C ++版
int main()
{char arr[] = "hello wolde";cout << arr << endl;return 0;
}
五、命名空間(namespace)
????????1、namespace的使用價值
? ? ? ? 在工作或者學習中我們自己定義的變量、類、函的名稱有可能和庫里面、其他人定義的一樣,為了防止這樣的情況出現,我們使用namespace就能能很好解決這樣的問題。
????????2、namespace的定義
? ? ? ? ???1)namespace的書寫方式:namespace + 名字。
示例:
namespace LA
{}int main()
{return 0;
}
? ? ? ? 注意:namespace只能寫在全局域。他和結構體不一樣。
? ? ? ? 2)命名空間里面可以寫些什么?定義變量/函數/類型。
例如:
namespace LA
{int a = 0;int add(int x, int y){return x + y;}struct node{struct node* next;int data;};}
? ? ? ? 3)在C++中有四個域:局部域、全局域、類域、命名空間域。不同域可以定義同名變量。
除了命名空間域和類域不影響變量生命周期,其他會影響生命周期。
例如:
namespace LA
{int a = 0;//命名空間域int add(int x, int y){return x + y;}struct node{struct node* next;int data;};}
//C語言版int a = 8;//全局域
int main()
{int a = 1;//局部域char arr[] = "hello wolde";printf("%s\n",arr);return 0;
}
? ? ? ? 注意:如果訪問同名變量,先訪問局部再訪問全局(就近原則)。如果要訪問命名空間域的要使用域作用限定符( ::),::a 訪問全局域的a,LA::a訪問命名空間域的a。編譯器默認的查找規則:先局部再全局。
? ? ? ? 4)在工作中,如果自己定義類型或者變量與其他人一樣,各自可以把自己寫的變量和類型放到的自己的命名空間域里面,這樣就解決了命名沖突的問題。
? ? ? ? 5)命名空間域可以嵌套使用。一般只嵌套兩層,多個嵌套的命名空間域會認為是一個命名空間域。
例如:
namespace LA
{int a = 0;//命名空間域namespace LB{int a = 10;}int add(int x, int y){return x + y;}struct node{struct node* next;int data;};}
//C語言版int a = 8;//全局域
int main()
{int a = 1;//局部域//char arr[] = "hello wolde";printf("%d\n",LA::LB::a);return 0;
}
? ? ? ? 6)C++標準庫放在std(standard)命名空間里面。
? ? ? ? 3、命名空間的使用
? ? ? ? ? ? 訪問命名空間的三種方法:指定命名空間訪問(::)、展開命名空間中全部成員、using將命名空間中某個成員展開。
? ? ? ? ? ? 域作用限定符之前講過了,我們看看展開命名空間中全部成員:
? ? ? ? ? ? 1)展開命名空間中全部成員即:影響編譯器的查找規則,此時他們從命名空間域變成了全局域了,不用使用域作用限定符就能訪問。
示例:
namespace LA
{int a = 0;//命名空間域int b = 1;namespace LB{int a = 10;}int add(int x, int y){return x + y;}struct node{struct node* next;int data;};}
//C語言版
using namespace LA;
//int a = 8;//全局域
//using LA::b;int main()
{//int a = 1;//局部域//char arr[] = "hello wolde";printf("%d\n",a);
注意:這樣的沖突風險極大,項目不推薦。
? ? ? ?2) 如果經常使用命名空間中的某個成員可以指定的把他展開:
例如:
namespace LA
{int a = 0;//命名空間域int b = 1;namespace LB{int a = 10;}int add(int x, int y){return x + y;}struct node{struct node* next;int data;};}
//C語言版
//using namespace LA;
//int a = 8;//全局域
using LA::b;int main()
{//int a = 1;//局部域//char arr[] = "hello wolde";printf("%d\n",b);return 0;
}
? ? ? ? 因為我們經常使用C++中的cout、cin、endl(換行),所以我們一開始就展開標準庫。
例如:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<iostream>//初學階段一旦我們使用流插入(cin)和流提取(cout)就要包含這個文件
using namespace std;
? ? ? ? 如果不展開std,我們使用cout、cin等要指定std命名空間域:
#include<stdio.h>
#include<iostream>//初學階段一旦我們使用流插入(cin)和流提取(cout)就要包含這個文件
//using namespace std;int main()
{std::cout << "hello world" << std::endl;return 0;
}
六、C++輸入和輸出
? ? ? ? 我們一開始就<iostream>文件,iostream是Input Output Stream的縮小,是標準的輸入輸出庫,定義了標準的輸入和輸出對象。
? ? ? ? 1)std::cin是istream類的對象,它主要面向窄字符的標準輸入流。
? ? ? ? 2)std::cout是ostream類的對象,它主要面向窄字符的標準輸出流。
? ? ? ? 3)std::endl是一個函數,流插入輸出時,相當于插入一個換行字符加刷新緩沖區。
? ? ? ? 4)<<是流插入運算符,>>是流提取運算符。在C語言里面他們分別為位運算左移和右移。
? ? ? ? 5)使用C++的輸入輸出它不用像C語言那樣指定類型,更加方便,其實它的本質就是運算符重載。
#include<iostream>//初學階段一旦我們使用流插入(cin)和流提取(cout)就要包含這個文件
//using namespace std;int main()
{std::cout << "hello world" << '\n'<<"abc"<<std::endl;int a;std::cin >> a ;//不能使用換行return 0;
}
? ? ? ? 6)我們包含<iosream>其實就包含了<stdio.h>,我們包含<stdio.h>也可以用printf和scanf函數(VS是這樣,其他不知道)。
?七、函數重載
? ? ? ? C語言是不支持同名函數在同一個作用域的,而C++支持,不過C++對于同名函數的書寫是有要求的,具體如下:
? ? ? ? 1)同名函數的形參不同。
#include<iostream>//初學階段一旦我們使用流插入(cin)和流提取(cout)就要包含這個文件
using namespace std;int Add(int x)
{cout << "Add(int x,int y)"<<endl;return 1;
}int Add(char x, int y)
{cout << "Add(int x,int y)" << endl;return 4;
}int main()
{Add(1, 'a');Add('d', 1);return 0;
? ? ? ? 2)同名函數的類型不同。
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<iostream>//初學階段一旦我們使用流插入(cin)和流提取(cout)就要包含這個文件
using namespace std;double Add(double x, double y)
{cout << "double" << endl;return x + y;
}int Add(int x, int y)
{cout << "int" << endl;return x + y;
}
int main()
{int a = 1;int b = 2;double a1 = 3.1;double b2 = 3.14;Add(a, b);Add(a1, b2);return 0;
}
? ? ? ? 3)同名函數的參數個數不同。
using namespace std;void fun()
{cout << "void fun()" << "\n" << endl;
}void fun(int a)
{cout << "void fun(int a)" << "\n" << endl;
}int main()
{fun();int f = 1;fun(f);return 0;
}
? ? ? ? 4)參數類型的順序不同(本質是類型不同)。
#include<iostream>//初學階段一旦我們使用流插入(cin)和流提取(cout)就要包含這個文件
using namespace std;int Add(int x, char y)
{cout << "Add(int x,int y)"<<endl;return 1;
}int Add(char x, int y)
{cout << "Add(int x,int y)" << endl;return 4;
}int main()
{Add(1, 'a');Add('d', 1);return 0;
}
注意:返回值不同不能構成函數重載。
如:
int fun()
{return 2;
}
int fun()
{return 3;
}int main()
{/*int z = Add();cout << z << endl;*/fun();return 0;
}
八、缺省參數(默認參數)
? ? ? ? 缺省參數是定義函數或者聲明的時候指定一個缺省值,在調用該函數的時候如果沒有指定實參就采用缺省值,否則就使用指定的實參。缺省參數分為全缺省參數和半缺省參數。
?例如:
#include<iostream>
using namespace std;void fun(int i=1)
{cout << i << endl;
}int main()
{fun();return 0;
}
?打印結果:
#include<iostream>
using namespace std;void fun(int i=1)
{cout << i << endl;
}int main()
{//fun();fun(2);return 0;
}
打印結果:
1、全缺省
? ? ? ? 全缺省是全部形參都給缺省值。
例如:
//全缺省
void fun(int a = 1,int b = 2,int c = 3)
{cout << a << endl;cout << b << endl;cout << c << endl;}int main()
{fun();//如果一個不傳實參,就用缺省值來打印fun(10);//傳一個則a=10,b,c就用缺省值來打印fun(10.20);//傳兩個實參,則a=10,b=20,c就用缺省值來打印fun(10, 20, 30);//傳三個實參,就用這三個值來打印return 0;
}
?打印結果:
注意:不能跳躍給實參。
如:?
#include<iostream>
using namespace std;//全缺省
void fun(int a = 1,int b = 2,int c = 3)
{cout << a << endl;cout << b << endl;cout << c << endl;cout <<'\n' << endl;}int main()
{//fun();//如果一個不傳實參,就用缺省值來打印//fun(10);//傳一個則a=10,b,c就用缺省值來打印//fun(10,20);//傳兩個實參,則a=10,b=20,c就用缺省值來打印//fun(10, 20, 30);//傳三個實參,就用這三個值來打印fun(, , 30);return 0;
}
?2、半缺省
? ? ? ? 半缺省就是部分形參是缺省值。
? ? ? ? 注意:給缺省值只能從右往左傳。
如:
//半缺省
void fun(int a,int b,int c = 3)
{cout << a << endl;cout << b << endl;cout << c << endl;cout <<'\n' << endl;}int main()
{//fun();//如果一個不傳實參,就用缺省值來打印//fun(10);//傳一個則a=10,b,c就用缺省值來打印fun(10,20);//傳兩個實參,則a=10,b=20,c就用缺省值來打印fun(10, 20, 30);//傳三個實參,就用這三個值來打印return 0;
}
注意:不能跳躍著給缺省值。
如:
#include<iostream>
using namespace std;//半缺省
void fun(int a=1,int b,int c = 3)
{cout << a << endl;cout << b << endl;cout << c << endl;cout <<'\n' << endl;}int main()
{//fun();//如果一個不傳實參,就用缺省值來打印//fun(10);//傳一個則a=10,b,c就用缺省值來打印fun(10,20);//傳兩個實參,則a=10,b=20,c就用缺省值來打印fun(10, 20, 30);//傳三個實參,就用這三個值來打印return 0;
}
注意:傳實參的時候形參有幾個沒有給缺省值就至少要傳幾個實參。
如:
#include<iostream>
using namespace std;//半缺省
void fun(int a,int b,int c = 3)
{cout << a << endl;cout << b << endl;cout << c << endl;cout <<'\n' << endl;}int main()
{//fun();//如果一個不傳實參,就用缺省值來打印//fun(10);//傳一個則a=10,b,c就用缺省值來打印fun(10,20);//傳兩個實參,則a=10,b=20,c就用缺省值來打印fun(10, 20, 30);//傳三個實參,就用這三個值來打印return 0;
}
3、聲明和定義不能同時給缺省值
正確代碼:
//聲明
#pragma onceint Add(int x = 10, int y = 20);
//定義
int Add(int x, int y)
{return x + y;
}int main()
{int z = Add();cout << z << endl;//打印結果為30return 0;
}
錯誤代碼:
//聲明
#pragma onceint Add(int x = 10, int y = 20);
//定義
int Add(int x=1, int y=2)
{return x + y;
}int main()
{int z = Add();cout << z << endl;return 0;
}
注意:不能在定義那里給缺省值,而定義那里給,因為我們包含的是頭文件,別人使用是聲明的那個。
注意:如果一個函數有缺省值,另外一個沒有,則他們構成函數重載,但是他們在調用的使用有些問題。
如:
void fun()
{cout << "void fun "<<endl;
}
void fun(int a=1)
{cout << "void fun(int a=1)" << endl;
}int main()
{/*int z = Add();cout << z << endl;*/fun(10);//沒有問題fun();//有問題,不知道調用哪個return 0;
}
九、引用
? ? ? ? 引用就是給一個變量取別名,例如:我叫橘頌,別人叫我小橘子,那么小橘子就是我,小橘子的改變等于我的改變。引用的符號為:&,引用的底層是指針,引用不能代替指針(例如:鏈表)。
代碼示例:
int main()
{int a;int& b = a;b = 10;//b=10,那么a也等于10;cout << a << endl;cout << b << endl;return 0;
}
打印結果:
注意:他們的地址也是一樣的,如:
int main()
{int a;int& b = a;b = 10;//b=10,那么a也等于10;cout << &a << endl;cout << &b << endl;return 0;
}
?打印結果:
1、引用的特性
? ? ? ? 1、1引用定義時必須初始化
int main()
{//錯誤代碼int a = 1;int& b;//編譯報錯,必須確定引用的對象是什么b = a;//正確代碼int a = 1;int& b = a;return 0;
}
? ? ? ? 1、2一個變量可以有多個引用
int main()
{int a = 1;int& b = a;int& c = b;int& d = a;return 0;
}
? ? ? ? 1、3引用一旦引用一個實體就不能再引用其他實體
int main()
{int a = 1;int b = 2;int& c = a;c = b;//這里不是引用而是賦值return 0;
}
2、引用的使用
? ? ? ? 2.1引用傳參
? ? ? ? 原來我們傳地址過去,通過解引用來改變他們,選擇同取別名來直接改變,增強了代碼的可讀性。
void swap(int& x, int& y)//相當于int& x=a,int& y=b
{int tmp = x;x = y;y = tmp;
}int main()
{int a = 10;int b = 20;swap(a, b);//不能swap(10,20),常量不能做變量的別名,如果真的要這樣傳可以swap(const int& x,const int& y)讓x具有常屬性cout << a << endl;cout << b << endl;return 0;
}
? 打印結果:
typedef struct test
{int a;
}test;int Add(test& x)//x就是a
{x.a = 2;return x.a + 2;
}
int main()
{test a;Add(a);//不用傳地址過去return 0;
}
2、2引用在引用傳參和引用做返回值中可以提高效率和減少拷貝并且改變引用對象的同時也改變被引用對象
? ? ? ? 我們以前在使用C語言傳值調用的時候會進行臨時對象的拷貝而引用不用這樣提高了代碼執行的效率,當然我們使用傳址傳參的時候會開空間,而引用不用這樣也提高了代碼的效率。
int tmp;
int& Add(int x, int y)
{tmp = x + y;//如果定義成int tmp=x+y,把第一行代碼刪除,當函數棧幀銷毀的時候tmp就成了野引用return tmp;//返回值必須為左值(變量),不能是常量
}
int main()
{tmp += Add(2, 3);//tmp=5+5;cout << tmp << endl;return 0;
}
注意:如果我們返回是常量,這時候會拷貝臨時對象作為返回值,而臨時對象具有常屬性,不能讓引用作為返回值。左值:能取地址的就是左值,左值能被修改。右值和左值相反。
示例:
int Add(int x, int y)
{return x+y;//傳值返回的時候會生成它的拷貝即臨時對象,臨時對象具有常性,相當于被const修飾了
}
int main()
{Add(2, 3)+=2;//相當于5=5+2,這樣是不對的,一般都是變量在左邊:a+=2;a=a+2;return 0;
}
課外知識:
int main()
{int arr[10];//越界讀,編譯器通常不報錯cout << arr[10] << endl;cout << arr[11] << endl;cout << arr[12] << endl;return 0;
}
int main()
{int arr[10];//越界讀,編譯器通常不報錯//cout << arr[10] << endl;//cout << arr[11] << endl;//cout << arr[12] << endl;//越界寫通常會報錯,但是數組檢查一般是抽查,如果多次越界寫偶爾不會報錯arr[10] = 10;return 0;
}
十、const引用
補充知識:
int main()
{const int a = 2;//表示a指向內容不能被修改,例如:a++int const b = 2;//也表示b指向的內容不能改變,和上一句代碼一樣int const b=2和const int b=2,一樣//在const修飾指針的時候不一樣int c = 2;int d = 3;const int* f = &c;//const在*號的左邊,表示指向的內容不能別修改如:*f=20;但是本身可以改變:d=*f;int* const f = &c;//本身不能被修改如:f=NULL;但是指向的內容可以改變如:*f=20return 0;
}
1、權限的放大和縮小
int main()
{const int a = 1;int& b = a;//權限的放大,本來a是不能改變它指向的內容,你b是a的別名之后有權力改變,倒反天罡啊!!const int& b = a;//正確代碼return 0;
}
int main()
{//const int a = 1;//int& b = a;//權限的放大,本來a是不能改變它指向的內容,你b是a的別名之后有權力改變,倒反天罡啊!!//const int& b = a;//正確代碼int b = 2;const int& a = b;//權限的縮小,但是a不能a++return 0;
}
注意:權限可以縮小不能放大。
2、const使用的場景
1):
void swap(int& x, int& y)
{//
}
int main()
{const int a = 10;const int b = 30;swap(a, b);//權限的放大return 0;
}
正確代碼:
void swap(const int& x,const int& y)
{//
}
int main()
{const int a = 10;const int b = 30;swap(a, b);return 0;
}
2):?
void swap(int& x,int& y)
{//
}
int main()
{const int a = 10;const int b = 30;swap(10, 10);//編譯錯誤return 0;
}
正確代碼:
void swap(const int& x,const int& y)
{//
}
int main()
{const int a = 10;const int b = 30;swap(10, 10);//int& x = 1;//變量不能做常量的別名const int& x = 1;//const修飾變量x讓它具有常屬性,而且不能改變它的內容即:x++return 0;
}
3):
int main()
{double a = 3.14;int b = a;//進行隱式類型轉換,過程是a創建一個臨時對象給b,把a的整形(3)給它//int& c = a;//錯誤代碼,因為臨時對象具有常屬性,引用的是它的臨時對象,相當于權限放大。const int& c = a;//正確代碼return 0;
}
注意:臨時對象一般存儲在寄存器,而寄存器一般是4個字節或者8個字節,存儲不下之后放到內存的某塊區域。臨時對象的產生的場景:函數傳值返回,表達式運算(a+b),類型轉換等。
4)權限的放大和縮小只涉及到引用和指針(大白話:除了引用和指針有權限的放大和縮小,其他都沒有),不涉及到變量。
int main()
{const int a = 3;int b = a;//a的改變不影響b,b的改變也不影響a,這個代碼實現的過程是把a的值拷貝給c,他們是兩個不同的空間return 0;
}
十一、指針和引用的關系
? ? ? ? 1)指針和引用互相不能替代,在語法層面上:引用是不開空間,指針要開空間(存儲地址),但是底層就不一定咯,就像老婆餅沒有老婆,引用的底層也是要開空間的,我們可以通過它匯編代碼來看一下:
int main()
{int a = 2;int& b = a;int* c = &a;*c = 3;return 0;
}
匯編代碼:
int main()
{
001118C0 push ebp
001118C1 mov ebp,esp
001118C3 sub esp,0E8h
001118C9 push ebx
001118CA push esi
001118CB push edi
001118CC lea edi,[ebp-28h]
001118CF mov ecx,0Ah
001118D4 mov eax,0CCCCCCCCh
001118D9 rep stos dword ptr es:[edi]
001118DB mov eax,dword ptr [__security_cookie (011A040h)]
001118E0 xor eax,ebp
001118E2 mov dword ptr [ebp-4],eax
001118E5 mov ecx,offset _889F381A_C++_test@cpp (011C066h)
001118EA call @__CheckForDebuggerJustMyCode@4 (0111343h) int a = 2;
001118EF mov dword ptr [a],2 int& b = a;
001118F6 lea eax,[a] //和下面指針代碼執行指令一模一樣,都要開空間存儲地址
001118F9 mov dword ptr [b],eax int* c = &a;
001118FC lea eax,[a]
001118FF mov dword ptr [c],eax *c = 3;
00111902 mov eax,dword ptr [c]
00111905 mov dword ptr [eax],3 return 0;
0011190B xor eax,eax
}
總結:引用的底層是指針。
? ? ? ? 2)引用和指針一樣,有野指針也有野引用。引用在定義的時候必須初始化,而指針可以不初始化(建議初始化防止成為野指針)
? ? ? ? 3)引用初始化引用一個對象之后就不能引用其他對象而指針可以。
? ? ? ? 4)sizeof計算引用的大小是引用類型的大小,而指針始終是地址空間所占字節個數(32位平臺下占4個字節,64位下是8字節)
示例:
//64位下
int main()
{int a = 2;int& b = a;cout << sizeof(b) << '\n' << endl;int* c = &a;*c = 3;cout << sizeof(c) << '\n' << endl;return 0;
}
打印結果:
? ? ? ? 5)指針很容易出現空指針和野指針的問題,而引用很少出現,能用引用就不用指針。
十二、inline(內聯)
補充知識:
//正確的宏函數的書寫
#define ADD(a,b) ((a)+(b))
//1)為什么不能加分號?
//2)為什么要加外面的括號?
//3)為什么要加里面的括號?int main()
{//1)ADD(a,b)((a)+(b));//int a = 2;//int b = 3;//if(ADD(a,b))//if((((2)+(3));)錯誤//{// ////}//2)ADD(a,b) (a)+(b)//int a = 2;//int b = 3;//cout << ADD(2, 3) * 5 << endl;//本來計算結果是25,但是(2)+(3)*5,結果是17,錯誤//3)ADD(a,b) a+b//cout << ADD(2&1, 3&1) << endl;//當出現比加號的優先級低的就會出現問題:本來預想:(2&1)+(3&1)結果:2&(1+3)&1return 0;
}
1)我們在C語言部分的時候經常定義宏函數,從上面的知識補充可知宏函數很復雜而且容易出錯,還不能調試,這些都是宏函數的缺點。宏函數的優點:預處理的時候直接替換不用建立函數棧幀,效率提高。這里我們C++設計了inline目的就是為了解決宏函數的缺點來代替宏函數。
2)函數可以解決宏函數的缺點但是會建立函數棧幀,效率降低。我們可以把函數放到內聯里面,這樣編譯C++時候調用的時候展開內聯函數,不用建立函數棧幀,提高效率。
小知識:內聯代替宏函數,const和enum代替宏常量。
3)inline對于編譯器來說只是一個建議,既然是個建議編譯器可以在調用的時候展開inline也不展開。不同編譯器關于inline什么情況展開各不相同,因為C++標準沒有規定。一般來說放到內聯的函數代碼較少的,會展開,反之會被編譯器忽略。
注意:關于函數棧幀的創立、銷毀和匯編代碼指令的理解我們要補充一下相關知識:
代碼示例:
inline int Add(int x, int y)
{
005A1910 push ebp
005A1911 mov ebp,esp
005A1913 sub esp,0CCh
005A1919 push ebx
005A191A push esi
005A191B push edi
005A191C lea edi,[ebp-0Ch]
005A191F mov ecx,3
005A1924 mov eax,0CCCCCCCCh
005A1929 rep stos dword ptr es:[edi]
005A192B mov ecx,offset _ACF1371A_test3@cpp (05AC15Ch)
005A1930 call @__CheckForDebuggerJustMyCode@4 (05A133Eh) int ret = x + y;
005A1935 mov eax,dword ptr [x]
005A1938 add eax,dword ptr [y]
005A193B mov dword ptr [ret],eax return ret;
005A193E mov eax,dword ptr [ret]
}
005A1941 pop edi
005A1942 pop esi
005A1943 pop ebx
005A1944 add esp,0CCh
005A194A cmp ebp,esp
005A194C call __RTC_CheckEsp (05A125Dh)
005A1951 mov esp,ebp
005A1953 pop ebp
005A1954 ret
int main()
{
005A1970 push ebp
005A1971 mov ebp,esp
005A1973 sub esp,0CCh
005A1979 push ebx
005A197A push esi
005A197B push edi
005A197C lea edi,[ebp-0Ch]
005A197F mov ecx,3
005A1984 mov eax,0CCCCCCCCh
005A1989 rep stos dword ptr es:[edi]
005A198B mov ecx,offset _ACF1371A_test3@cpp (05AC15Ch)
005A1990 call @__CheckForDebuggerJustMyCode@4 (05A133Eh) int ret = Add(2, 3);
005A1995 push 3
005A1997 push 2
005A1999 call Add (05A11D6h)
005A199E add esp,8
005A19A1 mov dword ptr [ret],eax cout << ret << endl;
005A19A4 mov esi,esp
005A19A6 push offset std::endl<char,std::char_traits<char> > (05A103Ch)
005A19AB mov edi,esp
005A19AD mov eax,dword ptr [ret]
005A19B0 push eax
005A19B1 mov ecx,dword ptr [__imp_std::cout (05AB0A8h)]
005A19B7 call dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (05AB09Ch)]
005A19BD cmp edi,esp
005A19BF call __RTC_CheckEsp (05A125Dh)
005A19C4 mov ecx,eax
005A19C6 call dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (05AB0A0h)]
005A19CC cmp esi,esp
005A19CE call __RTC_CheckEsp (05A125Dh) return 0;
005A19D3 xor eax,eax
}
圖片解疑:
好了我們接著解讀內聯是否展開的問題,有了前面函數棧幀的創建和銷毀的知識鋪墊我們可以得出一個結論:
通過匯編代碼觀察內聯是否展開,如果匯編代碼出現call Add就是沒有展開,反之就是展開了。
代碼示例:
int ret = Add(2, 3);
005A1995 push 3
005A1997 push 2
005A1999 call Add (05A11D6h) //沒有展開,根據Add函數的地址跳到Add函數執行代碼
005A199E add esp,8
005A19A1 mov dword ptr [ret],eax
注意:VS的call Add后面跟的不是Add函數的地址,它做了一些優化。如:
現在Add后面跟的才是Add函數的地址。
總結欸:通過上面的知識我們可得:inline在一定程度上會讓編譯后的可執行程序變大。可執行程序變大,用戶體驗降低。如果inline展開受程序員來控制(本質就是對程序員的不放心),那么可執行程序會變大,所以inline受編譯器來控制是否展開。
示例:函數編譯號之后一共10行指令,調用10000次,inline展開合計:10000*10行,不展開:10000+10行(原因:通過調用函數來棧幀來完成)?
4)VS編譯器debug版本下默認是不展開inline的,這樣方便調試。debug版本如果想展開需要設置一下:
?這時候就展開了,代碼示例:
int ret = Add(2, 3);
00301560 mov eax,2
00301565 add eax,3
00301568 mov dword ptr [ebp-8],eax
0030156B mov ecx,dword ptr [ebp-8]
0030156E mov dword ptr [ret],ecx
5)inline不建議聲明和定義分離,分離會導致鏈接錯誤,因為inline被展開,就沒有函數地址,鏈接就會出現報錯。
//test2.cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include"test2.h"
using namespace std;inline int Add(int x, int y)
{int ret = x + y;return ret;
}
//test2.h
#pragma onceinline int Add(int x, int y);
//test3.cpp
#define _CRT_SECURE_NO_WARNINGS 1
using namespace std;
#include<iostream>#include"test2.h"
int main()
{int ret = Add(2, 3);cout << ret << endl;return 0;
}
結果:
解讀:鏈接錯誤,找不到Add函數。一般來說我們只要定義和聲明分離,然后在使用Add函數的時候我們在前面聲明一下,給編譯器說一聲這個函數是有的,然后在鏈接過程中在其他文件的符號表找到Add函數的地址填充使用Add函數的時候給他一個地址,這樣就算是完整的鏈接了(聲明的時候是沒有Add函數的地址的,定義才有)。那么inline使用定義和聲明分離,在inline定義的那個文件上,鏈接過程中是不會放到符號表的,如果加static也是不會放到符號表的。
正確書寫inline的定義:不要定義和聲明分離,把它直接放到頭文件就行。
代碼示例:
//test2.h
#pragma onceinline int Add(int x, int y)
{int ret = x + y;return ret;
}
十三、nullptr
? ? ? ? 在C語言中我們知道NULL是個宏,被定義為((void*)0),空指針不是一個無效的地址,它是內存地址的最開始的第一個字節的編號(地址是指向一個一個的字節),系統認為它沒有人用會自動的把它空出來進行初始化,如果訪問就會報錯。在C++中被定義為0。
? ? ? ? NULL的缺陷:
void fun(int x)
{cout << "void fun(int x)" << endl;
}
void fun(int* ptr)
{cout << "void fun(int* ptr)" << endl;
}
int main()
{fun(0);//調用第一個函數fun(NULL);//在C++中NULL在預處理的時候被替換成0,所以調用第一個函數fun((int*)NULL);//如果想調用第二個函數就要對NUULL進行強轉,太麻煩了。所以在C++創建了nullptrfun(nullptr);return 0;
}
運行結果:
? ? ? ? 注意:nullptr是一個特殊的關鍵字,它可以自動轉換成任何類型的指針類型,但是不會轉換整型,為什么?如果能轉換成整型要NULL干嘛!,就是說在C++中NULL表示0,而nullptr表示指針。?
完!!