第3部分
C++世界眾生相
在聽過了HelloWorld.exe的自我介紹,完成了與C++世界的第一次親密接觸後,大家是不是都急不可待地想要一試身手,開始編寫C++程序了呢?程序的兩大任務是描述數據和處理數據。那麼,接下來我們將面臨的第一個問題就是:如何在C++中描述數據?
編程就是使用程序設計語言來描述和表達現實世界。現實世界中有很多客觀存在的事物,例如,電腦、人、汽車等。我們總是用各種數據來描述這些事物的不同屬性,比如,我們用一個字符串“ChenLiangqiao”來描述某個人的名字;用一個數字“175”來描述他的身高。而其中的某些數據往往可以歸結為同一類型,比如,描述人的身高和電腦屏幕尺寸的數據都是數值數據,而描述人的名字和汽車牌照的數據都是字符串數據。對應的,在C++中,為了描述這些數據,我們將相同類型的數據抽象成某一數據類型,然後使用這一數據類型定義的變量來表達這類數據。例如,我們將現實世界中的各種整數(表示身高的175,表示屏幕尺寸的21)抽象成C++中的int這種數據類型,然後用int定義的變量來描述某個具體的整數數據。比如,我們可以定義一個int類型的變量nHeight來表示某個人的身高;定義另外一個int類型的變量nSize來表示某台電視機的尺寸。
這裡我們也可以看到,相同的數據類型(int)可以定義多個不同的變量(nHeight、nSize),分別用於表示多個不同的具體事物(身高、尺寸)。反過來,表示不同事物的多個變量(nHeight、nSize),也可以是同一數據類型(int)。這就像現實世界中的百家姓一樣,姓陳(int)的可以有好多人(nHeight、nSize),而好多人(nHeight、nSize)也都可以姓陳(int)。一個數據的數據類型,決定了這個數據是哪一家的人,而既然是同一家的人,那麼這一家人都有著某些相同的特征。比如,它們都占用相同的內存字節數,所能夠表示的數據范圍都相同等等。如圖3-1所示。
在C++中,按照所能夠表達數據的復雜程度,數據類型可分為基本數據類型和構造數據類型。
在現實世界中有很多簡單的數據,比如某個數字、某個字符等。為了表達這些簡單數據,C++將這些數據分門別類地抽象成了多種基本數據類型。比如,表示人身高的175、表示電視機尺寸的21都是整數,C++將這些整數抽象成int數據類型;表示選擇題選項的A、B、C、D都是字符,C++將字符抽象成char數據類型。基本數據類型是C++中最基礎的數據類型,是C++世界最底層的民眾,都具有自我說明的特點,不再具有可分性。
現實世界是復雜的,只使用C++所提供的基本數據類型還不能夠完全描述復雜的現實世界。比如,我們無法用一個基本數據類型的數據來描述一個矩形,因為矩形有長和寬兩個屬性需要描述。但是我們發現,復雜事物都是由簡單事物組成的,一個復雜的事物可以分解成多個簡單事物,而將多個簡單事物進行組合,也就構成了一個復雜事物。與現實世界相對應,C++中也提供了結構體和類等組合機制,可以將多個基本數據類型組合起來,構成一個比較復雜的構造數據類型以描述更加復雜的事物。例如,可以將兩個簡單的基本數據類型int組合起來形成一個新的數據類型Rect,用來描述更復雜的矩形。在C++中,可以使用struct關鍵字來創建一個新的構造數據類型:
// 創建描述矩形的數據結構Rect struct Rect { int m_nLength; // 表示矩形的長 int m_nWidth; // 表示矩形的寬 }; // 使用Rect構造數據類型定義一個表示矩形的變量r Rect r; r.nLength = 4; // 讓矩形的長為4 r.nWidth = 3; // 讓矩形的寬為3
一個構造數據類型可以分解成若干個“成員”或“元素”。每個“成員”都是一個基本數據類型或另一個構造數據類型。如果我們將基本數據類型看成是化學中的原子的話,那麼,構造數據類型就可以看成是由原子(int)組合而成的分子(Rect)。按照組合的形式不同,C++中的構造數據類型可以分為4種:①數組類型;②結構類型;③聯合類型;④枚舉類型。這些構造數據類型我們將在稍後的章節中詳細介紹。
C++世界住滿了各種數據量。從本質上講,它們都是保存在某個內存位置上的數據。其中的某些數據需要在程序的運行過程中發生變化。比如,表示一個人身高的數據175,有可能在程序運行過程中變為180。同時,程序中還有另外一類數據量,它們在整個程序的運行過程中始終保持不變。比如表示圓周率的3.14159,在程序運行的任何時刻都不會變化。我們將那些在程序運行過程中可能會發生變化的數據量稱為變量,而將那些始終保持不變的數據量稱為常量。
為了保存數據,我們首先需要為它開辟合適的內存空間。同時,對於程序中的變量而言,我們往往需要對其進行多次讀寫訪問。為了便於訪問變量,我們往往需要給變量一個名字,然後通過變量名訪問它所代表的數據。如果我們想要表達現實世界中的某個具體的可變化的數據,就可以使用這個數據所對應的數據類型,按照如下的語法格式來定義一個變量:
數據類型說明符 變量名; // 同時定義相同類型的多個變量 // 不推薦的形式 ,多個變量容易讓人混淆,代碼缺乏可讀性 數據類型說明符 變量名1,變量名2,變量名n;
變量定義由數據類型說明符和變量名兩部分構成。數據類型是對變量類型的說明,用於指定這個變量是整型、浮點型還是自定義數據類型等,因而它也決定了這個變量的一些基本特征,比如占用的內存字節數、取值范圍等。變量名是用來標記變量的符號,就相當於變量的名字一樣,我們可以通過變量名對變量所表示的數據進行讀寫訪問。例如,我們想要在程序中表示一個人的可變化的身高數據175,而175這個數據的類型是整數,與之相對應的C++數據類型是int,所以我們選擇int作為變量的數據類型。又因為這個變量所表示的是人的身高,所以我們選擇nHeight(n表示這是一個整數,Height表示這是身高數據)作為變量名:
// 定義一個int類型的變量nHeight,用來表示身高 int nHeight;
完成這樣的變量定義後,就相當於為即將保存的175數據開辟了4個字節的內存空間(int類型的變量在內存中占有4個字節的空間),同時指定了這個變量的名字是nHeight,進而可以通過nHeight這個變量名將175身高數據保存到內存或者是對其進行讀寫訪問:
// 通過nHeight寫入數據 // 將175這個身高數據保存到內存 nHeight = 175; // 通過nHeight讀取數據 // 將nHeight變量代表的身高數據175顯示到屏幕 cout<<"身高:"<<nHeight<<endl; // 通過變量名將身高數據修改為180 nHeight = 180; // 輸出修改後的身高數據 cout<<"修改後的身高:"<<nHeight<<endl;
定義變量時,應注意以下幾點。
l 不能用C++關鍵字作為變量名。比如常見的bool、for、do、case等關鍵字(在IDE中顯示為藍色等特殊顏色)都不能作為變量名;變量名不能以數字開始。例如,以下變量定義都是錯誤的:
int case; // 錯誤:case是關鍵字 int 2member; // 錯誤:變量名以數字開始
知道更多:C++中到底有多少關鍵字?
關鍵字(keyword)又稱保留字,是C++在整個語言范圍內預先保留的標識符。每個C++關鍵字都有特殊的含義,用以完成某項特定的功能。比如,int表示整數數據類型;for表示定義一個循環語句;class表示定義一個類等等。因為關鍵字已經擁有預先定義的含義,所以無法用做標識符(變量名、函數名或類名等)。C++的關鍵字如下表所示。
表3-1 C++中的關鍵字
alignas (C++11 啟用)
alignof (C++11 啟用)
and
and_eq
asm
auto
bitand
bitor
bool
break
case
catch
char
char16_t(C++11 啟用)
char32_t(C++11 啟用)
class
compl
const
constexpr(C++11 啟用)
const_cast
continue
decltype(C++11 啟用)
default
delete
do
double
dynamic_cast
else
enum
explicit
export
extern
false
float
for
friend
goto
if
inline
int
long
mutable
namespace
new
noexcept(C++11 啟用)
not
not_eq
nullptr (C++11 啟用)
operator
or
or_eq
private
protected
public
register
reinterpret_cast
return
short
signed
sizeof
static
static_assert(C++11 啟用)
static_cast
struct
switch
template
this
thread_local(C++11 啟用)
throw
true
try
typedef
typeid
typename
union
unsigned
using
virtual
void
volatile
wchar_t
while
xor
xor_eq
這裡列出了C++中所有的84個關鍵字,但其中的大部分關鍵字我們很少用到。對於這些關鍵字,我們沒有必要全部掌握。我們只需要掌握其中最常用的一、二十個,至於其他的關鍵字,需要用到的時候再去查資料就可以了。
l 允許在一個數據類型說明符後同時定義多個相同類型的變量。各變量名之間用逗號(這裡的逗號必須是英文逗號)間隔。例如:
// 同時定義三個int類型的變量, // 分別表示學生的ID(nStuID),年齡(nAge)和身高(nHeight) int nStuID,nAge,nHeight;
l 數據類型說明符與變量名之間至少要有一個空格間隔。
l 最後一個變量名之後必須以“;”結尾,表示語句的結束。
l 變量定義必須放在變量使用之前。換句話說,也就是變量必須先定義後使用。
最佳實踐:變量定義應盡可能地靠近變量使用的位置
我們知道,變量在定義之後才可使用,也就是變量在使用之前必須先被定義。那麼,這個“使用之前”到底“前”到什麼程度合適呢?是“使用之前”的1行代碼恰當還是100行代碼合適?面對這個問題,我們並沒有一個固定的標准答案,但是我們有一個應當遵循的原則:變量定義應盡可能地靠近變量使用的位置。
如果變量定義的位置和變量實際使用的位置相距太遠,則這中間可能會發生很多事情。比如,程序可能中途退出,定義的變量並沒有得到使用而白白浪費;也可能在中間被錯誤地使用而給程序帶來難以發現的問題。另外一方面,如果兩者相距太遠,我們在使用一個變量的時候,卻難以找到它定義的位置,從而無法輕易地得知這個變量的數據類型等基本信息,影響我們對變量的使用。所以,為了避免這些可能存在的麻煩,一個簡單而有效的方法是,盡可能地推遲變量定義的時機,盡可能地靠近其實際使用的位置。
在定義變量的時候,除了確定變量的數據類型之外,另外一個重要的工作就是給變量取一個好名字。一個人如果有個好名字,就很容易給人留下良好而深刻的印象,而變量的名字也一樣。合適的變量名包含跟變量相關的信息,可以自我解釋,讓人更容易理解和使用,從而提高代碼的可讀性。那麼如何給變量取一個合適的名字呢?比較下面這四個變量名:
// 記錄學生數量的變量 int nStuNum; int N; int theNumberofStudent; int xssl;
這四個變量都是用來表示學生數量的。如果要問這四個變量名哪個最好,大家肯定會說第一個變量最好,因為第一個變量名一看就知道是用來表示學生數量的。而其他幾個,都有各自的缺點:第二個太短,不知道這個變量的具體含義;第三個太長,書寫繁瑣;第四個使用漢語拼音的首字母縮寫,更是讓人一頭霧水。
好的變量名可以恰當地解釋變量所表示的意義,無需過多的注釋或文檔,整個代碼就清晰可讀,可以做到“望文生義”。要為變量取一個好名字,通常要遵循某種命名規則。目前業界比較流行的命名規則當屬微軟公司提倡的“匈牙利命名法”。在匈牙利命名法中,一個變量名主要由三部分構成:
變量名 = 屬性 + 類型 + 對象描述
其中,屬性通常用來對這個變量的一些附加信息進行說明。例如,我們通常用“m_”前綴表示這個變量是某個類的成員(member)變量,而使用“g_”前綴表示這是一個全局(global)變量;類型表示這個變量的數據類型,通常用各種數據類型的縮寫表示,例如我們通常用n表示int類型,用f表示float類型等;而對象描述就是對這個變量含義的說明了,它通常是一個名詞。將這三個部分組合起來,就構成了一個完整的變量名,可以表達豐富的關於這個變量的信息。例如,某個變量的名字是“m_unAge”,一看變量名就知道這個變量表達的意義就是:這是某個類的成員變量(m_),它的數據類型是unsigned int(un),而它是用於描述這個類的年齡(Age)屬性的。
知道更多:匈牙利命名法中的“匈牙利”是怎麼來的?
匈牙利命名法是由一位叫 Charles Simonyi 的匈牙利程序員首先提出的,後來他在微軟公司工作了數年,因為這種命名法用很少的文字很好地概括了變量的最重要信息,因而受到微軟公司的認同並逐漸在微軟內部流行起來。又因為微軟在業界的強大影響力,匈牙利命名法也開始通過微軟的各種產品和文檔資料向全世界傳播開,逐漸成為業界最為流行的變量命名方法。對於大部分程序員而言,無論自己使用的是何種開發語言,或多或少都會使用這種變量命名法。
這種命名法之所以被稱為“匈牙利命名法”,就是為了紀念這位發明者所來自的國家。
使用匈牙利命名法,可以在一個變量名內表達豐富的信息,在一定程度上提高了代碼的可讀性。但是它也有一個最大的缺點——繁瑣。有時候,一些過長而又意義不大的前綴為變量名增加了額外的負擔。而這也是它並沒有受到全世界所有程序員全都使用的原因。世界上並無所謂最好的命名規則。在實踐中,可以根據業界流行的一些共性規則,再結合項目的實際情況來制定一種令大多數項目成員都滿意的命名規則,並在項目中貫徹實施。“最合適”的規則就是“最好”的規則了。經過實踐的檢驗,業界流行的一些共性命名規則主要有以下幾點。
變量名應當直觀,方便拼讀,可望文而生義。變量名最好采用英文單詞或組合,便於記憶和閱讀;切忌使用漢語拼音來命名,因為這樣的變量名,只有你一個人能看懂;程序中的英文單詞不宜太過復雜,用詞應當盡量做到地道、准確。例如,把表示“當前數值”的變量命名為“fNowVal”,雖然能夠表達變量的含義,但遠沒有“fCurVal”來得地道。
通常,編譯器對變量名的長度沒有限制。一般來說,長名字能更好地表達變量的含義,所以C++中的變量名長達十幾個字符也不足為奇。既然沒有限制,那麼變量的名字是不是越長越好呢?不見得。只要能夠完整地表達變量的含義,變量名應該越簡單越好。例如,同樣是表示最大值,變量名“max”就要比“maxValueUntilOverflow”好用,因為它用最短的長度表達了最大的信息量。
關於變量名的長度,我們還可以記住這樣一條簡單的規則:變量名(或者後文將介紹的函數名等)的長度與它的作用域的大小成正相關。所謂的作用域,也就是某個標識符(變量名或者函數名)發生作用的代碼范圍,具體介紹可以參考後繼的7.3.3小節。換句話說,也就是如果一個變量的作用域比較大,那麼在這個作用域內的變量就會比較多,為了避免沖突便於區分,變量名就應該比較長。反之亦然。例如,在一個函數內部,我們可以用一個簡單的i來給一個局部變量命名,而在全局范圍內,再使用i來給一個全局變量命名就不太合適了。
變量表示的是現實世界中的一個具體事物,其本質是一個數據實體,所以變量名的核心應該是一個名詞,因而它應當使用單獨的一個“名詞”或者“形容詞+名詞”的組合形式。例如:
float fWeight; // 名詞,表示某個人的體重 float fLastWeight; // 形容詞 + 名詞,表示上一次的體重 float fCurWeight; // 形容詞 + 名詞,表示當前體重
4. 不要使用數字編號
盡量避免變量名中出現數字編號,如“Value1”、“Value2”等,除非邏輯上的確需要編號。
常量是某一類特殊的變量,它的特殊性就在於它不可修改。這種特殊性體現在命名上,就是我們通常用大寫字母表示變量名,如果常量名中有多個單詞,則用下劃線加以分割。例如:
const float PI = 3.14159; // 用const關鍵字定義一個float類型的常量PI const int MAX_LEN = 1024; // 用下劃線分割常量名
6. 使用約定俗成的前綴
約定俗成的一些變量名前綴,可以很好地解釋變量的某些屬性,讓變量的含義一目了然。例如:變量加前綴s_,表示靜態(static)變量;變量加前綴g_,表示全局(global)變量;變量加前綴m_,表示成員(member)變量。
當完成變量的定義後,系統會為這個變量分配內存空間,進而我們可以通過變量名對這塊內存進行讀寫訪問,將數據保存到內存或者是從內存讀取數據。但是,在真正使用變量之前,我們往往還需要對其進行合理的初始化。這是因為變量定義後,如果不進行初始化,則系統會給定一個不確定的隨機值作為其初始值。而根據編譯器和目標平台的不同,這個隨機值則可能有所不同。這樣就可能導致同一程序在不同平台上行為的不一致,帶來移植問題。同時,如果不小心使用了這個隨機值進行操作,則可能導致程序運行結果出錯,甚至程序崩潰,那就是一場災難了。而變量初始化會給變量一個合理的初始值,可以很好地避免上面的這些問題。所以,在學習C++的一開始,就應該養成“在定義變量的同時進行初始化”的好習慣。
那麼,我們該如何進行變量的初始化呢?
第一種方式,可以在定義變量的同時,使用“=”賦值符將合適的初始值賦值給這個變量。例如:
// 定義一個int類型的變量nHeight,並利用“=”將其值初始化為175 int nHeight = 175;
第二種方式,就是在定義變量時在變量名之後用“()”給出初始值,系統會用這個初始值完成變量的創建,從而完成初始化工作。例如:
// 通過“()”將其值初始化為175 int nHeight(175);
除了以上兩種方式之外,在 C++11標准中,我們還可以利用一對大括號“{}”表示的初始化列表(initializer list)在定義變量時完成變量的初始化工作。例如:
//通過初始化列表將其值初始化為175 int nHeight{175};
最佳實踐:為什麼要使用初始化列表?
到這裡,大家很自然地會提出這樣一個問題:C++中已經有“=”和“()”可以完成變量的初始化了,為什麼還要使用初始化列表來進行變量的初始化?
初始化列表是C++11標准新引入的一個特性,除了統一變量初始化的形式之外,它還帶來另外一個好處:它可以預防變量初始化時的數據類型截斷,防止數據精度丟失。所謂的數據類型截斷,簡而言之,就是在使用某種精度較高的數據類型(例如,double)的數據對另一種精度較低的數據類型(例如,int)的變量進行賦值時,C++會進行隱式的類型截斷以滿足類型轉換的需要。例如:
int x = 7.3; // 一個double類型的數據7.3被截斷成int類型的數據7
在編譯上面的代碼時,雖然在這個過程中丟失了0.3這個數據,但編譯器不會給出任何錯誤或者警告信息。但是,在C++11中,如果使用初始化列表“{}”來進行初始化,編譯器則會對這種數據類型截斷發出警告,提示用戶數據精度的丟失。例如:
// 警告:用double類型的數據初始化int類型的變量會產生類型截斷,丟失數據精度 int x1 = {7.3}; // 正確:雖然7是一個int類型的數據,但是可以使用char類型精確地表達, // 因而不會導致數據類型截斷而丟失精度錯誤 char x2{7};
在C++中,如果一個初始值可以被精確地表達為目標類型,那麼就不存在數據類型截斷。但請注意,double類型至int類型的轉換通常都會被認為是數據類型截斷,會產生精度的丟失,即使是從7.0轉換至7。初始化列表對於類型轉換的處理增強了C++靜態類型系統的安全性。傳統的依賴於編程人員的初始化類型安全檢查,在 C++11中,通過初始化列表由編譯器實施,這樣會減輕編程人員的負擔,也更加安全。所以,如果可以,應該盡可能地使用初始化列表來完成變量的初始化。
這裡需要特別指出的是,我們不能使用初始化列表對auto 類型(一種特殊的數據類型,它相當於一種數據類型的占位符。用它作為數據類型定義變量時,它並不具體地指定變量的數據類型,變量的真實數據類型將在編譯時根據其初始值自動推斷而得。在稍後的3.5.2小節中,我們將詳細加以介紹)的變量進行初始化。如果那樣的話,這個變量的類型會被編譯器檢測為初始化列表類型,而不是初始化列表中真正的初始值的類型。
知道更多:用戶自定義數據標識(User-defined literals)創建特殊數據
在表示數據的時候,C++提供了許多內建的數據類型的數據標識供我們使用,這樣我們在表達數據的時候,加上相應的數據標識,將使得我們要表達的數據更加准確而直觀。例如:
1.2 // 默認double雙精度浮點型 1.2F // F指定float單精度浮點型 19821003ULL // ULL表示unsigned long long 64位無符號長整型
除了這些內建的數據標識之外,C++11還通過在變量後面加上一個用戶自定義的後綴來標定所需的數據類型以支持“用戶自定義數據標識”,例如:
// 定義表示英寸的數據標識inch constexpr double operator"" _inch(const long double in) { return in*0.0254; // 將英寸單位換算成米單位 }
在這裡,我們實際上定義了一個操作符(必須以“_”開始),它以操作符之前的數據為參數,參數類型就是相應的數據取值范圍最大的類型。例如,表示整型數的是unsigned long long、表示浮點數的是long double。當然,也可以將整個數據作為一個字符串,以const char*的參數形式傳入。傳入的參數在經過一定的處理後,返回的就是這個數據加上數據標識後表示的真實數據。這樣,我們就可以直接在程序中使用“_inch”這個用戶自定義的數據標識,直接定義以英寸為單位的數據。例如:
// 定義一台電視機的尺寸為54英寸, // 然後換算成以米為單位的數據賦值給變量tvsize double tvsize = 54.0_inch; cout<<"54 inch = "<<tvsize<<" m"<<endl;
在上面這段代碼中,我們使用“_inch”數據標識直接定義了電視機的尺寸。當編譯器在編譯這段代碼時,首先會分析為變量賦值數據的後綴,然後將後綴之前的數據作為參數,調用定義數據標識的函數,並將函數的返回值作為這個數據的真實數值。用戶自定義數據標識機制只是簡簡單單的允許用戶制定一個新的後綴,並決定如何對它之前的數據進行處理。在其中,我們可以進行單位的換算,例如將英寸換算成米,也可以將其構造成新的數據類型,例如將一個二進制數構造成十進制數等等。
因為我們在定義數據標識的時候使用了constexpr關鍵字,constexpr關鍵字的作用是實現在編譯時期進行預處理計算,所以在編譯的時候,編譯器會直接將這個以英寸為單位的數據換算成以米為單位的數據,並賦值給相應的變量。
通過這樣的方式,我們可以直接在代碼中表示各種不同類型的數據,例如英寸長度、角度、二進制數等等,使得我們在使用不同類型的數據的時候將更加直觀,也更加方便,更加人性化。
是一樣的。。都是置0操作,建議用前者。。這樣更直觀和高效率
初始化時,會為變量開辟存儲空間,然後把字節變成0000 0000(就相當於復位)。
存在這種可能,該地址以前已使用過,但系統並沒有回收,擦除。就存在髒數據。