首頁 > 軟體

C++詳細講解物件的構造

2022-04-19 19:00:04

一、物件的構造(上)

1.1 物件的初始值

問題:物件中成員變數的初始值是多少?

下面的類定義中成員變數 i 和 j 的初始值為多少?

下面看一段成員變數初始值的程式碼:

#include<stdio.h>
 
class Test
{
    private:
        int i;
        int j;
    public:
        int getI() {return i;}
        int getJ() {return j;}
};
 
Test gt;
 
int main()
{
    printf("gt.i = %dn", gt.getI());
    printf("gt.j = %dn", gt.getJ());
    
    Test t1;
    
    printf("t1.i = %dn", t1.getI());
    printf("t1.j = %dn", t1.getJ());
    
    Test* pt = new Test;
    
    printf("pt->i = %dn", pt->getI());
    printf("pt->j = %dn", pt->getJ());   
    
    delete pt;
    
    return 0;
}

下面為輸出結果:

物件t1 所佔用的儲存空間在棧上面,而且成員變數 i 和 j 也沒有明確的初始值,所以初始值就不定。物件 gt 所佔用的儲存空間在全域性資料區,所以初始值統一為 0。

Test* pt = new Test;意味著在堆空間中生成一個 Test 物件,雖然 pt->i 和 pt->j 均為 0,這只是巧合罷了,因為在堆上建立物件時,成員變數初始為隨機值。

注:類得到的其實是資料型別,所以說通過這種資料型別在全域性資料區、棧和堆上面都能夠生成物件。

1.2 物件的初始化

從程式設計的角度,物件只是變數,因此:

  • 在棧上建立物件時,成員變數初始為隨機值
  • 在堆上建立物件時,成員變數初始為隨機值
  • 在靜態儲存區建立物件時,成員變數初始為 0 值

生活中的物件都是在初始化後上市的

初始狀態(出廠設定)是物件普遍存在的一個狀態

—股而言,物件都需要—個確定的初始狀態

解決方案

  • 在類中提供一個 public 的 initialize 函數
  • 物件建立後立即呼叫 initialize 函數進行初始化

如下:

下面看一段初始化函數的程式碼:

#include<stdio.h>
 
class Test
{
    private:
        int i;
        int j;
    public:
        int getI() {return i;}
        int getJ() {return j;}
        void initialize()
        {
            i = 1;
            j = 2;
        }
};
 
Test gt;
 
int main()
{
    gt.initialize();
    
    printf("gt.i = %dn", gt.getI());
    printf("gt.j = %dn", gt.getJ());
    
    Test t1;
    
    t1.initialize();
    
    printf("t1.i = %dn", t1.getI());
    printf("t1.j = %dn", t1.getJ());
    
    Test* pt = new Test;
    
    pt->initialize();
    
    printf("pt->i = %dn", pt->getI());
    printf("pt->j = %dn", pt->getJ());   
    
    delete pt;
    
    return 0;
}

下面為輸出結果:

存在的問題

  • initialize 只是一個普通函數,必須顯示呼叫
  • 如果未呼叫 initialize 函數,執行結果是不確定的

下面為解決辦法:

C++中可以定義與類名相同的特殊成員函數

這種特殊的成員函數叫做建構函式

  • 構造沒有任何返回型別的宣告
  • 建構函式在物件定義時自動被呼叫

下面來體驗一下建構函式:

#include<stdio.h>
 
class Test
{
    private:
        int i;
        int j;
    public:
        int getI() {return i;}
        int getJ() {return j;}
        Test()
        {
            printf("Test() Beginn");
        
            i = 1;
            j = 2;
        
            printf("Test() Endn");
        }
};
 
Test gt;
 
int main()
{
    printf("gt.i = %dn", gt.getI());
    printf("gt.j = %dn", gt.getJ());
    
    Test t1;
    
    printf("t1.i = %dn", t1.getI());
    printf("t1.j = %dn", t1.getJ());
    
    Test* pt = new Test;
    
    printf("pt->i = %dn", pt->getI());
    printf("pt->j = %dn", pt->getJ());   
    
    delete pt;
    
    return 0;
}

下面為輸出結果:

可以看到,Test() Begin 和 Test() End 出現了三次,也就是說,Test() 這個建構函式被呼叫了三次,這是因為建立了三個物件。

1.3 小結

  • 每個物件在使用之前都應該初始化
  • 類別建構函式用於物件的初始化
  • 建構函式與類同名並且沒有返回值
  • 建構函式在物件定義時自動被呼叫

二、物件的構造(中)

2.1 建構函式

帶有引數的建構函式

  • 建構函式可以根據需要定義引數
  • 一個類中可以存在多個過載的建構函式
  • 建構函式的過載遵循 C++ 過載的規則

如下:

友情提醒

物件定義和物件宣告不同

  • 物件定義--申請物件的空間並呼叫建構函式
  • 物件宣告--告訴編譯器存在這樣一個物件

如下:

建構函式的自動呼叫

如下:

下面看一段帶引數的建構函式的程式碼:

#include <stdio.h>
 
class Test
{
    public:
        Test() 
        { 
            printf("Test()n");
        }
        Test(int v) 
        { 
            printf("Test(int v), v = %dn", v);
        }
};
 
int main()
{
    Test t;      // 呼叫 Test()
    Test t1(1);  // 呼叫 Test(int v)
    Test t2 = 2; // 呼叫 Test(int v)
    
    return 0;
}

下面為輸出結果,和預想中的一致。

這裡需要明確一個問題,int i = 1;與 int i; i = 1;的不同。前者是初始化,後者是先定義,再賦值。後者由於定義 i 時沒有初始化,所以 i 的值時隨機的。C語言中這兩者差別很小,但是在 C++ 中兩者差異很大。差別在於在 C++ 中初始化會呼叫建構函式。下面看一個例子,在上述程式碼的基礎上加一行程式碼 t = t2;

#include <stdio.h>
 
class Test
{
    public:
        Test() 
        { 
            printf("Test()n");
        }
        Test(int v) 
        { 
            printf("Test(int v), v = %dn", v);
        }
};
 
int main()
{
    Test t;      // 呼叫 Test()
    Test t1(1);  // 呼叫 Test(int v)
    Test t2 = 2; // 呼叫 Test(int v)
    
    t = t2;
    
    return 0;
}

下面為輸出結果,可以看到與上面的程式碼輸出結果一模一樣。這就因為 C++ 中初始化和賦值不同,初始化會呼叫建構函式,賦值的時候則不用。

下面再看一個例子:

#include <stdio.h>
 
class Test
{
    public:
        Test() 
        { 
            printf("Test()n");
        }
        Test(int v) 
        { 
            printf("Test(int v), v = %dn", v);
        }
};
 
int main()
{
    Test t;      // 呼叫 Test()
    Test t1(1);  // 呼叫 Test(int v)
    Test t2 = 2; // 呼叫 Test(int v)
    
    int i(100);
    
    printf("i = %dn", i);
    
    return 0;
}

下面為輸出結果:

建構函式的呼叫

  • 一般情況下,建構函式在物件定義時被自動呼叫
  • —些特殊情況下,需要手工呼叫建構函式

下面看一段建構函式手動呼叫的程式碼:

#include <stdio.h>
 
class Test
{
    private:
        int m_value;
    public:
        Test() 
        { 
            printf("Test()n");
        
            m_value = 0;
        }
        
        Test(int v) 
        { 
            printf("Test(int v), v = %dn", v);
        
            m_value = v;
        }
        
        int getValue()
        {
            return m_value;
        }
};
 
int main()
{
    Test ta[3] = {Test(), Test(1), Test(2)};      
    
    for (int i = 0; i < 3; i++)
    {
        printf("ta[%d].getValue() = %dn", i, ta[i].getValue());
    }
    
    Test t = Test(100);
    
    printf("t.getValue() = %dn", t.getValue());
    
    return 0;
}

下面為輸出結果,可以看到,Test(1)、Test(2) 和 Test(100) 均為手動呼叫建構函式。

2.2小范例

需求:開發一個陣列類解決原生陣列的安全性問題

  • 提供函數獲取陣列長度
  • 提供函數獲取陣列元素
  • 提供函數設定陣列元素

IntArray.h:

#ifndef _INTARRAY_H_
 
#define _INTARRAY_H_

class IntArray
 
{
    private:
        int m_length;
 
        int* m_pointer;
    public:
        IntArray(int len);
 
        int length();
 
        bool get(int index, int& value);
 
        bool set(int index ,int value);
 
        void free();
};
#endif

IntArray.cpp:

#include "IntArray.h"
 
 
 
IntArray::IntArray(int len)
 
{
 
    m_pointer = new int[len];
 
    
 
    for (int i = 0; i < len; i++)
 
    {
 
        m_pointer[i] = 0;
 
    }
 
    
 
    m_length = len;
 
}
 
 
 
int IntArray::length()
 
{
 
    return m_length;
 
}
 
 
 
bool IntArray::get(int index, int& value)
 
{
 
    bool ret = (0 <= index) && (index < length());
 
    
 
    if( ret )
 
    {
 
        value = m_pointer[index];
 
    }
 
    
 
    return ret;
 
}
 
 
 
bool IntArray::set(int index, int value)
 
{
 
    bool ret = (0 <= index) && (index < length());
 
    
 
    if( ret )
 
    {
 
        m_pointer[index] = value;
 
    }
 
    
 
    return ret;
 
}
 
 
 
void IntArray::free()
 
{
 
    delete[]m_pointer;
 
}

main.cpp:

#include <stdio.h>
 
#include "IntArray.h"
 
 
 
int main()
 
{
 
    IntArray a(5);    
 
    
 
    for (int i = 0; i < a.length(); i++)
 
    {
 
        a.set(i, i + 1);
 
    }
 
    
 
    for (int i = 0; i < a .length(); i++)
 
    {
 
        int value = 0;
 
        
 
        if( a.get(i, value) )
 
        {
 
            printf("a[%d] = %dn", i, value);
 
        }
 
    }
 
    
 
    a.free();
 
    
 
    return 0;
 
}

下面為輸出結果:

這樣寫出來的陣列很安全,沒有陣列越界問題。

2.3 小結

  • 建構函式可以根據需要定義引數
  • 建構函式之間可以存在過載關係
  • 建構函式遵循 C++ 中過載函數的規則
  • 物件定義時會觸發建構函式的呼叫
  • 在一些情況下可以手動呼叫建構函式

三、物件的構造(下)

3.1 特殊的建構函式

兩個特殊的建構函式

無參建構函式

  • 沒有引數的建構函式
  • 當類中沒有定義建構函式時,編譯器預設提供一個無參建構函式,並且其函數體為空

拷貝建構函式

  • 引數為 const class_name& 的建構函式
  • 當類中沒有定義拷貝建構函式時,編譯器預設提供一個拷貝建構函式,簡單的進行成員變數的值複製

下面看一段無引數建構函式的程式碼(程式碼3-1):

#include <stdio.h>
 
class Test
{
    private:
        int i;
        int j;
    public:
        int getI()
        {
            return i;
        }
        int getJ()
        {
            return j;
        }
};
 
int main()
{
    Test t;
    
    return 0;
}

可以看到,編譯通過:

建立一個類的物件必須要呼叫建構函式,為什麼能夠編譯通過呢?這是因為編譯器在發現我們沒有定義建構函式時,會預設提供一個無參建構函式,等效如(程式碼3-2):

#include <stdio.h>
 
class Test
{
    private:
        int i;
        int j;
    public:
        int getI()
        {
            return i;
        }
        int getJ()
        {
            return j;
        }
        Test()
        {
        }
};
 
int main()
{
    Test t;
    
    return 0;
}

小貼士:所以說,class T { }; 裡面不是什麼都沒有,裡面至少有一個無參建構函式。

下面再來看一段程式碼(程式碼3-3):

#include <stdio.h>
 
class Test
{
    private:
        int i;
        int j;
    public:
        int getI()
        {
            return i;
        }
        int getJ()
        {
            return j;
        }
};
 
int main()
{
    Test t1;
    Test t2 = t1;
    
    printf("t1.i = %d, t1.j = %dn", t1.getI(), t1.getJ());
    printf("t2.i = %d, t2.j = %dn", t2.getI(), t2.getJ());
    
    return 0;
}

下面為輸出結果:

這裡的 i 和 j 列印出來的都是隨機值,這是因為類裡面沒有手工編寫的建構函式,所以 t1 和 t2 所採用的就是編譯器提供的預設無參建構函式構造的,編譯器提供的無參建構函式為空,所以 i 和 j 的值就是隨機的。

上述程式碼就相當於(程式碼3-4):

#include <stdio.h>
 
class Test
{
    private:
        int i;
        int j;
    public:
        int getI()
        {
            return i;
        }
        int getJ()
        {
            return j;
        }
        Test(const Test& t)
        {
            i = t.i;
            j = t.j;        
        }
};
 
int main()
{
    Test t1;
    Test t2 = t1;
    
    printf("t1.i = %d, t1.j = %dn", t1.getI(), t1.getJ());
    printf("t2.i = %d, t2.j = %dn", t2.getI(), t2.getJ());
    
    return 0;
}

但是編譯的時候會報錯:

這是因為在類裡面沒有編寫任何建構函式時,編譯器才提供預設的無參建構函式。這裡手工編寫了一個拷貝建構函式,編譯器就不會提供預設的無參建構函式,需要自己把無參建構函式加上。

如下,自己加上無參建構函式(程式碼3-5):

#include <stdio.h>
class Test
{
    private:
        int i;
        int j;
    public:
        int getI()
        {
            return i;
        }
        int getJ()
        {
            return j;
        }
        Test(const Test& t)
        {
            i = t.i;
            j = t.j;        
        }
        Test()
        {
        }
};
 
int main()
{
    Test t1;
    Test t2 = t1;
    
    printf("t1.i = %d, t1.j = %dn", t1.getI(), t1.getJ());
    printf("t2.i = %d, t2.j = %dn", t2.getI(), t2.getJ());
    
    return 0;
}

這樣就能編譯通過了,而且效果跟程式碼3-3的相同:

3.2 拷貝建構函式

拷貝建構函式的意義

相容C語言的初始化方式

初始化行為能夠符合預期的邏輯

淺拷貝

  • 拷貝後物件的物理狀態相同(物理狀態指的是物件佔據的記憶體當中每個位元組是否相等,如程式碼3-6)

深拷貝

  • 拷貝後物件的邏輯狀態相同(邏輯狀態指的是指標所指向的記憶體空間的值是否相同,如程式碼3-9)

注:編譯器提供的拷貝建構函式只進行淺拷貝!

下面看一段程式碼(程式碼3-6):

#include <stdio.h>
 
class Test
{
    private:
        int i;
        int j;
        int* p;
    public:
        int getI()
        {
            return i;
        }
        int getJ()
        {
            return j;
        }
        int* getP()
        {
            return p;
        }
        Test(int v)
        {
            i = 1;
            j = 2;
            p = new int;
        
            *p = v;
        }
 
};
 
int main()
{
    Test t1(3);
    Test t2 = t1;
    
    printf("t1.i = %d, t1.j = %d, t1.p = %pn", t1.getI(), t1.getJ(), t1.getP());
    printf("t2.i = %d, t2.j = %d, t2.p = %pn", t2.getI(), t2.getJ(), t2.getP());
    
    return 0;
}

下面為輸出結果:

這段程式的第一個問題就是 t1 和 t2 的 p 指標都指向同一個堆空間中的地址,第二個問題就是申請了記憶體並沒有釋放,會造成記憶體漏失。

下面加上釋放記憶體的程式碼(程式碼3-7):

#include <stdio.h>
 
class Test
{
    private:
        int i;
        int j;
        int* p;
    public:
        int getI()
        {
            return i;
        }
        int getJ()
        {
            return j;
        }
        int* getP()
        {
            return p;
        }
        Test(int v)
        {
            i = 1;
            j = 2;
            p = new int;
        
            *p = v;
        }
        void free()
        {
            delete p;
        }
 
};
 
int main()
{
    Test t1(3);
    Test t2 = t1;
    
    printf("t1.i = %d, t1.j = %d, t1.p = %pn", t1.getI(), t1.getJ(), t1.getP());
    printf("t2.i = %d, t2.j = %d, t2.p = %pn", t2.getI(), t2.getJ(), t2.getP());
    
    t1.free();
    t2.free();
    
    return 0;
}

下面為輸出結果,編譯能通過,但是執行時發生了錯誤,釋放了兩次堆空間的記憶體:

下面為解決方法(程式碼3-8):

#include <stdio.h>
 
class Test
{
    private:
        int i;
        int j;
        int* p;
    public:
        int getI()
        {
            return i;
        }
        
        int getJ()
        {
            return j;
        }
        
        int* getP()
        {
            return p;
        }
        
        Test(const Test& t)
        {
            i = t.i;
            j = t.j;
            p = new int;
            
            *p = *t.p;
        }
        
        Test(int v)
        {
            i = 1;
            j = 2;
            p = new int;
        
            *p = v;
        }
        
        void free()
        {
            delete p;
        }
 
};
 
int main()
{
    Test t1(3);
    Test t2(t1);
    
    printf("t1.i = %d, t1.j = %d, t1.p = %pn", t1.getI(), t1.getJ(), t1.getP());
    printf("t2.i = %d, t2.j = %d, t2.p = %pn", t2.getI(), t2.getJ(), t2.getP());
    
    t1.free();
    t2.free();
    
    return 0;
}

下面為輸出結果,可以到 t1 和 t2 的 p 指標分別指向不同的堆空間地址:

如果我們看一下邏輯狀態,也就是 *t1.p 和 *t2.p 的值,程式碼如下(程式碼3-9):

#include <stdio.h>
 
class Test
{
    private:
        int i;
        int j;
        int* p;
    public:
        int getI()
        {
            return i;
        }
        
        int getJ()
        {
            return j;
        }
        
        int* getP()
        {
            return p;
        }
        
        Test(const Test& t)
        {
            i = t.i;
            j = t.j;
            p = new int;
            
            *p = *t.p;
        }
        
        Test(int v)
        {
            i = 1;
            j = 2;
            p = new int;
        
            *p = v;
        }
        
        void free()
        {
            delete p;
        }
 
};
 
int main()
{
    Test t1(3);
    Test t2(t1);
    
    printf("t1.i = %d, t1.j = %d, t1.p = %pn", t1.getI(), t1.getJ(), t1.getP());
    printf("t2.i = %d, t2.j = %d, t2.p = %pn", t2.getI(), t2.getJ(), t2.getP());
    
    t1.free();
    t2.free();
    
    return 0;
}

下面為輸出結果,可以看到 *t1.p 和 *t2.p 的值相同,也就是說邏輯狀態相同,這就叫做深拷貝。

什麼時候需要進行深拷貝?

物件中有成員指代了系統中的資源

  • 成員指向了動態記憶體空間
  • 成員開啟了外存中的檔案
  • 成員使用了系統中的網路埠
  • ......

問題分析

下面就是淺拷貝:

一般性原則

自定義拷貝建構函式,必然需要實現深拷貝!!!

下面看一個使用深拷貝,對前面陣列的程式碼進行改造。

IntArray.h:

#ifndef _INTARRAY_H_
 
#define _INTARRAY_H_
class IntArray
 
{
 
    private:
 
        int m_length;
 
        int* m_pointer;
 
    public:
 
        IntArray(int len);
 
        IntArray(const IntArray& obj);
 
        int length();
 
        bool get(int index, int& value);
 
        bool set(int index ,int value);
 
        void free();
 
};
#endif

IntArray.cpp:

#include "IntArray.h"
 
 
 
IntArray::IntArray(int len)
 
{
 
    m_pointer = new int[len];
 
    
 
    for (int i = 0; i < len; i++)
 
    {
 
        m_pointer[i] = 0;
 
    }
 
    
 
    m_length = len;
 
}
 
 
 
IntArray::IntArray(const IntArray& obj)
 
{
 
    m_length = obj.m_length;
 
    
 
    m_pointer = new int[obj.m_length];
 
    
 
    for (int i = 0; i < obj.m_length; i++)
 
    {
 
        m_pointer[i] = obj.m_pointer[i];
 
    }
 
}
 
 
 
int IntArray::length()
 
{
 
    return m_length;
 
}
 
 
 
bool IntArray::get(int index, int& value)
 
{
 
    bool ret = (0 <= index) && (index < length());
 
    
 
    if( ret )
 
    {
 
        value = m_pointer[index];
 
    }
 
    
 
    return ret;
 
}
 
 
 
bool IntArray::set(int index, int value)
 
{
 
    bool ret = (0 <= index) && (index < length());
 
    
 
    if( ret )
 
    {
 
        m_pointer[index] = value;
 
    }
 
    
 
    return ret;
 
}
 
 
 
void IntArray::free()
 
{
 
    delete[]m_pointer;
 
}

main.cpp:

#include <stdio.h>
 
#include "IntArray.h"
 
 
 
int main()
 
{
 
    IntArray a(5);    
 
    
 
    for (int i = 0; i < a.length(); i++)
 
    {
 
        a.set(i, i + 1);
 
    }
 
    
 
    for (int i = 0; i < a.length(); i++)
 
    {
 
        int value = 0;
 
        
 
        if( a.get(i, value) )
 
        {
 
            printf("a[%d] = %dn", i, value);
 
        }
 
    }
 
    
 
    IntArray b = a;
 
    
 
    for (int i = 0; i < b.length(); i++)
 
    {
 
        int value = 0;
 
        
 
        if( b.get(i, value) )
 
        {
 
            printf("b[%d] = %dn", i, value);
 
        }
 
    }
 
    
 
    a.free();
 
    b.free();
 
    
 
    return 0;
 
}

下面為輸出結果:

可以看到 b 陣列裡面的元素與 a 陣列裡面的元素相同,這就是深拷貝建構函式的結果。

3.3 小結

C++ 編譯器會預設提供建構函式

無參建構函式用於定義物件的預設初始狀態

拷貝建構函式在建立物件時拷貝物件的狀態

物件的拷貝有淺拷貝和深拷貝兩種方式

  • 淺拷貝使得物件的物理狀態相同
  • 深拷貝使得物件的邏輯狀態相同

到此這篇關於C++詳細講解物件的構造的文章就介紹到這了,更多相關C++ 物件的構造內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com