Cpp-入門


title: 入門
date: 2019-03-13 21:10:33
tags:
- Cpp
categories:
- Cpp
toc: true

從 C 到 C++

  1. cout中的 c 指的是 console。

  2. endl => end line。

  3. 作用域限定符::相當於中文,表示作用域或所屬關係。

  4. extern "C"

    有時候在 C++ 工程中可能需要將某些函數按照 C 的風格來編譯,在代碼前加 extern “C” ,意思是告訴編譯器,將該函數按照C語言規則來編譯。

     extern “C”
     {
     	something();
     }
    
  5. typeid(變量名).name輸出變量的類型。

  6. bool類型,true 爲真,false 爲假。

    任何基本類型都可以隱士轉換爲 bool 類型,非 0 即真,0 即假。

    /* 
    	boolalpha -> 相當於一個開關,表示開,打印 true / false
    	noboolalpha -> 表示關,關閉後打印 0 / 1
    */
    bool a = false;
    cout << a << " " << boolalpha << a << endl;
    a = 20;
    cout << noboolalpha << a << endl;
    a = *("abc"+3); // 0->a 1->b 2->c 3->\0
    cout << boolalpha << a << endl;
    /*
        這三種意思相同
        a = *("abc"+3);
        a = "abc"[3];
        a = 3["abc"];
     */
    

命名空間(namespace)

在 C/C++ 中,變量、函數和後面要學到的類都是大量存在的,這些變量、函數和類的名稱將都存在於全局作用域中,可能會導致很多衝突。使用命名空間的目的是對標識符的名稱進行本地化,以避免命名衝突或名字污染,namespace 關鍵字的出現就是針對這種問題的。

  1. 相同名字的 namespace 作用域相同,同一個工程中允許存在多個相同名稱的命名空間,編譯器最後會合成同一個命名空間中。

  2. 命名空間的作用:避免名字衝突、劃分邏輯單元,名字空間中的聲明和定義可以分開。

  3. 名字空間可以嵌套,使用時候得一層一層的扒皮。

  4. 名字空間也可以賦值取別名,🌰 栗子:

    namespace A1
    {
        namespace A11
        {
            namespace A12
            {
                something();
            }
        }
    }
    namespace A = A1::A11::A12;
    // A1::A11::A12::something(); 等價於 A::something();
    
  5. using namespace A1;就相當於”裸奔“ ,把 A1 中的東西暴露在當前作用域下。

    using namespace std;也是一樣,把 cout 暴露在全局下。

    風險:可能會出現命名衝突,一般還是帶上::

函數

缺省參數

缺省參數是聲明或定義函數時爲函數的參數指定一個默認值。在調用該函數時,如果沒有指定實參則採用該默認值,否則使用指定的實參。

全缺省參數

void TestFunc(int a = 10, int b = 20, int c = 30);

半缺省參數

void TestFunc(int a, int b = 10, int c = 20);
  1. 缺省參數必須從右開始設置。

    /*ERROR*/ void fun(int a = 3,char b,char *c = "ahoj");
    
  2. 缺省參數不能在函數聲明和定義中同時出現,建議聲明時指定。如果生命與定義位置同時出現,恰巧兩個位置提供的值不同,那編譯器就無法確定到底該用那個缺省值。

  3. 缺省值必須是常量或者全局變量。

  4. C語言不支持(編譯器不支持)。

啞元

只指定類型而不指定名稱的函數,佔着茅坑不拉屎。

🌰 栗子:

void ya(int,int b)
{
    cout << b << endl;
}
int main()
{   
    ya(10,100);
    return 0;
}
  1. 兼容老版本。
  2. 支持函數重載。

重載

自然語言中,一個詞可以有多重含義,人們可以通過上下文來判斷該詞真實的含義,即該詞被重載了。
比如:以前有一個笑話,國有兩個體育項目大家根本不用看,也不用擔心。一個是乒乓球,一個是男足。前者是“誰也贏不了”,後者是“誰也贏不了” 。

函數重載:是函數的一種特殊情況,C++ 允許在同一作用域中聲明幾個功能類似的同名函數,這些同名函數的形參列表(參數個數 / 類型 / 順序)必須不同,常用來處理實現功能類似數據類型不同的問題。

🌰 栗子:

void foo(){
    cout << "void foo();" << endl;
}
void foo(int a){
    cout << "void foo(int a);" << endl;
}
void foo(int a,int b){
    cout << "void foo(int a,int b);" << endl;
}
void foo(int a,double b){
    cout << "void foo(int a,double b);" << endl;
}
void foo(double a,int b){
    cout << "void foo(double a,int b);" << endl;
}

int main()
{
    foo();
    foo(1);
    foo(1 , 2);
    foo(1 , 3.14);
    foo(3.14 , 1);
}
  1. 同一作用域,函數名相同,參數表不同的函數。
  2. 參數表不同:
    • 參數類型不同
    • 參數個數不同
    • 參數順序不同
  3. 重載和形參名無關。
  4. 重載和返回類型無關。
  5. 不同作用域同名函數遵循就近原則。

重載的原理

nm a.out,查看 C++ 編譯器給看書取得名字:

 00000001000010a0 T __Z3Maxii
 0000000100000de0 T __Z3foodi
 0000000100000cf0 T __Z3fooi
 0000000100000d90 T __Z3fooid
 0000000100000d40 T __Z3fooii
 0000000100000b50 T __Z3foov

C++ 函數重載通過編譯器改名實現。

名字修飾(Name Mangling)

在 C/C++ 中,一個程序要運行起來,需要經歷:預處理、編譯、彙編、鏈接。

Name Mangling 是一種在編譯過程中,將函數、變量的名稱重新改編的機制,簡單來說就是編譯器爲了區分各個函數,將函數通過某種算法,重新修飾爲一個全局唯一的名稱。

C語言的名字修飾規則非常簡單,只是在函數名字前面添加了下劃線。

C++ 要支持函數重載、命名空間等,使得其修飾規則比較複雜,不同編譯器在底層的實現方式可能都有差
異。

被重新修飾後的名字中包含了:函數的名字以及參數類型。這就是爲什麼函數重載中幾個同名函數要求其參數
列表不同的原因。只要參數列表不同,編譯器在編譯時通過對函數名字進行重新修飾,將參數類型包含在最終
的名字中,就可保證名字在底層的全局唯一性。

📒 文章:

C++的函數重載

引用(&)

引用不是新定義一個變量,而是給已存在變量取了一個別名,編譯器不會爲引用變量開闢內存空間,它和它引用的變量共用同一塊內存空間。

李白 <=> 李太白 青蓮居士 詩仙 ……

🌰 栗子:

void foo(int& a)
{
    a++;
}

int main()
{
    int a = 20;
    int& b = a;
    int& c = b;
    cout << a << b << c << endl;
    c = 10;
    cout << a << b << c << endl;
    cout << &a << " " << &b << " " << &c << " " << endl;
    
    cout << "==========" << endl;
    foo(a);
    cout << a << endl;
    
    return 0;
}
  1. 引用必須初始化且不能爲空。
  2. 引用不能更換目標。
  3. 一個變量可以有多個引用。
  4. 引用不佔用額外的內存。
  5. 引用類型必須和引用實體是同種類型的。

常引用

void TestConstRef()
{
    const int a = 10;
    // int& ra = a; // 該語句編譯時會出錯,a爲常量 const int& ra = a;
    // int& b = 10; // 該語句編譯時會出錯,b爲常量 const int& b = 10;
    double d = 12.34;
    // int& rd = d; // 該語句編譯時會出錯,類型不同 const int& rd = d;
}

使用場景

  1. 做參數

    void Swap(int& left, int& right)
    {
        int temp = left;
        left = right;
        right = temp;
    }
    
  2. 做返回值

    int& TestRefReturn(int& a)
    {
        a += 10;
        return a; 
    }
    

⚠️

int& Add(int a, int b)
{
    int c = a + b;
    return c;
}
int main() 
{
    int& ret = Add(1, 2);
    Add(3, 4);
    cout << "Add(1, 2) is :"<< ret <<endl;
    return 0;
}
// 這段代碼輸出結果爲一個隨機值

如果函數返回時,離開函數作用域後,其棧上空間已經還給系統,因此不能用棧上的空間作爲引用類型
返回。如果以引用類型返回,返回值的生命週期必須不受函數的限制(即比函數生命週期長)。

傳值和傳引用(作爲參數 / 作爲返回值)在效率上的差距!

引用和指針的區別

  1. 在語法概念上引用就是一個別名,沒有獨立空間,和其引用實體共用同一塊空間。

  2. 在底層實現上實際是有空間的,因爲引用是按照指針方式來實現的。

    // a.cpp
    int main()
    {
        int a = 10;
        int& ra = a;
        ra = 20;
        int* pa = &a;
        *pa = 20;
        return 0;
    }
    

    查看彙編代碼:

    保留編譯過程中生成的臨時文件:g++ a.cpp -save-temps

    其中a.s就是彙編文件,在 VS 裏面可以 DEBUG 起來,直接看彙編代碼,比較方便。

    Cpp-入門-pic-1.png

  3. 引用在定義時必須初始化,指針沒有要求。

  4. 沒有NULL引用,但有NULL指針。

  5. 在 sizeof 中含義不同:引用結果爲引用類型的大小,但指針始終是地址空間所佔字節個數( 32 位平臺下佔 4

    個字節) 。

  6. 引用自加即引用的實體增加 1 ,指針自加即指針向後偏移一個類型的大小。

  7. 有多級指針,沒有多級引用。

  8. 訪問實體方式不同,指針需要顯式解引用,引用編譯器自己處理。

  9. 引用比指針使用起來相對更安全。

內聯函數(inline)

以 inline 修飾的函數叫做內聯函數,編譯時 C++ 編譯器會在調用內聯函數的地方展開,沒有函數壓棧的開銷,內聯函數提升程序運行的效率。

以下是沒有加 inline 的彙編代碼。

Cpp-入門-pic-2.png

在 Add 前加了 inline 後,在編譯期間編譯器會用函數體替換函數的調用。

Cpp-入門-pic-3

  1. inline 是一種以空間換時間的做法,省去調用函數額開銷。所以代碼很長或者有循環 / 遞歸的函數不適宜使
    用作爲內聯函數。

  2. inline 對於編譯器而言只是一個建議,編譯器會自動優化,如果定義爲inline的函數體內有循環/遞歸等等,編譯器優化時會忽略掉內聯。

  3. inline 不建議聲明和定義分離,分離會導致鏈接錯誤。因爲 inline 被展開,就沒有函數地址了,鏈接就會找不到。

    // F.h
    #include <iostream>
    using namespace std;
    inline void f(int i);
    
    // F.cpp
    #include "F.h"
    void f(int i)
    {
        cout << i << endl;
    }
    
    // main.cpp
    #include "F.h"
    int main() 
    {
        f(10);
        return 0;
    }
    // 鏈接錯誤:main.obj : error LNK2019: 無法解析的外部符號 "void __cdecl f(int)" (?f@@YAXH@Z),該符號在函數 _main 中被引用
    

auto(C++11)

在早期 C/C++ 中 auto 的含義是:使用 auto 修飾的變量,是具有自動存儲器的局部變量,但遺憾的是一直沒有人去使用它。

C++11 中,標準委員會賦予了 auto 全新的含義即:auto 不再是一個存儲類型指示符,而是作爲一個新的類型指示符來指示編譯器,auto 聲明的變量必須由編譯器在編譯時期推導而得。

🌰 栗子:

int TestAuto()
{
    return 20;
}
auto b = 10;
auto c = 'a';
auto d = TestAuto();
//auto e; 無法通過編譯,使用auto定義變量時必須對其進行初始化

⚠️

  1. 使用 auto 定義變量時必須對其進行初始化,在編譯階段編譯器需要根據初始化表達式來推導 auto 的實際類
    型。因此 auto 並非是一種“類型”的聲明,而是一個類型聲明時的“佔位符”,編譯器在編譯期會將 auto 替換爲變量實際的類型。

  2. 用 auto 聲明指針類型時,用 auto 和 auto* 沒有任何區別,但用 auto 聲明引用類型時則必須加 &。

    int main()
    {
        int x = 10;
        auto a = &x;
        auto* b = &x;
        auto& c = x;
        cout << typeid(a).name() << endl;
        cout << typeid(b).name() << endl;
        cout << typeid(c).name() << endl;
        *a = 20;
        *b = 30;
         c = 40;
        return 0; 
    }
    
  3. 當在同一行聲明多個變量時,這些變量必須是相同的類型,否則編譯器將會報錯,因爲編譯器實際只對第一個類型進行推導,然後用推導出來的類型定義其他變量。

    auto c = 3, d = 4.0; // 該行代碼會編譯失敗,因爲c和d的初始化表達式類型不同
    

auto 不能推導的場景

  1. auto 不能作爲函數的參數。

    void fun(auto a) {}
    
  2. auto 不能直接用來聲明數組。

  3. 爲了避免與 C++98 中的 auto 發生混淆,C++11 只保留了 auto 作爲類型指示符的用法。

  4. auto 在實際中最常見的優勢用法就是跟以後會講到的 C++11 提供的新式 for 循環,還有 lambda 表達式等進

    行配合使用。

  5. auto 不能定義類的非靜態成員變量。

  6. 實例化模板時不能使用 auto 作爲模板參數。

基於範圍的 for 循環(C++11)

🌰 栗子:

void TestFor()
{
    int array[] = { 1, 2, 3, 4, 5 };
    for(auto& e : array)
    {
         e *= 2;
    }
    for(auto e : array)
    {
        cout << e << " ";
    }
    return 0;
}

⚠️

  1. 與普通循環類似,可以用 continue 來結束本次循環,也可以用 break 來跳出整個循環。

  2. for 循環迭代的範圍必須是確定的,對於數組而言,就是數組中第一個元素和最後一個元素的範圍;對於類而言,應該提供 begin 和 end 的方法,begin 和 end 就是 for 循環迭代的範圍。

  3. // 下面這段代碼就有問題
    void TestFor(int array[])
    {
        for(auto& e : array)
        {
            cout<< e <<endl;
        }
    }
    

nullptr(C++11)

NULL 實際是一個宏,在傳統的 C 頭文件stddef.h中:

#ifndef NULL
#ifdef __cplusplus
#define NULL    0
#else
#define NULL    ((void *)0)
#endif
#endif

NULL 可能被定義爲字面常量 0,或者被定義爲無類型指針 (void*) 的常量。不論採取何種定義,在
使用空值的指針時,都不可避免的會遇到一些麻煩,如下:

void f(int) 
{
    cout<<"f(int)"<<endl;
}
void f(int*)
{
    cout<<"f(int*)"<<endl;
}
int main()
{
    f(0);
    f(NULL);
    f((int*)NULL);
    return 0;
}

程序本意是想通過 f(NULL) 調用指針版本的 f(int*) 函數,但是由於 NULL 被定義成 0,因此與程序的初衷相悖。 在C++98 中,字面常量 0 既可以是一個整形數字,也可以是無類型的指針 (void*) 常量,但是編譯器默認情況下將其看成是一個整形常量,如果要將其按照指針方式來使用,必須對其進行強轉 (void *)0。

爲了避免混淆,C++11 提供了 nullptr ,即:nullptr 代表一個指針空值常量。nullptr 是有類型的,其類型爲nullptr_t ,僅僅可以被隱式轉化爲指針類型,nullptr_t 被定義在頭文件中:

typedef decltype(nullptr) nullptr_t;

⚠️

  1. 在使用 nullptr 表示指針空值時,不需要包含頭文件,因爲 nullptr 是 C++11 作爲新關鍵字引入的。
  2. 在 C++11 中,sizeof(nullptr) 與 sizeof((void*)0) 所佔的字節數相同。
  3. 爲了提高代碼的健壯性,在後續表示指針空值時建議最好使用nullptr。

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