《C++ Primer Plus》第三章

第三章 數據處理(Dealing with data)

摘要:

  • 變量命名規則
  • C++基本整數類型:unsigned long, long, unsigned int, int, unsigned short, short, char, unsigned char, signed char, bool
  • C++11新增:unsigned long long, long long
  • climits文件,定義了每種數字類型的上限
  • 每種整數類型的數字常量
  • 用const定義符來創建一個常量
  • C++基本浮點數類型:float, double and long double
  • cfloat文件,定義了每種浮點數的上限
  • 每種浮點數的常量
  • C++的計算符
  • 自動類型轉換
  • 強制類型轉換

OOP編程的核心的就是設計和擴展你自己的數據類型。設計自己的數據類型代表你試圖讓類型去符合你的數據。

C++基本類型分兩種,基礎數據類型和符合數據類型。這一章將會講到前一種:整數與浮點數。到了第四章就會講到在這些基礎類型上定義的複合類型:數組,字符串,指針和結構


簡單變量


要存儲一個數據,程序需要知道三件事:

  1. 存在哪
  2. 存的值是幾
  3. 存了什麼類型

比如以下這兩句:

int braincount;

braincount = 5;

就說的要存一個int類型,值是5的數據。程序就會去找一塊足夠能放下一個int的存儲空間,記下地址,然後把5存進去。然後你就可以用braincount這個變量名去訪問這個存儲地址。當然,在這個程序裏你是看不到存儲地址的,而是程序自己來記住那個地址。不過你也可以用&操作符來獲得這個地址,這個會在下一章講指針的時候講到。


變量名

C++是鼓勵你創建有意義的變量名的。my_name, my_id這種變量名肯定比a, b要更合理。另外還有必須幾條遵守命名規則:

  • 變量名只能由字母、數字和下劃線(_)組成
  • 第一個字符不能是數字
  • 大小寫是不同的
  • 不能把C++關鍵字當名字
  • 以兩個下劃線或者一個下劃線加一個大寫字母開頭的名字是給接口(也就是編譯器和它用到的資源文件)用的。一個下劃線開頭的是給接口用來當做全局標識(global identifier)的
  • C++不限制名字的長度,名字裏每個字符都是必要的。但有些平臺可能會限制長度。

倒數第二點和其他不同,你寫了一個類似__time或_Time的變量不會導致編譯錯誤,但會讓程序產生未定義的行爲,也就是你不知道這樣會發生什麼。不觸發編譯錯誤是因爲下劃線開頭的命名並不是違規的,只是被預留給接口了。而全局名(global name)是指變量被聲明的地方,第四章將會講到這個問題。


整數類型(Integer type)

整數就是沒有小數的數,比如2,98,-256和0之類的。整數當然是無限的,但計算機不可能有足夠內存來存儲無限大的整數,所以一門程序語言只能表示出整數的一部分。有些程序只提供一種整數類型,但C++提供了好幾種選擇。這讓你可以從中選擇更符合你需求的類型。

這些不同的整數類型其實就是他們佔據了不同大小的存儲空間。一個佔據更大存儲空間的數據類型自然可以表示更大範圍的整數。而有些類型(signed type)可以表示正和負,而其他的(unsigned type)則不行。通常我們用寬度(width)來表示一個整數類型使用了多少的存儲空間。C++的基礎整數類型,寬度從小到大排分別是char, short, int, long和C++11纔有的long long。每一種都分爲signed和unsigned兩種,所以你就有多達十種整數類型可選。現在我們就來講講這幾個類型,因爲char有着些和其他類型不同的特性(畢竟它更多是用來表示字符而不是數字),下面我們會先講另外的幾種。

short,int,long還有long long

計算機存儲單位是bits,C++的類型short,int,long和long long就是使用了不同數量的bits。當然,如果在所有系統裏面,這四個類型使用的bits個數都一樣就最好了。但事實卻並非如此,並不是每個計算機的設計都是相同的。C++提供了一個靈活的標準,採用了C語言的下限標準:

  • short至少要16bits長
  • int至少要和short一樣長
  • long至少要32bits長且至少要和int一樣長
  • long long至少要64bits長且至少要和long一樣長

許多系統現在使用的都是這個最小值標準,就是short16bits,long32bits。這樣就讓int比較靈活,它可以是16,24,32bits。它甚至可以是64bits,當然這樣long和long long就也要至少長64bits了。通常來說,int在老式的IBM PC接口裏是16bits(和short一樣)而在WindowsXP到Windows7還有Macintosh OS X和VAX還有許多迷你計算機接口裏是32bits(和long一樣)。有些接口甚至允許你選擇怎麼處理int。這些對於int的不同定義讓C++在移植時很容易發生問題。不過接下來我們會提到一些降低這些問題出現的方法。

提一下bits和bytes的區別。bit是計算機存儲的單元。你可以吧一個bit當成一個開關。它開就代表1,關就代表0.一個8-bit的空間就有2的8次方也就是256種組合。也就是,8-bit的空間可以表示從0到255或者-128到127這個範圍的數。往上推就可以知道16-bit可以有65536種可能,32-bit可以有4294672296種,64-bit有18446744073709551616種。也就是說,一個unsigned long沒法存下現在地球的人口數,但long long可以。

byte通常指一個8-bit的空間。它是指計算機存儲空間的一個計量單位。平時我們看的kb,mb,gb就是kilobyte(1024 byte),megabyte(1024 kb),gigabyte(1024 mb).然而C++不是這麼定義的。C++的byte是由足夠能存儲接口的一個基本字符的bits組成。也就是一個byte要能存下一個獨立的字符。在美國,計算機一般用的是ASCII和EBCDIC字符集,一個字符8bits就足夠了。但在一些國際程序會使用更大的字符集比如Unicode,所以有些接口會使用16-bit甚至32-bit的byte。

sizeof操作符和climits頭文件

sizeof操作符會告訴你一個int在基本系統裏是4byte長(一個byte是8bits)。你可以對類型名字使用sizeof,你要用括號把類型名字括起來。但如果你是用在一個變量上面,括號就不是必須的了。舉個例子:

sizeof(int);

sizeof a;

climits頭文件定義了符號化常量(symbolic constants)來表示每個類型的上限。INT_MAX就是最大的int。在Windows7就會得到2147483647,而另外的16-bit的int的編譯器就會得到32767。這個編譯器製造方會提供和他們的編譯器相符合的climits頭文件。

關於符號化常量(symbolic constants),在climits頭文件會有類似這樣的定義語句:

#define INT_MAX 32767

我們還記得C++的編譯器會首先處理頭文件。這個#define和#Include一樣是個預處理指令,它的意思是:在整個程序中找到所有INT_MAX,然後將它們全部替換成32767.所以#define基本上和文檔編輯器裏的全局查找替換功能差不多。

初始化Initialization

初始化包括了聲明和賦值。比如:

int n_int = INT_MAX;

你也可以用數字比如255來初始化,也可以使用另一個變量(當然是要在這之前就初始化了的變量)。你甚至可以用一個表達式來初始化。比如:

int uncles = 25;

int aunts = uncles;

int chairs = aunts + uncles + 4;

如果你把uncles的初始化移到了這三句的最後,這程序就是無效的了。因爲這樣aunts和chairs在初始化時會沒法知道uncles是什麼。

上面這種初始化格式是來自C語言的,C++有自己新的初始化格式:

int mother(1); //mother賦值爲1

當然如果你在初始化變量的時候還不清楚它的值,你可以暫時不給它賦值:

int year;

year = 2018;

不過在初始化時就賦值可以防止你放了給它賦值。

C++11的初始化

有一種初始化格式是用於數組(array)和結構(structure)的,但在C++98裏單值變量也可以這麼用:

int pizza = {30};

用大括號初始化之前並不常見,但C++11擴展了這種用法。首先,它可以省略等號:

int pizza{30};

其次,大括號裏面可以爲空,變量將會被賦值爲0:

int pizza{};

第三,這樣會針對類型轉換起到更好的保護作用,這個會在本章晚些提到。


unsigned類型

先前介紹的4種整數類型都是unsigned類型,是無法表示負數的。這樣的優勢是它們可以表示更大的數。比如signed類型的short可以表示-32768到32767,而unsigned的short可以表示0到65535.當然,你只應該在沒有負數的情況下使用unsigned類型。

#include <iostream>
#define ZERO 0
#include <climits>

int main()
{
    using namespace std;
    short sht = SHRT_MAX;
    unsigned short u_sht = sht;
    cout << "short:" << sht << endl;
    cout << "unsigned short:" << u_sht <<endl;
    sht = sht + 1;
    u_sht = u_sht + 1;
    cout << "short:" << sht << endl;
    cout << "unsigned short:" << u_sht << endl;
    sht = u_sht = ZERO;
    sht = sht - 1;
    u_sht = u_sht - 1;
    cout << "short:" << sht << endl;
    cout << "unsigned short:" << u_sht << endl;
    return 0;
}

以上程序輸出是:

short:32767

unsigned short:32767

short:-32768

unsigned short:32768

short:-1

unsigned short:65535

可以看到signed類型的最大值加一會得到它表示範圍內的最小值。而unsigned類型的0減一會得到它的最大值。這其實涉及到計算機組成原理講到的機器表示數的知識。在這裏就不細寫了。


如何選擇整數類型?

有這麼多整數類型,怎麼選?自然,int是最“自然”的類型。最“自然”的類型意味着計算機處理它是最有效率的。如果沒什麼特別的理由,int應該就是最佳的選擇。

但爲什麼你會需要其他的類型呢?如果一個數據不可能爲負,比如人數之類的,你就應該使用unsigned類型,這樣變量可以容納更大的值。

如果變量的取值可能會超過16bits,你應該使用long,即便你的系統裏int是32bits的。因爲你要考慮到你的程序在int只有16bits的系統上運行的情況。

當short比int小時,使用short可以節省一些存儲空間。一般來說,只有你的數組非常非常大的時候這個選擇纔有意義。(數組就是一種將多個同類型數據順序存儲的數據結構)如果節省存儲空間對你來說很重要,那麼即便你的系統裏short和int一樣長,你也應該採用short,理由和爲什麼要使用long一樣。


常數

常數或常量就是你明確寫出來的數,比如225和1024.C++和C一樣允許你用三種進制來寫常數:十進制(大衆喜好),八進制(老式Unix喜好)和十六進制(硬件駭客喜好)。C++使用數字的第一或第一二位來區分進制。如果數字開頭是1-9,那就是個十進制數。如果開頭是0,第二位是1-7,那就是個八進制數。如果前二位是0x或0X,那就是個十六進制數。對於十六進制數,a-f或者A-F表示十六進制下10-15的取值。比如15的十六進制是0xf,0xA5的十進制是165.


C++如何識別常數的類型?

聲明語句會明確告訴編譯器標量的類型,但常量呢?比如你在語句裏寫一個1024,程序會把它存爲一個int還是long還是short?答案是C++會把它默認存爲int除非有必要存成別的類型,比如你用特殊的後綴指明瞭常數的類型又或者常數大於int的可表示上限。

首先我們來看看後綴。你可以放一些字母在數字末尾來表明它的類型。l或者L代表long,u或者U代表unsigned int,ul(大小寫任意組合)代表unsigned long。(不過因爲l和1看起來很像,所以還是儘量用L比較好)

還有就是C++11下當然還會有ll(或LL)代表long long,和ull(大小寫任意,但兩個l的大小寫要一致)代表unsigned long long。

然後再來看看大小問題。C++在十進制整數處理上和十六進制和八進制不太一樣。一個沒有後綴的十進制數會以int,long和long long中最小的又足夠大的一種類型存儲。而十六進制還有八進制則會以int,unsigned int,long,unsigned long,long long和unsigned long long中最小又足夠大的一種類型存儲。


char:字符和小整數

終於我們說到了最後一種整數類型:char。就像你猜的那樣,char是設計用來存儲字符的,比如字母和數字。我們都清楚存數字對計算機來說沒什麼難的,但存儲字符可是另一回事。所以char是一種特殊的整數類型,它一定足夠大來表示字符集中的每一個字符。實際應用中,很多系統僅支持少於128個字符,所以一個byte就夠用了。所以,雖然char是用來存儲字符的,但你同樣可以把它當做比short更小的一個整數類型來使用。

美國最常用的字符集是ASCII。一個ASCII碼代表一個字符,比如65代表A,77代表M。爲了方便,本書中默認程序使用的是ASCII碼。但C++的接口使用的字符集是取決於它的原初系統,比如在IBM的主框架下使用的是EBCDIC。無論是ASCII和EBCDIC都無法滿足國際程序的需要,所以C++支持一種寬字符(wide character)類型來存儲更大的字符碼,比如用於字符集Unicode。本章晚些就會講到這個w_chat類型。

注意

char是一個整數類型,在內存裏面它存儲的就是一個整數,你可以對它進行任何的整數適用的操作,比如給它加一。之前便提到過,是類型讓cout決定如何輸出這個值——又一個智能對象的表現。

C++將字符當做整數處理是非常方便的一個做法,這樣你就不需要使用麻煩的轉換方法來讓值在整數和ASCII碼之間反覆轉換。

就算你通過鍵盤輸入數字也可以被識別爲字符,比如:

char ch;

cin >> ch;

如果你輸入一個7,那麼程序將讀入爲‘7’這個數字,將‘7’的ASCII碼55存入ch。而下面這兩行:

int n;

cin >> n;

同樣輸入一個7,程序會讀入7這個數值,然後將7存入n。

成員方法:cout.put()

這個cout.put()是什麼?這是你第一個關於C++ OOP的例子:成員方法。先前說過類定義瞭如何表示數據以及如何操作它們。比如ostream這個類,它就有一個put方法,設計用於輸出。

要通過一個對象比如cout來使用一個方法比如put,你要用一個點(.)來吧cout和put()鏈接起來。這個小數點叫成員操作符(membership operator)。cout.put()表示通過對象cout來調用成員方法put。第十章會講到這方面更詳細的細節。

cout.put()這個成員方法還提供了一個另外的選擇:用<<操作符來輸出。也就是先前我們一直在用的那種。你可能會奇怪那cout.put()這個方法還有什麼必要。這個涉及到一些歷史性的因素。在C++2.0發佈之前,cout會把字符變量輸出爲字符,但會把字符常量比如'M'輸出爲數字。這是因爲早期的C++和C一樣,將字符常量存爲int。也就是說'M'在內存裏是16bits或32bits長的值爲77的int。而以下這一句:

char ch = 'M';

被聲明的ch是一個長爲8bits的char。也就是說,對於cout,'M'和ch差別挺大的,儘管他們理論上應該是一樣的值。所以以下這兩句:

cout << 'M';

cout.put('M');

第一句會輸出77,第二句會輸出M。

現在C++2.0發佈了,C++會將字符常量存儲爲char。所以這個問題就解決了。

cin對象讀入字符有好幾種不同的方式,你可以寫一個循環讀入再輸出的程序反覆輸入不同的字符來研究一下。詳細的會在第五章講循環的時候講到。

char常量

雖然我們說字符在程序中都是存儲爲整數的,數值和字符一一對應。但在寫字符常量的時候,還是寫字符的好。一來這樣更清楚不容易犯錯,二來不同字符集中字符和整數的對應是不一樣的。ASCII中65是A,但EBCDIC中65可就不是A了。

另外,有些符號你是不能直接通過鍵盤寫入程序的。比如你不能用回車鍵讓字符串換行,因爲回車是用來讓源代碼換行的。其他字符同樣有這個問題因爲C++將它們用作重要的標識。比如雙引號是用來標識字符串常量的首尾的,所以你沒法在字符串中間插一個雙引號。C++有一個特殊的符號叫轉義序列,專門爲這種字符服務。下表列出了它們:

C++轉義序列
名字 ASCII符號 C++代碼 ASCII碼(十進制) ASCII碼(十六進制)
換行 NL(NF) \n 10 0xA
水平縮進 HT \t 9 0x9
垂直縮進 VT \v 11 0xB
後退 BS \b 8 0x8
回車 CR \r 13 0xD
提醒(響一聲) BEL \a 7 0x7
反斜槓 \ \\ 92 0x5C
問號 ? \? 63 0x3F
單引號 ' \' 39 0x27
雙引號 " \" 34 0x22

signed和unsigned char

不像int,char並不是默認爲signed,也不默認爲unsigned,而是讓C++接口來決定。這樣讓編譯器開發者可以更好地作出符合它們的硬件情況的決定。如果signed和unsigned char的區別對你很重要,你可以直接寫明:

unsigned char u_ch;

signed char s_ch;

不過這隻有在你把它們當做整數類型來用的時候纔有意義。unsigned類型的範圍是0到255而signed類型是-128到127。

當你想要更多:wchar_t

當你的字符集太大,8bits的char沒法滿足你了,C++提供了兩種處理辦法。一,如果這個巨型字符集是系統的基礎字符集,那麼C++接口可以將char定義爲16bits長或更長。二,如果接口可以同時支持一個小的字符集和一個擴展字符集,那麼基本的8bits的char可以用來表示小的字符集,另一個類型:wchar_t,可以用來表示擴展字符集。wchar_t是一個更長的char,wchar_t的大小和大小和sign類型和另一個叫做底層類型(underlying type)是一樣的。而底層類型的選擇是由接口決定的,所以有些系統裏是unsigned short,有些系統裏是int。

cin和cout是用於處理char流的,所以它們並不合適用於處理wchar_t類型。iostream頭文件提供了類似的一個工具:wcin和wcout。還有你可以使用L作爲前綴來指明寬字符常量或字符串。以下是一個例子:

wchar_t w_ch = L'p';

wcout << L"tall" << endl;

這本書裏不會用到wchar_t,不過你還是需要了解一下。

C++11:char16_t, char32_t

就是unsigned,一個16bits一個32bits長的char。u前綴代表char16_t,U前綴代表char32_t。


bool類型

ANSI/ISO C++標準添加了一個新的類型:bool。這個名字是爲了紀念發明了數學邏輯學的數學家George Boole。在計算機中,boolean變量的值可以是true或者false。以前C++和C一樣,沒有Boolean類型。關於這一點的細節會在第五第六章講到,C++將非0的值當做true,而0處理爲false。現在你可以用bool這個類型來表示true和false了,而true和false被預定義爲了對應的值。也就是說你可以這樣寫:

bool is_empty = true;

true和false常量可以被轉成int類型,true會轉成1,false轉成0:

int a = true;  // a = 1

同樣地,數值或指針值可以被隱式地轉爲Boolean。非0的是true,0是false:

bool b = -100; // b = true


const標識符


現在我們說回符號化常量。當你在程序的多處使用一個常量的時候,你可以通過將其定義爲符號化常量並可以通過改變它的定義來簡單更改它的值。本章早些講到#define時說過,C++有一個更好的方法來處理符號化常量。那就是使用const來標識變量的聲明和初始化。舉個例子:

const int Months = 12;

現在你可以在程序裏寫Months而不用寫12.(在程序裏簡單寫一個數字12,這個12可能是長度可能是人數可能是很多東西。但在程序裏寫一個Months就很清楚是月份數。)在你初始化了這樣一個常量後,這個常量就是固定的了。編譯器不會允許你在之後的程序中更改這個Months的值。

比較常見的做法是將常量的第一個字母大寫來提醒自己這個是不可更改的常量。不過這不是約定俗成的,也有很多其他的比如將常量名全部大寫,這常見於#define定義的常量。也有一種是在常量名之前加k,比如kmonths。也有許許多多其他的做法。

注意的是你要在常量的聲明語句中將它初始化。以下的做法是不行的:

const int a;

a = 1;

如果你在常量聲明語句中沒有初始化,那麼這個常量就會保持未定義,並且也無法更改。

如果你學過C,你會覺得#define就已經夠用了。但const是更好的做法。首先,它讓你明確寫出了常量的類型。第二,你可以用C++的範圍規則來讓常量的作用範圍侷限於某個方法或文件內。(範圍規則描述了對於不同模型一個名稱的作用範圍。這個會在第九章中講到。)第三,你可以用const來定義更多種類型的常量,比如會在第四章講到數組和結構。

ANSI C也有const標識符,這是從C++借來的。如果你對ANSI C很熟,你要注意C++的版本是不太一樣的。其中一個不同是關於範圍規則,第九章會講到。另一個主要不同是在C++裏,你可以用const常量來定義數組的大小。這個會在第四章看到例子。


浮點數(Floating-Point Numbers)


浮點數是C++的第二種基礎類型,用於表示帶小數的數,或者過於大的數。計算機把小數存儲爲兩部分,一部分是數值,另一部分是數值擴展方向。比如34.125和3.4125的第一部分是一樣的。因爲它們都可以寫成0.34125*100和0.34125*10.C++是採用類似這樣的方式來存儲浮點數,只不過是二進制形式的,所以第二部分的單位是*2而不是*10.但我們不需要記住這種內部的表示,只要知道浮點數這個類型就可以了。


浮點數的使用

C++有兩種浮點數寫法,第一種就是日常的用法,8.0,3.25這樣的。第二種寫法是用E,比如這樣:3.1E6.這個數代表3.1乘10的六次方,也就是3100000.E後面也可以是負數:2.67E-3,這個數的值是0.00267.


浮點數類型

C++有三種浮點數類型:float,double和long double。它們區別在於它們可表示的關鍵數的數量和可允許的指數的最小範圍。關鍵數是指一個數中有意義的數字。比如14265有五個關鍵數,而14000則只有兩個。關鍵數個數和小數點的位置沒有關係

C++和C要求float的關鍵數不能少於32bits,double不能少於48bits且不能少於float,還有long double至少要和double一樣。這三個實際上是可以一樣大小的,但一般來說,float是32bits,double是64bits,long double是80,96甚至128bits。而指數範圍對於三者都是至少是-37到37.你可以在cfloat或float.h頭文件中查看你的系統的設定。

以下這段程序驗證了float和double在關鍵數方面的精度差別。程序中使用了17章將會講到的ostream中的setf方法。這個強制輸出不以E符號形式輸出小數以更好地對比精度,同時它強制輸出小數點後六位。參數ios_base::fixed和ios_base::floatfield是iostream中的常量。

#include <iostream>
int main()
{
    using namespace std;
    cout.setf(ios_base::fixed, ios_base::floatfield);
    float f = 10.0/3.0;
    double d = 10.0/3.0;
    const float million = 1.0e6;

    cout << "f = " << f << endl;
    cout << "f * million = " << f * million << endl;
    cout << "f * million * 10 = " << f * million * 10 << endl;
    cout << "d = " << d << endl;
    cout << "d * million = " << d * million << endl;
    return 0;
}

輸出:

f = 3.333333
f * million = 3333333.250000
f * million * 10 = 33333332.000000
d = 3.333333
d * million = 3333333.333333

注意

一般來說,cout會把多於的0去掉。比如33333.250000會輸出爲33333.25.調用cout.setf()改寫了這個行爲。

更主要的是上面的程序展示了float的精度比double低了多少。f和d都是用10.0/3.0初始化的。它們的值應爲3.3333333(無限循環)。因爲cout輸出小數點的後六位,你可以看到f和d都保持準確。但當它們乘以1000000,你發現f在第七個3之後的值就不對了。f在7個關鍵數內是準確的。而double類型的變量顯示了13個3,所以它可以保證13個關鍵數的精度。因爲系統其實可以保證double有15個關鍵數精度。另外你發現f*1000000再乘以10後,結果進一步出錯,這再一次指出了float的精度問題。


浮點數常量

浮點數常量會默認存儲爲double,你可以用後綴來指明常量的類型。f或F代表float,l或L代表long double(l看起來和1差不多,所以還是儘量用L)。比如

1.234f   //float

2.65e8 //double

2.2L   //long double


浮點數優缺點

浮點數對於整數來說有兩個有點:一、它可以表示整數之間的數。二、因爲科學計數法,它可以表示大很多很多的數。另一方面,浮點數運算通常比整數運算稍慢一點點,且你會損失精度。

#include <iostream>
int main()
{
    using namespace std;
    float a = 2.34e+22f;
    float b = a + 1.0f;

    cout << "a = " << a << endl;
    cout << "b - a = " << b - a << endl;
    return 0;
}

輸出:

a = 2.34e+022
b - a = 0

問題出在了,2.34e+22表示了一個小數點左邊有23位長的數,也就是23400000000000000000000.你給這個數加一,得到23400000000000000000001.但float只能存儲六七個關鍵數,也就是2340000,最後那個1無法保留。


C++ 算符


C++擁有以下的基本算符:

  • + - * / 對應加減乘除
  • %是模,這個符號會算出左邊數除以右邊數得到的餘數。比如19%6=1.

算符順序:算符優先級與關聯

你可以在C++裏做複合運算,比如:

int a  = 3 + 5 * 4;

但你要搞清楚運算的優先級。C++的優先級和普通代數學的優先級是一樣的,先算乘除和模,再算加減。也就是上面那個複合運算應該是3 + (5 * 4)。但有時還有優先級解釋不了的情況,比如:

float f = 120 / 4 * 5;

乘除是沒有優先級之分的。C++採用的是從左到右的規則。先算左邊,再算右邊。但還有另一種情況:

int b = 20 * 5 + 4 * 3;

根據優先級,在做加法之前,程序要先算出20*5和4*3.那先算哪個呢?從左到右嗎?並不是,因爲這兩個運算沒有共享一個參數,所以從左到右規則不適用。這裏C++讓接口來決定運算順序。在這裏,先算哪個並沒有關係,但在有些情況下是有關係的,第五章將會講到這個問題。


除法分歧

除號的運算取決於參數的類型的。如果兩個參數都是整數類型,C++會進行整數除法。也就是算出來的結果的小數部分會被丟掉,得到的結果是個整數。如果有一個或者兩個參數都是浮點數,那麼結果的小數部分就會保留,得到的結果是個浮點數。


類型轉換

C++定義了許多自動的類型轉換:

  • 當你將一種算術類型的值賦值給另一種算術類型的變量
  • 當你在表達式中使用多種類型
  • 當你向方法傳入參數

初始化與賦值時的轉換

將一個類型的值賦給更大類型的變量(比如將int類型的值賦給long類型變量)通常不會導致問題。但將一個很大的long比如21556547賦給float變量就會損失精度了,因爲float只能存下6個關鍵數。得到的float值大概是2.15565e+7。所以有些類型轉換是安全的,有些就會導致問題了,看下圖:

數值類型轉換潛在問題
轉換類型 潛在問題
大浮點數類型->小浮點數類型 如 double->float 損失精度(關鍵數個數);值可能會超出目標類型的範圍,其轉換結果是未定義的
浮點數->整數 失去小數部分;原數值可能超出目標數值範圍,其轉換結果是未定義的
大整數類型->小整數類型 如long->int 原數值可能超出目標數值範圍;一般來說只有低位的byte會被複制

初始化時的類型轉換與賦值一樣。

使用{}初始化時的類型轉換(C++11)

大括號初始化不允許進行變窄的類型轉換,比如浮點數轉整數。但如果是用常量對小類型初始化,如果值是小類型變量可以存儲的,那麼初始化也是允許的。比如:

const int code = 66;

int X = 66;

char c1 {21212};  //這是變窄,不行

char c2 {66};        //可以,因爲char可以存下66

char c3 {code};     //同上

char c4 {X};          //不行,X不是常量

X = 21212;

char c5 = X;          //這種形式初始化時允許的

對於c4的初始化,我們知道X的值是66.但由於X是個變量,它的值是可變的。程序並不會對這個變量的值得變化保持追蹤,所以編譯器無法確定X在初始化和賦值之間值有沒有發生變化。

表達式內的類型轉換

在一個算術表達式中使用兩個不同類型的參數,C++會進行兩種自動類型轉換。一,有些類型只要出現就會被轉換。二,有些類型在和其他類型出現在同一表達式中時會被轉換。

首先來看看自動轉換。在評估一個表達式時,C++會將bool,char,unsigned char,signed char,short都轉成int。true會轉成1,false轉成0.這叫整體升級(integral promotion)看如下幾行:

short pigs = 11;

short dogs = 13;

short all = pigs + dogs;

執行第三行時,C++首先將pigs和dogs的值轉爲int,算出結果後,再把結果從int轉回short,因爲結果被賦給了一個short變量。這是作爲最自然的類型,int的運算比其他類型運算更快一些的原因。

還有更多的自動轉換。如果short比int小,那麼unsigned short會被轉爲int。而如果二者相同,unsigned short 會被轉成unsigned int。這個規則保證了轉換中不會出現數據損失。類似的,wchar_t會被轉成這幾種類型中最小又足夠大的類型:int,unsigned int,long,unsigned long.

然後是多類型組合時的類型轉換,比如用int加float。當運算涉及兩個類型時,較小的那個類型會被轉成較大的那個類型。比如9.0/3,9.0是個double,3是個int。那麼3會被轉爲一個double。一般來說,C++會通過查表來確定哪一個類型是較小的。下表是C++11稍作修改後的表,編譯器會順序逐條查看:

  1. 如果有一個long double,那麼另一個就會被轉成long double
  2. 當以上不成立,如果有一個是double,那麼另一個會被轉爲double
  3. 當以上不成立,如果有一個是float,那麼另一個會被轉爲float
  4. 當以上不成立,也就是參數都是整數,進行整體升級(integral promotion)
  5. 在第四條中,如果兩個參數都是signed或都是unsigned,且一個比另一個小,小的會被轉成大的
  6. 如果第五條不成立,也就是一個是signed一個unsigned。如果unsigned那個是更大的類型,signed那個會被轉成unsigned的那個類型
  7. 如果第六條不成立,如果signed的那個類型可以表示unsigned那個類型的所有值,unsigned的這個值會被轉成signed的那個類型
  8. 如果第七條不成立,兩個值都會被轉成signed那個類型對應的unsigned類型

傳遞參數時的類型轉換

一般來說,C++方法原型會控制傳參時的類型轉換,這個第七章會學到。但你也可以,雖然不太明智,放棄原型對傳參的控制。在這個情況下,C++對char和short進行整體升級(integral promotion)同時爲了保證對於經典C中大量代碼的適應性,在向放棄了方法原型的方法傳參時,C++會將所有float參數轉爲double。

強制轉換

C++允許使用通過cast機制強制進行類型轉換。強轉有兩種格式,比如你要將一個名爲juice的int類型變量強轉爲long,你可以這麼寫:

(long) juice

long (juice)

強轉並不會改變juice變量本身,而是將其值強轉後作爲新值返回。以上格式第一種是來自C語言,第二種則是C++纔會採用的。

C++還有另一種更嚴格的強轉操作符

static_cast<typeName> (value)


C++11的自動聲明

C++11新添了讓編譯器根據初始化值推測類型的功能。通過auto關鍵字來使用這個功能。就在聲明語句中用auto代替類型,編譯器就會讓變量類型和初始化的值得類型一致。

auto n = 100;  //n是int

auto x = 1.5;    //x是double

auto y = 1.3e2L   //y是long double

但auto並不是用在這種簡單的地方的。甚至你可能會因爲這個出錯,比如假設x,y,z都應該是double,然後你寫了以下語句:

auto x = 0.0;    //可以,x是double因爲0.0是double

double y = 0;    //可以,0被轉成了double

auto z = 0;       //不行,z是int因爲0是int

auto更多還是用在複雜類型的處理,比如C++98種可能有這兩句:

std::vector<double> scores;

std::vector<double>::iterator pv = scores.begin();

在C++11,你就可以這麼寫上面兩句:

std::vector<double> scores;

auto pv = scores.begin();


總結


  • C++有兩種基本類型,整數和浮點數。整數類型之間區別在於不同的存儲空間還有它們是signed還是unsigned。整數類型從小到大排是:bool,char,signed char,unsigned char,short,signed short,unsigned short,int,unsigned int,long,unsigned long還有C++11的long long和unsigned long long。另外還有寬字符wchar_t以及C++11添加的char16_t和char32_t。
  • 字符是用其數碼錶示的,I/O系統決定了這些碼應該被翻譯成字符還是數字。
  • 浮點數有三種類型,float,double和long double。C++規定float不能比double大,而double不能比long double大。一般來說,float是32bits,double是64bits,long double在80到128bits之間。
  • C++提供了基本的算符:加減乘除和模。當兩個算符共享了一個參數,C++的優先級和關聯性決定了哪個算符先執行。
  • 當你賦值,在運算中使用多種類型還有使用cast進行強轉,C++就會進行類型轉換。一些轉換是安全的,而另一些則需要注意一點。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章