C++筆記(3):封裝篇(下)

封裝篇(下)

1.對象成員的小總結

  1. 實例化對象A時,如果對象A有對象成員B,那麼先執行對象B的構造函數,再執行A的構造函數。

  2. 如果對象A中有對象成員B,那麼銷燬對象A時,先執行對象A的析構函數,再執行B的析構函數。

  3. 如果對象A中有對象成員B,對象B沒有默認構造函數,那麼對象A必須在初始化列表中初始化對象B。

2.拷貝構造函數-淺拷貝與深拷貝

淺拷貝1:

Array.h

class Array {
public:
    // 構造&拷貝構造&析構
    Array();
    Array(const Array&arr);
    ~Array();
	// 屬性操作方法
    void setCount(int count);
    int getCount();
private:
    int m_iCount;
};

Array.c

// 構造&拷貝構造
Array::Array() {
    cout << "Array" << endl;
}
Array::Array(const Array &arr) {
    m_iCount = arr.m_iCount;
    cout << "Array &" << endl;
}
// 析構
Array::~Array(){
    cout << "~Array" << endl;
}

// 屬性操作方法
void Array::setCount(int count) {m_iCount = count;}
int Array::getCount() {return m_iCount;}

main.c

int main(){

    Array arr1;
    arr1.setCount(5);

    Array arr2(arr1);
    cout << arr2.getCount() << endl;

    return 0;
}
/*輸出:
Array
Array &
5
~Array
~Array
*/

淺拷貝2:

此次在類中加入指針屬性,所以需要注意的是關於類中指針的拷貝。

Array.h

class Array {
public:
    Array(int count);
    Array(const Array&arr);
    ~Array();

    void setCount(int count);
    int getCount();

    void printAddr();
private:
    int m_iCount;   // 指針元素個數
    int *m_pArr;    // 用於指向數組的指針
};

Array.c

Array::Array(int count) {
    
    // 爲指針變量賦值
    m_iCount = count;
    m_pArr = new int[m_iCount];

    // 給數組賦值,方便查看
    for(int i = 0; i < m_iCount; i++){
        m_pArr[i] = i;
    }
    cout << "Array" << endl;
}
// 只進行淺拷貝,導致指針只是直接拷貝了地址,
// 之後析構函數進行指針資源釋放時會崩潰
Array::Array(const Array &arr) {
    
    // 進行指針的拷貝(淺拷貝)
    m_iCount = arr.m_iCount;
    m_pArr = arr.m_pArr;

    cout << "Array &" << endl;
}

Array::~Array(){
    // 釋放類中的指針屬性
    delete []m_pArr;
    m_pArr = nullptr;
    cout << "~Array" << endl;
}

// 屬性操作方法
void Array::setCount(int count) {m_iCount = count;}
int Array::getCount() {return m_iCount;}

void Array::printAddr() {cout << "m_pArrd = " << m_pArr << endl;}

main.c

int main(){

    Array arr1(5);
    Array arr2(arr1);

    arr1.printAddr();
    arr2.printAddr();

    return 0;
}
/*輸出
Array
Array &
m_pArrd = 0xb516c0
m_pArrd = 0xb516c0
~Array

Process finished with exit code -1073740940 (0xC0000374)
*/

可見最後析構函數只完整運行了1次(輸出一個~Array),應爲類中的指針都指向同一塊內存地址,所以第二次析構函數進行指針資源釋放會出現錯誤,導致程序崩潰。所以就需要用到深拷貝了。

深拷貝:

Array.h和main.c這兩個文件的內容完全不變,只需要修改Array.c中拷貝構造函數即可。注意觀察兩個類中指針屬性指向的地址。

Array.c

Array::Array(int count) {

    m_iCount = count;
    m_pArr = new int[m_iCount];

    // 給數組賦值,可以方便後面進行輸出查看
    for(int i = 0; i < m_iCount; i++){
        m_pArr[i] = i;
    }
    cout << "Array" << endl;
}

Array::Array(const Array &arr) {

    m_iCount = arr.m_iCount;

    // 新創建指針,開闢新的堆空間,
    // 並將“被拷貝的類”中指針指向的數組的內容拷貝過來(深拷貝)
    m_pArr = new int[m_iCount];
    for(int i = 0; i < m_iCount; i++){
        m_pArr[i] = arr.m_pArr[i];
    }
    cout << "Array &" << endl;
}

Array::~Array(){

    delete []m_pArr;
    m_pArr = nullptr;
    cout << "~Array" << endl;
}

void Array::setCount(int count) {m_iCount = count;}
int Array::getCount() {return m_iCount;}
void Array::printAddr() {cout << "m_pArrd = " << m_pArr << endl;}

main.c文件輸出

/*輸出
Array
Array &
m_pArrd = 0xa816c0
m_pArrd = 0xa816e0
~Array
~Array
*/

可以觀察到,這兩個類中的指針,指向的地址並不一樣;所以析構函數能完整的運行兩次,將所有的類中的指針屬性正常釋放掉。

3.this指針、對象指針

this指針的使用:

  1. this指針無需用戶定義,是編譯器自動產生的。

  2. this指針也是指針類型,所以在32位編譯器下也佔用4個基本的內存單元,即sizeof(this)的結果爲4。

  3. 當成員函數的參數或臨時變量與數據成員同名時,可以使用this指針區分同名的數據成員。

對上面的第3項舉個例子:

如果我們在類中寫了2個屬性,分別是int x;int y;,然後在這個類的構造函數中,括號裏的形參名也是x和y,在這種情況下,我們就可以使用 this指針 代表 當前類的指針 ,this->x就代表 當前類指針的x,這樣就可以將臨時變量x和當前類的x屬性進行區分了。

Coordinate::Coordinate(int x,y){
    this->x = x;
    this->y = y;
}

這裏只展示其中一種 對象指針 比較少用到的寫法:

Coordinate p1;
Coordinate *p2 = &p1;

這裏指針的寫法和一般的變量一樣。例如:int x=1; int *p = &x;

很多情況下都是 指針和堆內存 結合在一起使用的,但是這裏是 指針和棧內存 結合在一起使用。

實例1,當 對象 作爲 返回值 時:

要注意此時會調用拷貝構造函數,故會多生成一個對象,和原來的對象無關。如果我們對這個對象進行操作,之前的對象是不會受到任何影響的。

Array.h

class Array {
public:
    Array(int len);
    Array(const Array& arr);
    ~Array();
    void setLen(int len);
    int getLen();
    Array printInfo();
private:
    int m_iLen;
};

Array.c

Array::Array(int len){
    m_iLen = len;
    cout << "Array(int len)" << endl;
}
Array::Array(const Array &arr) {
    cout << "Array(const Array &arr)" << endl;
}
Array::~Array(){
    cout << "~Array()" << endl;
}

void Array::setLen(int len){m_iLen = len;}
int Array::getLen(){return m_iLen;}

// 當返回值是對象時,會調用拷貝構造函數,返回一個新的類,
// 所以程序結束的時候,會發現多調用了一次析構函數。
Array Array::printInfo(){
    cout << "printInfo(),return Array" << endl;
    return *this; // this是指針,*this是對象
}

main.c

int main() {
    
    Array arr1(10);
    
    // 對printInfo()返回的 【對象】 進行操作
    //(調用getLen()將m_iLen變量設置爲5),
    // 但是此對象是通過拷貝構造函數新生成的,
    // 所以通過arr1.getLen()輸出的值是不會有變化的。
    arr1.printInfo().setLen(5); 
    
    // 輸出arr1這個對象的將m_iLen屬性的值
    cout << arr1.getLen() << endl;
    
    return 0;
}
/* 輸出:
Array(int len)
printInfo(),return Array
Array(const Array &arr)
~Array()
10
~Array()
*/

輸出了兩次~Array(),說明在程序的運行過程中,確實出現了兩個Array的對象。

通過printInfo()和下一行的Array(const Array &arr)可知,在進入到printInfo()函數中返回Array對象之後,確實調用了拷貝構造函數

提示:如何避免這種情況?那麼就只有使用 指針 或 引用 了,之後的例子裏會講。

實例2,當 對象引用 作爲 返回值 時:

修改一下Array類的聲明和實現。

Array.h

//Array printInfo();

Array& printInfo();

Array.c

//Array Array::printInfo(){
//    cout << "printInfo(),return Array" << endl;
//    return *this; // this是指針,*this是對象
//}

// 當返回值是對象的引用時,就不會調用拷貝構造函數
Array& Array::printInfo(){
    cout << "printInfo(),return Array&" << endl;
    return *this; // this是指針,*this是對象
}

main.c

int main() {
    
    Array arr1(10);
    
    // 對返回的 【對象引用】 進行操作
    // 但是此對象是
    // 所以通過arr1.getLen()輸出的值是不會有變化的。
    arr1.printInfo().setLen(5); 
    
    // 輸出arr1這個對象的將m_iLen屬性的值
    cout << arr1.getLen() << endl;
    
    return 0;
}

/* 輸出:
Array(int len)
printInfo(),return Array&
5
~Array()
*/

當傳遞對象的引用的時候,就不會調用拷貝構造函數創建一個新的對象了。而且通過引用我們可以對原對象()進行操作,就和我們使用指針的時候一樣。

**返回對象引用的修改方法及其調用方法:**到此肯定注意到了在main中使用了arr1.printInfo().setLen(5)這種調用方式。如果我們將其它函數改造成返回對象的引用return *this,我們就可以使用這種調用方式,相當於每次返回的都是當前對象的引用,我們通過.對這個對象引用中的函數進行調用。

舉個例子,如果我們將setLen也改成返回對象的引用,那麼我們就可以使用如下的調用方式:

arr1.printInfo().setLen(5).printInfo();

在這一連串的.成員調用中,我們實際上都是對arr1這個對象中的屬性/方法進行調用。

實例3,當 對象指針 作爲 返回值 時:

將前面例2的代碼由引用形式改成指針形式即可。

Array.h

//Array& printInfo();
Array* printInfo();

Array.c

// 當返回值是對象的引用時,就不會調用拷貝構造函數
//Array& Array::printInfo(){
//    cout << "printInfo(),return Array&" << endl;
//    return *this; // this是指針,*this是對象
//}
Array* Array::printInfo(){
    cout << "printInfo(),return Array*" << endl;
    return this; // this是指針,*this是對象
}

main.c

int main() {
    
    Array arr1(10);
    
    //arr1.printInfo().setLen(5); 
    // 對返回的 【指針】 進行操作,得到的結果和使用引用的一樣
    arr1.printInfo()->setLen(5);
    
    // 輸出arr1這個對象的將m_iLen屬性的值
    cout << arr1.getLen() << endl;
    
    return 0;
}

/* 輸出:
Array(int len)
printInfo(),return Array*
5
~Array()
*/

輸出的內容和使用引用輸出的一樣,都是對arr1進行操作。

4.常量對象和常量成員函數之間的調用關係

1.如果我們用const修飾對象成員函數,會發現如果我們對函數體內的成員進行修改,會出現錯誤,代碼如下:

// 錯誤
void Coordinate::changeX() const{
    m_iX = 10;
}

// 如果不用const修飾則沒有問題
void Coordinate::changeX(){
    m_iX = 10;
}

2.上面的問題,和this指針有關,這裏就來說明一下:

對於用戶來說,編譯器會幫我們添加this指針。

// 當我們定義changeX()這個成員函數的時候,看起來沒有任何的參數
void Coordinate::changeX(){
    m_iX = 10;
};
// 但是實際上卻隱含着一個參數,就是this指針
void changeX(Coordinate *this){
    this->m_iX = 10;
};

3.所以實際上代碼爲1中的代碼是這種表現:

void Coordinate::changeX() const{
    m_iX = 10;
}
// 實際表現如下:
void changeX(const Coordinate *this){
    this->m_iX = 10;// 會出現錯誤
}

可以看出,對於changeX()這個對象成員函數,使用 const 進行修飾,就相當於對 this指針 進行const修飾,原來的Coordinate *this就被修飾成const Coordinate *this,this是一個常量指針(既這個指針指向一個常量),所以我們不能對 this 指向的對象的內容進行修改,而m_iX就是對象中的屬性,所以我們當然不能對其進行修改了。

互爲重載與其調用:

// 互爲重載的聲明
class Coordinate{
public:
    Coordinate(int x, int y);
    void changeX() const;	// 和下一行的void changeX()互爲重載
    void changeX();
private:
    int m_iX;
    int m_iY;  
}

// 調用void changeX()
int main(){
    Coordinate coordinate(3,5);
    coordinate.changeX();
    return 0;
}

// 調用void changeX() const
int main(){
    const Coordinate coordinate(3,5);// 在實例化對象的時候,加上const
    coordinate.changeX();
    return 0;
}

使用const實例化出來的對象,我們稱爲常量對象

通過常量對象const Coordinate coordinate(3,5)調用的就是常量對象函數void changeX() const

這裏再說明一下常量對象與常量對象函數:

1.常量對象:

如果對象使用const修飾,那麼就稱爲常量對象;常量對象和一般的常量(const int x之類)類似,其內容都是不可以修改的,也就是其中屬性除了初始化的時候進行賦值,其它任何時候都不能對常量對象中的屬性進行修改。

2.常量對象函數:

如果類中的函數採用const修飾,那麼就稱爲常量對象函數;常量對像函數的隱含參數this指針,也會受到const的影響,導致this指針指向的對象的內容不可以修改(即通過這種方式,只有讀權限,沒有寫權限)。

所以什麼樣的對象函數可以改成常量對象函數?

如果一個對象函數不需要對 對象的屬性 進行寫操作,那麼就可以改爲常量對象函數。

注意1:

對象改成常量對象,對應的 獲取對象屬性的對象函數 也需要改成const修飾的。(修改對象屬性的對象函數沒法改,原因看上面“所以什麼樣的對象函數可以改成常量對象函數?”這個問題)

這是因爲這個對象函數如果不用const修飾,那麼隱含的this指針參數權限就是可讀、可寫的。而我們的常對象因爲有const限制,所以權限只有可讀。所以 隱含的this指針常量對象 的類型其實不同,也就不能用this指向常對象,否則會導致編譯出現錯誤。

對應的程序代碼在“04const2”

注意2:

當然,如果對象不是常量對象,對象函數對象函數可以用普通的對象函數,也可以用常量對象函數。

5.常量指針和常量引用的使用

普通的 對象引用 和 對象指針:

int main(){
    
    Coordinate coor1(3,5);
    Coordinate &coor2 = coor1;	// 對象的引用
    Coordinate *pCoor = &coor1;	// 對象的指針
    
    // 打印的都是coor1這種對象中的內容
    coor1.printInfo();
    coor2.printInfo();
    pCoor->printInfo();
    
    return 0;
}

常量指針和常量引用:

Coordinate.cpp

int Coordinate::getX(){
    return m_iX;
}
int Coordinate::getY(){
    return m_iY;
}
void Coordinate::printInfo() const {
    cout << "(" << m_iX << "," << m_iY << ")" << endl;
}

main.c

int main(){

    Coordinate coor1(3,5);
    const Coordinate &coor2 = coor1;	// 常量對象的引用
    const Coordinate *pCoor = &coor1;	// 常量對象的指針

    coor1.printInfo();
    coor2.getX();	// 報錯
    pCoor->getY();	// 報錯,'this' argument to member function 'getY' has type 'const Coordinate', but function is not marked const

    return 0;
}

和之前說的同理,引用和指針加上const修飾,就只有可讀權限,而getX()getY()都是普通對象函數,其中隱含的this指針爲可讀、可寫權限。類型不同,當然會報錯了。

上面報錯的翻譯:

‘this’ argument to member function ‘getY’ has type ‘const Coordinate’, but function is not marked const

成員函數“getY”的這個參數的類型是“const Coordinate”,但是函數沒有標記const

故,如果函數也用const修飾,則可以正常運行。

6.對象指針常量的使用

Coordinate.cpp

int Coordinate::getY(){
    return m_iY;
}
void Coordinate::printInfo() const {
    cout << "(" << m_iX << "," << m_iY << ")" << endl;
}

main.c

int main(){
    
    Coordinate coor1(3,5);
    Coordinate coor2(7,9);
    Coordinate * const pCoor = &coor1;
    
    pCoor->getY();	// 正常調用
    pCoor = coor2;	// 錯誤
    pCoor->printInfo();	// 正常調用
    
    return 0;
}

pCoor是一個指針常量,本身不可以修改,但是指向的內存上的內容是可以修改的,也就是說具有可讀、可寫的權限。

  1. pCoor->getY():這裏是符合getY()這個對象函數的要求的,所以可以正常調用。

  2. pCoor = coor2:這裏很明顯的是要改變pCoor的指向,但是pCoor是常量,無法改變其指向的地址,所以是錯誤的。

  3. pCoor->printInfo():這個在本文第4章末尾的 注意2 有提及,對象是可讀、可寫的,而printInfo()中this指針只要求是可讀的,所以當然也是可以正常調用的。

7.第5、6章小總結

  1. 常量對象只能調用常量成員函數,不能調用普通成員函數。
  2. 普通對象能夠調用常量成員函數,也能夠調用普通成員函數。
  3. 常量指針和常引用都只能調用對象的常量成員函數。

本篇爲視頻教程筆記,視頻如下:

C++遠征之封裝篇(下)

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