天天看點

一些基礎的c++概念

最近有時間,看了《高品質c++程式設計》把不清晰的概念,列出來了。

1:   當數組作為函數的參數進行傳遞時,該數組自動退化為同類型的指針

一些基礎的c++概念

void  Func( char  a[ 100 ])

一些基礎的c++概念
一些基礎的c++概念

... {

一些基礎的c++概念

    cout<< sizeof(a) << endl; // 4 位元組而不是100 位元組

一些基礎的c++概念

}

2:  char *p = “world”; // 注意p 指向常量字元串

     p[0] = ‘X’; // 編譯器不能發現該錯誤

指針p 指向常量字元串“world”(位于靜态存儲區,内容為world/0),常量字元串的内容是不可以被修改的。從文法上看,編譯器并不覺得語句p[0]= ‘X’有什麼不妥,但是該語句企圖修改常量字元串的内容而導緻運作錯誤

3.

如果函數傳回值是一個對象,要考慮return 語句的效率。例如return String(s1 + s2);

這是臨時對象的文法,表示“建立一個臨時對象并傳回它”。不要以為它與“先建立一個局部對象temp 并傳回它的結果”是等價的,如

String temp(s1 + s2);

return temp;

實質不然,上述代碼将發生三件事。首先,temp 對象被建立,同時完成初始化;然後拷貝構造函數把temp 拷貝到儲存傳回值的外部存儲單元中;最後,temp 在函數結束時被銷毀(調用析構函數)。然而“建立一個臨時對象并傳回它”的過程是不同的,編譯器直接把臨時對象建立并初始化在外部存儲單元中,省去了拷貝和析構的化費,提高了效率。

類似地,我們不要将

return int(x + y); // 建立一個臨時變量并傳回它

寫成

int temp = x + y;

return temp;

4. 函數的傳回值

一個函數, 用于出錯處理的傳回值一定要清楚,讓使用者不容易忽視或誤解錯誤

5.  常量定義規則

    需要對外公開的常量放在頭檔案中,不需要對外公開的常量放在定義檔案的頭部。為便于管理,可以把不同子產品的常量集中存放在一個公共的頭檔案中

    如果某一常量與其它常量密切相關,應在定義中包含這種關系,而不

   應給出一些孤立的值。

   例如:

  const float RADIUS = 100;

  const float DIAMETER = RADIUS * 2;

6  類中的常量

有時我們希望某些常量隻在類中有效。由于#define 定義的宏常量是全局的,不能達到目的,于是想當然地覺得應該用const 修飾資料成員來實作。const 資料成員的确是存在的,但其含義卻不是我們所期望的。const 資料成員隻在某個對象生存期内是常量,而對于整個類而言卻是可變的,因為類可以建立多個對象,不同的對象其const 資料成員的值可以不同。不能在類聲明中初始化const 資料成員。以下用法是錯誤的,因為類的對象未被建立時,編譯器不知道SIZE 的值是什麼。

class A

{⋯

const int SIZE = 100; // 錯誤,企圖在類聲明中初始化const 資料成員

int array[SIZE]; // 錯誤,未知的SIZE

};

const 資料成員的初始化隻能在類構造函數的初始化表中進行,例如

class A

{⋯

A(int size); // 構造函數

const int SIZE ;

};

A::A(int size) : SIZE(size) // 構造函數的初始化表

{

}

A a(100); // 對象 a 的SIZE 值為100

A b(200); // 對象 b 的SIZE 值為200

怎樣才能建立在整個類中都恒定的常量呢?别指望const 資料成員了,應該用類中

的枚舉常量來實作。例如

class A

{⋯

enum { SIZE1 = 100, SIZE2 = 200}; // 枚舉常量

int array1[SIZE1];

int array2[SIZE2];

};

枚舉常量不會占用對象的存儲空間,它們在編譯時被全部求值。枚舉常量的缺點是:

它的隐含資料類型是整數,其最大值有限,且不能表示浮點數(如PI=3.14159)

7: 靜态變量加字首s_(表示static)

8  布爾變量與零值比較

不可将布爾變量直接與TRUE、FALSE 或者1、0 進行比較。

根據布爾類型的語義,零值為“假”(記為FALSE),任何非零值都是“真”(記為TRUE)。TRUE 的值究竟是什麼并沒有統一的标準。例如Visual C++ 将TRUE 定義為1,而Visual Basic 則将TRUE 定義為-1。

假設布爾變量名字為flag,它與零值比較的标準if 語句如下:

if (flag) // 表示flag 為真

if (!flag) // 表示flag 為假

其它的用法都屬于不良風格,例如:

if (flag == TRUE)

if (flag == 1 )

if (flag == FALSE)

if (flag == 0)

9: 整型變量與零值比較

應當将整型變量用“==”或“!=”直接與0 比較。

假設整型變量的名字為value,它與零值比較的标準if 語句如下:

if (value == 0)

if (value != 0)

不可模仿布爾變量的風格而寫成

if (value) // 會讓人誤解 value 是布爾變量

if (!value)

10:浮點變量與零值比較

不可将浮點變量用“==”或“!=”與任何數字比較。

千萬要留意,無論是float 還是double 類型的變量,都有精度限制。是以一定要

避免将浮點變量用“==”或“!=”與數字比較,應該設法轉化成“>=”或“<=”形式。

假設浮點變量的名字為x,應當将

if (x == 0.0) // 隐含錯誤的比較

轉化為

if ((x>=-EPSINON) && (x<=EPSINON))

其中EPSINON 是允許的誤差(即精度)。

11:  指針變量與零值比較

應當将指針變量用“==”或“!=”與NULL 比較。

指針變量的零值是“空”(記為NULL)。盡管NULL 的值與0 相同,但是兩者意義不

同。假設指針變量的名字為p,它與零值比較的标準if 語句如下:

if (p == NULL) // p 與NULL 顯式比較,強調p 是指針變量

if (p != NULL)

不要寫成

if (p == 0) // 容易讓人誤解p 是整型變量

if (p != 0)

或者

if (p) // 容易讓人誤解p 是布爾變量

if (!p)

12:  循環語句的效率

在多重循環中,如果有可能,應當将最長的循環放在最内層,最短的循環放在最外層,以減少CPU 跨切循環層的次數

13:  如果函數的傳回值是一個對象,有些場合用“引用傳遞”替換“值傳遞”可以提高效率。而有些場合隻能用“值傳遞”而不能用“引用傳遞”,否則會出錯。例如:

class String

{⋯

// 指派函數

String & operate=(const String &other);

// 相加函數,如果沒有friend 修飾則隻許有一個右側參數

friend String operate+( const String &s1, const String &s2);

private:

char *m_data;

}

String 的指派函數operate = 的實作如下:

String & String::operate=(const String &other)

{

if (this == &other)

return *this;

delete m_data;

m_data = new char[strlen(other.data)+1];

strcpy(m_data, other.data);

return *this; // 傳回的是 *this 的引用,無需拷貝過程

}

對于指派函數,應當用“引用傳遞”的方式傳回String 對象。如果用“值傳遞”的方式,雖然功能仍然正确,但由于return 語句要把 *this 拷貝到儲存傳回值的外部存儲單元之中,增加了不必要的開銷,降低了指派函數的效率。例如:

String a,b,c;

a = b; // 如果用“值傳遞”,将産生一次 *this 拷貝

a = b = c; // 如果用“值傳遞”,将産生兩次 *this 拷貝

String 的相加函數operate + 的實作如下:

String operate+(const String &s1, const String &s2)

{

String temp;

delete temp.data; // temp.data 是僅含‘/0’的字元串

temp.data = new char[strlen(s1.data) + strlen(s2.data) +1];

strcpy(temp.data, s1.data);

strcat(temp.data, s2.data);

return temp;

}

對于相加函數,應當用“值傳遞”的方式傳回String 對象。如果改用“引用傳遞”,那麼函數傳回值是一個指向局部對象temp 的“引用”。由于temp 在函數結束時被自動銷毀,将導緻傳回的“引用”無效。

14:

void *memcpy(void *pvTo, const void *pvFrom, size_t size)

{

assert((pvTo != NULL) && (pvFrom != NULL)); // 使用斷言

byte *pbTo = (byte *) pvTo; // 防止改變pvTo 的位址

byte *pbFrom = (byte *) pvFrom; // 防止改變pvFrom 的位址

while(size -- > 0 )

*pbTo ++ = *pbFrom ++ ;

return pvTo;

}

15: 引用與指針的比較

引用的一些規則如下:

(1)引用被建立的同時必須被初始化(指針則可以在任何時候被初始化)。

(2)不能有NULL 引用,引用必須與合法的存儲單元關聯(指針則可以是NULL)。

(3)一旦引用被初始化,就不能改變引用的關系(指針則可以随時改變所指的對象)

引用的主要功能是傳遞函數的參數和傳回值。C++語言中,函數的參數和傳回值的傳遞方式有三種:值傳遞、

指針傳遞和引用傳遞

16:

記憶體配置設定方式

記憶體配置設定方式有三種:

(1) 從靜态存儲區域配置設定。記憶體在程式編譯的時候就已經配置設定好,這塊記憶體在程式的整個運作期間都存在。例如全局變量,static 變量。

(2) 在棧上建立。在執行函數時,函數内局部變量的存儲單元都可以在棧上建立,函數執行結束時這些存儲單元自動被釋放。棧記憶體配置設定運算内置于處理器的指令集中,效率很高,但是配置設定的記憶體容量有限。

(3) 從堆上配置設定,亦稱動态記憶體配置設定。程式在運作的時候用malloc 或new 申請任意多少的記憶體,程式員自己負責在何時用free 或delete 釋放記憶體。動态記憶體的生存期由我們決定,使用非常靈活,但問題也最多

17: return 語句傳回常量字元串

char *GetString2(void)

{

char *p = "hello world";

return p;

}

void Test5(void)

{

char *str = NULL;

str = GetString2();

cout<< str << endl;

}

函數Test5 運作雖然不會出錯,但是函數GetString2 的設計概念卻是錯誤的。因為GetString2 内的“hello world”是常量字元串,位于靜态存儲區,它在程式生命期内恒定不變。無論什麼時候調用GetString2,它傳回的始終是同一個“隻讀”的記憶體塊。

18: new/delete,  malloc/free

對于非内部資料類型的對象而言,光用maloc/free 無法滿足動态對象的要求。對象

在建立的同時要自動執行構造函數, 對象在消亡之前要自動執行析構函數。由于

malloc/free 是庫函數而不是運算符,不在編譯器控制權限之内,不能夠把執行構造函數

和析構函數的任務強加于malloc/free

19: 記憶體耗盡怎麼辦

如果在申請動态記憶體時找不到足夠大的記憶體塊,malloc 和new 将傳回NULL 指針,宣告記憶體申請失敗。通常有三種方式處理“記憶體耗盡”問題。

(1)判斷指針是否為NULL,如果是則馬上用return 語句終止本函數。例如:

void Func(void)

{

A *a = new A;

if(a == NULL)

{

return;

}

}

(2)判斷指針是否為NULL,如果是則馬上用exit(1)終止整個程式的運作。例如:

void Func(void)

{

A *a = new A;

if(a == NULL)

{

cout << “Memory Exhausted” << endl;

exit(1);

}

}

(3)為new 和malloc 設定異常處理函數。例如Visual C++可以用_set_new_hander 函數為new 設定使用者自己定義的異常處理函數,也可以讓malloc 享用與new 相同的異常處理函數。詳細内容請參考C++使用手冊。

上述(1)(2)方式使用最普遍。如果一個函數内有多處需要申請動态記憶體,那麼方式(1)就顯得力不從心(釋放記憶體很麻煩),應該用方式(2)來處理。很多人不忍心用exit(1),問:“不編寫出錯處理程式,讓作業系統自己解決行不行?”不行。如果發生“記憶體耗盡”這樣的事情,一般說來應用程式已經無藥可救。如果

不用exit(1) 把壞程式殺死,它可能會害死作業系統

20: extern "c"

extern “C”

{

void foo(int x, int y);

⋯ // 其它函數

}

或者寫成

extern “C”

{

#include “myheader.h”

⋯ // 其它C 頭檔案

}

這就告訴C++編譯譯器,函數foo 是個C 連接配接,應該到庫中找名字_foo 而不是找_foo_int_int。C++編譯器開發商已經對C 标準庫的頭檔案作了extern“C”處理,是以我們可以用#include 直接引用這些頭檔案

21: 令人迷惑的隐藏規則

本來僅僅差別重載與覆寫并不算困難,但是C++的隐藏規則使問題複雜性陡然增加。

這裡“隐藏”是指派生類的函數屏蔽了與其同名的基類函數,規則如下:

(1)如果派生類的函數與基類的函數同名,但是參數不同。此時,不論有無virtual

關鍵字,基類的函數将被隐藏(注意别與重載混淆)。

(2)如果派生類的函數與基類的函數同名,并且參數也相同,但是基類函數沒有virtual

關鍵字。此時,基類的函數被隐藏(注意别與覆寫混淆)。

22: 運算符重載

運算符 規則

所有的一進制運算符 建議重載為成員函數

= () [] -> 隻能重載為成員函數

+= -= /= *= &= |= ~= %= >>= <<= 建議重載為成員函數

所有其它運算符 建議重載為全局函數

23: 函數内聯

inline 是一種“用于實作的關鍵字”,而不是一種“用于聲明的關鍵字”。

定義在類聲明之中的成員函數将自動地成為内聯函數,例如

class A

{

public:

void Foo(int x, int y) { ⋯ } // 自動地成為内聯函數

}

将成員函數的定義體放在類聲明之中雖然能帶來書寫上的友善,但不是一種良好的程式設計

風格,上例應該改成:

// 頭檔案

class A

{

public:

void Foo(int x, int y);

}

// 定義檔案

inline void A::Foo(int x, int y)

{

}

24:構造函數的初始化清單

B::B(const A &a)  //初始化表裡調用了類A 的拷貝構造函數,進而将成員對象m_a 初始化。

: m_a(a)

{

}

B::B(const A &a)  //構造函數幹了兩件事:先暗地裡建立m_a對象(調用了A 的無參數構造函數),再調用類A 的指派函數,将參數a 賦給m_a

{

m_a = a;

25: 構造和析構的次序

構造從類層次的最根處開始,在每一層中,首先調用基類的構造函數,然後調用成員對象的構造函數。析構則嚴格按照與構造相反的次序執行,該次序是唯一的,否則編譯器将無法自動執行析構過程。

一個有趣的現象是,成員對象初始化的次序完全不受它們在初始化表中次序的影響,隻由成員對象在類中聲明的次序決定。這是因為類的聲明是唯一的,而類的構造函數可以有多個,是以會有多個不同次序的初始化表。如果成員對象按照初始化表的次序進行構造,這将導緻析構函數無法得到唯一的逆序

26: 拷貝構造函數與指派函數

編譯器将以“位拷貝”的方式自動生成預設的函數。倘若類中含有指針變量,那麼這兩個預設的函數就隐含了錯誤。

拷貝構造函數和指派函數非常容易混淆,常導緻錯寫、錯用。拷貝構造函數是在對象被建立時調用的,而指派函數隻能被已經存在了的對象調用。以下程式中,第三個語句和第四個語句很相似,你分得清楚哪個調用了拷貝構造函數,哪個調用了指派函數嗎?

String a(“hello”);

String b(“world”);

String c = a; // 調用了拷貝構造函數,最好寫成 c(a);

c = b; // 調用了指派函數

本例中第三個語句的風格較差,宜改寫成String c(a) 以差別于第四個語句

27:

// 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(const String &other)

{

// 允許操作other 的私有成員m_data

int length = strlen(other.m_data);

m_data = new char[length+1];

strcpy(m_data, other.m_data);

}

// 指派函數

String & String::operate =(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 的析構函數

String::~String(void)

{

delete [] m_data;

// 由于m_data 是内部資料類型,也可以寫成 delete m_data;

}

28: 在編寫派生類的指派函數時,注意不要忘記對基類的資料成員重新指派

class Base

{

public:

Base & operate =(const Base &other); // 類Base 的指派函數

private:

int m_i, m_j, m_k;

};

class Derived : public Base

{

public:

Derived & operate =(const Derived &other); // 類Derived 的指派函數

private:

int m_x, m_y, m_z;

};

Derived & Derived::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;

}

29;

函數傳回值采用“引用傳遞”的場合并不多,這種方式一般隻出現在類的指派函數中,目的是為了實作鍊式表達。

例如

class A

{⋯

A & operate = (const A &other); // 指派函數

};

A a, b, c; // a, b, c 為A 的對象

a = b = c; // 正常的鍊式指派

(a = b) = c; // 不正常的鍊式指派,但合法

如果将指派函數的傳回值加const 修飾,那麼該傳回值的内容不允許被改動。上例中,語句 a = b = c 仍然正确,但是語句 (a = b) = c 則是非法的。

30: 繼承群組合的規則

(1)若在邏輯上B 是A 的“一種”,并且A 的所有功能和屬性對B 而言都有意義,則允許B 繼承A 的功能和屬性。

(2)若在邏輯上A 是B 的“一部分”(a part of),則不允許B 從A 派生,而是要用A 和其它東西組合出B。

31:  引用本身不占存儲單元,系統也不給引用配置設定存儲單元。故:對引用求位址,就是對目标變量求位址。&ra與&a相等