本文總結了幾乎所有不易了解或是容易忘記的C++知識,可作為手冊查閱,内容參考自清華大學鄭莉教授的C++課程。
内聯函數
聲明時使用關鍵字 inline
編譯時在調用處用函數體進行替換,節省了參數傳遞、控制轉移等開銷
注意:
内聯函數體内不能有循環語句和switch語句
内聯函數的定義必須出現在内聯函數第一次被調用之前
對内聯函數不能進行異常接口聲明
定義内聯函數,可以顯式用inline聲明,也可以直接在類内定義好實作
編譯器并不一定遵從我們的inline
constexpr函數
constexpr修飾的函數在其所有參數都是constexpr時,一定傳回constexpr
函數體中必須有且僅有一條return語句
constexpr的變量的值必須是編譯器在編譯的時候就可以确定的
constexpr int get_size() { return 20; }
constexpr int foo = get_size(); //正确:foo是一個常量表達式
複制
重載函數
通過形參的個數不同或者類型不同進行區分
無法通過傳回值區分
構造函數
預設構造函數
//下面兩個都是預設構造函數,如在類中同時出現,将産生編譯錯誤:
Clock();
Clock(int newH=0,int newM=0,int newS=0);
複制
隐含生成的構造函數
如果程式中未定義構造函數,編譯器将在需要時自動生成一個預設構造函數
參數清單為空,不為資料成員設定初始值
如果類内定義了成員的初始值,則使用類内定義的初始值
如果沒有定義類内的初始值,則以預設方式初始化
基本類型的資料預設初始化的值是不确定的
=default
如果程式中已定義構造函數,預設情況下編譯器就不再隐含生成預設構造函數。如果此時依然希望編譯器隐含生成預設構造函數,可以使用
=default
class Clock {
public:
Clock() =default; //訓示編譯器提供預設構造函數
Clock(int newH, int newM, int newS); //構造函數
private:
int hour, minute, second;
};
複制
委托構造函數
類中往往有多個構造函數,隻是參數表和初始化清單不同,其初始化算法都是相同的,這時,為了避免代碼重複,可以使用委托構造函數
不使用委托構造函數:
//構造函數
Clock(int newH, int newM, int newS) : hour(newH),minute(newM),second(newS) {}
//預設構造函數
Clock() : hour(0),minute(0),second(0) {}
複制
使用委托構造函數:
Clock(int newH, int newM, int newS) : hour(newH),minute(newM),second(newS) {}
Clock(): Clock(0, 0, 0) {}
複制
複制構造函數
複制構造函數是一種特殊的構造函數,其形參為本類的對象引用,作用是用一個已存在的對象去初始化同類型的新對象
定義一個對象時,以本類另一個對象作為初始值,發生複制構造
如果函數的形參是類的對象,調用函數時,将使用實參對象初始化形參對象,發生複制構造
如果函數的傳回值是類的對象,函數執行完成傳回主調函數時,将使用return語句中的對象初始化一個臨時無名對象,傳遞給主調函數,此時發生複制構造
隐含的複制構造函數
如果程式員沒有為類聲明拷貝初始化構造函數,則編譯器自己生成一個隐含的複制構造函數
這個構造函數執行的功能是:用作為初始值的對象的每個資料成員的值,初始化将要建立的對象的對應資料成員(淺拷貝)
=delete
C++11做法:用
=delete
訓示編譯器不生成預設複制構造函數。
class Point { //Point 類的定義
public:
Point(int xx=0, int yy=0) { x = xx; y = yy; } //構造函數,内聯
Point(const Point& p) =delete; //訓示編譯器不生成預設複制構造函數
private:
int x, y; //私有資料
};
複制
類的組合
構造組合類對象時的初始化次序
首先對構造函數初始化清單中列出的成員(包括基本類型成員和對象成員)進行初始化,初始化次序是成員在類體中定義的次序
成員對象構造函數調用順序:按對象成員的聲明順序,先聲明者先構造
初始化清單中未出現的成員對象:調用用預設構造函數(即無形參的)初始化
處理完初始化清單之後,再執行構造函數的函數體
前向引用聲明
如果需要在某個類的聲明之前,引用該類,則應進行前向引用聲明
前向引用聲明隻為程式引入一個辨別符,但具體聲明在其他地方
class B; //前向引用聲明
class A {
public:
void f(B b);
};
class B {
public:
void g(A a);
};
複制
使用前向引用聲明雖然可以解決一些問題,但它并不是萬能的
在提供一個完整的類聲明之前,不能聲明該類的對象,也不能在内聯成員函數中使用該類的對象
當使用前向引用聲明時,隻能使用被聲明的符号,而不能涉及類的任何細節
class Fred; //前向引用聲明
class Barney {
Fred x; //錯誤:類Fred的聲明尚不完善
};
class Fred {
Barney y;
};
複制
聯合體
成員共用同一組記憶體單元
任何兩個成員不會同時有效
class ExamInfo {
private:
string name; //課程名稱
enum { GRADE, PASS, PERCENTAGE } mode;//計分方式
union {
char grade; //等級制的成績
bool pass; //隻記是否通過課程的成績
int percent; //百分制的成績
};
public:
//三種構造函數,分别用等級、是否通過和百分初始化
ExamInfo(string name, char grade)
: name(name), mode(GRADE), grade(grade) { }
ExamInfo(string name, bool pass)
: name(name), mode(PASS), pass(pass) { }
ExamInfo(string name, int percent)
: name(name), mode(PERCENTAGE), percent(percent) { }
void show();
}
void ExamInfo::show() {
cout << name << ": ";
switch (mode) {
case GRADE: cout << grade; break;
case PASS: cout << (pass ? "PASS" : "FAIL"); break;
case PERCENTAGE: cout << percent; break;
}
cout << endl;
}
int main() {
ExamInfo course1("English", 'B');
ExamInfo course2("Calculus", true);
ExamInfo course3("C++ Programming", 85);
course1.show();
course2.show();
course3.show();
return 0;
}
//運作結果:
//English: B
//Calculus: PASS
//C++ Programming: 85
複制
枚舉類
//enum class 枚舉類型名: 底層類型 {枚舉值清單};
enum class Type { General, Light, Medium, Heavy};
enum class Type: char { General, Light, Medium, Heavy};
enum class Category { General=1, Pistol, MachineGun, Cannon};
複制
枚舉類的優勢
強作用域,其作用域限制在枚舉類中
轉換限制,枚舉類對象不可以與整型隐式地互相轉換。
可以指定底層類型
#include<iostream>
using namespace std;
enum class Side{ Right, Left };
enum class Thing{ Wrong, Right }; //不沖突
int main()
{
Side s = Side::Right;
Thing w = Thing::Wrong;
cout << (s == w) << endl; //編譯錯誤,無法直接比較不同枚舉類
return 0;
}
複制
類的友元
友元是C++提供的一種破壞資料封裝和資料隐藏的機制
通過将一個子產品聲明為另一個子產品的友元,一個子產品能夠引用到另一個子產品中本是被隐藏的資訊
為了確定資料的完整性,及資料封裝與隐藏的原則,建議盡量不使用或少使用友元
友元函數
友元函數是在類聲明中由關鍵字friend修飾說明的非成員函數,在它的函數體中能夠通過對象名通路 private 和protected成員
作用:增加靈活性,使程式員可以在封裝和快速性方面做合理選擇
通路對象中的成員必須通過對象名
友元類
若一個類為另一個類的友元,則此類的所有成員都能通路對方類的私有成員
聲明文法:将友元類名在另一個類中使用friend修飾說明
類的友元關系是單向的
如果聲明B類是A類的友元,B類的成員函數就可以通路A類的私有和保護資料,但A類的成員函數卻不能通路B類的私有、保護資料
常類型
對于既需要共享、又需要防止改變的資料應該聲明為常類型(用const進行修飾)
const關鍵字可以被用于參與對重載函數的區分
通過常對象隻能調用它的常成員函數
#include<iostream>
using namespace std;
class R {
public:
R(int r1, int r2) : r1(r1), r2(r2) { }
void print();
void print() const;
private:
int r1, r2;
};
void R::print() {
cout << r1 << ":" << r2 << endl;
}
void R::print() const {
cout << r1 << ";" << r2 << endl;
}
int main() {
R a(5,4);
a.print(); //調用void print()
const R b(20,52);
b.print(); //調用void print() const
return 0;
}
複制
常成員函數可以被非常對象調用,但常對象不可調用非常成員函數
多檔案結構
外部變量
如果一個變量除了在定義它的源檔案中可以使用外,還能被其它檔案使用,那麼就稱這個變量是外部變量
檔案作用域中定義的變量,預設情況下都是外部變量,但在其它檔案中如果需要使用這一變量,需要用extern關鍵字加以聲明
外部函數
在所有類之外聲明的函數(也就是非成員函數),都是具有檔案作用域的
這樣的函數都可以在不同的編譯單元中被調用,隻要在調用之前進行引用性聲明(即聲明函數原型)即可。也可以在聲明函數原型或定義函數時用extern修飾,其效果與不加修飾的預設狀态是一樣的
編譯預處理指令
預處理在編譯前進行
每條預處理指令必須單獨占用一行
預處理指令可以出現在程式的任何位置
指針
空值nullptr
-
以往用0或者NULL去表達空指針的問題:
C/C++的NULL宏是個被有很多潛在BUG的宏。因為有的庫把其定義成整數0,有的定義成 (void*)0。在C的時代還好。但是在C++的時代,這就會引發很多問題
- C++11使用nullptr關鍵字,是表達更準确,類型安全的空指針
指向常量的指針
不能通過指向常量的指針改變所指對象的值,但指針本身可以改變,可以指向另外的對象。
int a;
const int *p1 = &a; //p1是指向常量的指針
int b;
p1 = &b; //正确,p1本身的值可以改變
*p1 = 1; //編譯時出錯,不能通過p1改變所指的對象
複制
指針類型的常量
若聲明指針常量,則指針本身的值不能被改變。
int a;
int * const p2 = &a;
p2 = &b; //錯誤,p2是指針常量,值不能改變
複制
函數指針
int f(int a, int b) {
return a + b;
}
int main() {
int (*p)(int, int) = f;
cout<<p(1, 2)<<endl;
return 0;
}
複制
智能指針
顯式管理記憶體在是能上有優勢,但容易出錯
C++11提供智能指針的資料類型,對垃圾回收技術提供了一些支援,實作一定程度的記憶體管理
- unique_ptr :不允許多個指針共享資源,可以用标準庫中的move函數轉移指針
- shared_ptr :多個指針共享資源
- weak_ptr :可複制shared_ptr,但其構造或者釋放對資源不産生影響
移動構造
移動構造可以減少不必要的複制,帶來性能上的提升
C++11之前,如果要将源對象的狀态轉移到目标對象隻能通過複制。在某些情況下,我們沒有必要複制對象——隻需要移動它們
有可被利用的臨時對象時,觸發移動構造
//函數傳回含有指針成員的對象
//将要傳回的局部對象轉移到主調函數,省去了構造和删除臨時對象的過程
#include<iostream>
using namespace std;
class IntNum {
public:
IntNum(int x = 0) : xptr(new int(x)){ //構造函數
cout << "Calling constructor..." << endl;
}
IntNum(const IntNum & n) : xptr(new int(*n.xptr)){//複制構造函數
cout << "Calling copy constructor..." << endl;
}
//&&是右值引用
//函數傳回的臨時變量是右值
IntNum(IntNum && n): xptr(n.xptr){ //移動構造函數
n.xptr = nullptr;
cout << "Calling move constructor..." << endl;
}
~IntNum(){ //析構函數
delete xptr;
cout << "Destructing..." << endl;
}
private:
int *xptr;
};
//傳回值為IntNum類對象
IntNum getNum() {
IntNum a;
return a;
}
int main() {
cout << getNum().getInt() << endl; return 0;
}
/*
運作結果:
Calling constructor...
Calling move constructor...
Destructing... //這裡釋放了nullptr
0
Destructing...
*/
複制
左值和右值
左值和右值都是針對表達式而言的
左值是指表達式結束後依然存在的持久對象
右值指表達式結束時就不再存在的臨時對象——顯然右值不可以被取位址
讀入字元串
用cin的>>操作符輸入字元串,會以空格作為分隔符,空格後的内容會在下一回輸入時被讀取
getline可以輸入整行字元串(要包string頭檔案),例如:
getline(cin, s2);
輸入字元串時,可以使用其它分隔符作為字元串結束的标志(例如逗号、分号),将分隔符作為getline的第3個參數即可,例如:
getline(cin, s2, ',');
#include <iostream>
#include <string>
using namespace std;
int main() {
for (int i = 0; i < 2; i++){
string city, state;
getline(cin, city, ',');
getline(cin, state);
cout << "City:" << city << “ State:" << state << endl;
}
return 0;
}
/*
運作結果:
Beijing,China
City: Beijing State: China
San Francisco,the United States
City: San Francisco State: the United States
*/
複制
繼承
公有繼承(public)
繼承的通路控制
- 基類的public和protected成員:通路屬性在派生類中保持不變
- 基類的private成員:不可直接通路
通路權限
- 派生類中的成員函數:可以直接通路基類中的public和protected成員,但不能直接通路基類的private成員
- 通過派生類的對象:隻能通路public成員
私有繼承(private)
繼承的通路控制
- 基類的public和protected成員:都以private身份出現在派生類中
- 基類的private成員:不可直接通路
通路權限
- 派生類中的成員函數:可以直接通路基類中的public和protected成員,但不能直接通路基類的private成員
- 通過派生類的對象:不能直接通路從基類繼承的任何成員
保護繼承(protected)
繼承的通路控制
- 基類的public和protected成員:都以protected身份出現在派生類中
- 基類的private成員:不可直接通路
通路權限
- 派生類中的成員函數:可以直接通路基類中的public和protected成員,但不能直接通路基類的private成員
- 通過派生類的對象:不能直接通路從基類繼承的任何成員
protected 成員的特點與作用
- 對建立其所在類對象的子產品來說,它與 private 成員的性質相同
- 對于其派生類來說,它與 public 成員的性質相同
- 既實作了資料隐藏,又友善繼承,實作代碼重用
class A{
public:
void setA(int);
private:
int a;
};
class B{
public:
void setB(int);
private:
int b;
};
class C:public A, private B{
public:
void setC(int, int, int);
private:
int c;
};
void A::setA(int x){
a = x;
}
void B::setB(int x){
b = x;
}
void C::setC(int x, int y, int z){
setA(x);
setB(y);
c = z;
}
int main(int argc, const char * argv[]) {
C obj;
obj.setA(5); // 正确
obj.setB(6); // 錯誤
obj.setC(6, 7, 9); // 正确
return 0;
}
複制
派生類的構造函數
預設情況
- 基類的構造函數不被繼承
- 派生類需要定義自己的構造函數
C++11規定
- 可用using語句繼承基類構造函數
-
但是隻能初始化從基類繼承的成員
派生類新增成員可以通過類内初始值進行初始化
-
文法形式:
using B::B;
多繼承且有對象成員時派生的構造函數定義文法
派生類名::派生類名(形參表):
基類名1(參數), 基類名2(參數), ..., 基類名n(參數),
本類成員(含對象成員)初始化清單
{
//其他初始化
};
複制
構造函數的執行順序
-
調用基類構造函數
順序按照它們被繼承時聲明的順序(從左向右)
-
對初始化清單中的成員進行初始化
順序按照它們在類中定義的順序
對象成員初始化時自動調用其所屬類的構造函數,由初始化清單提供參數
- 執行派生類的構造函數體中的内容
派生類複制構造函數
派生類未定義複制構造函數的情況
編譯器會在需要時生成一個隐含的複制構造函數
先調用基類的複制構造函數
再為派生類新增的成員執行複制
派生類定義了複制構造函數的情況
一般都要為基類的複制構造函數傳遞參數
複制構造函數隻能接受一個參數,既用來初始化派生類定義的成員,也将被傳遞給基類的複制構造函數
基類的複制構造函數形參類型是基類對象的引用,實參可以是派生類對象的引用
例如:
C::C(const C &c1): B(c1) {…}
派生類的析構函數
析構函數不被繼承,派生類如果需要,要自行聲明析構函數
聲明方法與無繼承關系時類的析構函數相同
不需要顯式地調用基類的析構函數,系統會自動隐式調用
先執行派生類析構函數的函數體,再調用基類的析構函數
通路從基類繼承的成員
當派生類與基類中有相同成員時:
- 若未特别限定,則通過派生類對象使用的是派生類中的同名成員
- 如要通過派生類對象通路基類中被隐藏的同名成員,應使用基類名和作用域操作符(::)來限定
如果從不同基類繼承了同名成員,但是在派生類中沒有定義同名成員,“派生類對象名或引用名.成員名”、“派生類指針->成員名”通路成員存在二義性問題
- 解決方式:用類名限定
虛基類
需要解決的問題
- 當派生類從多個基類派生,而這些基類又共同基類,則在通路此共同基類中的成員時,将産生備援,并有可能因備援帶來不一緻性
虛基類聲明
- 以virtual說明基類繼承方式
- 例:
class B1:virtual public B
作用
- 主要用來解決多繼承時可能發生的對同一基類繼承多次而産生的二義性問題
- 為最遠的派生類提供唯一的基類成員,而不重複産生多次複制
注意:
- 在第一級繼承時就要将共同基類設計為虛基類
虛基類及其派生類構造函數
建立對象時所指定的類稱為最遠派生類
虛基類的成員是由最遠派生類的構造函數通過調用虛基類的構造函數進行初始化的
在整個繼承結構中,直接或間接繼承虛基類的所有派生類,都必須在構造函數的成員初始化表中為虛基類的構造函數列出參數。如果未列出,則表示調用該虛基類的預設構造函數
在建立對象時,隻有最遠派生類的構造函數調用虛基類的構造函數,其他類對虛基類構造函數的調用被忽略
#include <iostream>
using namespace std;
class Base0 {
public:
Base0(int var) : var0(var) { }
int var0;
void fun0() { cout << "Member of Base0" << endl; }
};
class Base1: virtual public Base0 {
public:
Base1(int var) : Base0(var) { }
int var1;
};
class Base2: virtual public Base0 {
public:
Base2(int var) : Base0(var) { }
int var2;
};
class Derived: public Base1, public Base2 {
public:
Derived(int var) : Base0(var), Base1(var), Base2(var) { }
int var;
void fun()
{ cout << "Member of Derived" << endl; }
};
int main() { //程式主函數
Derived d(1);
d.var0 = 2; //直接通路虛基類的資料成員
d.fun0(); //直接通路虛基類的函數成員
return 0;
}
複制
運算符重載
雙目運算符重載規則
如果要重載 B 為類成員函數,使之能夠實作表達式 oprd1 B oprd2,其中 oprd1 為A 類對象,則 B 應被重載為 A 類的成員函數,形參類型應該是 oprd2 所屬的類型
經重載後,表達式 oprd1 B oprd2 相當于 oprd1.operator B(oprd2)
#include <iostream>
using namespace std;
class Complex {
public:
Complex(double r = 0.0, double i = 0.0) : real(r), imag(i) { }
//運算符+重載成員函數
Complex operator + (const Complex &c2) const;
//運算符-重載成員函數
Complex operator - (const Complex &c2) const;
void display() const; //輸出複數
private:
double real; //複數實部
double imag; //複數虛部
};
//複數類加減法運算重載為成員函數
Complex Complex::operator + (const Complex &c2) const{
//建立一個臨時無名對象作為傳回值
return Complex(real+c2.real, imag+c2.imag);
}
Complex Complex::operator - (const Complex &c2) const{
//建立一個臨時無名對象作為傳回值
return Complex(real-c2.real, imag-c2.imag);
}
void Complex::display() const {
cout<<"("<<real<<", "<<imag<<")"<<endl;
}
//複數類加減法運算重載為成員函數
int main() {
Complex c1(5, 4), c2(2, 10), c3;
cout << "c1 = "; c1.display();
cout << "c2 = "; c2.display();
c3 = c1 - c2; //使用重載運算符完成複數減法
cout << "c3 = c1 - c2 = "; c3.display();
c3 = c1 + c2; //使用重載運算符完成複數加法
cout << "c3 = c1 + c2 = "; c3.display();
return 0;
}
複制
前置單目運算符重載規則
如果要重載 U 為類成員函數,使之能夠實作表達式 U oprd,其中 oprd 為A類對象,則 U 應被重載為 A 類的成員函數,無形參。
經重載後,表達式 U oprd 相當于 oprd.operator U()
後置單目運算符 ++和--重載規則
如果要重載 ++或--為類成員函數,使之能夠實作表達式 oprd++ 或 oprd-- ,其中 oprd 為A類對象,則 ++或-- 應被重載為 A 類的成員函數,且具有一個 int 類型形參。
經重載後,表達式 oprd++ 相當于 oprd.operator ++(0)
#include <iostream>
using namespace std;
class Clock {//時鐘類定義
public:
Clock(int hour = 0, int minute = 0, int second = 0);
void showTime() const;
//前置單目運算符重載
Clock& operator ++ ();
//後置單目運算符重載
Clock operator ++ (int);
private:
int hour, minute, second;
};
Clock::Clock(int hour, int minute, int second) {
if (0 <= hour && hour < 24 && 0 <= minute && minute < 60
&& 0 <= second && second < 60) {
this->hour = hour;
this->minute = minute;
this->second = second;
} else
cout << "Time error!" << endl;
}
void Clock::showTime() const { //顯示時間
cout << hour << ":" << minute << ":" << second << endl;
}
//重載前置++和後置++為時鐘類成員函數
Clock & Clock::operator ++ () {
second++;
if (second >= 60) {
second -= 60; minute++;
if (minute >= 60) {
minute -= 60; hour = (hour + 1) % 24;
}
}
return *this;
}
Clock Clock::operator ++ (int) {
//注意形參表中的整型參數
Clock old = *this;
++(*this); //調用前置“++”運算符
return old;
}
int main() {
Clock myClock(23, 59, 59);
cout << "First time output: ";
myClock.showTime();
cout << "Show myClock++: ";
(myClock++).showTime();
cout << "Show ++myClock: ";
(++myClock).showTime();
return 0;
}
複制
運算符重載為非成員函數
有些運算符不能重載為成員函數,例如二進制運算符的左操作數不是對象,或者是不能由我們重載運算符的對象
運算符重載為非成員函數的規則
- 函數的形參代表依自左至右次序排列的各操作數
- 參數個數=原操作數個數(後置++、--除外)
- 至少應該有一個自定義類型的參數
- 後置單目運算符 ++和--的重載函數,形參清單中要增加一個int,但不必寫形參名
- 如果在運算符的重載函數中需要操作某類對象的私有成員,可以将此函數聲明為該類的友元
- 雙目運算符 B重載後,表達式oprd1 B oprd2等同于operator B(oprd1,oprd2 )
- 前置單目運算符 B重載後,表達式 B oprd等同于operator B(oprd )
- 後置單目運算符 ++和--重載後,表達式 oprd B等同于operator B(oprd,0 )
//重載Complex的加減法和“<<”運算符為非成員函數
//将+、-(雙目)重載為非成員函數,并将其聲明為複數類的友元,兩個操作數都是複數類的常引用。 • 将<<(雙目)重載為非成員函數,并将其聲明為複數類的友元,它的左操作數是std::ostream引用,右操作數為複數類的常引用,傳回std::ostream引用
#include <iostream>
using namespace std;
class Complex {
public:
Complex(double r = 0.0, double i = 0.0) : real(r), imag(i) { }
friend Complex operator+(const Complex &c1, const Complex &c2);
friend Complex operator-(const Complex &c1, const Complex &c2);
friend ostream & operator<<(ostream &out, const Complex &c);
private:
double real; //複數實部
double imag; //複數虛部
};
Complex operator+(const Complex &c1, const Complex &c2){
return Complex(c1.real+c2.real, c1.imag+c2.imag);
}
Complex operator-(const Complex &c1, const Complex &c2){
return Complex(c1.real-c2.real, c1.imag-c2.imag);
}
ostream & operator<<(ostream &out, const Complex &c){
out << "(" << c.real << ", " << c.imag << ")";
return out;
}
int main() {
Complex c1(5, 4), c2(2, 10), c3;
cout << "c1 = " << c1 << endl;
cout << "c2 = " << c2 << endl;
c3 = c1 - c2; //使用重載運算符完成複數減法
cout << "c3 = c1 - c2 = " << c3 << endl;
c3 = c1 + c2; //使用重載運算符完成複數加法
cout << "c3 = c1 + c2 = " << c3 << endl;
return 0;
}
複制
虛函數
- 用virtual關鍵字說明的函數
- 虛函數是實作運作時多态性基礎
- C++中的虛函數是動态綁定的函數
- 虛函數必須是非靜态的成員函數,虛函數經過派生之後,就可以實作運作過程中的多态
- 一般成員函數可以是虛函數
- 構造函數不能是虛函數
- 析構函數可以是虛函數
- 虛函數聲明隻能出現在類定義中的函數原型聲明中,而不能在成員函數實作的時候
- 在派生類中可以對基類中的成員函數進行覆寫
- 虛函數一般不聲明為内聯函數,因為對虛函數的調用需要動态綁定,而對内聯函數的處理是靜态的
virtual 關鍵字
- 派生類可以不顯式地用virtual聲明虛函數,這時系統就會用以下規則來判斷派生類的一個函數成員是不是虛函數:
- 該函數是否與基類的虛函數有相同的名稱、參數個數及對應參數類型
- 該函數是否與基類的虛函數有相同的傳回值或者滿足類型相容規則的指針、引用型的傳回值
- 如果從名稱、參數及傳回值三個方面檢查之後,派生類的函數滿足上述條件,就會自動确定為虛函數。這時,派生類的虛函數便覆寫了基類的虛函數
- 派生類中的虛函數還會隐藏基類中同名函數的所有其它重載形式
- 一般習慣于在派生類的函數中也使用virtual關鍵字,以增加程式的可讀性
虛析構函數
為什麼需要虛析構函數? - 可能通過基類指針删除派生類對象; - 如果你打算允許其他人通過基類指針調用對象的析構函數(通過delete這樣做是正常的),就需要讓基類的析構函數成為虛函數,否則執行delete的結果是不确定的
#include <iostream>
using namespace std;
class Base{
public:
virtual ~Base();
};
Base::~Base()
{
cout << "Base ";
}
class Derived: public Base{
public:
virtual ~Derived();
};
Derived::~Derived(){
cout << "Derived ";
}
void fun(Base *b){
delete b;
}
int main(int argc, const char * argv[]) {
Base *b = new Derived();
fun(b);
return 0;
}
複制
虛表與動态綁定
虛表
- 每個多态類有一個虛表(virtual table)
- 虛表中有目前類的各個虛函數的入口位址
- 每個對象有一個指向目前類的虛表的指針(虛指針vptr)
動态綁定的實作
- 構造函數中為對象的虛指針指派
- 通過多态類型的指針或引用調用成員函數時,通過虛指針找到虛表,進而找到所調用的虛函數的入口位址
- 通過該入口位址調用虛函數
class A{
public:
virtual void fun();
};
//在32位機器上,sizeof(A)為:4;在64位機器上,sizeof(A)為:8
//因為A中含有一個指向虛表的指針,在32位機器上,指針占4個位元組;在64位機器上,指針占8個位元組
複制
抽象類和純虛函數
純虛函數是一個在基類中聲明的虛函數,它在該基類中沒有定義具體的操作内容,要求各派生類根據實際需要定義自己的版本,純虛函數的聲明格式為:virtual 函數類型 函數名(參數表) = 0;
帶有純虛函數的類稱為抽象類
抽象類作用
- 抽象類為抽象和設計的目的而聲明
- 将有關的資料和行為組織在一個繼承層次結構中,保證派生類具有要求的行為
- 對于暫時無法實作的函數,可以聲明為純虛函數,留給派生類去實作
注意:
- 抽象類隻能作為基類來使用。
- 不能定義抽象類的對象。
#include <iostream>
using namespace std;
class Base1 {
public:
virtual void display() const = 0; //純虛函數
};
class Base2: public Base1 {
public:
virtual void display() const; //覆寫基類的虛函數
};
void Base2::display() const {
cout << "Base2::display()" << endl;
}
class Derived: public Base2 {
public:
virtual void display() const; //覆寫基類的虛函數
};
void Derived::display() const {
cout << "Derived::display()" << endl;
}
void fun(Base1 *ptr) {
ptr->display();
}
int main() {
Base2 base2;
Derived derived;
fun(&base2);
fun(&derived);
return 0;
}
複制
override
C++11 引入顯式函數覆寫,在編譯期而非運作期捕獲此類錯誤。 - 在虛函數顯式重載中運用,編譯器會檢查基類是否存在一虛拟函數,與派生類中帶有聲明override的虛拟函數,有相同的函數簽名(signature);若不存在,則會回報錯誤
- 多态行為的基礎:基類聲明虛函數,繼承類聲明一個函數覆寫該虛函數
- 覆寫要求: 函數簽名(signatture)完全一緻
- 函數簽名包括:函數名 參數清單 const
final
C++11提供final,用來避免類被繼承,或是基類的函數被改寫 例:
struct Base1 final { };
struct Derived1 : Base1 { }; // 編譯錯誤:Base1為final,不允許被繼承
struct Base2 { virtual void f() final; };
struct Derived2 : Base2 { void f(); // 編譯錯誤:Base2::f 為final,不允許被覆寫 };
複制
模闆
函數模闆
文法形式:
template <模闆參數表>
模闆參數表的内容:
- 類型參數:class(或typename) 辨別符
- 常量參數:類型說明符 辨別符
- 模闆參數:template <參數表> class辨別符
注意:
- 一個函數模闆并非自動可以處理所有類型的資料
- 隻有能夠進行函數模闆中運算的類型,可以作為類型實參
- 自定義的類,需要重載模闆中的運算符,才能作為類型實參
#include <iostream>
using namespace std;
template <class T> //定義函數模闆
void outputArray(const T *array, int count) {
for (int i = 0; i < count; i++)
cout << array[i] << " "; //如果數組元素是類的對象,需要該對象所屬類重載了流插入運算符“<<”
cout << endl;
}
int main() {
const int A_COUNT = 8, B_COUNT = 8, C_COUNT = 20;
int a [A_COUNT] = { 1, 2, 3, 4, 5, 6, 7, 8 };
double b[B_COUNT] = { 1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8 };
char c[C_COUNT] = "Welcome!";
cout << " a array contains:" << endl;
outputArray(a, A_COUNT);
cout << " b array contains:" << endl;
outputArray(b, B_COUNT);
cout << " c array contains:" << endl;
outputArray(c, C_COUNT);
return 0;
}
複制
類模闆
使用類模闆使使用者可以為類聲明一種模式,使得類中的某些資料成員、某些成員函數的參數、某些成員函數的傳回值,能取任意類型(包括基本類型的和使用者自定義類型)
類模闆 template <模闆參數表> class 類名 {類成員聲明};
如果需要在類模闆以外定義其成員函數,則要采用以下的形式: template <模闆參數表> 類型名 類名<模闆參數辨別符清單>::函數名(參數表)
#include <iostream>
#include <cstdlib>
using namespace std;
struct Student {
int id; //學号
float gpa; //平均分
};
template <class T>
class Store {//類模闆:實作對任意類型資料進行存取
private:
T item; // item用于存放任意類型的資料
bool haveValue; // haveValue标記item是否已被存入内容
public:
Store();
T &getElem(); //提取資料函數
void putElem(const T &x); //存入資料函數
};
template <class T>
Store<T>::Store(): haveValue(false) { }
template <class T>
T &Store<T>::getElem() {
//如試圖提取未初始化的資料,則終止程式
if (!haveValue) {
cout << "No item present!" << endl;
exit(1); //使程式完全退出,傳回到作業系統。
}
return item; // 傳回item中存放的資料
}
template <class T>
void Store<T>::putElem(const T &x) {
// 将haveValue 置為true,表示item中已存入數值
haveValue = true;
item = x; // 将x值存入item
}
int main() {
Store<int> s1, s2;
s1.putElem(3);
s2.putElem(-7);
cout << s1.getElem() << " " << s2.getElem() << endl;
Student g = { 1000, 23 };
Store<Student> s3;
s3.putElem(g);
cout << "The student id is " << s3.getElem().id << endl;
Store<double> d;
cout << "Retrieving object D... ";
cout << d.getElem() << endl;
//d未初始化,執行函數D.getElement()時導緻程式終止
return 0;
}
複制
數組類模闆
自己實作一個動态數組
#ifndef ARRAY_H
#define ARRAY_H
#include <cassert>
template <class T> //數組類模闆定義
class Array {
private:
T* list; //用于存放動态配置設定的數組記憶體首位址
int size; //數組大小(元素個數)
public:
Array(int sz = 50); //構造函數
Array(const Array<T> &a); //複制構造函數
~Array(); //析構函數
Array<T> & operator = (const Array<T> &rhs); //重載"=“
T & operator [] (int i); //重載"[]”
const T & operator [] (int i) const; //重載"[]”常函數
operator T * (); //重載到T*類型的轉換
operator const T * () const;
int getSize() const; //取數組的大小
void resize(int sz); //修改數組的大小
};
template <class T> Array<T>::Array(int sz) {//構造函數
assert(sz >= 0);//sz為數組大小(元素個數),應當非負
size = sz; // 将元素個數指派給變量size
list = new T [size]; //動态配置設定size個T類型的元素空間
}
template <class T> Array<T>::~Array() { //析構函數
delete [] list;
}
template <class T>
Array<T>::Array(const Array<T> &a) { //複制構造函數
size = a.size; //從對象x取得數組大小,并指派給目前對象的成員
list = new T[size]; // 動态配置設定n個T類型的元素空間
for (int i = 0; i < size; i++) //從對象X複制數組元素到本對象
list[i] = a.list[i];
}
//重載"="運算符,将對象rhs指派給本對象。實作對象之間的整體指派
template <class T>
Array<T> &Array<T>::operator = (const Array<T>& rhs) {
if (&rhs != this) {
//如果本對象中數組大小與rhs不同,則删除數組原有記憶體,然後重新配置設定
if (size != rhs.size) {
delete [] list; //删除數組原有記憶體
size = rhs.size; //設定本對象的數組大小
list = new T[size]; //重新配置設定size個元素的記憶體
}
//從對象X複制數組元素到本對象
for (int i = 0; i < size; i++)
list[i] = rhs.list[i];
}
return *this; //傳回目前對象的引用
}
//重載下标運算符,實作與普通數組一樣通過下标通路元素,具有越界檢查功能
template <class T>
T &Array<T>::operator[] (int n) {
assert(n >= 0 && n < size); //檢查下标是否越界
return list[n]; //傳回下标為n的數組元素
}
template <class T>
const T &Array<T>::operator[] (int n) const {
assert(n >= 0 && n < size); //檢查下标是否越界
return list[n]; //傳回下标為n的數組元素
}
//重載指針轉換運算符,将Array類的對象名轉換為T類型的指針
template <class T>
Array<T>::operator T * () {
return list; //傳回目前對象中私有數組的首位址
}
//取目前數組的大小
template <class T>
int Array<T>::getSize() const {
return size;
}
// 将數組大小修改為sz
template <class T>
void Array<T>::resize(int sz) {
assert(sz >= 0); //檢查sz是否非負
if (sz == size) //如果指定的大小與原有大小一樣,什麼也不做
return;
T* newList = new T [sz]; //申請新的數組記憶體
int n = (sz < size) ? sz : size;//将sz與size中較小的一個指派給n
//将原有數組中前n個元素複制到新數組中
for (int i = 0; i < n; i++)
newList[i] = list[i];
delete[] list; //删除原數組
list = newList; // 使list指向新數組
size = sz; //更新size
}
#endif //ARRAY_H
複制
泛型程式設計與STL
疊代器
疊代器是算法和容器的橋梁
- 疊代器用作通路容器中的元素
- 算法不直接操作容器中的資料,而是通過疊代器間接操作
算法和容器獨立
- 增加新的算法,無需影響容器的實作
- 增加新的容器,原有的算法也能适用
#include <algorithm>
#include <iterator>
#include <vector>
#include <iostream>
using namespace std;
//将來自輸入疊代器的n個T類型的數值排序,将結果通過輸出疊代器result輸出
template <class T, class InputIterator, class OutputIterator>
void mySort(InputIterator first, InputIterator last, OutputIterator result) {
//通過輸入疊代器将輸入資料存入向量容器s中
vector<T> s;
for (;first != last; ++first)
s.push_back(*first);
//對s進行排序,sort函數的參數必須是随機通路疊代器
sort(s.begin(), s.end());
copy(s.begin(), s.end(), result); //将s序列通過輸出疊代器輸出
}
int main() {
//将s數組的内容排序後輸出
double a[5] = { 1.2, 2.4, 0.8, 3.3, 3.2 };
mySort<double>(a, a + 5, ostream_iterator<double>(cout, " "));
cout << endl;
//從标準輸入讀入若幹個整數,将排序後的結果輸出
mySort<int>(istream_iterator<int>(cin), istream_iterator<int>(), ostream_iterator<int>(cout, " "));
cout << endl;
return 0;
}
複制
逆向疊代器
- rbegin() :指向容器尾的逆向疊代器
- rend():指向容器首的逆向疊代器
逆向疊代器的類型名的表示方式如下:
- S::reverse_iterator:逆向疊代器類型
- S::const_reverse_iterator:逆向常疊代器類型
函數對象
一個行為類似函數的對象
可以沒有參數,也可以帶有若幹參數
其功能是擷取一個值,或者改變操作的狀态
普通函數就是函數對象
重載了“()”運算符的類的執行個體是函數對象
#include <iostream>
#include <numeric> //包含數值算法頭檔案
using namespace std;
class MultClass{ //定義MultClass類
public:
//重載操作符operator()
int operator() (int x, int y) const { return x * y; }
};
int main() {
int a[] = { 1, 2, 3, 4, 5 };
const int N = sizeof(a) / sizeof(int);
cout << "The result by multipling all elements in a is "
<< accumulate(a, a + N, 1, MultClass()) //将類multclass傳遞給通用算法
<< endl;
return 0;
}
複制
#include<functional>
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
int main() {
int intArr[] = { 30, 90, 10, 40, 70, 50, 20, 80 };
const int N = sizeof(intArr) / sizeof(int);
vector<int> a(intArr, intArr + N);
cout << "before sorting:" << endl;
copy(a.begin(),a.end(),ostream_iterator<int>(cout,"\t"));
cout << endl;
sort(a.begin(), a.end(), greater<int>()); //STL中的二進制謂詞函數對象
cout << "after sorting:" << endl;
copy(a.begin(),a.end(),ostream_iterator<int>(cout,"\t"));
cout << endl;
return 0;
}
複制
I/O流
操縱符(manipulator)
//使用width控制輸出寬度
#include <iostream>
using namespace std;
int main() {
double values[] = { 1.23, 35.36, 653.7, 4358.24 };
for(int i = 0; i < 4; i++) {
cout.width(10);
cout << values[i] << endl;
}
return 0;
}
/*
輸出結果:
1.23
35.36
653.7
4358.24
*/
複制
//使用setw操縱符指定寬度
#include <iostream>
#include <iomanip>
#include <string>
using namespace std;
int main() {
double values[] = { 1.23, 35.36, 653.7, 4358.24 };
string names[] = { "Zoot", "Jimmy", "Al", "Stan" };
for (int i = 0; i < 4; i++)
cout << setw(6) << names[i]
<< setw(10) << values[i] << endl;
return 0;
}
/*
輸出結果:
Zoot 1.23
Jimmy 35.36
Al 653.7
Stan 4358.24
*/
複制
//設定對齊方式
#include <iostream>
#include <iomanip>
#include <string>
using namespace std;
int main() {
double values[] = { 1.23, 35.36, 653.7, 4358.24 };
string names[] = { "Zoot", "Jimmy", "Al", "Stan" };
for (int i=0;i<4;i++)
cout << setiosflags(ios_base::left)//左對齊
<< setw(6) << names[i]
<< resetiosflags(ios_base::left)
<< setw(10) << values[i] << endl;
return 0;
}
/*
輸出結果:
Zoot 1.23
Jimmy 35.36
Al 653.7
Stan 4358.24
*/
複制
//控制輸出精度——未指定fixed或scientific
#include <iostream>
#include <iomanip>
#include <string>
using namespace std;
int main() {
double values[] = { 1.23, 35.36, 653.7, 4358.24 };
string names[] = { "Zoot", "Jimmy", "Al", "Stan" };
for (int i=0;i<4;i++)
cout << setiosflags(ios_base::left)
<< setw(6) << names[i]
<< resetiosflags(ios_base::left)//清除左對齊設定
<< setw(10) << setprecision(1) << values[i] << endl;
return 0;
}
/*
輸出結果:
Zoot 1
Jimmy 4e+001
Al 7e+002
Stan 4e+003
*/
複制
//控制輸出精度——指定fixed
#include <iostream>
#include <iomanip>
#include <string>
using namespace std;
int main() {
double values[] = { 1.23, 35.36, 653.7, 4358.24 };
string names[] = { "Zoot", "Jimmy", "Al", "Stan" };
cout << setiosflags(ios_base::fixed);
for (int i=0;i<4;i++)
cout << setiosflags(ios_base::left)
<< setw(6) << names[i]
<< resetiosflags(ios_base::left)//清除左對齊設定
<< setw(10) << setprecision(1) << values[i] << endl;
return 0;
}
輸出結果:
Zoot 1.2
Jimmy 35.4
Al 653.7
Stan 4358.2
複制
//控制輸出精度——指定scientific
#include <iostream>
#include <iomanip>
#include <string>
using namespace std;
int main() {
double values[] = { 1.23, 35.36, 653.7, 4358.24 };
string names[] = { "Zoot", "Jimmy", "Al", "Stan" };
cout << setiosflags(ios_base::scientific);
for (int i=0;i<4;i++)
cout << setiosflags(ios_base::left)
<< setw(6) << names[i]
<< resetiosflags(ios_base::left)//清除左對齊設定
<< setw(10) << setprecision(1) << values[i] << endl;
return 0;
}
輸出結果:
Zoot 1.2e+000
Jimmy 3.5e+001
Al 6.5e+002
Stan 4.4e+003
複制
二進制檔案流
使用ofstream構造函數中的模式參量指定二進制輸出模式或以通常方式構造一個流,然後使用setmode成員函數,在檔案打開後改變模式
//向二進制檔案輸出
#include <fstream>
using namespace std;
struct Date {
int mon, day, year;
};
int main() {
Date dt = { 6, 10, 92 };
ofstream file("date.dat", ios_base::binary);
file.write(reinterpret_cast<char *>(&dt),sizeof(dt));
file.close();
return 0;
}
複制
字元串輸出流( ostringstream )
将字元串作為輸出流的目标,可以實作将其他資料類型轉換為字元串的功能
//用ostringstream将數值轉換為字元串
#include <iostream>
#include <sstream>
#include <string>
using namespace std;
//函數模闆toString可以将各種支援“<<“插入符的類型的對象轉換為字元串。
template <class T>
inline string toString(const T &v) {
ostringstream os; //建立字元串輸出流
os << v; //将變量v的值寫入字元串流
return os.str(); //傳回輸出流生成的字元串
}
int main() {
string str1 = toString(5);
cout << str1 << endl;
string str2 = toString(1.2);
cout << str2 << endl;
return 0;
}
/*
輸出結果:
5
1.2
*/
複制
輸入流
重要的輸入流類
- istream類最适合用于順序文本模式輸入,cin是其執行個體
- ifstream類支援磁盤檔案輸入
- istringstream類支援從記憶體中的字元串輸入
//get函數應用舉例
#include <iostream>
using namespace std;
int main() {
char ch;
while ((ch = cin.get()) != EOF)
cout.put(ch);
return 0;
}
複制
//為輸入流指定一個終止字元
#include <iostream>
#include <string>
using namespace std;
int main() {
string line;
cout << "Type a line terminated by 't' " << endl;
getline(cin, line, 't');
cout << line << endl;
return 0;
}
複制
//從檔案讀一個二進制記錄到一個結構中
#include <iostream>
#include <fstream>
#include <cstring>
using namespace std;
struct SalaryInfo {
unsigned id;
double salary;
};
int main() {
SalaryInfo employee1 = { 600001, 8000 };
ofstream os("payroll", ios_base::out | ios_base::binary);
os.write(reinterpret_cast<char *>(&employee1), sizeof(employee1));
os.close();
ifstream is("payroll", ios_base::in | ios_base::binary);
if (is) {
SalaryInfo employee2;
is.read(reinterpret_cast<char *>(&employee2), sizeof(employee2));
cout << employee2.id << " " << employee2.salary << endl;
} else {
cout << "ERROR: Cannot open file 'payroll'." << endl;
}
is.close();
return 0;
}
複制
//用seekg函數設定位置指針
int main() {
int values[] = { 3, 7, 0, 5, 4 };
ofstream os("integers", ios_base::out | ios_base::binary);
os.write(reinterpret_cast<char *>(values), sizeof(values));
os.close();
ifstream is("integers", ios_base::in | ios_base::binary);
if (is) {
is.seekg(3 * sizeof(int));
int v;
is.read(reinterpret_cast<char *>(&v), sizeof(int));
cout << "The 4th integer in the file 'integers' is " << v << endl;
} else {
cout << "ERROR: Cannot open file 'integers'." << endl;
}
return 0;
}
複制
//讀一個檔案并顯示出其中0元素的位置
int main() {
ifstream file("integers", ios_base::in | ios_base::binary);
if (file) {
while (file) {//讀到檔案尾file為0
streampos here = file.tellg();
int v;
file.read(reinterpret_cast<char *>(&v), sizeof(int));
if (file && v == 0)
cout << "Position " << here << " is 0" << endl;
}
} else {
cout << "ERROR: Cannot open file 'integers'." << endl;
}
file.close();
return 0;
}
複制
字元串輸入流( istringstream)
将字元串作為文本輸入流的源,可以将字元串轉換為其他資料類型
//用istringstream将字元串轉換為數值
template <class T>
inline T fromString(const string &str) {
istringstream is(str); //建立字元串輸入流
T v;
is >> v; //從字元串輸入流中讀取變量v
return v; //傳回變量v
}
int main() {
int v1 = fromString<int>("5");
cout << v1 << endl;
double v2 = fromString<double>("1.2");
cout << v2 << endl;
return 0;
}
/*
輸出結果:
5
1.2
*/
複制
輸入/輸出流
兩個重要的輸入/輸出流
- 一個iostream對象可以是資料的源或目的
- 兩個重要的I/O流類都是從iostream派生的,它們是fstream和stringstream。這些類繼承了前面描述的istream和ostream類的功能
fstream類
- fstream類支援磁盤檔案輸入和輸出
- 如果需要在同一個程式中從一個特定磁盤檔案讀并寫到該磁盤檔案,可以構造一個fstream對象
- 一個fstream對象是有兩個邏輯子流的單個流,兩個子流一個用于輸入,另一個用于輸出
stringstream類
- stringstream類支援面向字元串的輸入和輸出
- 可以用于對同一個字元串的内容交替讀寫,同樣是由兩個邏輯子流構成
異常處理
異常接口聲明
一個函數顯式聲明可能抛出的異常,有利于函數的調用者為異常處理做好準備
可以在函數的聲明中列出這個函數可能抛擲的所有異常類型
void fun() throw(A,B,C,D);
複制
若無異常接口聲明,則此函數可以抛擲任何類型的異常
不抛擲任何類型異常的函數聲明如下:
void fun() throw();
複制
#include <iostream>
using namespace std;
int divide(int x, int y) {
if (y == 0)
throw x;
return x / y;
}
int main() {
try {
cout << "5 / 2 = " << divide(5, 2) << endl;
cout << "8 / 0 = " << divide(8, 0) << endl;
cout << "7 / 1 = " << divide(7, 1) << endl;
} catch (int e) { //若改成float e就不會catch到
cout << e << " is divided by zero!" << endl;
}
cout << "That is ok." << endl;
return 0;
}
複制
自動的析構
找到一個比對的catch異常處理後
- 初始化異常參數
- 将從對應的try塊開始到異常被抛擲處之間構造(且尚未析構)的所有自動對象進行析構
從最後一個catch處理之後開始恢複執行
#include <iostream>
#include <string>
using namespace std;
class MyException {
public:
MyException(const string &message) : message(message) {}
~MyException() {}
const string &getMessage() const { return message; }
private:
string message;
};
class Demo {
public:
Demo() { cout << "Constructor of Demo" << endl; }
~Demo() { cout << "Destructor of Demo" << endl; }
};
void func() throw (MyException) {
Demo d;
cout << "Throw MyException in func()" << endl;
throw MyException("exception thrown by func()");
}
int main() {
cout << "In main function" << endl;
try {
func();
} catch (MyException& e) {
cout << "Caught an exception: " << e.getMessage() << endl;
}
cout << "Resume the execution of main()" << endl;
return 0;
}
/*
運作結果:
In main function
Constructor of Demo
Throw MyException in func()
Destructor of Demo
Caught an exception: exception thrown by func()
Resume the execution of main()
*/
複制