構造析構順序
拷貝構造
String類
目錄
1.構造函數和析構函數
構造函數:在創建對象時,系統自動調用它來初始化數據成員。構造函數可以重載。
析構函數:在對象生命週期結束的時候,自動調用來釋放該對象。一個類只能定義一個析構函數,析構函數不能重載。
class Cdate
{
public:
Cdate(int y, int m,int d);
private:
int year;
int month;
int day;
};
1)使用初始化表來實現對數據成員的初始化
初始化表的一般格式:
類名::構造函數名(參數列表):初始化表
{
構造函數其他實現代碼
}
初始化表的格式:
對象成員1(參數名或常量),對象成員2(參數名或常量),……對象成員n(參數名或常量)
例如CDate的構造函數可以改用以下形式:
Cdate:: Cdate(int y, int m,int d): year(y), month(m),day(d){ }
2)關於構造函數C++規定:
(1)每個類必須有一個構造函數,如果沒有就不能創建任何對象;
(2)若沒有定義任何一個構造函數,C++提供一個默認的構造函數,該構造函數沒有參數,不做任何工作,相當一個空函數,例如:
Cdate::Cdate()
{ }
所以在講構造函數以前也可以定義一個對象,就是因爲系統提供的默認構造函數。
(3)只要C++提供一個構造函數(不一定是沒有參數的),C++不再提供默認的構造函數。也就是說爲類定義了一個帶參數的構造函數,還想要創建無參的對象時,則需要自己定義一個默認構造函數 。
3)拷貝構造函數和默認拷貝構造函數
·拷貝構造函數的作用:用一個已知對象來初始化另一個對象。
·拷貝構造函數定義格式
類名::拷貝構造函數名(類名& 引用名)
Tdate ::Tdate(Tdate & d); //形參是一個對象的引用
CString( const CString & stringSrc );
·通常在下述三種情況下,需要用拷貝初始化構造函數:
(1)明確表示由一個對象初始化另一個對象時;如Cdate day3(d1);
(2)當對象作爲函數實參傳遞給函數形參時;如 fun(Cdate day);
(3)當對象作爲函數的返回值,創建一個臨時對象時。(因爲返回的局部變量在函數結束時已被銷燬,所以編譯器都會先建立一個此對象的臨時拷貝,而在建立該臨時拷貝時就會調用類的拷貝構造函數。)
#include <iostream.h>
class CComplex
{
public:
CComplex(double, double);
CComplex(CComplex &c);
CComplex add(CComplex & x);
void Print();
private:
double real;
double imag;
};
CComplex::CComplex (double r=0.0, double i=0.0)
{
real = r;
imag = i;
cout<<"調用兩個參數的構造函數"<<endl;
}
CComplex::CComplex (CComplex &c)
{
real = c.real;
imag = c.imag;
cout<<"調用拷貝構造函數"<<endl;
}
void CComplex::Print()
{
cout << "(" << real << "," << imag << ")" << endl;
}
void f(CComplex n) //對象作爲函數參數
{
cout<<"n=";
n. Print();
}
CComplex CComplex::add(CComplex & x)
{
CComplex y(real+x.real ,imag+x.imag );
return y;
}
void main(void)
{
CComplex a(3.0,4.0), b(5.6,7.9);
CComplex c(a);
cout << "a = ";
a.Print();
cout << "c = ";
c.Print();
f(b);
c=a.add(b);
c.Print ();
}
當用戶自定義了拷貝構造函數,所用一個對象創建另一個對象時,系統自動調用了用戶自定義拷貝構造函數。如果用戶沒有自己定義拷貝構造函數,那麼編譯系統會自動會提供一個默認的拷貝構造函數。
默認的拷貝構造函數所做的工作是將一個對象的全部數據成員賦值另一個對象的數據成員。C++把這種只進行對象數據成員簡單賦值,稱之爲“淺拷貝”。
#include <iostream.h>
#include <string.h>
class CClass
{
public:
CClass (char *cName="",int snum=0);
~ CClass ();
void Print();
private:
char * pname;
int num;
};
CClass::CClass (char *cName,int snum)
{
int length = strlen(cName);
pname = new char[length+1];
if (pname!=NULL)
{
strcpy(pname,cName);
}
num=snum;
cout<<"創建班級:"<<pname<<endl;
}
CClass::~CClass ()
{
cout<<"析構班級:"<<pname<<endl;
delete pname;
}
void CClass::Print()
{
cout<<pname<<"班的人數爲:"<<num<<endl;
}
void main()
{
CClass c1("計算機061班",56);
CClass c2 (c1);
c1.Print();
c2.Print();
}
此時c1,c2內存分配情況(調用默認拷貝構造函數,淺拷貝)
默認構造函數的淺拷貝意味着執行c2.pname = c1.pname.這將造成三個錯誤:一是c1.pname和c2.pname指向同一塊內存,c1或c2任意一方變動都會影響另一方。二是對象被析構時pnme被釋放了兩次。
應自定義一個深拷貝的拷貝構造函數
CClass (CClass &p)
{
pname = new char[strlen(p.pname )+1];
if (pname!=0)
{
strcpy(pname,p.pname);
}
num=p.num ;
cout<<"創建班級的拷貝:"<<pname<<endl;
}
4)拷貝構造函數和賦值函數
拷貝構造函數和賦值函數很容易混淆。拷貝構造函數時對象在創建時調用的,而賦值函數只能被已存在了的對象調用。
String a("hello");
String b("world");
String c = a;//調用了拷貝構造函數,最好寫成c(a);
c = b;//調用了賦值函數
5)類對象作爲成員
有類對象作爲成員稱爲組合類
·通過構造函數的初始化表爲內嵌對象初始化
格式爲:
類名::構造函數(參數表):內嵌對象1(參數表1),內嵌對象2(參數表2),…
{
構造函數體
}
·組合類構造函數的執行順序爲:
(1)按內嵌對象的聲明順序依次調用內嵌對象的構造函數
(2)然後執行組合類本身的構造函數。
#include <iostream.h>
#include <string.h>
class Cdate
{
public:
Cdate(int y=1985, int m=1,int d=1)
{
year=y;
month=m;
day=d;
cout<<"調用日期類的構造函數"<<endl;
}
Cdate(Cdate &s)
{
year=s.year;
month=s.month;
day=s.day;
cout<<"調用日期類的拷貝構造函數"<<endl;
}
~ Cdate()
{
cout<<"調用日期類的析構函數"<<endl;
}
private:
int year,month ,day;
};
class CStudentID
{
public:
CStudentID(int i)
{
value=i;
cout<<"構造學號"<<value<<endl;
}
~CStudentID()
{
cout<<"析構學號"<<value<<endl;
}
private:
int value;
};
class CStudent
{
public:
CStudent (char*,char,int, Cdate &);
~ CStudent ();
private:
char name[20];
char sex;
CStudentID id ;
Cdate birthday;
};
CStudent:: CStudent (char *na, char s ,int i, Cdate &d):id(i),birthday(d)
{
strcpy(name,na);
sex=s;
cout<<"調用學生" <<name<< "的構造函數"<<endl;
}
CStudent::~ CStudent ()
{
cout<<"調用學生" <<name<<"的析構函數"<<endl;
}
int main( )
{
Cdate day1(2000,1,1);
//定義學生對象
CStudent stud1("張三“, 'm', 2006102, day1);
return 0;
}
6)關於類的兩種初始化方式
class A
{...
A(int x);//A的構造函數
};
class B: public A
{...
B(int x, int y);//B的構造函數
};
B::B(int x, int y): A(x)//在初始化表裏調用A的構造函數
{
...
}
初始化表位於函數參數表之後,卻在函數體{}之前。說明該表裏的初始化工作發生在函數體內的任何代碼被執行之前。
·如果類存在繼承關係,派生類必須在其初始化表裏調用基類的構造函數
·類的數據成員可以採用初始化表或函數體內賦值兩種方式,這兩種方式的效率不完全相同。
非內部數據類型的成員對象應當採用第一種方式初始化,以獲取更高效率。例如:
class A
{...
A();//無參構造函數
A(const A &other);//拷貝構造函數
A & operate =(const A &other);//賦值函數
};
class B
{
public:
B(const A &a);//B的構造函數
private:
A m_a;//成員對象
};
示例(a)
B::B(const A &a) :m_a(a)
{
...
}
示例(b)
B::B(const A &a)
{
m_a = a;
...
}
示例(a)中,類B的構造函數在其初始化表裏調用了類A的拷貝構造函數,從而將成員對象m_a初始化。
示例(b)中,類B的構造函數在函數體內用賦值的方式將成員對象m_a初始化。我們看到的只是一條賦值語句,但實際上B的構造函數幹了兩件事:先暗地裏創建m_a對象(調用了A的無參構造函數),再調用類A的賦值函數,將參數a賦給m_a.
示例(a)成員對象在初始化表中被初始化,示例(b)在函數體內被初始化。
對於內部數據類型的數據成員而言,兩種初始化方式的效率幾乎沒有區別,但後者的似乎更清晰些。
7)派生類構造函數:
構造函數和析構函數是不能被繼承的。
派生類的成員是由基類中的數據成員和派生類中新增的數據成員共同構成。
對繼承過來的基類成員的初始化工作也得由派生類的構造函數完成。也就是說在定義派生類的構造函數時,既要初始化派生類新增數據,又要初始化基類的成員。
所以,在定義派生類的構造函數時,有兩步需要做:
l編寫代碼完成自己的數據成員進行初始化
l調用基類構造函數使基類數據成員得以初始化。
單一繼承的構造函數的定義形式爲:
派生類名: 派生類構造函數名(參數總表) : 基類構造函數名 (參數名錶)
{
派生類新增成員的初始化語句
};
定義派生類的構造函數時,在構造函數的參數總表中包括基類構造函數所需的參數和派生類新增的數據成員初始化所需的參數。冒號後面基類構造函數名 (參數名錶),表示要調用基類的構造函數。
一個派生類中新增加的成員可以是簡單的數據成員,也可以是類對象。派生類可以是單一繼承,也可以是多重繼承。假如派生類是多重繼承,並且新增數據成員有一個或多個類對象,那麼派生類需要初始化的數據有三部分:繼承的成員、新增類對象的成員和新增普通成員。這種複雜派生類的構造函數定義如下:
派生類名::派生類構造函數名(總參數表)
:基類構造函數名1 (參數表1),
基類構造函數名2 (參數表2), ……
子對象名1(參數表n),
子對象名2(數表n+1) ……
{
派生類新增普通數據成員的初始化;
}
8)派生類析構函數
析構函數的功能是做善後工作,析構函數無返回類型也沒有參數。
派生類析構函數定義格式與非派生類無任何差異,只要在函數體內把派生類新增一般成員處理好就可以了。基類成員的善後工作,系統自己調用基類的析構函數來完成。
如果沒有顯示的定義析構函數,系統會自動生成一個默認的析構函數。
析構函數各部分執行次序與構造函數相反,首先對派生類新增成員析構,然後對基類成員析構。
9)構造和析構順序
構造函數在創建對象時自動調用,調用的順序是按照對象定義的次序。析構函數的調用順序正好與構造函數相反。對於同一存儲類別的對象是先構造的對象後析構,後構造的對象先析構。
派生類構造函數的調用順序如下:
(1)基類構造函數。按它們在派生類定義中的先後順序,依次調用。
(2)子對象的構造函數。按它們在派生類定義中的先後順序(不受它們在初始化表中的次序的影響),依次調用。
(3)派生類的構造函數。
複雜派生類的析構函數,只需要編寫對新增普通成員的善後處理,而對類對象和基類的善後工作是由類對象和基類的析構函數完成的。析構函數的調用順序與構造函數相反。
10)析構順序筆試題
下面代碼輸出是什麼?
#include<iostream>
using namespace std;
class C
{
int a;
public:
C(int aa=0) { a=aa; }
~C() { cout<<"Destructor C!"<<a<<endl;}
};
class D: public C
{
int b;
public:
D(int aa=0, int bb=0):C(aa) { b=bb; }
~D() { cout<<"Destructor D!"<<b<<endl;}
};
void test()
{
D x(5);
D y(6,7);
}
void main()
{
test();
}
Destructor D!7
Destructor C!6
Destructor D!0
Destructor C!5
2.String類
每個類只有一個析構函數和一個賦值函數,但可以有多個構造函數(包括一個拷貝構造函數,其它的成爲普通構造函數)。對於任意一個類A,如果不想編寫上述函數,C++編譯器將自動爲A產生四個缺省的函數,如
A();//缺省的無參構造函數
A(const A &a);//缺省的拷貝構造函數
~A();//缺省的析構函數
A & operate =(const A &a);//缺省的賦值函數
既然能自動生成函數,爲什麼還要自己編寫呢?
原因如下:
(1)如果使用“缺省的無參構造函數”和“缺省的析構函數”,等於放棄了自主“初始化”和“清除”的機會。
(2)“缺省的拷貝構造函數”和“缺省的賦值函數”均採用“位拷貝”而非“值拷貝”的方式實現,倘若類中含有指針變量,這兩個函數註定將出錯。
//String.h
#include <stdio.h>
class String
{
public:
String(const char * str = NULL); //普通構造函數
String(const String &other); //拷貝構造函數
~String(); //析構函數
String & operator=(const String &other); //賦值函數
private:
char *m_data; //用於保存字符串
};
//String.cpp
#include "String.hpp"
#include <string.h>
//String的普通構造函數
String::String(const char * str)
{
if(str == NULL)
{
m_data = new char[1];
*m_data = '\0';
}
else
{
int length = strlen(str);
m_data = new char[length+1];
strcpy(m_data, str);
}
}
//String的析構函數
String::~String()
{
if(m_data != NULL)
{
delete [] m_data;
}
}
//String的拷貝構造函數
String::String(const String &other)
{
//允許操作other的私有成員
int length = strlen(other.m_data);
m_data = new char[length+1];
strcpy(m_data, other.m_data);
}
//賦值函數
String & String::operator=(const String &other)
{
//(1)檢查自賦值
if(this == &other)
{
return *this;
}
//(2)釋放原有的內存資源
delete [] m_data;
//(3)分配新的內存資源,並複製內容
int length = strlen(other.m_data);
m_data = new char[length+1];
strcpy(m_data, other.m_data);
//(4)返回本對象的引用
return *this;
}
類String拷貝構造函數與普通構造函數的區別是:在函數入口處無需與NULL進行比較,這是因爲“引用”不可能是NULL,而“指針”可以爲NULL。
賦值函數分四步實現:
(1)第一步,檢查自賦值。你可能會認爲多此一舉,難道有人會愚蠢到寫出a=a這樣的自賦值語句!的確不會。但是間接的自賦值仍有可能出現,例如
//內容自賦值
b = a;
...
c = b;
...
a = c;
//地址自賦值
b = &a;
...
a = *b;
也許有人會說“即使出現自賦值,可以不理睬,大不了花點時間讓對象複製自己而已,反正不會出錯。”
他真的說錯了。看看第二步的delete,自殺後還能複製自己嗎?所以,如果發現自賦值,應該立即終止函數。注意不要將檢查自賦值的if語句
if(this == &other)
錯寫成
if(*this == other)
(2)第二步,用delete釋放原有的內存資源。如果現在不釋放,以後就沒有機會了,將會造成內存泄漏。
(3)第三步,分配新的內存資源,並複製字符串。注意函數strlen返回的是有效字符串長度,不包含結束符'\0'。函數strcpy則連'\0'一起復制。
(4)第四步,返回本對象的引用,目的是爲了實現對象a = b =c這樣的鏈式表達。注意不要將return *this錯寫成 return this。那麼能寫成return other嗎?效果不是一樣?不可以,我們不知道參數other的生命週期。有可能other是個臨時對象,在賦值結束後立馬消失,那麼return other返回的將是垃圾。另外,如果不採用“引用傳遞”的方式返回而採用“值傳遞”的方式,雖然功能仍然正確,但由於return語句要把*this拷貝到保存返回值的外部存儲單元之中,增加了不必要的開銷,降低了賦值函數的效率。如:
String a,b,c;
...
a = b;//如果用“值傳遞”,將產生一次*this拷貝
a = b = c;//如果用“值傳遞”,將產生兩次*this拷貝
int a = 1;
int b = 2;
int c = 3;
a=b=c;
cout<<a<<b<<c<<endl; //333,因爲=是從右往左結合的,所以是相當於先是b=c,然後是a=b。
如果將a=b=c換成(a=b)=c,則輸出結果就變成323,相當於是a=b,然後a=c(因爲a=b返回的是對a的引用)。
若const A& operator=(const A& a);將返回值設成const是不正確的。a=b=c依然正確。(a=b)=c就不正確了。
在const A::operator=(const A& a)中,參數列表中的const的用法正確,而當這樣連續賦值的時侯,問題就出現了:
A a,b,c:
(a=b)=c;
因爲a.operator=(b)的返回值是對a的const引用,不能再將c賦值給const常量。
3.在類的派生類中實現類的基本函數
基類的構造函數、析構函數、賦值函數都不能被派生類繼承。如果類之間存在繼承關係,在編寫上述基本函數時應該注意以下事項:
·派生類的構造函數應在其初始化表裏調用基類的構造函數
·基類與派生類的析構函數應該爲虛函數
·在編寫派生類的賦值函數時,注意不要忘記對基類的數據成員重新賦值。例如:
class Base
{
public:
...
Base & operate =(const Base &other);//類Base的賦值函數
private:
int m_i;
int m_j;
int m_k;
};
class Derived : public Base
{
public:
...
Drived & operate =(const Derived &other);//類Drived的賦值函數
private:
int m_x;
int m_y;
int m_z;
};
Drived & Drived::operate =(const Derived &other)
{
//(1)檢查自賦值
if(this == &other)
{
return *this;
}
//(2)對基類的數據成員重新賦值
Base::operate =(other);//因爲不能直接操作私有數據成員
//(3)對派生類的數據成員賦值
m_x = other.m_x;
m_y = other.m_y;
m_z = other.m_z;
//(4)返回本對象的引用
return *this;
}
總結:構造析構順序
不存在繼承關係:對象成員(聲明順序),類本身
存在繼承關係:基類,派生類對象成員,派生類
析構順序則和構造順序完全相反。