現在看來,C++ 和C語言雖然是兩門獨立的語言,但是它們卻有着扯也扯不清的關係。
早期並沒有「C++」這個名字,而是叫做「帶類的C」。「帶類的C」是作爲C語言的一個擴充套件和補充出現的,它增加了很多新的語法,目的是提高開發效率。
C++是一門物件導向的程式語言,理解 C++,首先要理解**類(Class)和物件(Object)**這兩個概念。
C++ 中的類(Class)可以看做C語言中結構體(Struct)的升級版。結構體是一種構造型別,可以包含若幹成員變數,每個成員變數的型別可以不同;可以通過結構體來定義結構體變數,每個變數擁有相同的性質。
C++ 中的類也是一種構造型別,但是進行了一些擴充套件,類的成員不但可以是變數,還可以是函數;通過類定義出來的變數也有特定的稱呼,叫做「物件」。
class 和 public 都是 C++ 中的關鍵字,初學者請先忽略 public(後續會深入講解),把注意力集中在 class 上。
C語言中的 struct 只能包含變數,而 C++ 中的 class 除了可以包含變數,還可以包含函數。
C語言結構體裏面我們只能包含變數,函數必須在外面宣告。
類是一個通用的概念,C++、Java、C#、PHP 等很多程式語言中都支援類,都可以通過類建立物件。可以將類看做是結構體的升級版,C語言的晚輩們看到了C語言的不足,嘗試加以改善,繼承了結構體的思想,並進行了升級,讓程式設計師在開發或擴充套件大中型專案時更加容易。
因爲 C++、Java、C#、PHP 等語言都支援類和物件,所以使用這些語言編寫程式也被稱爲物件導向程式設計,這些語言也被稱爲物件導向的程式語言。C語言因爲不支援類和物件的概念,被稱爲程序導向的程式語言。
在C語言中,我們會把重複使用或具有某項功能的程式碼封裝成一個函數,將擁有相關功能的多個函數放在一個原始檔,再提供一個對應的標頭檔案,這就是一個模組。使用模組時,引入對應的標頭檔案就可以。
而在 C++ 中,多了一層封裝,就是類(Class)。類由一組相關聯的函數、變數組成,你可以將一個類或多個類放在一個原始檔,使用時引入對應的類就可以。
C++原始檔的後綴
C語言原始檔的後綴非常統一,在不同的編譯器下都是.c
。C++ 原始檔的後綴則有些混亂,不同的編譯器支援不同的後綴,下表是一個簡單的彙總:
編譯器 | Microsoft Visual C++ | GCC(GNU C++) | Borland C++ | UNIX |
---|---|---|---|---|
後綴 | cpp、cxx、cc | cpp、cxx、cc、c++、C | cpp | C、cc、cxx |
UNIX 是昂貴的商業操作系統,初學者幾乎用不到;Microsoft Visual C++ 是微軟的 C/C++ 編譯器,VC 6.0、VS 都使用該編譯器。我推薦使用.cpp
作爲 C++ 原始檔的後綴,這樣更加通用和規範。
一箇中大型軟體往往由多名程式設計師共同開發,會使用大量的變數和函數,不可避免地會出現變數或函數的命名衝突。當所有人的程式碼都測試通過,沒有問題時,將它們結合到一起就有可能會出現命名衝突。
我們所以引入了名稱空間的相關概念:
爲了解決合作開發時的命名衝突問題,C++ 引入了**名稱空間(Namespace)**的概念。請看下面 下麪的例子:
namespace Li{ //小李的變數定義
FILE fp = NULL;
}
namespace Han{ //小韓的變數定義
FILE fp = NULL
}
小李與小韓各自定義了以自己姓氏爲名的名稱空間,此時再將他們的 fp 變數放在一起編譯就不會有任何問題。
名稱空間有時也被稱爲命名空間、名稱空間。
namespace 是C++中的關鍵字,用來定義一個名稱空間,語法格式爲:
namespace name{
//variables, functions, classes
}
name
是名稱空間的名字,它裏面可以包含變數、函數、類、typedef、#define 等,最後由{ }
包圍。
使用變數、函數時要指明它們所在的名稱空間。以上面的 fp 變數爲例,可以這樣來使用:
Li::fp = fopen("one.txt", "r"); //使用小李定義的變數 fpHan::fp = fopen("two.txt", "rb+"); //使用小韓定義的變數 fp
::
是一個新符號,稱爲域解析操作符,在C++中用來指明要使用的名稱空間。
除了直接使用域解析操作符,還可以採用 using 關鍵字宣告,例如:
純文字複製
using Li::fp;fp = fopen("one.txt", "r"); //使用小李定義的變數 fpHan :: fp = fopen("two.txt", "rb+"); //使用小韓定義
using 宣告不僅可以針對名稱空間中的一個變數,也可以用於宣告整個名稱空間,例如:
using namespace Li;fp = fopen("one.txt", "r"); //使用小李定義的變數 fpHan::fp = fopen("two.txt", "rb+"); //使用小韓定義的變數 fp
如果名稱空間 Li 中還定義了其他的變數,那麼同樣具有 fp 變數的效果。在 using 宣告後,如果有未具體指定名稱空間的變數產生了命名衝突,那麼預設採用名稱空間 Li 中的變數。
C++ 是在C語言的基礎上開發的,早期的 C++ 還不完善,不支援名稱空間,沒有自己的編譯器,而是將 C++ 程式碼翻譯成C程式碼,再通過C編譯器完成編譯。這個時候的 C++ 仍然在使用C語言的庫,stdio.h、stdlib.h、string.h 等標頭檔案依然有效;此外 C++ 也開發了一些新的庫,增加了自己的標頭檔案:
和C語言一樣,C++ 標頭檔案仍然以.h
爲後綴,它們所包含的類、函數、宏等都是全域性範圍的。
後來 C++ 引入了名稱空間的概念,計劃重新編寫庫,將類、函數、宏等都統一納入一個名稱空間,這個名稱空間的名字就是std
。std 是 standard 的縮寫,意思是「標準名稱空間」。
但是這些是老式程式碼了,在C++發展的歷程裏面,我們對程式碼做出了改進:
C++ 開發人員想了一個好辦法,保留原來的庫和標頭檔案,它們在 C++ 中可以繼續使用,然後再把原來的庫複製一份,在此基礎上稍加修改,把類、函數、宏等納入名稱空間 std 下,就成了新版 C++ 標準庫。這樣共存在了兩份功能相似的庫,使用了老式 C++ 的程式可以繼續使用原來的庫,新開發的程式可以使用新版的 C++ 庫。
爲了避免標頭檔案重名,新版 C++ 庫也對標頭檔案的命名做了調整,去掉了後綴.h
,所以老式 C++ 的iostream.h
變成了iostream
,fstream.h
變成了fstream
。而對於原來C語言的標頭檔案,也採用同樣的方法,但在每個名字前還要新增一個c
字母,所以C語言的stdio.h
變成了cstdio
,stdlib.h
變成了cstdlib
。
需要注意的是,舊的 C++ 標頭檔案是官方所反對使用的,已明確提出不再支援,但舊的C標頭檔案仍然可以使用,以保持對C的相容性。實際上,編譯器開發商不會停止對客戶現有軟體提供支援,可以預計,舊的 C++ 標頭檔案在未來數年內還是會被支援。
我們還是可以使用C語言的printf 或者 scanf進行輸入輸出。我們來看一個例子:
#include<iostream>
using namespace std;
int main(){
int x;
float y;
cout<<"Please input an int number:"<<endl;
cin>>x;
cout<<"The int number is x= "<<x<<endl;
cout<<"Please input a float number:"<<endl;
cin>>y;
cout<<"The float number is y= "<<y<<endl;
return 0;
}
我們在C++裏面可以採用cin與cout進行輸入與輸出。
C++ 中的輸入與輸出可以看做是一連串的數據流,輸入即可視爲從檔案或鍵盤中輸入程式中的一串數據流,而輸出則可以視爲從程式中輸出一連串的數據流到顯示屏或檔案中。它減小了C語言裏面輸入與輸出的麻煩
在編寫 C++ 程式時,如果需要使用輸入輸出時,則需要包含標頭檔案iostream
,它包含了用於輸入輸出的物件,例如常見的cin
表示標準輸入、cout
表示標準輸出、cerr
表示標準錯誤。
cout 和 cin 都是 C++ 的內建物件,而不是關鍵字。C++ 庫定義了大量的類(Class),程式設計師可以使用它們來建立物件,cout 和 cin 就分別是 ostream 和 istream 類的物件。
其中endl
表示換行,與C語言裡的\n
作用相同。當然這段程式碼中也可以用\n
來替代endl
,這樣就得寫作:
cout<<「Please input an int number:\n」;
endl 最後一個字元是字母「l」,而非阿拉伯數位「1」,它是「end of line」的縮寫。
cin連續輸入:
#include<iostream>
using namespace std;
int main(){
int x;
float y;
cout<<"Please input an int number and a float number:"<<endl;
cin>>x>>y;
cout<<"The int number is x= "<<x<<endl;
cout<<"The float number is y= "<<y<<endl;
return 0;
}
至於爲什麼這麼使用,我們後面會在運算子過載裏面進行使用。
在C語言中,關係運算和邏輯運算的結果有兩種,真和假:0 表示假,非 0 表示真。
C語言並沒有徹底從語法上支援「真」和「假」,只是用 0 和非 0 來代表。這點在 C++ 中得到了改善,C++ 新增了 bool 型別(布爾型別),它一般佔用 1 個位元組長度。bool 型別只有兩個取值,true 和 false:true 表示「真」,false 表示「假」。
遺憾的是,在 C++ 中使用 cout 輸出 bool 變數的值時還是用數位 1 和 0 表示,而不是 true 或 false。Java、PHP、JavaScript 等也都支援布爾型別,但輸出結果爲 true 或 false。
但是我們可以這樣進行,把他們賦值爲true和false。
注意,true 和 false 是 C++ 中的關鍵字,true 表示「真」,false 表示「假」。
在C語言中,const 用來限制一個變數,表示這個變數不能被修改,我們通常稱這樣的變數爲常數。
我們知道,變數是要佔用記憶體的,即使被 const 修飾也不例外。m、n 兩個變數佔用不同的記憶體,int n = m;
表示將 m 的值賦給 n,這個賦值的過程在C和C++中是有區別的。
C++ 中的常數更類似於#define
命令,是一個值替換的過程,只不過#define
是在預處理階段替換,而常數是在編譯階段替換。
C++ 對 const 的處理少了讀取記憶體的過程,優點是提高了程式執行效率,缺點是不能反映記憶體的變化,一旦 const 變數被修改,C++ 就不能取得最新的值。
但是在C++裏面,const通過指針仍然可以修改。下面 下麪的程式碼演示瞭如何通過指針修改 const 變數:
#include <stdio.h>
int main(){
const int n = 10;
int *p = (int*)&n; //必須強制型別轉換
*p = 99; //修改const變數的值
printf("%d\n", n);
return 0;
}
當然,這種修改常數的變態程式碼在實際開發中基本不會出現,本例只是爲了說明C和C++對 const 的處理方式的差異:C語言對 const 的處理和普通變數一樣,會到記憶體中讀取數據;C++ 對 const 的處理更像是編譯時期的#define
,是一個值替換的過程。
C和C++中全域性 const 變數的作用域相同,都是當前檔案,不同的是它們的可見範圍:C語言中 const 全域性變數的可見範圍是整個程式,在其他檔案中使用 extern 宣告後就可以使用;而C++中 const 全域性變數的可見範圍僅限於當前檔案,在其他檔案中不可見,所以它可以定義在標頭檔案中,多次引入後也不會出錯。
C++ 中的 const 變數雖然也會佔用記憶體,也能使用&
獲取得它的地址,但是在使用時卻更像編譯時期的#define
;#define
也是值替換,可見範圍也僅限於當前檔案。
在C語言中,動態分配記憶體用 malloc() 函數,釋放記憶體用 free() 函數。如下所示:
int *p = (int*) malloc( sizeof(int) * 10 ); //分配10個int型的記憶體空間free(p); //釋放記憶體
在C++中,這兩個函數仍然可以使用,但是C++又新增了兩個關鍵字,new 和 delete:new 用來動態分配記憶體,delete 用來釋放記憶體。
用 new 和 delete 分配記憶體更加簡單:
純文字複製
int *p = new int; //分配1個int型的記憶體空間
delete p; //釋放記憶體
new 操作符會根據後面的數據型別來推斷所需空間的大小。
如果希望分配一組連續的數據,可以使用 new[]:
int *p = new int[10]; //分配10個int型的記憶體空間
delete[] p;
用 new[] 分配的記憶體需要用 delete[] 釋放,它們是一一對應的。
和 malloc() 一樣,new 也是在堆區分配記憶體,必須手動釋放,否則只能等到程式執行結束由操作系統回收。爲了避免記憶體泄露,通常 new 和 delete、new[] 和 delete[] 操作符應該成對出現,並且不要和C語言中 malloc()、free() 一起混用。
在C++中,建議使用 new 和 delete 來管理記憶體,它們可以使用C++的一些新特性,最明顯的是可以自動呼叫建構函式和解構函式,後續我們將會講解。
一個 C/C++ 程式的執行過程可以認爲是多個函數之間的相互呼叫過程,它們形成了一個或簡單或複雜的呼叫鏈條,這個鏈條的起點是 main(),終點也是 main()。當 main() 呼叫完了所有的函數,它會返回一個值(例如return 0;
)來結束自己的生命,從而結束整個程式。
函數呼叫是有時間和空間開銷的。程式在執行一個函數之前需要做一些準備工作,要將實參、區域性變數、返回地址以及若幹暫存器都壓入棧中,然後才能 纔能執行函數體中的程式碼;函數體中的程式碼執行完畢後還要清理現場,將之前壓入棧中的數據都出棧,才能 纔能接着執行函數呼叫位置以後的程式碼。
如果函數體程式碼比較多,需要較長的執行時間,那麼函數呼叫機制 機製佔用的時間可以忽略;如果函數只有一兩條語句,那麼大部分的時間都會花費在函數呼叫機制 機製上,這種時間開銷就就不容忽視。
爲了消除函數呼叫的時空開銷,C++ 提供一種提高效率的方法,即在編譯時將函數呼叫處用函數體替換,類似於C語言中的宏展開。這種在函數呼叫處直接嵌入函數體的函數稱爲行內函式(Inline Function),又稱內嵌函數或者內建函數。
注意,要在函數定義處新增 inline 關鍵字,在函數宣告處新增 inline 關鍵字雖然沒有錯,但這種做法是無效的,編譯器會忽略函數宣告處的 inline 關鍵字。
當函數比較複雜時,函數呼叫的時空開銷可以忽略,大部分的 CPU 時間都會花費在執行函數體程式碼上,所以我們一般是將非常短小的函數宣告爲行內函式。
我們慢慢講解:
我們在宏定義的時候也會這麼操作:
#include <iostream>
using namespace std;
#define SQ(y) y*y
int main(){
int n, sq;
cin>>n;
sq = SQ(n);
cout<<sq<<endl;
return 0;
}
輸入9以後算的81
但是:
#include <iostream>
using namespace std;
#define SQ(y) y*y
int main(){
int n, sq;
cin>>n;
sq = SQ(n+1);
cout<<sq<<endl;
return 0;
}
我們期望的結果是 100,但這裏卻是 19,兩者大相徑庭。這是因爲,宏展開僅僅是字串的替換,不會進行任何計算或傳值,上面的sq = SQ(n+1);
在宏展開後會變爲sq = n+1*n+1;
,這顯然是沒有道理的。
宏定義是一項「細思極密」的工作,一不小心就會踩坑,而且不一定在編譯和執行時發現,給程式埋下隱患。
我們引入了行內函式,這麼幹:
#include <iostream>
using namespace std;
//宣告行內函式
void swap1(int *a, int *b); //也可以新增inline,但編譯器會忽略
int main(){
int m, n;
cin>>m>>n;
cout<<m<<", "<<n<<endl;
swap1(&m, &n);
cout<<m<<", "<<n<<endl;
return 0;
}
//定義行內函式
inline void swap1(int *a, int *b){
int temp;
temp = *a;
*a = *b;
*b = temp;
}
在C++中,定義函數時可以給形參指定一個預設的值,這樣呼叫函數時如果沒有給這個形參賦值(沒有對應的實參),那麼就使用這個預設的值。也就是說,呼叫函數時可以省略有預設值的參數。
看一個例子:
#include<iostream>
using namespace std;
//帶預設參數的函數
void func(int n, float b=1.2, char c='@'){
cout<<n<<", "<<b<<", "<<c<<endl;
}
int main(){
//爲所有參數傳值
func(10, 3.5, '#');
//爲n、b傳值,相當於呼叫func(20, 9.8, '@')
func(20, 9.8);
//只爲n傳值,相當於呼叫func(30, 1.2, '@')
func(30);
return 0;
}
執行結果:
10, 3.5, #
20, 9.8, @
30, 1.2, @
指定了預設參數後,呼叫函數時就可以省略對應的實參了。
C++規定,預設參數只能放在形參列表的最後,而且一旦爲某個形參指定了預設值,那麼它後面的所有形參都必須有預設值。實參和形參的傳值是從左到右依次匹配的,預設參數的連續性是保證正確傳參的前提。
注意:預設參數賦值必須從右到左進行!!並且不能擁有空格
下面 下麪的寫法是正確的:
void func(int a, int b=10, int c=20){ }
void func(int a, int b, int c=20){ }
但這樣寫不可以:
純文字複製
void func(int a, int b=10, int c=20, int d){ }
void func(int a, int b=10, int c, int d=20){ }
我們究竟是在定義裏面還是宣告裏面來使用。
C++ 規定,在給定的作用域中只能指定一次預設參數。就相當於只能宣告一次了。
但是可以連續宣告不同的參數。
在實際開發中,有時候我們需要實現幾個功能類似的函數,只是有些細節不同。例如希望交換兩個變數的值,這兩個變數有多種型別,可以是 int、float、char、bool 等,我們需要通過參數把變數的地址傳入函數內部。在C語言中,程式設計師往往需要分別設計出三個不同名的函數,其函數原型與下面 下麪類似:
void swap1(int *a, int *b); //交換 int 變數的值
void swap2(float *a, float *b); //交換 float 變數的值
void swap3(char *a, char *b); //交換 char 變數的值
void swap4(bool *a, bool *b); //交換 bool 變數的值
但在C++中,這完全沒有必要。C++ 允許多個函數擁有相同的名字,只要它們的參數列表不同就可以,這就是函數的過載(Function Overloading)。藉助過載,一個函數名可以有多種用途。
#include <iostream>
using namespace std;
//交換 int 變數的值
void Swap(int *a, int *b){
int temp = *a;
*a = *b;
*b = temp;
}
//交換 float 變數的值
void Swap(float *a, float *b){
float temp = *a;
*a = *b;
*b = temp;
}
//交換 char 變數的值
void Swap(char *a, char *b){
char temp = *a;
*a = *b;
*b = temp;
}
//交換 bool 變數的值
void Swap(bool *a, bool *b){
char temp = *a;
*a = *b;
*b = temp;
}
int main(){
//交換 int 變數的值
int n1 = 100, n2 = 200;
Swap(&n1, &n2);
cout<<n1<<", "<<n2<<endl;
//交換 float 變數的值
float f1 = 12.5, f2 = 56.93;
Swap(&f1, &f2);
cout<<f1<<", "<<f2<<endl;
//交換 char 變數的值
char c1 = 'A', c2 = 'B';
Swap(&c1, &c2);
cout<<c1<<", "<<c2<<endl;
//交換 bool 變數的值
bool b1 = false, b2 = true;
Swap(&b1, &b2);
cout<<b1<<", "<<b2<<endl;
return 0;
}
執行結果:
200, 100
56.93, 12.5
B, A
1, 0
本例之所以使用Swap
這個函數名,而不是使用swap
,是因爲 C++ 標準庫已經提供了交換兩個變數的值的函數,它的名字就是swap
,位於algorithm
標頭檔案中,爲了避免和標準庫中的swap
衝突,本例特地將S
大寫。
通過本例可以發現,過載就是在一個作用範圍內(同一個類、同一個名稱空間等)有多個名稱相同但參數不同的函數。過載的結果是讓一個函數名擁有了多種用途,使得命名更加方便(在中大型專案中,給變數、函數、類起名字是一件讓人苦惱的問題),呼叫更加靈活。
C++ 是如何做到函數過載的
C++程式碼在編譯時會根據參數列表對函數進行重新命名,例如void Swap(int a, int b)
會被重新命名爲_Swap_int_int
,void Swap(float x, float y)
會被重新命名爲_Swap_float_float
。當發生函數呼叫時,編譯器會根據傳入的實參去逐個匹配,以選擇對應的函數,如果匹配失敗,編譯器就會報錯,這叫做過載決議
過載時會發生二義性,就是不知道匹配宣告而報錯,所以過載時候一定要想清楚。
因爲在C語言裏面我們發現會進行型別轉換。
C++ 標準規定,在進行過載決議時編譯器應該按照下面 下麪的優先順序順序來處理實參的型別:
優先順序 | 包含的內容 | 舉例說明 |
---|---|---|
精確匹配 | 不做型別轉換,直接匹配 | (暫無說明) |
只是做微不足道的轉換 | 從陣列名到陣列指針、從函數名到指向函數的指針、從非 const 型別到 const 型別。 | |
型別提升後匹配 | 整型提升 | 從 bool、char、short 提升爲 int,或者從 char16_t、char32_t、wchar_t 提升爲 int、long、long long。 |
小數提升 | 從 float 提升爲 double。 | |
使用自動型別轉換後匹配 | 整型轉換 | 從 char 到 long、short 到 long、int 到 short、long 到 char。 |
小數轉換 | 從 double 到 float。 | |
整數和小數轉換 | 從 int 到 double、short 到 float、float 到 int、double 到 long。 | |
指針轉換 | 從 int * 到 void *。 |
看了這麼多,我們終於進入了C++的核心:物件導向。
物件導向我們之前做了很多鋪墊,現在終於進入了。
類可以看做是一種數據型別,它類似於普通的數據型別,但是又有別於普通的數據型別。類這種數據型別是一個包含成員變數和成員函數的集合。
類的成員變數和普通變數一樣,也有數據型別和名稱,佔用固定長度的記憶體。但是,在定義類的時候不能對成員變數賦值,因爲類只是一種數據型別或者說是一種模板,本身不佔用記憶體空間,而變數的值則需要記憶體來儲存。
類的成員函數也和普通函數一樣,都有返回值和參數列表,它與一般函數的區別是:成員函數是一個類的成員,出現在類體中,它的作用範圍由類來決定;而普通函數是獨立的,作用範圍是全域性的,或位於某個名稱空間內。
我們來看一個例子:
class Student{
public:
//成員變數
char *name;
int age;
float score;
//成員函數
void say(){
cout<<name<<"的年齡是"<<age<<",成績是"<<score<<endl;
}
};
這是一個比較典型的類的構建。這段程式碼在類體中定義了成員函數。你也可以只在類體中宣告函數,而將函數定義放在類體外面,如下圖所示:
class Student{
public:
//成員變數
char *name;
int age;
float score;
//成員函數
void say(); //函數宣告
};
//函數定義
void Student::say(){
cout<<name<<"的年齡是"<<age<<",成績是"<<score<<endl;
}
在類體中直接定義函數時,不需要在函數名前面加上類名,因爲函數屬於哪一個類是不言而喻的。
但當成員函數定義在類外時,就必須在函數名前面加上類名予以限定。::
被稱爲域解析符(也稱作用域運算子或作用域限定符),用來連線類名和函數名,指明當前函數屬於哪個類。
在類體中定義的成員函數會自動成爲行內函式,在類體外定義的不會。當然,在類體內部定義的函數也可以加 inline 關鍵字,但這是多餘的,因爲類體內部定義的函數預設就是行內函式。
行內函式定義在外部的例子:
class Student{
public:
char *name;
int age;
float score;
void say(); //行內函式宣告,可以增加 inline 關鍵字,但編譯器會忽略
};
//函數定義
inline void Student::say(){
cout<<name<<"的年齡是"<<age<<",成績是"<<score<<endl;
}
這樣,say() 就會變成行內函式。
我們建立好類以後,就要範例化,因爲類是抽象的,我們要具體化。
在建立物件時,class 關鍵字可要可不要,但是出於習慣我們通常會省略掉 class 關鍵字,例如:
class Student LiLei; //正確
Student LiLei; //同樣正確
存取成員:
#include <iostream>
using namespace std;
//類通常定義在函數外面
class Student{
public:
//類包含的變數
char *name;
int age;
float score;
//類包含的函數
void say(){
cout<<name<<"的年齡是"<<age<<",成績是"<<score<<endl;
}
};
int main(){
//建立物件
Student stu;
stu.name = "小明";
stu.age = 15;
stu.score = 92.5f;
stu.say();
return 0;
}
建立物件以後,可以使用點號.
來存取成員變數和成員函數,這和通過結構體變數來存取它的成員類似。
stu 是一個物件,佔用記憶體空間,可以對它的成員變數賦值,也可以讀取它的成員變數。
類通常定義在函數外面,當然也可以定義在函數內部,不過很少這樣使用。
我們在C++裏面還可以使用物件指針。
指向物件的指針,沒有它就不能實現某些功能。
上面程式碼中建立的物件 stu 在棧上分配記憶體,需要使用&
獲取它的地址,例如:
Student stu;
Student *pStu = &stu;
pStu 是一個指針,它指向 Student 型別的數據,也就是通過 Student 建立出來的物件。
當然,你也可以在堆上建立物件,這個時候就需要使用前面講到的new
關鍵字
Student *pStu = new Student;
也就是說,使用 new 在堆上建立出來的物件是匿名的,沒法直接使用,必須要用一個指針指向它,再藉助指針來存取它的成員變數或成員函數。
有了物件指針後,可以通過箭頭->
來存取物件的成員變數和成員函數,這和通過結構體指針來存取它的成員類似,請看下面 下麪的範例:
pStu -> name = "小明";
pStu -> age = 15;
pStu -> score = 92.5f;
pStu -> say();
我們看一個完整的例子:
#include <iostream>
using namespace std;
class Student{
public:
char *name;
int age;
float score;
void say(){
cout<<name<<"的年齡是"<<age<<",成績是"<<score<<endl;
}
};
int main(){
Student *pStu = new Student;
pStu -> name = "小明";
pStu -> age = 15;
pStu -> score = 92.5f;
pStu -> say();
delete pStu; //刪除物件
return 0;
}
雖然在一般的程式中無視垃圾記憶體影響不大,但記得 delete 掉不再使用的物件依然是一種良好的程式設計習慣。
本節重點講解了兩種建立物件的方式:一種是在棧上建立,形式和定義普通變數類似;另外一種是在堆上使用 new 關鍵字建立,必須要用一個指針指向它,讀者要記得 delete 掉不再使用的物件。
通過物件名字存取成員使用點號.
,通過物件指針存取成員使用箭頭->
,這和結構體非常類似。
我們現在講一下面 下麪向物件的三大特性:封裝性、繼承性與多型性。
前面我們在定義類時多次使用到了 public 關鍵字,表示類的成員具有「公開」的存取許可權,這節我們就來詳細講解。
C++通過 public、protected、private 三個關鍵字來控製成員變數和成員函數的存取許可權,它們分別表示公有的、受保護的、私有的,被稱爲成員存取限定符。所謂存取許可權,就是你能不能使用該類中的成員。
Java、C# 程式設計師注意,C++ 中的 public、private、protected 只能修飾類的成員,不能修飾類,C++中的類沒有共有私有之分。
在類的內部(定義類的程式碼內部),無論成員被宣告爲 public、protected 還是 private,都是可以互相存取的,沒有存取許可權的限制。
在類的外部(定義類的程式碼之外),只能通過物件存取成員,並且通過物件只能存取 public 屬性的成員,不能存取 private、protected 屬性的成員。
#include <iostream>
using namespace std;
//類的宣告
class Student{
private: //私有的
char *m_name;
int m_age;
float m_score;
public: //共有的
void setname(char *name);
void setage(int age);
void setscore(float score);
void show();
};
//成員函數的定義
void Student::setname(char *name){
m_name = name;
}
void Student::setage(int age){
m_age = age;
}
void Student::setscore(float score){
m_score = score;
}
void Student::show(){
cout<<m_name<<"的年齡是"<<m_age<<",成績是"<<m_score<<endl;
}
int main(){
//在棧上建立物件
Student stu;
stu.setname("小明");
stu.setage(15);
stu.setscore(92.5f);
stu.show();
//在堆上建立物件
Student *pstu = new Student;
pstu -> setname("李華");
pstu -> setage(16);
pstu -> setscore(96);
pstu -> show();
return 0;
}
執行結果:
小明的年齡是15,成績是92.5
李華的年齡是16,成績是96
類的宣告和成員函數的定義都是類定義的一部分,在實際開發中,我們通常將類的宣告放在標頭檔案中,而將成員函數的定義放在原始檔中。
類中的成員變數 m_name、m_age 和m_ score 被設定成 private 屬性,在類的外部不能通過物件存取。也就是說,私有成員變數和成員函數只能在類內部使用,在類外都是無效的。
成員函數 setname()、setage() 和 setscore() 被設定爲 public 屬性,是公有的,可以通過物件存取。
private 後面的成員都是私有的,直到有 public 出現纔會變成共有的;public 之後再無其他限定符,所以 public 後面的成員都是共有的。
因爲三個成員變數都是私有的,不能通過物件直接存取,所以必須藉助三個 public 屬性的成員函數來修改它們的值。
關於封裝:
private 關鍵字的作用在於更好地隱藏類的內部實現,該向外暴露的介面(能通過物件存取的成員)都宣告爲 public,不希望外部知道、或者只在類內部使用的、或者對外部沒有影響的成員,都建議宣告爲 private。
根據C++軟體設計規範,實際專案開發中的成員變數以及只在類內部使用的成員函數(只被成員函數呼叫的成員函數)都建議宣告爲 private,而只將允許通過物件呼叫的成員函數宣告爲 public。
另外還有一個關鍵字 protected,宣告爲 protected 的成員在類外也不能通過物件存取,但是在它的派生類內部可以存取,這點我們將在後續章節中介紹,現在你只需要知道 protected 屬性的成員在類外無法存取即可。
有讀者可能會提出疑問,將成員變數都宣告爲 private,如何給它們賦值呢,又如何讀取它們的值呢?
我們可以額外新增兩個 public 屬性的成員函數,一個用來設定成員變數的值,一個用來獲取成員變數的值。上面的程式碼中,setname()、setage()、setscore() 函數就用來設定成員變數的值;如果希望獲取成員變數的值,可以再新增三個函數 getname()、getage()、getscore()。
宣告爲 private 的成員和宣告爲 public 的成員的次序任意,既可以先出現 private 部分,也可以先出現 public 部分。如果既不寫 private 也不寫 public,就預設爲 private。
在C++中,有一種特殊的成員函數,它的名字和類名相同,沒有返回值,不需要使用者顯式呼叫(使用者也不能呼叫),而是在建立物件時自動執行。這種特殊的成員函數就是建構函式(Constructor)。
我們通過成員函數 setname()、setage()、setscore() 分別爲成員變數 name、age、score 賦值,這樣做雖然有效,但顯得有點麻煩。有了建構函式,我們就可以簡化這項工作,在建立物件的同時爲成員變數賦值。
#include <iostream>
using namespace std;
class Student{
private:
char *m_name;
int m_age;
float m_score;
public:
//宣告建構函式
Student(char *name, int age, float score);
//宣告普通成員函數
void show();
};
//定義建構函式
Student::Student(char *name, int age, float score){
m_name = name;
m_age = age;
m_score = score;
}
//定義普通成員函數
void Student::show(){
cout<<m_name<<"的年齡是"<<m_age<<",成績是"<<m_score<<endl;
}
int main(){
//建立物件時向建構函式傳參
Student stu("小明", 15, 92.5f);
stu.show();
//建立物件時向建構函式傳參
Student *pstu = new Student("李華", 16, 96);
pstu -> show();
return 0;
}
在類裏面也可以這麼宣告建構函式。
該例在 Student 類中定義了一個建構函式Student(char *, int, float)
,它的作用是給三個 private 屬性的成員變數賦值。要想呼叫該建構函式,就得在建立物件的同時傳遞實參,並且實參由( )
包圍,和普通的函數呼叫非常類似。
建構函式必須是 public 屬性的,否則建立物件時無法呼叫。當然,設定爲 private、protected 屬性也不會報錯,但是沒有意義。
建構函式也可以過載。
建構函式的呼叫是強制性的,一旦在類中定義了建構函式,那麼建立物件時就一定要呼叫,不呼叫是錯誤的。如果有多個過載的建構函式,那麼建立物件時提供的實參必須和其中的一個建構函式匹配;反過來說,建立物件時只有一個建構函式會被呼叫。
如果使用者自己沒有定義建構函式,那麼編譯器會自動生成一個預設的建構函式,只是這個建構函式的函數體是空的,也沒有形參,也不執行任何操作。比如上面的 Student 類,預設生成的建構函式如下:
Student(){}
一個類必須有建構函式,要麼使用者自己定義,要麼編譯器自動生成。一旦使用者自己定義了建構函式,不管有幾個,也不管形參如何,編譯器都不再自動生成。
最後需要注意的一點是,呼叫沒有參數的建構函式也可以省略括號。
初始化列表,使式子更加簡潔:
Student::Student(char *name, int age, float score): m_name(name), m_age(age), m_score(score){
//TODO:
}
建構函式這裏改改就可以了。
const成員變數的初始化:
初始化 const 成員變數的唯一方法就是使用初始化列表。
class VLA{
private:
const int m_len;
int *m_arr;
public:
VLA(int len);
};
//必須使用初始化列表來初始化 m_len
VLA::VLA(int len): m_len(len){
m_arr = new int[len];
}
建立物件時系統會自動呼叫建構函式進行初始化工作,同樣,銷燬物件時系統也會自動呼叫一個函數來進行清理工作,例如釋放分配的記憶體、關閉開啓的檔案等,這個函數就是解構函式。
解構函式(Destructor)也是一種特殊的成員函數,沒有返回值,不需要程式設計師顯式呼叫(程式設計師也沒法顯式呼叫),而是在銷燬物件時自動執行。建構函式的名字和類名相同,而解構函式的名字是在類名前面加一個~
符號。
注意:解構函式沒有參數,不能被過載,因此一個類只能有一個解構函式。如果使用者沒有定義,編譯器會自動生成一個預設的解構函式。
我們看一個範例:
#include <iostream>
using namespace std;
class VLA{
public:
VLA(int len); //建構函式
~VLA(); //解構函式
public:
void input(); //從控制檯輸入陣列元素
void show(); //顯示陣列元素
private:
int *at(int i); //獲取第i個元素的指針
private:
const int m_len; //陣列長度
int *m_arr; //陣列指針
int *m_p; //指向陣列第i個元素的指針
};
VLA::VLA(int len): m_len(len){ //使用初始化列表來給 m_len 賦值
if(len > 0){ m_arr = new int[len]; /*分配記憶體*/ }
else{ m_arr = NULL; }
}
VLA::~VLA(){
delete[] m_arr; //釋放記憶體
}
void VLA::input(){
for(int i=0; m_p=at(i); i++){ cin>>*at(i); }
}
void VLA::show(){
for(int i=0; m_p=at(i); i++){
if(i == m_len - 1){ cout<<*at(i)<<endl; }
else{ cout<<*at(i)<<", "; }
}
}
int * VLA::at(int i){
if(!m_arr || i<0 || i>=m_len){ return NULL; }
else{ return m_arr + i; }
}
int main(){
//建立一個有n個元素的陣列(物件)
int n;
cout<<"Input array length: ";
cin>>n;
VLA *parr = new VLA(n);
//輸入陣列元素
cout<<"Input "<<n<<" numbers: ";
parr -> input();
//輸出陣列元素
cout<<"Elements: ";
parr -> show();
//刪除陣列(物件)
delete parr;
return 0;
}
執行結果:
Input array length: 5
Input 5 numbers: 99 23 45 10 100
Elements: 99, 23, 45, 10, 100
~VLA()
就是 VLA 類的解構函式,它的唯一作用就是在刪除物件(第 53 行程式碼)後釋放已經分配的記憶體。
函數名是識別符號的一種,原則上識別符號的命名中不允許出現~
符號,在解構函式的名字中出現的~
可以認爲是一種特殊情況,目的是爲了和建構函式的名字加以對比和區分。
解構函式在物件被銷燬時呼叫,而物件的銷燬時機與它所在的記憶體區域有關。
在所有函數之外建立的物件是全域性物件,它和全域性變數類似,位於記憶體分割區中的全域性數據區,程式在結束執行時會呼叫這些物件的解構函式。
new 建立的物件位於堆區,通過 delete 刪除時纔會呼叫解構函式;如果沒有 delete,解構函式就不會被執行。
物件陣列宣告和普通陣列差不多。
建立封閉類的物件時,它包含的成員物件也需要被建立,這就會引發成員物件建構函式的呼叫。如何讓編譯器知道,成員物件到底是用哪個建構函式初始化的呢?這就需要藉助封閉類建構函式的初始化列表。
對於基本型別的成員變數,「參數表」中只有一個值,就是初始值,在呼叫建構函式時,會把這個初始值直接賦給成員變數。
我們看一個例子:
#include <iostream>
using namespace std;
//輪胎類
class Tyre{
public:
Tyre(int radius, int width);
void show() const;
private:
int m_radius; //半徑
int m_width; //寬度
};
Tyre::Tyre(int radius, int width) : m_radius(radius), m_width(width){ }
void Tyre::show() const {
cout << "輪轂半徑:" << this->m_radius << "吋" << endl;
cout << "輪胎寬度:" << this->m_width << "mm" << endl;
}
//引擎類
class Engine{
public:
Engine(float displacement = 2.0);
void show() const;
private:
float m_displacement;
};
Engine::Engine(float displacement) : m_displacement(displacement) {}
void Engine::show() const {
cout << "排量:" << this->m_displacement << "L" << endl;
}
//汽車類
class Car{
public:
Car(int price, int radius, int width);
void show() const;
private:
int m_price; //價格
Tyre m_tyre;
Engine m_engine;
};
Car::Car(int price, int radius, int width): m_price(price), m_tyre(radius, width)/*指明m_tyre物件的初始化方式*/{ };
void Car::show() const {
cout << "價格:" << this->m_price << "¥" << endl;
this->m_tyre.show();
this->m_engine.show();
}
int main()
{
Car car(200000, 19, 245);
car.show();
return 0;
}
執行結果:
價格:200000¥
輪轂直徑:19吋
輪胎寬度:245mm
排量:2L
Car 是一個封閉類,它有兩個成員物件:m_tyre 和 m_engine。在編譯第 51 行時,編譯器需要知道 car 物件中的 m_tyre 和 m_engine 成員物件該如何初始化。
成員物件消失:
封閉類物件生成時,先執行所有成員物件的建構函式,然後才執行封閉類自己的建構函式。成員物件建構函式的執行次序和成員物件在類定義中的次序一致,與它們在建構函式初始化列表中出現的次序無關。
當封閉類物件消亡時,先執行封閉類的解構函式,然後再執行成員物件的解構函式,成員物件解構函式的執行次序和建構函式的執行次序相反,即先構造的後解構,這是 C++ 處理此類次序問題的一般規律。
就是與宣告的順序相關。
this 是 C++ 中的一個關鍵字,也是一個 const 指針,它指向當前物件,通過它可以訪問當前物件的所有成員。
所謂當前物件,是指正在使用的物件。例如對於stu.show();
,stu 就是當前物件,this 就指向 stu。
案例:
#include <iostream>
using namespace std;
class Student{
public:
void setname(char *name);
void setage(int age);
void setscore(float score);
void show();
private:
char *name;
int age;
float score;
};
void Student::setname(char *name){
this->name = name;
}
void Student::setage(int age){
this->age = age;
}
void Student::setscore(float score){
this->score = score;
}
void Student::show(){
cout<<this->name<<"的年齡是"<<this->age<<",成績是"<<this->score<<endl;
}
int main(){
Student *pstu = new Student;
pstu -> setname("李華");
pstu -> setage(16);
pstu -> setscore(96.5);
pstu -> show();
return 0;
}
this 只能用在類的內部,通過 this 可以存取類的所有成員,包括 private、protected、public 屬性的。
注意,this 是一個指針,要用->
來存取成員變數或成員函數。
this 雖然用在類的內部,但是隻有在物件被建立以後纔會給 this 賦值,並且這個賦值的過程是編譯器自動完成的,不需要使用者幹預,使用者也不能顯式地給 this 賦值。
this 確實指向了當前物件,而且對於不同的物件,this 的值也不一樣。
物件的記憶體中包含了成員變數,不同的物件佔用不同的記憶體。可是有時候我們希望在多個物件之間共用數據,物件 a 改變了某份數據後物件 b 可以檢測到。共用數據的典型使用場景是計數,以前面的 Student 類爲例,如果我們想知道班級中共有多少名學生,就可以設定一份共用的變數,每次建立物件時讓該變數加1.
在C++中,我們可以使用靜態成員變數來實現多個物件共用數據的目標。靜態成員變數是一種特殊的成員變數,它被關鍵字static
修飾,例如:
class Student{
public:
Student(char *name, int age, float score);
void show();
public:
static int m_total; //靜態成員變數
private:
char *m_name;
int m_age;
float m_score;
};
這段程式碼宣告瞭一個靜態成員變數 m_total,用來統計學生的人數。
static 成員變數屬於類,不屬於某個具體的物件,即使建立多個物件,也只爲 m_total 分配一份記憶體,所有物件使用的都是這份記憶體中的數據。當某個物件修改了 m_total,也會影響到其他物件。
static 成員變數必須在類宣告的外部初始化,具體形式爲:
type class::name = value;
type 是變數的型別,class 是類名,name 是變數名,value 是初始值。將上面的 m_total 初始化:
int Student::m_total = 0;
靜態成員變數在初始化時不能再加 static,但必須要有數據型別。被 private、protected、public 修飾的靜態成員變數都可以用這種方式初始化。
注意:static 成員變數的記憶體既不是在宣告類時分配,也不是在建立物件時分配,而是在(類外)初始化時分配。反過來說,沒有在類外初始化的 static 成員變數不能使用。
static 成員變數既可以通過物件來存取,也可以通過類來存取。
注意:static 成員變數不佔用物件的記憶體,而是在所有物件之外開闢記憶體,即使不建立物件也可以存取。
#include <iostream>
using namespace std;
class Student{
public:
Student(char *name, int age, float score);
void show();
private:
static int m_total; //靜態成員變數
private:
char *m_name;
int m_age;
float m_score;
};
//初始化靜態成員變數
int Student::m_total = 0;
Student::Student(char *name, int age, float score): m_name(name), m_age(age), m_score(score){
m_total++; //操作靜態成員變數
}
void Student::show(){
cout<<m_name<<"的年齡是"<<m_age<<",成績是"<<m_score<<"(當前共有"<<m_total<<"名學生)"<<endl;
}
int main(){
//建立匿名物件
(new Student("小明", 15, 90)) -> show();
(new Student("李磊", 16, 80)) -> show();
(new Student("張華", 16, 99)) -> show();
(new Student("王康", 14, 60)) -> show();
return 0;
}
結果是:
小明的年齡是15,成績是90(當前共有1名學生)
李磊的年齡是16,成績是80(當前共有2名學生)
張華的年齡是16,成績是99(當前共有3名學生)
王康的年齡是14,成績是60(當前共有4名學生)
靜態成員變數既可以通過物件名存取,也可以通過類名存取,但要遵循 private、protected 和 public 關鍵字的存取許可權限制。當通過物件名存取時,對於不同的物件,存取的是同一份記憶體。
普通成員變數佔用物件的記憶體,靜態成員函數沒有 this 指針,不知道指向哪個物件,無法存取物件的成員變數,也就是說靜態成員函數不能存取普通成員變數,只能存取靜態成員變數。
普通成員函數必須通過物件才能 纔能呼叫,而靜態成員函數沒有 this 指針,無法在函數體內部存取某個物件,所以不能呼叫普通成員函數,只能呼叫靜態成員函數。
靜態成員函數與普通成員函數的根本區別在於:普通成員函數有 this 指針,可以存取類中的任意成員;而靜態成員函數沒有 this 指針,只能存取靜態成員(包括靜態成員變數和靜態成員函數)。
#include <iostream>
using namespace std;
class Student{
public:
Student(char *name, int age, float score);
void show();
public: //宣告靜態成員函數
static int getTotal();
static float getPoints();
private:
static int m_total; //總人數
static float m_points; //總成績
private:
char *m_name;
int m_age;
float m_score;
};
int Student::m_total = 0;
float Student::m_points = 0.0;
Student::Student(char *name, int age, float score): m_name(name), m_age(age), m_score(score){
m_total++;
m_points += score;
}
void Student::show(){
cout<<m_name<<"的年齡是"<<m_age<<",成績是"<<m_score<<endl;
}
//定義靜態成員函數
int Student::getTotal(){
return m_total;
}
float Student::getPoints(){
return m_points;
}
int main(){
(new Student("小明", 15, 90.6)) -> show();
(new Student("李磊", 16, 80.5)) -> show();
(new Student("張華", 16, 99.0)) -> show();
(new Student("王康", 14, 60.8)) -> show();
int total = Student::getTotal();
float points = Student::getPoints();
cout<<"當前共有"<<total<<"名學生,總成績是"<<points<<",平均分是"<<points/total<<endl;
return 0;
}
結果是:
小明的年齡是15,成績是90.6
李磊的年齡是16,成績是80.5
張華的年齡是16,成績是99
王康的年齡是14,成績是60.8
當前共有4名學生,總成績是330.9,平均分是82.725
靜態成員函數在宣告時要加 static,在定義時不能加 static。靜態成員函數可以通過類來呼叫(一般都是這樣做),也可以通過物件來呼叫,上例僅僅演示瞭如何通過類來呼叫。
const成員變數
const 成員變數的用法和普通 const 變數的用法相似,只需要在宣告時加上 const 關鍵字。初始化 const 成員變數只有一種方法,就是通過建構函式的初始化列表,這點在前面已經講到了
const成員函數(常成員函數)
const 成員函數可以使用類中的所有成員變數,但是不能修改它們的值,這種措施主要還是爲了保護數據而設定的。const 成員函數也稱爲常成員函數。
常成員函數需要在宣告和定義的時候在函數頭部的結尾加上 const 關鍵字,請看下面 下麪的例子:
class Student{
public:
Student(char *name, int age, float score);
void show();
//宣告常成員函數
char *getname() const;
int getage() const;
float getscore() const;
private:
char *m_name;
int m_age;
float m_score;
};
Student::Student(char *name, int age, float score): m_name(name), m_age(age), m_score(score){ }
void Student::show(){
cout<<m_name<<"的年齡是"<<m_age<<",成績是"<<m_score<<endl;
}
//定義常成員函數
char * Student::getname() const{
return m_name;
}
int Student::getage() const{
return m_age;
}
float Student::getscore() const{
return m_score;
}
getname()、getage()、getscore() 三個函數的功能都很簡單,僅僅是爲了獲取成員變數的值,沒有任何修改成員變數的企圖,所以我們加了 const 限制,這是一種保險的做法,同時也使得語意更加明顯。
需要強調的是,必須在成員函數的宣告和定義處同時加上 const 關鍵字。
const char * getname()
。char * getname() const
。在 C++ 中,const 也可以用來修飾物件,稱爲常物件。一旦將物件定義爲常物件之後,就只能呼叫類的 const 成員(包括 const 成員變數和 const 成員函數)了。
定義常物件的語法和定義常數的語法類似:
const class object(params);
class const object(params);
當然你也可以定義 const 指針:
const class *p = new class(params);
class const *p = new class(params);
class
爲類名,object
爲物件名,params
爲實參列表,p
爲指針名。兩種方式定義出來的物件都是常物件。
const 離變數名近就是用來修飾指針變數的,離變數名遠就是用來修飾指針指向的數據,如果近的和遠的都有,那麼就同時修飾指針變數以及它指向的數據。
總結:如果這個變數被const修飾了,就只能存取const修飾的成員了,不能存取非const修飾的了。
看一個案例:
#include <iostream>
using namespace std;
class Student{
public:
Student(char *name, int age, float score);
public:
void show();
char *getname() const;
int getage() const;
float getscore() const;
private:
char *m_name;
int m_age;
float m_score;
};
Student::Student(char *name, int age, float score): m_name(name), m_age(age), m_score(score){ }
void Student::show(){
cout<<m_name<<"的年齡是"<<m_age<<",成績是"<<m_score<<endl;
}
char * Student::getname() const{
return m_name;
}
int Student::getage() const{
return m_age;
}
float Student::getscore() const{
return m_score;
}
int main(){
const Student stu("小明", 15, 90.6);
//stu.show(); //error
cout<<stu.getname()<<"的年齡是"<<stu.getage()<<",成績是"<<stu.getscore()<<endl;
const Student *pstu = new Student("李磊", 16, 80.5);
//pstu -> show(); //error
cout<<pstu->getname()<<"的年齡是"<<pstu->getage()<<",成績是"<<pstu->getscore()<<endl;
return 0;
}
裏面的成員物件就只能存取成員函數了。就是被const修飾的。
在 C++ 中,一個類中可以有 public、protected、private 三種屬性的成員,通過物件可以存取 public 成員,只有本類中的函數可以存取本類的 private 成員。現在,我們來介紹一種例外情況——友元(friend)。藉助友元(friend),可以使得其他類中的成員函數以及全域性範圍內的函數訪問當前類的 private 成員。
在 C++ 中,這種友好關係可以用 friend 關鍵字指明,中文多譯爲「友元」,藉助友元可以存取與其有好友關係的類中的私有成員。
友元函數:
在當前類以外定義的、不屬於當前類的函數也可以在類中宣告,但要在前面加 friend 關鍵字,這樣就構成了友元函數。友元函數可以是不屬於任何類的非成員函數,也可以是其他類的成員函數。
友元函數可以訪問當前類中的所有成員,包括 public、protected、private 屬性的。
友元函數可以訪問當前類中的所有成員,包括 public、protected、private 屬性的。
看個例子:
#include <iostream>
using namespace std;
class Student{
public:
Student(char *name, int age, float score);
public:
friend void show(Student *pstu); //將show()宣告爲友元函數
private:
char *m_name;
int m_age;
float m_score;
};
Student::Student(char *name, int age, float score): m_name(name), m_age(age), m_score(score){ }
//非成員函數
void show(Student *pstu){
cout<<pstu->m_name<<"的年齡是 "<<pstu->m_age<<",成績是 "<<pstu->m_score<<endl;
}
int main(){
Student stu("小明", 15, 90.6);
show(&stu); //呼叫友元函數
Student *pstu = new Student("李磊", 16, 80.5);
show(pstu); //呼叫友元函數
return 0;
}
show() 是一個全域性範圍內的非成員函數,它不屬於任何類,它的作用是輸出學生的資訊。但是,友元函數不同於類的成員函數,在友元函數中不能直接存取類的成員,必須要藉助物件。
friend 函數不僅可以是全域性函數(非成員函數),還可以是另外一個類的成員函數。請看下面 下麪的例子:
看個程式碼:
#include <iostream>
using namespace std;
class Address; //提前宣告Address類
//宣告Student類
class Student{
public:
Student(char *name, int age, float score);
public:
void show(Address *addr);
private:
char *m_name;
int m_age;
float m_score;
};
//宣告Address類
class Address{
private:
char *m_province; //省份
char *m_city; //城市
char *m_district; //區(市區)
public:
Address(char *province, char *city, char *district);
//將Student類中的成員函數show()宣告爲友元函數
friend void Student::show(Address *addr);
};
//實現Student類
Student::Student(char *name, int age, float score): m_name(name), m_age(age), m_score(score){ }
void Student::show(Address *addr){
cout<<m_name<<"的年齡是 "<<m_age<<",成績是 "<<m_score<<endl;
cout<<"家庭住址:"<<addr->m_province<<"省"<<addr->m_city<<"市"<<addr->m_district<<"區"<<endl;
}
//實現Address類
Address::Address(char *province, char *city, char *district){
m_province = province;
m_city = city;
m_district = district;
}
int main(){
Student stu("小明", 16, 95.5f);
Address addr("陝西", "西安", "雁塔");
stu.show(&addr);
Student *pstu = new Student("李磊", 16, 80.5);
Address *paddr = new Address("河北", "衡水", "桃城");
pstu -> show(paddr);
return 0;
}
本例定義了兩個類 Student 和 Address,程式第 27 行將 Student 類的成員函數 show() 宣告爲 Address 類的友元函數,由此,show() 就可以存取 Address 類的 private 成員變數了。
友元類:
不僅可以將一個函數宣告爲一個類的「朋友」,還可以將整個類宣告爲另一個類的「朋友」,這就是友元類。友元類中的所有成員函數都是另外一個類的友元函數。
例如將類 B 宣告爲類 A 的友元類,那麼類 B 中的所有成員函數都是類 A 的友元函數,可以存取類 A 的所有成員,包括 public、protected、private 屬性的。
看個程式碼:
#include <iostream>
using namespace std;
class Address; //提前宣告Address類
//宣告Student類
class Student{
public:
Student(char *name, int age, float score);
public:
void show(Address *addr);
private:
char *m_name;
int m_age;
float m_score;
};
//宣告Address類
class Address{
public:
Address(char *province, char *city, char *district);
public:
//將Student類宣告爲Address類的友元類
friend class Student;
private:
char *m_province; //省份
char *m_city; //城市
char *m_district; //區(市區)
};
//實現Student類
Student::Student(char *name, int age, float score): m_name(name), m_age(age), m_score(score){ }
void Student::show(Address *addr){
cout<<m_name<<"的年齡是 "<<m_age<<",成績是 "<<m_score<<endl;
cout<<"家庭住址:"<<addr->m_province<<"省"<<addr->m_city<<"市"<<addr->m_district<<"區"<<endl;
}
//實現Address類
Address::Address(char *province, char *city, char *district){
m_province = province;
m_city = city;
m_district = district;
}
int main(){
Student stu("小明", 16, 95.5f);
Address addr("陝西", "西安", "雁塔");
stu.show(&addr);
Student *pstu = new Student("李磊", 16, 80.5);
Address *paddr = new Address("河北", "衡水", "桃城");
pstu -> show(paddr);
return 0;
}
關於友元,有兩點需要說明:
C++ 大大增強了對字串的支援,除了可以使用C風格的字串,還可以使用內建的 string 類。string 類處理起字串來會方便很多,完全可以代替C語言中的字元陣列或字串指針。
string 是 C++ 中常用的一個類,它非常重要,我們有必要在此單獨講解一下。
使用 string 類需要包含標頭檔案<string>
,下面 下麪的例子介紹了幾種定義 string 變數(物件)的方法:
看看相關程式碼:
#include <iostream>
#include <string>
using namespace std;
int main(){
string s1;
string s2 = "c plus plus";
string s3 = s2;
string s4 (5, 's');
return 0;
}
變數 s1 只是定義但沒有初始化,編譯器會將預設值賦給 s1,預設值是""
,也即空字串。
變數 s2 在定義的同時被初始化爲"c plus plus"
。與C風格的字串不同,string 的結尾沒有結束標誌'\0'
。
變數 s3 在定義的時候直接用 s2 進行初始化,因此 s3 的內容也是"c plus plus"
。
變數 s4 被初始化爲由 5 個's'
字元組成的字串,也就是"sssss"
。
從上面的程式碼可以看出,string 變數可以直接通過賦值操作符=
進行賦值。string 變數也可以用C風格的字串進行賦值,例如,s2 是用一個字串常數進行初始化的,而 s3 則是通過 s2 變數進行初始化的。
要想知道字串長度,可以呼叫length()方法。
與C風格的字串不同,當我們需要知道字串長度時,可以呼叫 string 類提供的 length() 函數。如下所示:
純文字複製
string s = "http://c.biancheng.net";
int len = s.length();
cout<<len<<endl;
輸出結果爲22
。由於 string 的末尾沒有'\0'
字元,所以 length() 返回的是字串的真實長度,而不是長度 +1。
雖然 C++ 提供了 string 類來替代C語言中的字串,但是在實際程式設計中,有時候必須要使用C風格的字串(例如開啓檔案時的路徑),爲此,string 類爲我們提供了一個轉換函數 c_str(),該函數能夠將 string 字串轉換爲C風格的字串,並返回該字串的 const 指針(const char*)。請看下面 下麪的程式碼:
string path = "D:\\demo.txt";
FILE *fp = fopen(path.c_str(), "rt");
爲了使用C語言中的 fopen() 函數開啓檔案,必須將 string 字串轉換爲C風格的字串。
從中可以看出C與C++相容性很強。
string 類過載了輸入輸出運算子,可以像對待普通變數那樣對待 string 變數,也就是用>>
進行輸入,用<<
進行輸出。請看下面 下麪的程式碼:
#include <iostream>#include <string>using namespace std;int main(){ string s; cin>>s; //輸入字串 cout<<s<<endl; //輸出字串 return 0;}
執行結果:
http://c.biancheng.net http://vip.biancheng.net↙輸入
http://c.biancheng.net 輸出
雖然我們輸入了兩個由空格隔開的網址,但是隻輸出了一個,這是因爲輸入運算子>>
預設會忽略空格,遇到空格就認爲輸入結束,所以最後輸入的http://vip.biancheng.net
沒有被儲存到變數 s。
string 字串也可以像C風格的字串一樣按照下標來存取其中的每一個字元。string 字串的起始下標仍是從 0 開始。請看下面 下麪的程式碼:
#include <iostream>
#include <string>
using namespace std;
int main(){
string s = "1234567890";
for(int i=0,len=s.length(); i<len; i++){
cout<<s[i]<<" ";
}
cout<<endl;
s[5] = '5';
cout<<s<<endl;
return 0;
}
執行結果:
1 2 3 4 5 6 7 8 9 0
1234557890
本例定義了一個 string 變數 s,並賦值 「1234567890」,之後用 for 回圈遍歷輸出每一個字元。藉助下標,除了能夠存取每個字元,也可以修改每個字元,s[5] = '5';
就將第6個字元修改爲 ‘5’,所以 s 最後爲 「1234557890」。
有了 string 類,我們可以使用+
或+=
運算子來直接拼接字串,非常方便,再也不需要使用C語言中的 strcat()、strcpy()、malloc() 等函數來拼接字串了,再也不用擔心空間不夠會溢位了。
用+
來拼接字串時,運算子的兩邊可以都是 string 字串,也可以是一個 string 字串和一個C風格的字串,還可以是一個 string 字串和一個字元陣列,或者是一個 string 字串和一個單獨的字元。
C++ 提供的 string 類包含了若幹實用的成員函數,大大方便了字串的增加、刪除、更改、查詢等操作。
insert() 函數可以在 string 字串中指定的位置插入另一個字串,它的一種原型爲:
string& insert (size_t pos, const string& str);
pos 表示要插入的位置,也就是下標;str 表示要插入的字串,它可以是 string 字串,也可以是C風格的字串。
insert() 函數的第一個參數有越界的可能,如果越界,則會產生執行時異常。
#include <iostream>
#include <string>
using namespace std;
int main(){
string s1, s2, s3;
s1 = s2 = "1234567890";
s3 = "aaa";
s1.insert(5, s3);
cout<< s1 <<endl;
s2.insert(5, "bbb");
cout<< s2 <<endl;
return 0;
}
執行結果:
12345aaa67890
12345bbb67890
erase() 函數可以刪除 string 中的一個子字串。它的一種原型爲:
string& erase (size_t pos = 0, size_t len = npos);
pos 表示要刪除的子字串的起始下標,len 表示要刪除子字串的長度。如果不指明 len 的話,那麼直接刪除從 pos 到字串結束處的所有字元(此時 len = str.length - pos)。
#include <iostream>
#include <string>
using namespace std;
int main(){
string s1, s2, s3;
s1 = s2 = s3 = "1234567890";
s2.erase(5);
s3.erase(5, 3);
cout<< s1 <<endl;
cout<< s2 <<endl;
cout<< s3 <<endl;
return 0;
}
執行結果:
1234567890
12345
1234590
有讀者擔心,在 pos 參數沒有越界的情況下, len 參數也可能會導致要刪除的子字串越界。但實際上這種情況不會發生,erase() 函數會從以下兩個值中取出最小的一個作爲待刪除子字串的長度:
說得簡單一些,待刪除字串最多隻能刪除到字串結尾。
substr() 函數用於從 string 字串中提取子字串,它的原型爲:
string substr (size_t pos = 0, size_t len = npos) const;
pos 爲要提取的子字串的起始下標,len 爲要提取的子字串的長度。
系統對 substr() 參數的處理和 erase() 類似:
請看下面 下麪的程式碼:
#include <iostream>
#include <string
>using namespace std;
int main(){
string s1 = "first second third";
string s2;
s2 = s1.substr(6, 6);
cout<< s1 <<endl;
cout<< s2 <<endl;
return 0;
}
執行結果:
first second third
second
string 類提供了幾個與字串查詢有關的函數,如下所示。
find() 函數
find() 函數用於在 string 字串中查詢子字串出現的位置,它其中的兩種原型爲:
size_t find (const string& str, size_t pos = 0) const;
size_t find (const char* s, size_t pos = 0) const;
第一個參數爲待查詢的子字串,它可以是 string 字串,也可以是C風格的字串。第二個參數爲開始查詢的位置(下標);如果不指明,則從第0個字元開始查詢。
請看下面 下麪的程式碼:
#include <iostream>#include <string>using namespace std;int main(){ string s1 = "first second third"; string s2 = "second"; int index = s1.find(s2,5); if(index < s1.length()) cout<<"Found at index : "<< index <<endl; else cout<<"Not found"<<endl; return 0;}
執行結果:
Found at index : 6
find() 函數最終返回的是子字串第一次出現在字串中的起始下標。本例最終是在下標6處找到了 s2 字串。如果沒有查詢到子字串,那麼會返回一個無窮大值 4294967295。
參照是 C++ 的新增內容,在實際開發中會經常使用;C++ 用的參照就如同C語言的指針一樣重要,但它比指針更加方便和易用,有時候甚至是不可或缺的。
同指針一樣,參照能夠減少數據的拷貝,提高數據的傳遞效率。
本專題不僅僅從語法層面講解 C++ 參照,而是深入 C++ 參照的本質,讓大家不但知其然,而且知其所以然。
我們知道,參數的傳遞本質上是一次賦值的過程,賦值就是對記憶體進行拷貝。所謂記憶體拷貝,是指將一塊記憶體上的數據複製到另一塊記憶體上。
而陣列、結構體、物件是一系列數據的集合,數據的數量沒有限制,可能很少,也可能成千上萬,對它們進行頻繁的記憶體拷貝可能會消耗很多時間,拖慢程式的執行效率。
但是在 C++ 中,我們有了一種比指針更加便捷的傳遞聚合型別數據的方式,那就是參照(Reference)。
參照(Reference)是 C++ 相對於C語言的又一個擴充。參照可以看做是數據的一個別名,通過這個別名和原來的名字都能夠找到這份數據。
參照還類似於人的綽號(筆名),使用綽號(筆名)和本名都能表示一個人。
參照的定義方式類似於指針,只是用&
取代了*
,語法格式爲:
type &name = data;
type 是被參照的數據的型別,name 是參照的名稱,data 是被參照的數據。參照必須在定義的同時初始化,並且以後也要從一而終,不能再參照其它數據,這有點類似於常數(const 變數)。
下面 下麪是一個演示參照的範例:
純文字複製
#include <iostream>
using namespace std;
int main() {
int a = 99;
int &r = a;
cout << a << ", " << r << endl;
cout << &a << ", " << &r << endl;
return 0;}
執行結果:
99, 99
0x28ff44, 0x28ff44
本例中,變數 r 就是變數 a 的參照,它們用來指代同一份數據;也可以說變數 r 是變數 a 的另一個名字。就相當於r參照了a。
注意,參照在定義時需要新增&
,在使用時不能新增&
,使用時新增&
表示取地址。
我們可以通過參照來改變原來被參照的值。
由於參照 r 和原始變數 a 都是指向同一地址,所以通過參照也可以修改原始變數中所儲存的數據,請看下面 下麪的例子:
#include <iostream>
using namespace std;
int main() {
int a = 99;
int &r = a;
r = 47;
cout << a << ", " << r << endl;
return 0;}
執行結果:
47, 47
最終程式輸出兩個 47,可見原始變數 a 的值已經被參照變數 r 所修改。
如果讀者不希望通過參照來修改原始的數據,那麼可以在定義時新增 const 限制,形式爲:
const type &name = value;
也可以是:
type const &name = value;
這種參照方式爲常參照
在定義或宣告函數時,我們可以將函數的形參指定爲參照的形式,這樣在呼叫函數時就會將實參和形參系結在一起,讓它們都指代同一份數據。如此一來,如果在函數體中修改了形參的數據,那麼實參的數據也會被修改,從而擁有「在函數內部影響函數外部數據」的效果。
#include <iostream>
using namespace std;
void swap1(int a, int b);
void swap2(int *p1, int *p2);
void swap3(int &r1, int &r2);
int main() {
int num1, num2;
cout << "Input two integers: ";
cin >> num1 >> num2;
swap1(num1, num2);
cout << num1 << " " << num2 << endl;
cout << "Input two integers: ";
cin >> num1 >> num2;
swap2(&num1, &num2);
cout << num1 << " " << num2 << endl;
cout << "Input two integers: ";
cin >> num1 >> num2;
swap3(num1, num2);
cout << num1 << " " << num2 << endl;
return 0;}
//直接傳遞參數內容
void swap1(int a, int b) {
int temp = a;
a = b;
b = temp;}
//傳遞指針
void swap2(int *p1, int *p2) {
int temp = *p1;
*p1 = *p2;
*p2 = temp;}
//按參照傳參
void swap3(int &r1, int &r2) {
int temp = r1;
r1 = r2;
r2 = temp;}
執行結果:
Input two integers: 12 34↙
12 34
Input two integers: 88 99↙
99 88
Input two integers: 100 200↙
200 100
swap3() 是按參照傳遞,能夠達到交換兩個數的值的目的。呼叫函數時,分別將 r1、r2 系結到 num1、num2 所指代的數據,此後 r1 和 num1、r2 和 num2 就都代表同一份數據了,通過 r1 修改數據後會影響 num1,通過 r2 修改數據後也會影響 num2。
從以上程式碼的編寫中可以發現,按參照傳參在使用形式上比指針更加直觀。
其實參照只是對指針進行了簡單的封裝,它的底層依然是通過指針實現的,參照佔用的記憶體和指針佔用的記憶體長度一樣
參照必須在定義時初始化,並且以後也要從一而終,不能再指向其他數據;而指針沒有這個限制,指針在定義時不必賦值,以後也能指向任意數據。
可以有 const 指針,但是沒有 const 參照。
指針和參照的自增(++)自減(–)運算意義不一樣。對指針使用 ++ 表示指向下一份數據,對參照使用 ++ 表示它所指代的數據本身加 1;自減(–)也是類似的道理
參照型別的函數形參請儘可能的使用 const
當參照作爲函數參數時,如果在函數體內部不會修改參照所系結的數據,那麼請儘量爲該參照新增 const 限制。
下面 下麪的例子演示了 const 參照的靈活性:
#include <cstdio>
using namespace std;
double volume(const double &len, const double &width, const double &hei){
return len*width*2 + len*hei*2 + width*hei*2;
}
int main(){
int a = 12, b = 3, c = 20;
double v1 = volume(a, b, c);
double v2 = volume(10, 20, 30);
double v3 = volume(89.4, 32.7, 19);
double v4 = volume(a+12.5, b+23.4, 16.78);
double v5 = volume(a+b, a+c, b+c);
printf("%lf, %lf, %lf, %lf, %lf\n", v1, v2, v3, v4, v5);
return 0;}
執行結果:
672.000000, 2200.000000, 10486.560000, 3001.804000, 3122.000000
volume() 函數用來求一個長方體的體積,它可以接收不同類型的實參,也可以接收常數或者表達式。