tunyk avatar

C++基礎入門教程

🕕 by tunyk

什麼是C++

C語言是結構化和模組化的語言,適合處理較小規模的程式。 對於複雜的問題,規模較大的程式,需要高度 的抽象和建模時,C語言則不合適。 為了解決軟體危機, 20世紀80年代, 計算機界提出了OOP(object

oriented programming:面向物件)思想,支援面向物件的程式設計語言應運而生。 1982年,Bjarne Stroustrup博士在C語言的基礎上引入並擴充了面向物件的概念,發明瞭一種新的程序 語 言。 為了表達該語言與C語言的淵源關係,命名為C++。 因此:C++是基於C語言而產生的,它既可以進行C語 言的過程化程序設計,又可以進行以抽象數據類型為特點的基於物件的程序設計,還可以進行面向物件的 程 序設計。


C++的發展史

1979年,貝爾實驗室的本賈尼等人試圖分析unix內核的時候,試圖將內核模組化於是在C語言的基礎上進行擴展,增加了類的機制,完成了一個可以運行的預處理程式,稱之為C with classes。 語言的發展也是隨著時代的進步,在逐步遞進的,讓我們來看看C++的歷史版本:

階段 內容
C with classes 類及派生類、公有和私有成員、類的構造析構、友元、內聯函數、賦值運算元重載等
C++1.0 添加虛函數概念,函數和運算元重載,引用、常量等
C++2.0 更加完善支援面向物件,新增保護成員、多重繼承、物件的初始化、抽象類、靜態成員以及const成員函數
C++3.0 進一步完善,引入範本,解決多重繼承產生的二義性問題和相應構造和析構的處理
C++98 C++標準第一個版本,絕大多數編譯器都支援,得到了國際標準化組織(ISO)和美國標準化協會認可,以範本方式重寫C++標準庫,引入了STL(標準範本庫)
C++03 C++標準第二個版本,語言特性無大改變,主要∶修訂錯誤、減少多異性
C++05 C++標準委員會發佈了一份計數報告(Technical Report,TR1),正式更名C++0x,即∶計劃在本世紀第一個10年的某個時間發佈
C++11 增加了許多特性,使得C++更像一種新語言,比如∶正則表達式、基於範圍for迴圈、auto關鍵字、新容器、清單初始化、標準線程庫等
C++14 對C++11的擴展,主要是修復C++11中漏洞以及改進,比如∶泛型的lambda表達式,auto的返回值類型推導,二進位字面常量等
C++17 在C++11上做了一些小幅改進,增加了19個新特性,比如∶static_assert()的文本資訊可選,Fold表達式用於可變的範本,if和switch語句中的初始化器等

C++關鍵字

C++中總計有63個關鍵字:在這裡插入圖片描述

其中畫圈的是C語言的關鍵字。这里要注意了:false和true并不是C语言的关键字。


命名空間

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

命名空間的定義

定義命名空間,需要使用到namespace關鍵字,後面跟命名空間的名字,然後接一對{}即可,{}中即為命名 空間的成員。

注意:一个命名空间就定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间中

1.命名空間的普通定義

1
2
3
4
5
6
7
8
9
10
//1. 普通的命名空间,里面可以定义变量,也可以定义函数
namespace xjt    
{
	int printf = 1;
	int rand = 2;
	int Add(int a, int b)
	{
		return a + b;
	}
}

2.命名空間可以嵌套

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//2.命名空间可以嵌套
namespace xjt
{
	int printf = 1;
	int rand = 2;
	int Add(int a, int b)
	{
		return a + b;
	}
	namespace xjt2
	{
		int a = 0; 
		int Sub(int a, int b)
		{
			return a - b;
		}
	}
}

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

1
2
3
4
5
6
//3. 同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中。
namespace xjt
{
	int a = 3;
	int b = 1;
}

它會與上面的xjt命名空間合併

命名空間使用

下面來看這麼一段代碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
namespace xjt
{
	int printf = 1;
	int rand = 2;
	int Add(int a, int b)
	{
		return a + b;
	}
}
#include<iostream>

int main()
{
	
	printf("%d\n",printf);  //这样打印出来的结果和我们预期的不一样,因为你这样调用的是printf的地址通过下面两个可以加深理解
	printf("%p\n", printf);  //6A35CE70
	printf("%p\n", rand);   //6A42FAB0;
}

在這裡插入圖片描述

很显然直接打印printf是不可能的,因为你这样调用的是printf的地址,所以会出现的这样的结果,正面的调用方法为以下三种。

1.加命名空间名称及作用域限定符

符号“::”在C++中叫做作用域限定符,我们通过“命名空间名称::命名空间成员”便可以访问到命名空间中相应的成员

在這裡插入圖片描述

2.使用using namespace 命名空間名稱引入

在這裡插入圖片描述

但是這種方式存在著一些弊端,如果我們在命名空間中定義了一個名字為printf的變數,那麼之後再將namespace xjt這個命名空間引入的話,就會造成命名的污染了。 為了解決這個問題,出現了第三種引入在這裡插入圖片描述 方法。

3.使用using將命名空間中的成員引入

在這裡插入圖片描述

這種方法可以防止命名的污染,因為它只引入了一部分。


C++中的输入和输出

新生婴儿会以自己独特的方式向这个崭新的世界打招呼,C++刚出来后,也算是一个新事物,那C++是否也应该向这个美好的世界来声问候呢?我们来看下C++是如何来实现问候的。

1
2
3
4
5
6
7
#include<iostream>
using namespace std;
int main()
{
 cout<<"Hello world!!!"<<endl;
 return 0;
}

在C语言中有标准输入输出函数scanf和printf,而在C++中有cin标准输入cout标准输出。在C语言中使用scanf和printf函数,需要包含头文件stdio.h。在C++中使用cin和cout,需要包含头文件iostream以及std标准命名空间

C++的输入输出方式与C语言更加方便,因为C++的输入输出不需要控制格式,例如:整型为%d,字符型为%c。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include<iostream>
using namespace std;

int main()
{
	int a = 1;
	float b = 2.1;
	double c= 2.111;
	char arr[10] = { 0 };
	char d[] = "hello world";
	cin >> arr;
	cout << arr << endl;
	cout << a << endl;
	cout << b << endl;
	cout << c << endl;
	cout << d << endl;
	return 0;
}

注意:endl,這其中的l不是阿拉伯數位1,而是26個英文字母的l,它的作用相當於換行。

這裏我們還要注意下cin的特點,他和C語言中的gets有些像,gets是遇到換行符停止,而cin是以遇到空格,tab或者換行符作為分隔符的,因此這兒輸入hello world會被空格符分隔開來。 這兒我輸入的是hello world,但因為輸入時出現了空格,所以之後的內容並不會讀入,因此arr中存的就是hello。 在這裡插入圖片描述


缺省參數

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//缺省参数
#include<iostream>

using namespace std;

//这儿的0就相当于缺省参数,如果实参什么都没传过来,缺省参数就赋值给a,相当于备胎的意思。
void func(int a = 0)
{
	cout << a << endl;
}

int main()
{
	func(10);
	func();  //在c语言中这样写肯定是不行的,但是在c++中有了缺省参数,如果你什么都不传,只要你前面有缺省参数的存在,就能过。
	return 0;
}

在這裡插入圖片描述

全缺省

全缺省参数,即函数的全部形参都设置为缺省参数。

1
//全缺省#include<iostream>using namespace std;void func(int a = 0, int b = 1, int c = 2){	cout <<"a="<< a << endl;	cout << b << endl;	cout << c << endl;}int main(){	func();	return 0;}

半缺省參數

1
void func(int a, int b, int c = 2){	cout << a << endl;	cout << b << endl;	cout << c << endl;}

注意: 1、半缺省參數必須從右往左依次給出,不能間隔著給。

1
//错误示例void func(int a, int b = 2, int c){	cout << a << endl;	cout << b << endl;	cout << c << endl;}

2、缺省參數不能在函數聲明和定義中同時出現

1
//错误示例//test.hvoid func(int a, int b, int c = 3);//test.cvoid func(int a, int b, int c = 2){	cout << a << endl;	cout << b << endl;	cout << c << endl;}

因為:如果聲明與定義位置同時出現,恰巧兩個位置提供的值不同,那編譯器就無法確定到底該用那 個缺省值。

3、缺省值必須是常量或者全域變數。

1
//正确示例int x = 3;//全局变量void func(int a, int b = 2, int c = x){	cout << a << endl;	cout << b << endl;	cout << c << endl;}


函數重載

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

1
#include <iostream>using namespace std;int Add(int x, int y){	return x + y;}double Add(double x, double y){	return x + y;}int main(){	cout << Add(0,1) << endl;//打印0+1的结果	cout << Add(1.1,2.2) << endl;//打印1.1+2.2的结果	return 0;}

注意:若僅僅只有返回值不同,其他都相同,則不構成函數重載。

1
short Add(short left, short right) {    return left+right; }int Add(short left, short right) {    return left+right; }

函數重載的原理

為什麼C++支援函數重載,而C語言不可以了? 這裡我們就要回顧一下以前的知識了,在運行到執行檔前,要經過:預編譯,編譯,彙編,鏈接這些階段 其實問題就出在編譯完之後的彙編階段,因為在這裡C++和C語言有著些許的不同,下面我們來看看:

採用C語言編譯器編譯之後

在這裡插入圖片描述

採用C++編譯器編譯之後

在這裡插入圖片描述

總結:

1.其實歸根到底,還是因為C編譯器和C++編譯器對函數名的修飾不同。 在gcc下的修飾規則是:【_Z+函數長度+函數名+類 型首字母】。 2.這其實也告訴我們為什麼函數的返回類型不同,不會構成函數重載,因為修飾規則並不會受返回值的影響。


extern “C”

有時候在C++工程中可能需要將某些函數按照C的風格來編譯,在函數前加extern "C",意思是告訴編譯器 , 將該函數按照C語言規則來編譯。 比如:tcmalloc是google用C++實現的一個專案,他提供tcmallc()和tcfree 兩個介面來使用,但如果是C專案就沒辦法使用,那麼他就使用extern "C"來解決。


引用

引用不是新定義一個變數,而是給已存在變數取了一個別名,編譯器不會為引用變數開闢記憶體空間,它和它 引用的變數共用同一塊記憶體空間

類型& 引用變數名稱(物件名) = 引用實體;

1
#include<iostream>using namespace std;int main(){	int a = 1;	int&b = a; //相当于给a起了一个别名为b,int是b的类型	cout << a << endl;	cout << b << endl;	b = 3;  //改变b也就相当于改变了a	cout << b << endl;	cout << a << endl;}

在這裡插入圖片描述

注意:參考類型必須與參考實體是同種類型的

引用的特徵

1.引用在定義時必須初始化

1
//正确示例int a = 10;int& b = a;//引用在定义时必须初始化//错误示例int a = 10;int &b;//定义时未初始化b = a;

2.一個變數可以有多個引用

1
int a = 10;int& b = a;int& c = a;int& d = a;

3.引用一旦引用了一個實體,就不能再引用其他實體

1
int a = 10;	int& b = a;	int c = 20;	b = c;//你的想法:让b转而引用c

在這裡插入圖片描述

但實際的效果,確實將c的值賦值給b,又因為b是a的引用,所以a的值見解變成了20。

常引用

上面提到,引用類型必須和引用實體是同種類型的。 但是僅僅是同種類型,還不能保證能夠引用成功,這兒我們還要注意可否可以修改的問題。

1
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;  }

這裡的a,b,d都是常量,常量是不可以被修改的,但是如果你用int&ra等這樣來引用a的話,那麼引用的這個a是可以被修改的,因此會出問題。 下面我們來看這麼一段代碼:

1
#include<iostream>using namespace std;int main(){	int a = 10;	double&ra = a;}

這個引用對嗎? 想要弄明白這個問題,首先要明白隱士類型提升的問題,在這裡int到double存在隱士類型的提升,而在提升的過程中系統會創建一個常量區來存放a類型提升后的結果。 因此到這兒,這段代碼一看就是錯了,因為你隱士類型提升時a是存放在常量區中的,常量區是不可以被修改的,而你用double&ra去引用他,ra這個引用是可以被修改的。

加個const就可以解決這個問題。

1
#include<iostream>using namespace std;int main(){	int a = 10;	const double&ra = a;}

注意:將不可修改的量用可讀可寫的量來引用是不可以的,但是反過來是可以的,將可讀可寫的量用只可讀的量來引用是可以的。

引用的使用場景

1.引用做參數

還記得C語言中的交換函數,學習C語言的時候經常用交換函數來說明傳值和傳址的區別。 現在我們學習了引用,可以不用指標作為形參了。 因為在這裡a和b是傳入實參的引用,我們將a和b的值交換,就相當於將傳入的兩個實參交換了。

1
//交换函数void Swap(int& a, int& b){	int tmp = a;	a = b;	b = tmp;}

2.引用做返回值

當然引用也能做返回值,但是要特別注意,我們返回的數據不能是函數內部創建的普通局部變數,因為在函數內部定義的普通的局部變數會隨著函數調用的結束而被銷毀。 我們返回的數據必須是被static修飾或者是動態開闢的或者是全局變數等不會隨著函數調用的結束而被銷毀的數據。

不加static的後果

在這裡插入圖片描述

你是不是疑惑為什麼列印的不是2而是7了?

在這裡插入圖片描述

這人就更奇怪了,為什麼中間加了一句printf,就列印隨機值了? 下面我們來看看分析:

在這裡插入圖片描述

為什麼會出現隨機值,因為你在函數里定義的變數是臨時變數,出了函數函數是會銷毀的,這時它就隨機指向記憶體中的一塊空間了。 所以在引用做函數返回值時最好還是給在函數中定義的變數加上static。

這時你覺得你真的懂這段代碼了嗎?

1
#include<iostream>using namespace std;int& Add(int a, int b){	static int c = a + b;	return c;}int main(){	int& ans = Add(1,2);	Add(3, 4);	cout << ans << endl;}

在這裡插入圖片描述

可能你會好奇了? 為什麼這兒是3了? 下面來看看分析

在這裡插入圖片描述

其實你換種寫法,這兒的結果就會換成7,原因也很簡單,正是上面圖片中說的原因

在這裡插入圖片描述

注意:如果函數返回時,出了函數作用域,返回物件還未還給系統,則可以使用引用返回;如果已經還給系統了,則必須使用傳值返回。 這句話說的是下面這種例子:

1
int& Add(int a, int b){    int c=a+b;  //出了函数作用域,c不在,回给了系统	return c;}int& Add(int a,int b){    static c=a+b;  //出了函数作用域,c还在,可以用引用返回    return c;}

大家是不是感覺這個傳引用返回用起來很怪了,下面我們來分析一下它是如何返回的。

在這裡插入圖片描述

總結: 传值的过程中会产生一个拷贝,而传引用的过程中不会,其实在做函数参数时也具有这个特点。

引用和指標的區別

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

1
int main(){ int a = 10; int& ra = a;  cout<<"&a = "<<&a<<endl; cout<<"&ra = "<<&ra<<endl; return 0; }

在這裡插入圖片描述

在底層實現上實際是有空間的,因為引用是按照指標方式來實現的。

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

我們來看下引用和指標的彙編代碼對比

在這裡插入圖片描述

引用和指標的區別 1、引用在定義時必須初始化,指標沒有要求。 2、引用在初始化時引用一個實體后,就不能再引用其他實體,而指標可以在任何時候指向任何一個同類型實體。 3、沒有NULL引用,但有NULL指標。 4、在sizeof中的含義不同:引用的結果為引用類型的大小,但指標始終是位址空間所占位元組個數(32位平臺下佔4個字節)。 5、引用進行自增操作就相當於實體增加1,而指標進行自增操作是指針向後偏移一個類型的大小。 6、有多級指標,但是沒有多級引用。 7、訪問實體的方式不同,指標需要顯示解引用,而引用是編譯器自己處理。 8、引用比指標使用起來相對更安全。


內聯函數

概念:以inline修飾的函數叫做內聯函數,編譯時C++編譯器會在調用內聯函數的地方展開,沒有函數壓棧的開 銷, 內聯函數提升程序運行的效率。 (看到在加粗部分時,小夥伴肯定會想,這和c語言中的宏是不是很像了? )

在這裡插入圖片描述

如果在上述函數前增加inline關鍵字將其改成內聯函數,在編譯期間編譯器會用函數體替換函數的調用

在這裡插入圖片描述

特性

  1. inline是一種以空間換時間的做法,省去調用函數額開銷。 所以代碼很長/遞歸的函數不適宜 使用作為內聯函數。
  2. inline對於編譯器而言只是一個建議,編譯器會自動優化,如果定義為inline的函數體內代碼比較長/遞歸等 等,編譯器優化時會忽略掉內聯。
  3. inline不建議聲明和定義分離,分離會導致連結錯誤。 因為inline被展開,就沒有函數位址了,連結就會 找不到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//F.h
#include <iostream>
using namespace std;

inline void f(int i);

// F.
#include "F.h"
void f(int i) {
	cout << i << endl;
}

// main.
#include "F.h"
int main()
{
	f(10);
	return 0;
}
// 链接错误:main.obj : error LNK2019: 无法解析的外部符号 "void __cdecl f(int)" (?
// [email protected]@[email protected]),该符号在函数 _main 中被引用

c++有哪些技術可以代替宏

C++有哪些技術替代宏?

  1. 常量定義 換用const
  2. 函數定義 換用內聯函數

auto關鍵字(C++11)

在早期的C/C++中auto的含義是:使用auto修飾的變數是具有自動記憶體的局部變數,但遺憾的是一直沒有人去使用它。 在C++11中,標準委員會賦予了auto全新的含義:auto不再是一個存儲類型指示符,而是作為一個新的類型指示符來指示編譯器,auto聲明的變數必須由編譯器在編譯時期推導而得。 可能光看這一句話,你不一定能懂,下面我們舉幾個例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include<iostream>
using namespace std;
int TestAuto()
{
	return 10;
}
int main()
{
	int a = 10;
	auto b = a;
	auto c = 'a';
	auto d = TestAuto();

	cout << typeid(b).name() << endl; //这个地方要学到后面类的时候才可以解释,这里打印出的是类型名
	cout << typeid(c).name() << endl;
	cout << typeid(d).name() << endl;

	cout << a << endl;
	cout << b<< endl;
	cout << c << endl;
	cout << d << endl;

	//auto e; 无法通过编译,使用auto定义变量时必须对其进行初始化
	return 0;
}

在這裡插入圖片描述

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

auto的使用細則

1.auto與指標和引用結合起來使用

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
using namespace std;
int main()
{
	int a = 10;
	auto b = &a;   //自动推导出b的类型为int*
	auto* c = &a;  //自动推导出c的类型为int*
	auto& d = a;   //自动推导出d的类型为int
	//打印变量b,c,d的类型
	cout << typeid(b).name() << endl;//打印结果为int*
	cout << typeid(c).name() << endl;//打印结果为int*
	cout << typeid(d).name() << endl;//打印结果为int
	return 0;
}

注意:用auto聲明引用時必須加&,否則創建的只是與實體類型相同的普通變數,只不過將其換了個姓名而已。

2.在同一行定義多個變數

當在同一行聲明多個變數時,這些變數必須是相同的類型,否則編譯器將會報錯,因為編譯器實際只對 第一個類型進行推導,然後用推導出來的類型定義其他變數。

1
2
3
4
5
void TestAuto()
{
 auto a = 1, b = 2; 
 auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同
}

auto不能推導的場景

1.auto做為函數的參數

1
2
3
// 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
void TestAuto(auto a)
{}

2.auto不能直接用來聲明陣列

1
2
3
4
5
void TestAuto()
{
 int a[] = {1,2,3};
 auto b[] = {4,5,6};
}

為了避免與C++98中的auto發生混淆,C++11隻保留了auto作為類型指示符的用法 auto在實際中最常見的優勢用法就是跟以後會講到的C++11提供的新式for迴圈,還有lambda表達式 等 進行配合使用。


基於範圍的for迴圈(C++11)

範圍for的語法

在C++98中如果要遍曆一個陣列,可以按照以下方式進行:

1
2
3
4
5
6
7
8
9
void TestFor()
{
 int array[] = { 1, 2, 3, 4, 5 };
 //将数组所有元素乘以2
 for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
 array[i] *= 2;
 
 for (int* p = array; p < array + sizeof(array)/ sizeof(array[0]); ++p)
 cout << *p << endl; }

對於一個有範圍的集合而言,由程式師來說明迴圈的範圍是多餘的,有時候還會容易犯錯誤。 因此C++11中 引入了基於範圍的for迴圈。 for迴圈後的括弧由冒號" :"分為兩部分:第一部分是範圍內用於反覆運算的變數, 第二部分則表示被反覆運算的範圍。

注意不能寫成auto,不然改變不了原陣列

在這裡插入圖片描述

正確的寫法

1
2
3
4
5
6
7
8
9
10
11
12
void TestFor()
{
 int array[] = { 1, 2, 3, 4, 5 };
 //将数组中所有元素乘以2
 for(auto& e : array)
 e *= 2;
 
 for(auto e : array)
 cout << e << " ";
 
 return 0; 
 }

注意:與普通迴圈類似,可用continue來結束本次迴圈,也可以用break來跳出整個迴圈。

範圍for的使用條件

1.for迴圈反覆運算的範圍必須是確定的

對於陣列而言,就是陣列中第一個元素和最後一個元素的範圍;對於類而言,應該提供begin和end的 方法,begin和end就是for迴圈反覆運算的範圍。

注意:以下代碼就有問題,因為for的範圍不確定

1
2
3
4
void TestFor(int array[])
{
 for(auto& e : array)  //这里的array其实不是数组,数组在传参时会退化成指针
 cout<< e <<endl; }

2. 反覆運算的物件要實現++和==的操作。

關於反覆運算器這個問題,以後會講,現在大家瞭解一下就可以了。


指標空值nullptr

C++98中的指標空值

在良好的C/C++程式設計習慣中,在聲明一個變數的同時最好給該變數一個合適的初始值,否則可能會出現不可預料的錯誤。 比如未初始化的指標,如果一個指標沒有合法的指向,我們基本都是按如下方式對其進行初始化:

1
2
int* p1 = NULL;
int* p2 = 0;

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

1
2
3
4
5
6
7
#ifndef NULL
#ifdef __cplusplus
#define NULL    0
#else  
#define NULL    ((void *)0)
#endif  
#endif

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
using namespace std;
void Fun(int p)
{
	cout << "Fun(int)" << endl;
}
void Fun(int* p)
{
	cout << "Fun(int*)" << endl;
}
int main()
{
	Fun(0);           //打印结果为 Fun(int)
	Fun(NULL);        //打印结果为 Fun(int)
	Fun((int*)NULL);  //打印结果为 Fun(int*)
	return 0;
}

程式本意本意是想通過Fun(NULL)調用指標版本的Fun(int* p)函數,但是由於NULL被定義為0,Fun(NULL)最終調用的是Fun(int p)函數。

注:在C++98中字面常量0,既可以是一個整型數位,也可以是無類型的指標(void*)常量,但編譯器默認情況下將其看成是一個整型常量,如果要將其按照指標方式來使用,必須對其進行強制轉換。

C++11中的指標空值

對於C++98中的問題,C++11引入了關鍵字nullptr。 在使用nullptr表示指標空值時,不需要包含頭文件,因為nullptr是C++11作為關鍵字引入的。 在C++11中,sizeof(nullptr)與sizeof((void*)0)所佔的位元元組數相同,大小都為4。

💘 相关文章

写一条评论

Based on Golang + fastHTTP + sdb | go1.16.4 Processed in 2ms