這個筆記本主要記錄了我在學習C++Primer(第5版,中文版)的過程中遇到的重難點及其分析。因為第一、二章都比較簡單,因此這里合並這兩章我遇到的問題。
這一章在第一部分之前,是一個helloworld式的章節,包含基本的函數,io流以及類的介紹。依靠這一章的內容可以創建第一個完整可被編譯的cpp文件。
知識點1:P19,1.5,文件重定向(英文版22頁)
可以在windows下的cmd中或者mac,linux系統的終端窗口中用輸入命令的形式執行程序並使它從一個文件中讀入數據,再把標准輸出改為輸出到另一個文件里。
如果
我們編譯好的程序為a.exe,程序需要輸入兩個整數,整數之間用逗號間隔,然后需要檢測到EOF(End_Of_File,文件結束符)程序才會停止。這時為了文件重定向,我們可以新建一個文本文檔(b.txt),接着我們在文檔里記錄”1,2“然后保存起來,保存后系統會在這個文件的末尾自動添加文件結束符。然后再新建一個空白文本文檔(c.txt),使a.exe,b.txt,c.txt在同一個目錄下。這時打開終端,轉到這個目錄,輸入a <b.txt>c.txt回車確認。之后我們就實現了文件重定向了。
但是不被緩沖的cerr的數據沒法被重定向到指定文件里。這是終端的命令提示符的行為,並不是C++語法中的一部分。
(交叉引用:P23,第一章小結)cerr和clog的內容都不會馬上輸出,都會先被存儲到緩存區里面,因此你可以在緩存區被刷新之前,指定(重定向)它們輸出的位置。而cerr是直接輸出的,因此只能做標准輸出,不能重定向。讀cin,程序意外終止,使用endl都會刷新緩存區。
第一章補充說明的內容:根據C++Primer這本書的設定,有一些書上的內容並非C++語法,但是在書中起到了輔助我們編程和理解概念的作用,這部分的內容在筆記里將成為補充的部分被記錄下來。
1.關於練習的情景假設:
為了練習編寫程序,C++primer這本書很多地方都用到了假定我們要編程處理一個書店的問題。並在此問題上慢慢擴展,最后實現這個書店的問題的編程。這個問題的假定是這樣的:首先,書店程序將記錄所有的交易記錄。每條銷售記錄都會包含一下內容:ISBN號,用來記錄本次交易的書的isbn號,isbn是書的唯一標識。之后,每條交易記錄還含有本次交易的書的單價和銷售本數。書店銷售記錄支持的操作包括查看書的銷量,平均價格,計算每本書的銷售額度,錄入和讀取每本銷售記錄等等。
另外,我們能夠對銷售記錄結構體進行的操作包括:讀取書目的isbn號,把兩個銷售記錄累加起來。輸入輸出一組交易記錄。
2.關於一些經常出現的詞匯:
當我們說類型的時候,其實是在說一個對象的屬性,或者多個對象的共同屬性。當我們在說對象的時候,往往是說按照類型分類存在的變量實體。比如a是b類型的對象。b是a的類型。這種說法會在學習C++的過程中被多次提到。C++默認的類型加上我們自己定義的類型有很多,我們可以把任何概念使用這門語言的類型等設施抽象到代碼中。所以一切都是對象這種說法就指的是這種通過語言提煉事物的基本信息,並把這種可以量化的信息記錄保存和抽象的特征。
3
.IDE:
IDE是指集成開發環境。如何使用代碼生成可執行程序,如何在各個系統和各個系統的各種版本支持C++,怎樣提供調試功能,這些點就是IDE解決的問題。基本上這部分的知識和C++語法沒有關系,反過來,對C++和一些計算機知識了解的越多,就越能夠理解IDE的各種功能和作用。因此,書中只是簡單地提到了IDE的概念,關於如果下載,安裝IDE,配置IDE或者使用IDE的特性不在此書的知識體系范圍之內,應該另行學習使用IDE。因此書上只建議IDE應該選擇起碼支持C++11的版本。
4
.回車鍵:
不同系統版本對按下回車鍵這個動作的處理是不一樣的。通常我們只是想在控制台里敲入回車鍵來確認我們的輸入,但是有的系統里也同時提供了一個回車鍵作為字符(換行符)輸入。遇到一些復雜的情況回車鍵的使用可能導致未定義的行為。在第三章我們將看到,可以使用getline從輸入流中讀取換行符(回車)並拋棄它。因此,謹慎的使用回車鍵是很重要的。
5
.進制轉換和原反補碼的概念:
不同進制的手動轉換和原碼反碼補碼這些概念都是一般高校C++課程中的考察范圍內概念,不過C++Primer這本書沒有對此特殊說明,因此在這里特別地補充這些概念。
1.十進制整數轉換為R進制整數。
這里介紹除R取余法,要把數轉換成哪個進制的數,R就是多少。這個方法把要轉換為R進制的十進制整數不斷除R,直到商為0為止,先計算出的余數就是被轉化進制數的低位,后計算出的余數就是被轉化進制數的高位。例如把10進制68轉為二進制數,先68/2,得34,余0。因此對應的二進制數倒數第一位是一個0;34/2=17.......0,因此二進制數的倒數第二位也是0,17/2=8....1,倒數第三位是1;8/2=4.......0,倒數第四位是0;4/2=2.......0,因此倒數第五位是0;2/2=1......0,因此倒數第六位是0;1/2=0........1;商為0,停止這個過程,最高位為1.因此,對應的二進制數為1000100。完成進制轉換。
2.十進制純小數轉換為R進制純小數。
純小數就是整數部分為0的小數。十進制純小數可以不斷乘R直到小數部分為0或者小數位數到達要求的精度對應的小數位數,這個方法也被叫做乘R取整法。比如把0.2135轉化成對應的二進制數。首先0.3125*2=0.625。取整數部分0,這個0就是最高位。然后0.625*2=1.25。取整數部分1。剩下的部分是0.25,0.25*2=0.5,取整數部分0。0.5*2=1.0,取整數部分1。結果就是0.0101。
綜上所述,把一個整數部分不為0,小數部分也不為0的十進制數轉化為R進制數只需要把這個數拆成整數部分為0的純小數和小數部分為0的整數,分為轉換並加在一起就行了。
3.R進制數轉化為10進制數。
R進制數每一位乘R的數的權值次冪,得到的結果就是對應的十進制數,比如二進制數1101.11轉換為10進制等於1*2^3+1*2^2+0*2^1+1*2^0+1*2^-1+1*2^-2。這個數就是十進制的13.75。
4.二,八,十六進制的互相轉換
因為二、八、十六進制之間有內在聯系,因此可以很方便的互相轉換。每位八進制數相當於三位二進制數;每位十六進制數相當於四位二進制數字。因此,遇到一個二進制數轉化為八/十六進制數,我們可以把這個二進制數划為3/4個數字一組的方式轉換為對應的八/十六進制數字。比如有二進制數字1000100,把這個二進制數轉化為八進制數。首先分組,3位數字一組。這個數就變成了001 000 100,每三個二進制數轉為一個八進制數,100轉為八進制是4,000轉為八進制是0,001轉為八進制數是1,因此這個數就相當於八進制的104。
類似地,反過來每一個八/十六進制都是3/4個二進制數。比如十六進制的F7。首先7轉為十進制是7,
F轉為十進制是15 。然后
7轉為四位2進制就是0111,15轉為四位二進制數就是1111。合起來就是11110111。
另外,數值在機器中的表示也是非常重要的,下面是有關概念的介紹:
原碼:把符號位化為0或者1,之后把符號位和數值的絕對值在一起編碼,之后得到的編碼就是原碼。一般把正數的符號位設為0,負數的符號位設為1。對於一個純小數,整數位為符號位。
反碼:正數的反碼是它本身,負數的反碼是它去掉符號位之后按位取反。
補碼:正數的補碼是它本身,負數的補碼是負數的反碼加一。符號位參與運算。
第I部分:C++基礎
第二章 變量和基本類型
C++Primer由四部分組成,這里是比較基礎的第一部分,這部分共有六章。
第二章主要講變量,變量類型,定義變量的各種情況和規則以及變量之間互相轉換的規則。
知識點2:P30,2.1.1,C++語言關於類型的規定(英文版32頁)
C++語言的基本類型的設定與硬件緊密相關,因此很多類型的內存尺寸也都只是給了一個范圍,其實各家IDE(LLVM,GCC,Visaul C++)的實現都是在范圍內,具體的實現細節都是不確定的。
其中bool最小尺寸未定義,char最小尺寸是8位,wchar_t和char16_t的最小尺寸都是16位,char32_t的最小尺寸是32位,int的最小尺寸是16位,long和long long的最小尺寸分別是32位和64位,對於浮點型數據的表現尺寸是按照精度計算的,其中float的最小尺寸精度是小數點后6位,double和long double的最小尺寸精度則是小數點后10位(實際可能比這個精度要大一些)。int不得小於short,long不得小於int,long long不得小於long。float,double,long double也應該是精度遞增(或者相同)的關系。
知識點3:P32,2.1.2,類型轉換
(英文版35頁)
程序自動執行的類型轉換操作發生在程序里IDE預期我們使用A類型但是實際上我們使用B類型的時候,B
類型的對象會自動轉換為A類型的,如果沒法轉換,程序就會報錯。
我們先看賦值操作里表達式里面發生的自動轉換,賦值操作A=B中,等號左邊的A被叫做左值,B被叫做右值,程序期待事情是你給定的右值和左值類型完全相同。如果不相同,這里就會發生強制的類型轉換,即把B的類型轉化為A的類型。如果把一個超出左值類型表達范圍的數賦值給左值,左值又是一個無符號類型,比如unsigned char c=-1;這時-1(整型,負的),右值會轉化為無符號字符型,初始值對無符號類型表示數值總數取模,然后求余數,這個余數就是轉化后的數。
因為C++沒有明確規定有符號類型的數應該如何表示,因此如果把一個超出左值類型表達范圍的數賦值給左值,左值又是一個有符號類型,這種行為的結果是不一定的。我們把這種不確定造成結果行為叫做未定義行為。
知識點4:P36,2.1.3,轉義序列
(英文版39頁)
字符的轉義序列可以為\后面加上最多3個8進制數字,或者\x后面加上最多兩個16進制數字。數字轉換成10進制后的大小不得超過字符集的限定范圍。
知識點5:P55,2.4.1,const的引用和指針
(英文版62頁)
const是用來聲明常量的標識符,代表我們不能夠使用const后聲明的變量名更改變量的值。如有前提const int i=32;,則之后給i賦值的語句就都是錯誤的。但是在int a=0;const int &p=a;的前提下,給a賦值的語句卻是正確的,因為a不被const修飾而聲明,這僅僅意味着,我們不能夠通過p這個名稱更改p的實體——a的值。由此可見,我們允許把引用綁定到const對象上,但是必須用const int &i=ci;這種形式,其中ci可以是也可以不是一個const對象,但是這個綁定操作之后i的值是不可更改的。
對一個常量的訪問也可以用指向常量的引用或者指向常量的指針實現,不同的是,指向常量的引用是常量的一個別名,不能夠被賦值,不過指向常量的指針本身是一個對象,它的本身值可以更改,不過指向的對象的值不能夠被更改。
如果不想讓指針指向的對象被更改,可以用const指針,int a=1;int *const p=a;這樣指針本身的值不可以被改變但是它指向的對象的值是可以被改變的。如const int *const p=a;這種語句使指針本身和它指向的對象值都不會被改變。
知識點6:P57,2.4.3,頂層const
(英文版64頁)
頂層const是對const而言的,“頂層”可以用來修飾const狀態的形容詞。一個const使對象本身的值固定,這個const就被稱為頂層const,一個const是對象指向或引用的對象成為固定值,這個const就被稱為底層const。頂層和底層const對拷貝來說密切相關,有相同底層const資格的兩個對象才能夠互相拷貝,而且頂層const聲明變量之后不允許再次改變const的值。int p,const int *a=&p;這種語句中的const就是底層const。
像int v1=9;const int *p=&v1;int *p2=p;這種語句如果能夠通過編譯,那么我們就可以使用p2的性質改變p1指向的常量的值,但是常量的值是不能夠被改變的,因此這種變相改變常量的值的表達式都是錯誤的。可以通過分析const級別得到表達式中常量是否被更改,從而判斷語句的正確性。
說到底,頂層底層說的是對拷貝控制的約束。總的規則就是“不能改變常量的值”。因此“拷入和拷出的對象都要有相同的底層const資格,或者兩個對象數據類型必須能轉換”,例如,有int *p1,const int *p2;。p1沒有底層const,p2有底層const。p1=p2;這時const int*不能轉換成int *(如果轉換,就違反了“不能改變常量的值這一約束條件”),因此p1=p2;不合法。p2=p1;int * 能夠轉換成const int *,因此p2=p1合法。
知識點7:P58,2.4.4,constexpr
(英文版65頁)
我們在了解constexpr之前,應該先了解常量表達式。所謂常量就是固定的量,那么常量表達式就是值固定不變的表達式,這里“值固定不變”,指的是程序編譯階段,常量表達式的值就能被確定下來之后也不能對其進行任何種方式的修改。因此這個固定,是編譯之后固定的。像cout<<1234<<endl;中的1234,就是常量表達式,顯然,字面值是常量表達式。
constexpr的作用之一就是幫助程序員在IDE的提示下查看一個賦值語句是不是常量表達式。使用的方式包含在聲明語句里面,形如constexpr 變量類型 變量名=右值;如果右值是一個常量,這條語句就是正確的。在所有函數體外聲明的全局變量的地址就符合“在編譯期間能確定,編譯后值不被改變”這兩個條件,因此也屬於常量。
另外,用constexpr聲明的指針(比如,constexpr int *p=&v1;中的*p,相當於int *const p=&v1; )都是頂層const,即指針本身值固定。但是指針指向的內容是可以變的。引用也一樣。
知識點8:P61,2.4.4,類型別名
(英文版68頁)
使用typedef int zhengxing;這種對簡單的類型名進行替換的方式無疑是非常直觀並且好理解的,但是在涉及到復雜的類型名的時候往往會出現各種各樣的問題。比如typedef char *Pstring;這條語句是不是就意味着我們看到Pstring就可以用char *替換呢。其實並不是,實際上類型別名不只是替換的規則,要復雜很多。比如我們遇到const Pstring a;的時候,按照替換的規則,這條語句就相當於const char * a;這里的const這種情況下是底層const,但是結果並不是這樣的,這條語句正確的等同語句應該是char *const a;是一個頂層const,即指針本身是一個常量。讓我們來分析一下為什么是這個樣子,不是簡單的替換就行了。
typedef char *Pstring;
這條語句就是說Pstring是一個類型別名,它是什么類型的類型別名呢?Pstring是 指向char的指針的類型別名 ,也就是說,這個類型修飾的對象必須是一個指針,這個指針也必須指向char而不能指向別的什么東西,比如,不能指向const char。我們再看看const Pstring a;這個語句,首先a一定是指向char的指針。所以這個前面的const應該是用來修飾這個指針本身。也就是說,這個指針是常量指針而非指向常量的指針。這一點非常重要。
const char * a
這個語句里面,實際上類型是const char,*是聲明符的一部分。我們說過,定義一個變量由兩部分組成,類型名和聲明符,聲明符可以是*或者&加上變量名的形式。而類型別名只是給類行一個別名,至於聲明符是怎樣並不管。因此在有const的情況下,就可以看出來這兩者之間的區別還是很明顯的。
知識點9:P61,2.5.2,auto和decltype類型聲明/指示符
(英文版69頁)
auto變量通過初始化語句,計算出右值的類型,並推導出左值的類型。這個過程中auto將會忽視頂層const和引用類型,可用const auto &a=i;這種方式顯式地指出了:指出要推導的結果是帶頂層指針屬性的或者是引用屬性的。auto推導多個值時,這些值的類型必須是一樣的。因為auto是利用初始化賦值,因為此它的行為基本上也和初始化有關。
decltype不通過計算,只通過推算出變量應有的值,表達式本身應有的值和函數的返回值來推導類型。對於變量類型,decltype保留頂層const和引用的屬性。對於表達式,解引用表達式(如:int i=1; int *p=&i; decltype (*p) a=i;中的*p,對p解引用是int &類型的)和帶括號的表達式,(如:decltype ((a+1)) c=i;)的結果都將是引用類型。因為decltype通過處理表達式得到結果,因此更詳細的內容在第四章將會被提到。有的表達式返回左值,有的表達式返回右值,返回左值的表達式在decltype類型推導下得到的將是引用的結果。
知識點10:P65,2.6.1,用關鍵字struct自定義數據結構
(英文版73頁)
使用struct關鍵字定義類的形式如struct 類名{數據成員類型1 數據成員名1;數據成員類型2 數據成員名2;};C++11規定可以給類內成員提供類內初始值用於初始化用我們自定義類創建的對象實例中的成員的值。形式如下:
struct MyClass
{
int student=0;
float numbers=1;
};
第二章補充說明的內容:
1.對於已知的類型范圍的編程假定:
為了很多情況下我們都會默認我們開發機的一些環境就是客戶機的使用環境。因為類型本身是依賴於機器的,因此這里也應該使用固定的類型值比如int32_t類型。另外,使用防越界溢出的良好編程風格也對程序有很大幫助。使用C++語言編程時,可以執行仍然是我們需要時刻考慮的問題。
2.不同語境下對象這個詞的不同含義:
通常情況下對象指一塊能存儲數據的內存空間,但是當我們想區分命名的對象和未命名的對象的時候,通常把命名了的對象叫做變量,相應地,這時並列提出的對象可能指的是普通的內存區間;當我們區分對象和值的時候,又衍生出來了左值和右值的概念(第4章),這時會把對象所具有的屬性作為對象,對象本身的內容作為值的說法被表述。因為一切都可以叫做對象,所以這是一個意義廣泛的概念。通常交流中我們想表達具體的概念的時候也會使用對象這個詞匯來表達我們的想法。但這時候要聯系上下文和語境來猜測這個表達出來“對象”究竟指的是什么。
3.關於extern:
extern外部變量聲明其實是在IDE進行編譯的時候告訴IDE,這有一個外部變量你要去別的地方找。因此我們應該掌握編譯鏈接這套流程才能夠更加方便的會用extern。假設有一個頭文件a.h,這個頭文件里面定義了int aaa=0;還有一個源文件b.cpp。這個b.cpp里面使用了extern int aaa;這樣的語句,那么這個b.cpp是編譯不了的。因為頭文件如果不被別的源文件引用,是不參與被編譯為obj的過程的,一旦它不參與這個過程,它里面聲明的aaa這個全局變量其實就不存在,因此在b.cpp里面外部生命一個不存在的變量自然就是非法的。另外,使用extern也要和static做區分並考量它在別的文件中會不會造成內存污染等問題。這里應該掌握分離式編譯的編譯和鏈接特性再使用extern比較好。