1、C++ 基礎(chǔ)-變量和基本類型 ——Primer C++

基本內(nèi)置類型

  • 算術(shù)類型
    • 字符
    • 整型
    • 布爾值
    • 浮點數(shù)
  • 空類型(void)

算術(shù)類型

類型 說明 最小尺寸 測試尺寸
bool 布爾類型(true、false) 未定義 1 Byte
char 字符 1 Byte 1 Byte
wchar_t 寬字符 2 Byte 4 Byte
char16_t Unicode 字符 2 Byte 未測試
char32_t Unicode 字符 4 Byte 未測試
short 短整型 2 Byte 2 Byte
int 整型 2 Byte 4 Byte
long 長整型 4 Byte 8 Byte
long long 長整型(C++ 11) 8 Byte 8 Byte
float 單精度浮點數(shù) 6位有效數(shù)字 4 Byte,一般7位有效數(shù)字
double 雙精度浮點數(shù) 10位有效數(shù)字 8 Byte,一般16位有效數(shù)字
long double 擴展精度浮點數(shù) 10 位有效數(shù)字 16 Byte,具體實現(xiàn)不同精度不同
  • 帶符號類型和無符號類型

    • int、short、long、 long long都是帶符號的,通過類型前面添加unsigned 得到無符號類型,如 unsigned int,可以縮寫為unsigned。
    • char 類型 帶符號的 signed char 和不帶符號的unsigned char ,char 類型具體為哪一種和編譯器有關(guān)。
    • 無符號類型所有bit 都被用來表示數(shù)字,有符號的最高位為符號位
  • 如何選擇類型

    • 當數(shù)值不可能為負時 ,選用無符號類型
    • 使用int執(zhí)行整數(shù)運算。
    • 算術(shù)表達中不要 使用char 或bool,只有在存放字符或布爾值才使用。如果需要一個不大的整數(shù),明確指出類型是signed char 還是unsigned char
    • 執(zhí)行浮點運算使用double,因為float精度不夠而且單雙精度浮點運算代價相差無幾。

類型轉(zhuǎn)換淺談

當在程序中我們使用一種類型而其實對象應該取另一種類型時,程序會自動進行類型轉(zhuǎn)換,當我們像下面這樣把一種算術(shù)類型的值賦給另外一種類型時:

bool b=42; //b 位true
int i= b; //i 為1
i =3.14 ; //i 為3
double pi=i ;//pi 為3.0
unsigned char c =-1; //char 占一個字節(jié)時,c 值為255
signed char c2=256 ;//char 占一個字節(jié)時, c 未定義

類型所能表示的值得范圍決定了轉(zhuǎn)換的過程:

  • 非布爾類型賦值給布爾類型時,初始值為0 ,結(jié)果為false ,否則結(jié)果為true。
  • 布爾類型賦值給非布爾類型時,初始值為false ,結(jié)果為0,初始值為true 則結(jié)果為1。
  • 浮點數(shù)賦值給整型時,將僅保留浮點數(shù)中小數(shù)點之前的整數(shù)部分。
  • 整數(shù)賦值給浮點數(shù)時,小數(shù)部分記為0,若整數(shù)所占的空間超過浮點類型的容量,精度可能損失。
  • 當我們賦給無符號類型一個超出它表示范圍的值時,結(jié)果是初始值對無符號類型表示數(shù)值總數(shù)取模后的余數(shù)。
  • 當我們賦給帶符號類型一個超出它表示范圍的值時,結(jié)果是未定義的。

含有無符號類型的表達式:

  • 盡管我們不會故意給無符號對象賦一個負值,卻可能寫出這樣的代碼
  • 當一個表達式中既有無符號數(shù)又有int值時,int值會被轉(zhuǎn)為為無符號數(shù)。
unsigned u=10;
int i= -42;
std::cout <<i+i<<std::endl; //-84
std::cout <<u+i<<std::endl; //如果int 占32 位,輸出4294967264

注意: 不要混用帶符號類型和無符號類型

字面值常量

什么是字面值常量? 形如42這樣的值就是,一看就知道是多少,每種字面值常量對應一種數(shù)據(jù)類型,字面值常量的形式和值決定了它的數(shù)據(jù)類型。

  • 整型和浮點型字面值
    整型字面值可以寫作十進制數(shù),八進制數(shù)或者16進制的形式,以0開頭的代表八進制數(shù),以0x或0X開頭的代表十六進制數(shù)。

    • 十進制字面值的類型是int ,long,long long,中尺寸最小的那個(例如,三者當中最小是int),當然前提是這種類型要能容納下當前的值。
    • 八進制和十六進制字面值類型是能夠容納其數(shù)值的int,unsigned int,long ,unsigned long, long long和unsigned long long 中的最小者。如果一個字面值連與之相連的最大數(shù)據(jù)類型都放不下,將產(chǎn)生錯誤。
    • 盡管整型字面值可以存儲在帶符號數(shù)據(jù)類型中,但嚴格來說,十進制字面值不會是負數(shù),它的作用僅僅是對字面值取負值而已。
    • 浮點型字面值是一個double
  • 字符和字符串字面值
    由單引號括起來的一個字符稱為char型字面值,雙引號括起來的0個或多個字符則構(gòu)成字符串型字面值。
    'a' //字符字面值
    “hello world!” //字符串字面值
    字符串字面值的類型實際上是由常量字符構(gòu)成的數(shù)組(array),編譯器在每個字符串的結(jié)尾處添加一個空字符('\0')

  • 轉(zhuǎn)義序列
    有兩類字符程序員不能直接使用:一類是不可打印字符,如退格或其他控制字符,因為他們沒有可視的圖符,另一類是語言中有特殊含義的字符,(單引號,雙引號,問好,。。)。在這些情況下需要用到轉(zhuǎn)義序列:

名稱 形式 名稱 形式 名稱 形式
換行符 \n 橫向制表符 \t 報警(響鈴)符 \a
縱向制表符 \v 退格符 \b 雙引號 "
反斜線 |問號 ? 單引號 \’
回車符 \r 進紙符 \f
在程序中,轉(zhuǎn)義序列被當做一個字符使用。
  • 指定字面值的類型

字符和字符串字面值

前綴 含義 類型
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
ex:
    L'a' // 寬字符型字面值,類型時wchar_t
    u8"hi!" //utf-8 用8位編碼一個unicode 字符
    42ULL  //無符號整型字面值,類型是unsigned long long 
    1E-3F  // 單精度浮點型字面值,類型是float

變量

變量對應一個可供操作的存儲空間,C++中的每個變量都有其數(shù)據(jù)類型,數(shù)據(jù)類型決定著變量所占內(nèi)存空間的大小和布局方式,該空間能存儲的值得范圍,以及變量能參與的運算。對C++程序員來說,“變量”和“對象”一般可以互換使用。

變量的定義

基本形式: 類型說明符 隨后緊跟一個或多個變量名組成的列表,以逗號分隔,分號結(jié)束定義。列表中每個變量名的類型都由類型說明符指定,定義時還可以為一個或多個變量賦初值:

int a=0,b,c=0; // abc 都為int 類型
Sales_item item; //item 的類型是sales_item 自定義類型
std::string book("0-12-33-x"); //book 通過一個string 字面值初始化,string 庫類型

初始值
當對象(變量)在創(chuàng)建時獲得了一個特定的值,我們說這個對象被初始化了。用于初始化變量的值可以是任意復雜的表達式。當一次定義了兩個或多個變量時,對象的名字隨著定義也就馬上可以使用了。因此在同一條定義語句中,可以用先定義的變量值去初始化后定的其他變量。
在C++中,初始化是一個異常復雜的問題,很多程序員對于使用等號來初始化變量的方式倍感困惑,這種方式容易讓人認為初始化是賦值的一種。事實上,初始化和復制是兩個完全不同的操作,然而在很多編程語言中二者的區(qū)別幾乎可以忽略不計,即使在C++中有時這種區(qū)別也無關(guān)緊要,所以人們特別容易吧二者混為一談。但是,這是兩種不同的操作,這個概念很重要。

  • 初始化:創(chuàng)建變量時賦予其一個初始值。
  • 賦值: 把對象(變量)的當前值擦除,以一個新值來替代。

列表初始化
C++語言定義了初始化的好幾種不同形式,這也是初始化問題復雜性的一個體現(xiàn)。例如,要想定義一個名為units_sold 的int變量并初始化為0,以下的4條語句都可以做到這一點:

int units_sold = 0;
int units_sold = {0} ;
int units_sold{0};
int units_sold(0);

作為C++ 11 新標準用花括號來初始化變量得到了全面應用,而在此之前,這種初始化的形式僅在某受限的場合下才能使用。這種初始化的形式被稱為列表初始化?,F(xiàn)在無論是初始化對象還是某些時候為對象賦新值,都可以使用這樣一組由花括號括起來的初始值了。
當用于內(nèi)置類型的變量時, 這種初始化形式有一個重要特點:如果我們使用列表初始值存在丟失信息的風險,則編譯器將報錯:

long double ld =3.1415925;
int a{ld}, b={ld}; //錯誤: 轉(zhuǎn)換未執(zhí)行,因為存在丟失信息的危險
int c(ld), d=ld;      //正確: 轉(zhuǎn)換執(zhí)行,且確實丟失了部分值

使用long double 值初始化int變量時可能丟失數(shù)據(jù),所以編譯器拒絕了a 和 b 的初始化請求。其中,至少ld的小數(shù)部分會丟失掉,而且int也可能存不下ld的整數(shù)部分。

默認初始化
如果定義變量時沒有指定初值,則變量被默認初始化(default initialized),此時變量被賦予了“默認值”。默認值到底是什么由變量類型決定,同時定義變量的位置也會對此有影響。
如果是內(nèi)置類型的變量未被顯示初始化,它的值由定義的位置決定。定義任何函數(shù)體之外的變量被初始化為0。然而,一種例外情況是,定義在函數(shù)體內(nèi)部的內(nèi)置類型變量將不被初始化。一個未被初始化的內(nèi)置類型變量的值是未定義的,如果試圖拷貝或以其他形式訪問此類值將引發(fā)錯誤。
每個類各自決定其初始化對象的方式。而且,是否允許不經(jīng)初始化就定義對象也由類自己決定。如果類允許這種行為,他將決定對象的初始值到底是什么。
絕大多數(shù)類都支持無需顯示初始化而定義對象,這樣的類提供了一個合適的默認值:

std::string empty; //empty 非顯式地初始化為一個空串
Sales_item item; // 被默認初始化的Sales_item 對象

一些類要求每個對象都顯式初始化,此時如果創(chuàng)建了一個該類的對象而未對其做明確的初始化操作,將引發(fā)錯誤。

變量聲明和定義的關(guān)系

為了允許把程序拆成多個邏輯部分來編寫,c++語言支持分離式編譯(separate compilation) 機制,該機制允許將程序分割為若干個文件,每個文件可被獨立編譯。
如果將程序分為多個文件,則需要在文件間共享代碼的方法。 例如,一個文件的代碼可能需要使用另一個文件中定義的變量。
為了支持分離式編譯,c++語言將聲明和定義區(qū)分開來,聲明使得名字為程序所知,一個文件如果想使用別處定義的名字則必須包含對那個名字的聲明。而定義負責與名字關(guān)聯(lián)的實體。
變量聲明規(guī)定了變量的類型和名字,在這一點上定義與之相同,但是除此之外,定義還申請存儲空間,也可能會為變量賦一個初始值。如果想聲明一個變量而非定義它,就在變量名前添加關(guān)鍵字 extern,而且不要顯式地初始化變量:

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

任何包含了顯式初始化的聲明即成為定義。我們能給由extern 關(guān)鍵字標記的變量賦一個初始值,但是這么做也就抵消了 extern 的作用。extern 語句如果包含初始值就不再是聲明,而變成定義了:

extern double pi =3.1416; // 定義

在函數(shù)體內(nèi)部,如果試圖初始化一個由extern 關(guān)鍵字標記的變量,將引發(fā)錯誤。
變量能且只能被定義一次,但是可以被多次聲明。

注意:c++是一種靜態(tài)類型語言,含義是在編譯階段檢查類型,其中,檢查類型的過程稱為類型檢查。

標識符

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

// 定義4個不同的int 變量
int somename , someName ,SomeName,SOMENAME; 

用戶自定義的標識符中不能連續(xù)出現(xiàn)兩個下劃線,也不能以下劃線緊連大寫字母開頭。此外,定義在函數(shù)體外的標識符不能以下劃線開頭。

變量命名規(guī)范

  • 標識符要能體現(xiàn)實際含義
  • 變量名一般用小寫字母,如 index,不要使用Index 或INDEX
  • 用戶自定義的類名一般以大寫字母開頭,如Sale_item
  • 如果標識符由多個單詞組成,則單詞之間應有明顯區(qū)分,如 student_loan 或 studenLoan ,不要使用studentloan

名字的作用域

不論是在程序的什么位置,使用到的每個名字都會指向一個特定的實體: 變量、函數(shù)、類型等。然而,同一個名字如果出現(xiàn)在程序的不同位置,也可能指向的是不同的實體
作用域是程序的一部分,在其中名字有其特定的含義。C++語言中大多數(shù)作用域都以花括號分隔。
同一個名字在不同的作用域中可能指向不同的實體,名字的有效區(qū)域始于名字的聲明語句,以聲明語句所在的作用域末端為結(jié)束

#include <iostream>
int main()
{
    int sum=0;
    //sum 用于存放從1 到10 所有數(shù)的和
    for(int val =1 ;val <=10;++val)
    sum+=val; //等價于sum =sum+val
    
    std::cout <<"Sum of 1 to 10 inclusive is "
              <<sum <<std::endl;
    return 0;
}

這段程序定義了3個名字:main、sum 和 val ,同時使用了命名空間名字std,該空間提供了2個名字cout 和 cin 供程序使用。
名字main 定義所有花括號之外,他和其他大多數(shù)定義在函數(shù)體之外的名字一樣擁有全局作用域,一旦聲明之后,在整個程序的范圍內(nèi)都可以使用。名字sum定義在mian函數(shù)所限定的作用域之內(nèi),從聲明sum開始直到main函數(shù)結(jié)束為止都可以訪問它,但是除了main函數(shù)所在的塊就無法訪問了,因此說變量sum擁有塊作用域,名字val定義在for語句內(nèi),在for語句內(nèi)可以訪問val,但是在main函數(shù)的其它部分就不能訪問它了。
建議:第一次使用變量時再去定義它

嵌套的作用域
作用域能彼此包含,被包含(或者說被嵌套)的作用域稱為內(nèi)層作用域,包含著別的作用域的作用域稱為外層作用域。
作用中一旦聲明名了某個名字,它所嵌套著的所有作用域都能訪問該名字。同時,允許在內(nèi)層作用域中重新定義外層作用域已有的名字:

#include <iosteam>
//該程序僅用于說明:函數(shù)內(nèi)部不宜定義與全局變量同名的新變量
int reused = 42; //reused 擁有全局作用域
int main()
{
    int unique =0 ;//unique 擁有塊作用域
    std::cout <<reused <<" "<<unique <<std::endl;
    int reused =0;//新建局部變量reused
    std::cout <<reused <<" "<<unique <<std::endl;//局部變量
    std::cout <<::reused <<" "<<unique<<std::endl;
    return 0;
}

第一次輸出全局變量reused值,第二次輸出局部變量reused值,第三次輸出全局變量reused值。 因為全局作用域本身并沒有名字,所以,當作用域操作符的左側(cè)為空是,向全局作用域發(fā)出請求獲取作用于操作符右側(cè)名字對應的變量。
Note:如果函數(shù)有可能用到某個全局變量,則不宜再定義一個同名的局部變量

復合類型

復合類型 是指基于其他類型定義的類型。C++中有好幾種復合類型,本章將介紹其中的兩種: 引用和指針
與我們已經(jīng)掌握的變量聲明相比,定義復合類型的變量要復雜很多。

引用

引用為對象起了另外一個名字,引用類型引用(refers to)另外一種類型。通過將聲明符寫成&d的形式來定義引用類型,其中d就是聲明的變量名;

int ival =1024;
int &refval=ival; //refvel 指向ival(是ival 的另一種類型)
int &refval2; //錯誤: 應用必須被初始化

一般在初始化變量時,初始值就會被拷貝到新建的對象中,然而定義引用時,程序把引用和其初始值綁定在一起,而不是將初始值拷貝給引用,而且無法令引用重新綁定到另一個對象,因此引用必須初始化。

引用即別名:就是為變量新起了一個名字

引用的定義
允許在同一條語句內(nèi)定義多個引用,其中每個引用標識符都必須以符號&開頭

int i=1024,i2=2048; //i 和i2 都是int
int &r = i,r2= i2; //r 是一個引用,與 i綁定在一起,r2 int
int i3=1024,&ri=i3;
int &r3=i3,&r4 = i2;

引用的類型要和與之綁定對象嚴格匹配,引用只能綁定在對象上,不能與字面值或某個表達式的計算結(jié)果綁定在一起,

int &refval4=10; //error
double dval=22.22;
int &ff=dval; //error

指針

指針(pointer)是 "指向"另外一種類型的復合類型。與引用類似,指針也實現(xiàn)了對其他對象的間接訪問
指針與引用相比有很多不同點:

  • 指針是一個對象,允許對指針賦值和拷貝,而且在指針的生命周期內(nèi)可以先后指向多個不同的對象
  • 指針無需再定義時賦初值
  • 和其他內(nèi)置類型一樣,在塊作用域內(nèi)定義的指針如果沒有初始化,也將擁有一個不確定的值。

定義指著類型的方法將聲明符寫成d的形式,其中d是變量名。如果在一條語句定義了多個指針變量,則每個變量前面都必須有符號

int *p1,*p2; //聲明了兩個int型對象的指針
double dp,*dp2;//dp 是double 對象,dp2 是指向double類型的指針

獲取對象的地址
指針存放某個對象的地址,要想獲取該地址,需要使用取地址符(&)

int vial =42;
int *p=&vial; // p 存放變量ival 的地址

由于引用不是對象,沒有自己的地址,所以不能定義指向引用的指針。
指針的類型都要和其所指向的對象嚴格匹配,除了一些特殊情況,譬如 void * 。
指針的值應是下面幾種狀態(tài)之一:

  • 指向一個對象
  • 指向緊鄰對象所占空間的下一個位置
  • 空指針,沒有指向任何對象
  • 無效指針,上述情況之外的其他值

試圖拷貝或以其他方式訪問無效指針的值都將引發(fā)錯誤,編譯器并不負責檢查此類錯誤,這一點和試圖使用未經(jīng)初始化的變量是一樣的。訪問無效指針的后果無法對象。

利用指針訪問對象
如果指針指向了一個對象,則允許使用解引用符(*來操作對象:

int ival =42;
int *p =&ival; //p 存放著變量ival 的地址,或者說p 是指向變量ival 的指針
count << *p; //  42

對指針解引用將會得出所指的對象,因此如果將解引用的結(jié)果賦值,實際上也就是給指針所指的對象賦值:

*p = 0; //由符號*得到指針p 所指的對象,即可經(jīng)由 P為變量ival 賦值
count <<*p ; //0

由此可以得出: 某些符號具有多重含義,如 &

空指針
空指針 不指向任何對象,在試圖使用一個指針之前代碼可以首先檢查它是否為空。下面列出幾個生成空指針的方法:

int *p1 =nullptr; //c++ 新標準剛引入的一種方法,nullptr 是一種特殊類型的字面值??梢员晦D(zhuǎn)化成任意其他的指針類型
int *p2=0;
int *p3 = NULL; //需要 #include cstdlib 

初始化所有指針:使用未經(jīng)初始化的指針是引發(fā)運行時錯誤的一大原因

賦值和指針
指針和引用都能提供對其他對象的簡介訪問,然而在具體實現(xiàn)細節(jié)上二者有很大不同,其中最重要的一點就是引用本身并非一個對象。
有時候想要搞清楚一條賦值語句到底是改變了指針的值還是改變了指針所指對象的值不太容易,最好的辦法就是記住賦值永遠改變的是等號左側(cè)的對象。

pi=&val; // pi的值改變,pi 指向了ival
* pi =0; //ival 的值改變,指針pi并沒有改變

其他指針操作
只要指針擁有一個合法值,就能將他用在條件表達式中,和采用算術(shù)值作為條件遵循的規(guī)則類似,如果指針的值為0,條件取false。

void * 指針
void* 指針是一種特殊的指針類型,可用于存放任意對象的地址,一個 void* 指針存放著一個地址,這一點和其他指針類似,但不同的是,我們對該地址中到底是個什么類型的對象并不了解:

double obj =3.14, * pd=&obj;
void *pv=&obj; //void 指針類型可以存放任意類型的對象地址
pv=pd;

由于不知道void * 指針所存的對象的具體類型,所以無法直接操作void 指針所指的對象,從void * 的視角來看內(nèi)存空間也就僅僅是內(nèi)存空間,沒辦法訪問內(nèi)存空間中所存的對象。

理解復合類型的聲明

如前所述,變量的定義包括一個基本數(shù)據(jù)類型 (base type) 和一組聲明符。在同一條定義語句中,雖然基本數(shù)據(jù)類型只有一個,但是聲明符的形式卻可以不同。也就是說,一條定義語句可能定義出不同類型的變量:

// i int , p  int 型指針,r int 型引用
int i=1024,* p= &i, &r =i;

很多程序員容易迷惑于基本數(shù)據(jù)類型和類型修飾符的關(guān)系,其實后者不過是聲明符的一部分罷了。

定義多個變量
經(jīng)常有一種觀點會誤以為,在定義語句中,類型修飾符(* 或 &)作用與本次定義的全部變量。原因之一是由于可以把空格寫在類型修飾符和變量名之間:

int * p; //合法但是容易產(chǎn)生誤導

這種寫法可能產(chǎn)生誤導是因為int * 放在一起好像是這條語句中所有變量共同的類型一樣。其實恰恰相反,基本數(shù)據(jù)類型是int 而非 int * 。 * 僅僅是修飾了 P 而已,對該聲明語句中的其他變量,它并不產(chǎn)生任何副作用:

int * p1, p2; //p1 int 型指針,p2 int

涉及指針或引用的聲明,一般有兩種寫法,第一種把修飾符和變量標識符寫在一起:

int *p1, *p2; //強調(diào)變量具有的復合類型 

第二種把修飾符和類型名寫在一起,并且每條語句只定義一個變量:

int* p1;
int* p2; //著重強調(diào)本次聲明定義了一種復合類型; 

tips:上面兩種寫法沒有什么誰對誰錯,關(guān)鍵是選擇并堅持其中一種寫法,不要老是變來變?nèi)?/p>

指向指針的指針
一般來說, 聲明符中修飾符的個數(shù)并沒有限制,當有多個修飾符連寫在一起時,按照其邏輯關(guān)系加以解釋即可。以指針為例,指針是內(nèi)存中的對象,像其他對象一樣也有自己的地址,因此允許把指針的地址再存放到另一個指針當中。
通過*的個數(shù)可以區(qū)分指針的級別,也就是說,兩個表示指向指針的指針,三個表示指向指針的指針的指針,以此類推。

int ival=1024;
int *pi=&ival;
int **ppi=&pi;

指向關(guān)系如下:
ppi>>>pi>>>ival(1024)

cout <<"The Value of ival \n"
        <<"direct value :" <<ival <<"\n"
        <<"indirect value:" <<*pi<<"\n"
        <<"doubly indirect value: " << **ppi
        << endl;

上面使用三種不同方式輸出了變量ival的值。

指向指針的引用
引用本身不是一個對象,因此不能定義指向引用的指針,但是指針是對象,所以存在對指針的引用:

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

r =&i;  //r 引用了一個指針,因此給r賦值&i就是p指向i
*r =0; //*p=0,i=0

const 限定符

有時候我們希望定義這樣一種變量,它的值不能被改變。為了滿足這一要求,可以用關(guān)鍵字const 來對變量的類型加以限定。
const int buffsize =512 ; // 輸入緩沖區(qū)大小
這樣就把bufsize定義為了一個常量,任何試圖為bufsize 賦值的行為都將引發(fā)錯誤:

bufsize =512; //錯誤,試圖向const 對象寫值

因為const對象一旦創(chuàng)建后其值就不能在改變,所以const 對象必須初始化。一如既往,初始化可以是任意復雜的表達式:

const int i=get_size(); // 正確 運行時初始化
const int j=42; //編譯時初始化,正確
const int k; //error ,k 是一個未經(jīng)初始化的常量

初始化和const
正如之前所反復提到的,對象的類型決定了其上的操作。與非const 類型所能參與的操作相比,const類型的對象能完成其中大部分,但也不是所有的操作都適合,主要的限制就是只能在const 類型的對象上執(zhí)行不改變內(nèi)容的操作。 例如, const int 和普通的int一樣都能參與算術(shù)運算,也都能轉(zhuǎn)換成一個布爾值。。。。。。
注意: 默認情況下const 對象僅僅在本文件內(nèi)有效
當以編譯時初始化的方式定義一個const 對象時,就如對bufsize的定義一樣:
const int bufsize =512; // 輸入緩沖區(qū)大小
編譯器將在編譯過程中把用到的該變量的地方都替換成相應的值,也就是說,編譯器會找到代碼中所有用到bufsize的地方,然后用512 替換。
為了執(zhí)行上述轉(zhuǎn)換,編譯器必須知道變量的初始值,如果包含多個文件,就必須在每一個用到變量的文件中都有對它的定義。為了支持這一用法,同時避免對同一個變量的重復定義,默認情況下,const 對象被設(shè)定為僅在本文件內(nèi)有效。當多個文件中同時出現(xiàn)了同名的const 變量時,其實等同與在不同文件中分別定義了獨立的變量。

有時候有這樣一種const 變量,它的初始值不是一個常量表達式,但又確實有必要在文件間共享。這種情況下,我們不希望編譯器為每個文件分別生成獨立的變量。相反,我們想讓這種const 對象像其他(非常量)對象一樣工作,也就是說,只在一個文件內(nèi)定義const,而在其他多個文件中聲明并使用它。
解決的方法是:對于const 變量不管是聲明還是定義都要添加** extern ** 關(guān)鍵字,這樣只需要定義一次就可以了;

//file_1.cc 定義并初始化了一個常量,該常量能被其他文件訪問
extern const int bufsize =fcn();
// file_1.h 頭文件
extern const int bufsize ; //與上面定義的bufsize是同一個

如上述程序所示,file_1.cc 定義并初始化了bufsize,因為這條語句包含了初始值,所以它是一次定義,然而,因為bufsize 是一個常量,必須用extern 加以限定才能被其他文件使用。

file_1.h 頭文件中的聲明也由extern 做了限定,其作用是指明bufsize 并非本文件獨有,它的定義將在別處出現(xiàn)。
如果想要在多個文件之間共享const 對象,必須在變量的定義之前添加extern關(guān)鍵字

const 的引用

可以把引用綁到const 對象上,就像綁定到其他對象一樣,稱之為對常量的引用,與普通引用不同之處在于,對常量的引用不能用作修改它所綁定的對象: 就是說不能更改

const int ci =104;
const int &refci =ci ;
refci =42; //error 不能改
int &refci2 =ci;  //錯誤: 試圖讓一個非常量引用指向一個常量對象

常量引用是對const 的引用

初始化和對cosnt 的引用
前面所述,引用的類型必須與其所引用對象的類型一致植,但是有兩個例外

  • 第一種例外是在初始化常量引用時允許用任意表達式作為初始值,只要該表達式的結(jié)果能轉(zhuǎn)換成引用的類型即可,尤其允許為一個常量引用綁定非常量的對象,字面值或是個一般表達式
int i=42;
const int &r1  =i ;// 允許將const int& 綁定到一個普通int 對象上
const int &r2 =42; // ok
const int &r3 =r1*2; //ok
int &r4= r1* 32; //error 

要想理解這種例外情況的原因,最簡單的方式是弄清楚當一個常量引用綁定到另外一個類型上時到底發(fā)生了什么:

double dval =3.14; //由雙精度浮點數(shù)生成一個臨時的整型常量
const int &ri =dval;

此時ri 引用了一個int 型的數(shù),對ri的操縱應該是整數(shù)運算,但是dval 是一個雙精度浮點數(shù)而不是整數(shù),因此為了確保讓ri綁定一個整數(shù),編譯器把上述代碼變成了如下形式:

const int temp =dval ; //由 雙精度浮點數(shù)生成一個臨時的整型變量
const  int & ri =temp ;// ri 綁定這個臨時變量

在這種情況下,ri 綁定了一個臨時量對象,所謂臨時量 對象就是當編譯器需要一個空間來暫存表達式的求值結(jié)果時臨時創(chuàng)建的一個未命名的對象。
接下來談論當ri不是常量時,如果執(zhí)行了類似于上面的初始化過程將帶來什么樣的后果,如果ri不是常量,就允許對ri賦值,這樣就會改變ri所引用對象的值。注意,此時綁定的對象是一個臨時量而非dval。程序員既然讓ri引用dval,就肯定想通過ri改變dval的值,否則干什么要給ri賦值呢?如此看來,既然大家基本上不會想著把引用綁定到臨時變量上,C++ 語言也就把這種行為歸為非法。
對const 的引用可能引用一個并非const的對象
必須認識到,常量引用僅對引用可參與的操縱做出了限定,對于引用的對象本身是不是一個常量未做限定,因為對象也可能是個非常量,所以允許通過其他途徑改變它的值:

int i=42;
int &ri=i;
const int &r2 =i;
r1=0;
r2=0;  //錯誤,r2是一個而常量引用

指針和const

與引用一樣,類比常量引用,指向常量的指針不能用于改變所指對象的值,要想存放常量對象的地址,只能使用指向常量的指針:

const double pi =3.14; //pi 是個常量,它的值不能改變
double * ptr =&pi;  //錯誤
const double *cptr =&pi; //ok
* cptr =42; //error

如前文所說,指針的類型必須與其所指對象的類型一致,但是有兩個例外,第一種例外情況是允許另一個指向常量的指針指向一個非常量對象:

double dval=3.14;
cptr=&dval;                //right ,but can't modified dval by cptr;

和常量引用一樣,指向常量的指針也沒有規(guī)定其所指的對象必須是一個常量,所謂指向常量的指針僅僅要求不能通過該指針改變對象的值,而沒有規(guī)定那個對象的值不能通過其他途徑改變。
const 指針
指針是對象而引用不是,因此就像其他對象類型一樣,允許把指針本身定為常量,常量指針必須初始化,一旦初始化,它的值就不能在改變了,把*放在const關(guān)鍵字之前用來說明指針是一個常量,這樣的書寫形式意味著指針本身的值是不變的而并非指向的那個值:

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

頂層const

如前所述,指針本是是一個對象,它又可以指向另外一個對象,因此,指針本身是不是常量以及指針所指的是不是一個常量就是兩個相互獨立的問題,用名詞頂層const和底層const來分別表示這兩個問題:

  • 頂層const: 表示指針本身是個常量
  • 底層const: 表示指針所指的對象是一個常量

更一般的,頂層const可以表示任意的對象是常量,這一點對任何數(shù)據(jù)類型都適用,如算術(shù)類型,類、指針等。底層const則與指針和引用等復合類型的基本類型部分有關(guān)。比較特殊的是,指針類型既可以是頂層const 也可以是底層const,這一點和其他類型相比區(qū)別明顯:

int i=0;
int *const p1=&i;    //頂層const
const int ci =42;  //頂層const
const int *p2=&ci; //底層const
const int * const p3=p2;
const int &r =ci; //用于聲明引用的const 都是底層const

當執(zhí)行對象的拷貝操作時,拷入和拷出的對象必須具有相同的底層const資格,或者兩個對象的類型必須能夠轉(zhuǎn)換,一般來說,非常量可以轉(zhuǎn)化成常量,反值則不行:

int * p=p3; //錯誤:p3 包含底層const 定義,p沒有
p2=p3; //ok
p2=&i; //ok
int &r=ci ;// error 普通的int& 不能綁定到int常量上
const int &r2 =i ; //正確: const int & 可以綁定到一個普通int 上

constexpr 和常量表達式

常量表達式是指值不會改變并且在編譯過程就能得到計算結(jié)果的表達式,顯然字面值屬于常量表達式,用常量表達式初始化的const對象也是常量表達式。后面將會提到,C++語言中有幾種情況下是要用到常量表達式的。
一個對象(表達式)是不是常量表達式由它的數(shù)據(jù)類型和初始值共同決定,例如:

const int max_files =20 ;//是常量表達式
const int limit =max_files +1 ; //limit 是常量表達式
int staff_size =27 ; //staff_size 不是常量表達式

雖然不能使用普通函數(shù)作為constexpr 變量的初始值,但是新標準定義了一種特殊的constexpr 函數(shù),這種函數(shù)應該足夠簡單以使得編譯時可以計算器結(jié)果,這樣就能用constexpr函數(shù)去初始化cosntexpr變量了。

字面值類型
常量表達式的值需要在編譯時就得到計算,因此對聲明constexpr時用到的類型必須有所限制。因為這些類型一般比較簡單,值也顯而易見,容易得到,就把它們稱為“字面值類型”(literal type)。
目前為止所接觸到的數(shù)據(jù)類型中,算術(shù)類型,引用和指針類型,都屬于字面值類型,而自定義類sales_item、IO 庫等類型不屬于字面值類型。
盡管指針和引用都能被定義為constexpr,但它們的初始值卻受到嚴格限制,一個consteptr指針的初始值必須是nullptr或0,或者是存儲于某個固定地址的變量(對象)(全局變量或者局部變量中的靜態(tài)變量(staitc 修飾的))。
函數(shù)體內(nèi)定義的變量一般并非固定的地址,因此constexpr指針不能指向這樣的變量,相反的,定義與所有函數(shù)體之外的對象其地址固定不變,能用來初始化constexpr指針,函數(shù)體內(nèi)定義一類有效范圍超出函數(shù)本身的變量,這類變量和定義在函數(shù)體之外的變量一樣也有固定地址,因此,constepr引用能綁定到這樣的變量上,cosntexpr指針也能指向這樣的變量。
指針和constexpr
在constexpr聲明中如果定義了一個指針,限定符constexpr進對指針有效,與指針所指的對象無關(guān):

const int * p=nullptr; // p 是一個指向整型常量的指針
constexpr int * q =nullptr;// q 是一個指向整數(shù)的常量指針

處理類型###

類型別名

類型別名是一個名字,有兩種方法可用于定義類型別名,傳統(tǒng)的方法是試用關(guān)鍵字typedef

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

新標準規(guī)定一種新的方法,試用別名聲明來定義類型的別名:

using SI =Sales_item; //SI 是Sales_item 的同義詞

這種方法用關(guān)鍵字using作為別名聲明的開始,其后緊跟別名和等號,其作用是把等號左側(cè)的名字規(guī)定成等號右側(cè)類型的別名。
類型別名和類型名字等價,只要是類型的名字能出現(xiàn)的地方,就能使用類型別名。
指針、常量和類型別名
如果某個類型別名指的是復合類型或常量,那么把它用到聲明語句中就會產(chǎn)生想不到的后果,如下:

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

pstring 實際上是指向char的指針,因此,const pstring 就是指向char 的常量指針,而非指向常量字符的指針。
遇到一條使用了類型別名的聲明語句時,人們往往會錯誤的嘗試將類型別名替換成它本來的樣子,來理解該語句的含義:

const char * cstr =0;// 是對const pstring cstr 的錯誤理解

再強調(diào)一遍,這種理解是錯誤的,聲明語句中用到pstring 時,其基本數(shù)據(jù)類型是指針,可是用char *重寫了聲明語句后,數(shù)據(jù)類型就變成了char, *成為了聲明符的一部分,這樣改寫的結(jié)果是,const char 成了基本數(shù)據(jù)類型,前后兩種聲明含義截然不同,前者聲明了一個指向char的常量指針,改寫后的形式則聲明了一個指向const char的指針。

auto 類型說明符

C++11 新標準引用了auto類型說明符,用它就能讓編譯器替我們?nèi)シ治霰磉_式所屬的類型,顯然,auto定義的變量必須有初始值:

//由 val1 和val2 想加的結(jié)果就可以推斷出item的類型
auto item = val1+val2 ;//item 初始化為val1 和val2 想加的結(jié)果

此處編譯器將自己推斷item的類型,使用auto也能在一條語句中聲明多個變量,因為一條聲明語句只能有一個基本數(shù)據(jù)類型,所以該語句中所有變量的初始基本數(shù)據(jù)類型都必須一樣:

auto i=0, * p=&i; //right: i is int ,p is int point 
auto sz=0 ,pi=3.14 ;// error: sz 和 pi的類型不一致

復合類型、常量和auto
編譯器推斷出的auto 類型有時候會和初始值的類型并不完全一樣,編譯器會適當?shù)母淖兘Y(jié)果類型使其更符合初始化規(guī)則。

int i=0,&r=i; 
auto a =r ; //a is int 

其次,auto 一般會忽略頂層const,同時底層const則會保留下來,比如當初始值是一個指向常量的指針時:

const int ci =i, &cr =ci ;
auto b =ci ;   //b 是一個整數(shù) (ci 的頂層const 特性被忽略掉了)
auto c =cr ;   //c 是一個整數(shù) (cr是ci 的別名,ci 本身是一個頂層const )
auto d =&i;    //d 是一個整型指針(整數(shù)的地址就是指向整數(shù)的指針)
auto e =&ci ;  //e 是一個指向整數(shù)常量的指針(對常量對象取地址是一種底層const )

如果希望推斷出的auto類型是一個頂層const ,需明確指出:

const auto f=ci ;  

decltype類型指示符

有時候遇到這種情況,希望從表達式的類型推斷出要定義的變量的類型,但是不想用該表達式的值初始化變量,為了滿足這一要求,C++11 新標準引入了第二種類型說明符decltype,它的作用是選擇并返回操作數(shù)的數(shù)據(jù)類型,在此過程中,編譯器分析表達式并得到它的類型,卻不實際計算表達式的值:

decltype (f()) sum =x ; // sum 的類型就是函數(shù)f 的返回類型

編譯器并不實際調(diào)用函數(shù)f,而是使用當調(diào)用發(fā)生時f的返回值類型作為sum的類型。decltype 處理頂層const 和引用的方式與auto有些許不同,如果decltype 使用的表達式是一個變量,則decltype 返回該變量的類型(包括頂層const 和引用在內(nèi)):

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; // error z 是一個引用,必須初始化

decltype 和引用

//decltype 的結(jié)果可以是引用類型
int i=42 ,* p=&i ,&r =i ;
decltype (r + 0) b; //正確: 加法的結(jié)果是int
decltype (*p ) c; //error : c是int& ,必須初始化

decltype 和 auto 的另一處重要區(qū)別是,decltype 的結(jié)果類型與表達式形式密切相關(guān),有一種情況需要特別注意,加上括號與不加括號的區(qū)別:

//decltype 的表達式如果是加上了括號的變量,結(jié)果將是引用
decltype((i))d; //錯誤: d 是int & ,必須初始化 
decltype(i) e ; //正確,e 是一個int

自定義數(shù)據(jù)結(jié)構(gòu)

定義Sales_data 類型

使用Sales_data 類型

編寫自己的頭文件

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

  • 前言 把《C++ Primer》[https://book.douban.com/subject/25708312...
    尤汐Yogy閱讀 9,661評論 1 51
  • 數(shù)據(jù)類型決定了程序中數(shù)據(jù)和操作的意義。 2.1 基本內(nèi)置類型 基本數(shù)據(jù)類型:** 算數(shù)類型 空類型(void) ...
    小二三不烏閱讀 642評論 0 0
  • 初始化與賦值初始化的含義是創(chuàng)建變量的時候賦予其一個初始值,賦值的含義是把對象的當前值擦除,而以一個新值來替代。列表...
    KevinCool閱讀 373評論 0 0
  • 暗戀一個人注定是無果的,沒有人知道,你一個人撐起了一部感情戲。 初中的時候,就發(fā)生了我的第一次暗戀。周五,學校...
    韋豆子閱讀 276評論 1 2
  • 舉世皆濁唯他獨清,眾人皆醉唯他獨醒。屈原,一個不屈的信念,在千年之后,依然熠熠發(fā)光。 他一生致力于在楚國推行美政,...
    韶華久歌閱讀 425評論 0 3

友情鏈接更多精彩內(nèi)容