【C++】继承
概念
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保
持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象
程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继
承是类设计层次的复用。
被继承的类称为基类/父类,而继承基类/父类的类称为派生类/子类
子类是在父类的基础上进行了特化,从而延展出特定的功能,体现了类设计代码的复用
例如,Person
类是父类,而Teacher
、Student
等类继承了Person
类,属于子类,除了具备父类的属性和方法,还各自特化出了不同的属性和方法
定义
以Person
和Student
为例,类的继承定义如下:
class Person
{
// ...
};
// 子类 继承方式 父类
class Student :public Person
{
// ...
};
其中,Student
为子类,public
为继承方式,Person
为父类
继承方式与访问权限
父类的成员有public
、protected
、private
三种访问限定符,而子类也有public
、protected
、private
三种继承方式
这样组合起来,子类继承后的成员就有以下9种情况
父类成员/继承方式 | public | protected | private |
---|---|---|---|
父类的public成员 | 子类的public成员 | 子类的protected成员 | 子类的private成员 |
父类的protected成员 | 子类的protected成员 | 子类的protected成员 | 子类的private成员 |
父类的private成员 | 子类中不可见 | 子类中不可见 | 子类中不可见 |
通过上表,我们可以知道:
-
子类的成员权限 = min(父类成员的访问限定符,继承方式)
-
父类的
private
成员,无论以何种方式继承,在子类中都不可见。不可见是指:在子类中存在,但无论在类中还是在类外,都不可直接访问class Person { public: string _name; private: // 私有成员,继承后子类不可直接访问 int _age; }; class Student :public Person { public: void func() { // 访问从父类继承的公有成员 cout << _name << endl; // 访问从父类继承的私有成员 cout << _age << endl; } protected: int _studentid; };
-
父类的
private
成员在子类中不可访问。如果不想父类的成员在类外被访问,同时想被子类访问,那父类的成员就可以用protected
修饰,而不是private
class Person { public: string _name; protected: // 保护成员,继承后在子类中可直接访问,在类外不可直接访问 int _age; }; class Student :public Person { public: void func() { cout << _name << endl; // 类中访问保护成员 cout << _age << endl; } protected: int _stuid; }; int main() { Student s1; s1._name = "abc"; // 类外访问保护成员 _age s1._age = 10; return 0; }
- 一般情况下,建议继承方式为
public
,因为protected
/private
继承后的子类成员不能在类外访问,使用起来过于不便
父类和子类对象的赋值转换
通过上面,我们已经了解到:每一个子类对象都是一个特殊的父类对象,这就可以让子类对象赋值给父类对象
子类对象将不属于父类对象的成员切割,将属于父类对象的成员赋值给父类对象。这种赋值方式被称为切片或者切割
下面我们通过代码来验证一下
class Person
{
protected:
string _name;
string _sex;
int _age;
};
class Student :public Person
{
public:
void Init(string name, string sex, int age, int sid)
{
_name = name;
_sex = sex;
_age = age;
_stuid = sid;
}
protected:
int _stuid;
};
int main()
{
Person p1;
Student s1;
// 给子类对象赋值
s1.Init("张三", "男", 18, 111);
// 子类对象赋值给父类对象
p1 = s1;
return 0;
}
我们通过调试来查看结果:
那能不能反过来,父类对象赋值给子类对象呢?尝试一下
很明显,结果是不可以
此外,切片赋值同样适用于指针与引用,我们可以将子类对象的指针/引用赋值给父类对象
int main()
{
Person p1;
Student s1;
// 给子类对象赋值
s1.Init("张三", "男", 18, 111);
// 指针赋值
Person* pp = &s1;
// 引用赋值
Person& pf = s1;
return 0;
}
继承中的作用域
我们知道,每一个类都是一个独立的域,虽然子类继承于父类,但子类和父类都是独立的作用域
子类和父类作用域独立,是否就意味着子类成员可以和父类成员同名呢?
——可以同名,但是会出现这样的现象:子类会屏蔽对父类同名成员的直接访问,默认访问子类的同名成员,这叫做隐藏,也叫重定义
下面让我们来看一看具体是怎样的
// 父类A中的 _num 与 子类中的 _num 相同,构成隐藏
class A
{
protected:
string _name = "张三";
int _num = 111;
};
class B :public A
{
public:
void Print()
{
cout << "_name->" << _name << endl;
cout << "_num->" << _num << endl;
}
protected:
int _num = 222;
};
实例化一个B对象,看看结果:
int main()
{
B b;
b.Print();
return 0;
}
对于同名成员,默认访问的是子类的成员。如果想访问父类的同名成员,可以这样:父类::父类成员
class B :public A
{
public:
void Print()
{
cout << "_name->" << _name << endl;
// 访问父类同名成员
cout << "A::_num->" << A::_num << endl;
cout << "B::_num->" << _num << endl;
}
protected:
int _num = 222;
};
以上是同名成员变量的隐藏,而同名函数也会隐藏,函数名相同即可形成隐藏
// 父类 A 中的 fun 与子类 B 中的 fun 同名,构成隐藏
class A
{
public:
void fun()
{
cout << "fun()->A" << endl;
}
};
class B :public A
{
public:
void fun(int i)
{
cout << "fun(int i)->B" << endl;
}
};
int main()
{
B b;
b.fun(10);
return 0;
}
运行结果:
注意:B
中的fun
与A
中的fun
虽然函数名相同参数不同,但是并不构成重载,因为它们并不在同一个作用域
如果想访问父类的同名函数,同样可以这样:父类::父类成员
int main()
{
B b;
b.fun(10);
b.A::fun();
return 0;
}
子类的默认成员函数
默认,意思是不用我们写,编译器就会自动生成,那么在子类中默认成员函数又是如何生成的?下面我们就来看一下
构造
子类在构造时可以分为两部分来构造:父类的成员,子类的成员
子类成员构造时
- 子类成员是内置类型,那就不做处理
- 子类成员是自定义类型,则调用自定义类型的构造
父类成员构造时
- 父类如果有默认构造,则调用父类的默认构造
- 父类如果没有默认构造,那么必须在子类构造的函数的初始化列表显示调用父类的构造函数
默认构造有以下3种
- 自己写的无参构造函数
- 自己写的全缺省构造函数
- 自己没写,编译器自动生成的构造函数
下面是父类有默认构造的例子
class Person
{
public:
// 全缺省的默认构造
Person(const char* name = "张三")
:_name(name)
{
cout << "Person()" << endl;
}
protected:
string _name; // 姓名
};
class Student :public Person
{
public:
// 没有写默认构造,编译器自己生成默认构造
// 父类部分会调用父类的构造
// 子类部分,内置类型不处理,自定义类型调用其构造
protected:
int _num; // 学号
};
实例化一个Student
对象s1
,通过调试查看s1
的成员:
可以看到,编译器生成的默认构造函数中,父类部分确实调用了自己的构造,而子类部分的_num
是内置类型,所以不做处理
下面我们来看父类中没有默认构造的情况
class Person
{
public:
// 构造函数,非默认构造
Person(const char* name)
:_name(name)
{
cout << "Person()" << endl;
}
protected:
string _name; // 姓名
};
class Student :public Person
{
public:
// 没有写默认构造,编译器自己生成默认构造
// 父类部分会调用父类的构造
// 子类部分,内置类型不处理,自定义类型调用其构造
protected:
int _num; // 学号
};
父类中的构造函数,既不是无参,也不是全缺省,所以不是默认构造;而只要我们写了构造函数之后,编译器也不会去生成默认构造了。这就导致父类中没有默认构造
这时再去实例化一个Student
对象s1
,运行就会报错:
在父类中没有默认构造的情况下,子类中就不能用编译器生成的默认构造了,需要我们自己去写构造函数。通常情况下,为了满足我们的需要,我们都会自己写构造函数,不会去用编译器生成的默认构造
在子类的构造函数中,需要在初始化列表阶段对父类进行构造
先来看一下错误的构造方式:
class Person
{
public:
// 非默认构造
Person(const char* name)
:_name(name)
{
cout << "Person()" << endl;
}
protected:
string _name; // 姓名
};
class Student :public Person
{
public:
Student(const char* name, int num)
:_name(name) // _name 为父类成员
,_num(num)
{
cout << "Student()" << endl;
}
protected:
int _num; // 学号
};
报错显示_name
不是基或成员,这就表明:初始化时,要么是对子类成员初始化,要么是对父类初始化
子类的初始化列表阶段,要将父类当成一个整体进行构造,而不是对父类的成员初始化,以下是正确的构造
class Person
{
public:
// 非默认构造
Person(const char* name)
:_name(name)
{
cout << "Person()" << endl;
}
protected:
string _name; // 姓名
};
class Student :public Person
{
public:
Student(const char* name, int num)
:Person(name) // 将父类视为一个整体,调用父类构造
,_num(num)
{
cout << "Student()" << endl;
}
protected:
int _num; // 学号
};
运行结果:
总结:子类中的父类成员要单独看作一个整体,要将其当成一个整体的自定义类型。
后面要说的默认成员函数也是这样的,就不再花很大的篇幅解释了
拷贝构造
在子类中的拷贝构造中,同样要将父类成员视作整体,要调用父类的拷贝构造,形式如下
父类拷贝构造
// 父类拷贝构造
Person(const Person& p)
{
cout << "Person(const Person& p)" << endl;
if (this != &p)
_name = p._name;
}
子类拷贝构造
// 子类拷贝构造
Student(const Student& s)
:Person(s) // 将父类当作整体,调用父类拷贝构造
,_num(s._num)
{
cout << "Student(const Student& s)" << endl;
}
在子类的拷贝构造中,调用父类拷贝构造将子类对象传给父类,发生了切片
下面我们测试一下
class Person
{
public:
// 默认构造
Person(const char* name = "张三")
:_name(name)
{
cout << "Person()" << endl;
}
// 拷贝构造
Person(const Person& p)
{
cout << "Person(const Person& p)" << endl;
if (this != &p)
_name = p._name;
}
protected:
string _name; // 姓名
};
class Student :public Person
{
public:
// 构造
Student(const char* name, int num)
:Person(name)
,_num(num)
{
cout << "Student()" << endl;
}
// 拷贝构造
Student(const Student& s)
:Person(s) // 将父类当作整体,调用父类拷贝构造
,_num(s._num)
{
cout << "Student(const Student& s)" << endl;
}
protected:
int _num; // 学号
};
int main()
{
Student s1("李四", 18);
Student s2(s1);
return 0;
}
运行结果:
赋值重载
没什么好说的,和上面一样,子类的赋值重载,要单独调用父类的赋值重载
注意:父类和子类的赋值重载函数名相同,会构成隐藏,因此在子类中调用父类赋值重载时,要显式 调用
class Person
{
public:
// 默认构造
Person(const char* name = "张三")
:_name(name)
{
cout << "Person()" << endl;
}
// 拷贝构造
Person(const Person& p)
{
cout << "Person(const Person& p)" << endl;
if (this != &p)
_name = p._name;
}
// 赋值重载
Person& operator=(const Person& p)
{
cout << "Person& operator=(const Person& p)" << endl;
if (this != &p)
_name = p._name;
return *this;
}
protected:
string _name; // 姓名
};
class Student :public Person
{
public:
// 构造
Student(const char* name, int num)
:Person(name)
,_num(num)
{
cout << "Student()" << endl;
}
// 拷贝构造
Student(const Student& s)
:Person(s) // 将父类当作整体,调用父类拷贝构造
,_num(s._num)
{
cout << "Student(const Student& s)" << endl;
}
// 赋值重载
Student& operator=(const Student& s)
{
cout << "Student& operator=(const Student& s)" << endl;
if (this != &s)
{
// 显式调用
Person::operator=(s);
_num = s._num;
}
return *this;
}
protected:
int _num; // 学号
};
int main()
{
Student s1("李四", 18);
Student s2("王五", 20);
// 赋值
s2 = s1;
return 0;
}
析构
到了析构这里,我们依然沿用前面的思想,在子类中调用父类的析构
// 父类析构
~Person()
{
cout << "~Person()" << endl;
}
// 子类析构
~Student()
{
~Person();
cout << "~Student()" << endl;
}
然后发生了报错:
可以看到,这里将~
识别为按位取反,并未识别出~Person
是析构函数,这是为什么?
这是因为父类和子类的析构函数,构成了隐藏。~Person
和~Student
名字并不相同,却构成了隐藏
其实这是C++多态后面挖的坑,为了满足多态,析构函数的名字会被统一处理为destructor
,所以在这里子类的析构会隐藏父类
所以我们需要显式调用父类的析构
// 子类析构
~Student()
{
// 显式调用
Person::~Person();
cout << "~Student()" << endl;
}
实例化一个Student
对象s1
,s1生命周期结束,自动调用析构
然后出现了一个问题:父类的析构多调用了一次
从程序的运行结果可以看出:多调用的一次是在子类析构调用结束后才调用的,这就说明子类析构调用结束后会自动调用父类的析构
要说为什么,规定是这样的:构造时先构造父类,再构造子类;析构时先析构子类,再析构父类
析构先子后父,还为了预防这种情况:先析构了父类,子类就不能访问父类成员了
~Student()
{
Person::~Person();
// 访问父类成员
cout << _name << endl;
cout << "~Student()" << endl;
}
还有两个默认成员函数:普通对象和const对象取地址重载,由于这两个基本不需要自己写,这里就不讲了
继承与友元
当父类中存在友元时,子类是继承不到的。这就导致父类中的友元访问不到子类的protected
或private
成员
class Student;
class Person
{
public:
// 父类的友元
friend void Display(const Person& p, const Student& s);
protected:
string _name;
};
class Student : public Person
{
protected:
int _stuid;
};
// 由于子类未继承父类的友元,所以访问不了子类的保护成员
void Display(const Person& p, const Student& s)
{
cout << p._name << endl;
cout << s._stuid << endl;
}
int main()
{
Person p;
Student s;
Display(p, s);
return 0;
}
运行结果:
子类不能继承父类的友元,那只能由我们手动加上了
class Student : public Person
{
public:
// 友元
friend void Display(const Person& p, const Student& s);
protected:
int _stuid;
};
继承与静态成员
父类中定义了一个静态成员,那么这个父类的继承体系中就共用这个静态成员。无论这个父类派生出多少子类,都不会产生新的静态成员。
以下是Person
类的一个继承体系
class Person
{
public:
Person()
{
_count++;
}
protected:
string _name;
public:
// 静态成员,用来计数
static int _count;
};
int Person::_count = 0;
class Student :public Person
{
protected:
int _stuid;
};
class Graduate :public Student
{
protected:
int _course; // 研究科目
};
我们可以将父类和子类的静态成员_count
的地址取出,查看是否相同;同时可以实例化数个子类和父类,子类构造时会调用父类构造,都会使静态成员_count
加一
int main()
{
Person p;
Student s;
Graduate g;
// 查看地址
cout << &Person::_count << endl;
cout << &Student::_count << endl;
cout << &Graduate::_count << endl;
// 查看_count的值
cout << "Person::_count->" << Person::_count << endl;
// 在子类中将_count置零
Student::_count = 0;
cout << "Person::_count->" << Person::_count << endl;
return 0;
}
可以看到,在同一个继承体系中,定义的某个静态成员只有一个,是共用的
菱形继承
单继承
当一个子类只有一个直接父类时,这种继承关系称为单继承
多继承
当一个子类有两个或以上的父类时,这种继承关系称为多继承
菱形继承
菱形继承也是多继承,特殊的多继承
class Person
{
public:
string _name;
};
class Student : public Person
{
protected:
int _stuid;
};
class Teacher :public Person
{
protected:
int _id;
};
class Assistant : public Student, public Teacher
{
protected:
string _course;
};
菱形继承存在两个问题:
- 数据冗余
- 二义性
数据冗余
从结构图可以看出,Assistant
类继承了Student
类和Teacher
类,而Student
类和Teacher
类又继承了Person
类,这就导致Assistant
类中有两份Person
类数据,造成数据冗余
二义性
当Assistant
类要访问Person
类成员_name
时,由于_name
有两份,无法明确访问哪一个
int main()
{
Assistant a;
// 访问存在二义性
a._name;
}
显式写是哪个父类的成员可以解决二义性问题
int main()
{
Assistant a;
a.Student::_name = "张三";
cout <<"Student::name->" << a.Student::_name << endl;
}
这样虽然解决了菱形继承的二义性问题,但是没有解决数据冗余问题
虚拟继承
虚拟继承可以解决菱形继承的二义性及数据冗余问题。以前面的Assistant为例,虚拟继承会将两个_name
合并为一个,变成Student
和Teacher
公用的
在腰部的类继承时,加上virtual
即可构成虚拟继承
注意:只有发生菱形继承时要用虚拟继承,其他地方不要用
下面我们建一个简单的继承体系,通过调试来看一下虚拟继承到底是怎样的
先来看菱形继承情况下,类对象中成员在物理空间的存储情况
class A
{
public:
int _a;
};
class B : public A
{
public:
int _b;
};
class C : public A
{
public:
int _c;
};
class D : public B, public C
{
public:
int _d;
};
实例化一个D对象,并为其成员赋值
int main()
{
D d;
// B类
d.B::_a = 1;
d._b = 2;
// C类
d.C::_a = 3;
d._c = 4;
d._d = 5;
return 0;
}
然后用调试的内存窗口,取d
的地址,查看d
的内存:
可以看到,内存中的数值与我们给d
成员的赋值匹配
因为D
类是先继承的B
类,后继承的C
类,所以先存B
类成员,再存C
类成员,最后是D
类成员
如果先继承C
类再继承B
类,那么就先存C
类成员,再存B
类成员,这里就不做演示了
class D : public C, public B
下面使用虚拟继承
class A
{
public:
int _a;
};
// 在腰部使用虚拟继承
class B : virtual public A
{
public:
int _b;
};
// 在腰部使用虚拟继承
class C : virtual public A
{
public:
int _c;
};
class D : public B, public C
{
public:
int _d;
};
int main()
{
D d;
// B类
d.B::_a = 1;
d._b = 2;
// C类
d.C::_a = 3;
d._c = 4;
d._d = 5;
return 0;
}
与前面的步骤相同, 查看虚拟继承下的D
类对象
可以看到,在D
类对象中,A
类成员确实变为了公共的,并且被放到了D
类的最下面
A就叫做虚基类,虚基类是公共的,被放在类的最下面
了解了什么是虚基类后,再来看另一个问题:B
类和C
类中原来是A
类成员的地方,变成了两个指针,这两个指针代表了什么?
我们直接去这两个指针指向的地方看一下
两个指针指向的位置的下一个位置,各自存了一个数:14
和0c
,换算为十进制就是20
和12
这两个数字又是什么?这时我们看B
和C
的两个指针的地址与虚基类的地址的偏移量,恰好是20
和12
,就是说,通过偏移量,可以找到下面的虚基类
此时我们来做个总结:A为虚基类,B和C的两个指针称为虚基表指针,指向的是两张表,叫做虚基表。虚基表存的是偏移量,通过偏移量可以找到虚基类
到这里,我们可以感受到为了解决多继承的菱形继承问题,虚拟继承的底层实现是很复杂的
关于继承的总结与反思
-
多继承的语法复杂。有了多继承,就有可能写出菱形继承,为了解决菱形继承的问题,又需要用到虚拟继承。而虚拟继承的底层又是很复杂的,使用起来容易出问题
-
多继承算是C++的缺陷之一,以至于后来的语言有继承但没有多继承,例如
Java
-
继承和组合
-
我们已经了解了继承,组合是什么?
组合与继承类似,例如
B
类继承A
类,用组合可以这样实现:class A { protected: int _a; }; // B中有A,即为组合 class B { protected: A _A; };
-
继承是
is-a
的关系,即每一个子类都是一个父类对象
组合则是has-a
的关系,B
组合A
,则表示B
中有A
-
优先使用对象组合,而不是类继承
-
在继承中,父类的成员对子类可见,在一定程度上破环了父类的封装;而父类改变,子类也会受影响。父类与子类之间依赖关系很强,导致耦合度高
-
而在组合中,组合对象的内部细节是不可见的,相互之间也不易影响,所以组合类之间没有很强的依赖关系,耦合度低
-
因为继承耦合度高,组合耦合度低,所以为了更好的代码维护性,推荐使用组合。当然也不是不能使用继承,适合用的场景就用,而且多态的实现也离不开继承。
结束,再见 😄
-