C++類&&對象的深入研究

沒錯,C_T開學了
親愛的副教授視頻上課,真開心啊


菜鳥教程之C++類&對象教程

先複習一下類&&對象函數的知識點

在這裏插入圖片描述

常見的數據類型有:

  • 基本數據類型
    • 整型,實型(單精度,雙精度)
    • 字符型
  • 構造類型
    • 枚舉類型
    • 數組類型
    • 結構體類型,聯合類型,類
  • 引用
  • 指針類型
  • 空類型

而這裏我們要重點討論的是——

類由數據和處理數據的函數封裝而成
類是一種可以 " 發展 " 的數據類型,即一個類可以派生出另外一個類,派生出來的類不僅具有原類的一切特徵,還可具備擴充的新特徵

在這裏插入圖片描述


預處理問題

預處理:在進行編譯的第一遍詞法掃描和語法分析之前所作的工作
預處理命令包括:#include#define

擴展知識:宏定義(4種)
宏由編譯預處理程序執行

  • #define<宏名><字符串>
    字符串及註釋中的相應值不會被替換,如#define PI 3.14
  • #define<宏名>(<參數表>)<字符串>
    出現<宏名>的地方用文字穿代替,<文字串>中參數(相當於形參)將被替換成<宏名>提供的參數(相當於實參),如果一行寫不下,可在本行最後用續行符 ’ \ ’ 後跟一個回車轉到下一行
  • #define<宏名>
    告知編譯程序<宏名>已被定義,不做文本替換,實現條件編譯
  • #undef<宏名>
    取消某個宏的定義,其後的<宏名>不再進行替換,不再有意義

條件編譯

一般情況下,源程序中所有的行都參加編譯
但是有時希望其中一部分內容只在滿足一定條件時才進行不安逸,也就是對一部分內容指定編譯的條件

#ifdef FLAG
cout<<""<<endl;
#endif

#ifndef FLAG
cout<<"wolrd"<<endl;
#endif
//------------------
#ifdef FLAG
cout<<"hello"<<endl;
#else
cout<<"world"<<endl;
#endif

爲什麼要介紹這個呢?
我們觀察下面的這個栗子:
在這裏插入圖片描述

編譯失敗,提示:class A被重定義了
也就是說,#include的含義實際上是對引用的頭文件裏所有內容進行一次全新定義
如果不同的頭文件中具有相同的定義,就會引發錯誤
怎麼解決這個問題呢?
在這裏插入圖片描述
所以從今往後,我們建議

  • 所有頭文件均使用預處理器封裝,以避免重複的頭文件引用
#ifndef CLASS_H
#define CLASS_H

#endif
  • 儘量將接口和實現分離,這樣當添加新的類型進行程序擴展時,原有的操作接口代碼不需要進行修改,可以使程序更易於擴展

創建對象

class Time{...};

Time sunset;                     //創建Time類的對象
Time arrayOfTimes[5];            //創建Time類對象的數組
Time &dinnerTime=sunset;         //對象sunset的引用
Time *timeptr1=&dinnerTime;      //指向對象的指針 
Time *timeptr2=&sunset;          //指向對象的指針

需要強調的是:只有在缺省構造函數的條件下,纔可以創建某個類對象的數組,自定義的帶參數構造函數是不行的


對象的佔用空間

當我們申請一個對象的時候,ta到底佔用了多少空間呢?

在默認情況下,VC規定各成員變量存放的起始地址相對於結構的起始地址的偏移量,必須爲該變量的類型所佔用的字節數的倍數
VC爲了確保結構的大小爲結構的字節邊界數(即該結構中佔用最大空間的類型所佔用的字節數)的倍數,所以在爲最後一個成員變量申請空間後 ,還會根據需要自動填充空缺的字節。

class test{
private:
    int num;
}

int類型4個字節,num存儲在1~4字節
所以一共需要4字節

class test{
private:
    int num;
    char s,c;
}

int類型4個字節,num存儲在1~4字節
char類型1個字節,s存儲在第5字節,c存儲在第6字節
確保結構的大小爲結構的字節邊界數(即該結構中佔用最大空間的類型所佔用的字節數)的倍數
所以一共需要8字節

class test{
private:
    char s;
    int num;
    char c,t;
}

char類型1個字節,s存儲在第1字節
int類型4個字節,由於存放的起始地址相對於結構的起始地址的偏移量,必須爲該變量的類型所佔用的字節數的倍數,所以num存儲在5~8字節
c存儲在第9字節,t存儲在第10字節
確保結構的大小爲結構的字節邊界數(即該結構中佔用最大空間的類型所佔用的字節數)的倍數
所以一共需要12字節

class test{
private:
    int num;
    char s,c,t
}

int類型4個字節,num存儲在1~4字節
char類型1個字節,s存儲在第5字節,c存儲在第6字節,t存儲在第7字節
確保結構的大小爲結構的字節邊界數(即該結構中佔用最大空間的類型所佔用的字節數)的倍數
所以一共需要8字節(定義變量時的順序不同,佔用的空間就會發生變化)

class test{
private:
    static char s;
    int num;
}

static類型時靜態存儲類,依賴於全局內存空間,不計算入對象的佔用空間中
int類型4個字節,num存儲在1~4字節
所以一共需要4字節


作用域問題

作用域分爲很多種:
函數作用域(Function Scope),全局作用域(File Scope),模塊作用域(Block Scope),還有類作用域(Class Scope)

類裏面定義的數據成員和成員函數都在類作用域之下
類內:可以調用所有數據成員和成員函數
類外:可以通過句柄調用public類型的數據成員和成員函數

句柄 handles
句柄調用主要有四種形式:

class Test{...};

Test member;
Test &memberRef = member;
Tset *memberPtr = &member;

member.set(1);
member.print();

memberRef.set(1);
memberRef.print();

(*memberPtr).set(3);
(*memberPtr).print();

memberPtr->set(4);
memberPtr->print();
  • 對象名稱 + " . "
  • 對象的引用 + " . "
  • 用指針和 " * " 表示對象 + " . "
  • 對象指針 + " -> "

類中定義的數據成員對該類的每個對象都有一個拷貝
類中的成員函數對該類的所有對象只有一個拷貝
在這裏插入圖片描述
那麼問題來嘍!

類中成員函數對該類的所有對象只有一個拷貝, 被調用時如何知道是對那一個對象操作呢?

每一個成員函數都有一個隱藏的指針類型形參:this

  • <類名> *const this
  • 通過對象調用成員函數時,編譯程序會把對象地址作爲隱含參數傳遞給形參this
  • 訪問thsi指向的對象時:this->
  • 一般情況下省略this

在這裏插入圖片描述


接口和實現分離

Class Code分爲接口 ( Interface ) 和實現 ( Implementation ) 兩部分,
一般情況下,接口 ( Interface ) 包括inline函數(內聯函數)和私有數據成員等信息
內聯函數通常直接在頭文件中定義
private數據成員在頭文件中定義,且只能在類內部使用

我們一再強調,將接口和實現分離
簡單來講,用戶只能得知類內成員函數和數據成員的定義,但是並不知道其中的實現方法,這樣就從一定程度上保護了開發者的知識產權

軟件供應商在他們的產品中只需提供頭文件和類庫(目標模塊),而不需要提供源代碼

在這裏插入圖片描述


訪問函數和工具函數

訪問函數 (access function)

  • 用來讀取或顯示數據
  • 常見用法之一是測試條件的真假(判斷函數):例如vector的empty函數,可以判斷vector是否爲空

工具函數 (utility function)

  • 類的private成員函數,從而避免被類的客戶直接使用
  • 不屬於類的public成員函數,用來支持類的public成員函數

一般情況下,類的數據成員和在類內部使用的成員函數應該被指定爲private類型,只有提供給外界使用的成員函數才能指定爲public類型
而操作一個對象時,只能通過訪問對象中的public成員來實現
在這裏插入圖片描述


具有默認實參的構造函數

我們在函數講解中提到了默認實參的處理方式:

int cal(int =0,int =0,int =0);

int cal(int x,int y,int z) {
    return x*y*z;
}

構造函數也是函數啊,所以也可以擁有默認實參,而且實現方法也基本一致
當然,有時候程序員並沒有自定義構造函數
這種情況下,程序在執行的時候會偷摸地調用編譯器內部的缺省構造函數

什麼是缺省構造函數?
缺省構造函數是不帶參數或者所有參數都是默認值的構造函數
簡單來說,缺省構造函數就是不指定實參即可調用的構造函數

注意:

  • 函數重載 (overloading)
    C++要求每個函數的函數簽名不同,即函數名稱可以相同,但是參數列表要相異(變量數,類型,順序)
    所以我們也可以對構造函數進行重載
  • 構造函數可以指定默認實參
    不帶參數(或所有參數都是默認值)的構造函數稱爲缺省構造函數
    缺省構造函數可以在不提供任何參數的情況下,仍能夠初始化數據成員,保證對象的數據成員處於正常狀態
  • 只要類中提供了自定義構造函數,編譯器就不再提供缺省構造函數
  • 構造函數通過調用其他成員函數對數據成員初始化
    如果在構造函數內部直接給數據成員賦值,那就叫賦值,不叫初始化
  • 類的成員函數對數據成員的訪問儘量都通過set和get函數實現
    一方面增強了函數的模塊化程度
    另一方面方便了對訪問操作的統一修改

類的應用實例:高精度加減


析構函數

析構函數是一個特殊的成員函數,名字同類名,並在前面加 " ~ " 字符,用來與構造函數加以區別

注意:

  • 析構函數不指定數據類型,也沒有參數

  • 一個類中只能定義一個析構函數,析構函數不能重載

  • 析構函數可以被調用,也可以由系統調用。
    以下兩種情況,析構函數會被自動調用:

    • 如果一個對象被定義在一個函數體內,當這個函數結束時,該對象的析構函數會被自動調用
    • 當一個對象是使用new運算符被動創建的,在使用delete運算符釋放它時,delete將會自動調用析構函數

與構造函數的區別:

  • 構造函數是成員函數,函數體可寫在類體內,也可寫在類體外
  • 函數名與類名相同,該函數不指定類型說明,有隱含的返回值,該值由系統內部使用
  • 該函數可以有若干個參數,也可以沒有參數
  • 構造函數可以被重載
  • 程序不能直接調用構造函數,只能在創建對象時由系統自動調用

下面我們要討論一個很厲害的話題:

析構函數的調用法則

還記得存儲類別嗎?不記得也沒關係,看這張圖快速想起來!在這裏插入圖片描述

當然啦,對象也有各種各樣的存儲類別,針對不同的存儲類別,析構函數的調用時間是不同的

  • 全局變量:在任何函數(含main)執行前構造;在程序結束時析構
  • 局部變量:
    • 自動變量:對象定義時構造,塊結束時析構
    • 靜態變量:首次定義時構造,程序結束時析構(靜態變量存在於整個程序的運行期間,所以實際上依賴於全局內存,屬於全局數據)
  • 全局對象,靜態對象(均爲靜態存儲類別)析構順序恰好與構造順序相反:先構造的對象後析構(棧操作)
特殊情況一:調用exit函數退出程序執行時,不調用剩餘對象的析構函數
特殊情況二:調用abort函數退出程序執行時,不調用任何剩餘對象的析構函數
#include<string>
using std::string;

#ifndef CREATE_H
#define CREATE_H
class CreateAndDestroy {
public:
	CreateAndDestroy(int,string);
	~CreateAndDestroy();
private:
	int ID;
	string message;
};
#endif
//CreateAndDestroy.cpp
#include<iostream>
#include<string>
#include "CreateAndDestroy.h"
using namespace std;

CreateAndDestroy::CreateAndDestroy(int x,string s) {
	ID=x;
	message=s;
	cout<<"Create "<<ID<<" ("<<message<<")\n";
}

CreateAndDestroy::~CreateAndDestroy() {
	cout<<"Destroy "<<ID<<"("<<message<<")\n";
}
//Test.cpp
#include<iostream>
#include<string>
#include "CreateAndDestroy.h"
using namespace std;

CreateAndDestroy first(1,"global");

void create() {
	CreateAndDestroy fifth(5,"local");
	static CreateAndDestroy sixth(6,"static in local");
	CreateAndDestroy seventh(7,"local");
}

int main() 
{
	CreateAndDestroy second(2,"main");
	static CreateAndDestroy third(3,"static in main");
	create();
	CreateAndDestroy fourth(4,"main");
	return 0;
}
// Output
Create 1 (global)
Create 2 (main)
Create 3 (static in main)
Create 5 (local)
Create 6 (static in local)
Create 7 (local)
Destroy 7 (local)
Destroy 5 (local)
Create 4 (main)
Destroy 4 (main)
Destroy 2 (main)
Destroy 6 (static in local)
Destroy 3 (static in main)
Destroy 1 (global)

在這裏插入圖片描述


返回引用

函數返回引用這個知識點我記得我由學過,但是我找不到我學過ta的證據。。。
下面這個栗子會有一點難以理解,我就簡單說幾句提示一下吧:

  • 只要調用了test()就會產生輸出,一共六次調用,對應着六個輸出
  • test()返回的是靜態局部變量val的引用,因此val_ref就是val本人,對val_ref的賦值就是對val的修改
  • 當然啦,val_ref只是一個變量名,目的就是方便我們對引用進行操作
    test()本身返回的就是一個引用,所以直接對test()賦值也可以實現對val的修改
#include<iostream>
using namespace std;

int &test() {
	static int val=0;    
	//想要返回局部變量的引用,就必須是靜態局部變量
	val++;
	cout<<val<<endl;
	return val;
}

int main() 
{
	test(); test(); test();
	int &val_ref=test();
	val_ref=100;
	test()=200;
	test();
	system("pause");
	return 0;
}

// Output
1
2
3
4
101
201

類的成員函數也可以返回私有數據成員的引用,但是這是一種非常危險的行爲
假若如此,用戶就能夠對類中的私有數據成員直接進行修改和調用
這就和我們想方設法 " 將接口和實現分開 " 背道而馳了


拷貝構造函數

最難的知識點來咯!!!

類的對象支持一般意義的賦值操作

class Data{
private:
    int x,y,z;
};

Data A,B;
A=B;
// equal to
A.x=B.x;
A.y=B.y;
A.z=B.z;

但是以下兩種情況不是賦值,而是調用拷貝構造函數

void copyData(Data B);
Data A;
copyDate(A);

情況一:在函數傳參的時候,Data B的定義就調用了拷貝構造函數,將A的信息拷貝到了B中

Data A;
Data B=A;

情況二:在定義對象時,直接進行初始化Data B=A時調用了拷貝構造函數

這兩種情況有一個重要的共同之處:對象名稱前就是類名稱,本質上都是申請新的對象

一般情況下,編譯器會提供缺省拷貝構造函數,將原對象的每個數據成員的值拷貝到新對象的相應成員
當然我們也可以自定義拷貝構造函數,語法也很簡單:類名稱(類名稱 & 形參)

class Data{
public:
    Data(int a=0,int b=0,int c=0) {setData(a,b,c);}
    ~Data() {cout<<"Dastroy data";}
    Data(Data &t) {
        setData(t.x,t.y,t.z);
        cout<<"Copy Constructor is called.\n";
    }
    void setData(int a,int b,int c) {x=a;y=b;z=c;}
private:
    int x,y,z;
};

一般情況下,我們是不用特意構造拷貝構造函數,編譯器的缺省拷貝構造函數就可以滿足我們的需求
但是缺省拷貝構造函數有一個致命的缺點:數據成員爲指針時會出現錯誤
爲什麼?
我們可以這樣理解:拷貝構造函數在一定程度上可以認爲是簡單的內容拷貝
所以數據成員爲指針時,拷貝構造函數會直接將同一個地址賦值給新對象的指針
這就造成了一個十分尷尬的局面:新舊兩個對象的指針都指向了同一個地址
對其中一個對象的指針所指內容修改,就會牽連另一個對象的內容
在調用析構函數時,同一塊內存會被delete釋放兩次,導致程序異常

class test{
public:
    test(int n) {    
        pArray = new int[n];
    }
    ~test() {delete []pArray;}
private:
    int *pArray;
};

怎麼解決這個問題?
那是下節課的內容啦 * ★°*:.☆( ̄▽ ̄)/$:*.°★ *

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