前言
本系列文章承接C语言的学习,需要有++C语言的基础++ 才能学会哦~
第16篇主要讲的是有关于C++的++继承++ 。
C++已经进入进阶,加油!!
目录
继承
是实现面向对象编程的复用代码的一个重要手段。
当两个类有重复的功能或函数时,就把这些重复功能创建为父类,让这两个类去继承父类。这样一来,这两个类只用写独自独特的成员函数,重复部分都复用父类的代码即可。
普通类继承
语法
cpp
//父类(基类)
class person
{
public:
str _name;
int _number;
}
//子类(派生类)
class Student: public person
{
public:
string _class;
}
此处student类public方式继承于person类,student就也有父类的成员了。
子类访问父类成员的访问限定符限制
|----------------|----------------|----------------|--------------|
| 类成员/继承方式 | public继承 | protected继承 | private继承 |
| 基类的public成员 | 派生类public成员 | 派生类protected成员 | 派生类private成员 |
| 基类的protected成员 | 派生类protected成员 | 派生类protected成员 | 派生类private成员 |
| 基类的private成员 | 派生类中不可见 | 派生类中不可见 | 派生类中不可见 |
①可父类成员在子类中的访问方式取决于谁的限定符限制更大。比如基类protected和子类private继承,private限制大,所以就能访问方式为private,此时可以访问基类的protected和public成员。
②父类的private成员,无论如何都无法被子类直接使用。只能靠调用父类的为被限制的函数进行访问。
③使用class则默认为private继承,使用struct则默认为public继承。
继承类模板
当基类是类模板时,需要指定类域。
因为模板是按需实例化,还未被使用的父类成员函数由于未实例化,无法被找到,编译会报错,所以要指明类域来避免这种错误。
同样的,如果父类有内部类,也要注意是否被实例化的问题。
基类与派生类之间的转换
①public继承的派生类对象可以赋值给基类的指针/引用。(也叫赋值兼容转换)
cpp
Student sobj;
Person *pp = &sobj;
Person &rp = sobj;
把sobj赋值给了Person类指针/引用,这时我们使用这些指针/引用时就不会访问到sobj的独有成员。
这种操作也叫做切片、切割。
②但是基类对象不可以赋值给派生类对象。
继承的隐藏作用域
若子类和父类有同名的成员,访问时默认优先访问最近类域中的成员,我们也可以自行指明类域来访问。
那么不访问的同名成员,就会被隐藏,这些同名的成员构成隐藏关系。
被隐藏的成员无法被使用。
cpp
class A
{
public:
void fun()
{
cout << "Hi" << endl;
}
};
class B : public A
{
public:
void fun(int i)
{
cout <<"int i" << endl;
}
};
此处A类的fun被隐藏,若要使用A的fun函数,就要指定类域来使用。
派生类的默认成员函数
构造函数
派生类的默认构造,最好要自己写。对继承的父类成员,我们要用父类的构造函数进行构造。
cpp
class Person
{
public:
Person(const char* name)
:_name(name)
{}
protected:
string _name;
};
class Student : public Person
{
public:
Student(int num, const char* address, const char* name)
:_num(num)
,_address(address)
,Person(name)//父类成员使用父类的构造。
{}
protected:
int _num;
string _address;
};
父类的部分,我们把它们看作一个整体,统一走父类的构造函数来构造。
注意格式哦!
ps:若某类的默认构造为私有的,那么这个类将无法被继承。
或者被final修饰之后就不可以被继承了(关键词final在C++11支持)。
拷贝构造
一般使用默认生成的拷贝即可。同样的,父类的成员要调用父类的拷贝构造。默认生成的拷贝是浅拷贝,如果不能满足需要就要自己写一个深拷贝的拷贝构造。
要注意的是,子类的赋值运算符重载要用到父类的赋值运算符重载,父类的赋值运算符重载会被隐藏,需要指定类域来使用。
析构函数
一般用默认生成的析构即可。顺序是先析构子类,再析构父类。
当我们自己写析构函数的时候,要注意不用显示调用父类的析构时。
在默认情况下,父类的析构函数与子类的构成隐藏关系,这是规定,详细原因再有关多态的部分可以讲。
继承与友元
一句话概括,友元关系不可以继承。
继承和静态成员
一句话概括,父类和子类共用父类的静态成员
单继承、多继承和菱形继承
语法:
cpp
class Assistant:public Student,public Teacher
单继承:一个子类只有一个直接父类。

多继承:一个子类(C)有两个或以上的直接父类(A、B)。

菱形继承:某子类(D)间接多次继承同一个基类(A)。

此时因为D间接继承了两次父类A,所有会有两份父类A的成员,导致数据冗余。
多继承和菱形继承会造成数据冗余和二义性(若有同名变量可能会不发辨别是哪个类的变量,或者这个变量是哪个父类或者哪个子类的),即使二义性可以靠指定类域解决,但是数据冗余的问题无法解决。
虚继承virtual
用于解决菱形继承的二义性和数据冗余的问题。
用关键字virtual修饰菱形腰部的类(上图的B、C),编译时就只会保留一份数据。
了解即可,实际上最好不要使用多继承,即使使用多继承,也最好不要出现菱形继承的情况。
多继承的指针偏移
cpp
//D多继承于B1和B2
class B1 { public: int _b1;};
class B2 { public: int _b2;};
class D : public B1, public B2 { public: int _d;};
int main()
{
D d;
B1* p1 = &d;
B2* p2 = &d;
D* p3 = &d;
return 0;
}
三个指针都用&d来赋值,但是p1 == p3 != p2。
可以用赋值兼容转换的切割来解释。
如图

p1和p2只能指向d继承父类的部分,p1指向B1部分,p2指向B2部分。而且,虽然p1 == p3但是两个指针的含义也是不同的,p1是对继承父类B1的切片,p3指向的是整个D。
继承和组合
继承是is-a的关系(如Student类继承于Person类,即Student is a Person)。
组合是has-a的关系(如Car类有Seat类的成员,即Car has a Seat)。
两者都对代码进行了复用,能够提高开发效率。
不同的是:
|---------|------------------|--------------------|
| 特性/复用方式 | 继承(白箱复用) | 组合(黑箱复用) |
| 可见性 | 父类的成员对子类可见 | 内部细节不可见 |
| 封装性 | 破坏基类封装,派生类依赖基类实现 | 保持类的封装,组合类依赖接口而非实现 |
| 耦合性 | 高耦合 | 低耦合 |
白箱和黑箱是相对于可见性而言的,白箱可见细节,黑箱不可见细节。
耦合性高的代码,每个部分环环相扣,如果某处出错或者进行修改,就会牵一发而动全身,这对代码的维护工作来说是不利的。
因此,实际应用中尽量使用组合,主要还是看类与类之间的关系。如果是更符合is-a关系,就用继承;如果更符合has-a关系,就用组合;如果两者都符合(比如链表和栈),那就优先使用组合。
❤~~本文完结!!感谢观看!!接下来更精彩!!欢迎来我博客做客~~❤