文章目錄
面向對象基礎2
帶有指針的class。即class成員中有指針數據成員。
1.Big Three
構造函數
由於成員有指針,在構造該類對象的時候,需要自己初始化這個指針。
析構函數
在析構函數中,需要用戶自己維護這個指針成員數據,即確保對象銷燬(析構)時,指針成員指向的空間也被回收。
// 析構函數
inline
String::~String()
{
// delete的時候是delete [] ,這個括號不能省略,告訴編譯器,這個m_data是個數組,通常和new []成對出現
delete[] m_data;
}
inline
String::String(const char* cstr = 0)
{
// 默認值是0也是空指針,表示空字符串。
if (cstr) {
m_data = new char[strlen(cstr)+1];
strcpy(m_data, cstr);
}
else { // 未指定初值
m_data = new char[1];
*m_data = '\0';
}
}
拷貝構造(copy constructor)
下圖所示的是沒有拷貝構造時候的系統默認的“淺拷貝”模型。首先創建對象a、b,他們內部的指針分別指向了兩個string。當b=a;
代碼執行時,c++默認將a中的數據成員拷貝給b,所以他們的指針成員變量的值就是一樣的了,即指向同一個string。這是出現兩個問題
- 原來b的那個空間(“world”)內存泄漏了。
- 現在a和b指向了同一塊內存(“hello”),導致a和b糾纏,改一個,另一個也會改,我們需要的是兩個獨立但內容相同的對象。
“深拷貝”:在構造函數中,自己完成值的拷貝過程。
inline
String::String(const String& str)
{
// new在堆區申請了另一塊空間,然後將str中的字符拷貝到這個空間中
// 這樣兩個對象就不是在操作同一塊內存了。但值仍然相同。
m_data = new char[ strlen(str.m_data) + 1 ];
strcpy(m_data, str.m_data);
}
// 這裏有個問題,就是原來該string對象的m_data的內存區域並沒有釋放。還是會泄漏。
拷貝賦值(copy operator=)
與拷貝構造一樣,在賦值時,構建新對象,進行“深拷貝”。
inline
String& String::operator=(const String& str)
{
// 自我檢測,應對:a=a的情況
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;
}
// 使用
{
String s1("hello ");
String s2(s1);
s2 = s1;
}
這裏的自我檢測部分一定要注意!!!
若沒有自我檢測,則當有代碼a=a;
執行時,會訪問錯誤的內存空間!!
完整案例
#pragma once
#include<cstring>
#include<iostream>
using namespace std;
namespace MyString {
class String {
public:
String(const char* cstr);
String(const String& str);
String& String::operator=(const String& str);
~String();
private:
char* m_data;
};
inline
String::String(const String& str)
{
cout << "copy constructor..." << endl;
// new在堆區申請了另一塊空間,然後將str中的字符拷貝到這個空間中
// 這樣兩個對象就不是在操作同一塊內存了。但值仍然相同。
m_data = new char[strlen(str.m_data) + 1];
strcpy_s(m_data, strlen(str.m_data) + 1,str.m_data);
}
inline
String& String::operator=(const String& str)
{
cout << "copy op=..." << endl;
// 自我檢測,應對:a=a的情況
if (this == &str)
return *this;
// 刪除原來的空間
delete[] m_data;
// 創建新空間
m_data = new char[strlen(str.m_data) + 1];
// 拷貝內容
strcpy_s(m_data, strlen(str.m_data) + 1, str.m_data);
return *this;
}
inline
String::~String()
{
// delete的時候是delete [] ,這個括號不能省略,告訴編譯器,這個m_data是個數組,通常和new []成對出現
delete[] m_data;
}
inline
String::String(const char* cstr = 0)
{
cout << "normal constructor..." << endl;
// 默認值是0也是空指針,表示空字符串。
if (cstr) {
m_data = new char[strlen(cstr) + 1];
strcpy_s(m_data, strlen(cstr) + 1, cstr);
}
else { // 未指定初值
m_data = new char[1];
*m_data = '\0';
}
}
void test() {
String a("hello"); // normal constructor...
String b(a); // copy constructor...
String c = b; // copy constructor...
c = a; // copy op=...
}
}
2.stack and heap
Stack
Stack是存在於某作用域 (scope) 的一塊內存空間 (memory space)。例如當你調用函數,函數本身即會形成一個 stack 用來放置它所接收的參數,以及返回地址。在函數本體 (function body) 內聲明的任何變量,其所使用的內存塊都取自上述 stack。
Heap
Heap,或謂 system heap,是指由操作系統提供的一塊 global 內存空間,程序可動態分配 (dynamic allocated) 從某中獲得若干區塊 (blocks)。
class Complex { … };
...
{
Complex c1(1,2); // c1 所佔用的空間來自 stack
Complex* p = new Complex(3);
// Complex(3) 是個臨時對象,其所佔用的空間乃是以 new 自 heap 動態分配而得,並由 p 指向。
// 這裏的p指針變量任然是stack中的local變量
}
3.對象的生命週期
stack object
class Complex { ... };
...
{
Complex c1(1,2);
}
c1 便是所謂 stack object,其生命在作用域 (scope) 結束之際結束。這種作用域內的 object,又稱為 auto object,因為它會被「自動」清理(作用域結束時,自動調用析構函數)。
static local objects
class Complex { … };
...
{
static Complex c2(1,2);
}
c2 便是所謂 static object,其生命在作用域 (scope) 結束之後仍然存在,直到整個程序結束。之前也提過,static的變量是存在全局數據區的,會一致存在在整個代碼執行期間。
global object
class Complex { … };
...
Complex c3(1,2);
int main()
{
...
}
c3 便是所謂 global object,其生命在整個程序結束之後才結束。你也可以把它視為一種 static object,其作用域是「整個程序」。與static變量一樣,全局變量global var也是存在於全局數據區的。
C++程序的內存格局通常分爲四個區:全局數據區(data area),代碼區(code area),棧區(stack area),堆區(heap area)(即自由存儲區)。
heap objects
class Complex { … };
...
{
Complex* p = new Complex;
...
delete p;
}
P 所指的便是 heap object,其生命在它被 deleted 之際結束。所以若不手動delete該內存區域,p 所指的 heap object 仍然存在,但指針 p 的生命卻結束了,作用域之外再也看不到 p(也就沒機會 delete p),也就造成了內存泄漏。
4.new and delete
new的內部機制
Complex* pc = new Complex(1,2);
- 分配內存:operator new函數,內部調用c的malloc函數。
- 類型轉換:malloc返回的是void類型的指針,需要轉換到目標類型。
pc = static_cast<target_type>(p)
- 調用構造函數:
pc->Complex::Complex(1,2);
這裏默認傳入了當前對象,即指針pc。
delete的內部機制
String * ps = new String("Hello");
...
delete ps;
- 先調用析構:String::~String(ps);這一步的析構是要求用戶自定義實現的,銷燬String內的指針開闢的空間,即用戶自己申請的資源要在這一步銷燬。
- 再調用operator delete(ps)函數:這一步是編譯器自己執行的,用來銷燬當前對象本身,即當前String對象ps。內部調用的是C的free(ps)函數。
- 這裏需要有個概念,就是帶有指針的對象它實際上是包含兩部分的,其一是對象本身。其二是對象的指針成員指向的用戶自己開闢的額外空間,這部分用戶自己維護,所以要在析構中自己回收。
5.動態內存分配
new的時候申請了多少內存?
- debug模式和release模式不同,debug模式會額外申請內存。
- 同時對象只存儲其數據區,函數是共用的,不會每個對象都額外拷貝一份函數代碼。
- 分配的內存首尾會分配一個標誌,cookie。它用來紀錄整個分配空間的大小,以及分配狀態。
- 又分配的內存需要時16的倍數,若不夠,需要填充:padding。
動態分配array
,即 new []
- 連續型內存分配。首先數組就是空間線性分配的。(相對的就是鏈式存儲)
- array new需要搭配array delete。
若不delete [],而只是delete,那麼它只會銷燬該數組,但是該數組中的每個元素(對象)指向的堆空間不會被回收。
6.static
C中的static var
- 普通變量存在於棧空間,作用域結束後,自動回收該棧空間,即出入棧操作。
- 靜態局部變量使用static修飾符定義,靜態局部變量存儲於進程的全局數據區,作用域結束,該內存區域也任然存在。
- 全局變量定義在函數體外部,在全局數據區分配存儲空間。與static的local 變量類似。
C中的static func
在函數的返回類型前加上static,就是靜態函數。
- 靜態函數只能在聲明它的文件中可見,其他文件不能引用該函數
- 不同的文件可以使用相同名字的靜態函數,互不影響
- 即將函數隱藏在本文件中,不得外部訪問。
c++中的static
類class的static數據成員變量:該數據成員就是類內的靜態數據成員
- 靜態數據成員存儲在全局數據區,靜態數據成員在定義時分配存儲空間,所以不能在類聲明中定義。
- 它屬於類,而不單獨屬於類的任何一個object
- 靜態數據成員的拷貝只有一個,且對該類的所有對象可見。(而對於非靜態數據成員,每個對象都有自己的一份拷貝。)
- 靜態數據成員的初始化格式:
<數據類型><類名>::<靜態數據成員名>=<值>
,必須在聲明class的時候爲靜態數據成員初始化。 - 類的靜態數據成員有兩種訪問方式:
<類對象名>.<靜態數據成員名> 或 <類類型名>::<靜態數據成員名>
類class的static成員函數:靜態成員函數屬於整個類,而不是某一個對象
- 靜態成員函數沒有this指針,它無法訪問屬於類對象的非靜態數據成員,也無法訪問非靜態成員函數,它只能調用其餘的靜態成員函數。
- 非靜態成員函數可以任意地訪問靜態成員函數和靜態數據成員。(類的公共屬性(靜態數據成員和函數成員)不能訪問具體的某個對象的屬性,但對象個體可以訪問該class的共同的公共屬性)
-
c1.real,c2.real調用的是同一個Complex::real,但是通過傳入的調用者的指針(this)不同,實現了該函數訪問不同的對象的數據。
-
static的data member和static的member function,以及所有的member functions都只有一份,但member function會根據傳入的this指針訪問不同對象的數據,但,static修飾的data member和function
沒有this指針
,它屬於整個class
,不能處理到某個確定的object。 -
static成員變量必須在類外部初始化。調用的時候既可以通過object調用,也可以通過class name調用。
class Account { public: static double m_rate; static void set_rate(const double& x) { m_rate = x; } }; double Account::m_rate = 8.0; int main() { Account::set_rate(5.0); Account a; a.set_rate(7.0); }
通過static實現單例模式
class A {
public:
static A& getInstance();
setup() { ... }
private:
A();
A(const A& rhs);
...
};
A& A::getInstance()
{
static A a;
return a;
}
7.template 模板
類模板
template<typename T> class cla_name{}
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&);
};
// 使用
{
complex<double> c1(2.5,1.5);
complex<int> c2(2,6);
...
}
模板類會根據實際類型,爲每個類型都編譯出一份代碼,這就是所謂“代碼膨脹”。所以上例中,如果有static數據的話,complex<double>和complex<int>將會有兩份static,而不是1份。
若是Java,靜態數據就只會有1份。
函數模板function template
與類模板最大的區別是,函數模板不用顯式規定模板類型,它會根據傳入參數的類型自動推斷。
class stone
{
public:
stone(int w, int h, int we) : _w(w), _h(h), _weight(we){ }
bool operator< (const stone& rhs) const {
// 這裏需要T的實參類設計者實現<的操作符重載操作
return _weight < rhs._weight;
}
private:
int _w, _h, _weight;
};
// 定義
template <class T>
inline const T& min(const T& a, const T& b)
{
return b < a ? b : a;
}
// 使用
{
stone r1(2,3), r2(3,3), r3;
r3 = min(r1, r2);
stone r1(2,3), r2(3,3), r3;
r3 = min(r1, r2);
}
8.explicit
防止隱式轉換類型而構造對象
。
explicit關鍵字只需用於類內的單參數構造函數前面。由於無參數的構造函數和多參數的構造函數總是顯示調用,這種情況在構造函數前加explicit無意義。
google的c++規範中提到explicit的優點是可以避免不合時宜的類型變換,缺點無。所以google約定所有單參數的構造函數都必須是顯示的,只有極少數情況下拷貝構造函數可以不聲明稱explicit。例如作爲其他類的透明包裝器的類。
ref:https://www.cnblogs.com/rednodel/p/9299251.html
class CxString // 沒有使用explicit關鍵字的類聲明, 即默認爲隱式聲明
{
public:
char *_pstr;
int _size;
CxString(int size)
{
_size = size; // string的預設大小
_pstr = malloc(size + 1); // 分配string的內存
memset(_pstr, 0, size + 1);
}
CxString(const char *p)
{
int size = strlen(p);
_pstr = malloc(size + 1); // 分配string的內存
strcpy(_pstr, p); // 複製字符串
_size = strlen(_pstr);
}
// 析構函數這裏不討論, 省略...
};
// 下面是調用:
CxString string1(24); // 這樣是OK的, 爲CxString預分配24字節的大小的內存
CxString string2 = 10; // 這樣是OK的, 爲CxString預分配10字節的大小的內存
CxString string3; // 這樣是不行的, 因爲沒有默認構造函數, 錯誤爲: “CxString”: 沒有合適的默認構造函數可用
CxString string4("aaaa"); // 這樣是OK的
CxString string5 = "bbb"; // 這樣也是OK的, 調用的是CxString(const char *p)
// 這裏注意!!!並不是創建c字符串!
CxString string6 = 'c'; // 這樣也是OK的, 其實調用的是CxString(int size), 且size等於'c'的ascii碼
string1 = 2; // 這樣也是OK的, 爲CxString預分配2字節的大小的內存
string2 = 3; // 這樣也是OK的, 爲CxString預分配3字節的大小的內存
string3 = string1; // 這樣也是OK的, 至少編譯是沒問題的, 但是如果析構函數裏用free釋放_pstr內存指針的時候可能會報錯, 完整的代碼必須重載運算符"=", 並在其中處理內存釋放
// 這裏的 CxString string2 = 10; 就是一個隱式的類型轉換構造,等同於
CxString temp(10);
CxString string2 = temp;
// 若將構造函數聲明成:explicit CxString(int size)
// 則CxString string2 = 10;會調用失敗
9.對象關係
C++中的面向對象,往往被描述爲三大特性:封裝、繼承、多態。但其實對象之間的關係也是面向對象編程設計的一個重要概念。它被分爲3類:複合(composition)、委託(delegation)、繼承(inheritance)
複合(composition)
-
class A中的成員爲class B對象。即A中需要用到B提供的內容(數據、函數)。這種情況下,表示A中有B(has-a),需要注意兩點:內存模型和生命週期。
-
內存模型:如標準庫中的queue類。queue複合了deque,deque複合了兩個Itr對象。
內存中,是包含關係。queue是40個字節,其數據成員只有一個deque對象,deque正好是40字節,而deque又是兩個16字節的Itr(這個Itr從命名以及struct結構看,應該是一個迭代器)。 -
生命週期:構造函數、析構函數。
container的構造函數先調用component默認的構造函數(這是編譯器默認的,若compoment沒有默認構造怎麼辦?——被用戶override了),然後才執行自己的構造函數。
如果一個類,你沒有定義構造函數,那麼系統默認會有一個無參的構造函數。但如果你定義了一個有參的構造函數,爲了保證正確性,系統不會創建無參構造函數,這時候,如果你還想允許無參構造,就必須顯式的聲明一個
#include<iostream>
using namespace std;
class componet_a {
public:
// 不override默認的無參構造器
// 不寫任何構造函數
// 情況三:什麼都不做,編譯器自動給出一個默認的無參數的構造器
private:
// 隨便給個數據
int a;
};
class componet_b {
public:
// 給定一個構造器
componet_b(int i=0) :a(i) {
cout << "componet_b, constructor with parameter.." << endl;
}
/*
情況一:int i,不給默認值0,此時相當於component給了一個帶參數的構造器,此時編譯器不再爲該class提供默認的無參構造器,main中container ct;這句會編譯不通過。
情況二:int i=0,此時等價於提供了一個無參構造器,因爲有默認值i,所以可以無參數,這樣container知道如何初始化自己的成員componet_b comb。
*/
private:
int a;
};
class container {
private:
componet_a coma;
componet_b comb;
public:
container() {
cout << "container constructor.." << endl;
}
};
int main() {
container ct;
return 0;
}
container的析構函數首先執行自己,然後函數體內部默認調用各個componet的析構函數(可以看一下析構的順序,就是說默認的析構component的代碼是在container的析構代碼的開頭還是結尾部分)。
#include<iostream>
using namespace std;
class componet_b {
public:
// override析構函數
~componet_b() {
cout << "componet_b, destructor .." << endl;
}
private:
int a;
};
class container {
private:
componet_b comb;
public:
~container() {
cout << "container destructor.." << endl;
// component的析構等同於在這個位置默認調用!
}
};
int main() {
container ct;
return 0;
}
// output:
// container destructor..
// componet_b, destructor ..
委託(delegation)
所謂委託其實和複合差不多,唯一區別就是委託並不直接在container中包含component,而是包含component的指針。由於是指針,所以內存模型和生命週期相對複合發生了變化。
-
內存模型:container並不直接包含componet,即元素大小不再如複合那樣了。因爲指針是4個byte。component的實際存儲空間不是包含在container空間中,而是另一塊內存區域。他們之間通過指針聯繫。
-
生命週期:container析構時只會默認銷燬這個指針變量,但指針所指的那個對象需要手動調用析構,所以帶有指針的class,需要自己維護這個指針(new和delete需要自己處理,通常在析構函數中delete掉這個指針指向的對象內存)。
-
由於是指針,需要注意多個指針指向同內存對象的問題。而標準庫利用了這點,進行了字符串的緩存(多個相同的字符串實際在內存中只有一份,若需要修改某一個時,單獨將它拷貝出一份)。
如圖標準庫中的String類,實際是通過StringRep指針實現字符串的。而StringRep中有一個reference counting(int count,這個計數應該是對象的,而不是class的——不是static的,因爲這裏是計數相同字符串的“引用”的個數,而不是內存中有多少個StringRep對象的計數,後者通常用static,在Java中jvm的垃圾回收GC好像是類似的做法)。這裏三個String對象a、b、c內容都是“Hello”,所以其實內存中只有一份“hello”,通過指針鏈接到這三個String對象,若其中一個(例如a)需要修改成“Hi”,那麼爲它單獨重新創建一個string“Hi”,然後指針指到“Hi”。
繼承(inheritance)
繼承表示的是“is-a”的關係。子類(derived)擁有父類(base)的部分成分(根據繼承的類別)。如圖,_List_node包含父類**_List_node_base**的數據成員的。(通常說的時候是pair-wise:派生類-基類,子類-父類)
繼承:1)繼承了數據。2)繼承了函數的調用權!
子類需要重新定義函數嗎?(override)
- non-virtual,不希望子類覆寫的函數。(普通函數)
- virtual,希望子類覆寫,但是在父類中給了默認的實現,子類不覆寫也不會編譯錯誤。(函數用virtual修飾,並且給出函數定義,或者說實現)
- pure virtual,純虛函數,父類不實現該函數(類似於接口),子類必須實現該函數。(virtual修飾,且無函數定義)
class A {
public:
A();
virtual ~A();
void f1();
virtual void f2();
virtual void f3()=0;
};
class B:public A{
public:
B();
virtual ~B();
void f1();
virtual void f2();
virtual void f3();
};
int main(int argc,char * argv[]) {
A *m_j = new B();
m_j -> f1();
m_j -> f2();
m_j -> f3();
delete m_j;
return 0;
}
// f1()是一個隱藏,關於函數的隱藏,可以參考其它詞條.
// 調用m_j->f1();會去調用A類中的f1(),它是在我們寫好代碼的時候就會定好的.也就是根據它是由A類定義的,這樣就調用這個類的函數.
// f2()是重寫(覆蓋).
// 調用m_j->f2();會調用m_j中到底保存的對象中,對應的這個函數.這是由於new的B對象.(調用派生類的f2())
// f3()與f2()一樣,只是在基類中不需要寫函數實現.
// f2與f2這裏是多態現象,多態的實現基於虛函數表。而f1本身不是虛函數,所以這裏f1調用的還是A類的方法。
繼承的構造與虛構
- derived子類的構造函數先調用base父類的的默認構造函數,再執行自己的構造函數。
- derived的析構函數首先執行自己,然後才調用base的析構函數。
#include<iostream>
using namespace std;
class base {
public:
// 這裏若不給無參構造,則由於給了有參構造,編譯器默認的無參構造將無效,此時derived的構造函數編譯錯誤。
base() : a(100){
cout << "base, constructor with non-parameter.." << endl;
}
// 給定一個構造器
base(int i) :a(i) {
cout << "base, constructor with parameter.." << endl;
}
~base(){
cout << "base, destructor .." << endl;
}
protected:
// 隨便給個數據
int a;
};
class derived : public base {
private:
float b;
public:
// 這裏會默認調用父類base的無參構造函數
// 可以可以改爲:derived(double i):base(i) ,這樣可以指定調用父類的有參構造,且這裏發生double自動向上轉型爲int
derived() {
cout << "derived constructor.." << endl;
}
~derived(){
cout << "derived, destructor .." << endl;
// 父類的析構函數在這裏被自動調用
}
int get_a() {
return this->a;
}
};
int main() {
derived dr;
cout << dr.get_a() << endl;
return 0;
}
// base, constructor with non-parameter..
// derived constructor..
// 100
// derived, destructor ..
// base, destructor ..
繼承+複合
上圖第一種情況:
-
Derived 的構造函數首先調用 Base 的 default 構造函數,然後調用 Component 的 default 構造函數,然後才執行自己。
Derived::Derived(…): Base(),Component() { … };
-
Derived 的析構函數首先執行自己,然後調用 Component 的 析構函數,然後調用 Base 的析構函數。
Derived::~Derived(…){ … ~Component(), ~Base() };
第二種情況是簡單的嵌套,調用順序簡單不再多說。
委託+繼承
(常用!尤其見於各種設計模式)
應用:觀察者模式
(觀察者模式和監聽模式應該是常用的兩種模式了,在許多GUI框架裏,比如Java的swing裏,有大量的listener,就是採用的監聽模式。在Android開發中,也常用觀察者模式來觸發UI刷新)
10.虛函數與多態
多態:
在基類的函數前加上virtual關鍵字,在派生類中重寫該函數,運行時將會根據對象的實際類型來調用相應的函數。如果對象類型是派生類,就調用派生類的函數;如果對象類型是基類,就調用基類的函數。注意new的類型,與申明的類型。new的類型纔是真正的類型,是內存中對象類型。
如Son son;Father *pFather=&son;或者 Father *pFather = new Son;
- 用virtual關鍵字申明的函數叫做虛函數,虛函數肯定是類的成員函數。
- 存在虛函數的類都有一個一維的虛函數表叫做虛表,類的對象有一個指向虛表開始的虛指針。虛表是和類對應的,虛表指針是和對象對應的。
- 純虛函數是虛函數再加上 = 0;
- 抽象類是指包括至少一個純虛函數的類。(即子類必須覆寫方法)
這裏的圖比較清晰:https://www.cnblogs.com/dormant/p/5223215.html
基類:
class Shape {
public:
Shape();
virtual ~Shape();
virtual void render();
void move(const pos&);
virtual void resize();
protected:
pos center;
};
內存模型:
派生類:
class Ellipse : public Shape{
public:
Ellipse (float majr, float minr);
virtual void render();
protected:
float major_axis;
float minor_axis;
};
內存分佈:
這裏的vtable不是對象的,而是屬於類的。
附另一個參考:https://blog.csdn.net/u012630961/article/details/81226351(代碼、圖示、debug非常清楚,贊一個!)
應用:利用多態實現 模板設計模式
#include<iostream>
using namespace std;
// 模板類:泡一杯飲料
// 反正方法都一樣:1,燒開水放到水杯中; 2,倒入飲料; 3, 加配料。
// 但具體泡啥(茶、咖啡都可以),加啥(糖、冰、奶都可以),由用戶決定
class make_drink {
public:
void make() {
water(); // 燒水並倒入杯中
add(); // 添加飲料
flavouring(); // 添加配料
}
void water() {
cout << "燒水並倒入杯中 .." << endl;
}
virtual void add() = 0;
virtual void flavouring() {
// 用戶不說口味,那就默認加糖
cout << "加糖 .." << endl;
}
};
class make_coffee : public make_drink {
public:
virtual void add() {
cout << "倒入coffee .." << endl;
}
};
class make_juice : public make_drink {
public:
virtual void add() {
cout << "倒入juice .." << endl;
}
virtual void flavouring() {
cout << "加冰,來點檸檬 .." << endl;
}
};
int main(){
make_drink* mc = new make_coffee;
mc->make();
make_drink* mc2 = new make_juice;
mc2->make();
return 0;
}
/*
燒水並倒入杯中 ..
倒入coffee ..
加糖 ..
燒水並倒入杯中 ..
倒入juice ..
加冰,來點檸檬 ..
*/