C++基礎-類和對象

本文爲 C++ 學習筆記,參考《Sams Teach Yourself C++ in One Hour a Day》第 8 版、《C++ Primer》第 5 版、《代碼大全》第 2 版。ios

面向對象編程有四個重要的基礎概念:抽象封裝繼承多態。本文整理 C++ 中類與對象的基礎內容,涉及抽象封裝兩個概念。《C++基礎-繼承》一文講述繼承概念。《C++基礎-多態》一文講述多態概念。這些內容是 C++ 中最核心的內容。程序員

抽象編程

抽象是一種忽略個性細節、提取共性特徵的過程。當用「房子」指代由玻璃、混凝土、木材組成的建築物時就是在使用抽象。當把鳥、魚、老虎等稱做「動物」時,也是在使用抽象。ide

基類是一種抽象,可讓用戶關注派生類的共同特性而忽略各派生類的細節。類也是一種抽象,用戶能夠關注類的接口自己而忽視類的內部工做方式。函數接口、子系統接口都是抽象,各自位於不一樣的抽象層次,不一樣的抽象層次關注不一樣的內容。函數

抽象能令人以一種簡化的觀點來考慮複雜的概念,忽略繁瑣的細節能大大下降思惟及實現的複雜度。若是咱們在看電視前要去關注塑料分子、琉璃分子、金屬原子是如何組成一部電視機的、電與磁的原理是什麼、圖像是如何產生的,那這個電視不用看了。咱們只是要用一臺電視,而不關心它是怎麼實現的。同理,軟件設計中,若是不使用各類抽象層次,那麼這一堆代碼將變得沒法理解沒法維護甚至根本沒法設計出來。性能

封裝學習

抽象是從一種高層的視角來看待一個對象。而封裝則是,除了那個抽象的簡化視圖外,不能讓你看到任何其餘細節。簡言之,封裝就是隱藏實現細節,只讓你看到想給你看的。this

在程序設計中,就是把類的成員(屬性和行爲)進行整合和分類,肯定哪些成員是私有的,哪些成員是公共的,私有成員隱藏,公共成員開放。類的用戶(調用者)只能訪問類的公共接口。spa

1. 類與對象

// 類:人類
class Human
{
pubilc:
    // 成員方法:
    void Talk(string textToTalk);   // 說話
    void IntroduceSelf();           // 自我介紹

private:
    // 成員屬性:
    string name;                    // 姓名
    string dateOfBirth;             // 生日
    string placeOfBirth;            // 出生地
    string gender;                  // 性別

    ...
};

// 對象:具體的某我的
Human xiaoMing;
Human xiaoFang;

對象是類的實例。語句 Human xiaoMing;int a; 本質上並沒有不一樣,對象和類的關係,等同於變量和類型的關係。設計

不介意外部知道信息使用 public 關鍵字限定,須要保密的信息使用 private 關鍵字限定。

2. 構造函數

2.1 構造函數

構造函數在建立對象時被調用。執行初始化操做。

  1. 構造函數名字與類名相同
  2. 構造函數無返回值
  3. 構造函數能夠重載,一個類可有多個構造函數
  4. 構造函數不能被聲明爲 const,由於一個 const 對象也是經過構造函數完成初始化的,構造函數完成初始化以後,const 對象才真正取得"常量"屬性。

構造函數形式以下:

class Human
{
public:
    Human();    // 構造函數聲明
};

Human::Human()  // 構造函數實現(定義)
{
    ...
}

2.2 默認構造函數

可不提供實參調用的構造函數是默認構造函數,包括以下兩種:
1) 不帶任何函數形參的構造函數是默認構造函數
2) 帶有形參但全部形參都提供默認值的構造函數也是默認構造函數,由於這種既能夠攜帶實參調用,也能夠不帶實參調用

2.3 合成的默認構造函數

當用戶未給出任何構造函數時,編譯器會自動生成一個構造函數,叫做合成的默認構造函數,此函數對類的數據成員初始化規則以下:
1) 若數據成員存在類內初始化值,則用這個初始化值來初始化數據成員
2) 不然,執行默認初始化。默認值由數據類型肯定。參"C++ Primer 5th"第 40 頁

下面這個類由於沒有任何構造函數,因此編譯器會生成合成的默認構造函數:

class Human
{
pubilc:
    // 成員方法:
    void Talk(string textToTalk);   // 說話
    void IntroduceSelf();           // 自我介紹

private:
    // 成員屬性:
    string name;                    // 姓名
    string dateOfBirth;             // 生日
    string placeOfBirth;            // 出生地
    string gender;                  // 性別
};

2.4 參數帶默認值的構造函數

函數能夠有帶默認值的參數,構造函數固然也能夠。

class Human
{
private:
    string name;
    int age;

public:
    // overloaded constructor (no default constructor)
    Human(string humansName, int humansAge = 25)
    {
        name = humansName;
        age = humansAge;
        ...
    };

可使用以下形式的實例化

Human adam("Adam"); // adam.age is assigned a default value 25
Human eve("Eve, 18); // eve.age is assigned 18 as specified

2.5 帶初始化列表的構造函數

初始化列表是一種簡寫形式,將相關數據成員的初始化列表寫在函數名括號後,從而能夠省略函數體中的相應數據成員賦值語句。

Human::Human(string humansName, int humansAge) : name(humansName), age(humansAge)
{
}

上面這種寫法和下面這種寫法具備一樣的效果:

Human::Human(string humansName, int humansAge)
{
    name = humansName; 
    age = humansAge;
}

2.6 複製構造函數

2.6.1 淺複製及其問題

複製一個類的對象時,只複製其指針成員但不復制指針指向的緩衝區,其結果是兩個對象指向同一塊動態分配的內存。銷燬其中一個對象時,delete[] 釋放這個內存塊,致使另外一個對象存儲的指針拷貝無效。這種複製被稱爲淺複製。

以下爲淺複製的一個示例程序:

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

class MyString
{
private:
    char* buffer;

public:
    MyString(const char* initString)
    {
        buffer = NULL;
        if(initString != NULL)
        {
            buffer = new char [strlen(initString) + 1];
            strcpy(buffer, initString);
        }
    }

    ~MyString()
    {
        cout << "Invoking destructor, clearing up" << endl;
        delete [] buffer;
    }

    int GetLength() { return strlen(buffer); }
    const char* GetString() { return buffer; }
};

void UseMyString(MyString str)
{
    cout << "String buffer in MyString is " << str.GetLength();
    cout << " characters long" << endl;

    cout << "buffer contains: " << str.GetString() << endl;
    return;
}

int main()
{
    MyString sayHello("Hello from String Class");
    UseMyString(sayHello);

    return 0;
}

分析一下 UseMyString(sayHello); 這一語句:

  1. 執行對象淺複製,將實參 sayHello 複製給形參 str,複製了數據成員(指針)的值,但未複製成員指向的緩衝區。所以兩份對象拷貝的指針數據成員(char *buffer)指向同一內存區。
  2. UseMyString() 返回時,str 析構,內存區被回收
  3. main() 返回時,sayHello 析構,再次回收內存區,致使段錯誤

2.6.2 複製構造函數:確保深複製

複製構造函數是一個重載的構造函數,由編寫類的程序員提供。每當對象被複制時,編譯器都將調用複製構造函數。

複製構造函數函數語法以下:

class MyString
{
    MyString(const MyString& copySource); // copy constructor
};

MyString::MyString(const MyString& copySource)
{
    // Copy constructor implementation code
}

複製構造函數接受一個以引用方式傳入的當前類的對象做爲參數。這個參數是源對象的別名,您使用它來編寫自定義的複製代碼,確保對全部緩衝區進行深複製。

複製構造函數的參數必須按引用傳遞,不然複製構造函數將不斷調用本身,直到耗盡系統的內存爲止。緣由就是每當對象被複制時,編譯器都將調用複製構造函數,若是參數不是引用,實參不斷複製給形參,將生成不斷複製不斷調用複製構造函數。

示例程序以下:

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

class MyString
{
private:
    char* buffer;

public:
    MyString() {}
    MyString(const char* initString) // constructor
    {
        buffer = NULL;
        cout << "Default constructor: creating new MyString" << endl;
        if(initString != NULL)
        {
            buffer = new char [strlen(initString) + 1];
            strcpy(buffer, initString);

            cout << "buffer points to: 0x" << hex;
            cout << (unsigned int*)buffer << endl;
        }
    }

    MyString(const MyString& copySource) // Copy constructor
    {
        buffer = NULL;
        cout << "Copy constructor: copying from MyString" << endl;
        if(copySource.buffer != NULL)
        {
            // allocate own buffer 
            buffer = new char [strlen(copySource.buffer) + 1];

            // deep copy from the source into local buffer
            strcpy(buffer, copySource.buffer);

            cout << "buffer points to: 0x" << hex;
            cout << (unsigned int*)buffer << endl;
        }
    }

    // Destructor
    ~MyString()
    {
        cout << "Invoking destructor, clearing up" << endl;
        delete [] buffer;
    }

    int GetLength() 
    { return strlen(buffer); }

    const char* GetString()
    { return buffer; }
};

void UseMyString(MyString str)
{
    cout << "String buffer in MyString is " << str.GetLength();
    cout << " characters long" << endl;

    cout << "buffer contains: " << str.GetString() << endl;
    return;
}

int main()
{
    MyString sayHello("Hello from String Class");
    UseMyString(sayHello);

    return 0;
}

再看 UseMyString(sayHello); 這一語句:

  1. 每當對象被複制時,編譯器都將調用複製構造函數。將實參複製給形參時,編譯器就會調用複製構造函數。
  2. 因此這裏的 str 是經過調用複製構造函數進行的初始化,對實參進行了深複製。形參與實參中的指針成員各指向本身的緩衝區。
  3. 因此析構是正常的,示例程序運行沒有問題。

一樣,若是沒有提供複製賦值運算符 operator=,編譯器提供的默認複製賦值運算符將致使淺複製。

關於複製構造函數的注意事項以下:

  1. 類包含原始指針成員(char *等)時,務必編寫複製構造函數和複製賦值運算符。
  2. 編寫複製構造函數時,務必將接受源對象的參數聲明爲 const 引用。
  3. 聲明構造函數時務必考慮使用關鍵字 explicit,以免隱式轉換。
  4. 務必將類成員聲明爲 std::string 和智能指針類(而不是原始指針),由於它們實現了複製構造函數,可減小您的工做量。除非萬不得已,不要類成員聲明爲原始指針。

2.6.3 移動構造函數:改善性能

class MyString
{
    // 代碼同上一示例程序,此處略
};

MyString Copy(MyString& source)
{
    MyString copyForReturn(source.GetString()); // create copy
    return copyForReturn;                       // 1. 將返回值複製給調用者,首次調用複製構造函數
}

int main()
{
    MyString sayHello ("Hello World of C++");
    MyString sayHelloAgain(Copy(sayHello));     // 2. 將 Copy() 返回值做實參,再次調用複製構造函數
    return 0;
}

上例中,參考註釋,實例化 sayHelloAgain 對象時,複製構造函數被調用了兩次。若是對象很大,兩次複製形成的性能影響不容忽視。

爲避免這種性能瓶頸, C++11 引入了移動構造函數。移動構造函數的語法以下:

// move constructor
MyString(MyString&& moveSource)
{
    if(moveSource.buffer != NULL)
    {
        buffer = moveSource.buffer;     // take ownership i.e. 'move'
        moveSource.buffer = NULL;       // set the move source to NULL
    }
}

有移動構造函數時,編譯器將自動使用它來「移動」臨時資源,從而避免深複製。增長移動構造函數後,上一示例中,將首先調用移動構造函數,而後調用複製構造函數,複製構造函數只被會調用一次。

3. 析構函數

析構函數在對象銷燬時被調用。執行去初始化操做。

  1. 析構函數只能有一個,不能被重載。
  2. 若用戶未提供析構函數,編譯器會生成一個僞析構函數,可是這個僞析構函數是空的,不會釋放堆內存。

每當對象再也不在做用域內或經過 delete 被刪除進而被銷燬時,都將調用析構函數。這使得析構函數成爲重置變量以及釋放動態分配的內存和其餘資源的理想場所。

4. 構造函數與析構函數的其餘用途

4.1 不容許複製的類

假設要模擬國家政體,一個國家只能一位總統,President 類的對象不容許複製。

要禁止類對象被複制,可將複製構造函數聲明爲私有的。爲禁止賦值,可將賦值運算符聲明爲私有的。複製構造函數和賦值運算符聲明爲私有的便可,不須要實現。這樣,若是代碼中有對對象的複製或賦值,將沒法編譯經過。形式以下:

class President
{
private:
    President(const President&);              // private copy constructor
    President& operator= (const President&);  // private copy assignment operator
    // … other attributes
};

4.2 只能有一個實例的單例類

前面討論的 President 不能複製,不能賦值,但存在一個缺陷:沒法禁止經過實例化多個對象來建立多名總統:

President One, Two, Three;

要確保一個類不能有多個實例,也就是單例的概念。實現單例,要使用私有構造函數、私有賦值運算符和靜態實例成員。

將關鍵字 static 用於類的數據成員時,該數據成員將在全部實例之間共享。
將關鍵字 static 用於成員函數(方法)時,該方法將在全部成員之間共享。
將 static 用於函數中聲明的局部變量時,該變量的值將在兩次調用之間保持不變。

4.3 禁止在棧中實例化的類

將析構函數聲明爲私有的。略

4.4 使用構造函數進行類型轉換

5. this 指針

在類中,關鍵字 this 包含當前對象的地址,換句話說, 其值爲&object。在類成員方法中調用其餘成員方法時, 編譯器將隱式地傳遞 this 指針。

調用靜態方法時,不會隱式地傳遞 this 指針,由於靜態函數不與類實例相關聯,而由全部實例共享。要在靜態函數中使用實例變量,應顯式地聲明一個形參,並將實參設置爲 this 指針。

6. sizeof 用於類

sizeof 用於類時,值爲類聲明中全部數據屬性佔用的總內存量,單位爲字節。是否考慮對齊,與編譯器有關。

7. 結構與類的不一樣之處

結構 struct 與類 class 很是類似,差異在於程序員未指定時,默認的訪問限定符(public 和 private)不一樣。所以,除非指定了,不然結構中的成員默認爲公有的(而類成員默認爲私有的);另外,除非指定了,不然結構以公有方式繼承基結構(而類爲私有繼承)。

相關文章
相關標籤/搜索