第七節(jié) 三大函數(shù):拷貝函數(shù)氏堤,拷貝賦值评肆,析構(gòu)
今天開始學(xué)習(xí)另一個(gè)經(jīng)典類,String(僅從三大函數(shù)的角度)毁嗦。
- 構(gòu)造函數(shù)
- 拷貝構(gòu)造函數(shù)
- 拷貝賦值函數(shù)
- 析構(gòu)函數(shù)
大家都知道,我們?cè)趧?chuàng)建實(shí)例的時(shí)候回铛,編譯器會(huì)自動(dòng)調(diào)用class中的構(gòu)造函數(shù)對(duì)實(shí)例進(jìn)行初始化狗准,而在銷毀實(shí)例或生命周期結(jié)束時(shí)會(huì)自動(dòng)調(diào)用其析構(gòu)函數(shù)來清理內(nèi)存。那么拷貝構(gòu)造函數(shù)與拷貝賦值函數(shù)是什么呢茵肃?它們的作用是腔长?
先來看一段String類接口:
class String
{
private:
char *m_data;
public:
String(const char* cstr=0);//構(gòu)造函數(shù)
String(const String& str);//拷貝構(gòu)造函數(shù)
String& operator=(const String&str);//拷貝賦值函數(shù)
~String();//析構(gòu)函數(shù)
char* get_c_str() const{return m_data};
};
再來看下面的代碼:
int main()
{
String s1();//調(diào)用構(gòu)造函數(shù)
//String S1;//調(diào)用構(gòu)造函數(shù)
String s2("hello");//調(diào)用構(gòu)造函數(shù)
String s3(s1);//調(diào)用拷貝構(gòu)造函數(shù)
String s4=s1;//調(diào)用拷貝構(gòu)造函數(shù)
s4=s2;//拷貝賦值函數(shù)
return 0;
}
看了上面的代碼,是不是對(duì)四個(gè)函數(shù)有了基本的印象呢验残,下面我們來深入學(xué)習(xí)捞附。
構(gòu)造函數(shù)/析構(gòu)函數(shù)
inline
String::String(const char* cstr=0)//用來創(chuàng)建無參實(shí)例或以字符串("aaaa"或char[5])為參數(shù)的實(shí)例
{
if(cstr)//有參
{
m_data=new char[strlen(cstr)+1];//為m_data開辟空間,大小為參數(shù)長度+1(存放'\0')
strcpy(m_data,cstr);//賦值
}
else//無參
{
m_data=new char[1];//開辟大小為1的空間您没,存放'\0'
*m_data='\0';
}
}//構(gòu)造函數(shù)
inline
String::~String()
{
delete[] m_data;//釋放內(nèi)存鸟召,注意用[]
}//析構(gòu)函數(shù)
int main()
{
String* p=new String("hello");//調(diào)用構(gòu)造函數(shù)
delete p;//調(diào)用析構(gòu)函數(shù)
return 0;
}//測(cè)試函數(shù)
比較中規(guī)中矩的用法。
拷貝構(gòu)造函數(shù)/拷貝賦值函數(shù)/析構(gòu)函數(shù)
inline
String::String(const String& str)
{
m_data=str.m_data;
}//淺拷貝構(gòu)造函數(shù)(編譯器自帶的那一套)
String s1="hello world";
String s2=s1;
我們知道氨鹏,在執(zhí)行String s1="hello world";String s2=s1;這樣的操作時(shí)欧募,編譯器會(huì)調(diào)用默認(rèn)拷貝構(gòu)造函數(shù),然而這種默認(rèn)的拷貝構(gòu)造函數(shù)只是單純的將s1中的數(shù)據(jù)一個(gè)bit一個(gè)bit的復(fù)制過去仆抵,是一種淺拷貝跟继,當(dāng)我們的class中有point成員時(shí),這種淺拷貝會(huì)使s1,s2中的兩個(gè)point指向同一塊內(nèi)存镣丑,這是很危險(xiǎn)的舔糖。這時(shí),我們需要為編譯器提供一個(gè)新的莺匠、深拷貝的拷貝構(gòu)造函數(shù)剩盒。
inline
String::String(const String& str)
{
m_data=new char[strlen(str.m_data)+1];
strcpy(m_data,str.m_data);
}//深拷貝構(gòu)造函數(shù)(無內(nèi)存問題)
以上為拷貝構(gòu)造函數(shù)只能解決在新建實(shí)例時(shí)對(duì)class中point成員指向問題,在實(shí)例已經(jīng)創(chuàng)建完成后的賦值動(dòng)作慨蛙,同樣會(huì)存在指針指向相同內(nèi)存的問題辽聊,我們需要通過重載操作符'='來解決這個(gè)問題,即拷貝賦值函數(shù)期贫。
inline
String& String::operator =(const String& str)
{
if(this==&str)//檢測(cè)自我復(fù)制(self assignment)
return *this;
delete[] m_data;//原理內(nèi)存(原內(nèi)存長度不可更改)
m_data=new char[strlen(str.m_data)+1];//重新開辟長度適當(dāng)?shù)目臻g
strcpy(m_data,str.m_data);//賦值
return *this;//返回this
}//拷貝賦值函數(shù)
String s1("hello world");
String s2;
s2=s1;//調(diào)用拷貝賦值函數(shù)
以上就是String中的big three跟匆。
最后再來重載一下'<<'方便輸出String實(shí)例
#include<iostream>
ostream& operator<<(ostream& os,const String& str)
{
os<<str.get_c_str();
return os;
}
{
String s1("hello ");
cout<<s1;
}
第八節(jié) 堆,棧與內(nèi)存管理
- 棧(Steak)
是存在于某作用域(scope)的一塊內(nèi)存空間(memory space)通砍。
- 堆(Heap)
也成system heap,是由操作系統(tǒng)提供的一塊global內(nèi)存空間玛臂,程序可以動(dòng)態(tài)分配(dynamic allocated)從其中獲得若干區(qū)塊(blocks)烤蜕。
class Complex{...};
...
{
Complex c1(1,2);//分配棧空間
Complex* p=new Complex(3);//動(dòng)態(tài)分配堆空間
}
static object與global object兩種都在main之前存在迹冤,(聯(lián)想構(gòu)造函數(shù))在程序結(jié)束時(shí)銷毀(析構(gòu)函數(shù))讽营。
new:先分配memory,再調(diào)用ctor
Complex* pc=new Complex(1,2);
//編譯器轉(zhuǎn)化為
void* mem=operator new(sizeof(Complex));//分配內(nèi)存(大小決定于class數(shù)據(jù)類型),內(nèi)部會(huì)調(diào)用malloc(n)
pc=static_cast<Complex*>(mem);//轉(zhuǎn)型
pc->Complex::Complex(1,2);//構(gòu)造函數(shù)
delete:先調(diào)用dt,在釋放memory
String* ps=new String(Hello);
delete ps;
//編譯器轉(zhuǎn)化為
String::~String(ps);//調(diào)用析構(gòu)函數(shù)泡徙,殺掉class中動(dòng)態(tài)分配的內(nèi)存
operator delete(ps);//釋放內(nèi)存橱鹏,內(nèi)部調(diào)用free(ps),free(ps)用來殺掉class本身(即數(shù)據(jù)成員);
小結(jié):構(gòu)造函數(shù)與析構(gòu)函數(shù)的作用是初始化和清理class中動(dòng)態(tài)分配的內(nèi)存堪藐,真正執(zhí)行創(chuàng)建和銷毀object的操作是malloc(),與free()
動(dòng)態(tài)分配所得的內(nèi)存塊,in VC
我們創(chuàng)建一個(gè)Complex類(雙double成員)莉兰,一般會(huì)認(rèn)為被動(dòng)態(tài)分配8個(gè)bit。
但實(shí)際上礁竞,在VC中的情況是這樣的(VC中每個(gè)內(nèi)存塊一定是16的倍數(shù)):
|Complex
|00000041(Cookies)
|Debugger Header(debug,32+4bit)
|Complex Object(data,8bit)
|00000000(pad,16bit)
|00000041(Cookies)
8+(32+4)+(4*2)
->52
->64(debug模式下)
| 00000011(Cookies)
| Complex(data,8bit)
| 00000011(Cookies)
8+(4*2)
->16
以上Debug標(biāo)注為在調(diào)試模式下所分配的內(nèi)存糖荒。而Cookies用來記錄分配內(nèi)存的大小,上例中分別為00000040與00000010,利用最后一位記錄是否已經(jīng)分配,分配后則為00000041與00000011模捂。
以上為Complex object,String object與其結(jié)構(gòu)相似在這就不多做贅述捶朵。
下面分析一下動(dòng)態(tài)分配所得的array:
| Complex[3]
| 51h(Cookies)
| Debugger Header(debug,32+4bit)
| 3(4bit)
| double(24bit)
| 00000000(pad,8bit)
| 51h(Cookies)
(8 *3)+(32+4)+(4 *2)+4
->72
->80
非debug模式下為:
(8 *3)+(4 *2)+4
->36
->48
| String[3]
| 41h(Cookies)
| Debugger(32+4bit)
| 3
| pointer(12bit)
| 00000000(pad,4bit)
| 41h(Cookies)
(4 *3)+(32+4)+(4 *2)+4
->60
->64
array new一定要搭配array delete
String* p=new String[3];
delete[] p;//喚起3次dtor
//只有加"[]"編譯器才會(huì)明白要清理的對(duì)象是一個(gè)數(shù)組,才會(huì)讀取3,喚起三次dtor(自寫狂男,處理class中動(dòng)態(tài)分配內(nèi)存)
如果例子中是Complex類综看,則不論是不是數(shù)組使用delete p都沒有問題,因?yàn)椴淮嬖陬愔袆?dòng)態(tài)分配內(nèi)存并淋,所以不需要dtor,只要調(diào)用free()清理double即可珍昨,但是為了規(guī)范县耽,方便記憶,單個(gè)object使用delete,數(shù)組使用delete[]镣典。
[]的本質(zhì)是命令編譯器讀取pointer(12bit)或double(23bit)上方的用來記錄數(shù)組長度的內(nèi)存來獲取數(shù)組長度兔毙,并喚起dtor該長度的次數(shù)。
第九節(jié) 擴(kuò)展補(bǔ)充:類模板兄春,函數(shù)模板澎剥,及其他
補(bǔ)充:static
- 通常100個(gè)實(shí)例就有100組數(shù)據(jù),而函數(shù)保存在代碼區(qū)赶舆,只有一份哑姚。一份函數(shù)處理多組數(shù)據(jù),靠this指針芜茵。調(diào)用方法:c1.real()->complex::real(&c1);將地址傳入this用來識(shí)別具體是哪一組數(shù)據(jù)叙量。
- static修飾的實(shí)例類作用域?yàn)槿郑诔绦蚪Y(jié)束時(shí)銷毀九串。
- static會(huì)使數(shù)據(jù)與函數(shù)與類分離绞佩,在內(nèi)存中單獨(dú)存儲(chǔ)寺鸥,且只有一份。因?yàn)闆]有this指針品山,所以static函數(shù)只能處理static數(shù)據(jù)胆建。
- 類中聲明靜態(tài)實(shí)例不會(huì)分配內(nèi)存(聲明)。在類外初始化時(shí)分配(定義)肘交。
- 靜態(tài)函數(shù)的調(diào)用方法:(1)通過實(shí)例調(diào)用笆载。例:Account a;a.set();(2)通過class name來調(diào)用。例:Account::set;(與臨時(shí)變量語法類似)酸些。
補(bǔ)充:把ctors放在private中
單例模式(Singleton)
class A
{
public:
static A& getInstance(return a;);//對(duì)外窗口
setup(){...}
private:
A();//構(gòu)造函數(shù)
A(const A& rhs);//拷貝構(gòu)造函數(shù)
static A a;//實(shí)例
};
然而上述方法還不夠完美宰译,實(shí)例a在object創(chuàng)建時(shí)就已經(jīng)存在,有些浪費(fèi)空間魄懂,最好是在使用時(shí)才創(chuàng)建沿侈。只需要將創(chuàng)建實(shí)例的過程放到對(duì)外窗口中即可,只有外界通過窗口訪問時(shí)再創(chuàng)建市栗。
class A
{
public:
static A& getInstance();
setup(){...}
private:
A();
A(const A& rhs);
...
};
A& A::getInstance()
{
static A a;
return a;
}
解析:實(shí)現(xiàn)單例要解決兩個(gè)問題:
(1)控制權(quán)限缀拭,不允許到處創(chuàng)建實(shí)例。解決方法:將構(gòu)造函數(shù)放在private中填帽,則只允許在類內(nèi)創(chuàng)建實(shí)例蛛淋。
(2)控制數(shù)量,只允許創(chuàng)建一個(gè)實(shí)例篡腌。解決方法:使用static修飾實(shí)例褐荷。
補(bǔ)充:cout
在這里先拋出一個(gè)問題,為什么cout可以接受可種各樣的數(shù)據(jù)并彈出呢嘹悼?
cout屬于_IO_ostream_withassign類叛甫,繼承于ostream,我們來看一下ostream的定義杨伙。
class ostream:virtual public ios
{
public:
ostream& operator<<(char c);
ostream& operator<<(unsigned char c){return(*this)<<(char)c;}
ostream& operator<<(signed char c){return (*this)<<(char)c;}
ostream& operator<<(const char *s);
ostream& operator<<(const unsigned char *s){return (*this)<<(const char*)s;}
ostream& operator<<(const signed char *s){return (*this)<<(const char*)s;}
ostream& operator<<(const void *p);
ostream& operator<<(int n);
ostream& operator<<(unsigned int n);
ostream& operator<<(long n);
ostream& operator<<(unsigned long n);
...
}
不難發(fā)現(xiàn)ostream中對(duì)操作符"<<"進(jìn)行了大量重載其监,正因如此cout才可以接收各種各樣的數(shù)據(jù)類型。
補(bǔ)充:類模板(class template)
適用情況:相同操作限匣,不同數(shù)據(jù)
template<typename T>
class A
{
private:
T data;
public:
A();
T get(return data);
};
int main()
{
A<int> a;//綁定
cout<<a.get()<<endl;
return 0;
}
在定義類時(shí)不指定數(shù)據(jù)類型抖苦,而是使用一個(gè)符號(hào)T來代替,之后在使用時(shí)綁定類型米死。
補(bǔ)充:函數(shù)模板(function template)
顧名思義使用在非類成員函數(shù)的模板锌历。
template<class T>
T min(T& a,T& b)
{
return a>b?b:a;//T的類型會(huì)影響具體調(diào)用那一種'>'操作符重載
}
int main()
{
int a,b;
cin>>a>>b;
cout<<min(a,b);
return 0;
}
在調(diào)用使用模板的函數(shù)時(shí)編譯器會(huì)進(jìn)行“引數(shù)推倒(argument deduction)”,即利用傳進(jìn)的實(shí)參在決定T類型峦筒,并推出返回類型或重載類型辩涝。
補(bǔ)充:命名空間(namespace)
以標(biāo)準(zhǔn)庫為例,標(biāo)準(zhǔn)庫中所有的命名都被包含在了命名空間std中勘天,使用的方法有幾種:
- using namespace std;cin,cout;
- using std::cout;std::cin,cout;
- (NONE);std::cin,stu::cout;
更多細(xì)節(jié)深入
一下是不包含在課程中的內(nèi)容怔揩,順便記錄一下捉邢。
- operator type()const;//轉(zhuǎn)換函數(shù)
- explicit complex(...):initialization list{}
- pointer-like object
- function-like object
- Namespace
- template specialization
- Standard Library
- variadic template(c11)
- move ctor(c11)
- Rvalue reference(c11)
- auto(c11)
- lambda(c11)
- range-base for loop(c11)
- unordered containers(c11)
...
今天就到這吧。商膊。伏伐。