title: 類和對象
date: 2019-03-15 14:05:24
tags: Cpp
categories: Cpp
類的引入
萬物皆對象。類是一種用戶自定義的數據類型,包括表示屬性的成員變量和表示行爲的成員函數,類是現實世界對象的抽象,對象是類虛擬世界的實例。
-
C++ 中 struct 中不僅可以有變量,還可以有函數。
-
類定義結束時後面分號。
-
成員函數聲明和定義可以全部放在類體中,需要注意:成員函數如果在類中定義,編譯器可能會將其當成內聯函數處理。
-
一般定義在 .h 文件中,定義放在 .cpp 文件中。
-
面向對象三大特性:封裝、繼承、多態。
訪問限定符及封裝
訪問限定符
C++ 實現封裝的方式:用類將對象的屬性與方法結合在一塊,讓對象更加完善,通過訪問權限選擇性的將其接口提供給外部的用戶使用。
public 公有的、protected 保護的、private 私有的。
-
public 修飾的成員在類外可以直接被訪問。
-
protected 和 private 修飾的成員在類外不能直接被訪問。
-
訪問權限作用域從該訪問限定符出現的位置開始直到下一個訪問限定符出現時爲止。
-
class 的默認訪問權限爲 private ,struct 默認爲 public (因爲struct要兼容C)。
-
訪問限定符只在編譯時有用,當數據映射到內存後,沒有任何訪問限定符上的區別。
封裝
封裝:將數據和操作數據的方法進行有機結合,隱藏對象的屬性和實現細節,僅對外公開接口來和對象進行交互。
封裝本質上是一種管理:我們如何管理兵馬俑呢?比如如果什麼都不管,兵馬俑就被隨意破壞了。那麼我們
首先建了一座房子把兵馬俑給封裝起來。但是我們目的全封裝起來,不讓別人看。所以我們開放了售票通
道,可以買票突破封裝在合理的監管機制下進去參觀。類也是一樣,我們使用類數據和方法都封裝到一下。
不想給別人看到的,我們使用 protected/private 把成員封裝起來。開放一些共有的成員函數對成員合理的訪
問。所以封裝本質是一種管理。
類的實例化
用類類型創建對象的過程,稱爲類的實例化。
類好比是一個房子的設計圖紙,類的實例化就是按照圖紙建造一個房子。一個設計圖可以建好幾套房子。
造對象~~~。
類的對象模型
計算類對象的大小
❓ 類中既可以有成員變量,又可以有成員函數,那麼一個類的對象中包含了什麼?如何計算一個類的大小?
✔️ 只保存成員變量,成員函數存放在公共的代碼段。一個類的大小,實際就是該類中”成員變量”之和,當然也要進行內存對齊,注意空類的大小,空類比較特殊,編譯器給了空類一個字節來唯一標識這個類。
🌰 栗子:
class A1
{
public:
void f1() {}
private:
int _a;
};
// 類中僅有成員函數
class A2
{
public:
void f2() {}
};
// 類中什麼都沒有---空類
class A3
{
};
int main()
{
cout << "sizeof(A1) : " << sizeof(A1) << endl; // 4
cout << "sizeof(A2) : " << sizeof(A2) << endl; // 1
cout << "sizeof(A3) : " << sizeof(A3) << endl; // 1
return 0;
}
內存對齊規則
結構體內存對齊規則:
-
第一個成員在與結構體偏移量爲 0 的地址處。
-
其他成員變量要對齊到某個數字(對齊數)的整數倍的地址處。
-
對齊數 = 編譯器默認的一個對齊數與該成員大小的較小值。
-
VS 中默認的對齊數爲 8,gcc 中的對齊數爲 4 。
-
-
結構體總大小爲:最大對齊數(所有變量類型最大者與默認對齊參數取最小)的整數倍。
-
如果嵌套了結構體的情況,嵌套的結構體對齊到自己的最大對齊數的整數倍處,結構體的整體大小就是所有最大對齊數(含嵌套結構體的對齊數)的整數倍。
結構體的內存對齊規則適用於類。
⚠️ 類中嵌套的類是不佔用空間的,只有當有了類的對象後,纔會佔用空間。
class A1
{
public:
void f1() {}
private:
int _a;
class A2
{
};
}; // sizeof(A1) = 4
❓ 爲什麼要進行內存對齊?
✔️ https://www.cnblogs.com/jijiji/p/4854581.html
❓ 如何讓結構體按照指定的對齊參數進行對齊?
✔️ #pragma pack(4)
❓ 如何知道結構體中某個成員相對於結構體起始位置的偏移量?
✔️ 造個對象,地址相減。
✔️ &(((type*)0)->m)
。
❓ 什麼是大小端?如何測試某臺機器是大端還是小端?有沒有遇到過要考慮大小端的場景?
✔️ 在網絡通信中會涉及到。
this 指針
成員函數最終會被編譯成與對象無關的普通函數。除了成員變量,丟失所有信息。
C++ 編譯器給每個“成員函數“增加了一個隱藏的指針參數,讓該指針指向當前對象(函數運行時調用該函數的對象),在函數體中所有成員變量的操作,都是通過該指針去訪問。只不過所有的操作對用戶是透明的,即用戶不需要來傳遞,編譯器自動完成。
-
this 指針是 const 的,類型爲對象的類型。
-
只能在“成員函數”的內部使用。
-
this 指針本質上其實是一個成員函數的形參,是對象調用成員函數時,將對象地址作爲實參傳遞給 this 形參。所以對象中不存儲 this 指針。
-
this 指針是成員函數第一個隱含的指針形參,一般情況由編譯器通過 ecx 寄存器自動傳遞,不需要用戶傳遞。
6 個默認成員函數
任何一個類在我們不寫的情況下,都會自動生成下面6個默認成員函數。
class Date {};
- 初始化和清理
- 拷貝複製
- 拷貝構造:使用同類對象初始化創建對象
- 賦值重載:把一個對象賦值給另一個對象
- [取地址重載](#取地址及 const 取地址操作符重載)
- 主要是普通對象和 const 對象取地址,這兩個很少會自己實現
構造函數
構造函數是一個特殊的成員函數,名字與類名相同,創建類類型對象時由編譯器自動調用,保證每個數據成員都有一個合適的初始值,並且在對象的生命週期內只調用一次。
-
構造函數可以重載。
-
爲成員變量賦初始值,分配資源(是給對象的成員變量分配資源),設置對象的初始狀態。
-
函數名與類名相同,沒有返回類型。
-
對象創建時自動調用且只調用一次。
- 棧區創建對象:對象定義語句。
- 堆區創建對象:new 操作符。
-
自定義成員類型要調用自己的構造函數,編譯器自動生成的構造函數會自己調用它。
-
對象創建過程:
- 爲整個對象分配內存;
- 構造基類部分(如果存在基類);
- 構造成員變量;
- 執行構造函數代碼。
-
一般訪問屬性爲 public,除非我們不允許外部創建對象。
⚠️
- 構造函數的雖然名稱叫構造,但是需要注意的是構造函數的主要任務並不是給對象開空間創建對象,而是初始化對象,給對象裏的成員變量開空間、賦值。
- 如果通過無參構造函數創建對象時,對象後面不用跟括號,否則就成了函數聲明。如
Date d3();
,聲明瞭d3函數,該函數無參,返回一個日期類型的對象。 - 如果類中沒有顯式定義構造函數,則編譯器會自動生成一個無參的默認構造函數,一旦用戶顯式定義編譯器將不再生成。
- 無參的構造函數和全缺省的構造函數都稱爲默認構造函數,並且默認構造函數只能有一個。
- 無參構造和全缺省構造都稱爲默認構造函數,並且默認只能有一個。
成員變量命名風格
m_name
、_age
,具體看要求~,主要是和參數做區分。
析構函數
與構造函數功能相反,析構函數不是完成對象的銷燬,局部對象銷燬工作是由編譯器完成的。而對象在銷燬時會自動調用析構函數,完成類的一些資源清理工作。
- 無參數無返回值。
- 只有一個析構函數,不能重載。
- 負責對象銷燬時回收對象佔用資源。
- 自定義成員類型要調用自己的析構,編譯器自動生成的析構函數會自己調用它。
- 在析構函數中 delete / free 構造函數中 new / malloc 的東西。
- 在用完對象後 delete 對象,調用析構函數,當對象的生命週期後,自動調用析構函數。
⚠️ 析構函數中,釋放空間前要判斷要釋放的空間是否爲 NULL / nullptr,釋放完後要指向空,避免野指針。
拷貝構造
用已存在的類類型對象創建新對象時由編譯器自動調用。
-
拷貝構造函數是構造函數的一個重載形式。
-
參數只有一個且必須使用引用傳參(並且加 const 修飾),使用傳值方式會引發無窮遞歸調用。
-
如果一個類沒有定義拷貝構造函數,那麼編譯器提供一個默認拷貝構造函數(public)。
-
系統生成默認的拷貝構造函數。 默認的拷貝構造函數對象按內存存儲按字節序完成拷
貝,這種拷貝我們叫做淺拷貝,或者值拷貝。例如:成員變量中有一個
char *
的成員變量,系統生成的默認拷貝構造會拷貝這個指針變量的值(指向的地址)給新的對象,這樣,這兩個對象就指向的同一塊空間,在析構的時候會出現問題。
類型轉換構造
構造函數不僅可以構造與初始化對象,對於單個參數的構造函數,還具有類型轉換的作用。
class Date {
public:
Date(int year):_year(year){}
private:
int _year;
int _month:
int _day;
};
void TestDate()
{
Date d1(2018);
// 用一個整形變量給日期類型對象賦值
// 實際編譯器背後會用2019構造一個無名對象,最後用無名對象給d1對象進行賦值
d1 = 2019;
}
上述代碼可讀性不是很好,用 explicit 修飾構造函數,將會禁止單參構造函數的隱式轉換。
explicit Date(int year):_year(year){}
運算符重載
- 不能通過連接其他符號來創建新的操作符:比如 operator@。
- 重載操作符必須有一個類類型或者枚舉類型的操作數,
int operator+(int a, int b) {}
,就是不可以的。 - 作爲類成員的重載函數時,其形參看起來比操作數數目少 1 成員函數的操作符有一個默認的形參 this,限定爲第一個形參,
bool operator==(Date* this, const Date& d2)
,調用cout<<(d1 == d2)<<endl;
相當於d1.operator==(d2)
。 - 一個類如果沒有顯式定義賦值運算符重載,編譯器也會生成一個,完成對象按字節序的值拷貝。
#
、->*
、.*
、::
、sizeof
、?:
、.
注意以上5個運算符不能重載。這個經常在筆試選擇題中出現。
⚠️ .
、.*
運算符不能重載是爲了保證訪問成員的功能不能被改變,域運算符合sizeof
運算符的運算對象是類型而不是變量或一般表達式,不具備重載的特徵。
❓ 運算符重載成全局的就需要成員變量是共有的,封裝性如何保證?
✔️ 友元或者直接重載成成員函數。
⚠️
⚠️ 重載前置++(Complex& operator++()
)和後置++(Complex operator++(int)
,啞元佔位,用來區分)這種的運算符應注意!
const
const 修飾類的成員變量
必須使用初始化參數列表初始化,初始化之後不能修改。
class A
{
A():m_a(10){} //常成員變量必須用這種方式賦初始值
~A(){}
private:
const int m_a;
};
const 修飾類的成員函數
將 const 修飾的類成員函數稱之爲 const 成員函數,const 修飾類成員函數,實際修飾該成員函數隱含的 this 指針,表明在該成員函數中不能對類的任何成員進行修改,也叫常函數。
⚠️ 常函數內部無法修改成員變量的值,除非 mutable 修飾該成員變量。
void Display() const{...}
<=> void Display(const Date* this){...}
const 對象
-
這個對象裏的成員變量是無法修改的。
-
被const修飾的對象,對象指針或對象引用,統稱爲常對象。
-
常對象只能調用常函數,非常對象即可調用常函數,也可調用非常函數,優先調用非常版本。
-
成員函數常版本和非常版本可以構成重載。
⚠️ 區別的標記並不是誰離得近,而是他在*
號的前面還是後面。
const Person* p = &p1;//對象是const
Person const* p = &p1;//對象是const
Person *const p = &p1;//指針是const
取地址及 const 取地址操作符重載
這兩個默認成員函數一般不用重新定義,編譯器默認會生成。
class Date
{
public :
Date* operator&()
{
return this ;
}
const Date* operator&()const
{
return this ;
}
private :
int _year ; // 年
int _month ; // 月
int _day ; // 日
};
⚠️ 這兩個運算符一般不需要重載,使用編譯器生成的默認取地址的重載即可,只有特殊情況,才需要重載,比
如想讓別人獲取到指定的內容!
再談構造函數
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
雖然上述構造函數調用之後,對象中已經有了一個初始值,但是不能將其稱作爲類對象成員的初始化,構造
函數體中的語句只能將其稱作爲賦初值,而不能稱作初始化。因爲初始化只能初始化一次,而構造函數體內
可以多次賦值。
class Person { ... };
class Currency { ... };
class SavingsAccount
{
public:
SavingsAccount(const char* name,const char* address,int cents); // 這裏是不合適的
// Person and Currency都有自己的構造函數,應該由自己來初始化自己,而不是SavingsAccount來構造
~SavingAccount();
void print();
private:
Person m_saver;
Currency m_balance;
};
初始化列表
初始化列表:以一個冒號開始,接着是一個以逗號分隔的數據成員列表,每個"成員變量"後面跟一個放在括 號中的初始值或表達式。
// So:
SavingsAccount::SavingsAccount(const char * name,const char* address,int cents):m_saver(name,address),m_balance(0,cents){}
void SavingsAccount::print()
{
m_saver.print(); //print() in saver
m_balance.print(); //print() in balance
}
⚠️
- 每個成員變量在初始化列表中只能出現一次(初始化只能初始化一次)。
- 類中包含以下成員,必須放在初始化列表位置進行初始化:
- 引用成員變量(引用在定義的是時候必須初始化)
- const 成員變量(和上面一個道理,const在定義的時候必須初始化一個值)
- 類類型成員(該類沒有默認構造函數,如果有,要通過自己的構造函數來初始化)
- 成員變量在類中聲明次序就是其在初始化列表中的初始化順序,與其在初始化列表中的先後次序無關。
C++ 11成員初始化新方式
非靜態成員變量,可以在成員聲明時,直接初始化。
相當於給無參構造函數一個缺省參數。
static 成員
聲明爲 static 的類成員稱爲類的靜態成員,用 static 修飾的成員變量,稱之爲靜態成員變量;用 static 修飾的成員函數,稱之爲靜態成員函數。
面試題:實現一個類,計算中程序中創建出了多少個類對象。
class A
{
public:
A() {++_scount;}
A(const A& t) {++_scount;}
static int GetACount() { return _scount;}
private:
static int _scount;
};
int Test::_count = 0;
void TestA()
{
cout<<A::GetACount()<<endl;
A a1, a2;
A a3(a1);
cout<<A::GetACount()<<endl;
}
⚠️
- 靜態的成員變量一定要在類外進行初始化。
- 靜態成員爲所有類對象所共享,不屬於某個具體的實例。
- 靜態成員變量必須在類外定義,定義時不添加static關鍵字。
- 類靜態成員即可用
類名::靜態成員
或者對象.靜態成員
來訪問。 - 靜態成員函數沒有隱藏的 this 指針,不能訪問任何非靜態成員。
- 靜態成員和類的普通成員一樣,也有public、protected、private3種訪問級別,也可以具有返回值,const修飾符等參數。
❓ 靜態成員函數可以調用非靜態成員函數嗎?
✔️ 不可以,非靜態的成員函數默認有一個參數(this指針),但是靜態成員函數沒有這個 this 指針傳給他,所以不可以調用。
❓ 非靜態成員函數可以調用類的靜態成員函數嗎?
✔️ 可以。
友元
友元提供了一種突破封裝的方式,有時提供了便利。但是友元會增加耦合度,破壞了封裝,所以友元不宜多用。
友元函數
友元函數可以直接訪問類的私有成員,它是定義在類外部的普通函數,不屬於任何類,但需要在類的內部聲明,聲明時需要加 friend 關鍵字。
- 友元函數可訪問類的私有成員,但不是類的成員函數
- 友元函數不能用 const 修飾
- 友元函數可以在類定義的任何地方聲明,不受類訪問限定符限制
- 一個函數可以是多個類的友元函數
- 友元函數的調用與普通函數的調用和原理相同
問題:現在我們嘗試去重載operator<<,然後發現我們沒辦法將operator<<重載成成員函數。因爲 cout 的輸出流對象和隱含的this指針在搶佔第一個參數的位置。this 指針默認是第一個參數也就是左操作數了。但是實際使用中cout需要是第一個形參對象,才能正常使用。所以我們要將operator<<重載成全局函數。但是這樣的話,又會導致類外沒辦法訪問成員,那麼這裏就需要友元來解決。operator>>同理。
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day) {}
ostream& operator<<(ostream& _cout)
{
_cout<<d._year<<"-"<<d._month<<"-"<<d._day;
return _cout;
}
private:
int _year;
int _month;
int _day
};
int main() {
Date d(2017, 12, 24);
d<<cout; // 這種使用方式很彆扭,不符合使用習慣
return 0;
}
使用友元修改後:
class Date
{
friend ostream& operator<<(ostream& _cout, const Date& d);
friend istream& operator>>(istream& _cin, const Date& d);
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day
};
ostream& operator<<(ostream& _cout, const Date& d)
{
_cout<<d._year<<"-"<<d._month<<"-"<<d._day;
return _cout;
}
istream& operator>>(istream& _cin, const Date& d)
{
_cin>>d._year;
_cin>>d._month;
_cin>>d._day;
return _cin;
}
int main() {
Date d;
cin>>d;
cout<<d<<endl;
return 0;
}
友元類
友元類的所有成員函數都可以是另一個類的友元函數,都可以訪問另一個類中的非公有成員。
-
友元關係是單向的,不具有交換性。
我把你當朋友,帶你去我家吃好吃的,你卻不把我當朋友,不帶我去你家。
-
友元關係不能傳遞。
如果 B 是 A 的友元,C 是 B 的友元,則不能說明 C 時 A 的友元。
內部類
如果一個類定義在另一個類的內部,這個內部類就叫做內部類。注意此時這個內部類是一個獨立的類,它不屬於外部類,更不能通過外部類的對象去調用內部類。外部類對內部類沒有任何優越的訪問權限。
- 內部類可以定義在外部類的 public、protected、private 都是可以的。
- 內部類可以直接訪問外部類中的 static、枚舉成員,不需要外部類的對象/類名。
- sizeof(外部類)=外部類,和內部類沒有任何關係。
⚠️ 內部類就是外部類的友元類。注意友元類的定義,內部類可以通過外部類的對象參數來訪問外部類中的所有成員。但是外部類不是內部類的友元。
class A
{
private:
static int k;
int h;
public:
class B
{
public:
void foo(const A& a)
{
cout << k << endl;//OK
cout << a.h << endl;//OK
}
};
};
int A::k = 1;
int main()
{
A::B b;
b.foo(A());
return 0;
}