C++面向對象開發上
培養正規的、大氣的編程習慣
0. 面向對象三大特征 —— 封裝、繼承、多態
封裝
- 把客觀事物封裝成抽象的類,並且類可以把自己的數據和方法只讓可信的類或者對象操作,對不可信的進行信息隱藏。
繼承
- 基類(父類)——> 派生類(子類)
多態
- 多態,是以封裝和繼承為基礎,使得消息可以多種形式顯示。
0) C++ 多態分類及實現:
- 重載多態(Ad-hoc Polymorphism,編譯期):函數、運算符重載
- 子類型多態(Subtype Polymorphism,運行期):虛函數
- 參數多態性(Parametric Polymorphism,編譯期):類模板、函數模板
- 強制多態(Coercion Polymorphism,編譯期/運行期):基本類型轉換、自定義類型轉換
The Four Polymorphisms in C++
1) 靜態多態(編譯期/早綁定) 函數重載
class A
{
public:
void do(int a);
void do(int a, int b);
};
//參數多態性
template <typename T>
class Complex{
T re, im;
public:
Complex(T re, T im) {}
};
Complex<int>(5, 4);
Complex<float>(5.0, 4.0);
2) 動態多態(運行期期/晚綁定)虛函數
-
虛函數:
用 virtual 修飾成員函數(含基類的虛構函數),使其成為虛函數.
-
非虛函數:
- 普通(全局)函數(非類成員函數)
- 靜態函數(static)
- 構造函數(因為在調用構造函數時,虛表指針並沒有在對象的內存空間中,必須要構造函數調用完成后才會形成虛表指針)
- 內聯函數不能是表現多態性時的虛函數,解釋見:虛函數(virtual)可以是內聯函數(inline)嗎?
一、C++編程簡介
基礎知識
曾學過procedural language (C 語言最佳),知道如何對程序編譯、鏈接、執行。
變量(variables)
類型(types) : int, float, char, struct …
作用域(scope)
循環(loops) : while, for,
流程控制: if-else, switch-case
基於對象分類:
-
基於對象:一個class的編程 object based
-
面向對象:幾個class的編程 object oriented
class的經典分類:
- class without pointer members ——>e.g: complex 復數
- class with pointer members ——>e.g: string 字符串
class之間的關系:
- 繼承inheritance、
- 復合composition
- 委托delegation
C++書籍(STL是標准庫的前身)
基礎:Language
《C++Primer》
《C++programming Language》
提高:Standard Library
《Effective C++ Third Edition》及中文
《The C++ Standard Library》
《STL源碼剖析》
二、頭文件與類的聲明
//flie XXX.h
#ifndef __complex__
#define __complex__
#program once //編譯器宏
// class的布局:
class base; //前置聲明
class header{// class header
// class body
...
}
#endif //XXX.h end
// XXX.cpp
#include"XXX.h" //c
#include<cstdio> //C++
#include<iostream>
三、構造函數
(1)inline內聯函數:
特征(程序員期望能inline,編譯器決定是否inline)
- 相當於把內聯函數里面的內容寫在調用內聯函數處;
- 相當於不用執行進入函數的步驟,直接執行函數體;
- 相當於宏,卻比宏多了類型檢查,真正具有函數特性;
- 編譯器一般不內聯包含循環、遞歸、switch 等復雜操作的內聯函數;
- 在類聲明中定義的函數,除了虛函數的其他函數都會自動隱式地當成內聯函數。
//--------------------------聲明-----------------------------------------------
// 聲明1(加 inline,建議使用)
inline int functionName(int first, int second,...);
// 聲明2(不加 inline)
int functionName(int first, int second,...);’
//--------------------------定義-----------------------------------------------
// 類內定義並實現,隱式內聯
class A {
int doA() { return 0; } // 隱式內聯
}
// 類外定義,需要顯式內聯
class A {
int doA();
int functionName(int first, int second,...);
}
inline int A::doA() { return 0; } // 需要顯式內聯
inline int functionName(int first, int second,...) {/** ...**/};// 需要顯式內聯
編譯器對 inline 函數的處理步驟
- 將
inline函數體復制到inline函數調用點處;- 為所用
inline函數中的局部變量分配內存空間;- 將
inline函數的輸入參數和返回值映射到調用方法的局部變量空間中;- 如果
inline函數有多個返回點,將其轉變為inline函數代碼塊末尾的分支(使用GOTO)
優缺點 主要與宏定義比較
優點
- 內聯函數同宏函數一樣將在被調用處進行代碼展開,省去了參數壓棧、棧幀開辟與回收,結果返回等,從而提高程序運行速度。
- 內聯函數相比宏函數來說,在代碼展開時,會做安全檢查或自動類型轉換(同普通函數),而宏定義則不會。
- 在類中聲明同時定義的成員函數,自動轉化為內聯函數,因此內聯函數可以訪問類的成員變量,宏定義則不能。
- 內聯函數在運行時可調試,而宏定義不可以。
缺點
- 代碼膨脹。內聯是以代碼膨脹(復制)為代價,消除函數調用帶來的開銷。如果執行函數體內代碼的時間,相比於函數調用的開銷較大,那么效率的收獲會很少。另一方面,每一處內聯函數的調用都要復制代碼,將使程序的總代碼量增大,消耗更多的內存空間。
- inline 函數無法隨着函數庫升級而升級。inline函數的改變需要重新編譯,不像 non-inline 可以直接鏈接。
- 是否內聯,程序員不可控。內聯函數只是對編譯器的建議,是否對函數內聯,決定權在於編譯器。
(2)access level 訪問級別
public成員:可以被任意實體訪問
private成員:只允許被本類的成員函數訪問
protected成員:只允許被子類及本類的成員函數訪問(虛函數使用較多)
private:數據的部分用盡量用private
public:函數的部分,大部分用public
(3)構造函數
創建一個對象的時候,構造函數自動被調用,構造函數可設置默認參數,並設置參數初始化列表:
pair(const T1& a, const T2& b) : first(a), second(b) {}
參數initializition list和在body里對參數賦值的區別:
一個是參數初始化;
一個是賦值,是一個執行的過程,多了計算量;
創建一個對象,可以有參數,也可以無參數,也可動態創建:
class complex *p = new complex(4); //動態創建
class定義了多個構造函數,就是重載overloading
(4)friend 友元類和友元函數:
- 能訪問私有成員
- 破壞封裝性
- 友元關系不可傳遞
- 友元關系的單向性
- 友元聲明的形式及數量不受限制
- 相同class 的各objects 互friends (友元)
#include <iostream> class A { friend class B; // Friend Class }; class B { public: void showA(A& x) { std::cout << "A::a=" << x.a; } }; //OR class B; class A { public: void showB(B&); }; class B { friend void A::showB(B& x); // Friend function }; void A::showB(B& x) { std::cout << "B::b = " << x.b; }
item23. 寧以 non-member、non-friend 替換 member 函數(可增加封裝性、包裹彈性(packaging flexibility)、機能擴充性)
//member
class WebBrowser {
public:
void clearCache();
void clearHistory();
void removeCookies();
void clearEverything(); //調用上述的三個函數
};
//non-member
void clearBrowser (WebBrowser& wb)
{
wb.clearCache();
wb.clearHistory();
wb.removeCookies();
}
(5)static :
class Account {
public:
static double m_rate;
static void set_rate(const double& x) { m_rate = x; }
};
double Account::m_rate = 8.0; //static value
int main() {
Account::set_rate(5.0);
Account a;
a.set_rate(7.0);
}
利用static實現單例模式(Singleton)
// Meyers Singleton
class A {
public:
static A& getInstance();
setup() { ... }
A() = delete; // c++ 11
A(const A& rhs) = delete; // c++ 11
private:
A(); //before c++ 11
A(const A& rhs); //before c++ 11
};
A& A::getInstance()
{
static A a;
return a;
}
// Singleton
class A {
public:
static A& getInstance(){ return a;} //???? 猜測結果
static A& getInstance( return a; ); //???? ppt中
setup() { ... }
private:
A();
A(const A& rhs);
static A a;
};
//call 多線程中不安全.
A::getInstance().setup();
A& p = A::getInstance();
p.setup();
四、參數傳遞和返回值以及const
盡量使用傳引用的方式才傳遞參數,因為引用的底層是指針(C語言中指針作為參數傳遞類似),傳遞的數據大小為4個字節,速度會很快。同樣,值的返回也盡量返回引用(如果可以的話)。
- 數據放在
private里- 參數用
reference,是否用const- 在類的
body里的函數是否加const- 構造函數的
initial list- return by reference,不能為local object.
指針&引用:
- 都是地址的概念;指針是一個實體,指向一塊內存,它的內容是所指內存的地址;引用則是某塊內存的別名。
- 使用sizeof指針本身大小一般是(4),而引用則是被引用對象的大小;
- 引用不能為空,指針可以為空;指針可以被初始化為NULL,而引用必須被初始化且必須是一個已有對象的引用,之后不可變;指針可變;引用“從一而終”,指針可以“見異思遷”;
- 作為參數傳遞時,指針需要被解引用才可以對對象進行操作,而直接對引用的修改都會改變引用所指向的對象;
- 引用沒有
const,指針有const,const的指針不可變;
具體指沒有int& const a這種形式,而const int& a是有的,前者指引用本身即別名不可以改變,這是當然的,所以不需要這種形式,后者指引用所指的值不可以改變)- 指針在使用中可以指向其它對象,但是引用只能是一個對象的引用,不能 被改變;
- 指針可以有多級指針(**p),而引用至於一級;
- 指針和引用使用自增(++)運算符的意義不一樣;
- 如果返回動態內存分配的對象或者內存,必須使用指針,引用可能引起內存泄露。
- 引用是類型安全的,而指針不是 (引用比指針多了類型檢查)
const作用
當一個方法不會對數據進行修改時,盡量將方法指定為const。細分頂層const、底層const
"effective c++"第三條講到: 只需要判斷const是在 * 的左邊還是右邊即可。左邊則是修飾被指物,即被指物是常量,不可以修改它的值;右邊則是修飾指針,即指針是常量,不可以修改它的指向;在左右兩邊,則被指物和指針都是常量,都不可以修改。
- 修飾變量,說明該變量不可以被改變;
- 修飾指針,分為指向常量的指針和指針常量;
- 常量引用,經常用於形參類型,即避免了拷貝,又避免了函數對值的修改;
- 修飾成員函數,說明該成員函數內不能修改成員變量。
//const 使用
// 類
class A
{
private:
const int a; // 常對象成員,只能在初始化列表賦值
public:
// 構造函數
A() : a(0) { }; // 初始化列表
A(int x) : a(x) { }; // 初始化列表
// const可用於對重載函數的區分
int getValue(); // 普通成員函數
int getValue() const; // 常成員函數,不得修改類中的任何數據成員的值
// 可以供常對象使用,否則報錯,調用有可能被改變值,編譯器報錯。
};
void function()
{
// 對象
A b; // 普通對象,可以調用全部成員函數
const A a; // 常對象,只能調用常成員函數、更新常成員變量
const A *p = &a; // 常指針
const A &q = a; // 常引用
// 指針
char greeting[] = "Hello";
char* p1 = greeting; // 指針變量,指向字符數組變量
const char* p2 = greeting; // 指針變量,指向字符數組常量
char* const p3 = greeting; // 常指針,指向字符數組變量
const char* const p4 = greeting; // 常指針,指向字符數組常量
}
// 函數
void function1(const int Var); // 傳遞過來的參數在函數內不可變
void function2(const char* Var); // 參數指針所指內容為常量
void function3(char* const Var); // 參數指針為常指針
void function4(const int& Var); // 引用參數在函數內為常量
// 函數返回值
const int function5(); // 返回一個常數
const int* function6(); // 返回一個指向常量的指針變量,使用:const int *p = function6();
int* const function7(); // 返回一個指向變量的常指針,使用:int* const p = function7();
五、操作符重載與臨時對象
1、成員函數帶有隱藏的參數
this,誰調用這個函數誰就是 this.臨時對象 complex() ->typename();
2、在類外Complex Complex::operator+=(complex &c2) 這個是成員函數operator+=的實現,所以需要 Complex:: 具有this指針。例如:
返回& 為了鏈式連續操作,在stream流的類中非常常見
運算符重載——成員函數
inline complex&
__doapl(complex* ths, const complex& r)
{ //第一參數將會被改動 //第二參數不會被改動
ths->re += r.re;
ths->im += r.im;
return *ths;
}
inline complex&
class complex::operator += (const complex& r) //成員函數 this
{
return __doapl (this, r);
}
運算符重載——全局函數
//而下面屬於運算符重載,不是成員函數的時候,就沒有Complex:: 。
inline complex
operator - (const complex& x, double y)
{
return complex (real (x) - y, imag (x));
}
六、總結
1.使用初始化列表,構造函數中,Complex(double r = 0, double i = 0) : re(r), im(i) {}
2.成員函數是否指明const,若不修改數據,則盡可能指明const。const對象無法調用非const成員方法。
3.參數傳遞和結果返回,盡量使用傳遞和返回引用。效率更高(不要返回局部變量的引用)。
4.數據private,對外接口public,內部方法private(注意封裝來隱藏數據)。
5.同一個類衍生出的對象之間都互為友元,可以直接調用對方的私有變量。
6.操作符重載都是作用在左邊的變量上的(即調用該操作符函數的對象),注意連續調用的情況,操作符重載可以是成員方法,也可全局方法。
7.成員方法在類定義中實現,默認聲明為inline方法(視編譯器的決定)。若為全局函數,需加inline關鍵字,讓編譯器盡量的將其變為inline函數。
七、三大函數:拷貝構造,拷貝賦值,析構
構造函數(可以重載,類內創建private,C++11有delete)
拷貝構造函數
拷貝賦值函數
析構函數
常量成員函數 const修飾成員函數,防止常量對象進行調用出錯。
class with pointer members 必須有copy ctor 和copy op=
一定要在operator= 中檢查是否self assignment
inline String& String::operator=(const String& str) //后面有String的整體代碼
{
if (this == &str)
return *this;
delete[] m_data;
m_data = new char[ strlen(str.m_data) + 1 ];
strcpy(m_data, str.m_data);
return *this;
}
#include <iostream.h>
//定義成員方法,獲取m_data指針,不修改數據,加上const
inline char * String::get_c_str() const {
return this->m_data;
}
//重載操作符<<
ostream& operator<<(ostream& os, const String& str)
{
os << str.get_c_str();
return os;
}
——》類含指針,就需要 拷貝構造、 拷貝賦值函數
淺拷貝——》拷貝構造函數 ,淺拷貝的影響:
1、造成內存泄漏;
2、造成有兩個指針 指向同一塊內存
深拷貝——》拷貝賦值函數
步驟:delete ;new; strcpy;
class里面有默認的拷貝構造和拷貝賦值函數。如果自己不定義一個拷貝構造函數,在調用拷貝構造函數的時候,就會調用默認的淺拷貝構造函數,就會造成問題,所以一定要自己定義拷貝構造函數——深拷貝。
八、堆,棧與內存管理
1、static local objects的生命周期
- static的生命周期 :object的對象在scope結束以后仍然存在,直到整個程序結束;
- 非static 的生命周期:object的對象在在scope結束以后就結束了。
- global objects的生命周期:對象 objects 生命結束,就是什么時候析構函數被調用。
2、new——》operator new。
new動態創建對象,分三步:
- 先轉化為operator new 函數,申請分配內存。
- 做類型轉化。
- 調用構造函數
delete ——》operator delete。刪除對象,分兩步:
- 先調用析構函數,
- 再調用operator delete函數。
3、帶中括號[ ]的new[ ]叫做array new,帶中括號[ ]的delete[ ] 叫做array delete。
動態分配所得到的數組array:complex *p = new complex[3];
new [] ——》delete[] ——》表示調用幾次析構函數
new 字符串 ——》delete 指針delete[n] :array new一定要調用array delete,delete[n]會調用n次析構函數,而delete僅調用一次。
Stack,是存在於某作用域(scope) 的一塊內存空間(memory space)。例如當你調用函數,函數本身即會形成一個stack 用來放置它所接收的參數,以及返回地址。在函數本體(function body) 內聲明的任何變量,其所使用的內存塊都取自上述stack。
Heap,是指由操作系統提供的一塊global 內存空間,程序可動態分配(dynamic allocated) 從某中獲得若干區塊(blocks),記得釋放,盡量用RAII管理。
new和delete關鍵字的工作流程
{
class Complex* p = new Complex;
...
delete p; //若未刪除,內存泄露,再也無法刪除了,p退出作用域,作用域外無法看到p。
//編譯器實現如下
Complex *pc;
void* mem = operator new( sizeof(Complex) ); //分配內存
pc = static_cast<Complex*>(mem); //轉型,static_cast多了類型檢查
pc->Complex::Complex(1,2); //構造函數
Complex::~Complex(pc); // 析構函數
operator delete(pc); // 釋放內存
}
{
String* ps = new String("Hello");
//編譯器實現如下
String* ps;
void* mem = operator new( sizeof(String) ); //分配內存
ps = static_cast<String*>(mem); //轉型
ps->String::String("Hello"); //構
}
VC當中的內存分配
注:僅限new的情況下。
Debug(左)Release(右)模式下:

淺綠色:Complex對象所占實際空間,大小為8bytes。
上下磚紅色:各4bytes,一共8bytes。是cookie,用來保存總分配內存大小,以及標志是給出去還是收回來。例如00000041,該數為16進制,4表示64,即總分配內存大小為64,1表示給出去(0表示收回來)。
灰色:Debug模式下使用的額外空間,前面32bytes,后面1bytes,一共36bytes。
深綠色:內存分配大小必須是16的倍數(這樣磚紅色部分里的數字最后都是0,可以用來借位表示給出去還是收回來),所以用了12byte的填充(padding)。
同樣,String對象的空間分配,如圖:(左Debug,右Release)

Debug(左)Release(右)模式下,數組空間的分配:

灰色:即3個Complex對象的大小,每個是8bytes,一共24bytes。
深綠色:填充為16的倍數。
前后白色:51表示80bytes,“給出去”。
黃色:Debug模式額外占用空間。
中間白色:用一個整數表示數組中對象個數。
九、復習String類的實現過程
class String
{
public:
String(const char* cstr = 0)
{
if (cstr) {
m_data = new char[strlen(cstr)+1];
strcpy(m_data, cstr);
}else { // 未指定初值
m_data = new char[1];
*m_data = '\0';
}
}
String(const String& str)
{
m_data = new char[ strlen(str.m_data) + 1 ];
strcpy(m_data, str.m_data);
}
String& operator=(const String& str);
~String()
{
delete[] m_data;
}
char* get_c_str() const { return m_data; }
private:
char* m_data;
};
inline String& String::operator=(const String& str)
{
if (this == &str)
return *this;
delete[] m_data;
m_data = new char[ strlen(str.m_data) + 1 ];
strcpy(m_data, str.m_data);
return *this;
}
十、擴展補充:類模板,函數模板,及其他
//class template,類模板
template<typename T>
class complex
{ public:
complex (T r = 0, T i = 0): re (r), im (i){ }
complex& operator += (const complex&);
T real () const { return re; }
T imag () const { return im; }
private:
T re, im;
friend complex& __doapl (complex*, const complex&);
};
//函數模板
template <class T>
inline const T& min(const T& a, const T& b)
{
return b < a ? b : a;
}
class stone
{ public:
stone(int w, int h, int we)
: _w(w), _h(h), _weight(we){ }
bool operator< (const stone& rhs) const
{ return _weight < rhs._weight; }
private:
int _w, _h, _weight;
};
//引數推導的結果,T 為stone,於是調用stone::operator<
//調用函數,那么就會傳實參,編譯器就會進行實參推導。
stone r1(2,3), r2(3,3), r3;
r3 = min(r1, r2);
(1)static :靜態數據 屬於所有對象(類)。
靜態函數 沒有this pointer,而非靜態函數有 this pointer,可以用this去取數據,靜態函數要處理數據只能處理靜態數據。靜態數據一定要在class外面定義。
(2)template:類模板 函數模板
(3)inline namespace:(C++11)
namespace Program {
namespace Version1 {
int getVersion() { return 1; }
bool isFirstVersion() { return true; }
}
inline namespace Version2 {//inline
int getVersion() { return 2; }
}
}
int version {Program::getVersion()}; // Uses getVersion() from Version2
int oldVersion {Program::Version1::getVersion()}; // Uses getVersion() from Version1
bool firstVersion {Program::isFirstVersion()}; // Does not compile when Version2 is added
十 一、組合 has-a 與繼承 is-a
組合表示has-a。即A類里有B類的對象(非指針)實心,實在成員對象(UML)。

由內而外template <class T>
struct Itr {
T* cur;
T* first;
T* last;
T** node;
//Sizeof : 4 * 4
};
template <class T>
class deque {
protected:
Itr<T> start;
Itr<T> finish;
T** map;
unsigned int map_size;
//Sizeof : 16 * 2 + 4 + 4
};
//適配器模式(queue 適配器,deque成熟的類)
template <class T>
class queue {
protected:
deque<T> c;
//Sizeof : 40
};
//組合
//構造順序: Itr->deque->queue 由內到外
//析構順序: queue->deque->Itr 由外到內
//queue內存大小:queue的內存大小+deque的內存大小
(2)委托delegation,即composition by reference:在body中聲明一個帶指針的另一個類 composition by reference 生命時間: classA 用一個指針指向classB,需要的時候才調用classB,而不是一直擁有classB。叫做“Copy on write”

指針有點虛(UML)
//委托delegation
// file String.hpp
class StringRep;
class String {//Handle,穩定
private:
StringRep* rep; // pimpl ,指針指向實現的類(Handle/Body)
};
// file String.cpp
#include "String.hpp"
namespace {
class StringRep {//Body ,有彈性變化
friend class String; //friend
StringRep(const char* s);
~StringRep();
int count;
char* rep;
};
}
String::String(){ ... }
(3)繼承Inheritance:(三種繼承方式:public protected private)is-a,繼承主要搭配虛函數來使用函數的繼承:指的是繼承函數的調用權,子類可以調用父類的函數,如B繼承A,則說明B是A的一種。

//繼承中的 構造函數與析構調用順序
//構造順序: 父類->子類 由內到外
//析構順序: 子類->父類 由外到內
//繼承+組合模式 構造函數與析構調用順序
Derived::Derived(...):Base(),Component() { ... };
Derived::~Derived(...){ ... ~Component(), ~Base() };
繼承+組合模式
//繼承+組合模式 構造函數與析構調用順序
Derived::Derived(...):Base(),Component() { ... };
Derived::~Derived(...){ ... ~Component(), ~Base() };
十二、虛函數與多態
//虛函數 你期待derived class的行為
non-virtual 函數:不重新定義(override, 覆寫它).
virtual 函數:重新定義(override, 覆寫) 它,且你對它已有默認定義。
pure virtual 函數:必須重新定義(override 覆寫)它,你對它沒有默認定義,不能直接實例化對象。
(1)虛函數:virtual 純虛函數:一定要重新定義。
(A)Inheritance + composition下的構造和析構
(B)delegation + Inheritance ——》 功能最強大的一種
十三、設計模式
建議看李建忠老師視頻23種C++設計模式
圖形展示




Template Method模式
步驟:
1.在父類CDocument中,實現共同的方法,例如OpenFile、CloseFile等。
2.CDocument中,將讀文件內容的方法Serialize設計為虛函數或純虛函數。
3.CMyDoc繼承CDocument,實現Serialize()。
4.使用子類CMyDoc調用父類方法OnFileOpen(),按圖中灰色曲線的順序來調用內部函數。
這樣就實現了關鍵功能的延遲實現,實現應用與架構分離(Application framework)這就是典型的Template Method。
為什么會有灰色曲線的調用過程:
1.當子類myDoc調用OnFileOpen()的時候,實際上對於編譯器是CDocument::OnFileOpen(&myDoc);因為誰調用,this指針就指向誰,所以調用這個函數,myDoc的地址被傳進去了。
2.當OnFileOpen()函數運行到Serilize()的時候,實際上是運行的this->Serialize();由於this指向的是myDoc,所以調用的是子類的Serilize()函數。
//框架開發人員
class CDocument {
public:
void OnFileOpen() {
cout << "dialog..." << endl;
cout << "check file status..." << endl;
cout << "open file..." << endl;
Serialize(); //子類再實現
cout << "close file..." << endl;
cout << "update status..." << endl;
}
//父類的虛函數,當然這里是純虛函數也是可以的,virtual void Serialize() = 0
virtual void Serialize() {}
};
//應用開發人員
class CMyDoc :public CDocument {
public:
//這里實現了父類的虛函數Serialize()
virtual void Serialize() {
cout << "MyDoc Serialize..." << endl;
}
};
//call
#include "CDocument.h"
int main() {
CMyDoc mc;
mc.OnFileOpen();
return 0;
}
output
dialog...
check file status...
open file...
MyDoc Serialize...
close file...
update status...
//程序庫開發人員
class Library
{
public:
//穩定 template method
void Run()
{
Step1();
Step2();//支持變化 ==> 虛函數的多態調用
Step3();
Step4(); //支持變化 ==> 虛函數的多態調用
Step5();
}
virtual ~Library() {}
protected:
void Step1(){}//穩定
void Step3(){}//穩定
void Step5(){}//穩定
virtual bool Step2() = 0; //變化
virtual void Step4() = 0; //變化
};
//應用程序開發人員
class Application : public Library
{
protected:
virtual bool Step2(){ //... 子類重寫實現
return true;
}
virtual void Step4(){ //... 子類重寫實現
}
};
int main()
{
Library *pLib = new Application();
pLib->Run();
delete pLib;
}
Observer模式
我們的數據設計在類Subject中,窗口(觀察者)設計為Observer,這是一個父類,可以被繼承(即可以支持派生出不同類型的觀察者)。

用如下代碼來實現:
//數據類
class Subject {
int m_value;
vector<Observer*> m_views;
public:
void attach(Observer* obs) { m_views.push_back(obs);
}
void set_value(int value) {
m_value = value;
notify();
}
void notify() {
for (int i = 0;i < m_views.size();++i) {
m_views[i]->update(this, m_value);
}
}
};
//觀察者基類
class Observer {
public:
//純虛函數,提供給不同的實際觀察者類來實現不同的特性
virtual void update(Subject*, int value) = 0;
};
用圖形來描述:

組合模式
在計算機文件系統中,有文件夾的概念,文件夾里面既可以放入文件也可以放入文件夾,但是文件中卻不能放入任何東西。文件夾和文件構成了一種遞歸結構和容器結構。
雖然文件夾和文件是不同的對象,但是他們都可以被放入到文件夾里,所以一定意義上,文件夾和文件又可以看作是同一種類型的對象,所以我們可以把文件夾和文件統稱為目錄條目(directory entry)。在這個視角下,文件和文件夾是同一種對象。
所以,我們可以將文件夾和文件都看作是目錄的條目,將容器和內容作為同一種東西看待,可以方便我們遞歸的處理問題,在容器中既可以放入容器,又可以放入內容,然后在小容器中,又可以繼續放入容器和內容,這樣就構成了容器結構和遞歸結構。
這就引出了composite模式,也就是組合模式,組合模式就是用於創造出這樣的容器結構的。是容器和內容具有一致性,可以進行遞歸操作。
圖中Primitive代表基本的東西,即文件。Composite代表合成物,即文件夾。Component表示目錄條目。
Primitive和Composite都是一種Component,而Composite中可以存放其他的Composite和Primitive,所以Composite中的Vector存放的類型時Component指針,也就包含了Primitive和Composite兩種對象的指針。
代碼框架如下:
//一個比較抽象的類,相當於目錄條目
class Component {
int value;
public:
Component(int val) :value(val){}
virtual void add(Component*) {}
};
//相當於 文件類
class Primitive {
public:
Primitive(int val):Component(val){}
};
//相當於 文件夾類
class Composite {
vector<Component*> c;
public:
Composite(int val) :Component(val){}
void add(Component* elem) {
c.push_back(elem);
}
};
Prototype模式
設計應用架構時,並不知道以后實現的子類名稱,但有要提供給Client調用子類的功能怎么辦?
例如十年前設計的架構,子類在十年后繼承父類並實現功能。Client只能調用架構中的父類,如何通過父類調用到不知道名字的子類對象。使用Prototype模式:
#include <iostream>
#include <vector>
using namespace std;
//可能是十年前寫的框架,我們不知道子類的名字,但又希望通過該基類來產生子類對象
class Prototype {
//用於保存子類對象的指針(讓子類自己上報)
static vector<Prototype *> vec;
public:
//純虛函數clone,讓以后繼承的子類來實現,也是獲取子類對象的關鍵
virtual Prototype* clone() = 0;
//子類上報自己模板用的方法
static void addPrototype(Prototype* se) {
vec.push_back(se);
}
//利用該基類在vec中查找子類模板,並且通過模板來克隆更多的子類對象
static Prototype* findAndClone(int idx) {
return vec[idx]->clone();
}
//子類實現自己操作的函數,hello()只是個例子
virtual void hello() const = 0;
};
//定義靜態vector,很重要,class定義中只是聲明
vector<Prototype *> Prototype::vec;
//十年后實現的子類,繼承了Prototype
class ConcreatePrototype : public Prototype{
public:
//用於在Prototype.findAndClone()中克隆子類對象用
Prototype * clone() {
//使用另一個構造函數,為了區分創建靜態對象的構造函數,添加了一個無用的int參數
return new ConcreatePrototype(1);
}
//子類實現的具體操作
void hello() const {
cout << "hello" << endl;
}
private:
//靜態屬性,自己創建自己,並上報給父類Prototype
static ConcreatePrototype se;
//上報靜態屬性給父類
ConcreatePrototype() {
addPrototype(this);
}
//clone時用的構造方法,參數a無用,只是用來區分兩個構造方法
ConcreatePrototype(int a) {}
};
//定義靜態屬性,很重要,有了這句,才會創建靜態子類對象se
ConcreatePrototype ConcreatePrototype::se;
步驟:
1.子類繼承Prototype父類,定義靜態屬性的時候,自己創建一個自己的對象,此時調用的是無參數的構造函數。並將創建好的自己的指針通過addPrototype(this)上傳給基類的vector容器保存。
2.基類定義好的純虛函數clone(),由子類實現,並在其中通過另一個構造函數產生對象並返回。
3.在Client端,使用基類的findAndClone(),獲取vector中的子類對象模板的指針,來調用子類對象的clone功能,返回一個新的子類對象,調用多次則可創建多個對象供用戶使用。
4.創建出的子類對象可以調用在子類中實現的hello()方法,進行想要的操作。
Prototype* p = Prototype::findAndClone(0);
p->hello();
附錄*、overload override overwrite的區別
深入請參考C++ primer
1. Overload(重載)
在同一作用域中,定義了多個同名不同參數(類型或者個數)函數。特征:
(1)同一作用域;
(2)函數名字相同;
(3)參數不同,底層const算重載;
(4)virtual 關鍵字可有可無。
2. Override(覆蓋)
用來實現C++多態性的,子類改寫父類的virtual函數。
通過override顯示聲明,使得程序員的意圖更加清晰的同時讓編譯器可以為我們發現一些錯誤。
(1)不同的范圍(分別位於派生類與基類);
(2)函數名字相同;
(3)參數列表完全相同;
(4)基類函數必須有virtual 關鍵字。
通常情況下,覆蓋函數必須與虛函數的參數類型及返回類型相同:例外是,當類的虛函數返回類型是類本身的指針或引用時
3. Overwrite(改寫)
基類與派生類之間同名函數重載:派生類重寫函數名屏蔽了基類中的同名函數。
解決辦法:在派生類中通過using為父類函數成員提供聲明
(1)若派生類的函數與基類的函數同名,不同參數。基類的函數將被隱藏。
(2)若派生類的函數與基類的函數同名,也同參數,但基類函數無virtual關鍵字。基類函數被隱藏。
class Derived : public Base {
public:
using Base::print;//解決辦法
void print() {
cout << "print() in Derived." << endl;
}
};
【1】大綱是Gayhub上熱心網友的,進行了部分補充,非常感謝共享 。
因個人水平有限,歡迎大家指導。 yzhu798#gmail.com 。2020.03.07

