前言
C語言是面向過程的編程語言,C++是面向對象的編程語言,這是兩種不同的編程語言。C語言是C++的子集,C++是C語言的超集,C++進一步擴充和完善了C語言,其中大部分是對於面向對象編程的拓展。C++既可以進行C語言的過程化程序設計,又可以進行以抽象數據類型為特點的基於對象的程序設計,還可以進行以繼承和多態為特點的面向對象的程序設計。
從“Hello world!”講起
傳承學習編程語言的優良傳統,我們來寫一段“Hello world!”:
#include <iostream>
using namespace std;
int main()
{
cout << "Hello World";
return 0;
}
類
類(class)是用戶自定義的數據類型,是一種構造類型,與C語言結構體類似,但是進行了一些擴展,類的成員不但可以是變量,還可以是函數,通過類定義出來的變量也有特定的稱呼,叫做“對象”。類一般分為兩部分,分別寫在不同的文件當中,其一是頭文件,用來聲明這種類所提供的功能,另一個文件包含了完成這些操作的代碼。想要使用類,就必須現在程序中包含頭文件。
標准“輸入/輸出庫”
在 C++ 標准的“輸入/輸出庫”名為“ iostream ”,iostream 這個單詞是由3個部分組成的,即 i-o-stream ,意為輸入輸出流。在 iostream 類庫中包含許多用於輸入輸出的類,包含了支持對終端和文件的輸入和輸出的類。我們必須先包含
cout 和 cin
C++ 的輸入輸出操作
cout(讀作see out) 和 cin(讀作see in) 不是運算符,也不是關鍵字,它們都是C++的內置對象(提前創建好的對象)。cout 和 cin 就分別是 ostream 和 istream 類的對象,是由標准庫的開發者提前創建好的,可以直接拿來使用。
C++的輸出和輸入是用“流”(stream)的方式實現的,cout 是標准輸出流對象的,即 ostream 對象,cin 是標准輸入流對象,即 istream 對象,關流對象 cin、cout 和流運算符的定義等存放在輸入輸出流庫中,因此如果在程序中使用cin、cout和流運算符,就必須把頭文件 stream 包含到本文件中。
“>>” 和 “<<”運算符
符號 | 功能 |
---|---|
>> | output運算符,也可以稱之為插入器,可以將數據定向到流中,用於向流輸入數據 |
<< | input運算符,也可以稱之為析取器,可以將輸入內容定向到具有適當類型的對象上,用於從流中輸入數據 |
例如:
#include <iostream>
#include <string>
using namespace std;
int main()
{
string name;
cin >> name;
cout << "Hello, "
<< name
<< "!"
<< endl;
return 0;
}
運行效果為:
在這段代碼中,我們先定義了一個 “string” 類型的對象,用於存儲輸入的內容,然后利用 cin 流和“>>”運算符,把輸入的內容存儲到“name”對象中,我們將我們希望輸出的內容依次通過“<<”輸出到 cout 流中。我們可以看到,我們不需要每輸入一些內容就寫一個“cout << ”,可以將一系列輸出語句連接成一條輸出語句,以 endl作為結束。
endl
endl是C++標准庫中的操控器,英語意思是end of line,即一行輸出結束,常與cout搭配使用,意思是輸出結束。功能有:
- 將換行符寫入輸出流,並將與設備關聯的緩沖區的內容刷到設備中,保證目前為止程序所暫存的所有輸出都真正寫入輸出流;
- 清空輸出緩沖區。
對比 scanf() 和 printf()
C++ 關鍵字 cout 和 cin 在時間效率上,其實並不如 scanf() 和 printf()。這個問題深究起來也不難理解,因為這 2 個關鍵字是對輸入和輸出的強化,可以適應包括 STL 庫容器的輸入輸出,也自帶清理緩沖區等性能。由於這種操作的功能更強大,因此就需要更多的時間來進行附加功能的實現,以及健壯性的處理,因此會耗費更多的時間。而 C 語言的 scanf() 和 printf() 函數來進行輸入和輸出,功能上就比較弱一些,所以執行的速度就比較快。即使有效率上的問題,但是還是很推薦使用 cout 和 cin 進行功能更強的輸入和輸出。
“using namespace std”是什么?
std 是標准庫所駐的命名空間的名稱,標准庫提供的任何內容都被封裝在命名空間 std 中。命名空間是一種將庫名稱封裝起來的方法,可以避免和應用程序發生命名沖突的問題。
因此,若要在程序中使用 string class 以及 cin、cout 對象,除了要包含頭文件,還要用 using namespace 這兩個關鍵字把命名空間 std 的內容曝光。
初始化
C++為我們提供了另一種初始化的方式,即“構造函數語法”,形如:
int num(0);
不過我們提到了初始化,很自然我們會這么做:
int num = 1;
這么做當然沒有問題了,我們學C語言的時候都是這么干的。但是為什么C++會提供另外一種初始化的方式呢?
舉個例子,C++的標准庫有一種類叫做復數類,一個復數類對象由2部分組成,分別是實部和虛部,這也就說明了我們初始化復數類對象時,需要同時對實部和虛部進行初始化,這時候原來的“用 assignment 運算符(=)初始化就不好用了。
為了解決“多值初始化的問題”,C++提供了構造函數語法:
#include <complex>
complex<double> purei(0,1);
引用
什么是引用
下面的寫法定義了一個引用,並將其初始化為引用某個變量。
類型名 & 引用名 = 變量名
引用變量是一個別名,某個變量的引用等價於這個變量。把引用初始化為某個變量,就可以使用該引用名來指向變量,原來的變量名仍然有效,仍可以用原來的變量名來指向變量。引用有以下注意事項:
- 不能是空引用,定義引用時必須初始化為引用某個變量;
- 引用是從一而終的,初始化飲用之后就一直引用初始化的變量,不能引用其他變量;
- 引用只能引用變量,不能引用常量和表達式。
為了更直觀地理解引用,我們來寫段代碼:
#include <iostream>
#include <string>
using namespace std;
int main()
{
int num1(0);
int & num2 = num1;
cout << num2 << endl;
num1 = 1;
cout << num1 << endl;
cout << num2 << endl;
num2 = 2;
cout << num1 << endl;
cout << num2 << endl;
return 0;
}
運行結果為:
我們首先定義了一個 int 類型的對象 num1,然后定義了一個引用 num2。在我們定義引用之后,num1 和 num2 這兩個就是一回事了,我們對 num1 操作等同於對 num2 操作,因此我們每次修改之后,輸出
num1 和輸出 num2 的數據是相同的。
常引用
定義引用時,在定義的語句前加 const 關鍵字,此時將定義一個常引用。常引用的作用是不能通過引用去修改其引用的內容,寫法為:
const 類型名 & 引用名 = 變量名
- 常引用不能修改引用的內容,但是被引用的內容本身可以修改。
常引用和非常引用
常引用和非常引用是不同的類型,引用和變量可以用來初始化常引用,但是常引用不能用來初始化引用。
實例
我們先還原一下我們該開始學編程時會犯的錯誤:
void swap(int a,int b)
{
int temp;
temp = a;
a = b;
b = temp;
}
我們都知道,這么寫是錯的,因為這涉及到實參和形參問題。傳入的變量 a、b 是對原來的數據的一個拷貝,是局部變量,一旦函數執行完畢, a、b 兩個變量就會消失。那后來我們是怎么解決這個問題的呢?我們可以傳指針進入函數,這樣就可以通過指針間接操作變量。
但是現在我們可以用引用來解決這個問題:
#include <iostream>
#include <string>
using namespace std;
void swap(int & a,int & b);
int main()
{
int num1(0);
int num2(1);
swap(num1, num2);
cout << "num1 = " << num1 << endl;
cout << "num2 = " << num2 << endl;
return 0;
}
void swap(int & a,int & b)
{
int temp;
temp = a;
a = b;
b = temp;
}
輸出結果為:
我們將 a、b 的類型改為了引用,當我們向函數傳值的時候,a 將引用傳入的第一個參數,b 將引用傳入的第二個參數。根據引用的定義,a、b 就分別和傳入的兩個變量是一回事了,這個時候我們操作 a、b,就等同於操作傳入的參數本身,就不需要想以前那樣傳入地址了。
引用和指針
引用和指針有一定的相似之處,它們主要有三處不同:
- 引用不能是空引用,引用必須連接到一塊合法的內存。指針可以是空指針。
- 引用被初始化為一個對象,之后就不能被引用其他對象。指針可以在任何時候指向另一個對象。
- 引用必須在創建時被初始化。指針可以在任何時間被初始化。
動態內存分配
分配
我們在C++中使用 new 運算符實現動態內存分配。
當我們只分配一個變量時,語法如下:
ptr = new Type
當我們分配一個數組時,語法如下:
ptr = new Type[N]
參數 | 說明 |
---|---|
Type | 任意類型名,可以是 class 類型 |
ptr | Type* 類型的指針名 |
N | 分配的數組元素個數,可以是整型表達式 |
使用 new 運算符之后,C++ 將分配出大小為 sizeof(Type) × N(分配變量時N為1) 的空間,並且將內存空間的起始地址賦值給 ptr 。new 運算符自動計算要分配類型的大小,不使用 sizeof 運算符,較為便捷且可以避免錯誤。new 運算符能自動地返回正確的指針類型,因此不用進行強制指針類型轉換。如果內存分配失敗,和 malloc函數 一樣,new 表達式返回 NULL,因此可以通過檢查返回值的方式得知內存分配成功與否。
釋放
有申請空間就一定要釋放,用 new 運算符分配的內存空間需要用 delete 運算符釋放。
申請變量時,釋放語法為:
delete ptr;
申請指針時,釋放語法為:
delete []ptr;
參數 | 說明 |
---|---|
ptr | 需要釋放的空間的指針名,必須是動態內存分配出的空間 |
- 我們無需檢驗 ptr 是否為空指針
內存泄漏
程序中動態分配了內存,卻沒有回收,這就造成這部分存儲空間不能再被利用,稱為內存泄漏(memory leak),如果程序需要長期運行,內存泄漏問題會耗盡所有內存,從而使程序崩潰。所以養成好的習慣,在所分配的內存不再使用后,及時回收內存。
new 和 malloc的區別
- new分配的空間不需要強制類型轉換;
- new分配的空間不需要判空;
- new分配的空間時可以初始化。
缺省值
給函數參數賦默認值
在C++中,定義函數時,我們可以讓最右邊的連續若干個參數具有默認值,這個默認值叫做缺省值。調用參數的時候,若相應位置不寫參數,那么該參數的值就會被賦值為缺省值。
實例
代碼實現:
int add(int a, int b = 20, int c = 30)
{
return a + b + c;
}
提供默認值的規則
- 默認值的解析操作從最右邊開始。如果我們為某個參數提供了默認值,則該參數右側的所有參數都必須具有默認值;
- 默認值只能指定一次,即函數聲明和函數定義只能有一個地方指定默認值,不能兩個地方都指定。
- 為了提高可見性,建議默認值在函數聲明的時候指定。
內聯函數
為什么會有“內聯函數”
當我們用函數封裝代碼時,函數的調用是有開銷的,除了時間上的開銷,頻繁調用的函數也可能大量消耗棧空間。如果我們寫了個功能強大的函數,這個函數有幾百行,那么函數調用的開銷相對會顯得比較小,可以忽略不計,就好比分析衛星運行的軌道半徑時不需要考慮衛星的長度參數一樣。但是如果是個只有幾條語句,運行速度很快的函數,例如:
這個函數的作用只是打印一條分割線,而且在程序中需要被多次調用。相比之下,這種函數被調用的開銷就成為了需要考慮的因素了。
inline 關鍵字
為了減少函數調用的開銷,C++引入了內聯函數的機制。將函數聲明為 inline,表示建議編譯器在調用這個函數的時候,將函數的內容展開,此時編譯器將會把函數的調用操作改為以一份代碼副本來替代。
例如我們要輸出100個“生日快樂!”:
#include <iostream>
using namespace std;
void put_a_message();
int main()
{
for(int i = 0; i < 100; i++)
put_a_message();
return 0;
}
inline void put_a_message()
{
cout << "生日快樂!" << endl;
}
當我們把 put_a_message 函數定義為內聯函數時,在內部就可能是以這種方式實現功能:
#include <iostream>
using namespace std;
int main()
{
for(int i = 0; i < 100; i++)
cout << "生日快樂!" << endl;
return 0;
}
使用 inline 的注意事項
- 使用限制:inline 只適合涵數體內代碼簡單的涵數使用,不能包含復雜的結構控制語句例如 while、switch,並且不能內聯函數本身不能是直接遞歸函數。
- inline是一個建議:將函數定義為 inline 函數僅僅是對編譯器提出的一個要求,編譯器不一定執行這項要求,所以最后能否真正內聯,看編譯器的意思。如果編譯器認為函數不復雜,能在調用點展開,就會真正內聯,並不是說聲明了內聯就會內聯。
- 適合定義為 inline 的函數:一般來說,最適合定義為內聯函數的函數的特點為:體積小、需要被多次調用、執行的操作簡單。
- inline 函數定義:由於內聯函數要在被調用的時候展開,所以編譯器必須隨處可見內聯函數的定義,因此 inline 函數的定義常放於頭文件。
- inline 是"用於實現的關鍵字":關鍵字 inline 必須與函數定義體放在一起才能使函數成為內聯,僅將 inline 放在函數聲明前面不起任何作用。
函數重載
為什么會有“函數重載”
例如我們要寫3個函數,分別要求出兩個整數,三個整數,兩個雙精度數的最大值。那么我們把這3和函數都命名為“max”是一件合情合理的事情,但是學習C語言時我們知道不同的函數時不能取相同的函數名的,這就需要我們為這3個函數分別去不同的函數名,甚至會取得又臭又長。
函數重載
我們實現一下前文的代碼:
#include <iostream>
using namespace std;
int myMax(int a, int b)
{
return (a > b ? a : b);
}
int myMax(int a, int b, int c)
{
b > a ? a = b : a = a;
return (a > c ? a : c);
}
double myMax(double a, double b)
{
return (a > b ? a : b);
}
int main()
{
cout << myMax(3,4) << endl;
cout << myMax(3,4,5) << endl;
cout << myMax(4.3,3.4) << endl;
}
C++允許我們寫出好幾個具有相同函數名的函數。一個或多個函數名相同,但是函數個數或參數類型不同的函數,叫做函數重載。
函數重載使得函數命名變得簡單,在調用函數的時候,編譯器會將調用函數時提供的實際參數和每個重載函數的參數對比,判斷應該調用哪個函數。
- 編譯器無法根據函數的返回類型來區分兩個函數名相同的函數。
函數指針
指向函數的指針
每一個函數都占用一段內存單元,它們有一個起始地址,指向函數入口地址的指針稱為函數指針。語法為:
Type (*ptr)(list);
參數 | 說明 |
---|---|
Type | 函數的返回值的類型 |
ptr | 指針變量名 |
list | 調用函數時傳遞的參數表 |
在函數指針變量賦值時,只需給出函數名,不必給出參數。函數指針變量指向的函數不是固定的,這表示定義了一個這樣類型的變量,用來存放函數的入口地址,函數指針指向的函數由我們在程序中把哪一個函數的地址賦給它而決定。
實例
代碼實現:
參考資料
《新標准C++程序設計》——郭煒 編著,高等教育出版社
《Essential C++》——[美]Stanley B.Lippman 著,侯捷 譯,電子工業出版社
《C++ Primer》——[美] Stanley B. Lippman Barbara E. Moo Josée LaJoie,譯 李師賢 等
C++里面的iostream是什么
C++構造函數初始化類對象
C++ new和malloc的區別
C++函數指針詳解
菜鳥教程
C語言中文網