第2章 變量和基本類型
基本內置類型(Primitive Built-in Types)
算數類型(Arithmetic Types)
算數類型分爲兩類:整型(integral type)、浮點型(floating-point type)。
bool
類型的取值是true
或false
。
一個char
的大小和一個機器字節一樣,確保可以存放機器基本字符集中任意字符對應的數字值。wchar_t
確保可以存放機器最大擴展字符集中的任意一個字符。
在整型類型大小方面,C++規定short
≤ int
≤ long
≤ long long
(long long
是C++11定義的類型)。
浮點型可表示單精度(single-precision)、雙精度(double-precision)和擴展精度(extended-precision)值,分別對應float
、double
和long double
類型。
除去布爾型和擴展字符型,其他整型可以分爲帶符號(signed)和無符號(unsigned)兩種。帶符號類型可以表示正數、負數和0,無符號類型只能表示大於等於0的數值。類型int
、short
、long
和long long
都是帶符號的,在類型名前面添加unsigned
可以得到對應的無符號類型,如unsigned int
。
字符型分爲char
、signed char
和unsigned char
三種,但是表現形式只有帶符號和無符號兩種。類型char
和signed char
並不一樣, char
的具體形式由編譯器(compiler)決定。
如何選擇算數類型:
-
當明確知曉數值不可能爲負時,應該使用無符號類型。
-
使用
int
執行整數運算,如果數值超過了int
的表示範圍,應該使用long long
類型。 -
在算數表達式中不要使用
char
和bool
類型。如果需要使用一個不大的整數,應該明確指定它的類型是signed char
還是unsigned char
。 -
執行浮點數運算時建議使用
double
類型。
類型轉換(Type Conversions)
進行類型轉換時,類型所能表示的值的範圍決定了轉換的過程。
- 把非布爾類型的算術值賦給布爾類型時,初始值爲0則結果爲
false
,否則結果爲true
。 - 把布爾值賦給非布爾類型時,初始值爲
false
則結果爲0,初始值爲true
則結果爲1。 - 把浮點數賦給整數類型時,進行近似處理,結果值僅保留浮點數中的整數部分。
- 把整數值賦給浮點類型時,小數部分記爲0。如果該整數所佔的空間超過了浮點類型的容量,精度可能有損失。
- 賦給無符號類型一個超出它表示範圍的值時,結果是初始值對無符號類型表示數值總數(8比特大小的
unsigned char
能表示的數值總數是256)取模後的餘數。 - 賦給帶符號類型一個超出它表示範圍的值時,結果是未定義的(undefined)。
避免無法預知和依賴於實現環境的行爲。
無符號數不會小於0這一事實關係到循環的寫法。
// WRONG: u can never be less than 0; the condition will always succeed
for (unsigned u = 10; u >= 0; --u)
std::cout << u << std::endl;
當u等於0時,--u的結果將會是4294967295。一種解決辦法是用while
語句來代替for
語句,前者可以在輸出變量前先減去1。
unsigned u = 11; // start the loop one past the first element we want to print
while (u > 0)
{
--u; // decrement first, so that the last iteration will print 0
std::cout << u << std::endl;
}
不要混用帶符號類型和無符號類型。
字面值常量(Literals)
以0
開頭的整數代表八進制(octal)數,以0x
或0X
開頭的整數代表十六進制(hexadecimal)數。在C++14中,0b
或0B
開頭的整數代表二進制(binary)數。
整型字面值具體的數據類型由它的值和符號決定。
C++14新增了單引號'
形式的數字分隔符。數字分隔符不會影響數字的值,但可以通過分隔符將數字分組,使數值讀寫更容易。
// 按照書寫形式,每3位分爲一組
std::cout << 0B1'101; // 輸出"13"
std::cout << 1'100'000; // 輸出"1100000"
浮點型字面值默認是一個double
。
由單引號括起來的一個字符稱爲char
型字面值,雙引號括起來的零個或多個字符稱爲字符串字面值。
字符串字面值的類型是由常量字符構成的數組(array)。編譯器在每個字符串的結尾處添加一個空字符'\0'
,因此字符串字面值的實際長度要比它的內容多一位。
轉義序列:
含義 | 轉義字符 |
---|---|
newline | \n |
horizontal tab | \t |
alert (bell) | \a |
vertical tab | \v |
backspace | \b |
double quote | \" |
backslash | \\ |
question mark | \? |
single quote | \' |
carriage return | \r |
formfeed | \f |
std::cout << '\n'; // prints a newline
std::cout << "\tHi!\n"; // prints a tab followd by "Hi!" and a newline
泛化轉義序列的形式是\x
後緊跟1個或多個十六進制數字,或者\
後緊跟1個、2個或3個八進制數字,其中數字部分表示字符對應的數值。如果\
後面跟着的八進制數字超過3個,則只有前3個數字與\
構成轉義序列。相反,\x
要用到後面跟着的所有數字。
std::cout << "Hi \x4dO\115!\n"; // prints Hi MOM! followed by a newline
std::cout << '\115' << '\n'; // prints M followed by a newline
添加特定的前綴和後綴,可以改變整型、浮點型和字符型字面值的默認類型。
使用一個長整型字面值時,最好使用大寫字母L
進行標記,小寫字母l
和數字1
容易混淆。
變量(Variables)
變量定義(Variable Definitions)
變量定義的基本形式:類型說明符(type specifier)後緊跟由一個或多個變量名組成的列表,其中變量名以逗號分隔,最後以分號結束。定義時可以爲一個或多個變量賦初始值(初始化,initialization)。
初始化不等於賦值(assignment)。初始化的含義是創建變量時賦予其一個初始值,而賦值的含義是把對象的當前值擦除,再用一個新值來替代。
用花括號初始化變量稱爲列表初始化(list initialization)。當用於內置類型的變量時,如果使用了列表初始化並且初始值存在丟失信息的風險,則編譯器會報錯。
long double ld = 3.1415926536;
int a{ld}, b = {ld}; // error: narrowing conversion required
int c(ld), d = ld; // ok: but value will be truncated
如果定義變量時未指定初值,則變量被默認初始化(default initialized)。
對於內置類型,定義於任何函數體之外的變量被初始化爲0,函數體內部的變量將不被初始化(uninitialized)。
定義於函數體內的內置類型對象如果沒有初始化,則其值未定義,使用該類值是一種錯誤的編程行爲且很難調試。類的對象如果沒有顯式初始化,則其值由類確定。
建議初始化每一個內置類型的變量。
變量聲明和定義的關係(Variable Declarations and Definitions)
聲明(declaration)使得名字爲程序所知。一個文件如果想使用其他地方定義的名字,則必須先包含對那個名字的聲明。
定義(definition)負責創建與名字相關聯的實體。
如果想聲明一個變量而不定義它,就在變量名前添加關鍵字extern
,並且不要顯式地初始化變量。
extern int i; // declares but does not define i
int j; // declares and defines j
extern
語句如果包含了初始值就不再是聲明瞭,而變成了定義。
變量能且只能被定義一次,但是可以被聲明多次。
如果要在多個文件中使用同一個變量,就必須將聲明和定義分開。此時變量的定義必須出現且只能出現在一個文件中,其他使用該變量的文件必須對其進行聲明,但絕對不能重複定義。
標識符(Identifiers)
C++的標識符由字母、數字和下劃線組成,其中必須以字母或下劃線開頭。標識符的長度沒有限制,但是對大小寫字母敏感。C++爲標準庫保留了一些名字。用戶自定義的標識符不能連續出現兩個下劃線,也不能以下劃線緊連大寫字母開頭。此外,定義在函數體外的標識符不能以下劃線開頭。
名字的作用域(Scope of a Name)
定義在函數體之外的名字擁有全局作用域(global scope)。聲明之後,該名字在整個程序範圍內都可使用。
最好在第一次使用變量時再去定義它。這樣做更容易找到變量的定義位置,並且也可以賦給它一個比較合理的初始值。
作用域中一旦聲明瞭某個名字,在它所嵌套着的所有作用域中都能訪問該名字。同時,允許在內層作用域中重新定義外層作用域已有的名字,此時內層作用域中新定義的名字將屏蔽外層作用域的名字。
可以用作用域操作符::
來覆蓋默認的作用域規則。因爲全局作用域本身並沒有名字,所以當作用域操作符的左側爲空時,會向全局作用域發出請求獲取作用域操作符右側名字對應的變量。
#include <iostream>
// Program for illustration purposes only: It is bad style for a function
// to use a global variable and also define a local variable with the same name
int reused = 42; // reused has global scope
int main()
{
int unique = 0; // unique has block scope
// output #1: uses global reused; prints 42 0
std::cout << reused << " " << unique << std::endl;
int reused = 0; // new, local object named reused hides global reused
// output #2: uses local reused; prints 0 0
std::cout << reused << " " << unique << std::endl;
// output #3: explicitly requests the global reused; prints 42 0
std::cout << ::reused << " " << unique << std::endl;
return 0;
}
如果函數有可能用到某個全局變量,則不宜再定義一個同名的局部變量。
複合類型(Compound Type)
引用(References)
引用爲對象起了另外一個名字,引用類型引用(refers to)另外一種類型。通過將聲明符寫成&d
的形式來定義引用類型,其中d是變量名稱。
int ival = 1024;
int &refVal = ival; // refVal refers to (is another name for) ival
int &refVal2; // error: a reference must be initialized
定義引用時,程序把引用和它的初始值綁定(bind)在一起,而不是將初始值拷貝給引用。一旦初始化完成,將無法再令引用重新綁定到另一個對象,因此引用必須初始化。
引用不是對象,它只是爲一個已經存在的對象所起的另外一個名字。
聲明語句中引用的類型實際上被用於指定它所綁定的對象類型。大部分情況下,引用的類型要和與之綁定的對象嚴格匹配。
引用只能綁定在對象上,不能與字面值或某個表達式的計算結果綁定在一起。
指針(Pointer)
與引用類似,指針也實現了對其他對象的間接訪問。
- 指針本身就是一個對象,允許對指針賦值和拷貝,而且在生命週期內它可以先後指向不同的對象。
- 指針無須在定義時賦初值。和其他內置類型一樣,在塊作用域內定義的指針如果沒有被初始化,也將擁有一個不確定的值。
通過將聲明符寫成*d
的形式來定義指針類型,其中d是變量名稱。如果在一條語句中定義了多個指針變量,則每個量前都必須有符號*
。
int *ip1, *ip2; // both ip1 and ip2 are pointers to int
double dp, *dp2; // dp2 is a pointer to double; dp is a double
指針存放某個對象的地址,要想獲取對象的地址,需要使用取地址符&
。
int ival = 42;
int *p = &ival; // p holds the address of ival; p is a pointer to ival
因爲引用不是對象,沒有實際地址,所以不能定義指向引用的指針。
聲明語句中指針的類型實際上被用於指定它所指向的對象類型。大部分情況下,指針的類型要和它指向的對象嚴格匹配。
指針的值(即地址)應屬於下列狀態之一:
- 指向一個對象。
- 指向緊鄰對象所佔空間的下一個位置。
- 空指針,即指針沒有指向任何對象。
- 無效指針,即上述情況之外的其他值。
試圖拷貝或以其他方式訪問無效指針的值都會引發錯誤。
如果指針指向一個對象,可以使用解引用(dereference)符*
來訪問該對象。
int ival = 42;
int *p = &ival; // p holds the address of ival; p is a pointer to ival
cout << *p; // * yields the object to which p points; prints 42
給解引用的結果賦值就是給指針所指向的對象賦值。
解引用操作僅適用於那些確實指向了某個對象的有效指針。
空指針(null pointer)不指向任何對象,在試圖使用一個指針前代碼可以先檢查它是否爲空。得到空指針最直接的辦法是用字面值nullptr
來初始化指針。
舊版本程序通常使用NULL
(預處理變量,定義於頭文件cstdlib中,值爲0)給指針賦值,但在C++11中,最好使用nullptr
初始化空指針。
int *p1 = nullptr; // equivalent to int *p1 = 0;
int *p2 = 0; // directly initializes p2 from the literal constant 0
// must #include cstdlib
int *p3 = NULL; // equivalent to int *p3 = 0;
建議初始化所有指針。
void*
是一種特殊的指針類型,可以存放任意對象的地址,但不能直接操作void*
指針所指的對象。
理解複合類型的聲明(Understanding Compound Type Declarations)
指向指針的指針(Pointers to Pointers):
int ival = 1024;
int *pi = &ival; // pi points to an int
int **ppi = π // ppi points to a pointer to an int
指向指針的引用(References to Pointers):
int i = 42;
int *p; // p is a pointer to int
int *&r = p; // r is a reference to the pointer p
r = &i; // r refers to a pointer; assigning &i to r makes p point to i
*r = 0; // dereferencing r yields i, the object to which p points; changes i to 0
面對一條比較複雜的指針或引用的聲明語句時,從右向左閱讀有助於弄清它的真實含義。
const限定符(Const Qualifier)
在變量類型前添加關鍵字const
可以創建值不能被改變的對象。const
變量必須被初始化。
const int bufSize = 512; // input buffer size
bufSize = 512; // error: attempt to write to const object
默認情況下,const
對象被設定成僅在文件內有效。當多個文件中出現了同名的const
變量時,其實等同於在不同文件中分別定義了獨立的變量。
如果想在多個文件間共享const
對象:
-
若
const
對象的值在編譯時已經確定,則應該定義在頭文件中。其他源文件包含該頭文件時,不會產生重複定義錯誤。 -
若
const
對象的值直到運行時才能確定,則應該在頭文件中聲明,在源文件中定義。此時const
變量的聲明和定義前都應該添加extern
關鍵字。// file_1.cc defines and initializes a const that is accessible to other files extern const int bufSize = fcn(); // file_1.h extern const int bufSize; // same bufSize as defined in file_1.cc
const的引用(References to const)
把引用綁定在const
對象上即爲對常量的引用(reference to const)。對常量的引用不能被用作修改它所綁定的對象。
const int ci = 1024;
const int &r1 = ci; // ok: both reference and underlying object are const
r1 = 42; // error: r1 is a reference to const
int &r2 = ci; // error: non const reference to a const object
大部分情況下,引用的類型要和與之綁定的對象嚴格匹配。但是有兩個例外:
-
初始化常量引用時允許用任意表達式作爲初始值,只要該表達式的結果能轉換成引用的類型即可。
int i = 42; const int &r1 = i; // we can bind a const int& to a plain int object const int &r2 = 42; // ok: r1 is a reference to const const int &r3 = r1 * 2; // ok: r3 is a reference to const int &r4 = r1 * 2; // error: r4 is a plain, non const reference
-
允許爲一個常量引用綁定非常量的對象、字面值或者一般表達式。
double dval = 3.14; const int &ri = dval;
編譯器把上述代碼變成了如下形式:
const int temp = dval; //由雙精度浮點數生成一個臨時的整形常量
const int &ri = temp; //讓ri綁定這個臨時量
指針和const(Pointers and const)
指向常量的指針(pointer to const)不能用於修改其所指向的對象。常量對象的地址只能使用指向常量的指針來存放,但是指向常量的指針可以指向一個非常量對象。
const double pi = 3.14; // pi is const; its value may not be changed
double *ptr = π // error: ptr is a plain pointer
const double *cptr = π // ok: cptr may point to a double that is const
*cptr = 42; // error: cannot assign to *cptr
double dval = 3.14; // dval is a double; its value can be changed
cptr = &dval; // ok: but can't change dval through cptr
定義語句中把*
放在const
之前用來說明指針本身是一個常量,常量指針(const pointer)必須初始化。
int errNumb = 0;
int *const curErr = &errNumb; // curErr will always point to errNumb
const double pi = 3.14159;
const double *const pip = π // pip is a const pointer to a const object
指針本身是常量並不代表不能通過指針修改其所指向的對象的值,能否這樣做完全依賴於其指向對象的類型。
頂層const(Top-Level const)
頂層const
表示指針本身是個常量,底層const
(low-level const)表示指針所指的對象是一個常量。指針類型既可以是頂層const
也可以是底層const
。
int i = 0;
int *const p1 = &i; // we can't change the value of p1; const is top-level
const int ci = 42; // we cannot change ci; const is top-level
const int *p2 = &ci; // we can change p2; const is low-level
const int *const p3 = p2; // right-most const is top-level, left-most is not
const int &r = ci; // const in reference types is always low-level
當執行拷貝操作時,常量是頂層const
還是底層const
區別明顯:
-
頂層
const
沒有影響。拷貝操作不會改變被拷貝對象的值,因此拷入和拷出的對象是否是常量無關緊要。i = ci; // ok: copying the value of ci; top-level const in ci is ignored p2 = p3; // ok: pointed-to type matches; top-level const in p3 is ignored
-
拷入和拷出的對象必須具有相同的底層
const
資格。或者兩個對象的數據類型可以相互轉換。一般來說,非常量可以轉換成常量,反之則不行。int *p = p3; // error: p3 has a low-level const but p doesn't p2 = p3; // ok: p2 has the same low-level const qualification as p3 p2 = &i; // ok: we can convert int* to const int* int &r = ci; // error: can't bind an ordinary int& to a const int object const int &r2 = i; // ok: can bind const int& to plain int
constexpr和常量表達式(constexpr and Constant Expressions)
常量表達式(constant expressions)指值不會改變並且在編譯過程就能得到計算結果的表達式。
一個對象是否爲常量表達式由它的數據類型和初始值共同決定。
const int max_files = 20; // max_files is a constant expression
const int limit = max_files + 1; // limit is a constant expression
int staff_size = 27; // staff_size is not a constant expression
const int sz = get_size(); // sz is not a constant expression
C++11允許將變量聲明爲constexpr
類型以便由編譯器來驗證變量的值是否是一個常量表達式。
constexpr int mf = 20; // 20 is a constant expression
constexpr int limit = mf + 1; // mf + 1 is a constant expression
constexpr int sz = size(); // ok only if size is a constexpr function
指針和引用都能定義成constexpr
,但是初始值受到嚴格限制。constexpr
指針的初始值必須是0、nullptr
或者是存儲在某個固定地址中的對象。
函數體內定義的普通變量一般並非存放在固定地址中,因此constexpr
指針不能指向這樣的變量。相反,函數體外定義的變量地址固定不變,可以用來初始化constexpr
指針。
在constexpr
聲明中如果定義了一個指針,限定符constexpr
僅對指針本身有效,與指針所指的對象無關。constexpr
把它所定義的對象置爲了頂層const
。
constexpr int *p = nullptr; // p是指向int的const指針
constexpr int i = 0;
constexpr const int *cp = &i; // cp是指向const int的const指針
const
和constexpr
限定的值都是常量。但constexpr
對象的值必須在編譯期間確定,而const
對象的值可以延遲到運行期間確定。
建議使用constexpr
修飾表示數組大小的對象,因爲數組的大小必須在編譯期間確定且不能改變。
處理類型(Dealing with Types)
類型別名(Type Aliases)
類型別名是某種類型的同義詞,傳統方法是使用關鍵字typedef
定義類型別名。
typedef double wages; // wages is a synonym for double
typedef wages base, *p; // base is a synonym for double, p for double*
C++11使用關鍵字using
進行別名聲明(alias declaration),作用是把等號左側的名字規定成等號右側類型的別名。
using SI = Sales_item; // SI is a synonym for Sales_item
auto類型說明符(The auto Type Specifier)
C++11新增auto
類型說明符,能讓編譯器自動分析表達式所屬的類型。auto
定義的變量必須有初始值。
// the type of item is deduced from the type of the result of adding val1 and val2
auto item = val1 + val2; // item initialized to the result of val1 + val2
編譯器推斷出來的auto
類型有時和初始值的類型並不完全一樣。
-
當引用被用作初始值時,編譯器以引用對象的類型作爲
auto
的類型。int i = 0, &r = i; auto a = r; // a is an int (r is an alias for i, which has type int)
-
auto
一般會忽略頂層const
。const int ci = i, &cr = ci; auto b = ci; // b is an int (top-level const in ci is dropped) auto c = cr; // c is an int (cr is an alias for ci whose const is top-level) auto d = &i; // d is an int*(& of an int object is int*) auto e = &ci; // e is const int*(& of a const object is low-level const)
如果希望推斷出的
auto
類型是一個頂層const
,需要顯式指定const auto
。const auto f = ci; // deduced type of ci is int; f has type const int
設置類型爲auto
的引用時,原來的初始化規則仍然適用,初始值中的頂層常量屬性仍然保留。
auto &g = ci; // g is a const int& that is bound to ci
auto &h = 42; // error: we can't bind a plain reference to a literal
const auto &j = 42; // ok: we can bind a const reference to a literal
decltype類型指示符(The decltype Type Specifier)
C++11新增decltype
類型指示符,作用是選擇並返回操作數的數據類型,此過程中編譯器不實際計算表達式的值。
decltype(f()) sum = x; // sum has whatever type f returns
decltype
處理頂層const
和引用的方式與auto
有些不同,如果decltype
使用的表達式是一個變量,則decltype
返回該變量的類型(包括頂層const
和引用)。
const int ci = 0, &cj = ci;
decltype(ci) x = 0; // x has type const int
decltype(cj) y = x; // y has type const int& and is bound to x
decltype(cj) z; // error: z is a reference and must be initialized
如果decltype
使用的表達式不是一個變量,則decltype
返回表達式結果對應的類型。如果表達式的內容是解引用操作,則decltype
將得到引用類型。如果decltype
使用的是一個不加括號的變量,則得到的結果就是該變量的類型;如果給變量加上了一層或多層括號,則decltype
會得到引用類型,因爲變量是一種可以作爲賦值語句左值的特殊表達式。
decltype((var))
的結果永遠是引用,而decltype(var)
的結果只有當var本身是一個引用時纔會是引用。
自定義數據結構(Defining Our Own Data Structures)
C++11規定可以爲類的數據成員(data member)提供一個類內初始值(in-class initializer)。創建對象時,類內初始值將用於初始化數據成員,沒有初始值的成員將被默認初始化。
類內初始值不能使用圓括號。
類定義的最後應該加上分號。
頭文件(header file)通常包含那些只能被定義一次的實體,如類、const
和constexpr
變量。
頭文件一旦改變,相關的源文件必須重新編譯以獲取更新之後的聲明。
預處理器概述
確保頭文件多次包含仍能安全工作的常用技術是預處理器(preprocessor),它由C++語言從C語言繼承而來。預處理器是在編譯之前執行的一段程序,可以部分地改變我們所寫的程序。之前已經用到了一項預處理功能#include,當處理器看到#include標記時就會用指定的頭文件的內容代替#include。
C++程序還會用到的一項預處理功能是頭文件保護符(header guard),頭文件保護符(header guard)依賴於預處理變量(preprocessor variable)。預處理變量有兩種狀態:已定義和未定義。#define
指令把一個名字設定爲預處理變量。#ifdef
指令當且僅當變量已定義時爲真,#ifndef
指令當且僅當變量未定義時爲真。一旦檢查結果爲真,則執行後續操作直至遇到#endif
指令爲止。
使用這些功能就能有效防止重複包含的發生:
#ifndef SALES_DATA_H
#define SALES_DATA_H
#include <string>
struct Sales_data
{
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
#endif
在高級版本的IDE環境中,可以直接使用#pragma once
命令來防止頭文件的重複包含。
預處理變量無視C++語言中關於作用域的規則。
整個程序中的預處理變量,包括頭文件保護符必須唯一。預處理變量的名字一般均爲大寫。
頭文件即使目前還沒有被包含在任何其他頭文件中,也應該設置保護符。