一、面向對象三大特性之一——封裝
封裝:将資料和操作資料的方法進行有機結合,隐藏對象的屬性和實作細節,僅對外公開接口來和對象進行互動。
1. C++中struct與class的差別:struct 中預設成員為公有 public ;而 class 中預設成員為私有 private 。在C語言中 struct 中隻能聲明成員變量,不能聲明成員函數函數,但是C++中 struct 可以聲明成員函數。
2. 類隻是一個像模型一樣的東西,限定了類有哪些成員,定義出一個類并沒有配置設定實際的記憶體空間來存儲它。隻有當建立類的對象時(執行個體化),才會為成員變量配置設定記憶體空間來存儲它。
二、類和對象的大小以及在記憶體中類的對齊規則
類和對象的大小:對象中隻儲存成員變量,成員函數存放在公共的代碼段。如果一個類中具有虛函數,則對象中還會有一個虛表指針,存放在對象的前四個位元組中。靜态成員變量也不計入類和對象的大小。綜上所述,類和對象的大小就是 成員變量+虛表指針。
需要注意的是,空類的大小為 1 。
我們來看一段代碼:
class A
{
public:
A(int _a,double _b)
{
a = _a;
b = _b;
}
private:
int a;
double b;
static int c;
};
int A::c = 30;
int main()
{
A obj(10, 20.5);
cout << sizeof(A) << endl;
cout << sizeof(obj) << endl;
return 0;
}
我們可看到,類中有三個成員變量,其中靜态成員變量不計入類和對象的大小,也就是說,隻有一個 int 型的變量和一個 double 類型的變量,那麼大小是多少?是 4+8=12 嗎?答案是 16。為什麼呢? 在記憶體中并不是緊挨着存放的,而是有對齊規則的。
類(結構體)在記憶體對齊規則:
1. 第一個成員在結構體變量偏移量為0的位址處。
2. 其他成員要對齊到對齊數的整數倍位址處。 對齊數=編譯器預設的對齊數與該成員變量大小的較小值。
vs 下對齊數預設為8 ,Linux 中的對齊數預設為 4 。
3. 結構體總大小為最大對齊數的整數倍。 最大對齊數為所有成員變量的對齊數的最大值。
4. 如果嵌套了結構體,那麼嵌套的結構體的對齊數就是自己的最大對齊數的整數倍處。
是以我們來看上面代碼,先在記憶體中存放變量 a 四個位元組,變量 b 對齊時需要對齊到偏移量為 8 處,是以類和對象的大小為:4+4+8=16 。
我們再來看另一段代碼:
class A
{
public:
A()
{
}
private:
int a;
char arr[9];
double b;
};
int main()
{
A obj;
cout << sizeof(A) << endl;
cout << sizeof(obj) << endl;
return 0;
}}
上面的代碼結果又是多少呢? 是 4+(5)+9+(6)+8=32 嗎?(括号中數字代表,為了記憶體對齊而空出來的位元組數)
答案是 4+9+(3)+8=24 。這是因為 arr[9] 的對齊數不是 9 而是 1。一個 char 類型的大小。
如果類中有虛函數,則對象中會有一個虛表指針,存放于首位址中 同樣遵循記憶體對齊規則。
我們已經知道結構體在記憶體中需要對齊,那麼為什麼需要對齊呢?
1、平台原因(移植原因):
不是所有的硬體平台都能通路任意位址上的任意資料的;某些硬體平台隻能在某些位址處取某些特定類型的資料,否則抛出硬體異常。
2、性能原因:資料結構(尤其是棧)應該盡可能地在自然邊界上對齊。
原因在于,為了通路未對齊的記憶體,處理器需要作兩次記憶體通路;而對齊的記憶體通路僅需要一次通路。(提高CPU處理速度)
三、this 指針
C++編譯器給每個“成員函數“增加了一個隐藏的指針參數,讓該指針指向目前對象(函數運作時調用該函數的對象),在函數體中所有成員變量的操作,都是通過該指針去通路。
當我們使用對象來調用類中的成員函數時,會将對象的位址傳給 this 指針,也就是說 this 指針指向的就是調用成員函數的對象。
我們來看一段代碼:
class A
{
public:
A(int _a,int _b)
{
a = _a;
b = _b;
}
void show()
{
cout << a << endl;
cout << b << endl;
}
private:
int a;
int b;
};
int main()
{
A obj(10, 20);
obj.show();
return 0;
}
執行函數 show 的時候編譯器會處理成 show(A* this); this 存放的就是對象 obj 的位址。
也就是說 cout<<a<<endl; 相當于 cout<<this->a<<endl; 這樣函數就知道是哪個對象調用了自己。
1. this指針的類型:類類型* const
2. 隻能在“成員函數”的内部使用
3. this指針本質上其實是一個成員函數的形參,是對象調用成員函數時,将對象位址作為實參傳遞給this形參。是以對象中不存儲this指針。
4. this指針是成員函數第一個隐含的指針形參。
5. this 指針存放在 ecx 寄存器中。
6. this 指針可以為空,為空時不能通過 this 指針來調用類中成員變量。
四、類中的預設成員函數
一個空類中并不是什麼都沒有的,它有六個預設的成員函數:構造函數、析構函數、拷貝構造函數、指派運算符重載函數、取位址操作符重載、const 取位址操作符重載。
1. 無參構造函數和全預設構造函數都是預設構造函數,類中隻能存在一個預設構造函數,沒有構造函數時,會自動生成一個無參構造函數。
2. 拷貝構造函數的參數隻有一個且必須使用引用傳參,使用傳值方式會引發無窮遞歸調用。
3. 有五個運算符不能被重載 . 點運算符 .* 成員指針通路運算符 :: 域運算符 sizeof 長度運算符 ?: 條件運算符。
4. 以下三種成員變量必須用初始化清單初始化: const 成員變量 ,引用成員變量,類類型成員(該類中沒有預設構造函數)
5. 成員變量在類中聲明次序就是其在初始化清單中的初始化順序,與其在初始化清單中的先後次序無關。
class A
{
public:
A(int a)
:_a(a)
{}
private:
int _a;
};
class B
{
public:
B(int a, int ref)
:_aobj(a)
,_ref(ref)
,_n(10)
{}
private:
A _aobj; // 沒有預設構造函數
int& _ref; // 引用
const int _n; // const
}
五、 explicit 關鍵字
在 C++ 中,單參數構造函數(或者是除了第一個參數,其餘參數都有預設值的多參構造函數)有兩個作用:一是初始化對象,二是隐式類型轉換。
class A
{
public:
A(int _a)
{
a = _a;
}
void print()
{
cout << a << endl;
}
private:
int a;
};
int main()
{
A obj(5);
obj = 10;
obj.print();
return 0;
}
上述代碼相當于用一個整型 10 給obj 指派。編譯器會用 10 建構一個無名對象,然後用這個無名對象給 obj 指派。
但是這樣的代碼不具有可讀性,所有引入了 explicit 關鍵字。用 explicit 修飾構造函數,将會禁止單參構造函數的隐式轉換。
class A
{
public:
explicit A(int _a)
{
a = _a;
}
void print()
{
cout << a << endl;
}
private:
int a;
};
int main()
{
A obj(5);
obj = 10;
obj.print();
return 0;
}
這個時候,編譯器就會報錯:沒有與這些操作數比對的 “=” 運算符。
六、 static 靜态成員。
static的類成員稱為類的靜态成員,用static修飾的成員變量,稱之為靜态成員變量;用static修飾的成員函數,稱之為靜态成員函數。靜态的成員變量一定要在類外進行初始化。在類外初始化時不需要加上 static。
1. 靜态成員為所有類對象所共享,不屬于某個具體的執行個體。
2. 類靜态成員即可用類名::靜态成員或者對象.靜态成員來通路
3. 靜态成員函數沒有隐藏的this指針,不能通路任何非靜态成員
4. 靜态成員和類的普通成員一樣,也有public、protected、private 3種通路級别,也可以具有傳回值,const修飾符等參數。
七、 C++11 類中成員初始化新方式
C++11 允許在類中聲明非靜态成員變量時直接初始化。
class B
{
public:
B(int b = 0)
:_b(b)
{}
int _b;
};
class A
{
public:
void Print()
{
cout << a << endl;
cout << b._b<< endl;
cout << p << endl;
}
private:
// 非靜态成員變量,可以在成員聲明時,直接初始化。
int a = 10;
B b = 20;
int* p = (int*)malloc(4);
static int n;
};
int A::n = 10;
int main()
{
A a;
a.Print();
return 0;
}
八、 友元
友元函數可以直接通路類的私有成員,它是定義在類外部的普通函數,不屬于任何類,但需要在類的内部聲
明,聲明時需要加friend關鍵字。
1. 友元函數可通路類的私有成員,但不是類的成員函數
2. 友元函數不能用const修飾
3. 友元函數可以在類定義的任何地方聲明,不受類通路限定符限制
4. 一個函數可以是多個類的友元函數
5. 友元函數的調用與普通函數的調用和原理相同
友元類的所有成員函數都可以是另一個類的友元函數,都可以通路另一個類中的非公有成員。
class Date; // 前置聲明
class Time
{
friend class Date; // 聲明日期類為時間類的友元類,則在日期類中就直接通路Time類中的私有成員變
量
public:
Time(int hour, int minute, int second)
: _hour(hour)
, _minute(minute)
, _second(second)
{}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
void SetTimeOfDate(int hour, int minute, int second)
{
// 直接通路時間類私有的成員變量
_t._hour = hour;
_t._minute = minute;
_t.second = second;
}
private:
int _year;
int _month;
int _day;
Time _t;
};
在一個類中聲明另一類為該類的友元類時,必須要前置聲明該友元類。
1. 友元關系是單向的,不具有交換性。
比如上述Time類和Date類,在Time類中聲明Date類為其友元類,那麼可以在Date類中直接通路Time類的私有成員變量,但想在Time類中通路Date類中私有的成員變量則不行。
2. 友元關系不能傳遞
如果B是A的友元,C是B的友元,則不能說明C時A的友元。
九、 内部類
如果一個類定義在另一個類的内部,這個内部類就叫做内部類。注意此時這個内部類是一個獨立的類,它不屬于外部類,更不能通過外部類的對象去調用内部類。外部類對内部類沒有任何優越的通路權限。
内部類就是外部類的友元類。注意友元類的定義,内部類可以通過外部類的對象參數來通路外部類中的所有成員。但是外部類不是内部類的友元
1. 内部類可以定義在外部類的public、protected、private都是可以的。
2. 注意内部類可以直接通路外部類中的static、枚舉成員,不需要外部類的對象/類名。
3. sizeof(外部類)=外部類,和内部類沒有任何關系。
class A
{
public:
A(int _a) :a(_a)
{
}
void print()
{
cout << a << endl;
}
class B
{
public:
B(int _b) :b(_b)
{}
void print(const A& obj)
{
cout << obj.a << endl; // a 為普通成員變量,需要通過類A 的對象來通路
cout << b << endl;
cout << aa << endl; // aa 為A類的靜态成員變量,可以不通過類A的對象或類名來通路
}
private:
int b;
};
private:
int a;
static int aa;
};
int A::aa = 100;
int main()
{
A obj1(10);
A::B obj2(20);
obj2.print(obj1);
cout << sizeof(A) << endl;
cout << sizeof(A::B) << endl;
return 0;
}
程式運作結果為:
10
20
100
4
4
十、const成員函數
用const修飾的成員函數稱為const成員函數。const修飾成員函數實際上是修飾成員函數隐含的this指針,表明在該成員函數中,不能對調用該成員函數的對象做任何修改。
const對象不可以調用非const成員函數,非const對象可以調用const成員函數;const成員函數内部不可以調用非const成員函數,非const成員函數内部可以調用const成員函數。