C++類和對象詳解

創建對象

兩種創建對象的方式:一種是在棧上創建,形式和定義普通變量類似;另外一種是在堆上使用 new 關鍵字創建,必須要用一個指針指向它,讀者要記得 delete 掉不再使用的對象。

通過對象名字訪問成員使用點號.,通過對象指針訪問成員使用箭頭->,這和結構體非常類似。

成員變量和函數

類可以看做是一種數據類型,它類似於普通的數據類型,但是又有別於普通的數據類型。類這種數據類型是一個包含成員變量和成員函數的集合。

類的成員變量和普通變量一樣,也有數據類型和名稱,佔用固定長度的內存。但是,在定義類的時候不能對成員變量賦值,因爲類只是一種數據類型或者說是一種模板,本身不佔用內存空間,而變量的值則需要內存來存儲。

類的成員函數也和普通函數一樣,都有返回值和參數列表,它與一般函數的區別是:成員函數是一個類的成員,出現在類體中,它的作用範圍由類來決定;而普通函數是獨立的,作用範圍是全局的,或位於某個命名空間內。
 

class Student{
public:
    //成員變量
    char *name;
    int age;
    float score;

    //成員函數
    void say(){
        cout<<name<<"的年齡是"<<age<<",成績是"<<score<<endl;
    }
};

但我們一般建議類內只是聲明函數,類外定義。 

class Student{
public:
    //成員變量
    char *name;
    int age;
    float score;

    //成員函數
    void say();  //函數聲明
};

//函數定義
void Student::say(){
    cout<<name<<"的年齡是"<<age<<",成績是"<<score<<endl;
}

在類體中直接定義函數時,不需要在函數名前面加上類名,因爲函數屬於哪一個類是不言而喻的。
但當成員函數定義在類外時,就必須在函數名前面加上類名予以限定。::被稱爲域解析符(也稱作用域運算符或作用域限定符),用來連接類名和函數名,指明當前函數屬於哪個類。

在類體中和類體外定義成員函數的區別

在類體中和類體外定義成員函數是有區別的:在類體中定義的成員函數會自動成爲內聯函數,在類體外定義的不會。當然,在類體內部定義的函數也可以加 inline 關鍵字,但這是多餘的,因爲類體內部定義的函數默認就是內聯函數。

內聯函數一般不是我們所期望的,它會將函數調用處用函數體替代,所以我建議在類體內部對成員函數作聲明,而在類體外部進行定義,這是一種良好的編程習慣,實際開發中大家也是這樣做的。

當然,如果你的函數比較短小,希望定義爲內聯函數,那也沒有什麼不妥的。

如果你既希望將函數定義在類體外部,又希望它是內聯函數,那麼可以在定義函數時加 inline 關鍵字。當然你也可以在函數聲明處加 inline,不過這樣做沒有效果,編譯器會忽略函數聲明處的 inline

構造函數

#include <iostream>
using namespace std;

class Student{
private:
    char *m_name;
    int m_age;
    float m_score;
public:
    //聲明構造函數
    Student(char *name, int age, float score);
    //聲明普通成員函數
    void show();
};

//定義構造函數
Student::Student(char *name, int age, float score){
    m_name = name;
    m_age = age;
    m_score = score;
}
//定義普通成員函數
void Student::show(){
    cout<<m_name<<"的年齡是"<<m_age<<",成績是"<<m_score<<endl;
}

int main(){
    //創建對象時向構造函數傳參
    Student stu("小明", 15, 92.5f);
    stu.show();
    //創建對象時向構造函數傳參
    Student *pstu = new Student("李華", 16, 96);
    pstu -> show();

    return 0;
}

該例在 Student 類中定義了一個構造函數Student(char *, int, float),它的作用是給三個 private 屬性的成員變量賦值。要想調用該構造函數,就得在創建對象的同時傳遞實參,並且實參由( )包圍,和普通的函數調用非常類似。

在棧上創建對象時,實參位於對象名後面,例如Student stu("小明", 15, 92.5f);在堆上創建對象時,實參位於類名後面,例如new Student("李華", 16, 96)

構造函數必須是 public 屬性的,否則創建對象時無法調用。當然,設置爲 private、protected 屬性也不會報錯,但是沒有意義。

構造函數沒有返回值,因爲沒有變量來接收返回值,即使有也毫無用處,這意味着:

  • 不管是聲明還是定義,函數名前面都不能出現返回值類型,即使是 void 也不允許;
  • 函數體中不能有 return 語句。

構造函數的重載

和普通成員函數一樣,構造函數是允許重載的。一個類可以有多個重載的構造函數,創建對象時根據傳遞的實參來判斷調用哪一個構造函數。

構造函數的調用是強制性的,一旦在類中定義了構造函數,那麼創建對象時就一定要調用,不調用是錯誤的。如果有多個重載的構造函數,那麼創建對象時提供的實參必須和其中的一個構造函數匹配;反過來說,創建對象時只有一個構造函數會被調用。

默認構造函數

如果用戶自己沒有定義構造函數,那麼編譯器會自動生成一個默認的構造函數,只是這個構造函數的函數體是空的,也沒有形參,也不執行任何操作。比如上面的 Student 類,默認生成的構造函數如下:

Student(){}

一個類必須有構造函數,要麼用戶自己定義,要麼編譯器自動生成。

一旦用戶自己定義了構造函數,不管有幾個,也不管形參如何,編譯器都不再自動生成。

實際上編譯器只有在必要的時候纔會生成默認構造函數,而且它的函數體一般不爲空。默認構造函數的目的是幫助編譯器做初始化工作,而不是幫助程序員。這是C++的內部實現機制,這裏不再深究,初學者可以按照上面說的“一定有一個空函數體的默認構造函數”來理解。

最後需要注意的一點是,調用沒有參數的構造函數也可以省略括號。對於示例的代碼,在棧上創建對象可以寫作Student stu()Student stu,在堆上創建對象可以寫作Student *pstu = new Student()Student *pstu = new Student,它們都會調用構造函數 Student()。

以前我們就是這樣做的,創建對象時都沒有寫括號,其實是調用了默認的構造函數。

初始化列表

構造函數的一項重要功能是對成員變量進行初始化,可以在構造函數的函數體中對成員變量一一賦值,還可以採用初始化列表。

#include <iostream>
using namespace std;

class Student{
private:
    char *m_name;
    int m_age;
    float m_score;
public:
    Student(char *name, int age, float score);
    void show();
};

//採用初始化列表
Student::Student(char *name, int age, float score): m_name(name), m_age(age), m_score(score){
    //TODO:
}

如本例所示,定義構造函數時並沒有在函數體中對成員變量一一賦值,其函數體爲空(當然也可以有其他語句),而是在函數首部與函數體之間添加了一個冒號:,後面緊跟m_name(name), m_age(age), m_score(score)語句,這個語句的意思相當於函數體內部的m_name = name; m_age = age; m_score = score;語句,也是賦值的意思。

使用構造函數初始化列表並沒有效率上的優勢,僅僅是書寫方便,尤其是成員變量較多時,這種寫法非常簡單明瞭。

注意,成員變量的初始化順序與初始化列表中列出的變量的順序無關,它只與成員變量在類中聲明的順序有關。

創建對象時系統會自動調用構造函數進行初始化工作,同樣,銷燬對象時系統也會自動調用一個函數來進行清理工作,例如釋放分配的內存、關閉打開的文件等,這個函數就是析構函數。


析構函數

 

析構函數(Destructor)也是一種特殊的成員函數,沒有返回值,不需要程序員顯式調用(程序員也沒法顯式調用),而是在銷燬對象時自動執行。構造函數的名字和類名相同,而析構函數的名字是在類名前面加一個~符號。

注意:析構函數沒有參數,不能被重載,因此一個類只能有一個析構函數。如果用戶沒有定義,編譯器會自動生成一個默認的析構函數。

#include <iostream>
using namespace std;

class VLA{
public:
    VLA(int len);  //構造函數
    ~VLA();  //析構函數
public:
    void input();  //從控制檯輸入數組元素
    void show();  //顯示數組元素
private:
    int *at(int i);  //獲取第i個元素的指針
private:
    const int m_len;  //數組長度
    int *m_arr; //數組指針
    int *m_p;  //指向數組第i個元素的指針
};

VLA::VLA(int len): m_len(len){  //使用初始化列表來給 m_len 賦值
    if(len > 0){ m_arr = new int[len];  /*分配內存*/ }
    else{ m_arr = NULL; }
}
VLA::~VLA(){
    delete[] m_arr;  //釋放內存
}
void VLA::input(){
    for(int i=0; m_p=at(i); i++){ cin>>*at(i); }
}
void VLA::show(){
    for(int i=0; m_p=at(i); i++){
        if(i == m_len - 1){ cout<<*at(i)<<endl; }
        else{ cout<<*at(i)<<", "; }
    }
}
int * VLA::at(int i){
    if(!m_arr || i<0 || i>=m_len){ return NULL; }
    else{ return m_arr + i; }
}

int main(){
    //創建一個有n個元素的數組(對象)
    int n;
    cout<<"Input array length: ";
    cin>>n;
    VLA *parr = new VLA(n);
    //輸入數組元素
    cout<<"Input "<<n<<" numbers: ";
    parr -> input();
    //輸出數組元素
    cout<<"Elements: ";
    parr -> show();
    //刪除數組(對象)
    delete parr;

    return 0;
}

析構函數的執行時機

析構函數在對象被銷燬時調用,而對象的銷燬時機與它所在的內存區域有關。
在所有函數之外創建的對象是全局對象,它和全局變量類似,位於內存分區中的全局數據區,程序在結束執行時會調用這些對象的析構函數。
在函數內部創建的對象是局部對象,它和局部變量類似,位於棧區,函數執行結束時會調用這些對象的析構函數。
new 創建的對象位於堆區,通過 delete 刪除時纔會調用析構函數;如果沒有 delete,析構函數就不會被執行。

#include <iostream>
#include <string>
using namespace std;

class Demo{
public:
    Demo(string s);
    ~Demo();
private:
    string m_s;
};
Demo::Demo(string s): m_s(s){ }
Demo::~Demo(){ cout<<m_s<<endl; }

void func(){
    //局部對象
    Demo obj1("1");
}

//全局對象
Demo obj2("2");

int main(){
    //局部對象
    Demo obj3("3");
    //new創建的對象
    Demo *pobj4 = new Demo("4");
    func();
    cout<<"main"<<endl;
  
    return 0;
}

this 是 一個關鍵字,也是一個 const指針,它指向當前對象,通過它可以訪問當前對象的所有成員。

所謂當前對象,是指正在使用的對象。例如對於stu.show();,stu 就是當前對象,this 就指向 stu。

注意,this 是一個指針,要用->來訪問成員變量或成員函數。
this 雖然用在類的內部,但是只有在對象被創建以後纔會給 this 賦值,並且這個賦值的過程是編譯器自動完成的,不需要用戶干預,用戶也不能顯式地給 this 賦值。

this 原理

this 實際上是成員函數的一個形參,在調用成員函數時將對象的地址作爲實參傳遞給 this。不過 this 這個形參是隱式的,它並不出現在代碼中,而是在編譯階段由編譯器默默地將它添加到參數列表中。


this 作爲隱式形參,本質上是成員函數的局部變量,所以只能用在成員函數的內部,並且只有在通過對象調用成員函數時纔給 this 賦值。

成員函數最終被編譯成與對象無關的普通函數,除了成員變量,會丟失所有信息,所以編譯時要在成員函數中添加一個額外的參數,把當前對象的首地址傳入,以此來關聯成員函數和成員變量。這個額外的參數,實際上就是 this,它是成員函數和成員變量關聯的橋樑。

靜態變量

有時候我們希望在多個對象之間共享數據,對象 a 改變了某份數據後對象 b 可以檢測到。共享數據的典型使用場景是計數,以前面的 Student 類爲例,如果我們想知道班級中共有多少名學生,就可以設置一份共享的變量,每次創建對象時讓該變量加 1。

class Student{
public:
    Student(char *name, int age, float score);
    void show();
public:
    static int m_total;  //靜態成員變量
private:
    char *m_name;
    int m_age;
    float m_score;
};

static 成員變量屬於類,不屬於某個具體的對象,即使創建多個對象,也只爲 m_total 分配一份內存,所有對象使用的都是這份內存中的數據。當某個對象修改了 m_total,也會影響到其他對象。


注意:static 成員變量的內存既不是在聲明類時分配,也不是在創建對象時分配,而是在(類外)初始化時分配。反過來說,沒有在類外初始化的 static 成員變量不能使用。

static 成員變量既可以通過對象來訪問,也可以通過類來訪問。

//通過類類訪問 static 成員變量
Student::m_total = 10;
//通過對象來訪問 static 成員變量
Student stu("小明", 15, 92.5f);
stu.m_total = 20;
//通過對象指針來訪問 static 成員變量
Student *pstu = new Student("李華", 16, 96);
pstu -> m_total = 20;

總結:

1) 一個類中可以有一個或多個靜態成員變量,所有的對象都共享這些靜態成員變量,都可以引用它。

2) static 成員變量和普通 static 變量一樣,都在內存分區中的全局數據區分配內存,到程序結束時才釋放。這就意味着,static 成員變量不隨對象的創建而分配內存,也不隨對象的銷燬而釋放內存。而普通成員變量在對象創建時分配內存,在對象銷燬時釋放內存。

3) 靜態成員變量必須初始化,而且只能在類體外進行。例如:

int Student::m_total = 10;

初始化時可以賦初值,也可以不賦值。如果不賦值,那麼會被默認初始化爲 0。全局數據區的變量都有默認的初始值 0,而動態數據區(堆區、棧區)變量的默認值是不確定的,一般認爲是垃圾值。

4) 靜態成員變量既可以通過對象名訪問,也可以通過類名訪問當通過對象名訪問時,對於不同的對象,訪問的是同一份內存。

靜態成員函數

靜態成員函數與普通成員函數的根本區別在於:普通成員函數有 this 指針,可以訪問類中的任意成員;而靜態成員函數沒有 this 指針,只能訪問靜態成員(包括靜態成員變量和靜態成員函數)。

C++ class和struct區別

cpp中保留了C語言的 struct 關鍵字,並且加以擴充。在C語言中,struct 只能包含成員變量,不能包含成員函數。而在C++中,struct 類似於 class,既可以包含成員變量,又可以包含成員函數。

C++中的 struct 和 class 基本是通用的,唯有幾個細節不同:

  • 使用 class 時,類中的成員默認都是 private 屬性的;而使用 struct 時,結構體中的成員默認都是 public 屬性的。
  • class 繼承默認是 private 繼承,而 struct 繼承默認是 public 繼承
  • class 可以使用模板,而 struct 不能

C++ 沒有拋棄C語言中的 struct 關鍵字,其意義就在於給C語言程序開發人員有一個歸屬感,並且能讓C++編譯器兼容以前用C語言開發出來的項目。

在編寫C++代碼時,我強烈建議使用 class 來定義類,而使用 struct 來定義結構體,這樣做語義更加明確。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章