《C++ Primer》閱讀筆記(二)變量和基本類型

第2章 變量和基本類型

基本內置類型(Primitive Built-in Types)

算數類型(Arithmetic Types)

算數類型分爲兩類:整型(integral type)、浮點型(floating-point type)。

bool類型的取值是truefalse

一個char的大小和一個機器字節一樣,確保可以存放機器基本字符集中任意字符對應的數字值。wchar_t確保可以存放機器最大擴展字符集中的任意一個字符。

在整型類型大小方面,C++規定short ≤ int ≤ long ≤ long longlong long是C++11定義的類型)。

浮點型可表示單精度(single-precision)、雙精度(double-precision)和擴展精度(extended-precision)值,分別對應floatdoublelong double類型。

除去布爾型和擴展字符型,其他整型可以分爲帶符號(signed)和無符號(unsigned)兩種。帶符號類型可以表示正數、負數和0,無符號類型只能表示大於等於0的數值。類型intshortlonglong long都是帶符號的,在類型名前面添加unsigned可以得到對應的無符號類型,如unsigned int

字符型分爲charsigned charunsigned char三種,但是表現形式只有帶符號和無符號兩種。類型charsigned char並不一樣, char的具體形式由編譯器(compiler)決定。

如何選擇算數類型:

  • 當明確知曉數值不可能爲負時,應該使用無符號類型。

  • 使用int執行整數運算,如果數值超過了int的表示範圍,應該使用long long類型。

  • 在算數表達式中不要使用charbool類型。如果需要使用一個不大的整數,應該明確指定它的類型是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)數,以0x0X開頭的整數代表十六進制(hexadecimal)數。在C++14中,0b0B開頭的整數代表二進制(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 = &pi;    // 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 = &pi;          // error: ptr is a plain pointer
const double *cptr = &pi;   // 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 = &pi;  // 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指針

constconstexpr限定的值都是常量。但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)通常包含那些只能被定義一次的實體,如類、constconstexpr變量。

頭文件一旦改變,相關的源文件必須重新編譯以獲取更新之後的聲明。

預處理器概述

確保頭文件多次包含仍能安全工作的常用技術是預處理器(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++語言中關於作用域的規則。

整個程序中的預處理變量,包括頭文件保護符必須唯一。預處理變量的名字一般均爲大寫。

頭文件即使目前還沒有被包含在任何其他頭文件中,也應該設置保護符。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章