天天看點

C++ 類和對象_預設成員函數

類和對象

1. 什麼是面向對象?

面向對象程式設計

概念:(Object Oriented Programming,縮寫:OOP)是一種程式設計範型,同時也是一種程式開發的方法。

對象指的是類的執行個體,将對象作為程式的基本單元,将程式和資料封裝其中,以提高軟體的重用性、靈活性和擴充性。

通俗的了解就是 , 把一種事物起一個名字(類名) , 然後把它有的各種屬性(成員變量)寫出來 ,

再把它能幹的事情(成員函數)寫出來 , 通過類名定義一個它的對象 , 然後這個對象就有這些屬性 , 就可以幹這些事情了 , 通過不同對象的組合 , 進而解決我們的問題

類 (class / struct)
{
    成員函數

    成員變量
} ;
           

2. 類的大小?為什麼要記憶體對齊?記憶體對齊的計算?空類的計算

C語言中計算結構體的大小需要記憶體對齊 , 類也一樣

對齊規則為 :

  1. 第一個成員在結構體變量偏移量為0的位址處
  2. 其他成員變量要對齊到對齊數的整數倍的位址處

對齊數 = min(編譯器預設的一個對齊數 , 該成員的大小)

VS中預設的值為 8

gcc中的預設值為 4

  1. 結構體總大小為最大對齊數(每個成員變量除了第一個成員都有一個對齊數)的整數倍
  2. 如果嵌套了結構體,嵌套的結構體對齊到自己的最大對齊數的整數倍處,結構體的總大小就是所有最大對齊數(含嵌套結構體的對齊數)的整數倍

例如 :

// gcc 對齊數 預設是 4
class A
{
public:
    char ch ; // 1 位元組
    double d ; // 8 位元組 對齊數是 4
}; // 1000 1111 1111 總大小為 12
class B
{
    char ch1 ; // 1 位元組
    A a ; // 12 位元組  最大對齊數是 4
    char ch2 ; // 1 位元組
}; // 1000 1111 1111 1111 1000 總大小是 20
class C
{
    void print()
    {
        _a = ;
        cout << _a << endl;
    }
private:
    int _a;
};
class D
{

};
void Test04()
{
    cout << sizeof(A) << endl; // 12
    cout << sizeof(B) << endl; // 20
    cout << sizeof(C) << endl; // 4
    cout << sizeof(D) << endl; // 1
}
           

那麼 , 為什麼要存在記憶體對齊呢 ? 這樣不是浪費了很多空間嗎 ?

有兩個原因

  1. 平台原因(移植原因):

    不是所有的硬體平台都能通路任意位址上的任意資料的;

    某些硬體平台隻能在某些位址處取某些特定類型的資料,否則抛出硬體異常。

  2. 性能原因:

    資料結構(尤其是棧)應該盡可能地在自然邊界上對齊。

    原因在于,為了通路未對齊的記憶體,處理器需要作兩次記憶體通路;

    而對齊的記憶體通路僅需要一次通路。

我們以為記憶體是這樣的

C++ 類和對象_預設成員函數

其實它是這樣的

C++ 類和對象_預設成員函數

CPU 把記憶體當成一塊一塊的 , 塊的大小可以是2,4,8,16位元組大小,是以CPU在讀取記憶體時是一塊一塊進行讀取的。

塊大小稱為

memory access granularity(粒度)

翻譯為 “記憶體讀取粒度”

例如 :

C++ 類和對象_預設成員函數

現在要讀取一個

int

類型的變量 , 4 位元組大小 , 如果這個變量存在 0 開始處 , 那麼一次讀 4 個 , 隻用一次就可以讀完 , 同樣如果它在 4 開始 , 也是一次讀完

但是 , 如果沒有記憶體對齊 , 它在 1 開始處 , CPU 隻能先讀取 0 ~ 3 , 再讀取 4 ~ 7 , 然後把 0 和 5 ~ 7 删除 , 得到 1 ~ 4 , 這樣才能讀出這個變量

顯然這樣效率就要差很多 , 是以記憶體對齊可以提高 CPU 讀取記憶體的速度 , 提高效率

3. 類的4個預設成員函數的詳細使用及細節

一個類 ,有 6 個預設的成員函數

  1. 構造函數
  2. 拷貝構造函數
  3. 析構函數
  4. 指派操作符的重載
  5. 取位址操作符的重載
  6. const

    修飾的取位址操作符的重載

其中最重要的是前 4 個

1. 構造函數

因為成員變量是私有的 , 無法在類外直接通路 , 是以需要一個預設的成員函數來對其進行初始化 , 并且這個預設的成員函數需要在對象被定義的時候自動執行一次 , 這個函數就叫做

構造函數 , 它有一些特點

  1. 沒有傳回值
  2. 函數名和類名相同
  3. 對象執行個體化時, 系統自動調用對應的構造函數
  4. 可以在類外定義 , 也可以在類中定義
  5. 如果類中沒有寫構造函數 , 編譯器會生成一個預設的構造函數 , 隻要我們定義了一個構造函數 , 系統就不會再生成預設構造函數
  6. 無參的構造函數和全預設的構造函數都認為是預設構造函數 , 并且預設構造函數隻能有一個
  7. 構造函數可以重載

無參的構造函數 和 有參的構造函數

class Date
{
    public :
        // 1.無參構造函數
        Date ()
        {
            cout << "Date()" << endl;
        }
        // 2.帶參構造函數
        Date (int year, int month , int day )
        {
            cout << "Date(...)" << endl;
            _year = year ;
            _month = month ;
            _day = day ;
        }
    private :
        int _year ;
        int _month ;
        int _day ;
};
void TestDate1 ()
{
    Date d1 ; // 調用無參構造函數
    Date d2 (, , ); // 調用帶參的構造函數
    // Date d3 (); // 這種寫法是錯誤的 , 這裡沒有調用 d3 的構造函數定義出 d3
}
           

帶預設參數的構造函數

class Date
{
    public :
        // 3.全預設參數的構造函數
            /*Date (int year = , int month = , int day = )
            {
                cout << "Date(.. .. ..)" << endl;
                _year = year ;
                _month = month ;
                _day = day ;
            }*/
        // 4.半預設參數的構造函數(不常用)
        Date (int year, int month = )
        {
            cout << "Date(  .. )" << endl;
            _year = year ;
            _month = month ;
            _day = ;
        }
    private :
        int _year ;
        int _month ;
        int _day ;
};
void Test()
{
    Date d1() ; // 調用半預設構造函數
    //Date d2 (2015, 2); // 調用半預設構造函數
}
           

注意 : 1. 預設參數隻能從右往左定義 2. 如果構造函數的定義和聲明分離 , 既可以在聲明中給預設參數 , 也可以在定義中給

2. 拷貝構造函數

建立對象時用同類的另一個對象來初始化 , 這是調用的構造函數稱為拷貝構造函數 , 拷貝構造函數其實就是構造函數的重載 , 有如下特點 :

  1. 必須使用引用傳參 , 如果用傳值可能引發無窮遞歸

因為傳值會發生形參到實參的拷貝 , 這個時候又會調用拷貝構造函數 , 調用拷貝構造函數又要發生形參到實參的拷貝 , 又要調拷貝構造函數 ..… 于是就會無窮遞歸

事實上 , 除了傳引用不是傳值外 , 其他傳參方式都是傳值的 , 指針也是 , 隻不過指針傳遞的是對象的位址的值 , 是以拷貝構造隻能傳引用 !

實際中 , 寫成傳值的方式也是編譯不過的

  1. 如果沒有定義拷貝構造函數 , 系統會自動生成預設的拷貝構造函數 , 它會依次拷貝類的成員進行初始化
class Date
{
public :
    Date()
    {}
    // 拷貝構造函數
    Date (const Date &d)
    {
        _year = d ._year;
        _month = d ._month;
        _day = d ._day;
    }
private :
    int _year ;
    int _month ;
    int _day ;
};
void TestDate1 ()
{
    Date d1 ;
    // 下面兩種用法都是調用拷貝構造函數,是等價的。
    Date d2 (d1); // 調用拷貝構造函數
    Date d3 = d1; // 調用拷貝構造函數
}
           

3. 析構函數

在一個對象的生命周期結束時 , 系統會自動調用一個成員函數來做一些清理工作 , 這個成員函數叫 析構函數 , 有如下特點 :

  1. 寫法 :

    ~ 類名( )

    例如

    ~Date()

  2. 沒有參數 , 沒有傳回值
  3. 在對象的聲明周期結束時自動被系統調用
  4. 如果自己沒有定義析構函數 , 系統會生成預設的析構函數
  5. 一個類中 , 析構函數隻能有一個
  6. 析構函數體内并不是删除這個對象 , 而是做一些清理工作
#include <malloc.h>
class Array
{
    public :
        Array (int size)
        {
            cout << "申請空間" << endl;
            _ptr = (int *)malloc( size * sizeof (int) );

        }
        // 這裡的析構函數需要完成的清理工作就是釋放空間
        ~ Array ()
        {
            cout << "釋放空間" << endl;
            if (_ptr )
            {
                free(_ptr );
                _ptr = ;
            }
        }
    private :
        int *_ptr ;
};

void Test05()
{
    Array arr();
}
           

4. 指派操作符的重載

為了增強程式的可讀性 , C++ 支援運算符的重載

運算符重載以後不能改變運算符的優先級 , 結合性 , 操作數

用法 :

傳回類型 operator 運算符 ()

例如 :

void operator+ ()

有 5 個運算符不能被重載 :

? :

: 條件運算符

::

: 作用域限定符

.

: 成員通路運算符

.*

: 成員指針通路運算符

sizeof

: 長度運算符
class Date
{
public :
    Date()
    {}
    // 拷貝構造函數
    Date (const Date &d)
        : _year(d._year)
        , _month(d._month)
        , _day(d._day)
    {
        cout << "拷貝構造" << endl;
    }
    // 指派操作符的重載
    // 1.為什麼 operator= 指派函數需要一個 Date& 的傳回值
    //   答 : 這樣可以不用調用拷貝構造函數 , 提高效率
    //        并且, 預設生成的拷貝構造函數是淺拷貝, 使用傳值傳回的話, 
    //        如果類中有申請釋放空間之類的操作, 就會出現問題, 
    //        比如一塊空間被釋放了兩次
    Date& operator= (const Date &d)
    {
        cout << "指派操作符的重載" << endl;
        // 2.這裡的if條件判斷是在檢查什麼?
        //   答 : 防止自己給自己指派
        if (this != &d)
        {
            this->_year = d. _year;
            this->_month = d. _month;
            this->_day = d. _day;
        }
        return *this ;
    }
private:
    int _year ;
    int _month ;
    int _day ;
};

void Test06 ()
{
    Date d1 ;
    Date d2 = d1; // 調用拷貝構造函數
    Date d3 ;
    d3 = d1 ; // 調用指派運算符的重載
}
           

當類的對象需要拷貝時,拷貝構造函數将會被調用。

以下情況都會調用拷貝構造函數 :

  1. 一個對象以值傳遞的方式傳入函數體
  2. 一個對象以值傳遞的方式從函數傳回
  3. 一個對象需要通過另外一個對象進行初始化

在類中沒有定義拷貝構造函數時 , 系統會預設生成拷貝構造函數 , 這個拷貝構造函數完成對象之間的淺拷貝

class TestCls{
    public:
        int a;
        int *p;

    public:
        TestCls(int _a = )   //無參構造函數
            :a(_a)
        {
            std::cout<<"TestCls()"<<std::endl;
            // p = new int;
        }

        ~TestCls()     //析構函數
        {
            // delete p;   
            std::cout<<"~TestCls()"<<std::endl;
        }
        void Set_a(int _a)
        {
            a = _a;
        }
        void print()
        {
            cout << a << endl;
            cout << &a << endl;
            cout << p << endl;
        }
    private:
        // 這樣這個類就不可以被拷貝了
        TestCls(const TestCls& ts)
        {}
};
void Test07()
{
    TestCls tc1();
    TestCls tc2 = tc1;
    tc1.print();
    tc2.print();
    tc2.Set_a();
    tc1.print();
    tc2.print();
}
           

成員變量的初始化有兩種方式 :

  1. 構造函數體内進行指派
  2. 初始化清單

其中 初始化清單 更加高效

用法 :

Date(int t_year, int t_month, int t_day)
    :m_year(t_year), m_month(t_month), m_day(t_day)
    {}
           

為什麼初始化清單更加高效 ?

因為即使不用初始化清單 , 這一步也會執行一次 , 是以用了初始化清單就免去了函數體内進行指派的操作 , 進而效率更高

有一些成員變量必須用初始化清單進行初始化

  1. const

    成員變量
  2. 引用類型的成員變量
  3. 沒有預設構造函數的類成員變量

注意 : 成員變量的初始化是按聲明的順序進行初始化的 , 而非初始化清單的順序

class Time
{
    public :
        Time (const Time &t)
        {
            cout << "Time (const Time& t)" << endl;
            _hour = t._hour;
            _minute = t._minute;
            _second = t._second;
        }
    private:
        int _hour ;
        int _minute ;
        int _second ;
};
class Date
{
    public :
        Date (int year, int month , int day, const Time &t)
            :_testConst(100), _testReference(_year), _t(t)
        {
            cout << "Date ()" << endl;
            _year = year ;
            _month = month ;
            _day = day ;
            _t = t ;
        }
    private :
        int _year ; // 年
        int _month ; // 月
        int _day ; // 日
        const int _testConst; // 1.測試 const 成員變量的初始化
        int &_testReference ; // 2.測試引用成員變量的初始化
        Time _t ; // 3.測試無預設構造函數的成員變量的初始化
};

void Test08()
{
    Time tm;
    Date dt(, , , tm);
}
           

繼續閱讀