目錄
一.inline內聯的詳細介紹
(1)為什么在調用內聯函數時不需要建立棧幀:
(2)為什么inline聲明和定義分離到兩個文件會產生鏈接錯誤,鏈接是什么,為什么沒有函數地址:
二.類,實例化和this指針
1.類的介紹(class):
2.實例化:
(1)實例化的概念:
(2)實例化的空間分配:
3.this指針:
4.關于空指針訪問成員變量的注意點:
三.類的默認成員函數
1.構造函數:
2.析構函數:
3.拷貝構造函數:
4.運算符重載:
四.日期類實現
一.inline內聯的詳細介紹
為了更清楚的明白類的定義與底層運行邏輯,我先從inline內聯開始講起:
? ?inline修飾的函數叫做內聯函數,編譯時C++編譯器會在調用的地方展開內聯函數,這樣調用內聯函數就不需要建立棧幀了,就可以提高效率 //(1)為什么不需要建立棧幀
? inline對于編譯器??只是?個建議,也就是說,你加了inline編譯器也可以選擇在調?的地方不展開,不同編譯器關于inline什么情況展開各不相同,因為C++標準沒有規定,inline適用于頻繁調?的短?函數,對于遞歸函數,代碼相對多?些的函數,加上inline也會被編譯器忽略(因為內聯的展開在需要頻繁調用短小函數的代碼里,可以大限度上減少函數調用指令(call)的使用,而在其他函數體本身較大的情況下,inline不展開的調用指令的方法可能會顯得更為簡便)?
? C語言實現宏函數也會在預處理時替換展開,但是宏函數實現很復雜很容易出錯的,且不?便調 試,C++設計了inline?的就是替代C的宏函數
? vs編譯器debug版本下?默認是不展開inline的,這樣方便調試
? inline不建議聲明和定義分離到兩個?件,分離會導致鏈接錯誤。因為inline被展開,就沒有函數地址,鏈接時會出現報錯//(2)為什么會產生鏈接錯誤,鏈接是什么,為什么沒有函數地址
(內聯的使用示例)
#include <iostream>// 顯式聲明為內聯函數 inline int add(int a, int b) {return a + b; }int main() {std::cout << add(3, 5) << std::endl; // 編譯器可能將 add(3,5) 展開為 `3 + 5`return 0; }
上述的闡述乍看總有種似懂非懂的感覺,但一旦深入想想就還有好些東西不明不白,下面我將對這段話中可能會產生的疑問做出一一解答:
(1)為什么在調用內聯函數時不需要建立棧幀:
(a).在理解這個問題之前,我們需要先搞明白函數的棧幀是怎么一回事:
? ? ? ?簡而言之棧幀是程序執行過程中用于保存函數調用狀態的臨時數據結構,它在函數調用時被創建,返回時銷毀。每個棧幀對應一次函數調用,記錄了函數的執行上下文信息
以下這張圖片就展示了函數Add在調用時所創建的棧幀,而其中的push等相關匯編命令我也附在下面:
? ? ? ??
(b).再讓我們區別以下函數的棧幀與整體代碼的編譯和鏈接的關系:
據上述代碼編譯的過程而言,函數棧幀的創建屬于程序運行時的動態數據結構,雖與編譯鏈接過程的靜態代碼無關,但編譯與鏈接依舊會在其運行時對其產生影響:如編譯階段為棧幀的創建和銷毀生成正確的指令,以及鏈接階段確定函數的位置以及符號引用,因此,一般情況下較小的函數被inline展開時,其函數名并不會進入符號表,而是直接在調用處替換代碼(發生在預處理中,編譯之前),自然也就跟棧幀的創建銷毀沒啥關系了
(2)為什么inline聲明和定義分離到兩個文件會產生鏈接錯誤,鏈接是什么,為什么沒有函數地址:
??
inline之所以不建議聲明定義分離,是因為當我們假設在head.h頭文件里定義了內聯函數add(自定義函數名)然后分別在a.cpp里定義add函數然后在b.cpp里調用add函數然后運行,那么在對程序進行編譯時,會發現對于add函數頭文件里只有聲明而沒有定義,因此編譯器會假設add為一個外部函數(這里類似于一般函數的跨文件調用),但與一般函數調用不同的是,一般函數在假設外部函數時會同時在符號表生成一個對函數的引用(包含了未解析的地址),然后再在鏈接過程中通過對各文件的鏈接重新補全符號表里未解析的地址,從而實現函數聲明定義的分開,但inline函數卻不一樣,它同樣會在符號表里生成一個未解析的地址,但由于inline函數的性質就是對函數體代碼的整體替換從而實現對指令代碼的節約使用,而且需要明確的內聯點才可以進行替換,因此這樣導致了其無法在鏈接時找到對應的內聯點進而不能像一般函數那樣在鏈接過程中補全對應的符號表里未解析的地址(內聯需要替換的代碼都找不到更別說地址了),從而發生鏈接的報錯
二.類,實例化和this指針
1.類的介紹(class):
其中有兩點需要特別注意:
(a)?類中的成員函數默認為內聯
(b)關于訪問限定符:
如下代碼:public和private是訪問限定符,在public后面的成員函數和成員變量可以直接在類的外部使用,private后面的成員函數和成員變量不能被直接使用。 ? ? ??
? ? ? ? 通常我們把成員函數定義為public,把成員變量定義為private
#include<iostream> using namespace std; class TEST { public://成員函數void test(){return;} private://成員變量int _a;int _b; }; //以上class為類的關鍵字,TEST為類的名字,{}中的為類的主體//但同樣的,C++由于相當于C的pro max版,同時也可以兼容C中的struct結構: typedef struct ListNodeC {struct ListNodeC* next;int val; }LTNode;int main() {return 0; }
關于類域:
#include<iostream> using namespace std;class TEST { public://成員函數聲明int test(int a, int b);private://成員變量int _a;int _b; }; //類定義了一個新的作用域,類的所有成員函數都在類的作用域中。在類體外定義成員時,需要使用類域名::來訪問成員 //如果不指定類域的話,在定義函數時,程序在全局域找不到函數的聲明就會報錯。編譯器不會主動去類域中尋找函數定義 int TEST::test(int a, int b) {return a + b; } int main() {TEST A;int c = 10; int d = 20;cout << A.test(c, d) << endl;return 0; }
2.實例化:
(1)實例化的概念:
(2)實例化的空間分配:
?對象的大小只包含成員變量的大小,成員函數不占內存空間
打個比方,現在實例化出了兩個類,分別為A,B但A和B的成員變量和地址是不同的,但如果訪問這兩個類的成員函數,他們都會鏈接到一個地址(只讀存儲區,靜態存儲),所以說我們sizeof(類對象)只用統計成員變量占用的空間
?成員變量占用的空間也符合內存對齊規則:
關于這個對齊其實有點比較容易遺忘,因此我再簡述一下:
1. 基本概念
?對齊:數據類型的起始地址必須是該類型大小的整數倍
?例如:?int?(4字節)的地址必須是 ?0x4, 0x8, 0xC...?
?未對齊:數據起始地址不滿足對齊規則,可能導致性能下降或硬件錯誤(如 ARM 架構)
2. 內存對齊規則
?a. 自然對齊
?規則:每個數據類型的地址必須是其自身大小的整數倍
?示例:struct AlignedStruct {char a; // 1字節 → 地址 0x0int b; // 4字節 → 地址 0x4(填充3字節)double c; // 8字節 → 地址 0x8(填充7字節) };
b. 結構體對齊
成員順序:成員按聲明順序排列,每個成員按自然對齊對齊舉例:
? struct CompactStruct {int a; // 0x0char b; // 0x4(填充3字節)short c; // 0x8 }; // 總大小:12字節(而非 16字節)?
3.this指針:
? Date類中有Init與Print兩個成員函數,函數體中沒有關于不同對象的區分,那當d1調?Init和 Print函數時,該函數是如何知道應該訪問的是d1對象還是d2對象呢?那么這?就要看到C++給了 ?個隱含的this指針解決這?的問題
? 編譯器編譯后,類的成員函數默認都會在形參第?個位置,增加?個當前類類型的指針,叫做this 指針。?如Date類的Init的真實原型為, void Init(Date* const this, int year, int month, int day)
? 類的成員函數中訪問成員變量,本質都是通過this指針訪問的,如Init函數中給_year賦值, this- >_year = year;
? C++規定不能在實參和形參的位置顯示的寫this指針(編譯時編譯器會處理),但是可以在函數體內顯示使?this指針
?另外需要注意一點,this指針其實存放在棧區,而不是對象里面
#include<iostream> using namespace std; class Date { public:// void Init(Date* const this, int year, int month, int day)void Init(int year, int month, int day){// this->_year = year;_year = year;this->_month = month;this->_day = day;}void Print(){cout << _year << "/" << _month << "/" << _day << endl;} private:// 這?只是聲明,沒有開空間 int _year;int _month;int _day; };int main() {Date A;return 0; }//成員函數在傳參時都有一個類的指針類型的this指針,這個this指針編譯器不會顯示出來,但實際上他是存在的,看上邊這串代碼,如果再函數調用賦值的時候,可以手動把this指針加上去,這樣其實并不會報錯。這就說明這個this指針是真實存在的
4.關于空指針訪問成員變量的注意點:
先看一下下面這兩個代碼:
? ? ? ?這兩串代碼運行的結果并不相同,已知第一個是正常運行,第二個是運行崩潰,首先我們應該知道不管是C語言中還是C++中,解引用空指針并不會編譯報錯,只會運行崩潰
其次再來分析問什么第二個是運行崩潰
? ? ? ? 首先成員函數不會占用物理內存,只有成員變量會,實例出nullptr說明沒開空間,但仔細看第一個程序是不需要訪問成員變量的,所以不開空間也沒有報錯,而第二個程序訪問了開空間的成員變量:_a,所以運行崩潰了
三.類的默認成員函數
默認成員函數就是??沒有顯式實現,編譯器會?動?成的成員函數稱為默認成員函數:
接下來我會對其中的幾個做出詳細介紹:
1.構造函數:
? ? ???構造函數也是一種成員函數,但他和我們寫的普通構造函數不同的是,他是在我們實例化類的對象是默認調用的,也就是說,實例化對象是他自己會去主動調用這個構造函數,其本質是要替代我們以前Stack和Date類中寫的Init函數的功能,構造函數?動調?的 特點就完美的替代的了Init
接下來說說它的基本特點:
函數名和類名相同;
沒有返回值:
#include<iostream> using namespace std; class DATE { public:DATE(int year = 2000, int mouth = 11, int day = 1){_year = year;_mouth = mouth;_day = day;} private://成員函數//private成員函數不能直接訪問,可以通過成員函數訪問int _year;int _mouth;int _day; }; int main() {DATE d1;return 0; }//上面這串代碼中定義了一個日期類,并實例化出一個對象d1,調試可以看到,實例化d1自動調用了DATE這個構造函數,給d1的三個成員變量進行了賦值//構造函數也有很多種,第一種無參構造函數。第二種是全缺省構造函數,第三種就是不寫構造時編譯器默認的構造函數(接下來我會具體說說這三種函數),如果我們在實例化的時候只寫這個對象就像上面這串代碼這樣:DATE d1; 這樣調用的構造函數叫默認構造
? ? ? ?
//無參構造函數 DATE() {_year = 1;_mouth = 1;_day = 1; }//全缺省構造函數 DATE(int year = 2000, int mouth = 11, int day = 1) {_year = year;_mouth = mouth;_day = day; }// 帶參構造函數 Date(int year, int month, int day) {_year = year;_month = month;_day = day; }
? ? ? ?除了以上幾點還有一點需要額外注意:如果類中沒有顯式定義構造函數,則C++編譯器會?動?成?個?參的默認構造函數,?旦用戶顯式定義編譯器將不再?成,也就是說我們不寫,編譯器默認?成的構造,對內置類型成員變量的初始化沒有要求,也就是說是是否初始化是不確定的,看編譯器。對于?定義類型成員變量,要求調?這個成員變量的默認構造函數初始化,? 如果這個成員變量,沒有默認構造函數,那么就會報錯,我們要初始化這個成員變量,需要?初始化列表才能解決,初始化列表的問題,本文先放一下,下一篇文章再作詳細介紹
? ? ? ?讀到這里,會發現一個問題就是既然系統會自動生成默認構造函數,那為什么我們還需要自己去寫構造函數?舉個例子:
當類需要動態分配內存(如 new ?或 malloc )時,默認構造函數無法自動釋放資源,必須手動管理:
class Buffer { public:int* data; // 動態內存// 自定義構造函數:初始化 dataBuffer(int size) : data(new int[size]) {std::cout << "Buffer initialized with size " << size << std::endl;}// 析構函數:釋放資源~Buffer() {delete[] data;std::cout << "Buffer destroyed" << std::endl;} };int main() {Buffer buf(1024); // 調用自定義構造函數return 0; }
默認構造函數不會初始化 data ,導致未定義行為(如懸空指針),自定義構造函數確保 data 正確分配內存
2.析構函數:
? ? ? ?析構函數可以在類對象銷毀時自動調用,釋放我們的內存空間。就好比之前實現的棧這個數據結構,我們需要把我們malloc出來的空間都free掉,那么這個時候如果是使用c++里面的類來完成的話,在我們的棧銷毀時(該對象生命周期結束時)就可以自動調用析構函數,釋放內存
~Stack() {cout << "~Stack()" << endl;free(_a);_a = nullptr;_top = _capacity = 0; }
析構的特點也很明顯:
1.?析構函數名是在類名前加上字符~
2.不需要寫返回值
3.和構造函數一樣,我們不寫編譯器?動?成的析構函數對內置類型成員不做處理,?定類型成員會調?他的析構函數
4.一個類只有一個析構且當類成員不需要釋放空間時,不需要自己寫析構函數
#include<iostream> using namespace std; typedef int STDataType; class Stack { public:Stack(int n = 4){_a = (STDataType*)malloc(sizeof(STDataType) * n);if (nullptr == _a){perror("malloc申請空間失敗");return;}_capacity = n;_top = 0;}~Stack(){cout << "~Stack()" << endl;free(_a);_a = nullptr;_top = _capacity = 0;} private:STDataType* _a;size_t _capacity;size_t _top; };
3.拷貝構造函數:
如果?個構造函數的第?個參數是??類類型的引?,且任何額外的參數都有默認值,則此構造函數 也叫做拷貝構造函數,也就是說拷貝構造是?個特殊的構造函數
#include<iostream> using namespace std;class DATE { public:DATE(int year, int mouth, int day){_year = year;_mouth = mouth;_day = day;}void Print(){cout << _year << "年" << _mouth << "月" << _day << "日" << endl;} private://成員函數//private成員函數不能直接訪問,可以通過成員函數訪問int _year;int _mouth;int _day; }; int main() {DATE d1(10,10,10);DATE d2(d1);//調用拷貝構造d1.Print();d2.Print();return 0; } //注意,第一個參數必須是引用。否則編譯器會報錯。為什么會報錯呢?理解一下,如果說我們傳入的第一個參數沒有引用,那么這個形參是得拷貝一份我們的實參,怎么拷貝呢?他還是得調用我們的拷貝構造函數去拷貝,那這就形成了閉環,而這樣無限拷貝下去編譯器是不允許的//對于沒有主動寫拷貝構造的類,編譯器也會默認生成一個拷貝構造,對于內置類型進行淺拷貝,也就是只拷貝值,對于自定義類型成員會調用他的拷貝構造。
但還有幾點需要注意:
1.像Date這樣的類成員變量全是內置類型且沒有指向什么資源,編譯器?動?成的拷?構造就可以完 成需要的拷?,所以不需要我們顯?實現拷?構造,但像Stack這樣的類,雖然也都是內置類型,但 是_a指向了資源,編譯器?動?成的拷?構造完成的值拷?/淺拷?不符合我們的需求,所以需要我們??實現深拷?(對指向的資源也進?拷?),像MyQueue這樣的類型內部主要是?定義類型 Stack成員,編譯器?動?成的拷?構造會調?Stack的拷?構造,也不需要我們顯?實現 MyQueue的拷?構造(前提是Stack這個類有析構)
2.傳值返回會產??個臨時對象調?拷?構造,傳值引?返回,返回的是返回對象的別名(引?),沒有產?拷?。但是如果返回對象是?個當前函數局部域的局部對象,函數結束就銷毀了,那么使? 引?返回是有問題的,這時的引?相當于?個野引?,類似?個野指針?樣。傳引?返回可以減少拷?,但是?定要確保返回對象,在當前函數結束后還在,才能?引?返回
4.運算符重載:
運算符重載簡而言之就是賦予我們常見的運算符以新的定義與使用場景,比如+號原來只可以用于數字之間的運算,但經過運算符重載之后,使其可以進行日期之間的計算,諸如此類:
bool operator==(DATE x) {return _year == x._year && _mouth == x._mouth && _day == x._day; }
以下是幾個注意點:
1.不能對c++沒有的符號進行重載
2、以下五個運算符不能進行重載:
.* ? ? ?:: ? ? ?sizeof ? ? ? ? : ? ? ? .
3.運算符重載的參數列表至少要含有一個自定義類型,不能通過運算符重載改變內置類型對象的含義,如: int operator+(int x, int y)
4.重載++運算符時,有前置++和后置++,運算符重載函數名都是operator++,無法很好的區分。 C++規定,后置++重載時,增加?個int形參,跟前置++構成函數重載,方便區分
四.日期類實現
//Date.h
#pragma once
#pragma once
#include<iostream>
#include<assert.h>
using namespace std;
class DATE
{
public:DATE(int year = 2000, int mouth = 11, int day = 1){_year = year;_mouth = mouth;_day = day;}//短小多次調用函數使用inline//clase默認inlineint GetMouthDay(int year, int mouth){assert(mouth > 0 && mouth < 13);static int mouthDayArray[13] = { -1,31,28,31,30,31,30,31,31,30,31,30,31 };//多次訪問直接定義靜態數組if (mouth == 2 && ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0))//讓容易不通過的條件放前面{return 29;}else return mouthDayArray[mouth];}DATE& operator+=(int day);//聲明運算符重載DATE& operator+(int day);void Print(){cout << _year << "年" << _mouth << "月" << _day << "日" << endl;}//日期比較bool operator>(DATE x){if (_year > x._year) return true;else if (_year < x._year) return false;if (_mouth > x._mouth) return true;else if (_mouth < x._mouth) return false;if (_day > x._day) return true;else return false;}bool operator==(DATE x){return _year == x._year && _mouth == x._mouth && _day == x._day;}bool operator < (DATE x){return !(*this > x) && !(*this == x);}bool operator!=(DATE& d2){return !(*this == d2);}DATE operator++(int){//后置加加返回原值//注意這個臨時變量出了這個函數就銷毀了所以不能引用返回DATE tmp(*this);_day++;if (_day > GetMouthDay(_year, _mouth)){_day = 1; _mouth++;}if (_mouth > 12){_year++;_mouth = 1;}return tmp;}//兩日期相減int operator-(DATE& d1);
private://成員函數//private成員函數不能直接訪問,可以通過成員函數訪問int _year;int _mouth;int _day;
};
//text.cpp#include"DATE.h"DATE& DATE::operator+=(int day)//表明所屬類
{//由于更改了自身所以重載的是+=//引用返回可以避免拷貝,節省開銷//不能返回空值,不然無法解決這種問題(a+=10)+=10//對于這種情況如果傳dATE返回的話也無法改變原值,不符合預期。_day += day;while (_day > GetMouthDay(_year, _mouth)){_day -= GetMouthDay(_year, _mouth);++_mouth;if (_mouth == 13){_year++;_mouth = 1;}}return *this;
}
DATE& DATE::operator+(int day)
{//DATE tmp(*this);//拷貝//默認構造函數DATE tmp = *this;///同樣也是調用默認構造函數tmp += day;return tmp;
}
int DATE::operator-(DATE& d1)
{int cnt = 0;int flag = 1;DATE max = *this;DATE min = d1;if (max < min){flag = -1;max = d1;min = *this;}while (max != min){cnt++;min++;}return cnt * flag;
}
以上就是關于日期類相關的函數代碼了
歐克,時間也不晚了,就到這里吧
全文終