C++ primer學習筆記——第二章 變數和基本型別
一、基本內建型別
C++基本內建型別包括算術型別和空型別。
1.算術型別
算術型別分為兩類:整形(包括字元和布林型在內)和浮點型
bool(布林型) 長度(位元組):1 取值範圍:false,true
char(字元型) 長度(位元組):1 取值範圍:-128~127
signed char(有符號字元型) 長度(位元組):1 取值範圍:-128~127
unsigned char(無符號字元型)長度(位元組):1 取值範圍:0~255
short(signed short) 長度(位元組):2 取值範圍:-32768~32767
unsigned short 長度(位元組):2 取值範圍:0~65535
int(signed int) 長度(位元組):4 取值範圍:-2147483648~2147483647
unsigned int 長度(位元組):4 取值範圍:0~4294967295
long(signed long) 長度(位元組):4 取值範圍:-2147483648~2147483647
unsigned long 長度(位元組):4 取值範圍:0~4294967295
float(浮點型,表示實數) 長度(位元組):4 取值範圍:3.4*10^-38~3.4*10^38
double(雙精度浮點型) 長度(位元組):8 取值範圍:1.7*10^-308~1.7*10^308
long double 長度(位元組):8 取值範圍:1.7*10^-308~1.7*10^308
與整型不同,字元型被分為了三種:char、signed char、unsigned char。儘管字元型有三種,但是字元的表現形式卻只有兩種:帶符號的和無符號的。型別char實際上會表現為上述兩種形式的一種,具體是哪種由編譯器決定。
2.型別轉換
bool b=42; //b為true
int i=b; //i的值為1
i=3.14; //i的值為
double pi=i; //pi=3.0
unsigned char c=-1; //假設c為8位元,c=256
signed char c2=256; //c2的值未定義
當我們賦予無符號型別一個超出它表示範圍的值時,結果是初始值對無符號型別表示數值總數取模後的餘數。
當我們賦予帶符號型別一個超出它表示範圍的值時,結果是未定義的。
unsigned u=10;
int i=-42;
std::cout<<i+i<<std::endl; //輸出-84
std::cout<<u+i<<std::endl; //如果int佔32位,輸出4294967264(i等於2的32次方減42)
如果表示式裡既有帶符號型別又有無符號型別,當帶符號型別取值為負時會出現異常,這是因為帶符號數會自動地轉換為無符號數。
把負數轉換為無符號數類似於直接給無符號數賦一個負值,結果等於這個負值加上無符號數的模。
3.字面值常量
一個形如42的值被稱為字面值常量,這樣的值一望而知。每個字面值都對應一種字元型別,字面值常量的形式和值決定了它的資料型別。
整形和浮點型字面值:
以0開頭的代表八進位制數,以0x或0X開頭的代表十六進位制——20/*十進位制*/ 024/*八進位制*/ 0x14/*十六進位制*/
十進位制字面值的型別是int、long、long long中尺寸最小的那個,當然前提是這種型別要能容納下當前的值。short沒有對應的字面值。
儘管整形字面值可以儲存在帶符號資料型別中,但嚴格來說,十進位制字面值不會是負數。例如-42,那個負號並不在字面值之內,它的作用僅僅是對字面值取負值而已。
預設的,浮點型字面值是一個double。
字元和字串字面值:
'a' //字元字面值
"hello world" //字串字面值
字串字面值的實際型別是由常量字元構成的陣列,編譯器在每個字串的結尾處新增一個空字元(‘\0’),因此,字串字面值的實際長度要比它的內容多1。
轉義序列
換行符 \n | 橫向製表符 \t | 報警(響鈴)符 \a |
縱向製表符 \v | 退格符 \b | 雙引號 \" |
反斜符 \\ | 問號 \? | 單引號 \' |
回車符 \r | 進紙符 \f |
在程式中,上述轉義序列被當做一個字元使用。
指定字面值的型別:
字首 | 含義 | 型別 |
---|---|---|
u | Unicode16字元 | char16_t |
U | Unicode32字元 | char32_t |
L | 寬字元 | wchar_t |
u8 | UTF-8(僅用於字串字面值常量) | char |
字尾 | 型別 |
u or U | unsigned |
l or L | long |
ll or LL | long long |
f or F | float |
l or L(浮點型) | long double |
布林型和指標型字面值:
true和false是布林型的字面值。
nullptr是指標字面值。
二、變數
變數提供一個具名的、可供程式操作的儲存空間。
對C++程式設計師來說,“變數”和“物件”一般可以互換使用。
1.變數定義
變數的定義並初始化:
double price=109.99,discount=peice*0.16;
double salePrice=applyDiscount(price,discount);
特別注意:初始化不是賦值,初始化的含義是建立變數時賦予其一個初始值,而賦值的含義是把物件的當前值擦除,而已一個新值來代替。
列表初始化:
int units_sold=0;
int units_sold={0};//列表初始化
int units_sold{0};//列表初始化
int units_sold(0);
當用於內建型別的變數時,這種初始化形式有一個重要特點:如果我們使用列表初始化且初始值存在丟失資訊的風險時,編譯器將報錯:
long double ld=3.1415926536;
int a{ld},b={ld};//錯誤:轉換未執行,因為存在丟失資訊的風險
int c(ld),d=ld;//正確:轉換執行,且確實丟失了部分值
預設初始化:
1.定於任何函式體之外的變數都被初始化0;
2.定義於函式體內部的內建型別的物件如果沒有初始化,則其值未定義。
3.類的物件如果沒有顯式的初始化,則其值由類確定
提示:未初始化變數可能引發執行時故障,建議初始化每一個內建型別的變數。雖然並非必須這麼做,但如果我們不能確保初始化後程序安全,那麼這麼做不失為一種簡單可靠的方法。
變數宣告和定義的關係:
如果想宣告一個變數而非定義它,就在變數名前新增關鍵字extern,而且不要顯示地初始化變數:
extern int i;//宣告i
int j;//宣告並定義j
任何包含了顯示初始化的宣告即成為定義:
extern double pi=3.1416;//定義
變數的定義必須出現在且只能出現在一個檔案中,而其他用到該變數的檔案必須對其進行宣告,卻絕對不能重複定義。
3.識別符號(命名規則)
①識別符號由字母、數字和下劃線組成,其中必須以字母或下劃線開頭
②一些關鍵字和操作符替代名不能用作識別符號
③使用者自定義的識別符號中不能連續出現兩個下劃線,也不餓能以下劃線緊連著大寫字母開頭
4.名字的作用域
作用域都以花括號{ }分割。
全域性作用域、塊作用域
巢狀的作用域:內層作用域、外層作用域
作用域中一旦聲明瞭某個名字,它所巢狀著的所有作用域中都能訪問該名字。同時,允許在內層作用域中重新定義外層作用域已有的名字:
#include <iostream>
int reused=42;
int main()
{
int unique=0;
std::cout<<reused<<" "<<unique<<std::endl;//輸出42 0
int reused=0;
std::cout<<reused<<" "<<unique<<std::endl;//覆蓋全域性變數,輸出0 0
std::cout<<::reused<<" "<<unique<<std::endl;//顯式地訪問全域性變數reused,輸出 42 0
return 0;
}
注:如果函式有可能用到某全域性變數,則不宜再定義一個同名的區域性變數。
三、複合型別
複合型別是基於其他型別定義的型別。
1.引用
通過將宣告符寫成&d的形式來定義引用型別,其中d為宣告的變數名。
int ival=1024;
int &refVal=ival; //reval指向ival(是ival的另一個名字)
int &refVal2; //錯誤:引用必須被初始化
定義引用時,程式把引用和它的初始值繫結在一起,而不是將初始值拷貝給引用。一旦初始化完成,引用將和它的初始值物件一直繫結在一起。因為無法令引用重新繫結在另外一個物件,因此引用必須初始化。
引用並非物件,相反的,它只是為一個已經存在的物件所起的另外一個名字。對引用的操作本質上都是對引用繫結的物件操作。
因為引用本身不是一個物件,所以不能定義引用的引用。
引用的定義:
①允許在一條語句中定義多個引用,其中每個引用識別符號都必須以符號&開頭;
②一般的,所有引用的型別都要和與之繫結的物件嚴格匹配;
③引用只能繫結到物件上,而不能與字面值或某個表示式的計算結果繫結在一起
int &refVal4=10; //錯誤,引用型別的初始值必須是一個物件
double dval=3.14;
int &refVal5=dval;//錯誤,此處引用型別的初始值必須是int型物件
2、指標
指標與引用都是符合型別,實現了對其他物件的間接訪問。
兩者不同點:
①指標本身就是一個物件,允許對指標賦值和拷貝,而且在指標的宣告週期內它可以先後指向幾個不同的物件。
②指標無需在定義時賦初值
int *ip1,*ip2; //ip1和ip2都是int型指標
double dp,*dp2; //dp是double型物件,dp2是double型指標
指標存放某個物件的地址,想要獲取該地址,需要使用取地址符(操作符&):
int ival=42;
int *p=&ival;
因為引用不是物件,沒有實際地址,所以不能定義指向引用的指標。
所有指標的型別都要和它所指向的物件嚴格匹配:
double dval;
double *pd=&dval;
double *pd2=pd;
int *pi=pd; //錯誤
pi=&dval; //錯誤
如果指標指向了一個物件,則允許使用解引用符(操作符*)來訪問該物件。
解引用操作僅適用於那些確實指向了某個物件的有效指標。
空指標:三種生成空指標的方法
int *p1=nullptr;
int *p2=0;
//需要首先#include cstdlib
int *p3=NULL;
把int變數直接賦給指標是錯誤的操作,即使int變數的值恰好等於0也不行:
int zero=0;
pi=zero; //錯誤,不能把int變數直接賦給指標
指標的賦值:
pi=&ival; //改變了指標本身的值
*pi=0; //改變了指標所指物件的值
指標與布林型:
任何非0指標對應的條件值都是true。
void*指標:
void*是一種特殊的指標型別,可用於存放任意物件的地址。
利用void*指標能做的事情比較有限;拿它和別的指標比較、作為函式的輸入與輸出、或者賦給另外一個void*指標。不能直接操作void*指標所指的物件,因為我們並不知道這個物件到底是什麼型別,也就無法確定能在這個物件上做哪些操作。
3.複合變數的宣告
一條定義語句可能定義出不同型別的變數:
int i=1024,*p=&i,&r=i;
基本資料型別是int而非int*。*僅僅是修飾了p而已,對該宣告語句中的其他變數,它並不產生任何作用。
指向指標的指標:
**表示指向指標的指標,***表示指向指標的指標的指標:
int ival=1024;
int *pi=&ival;
int **ppi=π
指向指標的引用:
引用本身不是一個物件,因此不能定義指向引用的指標。但指標是物件,所以存在對指標的引用:
int i=42;
int *p;
int *&r=p; //r是一個對指標p的引用
r=&i; //r引用了一個指標,因此給r賦值&i就是令p指向i
*r=0; //解引用r得到i,也就是p指向的物件,將i的值改為0
要理解r的型別到底是什麼,最簡單的辦法是從右向左閱讀r的定義。離變數名最近的符號對變數的型別有最直接的影響,因此r是一個引用。宣告符的其餘部分可以確定r的引用的型別是什麼,此例中的符號*說明r引用的是一個指標。最後,宣告的基本資料型別部分支出r引用的是一個int指標。
四、const限定符
const將變數限定成常量,其值不可改變。
因為const物件一旦建立後其值就不能再改變,所以const物件必須初始化。
只能在const型別的物件上執行不改變其內容的操作
編譯器將在編譯過程中把用到該變數的地方都替換成對應的值。
預設情況下,const物件被設定成僅在檔案內有效,當多個檔案中出現了同名的const變數時,其實等同於在不同檔案中分別設定了獨立的變數。
extern const int bufSize=fcn(); //file_1.cc定義並初始化了一個常量
extern const int bufSize; //與file_1.cc中定義的bufSize是同一個
1.const的引用
對const常量物件的引用必須是const常量,且與普通引用不同的是,對常量的引用不能被用作修改它所繫結的物件:
const int ci=1024;
const int &r1=ci; //正確:引用及其對應的物件都是常量
r1=42; //錯誤:r1是對常量的引用,不能修改繫結的物件
int &r2=ci; //錯誤:試圖讓一個非常量引用指向一個常量物件
初始化常量時允許用任意表達式作為初始值,只要該表示式的結果能轉換成引用的型別即可。尤其,允許為一個常量引用繫結非常量的物件、字面值,甚至是個一般表示式:
int i=42;
const int &r1=i; //正確
const int &r2=42; //正確
const int &r3=r1*2; //正確
int &r4=r1*2; //錯誤,r4是一個普通的非常量引用
const的引用可能引用一個非const的物件,但是不允許通過引用改變所繫結物件的值。
int i=42;
int &r1=i;
const int &r2=i;
r1=0;
r2=0; //錯誤
2、指標與const
以下幾點與常量引用類似:
①想要存放常量物件的地址,只能使用指向常量的指標
②允許令一個指向常量的指標指向一個非常量物件,但該指標不能用於改變其所指物件的值。
const指標:
指標是物件而引用不是,因此就像其他物件型別一樣,允許把指標本身定位常量。常量指標必須初始化,而且一旦初始化完成,它的值(存放在指標中的那個地址)就不能再改變了。把*放在const關鍵字之前用以說明指標是一個常量,這樣書寫隱含著一層意味,即不變的是指標本身而非指向的那個值:
int erroNumb=0;
int *const curErr=&errNumb;
const double pi=3.1415926;
const double *const pip=π //pip是一個指向常量物件的常量指標
指標本身是一個常量並不意味著不能通過指標修改其所指物件的值,能否這樣做完全依賴於所指物件的型別。例如,pip是一個指向常量的常量指標,則不論是pip所指的物件值還是pip自己儲存的那個地址都不能改變。相反的,curErr指向的是一個一般的非常量指標,那麼就完全可以用currErr去修改errNumb的值。
總結:
①指向常量的指標或常量引用前必須加const;
②const引用繫結的物件、指向常量的指標的物件也可以非常量物件,但不允許通過其改變所指物件的值;
③允許為一個常量引用繫結非常量的物件、字面值,甚至是個一般表示式,而非常量引用只能繫結到物件上,而不能與字面值或某個表示式的計算結果繫結在一起
④常量指標也可以指向非常量,不變的是指標本身的值而不是指向的那個值
⑤常量指標和常量不能被賦值,只能初始化
3.頂層const
頂層const表示物件本身是個常量,底層const表示指標或引用所指的是個常量。用於宣告引用的const都是底層const
int i=0;
int *const p1=&i; //頂層const
const int ci=42; //頂層const
const int *p2=&ci; //允許改變p2的值,底層const
const int *const p3=p2; //p3既是頂層const(右半部分),又是底層const(左半部分)
const int &r=ci; //用於宣告引用的const都是底層const
當拷貝物件時,頂層const不受影響。另一方面,底層const的限制卻不能忽略。當執行物件的拷貝操作時,拷入和拷出的物件必須具有相同的底層const資格,或者兩個物件的資料型別必須能夠轉換。一般來說,非常量可以轉換為常量,反之則不行。
i=ci; //正確,ci是一個頂層const,對此操作無影響
p2=p3; //正確,p2和p3指向的物件型別相同,p3頂層const的部分不影響
int *p=p3; //錯誤:p3包含底層const的定義,而p沒有
p2=p3; //正確:兩者都是底層const
p2=&i; //正確:int *能轉換成const int*
int &r=ci; //錯誤:普通的int&不能繫結到int常量上
const int &r2=i; //正確:const int&可以繫結到一個普通int上
4.常量表達式
常量表達式是指值不會改變並且在編譯過程中就能得到計算結果的表示式。
顯然,字面值屬於常量表達式,用常量表達式初始化的const物件也是常量表達式。
一般來說,如果認定變數是一個常量表達式,那就把它生命成constexpr型別。宣告成constexpr的變數一定是一個常量,而且必須用常量表達式初始化。
字面值型別:
型別一般比較簡單,值也顯而易見、容易得到。
算術型別、引用和指標都屬於字面值型別。
指標和constexpr:
在constexpr宣告中如果定義了一個指標,限定符constexpr僅對指標有效,與指標所指的物件無關(constexpr把它所定義的物件置為了頂層const):
const int *p=nullptr; //p是一個指向整型常量的指標
constexpr int *q=nullptr; //q是一個指向整數的常量指標
與其他常量指標類似,constexpr指標既可以指向常量也可以指向一個非常量。
五、處理型別
1.類型別名
類型別名是一個名字,它是某種型別的同義詞。
兩種方法定義:
typedef double wages;
typedef wages base,*p; //base是double的同義詞,p是double*的同義詞
using SI=Sales_items;
指標、常量和類型別名:
typedef char *pstring;
const pstring cstr=0; //cstr是指向char的常量指標
const pstring *ps; //ps是一個指標,它的物件是指向char的常量指標
pstring實際上是指向char的指標,因此,const pstring就是指向char的常量指標,而非指向常量字元的指標。
2.auto型別說明符
auto能讓編譯器替我們去分析表示式所屬的型別。
使用auto也能在一條表示式中宣告多個變數。因為一條宣告語句只能有一個基本資料型別,所以該語句中所有變數的初始基本資料型別都必須一樣。
複合型別、常量和auto:
當引用被用作初始值時,真正參與初始化的其實是引用物件的值,此時編譯器以引用物件的型別作為auto的型別:
int i=0,&r=i;
auto a=r; //a是一個整數
其次,auto一般會忽略掉頂層const,同時底層const會保留下來,比如當初始值是一個指向常量的指標時:
const int ci=i,&cr=ci;
auto b=ci; //b為int型別 (ci的頂層const特性被忽略了)
auto c=cr; //c為int型別(cr是ci的別名,ci本身是一個頂層const)
auto d=&i; //d為int*型別
auto e=&ci; //e為const int*型別(對常量物件取地址是一種底層const)
如果希望推斷出的auto型別是一個頂層const,需要明確指出:
const auto f=ci;
設定一個型別為auto的引用時,初始值中的頂層常量屬性仍然保留。和往常一樣,如果我們給初始值繫結一個引用,此時的常量就不再是頂層常量了。
auto &g=ci; //g是一個整形常量引用,繫結到ci
auto &h=42; //錯誤:不能為非常量引用繫結字面值
const auto &j=42; //正確:可以為常量引用繫結字面值
要在一條語句中定義多個變數,符號&和*只從屬於某個宣告符,而非基本資料型別的一部分,因此初始值必須是同一種類型。
3.decltype型別指示符
decltype的作用是選擇並返回運算元的資料型別。
如果decltype使用的表示式是一個變數,則decltype返回該變數的型別,包括頂層const和引用在內:
const int ci=0,&cj=ci;
decltype(ci) x=0; //x的型別為const int
decltype(cj) y=0; //y的型別為const int&,y繫結到變數x
decltype(cj) z; //錯誤:z是一個引用,必須初始化
decltype和引用:
int i=42,*p=&i,&r=i;
decltype(r+0) b; //正確:加法的結果是int而非int&,因此b是一個int
decltype (*p) c; //錯誤:c是int&,必須初始化
decltype((i)) d; //錯誤:d是int&,必須初始化
decltype(i) e; //正確:e是一個未初始化的int
當引用是表示式的一部分,decltype這個表達的結果是一個具體值而非一個引用。
如果表示式的內容是解引用操作,則decltype將得到引用型別。
decltype((variable))(注意是雙層括號)的結果永遠是引用,而decltype(variable)結果只有當variable本身就是一個引用時才是引用。
六、自定義資料結構
資料結構是把一組相關的資料元素組織起來然後使用它們的策略和方法,C++允許使用者以類的形式自定義資料結構。
定義Sales_data類:
struct Sales_data{/*...*/};
Sales_data accum,trans;
類定義的最後有分號,不要忘記加。
最好不要把物件的定義和類的定義放在一起。
類的資料成員:
類的資料成員定義了類的物件的具體內容,每個物件有自己的一份資料成員拷貝,修改一個物件的資料成員,不會影響其他Sales_data的物件。
定義資料成員的方法和定義普通變數一樣。
新標準規定,可以為資料成員提供一個類內初始值。建立物件時,類內初始值將用作初始化資料成員。沒有初始值的成員將被預設初始化。
標頭檔案:
為了確保各個檔案中類的定義一致,類通常被定義在標頭檔案中,而且類所在標頭檔案的名字應該與類的名字一樣。
標頭檔案一旦改變,相關的原始檔必須重新編譯以獲取更新過的宣告。
前處理器:
#include:當前處理器看到#include標記時就會用指定的標頭檔案的內容代替#include
標頭檔案保護符:
#define指令把一個名字設定成預處理變數,#ifdef當且僅當變數已定義時為真,#ifndef當且僅當變數未定義時為真。一旦檢查結果為真,則執行後續操作直至遇到#endif指令為止。
預處理變數無視C++語言中關於作用域的規則。
為了避免與程式中的其他實體發生名字衝突,一般把預處理變數的名字全部大寫。