目錄
- 前言
- 從結構體到類
- 類的聲明與使用
- 基礎聲明
- 繼承聲明
- 數據與函數聲明與調用
- 聲明
- 調用
- 類的訪問修飾符
- 類對象的內存分布
- 類內數據相關
- 靜態變量
- 非靜態變量
- 類成員函數相關
- 普通成員函數
- 友元函數
- 構造與析構函數
- 構造函數
- 析構函數
- 拷貝構造函數
- 總結
前言
?面向對象編程有三大特性,分別是封裝、繼承、多態。這些特性在類的語法使用中都得到了充分的體現,我也預計寫幾篇文章來介紹一下C++的類語法。這是第一篇:類基礎。
?類基礎主要目的是介紹一下類的基礎使用,重點在于數據和函數的封裝。
從結構體到類
?對于新手來說 類 這個概念可能比較陌生,但是提到結構體(struct)對于有C語言基礎的人應該比較熟悉。在C++中結構體和類的底層實現幾乎完全一致,結構體不僅可以封裝函數,甚至還可以繼承類。
#include <iostream>
#include <cstring>
using namespace std; class Base_Class{
protected:int id; //占用大小為4字節的內存空間
public:Base_Class(int i=0){id = i;cout << "Base_Class constructor called." << endl;cout << "id = " << id << endl;}
};struct Derived_struct : public Base_Class
{Derived_struct(int i) : Base_Class(i) {cout << "Derived_Struct constructor called." << endl;cout << "id = " << id << endl;}
};int main() {Derived_struct obj(10);return 0;
}
輸出結果為:
Base_Class constructor called.
id = 10
Derived_Struct constructor called.
id = 10
?是不是對類的感覺親切了許多?當然在C語言中結構體里是不能封裝函數的,也沒有繼承這個說法。數據和函數不封裝到一起,那么當我處理數據的時候(比如結構體)就要去其它地方找函數(公共聲明函數的地方),而不是通過對象直接可以調用處理的函數。
?打個比方,就像用勺子吃西瓜:C語言買了西瓜后要到廚房去找勺子才能吃西瓜,而C++封裝的特性就是讓買西瓜時,西瓜和勺子綁定在一起出售。因而C語言是面向過程編程的,C++是面向對象編程。
?當然了,結構體和類底層實現相同,這不意味著在C++中結構體和類能混用。從規范上來說,結構體用于數據聚合,基本上不封裝方法,就像C那樣(例如坐標點封裝,顏色封裝)而類則用于?對象進行封裝?,包括數據以及相關的方法。
類的聲明與使用
基礎聲明
類的基礎語法聲明如下:
舉例說明:
class Box
{public:double length; // 盒子的長度double width; // 盒子的寬度double height; // 盒子的高度int get_volume(){ //獲得盒子的體積return length*width*height;}
};
?類里面就兩樣東西:數據+方法
繼承聲明
繼承的語法如下:
?其中,derived_class是派生類的名稱,base_class是基類名稱,二者之間通過: + 訪問修飾符
連接。訪問修飾符 access-specifier 是 public、protected 或 private 其中的一個,如果未使用訪問修飾符 access-specifier,則默認為 private繼承。
// 基類
class Animal {
xxxx
};
//派生類
class Dog : public Animal {
xxxx
};
數據與函數聲明與調用
聲明
?類內變量與成員函數的聲明語法類外一致,值得注意的是成員函數的具體實現可以放在類外。具體方法如下:
- 首先在類內聲明這個函數:
class Dog {
public:void bark(); //類內聲明
};
- 然后在類外以
范圍解析運算符(::) + 函數名
的形式進行定義:
//類外實現
void Dog::bark(){cout << "汪汪汪" <<endl;
}
調用
?類的變量和函數的調用方法與結構體類似:
class Dog{
public: int age=10;//數據void bark() //方法{cout << "汪汪汪" <<endl;}
};
- 如果是類對象實例,使用
.
運算符訪問成員變量和函數方法:
Dog wangcai;
wangcai.age; //訪問變量
wangcai.bark(); //訪問函數
- 如果是類指針,使用
->
運算符訪問成員變量和函數方法:
Dog* wangcai;
wangcai->age; //訪問變量
wangcai->bark(); //訪問方法
類的訪問修飾符
?訪問修飾符用來控制外界對類內變量以及成員函數(類成員)的訪問權限。關鍵字 public、private、protected 稱為訪問修飾符。
- public(公有)
?該成員變量/方法可以被外部的函數直接訪問。這是權限最低的一種,類內、派生類以及類外都能訪問往往用于對外接口上。 - protected(保護)
?保護的類成員只能被類內以及派生類訪問,類外無法訪問。 - private(私有)
?如果沒有聲明訪問權限,私有是默認的訪問修飾符。是保護程度最高的一種,只能類內函數訪問,派生類和類外都是不能訪問的。往往只用于給內部成員函數數據交換
上面的內容總結來說就是:類內成員函數訪問類內的變量/方法時沒有任何限制,派生類能訪問public和protected,類外的普通函數就只能訪問public下的變量/方法(友元函數除外)
#include <iostream>
using namespace std;class Example {
public: // 公有成員int publicVar;void publicMethod() {cout << "Public method" << endl;}private: // 私有成員int privateVar;void privateMethod() {cout << "Private method" << endl;}protected: // 保護成員int protectedVar;void protectedMethod() {cout << "Protected method" << endl;}
};
類對象的內存分布
?首先要明確一下,類本身是一個抽象的概念,是不占用物理內存的。例如:
class Dog{
public: int age=10;//數據void bark() //方法{cout << "汪汪汪" <<endl;}
};
?如果不實例化這個類(Dog wangcai
)是不會有內存占用的。提這個點是為了解答后面學習多態時,有的教材會說虛函數表存儲在類里面,這樣容易引起誤解,類就像int 、float那樣,如果不實例化是不會創造出內存空間的。
類對象實例的內存分布如下:
- 最開頭: 虛函數表指針。
- 非靜態成員變量。(按照聲明順序依次排布)
此外,
- 對于靜態成員變量:存儲在全局數據區,不占用類實例內存空間。
- 對于成員函數:代碼存儲在代碼段,所有對象共享同一份函數代碼。
- 對于虛函數:虛函數實現也和成員函數一樣存儲在代碼段。
- 對于虛函數表:虛函數表在可執行文件中位于.rodata段(Linux/Unix)或.rdata段(Windows),程序加載后存于內存的?常量區?,具有只讀屬性。
類內數據相關
靜態變量
?靜態變量是類內以static
為修飾符定義的變量。要點如下:
- 對于類中的靜態變量來說,所有對象共享同一個靜態變量,修改會影響所有實例。(即存儲位置在全局數據區)
- 靜態變量的生命周期?在程序啟動時初始化,程序結束時銷毀。
- 靜態變量在類內聲明在類外定義
- 推薦使用類名訪問(推薦):ClassName::staticVar
示例代碼:
class Person {
public:static int count; // 靜態變量聲明
};
int Person::count = 0; // 必須在類外定義Person p1;
Person::count++; // 推薦:通過類名訪問
p1.count++; // 不推薦:通過對象訪問(合法但不清晰)
非靜態變量
?又被稱為實例變量,是指類內定義的普通成員變量。有以下特性:
- 每個對象獨立存儲?,不同對象的實例變量互不影響。(即存儲在實例的內存區域)
- 生命周期?:隨對象創建而分配,隨對象銷毀而釋放。
- 必須通過對象實例進行訪問。
舉例:
class Person {
public:std::string name; // 實例變量int age; // 實例變量
};
Person p1;
p1.name = "Alice"; // 訪問實例變量
類成員函數相關
普通成員函數
?要點:
- 普通成員函數暗含this指針,可以無需聲明使用成員變量。
- 成員函數的調用要通過對象實例進行,不能單獨拎出來使用。
友元函數
?友元函數本質就是類外聲明定義的函數,不與類對象綁定,即在類內用friend
聲明的外部函數,能突破封裝性直接訪問類的私有和保護成員,使其數據訪問權限等同于成員函數(也就是說可以訪問類的私有變量與函數)。
要點:
- 必須在類內使用
friend
修飾符進行聲明。 - 具體的實現放在類外。
示例:
class MyClass {
private:int secret;
public:friend void peek(MyClass& obj); // 聲明友元函數
};
void peek(MyClass& obj) { obj.secret = 42; } // 實現可訪問私有成員
構造與析構函數
#include <iostream>
using namespace std;class MyClass {
public:int value;// 構造函數MyClass(int v) {value = v;cout << "構造函數被調用,value=" << value << endl;}// 析構函數~MyClass() {cout << "析構函數被調用,value=" << value << endl;}
};int main() {MyClass obj1(10); // 構造函數調用{MyClass obj2(20); // 構造函數調用} // obj2離開作用域,析構函數調用return 0;
} // obj1離開作用域,析構函數調用
構造函數
?構造函數是一種特殊的成員函數,有以下要點:
- 無任何返回值(連void也沒有),可以傳入參數。
- 函數名與類名相同。
- 在類對象創建時執行一次,主要用來初始化類對象
- 不被繼承,即子類的構造函數中必須要先手動調用父類的構造函數(無參構造時編譯器會自動調用)。
?構造函數還有一個初始化列表的語法,可以用來初始化字段。例如上面的構造函數可以改為:
MyClass(int v):value(v){cout << "構造函數被調用,value=" << value << endl;
}
如果MyClass繼承自BaseClass,初始化列表的語法也可以用來初始化構造函數,可以寫成這樣下面這樣,以符合第四點的要求:
MyClass(int v):BaseClass(Base_args),value(v){cout << "構造函數被調用,value=" << value << endl;
};
(初始化列表時,用逗號,
隔開各個參數)
析構函數
?析構函數與構造函數相呼應,就是類對象在銷毀時自動調用的函數。(例如,程序結束時、局部對象離開作用域時)。要點如下:
- 無任何返回值(void也沒有)以及不傳入任何參數!。
- 函數名為
~
+類名
。 - 對象銷毀時自動執行,用來釋放類對象手動創建的內存空間,避免內存泄漏
- 同樣不被繼承,但是自動調用(因為是無參的,編譯器會自動調用)。順序為先調用派生類的析構,再調用基類的析構
拷貝構造函數
?為什么需要額外的拷貝構造函數?其實想想就能清楚,一個類對象的創建只能有兩種途徑:
- 自定義。使用默認的構造函數進行初始化。
- 從其它對象中復制。這時就要調用拷貝構造函數而不是構造函數進行初始化了。
why?拷貝構造函數必須要寫嗎?為什么之前寫的類沒有拷貝構造函數也能編譯通過?
回答這個問題首先要了解一下淺拷貝和深拷貝。
?其實C++中數據無非被分為兩種:普通變量和指針變量,對于淺拷貝來說,執行的就是簡單的賦值操作,不區分指針和值,這樣也就會造成一種情況:對象A里有個指針p,p指向某個地址。如果此時簡單的使用Class B = A
這樣初始化對象B就是淺拷貝。那么對象B內的指針p的值和A當中指針p的值是一樣的(淺拷貝只是簡單賦值),這樣就會造成對象A和B的指針p指向同一塊內存空間
,如果B中的p改動了指向的變量(*p),那么A中p指向的值也會隨之改變,在某些情況下這是很危險的。(即淺拷貝會造成多個指針指向同一個內存地址的情況)。
?對于深拷貝來說,就會區分普通變量和指針變量。對于普通變量,如int,float等就直接把值復制過去就行了。對于指針變量則會為指針成員分配新內存并復制內容,但是這一過程必須要手動實現,這就依賴我們的拷貝構造函數了!
?下面從使用場合、語法規則兩方面總結拷貝構造函數:
-
使用場合:需要以一個已經實例化的類對象為基礎,創建一個新的類對象(所謂構造),并且類的成員變量中有指針類型時,需要自定義拷貝構造函數為指針成員分配新內存,并將值賦值到新內存空間中(所謂深拷貝)。
?若未顯式定義拷貝構造函數,編譯器會生成默認拷貝構造函數執行?淺拷貝?(逐成員復制值)。拷貝構造二者缺一不可,如果僅僅是普通的賦值,例如B = A
,那么默認觸發淺拷貝,此時如果需要深拷貝應該要在類內重載運算符=
。 -
語法規則:
定義:
?簡單來說就是構造函數的基礎上,固定傳入的參數必須
為同類對象的?常量引用?(ClassName(const ClassName& obj)),若使用值傳遞(ClassName(const ClassName obj))會導致無限遞歸調用。
使用:
MyClass obj2 = obj1; // 隱式調用,此時=表示初始化而非賦值
MyClass obj3(obj1); // 顯式調用
示例:
class DeepString {
public:char* data;DeepString(const DeepString& other) {data = new char[strlen(other.data) + 1]; //重新分配地址strcpy(data, other.data); // 復制內容而非指針}
};
總結
?拷貝和構造缺一不可:
- 少了拷貝就會導致多個指針指向同一個地址。
- 少了構造就是普通的賦值操作,是在已存在對象間進行賦值(a = b)不屬于根據一個已有對對象初始化一個新對象。