title: 'c++面向對象: public 繼承, 虛方法, 動態(tài)綁定'
date: 2018-11-04 13:00:34
tags:
- oop
- public inheritance
- virtual methods
- dynamic binding
categories: cpp
blog: https://withas.me
學了java中的面向對象后再來學習c++的面向對象, 對兩者設計理念上的差異有不少的體會: c++更多地考慮了程序的運行效率(默認靜態(tài)綁定, 沒有垃圾回收等), 把很多繁瑣的操作留給了使用者; 而java是貫徹了面向對象的思維, 有一種徹頭徹尾的面向對象的感覺, 而把一些背后的機制向使用者隱藏了. 這篇blog主要記錄c++中public繼承相關的內容.
Public Inheritance
c++中有public, protected, private三種繼承方式, 其中public是最常用的.
public繼承描述的是一種is-a的關系, derived-class是base-class的一個子集且一般是真子集. 比如從Student類可以派生出Cadre類, Cadre一定是Student, 而Student不一定是Cadre. 這種關系是單向的, 不具有對稱性.
class Student {
private:
int id;
string name;
public:
int getId() const { return id; }
void setId(int id) { Student::id = id; }
const string &getName() const { return name; }
void setName(const string &name) { Student::name = name; }
};
class Cadre : public Student {
private:
string duty;
public:
const string &getDuty() const { return duty; }
void setDuty(const string &duty) { Cadre::duty = duty; }
};
Cadre由Student派生, 則Cadre直接繼承了Student的所有public成員. Private成員也成為了derived-class的一部分, 但是只能通過base-class的public和protected方法訪問. 例如Cadre從Student那里繼承了name, 但是不能直接訪問, 只能通過Student::getName
訪問.
class Cadre : public Student {
...
public:
...
void showInfo() const {
cout << Student::getName() << endl;
cout << Student::getId() << endl;
cout << duty << endl;
}
};
Constructos and Destructos
class Student {
private:
int id;
string name;
public:
Student(int id, const string &name) : id(id), name(name) {}
...
};
class Cadre : public Student {
private:
string duty;
public:
Cadre(int id, const string &name, const string &duty) : Student(id, name), duty(duty) {}
...
};
Derived-class不能直接直接訪問繼承自base-class的private成員, 所以不能在constructors里直接對它們初始化, 必須借助base-class的constructors. 并且base-class必須先于derived-class被構造. 如果要使用base-class的constructor有參數(shù)則必須在derived-class的constructor中以初始化參數(shù)列表的方式調用, 如上例. 如果不在derived-class的constructor中顯式地調用base-class的constructor, 則默認使用base-class的不帶參數(shù)的constructor(不存在則編譯會產生錯誤).
Base-class會先于derived-class被構造. 析構的方向恰與之相反, derived-class的destructor先于base-class被執(zhí)行.
Is-a
前面提到過public繼承是一種is-a的關系, 所以base-class的指針可以指向derived-class的對象, base-class的引用可以引用derived-class的對象.
Student *pc = new Cadre(1, "J", "monitor");
Student &rc = *pc;
但是通過base-class的指針和引用只能使用base-class存在的屬性, 如:
rc.getId();//ok
rc.getDuty();//error
Virtual Methods
Redefine
有時候為了讓derived-class有不同的功能我們可能需要重寫base-class的方法.
class Student {
private:
int id;
string name;
public:
void showInfo() const {
cout << name << endl;
cout << id << endl;
}
};
class Cadre : public Student {
private:
string duty;
public:
void showInfo() const {
cout << Student::getName() << endl;
cout << Student::getId() << endl;
cout << duty << endl;
}
};
這樣我們對Student對象和Cadre的對象分別調用showInfo的時候就會輸出不同的結果.
但是這樣會產生一個問題:
Cadre *pc = new Cadre(1, "J", "monitor");
Student &rc = *pc;
pc->showInfo();
cout << endl;
rc.showInfo();
/*output:
J
1
monitor
J
1*/
同一個Cadre對象, 用它自己的指針調用和用Student的引用調用時結果不一樣. 查看輸出結果可以知道用Student的引用調用的時候運行的是Student::showInfo
, 而非在Cadre中重寫的方法.
Virtual methods
為了讓base-class引用的derived-class可以調用derived-class重寫的方法, 我們可以使用virtual.
class Student {
public:
...
virtual void showInfo() const {
cout << name << "\t" << id;
}
};
class Cadre : public Student {
public:
...
virtual void showInfo() const {
Student::showInfo();//reusing code
cout << "\t" << duty;
}
};
再來看看輸出結果:
Cadre *pc = new Cadre(1, "J", "monitor");
Student &rc = *pc;//Student reference
pc->showInfo();
cout << endl;
rc.showInfo();//invoke Cadre::showInfo() instead of Student::showInfo()
/*output:
J 1 monitor
J 1 monitor
*/
如果不使用virtual, 調用的方法取決于引用或者指針的類型; 使用, 則由引用或者指向的對象本身決定, 也就是說可以使用base-class的引用或者指針調用在derived-class的重寫的方法.
當base-class中的方法被標記為virtual時, derived-class中重寫的方法也被自動標記為virtual. 不過一般為了方便閱讀, 也將derived-class中重寫的方法標記為virtual.
對于base-class中在derived-class被重寫的方法一般都會被標記為virtual. 另外有derived-class的base-class的destructors最好被標記為virtual.
class A {
private:
string *a;
public:
A() { a = new string("Hello"); }
~A() { delete a; }
};
class B : public A {
private:
string *b;
public:
B() : A() { b = new string("world"); }
~B() { delete b; }
};
int main() {
A *pb = new B();
delete pb;
}
delete pb
時會直接調用A::~A()
, B中b的內存沒有成功被釋放. 所以為了內存的正確釋放最好把base-class的destructor標記為virtual.
Static and Dynamic Binding
綁定是指調用哪個具體的方法. 例如上面不加virtual關鍵詞時rc.showInfo
會調用引用類型的showInfo
, 即Student::showInfo;
而加上virtual會調用Cadre::showInfo
.
Cadre *pc = new Cadre(1, "J", "monitor");
Student &rc = *pc;//Student reference
rc.showInfo();
static binding是指在編譯的時候就決定了調用的方法, 而dynamic binding是在程序運行過程中才能決定(因為對于一個base-class的指針可能指向base-class也可能指向derived-class). 這種dynamic binding的特性被稱為多態(tài).
c++默認的綁定方式是static binding. 對于dynamic binding對象內部會增加一個virtual function table (vtbl), 它負責記錄這個對象應該調用的方法. 盡管dynamic binding看上去有更多的好處, 然而c++默認的綁定方式是static binding, 這樣程序的運行效率更高. 與之對比, java中沒有virtual function, 它默認的就是動態(tài)綁定.
dynamic binding產生的多態(tài)有什么好處呢? 看下面這個例子:
class Class {
private:
vector<Student *> students;
public:
void addStudent(Student *student) {
students.push_back(student);
}
void listStudent() {
for (auto &s: students) {
s->showInfo();
cout << endl;
}
}
};
//
Class cla;
Student a = Student(1, "Y");
Cadre b = Cadre(2, "Z", "monitor");
Student c = Student(3, "W");
cla.addStudent(&a);
cla.addStudent(&b);
cla.addStudent(&c);
cla.listStudent();
/*output
Y 1
Z 2 monitor
W 3
*/
在Class中我們全都儲存的是Student的指針, 在打印Student List的時候就可以根據(jù)指向的具體對象輸出相應的信息.
Code
完整演示代碼:
#include <iostream>
#include <vector>
using namespace std;
class Student {
private:
int id;
string name;
public:
Student(int id, const string &name) : id(id), name(name) {}
int getId() const { return id; }
void setId(int id) { Student::id = id; }
const string &getName() const { return name; }
void setName(const string &name) { Student::name = name; }
virtual void showInfo() const {
cout << name << "\t" << id;
}
virtual ~Student() {}
};
class Cadre : public Student {
private:
string duty;
public:
Cadre(int id, const string &name, const string &duty) : duty(duty), Student(id, name) {}
const string &getDuty() const { return duty; }
void setDuty(const string &duty) { Cadre::duty = duty; }
virtual void showInfo() const {
Student::showInfo();
cout << "\t" << duty;
}
};
class Class {
private:
vector<Student *> students;
public:
void addStudent(Student *student) {
students.push_back(student);
}
void listStudent() {
for (auto &s: students) {
s->showInfo();
cout << endl;
}
}
};
int main() {
Cadre *pc = new Cadre(1, "J", "monitor");
Student &rc = *pc;
pc->showInfo();
cout << endl;
rc.showInfo();
delete pc;
cout << endl;
Class cla;
Student a = Student(1, "Y");
Cadre b = Cadre(2, "Z", "monitor");
Student c = Student(3, "W");
cla.addStudent(&a);
cla.addStudent(&b);
cla.addStudent(&c);
cla.listStudent();
return 0;
}
懶惰的我終于又開始更新了.