C語言到C++的改進

下面總結下C語言到C++後的一些改進,主要偏語法上哈

1. 類型敏感

//C and C++
unsigned int i = -1;    // ok
//C
unsigned int i = {-1};  // ok
//C++
unsigned int i = {-1};  // err int轉換到unsigned int需要收縮轉換


//C
float f = {3.14};  // warning 'const double ' to 'float '
//C++
float f = {3.14}; // err 從'double'轉換到'float'需要收縮轉換


NULL //類型不敏感
int n = NULL;    // ok
int n = nullptr; //err "std::nullptr_t" 類型的值不能用於初始化 "int" 類型的實體

2. C語言中的宏 -> C++中的const和內聯函數

首先,先來了解下C語言中符號常量宏

#define NUM 10

int main()
{
    //1. 類型不明確,由表達式確定
    //2. 編譯時被替換,調試困難
    //3. &NUM 獲取不到地址
    printf("%d\r\n",NUM + 3); 
    //printf("%d\r\n",&NUM);  //error '&' on constant
    system("pause");
    return 0;
}

OK,下面看一下const的使用

const int NUM = 10;

int main()
{
    //NUM = 5; //err “NUM” : 不能給常量賦值
    printf("%d\r\n",NUM + 3);
    printf("%p\r\n",&NUM);
    system("pause");
}

首先,給NUM賦值報錯,說明和上面define的功能一致。類型這個不用多說了,很明確是int類型,後面看下調試和取地址

下面再來看一下const的一些用法,使用const修飾指針

void test1(const char *str)
{
    //str[0] = 2; //err str指向的內容不可修改
    str = "123"; // ok
}

void test2(char * const str)
{
    str[0] = 2;     //ok
    //str = "123"; //err str不可修改
}

void test3()
{
    const char *str1 = nullptr;
    char * const str2 = nullptr;
    char *str3 = nullptr;
    //str2 = str1; err str2不可修改
    str1 = str2;
    str1 = str3;
    //str2 = str3; err str2不可修改
    //str3 = str1; err 無法從“const char *”轉換爲“char *”
    str3 = str2;
}

哈哈。看完上面的是不是比較蒙,沒事,其實上面的案例不用刻意去記,可以使用下面方法

//將其倒過來讀就會十分清晰
const char *str;  // str is a pointer to char const  
char const *str; // str is a pointer to const char
char * const str; // str is a const pointer to char
const char * const str; // str is a const pointer to char const

來分析下第一個,倒着翻譯過來就是 str 是一個指針 指向 char常量,說明str這個指針是可以改的,但是指向的內容是char const,不能修改。

然後上面代碼中的 const char * 不能轉 char * 這個應該也好理解了吧,char *的話可能會修改裏面的值,而const char * 不能修改裏面的值,所以編譯器當然不給我們過啦

OK,再來看C中有參宏,觀察如下代碼

#define MY_MAX(x,y) ((x) > (y) ? (x) : (y))

int main()
{
    int i = 1;
    int j = 2;
    printf("%d\r\n",MY_MAX(i,j));
    printf("%d\r\n",MY_MAX(++i,++j));
    system("pause");
    return 0;
}

通過運行結果可發現,上面的運行結果爲2,下面的運行結果爲4,等等,這裏有問題。很明顯我們的意圖是想對i和j都先自增後進行比較,按理運行結果爲3,這是爲什麼呢?因爲宏是替換的,每一個x,y都會被對應的替換,這是個宏的無解bug,所以注意使用有參宏的時候不要傳遞++,--值哦

那麼,我們其實可以使用函數拿來替換,當時相對於宏來說,函數的執行效率不是太高,因爲會有堆棧的消耗,那麼內聯函數就出來了,看如下代碼

inline int MY_MAX(int x, int y)
{
    return x > y ? x : y;
}

內聯函數有什麼好處呢?

自動優化  如果用函數性價比高,編譯後變函數,否則編譯後變宏
//1. debug 不會內聯,方便調試
//2. release纔會內聯
//3. 內不內聯由編譯器決定
//4. 一般inline寫在函數實現中,函數實現簡單纔會被編譯器內聯

我們可以觀察下release下的反彙編

 

    printf("%d\r\n", MY_MAX(i, j));
00E41040 6A 02                push        2  
00E41042 68 F8 20 E4 00       push        offset string "%d\r\n" (0E420F8h)  
00E41047 E8 C4 FF FF FF       call        printf (0E41010h)  
    printf("%d\r\n", MY_MAX(++i, ++j));
00E4104C 6A 03                push        3  
00E4104E 68 F8 20 E4 00       push        offset string "%d\r\n" (0E420F8h)  
00E41053 E8 B8 FF FF FF       call        printf (0E41010h)  

en...這個優化的已經挺乾淨的了,尷尬,直接把結果算出來拿去打印了。好了,內聯先到這裏,下一個知識點

3. C 指針 -> C++ 引用

首先來看一下指針和引用的用法,寫個兩數交換吧

void swap1(int *a, int *b)
{
    *a ^= *b;
    *b ^= *a;
    *a ^= *b;
}

void swap2(int& a, int& b)
{
    a ^= b;
    b ^= a;
    a ^= b;
}

int main()
{
    int i = 1;
    int j = 2;
    swap1(&i,&j);
    printf("%d %d\r\n",i,j);
    swap2(i,j);
    printf("%d %d\r\n",i,j);
    system("pause");
}

觀察運行結果,會發現引用和指針有着同樣的功能,那麼我們來看一下對應的反彙編代碼

    swap1(&i,&j);
00306C6C 8D 45 EC             lea         eax,[j]  
00306C6F 50                   push        eax  
00306C70 8D 4D F8             lea         ecx,[i]  
00306C73 51                   push        ecx  
00306C74 E8 DD A7 FF FF       call        swap1 (0301456h)  
00306C79 83 C4 08             add         esp,8 


void swap1(int *a, int *b)
{
00303410 55                   push        ebp  
00303411 8B EC                mov         ebp,esp  
00303413 81 EC C0 00 00 00    sub         esp,0C0h  
00303419 53                   push        ebx  
0030341A 56                   push        esi  
0030341B 57                   push        edi  
0030341C 8D BD 40 FF FF FF    lea         edi,[ebp-0C0h]  
00303422 B9 30 00 00 00       mov         ecx,30h  
00303427 B8 CC CC CC CC       mov         eax,0CCCCCCCCh  
0030342C F3 AB                rep stos    dword ptr es:[edi]  
    *a ^= *b;
0030342E 8B 45 08             mov         eax,dword ptr [a]  
00303431 8B 4D 0C             mov         ecx,dword ptr [b]  
00303434 8B 10                mov         edx,dword ptr [eax]  
00303436 33 11                xor         edx,dword ptr [ecx]  
00303438 8B 45 08             mov         eax,dword ptr [a]  
0030343B 89 10                mov         dword ptr [eax],edx  
    *b ^= *a;
0030343D 8B 45 0C             mov         eax,dword ptr [b]  
00303440 8B 4D 08             mov         ecx,dword ptr [a]  
00303443 8B 10                mov         edx,dword ptr [eax]  
00303445 33 11                xor         edx,dword ptr [ecx]  
00303447 8B 45 0C             mov         eax,dword ptr [b]  
0030344A 89 10                mov         dword ptr [eax],edx  
    *a ^= *b;
0030344C 8B 45 08             mov         eax,dword ptr [a]  
0030344F 8B 4D 0C             mov         ecx,dword ptr [b]  
00303452 8B 10                mov         edx,dword ptr [eax]  
00303454 33 11                xor         edx,dword ptr [ecx]  
00303456 8B 45 08             mov         eax,dword ptr [a]  
00303459 89 10                mov         dword ptr [eax],edx  
}



    swap2(i,j);
00306C91 8D 45 EC             lea         eax,[j]  
00306C94 50                   push        eax  
00306C95 8D 4D F8             lea         ecx,[i]  
00306C98 51                   push        ecx  
00306C99 E8 B3 A7 FF FF       call        swap2 (0301451h)  
00306C9E 83 C4 08             add         esp,8  


void swap2(int& a, int& b)
{
003033A0 55                   push        ebp  
003033A1 8B EC                mov         ebp,esp  
003033A3 81 EC C0 00 00 00    sub         esp,0C0h  
003033A9 53                   push        ebx  
003033AA 56                   push        esi  
003033AB 57                   push        edi  
003033AC 8D BD 40 FF FF FF    lea         edi,[ebp-0C0h]  
003033B2 B9 30 00 00 00       mov         ecx,30h  
003033B7 B8 CC CC CC CC       mov         eax,0CCCCCCCCh  
003033BC F3 AB                rep stos    dword ptr es:[edi]  
    a ^= b;
003033BE 8B 45 08             mov         eax,dword ptr [a]  
003033C1 8B 4D 0C             mov         ecx,dword ptr [b]  
003033C4 8B 10                mov         edx,dword ptr [eax]  
003033C6 33 11                xor         edx,dword ptr [ecx]  
003033C8 8B 45 08             mov         eax,dword ptr [a]  
003033CB 89 10                mov         dword ptr [eax],edx  
    b ^= a;
003033CD 8B 45 0C             mov         eax,dword ptr [b]  
003033D0 8B 4D 08             mov         ecx,dword ptr [a]  
003033D3 8B 10                mov         edx,dword ptr [eax]  
003033D5 33 11                xor         edx,dword ptr [ecx]  
003033D7 8B 45 0C             mov         eax,dword ptr [b]  
003033DA 89 10                mov         dword ptr [eax],edx  
    a ^= b;
003033DC 8B 45 08             mov         eax,dword ptr [a]  
003033DF 8B 4D 0C             mov         ecx,dword ptr [b]  
003033E2 8B 10                mov         edx,dword ptr [eax]  
003033E4 33 11                xor         edx,dword ptr [ecx]  
003033E6 8B 45 08             mov         eax,dword ptr [a]  
003033E9 89 10                mov         dword ptr [eax],edx  
}

en....很明顯是相同的兩份代碼,說明引用和指針本質上是沒有任何區別,使用引用只是編譯器幫我們幹了許多活。

那麼引用和指針就沒有區別了麼,那還是有的,觀察如下代碼

int main()
{
    int num1 = 3;
    int num2 = 7;
    
    int *p1 = &num1; //定義並初始化
    p1 = &num2; //更換  
    *p1 = 5;//修改指向的內容

    printf("%d %d\r\n", num1, num2); // 3 5

    //還原環境
    num1 = 3;
    num2 = 7;

    //使用引用重新模擬
    int& p2 = num1;
    p2 = num2;
    printf("%d %d\r\n", num1, num2); // 7 7

    p2 = 5;
    printf("%d %d\r\n", num1, num2); // 5 7

    system("pause");
}

觀察結果可得,引用只可初始化一次,並且不能變換引用對象,不可初始化常量,這個的話是分情況滴,看下面代碼

int main()
{
    const int a = 10;
    //常量都不可修改,使用引用無意義
    //int& pa = a; err  “初始化” : 無法從“const int”轉換爲“int &”
    

    const int& pa = 10; //ok
    //編譯器會做如下實際轉換
    // 1. int temp = 10;
    // 2. pa = &temp;
    system("pause");
}

可以觀察彙編代碼驗證

    const int& pa = 10; //ok
00D96E15 C7 45 E0 0A 00 00 00 mov         dword ptr [ebp-20h],0Ah   ;0x0A = 10 放入 ebp-20中
00D96E1C 8D 45 E0             lea         eax,[ebp-20h]  ;取地址
00D96E1F 89 45 EC             mov         dword ptr [pa],eax ;對pa賦值

4. 缺省參數

en...這個功能挺好的,因爲很實用,先來看代碼

struct Init{
    int a;
    int b;
    void init(int a, int b = 0)
    {
        this->a = a;
        this->b = b;
    }
};

int main()
{
    struct Init t1;
    t1.init(1); //如果不傳 b默認爲0
    printf("%d %d\r\n",t1.a,t1.b); // 1 0

    struct Init t2;
    t2.init(1,3);
    printf("%d %d\r\n", t2.a, t2.b);// 1 3
    system("pause");
}

可以看到,當我們對init傳遞一個參數時,另一個參數會被自動默認設置值,那麼這個參數真的是自動設置的麼,當然不可能,其實還是編譯器器幫我們傳值罷了,可以觀察其彙編代碼

    struct Init t1;
    t1.init(1); //b默認爲0
012A6FBE 6A 00                push        0  ;編譯器幫我們傳遞的參數
012A6FC0 6A 01                push        1  
012A6FC2 8D 4D F4             lea         ecx,[t1]  
012A6FC5 E8 96 A4 FF FF       call        Init::init (012A1460h)  


    struct Init t2;
    t2.init(1,3);
012A6FDF 6A 03                push        3  
012A6FE1 6A 01                push        1  
012A6FE3 8D 4D E4             lea         ecx,[t2]  
012A6FE6 E8 75 A4 FF FF       call        Init::init (012A1460h)  

哈哈,說明我們少做的事情,編譯器都會幫我們做,活是少不了滴,下面,我們來看一下缺省參數需要注意的事項

1. 缺省參數只能在右值(這個好理解吧,要不然編譯器都不知道幫我們傳啥值)
2. 聲明實現不能同時寫缺省參數(C++ 規定,在給定的作用域中只能指定一次默認參數)
3. 缺省參數在聲明中聲明,不要在實現中聲明

這裏的話可以補充下知識點了,C++的作用域

作用域是用來表示某個標識符在什麼範圍內有效。

C++的作用域主要有四種:
    1.函數原型作用域(即函數體去掉代碼塊的部分)
    2.塊作用域
    3.類作用域
    4.文件作用域(全局)

由大到小:文件作用域>類作用域>塊作用域>函數原型作用域

下面我們試一下函數聲明和實現都用缺省參數

struct Init{
    int a;
    int b;
    void init(int a, int b = 0);
};

void Init::init(int a, int b = 0)
{
    this->a = a;
    this->b = b;
}

編譯後是報錯了:“Init::init”: 重定義默認參數 : 參數 1 ,因爲此時都屬於類作用域

那麼我們稍修改代碼

struct Init {
    int a;
    int b;
    void init(int a, int b = 3);
};

void Init::init(int a = 2, int b)
{
    this->a = a;
    this->b = b;
}

int main()
{
    struct Init t1;
    t1.init(); 
    printf("%d %d\r\n", t1.a, t1.b); 

    system("pause");
}

運行結果2 和 3,說明缺省參數在聲明和定義時都有效哦,瞄一眼反彙編

    struct Init t1;
    t1.init(); 
00C5425E 6A 03                push        3  
00C54260 6A 02                push        2  
00C54262 8D 4D F4             lea         ecx,[t1]  
00C54265 E8 10 D1 FF FF       call        Init::init (0C5137Ah)  

5. 函數重載
函數重載屬於泛型編程中的一種手法,也是多態的一種體現。可以支持幾個功能類似的同名函數。這個功能對調用者來說是及其友好的,拿上面引用的例子來說,對於引用交換和指針交換都是交換,而當我調用時卻需要調用不同的函數名進行交換,對於邏輯上來說相對比較亂,那麼我們下面改進下交換函數

void swap(int *a, int *b)
{
    *a ^= *b;
    *b ^= *a;
    *a ^= *b;
}

void swap(int& a, int& b)
{
    a ^= b;
    b ^= a;
    a ^= b;
}

int main()
{
    int i = 1;
    int j = 2;
    swap(&i, &j); //call void swap(int *a, int *b)
    printf("%d %d\r\n", i, j);
    swap(i, j); // call void swap(int& a, int& b)
    printf("%d %d\r\n", i, j);
    system("pause");
}

這樣對於調用者來說舒服多了,那麼爲什麼這裏函數名相同,還是能調到呢?

說明函數名並不是該函數的唯一標識,這裏C++使用了一種名稱粉碎的機制,簡單點說就是重命名,例如我們可以將原名,類型編號,函數名,層級編號等進行重構(不同編譯器規則不同)

void fun()
{
    // snTest_int_fun_1
	static int snTest = 123;
    // snTest_*_fun_1  進行模糊查找,查找不到報錯
    printf("%d",snTest);
}

如上所示,這個只是打個比方,具體C++的名稱粉碎機制暫時先不深入研究了。

回到函數重載,這裏的重載調用時,會根據參數自動匹配。那麼如何才能算是重載構成的條件呢

1. 必須同作用域
2. 函數名稱相同
3. 參數 數量/類型/順序 不同

首先,作用域這個是前提條件哦,一定不能忘,第二,可能大家會有個疑惑,爲什麼返回值不能構成重載?

我們可以觀察下如下代碼:

int test()
{
    return 0;
}

void test()
{
    return;
}

int main()
{
    int i = test(); //一定調用 int test() 麼?
    test(); //一定調用 void test() 麼?
    system("pause");
}

上面的兩個結果分析下就知道很顯然不是的,雖然有些函數有返回值,但是沒有規定說函數有返回值就必須要接收,所以此時編譯器是無法從上下文中唯一確定函數的意思的。

重載雖然好,但是也有一些弊端,先看如下代碼:

int test(int a,int b=0)
{
    return 0;
}

void test(int a)
{
    return;
}

int main()
{
    //test(1); //err 有二義性(有多個 重載函數 "test" 實例與參數列表匹配)
    system("pause");
}

此時就造成了二義性問題,下面再來探究一些二義性的問題

首先看第一種情況,可以構成重載,比較好理解,引用類型的本質是指針,類型不一致

TYPE& 和 TYPE 構成重載

void test(int& a)
{

}

void test(int a)
{

}

這裏你可能會想到傳 test(10); 不會二義性麼?是的,不會,因爲引用不能賦值常量,所以會調test(int)函數

第二種情況,也可以構成重載,這個也比較好解釋,因爲有const其內容值是不能改變的,OK

const TYPE& 和 TYPE& 構成重載
const TYPE* 和 TYPE* 構成重載

void test(const int& num)
{

}

void test(int& num)
{

}

void test(const int* num)
{

}

void test(int* num)
{

}

第三種情況,不構成重載,爲啥呢,具體可以看下面解釋

1.TYPE* const 和 TYPE* 不構成重載
2.TYPE& const 和 TYPE& 不構成重載
3.const TYPE 和 TYPE 不構成重載

//err xxx已有主體
以上三者的情況相同,爲啥呢?因爲當const在中間時,說明的是該指針或者引用不可改,但是我們函數傳參
時,只是把值拷貝傳參而已,當然其值也可以拷貝給無const類型的。

//拿TYPE = char可以試驗下

    char * const str1 = "123";
    char * str2 = str1; //ok

    //所以無法區分

最後一種情況,使用typedef,這個也是不構成重載的

typedef int MYINT;

//err 函數“void test(MYINT)”已有主體
void test(MYINT a)
{

}
void test(int b)
{

}

這裏的話我們不用去記太多,只需考慮調用傳參能不能區分,能區分則可重載

OK,差不多就這樣了,先總結到這

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章