1.继承的重要性
继承是面向对象的三大特征之一,同时继承又是面向对象另一个特征“多态”的基础,
没有继承也就没有多态。
面向对象语言的强大之处在于能够非常轻松自如的按照分层思想开发程序,特别是
开发大型程序,分层思想对于搭建整个程序的宏观架构有着非常重要的作用。
继承使面向对象语言具有巨大的生命力,因为总是可以以继承方式从已经存在的代码(类)
基础上创造出具有新特点的代码(类)。
被继承的类被称为基类(或父类),继承产生的新类成为派生类(或子类)。
2.如何创建一个新类
(1)方式1:继承
并不是所有的新类的创建都需要使用继承方式,一般只有满足如下测试条件的才使用
继承方式创建新类,当然使用继承方式创建新类时,需要一个基类。
(1)子类是不是基类的子集,或者说子类与父类之间满不满足一般与特殊的关系。
显然动物类比人类具有更一般的特性,人类是动物类的子集。
(2)基类是否有特性无法应用于派生类,如果有就不能使用继承,否者可以使用继承。
经典的例子:
鸟类:都有飞翔的特性
鸵鸟:因为不能飞翔,因此不能继承于鸟类。
如果非要继承的话,继承关系应该是这样的。
一般的鸟类
|
--------------------
| |
| |
会飞的鸟类 不会飞的鸟类
| |
---------------- ------------
| | | |
鹰 燕子 鸵鸟 鸡
结构体也具有继承功能:
在讲结构体时就说过,c++中的结构体和class功能基本相同,只是成员的默认权限不同,
前面例子中的class换成结构的话。一样能够正常工作,请自行测试。
(2)方式2:聚合(组合)
(1)组合模式
实际上继承并不是产生新类的常用方法,如果一遇到问题就频繁使用继承的话,会
使得程序结构复杂混乱,继承往往只在实现整个程序的宏观结构时有着非常的重要
的作用,但是在绝大多数创建新类时,我们使用的更多的是聚合这种方式。有的地
方也成为组合这种模式,其实就是在类中定义其它类成员。
适不适合使用组合方式创建新类呢?就看包含类与被包含类是不是整体与局部的关系。
比如我们前面个章节提到的有关学生类和生日类的关系,这其实就是一种整体与局部
的包含关系,因为每个学生都有包含生日这个属性。
除了生日外,比如:
(1)学生的身体素质,比如身高,体重,年龄可以单独进行类定义。
(2)学生的身份信息,比如名字,身份证号,学号可以单独进行类定义。
(3)学生的联系方式,比如email,QQ,tel;
然后将以上描述的类组合在一起,就可以很清楚的描述一个学生类。
聚合的例子:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <iostream>
#include <string>
#include <cassert>
#include <errno.h>
using namespace std;
/* 生日类 */
class Birthday {
string year;
string month;
string day;
public:
Birthday(const string year="0", const string month="0", const string day="0")
:year(year), month(month), day(day){}
/* getter */
void set_year(const string year) { this->year = year;}
void set_month(const string month) { this->month = month;}
void set_day(const string day) { this->day = day;}
/* setter */
string get_year() { return this->year;}
string get_month() { return this->month;}
string get_day() { return this->day;}
};
/* 身份信息类 */
class Identity {
string name;
string stunum;
string idcard;
public:
Identity(const string name="xxx", const string stunum="0", const string idcard="0")
:name(name), stunum(stunum), idcard(idcard) {}
/* getter */
void set_name(const string name) {this->name = name;}
void set_stunum(const string stunum) {this->stunum = stunum;}
void set_idcard(const string idcard) {this->idcard = idcard;}
/* setter */
string get_name() {return this->name;}
string get_stunum() {return this->stunum;}
string get_idcard() {return this->idcard;}
};
/* 学生类 */
class Student {
Identity id;//身份id
Birthday brthday;//生日
public:
Student(const string name="xxx", const string stunum="0", const string idcard="0",
const string year="0", const string month="0", const string day="0")
:id(name, stunum, idcard), brthday(year, month, day) {}
/* getter */
Identity& get_idcard() {return id;}
Birthday& get_brthday() {return brthday;}
};
int main(void)
{
Student stu = Student("zhangsan", "201402343", "500230198623343", "1994", "8", "7");
/* 显示学生身份信息 */
cout << "学生身份信息" << endl;
cout << "姓 名:" << stu.get_idcard().get_name() << endl;
cout << "学 号:" << stu.get_idcard().get_stunum() << endl;
cout << "身份证号:" << stu.get_idcard().get_idcard() << endl<<endl;
/* 显示学生生日 */
cout << "学生生日信息" << endl;
cout << "年:" << stu.get_brthday().get_year() << endl;
cout << "月:" << stu.get_brthday().get_month() << endl;
cout << "日:" << stu.get_brthday().get_day() << endl;
return 0;
}
运行结果:
学生身份信息
姓 名:zhangsan
学 号:201402343
身份证号:500230198623343
学生生日信息
年:1994
月:8
日:7
例子分析:
例子中,学生由两个类对象成员组成,一个用于表示身份信息,一个用于表示生日信息。
类的数据成员都被设置为了隐藏,外部需要通过设置器和获取器才能对其进行访问。
(2)方式3:代理模式(聚合模式的改进版)
之前聚合模式的例子有一个小缺点,那就是如下输出函数,
cout << "学生身份信息" << endl;
cout << "姓 名:" << stu.get_idcard().get_name() << endl;
cout << "学 号:" << stu.get_idcard().get_stunum() << endl;
cout << "身份证号:" << stu.get_idcard().get_idcard() << endl<<endl;
/* 显示学生生日 */
cout << "学生生日信息" << endl;
cout << "年:" << stu.get_brthday().get_year() << endl;
cout << "月:" << stu.get_brthday().get_month() << endl;
cout << "日:" << stu.get_brthday().get_day() << endl;
这些成员函数实际上暴露了Student的内部结构,一看就知道它是由brthday和idcard两个成
员组成的,这不利于隐藏成员的信息,如果类成员的组成很机密,比如与金融相关的程序,
这类程序中的类的成员是高度隐藏,别人除了不能访问外,内部的组成结构也是不能暴露,
这个时候我们就需要将聚合模式改进为代理模式了。
当然除了隐藏这样的原因外,如果我们希望在获取信息前对信息做进一步加工的时候,这个
时候也需要使用代理方式,比如上面的例子中,如果我们需要在学生名字前面加籍贯,学号
前面加学校名称时,可以在代理函数中处理。
所以针对聚合的例子,我们需要将获取学生信息的方式进行修改,在Student类里面删除获
取器,然后在public区域加入如下代理函数:
string get_stu_name() {
string tmp = string("山东青岛 ")+id.get_name();
return tmp;
}
string get_stu_num() {
string tmp = string("清华附属一中 ")+id.get_num();
return tmp;
}
string get_stu_idcard() {return id.get_idcard();}
string get_stu_brthyear() {return brthday.get_year();}
string get_stu_brthmonth() {return brthday.get_month();}
string get_stu_brthday() {return brthday.get_day();}
然后在main函数中调用代理函数打印学生信息。
Student stu = Student("zhangsan", "201402343", "500230198623343", "1994", "8", "7");
/* 显示学生身份信息 */
cout << "学生身份信息" << endl;
cout << "姓 名:" << stu.get_stu_name() << endl;
cout << "学 号:" << stu.get_stu_num() << endl;
cout << "身份证号:" << stu.get_stu_idcard() << endl<<endl;
/* 显示学生生日 */
cout << "学生生日信息" << endl;
cout << "年:" << stu.get_stu_brthyear() << endl;
cout << "月:" << stu.get_stu_brthmonth() << endl;
cout << "日:" << stu.get_stu_brthday() << endl;
(3)组合模式和代理模式选哪个
(1)如果要求隐藏就是用代理模式
实际上,如果对类的隐藏要求不是很严格的情况下,没有必要时用代理模式,使用
普通的组合模式就可以了。
(2)如果需要对数据做一些附加处理时
(3)完全重新地创建一个类
如果既不符合继承模式,也不符合组合模式,那就只能完全重新地创建新类。
3. 派生类成员的访问级别
(1)public/protected/private的访问级别
(1)public权限最宽:
public修饰的成员,类内部/外部/子类均可以直接通过./->访问。
(2)protected权限次之:
protected修饰的成员,类内部/子类可以直接通过./->访问,但是类外部不可以直
接通过./->访问,就算要访问,只能通过成员函数接口实现间接访问。
(3)private权限最窄:
只能在类的内部访问,不能在类的外部和子类直接通过./->访问,就算要访问只能
通过父类提供的成员函数接口实现间接访问。
(2)当不涉及继承时(只用于修饰类成员时)
当不涉及继承时,对于类的成员来说,private和protected效果是一致的,因为这些以藏的
成员都都只能在类的内部被访问,外部希望访问的话,只能通过成员函数接口实现。
所以当不涉及继承时,对于类外部来说private和protected没有区别,只有当涉及继承的操
作时,private和protected的区别才会显现出来。
(3)从父类中继承的成员在子类中的新访问权限是怎样的
(1)被继承成员在子类中的访问权限受到到两个方面共同影响,
(1)被继承成员在基类中的原始权限
(2)实现继承时有三种继承权限:
(1)public方式继承
(2)protected方式继承
(3)private方式继承
当public/protected/private用在继承上时,它们又被称为基类访问指定符。
默认情况是private继承。
比如:
classs Student:People {
}
等价于
classs Student:private People {
}
(2)被影响的规则是怎样的
(1)基类中的private成员,是基类私有的,子类一定不能那个访问,其实原因很简单,
private修饰成员的权限最窄,它限定只能被类自己访问,外部和子类都不能访问。
(2)除开private修饰的成员外,另外两个public/protected修饰的成员被继承时权限被
修改的规则是:
(1)基类成员的权限>继承权限的,将其降为继承权限
(2)基类成员的权限<=继承权限的,保持不变
(3)例子
例子1:public继承
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <iostream>
#include <string>
#include <cassert>
#include <errno.h>
using namespace std;
class People {
public: string name;
protected:string age;
private: string id;
public: People(const string name="zhangsan", const string age="20", const string id="9527")
:name(name), age(age), id(id) { }
};
class Student : public People {
string num;
public:
Student(const string num="1172"):num(num) { }
string get_age() { return age; }
//string get_id() { return id; }//id是父类私有成员,子类无法访问
};
int main(void)
{
Student stu;
cout << stu.name << endl;
cout << stu.get_age() << endl;
//cout << stu.age << endl;//age在子类也是保护成员,外界无法访问
//cout << stu.id << endl;//id 是基类保护成员,子类都无法访问,外界更无法访问
return 0;
}
例子分析:
基类People的第一个成员是public<=继承权限public,保持不变,所以在子类中该成员也是public。
基类People的第二个成员是protected<=继承权限public,保持不变,所以在子类中该成员也是protected。
基类的第三个成员是private,子类无法访问
例子2:protected继承
还是上面的例子,将继承权限改为protected后,
基类People的第一个成员是public>继承权限protected,降为protected,所以在子类中该成员是protected。
基类People的第二个成员是protected<=继承权限protected,保持不变,所以在子类中该成员也是protected。
基类的第三个成员是private,子类无法访问
例子3:private继承
还是上面的例子,将继承权限改为private后,
基类People的第一个成员是public>继承权限private,降为private,所以在子类中该成员是private。
基类People的第二个成员是protected>继承权限private,降为private,所以在子类中该成员是private。
基类的第三个成员是private,子类无法访问
如果从子类继续派生出子类的子类,其规则与上面是完全一样。
(4)子类对基类private成员的进行间接访问
(1)基类的private成员,子类是无法直接访问的,但是我们可以通过父类
提供的设置器和获取器间接访问,但是要求设置器和获取器的权限至少为protected,否者子类无法访问。
(2)例子
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <iostream>
#include <string>
#include <cassert>
#include <errno.h>
using namespace std;
class People {
private: string id;
public: People():id("10001") { }
protected:
string getid() {return id;}
string setid(const string id) {this->id = id;}
};
class Student : public People {
string num;
public:
Student():num("1172") { }
string get_id(){return getid(); };
};
int main(void)
{
Student stu;
cout << stu.get_id() << endl;
return 0;
}
例子分析:
例子中,People的id是private的,为了能够访问它,在基类people中定
义了一个protected权限的成员id的设置器和设置器。
在Student在继承People的时候,是以public方式继承的,因此基类的设
置器和获取器在子类Student中也是protected,因此子类可以访问基类的
设置器和获取器,这样子类便可以间接的访问基类的private的成员。
(5)我们应该如何指定public/protected/private权限
(2)修饰类成员时
(1)公共接口都是用public,大多数的成员函数都会定义为公共的。
(2)非公共接口都是用protected或者private,数据成员基本都是
非公共接口。
(1)如果不希望子类直接访问基类成员,就指定为private
(2)如果希望子类能够直接访问,指定为protected
(2)作为基类访问指定符
(1)如果希望基类的成员在子类中权限保持不变,使用public继承
(2)如果希望基类的public和protected的成员在被子类继承后,子类再
作为基类时,这些原始基类的成员还可以被继承的子类访问时,
就使用protected继承。
(3)如果希望基类的public和protected成员被子类继承后,当子类作为
基类时,这些成员不能在被访问时,需要指定private。
总结:public和private的权限太过于极端,要么谁都可以访问,要么就只有自己
才能访问,但是对于“如果希望外界不能访问,但除了自己可以访问外,
自己的子类还可以访问”的情况,使用public和private是无法实现的,
因为public太开放了,但private又太自私了,而protected的权限恰到好
处。权如其名,别人能够在一定程度上亲近它的同时,又能在一定程度上
很好的保护自己。
(6)继承总结
(1)public>protected>private
(2)当修饰类成员时,protected与private的区别只有在涉及继承时才能显现出来
(3)当public/protected/private作为继承访问指定符的作用,主要是为了对基类
成员的权限做一定继承限制限制。
(4)父类的privae成员,子类虽然继承了,但是子类是无法直接访问的。
(7)改变继承成员的访问指定
可以使用using关键字,可以使基类成员免受继承访问指定符protected和private的影响。
(1)例子:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <iostream>
#include <string>
#include <cassert>
#include <errno.h>
using namespace std;
class People {
public:
People():id("10001") { }
protected:
string id;
};
class Student : private People {
protected:
using People::id;
public:
Student():num("1172") { }
private:
string num;
};
int main(void)
{
Student stu;
// cout << stu.id << endl;
return 0;
}
例子分析:
class People {
public:
People():id("10001") { }
protected:
string id;
};
在people基类中,id是protected的,在经过Student private继承后,继承过来的id
的权限在子类中应该是private,但是在Student子类中可以使用using语句可以将id重
新声明为了protected。
protected:
using People::id;
(2)需要注意的地方
(1)使用using时,必须使用基类限定成员名
(2)除了可以对数据成员使用using外,也可以对成员函数使用using,格式如下:
权限:
基类名:函数名
注意,格式中是不包含函数参数列表的。
(3)不能对父类的private成员使用using关键字
(4)using重新声明的权限可以大于父类指定的权限,也可以小于父类指定的权限(重
新声明为private也可以)。
4.继承时构造函数的调用
(1)显式的调用基类的构造函数
前面有关继承的例子中,在子类中并没有显式调用基类的构造函数,在这种情况下,调用的
是基类默认的构造函数,实际上我们完全可以在子类中显式的调用基类的构造函数。
例子:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <iostream>
#include <string>
#include <cassert>
#include <errno.h>
using namespace std;
class People {
public:
/* 默认构造函數 */
People():name("uname"), age("0"), id("0") {
cout << "People的默认构造函数" << endl;
}
/* 非默认构造函数,实际上完全可以将默认与非默认构造函数合为一个 */
People(const string name, const string age, const string id)
:name(name), age(age), id(id) {
cout << "People的非默认构造函数" << endl;
}
protected:
string name;
string age;
string id;
};
class Student : public People {
public:
Student():num("0") {
cout << "Student的默认构造函数" << endl;
}
Student(const string name, const string age, const string id, const string num)
:People(name, age, id), num(num) {
cout << "Student的非默认构造函数" << endl;
}
private:
string num;
};
int main(void)
{
Student stu1;
cout<<endl;
Student stu2("zhangsan", "25", "007", "111");
return 0;
}
例子分析
从例子明显看出,我么可以通过People(name, age, id)的方式直接调用
基类的构造函数。
(2)有继承的时候,构造函数的调用顺序
在有继承的情况下,实例化派生类时,一定会先实例化基类并调用基类的
构造函数,直到所有的基类都被实例化和构造函数被调用结束,才实例化
派生类和调用派生类的构造函数,这实际上是一个递归的过程。
从上面例子的打印结果很容易看出这过程。
(3)基类的数据成员不能在派生类的初始化列表中进行初始化
比如下面这个构学生的造函数:
Student(const string name, const string age, const string id, const string num)
:name(name), age(age), id(id), num(num) {
cout << "Student的非默认构造函数" << endl;
}
这个构造函数,看起来好像是对的,但是实际上是完全错误的,是无法通过编译的,因为这个时
候基类的成员还不存在。
要么像前面一样,显式调用基类的初始化函数,要么在初始化列表
Student(const string name, const string age, const string id, const string num)
:People(name, age, id), num(num) {
cout << "Student的非默认构造函数" << endl;
}
要么在构造函数体中进行赋值:
Student(const string name, const string age, const string id, const string num)
:num(num) {
this->name = name;
this->age = age;
this->id = id;
cout << "Student的非默认构造函数" << endl;
}
5. 派生类中副本构造函数
(1)派生类中副本构造函数存在的问题
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <iostream>
#include <string>
#include <cassert>
#include <errno.h>
using namespace std;
class People {
public:
/* 默认构造函數 */
People():name("uname"), age("0"), id("0") {
cout << "People的默认构造函数" << endl;
}
/* 非默认构造函数,实际上完全可以将默认与非默认构造函数合为一个 */
People(const string name, const string age, const string id)
:name(name), age(age), id(id) {
cout << "People的非默认构造函数" << endl;
}
People(const People &people) {
this->name = people.name;
this->age = people.age;
this->id = people.id;
}
public:
string name;
string age;
string id;
};
class Student : public People {
public:
Student():num("0") {
cout << "Student的默认构造函数" << endl;
}
Student(const string name, const string age, const string id, const string num)
:num(num) {
this->name = name;
this->age = age;
this->id = id;
cout << "Student的非默认构造函数" << endl;
}
//Student(const Student &stu) {
// this->num = stu.num;
//}
private:
string num;
};
int main(void)
{
Student stu1 = Student("zhangsan", "12", "9527", "007");
Student stu2(stu1);
cout<<endl;
cout<<stu2.name<<endl;
cout<<stu2.age<<endl;
cout<<stu2.id<<endl;
return 0;
}
运行结果:
People的默认构造函数
Student的非默认构造函数
zhangsan
12
9527
例子分析:
至少从运行结果来看,是实现了stu1的内容拷贝了一份副本到stu2中的,但是并
没有显式的实现Student的拷贝构造函数,因此实际上调用的是Sudent的默认副本
构造函数,过程中会自动的调用基类的副本构造函数,如果基类没有实现就调用基类的
默认副本构造函数。
但是如果我们将Student的如下副本构造函数打开的话,问题出现了。
Student(const Student &stu) {
this->num = stu.num;
}
打开后编译运行,结果如下:
People的默认构造函数
Student的非默认构造函数
People的默认构造函数
uname
0
0
分析:
从结果看出,调用Student的副本构造函数时,该函数没有调用基类的副本构造函
数,调用而是基类的默认构造函数,因此多输出了一句
People的默认构造函数
出现该结果的原因是因为在派生中的显式写出的副本构造函数中,我们需要显式的调用
基类的副本构造函数才行,将Student的副本构造函数改为如下形式。
Student(const Student &stu): People(stu) {
this->num = stu.num;
}
People(stu)中的stu会被自动向上转型为People基类类型。
改进后的运行结果如下:
People的默认构造函数
Student的非默认构造函数
zhangsan
12
9527
(2)在派生类副本构造函数应该注意的问题
在派生类中也应该注意有关深浅拷贝的问题,如果涉及到自动分配的成员时,必须
重写副本构造函数和析构函数,当然对于基类来说,也是如此,如果涉及自动分配
的成员时,必须重写基类的副本构造函数和析构函数实现深拷贝。
6. 继承时析构函数调用顺序问题
(1)例子
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <iostream>
#include <string>
#include <cassert>
#include <errno.h>
using namespace std;
class People {
public:
People(const string name="uname", const string age="0", const string id="0")
:name(name), age(age), id(id) {
cout << "People的默认构造函数" << endl;
}
~People() {
cout << "People的析构函数" << endl;
}
public:
string name;
string age;
string id;
};
class Student : public People {
public:
Student(const string name="uname", const string age="0", const string id="0", const string num="0")
:num(num),People(name, age, id) {
cout << "Student的默认构造函数" << endl;
}
~Student() {
cout << "Student的析构函数" << endl;
}
private:
string num;
};
int main(void)
{
Student stu1 = Student("zhangsan", "12", "9527", "007");
cout << endl;
return 0;
}
运行结果:
People的默认构造函数
Student的默认构造函数
Student的析构函数
People的析构函数
例子分析:
从事上面的运行结果来看,析构函数的调用顺序与构造函数的调用顺序相反。
(2)派生中析构函数调用总结
(1)调用顺序与构造函数相反
(1)构造函数的调用顺序函数
基类构造函数
......
派生类构造函数
(2)析构函数的调用顺序
派生类析构函数
......
基类的析构函数
(2)如果派生类和基类涉及动态分配的成员时,在各自的析构函数中,需要
将其释放,在副本拷贝函数中需要对动态分配成员进行深拷贝。
例子:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <iostream>
#include <string>
#include <cassert>
#include <errno.h>
using namespace std;
class People {
public:
People(const char* name="uname") {
this->name = new char(strlen(name));
strcpy(this->name, name);
//cout << "People的默认构造函数" << endl;
}
People(const People &people) {
this->name=new char(strlen(people.name));
strcpy(this->name, people.name);
}
~People() {
//cout << "People的析构函数" << endl;
delete(name);
}
public:
char* name;
};
class Student : public People {
public:
Student(const char *name="uname", const char* num="0")
:People(name) {
this->num = (char *)malloc(strlen(num));
strcpy(this->num, num);
//cout << "Student的默认构造函数" << endl;
}
Student(const Student &stu) {
this->num = (char *)malloc(strlen(stu.num));
strcpy(this->num, stu.num);
}
~Student() {
//cout << "Student的析构函数" << endl;
free(this->num);
}
public:
char *num;
};
int main(void)
{
Student stu1 = Student("zhangsan", "007");
Student stu2(stu1);
cout << endl;
strcpy(stu2.name, "aa");
strcpy(stu2.num, "ww");
cout << stu2.name << endl;
cout << stu2.num << endl;
return 0;
}
例子分析:
本例子中实现了基类和派生类的动态内存的深拷贝,实现了在析构
函数中动态内存的释放。
7. 继承时成员重名
(1)数据成员重名
在继承时是无法避免在派生类中出现与基类数据成员同名的情况的,毕竟派生的新类
并不知道基类到底有些个什么名字的数据成员。
如果数据成员重名,派生类成员会屏蔽基类同名成员:
(1)如果希望在子类中访问基类的同名数据成员,需要使用“基类名::成员名”的形式访问。
(2)如果希望在外部通过派生类访问基类同名成员,如果该成员是public的,这个时候我们
可以将派生类向上转型到基类,然后再调用基类同名成员。
向上转型时,可以使用static_cast<>()进行强制转型。
例子:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <iostream>
#include <string>
#include <cassert>
#include <errno.h>
using namespace std;
class People {
public:
People(const string name="zhanagsan"):name(name) { }
public:
string name;
};
class Student : public People {
public:
Student(const string name="wangwu"):name(name) { }
void show() {
cout << "基类name:"<<name<<endl;
cout << "派生类name:"<<People::name<<endl;
}
public:
string name;
};
int main(void)
{
Student stu1 = Student();
stu1.show();
cout << "基类name:"<<stu1.name<<endl;
cout << "派生类name:"<<static_cast<People>(stu1).name<<endl;
return 0;
}
(2)函数成员重名
函数重名涉及到函数的重写,函数重写是非常重要的机制,这个概念留在下一
章讲多态时再讲解。
8. 多重继承
网友评论