目錄
一、類的定義
二、實例化
三、this指針
四、類的默認成員函數
五、構造函數
六、析構函數
七、拷貝構造函數
八、賦值運算符重載
九、取地址運算符重載
十、初始化列表
十一、類型轉換
十二、static成員
十三、友元
十四、內部類
十五、匿名對象
十六、對象拷貝時的編譯器優化
一、類的定義
class為定義類的關鍵字,使用時的格式就像下面一樣,Month為類的名字,{}中為類的主體,注意類定義結束時後面分號不能省略。類體中內容稱為類的成員:類中的變量稱為類的屬性或成員變量; 類中的函數稱為類的方法或者成員函數。
在C++struct也可以兼容,class和struct功能有些類似但使用方法上大有不同。
class Month
{
int a;
char c;
public:
void Printf()
{
cout << a << endl;
}
};
int main()
{
Month a;
a.Printf();
return 0;
}
其中我們用到的public是訪問限定符,除了public之外還有為private和protected,這是C++一種類似訪問權限的方法,通過訪問權限選擇性的將其接口提供給外部的用户使用。
在現階段我們只能知道的,public修飾的成員在類外可以直接被訪問;protected和private修飾的成員在類外不能直接被訪問,protected和private是⼀樣的,以後學習才能體現出他們的區別。
訪問權限作用域從該訪問限定符出現的位置開始直到下一個訪問限定符出現時為止,如果後面沒有
訪問限定符,作用域就到 }即類結束。class定義成員沒有被訪問限定符修飾時默認為private,struct默認為public。
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
// 為了區分成員變量,一般習慣上成員變量前加上 _
int _year;
int _month;
int _day;
};
int main()
{
Date d;
d.Init(2024, 3, 31);
return 0;
}
類定義了⼀個新的作用域,類的所有成員都在類的作用域中,在類體外定義成員時,需要使用:: 作
用域操作符指明成員屬於哪個類域。這是C++的查找規則有關,如果不用::表明我們定義的函數是那個域,那這個函數就成了全局函數,不能使用那個域裏面protected和private下的變量。如下
class Date
{
public:
void Init(int year, int month, int day);
private:
// 為了區分成員變量,一般習慣上成員變量前加上 _
int _year;
int _month;
int _day;
};
//沒顯示函數是Data域
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
//有顯示
void Date::Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
int main()
{
Date d;
d.Init(2024, 3, 31);
return 0;
}
二、實例化
用類類型在物理內存中創建對象的過程,稱為類實例化出對象。類是對象的一種抽象化的描述,只是説明了有那些實例對象但這些對象還沒有被創建,他就像是房子的設計圖紙,房子還沒有被創建出來。實例化出的對象才可以分配物理內存存儲數據。
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
// 這裏只是聲明,沒有開空間
int _year;
int _month;
int _day;
};
int main()
{
// Date類實例化出對象d1和d2
Date d1;
Date d2;
d1.Init(2025, 11, 18);
d1.Print();
d2.Init(2025, 11, 19);
d2.Print();
return 0;
}
類實例化出的每個對象,都有獨立的數據空間,但函數被編譯後是一段指令,對象中沒辦法存儲,這些指令存儲在⼀個單獨的區域(代碼段),如果需要儲存也只能儲存它的指針,而且它並不是每次實例化成d1或d2都被存儲一次,我們想一下,如果每次實例化都儲存一次,那我們實例化1000次就需要儲存1000次,這樣很浪費空間。其實函數指針是不需要存儲的,函數指針是一個地址,調用函數被編譯成彙編指令[call 地址], 其實編譯器在編譯鏈接時,就要找到函數的地址,不是在運行時找,只有動態多態是在運行時找,就需要存儲函數地址。
C++規定類實例化的對象也要符合內存對齊的規則。對齊的規則我們在C語言中説過,所以這裏簡單提一下。
1. 基本數據類型的對齊值,每個基本類型有默認的自身對齊值(通常等於其字節大小)
2. 結構體的內存對齊規則,結構體的對齊需滿足3條核心規則:
1. 成員對齊:結構體每個成員的起始地址,必須是其“自身對齊值”的整數倍;
2. 整體對齊:結構體的總大小,必須是其“最大成員對齊值”的整數倍;
3. 編譯器對齊單位:若編譯器指定了對齊單位(如#pragma pack(n)),則對齊值取“自身對齊值”與n的較小值。
如果實例化對象沒有成員變量也會佔一個字節,因為如果一個字節都不給,怎麼表示對象存在過呢!
class A
{
public:
void Print()
{
cout << _ch << endl;
}
private:
char _ch;
int _i;
};
class B
{
public:
void Print()
{
//...
}
};
class C
{};
int main()
{
A a;
B b;
C c;
cout << sizeof(a) << endl;
cout << sizeof(b) << endl;
cout << sizeof(c) << endl;
return 0;
}
三、this指針
我們在一個類中調用函數時,函數體中沒有關於不同對象的區分,那該如何知道函數訪問的是那個對象呢。C++有一個隱含的this指針解決這裏的問題。編譯器編譯後,類的成員函數默認都會在形參第一個位置,增加一個當前類類型的指針,叫做this指針。所以我們看下面的例子,我們寫這個函數是這樣的,但本質上它是那樣的。
但是不能在實參和形參的位置顯示的寫this指針(編譯時編譯器會處理),但是可以在函數體內顯示使用this指針。
// void Init(Date* const this, int year, int month, int day)
/*void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}*/
//可以這樣寫
void Init(int year, int month, int day)
{
this->_year = year;
this->_month = month;
this->_day = day;
}
四、類的默認成員函數
類是默認成員函數是指我們沒有主動顯示實現但編譯器會自己主動生成的成員函數。一個類,我 們不寫的情況下編譯器會默認生成成以下6個默認成員函數,這裏我們主要了解前四個。有人可能會疑惑為什麼我們要主動了解這些函數,這難道不是編譯器會幫我們解決的嗎?我們是要搞明白編譯器生成了什麼樣的成員函數幫我們解決了問題,如果有編譯器生成的函數無法解決的問題我們又該如何自己去實現。
五、構造函數
構造函數是特別的成員函數,但我們要注意構造函數的本質並不是去開闢空間。雖然它可以做到,但它的本質是一個類似於初始化的過程。就像下面我們的類Data中實例化出對象但沒有開闢空間。但是對於Stack它就需要開闢空間。所以説我們可以把它理解為以前寫Satck時Init的作用。
#include
using namespace std;
class Data
{
public:
//無參
Data( )
{
_year = 1;
_month = 1;
_day = 1;
}
//帶參
Data (int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
//全缺省
Data(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print( )
{
cout << _year << '/' << _month << '/' << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Data d1;
Data d2(2025, 11, 19);
Data d3();
d1.Print();
d2.Print();
d3.Print();
}
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;
}
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
構造函數的特點是函數名與類名相同。無返回值。 (返回值啥都不需要給,也不需要寫void,不要糾結,C++規定如此) 。對象實例化時系統會自動調用對應的構造函數。構造函數可以重載。如果類中沒有顯式定義構造函數,則C++編譯器會自動生成一個無參的默認構造函數,一旦用户顯式定義編譯器將不再生成。
這三個函數有且只有一個存在,不能同時存在。無參構造函數和全缺省構造函數雖然構成函數重載,但是調用時會存在歧義。要注意很多同學會認為默認構造函數是編譯器默認生成那個叫默認構造,實際上無參構造函數、全缺省構造函數也是默認構造,總結一下就是不傳實參就可以調用的構造就叫默認構造。
class Data
{
public:
//無參
Data( )
{
_year = 1;
_month = 1;
_day = 1;
}
//帶參
Data (int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
//全缺省
Data(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print( )
{
cout << _year << '/' << _month << '/' << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
我們要注意如果使用無參構造函數時不要在對象後面跟括號,否則編譯器不知道你是想實例化對象還是函數聲明。
int main()
{
Data d1;
Data d2(2025, 11, 19);
Data d3();
d1.Print();
d2.Print();
//d3.Print();
還有編譯器默認生成的構造,對內置類型(int ,double,char)成員變量的初始化沒有要求,也就是説是是否初始化是不確定的,看編譯器。對於自定義類型(class,struct)成員變量,要求調用這個成員變量的默認構造函數初始化。如果這個成員變量,沒有默認構造函數,那麼就會報錯,我們要初始化這個成員變量,需要用初始化列表才能解決。
編譯器默認生成MyQueue的構造函數調用了Stack的構造,完成了初始化
六、析構函數
析構函數與構造函數功能相反,析構函數也不是銷燬的意思,因為局部對象是存在棧幀的,函數結束的時候就自己釋放了所以不用我們操心。析構函數更像是避免資源浪費,像我們在Stack裏的的Destroy功能,但是我們之前寫的Data就不需要析構函數,因為它沒有資源需要釋放,就差不多是沒有申請過空間,不需要把指針置空等等。
析構函數的特點:
1. 析構函數名是在類名前加上字符 ~。
2. 無參數無返回值。 (這裏跟構造類似,也不需要加void)
3. 一個類只能有一個析構函數。若未顯式定義,系統會自動生成默認的析構函數。
4. 對象生命週期結束時,系統會自動調用析構函數。
5. 跟構造函數類似,我們不寫編譯器自動生成的析構函數對內置類型成員不做處理,自定類型成員會調用他的析構函數。
6. 還需要注意的是我們顯示寫析構函數,對於自定義類型成員也會調用他的析構,也就是説自定義類型成員無論什麼情況都會自動調用析構函數。
自動生成默認的析構函數對資源的釋放不徹底。如
果默認生成的析構就可以用,也就不需要顯示寫析構,如MyQueue(因為它是底層還是Stack,所以我們寫了Stack的析構函數,MyQueue的析構函數就不寫默認的析構函數也是會調用Stack的);但是有資源申請時,一定要自己寫析構,否則會造成資源泄漏,如Stack。
注意:一個局部域的多個對象,C++規定後定義的先析構。
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;
};
class MyQueue
{
public:
private:
Stack pushst;
Stack popst;
};
int main()
{
Stack st;
MyQueue mq;
return 0;
}
七、拷貝構造函數
拷貝函數是一個特殊的構造函數。因為C++規定在傳值傳參時要調用拷貝構造,所以我們實現拷貝構造時要傳引用傳值。如果直接傳值傳參會形成新的拷貝構造會無限遞歸下去。如下
class Data
{
public:
void Print()
{
cout << _year << '/' << _month << '/' << _day << endl;
}
Data(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
Data(Data& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Data d1(2025, 11, 20);
Data d2(d1);
d2.Print();
}
拷貝構造函數的第一個參數必須是類類型對象的引用,可以有多個參數但後面的參數必須有缺省值。C++規定自定義類型對象進行拷貝行為必須調用拷貝構造,所以這裏自定義類型傳值傳參和傳值返回都會調用拷貝構造完成。而編譯器自己默認生成的拷貝構造函數只是一種“淺拷貝/值拷貝”並不適用很多情況,比如我們用一個值拷貝拷貝Stack類型的對象,因為它其中一個成員變量是指針,如果值拷貝會導致兩個對象的地址是指向同一個的,後續使用的過程中無論是添加數據還是解析等都會有很大問題。
MyQueue這樣的類型內部主要是自定義類型Stack成員,編譯器自動生成的拷貝構造會調用Stack的拷貝構造,也不需要我們顯示實現MyQueue的拷貝構造。
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(Stack& st)
{
_a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);
if (nullptr == _a)
{
perror("malloc申請空間失敗!!!");
return;
}
memcpy(_a, st._a, sizeof(STDataType) * st._top);
_top = st._top;
_capacity = st._capacity;
}
void Push(STDataType x)
{
if (_top == _capacity)
{
int newcapacity = _capacity * 2;
STDataType* tmp = (STDataType*)realloc(_a, newcapacity *
sizeof(STDataType));
if (tmp == NULL)
{
perror("realloc fail");
return;
}
_a = tmp;
_capacity = newcapacity;
}
_a[_top++] = x;
}
~Stack()
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
int main()
{
Stack st1;
st1.Push(1);
st1.Push(2);
//和Stack st2(st1)一樣
Stack st2 = st1;
//Data那個也可以寫成Data d2 = d1
return 0;
}
補充:
有人可能對這幾種傳值的方式不太理解,這裏給大家總結一下
• 傳值返回:返回變量的副本,函數結束後原局部變量銷燬,副本獨立存在,開銷=變量大小(適合基礎類型)。
• 指針返回:返回變量的內存地址,需確保指向的變量(如全局/靜態變量、堆內存)生命週期長於函數,避免返回局部變量指針(懸垂指針)。
• 引用返回:返回變量的別名(無副本開銷),本質是指針優化,必須指向有效變量(不能返回局部變量引用),可直接修改原變量。
// 傳值返回
int add(int a, int b) {
int res = a + b;
return res; // 返回res的副本
}
// 指針返回(靜態變量,生命週期長)
int* getStaticNum() {
static int num = 10;
return # // 安全,靜態變量在程序結束前不銷燬
}
// 引用返回(返回全局變量別名)
int g_num = 20;
int& getGlobalNum() {
return g_num; // 安全,全局變量生命週期貫穿程序
}
八、賦值運算符重載
C++語言允許我們通過運算符重載的形式指定新的含義。C++規定類類型對象使用運算符時,必須轉換成調用對應運算符重載,若沒有對應的運算符重載,則會編譯報錯。運算符重載是具有特殊名字的函數,他的名字是由operator和後面要定義的運算符共同構成。和其他函數一樣,它也具有其返回類型和參數列表以及函數體。
函數在類外面:重載運算符函數的參數個數和該運算符作用的運算對象數量⼀樣多。一元運算符有一個參數,二元運算符有兩個參數,二元運算符的左側運算對象傳給第一個參數,右側運算對象傳給第二個參數。(一元就類似於++,--,+=,-=。二元就類似於-,+,==,<,>)
我們要注意當重載運算符函數在類外面時在C++中是不允許重載內置類型(int,double)的運算符的。
函數在類裏面:如果一個重載運算符函數是成員函數,則它的第一個運算對象默認傳給隱式的this指針,因此運算符重載作為成員函數時,參數比運算對象少⼀個。運算符重載以後,其優先級和結合性與對應的內置類型運算符保持一致。
#include
using namespace std;
class Data
{
public:
void Print()
{
cout << _year << '/' << _month << '/' << _day << endl;
}
Data(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
Data operator+(const Data& d2)
{
return Data(_year + d2._year,
_month + d2._month,
_day + d2._day);
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Data d1(2025,11,22);
d1.Print();
Data d2(0, 0, 2);
Data d3 = d1 + d2;
d3.Print();
Data d4 = d1.operator+(d3);
d4.Print();
return 0;
}
不能通過連接語法中沒有的符號來創建新的操作符:比如operator@,而且有五個符號不能重載(sizeof .* :: . ?:)。
重載++運算符時,有前置++和後置++,運算符重載函數名都是operator++,無法很好的區分。
C++規定,後置++重載時,增加一個int形參,跟前置++構成函數重載,方便區分。
#include
using namespace std;
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
Date& operator++()
{
cout << "前置" << endl;
_day++;
return *this;
}
Date& operator++(int)
{
cout << "後置" << endl;
Date tmp = *this;
_day++;
return tmp;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2025,11,22);
d1.Print();
Date d2 = d1++;
d2.Print();
d1.Print();
Date d3 = ++d1;
d3.Print();
d1.Print();
return 0;
}
重載<<和>>時,需要重載為全局函數,因為重載為成員函數,this指針默認搶佔了第一個形參位
置,第一個形參位置是左側運算對象,調用時就變成了 對象<<cout,不符合使用習慣和可讀性。
重載為全局函數把ostream/istream放到第一個形參位置就可以了,第二個形參位置當類類型對
象。而且如果我們重載為全局函數就面臨無法訪問類中被限定為私有的成員變量,我們的解決辦法是友元函數(後面會詳細説明)/成員放為公有/重載為成員函數/
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}
賦值運算符重載是一個默認成員函數,用於完成兩個已經存在的對象直接的拷拷貝賦值,它和拷貝構造的區別是拷貝構造把a拷貝給b,b是正在被創建;賦值是把a賦值給b,b是已經被創建好的。
我們來總結一下幾種構造,看一下各自的使用情況
#include
using namespace std;
class Date
{
public:
//構造
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//賦值
Date& operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
// 拷貝構造
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
// 普通自定義構造
Date(const Date* d)
{
_year = d->_year;
_month = d->_month;
_day = d->_day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
//構造
Date d1(2025, 11, 23);
d1.Print();
// 拷貝構造
Date d2(d1);
d2.Print();
// 普通自定義構造
Date d3(&d1);
d3.Print();
//賦值
Date d4 = d1;
d4.Print();
return 0;
}
我們注意到對於賦值運算符重載的返回值我們一般都是引用返回,這樣做也是為了避免發生拷貝,也為了支持連續賦值場景。
而且賦值運算符重載也和前面提到過的Stack的情況一樣,像Date這樣的類成員變量全是內置類型且沒有指向什麼資源,編譯器自動生成的賦值運算符重載就可以完成需要的拷貝,所以不需要我們顯示實現賦值運算符重載。像Stack這樣的類,雖然也都是內置類型,但是_a指向了資源,編譯器自動生成的賦值運算符重載完成的值拷貝/淺拷貝不符合我們的需求,所以需要我們自己實現深拷貝(對指向的資源也進行拷貝)。像MyQueue這樣的類型內部主要是自定義類型Stack成員,編譯器自動生成的賦值運算符重載會調用Stack的賦值運算符重載,也不需要我們顯示實現MyQueue的賦值運算符重載。
我們學習到這個地方後就可以解決日期類的計算問題,大家可以自己嘗試一下,我的源代碼鏈接在這裏,關於這裏還有一些細節,下面我也會為大家解答。https://gitee.com/chen-chen-chen-chen-ch/c-.cpp/commit/44ec2dd5a5697f6b7c34e8192f72f1cd0ffacd49
九、取地址運算符重載
如果大家看到了我的日期類計算代碼就會看到我在很多成員函數的後面都加了const,這裏的const其實是在修飾成員函數,實際修飾該成員函數隱含的this指針,表明在該成員函數中不能對類的任何成員進行修改。比如void Print()函數的隱含的this指針就由Date* const this 變成了const Date* const this.
取地址運算符重載分為普通取地址運算符重載和const取地址運算符重載,一般編譯器自己生成的就夠我們使用了,不用特意去構造。除非有時候我們有需要不想讓別人取到當前類的地址就可以自己實現一個,隨便返回一個地址。
class Date
{
public:
Date * operator&()
{
return this;
return nullptr;
}
const Date * operator&()const
{
return this;
// return nullptr;
}
private:
int _year;
int _month;
int _day;
};
十、初始化列表
初始化列表實際上也是一種構造函數的方法,我們使用構造函數初始化成員變量是在函數體內進行賦值,初始化列表是以一個冒號開始,接着是一個以逗號分隔的數據成員列表,每個"成員變量"後面跟一個放在括號中的初始值或表達式。(初始化列表就是在構造函數的函數體外)無論是否顯示寫初始化列表,每個構造函數都有初始化列表。每個成員變量只能使用一次。
儘量使用初始化列表初始化,因為那些你不在初始化列表初始化的成員也會走初始化列表,如果這 個成員在聲明位置給了缺省值,初始化列表會用這個缺省值初始化。如果你沒有給缺省值,對於沒 有顯示在初始化列表初始化的內置類型成員是否初始化取決於編譯器,C++並沒有規定。對於沒有 顯示在初始化列表初始化的自定義類型成員會調用這個成員類型的默認構造函數,如果沒有默認構 造會編譯錯誤。
還有三個特殊的變量必須放在初始化列表位置進行初始化,否則會編譯報錯。
#include
using namespace std;
class Time
{
public:
Time(int hour)
:_hour(hour)
{
cout << "Time()" << endl;
}
private:
int _hour;
};
class Date
{
public:
Date(int& x, int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
, _t(12)
, _ref(x)
, _n(1)
{
}
void Print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
Time _t;
int& _ref;
const int _n;
};
int main()
{
int i = 0;
Date d1(i);
d1.Print();
return 0;
}
C++11支持在成員變量聲明的位置給缺省值,這個缺省值主要是給沒有顯示在初始化列表初始化的 成員使用的。(注意這裏的缺省值和我們在構造函數中説的那個不能同等理解,這裏的缺省值是隻有初始化列表沒有初始化的時候才會使用)
#include
using namespace std;
class Time
{
public:
Time(int hour)
:_hour(hour)
{
cout << "Time()" << endl;
}
private:
int _hour;
};
class Date
{
public:
Date()
:_month(2)
{
cout << "Date()" << endl;
}
void Print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year = 1;
int _month = 1;
int _day;
Time _t = 1;
const int _n = 1;
int* _ptr = (int*)malloc(12);
};
int main()
{
Date d1;
d1.Print();
return 0;
}
一般按聲明順序初始化。
十一、類型轉換
C++支持內置類型隱式類型轉換為類類型對象,需要有相關內置類型為參數的構造函數。換句話説,C++允許把內置類型(int,double這種基礎數據類型)自動轉化成某個類的對象但前提是這個類要有相應的構造函數來支持這種轉換。然後構造函數前面加explicit就不再支持隱式類型轉換。
#include
using namespace std;
//轉換
class A
{
public:
A(int a)
{
_a = a;
}
void Print()
{
cout << _a << endl;
}
private:
int _a;
};
int main()
{
A aa = 1;
aa.Print();
return 0;
}
//不能轉換
class A
{
public:
explicit A(int a)
{
_a = a;
}
void Print()
{
cout << _a << endl;
}
private:
int _a;
};
int main()
{
A aa = 1;
aa.Print();
return 0;
}
十二、static成員
用static修飾的成員變量,稱之為靜態成員變量,靜態成員變量⼀定要在類外進行初始化。所有類對象所共享,存放在靜態區。
用static修飾的成員函數,稱之為靜態成員函數,靜態成員函數沒有this指針,靜態成員函數中可以訪問其他的靜態成員,但是不能訪問非靜態的,因為沒有this指針。但非靜態成員函數可以訪問靜態成員和非靜態成員。
靜態成員變量不能在聲明位置給缺省值初始化,因為缺省值是個構造函數初始化列表的,靜態成員
變量不屬於某個對象,不走構造函數初始化列表。
class A
{
public:
A()
{
++_scount;
}
A(const A& t)
{
++_scount;
}
~A()
{
--_scount;
}
static int GetACount()
{
return _scount;
}
private:
static int _scount;
};
int A::_scount = 0;
int main()
{
cout << A::GetACount() << endl;
A a1, a2;
A a3(a1);
cout << A::GetACount() << endl;
cout << a1.GetACount() << endl;
return 0;
}
十三、友元
友元提供了⼀種突破類訪問限定符封裝的方式,友元分為:友元函數和友元類,在函數聲明或者類
聲明的前面加friend,並且把友元聲明放到一個類的裏面。代表這個函數或者類可以使用我們這個類裏面的成員函數和成員變量。
友元函數僅僅是一種聲明,他不是類的成員函數,可以在類定義的任何地方聲明,不受類訪問限定符限制。
#include
using namespace std;
class B;
class A
{
// 友元聲明
friend void func(const A& aa, const B& bb);
private:
int _a1 = 1;
int _a2 = 2;
};
class B
{
// 友元聲明
friend void func(const A& aa, const B& bb);
private:
int _b1 = 3;
int _b2 = 4;
};
void func(const A& aa, const B& bb)
{
cout << aa._a1 << endl;
cout << bb._b1 << endl;
}
int main()
{
A aa;
B bb;
func(aa, bb);
return 0;
}
十四、內部類
如果一個類定義在另一個類的內部,這個內部類就叫做內部類。內部類是一個獨立的類,跟定義在
全局相比,他只是受外部類類域限制和訪問限定符限制,所以外部類定義的對象中不包含內部類。我們可以把內部類當作外部類的友元類。
#include
using namespace std;
class A
{
private:
static int _k;
int _h = 1;
public:
class B
{
public:
void foo(const A& a)
{
cout << _k << endl; //OK
cout << a._h << endl; //OK
}
int _b1;
};
};
int A::_k = 1;
int main()
{
cout << sizeof(A) << endl;
A::B b;
A aa;
b.foo(aa);
return 0;
}
十五、匿名對象
用類型(實參) 定義出來的對象叫做匿名對象,相比之前我們定義的類型對象名(實參) 定義出來的叫有名對象,它的生命週期只在當前一行,一般臨時定義一個對象當前用⼀下即可,就可以定義匿名對象。
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
class Solution {
public:
int Sum_Solution(int n) {
//...
return n;
}
};
int main()
{
A aa1;
A();
A(1);
A aa2(2);
Solution().Sum_Solution(10);
return 0;
}
十六、對象拷貝時的編譯器優化
編譯器會為了儘可能提高程序的效率,在不影響正確性的情況下會盡可能減少一些傳參和傳返
回值的過程中可以省略的拷貝。這裏不再過多贅述,我們只要知道在匿名對象傳參,隱式類型轉換傳參,具有局部對象返回,匿名對象返回,對象之間初始化等場景下可以明顯看出優化效果。
class A
{
public:
//構造
A(int a1 = 0, int a2 = 0)
:_a1(a1)
, _a2(a2)
{
cout << "A(int a1 = 0, int a2 = 0)" << endl;
}
//拷貝
A(const A& aa)
:_a1(aa._a1)
{
cout << "A(const A& aa)" << endl;
}
//賦值
A& operator=(const A& aa)
{
cout << "A& operator=(const A& aa)" << endl;
if (this != &aa)
{
_a1 = aa._a1;
}
return *this;
}
//析構
~A()
{
//delete _ptr;
cout << "~A()" << endl;
}
void Print()
{
cout << "A::Print->" << _a1 << endl;
}
A& operator++()
{
_a1 += 100;
return *this;
}
private:
int _a1 = 1;
int _a2 = 1;
};
//int main()
//{
// A aa1 = 1;
// const A& aa2 = 1;
//
// return 0;
//}