C++继承与派生
继承和派生
引入
目的
子承父业,可以使子类站在更高的基础上,节省更多的精力。
解决原有类解决不了的问题
问题
继承来的数据结构应该怎么用?
派生类怎么初始化?
组合与继承?————多种形式的构造
继承来的冲突?
概念
继承和派生是同一过程从两个角度(子类、基类)看。
一个多继承示例
class Derived: access-specifier1 Base1, access-specifier2 Base2
{
public:
Derived();
~Derived();
};
继承的内容
- 基类成员
- 改造基类成员
- 添加派生成员
不继承的内容
- 基类的构造函数、析构函数和拷贝构造函数
- 基类的重载运算符
- 基类的友元函数
继承的形式
确定继承来的成员在派生类中的访问权限。
(几乎不用private和protected)
访问权限
访问形式 | public | protected | private |
---|---|---|---|
反身访问 | y | y | y |
派生类->基类 | y | y | n |
外部对象 | y | n | n |
- 这是类的定义(private成员只能通过类的本尊进行访问)。
- 基类的private成员永不可调用。(封装的实质)
- 从外部利用派生类对象(直接调用)访问基类时,只能访问public成员。(这与类的访问是一致的。)
补充
- 派生类可以使用基类成员,但基类不能访问派生类的独有成员。原理上,基类不知晓派生类多出来的成员。
访问控制
public最随和,任何成员的访问属性不变。
保护继承其次,在继承的时候,仅将public改成protected。
private……全部改成private
protected 成员的特点与作用
- 对建立其所在类对象的模块来说,它与 private 成员的性质相同。
- 对于其派生类来说,它与 public 成员的性质相同。
第二条给出了一种可以从外部访问基类保护成员的方法。即利用派生类的public接口调用基类
- 既实现了数据隐藏,又方便继承,实现代码重用。
- 如果派生类有多个基类,也就是多继承时,可以用不同的方式继承每个基类。
多继承举例
class A {
public:
void setA(int);
void showA() const;
private:
int a;
};
class B {
public:
void setB(int);
void showB() const;
private:
int b;
};
class C : public A, private B {
public:
void setC(int, int, int);
void showC() const;
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() {
C obj;
obj.setA(5);
obj.showA();
obj.setC(6,7,9);
obj.showC();
// obj.setB(6); 错误
// obj.showB(); 错误
return 0;
}
这个错误的原因就是C对B类的继承是private继承。所以不能调用。
这个例子是派生类访问基类权限的集中体现。
基类和派生类型转换
- 公有派生类对象可以被当作基类的对象使用,反之则不可。
- 派生类的对象可以隐含转换为基类对象;
- 派生类的对象可以初始化基类的引用;
- 派生类的指针可以隐含转换为基类的指针。
- 通过基类对象名、指针只能使用从基类继承的成员。这告诫我们,不要定义与继承而来的函数同名的非虚函数,它们是invalid的。如下的例子:
#include <iostream>
using namespace std;
class Base1 { //基类Base1定义
public:
void display() const {
cout << "Base1::display()" << endl;
}
};
class Base2: public Base1 { //公有派生类Base2定义
public:
void display() const {
cout << "Base2::display()" << endl;
}
};
class Derived: public Base2 { //公有派生类Derived定义
public:
void display() const {
cout << "Derived::display()" << endl;
}
};
void fun(Base1 *ptr) { //参数为指向基类对象的指针
ptr->display(); //"对象指针->成员名"
}
int main() { //主函数
Base1 base1; //声明Base1类对象
Base2 base2; //声明Base2类对象
Derived derived; //声明Derived类对象
fun(&base1); //用Base1对象的指针调用fun函数
fun(&base2); //用Base2对象的指针调用fun函数
fun(&derived); //用Derived对象的指针调用fun函数
return 0;
}
输出结果
Base1::display()
Base1::display()
Base1::display()
这表明,我们在派生类中定义的函数都没有发挥作用。
派生类的构造函数
继承构造函数
一般是不继承构造函数的。
为了实现子承父业的初衷,我们可以使用基类的构造函数,比如
class Derived: public Base{
public:
Derived(int i): Base(i){ };
}
或者在新成员并不多的情况下
class Derived : public Base{
public:
Derived(int i, int j) : Base(i), a(j){ };
private:
int a;
};
但C++11可以利用 using Base::Base
语句进行继承。这个语句的好处还在于避免了需要写多个构造函数的情况。
派生类构造函数格式
Derived::Derived(param_list):
Base1(param), Base2(param), ..., BaseN(param),
other_param
{
do sth;
};
派生类构造函数顺序
继承构造
(按照从基类继承向下的)顺序调用基类构造函数。可以在没有有参时隐式调用无参构造函数(几个讨论见下)。
成员构造
对初始化列表中的成员进行初始化。
- 顺序按照它们在类中定义的顺序。
- 对象成员初始化时自动调用其所属类的构造函数。由初始化列表提供参数。
构造函数的构造
执行派生类的构造函数体中的内容。
派生类构造和无参构造函数
在创建子类对象时候,如果子类的构造函数没有显示调用父类的构造函数且父类自己提供了无参构造函数,则会调用父类自己的无参构造函数。
主要是需要考虑已经定义过有参的情况,这时会找不到无参构造函数而报错。
一个关于无参构造函数的有趣例子
class Base{
public:
Base(int i): b(i){ cout << "Base::Base()" << endl;};
int b;
};
class Derived : public Base{
public:
friend ostream &operator<<(ostream &os, Derived &d);
int a;
};
ostream &operator<<(ostream &os, const Derived &d){ return os << d.a << "a" << d.b << 'b';}
int main()
{
// Derived tmp(); 这样写是定义函数
Derived tmp;//这样才是创建对象
cout << tmp << endl;
}
这里创建对象,会调用父类的无参构造函数,而其实它已经因为有参构造函数的定义而删除了,报错为 the default constructor of "Derived" cannot be referenced -- it is a deleted function
这里是清华郑莉老师的一个很有益的例子:
#include <iostream>
using namespace std;
class Base1 {//基类Base1,构造函数有参数
public:
Base1(int i)
{ cout << "Constructing Base1 " << i << endl; }
};
class Base2 {//基类Base2,构造函数有参数
public:
Base2(int j)
{ cout << "Constructing Base2 " << j << endl; }
};
class Base3 {//基类Base3,构造函数无参数
public:
Base3()
{ cout << "Constructing Base3 *" << endl; }
};
class Derived: public Base2, public Base1, public Base3 {
public:
Derived(int a, int b, int c, int d): Base1(a), member2(d), member1(c), Base2(b)
//此处的次序与构造函数的执行次序无关,初始化“列表”正是这个意思
{ }
private:
Base1 member1;
Base2 member2;
Base3 member3;
};
int main() {
Derived obj(1, 2, 3, 4);
return 0;
}
输出结果
Constructing Base2 2
Constructing Base1 1
Constructing Base3 *
Constructing Base1 3
Constructing Base2 4
Constructing Base3 *
对派生类的构造函数的说明:
- 初始化“列表”是无序列表,不决定构造函数的执行顺序。
- 这个输出结果中的前三行是声明的对象obj的三个基类的顺序构造。后三行是按照顺序的成员构造。
复制构造函数
派生类未定义复制构造函数
顺序:继承顺序。
无复制构造函数时调用无参。
派生类定义了复制构造函数的情况
- 复制构造函数只能接受一个参数,既用来初始化派生类定义的成员,也将被传递给基类的复制构造函数。
- 基类的复制构造函数形参类型是基类对象的引用,实参可以是派生类对象的引用
例如: C::C(const C &c1): B(c1)
析构函数
相比构造函数,由于不需要传参数,所以比较简单。
先构造,后析构。
ambigous(二义性)问题
- 派生类中定义与基类中同名成员:默认调用派生类
- 从不同基类继承了同名成员,但是在派生类中没有定义同名成员:必须用类名+作用域分辨符"::"限定
- 最远基类中成员在继承过程当中可能出现冗余的现象,这不仅是二义性的问题,我们以下可以专门来讨论解决这个问题的机制:虚基类
虚基类
利用virtual关键字说明基类继承方式
class B1:virtual public B
虚基类的成员是由最远派生类的构造函数通过调用虚基类的构造函数进行初始化的。
构造顺序仍然是从最远基类到最远派生类。
但是这个机制慎用于公共工程、类库等