沒錯,C_T開學了
和親愛的副教授視頻上課,真開心啊
常見的數據類型有:
- 基本數據類型
- 整型,實型(單精度,雙精度)
- 字符型
- 構造類型
- 枚舉類型
- 數組類型
- 結構體類型,聯合類型,類
- 引用
- 指針類型
- 空類型
而這裏我們要重點討論的是——
類
類由數據和處理數據的函數封裝而成
類是一種可以 " 發展 " 的數據類型,即一個類可以派生出另外一個類,派生出來的類不僅具有原類的一切特徵,還可具備擴充的新特徵
預處理問題
預處理:在進行編譯的第一遍詞法掃描和語法分析之前所作的工作
預處理命令包括:#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;
};
怎麼解決這個問題?
那是下節課的內容啦 * ★°*:.☆( ̄▽ ̄)/$:*.°★ *