1. 程式人生 > >C++ Primer 第二章筆記

C++ Primer 第二章筆記

Chapter 2 Variables and Basic Types

2.1 基本內建型別

2.1.1 算術型別

算術型別分為兩類:整型(integral type,包括字元和布林型別)和浮點型。

​ 基本字元型別是 char,一個 char 的空間應確保可以存放機器基本字符集中任意字元對應的數字值。也就是說,一個 char 的大小和一個機器位元組一樣。

​ 其他字元型別用於擴充套件字元型別,如 wchar_t、char16_t、char32_t、wchar_t 型別用於確保可以存放機器最大擴充套件字符集中的任意一個字元,型別 char16_t 和 char32_t 則為 Unicode 字符集服務。

​ 浮點型可表示單精度,雙精度和擴充套件精度值。通常,float 以一個字(32 bits)來表示,double 以兩個字(64 bits)來表示,long double 以 3 或 4 個字(96 or 128 bits)來表示。一般來說,型別 float 和 double 分別有 7 個和 16 個有效位。

​ 除去布林型和擴充套件的字元型之外,其他整型可劃分為 signed 和 unsigned 兩種。而字元型被分為了三種:char、signed char 和 unsigned char。型別 char 實際表現為哪一種,具體是由編譯器來決定的。

Type Meaning Minimum Size
bool boolean NA
char character 8 bits
wchar_t wide character 16 bits
char16_t Unicode character 16 bits
char32_t Unicode character 32 bits
short short integer 16 bits
int integer 16 bits
long long integer 32 bits
long long long integer 64 bits
float single-precision floating-point 6 significant digits
double double-precision floating-point 10 significant digits
long double extend-precision floating-point 10 significant digits

2.1.2 型別轉換

# include <iostream>
int main() {
    unsigned u = 10, u2 = 42;
    std::cout << u2 - u << std::endl;    // 32
    std::cout << u - u2 << std::endl;    // 4294967264

    int i = 10, i2 = 42;
    std::cout << i2 - i << std::endl;    // 32
    std::cout << i - i2 << std::endl;    // -32
    std::cout << i - u << std::endl;     // 0
    std::cout << u - i << std::endl;     // 0
    return 0;
}

2.1.3 字面值常量

我們的整型字面值寫作十進位制數、八進位制數、十六進位制數的形式。以 0 開頭的數代表八進位制數,以 0x 或 0X 開頭的代表十六進位制數。

預設情況下,是十進位制字面值是帶符號數,八進位制和十六進位制不確定。十進位制字面值是 int、long、long long 中尺寸最小的那個,前提是這種型別可容納當前的值。

有單引號括起來的一個字元稱為 char 型字面值,雙括號括起來的零個或多個字元則構成字元型字面值。

'a'             // 字元字面值 
'Hello World!'  // 字串字面值

轉義序列

自己查書去。

2.2 變數

2.2.1 變數定義

初始化不是賦值,初始化的含義是建立變數時賦予其一個初始值,而賦值的含義是把物件的當前值擦除,而以一個新值來替代。

如果內建型別的變數未被顯示初始化,它的值由定義的位置決定。定義於任何函式體外的變數被初始化為 0,定義在函式體內部的內建變數型別不被初始化,訪問這類值將引發錯誤。類的物件如果沒有顯示地初始化,則其值由類確定。

2.2.2 變數宣告和定義的關係

分離式編譯機制(separate compilation):允許將程式分割成若干個檔案,每個檔案可悲獨立編譯。

宣告(declaration)使得名字為程式所知,而定義(definition)負責建立與名字關聯的實體。

如果像宣告一個變數而非定義它,就在變數名錢新增關鍵字 extern,而不要顯式地初始化變數:

extern int i;   // 宣告 i 而非定義 i
int j;          // 宣告並定義 j
extern double pi = 3.1416     // 定義

變數能且只能被定義一次,但是可以被多次宣告。

2.2.3 識別符號

C++ 的識別符號由字母,數字和下劃線組成,其中必須以字母或下劃線開頭。識別符號的長度沒有限制,但對大小寫字母敏感。

使用者自定義的識別符號不能連續出現兩個下劃線,也不能以下劃線緊連大寫字母開頭。此外,定義在函式體外的標識不能以下劃線開頭。

變數命名規範

  • 識別符號要能體現實際含義
  • 變數名一般用小寫字母
  • 使用者自定義的類名一般以大寫字母開頭
  • 如果識別符號由多個單片語成,則單詞間應有明顯區分

2.3 複合型別

2.3.1 引用

引用為物件起了另外一個名字,將宣告符寫成 & 的形式來定義引用型別,其中 d 是宣告的變數名。定義引用時,程式把引用和它的初始值繫結在一起,而不是拷貝。一旦繫結,則一直繫結,無法重新繫結到新物件,因此必須初始化。

引用並非物件,只是一個已經存在的物件所起的另外一個名字。

引用本身不是一個物件,所以不能定義引用的引用。

int i, &ri = i;
i = 5; ri = 10;
std::cout << i << " " << ri << std::endl;
//  10 10

2.3.2 指標

指標本身就是一個物件,允許對指標賦值拷貝,而且在指標的生命週期內它可以先後指向幾個不同的物件。

指標無須在定義時賦初值。和其他內建型別一樣,在塊作用域內定義的指標如果沒有被初始化,也將擁有一個不確定的值。

定義指標型別的方法將宣告符寫成 *d 的形式,其中 d 是變數名。

獲取物件的地址

取地址符 &

double dval;    
double *pd = &dval;     // 正確:初始值是 double 型物件的地址
double *pd2 = pd;       // 正確:初始值是指向 double 物件的指標

int *pi = pd;           // 錯誤:指標 pi 的型別和 pd 的型別不匹配
pi = &dval;             // 錯誤:試圖把 double 型物件的地址賦給 int 型指標

指標值

指標的值(即地址)應屬於下列4種狀態之一:

  1. 指向一個物件。

  2. 指向緊鄰物件所佔空間的下一個位置。

  3. 空指標,意味著指標沒有指向任何物件。

  4. 無效指標,也就是上述情況之外的其他值。

    試圖拷貝或以其他的形式訪問無效指標的值都將引發錯誤。儘管第2中和第3種形式的指標是有效的,但使用同樣受到限制,試圖訪問此類指標(假定的)物件的行為不被允許。

利用指標訪問物件

如果指標指向了一個物件,則允許用解引用符(*)來訪問該物件:

int ival = 42;     
int *p = &ival;    // p 存放著變數 ival 的地址;p 是指向變數 ival 的指標 
cout << *p;        // 由符號*得到指標 p 所指的物件,輸出42

*p = 0;            // 由符號*得到指標 p 所指的物件, 即經過 p 為 ival 賦值
cout << *p;        // 輸出 0 

對指標解引用會得出所指的物件,因此如果給解引用的結果賦值,實際上也就是給指標所指的物件賦值。

空指標

int *p1 = nullptr;      // 等價於 int *p1 = 0;
int *p2 = 0;            // 直接將 p2 初始化為字面常量 0
// 需要首先 #include cstdlib
int *p3 = NULL;         // 等價於 int *p3 = 0;

2.4 const 限定符

const 物件一旦建立後就不能再改變,所以 const 物件必須初始化,且初始值可以是任意複雜的表示式。

初始化和 const

預設狀態下, const 物件僅在檔案內有效。當多個檔案中出現了同名的 const 變數時,其實等同於在不同檔案中分別定義了獨立的變數。

如果想在多個檔案之間共享 const 物件,必須在變數的定義之前新增 extern 關鍵字。

2.4.1 const 的引用

可以把引用繫結到 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

因為不允許直接為 ci 賦值,當然也不能通過引用去改變 ci。因此,對 r2 的初始化時錯誤的。假設該初始化合法,則可以通過 r2 來改變它引用的物件的值,顯然不正確。

初始化和對 const 的引用

引用的型別必須與其所引用物件的型別一致,但是有兩個例外。第一種例外是在初始化常量引用時允許用任意表達式作為初始值,只要該表示式的結果能轉換成引用的型別即可。尤其,允許作為一個常量引用繫結非常量的物件,字面值,甚至是一個表示式。

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 = r * 2;           // error: r4 is a plain, non const reference

對 const 的引用可能引用一個並非 const 的物件

常量引用並不限定引用的物件本身是不是一個常量。

int i = 42;
int &r1 = i;         // r1 bound to i
const int &r2 = i;   // r2 also bound to i; but cannot be used to change i
r1 = 0;              // r1 is not const; i is now 0
r2 = 0;              // error: r2 is a reference to const

2.4.2 指標和 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

2.3.2 提到,指標的型別必須與其所指物件的型別一致,但是有兩個例外。第一種情況似乎允許令一個指向常量的指標指向一個非常量物件:

double dval = 3.14;   // dval is a double; its value can be changed
cptr = &dval;         // ok: but can't change dval through cptr

Tip: It may be helpful to think of pointers and references to const as pointers or references “that think they point or refer to const.”

const 指標

指標是物件而引用不是,因此允許把指標本身定為常量。常量指標必須初始化,而一旦初始化,它的值(就是那個地址)不能再改變。

2.4.3 頂層 const

術語頂層 const(top-level 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

2.4.4 constexpr 和常量表達式

常量表達式是指值不會改變並且在編譯過程中得到計算結果的表示式。一個物件是不是常量表達式是由它的資料型別個初始值共同決定的:

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

constexpr 變數

C++11 新標準規定,允許將變數宣告為 constexpr 型別以便由編譯器來驗證變數的值是否是一個常量表達式。宣告為 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

在 constexpr 宣告中如果定義了一個指標,限定符 constexpr 僅僅對指標有效,與指標所指的物件無關:

const int *p = nullptr;         // p is a pointer to a const int
constexpr int *q = nullptr;     // q is a const pointer to int

constexpr 把它所定義的物件置為了頂層 const。

constexpr int *np = nullptr;    // np is a constant pointer to int that is null
int j = 0;
constexpr int i = 42;           // type of i is const int
// i and j must be defined outside any function
constexpr const int *p = &i;    // p is a constant pointer to the const int i
constexpr int *p1 = &j;         // p1 is a constant pointer to the int j

2.5 處理型別

2.5.1 類型別名

有兩種方法可以定義類型別名。傳統的方法時使用關鍵字 typedef:

typedef duoble wages;  // wages is a synonym for double
typedef wages base, *p;  // base is a synonym for double, p for double*

含有 typedef 的宣告語句定義的不再是變數而是類型別名。新標準規定了一個新的方法,使用別名宣告來定義型別的別名:

using SI = Sales_item;  // SI is a synonym for Sales_item

用 using 關鍵字作為別名宣告的開始,其後緊跟別名和等號,其作用是把等號左側的名字規定成等號右側型別的別名。

指標、常量和類型別名

typedef char *pstring;
const pstring cstr = 0;    // cstr is a constant pointer to char
const pstring *ps;         // ps is a pointer to a constant pointer to char
const char *cstr = 0;      // wrong interpretation of const pstring cstr, cstr use a pointerto const char

型別 pstring 是型別 char* 的別名。

2.5.2 auto 型別說明符

C++ 新標準引入了 auto 型別說明符,讓編譯器通過初始值來推算變數的型別。顯然,auto 定義的變數必須有初始值。

複合型別、常量和 auto

編譯器推斷出得 auto 型別有時候和初始值得型別並不完全一樣,編譯器會適當地改變結果型別使其更符合初始化規則。

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,需要明確指出:

const auto f = ci;  // deduced type of ci is int; f has type const int

要在一條語句中定義多個變數,切記,符號 & 和 * 只從屬於某個宣告符,而非基本資料型別的一部分,因此初始值必須是同一型別:

auto k = ci, &l = i;  // k is int; l is int&
auto &m = ci, *p = &ci;  // m is a const int&;p is a pointer to const int
auto &n = i, *p2 = &ci;  // error: type deduced from i is int; type deduced from &ci is const int

2.5.3 decltype 型別提示符

C++ 11 新標準引入了第二種型別說明符 decltype,它的作用是選擇並返回運算元的資料型別。在此過程中,編譯器分析表示式並得到它的型別,卻並不計算表示式的值:

decltype(f()) sum = x;  // sum has whatever type f returns

2.6 自定義資料結構

從最基本的層面理解,資料結構是把一組相關的資料元素組織起來然後使用它們的策略和方法。

2.6.1 定義 Sales_data 型別

初步定義 Sales_data 如下:

struct Sales_data {
	std::string bookNo;
	unsigned units_sold = 0;
	double revenue = 0.0;
};

這個類以關鍵字 struct 開始,緊跟著類名和類體(其中類體部分可以為空)。類體由花括號包圍形成了一個新的作用域。類內部定義的名字必須唯一,但是可以與類外的名字重複。

類體右側表示結束的花括號後必須寫一個分毫,因為類體後面緊跟變數名以示對該型別物件的定義,所以分號必不可少:

struct Sales_data { /* ... */ } accum, trans, *salesptr;
// equivalent, but better way to define these objects
struct Sales_data { /* ... */ };
Sales_data accum, trans, *salesptr;

2.6.2 使用 Sales_data 類

2.6.3 編寫自己的標頭檔案

為了確保各個檔案中類的定義一致,類通常被定義在標頭檔案中,而且類所在標頭檔案的名字應該與類的名字一樣。例如,把 Sales_data 類定義在名為 Sales_data.h 的標頭檔案中。

標頭檔案一旦被改變,相關的原始檔必須重新編譯以獲取更新過的宣告。

前處理器概述

​ 確保標頭檔案多次包含仍能安全工作的技術是前處理器(preprocessor),在編譯之前執行,比如預處理功能 #include。

​ 還有一項預處理功能是標頭檔案保護符(header guard),標頭檔案保護符依賴於預處理變數(2.3.2 節,p48)。預處理變數有兩種狀態:已定義和未定義。#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

預處理變數無視 C++ 語言中關於作用域的規則。