文章目錄
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 = π //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 = π //ptr是個普通指針,報錯:invalid conversion from ‘const double*’ to ‘double*’
const double *cptr = π //正確
*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 = π //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;
typedef
將 pstring
指定爲指向 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