C語言是結構化和模組化的語言,適合處理較小規模的程式。對於複雜的問題,規模較大的程式,需要高度
的抽象和建模時,C語言則不合適。為了解決軟體危機, 20世紀80年代, 計算機界提出了OOP(object
oriented programming:物件導向)思想,支援物件導向的程式設計語言應運而生。
1982年,Bjarne Stroustrup博士在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++中總計有63個關鍵字:
其中畫圈的是C語言的關鍵字。
這裡要注意了:false和true並不是C語言的關鍵字。
在C/C++中,變數、函數和類都是大量存在的,這些變數、函數和類的名稱都將作用於全域性作用域中,可能會導致很多命名衝突。
使用名稱空間的目的就是對識別符號和名稱進行在地化,以避免命名衝突或名字汙染,namespace關鍵字的出現就是針對這種問題的。
定義名稱空間,需要使用到namespace關鍵字,後面跟名稱空間的名字,然後接一對{}即可,{}中即為命名
空間的成員。
注意:一個名稱空間就定義了一個新的作用域,名稱空間中的所有內容都侷限於該名稱空間中
//1. 普通的名稱空間,裡面可以定義變數,也可以定義函數
namespace xjt
{
int printf = 1;
int rand = 2;
int Add(int a, int b)
{
return a + b;
}
}
//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. 同一個工程中允許存在多個相同名稱的名稱空間,編譯器最後會合成同一個名稱空間中。
namespace xjt
{
int a = 3;
int b = 1;
}
它會與上面的xjt名稱空間合併
下面來看這麼一段程式碼
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的地址,所以會出現的這樣的結果,正面的呼叫方法為以下三種。
符號「::」在C++中叫做作用域限定符,我們通過「名稱空間名稱::名稱空間成員」便可以存取到名稱空間中相應的成員
但是這種方式存在著一些弊端,如果我們在名稱空間中定義了一個名字為printf的變數,那麼之後再將namespace xjt這個名稱空間引入的話,就會造成命名的汙染了。
為了解決這個問題,出現了第三種引入方法。
這種方法可以防止命名的汙染,因為它只引入了一部分。
新生嬰兒會以自己獨特的方式向這個嶄新的世界打招呼,C++剛出來後,也算是一個新事物,那C++是否也應該向這個美好的世界來聲問候呢?我們來看下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 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。
預設引數是宣告或定義函數時為函數的引數指定一個預設值。在呼叫該函數時,如果沒有指定實參則採用該
預設值,否則使用指定的實參。
//預設引數
#include<iostream>
using namespace std;
//這兒的0就相當於預設引數,如果實參什麼都沒傳過來,預設引數就賦值給a,相當於備胎的意思。
void func(int a = 0)
{
cout << a << endl;
}
int main()
{
func(10);
func(); //在c語言中這樣寫肯定是不行的,但是在c++中有了預設引數,如果你什麼都不傳,只要你前面有預設引數的存在,就能過。
return 0;
}
全預設引數,即函數的全部形參都設定為預設引數。
//全預設
#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;
}
void func(int a, int b, int c = 2)
{
cout << a << endl;
cout << b << endl;
cout << c << endl;
}
注意:
1、半預設引數必須從右往左依次給出,不能間隔著給。
//錯誤範例
void func(int a, int b = 2, int c)
{
cout << a << endl;
cout << b << endl;
cout << c << endl;
}
2、預設引數不能在函數宣告和定義中同時出現
//錯誤範例
//test.h
void func(int a, int b, int c = 3);
//test.c
void func(int a, int b, int c = 2)
{
cout << a << endl;
cout << b << endl;
cout << c << endl;
}
因為:如果宣告與定義位置同時出現,恰巧兩個位置提供的值不同,那編譯器就無法確定到底該用那
個預設值。
3、預設值必須是常數或者全域性變數。
//正確範例
int x = 3;//全域性變數
void func(int a, int b = 2, 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(0,1) << endl;//列印0+1的結果
cout << Add(1.1,2.2) << endl;//列印1.1+2.2的結果
return 0;
}
注意:若僅僅只有返回值不同,其他都相同,則不構成函數過載。
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.這其實也告訴我們為什麼函數的返回型別不同,不會構成函數過載,因為修飾規則並不會受返回值的影響。
有時候在C++工程中可能需要將某些函數按照C的風格來編譯,在函數前加extern 「C」,意思是告訴編譯器,
將該函數按照C語言規則來編譯。比如:tcmalloc是google用C++實現的一個專案,他提供tcmallc()和tcfree
兩個介面來使用,但如果是C專案就沒辦法使用,那麼他就使用extern 「C」來解決。
參照不是新定義一個變數,而是給已存在變數取了一個別名,編譯器不會為參照變數開闢記憶體空間,它和它
參照的變數共用同一塊記憶體空間。型別& 參照變數名(物件名) = 參照實體;
#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;
}
注意:參照型別必須和參照實體是同種型別的
//正確範例
int a = 10;
int& b = a;//參照在定義時必須初始化
//錯誤範例
int a = 10;
int &b;//定義時未初始化
b = a;
int a = 10;
int& b = a;
int& c = a;
int& d = a;
int a = 10;
int& b = a;
int c = 20;
b = c;//你的想法:讓b轉而參照c
但實際的效果,確實將c的值賦值給b,又因為b是a的參照,所以a的值見解變成了20。
上面提到,參照型別必須和參照實體是同種型別的。但是僅僅是同種型別,還不能保證能夠參照成功,這兒我們還要注意可否可以修改的問題。
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是可以被修改的,因此會出問題。
下面我們來看這麼一段程式碼:
#include<iostream>
using namespace std;
int main()
{
int a = 10;
double&ra = a;
}
這個參照對嗎?想要弄明白這個問題,首先要明白隱士型別提升的問題,在這裡int到double存在隱士型別的提升,而在提升的過程中系統會建立一個常數區來存放a型別提升後的結果。因此到這兒,這段程式碼一看就是錯了,因為你隱士型別提升時a是存放在常數區中的,常數區是不可以被修改的,而你用double&ra去參照他,ra這個參照是可以被修改的。
加個const就可以解決這個問題。
#include<iostream>
using namespace std;
int main()
{
int a = 10;
const double&ra = a;
}
注意:將不可修改的量用可讀可寫的量來參照是不可以的,但是反過來是可以的,將可讀可寫的量用只可讀的量來參照是可以的。
還記得C語言中的交換函數,學習C語言的時候經常用交換函數來說明傳值和傳址的區別。現在我們學習了參照,可以不用指標作為形參了。因為在這裡a和b是傳入實參的參照,我們將a和b的值交換,就相當於將傳入的兩個實參交換了。
//交換函數
void Swap(int& a, int& b)
{
int tmp = a;
a = b;
b = tmp;
}
當然參照也能做返回值,但是要特別注意,我們返回的資料不能是函數內部建立的普通區域性變數,因為在函數內部定義的普通的區域性變數會隨著函數呼叫的結束而被銷燬。我們返回的資料必須是被static修飾或者是動態開闢的或者是全域性變數等不會隨著函數呼叫的結束而被銷燬的資料。
不加static的後果
你是不是疑惑為什麼列印的不是2而是7了?
這人就更奇怪了,為什麼中間加了一句printf,就列印隨機值了?
下面我們來看看分析:
為什麼會出現隨機值,因為你在函數裡定義的變數是臨時變數,出了函數函數是會銷燬的,這時它就隨機指向記憶體中的一塊空間了。所以在參照做函數返回值時最好還是給在函數中定義的變數加上static。
這時你覺得你真的懂這段程式碼了嗎?
#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,原因也很簡單,正是上面圖片中說的原因
注意:如果函數返回時,出了函數作用域,返回物件還未還給系統,則可以使用參照返回;如果已經還給系統了,則必須使用傳值返回。
這句話說的是下面這種例子:
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;
}
大家是不是感覺這個傳參照返回用起來很怪了,下面我們來分析一下它是如何返回的。
總結:
傳值的過程中會產生一個拷貝,而傳參照的過程中不會,其實在做函數引數時也具有這個特點。
在語法概念上參照就是一個別名,沒有獨立空間,和其參照實體共用同一塊空間。
int main()
{
int a = 10;
int& ra = a;
cout<<"&a = "<<&a<<endl;
cout<<"&ra = "<<&ra<<endl;
return 0; }
在底層實現上實際是有空間的,因為參照是按照指標方式來實現的。
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關鍵字將其改成行內函式,在編譯期間編譯器會用函數體替換函數的呼叫
- inline是一種以空間換時間的做法,省去呼叫函數額開銷。所以程式碼很長/遞迴的函數不適宜
使用作為行內函式。- 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()
{
f(10);
return 0;
}
// 連結錯誤:main.obj : error LNK2019: 無法解析的外部符號 "void __cdecl f(int)" (?
// f@@YAXH@Z),該符號在函數 _main 中被參照
C++有哪些技術替代宏?
- 常數定義 換用const
- 函數定義 換用行內函式
在早期的C/C++中auto的含義是:使用auto修飾的變數是具有自動記憶體的區域性變數,但遺憾的是一直沒有人去使用它。
在C++11中,標準委員會賦予了auto全新的含義:auto不再是一個儲存型別指示符,而是作為一個新的型別指示符來指示編譯器,auto宣告的變數必須由編譯器在編譯時期推導而得。 可能光看這一句話,你不一定能懂,下面我們舉幾個例子。
#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宣告指標型別時,用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宣告參照時必須加&,否則建立的只是與實體型別相同的普通變數,只不過將其換了個姓名而已。
當在同一行宣告多個變數時,這些變數必須是相同的型別,否則編譯器將會報錯,因為編譯器實際只對
第一個型別進行推導,然後用推匯出來的型別定義其他變數。
void TestAuto()
{
auto a = 1, b = 2;
auto c = 3, d = 4.0; // 該行程式碼會編譯失敗,因為c和d的初始化表示式型別不同
}
// 此處程式碼編譯失敗,auto不能作為形參型別,因為編譯器無法對a的實際型別進行推導
void TestAuto(auto a)
{}
void TestAuto()
{
int a[] = {1,2,3};
auto b[] = {4,5,6};
}
為了避免與C++98中的auto發生混淆,C++11只保留了auto作為型別指示符的用法
auto在實際中最常見的優勢用法就是跟以後會講到的C++11提供的新式for迴圈,還有lambda表示式等
進行配合使用。
在C++98中如果要遍歷一個陣列,可以按照以下方式進行:
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,不然改變不了原陣列
正確的寫法
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來跳出整個迴圈。
對於陣列而言,就是陣列中第一個元素和最後一個元素的範圍;對於類而言,應該提供begin和end的
方法,begin和end就是for迴圈迭代的範圍。
注意:以下程式碼就有問題,因為for的範圍不確定
void TestFor(int array[])
{
for(auto& e : array) //這裡的array其實不是陣列,陣列在傳參時會退化成指標
cout<< e <<endl; }
關於迭代器這個問題,以後會講,現在大家瞭解一下就可以了。
在良好的C/C++程式設計習慣中,在宣告一個變數的同時最好給該變數一個合適的初始值,否則可能會出現不可預料的錯誤。比如未初始化的指標,如果一個指標沒有合法的指向,我們基本都是按如下方式對其進行初始化:
int* p1 = NULL;
int* p2 = 0;
NULL其實是一個宏,在傳統的C標頭檔案(stddef.h)中可以看到如下程式碼:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
可以看到,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*)常數,但編譯器預設情況下將其看成是一個整型常數,如果要將其按照指標方式來使用,必須對其進行強制轉換。
對於C++98中的問題,C++11引入了關鍵字nullptr。
在使用nullptr表示指標空值時,不需要包含標頭檔案,因為nullptr是C++11作為關鍵字引入的。
在C++11中,sizeof(nullptr)與sizeof((void*)0)所佔的位元組數相同,大小都為4。