一、前言
我们众所周知的C++三大特性分别为:封装、继承、多态。
封装就是将接口实现统一化,隐藏那些不同的地方,在上层函数调用体现的方式一样,如各种容器的迭代器iterator,尽管底层实现的方式不同,但是在使用时都是一样的,这就是封装的魅力。而今天我们接触的继承,也有别样的魅力!
二、继承的概念
比如孩子继承了父亲的家业家产,这就属于从父亲那边得到了父亲的物品归自己所用,那也就是孩子可以复用父亲的家业和家产,所以继承的概念很简单,就是复用。
在C++中,关于继承是这样定义的:
继承机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,产生新的类;
新类可以使用原有类的成员函数和成员变量,就相当于新的类继承了原有类的成员。
以前我们接触的复用都是函数复用,本篇博客的继承是类设计层次的复用。
被继承的类:
基类 / 父类;
继承 基类/ 父类 的类:
派生类 / 子类
继承的示例:
#include <iostream>
using namespace std;
//基类
class Person
{
public:void Print(){cout << "name:" << _name << endl;cout << "age:" << _age << endl;}
protected:string _name = "peter"; // 姓名int _age = 18; // 年龄
};
//派生类
class Student : public Person //继承方式
{
private:int _stuid; // 学号
};
class Teacher : public Person //继承方式
{
private:int _jobid; // 工号
};
int main()
{Student s;Teacher t;s.Print();t.Print();return 0;
}
运行结果:
结果分析:
上述代码就是一个继承的样例,Student和Teacher都继承的是Person,那Student和Teacher为派生类,Person是基类,我们可以看到Stdent和Teacher都使用了Person的Print函数,也就是说明我们成功继承了Person的类里面的内容。
但是具体继承的规则是什么样的呢?
三、继承的规则
首先我们来回顾一下访问限定符,分别为public、protected、private
它们控制的是我们是否可以在类外面来使用类里面的成员函数或成员变量。众所周知,只有public允许我们来使用类里面的成员,而private和protected是不允许的。
当派生类继承基类的成员时,是否要遵守基类的访问限定符呢?
而派生类的继承方式,是否会控制基类的访问限定符呢?
下面是继承方式与访问限定符的权限统一表格:(权限依旧是只可缩小,不可放大)
public > protected > private
类成员 \ 继承方式 | public继承 | protected继承 | private继承 |
基类的public成员 | 变成 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
基类的protected成员 | 变成 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
基类的private成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
注意事项:
- 基类private成员被派生类继承了,但是派生类没有权限访问
- 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected,所以保护成员限定符是因继承才出现的。
- 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
- 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡
- 使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。
四、基类和派生类对象的赋值转换
基类对象/指针/引用 = 派生类对象
基类的对象、指针、引用都可以被派生类对象赋值,因为派生类是继承基类的,也就是说派生类既包含了基类的成员,也拥有自己的成员。所以在派生类赋值给基类时,仅仅会赋值基类那部分,形象来说就是把派生类中基类的那部分切割给了基类。反之基类对象不能赋值给派生类对象,可以大变小,不能小变大。
注意:
- 基类对象 不能赋值给 派生类对象,但是 基类的指针或引用 可以通过强制类型转换赋值给 派生类的指针或引用,但必须是基类的指针指向派生类时才安全,否则会存在越界访问。
class Person
{
protected :string _name; // 姓名string _sex; // 性别int _age; // 年龄
};
class Student : public Person
{
public :int _No ; // 学号
};
void Test ()
{Student sobj ;
// 1.子类对象可以赋值给父类对象/指针/引用Person pobj = sobj ;Person* pp = &sobj;Person& rp = sobj;
//2.基类对象不能赋值给派生类对象sobj = pobj;
// 3.基类的指针可以通过强制类型转换赋值给派生类的指针pp = &sobj;Student* ps1 = (Student*)pp; // 这种情况转换时可以的。ps1->_No = 10;pp = &pobj;Student* ps2 = (Student*)pp; // 这种情况转换时虽然可以,但是会存在越界访问ps2->_No = 10;
}
存在越界访问的原因:
首先明确一点,pp指向的是Person的对象,所以无论怎么对其类型转换,都是指向的Person的对象,pp开始可以去访问Person里的成员,因为这是它自身的权限,但是当强制类型转换为Student*,就将其访问权限扩大了,因为在Student中有_No成员,所以强制类型转换后理应可以去访问,但是别忘了我们pp指向的是Person 的对象,而Person对象中压根就没有_No成员,所以访问就会越界。
五、继承中的作用域
我们知道作用域其实就是花括号{ },一个花括号代表了一个作用域,所以在继承体系中,基类和派生类都有自己的花括号,也就是说明,基类和派生类都有自己独立的作用域。
当基类和派生类中有同名的成员,我们访问该成员,是访问的基类,还是派生类?
当基类和派生类中有同名成员,派生类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。
说白了就是有同名成员,基类的成员被隐藏,只能访问到派生类的成员,若想访问基类的同名成员,需要显示调用:基类::基类成员
成员变量构成重定义,只需变量名相同
成员函数构成重定义,只需函数名相同
class A
{
public:int Print(){cout << "A" << endl;return 0;}int _a;};
class B:public A
{
public:void Print() //成员函数,只需要函数名相同就构成隐藏{cout << "B" << endl;}
public:int _a;
};
void Test()
{B b;//访问同名成员,只会访问到派生类的b.Print();b._a = 3;//显示访问cout << b.B::_a << endl;cout << b.A::_a << endl;
}
int main()
{Test();return 0;
}
注意:
不要给自己找麻烦,最好不要定义同名成员
为什么是构成重定义,而不是重载?
因为构成重载的前提条件是在同一个作用域中,而基类和派生类属于不同的作用域
六、派生类的默认成员函数
1. 构造函数
派生类对象初始化: 先调用 基类构造 再调 派生类构造
派生类的构造函数会自动调用基类的默认构造函数;
如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
2. 拷贝构造函数
派生类的默认的拷贝构造函数会自动调用基类的默认的拷贝构造;
如果基类没有默认的拷贝构造,则必须在派生类构造函数的初始化列表阶段显示调用。
3. 赋值运算符重载
派生类的operator=必须要调用基类的operator=完成基类的复制。
4. 析构函数
派生类对象析构清理: 先调用 派生类析构 再调 基类的析构
派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
5. 代码
class Person
{
public://构造函数Person(){cout << "Person()" << endl;}Person(const string& name, const int& age): _name(name), _age(age){cout << "Person(const string& name, const int& age)" << endl;}//拷贝构造Person(const Person& p): _name(p._name), _age(p._age){cout << "Person(const Person& p)" << endl;}//赋值运算符重载Person& operator=(const Person& p){cout << "Person& operator=(const Person* p)" << endl;if(this != &p){_name = p._name;_age = p._age;}return *this;}//析构函数~Person(){cout << "~Person()" << endl;}
protected:string _name;int _age;
};
class Student:public Person
{
public:Student(){cout << "Student()" << endl;}Student(const string& name, const int& age, const int& id): Person(name, age), _id(id){cout << "Student(const string& name, const int& age, const int& id)" << endl;}Student(const Student& s): Person(s), _id(s._id){cout << "Student(const Student& s)" << endl;}Student& operator=(const Student& s){if(this != &s){Person::operator=((Person&)s);cout << "Student& operator=(const Student* s)" << endl;_id = s._id;}return *this;}~Student(){cout << "~Student()" << endl;}void Print(){cout << _name << endl;cout << _age << endl;cout << _id << endl;}
private:int _id;
};
七、继承与友元的关系
友元函数可以访问类的私有和保护成员,但是继承关系中,友元不会被继承。所以基类友元不可以使用派生类的私有或保护成员,因为爸爸的朋友,不是我的朋友。
结论:友元函数只能访问被友元的那个类的成员,不会被继承
class B;
class A
{
public:friend void Print(const A& a, const B& b);
private:int _a;
};class B:public A
{
public:
private:int _b;
};
void Print(const A& a, const B& b)
{cout << a._a << endl;cout << b._b << endl; //无法访问
}
八、继承与静态成员
首先回顾一下类的静态成员:
类中定义的静态成员,是属于整个类的,并不属于任何一个实例化对象,所以无论实例化多少个对象,静态成员只有一个。
回到继承,我们知道派生类会继承基类的成员,那就也一定会继承静态成员,由于静态成员在基类只有一份,所以在继承之后,基类和派生类共用这一份静态成员,所以无论派生出多少个派生类,也只有这一份静态成员。
结论:静态成员可以被派生类继承,但是整个继承体系中只有一份!
class A
{
public:static void Print(){cout << "_a = " << _a << endl;}
protected:static int _a;int _aa;
};
int A::_a = 2; //静态成员变量要在类外面初始化,并表明作用域class B:public A
{
public:static void Add() //静态成员函数,只能使用静态成员{ //因为静态成员函数属于整个类,不属于哪个实例化对象++_a; //所以静态成员函数里没有this指针,就无法使用非静态成员}
private:int _b;
};
九、单继承、多继承、菱形继承、菱形虚拟继承
1. 单继承
单继承:一对一的继承
class A
{
public:void APrint(){cout << "_a = " << _a << endl;}
protected:int _a = 1;
};
class B:public A
{
public:void BPrint(){APrint();cout << "_b = " << _b << endl;}
protected:int _b = 2;
};
class C:public B
{
public:void CPrint(){BPrint();cout << "_c = " << _c << endl;}
private:int _c = 3;
};
2. 多继承
多继承:一个派生类有两个或以上的直接基类
class A
{
public:void APrint(){cout << "_a = " << _a << endl;}
protected:int _a = 1;
};
class B
{
public:void BPrint(){cout << "_b = " << _b << endl;}
protected:int _b = 2;
};
class C:public A , public B
{
public:void CPrint(){APrint();BPrint();cout << "_c = " << _c << endl;}
private:int _c = 3;
};
3. 菱形继承
菱形继承:如下图
class P
{
public:void PPrint(){cout << "_p = " << _p << endl;}
protected:int _p = 0;
};
class A:public P
{
public:void APrint(){cout << "_a = " << _a << endl;++_p;PPrint();}
protected:int _a = 1;
};
class B:public P
{
public:void BPrint(){cout << "_b = " << _b << endl;++_p;PPrint();}
protected:int _b = 2;
};
class C:public A , public B
{
public:void CPrint(){APrint();BPrint();cout << "_c = " << _c << endl;}
private:int _c = 3;
};
运行结果:
首先继承方法是C继承A,B;A继承P,B继承P;从运行结果来看会发现P中的成员在A和B中分别拷贝了一份,A和B中的P是独立的,不同的。这就会使数据产生二义性和冗余
当我们去访问这个_p时,就不知道是访问的哪个_p,所以下面的代码就会有问题,在函数Add_P中,就不知道是访问哪个_p。
class P
{
public:
protected:int _p = 0;
};
class A:public P
{
public:
protected:int _a = 1;
};
class B:public P
{
public:
protected:int _b = 2;
};
class C:public A , public B
{
public:void Add_P(){_p = _p + 6; //不知道访问的哪个_p}
private:int _c = 3;
};
4. 菱形虚拟继承
菱形虚拟继承就是用来解决菱形继承问题,防止数据二义性和冗余。
class P
{
public:
protected:int _p = 0;
};class A:virtual public P
{
public:
protected:int _a = 1;
};class B:virtual public P
{
public:
protected:int _b = 2;
};
class C:public A , public B
{
public:void Add_P(){_p = _p + 6; }
private:int _c = 3;
};
十、继承与组合
- 继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
- 组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
- 优先使用对象组合,而不是类继承
继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用。
术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。
对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用,因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。
实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,可以用组合,就用组合。
十一、继承的反思
- 最好不要实现多继承,因为会引发菱形继承
- 能用组合用组合,避免使用继承,因为继承会破坏封装,提高耦合度
十二、继承的笔试面试题
1. 什么是菱形继承?菱形继承的问题是什么?
菱形继承就是一个派生类继承两个类,而这两个类继承同一个基类
问题:在初始化对象的时候会产生数据冗余和二义性
2. 什么是菱形虚拟继承?如何解决数据冗余和二义性的?
某几个类同时继承同一个类,在这几个类的继承方式前加virtual关键字,形成菱形继承
解决方法:通过虚基表指针,找到虚基表中的偏移量,再通过地址的偏移量来找到共有的数据地址。
3. 继承和组合的区别?什么时候用继承?什么时候用组合?
继承和组合的最大区别就是 数据的耦合度
继承的数据耦合度高,当改变基类的数据时,会影响派生类的成员;
但组合的耦合度低,因为基类的成员变量对派生类是有访问权限的,所以基类的成员变量改变,不会影响到派生类。
当我们要实现多态的时候,必须用继承;剩下的情况能用组合就用组合