動态記憶體和類
如果有這樣的strbad類:
#include<iosream>
#ifdef STRBAD_H_
#define SREBAD_H_
class strbad{
private:
char * str;
int len;
static int num;
public:
strbad(const char *s);
strbad();
~strbad();
friend std::ostream & operator<<(std::ostream &os,const strbad &st);
}
#endif
#include<cstring>
#include"stringbad.h"
using std::cout;
int strbad::num=0;//靜态成員變量不能在聲明中初始化
//除非靜态變量是const或者枚舉型
strbad::strbad(const char *s){
len=std::strlen(s);
str=new char[len+1];//str是指針,是以必須提供記憶體
std::strcopy(str,s);//不能用str=s,這樣的話沒有建立副本,而隻是複制了指針
num++;
}
strbad::strbad(){
len=4;
str=new char[4];
std::strcopy(str,"C++");
num++;
}
strbad::~strbad(){
num--;
delete[] str;
//new對應delete,new[]對應delete[]
}
std::ostream & operator<<(std::ostream &os,const strbad &st){
os<<st.str;
return os;
}
看似設計沒有什麼問題,但下面的語句證明它其實大有問題:
{
strbad a=srebad("a");
call(a);
cout<<a;//事實證明在這裡a已經被析構了
strbad b=a;
}
cout<<strbad::num;//發現num為負數
void call(strbad s);//這裡是值傳遞
所有的原因在于忽略了類的複制構造函數。
strbad(const strbad &);
編譯器自動生成的複制構造函數不知道如何處理num,導緻num被弄亂了。
下面一些建立對象的情況會導緻調用複制構造函數:
strbad ditto(motto);
strbad metto=motto;
strbad also=strbad(motto);
strbad * p=new strbad(motto);
當程式生成了對象副本時,會調用複制構造函數。具體比如:當函數按值傳遞對象(call方法)或函數傳回對象時,都将使用複制構造函數。是以我們應該盡可能按引用傳遞對象。
預設複制構造函數逐個複制非靜态成員,複制的是成員的值。
是以我們可以發現,在調用call方法時,s被a指派,但由于類中存儲的值為指針類型,是以使用在調用s的析構函數時會把s的指針(同樣是a的指針)指向的動态記憶體釋放,是以我們會發現a的動态記憶體已經被delete了。
我們可以定義一個顯式複制構造函數:
strbad:strbad(const strbad & st){
num++;
len=st.len;
str=new char[len+1];
std::strcpy(str,st.str);
}
有些類成員是使用new初始化的、指向資料的指針,而不是資料本身,是以我們需要深度複制(改寫預設複制構造函數)。
當然我們也要重載指派運算符。
strbad b;
b=a;//不會觸發複制構造函數,需要重載=
有以下注意點:
(1)目标對象可能引用了以前配置設定的資料,函數應使用delete來釋放它們。
(2)函數應當避免将對象賦給自身,否則可能會導緻在指派前删除了自身的内容。
(3)函數傳回一個指向調用對象的引用。
strbad & strbad::operator=(const strbad& st){
if (this==&st) return *this;
delete[] str;
len=st.len;
str=new char[len+1];
std::strcpy(str,st.str);
return *this;
}
如果指派為str=0,這說明str為空指針,此時使用delete[] str是可行的。
可以将成員函數聲明為靜态的,這樣就不能通過對象調用靜态成員函數,甚至不能用this,靜态成員函數也不與特定的對象相關聯,是以隻能使用靜态資料成員。
構造函數中new的注意事項
有如下注意點:
(1)如果在構造函數中用new,應該在析構函數中用delete。
(2)new和delete必須相容,new對應delete,new[]對應delete[]
(3)如果有多個構造函數,則必須用相同的方法new,要麼都帶中括号,要麼都不帶,因為隻有一個析構函數。delete(無論有沒有中括号)都可以用于空指針(0,NULL)
(4)應定義一個複制構造函數。
(5)應重載一個指派運算符。
對于定位new運算符,有如下注意點:
pc1=new(buffer) strbad;
pc2=new(buffer) strbad;//no
pc2=new(buffer+sizeof(strbad)) strbad("aaa");//yes
再執行buffer的delete操作前,需要顯式地調用析構函數:
pc1->~strbad();
pc2->~strbad();
delete[] buffer;//這樣才算完全銷毀
有關傳回對象的說明
1.傳回指向const對象的引用
如果函數傳回傳遞給它的對象,可以通過傳回引用來提高效率,因為傳回引用不會調用複制構造函數。而且,引用指向的對象應該在調用函數執行時存在。除此以外,傳回的類型必須為const才能比對。
2.傳回指向非const對象的引用
兩種常見的情形是,重載指派運算符和cout一起使用的<<。前者是提高效率,後者是必須那麼做。
3.傳回對象
如果被傳回的對象是被調用函數中的局部變量,則不應按引用方式傳回它。此時将使用複制構造函數來生成傳回的對象。
4.傳回const對象
net=force1+force2;
force1+force2=net;//傳回const對象将不允許那麼做
使用const能夠防止那樣奇怪的錯誤,當然傳回的對象将會是const的。