C++ Primer筆記 第二篇:C++基礎——基本類型和變量

1、基本內置類型

  • 算術類型
     (1) 整數類型:short、int、long、char、bool
     (2) 浮點數類型:float、double
  • 空類型:void
1.1 內置類型的機器實現

對算術類型,C++標準只規定了各類型所佔的最小尺寸,因此,在不同的機器上,同樣的算術類型可能具有不同的尺寸。這一點和Java不同(Java規定了每種內置類型的具體尺寸,是平臺無關的,這也是Java可移植性好的原因之一)。

1.2 選擇類型的經驗準則

和C語言一樣,C++的設計準則之一也是儘可能地接近硬件,C++的算術類型必須滿足各種硬件特質。一些選擇類型的經驗準則:

  • 當明確知道數值不爲負時,選擇無符號類型
  • 使用 int 執行整數運算:實際應用中,short 往往太小;long 往往和 int 尺寸一樣大。int 不夠用時,使用 long long
  • 算術表達式中不要使用 char、bool:不同機器是編譯器對 char 的處理可能不一樣,有些是有符號的,有些是無符號的。如果必須使用 char,請明確指明其類型是 signed char 或者 unsigned char
  • 浮點數計算使用 double:float通常精度不夠,而且單雙精度浮點數的計算代價相差無幾。long double 一般用不到。
1.3 基本算術類型的轉換
  • 非布爾類型的算術值賦給布爾類型時,非0爲true,0爲false;布爾值賦給非布爾類型時,true轉爲1,false轉爲0
  • 浮點數賦給整型時,只保留非小數部分;整型值轉爲浮點數時,小數部分記爲0,如果該整數所佔的空間超過浮點類型的容量,精度可能有損失。
  • 賦給無符號類型一個超出其範圍的值時,結果是初始值對無符號類型表示數值總數取模後的餘數。例如,8bit 大小的 unsigned char 可以表示0~255範圍的值,如果賦給一個超出範圍的值,結果是該值對256取模後的餘數。因此,-1賦給8bit 大小的 unsigned char 的結果是255。注意:切勿混用有符號類型和無符號類型。 因爲,如果表達式中既有帶符號類型又有無符號類型,帶符號類型會自動轉換成無符號數,當帶符號類型爲負值時會出現異常結果。
  • 賦給有符號類型一個超出其範圍的值時,結果是 未定義的(undefined) 。程序可能崩潰,也有可能產生垃圾數據。
1.4 字面值常量
    using namespace std;
    cout << "Hello World!" << endl;
    cout << 0.5 + 99 << endl;

上面代碼片段中,“Hello World!” 和 0.5、99 就是所謂的字面值常量,指的是它們的值就是字面上呈現的樣子,無法改變。

儘管我們沒有爲字面值常量明確指明其數據類型,但每個字面值常量都有對應的數據類型,否則計算機將不知道如何來存儲這些字面值常量。

字面值常量的形式和值決定了它的數據類型:

  • 十進制整型字面值的類型是在滿足能容納當前值的前提下,int、long、long long 中尺寸最小的那個
  • 浮點型字面值默認是 double 類型。可以在字面值後添加後綴來表示其它浮點類型
  • 單引號括起來的一個字符是 char 型字面值
  • 雙引號括起來的0個或多個字符是 字符串型字面值 。字符串字面值的類型本質上是由常量字符構成的字符數組。編譯器在每個字符串的結尾處添加一個空字符(’\0’) [注意它和字符0(‘0’)的區別]。因此,字符串字面值的實際長度要比它的內容多1。
  • true 和 false 是布爾類型字面值
  • nullptr是指針字面值

2、變量

2.1 基本概念

變量的本質是一個具名的、可供程序操作的 存儲空間。爲了標識和操作這塊存儲空間,給它起個名字,這個名字叫做 變量名

int a;    // 定義
extern int b;   // 聲明

因此,上述代碼中的 a 是變量名,它標識了一塊存儲空間,這塊存儲空間存儲的數據可以改變,所以本質上可變的是存儲空間中的數據。但一般不說這麼繞,直接說變量a。

  • 變量定義 是一個 申請存儲空間,並 將變量名和申請的存儲空間關聯起來 的過程。C++中,使用的變量必須先定義,否則沒有空間存儲數據。
  • 變量聲明 不申請存儲空間,僅僅是告訴後面的程序,有這麼一個某種類型的變量名。
    • 如果所有程序都在同一個源文件中,我們只需要變量定義就夠了,此時變量定義同時也起到了聲明的作用;變量聲明主要的作用在於支持C++的分離式編譯,實現代碼共享(一處定義,多處使用)。比如在一個頭文件中定義了變量b,我們在其它文件中使用這個變量b時,就需要先聲明,告訴程序我們要用的變量b是已經定義好的。此時,在變量名之前添加關鍵字 extern 即可。
    • 變量只能被定義一次,但可以被多次聲明。
  • 初始值是變量在定義時獲得的一個特定值。此時我們說變量被 初始化 了。
    • 初始化和賦值是兩個不同的概念。儘管很多時候我們使用 = 來初始化一個變量。強調:初始化不是賦值。初始化是創建變量時賦予其一個初始值;賦值是把變量的當前值擦除,然後用一個新值來替代。
    • C++中初始化有多種不同形式,不僅僅是=這一種。比如下面4條語句都能完成對變量的初始化:
      int t1 = 0;        // =初始化
      int t2 = {0};     // 列表初始化
      int t3{0};         // 列表初始化
      int t4(0);         // 列表初始化
      
    • 默認初始化:如果定義變量時沒有指定初始值,則變量會被 默認初始化,也就是被賦予一個默認值。內置類型在函數外(全局變量)被初始化爲0;在函數內(局部變量)不被初始化,其值是未定義的。

3、複合類型

複合類型是在其它類型基礎上定義的類型。

聲明變量的語法: 基本數據類型 聲明符列表
聲明符爲變量起了個名字(變量名),並指定該變量是和基本數據類型相關的某種類型。
因此,在基本內置類型的聲明語句中,聲明符就是變量名,此時變量的類型就是聲明中的基本數據類型。而在複合類型的聲明語句中,聲明符更加複雜。下面介紹兩種常用的複合類型,引用指針

3.1 引用(reference)類型

引用類型:引用另外一種類型。就像寫論文時引用文獻中的論據,一個引用類型的變量r簡稱一個引用)引用另一個類型變量s中的內容,代表變量r的內容來源於另一個變量s
本質上,引用就是爲變量s起了一個別名。打個比方,就像一個人既有大名又有小名,但不論叫大名還是小名,最終指向的都是這個人。

強調: 引用只是已有對象的別名。引用本身不創建對象。

引用必須初始化:
變量初始化時,初始值會被拷貝到新創建的對象中。而在聲明引用時,由於引用並不創建對象,所以程序是把引用和它的初始值 (即它引用的對象) 綁定到一起,而不是拷貝初始值給引用。一旦初始化完成,引用將和它的初始值對象一直綁定在一起,無法重新綁定到另一個對象,因此引用必須初始化。
另: 因爲引用本身不是一個對象,所以不能定義引用的引用。

int ival = 1024;
int &refVal = ival;   //聲明引用,並初始化。refVal引用ival
int &refVal2;         //報錯 ‘refVal2’ declared as reference but not initialized:引用必須初始化

引用的定義:

  • 爲了和一般變量的定義區別開,引用的聲明符在變量名之前加&前綴
  • 引用只能綁定在對象上,不能綁定在字面值和表達式的計算結果上
int &refVal1 = 10;     //錯誤:引用類型必須綁定在對象上
double val = 3.14;
int &refVal2 = val;    //錯誤:引用類型和綁定的對象的類型不匹配
3.2 指針(pointer)類型

與引用類似,指針也能實現對其他對象的間接訪問。指針和引用的不同點在於:1.指針本身就是一個對象,因此可以對指針賦值和拷貝,從而指針可以先後指向不同的對象(非常量指針)。2.指針無須再定義時賦初值,在塊作用域內如果指針未被初始化,也將擁有一個不確定的值。

指針的定義:

double *dp, dp2;

其中,聲明符*dp是一個整體:dp是變量名,*說明這是一個指針,*只對dp生效;dp2是一個普通的double型變量。

在定義變量時,*表示定義一個指針變量;而在訪問指針指向的對象時,*表示解引用符

空指針:
空指針(null pointer)不指向任何對象。生成空指針的方法:

// 下面3種方法本質上是等價的,都是給指針賦予一個初始值0
int *p1 = nullptr;      //使用字面值nullptr進行初始化
int *p2 = 0;            //將p2初始化爲字面常量0
int *p3 = NULL;         //需要#include cstdlib

// 不能直接使用int變量給指針賦值,即使這個int變量的值爲0
int zero = 0;
int *p4 = zero;         //報錯:invalid conversion from ‘int’ to ‘int*’

void* 指針:
void*是一種特殊的指針類型,可存放任意對象的地址
void*指針主要用於指針比較、函數的輸入輸出等。不能直接操作void*指針所指的對象,因爲不知道這個對象的具體類型,從而也就無法確定能在這個對象上進行哪些操作。

3.3 理解複合類型的聲明

變量定義:
基本數據類型 聲明符列表

在同一條定義語句中,基本數據類型只有一個,但聲明符的形式卻可以不同,從而一條定義語句可以定義出不同類型的變量

// 一條定義語句中,定義了int型變量a,int型指針b,int型引用c
int a = 1024, *b = &a, &c = a;

指向指針的指針:

int ival = 1024;
int *pi = &ival;    //pi指向一個int型數據
int **ppi = &pi;    //ppi指向一個int型的指針

指向指針的引用:
指針本身是一個對象,因此可以定義對指針的引用。

#include <iostream>

using namespace std;

int main()
{
    int i = 42;
    int *p;
    //int &r = p;    //r是一個指向int型變量的引用,無法和指針綁定
    int *&r = p;     //r是一個對指針的引用

    cout << "r=" << r << ", p=" << p << endl;
    cout << "*r=" << *r << ", *p=" << *p << ", i=" << i << endl;
    cout << "---------------------------------\n"<< endl;

    r = &i;
    cout << "r=" << r << ", p=" << p << endl;
    cout << "*r=" << *r << ", *p=" << *p << ", i=" << i << endl;
    cout << "---------------------------------\n"<< endl;

    *r = 0;
    cout << "r=" << r << ", p=" << p << endl;
    cout << "*r=" << *r << ", *p=" << *p << ", i=" << i << endl;

    return 0;
}

結果是:

r=0x400c61, p=0x400c61
*r=1961723208, *p=1961723208, i=42
---------------------------------

r=0x7fff8bbdec74, p=0x7fff8bbdec74
*r=42, *p=42, i=42
---------------------------------

r=0x7fff8bbdec74, p=0x7fff8bbdec74
*r=0, *p=0, i=0

示例代碼中的 r 是對指針 p 的引用,我們應該從右往左閱讀 r 的定義:離變量名最近的符號對變量的類型有最直接的影響,因此&告訴我們 r 是一個引用。聲明符的其餘部分用以確定 r 引用的類型是什麼,例子中的*說明 r 引用的是一個指針。最後,基本數據類型部分指出 r 引用的是一個int型指針。

指向引用的指針:

引用本身不是一個對象,因此不能定義指向引用的指針

4、const限定符

const準確的含義是只讀,即使用相同的值給一個const變量重新賦值也不行。const對象一旦創建後其值就無法改變,所以 const對象必須初始化

const int i = 42;
const int j = i;
j = i;      //即使用相同的值重新賦值也會報錯:assignment of read-only variable ‘j’

const int k;    //const變量沒有初始化,報錯:uninitialized const ‘k’

編譯器在編譯過程中,會把使用到const變量的地方都替換成const變量相應的值。const變量默認只在文件內生效,如果需要在多個文件之間共享const變量,必須在const變量定義之前添加extern關鍵字,其它文件內使用該變量時,在聲明之前也添加extern關鍵字,用於表明該變量的定義在別的文件中。

4.1 const和引用

把引用綁定在 const 對象上,稱之爲對常量的引用(reference to const)。和普通引用不同,對常量的引用不能用於修改其綁定的對象。

    const int ci = 1024;
    const int &r1 = ci;
    r1 = 42;   //error: assignment of read-only reference ‘r1’
    int &r2 = ci; //error: binding ‘const int’ to reference of type ‘int&’ discards qualifiers

const 對象不能被賦值,所以也就不能通過引用去改變ci。用ci初始化r2報錯的原因是,假如該初始化合法,那麼可以通過r2來改變它引用對象的值,這顯然是錯誤的。

對常量的引用(reference to const)在工作中也常簡稱爲**“常量引用”**。嚴格地說,由於引用本身不是一個對象,我們無法讓引用本身恆定不變,所以並不存在常量引用;C++中無法改變引用所綁定的對象,從這一層意義上看的話所有的引用又都算常量。無論引用的對象是不是常量,都不會影響引用和對象的綁定關係本身。

通常,引用的類型必須和其所引用對象的類型一致。但有兩個例外:
其一:常量引用初始化時,允許任何可轉化成引用的類型的表達式作爲初始值。

    int i = 42;
    // 允許爲常量引用綁定非常量對象、字面值、表達式 
    const int &r1 = i;    //綁定非常量對象
    const int &r2 = 42;   //綁定字面值
    const int &r3 = r1 * 2;  //綁定表達式
    
    int &r4 = r1 * 2;  //錯誤:r4是普通的非常量引用
    const int &r5 = 3.14;   //正確,double類型可轉化爲 const int 類型
4.2 const和指針

**指向常量的指針(pointer to const):**不能用於改變所指對象的值。要想保存常量對象的地址,只能使用指向常量的指針。但指向常量的對象可以指向一個非常量的對象。

    const double pi = 3.14;   //pi是常量,值不能改變
    double *ptr = &pi;   //ptr是個普通指針,報錯:invalid conversion from ‘const double*’ to ‘double*’
    const double *cptr = &pi;  //正確
    *cptr = 3.1415926;   //cptr指向常量,報錯:assignment of read-only location ‘* cptr’
    double dval = 10.24;
    cptr = &dval;  //正確,指向常量的指針可以指向一個非常量對象

const 指針:和引用不同,指針本身是對象,所以指針本身可以是常量,即常量指針(const pointer)。和一般 const 對象一樣,常量指針也必須初始化,而且一旦初始化,值就不能再改變。

*放在const關鍵字之前,用以說明指針是一個常量,指明不變的是指針本身的值而非指向的那個值。

    int errNumb = 0;
    int *const curErr = &errNumb;  //curErr將一直指向errNumb
    const double pi = 3.14;
    const double *const pip = &pi; //pip是一個指向常量對象的常量指針

如之前變量聲明所述,聲明的含義應該從右往左讀:*const curErr是一個整體,可以理解爲一個聲明符。離變量名curErr最近的是符號是const,代表curErr本身是一個常量對象;其次是符號*,說明curErr是一個指針,合起來指明curErr是一個常量指針。最後,基本數據類型部分確定了curErr指向一個 int 對象。
按這樣的套路來分析pip:聲明符整體說明pip是一個常量指針;基本數據類型部分確定了pip指向一個 double 類型的常量。最終,我們確定,pip是一個指向常量的常量指針。

4.3 頂層/底層 const
  • **頂層const:**對象本身是常量,如const pointer
  • **底層const:**指針、引用所指的對象是一個常量

指針本身是一個對象,指針既可以是頂層 const 也可以是底層 const;引用本身不是對象,因此引用只能是底層 const。

爲什麼要區分頂層/底層 const ?
在執行對象的拷貝操作時,頂層 const 不受什麼影響(特指拷貝的對象是頂層 const,拷入的對象當然不能是頂層 const)。
另一方面,當拷貝對象時,拷入和拷出的對象必須具有相同的底層 const
資格,或兩個對象的數據類型可以轉換
。通常,非 const 可以轉換成 const,反之則不可。

    int i = 0;
    const int *const p = &i;  // 正確,非const可以轉換成const
    int *p1 = p;     // 報錯:p包含底層const的定義,而p1沒有
    const int *p2 = p;   //正確,p和p2都是底層const,p的頂層const 部分不影響
    const int ci = 42;    //頂層const
    int &r = ci;          //非const不能綁定到const上
    const int &r2 = i;    //正確,非const可以轉換成const
4.4 constexpr 和常量表達式

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

字面值是常量表達式,用常量表達式初始化的 const 對象也是常量表達式。

5、處理類型

5.1 類型別名

兩種方式:
  (1). C風格方式
    typedef 舊類型名 新類型聲明符([修飾符]類型名);
    如:

typedef double wages;   // wages 是 double 的同義詞
typedef wages base, *p; // base 是 double 的同義詞,p是 double * 的同義詞

  (2). 新標準方式
    using 新類型 = 舊類型;

注意:
複合類型的別名的理解方式,如下代碼:

typedef char *pstring;
const pstring cstr = 0;
const pstring *ps;

typedefpstring指定爲指向 char 的指針類型的別名。因此,在後面使用過程中,pstring就應該始終被理解爲指針類型。把類型別名機械地進行文本替換,這種理解方式是錯誤的
類比const int a;,聲明瞭int型的常量a,意味着a是int類型,它本身是不變的;同理,const pstring cstr;意味着cstr是pstring類型(指向char的指針),且本身是不變的,也就是說,這條語句指明cstr是一個指向char的常量指針。
類似地,ps是一個指向常量的指針,ps指向一個char型常量指針。

    typedef int *pstring;  //pstring是指針類型的別名
    const pstring *ps;    //ps是指向常量指針的指針

    int i = 42;
    int *p = &i;
    ps = &p;             //非const可以轉換成const
    cout << "p=" << p << endl;           //p=0x7fff6fd55944
    cout << "*ps=" << *ps << endl;     //*ps=0x7fff6fd55944

    int j = 55;
    int *const q = &j;
    ps = &q;          //ps可以指向另一個對象,說明ps本身不是const的
    cout << "q=" << q << endl;         //q=0x7fff6fd55934
    cout << "*ps=" << *ps << endl;   //*ps=0x7fff6fd55934

    *ps = &i;   //報錯:assignment of read-only location ‘* ps’
5.2 auto 類型說明符

C++新標準引入了auto類型說明符,可以讓編譯器通過初始值來推算變量的類型。因此,auto定義的變量必須有初始值。
auto會忽略頂層const (引用例外),保留底層const

auto i = 0, *p = &i;     //推算出i是整數,p是整型指針
auto sz = 0, pi = 3.14;   //錯誤,sz和pi類型不一致
const int ci = i, &cr = ci;
auto b = ci;     //b是整數,ci的頂層const被忽略
auto c = cr;     //c是整數,cr是ci的別名,ci的頂層const被忽略
auto d = &i;    //d是整型指針
auto e = &ci;   //e是指向整型常量的指針,對e而言,ci是底層const
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章