《C++高級進階》讀書筆記 第一章 C++基礎知識 -开发者知识库

《C++高級進階》讀書筆記 第一章 C++基礎知識 -开发者知识库,第1张

1.2 文字常量和常變量

1、文字常量 p3

程序中的特殊標識符表達式,由於同時滿足:
(1)不可尋址(放在代碼區)
(2)值不可變
所以可視為文字常量。他們是 靜態數組名、枚舉變量、全局(靜態變量)首地址、#define定義的常量

整型文字常量:
(1)前加0表示 八進制
(2)前加0x表示 十六進制
(3)后加L(推薦)或l,表示long類型
(4)后加U(或u)表示無符號數

eg.1024UL

2、常變量Const

同其他變量一樣被分配空間,可尋址。

const是在高級語言的語義層面上定義的,是編譯器在編譯期做語法檢測來保證,但是運行時,const變量不是在只讀內存中,而是和一般變量一樣放在數據區,所以一樣可以對其進行修改。

所以:常變量是一種加了特殊限制的變量,理解成“只讀”變量

即使是const修飾,也是可以修改的

#include <iostream>
using namespace std;

void ShowValue(const int &i) {
cout<<i<<endl;
}

int main()
{
const int j=5;
void *p=(void *)&j;
int *ptr=(int *)p;
(*ptr) ;
//cout<<j<<endl; //還是會顯示5,因為編譯器優化的時候將j替換為文字常量5
//但如果是int i=5; const int j=i; 則無法替換,直接輸出j為6
ShowValue(j); //顯示6

return 0;
}

3、常變量替換

如果常變量有初始化賦初值,那編譯器將該常變量在其他地方替換成文字常量
但是如果開始不初始化就會錯誤

如:

void DefineArray(const int n){
int B[n]={}; //error,數組大小在編譯期確定
}

int main(){
const int m=5;
int A[m]={}; //ok
}

4、文字常量和常變量尋址

int &r=5; //error,無法尋址文字常量,無法建立引用

const int &r=5; //ok,在數據區開辟一個值為5的無名整數量,然后將引用r與這個整形兩綁定

1.3 const用法

1、const的位置

int const *p; //指向常量的指針(即常指針,const修飾的是int),指向的對象是const型,不可以修改,但是指針p的指向可以修改
int *const p; //指針常量(const修飾的是int*),指針變量p是const型,它的指向不可修改,但是指向的對象可以修改

const和數據類型結合在一起 —>“常類型”。(看成一個整體)

修飾類型時,既可以放在放前面,也可以放在后面;用常類型聲明 or 定義變量,const只出現在變量前

const和被修飾類型間不能有其他標識符存在。

引用本身可以理解成一個指針常量

故在引用前使用const沒有意義

int & const r4=i; //const是多余的,編譯器warning后忽略const存在

const配合二重指針,此例子中const在不同位置,結果不同

#include <iostream>
using namespace std;

int main()
{
int const **p1; //不是指針常量,指向 int count*(“int const*”是一個 指向整型常量的指針)
int *const *p2; //不是指針常量,但所指的變量是指針常量(int *const,即指向整型的指針常量,指向不能修改)

int i=5;
int j=6;

const int *ptr1=&i;
int *const ptr2=&j;

p1=&ptr1;
p2=&ptr2;

cout<<**p1<<endl;
cout<<**p2<<endl;

return 0;
}

輸出:
5
6

上述p1和p2 賦值有講究,如果 p1=&ptr2 或 p2=ptr1 就會編譯錯誤

2、const修飾某個類 —> 常對象 和 常函數

const修飾對象–>常對象
const修飾成員函數—>常函數

在常函數中,不允許對任何成員變量進行修改

通過常對象,只能調用該對象的常函數

#include <iostream>
using namespace std;

class A
{
int num;
public:
A() {num=5;}
void disp();
void disp() const;
void set(int n) {num=n;}

};

void A::disp() const {
cout<<num<<endl;
}

void A::disp() {
cout<<"non-const version of disp()"<<endl;
}

int main()
{
A a1;
a1.set(3);
a1.disp();
A const a2;
a2.disp();
}

以上注意:
(1)如果常函數聲明和定義分開,都需要加const,否則編譯錯誤

只有類的非靜態成員函數可以被聲明為常函數

(2)如果一個類的兩個成員函數,返回值、函數名、參數列表完全相同,其中之一是const,則重載。因為 常成員函數的參數傳入this指針是const Class*類型的,參數不同,導致函數簽名不同。

非只讀對象(如a1)調用某個函數(如 disp()),先找非const版本,如果沒有,再調用const版本。而常對象,只能調用類中定義的常函數,否則編譯器報錯。

如果一個非const對象(如a1)調用函數,同時有const和非const版本的函數,我們希望其調用const函數。就必須建立該對象的常引用,或指向該對象的常指針來達到目的。如: (const A&)a1.disp(); 或 (const A *)&a1->disp();

(3)常對象創建后,其數據成員不允許在修改。所以顯示構造函數來初始化該對象非常重要。

常對象,全體成員數據成員都是常量看待。
類對象的非靜態常量成員必須在構造函數中初始化,且只能借助初始化列表進行。

3、const修飾函數參數 函數返回值

#include <iostream>
using namespace std;

void disp1(const int &ri){
cout<<ri<<endl;
}

void disp2(const int i){
cout<<i<<endl;
}

const int disp3(const int& ri){
cout<<ri<<endl;
return ri;
}

int& disp4(int& ri){
cout<<ri<<endl;
return ri;
}

const int& disp5(int& ri){
cout<<ri<<endl;
return ri;
}


int main(int argc,char* argv[])
{
int n=5;
disp1(n);
disp2(n);
disp3(n);
disp4(n)=6; //修改引用返回值
disp5(n);//disp5(n)=6;是錯誤的
}

注意:
(1)const修飾參數,主要作用是被引用對象被指向對象,如果只是形參,就沒有多少意義。如:void disp2(const int i),這里的i在函數中改不改變,加不加const沒什么影響。

不但如此,同時定義一個相似的用const修飾參數和不用const修飾參數的函數,會引起重定義錯誤。比如:任何整型表達式的值,都可以傳給int型參變量,也可以傳給const int型參變量,故不重載。

(2)當返回值是一個普通數據,而非引用,const修飾也沒多少意義。因為函數返回值是一個非左值,本來就不能改變其值。故其上 const int disp3(const int& ri),對返回值修飾然並卵。

(3)如果返回值為引用,用const修飾可以阻止對被引用對象修改,disp5(n)=6;是錯誤的

(4)常見的對const的誤解。

誤解一:用const修改的變量值一定是不能改變的。const修飾的變量可通過指針可間接修改。

如:

const int j=5;
void *p=(void *)&j;
int *ptr=(int *)p;
(*ptr) ;

誤解二:常引用或常指針,只能指向常變量,這是一個極大的誤解。常引用或者常指針只能說明不能通過該引用(或者該指針)去修改被引用的對象,至於被引用對象原來是什么性質是無法由常引用(常指針)決定的。

1.4 const_cast 的用法

1、作用

const_cast 是 C 運算符,作用是去除符合類型中的const或者volatile

當大量使用const_cast是不明智的,只能說程序存在設計缺陷。使用方法見下例:

void constTest(){
int i;
cout<<"please input a integer:";
cin>>i;
const int a=i;
int& r=const_cast<int&>(a);//若寫成int& r=a;則發生編譯錯誤
r;
cout<<a<<endl;
}
int main(int argc,char* argv[])
{
constTest();
return 0;
}

輸入:
5

輸出:
6

總結:
(1)const_cast運算符的語法形式是const_cast< type> (expression)。 括號不可省略

(2)const_cast只能去除目標的const或者volatile屬性,不能進行不同類型的轉換。只能將 const type* 轉換為 type*,或者 const type & 轉換為 type &。
如下轉換就是錯誤的:

cons tint A={1,2,3}; 
char* p=const_cast< char*>(A); //不能由const int[]轉換為char*

(3)一個變量被定義為只讀變量(常變量),那么它永遠是常變量。cosnt_cast取消的是間接引用時的改寫限制,而不能改變變量本身的const屬性。 如下就是錯誤的:

int j = const_cast< int> (i);

(4)利用傳統的C語言中的強制類型轉換也可以將 const type* 類型轉換為 type* 類型,或者將 const type& 轉換為 type& 類型。但是使用 const_cast 會更好一些,因為 const_cast 寫法復雜(提醒程序猿不要輕易轉換),轉換能力較弱,目的明確,不易出錯,易查bug;而C風格的強制類型轉換能力太強,風險較大。

1.5 mutable 的用法

1、作用

mutable 用來解決常函數中不能修改對象的數據成員的問題。

如果在一些情況下,希望在常函數中仍然可以修改某個成員變量的值,就在該變量前加上mutable。能在保證常量對象大部分數據成員仍然“只讀”情況下,實現對個別成員的修改。

#include <iostream>
#include <string>
using namespace std;

class Student
{
string Name; //默認為private
int times_of_getname;
public:
Student(char *name)
:Name(name),times_of_getname(0) {

}

string get_name() {
times_of_getname ;
return Name;
}

void PrintTimes() const {
cout<<times_of_getname<<endl;
}
};

int main()
{
const Student s("Bill王");
cout<<s.get_name()<<endl;
cout<<s.get_name()<<endl;
s.PrintTimes();

return 0;
}

如上程序會報錯,因為常量對象s(信息不能被修改的學生實體),調用 非const 函數 get_name(),但是如果將 get_name() 改為 const,有無法修改 times_of_getname。

但如果修改為:

mutable int times_of_getname;

string get_name() const{
}

即可

2、使用注意事項

使用關鍵字mutable要注意以下幾點:

(1)mutable 只用於類的非靜態和非常量數據成員。

(2)mutable 關鍵字提示編譯器該變量可以被類的const函數修改。

(3)一個類中,用mutable修飾的變量只能是少數,或者根本不使用,大量使用代表程序設計上的缺陷。

1.6 求余運算符

1、概覽

%用於求余數,優先級與*和/相同,結合律也是從左至右。

要求兩個操作數均為整數(或可以隱式轉換成整數的類型),故:14.2%3就是錯誤的,因為double不能隱士轉換為整形。

#include <iostream>
using namespace std;

int main()
{
char c=253;
int i=5;
cout<<c%2<<endl;
cout<<i%c<<endl;
cout<<19%10%5<<endl;

return 0;
}

輸出:
-1
2
4

在c/c 中,char可視作單字節整形,取值范圍-128~127,故可以參與求余。

253對應的二進制是0xFD,即-3的補碼表示,C99標准規定:

如果%是正數,有 a%(-b) == a%b;如果%左邊是負數,有(-a)%b == -(a%b)

有因為%是從左向右結合,所以19%5相當於(19)%5,結果是4

1.7 sizeof 的用法

C 基本數據類型的變量占據內存字節數的多少跟運行的平台有關。

int i = 9;
sizeof(i); // 4
sizeof(i=5); // 4
cout<<i; //9

因為 sizeof 是在編譯時進行的運算,而與運行時無關,即可執行代碼中不包含sizeof。sizeof 真正關心的是變量或表達式的類型,而不是值。在sizeof看來,i 和 int 是一樣的。

類或結構體的sizeof

(1)不允許有長度為0的數據類型存在,至少占用1自己;

(2)類的成員函數(虛函數不是函數占空間)不占類的大小;

(3)內存對齊是為了提高cpu的存儲速度。需要滿足如下原則

1、VC 規定各成員變量存放的起始地址相對於對象的其實地址的偏移量必須為該變量的類型所占字節數倍數。

2、整個對象的大小必須是其成員變量最大尺寸的整數倍。

(4)如果一個類包含虛函數,那么編譯器會在該類對象中插入一個指向虛函數表的指針。

1.8 引用與指針常量

引用變量是c 引入的重要機制。

錯誤觀念:引用本質只是別名,在符號表中ri和i對應於相同的變量地址

int i=5;
0100437E mov dword ptr [i],5
int &ri=i;
01004385 lea eax,[i]
01004388 mov dword ptr [ri],eax
ri=8;
0100438B mov eax,dword ptr [ri]
ri=8;
0100438E mov dword ptr [eax],8

在底層實現上,引用是用指針常量實現的,如果用指針常量實現,反匯編是一毛一樣的。引用和指針常量關系如下:

(1)內存中占用都是4字節,存放都是被引用對象的地址,都必須在定義的同時進行初始化。

(2)指針常量本身允許尋址;引用變量不允許尋址,&r返回的是被引用對象的地址,就是變量r中的值,而不是變量r的地址,r的地址由編譯器掌握,程序猿無法直接對其進行存取。

(3)凡用引用的代碼,都可以用指針常量實現;反之不然,因為引用本身限制較多,不能完成指針所有的操作。

例如下面的代碼是合法的:

int i=5,j=6;
int *const array[] = {&i,&j}; //指針數組

但是如下代碼卻是非法的:

int i=5,j=6;
int &array[] = {i,j}; //不可能有引用的數組

雖然引用在初始化時會綁定一個變量,不過也是可以有特殊手段可以改變引用綁定關系:

int main()
{
//freopen("input.txt","r",stdin);\

int i=5,j=6;
int &r = i;
void *pi = &i,*pj = &j;
int* addr;
int dis = (int)pj-(int)pi;
addr = (int *)((int)pj dis); //精確計算r的地址

cout<<"&i == "; PRINT(pi); //i的地址
cout<<"&j == "; PRINT(pj); //j的地址
cout<<"&pi == "; PRINT(&pi); //pi的地址
cout<<"&pj == "; PRINT(&pj); //pj的地址
cout<<"dis == "; PRINT(dis);
PRINT(*addr);

//(*add) =dis;
(*addr)=(int)&j; PRINT(*addr); //將r指向j

r=666;
cout<<"i == "; PRINT(i);
cout<<"j == "; PRINT(j);

return 0;
}

答案輸出:

&i == 003CF9D0
&j == 003CF9C4
&pi == 003CF9AC
&pj == 003CF9A0
dis == -12
3996112
3996100
i == 5
j == 666

因為在內存中排布方式是:

dis 低地址
addr
pj
pi
r
j
i 高地址


&j == 003CF9C4
&pi == 003CF9AC
中隔了2個dis,其實就是夾了一個r

1.9 左值的概念

左值是c 中的一個基本概念,凡是可以出現在賦值運算左邊的表達式都是左值。右值跟左值相對,凡是可以出現在賦值運算右邊的表達式都是右值。

左值一定可以作為右值,而反過來不一定成立。

左值概念有:

(1)必須可尋址
(2)非只讀單元
(3)不能是臨時無名對象 如: i 1 =5 不行
(4)如果表達式運算結果是一個引用,可以作為左值。 如:int &fun() 函數可以,(i =1)=5 可以,i =運算結果是對i的引用

由此可知:
(1)並不是只有單個變量才能作為左值
(2)也不能僅由表達式的外在形式判斷是否為左值。要根據一個表達式的運算結果的性質判斷。

結合引用的性質可知:
(1)能建立普通引用的表達式一定是左值;
(2)不能作為左值的表達式只能建立常引用,而不能建立普通引用。

函數的參數聲明為引用,這樣在發生函數調用時可以減少運行時開銷。將函數的參數聲明為一般的引用還是聲明為常引用很有講究。

int Max(int &a,int &b) {
return (a>b)?a:b;
}

int main()
{
//freopen("input.txt","r",stdin);

int i=2;
cout<<Max(i,5)<<endl;

return 0;
}

以上代碼編譯不通過,顯示“非常引用的初始值必須為左值”,由於5不是左值,不能為它建立普通引用,所以編譯錯誤。於是在此時修改函數定義

int Max(int &a,const int &b);

可見:

將函數的參數聲明為常引用,不完全是因為參數的值在函數體內不能修改,還可能是接受非左值作為函數參數的情況。

常引用類型轉換

對某個變量(或表達式)建立常引用,允許發生類型轉換,而一般的引用不允許

int Max(const int &a,const int &b) {
return (a>b)?a:b;
}

int main()
{
//freopen("input.txt","r",stdin);

char c='a';
const int &rc = c;
PRINT((void*)&c);
PRINT((void*)&rc);

int i=7;
const int &ri = i;
PRINT((void*)&i);
PRINT((void*)&ri);
cout<<Max(rc,5.5)<<endl;

return 0;
}

輸出:

《C++高級進階》讀書筆記 第一章 C++基礎知識 -开发者知识库,這里寫圖片描述,第2张

如果將下句的const去掉則會報錯。原因是普通引用只能建立在相同的數據類型變量上。同樣,能允許 Max(rc,5.5) 這樣的函數調用也是因為函數 Max() 的第二個參數是常引用,因此將實參 5.5 先轉換為 int 型無名變量,然后再建立對該無名變量的常引用。

const int &rc = c;

對表達式建立常引用:

(1)首先要考慮該表達式結果是否能尋址
(2)其次還要考慮表達式結果的數據類型與引用數據類型是否一致
否則只能另外建立一個無名變量,該變量中存放非左值表達式的運算結果,然后再建立對該無名變量的常引用。

不過既然這里建立的是一個臨時的無名變量,但這個變量同時也占用棧空間,那就可以相辦法改變,而且改變這個值不會改變原有的變量:

int main()
{
//freopen("input.txt","r",stdin);

char c='a';
const int &rc = c;
PRINT(c);
PRINT(rc);
PRINT((void*)&c);
PRINT((void*)&rc);

int dis = (int)&rc-(int)&c;
PRINT(dis);
int* addr = (int*)((int)&c dis); //精確計算rc指向的無名變量的地址

*addr = 666;
PRINT(c);
PRINT(rc);

return 0;
}

輸出:

《C++高級進階》讀書筆記 第一章 C++基礎知識 -开发者知识库,這里寫圖片描述,第3张

1.10 goto語句

goto是一種無條件跳轉語句。

1974年,D. E. Knuth 對於 goto 語句的爭論作了全面公正的評述:

(1)不加限制的使用 goto,特別是使程序往回跳,會使程序的結構難於理解

(2)為提高程序效率(主要指的是跳出多層循環),同時又不破壞程序良好結構,有控制的使用一些 goto 是有必要的。

如此我可以總結一下 goto 語句的一般使用情景:

(1)跳出多層循環
(2)不往回跳

注:

(1)一個帶冒號的“標號”,代表的是程序中的某條語句在內存中的位置,必須跟在某條可執行語句之前。

(2)goto 語句只能跳轉到同一個函數體內的標號。因為每個函數有一個建立函數棧的過程,而直接跳過去,代表什么呢?顯然沒有一個建立函數棧的過程。

1.11 volatile的用法

volatile 是“易變的”、“不穩定”的意思。volatile是 c 的一個關鍵字,用來解決在“共享”環境下容易出現的讀取錯誤的問題。

在單任務的環境中,一個函數體內部,如果在兩次讀取變量的值之間的語句沒有對變量的值進行修改,那么編譯器就會設法對可執行代碼進行優化。由於訪問寄存器的速度要快過RAM(從RAM中讀取變量的值到寄存器),以后只要變量的值沒有改變,就一直從寄存器中讀取變量的值,而不對RAM進行訪問。

這雖然在單任務環境下是一個優化過程,但是卻是多任務環境下問題的起因。

多任務環境中,雖然在一個函數體內部,在兩次讀取變量之間沒有對變量的值進行修改,但是該變量仍然有可能被其他的程序(如中斷程序、另外的線程等)所修改。如果還是從寄存器而不是從RAM中讀取變量的值,就會出現被修改了的比阿郎的之不能及時的反應的問題。如下程序對這一現象進行了模擬:

#include <iostream>
using namespace std;

int main(int argc,char* argv[])
{
int i=10;
int a=i;
cout<<a<<endl;

_asm{
mov dword ptr [ebp-4],80
}

int b=i;
cout<<b<<endl;
return 0;
}

程序在VS2012環境下生成 release 版本(一定要極端優化,vs編譯環境下選擇優化 速度最大化 /O2),輸出結果也是:
10
10

順便說一下,ebp是擴展基址指針寄存器(extended base pointer) 其內存放一個指針,該指針指向系統棧最上面一個棧幀的底部。

本來事實上已經通過內聯匯編,修改過的值,為什么打印出來還是10呢

但是如果:

將 int i=10; 前加 volatile 就不會發生這種情況了。

跟蹤匯編代碼可以發現,凡是聲明為 volatile 的變量,每次拿到的值都是從內存中直接讀取的。

以下實驗在 vs2012 release 環境下進行。

不加 volatile

    int i=10;
int a=i;
tmp(a);
00D71273 push dword ptr ds:[0D73024h]
00D71279 mov ecx,dword ptr ds:[0D7303Ch]
00D7127F push 0Ah
00D71281 call dword ptr ds:[0D7302Ch]
00D71287 mov ecx,eax
00D71289 call dword ptr ds:[0D73028h]

_asm{
mov dword ptr [ebp-4],80
00D7128F mov dword ptr [ebp-4],50h
}

int b=i;
tmp(b);
00D71296 push dword ptr ds:[0D73024h]
00D7129C mov ecx,dword ptr ds:[0D7303Ch]
00D712A2 push 0Ah
00D712A4 call dword ptr ds:[0D7302Ch]
00D712AA mov ecx,eax
00D712AC call dword ptr ds:[0D73028h]

加了 volatile

    tmp(a);
01201274 push dword ptr ds:[1203024h]
volatile int i=10;
0120127A mov dword ptr [i],0Ah
int a=i;
01201281 mov eax,dword ptr [i]
tmp(a);
01201284 mov ecx,dword ptr ds:[120303Ch]
0120128A push eax
0120128B call dword ptr ds:[120302Ch]
01201291 mov ecx,eax
01201293 call dword ptr ds:[1203028h]

_asm{
mov dword ptr [ebp-4],80
01201299 mov dword ptr [i],50h
}

int b=i;
012012A0 mov eax,dword ptr [i]
tmp(b);
012012A3 push dword ptr ds:[1203024h]
012012A9 mov ecx,dword ptr ds:[120303Ch]
012012AF push eax
tmp(b);
012012B0 call dword ptr ds:[120302Ch]
012012B6 mov ecx,eax
012012B8 call dword ptr ds:[1203028h]

由於編譯器的極端優化,可以很明顯的看到,在沒有加 volatile 的情況下,甚至編譯器是直接使用操作數 0Ah 進行運算的。

而在加了 volatile 的情況下,每次都是從 ptr [i] 中讀取。

而且在速度極端優化的情況下,

void tmp(int t) {
cout<<t<<endl;
}

也自動 inline 處理了。

但是這里也拋出一個問題,為什么是 [ebp-4] 修改的就是i的值,更奇怪的是,我如果如下這樣寫代碼,那改的會是哪個變量的值呢:

#include <iostream>
using namespace std;

void tmp(int t) {
cout<<t<<endl;
}

int main(int argc,char* argv[])
{
volatile int ic=12;
volatile int i=10;
int a=i;
volatile int ib=11;
tmp(a);
tmp(ib);
tmp(ic); //必須使用,如果不使用,編譯器優化為使用同一塊內存地址

_asm{
mov dword ptr [ebp-4],80
}

int b=i;
tmp(b);

return 0;
}

為什么分配的總是 [ebp-4] 是復制給 a 的值呢?試驗過,如果將 ic 賦值給 a,那 [ebp-4] 存放的值將會是 ic

《C++高級進階》讀書筆記 第一章 C++基礎知識 -开发者知识库,這里寫圖片描述,第4张

閱讀以上程序,注意以下幾個要點:
(1)以上代碼必須在release模式下考查,因為只有Release模式(嚴格說需要在速度最大優化 /O2)下才會對程序代碼進行優化,而這種優化在變量共享的環境下容易引發問題。

(2)凡是需要被多個任務共享的變量(如可能被中斷服務程序訪問的變量、被其他線程訪問的變量等),都應聲明為 volatile 變量。而且為了提高執行效率,要減少對 volatile 不必要的使用。

(3)由於優化可能會將一些“無用”的代碼徹底去除,所以,如果確實希望在可執行文件中保留這部分代碼,也可以將其中的變量聲明為 volatile:

int main(int argc,char* argv[])
{
int s,i,j;
for(i=0;i<100; i)
for(j=0;j<100; j)
s=5;

return 0;
}

在生成 release 版本的程序時,由於循環體每次給 s 的值不變(簡化為執行1次),或者說沒有使用(1次都沒有),但如果此時程序猿是希望循環拖延時間,寫成 volatile 就可以了。

1.12 typedef 的用法

最佳答案:

本文经用户投稿或网站收集转载,如有侵权请联系本站。

发表评论

0条回复