一、继承的概念
继承 (inheritance) 机制是面向对象程序设计使代码可以复用的最重要的手段,它允许实现者保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称之为派生类;
继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程;
以前,我们接触的复用都是函数复用,继承是类设计层次的复用。
继承所表达的是类之间相关的关系,这种关系使得对象可以继承另外一类对象的特征和能力。
继承的作用:避免公用代码的重复开发,减少代码和数据冗余。
二、继承关系
继承基类成员访问方式的变化
三种继承方式代码示例:
//基类
class A
{
public:
int mA;
protected:
int mB;
private:
int mC;
};
//1. 公有(public)继承
class B : public A{
public:
void PrintB(){
cout << mA << endl; //可访问基类public属性
cout << mB << endl; //可访问基类protected属性
//cout << mC << endl; //不可访问基类private属性
}
};
class SubB : public B{
void PrintSubB(){
cout << mA << endl; //可访问基类public属性
cout << mB << endl; //可访问基类protected属性
//cout << mC << endl; //不可访问基类private属性
}
};
void test01(){
B b;
cout << b.mA << endl; //可访问基类public属性
//cout << b.mB << endl; //不可访问基类protected属性
//cout << b.mC << endl; //不可访问基类private属性
}
//2. 私有(private)继承
class C : private A{
public:
void PrintC(){
cout << mA << endl; //可访问基类public属性
cout << mB << endl; //可访问基类protected属性
//cout << mC << endl; //不可访问基类private属性
}
};
class SubC : public C{
void PrintSubC(){
//cout << mA << endl; //不可访问基类public属性
//cout << mB << endl; //不可访问基类protected属性
//cout << mC << endl; //不可访问基类private属性
}
};
void test02(){
C c;
//cout << c.mA << endl; //不可访问基类public属性
//cout << c.mB << endl; //不可访问基类protected属性
//cout << c.mC << endl; //不可访问基类private属性
}
//3. 保护(protected)继承
class D : protected A{
public:
void PrintD(){
cout << mA << endl; //可访问基类public属性
cout << mB << endl; //可访问基类protected属性
//cout << mC << endl; //不可访问基类private属性
}
};
class SubD : public D{
void PrintD(){
cout << mA << endl; //可访问基类public属性
cout << mB << endl; //可访问基类protected属性
//cout << mC << endl; //不可访问基类private属性
}
};
void test03(){
D d;
//cout << d.mA << endl; //不可访问基类public属性
//cout << d.mB << endl; //不可访问基类protected属性
//cout << d.mC << endl; //不可访问基类private属性
}
而以上三种继承方式,对于基类的私有成员(成员属性/成员方法)在派生类中不可见,即不论在派生类的类外还是类内都不可以访问:
三、继承方式总结
基类 private 成员在派生类中无论以什么方式继承都是不可见的。 这里的不可见指的是基类的私有成员虽然被继承到了派生类对象中,但是语法上限制了派生类对象访问它,无论是在派生类的类内还是类外,都无法访问;
如果基类成员不想让派生类在类外访问,但需要让派生类类内可以访问,那么该基类成员就可以定义为 protected,可以看到,protected 在继承角度有着很大的存在意义;
我们总结一下就会发现,除开基类的私有成员,基类的其他成员被子类继承后的访问方式 == Min (基类成员的访问限定符, 继承方式),private < protected < public;
class 的默认继承方式是 private,struct 的默认继承方式是public,但实际中,最好显示声明继承方式;
事实上,大多数的实际运用中,public 继承占绝大多数, 几乎很少使用 protected/private 继承,也不提倡 protected/private 继承, 因为 protected/private 继承下来的成员只能在派生类的类内使用,实际中扩展性并不强。
四、派生类的默认成员函数
默认成员函数即便我们不写,编译器也会自动生成一份,那么在派生类中,这些成员函数如何处理呢?
- 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员,如果基类没有默认的构造函数,则必须在派生类的构造函数的初始化列表阶段显式初始化基类的成员属性;
- 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类成员属性的初始化;
- 派生类的 operator= 必须要调用基类的 operator= 完成基类成员属性的赋值;
- 派生类的析构函数会在调用完成后自动调用基类的析构函数清理基类成员属性,因为这样才能保证派生类对象先清理派生类的成员属性再清理基类的成员属性;
- 派生类对象初始化资源时 (调用构造),先初始化基类成员属性,后初始化派生类成员属性;
- 派生类对象清理资源时 (调用析构),先清理派生类资源,在清理基类资源,即先调用派生类析构,然后编译器自动调用基类析构;
- 我们知道,重写的条件之一是函数名相同,可是基类和派生类的析构函数的函数名不相同啊,实际上,当派生类继承基类后,编译器会对析构函数进行特殊处理,析构函数的函数名统一为 destructor,所以,如果基类的析构函数不是虚函数,那么基类析构函数和父类析构函数构成隐藏关系。
我们一共学过六个默认成员函数:
1.派生类的构造函数
实例化一个派生类的对象后,结果却调用了基类的构造;
如果此时基类没有默认构造,那么实例化派生类对象时,会发生编译报错。
因此,实现者往往需要自己显式定义派生类的构造函数。
对于派生类的构造函数来说:
- 对于派生类的成员属性,跟以前的处理方式一样( 对于内置类型不处理,对于自定义类型调用它的默认构造函数 );
- 对于基类的成员属性,必须调用基类的构造函数初始化。
2.派生类的拷贝构造
对于派生类的拷贝构造来说:
- 对于派生类成员属性,跟以前的处理方式一样( 对于内置类型按照字节序的方式进行拷贝,对于自定义类型调用它的拷贝构造函数 );
- 对于基类成员属性,必须调用基类的拷贝构造函数初始化基类成员属性;
对于继承的基类成员而言,需要调用基类的拷贝构造初始化基类的成员属性。
3.派生类的赋值运算符重载
对于派生类来说,编译器默认生成的赋值运算符重载会:
- 对于自己本身的成员,跟以前的处理方式一样(对于内置类型按照字节序的方式进行拷贝,对于自定义类型调用它的赋值运算符重载);
- 对于继承父类的成员,必须调用父类的赋值运算符重载;
编译器默认生成的赋值运算符重载会调用父类的赋值运算符重载;
4.派生类的析构函数
对于派生类来说,编译器默认生成的析构函数,分两种情况:
- 对于自身的成员,对内置类型不处理,对自定义类型去调用它的析构函数,根以前一样;
- 对于继承的成员,需要去调用父类的析构函数处理。
而对于派生类来说,它需要保证:
派生类对象初始化时,基类成员先初始化 (先调用基类构造),派生类成员后初始化 (后调派生类构造);
派生类对象清理时,派生类成员先释放资源 (先调用派生类析构),基类成员后释放资源 (后调用基类析构);
因此编译器为了确保这种顺序,会自动调用基类的析构函数,不需要我们显式调用基类的析构函数。
5.取地址重载和const取地址重载
这两个函数跟以前一样,不需要做特殊处理,一般情况下我们也没必要显示实现;
四、继承与友元
友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员。
class worker;
// 基类
class person
{
public:
friend void display(const person& pobj, const worker& wobj);
protected:
string _name = "haha";
};
// 派生类
class worker : public person
{
protected:
string _job_number = "111";
};
void display(const person& pobj, const worker& wobj)
{
cout << pobj._name << endl;
cout << wobj._job_number << endl;
}
void Test1(void)
{
person pobj;
worker wobj;
display(pobj, wobj);
}
这段代码编译器会报错,提示 wobj._job_number 不可访问;
总而言之, 友元关系不可以从基类继承到派生类中,并且友元要慎用且不宜多用。