漫談C++:良好的編程習慣與編程要點

以良好的方式編寫C++ class

假設如今咱們要實現一個複數類complex,在類的實現過程當中探索良好的編程習慣。面試

① Header(頭文件)中的防衛式聲明
complex.h:編程

# ifndef  __COMPLEX__
# define __COMPLEX__
class complex
{
    
}
# endif

防止頭文件的內容被屢次包含。函數

② 把數據放在private聲明下,提供接口訪問數據this

# ifndef  __COMPLEX__
# define __COMPLEX__
class complex
{
    public:
        double real() const {return re;}
        double imag() const {return im;}
    private:
        doubel re,im;
}
# endif

③ 不會改變類屬性(數據成員)的成員函數,所有加上const聲明
例如上面的成員函數:設計

double real () `const` {return re;}
double imag() `const` {return im;}

既然函數不會改變對象,那麼就如實說明,編譯器能幫你確保函數的const屬性,閱讀代碼的人也明確你的意圖。
並且,const對象才能夠調用這些函數——const對象不可以調用非const成員函數。3d

④ 使用構造函數初始值列表指針

class complex
{
    public:
        complex(double r = 0, double i =0)
            : re(r), im(i)  { }
    private:
        doubel re,im;
}

在初始值列表中,纔是初始化。在構造函數體內的,叫作賦值。code

⑤若是能夠,參數儘可能使用reference to const對象

爲complex 類添加一個+=操做符:blog

class complex
{
    public:
        complex& operator += (const complex &)
}

使用引用避免類對象構造與析構的開銷,使用const確保參數不會被改變。內置類型的值傳遞與引用傳遞效率沒有多大差異,甚至值傳遞效率會更高。例如,傳遞char類型時,值傳遞只需傳遞一個字節;引用其實是指針實現,須要四個字節(32位機)的傳遞開銷。可是爲了一致,不妨統一使用引用。

⑥ 若是能夠,函數返回值也儘可能使用引用
以引用方式返回函數局部變量會引起程序未定義行爲,離開函數做用域局部變量被銷燬,引用該變量沒有意義。可是我要說的是,若是能夠,函數應該返回引用。固然,要返回的變量要有必定限制:該變量的在進入函數前,已經被分配了內存。以此條件來考量,很容易決定是否要返回引用。而在函數被調用時才建立出來的對象,必定不能返回引用。

說回operator +=,其返回值就是引用,緣由在於,執行a+=b時,a已經在內存上存在了。

operator + ,其返回值不能是引用,由於a+b的值,在調用operator +的時候才產生。

下面是operator+= 與'operator +' 的實現:

inline complex & complex :: operator += (const complex & r)
{
        this -> re+= r->re;
        this -> im+= r->im;
        return * this;
}
inline complex operator + (const complex & x , const complex & y)
{
        return complex ( real (x)+ real (y),                        //新建立的對象,不能返回引用
                                 imag(x)+ imag(y));
}

operator +=中返回引用仍是必要的,這樣可使用連續的操做:

c3 += c2 += c1;

⑦ 若是重載了操做符,就考慮是否須要多個重載

就咱們的複數類來講,+能夠有多種使用方式:

complex c1(2,1);
complex c2;
c2 = c1+ c2;
c2 = c1 + 5;
c2 = 7 + c1;

爲了應付怎麼多種加法,+須要有以下三種重載:

inline complex operator+ (const complex & x ,const complex & y)
{
    return complex (real(x)+real(y),
                    imag(x+imag(y););
}
inline complex operator + (const complex & x, double y)
{
    return complex (real(x)+y,imag(x));

inline complex operator + (double x,const complex &y)
{
    return complex (x+real(y),imag(y));
}

⑧ 提供給外界使用的接口,放在類聲明的最前面
這是某次面試中,面試官大哥告訴個人。想一想確實是有道理,類的用戶用起來也舒服,一眼就能看見接口。

Class with pointer member(s):記得寫Big Three

C++的類能夠分爲帶指針數據成員與不帶指針數據成員兩類,complex就屬於不帶指針成員的類。而這裏要說的字符串類String,通常的實現會帶有一個char *指針。帶指針數據成員的類,須要本身實現class三大件:拷貝構造函數、拷貝賦值函數、析構函數。

class String
{
    public:
        String (const char * cstr = 0);
        String (const String & str);
        String & operator = (const String & str);
        ~String();
        char * get_c_str() const {return m_data};
    private:
        char * m_data;
}

若是沒有寫拷貝構造函數、賦值構造函數、析構函數,編譯器默認會給咱們寫一套。然而帶指針的類不能依賴編譯器的默認實現——這涉及到資源的釋放、深拷貝與淺拷貝的問題。在實現String類的過程當中咱們來闡述這些問題。

①析構函數釋放動態分配的內存資源
若是class裏有指針,多半是須要進行內存動態分配(例如String),析構函數必須負責在對象生命結束時釋放掉動態申請來的內存,不然就形成了內存泄露。局部對象在離開函數做用域時,對象析構函數被自動調用,而使用new動態分配的對象,也須要顯式的使用delete來刪除對象。而delete實際上會調用對象的析構函數,咱們必須在析構函數中完成釋放指針m_data所申請的內存。下面是一個構造函數,體現了m_data的動態內存申請:

/*String的構造函數*/
inline 
String ::String (const char *cstr = 0)
{
    if(cstr)
    {
        m_data = new char[strlen(cstr)+1];   // 這裏,m_data申請了內存
        strcpy(m_data,cstr);
    }
    else
    {
        m_data= new char[1];
        *m_data = '\0';
    }
}

這個構造函數以C風格字符串爲參數,當執行

String *p = new String ("hello");

m_data向系統申請了一塊內存存放字符串hello

析構函數必須負責把這段動態申請來的內存釋放掉:

inline 
String ::~String()
{
    delete[]m_data;
}

②賦值構造函數與複製構造函數負責進行深拷貝

來看看若是使用編譯器爲String默認生成的拷貝構造函數與賦值操做符會發生什麼事情。默認的複製構造函數或賦值操做符所作的事情是對類的內存進行按位的拷貝,也稱爲淺拷貝,它們只是把對象內存上的每個bit複製到另外一個對象上去,在String中就只是複製了指針,而不復制指針所指內容。如今有兩個String對象:

String a("Hello");
String b("World");

a、b在內存上如圖所示:

若是此時執行

b = a;

淺拷貝體現爲:

存儲World\0的內存塊沒有指針所指向,已經成了一塊沒法利用內存,從而發生了內存泄露。不止如此,若是此時對象a被刪除,使用咱們上面所寫的析構函數,存儲Hello\0的內存塊就被釋放調用,此時b.m_data成了一個野指針。來看看咱們本身實現的構造函數是如何解決這個問題的,它複製的是指針所指的內存內容,這稱爲深拷貝

/*拷貝賦值函數*/
inline String &String ::operator= (const String & str)
{
    if(this == &str)           //①
        return *this;
    delete[] m_data;        //②
    m_data = new char[strlen(str.m_data)+1];        //③
    strcpy(m_data,str.m_data);            //④
    return *this
}

這是拷貝賦值函數的經典實現,要點在於:
① 處理自我賦值,若是不存在自我賦值問題,繼續下列步驟:
② 釋放自身已經申請的內存
③ 申請一塊大小與目標字符串同樣大的內存
④ 進行字符串的拷貝

對於a = b,②③④過程以下:

一樣的,複製構造函數也是一個深拷貝的過程:

inline String ::String(const String & str )
{
    m_data = new char[ strlen (str) +1];
    strcpy(m_data,str.m_data);
}

另外,必定要在operator = 中檢查是否self assignment 假設這時候確實執行了對象的自我賦值,左右pointers指向同一個內存塊,前面的步驟②delete掉該內存塊形成下面的結果。當企圖對rhs的內存進行訪問是,結果是未定義的。

static與類

① 不和對象直接相關的數據,聲明爲static
想象有一個銀行帳戶的類,每一個人均可以開銀行帳戶。存在銀行利率這個成員變量,它不該該屬於對象,而應該屬於銀行這個類,由全部的用戶來共享。static修飾成員變量時,該成員變量放在程序的全局區中,整個程序運行過程當中只有該成員變量的一份副本。而普通的成員變量存在每一個對象的內存中,若把銀行利率放在每一個對象中,是浪費了內存。

② static成員函數沒有this指針
static成員函數與普通函數同樣,都是隻有一份函數的副本,存儲在進程的代碼段上。不同的是,static成員函數沒有this指針,因此它不可以調用普通的成員變量,只能調用static成員變量。普通成員函數的調用須要經過對象來調用,編譯器會把對象取地址,做爲this指針的實參傳遞給成員函數:

obj.func() ---> Class :: fun(&obj);

而static成員函數便可以經過對象來調用,也能夠經過類名稱來調用。

③在類的外部定義static成員變量

另外一個問題是static成員變量的定義。static成員變量必須在類外部進行定義:

class A
{
    private:
        static int a; //①
}
int A::a = 10;  //②

注意①是聲明,②纔是定義,定義爲變量分配了內存。

④static與類的一些小應用

這些能夠用來應付一下面試,在實現單例模式的時候,static成員函數與static成員變量獲得了使用,下面是一種稱爲」餓漢式「的單例模式的實現:

class A
{
        public:
            static A& getInstance();
            setup(){...};
        private:
            A();
            A(const A & rhs);
            static A a;
}

這裏把class A的構造函數都設置爲私有,不容許用戶代碼建立對象。要獲取對象實例須要經過接口getInstance。」餓漢式「缺點在於不管有沒有代碼須要aa都被建立出來。下面是改進的單例模式,稱爲」懶漢式「:

class A
{
    public: 
        static  A& getInstance();
        setup(){....};
    private:
        A();
        A(const A& rsh);
        ...
};
A& A::getInstance()
{
        static A a;
        return a;
}

"懶漢式"只有在真正須要a時,調用getInstance才建立出惟一實例。這能夠當作一個具備拖延症的單例模式,不到最後關頭不幹活。不少設計都體現了這種拖延的思想,好比string的寫時複製,真正須要的時候才分配內存給string對象管理的字符串。

相關文章
相關標籤/搜索