Tips for C++ Primer Chapter 2 變量和基本類型

第2章 變量和基本類型

基本內置類型

基本內置類型:算術類型和空類型;算術類型又包括整型(字符和布爾類型也包括在內)和浮點型。express

 

可尋址的最小內存塊稱爲「字節」(byte),存儲的基本單元稱爲「字」(word);一個字一般由幾個字節組成。數據結構

1字節(byte)=8比特(bit),比特非0即1。函數

 

類型unsigned int能夠縮寫爲unsigned。spa

 

int與singed int相同(除了布爾型和擴展字符類型(例如寬字符wchat_t)以外,其餘的整型類型(例如short、long)也相似),但字符型例外,字符型有char、unsigned char、signed char三種類型,但char與signed char不必定相同;字符的表現形式只有兩種:帶符號的和無符號的,char實際上會表現爲上述二者的其中一種,具體由編譯器決定。指針

 

在算數表達式中不要使用char或bool。由於char在一些機器上多是有符號的,而在另外的機器上多是無符號的,使用char進行運算可能出錯;若是硬要使用一個不大的整數,那麼明確指定它爲unsigned char或signed char。bool取值非0即1,不適宜用於算數表達式。code

 

當賦給無符號類型一個超出它表示範圍的值時,結果是初始值對無符號類型表示數值的總數取模後的餘數。對象

例如:unsigned char佔8個比特(1個字節),能夠表示[0, 255]的共256個數值,當賦予區間外的一個值,假設是-1,則實際結果是(-1) % 256 = 255。blog

 

當賦給一個帶符號類型一個超出它表示範圍的值時,結果是未定義的,此時程序可能繼續工做、崩潰或產生垃圾數據。生命週期

 

當一個算數表達式中既有無符號數又有int值時,那個int值會轉換成無符號數。把int轉換成無符號數的過程至關於把int賦給無符號類型。ip

切勿混用帶符號類型和無符號類型。若是表達式既有帶符號類型又有無符號類型,當帶符號類型取值爲負時會出現異常結果,這是由於帶符號數會自動轉換成無符號數。

例如:當a=-1,b=1,c=a*b,若a、b都是int,則c爲-1;若a是int,b是unsigned int,則c的結果是(-1)%(2^32) * 1 = 4294967295(視當前機器上int所佔bit數而定,這裏假設int是32bit)。

 

整型字面值可寫做十進制數、八進制數、十六進制數的形式。

0開頭:八進制

0x、0X開頭:十六進制

 

浮點數字面值可省略整數部分或小數部分(若該部分爲0)。

例如:0.、.001

浮點數字面值默認是一個double。

 

能夠之前綴或後綴指定字面值類型。

前綴

  u:char16_t(Unicode16字符)

  U:char32_t(Unicode32字符)

  L:wchar_t(寬字符)

  u8:char(UTF-八、僅用於字符串字面常量)

後綴

對整型字面值:

  u、U:unsigned

  l、L:long

  ll、LL:long long

對浮點型字面值:

  f、F:float

  l、L:long double

 

變量

初始化與賦值是不一樣的概念。

  初始化:建立變量時賦予其一個初始值

  賦值:把對象的當前值擦除,並以一個新值替代

因此賦值的開銷大於初始化。

 

默認初始化規則

若是內置類型的變量未被顯式初始化,它的值由定義的位置決定。

  定義在任何函數體外:被初始化爲0

  定義在函數體內:將不被初始化,其值未定義

注:指針類型並不是基本內置類型,但也有上述特性。

若是類的對象沒有顯式初始化,其值由類肯定。(經過類的默認構造函數類內初始值等)

 

extern關鍵字

聲明使得名字爲程序所知,而定義建立與名字關聯的實體。

若是想聲明一個變量而非定義它,就在變量名前添加關鍵字extern,而不要顯式地初始化變量。

例如:

extern int i; //聲明i而非定義i

int j; //聲明並定義j

 

包含顯式初始化的聲明即成爲定義。

extern double pi = 3.14; //定義;容許在函數體外這麼作,但extern關鍵字的存在就沒意義了

不容許在函數體內部初始化一個由extern關鍵字標記的變量。

 

複合類型

引用自己並不是對象,也沒有實際的地址(若嘗試用取地址符&取得某個「引用的地址」,實際取得的是所引用的對象的地址),它只是已存在的對象的別名。沒法定義引用的引用,也沒法定義指向引用的指針。

引用必須初始化,由於沒法令引用從新綁定到另外一對象。

全部引用的類型都要和與之綁定的對象嚴格匹配(有兩個狀況例外,將在後面提到)。

 

引用必須綁定到對象上,不能綁定到字面值或某個表達式的計算結果。

(能夠將引用綁定到const對象上,就像綁定到其它的對象上同樣,稱之爲「對常量的引用」。與普通引用不一樣的是,對常量的引用不能被用做修改它所綁定的對象。)

 

指針自己是一個對象,容許對指針賦值和拷貝,指針在其生命週期內能夠指向不一樣對象。

指針無需在定義時賦初值。和基本內置類型相同,定義在全部函數體外的指針若未初始化,將有默認初值0,而塊做用域內定義的指針將擁有一個不肯定的值。

全部指針的類型都要和所指的對象嚴格匹配(有兩個狀況例外,將在後面提到)。

 

&、*出如今聲明語句中,用來組成複合類型;&出如今表達式中,是一個取地址符;*出如今表達式中,是一個解引用符(或乘法運算符)。

 

nullptr = NULL = 0

 

void*是一種特殊的指針類型,可用於存聽任意對象的地址。但不能直接操做void*指針所指的對象,由於對象的類型決定了能對對象所作的操做,而咱們並不知道對象的類型。

 

指向指針的指針

指針是內存中的對象,像其它對象同樣也有本身的地址,所以容許把指針的地址再存放到另外一個指針當中。

經過*的個數區分指針的級別。**是指向指針的指針,***是指向指針的指針的指針,等等。

int i = 1024;

int *pi = &i; //pi指向一個int型的數

int **ppi = π //ppi指向一個int型的指針

解引用int型指針會獲得一個int型的數,解引用一個指向指針的指針會獲得一個指針。

這裏有三種方式取得i的值:i、*pi、**ppi。

 

指向指針的引用

int i = 1;

int *p; // p是一個int型指針

int *&r = p; // r是一個對指針p的引用

r = &i; // 由於r是對指針p的引用,即r是p的別名,所以給r賦值&i就是令p指向i

*r = 0; // 解引用r獲得i,也就是p指向的對象,所以i的值將改成0

 

const限定符

const對象一旦建立後其值就不能再改變,因此const對象必須初始化(初始值容許是任意的表達式,不必定是字面量常量)。

const int i = get_size(); // i將在運行時初始化

const int j = 1; // j將在編譯時初始化

const int k; // 錯誤,k沒有初始化

 

默認狀態下,const對象僅在文件內有效;若是想在多個文件共享const對象,必須在變量的定義前添加extern關鍵字。

當多個文件出現了同名的const對象時,其實等同於在不一樣文件中分別定義了獨立的變量。

若是想要:只在一個文件中定義const對象,而在其它多個文件中聲明並使用它,則:對於const變量無論是聲明仍是定義都添加extern關鍵字,這樣只需定義一次就夠了。

【file_1.cc】

  int get_size() {...}

  extern const int bufSize = get_size(); //定義並初始化了一個常量(extern表示bufSize能被其它文件訪問)

【file_1.h】

  extern const int bufSize; //只是聲明,這個bufSize與file_1.cc中定義的bufSize是同一個(extern表示bufSize並不是本文件全部,它的定義在別處出現)

 

對const的引用

能夠將引用綁定到const對象上,就像綁定到其它的對象上同樣,稱之爲「對常量的引用」(「reference to const」,或「對const的引用」,或「常量引用」)。與普通引用不一樣的是,對常量的引用不能被用做修改它所綁定的對象。

const int ci = 1;

const int &r1 = ci; //正確;引用及其對應的對象都是常量

r1 = 2; //錯誤;r1是對常量的引用

int &r2 = ci; //錯誤;試圖讓一個很是量引用指向一個常量對象(設想:若這條語句合法,即默認「容許經過r2來修改ci的值」,而ci是常量,顯然矛盾)

 

對const的引用與初始化

前面提到引用的類型必須與其所引用的對象一致,可是有兩個例外。這裏說它的第一種例外:

在初始化常量引用時,容許用任意表達式做爲初始值(只要該表達式的結果能轉換成引用的類型便可);

尤爲,容許爲一個常量引用綁定很是量的對象字面值,或者通常的表達式

int i = 1;

const int &r1 = i; //容許將const int&綁定到一個普通int對象上

const int &r2 = 1; //正確;r2是一個常量引用

const int &r3 = r1 * 2; //正確;r3是一個常量引用

int &r4 = r1 * 2; //錯誤;r4是一個普通的很是量引用,不容許綁定到字面值或某個表達式的計算結果

 

看看當一個常量引用被綁定到另一種類型(合法)時發生了什麼:

  double dval = 3.14;

  const int &ri = dval; //合法;ri值爲3

ri引用了一個int型的數,但dval倒是一個double型的浮點數而非整數。所以爲了確保讓ri綁定一個整數,編譯器把上述代碼變成了:

  const int temp = dval; //由double型浮點數生成一個臨時的int型整數常量

  const int &ri = temp; //讓ri綁定這個臨時量(臨時量是臨時量對象的簡稱,是內存中的一個未命名對象)

 

順便,對前例,探討一下當ri不是常量(非法)時會發生什麼:

若是ri不是常量,意味着容許對ri賦值(或者說經過ri修改其綁定的對象)。上例談到,ri綁定到了一個臨時量,而非dval。

咱們既然讓ri引用dval,確定想經過ri改變dval的值,因此不會想要把引用綁定到臨時量上,因此,C++認爲「普通的很是量指針去綁定另一種常量類型」的行爲是非法的。

 

對const的引用可能引用一個並不是const的對象

對const的引用的機制是:不容許經過引用來修改所引用的對象的值,但所引用的對象的值可經過其它途徑修改。

int i = 1;

int &r1 = i; //引用r1綁定非const的對象i

const int &r2 = i; //r2也綁定對象i

r1 = 0; //合法;i的值修改成0

r2 = 0; //非法;r2是一個常量引用,不容許經過r2修改i的值

修改i的值的「其它途徑」能夠是各類合法的途徑,例如直接對i賦值。

 

指針和const

與引用同樣,也能夠令指針指向常量或很是量。

相似於常量引用,指向常量的指針(pointer to const)不能用於改變其所指對象的值。

要想存放常量對象的地址,只能使用指向常量的指針。

const double pi = 3.14;

double *ptr = π //非法;ptr是一個普通指針

const double *cptr = π //合法

*cptr = 1.0; //非法

 

前面提到,指針的類型必須與其所指的對象的類型一致,可是有兩個例外。這裏先討論第一種例外:

容許令一個指向常量的指針指向一個很是量對象。

double dval = 3.14; //dval是一個很是量對象

const double *cptr = &dval; //合法;可是不能經過cptr改變dval的值(與指向常量的引用相似,這裏的dval仍然可能經過其它途徑修改)

 

const指針(常量指針)

常量指針的意義是指針自己是常量。

注:注意「指向常量的指針」與「常量指針」相區別。前者機制是:不能經過指針改變所指對象的值;後者的機制是:不能改變指針自己的值(也就是存放在指針中的那個地址)。

注:在討論「對const的引用」(reference to const,對常量的引用)時,說「對const的引用」簡稱「常量引用」,但嚴格來講並不存在「常量引用」。由於引用並非一個對象,因此沒辦法讓引用「恆定不變」。事實上,因爲C++不容許改變引用所綁定的對象,因此從這層意義上理解,全部的引用都算是「常量」。因此以「常量引用」代稱「對const的引用」何嘗不可。

const double pi = 3.14;  //pi是一個常量對象

const double *const pip = π //pip是一個指向常量對象的常量指針

從右往左,離pip最近的那個const說明pip自己是一個常量對象,對象的類型由聲明符的其他部分決定。聲明符中的下一個符號是*,說明pip是一個常量指針,最後const double肯定了常量指針指向的是一個double型常量對象。

 

頂層const(top-level const)與底層const(low-level const)

當僅對指針而言:

頂層const:指針自己是個常量

底層const:指針所指的對象是一個常量

更通常的:

頂層const能夠表示任意對象是常量,這一點對任何數據類型都適用,好比算數類型、類、指針等;

底層const則與指針和引用等複合類型的基本類型部分有關。

特殊的是:

指針既能夠是頂層const也能夠是底層const;用於聲明引用的const都是底層const。

int i = 0;

int *const p1 = &i; //不能改變p1的值,頂層const

const int ci = 1; //不能改變ci的值,頂層const(PS:事實上,「const int ci = 1」等價於「int const ci = 1」)

const int *p2 = &ci; //容許改變p2的值,底層const

const int *const p3  = p2; //靠右的是頂層const,靠左的是底層const

const int &r = ci; //用於聲明引用的const都是底層const

 

當執行對象的拷貝操做時,常量的頂層const不須要被考慮,由於拷貝操做不改變被拷貝對象的值,所以,拷入(指的是對常量對象的初始化時的拷貝動做,固然,初始化完成後就不能對常量對象有拷入動做了)和拷出的對象是不是常量都沒什麼影響。(例如上述示例的倒數第二條語句)

底層const的限制不能被忽略,拷入和拷出的對象必須有相同的底層const資格,或者兩個對象的數據類型可以轉換。(很是量能夠轉換成常量,反之不行)

int *p = p3; //非法;p3包含底層const定義,而p沒有

p2 = p3; //合法;p二、p3都是底層const

p2 = &i; //合法;int*能轉換成const int*(很是量能夠轉換成常量,反之不行)

int &r = ci; //非法;普通的int&不能綁定到int常量上

const int &r2 = i; //合法;const int&能夠綁定到一個普通int上

 思考一下:爲什麼「很是量能夠轉換成常量,反之不行」?

看第一條語句(非法:常量不可轉爲很是量),假設第一條語句合法,也就是說,假設容許以p3初始化p,看會發生什麼:

p3包含底層const定義,即p3指向的對象是常量;而p不包含底層const定義,p指向的是一個普通的(很是量)整數,也就是說:「可能經過p修改它所指的對象的值」,但所指的對象是常量,這是矛盾的。

 

constexpr和常量表達式

常量表達式(const expression):值不會改變,且在編譯過程就能獲得計算結果的表達式。

const int i = 1; //i是常量表達式

const int j = i + 1; //j是常量表達式

int k = 2; //k不是常量表達式(值可能改變)

const int sz = get_size(); //sz不是常量表達式(在運行過程才能獲得具體值)

 

constexpr變量

C++11能夠用constexpr來明確聲明常量表達式。

constexpr int ce = 1; //ok

constexpr int sz = size(); //僅當size()函數是一個constexpr函數時纔是一條合法語句

 

字面值類型

字面值類型:包含算術類型、引用、指針、字面值常量類、枚舉類型。

字面值類型能夠定義成constexpr。

 

字面值類型定義constexpr的特殊狀況

指針和引用定義成constexpr時,它們的初始值有嚴格限制:

關於指針:

一個constexpr指針的初始值必須是0(nullptr),或者是存儲於某個固定地址中的對象;

  故constexpr指針不能指向函數體內定義的變量,能夠指向函數體外的變量;

  函數體內的local static object局部靜態對象與函數體外的對象同樣有固定地址,故constexpr指針能指向局部靜態對象。

在constexpr的聲明中若是定義了一個指針,限定符constexpr僅對指針有效,而與指針所指的對象無關;

  const int *p = nullptr; //p是一個指向整型常量指針(p自己可被改變)

  constexpr int *q = nullptr; //q是一個指向整數常量指針(q自己不可被改變)

  緣由是:constexpr把它所定義的對象置爲了頂層const。

與const指針相似,constexpr指針既能夠指向常量,也能夠指向很是量。(指向很是量時,必須是存儲於某個固定地址中的對象)

  constexpr int i = 1; //i是整型常量(i必須定義在函數體外,不然編譯錯誤)

  constexpr const int *p = &i; //p是常量指針,指向整型常量i

  注意這個特殊寫法,若以const限定符聲明常量指針,const的位置緊鄰p的左邊:const int *const p = &i;

  而這裏因爲constexpr的特殊性(限定符constexpr僅對指針有效)而有其特殊語法:constexpr const int *p = &i。

 

關於引用:

constexpr引用只能綁定到局部靜態對象。

 

處理類型

類型別名

方式一:使用關鍵字typedef。

  typedef double wages; //wages是double的同義詞

方式二:(C++11)使用別名聲明(alias declaration)

  using wages = double;

 

指針、常量和類型別名

若是某個類型別名指代的是複合類型或常量,那麼把它用到聲明語句裏會產生意想不到的後果。

  typedef char *pstring; //類型pstring是類型char*的別名

  const pstring cstr = 0; //cstr是指向char的常量指針(指針自己是常量)

  const pstring *ps; //ps是一個指針,它所指的對象是一個指向char的常量指針

  上述兩條語句的基本數據類型都是const pstring,const是對類型pstring的修飾,類型pstring是指向char的指針,所以const pstring就是指向char的常量指針而非指向常量字符對象的指針

一個錯誤的理解方式:

  嘗試把類型別名替換成它原本的樣子,以理解該語句的含義。(這種方式是錯誤的)

  嘗試把pstring替換成char*,有:

  const char *cstr = 0;

  這樣看來,*成了聲明符的一部分,用以說明cstr是一個指針,const char成了基本數據類型,const是對類型char的修飾,這條語句的含義就是「指向const char的(普通)指針」。這與替換前的實際意義大相徑庭。

 

auto類型說明符

使用auto也容許在一條語句中聲明多個變量,可是,由於一條語句只能有一個基本數據類型,因此該語句中的全部變量的初始基本數據類型都必須相同。

auto i = 0, *p = &i; //合法;i是int型、p是int型指針

auto sz = 0, pi = 3.14; //非法;sz是int,pi是double,類型不一致

const int ci = i;

auto &n = i, *p2 = &ci; //非法;i的類型是int,而ci的類型是const int

 

複合類型、常量和auto

編譯器推斷出來的auto類型有時候和初始值的類型並不徹底同樣,編譯器會適當地改變結果類型使其更符合初始化規則。

例如:

使用引用其實就是使用引用的對象,特別是當引用做爲初始值時,真正參與初始化的實際上是引用對象的值。此時編譯器以引用對象的類型做爲auto的類型。

int i = 0, &ri = i;

auto a = ri; //a是一個整數,由於ri是i的別名,而i是一個整數

 

其次,auto通常會忽略頂層const保留底層const。(後面會談到特例)

例如:

當初始值是一個指向常量的指針時:

const int ci = i, &cr = ci; //ci是一個常量,cr是對常量的引用

auto b = ci; //b是一個整數(非const),由於ci的頂層const特性被忽略了

auto c = cr; //c是一個整數(非const),由於cr是ci的別名,ci自己是一個頂層const,而頂層const被忽略了

auto d = &i; //d是一個整型指針,由於一個整數的地址也就是指向整數的指針

auto e = &ci; //e是一個指向整數常量(const)的指針,由於ci是一個常量對象,而對常量對象取地址是一種底層const,且auto會保留底層cons

 

若是但願推斷出的auto類型是一個頂層const,須要明確指出:

const auto f = ci; //ci的推演類型爲int(而非const int),但明確指出後,f是const int

 

前面說到「auto通常會忽略頂層const」,此例即爲特例:

當設置一個類型爲auto引用時,初始值的頂層const仍然保留

auto &g = ci; //g綁定到ci,g是一個整型常量引用(reference to const,對const的引用),由於ci的頂層const特性被保留了

 

decltype類型指示符

decltype:編譯器分析表達式並獲得它的類型,卻不實際計算表達式的值。

decltype(f()) sum = x; //sum的類型被設定爲函數f的返回類型(注意:編譯器並不實際調用函數f)

 

decltype處理頂層const和引用的方式與auto不一樣。若是decltype使用的表達式是一個變量,則decltype返回該變量的類型(包括頂層const和引用在內)。

const int ci = 0, &cj = ci;

decltype(ci) x = 0; //x的類型被設定爲const int

decltype(cj) y = x; //y的類型是const int&,y綁定到變量x

decltype(cj) z; //非法;z的類型是const int&,是一個引用,引用必須初始化

 

引用歷來都做爲其所指對象的同義詞出現,只有用在decltype處是一個例外。

也就是說:不要由於cj引用ci,就認爲「decltype(cj) y = x」等同於「decltype(ci) y = x」。

 

decltype和引用

若是decltype使用的表達式不是一個變量,則decltype返回表達式結果的對應類型。

int i = 1, *p = &i, &r = i;

decltype(r+0) b; //合法;加法的結果是int,所以b是一個未初始化的int

decltype(*p) c; //非法;c是int&(而非int;下面會解釋),必須初始化

解釋:由於r是一個引用,因此decltype(r)的結果是引用類型(而不是引用所指對象的類型)。若是想讓結果是r所指對象的類型,能夠把r做爲表達式的一部分,如r+0,顯然這個表達式的結果是一個具體值而非一個引用;

若是表達式的內容是解引用操做,則decltype將獲得引用類型。解引用指針能夠獲得指針所指的對象,並且還能給這個對象賦值。所以,decltype(*p)的結果是int&,而非int。

 

給變量加上一層或多層括號,編譯器就會把它看成一個表達式。變量是一個能夠做爲賦值語句左值的特殊表達式,因此這樣的decltype就會獲得引用類型。

int i = 1;

decltype(i) d; //合法;d是一個(未初始化的)int

decltype((i)) e; //非法;e是int&,必須初始化

總結:decltype((variable))的結果永遠是引用,而decltype(variable)的結果僅當variable自己就是一個引用的時候纔是引用。

 

補充例子:

賦值是會產生引用的一類表達式,引用的類型就是左值的類型。例如:若是i是int,則表達式i=x的類型是int&。

int a = 3, b = 4;

decltype(a) c = a; //c是int,值爲3

decltype(a=b) d = a; //d是int&,值爲3

d的值爲什麼不是4?

因爲「a=b」,故a的值變爲4,而d是a的引用,故d的值是4?這是錯誤的,「a=b」只是用於編譯器推斷表達式的類型,實際上該表達式並不會真的執行,因此a的值仍舊是它原來的值3。

 

自定義數據結構

C++11容許爲類內數據成員提供一個類內初始值(in-class initializer)。建立對象時,類內初始值將用於初始化數據成員。沒有初始值的成員將被默認初始化(前面討論過各類變量的默認初始化的規則)。

struct Sales_data

{

  std::string bookNo; //將會進行默認初始化;將會初始化爲空字符串

  unsigned sold = 0; //將由類內初始值初始化;將會初始化爲0

  double revenue; //將會進行默認初始化(結果:其值未定義,多是亂七八糟的值)

};

 

頭文件保護符

【Sales_data.h】(類一般定義在頭文件中,頭文件的名字應與類名相同)

#ifndef SALES_DATA_H

#define SALES_DATA_H

//#include <...>

struct Sales_data {

  //...

};

#endif

防止頭文件被屢次include,避免重複定義。

預處理指令#ifdef用於判斷給定預處理變量是否已經定義;#ifndef用於判斷給定預處理變量是否還沒有定義。

相關文章
相關標籤/搜索