以良好的方式編寫C++ class
假設(shè)現(xiàn)在我們要實(shí)現(xiàn)一個復(fù)數(shù)類complex,在類的實(shí)現(xiàn)過程中探索良好的編程習(xí)慣。
① Header(頭文件)中的防衛(wèi)式聲明
complex.h:
#ifndef__COMPLEX__
#define__COMPLEX__
classcomplex
{
}
#endif
防止頭文件的內(nèi)容被多次包含。
② 把數(shù)據(jù)放在private聲明下,提供接口訪問數(shù)據(jù)
#ifndef__COMPLEX__
#define__COMPLEX__
classcomplex
{
public:
doublereal()const{returnre;}
doubleimag()const{returnim;}
private:
doubelre,im;
}
#endif
③ 不會改變類屬性(數(shù)據(jù)成員)的成員函數(shù),全部加上const聲明
例如上面的成員函數(shù):
doublereal()`const`{returnre;}
doubleimag()`const`{returnim;}
既然函數(shù)不會改變對象,那么就如實(shí)說明,編譯器能幫你確保函數(shù)的const屬性,閱讀代碼的人也明確你的意圖。
而且,const對象才可以調(diào)用這些函數(shù)——const對象不能夠調(diào)用非const成員函數(shù)。
④ 使用構(gòu)造函數(shù)初始值列表
classcomplex
{
public:
complex(doubler=0,doublei=0)
:re(r),im(i){}
private:
doubelre,im;
}
在初始值列表中,才是初始化。在構(gòu)造函數(shù)體內(nèi)的,叫做賦值。
⑤如果可以,參數(shù)盡量使用reference to const
為complex 類添加一個+=操作符:
classcomplex
{
public:
complex&operator+=(constcomplex&)
}
使用引用避免類對象構(gòu)造與析構(gòu)的開銷,使用const確保參數(shù)不會被改變。內(nèi)置類型的值傳遞與引用傳遞效率沒有多大差別,甚至值傳遞效率會更高。
例如,傳遞char
類型時,值傳遞只需傳遞一個字節(jié);引用實(shí)際上是指針實(shí)現(xiàn),需要四個字節(jié)(32位機(jī))的傳遞開銷。但是為了一致,不妨統(tǒng)一使用引用。
⑥ 如果可以,函數(shù)返回值也盡量使用引用
以引用方式返回函數(shù)局部變量會引發(fā)程序未定義行為,離開函數(shù)作用域局部變量被銷毀,引用該變量沒有意義。但是我要說的是,如果可以,函數(shù)應(yīng)該返回引用。
當(dāng)然,要放回的變量要有一定限制:該變量的在進(jìn)入函數(shù)前,已經(jīng)被分配了內(nèi)存。以此條件來考量,很容易決定是否要放回引用。而在函數(shù)被調(diào)用時才創(chuàng)建出來的對象,一定不能返回引用。
說回operator +=
,其返回值就是引用,原因在于,執(zhí)行a+=b
時,a
已經(jīng)在內(nèi)存上存在了。
而operator +
,其返回值不能是引用,因?yàn)?code style="font-size:14px;font-family:'Operator Mono', Consolas, Monaco, Menlo, monospace;padding:2px 4px;margin-right:2px;margin-left:2px;color:rgb(233,105,0);background:rgb(248,248,248);">a+b的值,在調(diào)用operator +
的時候才產(chǎn)生。
下面是operator+=
與’operator +’ 的實(shí)現(xiàn):
inlinecomplex&complex::operator+=(constcomplex&r)
{
this->re+=r->re;
this->im+=r->im;
return*this;
}
inlinecomplexoperator+(constcomplex&x,constcomplex&y)
{
returncomplex(real(x)+real(y),//新創(chuàng)建的對象,不能返回引用
imag(x)+imag(y));
}
在operator +=
中返回引用還是必要的,這樣可以使用連續(xù)的操作:
c3+=c2+=c1;
⑦ 如果重載了操作符,就考慮是否需要多個重載
就我們的復(fù)數(shù)類來說,+
可以有多種使用方式:
complexc1(2,1);
complexc2;
c2=c1+c2;
c2=c1+5;
c2=7+c1;
為了應(yīng)付怎么多種加法,+
需要有如下三種重載:
inlinecomplexoperator+(constcomplex&x,constcomplex&y)
{
returncomplex(real(x)+real(y),
imag(x+imag(y););
}
inlinecomplexoperator+(constcomplex&x,doubley)
{
returncomplex(real(x)+y,imag(x));
inlinecomplexoperator+(doublex,constcomplex&y)
{
returncomplex(x+real(y),imag(y));
}
⑧ 提供給外界使用的接口,放在類聲明的最前面
這是某次面試中,面試官大哥告訴我的。想想確實(shí)是有道理,類的用戶用起來也舒服,一眼就能看見接口。
Class with pointer member(s):記得寫B(tài)ig Three
C++的類可以分為帶指針數(shù)據(jù)成員與不帶指針數(shù)據(jù)成員兩類,complex
就屬于不帶指針成員的類。而這里要說的字符串類String
,一般的實(shí)現(xiàn)會帶有一個char *
指針。帶指針數(shù)據(jù)成員的類,需要自己實(shí)現(xiàn)class三大件:拷貝構(gòu)造函數(shù)、拷貝賦值函數(shù)、析構(gòu)函數(shù)。
classString
{
public:
String(constchar*cstr=0);
String(constString&str);
String&operator=(constString&str);
~String();
char*get_c_str()const{returnm_data};
private:
char*m_data;
}
如果沒有寫拷貝構(gòu)造函數(shù)、賦值構(gòu)造函數(shù)、析構(gòu)函數(shù),編譯器默認(rèn)會給我們寫一套。然而帶指針的類不能依賴編譯器的默認(rèn)實(shí)現(xiàn)——這涉及到資源的釋放、深拷貝與淺拷貝的問題。在實(shí)現(xiàn)String類的過程中我們來闡述這些問題。
①析構(gòu)函數(shù)釋放動態(tài)分配的內(nèi)存資源
如果class里有指針,多半是需要進(jìn)行內(nèi)存動態(tài)分配(例如String),析構(gòu)函數(shù)必須負(fù)責(zé)在對象生命結(jié)束時釋放掉動態(tài)申請來的內(nèi)存,否則就造成了內(nèi)存泄露。
局部對象在離開函數(shù)作用域時,對象析構(gòu)函數(shù)被自動調(diào)用,而使用new動態(tài)分配的對象,也需要顯式的使用delete來刪除對象。而delete實(shí)際上會調(diào)用對象的析構(gòu)函數(shù),我們必須在析構(gòu)函數(shù)中完成釋放指針m_data所申請的內(nèi)存。下面是一個構(gòu)造函數(shù),體現(xiàn)了m_data的動態(tài)內(nèi)存申請:
/*String的構(gòu)造函數(shù)*/
inline
String::String(constchar*cstr=0)
{
if(cstr)
{
m_data=newchar[strlen(cstr)+1];//這里,m_data申請了內(nèi)存
strcpy(m_data,cstr);
}
else
{
m_data=newchar[1];
*m_data='';
}
}
這個構(gòu)造函數(shù)以C風(fēng)格字符串為參數(shù),當(dāng)執(zhí)行
String*p=newString("hello");
m_data
向系統(tǒng)申請了一塊內(nèi)存存放字符串hello
:
析構(gòu)函數(shù)必須負(fù)責(zé)把這段動態(tài)申請來的內(nèi)存釋放掉:
inline
String::~String()
{
delete[]m_data;
}
②賦值構(gòu)造函數(shù)與復(fù)制構(gòu)造函數(shù)負(fù)責(zé)進(jìn)行深拷貝
來看看如果使用編譯器為String默認(rèn)生成的拷貝構(gòu)造函數(shù)與賦值操作符會發(fā)生什么事情。默認(rèn)的復(fù)制構(gòu)造函數(shù)或賦值操作符所做的事情是對類的內(nèi)存進(jìn)行按位的拷貝,也稱為淺拷貝,它們只是把對象內(nèi)存上的每一個bit復(fù)制到另一個對象上去,在String中就只是復(fù)制了指針,而不復(fù)制指針?biāo)竷?nèi)容。現(xiàn)在有兩個String對象:
Stringa("Hello");
Stringb("World");
a、b在內(nèi)存上如圖所示:
如果此時執(zhí)行
b=a;
淺拷貝體現(xiàn)為:
存儲World
的內(nèi)存塊沒有指針?biāo)赶颍呀?jīng)成了一塊無法利用內(nèi)存,從而發(fā)生了內(nèi)存泄露。不止如此,如果此時對象a
被刪除,使用我們上面所寫的析構(gòu)函數(shù),存儲Hello
的內(nèi)存塊就被釋放調(diào)用,此時b.m_data
成了一個野指針。
來看看我們自己實(shí)現(xiàn)的構(gòu)造函數(shù)是如何解決這個問題的,它復(fù)制的是指針?biāo)傅膬?nèi)存內(nèi)容,這稱為深拷貝
/*拷貝賦值函數(shù)*/
inlineString&String::operator=(constString&str)
{
if(this==&str)//①
return*this;
delete[]m_data;//②
m_data=newchar[strlen(str.m_data)+1];//③
strcpy(m_data,str.m_data);//④
return*this
}
這是拷貝賦值函數(shù)的經(jīng)典實(shí)現(xiàn),要點(diǎn)在于:
① 處理自我賦值,如果不存在自我賦值問題,繼續(xù)下列步驟:② 釋放自身已經(jīng)申請的內(nèi)存③ 申請一塊大小與目標(biāo)字符串一樣大的內(nèi)存④ 進(jìn)行字符串的拷貝
對于a = b
,②③④過程如下:
同樣的,復(fù)制構(gòu)造函數(shù)也是一個深拷貝的過程:
inlineString::String(constString&str)
{
m_data=newchar[strlen(str)+1];
strcpy(m_data,str.m_data);
}
另外,一定要在operator = 中檢查是否self assignment 假設(shè)這時候確實(shí)執(zhí)行了對象的自我賦值,左右pointers指向同一個內(nèi)存塊,前面的步驟②delete掉該內(nèi)存塊造成下面的結(jié)果。當(dāng)企圖對rhs的內(nèi)存進(jìn)行訪問是,結(jié)果是未定義的。
static與類
① 不和對象直接相關(guān)的數(shù)據(jù),聲明為static
想象有一個銀行賬戶的類,每個人都可以開銀行賬戶。存在銀行利率
這個成員變量,它不應(yīng)該屬于對象,而應(yīng)該屬于銀行這個類,由所有的用戶來共享。
static修飾成員變量時,該成員變量放在程序的全局區(qū)中,整個程序運(yùn)行過程中只有該成員變量的一份副本。而普通的成員變量存在每個對象的內(nèi)存中,若把銀行利率
放在每個對象中,是浪費(fèi)了內(nèi)存。
② static成員函數(shù)沒有this指針
static成員函數(shù)與普通函數(shù)一樣,都是只有一份函數(shù)的副本,存儲在進(jìn)程的代碼段上。不一樣的是,static成員函數(shù)沒有this指針
,所以它不能夠調(diào)用普通的成員變量,只能調(diào)用static成員變量。普通成員函數(shù)的調(diào)用需要通過對象來調(diào)用,編譯器會把對象取地址,作為this指針的實(shí)參傳遞給成員函數(shù):
obj.func()--->Class::fun(&obj);
而static成員函數(shù)即可以通過對象來調(diào)用,也可以通過類名稱來調(diào)用。
③在類的外部定義static成員變量
另一個問題是static成員變量的定義。static成員變量必須在類外部進(jìn)行定義:
classA
{
private:
staticinta;//①
}
intA::a=10;//②
注意①是聲明,②才是定義,定義為變量分配了內(nèi)存。
④static與類的一些小應(yīng)用
這些可以用來應(yīng)付一下面試,在實(shí)現(xiàn)單例模式的時候,static成員函數(shù)與static成員變量得到了使用,下面是一種稱為”餓漢式“的單例模式的實(shí)現(xiàn):
classA
{
public:
staticA&getInstance();
setup(){...};
private:
A();
A(constA&rhs);
staticAa;
}
這里把class A的構(gòu)造函數(shù)都設(shè)置為私有,不允許用戶代碼創(chuàng)建對象。要獲取對象實(shí)例需要通過接口getInstance
。”餓漢式“缺點(diǎn)在于無論有沒有代碼需要a
,a
都被創(chuàng)建出來。下面是改進(jìn)的單例模式,稱為”懶漢式“:
classA
{
public:
staticA&getInstance();
setup(){....};
private:
A();
A(constA&rsh);
...
};
A&A::getInstance()
{
staticAa;
returna;
}
“懶漢式”只有在真正需要a
時,調(diào)用getInstance
才創(chuàng)建出唯一實(shí)例。這可以看成一個具有拖延癥的單例模式,不到最后關(guān)頭不干活。很多設(shè)計(jì)都體現(xiàn)了這種拖延的思想,比如string的寫時復(fù)制,真正需要的時候才分配內(nèi)存給string對象管理的字符串。
原文標(biāo)題:漫談 C++:良好的編程習(xí)慣與編程要點(diǎn)
文章出處:【微信公眾號:Linux愛好者】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
-
函數(shù)
+關(guān)注
關(guān)注
3文章
4307瀏覽量
62432 -
C++
+關(guān)注
關(guān)注
22文章
2104瀏覽量
73497 -
Class
+關(guān)注
關(guān)注
0文章
53瀏覽量
19714 -
CONST
+關(guān)注
關(guān)注
0文章
44瀏覽量
8151
原文標(biāo)題:漫談 C++:良好的編程習(xí)慣與編程要點(diǎn)
文章出處:【微信號:LinuxHub,微信公眾號:Linux愛好者】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論