文章目錄
- 什麼是C++?
- C++的發展史
- C++關鍵字(C++98)
- 命名空間
- 命名空間的定義
- 命名空間的使用
- C++中的輸入和輸出
- 預設參數
- 預設參數的概念
- 預設參數分類
- 全預設參數
- 半預設參數
- 函數重載
- 函數重載的概念
- 函數重載的原理(名字修飾)
- extern "C"
- 引用
- 引用的概念
- 引用的特性
- 常引用
- 引用的使用場景
- 引用和指針的差別
- 内聯函數
- 内聯函數的概念
- 内聯函數的特性
- auto關鍵字(C++11)
- auto簡介
- auto的使用細則
- auto不能推導的場景
- 基于範圍的for循環(C++11)
- 範圍for的文法
- 範圍for的使用條件
- 指針空值nullptr
- C++98中的指針空值
- C++11中的指針空值
本次内容大綱:
什麼是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++關鍵字(C++98)
C++中總計有63個關鍵字:
不要覺得很多,其實這其中有32個是C語言中的關鍵字:
命名空間
在C/C++中,變量、函數和類都是大量存在的,這些變量、函數和類的名稱都将作用于全局作用域中,可能會導緻很多命名沖突。
使用命名空間的目的就是對辨別符和名稱進行本地化,以避免命名沖突或名字污染,namespace關鍵字的出現就是針對這種問題的。
命名空間的定義
定義命名空間,需要使用到 namespace 關鍵字,後面跟命名空間的名字,然後接一對{}即可,{}中即為命名空間的成員。
一、命名空間的普通定義
//命名空間的普通定義
namespace N1 //N1為命名空間的名稱
{
//在命名空間中,既可以定義變量,也可以定義函數
int a;
int Add(int x, int y)
{
return x + y;
}
}
二、命名空間可以嵌套定義
//命名空間的嵌套定義
namespace N1 //定義一個名為N1的命名空間
{
int a;
int b;
namespace N2 //嵌套定義另一個名為N2的命名空間
{
int c;
int d;
}
}
三、同一個工程中允許存在多個相同名稱的命名空間,編譯器最後會将其成員合成在同一個命名空間中
是以我們不能在相同名稱的命名空間中定義兩個相同名稱的成員。
注意:一個命名空間就定義了一個新的作用域,命名空間中所有内容都局限于該命名空間中。
命名空間的使用
現在我們已經知道了如何定義命名空間,那麼我們應該如何使用命名空間中的成員呢?
命名空間的使用有以下三種方式:
一、加命名空間名稱及作用域限定符
符号“::”在C++中叫做作用域限定符,我們通過“命名空間名稱::命名空間成員”便可以通路到命名空間中相應的成員。
//加命名空間名稱及作用域限定符
#include <stdio.h>
namespace N
{
int a;
double b;
}
int main()
{
N::a = 10;//将命名空間中的成員a指派為10
printf("%d\n", N::a);//列印命名空間中的成員a
return 0;
}
二、使用using将命名空間中的成員引入
我們還可以通過“using 命名空間名稱::命名空間成員”的方式将命名空間中指定的成員引入。這樣一來,在該語句之後的代碼中就可以直接使用引入的成員變量了。
//使用using将命名空間中的成員引入
#include <stdio.h>
namespace N
{
int a;
double b;
}
using N::a;//将命名空間中的成員a引入
int main()
{
a = 10;//将命名空間中的成員a指派為10
printf("%d\n", a);//列印命名空間中的成員a
return 0;
}
三、使用using namespace 命名空間名稱引入
最後一種方式就是通過”using namespace 命名空間名稱“将命名空間中的全部成員引入。這樣一來,在該語句之後的代碼中就可以直接使用該命名空間内的全部成員了。
//使用using namespace 命名空間名稱引入
#include <stdio.h>
namespace N
{
int a;
double b;
}
using namespace N;//将命名空間N的所有成員引入
int main()
{
a = 10;//将命名空間中的成員a指派為10
printf("%d\n", a);//列印命名空間中的成員a
return 0;
}
C++中的輸入和輸出
我們在學C語言的時候,第一個寫的代碼就是在螢幕上輸出一個"hello world",按照學習計算機語言的習俗,現在我們也應該使用C++來和這個世界打個招呼了:
#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。
#include <iostream>
using namespace std;
int main()
{
int i;
double d;
char arr[20];
cin >> i;//讀取一個整型
cin >> d;//讀取一個浮點型
cin >> arr;//讀取一個字元串
cout << i << endl;//列印整型i
cout << d << endl;//列印浮點型d
cout << arr << endl;//列印字元串arr
return 0;
}
注:代碼中的endl的意思是輸出一個換行符。
預設參數
預設參數的概念
預設參數是指在聲明或定義函數時,為函數的參數指定一個預設值。在調用該函數時,如果沒有指定實參則采用該預設值,否則使用指定的實參。
#include <iostream>
using namespace std;
void Print(int a = 0)
{
cout << a << endl;
}
int main()
{
Print();//沒有指定實參,使用參數的預設值(列印0)
Print(10);//指定了實參,使用指定的實參(列印10)
return 0;
}
預設參數分類
全預設參數
全預設參數,即函數的全部形參都設定為預設參數。
void Print(int a = 10, int b = 20, int c = 30)
{
cout << a << endl;
cout << b << endl;
cout << c << endl;
}
半預設參數
半預設參數,即函數的參數不全為預設參數。
void Print(int a, int b, int c = 30)
{
cout << a << endl;
cout << b << endl;
cout << c << endl;
}
注意:
1、半預設參數必須從右往左依次給出,不能間隔着給。
//錯誤示例
void Print(int a, int b = 20, int c)
{
cout << a << endl;
cout << b << endl;
cout << c << endl;
}
2、預設參數不能在函數聲明和定義中同時出現
//錯誤示例
//test.h
void Print(int a, int b, int c = 30);
//test.c
void Print(int a, int b, int c = 30)
{
cout << a << endl;
cout << b << endl;
cout << c << endl;
}
預設參數隻能在函數聲明時出現,或者函數定義時出現(二者之一均正确)。
3、預設值必須是常量或者全局變量。
//正确示例
int x = 30;//全局變量
void Print(int a, int b = 20, int c = x)
{
cout << a << endl;
cout << b << endl;
cout << c << endl;
}
函數重載
函數重載的概念
函數重載是函數的一種特殊情況,C++允許在同一作用域中聲明幾個功能類似的同名函數,這些同名函數的形參清單必須不同。函數重載常用來處理實作功能類似,而資料類型不同的問題。
#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(1, 2) << endl;//列印1+2的結果
cout << Add(1.1, 2.2) << endl;//列印1.1+2.2的結果
return 0;
}
注意:形參清單不同是指參數個數、參數類型或者參數順序不同,若僅僅是傳回類型不同,則不能構成重載。
函數重載的原理(名字修飾)
為什麼C++支援函數重載,而C語言不支援函數重載呢?
我們知道,一個C/C++程式要運作起來都需要經曆以下幾個階段:預處理、編譯、彙編、連結。
我們知道,在編譯階段會将程式中的每個源檔案的全局範圍的變量符号分别進行彙總。在彙編階段會給每個源檔案彙總出來的符号配置設定一個位址(若符号隻是一個聲明,則給其配置設定一個無意義的位址),然後分别生成一個符号表。最後在連結期間會将每個源檔案的符号表進行合并,若不同源檔案的符号表中出現了相同的符号,則取合法的位址為合并後的位址(重定位)。
在C語言中,彙編階段進行符号彙總時,一個函數彙總後的符号就是其函數名,是以當彙總時發現多個相同的函數符号時,編譯器便會報錯。而C++在進行符号彙總時,對函數的名字修飾做了改動,函數彙總出的符号不再單單是函數的函數名,而是通過其參數的類型和個數以及順序等資訊彙總出 一個符号,這樣一來,就算是函數名相同的函數,隻要其參數的類型或參數的個數或參數的順序不同,那麼彙總出來的符号也就不同了。
注:不同編譯器下,對函數名的修飾不同,但都是一樣的。
總結:
1、C語言不能支援重載,是因為同名函數沒辦法區分。而C++是通過函數修飾規則來區分的,隻要函數的形參清單不同,修飾出來的名字就不一樣,也就支援了重載。
2、另外我們也了解了,為什麼函數重載要求參數不同,根傳回值沒關系。
extern “C”
有時候在C++工程中可能需要将某些函數按照C的風格來編譯,在函數前加“extern C”,意思是告訴編譯器,将該函數按照C語言規則來編譯。
注意:在函數前加“extern C”後,該函數便不能支援重載了。
引用
引用的概念
引用不是定義一個變量,而是已存在的變量取了一個别名,編譯器不會為引用變量開辟記憶體空間,它和它引用的變量共用同一塊記憶體空間。
其使用的基本形式為:類型& 引用變量名(對象名) = 引用實體。
#include <iostream>
using namespace std;
int main()
{
int a = 10;
int& b = a;//給變量a去了一個别名,叫b
cout << "a = " << a << endl;//a列印結果為10
cout << "b = " << b << endl;//b列印結果也是10
b = 20;//改變b也就是改變了a
cout << "a = " << a << endl;//a列印結果為20
cout << "b = " << b << endl;//b列印結果也是為20
return 0;
}
注:引用類型必須和引用實體是同種類型。
引用的特性
一、引用在定義時必須初始化
正确示例:
int a = 10;
int& b = a;//引用在定義時必須初始化
錯誤示例:
int c = 10;
int &d;//定義時未初始化
d = c;
二、一個變量可以有多個引用
例如:
int a = 10;
int& b = a;
int& c = a;
int& d = a;
此時,b、c、d都是變量a的引用。
三、引用一旦引用了一個實體,就不能再引用其他實體
例如:
int a = 10;
int& b = a;
此時,b已經是a的引用了,b不能再引用其他實體。如果你寫下以下代碼,想讓b轉而引用另一個變量c:
int a = 10;
int& b = a;
int c = 20;
b = c;//你的想法:讓b轉而引用c
但該代碼并沒有随你的意,該代碼的意思是:将b引用的實體指派為c,也就是将變量a的内容改成了20。
常引用
上面提到,引用類型必須和引用實體是同種類型的。但是僅僅是同種類型,還不能保證能夠引用成功,我們若用一個普通引用類型去引用其對應的類型,但該類型被const所修飾,那麼引用将不會成功。
int main()
{
const int a = 10;
//int& ra = a; //該語句編譯時會出錯,a為常量
const int& ra = a;//正确
//int& b = 10; //該語句編譯時會出錯,10為常量
const int& b = 10;//正确
return 0;
}
我們可以将被const修飾了的類型了解為安全的類型,因為其不能被修改。我們若将一個安全的類型交給一個不安全的類型(可被修改),那麼将不會成功。
引用的使用場景
一、引用做參數
還記得C語言中的交換函數,學習C語言的時候經常用交換函數來說明傳值和傳址的差別。現在我們學習了引用,可以不用指針作為形參了:
//交換函數
void Swap(int& a, int& b)
{
int tmp = a;
a = b;
b = tmp;
}
因為在這裡a和b是傳入實參的引用,我們将a和b的值交換,就相當于将傳入的兩個實參交換了。
二、引用做傳回值
當然引用也能做傳回值,但是要特别注意,我們傳回的資料不能是函數内部建立的普通局部變量,因為在函數内部定義的普通的局部變量會随着函數調用的結束而被銷毀。我們傳回的資料必須是被static修飾或者是動态開辟的或者是全局變量等不會随着函數調用的結束而被銷毀的資料。
int& Add(int a, int b)
{
static int c = a + b;
return c;
}
注意:如果函數傳回時,出了函數作用域,傳回對象還未還給系統,則可以使用引用傳回;如果已經還給系統了,則必須使用傳值傳回。
引用和指針的差別
在文法概念上,引用就是一個别名,沒有獨立的空間,其和引用實體共用同一塊空間。
int main()
{
int a = 10;
//在文法上,這裡給a這塊空間取了一個别名,沒有新開空間
int& ra = a;
ra = 20;
//在文法上,這裡定義了一個pa指針,開辟了4個位元組(32位平台)的空間,用于存儲a的位址
int* pa = &a;
*pa = 20;
return 0;
}
但是在底層實作上,引用實際是有空間的:
從彙編角度來看,引用的底層實作也是類似指針存位址的方式來處理的。
引用和指針的差別(重要):
1、引用在定義時必須初始化,指針沒有要求。
2、引用在初始化時引用一個實體後,就不能再引用其他實體,而指針可以在任何時候指向任何一個同類型實體。
3、沒有NULL引用,但有NULL指針。
4、在sizeof中的含義不同:引用的結果為引用類型的大小,但指針始終是位址空間所占位元組個數(32位平台下占4個位元組)。
5、引用進行自增操作就相當于實體增加1,而指針進行自增操作是指針向後偏移一個類型的大小。
6、有多級指針,但是沒有多級引用。
7、通路實體的方式不同,指針需要顯示解引用,而引用是編譯器自己處理。
8、引用比指針使用起來相對更安全。
内聯函數
内聯函數的概念
以inline修飾的函數叫做内聯函數,編譯時C++編譯器會在調用内聯函數的地方展開,沒有函數壓棧的開銷,内聯函數的使用可以提升程式的運作效率。
我們可以通過觀察調用普通函數和内聯函數的彙編代碼來進一步檢視其優勢:
int Add(int a, int b)
{
return a + b;
}
int main()
{
int ret = Add(1, 2);
return 0;
}
下圖左是以上代碼的彙編代碼,下圖右是函數Add加上inline後的彙編代碼:
從彙編代碼中可以看出,内聯函數調用時并沒有調用函數這個過程的彙編指令。
内聯函數的特性
1、inline是一種以空間換時間的做法,省了去調用函數的額外開銷。由于内聯函數會在調用的位置展開,是以代碼很長或者有遞歸的函數不适宜作為内聯函數。頻繁調用的小函數建議定義成内聯函數。
2、inline對于編譯器而言隻是一個建議,編譯器會自動優化,如果定義為inline的函數體内有遞歸等,編譯器優化時會忽略掉内聯。
3、inline不建議聲明和定義分離,分離會導緻連結錯誤。因為inline被展開,就沒有函數位址了連結就會找不到。
auto關鍵字(C++11)
auto簡介
在早期的C/C++中auto的含義是:使用auto修飾的變量是具有自動存儲器的局部變量,但遺憾的是一直沒有人去使用它。
在C++11中,标準委員會賦予了auto全新的含義:auto不再是一個存儲類型訓示符,而是作為一個新的類型訓示符來訓示編譯器,auto聲明的變量必須由編譯器在編譯時期推導而得。
#include <iostream>
using namespace std;
double Fun()
{
return 3.14;
}
int main()
{
int a = 10;
auto b = a;
auto c = 'A';
auto d = Fun();
//列印變量b,c,d的類型
cout << typeid(b).name() << endl;//列印結果為int
cout << typeid(c).name() << endl;//列印結果為char
cout << typeid(d).name() << endl;//列印結果為double
return 0;
}
注意:使用auto變量時必須對其進行初始化,在編譯階段編譯器需要根據初始化表達式來推導auto的實際類型。是以,auto并非是一種“類型”的聲明,而是一個類型聲明的“占位符”,編譯器在編譯期會将auto替換為變量實際的類型。
auto的使用細則
一、auto與指針和引用結合起來使用
用auto聲明指針類型時,用auto和auto*沒有任何差別,但用auto聲明引用類型時必須加&。
#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聲明引用時必須加&,否則建立的隻是與實體類型相同的普通變量。
二、在同一行定義多個變量
當在同一行聲明多個變量時,這些變量必須是相同的類型,否則編譯器将會報錯,因為編譯器實際隻對第一個類型進行推導,然後用推導出來的類型定義其他變量。
int main()
{
auto a = 1, b = 2; //正确
auto c = 3, d = 4.0; //編譯器報錯:“auto”必須始終推導為同一類型
return 0;
}
auto不能推導的場景
一、auto不能作為函數的參數
以下代碼編譯失敗,auto不能作為形參類型,因為編譯器無法對x的實際類型進行推導。
void TestAuto(auto x)
{}
二、auto不能直接用來聲明數組
int main()
{
int a[] = { 1, 2, 3 };
auto b[] = { 4, 5, 6 };//error
return 0;
}
基于範圍的for循環(C++11)
範圍for的文法
若是在C++98中我們要周遊一個數組,可以按照以下方式:
int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
//将數組元素值全部乘以2
for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
{
arr[i] *= 2;
}
//列印數組中的所有元素
for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
{
cout << arr[i] << " ";
}
cout << endl;
以上方式也是我們C語言中所用的周遊數組的方式,但對于一個有範圍的集合而言,循環是多餘的,有時還容易犯錯。是以C++11中引入了基于範圍的for循環。for循環後的括号由冒号分為兩部分:第一部分是範圍内用于疊代的變量,第二部分則表示被疊代的範圍。
int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
//将數組元素值全部乘以2
for (auto& e : arr)
{
e *= 2;
}
//列印數組中的所有元素
for (auto e : arr)
{
cout << e << " ";
}
cout << endl;
注意:與普通循環類似,可用continue來結束本次循環,也可以用break來跳出整個循環。
範圍for的使用條件
一、for循環疊代的範圍必須是确定的
對于數組而言,就是數組中第一個元素和最後一個元素的範圍;對于類而言,應該提供begin和end的方法,begin和end就是for循環疊代的範圍。
二、疊代的對象要實作++和==操作
這是關于疊代器的問題,大家先了解一下。
指針空值nullptr
C++98中的指針空值
在良好的C/C++程式設計習慣中,在聲明一個變量的同時最好給該變量一個合适的初始值,否則可能會出現不可預料的錯誤。比如未初始化的指針,如果一個指針沒有合法的指向,我們基本都是按如下方式對其進行初始化:
int* p1 = NULL;
int* p2 = 0;
NULL其實是一個宏,在傳統的C頭檔案(stddef.h)中可以看到如下代碼:
/* Define NULL pointer value */
#ifndef
#ifdef
#define
#else/* __cplusplus */
#define
#endif/* __cplusplus */
#endif/* NULL */
可以看到,NULL可能被定義為字面常量0,也可能被定義為無類型指針(void*)的常量。但是不論采取何種定義,在使用空值的指針時,都不可避免的會遇到一些麻煩,例如:
#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*)常量,但編譯器預設情況下将其看成是一個整型常量,如果要将其按照指針方式來使用,必須對其進行強制轉換。