類和對象
類是創建對象的模板,一個類可以創建多個對象,每個對象都是類類型的一個變量;創建對象的過程也叫類的實例化。每個對象都是類的一個具體實例(Instance),擁有類的成員變量和成員函數。
與結構體一樣,類只是一種複雜數據類型的聲明,不佔用內存空間。而對象是類這種數據類型的一個變量,或者說是通過類這種數據類型創建出來的一份實實在在的數據,所以佔用內存空間。
類的定義
類是用戶自定義的類型,如果程序中要用到類,必須提前說明,或者使用已存在的類(別人寫好的類、標準庫中的類等),C++語法本身並不提供現成的類的名稱、結構和內容。
一個簡單的類的定義:
#include<iostream>
using namespace std;
class Student{
private:
//成員變量
char *name;
int age;
float score;
public:
//成員函數
void say(){
cout << name << "的年齡是" << age << ",成績是" << score << endl;
}
};
class是 C++ 中新增的關鍵字,專門用來定義類。Student是類的名稱;類名的首字母一般大寫,以和其他的標識符區分開。{ }內部是類所包含的成員變量和成員函數,它們統稱爲類的成員(Member);由{ }包圍起來的部分有時也稱爲類體,和函數體的概念類似。
訪問限定符:public也是 C++ 的新增關鍵字,它只能用在類的定義中,表示類的成員變量或成員函數具有“公開”的訪問權限。
與之對應的是private關鍵字,有“保護”權限。
注意在類定義的最後有一個分號;,它是類定義的一部分,表示類定義結束了,不能省略。
整體上講,上面的代碼創建了一個 Student 類,它包含了 3 個成員變量和 1 個成員函數。
類只是一個模板(Template),編譯後不佔用內存空間,所以在定義類時不能對成員變量進行初始化,因爲沒有地方存儲數據。只有在創建對象以後纔會給成員變量分配內存,這個時候就可以賦值了。
類可以理解爲一種新的數據類型,該數據類型的名稱是 Student。與 char、int、float 等基本數據類型不同的是,Student 是一種複雜數據類型,可以包含基本類型,而且還有很多基本類型中沒有的特性,以後大家會見到。
創建對象
有了 Student 類後,就可以通過它來創建對象了,例如:
Student liLei; //創建對象
Student是類名,liLei是對象名。這和使用基本類型定義變量的形式類似:
int a; //定義整型變量
從這個角度考慮,我們可以把 Student 看做一種新的數據類型,把 liLei 看做一個變量。
在創建對象時,class 關鍵字可要可不要,但是出於習慣我們通常會省略掉 class 關鍵字,例如:
class Student LiLei; //正確
Student LiLei; //同樣正確
除了創建單個對象,還可以創建對象數組:
Student allStu[100];
該語句創建了一個 allStu 數組,它擁有100個元素,每個元素都是 Student 類型的對象。
訪問類的成員
創建對象以後,可以使用點號.來訪問成員變量和成員函數,這和通過結構體變量來訪問它的成員類似,如下所示:
#include<iostream>
using namespace std;
//類通常定義在函數外面
class Student{
public:
//成員變量
char *name;
int age;
float score;
public:
//成員函數
void say(){
cout << name << "的年齡是" << age << ",成績是" << score << endl;
}
};
int main(){
//創建對象
Student stu;
stu.name = "小明";
stu.age = 15;
stu.score = 92.5f;
stu.say();
return 0;
}
運行結果:
小明的年齡是15,成績是92.5
stu 是一個對象,佔用內存空間,可以對它的成員變量賦值,也可以讀取它的成員變量。
面現對象封裝性
封裝是實現信息隱蔽的一種技術,其目的是使類的定義和實現分離。C++的一個特性,相信很多的人自認爲理解,完全的理解,至少我是這麼認爲的,在三個特性中我認爲封裝性最容易理解,但是隨着對面向對象的瞭解和認識,我對封裝性有了新的認識,感覺以前的認識十分的片面。正如上面所說封裝性是c++語言的特性,而語言是用來約束程序員的,以前的理解就是類像一個包,而裏面有一些數據,程序員的代碼不能隨便訪問。然而這種封裝是比較片面的。
Everything has a purpose,而封裝存在的理由就是重用,重用就是寫的一個代碼庫可以在很多地方重用,而且易於擴充。在一塊內存中可以被許多應用程序運用,從開發的角度這樣十分的省事,不必做重複的工作,在使用的角度,十分的節約內存,以前一個程序要加載一個庫,現在幾個程序只需加載一個庫就可以了。這就是重用,使用以前的概念是無法實現這個目的的。聰明的程序員解決了這個問題。使用com組件技術,做到了二進制級別的代碼重用。相當於擴充了操作系統,因爲操作系統就是一個開放的接口,供應用程序調用功能。這就需要二進制級別的封裝,
而由於一樣的c++代碼由不同的編譯器編譯出來的代碼不一樣,造成了代碼之間的兼容問題。因爲你封裝起來的東西給了使用不同編譯器的程序員獲得的代碼是不一樣的,這樣就不具備擴充性和靈活性。不能實現代碼重用。
所以c++語言級別的封裝性是十分狹隘的。
各種角度看封裝
用戶:我只需要個能用的,能升級的產品。
客戶程序員:我需要的是可擴展的,封裝的,插件式的庫,這個庫的東西不能影響到程序框架中的其他部分
庫程序員:給客戶程序員一個接口和一個二進制級別的代碼。以實現可擴充行和可重用性。
內存:對我而言沒有封裝,計算機的一個基本概念,越往底層限制的東西越少。
編譯器:我編譯出來的代碼要具有封裝性,必須處理我和同行之間的差異。
C++語言:無辜的我是用來限制程序員的。但是也可以使用我的巧妙規則開提高你的水平,來實現你所需要的重用和擴充性。
對象的大小、計算
對象的大小計算依舊遵守內存對齊的原則
結構體內存對其規則:
1.第一個成員在與結構體變量偏移量爲0的地址處。
2.其他成員變量要對齊到某個數字(對齊數)的整數倍的地址處。
//對齊數=編譯器默認的一個對齊數與該成員大小的較小值。
VS中默認的值爲8
gcc中的默認值爲4
3.結構體總大小爲最大對齊數(每個成員變量除了第一個成員都有一個對齊數)的整數倍。
4.如果嵌套了結構體的情況,嵌套的結構體對齊到自己的最大對齊數的整數倍處,結構體的整體大小就是所有最大對齊數(含嵌套結構體
的對齊數)的整數倍。
空類對象(無成員變量的類)的大小也不一樣,VC中爲1。
實例化的原因(空類同樣可以被實例化),每個實例在內存中都有一個獨一無二的地址,爲了達到這個目的,編譯器往往會給一個空類隱含的加一個字節,這樣空類在實例化後在內存得到了獨一無二的地址,所以大小爲1。
當然在不同的編譯器上得到的結果可能不同,但是這個實驗告訴我們,不管類是否爲空類,是否有成員變量,這個類在創建對象的時候都是需要分配空間的。
四個默認成員函數
1.構造函數
成員變量爲私有的,要對它們進行初始化,必須用一個公有成員函數來進行。同時這個函數應該有且僅在定義對象時自動執行一次,這時
調用的函數稱爲構造函數(constructor)。
構造函數是特殊的成員函數,其特徵如下:
1. 函數名與類名相同。
2. 無返回值。
3. 對象構造(對象實例化)時系統自動調用對應的構造函數。
4. 構造函數可以重載。
5. 構造函數可以在類中定義,也可以在類外定義。
6. 如果類定義中沒有給出構造函數,則C++編譯器自動產生一個缺省的構造函數,但只要我們定義了一個構造函數,系統就不會自動
生成缺省的構造函數。
7. 無參的構造函數和全缺省值的構造函數都認爲是缺省構造函數,並且缺省的構造函數只能有一個。
2.拷貝構造函數
創建對象時使用同類對象來進行初始化,這時所用的構造函數稱爲拷貝構造函數(Copy Constructor),拷貝構造函數是特殊的構造函
數。
特徵:
1. 拷貝構造函數其實是一個構造函數的重載。
2. 拷貝構造函數的參數必須使用引用傳參,使用傳值方式會引發無窮遞歸調用。(思考爲什麼?)
先從一個小例子開始:(自己測試一下自己看看這個程序的輸出是什麼?)
#include<iostream>
using namespace std;
class CExample
{
private:
int m_nTest;
public:
CExample(int x) : m_nTest(x) //帶參數構造函數
{
cout << "constructor with argument"<<endl;
}
// 拷貝構造函數,參數中的const不是嚴格必須的,但引用符號是必須的
CExample(const CExample & ex) //拷貝構造函數
{
m_nTest = ex.m_nTest;
cout << "copy constructor"<<endl;
}
CExample& operator = (const CExample &ex)//賦值函數(賦值運算符重載)
{
cout << "assignment operator"<<endl;
m_nTest = ex.m_nTest;
return *this;
}
void myTestFunc(CExample ex)
{
}
};
int main(void)
{
CExample aaa(2);
CExample bbb(3);
bbb = aaa;
CExample ccc = aaa;
bbb.myTestFunc(aaa);
return 0;
}
這個例子的輸出結果是:
[cpp] view plain copy
constructor with argument // CExample aaa(2);
constructor with argument // CExample bbb(3);
assignment operator // bbb = aaa;
copy constructor // CExample ccc = aaa;
copy constructor // bbb.myTestFunc(aaa);
如果你能一眼看出就是這個結果的話, 恭喜你,可以站起來扭扭屁股,不用再往下看了。
如果你的結果和輸出結果有誤差, 那拜託你謙虛的看完。
第一個輸出: constructor with argument // CExample aaa(2);
如果你不理解的話, 找個人把你拖出去痛打一頓,然後嘴裏還喊着“我是二師兄,我是二師兄…….”
第二個輸出:constructor with argument // CExample bbb(3);
分析同第一個
第三個輸出: assignment operator // bbb = aaa;
第四個輸出: copy constructor // CExample ccc = aaa;
這兩個得放到一塊說。 肯定會有人問爲什麼兩個不一致。原因是, bbb對象已經實例化了,不需要構造,此時只是將aaa賦值給bbb,只會調用賦值函數,就這麼簡單,還不懂的話,撞牆去! 但是ccc還沒有實例化,因此調用的是拷貝構造函數,構造出ccc,而不是賦值函數,還不懂的話,我撞牆去!!
第五個輸出: copy constructor // bbb.myTestFunc(aaa);
實際上是aaa作爲參數傳遞給bbb.myTestFunc(CExample ex), 即CExample ex = aaa;和第四個一致的, 所以還是拷貝構造函數,而不是賦值函數, 如果仍然不懂, 我的頭剛纔已經流血了,不要再讓我撞了,你就自己使勁的再裝一次吧。
通過這個例子, 我們來分析一下爲什麼拷貝構造函數的參數只能使用引用類型。
看第四個輸出: copy constructor // CExample ccc = aaa;
構造ccc,實質上是ccc.CExample(aaa); 我們假如拷貝構造函數參數不是引用類型的話, 那麼將使得 ccc.CExample(aaa)變成aaa傳值ccc.CExample(CExample ex),即CExample ex = aaa,因爲 ex 沒有被初始化, 所以 CExample ex = aaa 繼續調用拷貝構造函數,接下來的是構造ex,也就是 ex.CExample(aaa),必然又會有aaa傳給CExample(CExample ex), 即 CExample ex = aaa;那麼又會觸發拷貝構造函數,就這下永遠的遞歸下去。
所以繞了那麼大的彎子,就是想說明拷貝構造函數的參數使用引用類型不是爲了減少一次內存拷貝, 而是避免拷貝構造函數無限制的遞歸下去。
3. 若未顯示定義,系統會默認缺省的拷貝構造函數。缺省的拷貝構造函數會,依次拷貝類成員進行初始化。
3.析構函數
當一個對象的生命週期結束時,C++編譯系統會自動調用一個成員函數,這個特殊的成員函數即析構函數(destructor)
構造函數是特殊的成員函數,其特徵如下:
1. 析構函數在類名加上字符~。
2. 析構函數無參數無返回值。
3. 一個類有且只有一個析構函數。若未顯示定義,系統會自動生成缺省的析構函數。
4. 對象生命週期結束時,C++編譯系統系統自動調用析構函數。
5. 注意析構函數體內並不是刪除對象,而是做一些清理工作。
4.賦值運算符重載
拷貝構造函數是創建的對象,使用一個已有對象來初始化這個準備創建的對象。
賦值運算符的重載是對一個已存在的對象進行拷貝賦值。
運算符重載
爲了增強程序的可讀性,C++支持運算符重載。
運算符重載特徵:
1. operator+合法的運算符構成函數名(重載<運算符的函數名:operator<)。
2. 重載運算符以後,不能改變運算符的優先級/結合性/操作數個數。
5個C++不能重載的運算符: .* /:: /sizeof /?: /.
隱含的this指針
- 每個成員函數都有一個指針形參,它的名字是固定的,稱爲this指針,this指針是隱式的。(構造函數比較特殊,沒有這個隱含this形參)
- 編譯器會對成員函數進行處理,在對象調用成員函數時,對象地址作實參傳遞給成員函數的第一個形參this指針。
- this指針是成員函數隱含指針形參,是編譯器自己處理的,我們不能在成員函數的形參中添加this指針的參數定義,也不能在調用時顯示傳遞對象的地址給this指針。