C++--入門基礎

C++入門基礎

1. C++的第一個程序

C++繼承C語言許多大多數的語法,所以以C語言實現的hello world也可以運行,C++中需要把文件定義為.cpp,vs編譯器看是.cpp就會調用C++編譯器編譯,linux下要用g++編譯,不再是gcc。

// test.cpp
#include <stdio.h>int main() 
{printf("hello world\n");return 0;
}

當然C++有一套自己的輸入輸出,嚴格說C++版本的hello world應該是這樣寫的。

// test.cpp
// 這里的std cout等我們都看不懂,沒關系,下面我們會依次講解
#include <iostream>
using namespace std;int main() 
{cout << "hello world\n" << endl;return 0;
}

因為C++是在C語言基礎上進行完善和在發展,所以二者的結構是十分相似的。比較上述兩個程序,將下面的C++程序與熟悉的C語言程序類比可知:

  • iostream是程序的頭文件,根據釋義可知其包含輸入輸出函數的頭文件。
  • cout是輸出函數。

然而其中using namespace std;可能無法理解,若要理解這條語句需要繼續學習命名空間的知識。

2. 命名空間

2.1 namespace的價值

在下面的程序中,程序中包含了 <stdlib.h> 頭文件,該頭文件中含有 rand 函數,如果再用 rand 作為變量名定義變量,就會造成重定義。

#include <stdio.h>
#include <stdlib.h>// 其中定義了函數rand(),若不包含此頭文件,則程序可以正常編譯運行int rand = 10;int main() 
{printf("%d\n", rand);return 0;
}
// 編譯報錯: error C2365: “rand”: 重定義;以前的定義是“函數”

外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳

在C/C++中,變量、函數和面向對象的類都是大量存在的,在編寫大型項目的時候這些變量、函數和類的名稱都在全局作用域內沖突,可能會導致很多命名沖突。使用命名空間的目的是對標準符的名稱進行本地化,以避免命名沖突或者符號污染,其中定義命名空間的關鍵字是 namespace。

2.2 命名空間的定義

定義命名空間,需要使用到namespace關鍵字,后面跟命名空間的名字,然后接一對{}即可,{}中即為命名空間內的變量/函數/類等。

  • 命名空間中可以定義變量/函數/類等。
  • namespace只能定義在全局,當然還可以嵌套定義
  • namespace后面的空間名不受限制,可以隨機取,一般取項目名稱作為空間名。

命名空間的定義:

//定義命名空間
namespace N1
{//定義變量int rand = 100;//定義函數int Add(int a, int b){return a + b;}//定義類型(結構體)typedef struct SLNode{int data;SLNode* next;}SLNode;
}

命名空間的嵌套定義:

//定義命名空間
namespace N1//定義一個名為N1的命名空間
{//定義變量int a = 100;namespace N2 //嵌套定義另一個名為N2的命名空間{int b = 200;}
}//嵌套定義的訪問
int main()
{printf("%d\n", N1::N2::b);
}

多文件命名空間的定義

  • 項目工程中多個文件中定義的同名namespace會認為是一個namespace,編譯器最后會將其成員合成在同一個命名空間中,不會沖突。
  • 所以不能在相同名稱的命名空間中定義兩個相同名稱的成員。
  • 注意:一個命名空間就定義了一個新的作用域,命名空間中所有內容都局限于該命名空間中。
2.2.1 命名空間的本質解釋
2.2.1.1 namespace
  • namespace本質是定義一個區域,這個區域跟區域各自獨立,不同的區域可以定義不同變量。
  • 上面那個C語言的程序之所以會報錯是因為主函數中定義的變量randstdilb.h這個頭文件中的rand函數都是被定義在全局域中,所以會產生命名沖突。在使用了namespace使用了命名空間這個概念之后就相當于形成了一個新的域,此時的rand因為在不同的域中則下面程序中rand不在沖突了。
  • 既然已經創建了兩個域,那么如何分別調用這不同域中的數據呢?這里就需要使用域作用限定符::,未加::默認訪問全局域,加上了::則默認訪問此域中的信息。

代碼示例:

#include<stdio.h>
#include<stdlib.h>namespace N2
{int rand = 100;
}int main()
{// 這里默認是訪問的是全局的rand函數指針printf("%p\n", rand);//這里指定訪問N2命名空間中的rand//::域作用限定符printf("%d\n", N2::rand);return 0;
}

運行結果:在這里插入圖片描述

這樣即可分別調用不同域中的同一個名稱為rand的信息了。

2.2.1.2 域
  • C++中域有函數局部域全局域命名空間域類域;區域影響的是編譯時查找一個變量/函數/類型出現的位置(聲明或定義),所有有了域隔離,名字沖突就解決了。

  • 局部域和全局域除了不會影響編譯查找邏輯,是會影響變量的生命周期的,命名空間域和類域不影響變量生命周期。

    • 在namespace中的定義的變量,其生命周期都是全局的,命名空間域只是起到隔離的作用,沒有影響變量生命周期。
    • 局部域中的變量只能在當前局部內訪問。
  • 不同的域中可以用同名變量,同一個域中不可以用同名變量。

示例代碼:

#include <stdio.h>//全局域
int x = 0;//局部域
void func()
{int x = 1;
}
//命名空間域
namespace N1
{int x = 2;
}int main()
{//局部域int x = 3;//打印局部域--打印3printf("%d\n", x);//打印命名空間域--打印2printf("%d\n", N1::x);//打印全局域--打印0printf("%d\n", ::x);
}
//這里的打印是在main函數中進行的,所以打印的就是當前局部域中的變量x,而不是func這個局部域中的局部變量

2.2.2 C++標準庫

  • C++標準庫都放在一個叫std(standard)的命名空間中。

這也就解釋了開頭using namespace std;namespace std;的含義,其表示要調用C++標準庫中的定義的變量和函數。

2.3 命名空間的使用

編譯查找一個變量的聲明/定義時,默認只會在局部或全局查找,不會主動到命名空間里面去查找。所以下面程序會編譯報錯。

#include <stdio.h>
namespace N1 {int a = 0;int b = 1;
}int main()
{// 編譯報錯: error C2065: “a”: 未聲明的標識符printf("%d\n", a);return 0;
}

可以使用命名空間中的變量/函數,有三種方式:

  • **法一:**指定命名空間訪問:項目中推薦這樣方式。
  • **法二:**using 將命名空間中的某些成員展開,項目中經常訪問的不在沖突的成員推薦這樣方式。
  • 法三:展開命名空間中全部成員,項目不推薦,沖突風險很大,日常小練習程序為了方便推薦使用。
//法一:指定命名空間訪問
int main()
{printf("%d\n", N1::a);return 0;
}//法二:using 將命名空間中某個成員展開
using N::b;
int main()
{printf("%d\n", N1::a);printf("%d\n", b);return 0;
}//法三:展開命名空間中全部成員
using namespace N1
int main()
{ptintf("%d\n", a);printf("%d\n", b);return 0;
}

? 這里再次回歸到上面那個第一個C++代碼中,就可以看懂這句using namespace std;了,他表示的是利用展開命名空間全部成員的方式展開std(C++標準庫)

3. C++ 輸入與輸出

? 這里再次引入,第一個C++代碼:

#include <iostream>
using namespace std;int main() 
{cout << "hello world\n" << endl;return 0;
}

? 在C語言中有標準輸入輸出函數scanfprintf,而在C++中有**cin標準輸入cout標準輸出**。在C語言中使用scanfprintf函數,需要包含頭文件stdio.h。在C++中使用cincout,需要包含頭文件iostream以及std標準命名空間(如果不寫則需要完整表示std::cout或者std::cin)。

  • 是 Input Output Stream 的縮寫,是標準的輸入、輸出流庫,定義了標準的輸入、輸出對象。

3.1 輸入輸出函數

3.1.1 cin函數和 cout函數
  • std::cin 是 istream 類的對象,它主要面向窄字符(narrow characters of type char) 的標準輸入流。

  • std::cout 是 ostream 類的對象,它主要面向窄字符的標準輸出流。

補充知識:cincout中的c是什么意思?

c的含義是窄字符,其本質思想是將內存中的各種數據類型或者原反補碼等數據都轉化成字符流。cin就相當于將輸入的字符流解析成內存中的各種數據,cout就相當于將內存中的各種數據轉化成字符流輸出。

只有在內存中才會有整型、浮點型各種類型和原反補碼等概念,因為CPU需要對這些二進制數據進行一系列運算;但是在其他環境中比如文件、網絡、終端控制臺中只有字符的概念。所以當內存中的一個整型數據想要在控制臺或者文件中來回傳遞都需要先經過字符流進行轉化。

3.1.2 endl函數
  • std::endl 是一個函數,流輸出時,相當于插入一個換行字符加刷新區。

  • endl其實是end line的縮寫。

3.2 運算符

  • << 是流插入運算符
    • 這個符號用于數據的輸出,可以想象運算符右邊的數據流進cout,然后輸出。
  • >> 是流提取運算符
    • 這個符號用于數據的輸入,可以想象運算符右邊的數據流入cin,然后輸入。
  • 這兩個運算符是對C語言中的進行復用,C語言還用這兩個運算符做位運算符左右移/右移。

3.3 輸入輸出示例

? 使用C++輸入輸出更方便,不需要像printf/scanf輸入輸出時那樣,需要手動指定格式,C++的輸入輸出可以自動識別變量類型(本質是通過函數模板重載實現的),這個以后會學到,其實最重要的是C++的流能夠更好地支持自定義類型對象的輸入輸出。

#include<iostream>
#include<stdio.h>int main()
{//打印字符串std::cout << "hello world\n";//打印整型int i = 10;std::cout << i << '\n' << "\n";//打印浮點型double d = 1.1;std::cout << d << std::endl;//輸入一個整型,一個浮點型并打印出來//C++和C語言可以混合使用并不干擾std::cin >> i >> d;scanf("%d%lf", &i, &d);std::cout << i << " " << d << std::endl;printf("%d %.2lf", i, d);
}

詳細講解

  • 利用語句std::cout進行輸出或者std::cin進行數據輸入的時候,不再需要像C語言中的printf一樣先輸入其類型的占位符再輸出,cout不需要指定其輸出內容是什么類型可以自動識別變量的類型
  • coutcin支持連續的字符流的輸出和插入,就像第8行代碼,可以在i輸出之后繼續在后面執行換行的命令,直接對流進行插入即可。
  • 最推薦的輸出換行方式就是利用函數endl進行換行操作,因為在不同的操作系統下可能有不同的換行符,但是使用這個函數只要程序使用C++的代碼編寫都可以執行換行的命令。
  • 在C++代碼中是可以進行和C語言進行混合使用的,并且有些目標的實現利用C語言的函數實現得更加簡單。如這里對于浮點數小數位數的控制則推薦使用printf,C++內置的控制函數過于復雜。
  • 這里沒有包含<stdio.h>,也可以使用printfscanf,在包含頭文件時,vs系列編譯器是這樣子的,其他編譯器可能會報錯。

3.4 補充知識

  • IO流涉及類和對象,運算符重載,繼承等很多面向對象的知識,這些知識還辦法進行闡釋,所以這里只能簡單認識一下C++ IO流的用法,后面會有專門的一個部分來細講IO流庫。
  • cout/cin/end等都屬于C++標準庫,C++標準庫都放在一個叫std(standard)的命名空間中,所以要通過命名空間的使用方式去調用它們
  • 因為C++中的coutcin的效率不高,至于為什么效率會不高也會在后面對IO流做細致講解的時候有介紹,如果IO需求較高則采用下面的方式。
#include <iostream>
using namespace std;int main()
{// 在io需求比較高的地方,如部分大量輸入的競賽題中,加上以下3行代碼// 可以提高C++IO效率ios_base::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr);return 0;
}

4. 缺省參數

4.1 缺省參數的概念

? 缺省參數是聲明或定義函數時為函數的參數指定一個缺省值。在調用該函數時,如果沒有指定實參,則采用該形參的缺省值,否則使用指定的實參,缺省參數為全缺省和半缺省參數。(有些地方把缺省參數也叫默認參數)

代碼示例:

#include <iostream>
#include <cassert>
using namespace std;void Func(int a = 0)
{cout << a << endl;
}int main()
{Func();    // 沒有傳參數時,使用參數的默認值Func(10);  // 傳參數時,使用指定的實參return 0;
}

運行結果:

0
10

4.2 缺省參數的分類

4.2.1 全缺省參數

全缺省就是全部形參給缺省值。

void Func(int a = 10, int b = 20, int c = 30)
{cout << "a = " << a << endl;cout << "b = " << b << endl;cout << "c = " << c << endl;
}
4.2.2 半缺省參數

半缺省就是部分形參給缺省值。

C++規定半缺省參數必須從左至右次序連續缺省,不能間隔跳躍缺省參數。

void Func(int a, int b = 10, int c = 20)
{cout << "a = " << a << endl;cout << "b = " << b << endl;cout << "c = " << c << endl;
}
4.2.3 注意事項
  • 帶缺省參數的函數調用,C++規定必須從左到右依次給實參,不能跳躍給實參。
  • 函數聲明和定義分離時,缺省參數不能在函數聲明和定義中同時出現,規定必須函數聲明給缺省值

缺省參數在實際代碼中的應用:

// Stack.h
#include <iostream>
#include <cassert>
using namespace std;typedef int STDataType;
typedef struct Stack
{STDataType* a;int top;int capacity;
}ST;void STInit(ST* ps, int n = 4);//半缺省,給的默認空間為4個類型大小的空間// Stack.cpp
#include "Stack.h"void STInit(ST* ps, int n)
{assert(ps && n > 0);ps->a = (STDataType*)malloc(n * sizeof(STDataType));ps->top = 0;ps->capacity = n;
}// test.cpp
#include "Stack.h"
int main()
{ST s1;STInit(&s1);//可以在初始化的時候,不指定空間大小,因為缺省參數,會自動把空間設置為4// 如果確定知道要插入1000個數據,初始化時把容量設置大,避免擴容,影響效率ST s2;STInit(&s2, 1000);//由此可以看出缺省參數十分靈活、好用return 0;
}

5. 函數重載

5.1 函數重載的概念

C++支持在同一作用域中出現同名函數,但是要求這些同名函數的形參不同,可以是參數個數不同或者參數類型不同。這樣C++函數調用表現出了多態行為,使用更靈活。C語言是不支持同一作用域中出現同名函數的。

#include <iostream>
using namespace std;// 1、參數類型不同
int Add(int left, int right)
{cout << "int Add(int left, int right)" << endl;return left + right;
}double Add(double left, double right)
{cout <<"double Add(int left, int right)" << endl;return left + right;
}//2、參數個數不同
void f()
{cout << "f()" << endl;
}void f(int a)
{cout << "f(int a)" << endl;
}//3、參數順序不同(本質還是類型不同)
void f(int a, char b)
{cout << "f(int a, char b)" << endl;
}void f(char b, int a)
{cout << "f(char b, int a)" << endl;
}int main()
{Add(10, 20);//打印int Add(int left, int right),并計算10+20的結果返回Add(10.1, 20.2);//打印double Add(int left, int right)并計算10.1+20.2的結果返回f();//打印f()f(10);//打印f(int a)f(10, 'a');//打印f(int a, char b)f('a', 10);//打印f(char b, int a)return 0;
}

補充知識:

  • 返回值不同不能作為重載條件,因為調用時也無法區分。(因為參數不同可以區分函數,但是返回值不同無法區分函數)
void f1()
{}int f1()
{return 0;
}

  • 構成重載的函數也有可能報錯,下面代碼兩個重載函數語法正確,但是會發生調用不明確的問題。
//下面兩個函數構成重載(參數不同)
//f() 但是調用時,會報錯,存在歧義,編譯器不知道調用哪個
void f1()
{cout << "f()" << endl;
}void f1(int a = 10)
{cout << "f(int a)" << endl;
}

在這里插入圖片描述

6. 引用

6.1 引用的概念和定義

引用不是新定義一個變量,而是給已存在變量取了一個別名,編譯器不會為引用變量開辟內存空間,它和它引用的變量共享同一塊內存空間。例如:水滸傳中李逵,宋江叫"鐵牛",江湖上人稱"黑旋風"; 林沖,外號豹子頭;

語法類型& 引用別名 = 引用對象;

C++中為了避免引用太多的運算符,會復用C語言的一些符號,比如前面的<<和>>,這里引用也和取址使用了同一個符號&,注意用法角度區分就可以。

這里和C語言中的typedef不一樣,typedef是給類型取別名,這里的引用是給變量取別名。

#include <iostream>
using namespace std;int main()
{int a = 0;// 引用:b和c是a的別名int& b = a;int& c = a;// 也可以給別名b取別名,d相當于a的別名int& d = b;++d;// 這里取地址看到是一樣的,d++他們都++cout << &a << endl;cout << &b << endl;cout << &c << endl;cout << &d << endl;return 0;
}

實際的底層情況如下:也就是一塊空間有多個名字。
在這里插入圖片描述

6.2 引用的特性

  • 引用在定義時必須初始化

    int a = 10;
    int& b = a;//引用在定義時必須初始化
    
  • 一個變量可以有多個引用

    int a = 10;
    int& b = a;
    int& c = a;
    //給別名起別名
    int& d = b;
    
  • 引用一旦引用一個實體,就不可以再引用其他實體

    這個特性也也就決定了別名是沒有辦法替代指針的。

    在鏈表這一個數據結構中,數據與數據之間利用指針相互連接,當刪除中間的一個數據的時候,需要將其前一個數據的指針地址由原來的指向此時刪除的數據改為指向現在刪除的這個數據的后一個數據,這樣的行為引用是沒有辦法做到的,因為引用的實體沒有辦法改變,也就無法實現將鏈表的刪除操作,所以其無法替代指針。

    int a = 10;
    int& b = a;
    int c = 20;
    b = c;
    //想法:讓b轉而引用c,但是實際操作的是把c中的20賦值給了a
    

6.3 引用的使用

引用在實踐中主要是用于引用傳參和引用返回值中減少拷貝(利用別名達到不開辟新空間的目的)提高效率和改變引用對象時改變被引用對象。

6.3.1 引用傳參

示例一:

引用傳參跟指針傳參功能是類似的,引用傳參更方便一些。

在這里插入圖片描述

  • 上面代碼體現了引用特性的功能2:改變引用對象時改變被引用對象
  • 這里使用引用的方式,將a作為rx的引用,e作為ry的引用;同樣可以起到傳址調用的效果。
  • 其實本質是讓形參作為實參的別名,讓形參的改變也會影響實參。

示例二:

struct A
{int arr[1000];
};void func(A aa)
{}int main()
{A aa1;func(aa1);return 0;
}

上述代碼中利用別名代替形參的方式,避免了再次創建四千個字節的情況,避免了空間的浪費。

6.3.2 引用作返回值

引用返回值的場景相對比較復雜,在這里只簡單講了一下場景,還有一些內容后續類和對象章節中會繼續深入講解。

#include<iostream>
int& Add(int a, int b)
{static int c = a + b;return c;
}int main()
{int a = 10, b = 20;Add(a, b)++;std::cout << Add(a, b) << std::endl;return 0;
}//運行結果:31

上述代碼就是引用作為返回值的應用,函數Add的返回值是c的別名,并在主函數中對對其++,最后打印結果,運行成功。

這里使用引用作為返回值是因為,函數的返回值的本質和形參的本質一樣也是將返回值的數值拷貝到一塊臨時空間,所以如果這里的返回值單純的使用int表示,編譯器則會報錯:

在這里插入圖片描述

注意:也并不是所有函數都可以使用別名作為返回值

函數返回的數據不能是函數內部創建的普通局部變量,因為在函數內部定義的普通的局部變量會隨著函數調用的結束而被銷毀。函數返回的數據必須是被static修飾或者是動態開辟的或者是全局變量等不會隨著函數調用的結束而被銷毀的數據,才可以使用引用將其返回。

補充:越界不一定報錯

  • 越界讀一定不報錯

    在這里插入圖片描述

  • 越界寫不一定報錯

    在這里插入圖片描述

    這是因為編譯器對于越界的檢查是抽查,在這里會在數組后面的幾個字節中固定寫入某個值,在程序運行中會去檢查這幾個字節位置的數據有沒有被更改,如果有則報錯,說明一定越界了。但是有時修改數據的位置可能不在抽查位置,這個時候程序就可以正常運行,不會報錯。

6.4 const引用

6.4.1 權限
  • 可以引用一個const對象,但是必須用const引用。const引用也可以引用普通對象,因為對象的訪問權限在引用過程中可以縮小或者平移,但是不能放大。
//const中的權限問題
int main()
{//訪問權限的放大const int a = 10;int& ra = a;// 編譯報錯: error C2440: “初始化”: 無法從“const int”轉換為“int &”// 權限可以縮小int b = 1;const int& rb = b;rb++;b++;//正常編譯運行//訪問權限的平移const int a = 10;const int& ra = a;//正常編譯運行//const的使用const int& ra = a;ra++;//const修飾的ra,所以就不能對ra進行修改// 編譯報錯: error C3892: “ra”: 不能給常量賦值//空間訪問權限和空間拷貝的辨析const int x = 0;int y = x;//這里的y僅僅是對x中的值進行拷貝,不涉及權限問題return 0;
}
//指針中的權限問題
int main()
{// 權限不能放大const int a = 10;const int* p1 = &a;int* p2 = p1;//報錯// 權限可以縮小int b = 20;int* p3 = &b;const int* p4 = p3;//正常編譯運行// 不存在權限放大,因為const修飾的是p5本身不是指向的內容int* const p5 = &b;int* p6 = p5;return 0;
}

6.4.2 臨時對象的常性

  • 需要注意的是類似 int& rb = a*3; double d = 12.34; int& rd = d; 這樣一些場景下,表達式a*3的結果保存在一個臨時對象中,int& rd = d也是類似,在類型轉換中會產生臨時對象儲存中間值,也就是此時rbrd引用的都是臨時對象,而C++規定臨時對象具有常性相當于被const修飾),所以這里就觸發了引用限制,必須要用常引用才可以。
  • 所調用臨時對象就是編譯器需要一個空間暫時存儲表達式的求值結果時臨時創建的一個未命名的對象,C++中把這個未命名對象做臨時對象。
  • 臨時對象一般用于存放表達式的結果或者是類型轉換時的中間值。
#include <iostream>
using namespace std;int main()
{int a = 10;int& rb = a * 3;// 報錯,原因是這里的a*3被保存在臨時對象中,相當于這里的指向a*3的這塊空間被const修飾了,現在用"int&"去修飾本質也是權限的放大//應該改為const int& rb = a * 3;即可double d = 12.34;int& rd = d;// 編譯報錯: “初始化”: 無法從“double”轉換為“int &”//在類型轉換的時候也會產生臨時空間存在d,臨時空間因為有常性,所以就相當于指向d的這塊空間被const修飾,所以現在用"int&"去修飾本質也是權限的放大//改為const int& rd = d;即可return 0;
}

6.4.3 const引用的使用場景

void f1(const int& rx)
{}int main()
{int a = 10;double b = 12.34;f1(a);f1(a * 3);f1(d);
}

在函數f1的形參使用const進行修飾,可以在調用此函數的對于形參的填寫形式更加寬泛,其實本質都是因為加上了const權限變得更小了,所以正常的各種參數都可以作為形參傳過去。

使用const可以引用const對象普通對象臨時對象,引用對象十分寬泛。

**注意:(非常重要!!!)**當然其實上述代碼,將形參改為int rx也可以,也是可以正常傳參的,雖然傳入的是實參的臨時拷貝,這樣其實也是可以的。這是因為現在使用的數據類型都是一些簡單數據類型類似intfloat等,但是如果這里的數據類型是A(一個極大地數據類型),這里使用傳值傳參就會拷貝這個極大的數據,這樣的代價就會很大,所以還是推薦引用傳參,使用引用傳參能接收更多類型的對象,并且要保證引用內容不被更改,所以使用const int& XX的方式作為函數的形參。這是后期C++學習常見的形參格式。

6.5 指針和引用的關系

C++中指針和引用就像兩個性格迥異的親兄弟,指針是哥哥,引用是弟弟,在實踐中它們相輔相成,功能能有重疊性,但是各有自己的特點,互相不可替代。

  • 語法概念上引用是一個變量的取別名不開辟新空間,指針是存儲一個變量地址,要開空間。

    • 從底層匯編語言來看,引用也是利用指針實現的,也需要開辟空間。

      在這里插入圖片描述

  • 引用在定義時必須初始化,指針建議初始化,也不是必須的。

  • 引用在初始化時引用一個對象后,就不能再引用其他對象;而指針可以不斷地改變指向對象。

  • 引用可以直接訪問指向對象,指針需要解引用才能訪問指向對象。

  • sizeof中含義不同,引用結果為引用類型的大小,但指針始終是地址空間所占字節個數(32位平臺下占4個字節,64位下是8個字節

  • 指針很容易出現空指針和野指針的問題,引用很少出現,引用使用起來相對更安全一些

7. 內聯函數

7.1 內聯函數的定義

用inline修飾的函數叫做內聯函數,編譯時C++編譯器會在調用的地方展開內聯函數,這樣調用內聯函數就不需要建立棧幀了,就可以提高效率。

#include <iostream>
using namespace std;inline int Add(int x, int y)
{int ret = x + y;ret += 1;ret += 1;ret += 1;return ret;
}int main()
{// 可以通過匯編程序是否展開// 有call Add語句就沒有展開,沒有就是展開了int ret = Add(1, 2);return 0;
}

下圖左是以上代碼的匯編代碼,下圖右是函數Add加上inline后的匯編代碼:

在這里插入圖片描述

7.2 inline與宏函數

7.2.1 回憶宏函數

C語言實現宏函數也會在預處理時替換展開,但是宏函數實現很容易出錯的,而且不方便調試。

// 正確的宏實現
#define ADD(a, b) ((a) + (b))
// 為什么不能分號?
// 為什么要加外面的括號?
// 為什么要加里面的括號?
//重點:宏函數雖然坑很多,但是因為替換機制讓其調用的時候不同開辟棧幀,提高程序運行效率int main()
{int ret = ADD(1, 2);//不加;的原因cout << ADD(1, 2) << endl;//加外面括號的原因cout << ADD(1, 2) * 5 << endl;//加里面括號的原因int x = 1, y = 2;//位運算符優先級較低,會先執行+-操作ADD(x & y, x | y);  // -> (x & y + x | y)return 0;
}
7.2.2 inline

為了彌補C語言中宏函數的各種坑,C++設計了inline的目的是替代C的宏函數。

7.2.2.1 inline的底層邏輯

inline對于編譯器而言只是一個建議,也就是說,加了inline編譯器也可以選擇在調用的地方不展開,不同編譯器關于inline什么情況展開各不相同,因為C++標準沒有規定這個。inline適用于頻繁調用的小函數,對于遞歸函數,代碼相對多一些的函數,加上inline也會被編譯器忽略。

  • vs編譯器debug版本下面默認是不展開inline的,這樣方便調試,debug版本想展開需要設置一下以下兩個地方。

    在這里插入圖片描述

下圖右邊是未展開內聯函數,在底層匯編中還是用了指令call創建了函數棧幀。左圖這是已經展開了內聯函數,可以看到并沒有調用指令call而是直接執行ADD函數的邏輯。

在這里插入圖片描述

但是當內聯函數行數過長(這里判斷過長函數一般是由編譯器決定的,比如這里的VS一般是10行以上)編譯器就不會將內聯函數展開。

補充:為什么將inline設置為對編譯器的建議,而不是將決定權交給程序員?

eg:現在有一個100行指令的ADD函數,在1000個位置調用這個函數。

比較:所需要的指令(這里的指令不是內存空間,不要弄混淆)

inline展開,占多少指令:10000*100

inline不展開,占多少指令:10000*1+100(call調用指令每個函數占一次)

要想理解其底層原理,需要對程序運行的本質有一個了解。

在計算機中編寫的所有程序都是一個個文件,在這些文件編譯之后會形成一個.exe(以windows為例)的可執行文件,這個文件中就是實際的指令,計算機會生成一個進程去分配內存去將這個可執行文件的指令加載到內存中。

有了以上鋪墊和上面的比較可以看出當內聯函數過長,會導致其所占的指令會很多,也就導致指令膨脹,就會導致可執行文件變大,所以將其加載到進程中所占據的內存就會變大,就會造成很多影響。

inline設計的本質思路是一種以空間換時間的做法,省去了調用函數的額外開銷。而將是否將內聯函數展開的決定權交給編譯器,這樣的設計本質是一種防御策略,害怕遇到那些不靠譜的程序員。

7.2.2.2 inline的注意事項
  • inline不建議聲明和定義分別到兩個文件,分離會導致鏈接錯誤。因為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()
{// 鏈接錯誤:無法解析的外部符號 "void __cdecl f(int)" (?f@YAXH@Z)f(10);return 0;
}

8. nullptr

NULL 實際上是一個宏,在傳統的 C 頭文件(stddef.h)中,可以看到如下代碼:

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

C++中 NULL 可能被定義為整數 0,或者 C 中被定義為無類型指針 (void*) 的常量。不論取何種定義,在使用空值的指針時,都不可避免的會遇到一些麻煩。

#include <iostream>
using namespace std;void f(int x)
{cout << "f(int x)" << endl;
}void f(int* ptr)
{cout << "f(int* ptr)" << endl;
}int main()
{f(0);// 本想通過f(NULL)調用指針版本的f(int*)函數,但是由于NULL被定義成0,調用了f(int x)f(NULL);f((int*)NULL);return 0;
}

本想通過 f(NULL) 調用指針版本的 f(int*) 函數,但是由于 NULL 被定義成 0,調用了 f(int x),因此與程序的初衷相悖。f((void*)NULL) 調用會報錯。

C++11中引入了 nullptrnullptr 是一個特殊的關鍵字,nullptr 是一種特殊類型的字面量,它可以轉換成任何其他類型的指針類型。使用 nullptr 定義空指針可以避免類型轉換的問題,因為 nullptr 只能被隱式地轉換為指針類型,而不能被轉換為整型類型。

#include <iostream>
using namespace std;void f(int x)
{cout << "f(int x)" << endl;
}void f(int* ptr)
{cout << "f(int* ptr)" << endl;
}int main()
{f(0);f(nullptr);//正常運行不會報錯return 0;
}

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/pingmian/79215.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/79215.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/79215.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

從實列中學習linux shell9 如何確認 服務器反應遲鈍是因為cpu還是 硬盤io 到底是那個程序引起的。cpu負載多高算高

在 Linux 系統中,Load Average(平均負載) 是衡量系統整體壓力的關鍵指標,但它本身沒有絕對的“高/低”閾值,需要結合 CPU 核心數 和 其他性能指標 綜合分析。以下是具體判斷方法: 一、Load Average 的基本含義 定義:Load Average 表示 單位時間內處于可運行狀態(R)和不…

聊一聊接口測試更側重于哪方面的驗證

目錄 一、功能性驗證 輸入與輸出正確性 參數校驗 業務邏輯覆蓋 二、數據一致性驗證 數據格式規范 數據完整性 數據類型與范圍 三、異常場景驗證 容錯能力測試 邊界條件覆蓋 錯誤碼與信息清晰度 四、安全與權限驗證 身份認證 數據安全 防攻擊能力 五、性能與可…

Fiddler抓取APP端,HTTPS報錯全解析及解決方案(一篇解決常見問題)

環境&#xff1a;雷電模擬器Android9系統 ? 你所遇到的fiddler中抓取HTTPS的問題可以分為三類&#xff1a;一類是你自己證書安裝上邏輯錯誤&#xff0c;另一種是APP中使用了“證書固定”的手段。三類fiddler中生成證書時的參數過程。 1.Fiddler證書安裝上的邏輯錯誤 更新Opt…

OpenGL-ES 學習(15) ----紋理

目錄 紋理簡介紋理映射紋理映射流程示例代碼&#xff1a;紋理的環繞和過濾方式紋理的過濾方式 紋理簡介 現實生活中&#xff0c;紋理(Texture) 類似于游戲中皮膚的概念&#xff0c;最通常的作用是裝飾 3D 物體&#xff0c;它像貼紙一樣貼在物體的表面&#xff0c;豐富物體的表…

OpenCV計算機視覺實戰(2)——環境搭建與OpenCV簡介

OpenCV計算機視覺實戰&#xff08;2&#xff09;——環境搭建與OpenCV簡介 0. 前言1. OpenCV 安裝與配置1.1 安裝 Python-OpenCV1.2 配置開發環境 2. OpenCV 基礎2.1 圖像讀取與顯示2.2 圖像保存 3. 攝像頭實時捕獲小結系列鏈接 0. 前言 OpenCV (Open Source Computer Vision …

ubuntu22.04安裝顯卡驅動與cuda+cuDNN

背景&#xff1a; 緊接前文&#xff1a;Proxmox VE 8.4 顯卡直通完整指南&#xff1a;NVIDIA 2080 Ti 實戰。在R740服務器完成了proxmox的安裝&#xff0c;并且安裝了一張2080ti 魔改22g顯存的的顯卡。配置完了proxmox顯卡直通&#xff0c;并將顯卡掛載到了vm 301&#xff08;…

A2A Python 教程 - 綜合指南

目錄 ? 介紹? 設置環境? 創建項目? 代理技能? 代理卡片? A2A服務器? 與A2A服務器交互? 添加代理功能? 使用本地Ollama模型? 后續步驟 介紹 在本教程中&#xff0c;您將使用Python構建一個簡單的echo A2A服務器。這個基礎實現將向您展示A2A提供的所有功能。完成本教…

MySQL基礎關鍵_005_DQL(四)

目 錄 一、分組函數 1.說明 2.max/min 3.sum/avg/count 二、分組查詢 1.說明 2.實例 &#xff08;1&#xff09;查詢崗位和平均薪資 &#xff08;2&#xff09;查詢每個部門編號的不同崗位的最低薪資 3.having &#xff08;1&#xff09;說明 &#xff08;2&#xff…

GAMES202-高質量實時渲染(Assignment 2)

目錄 作業介紹環境光貼圖預計算傳輸項的預計算Diffuse unshadowedDiffuse shadowedDiffuse Inter-reflection(bonus) 實時球諧光照計算 GitHub主頁&#xff1a;https://github.com/sdpyy1 作業實現:https://github.com/sdpyy1/CppLearn/tree/main/games202 作業介紹 物體在不同…

2025年- H21-Lc129-160. 相交鏈表(鏈表)---java版

1.題目描述 2.思路 當pa&#xff01;pb的時候&#xff0c;執行pa不為空&#xff0c;遍歷pa鏈表。執行pb不為空&#xff0c;遍歷pb鏈表。 3.代碼實現 // 單鏈表節點定義 class ListNode {int val;ListNode next;ListNode(int x){valx;nextnull;}}public class H160 {// 主方法…

win10系統安卓開發環境搭建

一 安裝jdk 下載jdk17 ,下載路徑:https://download.oracle.com/java/17/archive/jdk-17.0.12_windows-x64_bin.exe 下載完畢后,按照提示一步步完成,然后接著創建環境變量, 在cmd控制臺輸入java -version 驗證: 有上面的輸出代表jdk安裝并配置成功。 二 安裝Android stu…

【算法基礎】選擇排序算法 - JAVA

一、算法基礎 1.1 什么是選擇排序 選擇排序是一種簡單直觀的排序算法&#xff0c;它的工作原理是&#xff1a;首先在未排序序列中找到最小&#xff08;或最大&#xff09;元素&#xff0c;存放到排序序列的起始位置&#xff0c;然后再從剩余未排序元素中繼續尋找最小&#xf…

LabVIEW異步調用VI介紹

在 LabVIEW 編程環境里&#xff0c;借助結合異步 VI 調用&#xff0c;并使用 “Open VI Reference” 函數上的 “Enable simultaneous calls on reentrant VIs” 選項&#xff08;0x40&#xff09;&#xff0c;達成了對多個 VI 調用執行效率的優化。以下將從多方面詳細介紹該 V…

Leetcode刷題 | Day50_圖論02_島嶼問題01_dfs兩種方法+bfs一種方法

一、學習任務 99. 島嶼數量_深搜dfs代碼隨想錄99. 島嶼數量_廣搜bfs100. 島嶼的最大面積101. 孤島的總面積 第一類DFS&#xff08;主函數中處理第一個節點&#xff0c;DFS處理相連節點&#xff09;&#xff1a; 主函數中先將起始節點標記為已訪問DFS函數中不處理起始節點&…

深入理解網絡安全中的加密技術

1 引言 在當今數字化的世界中&#xff0c;網絡安全已經成為個人隱私保護、企業數據安全乃至國家安全的重要組成部分。隨著網絡攻擊的復雜性和頻率不斷增加&#xff0c;保護敏感信息不被未授權訪問變得尤為關鍵。加密技術作為保障信息安全的核心手段&#xff0c;通過將信息轉換為…

舊版本NotionNext圖片失效最小改動解決思路

舊版本NotionNext圖片失效最小改動解決思路 契機 好久沒寫博客了&#xff0c;最近在notion寫博客的時候發現用notionNext同步到個人網站時&#xff0c;圖片無法預覽。猜測是notion加了防盜鏈措施&#xff0c;去notionNext官方github上尋找解決方案&#xff0c;需要升級到4.8.…

深度學習筆記40_中文文本分類-Pytorch實現

&#x1f368; 本文為&#x1f517;365天深度學習訓練營 中的學習記錄博客&#x1f356; 原作者&#xff1a;K同學啊 | 接輔導、項目定制 一、我的環境 1.語言環境&#xff1a;Python 3.8 2.編譯器&#xff1a;Pycharm 3.深度學習環境&#xff1a; torch1.12.1cu113torchvision…

010302-oss_反向代理_負載均衡-web擴展2-基礎入門-網絡安全

文章目錄 1 OSS1.1 什么是 OSS 存儲&#xff1f;1.2 OSS 核心功能1.3 OSS 的優勢1.4 典型使用場景1.5 如何接入 OSS&#xff1f;1.6 注意事項1.7 cloudreve實戰演示1.7.1 配置cloudreve連接阿里云oss1.7.2 常見錯誤1.7.3 安全測試影響 2 反向代理2.1 正向代理和反向代理2.2 演示…

【 Node.js】 Node.js安裝

下載 下載 | Node.js 中文網https://nodejs.cn/download/ 安裝 雙擊安裝包 點擊Next 勾選使用許可協議&#xff0c;點擊Next 選擇安裝位置 點擊Next 點擊Next 點擊Install 點擊Finish 完成安裝 添加環境變量 編輯【系統變量】下的變量【Path】添加Node.js的安裝路徑--如果…