1. 继承的概念
继承是类设计层次的复用,使得程序员可以在原有类的基础上进行扩展。这样产生的新类称为派生类,原有的类称为基类。继承可以使得代码的复用程度更高,呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。
2. 基类成员在派生类中的权限变化
- 基类的private成员在派生类中不可见。
- class声明的类默认的继承方式是private,struct声明的类的继承方式是public。
继承方式 | 基类 public 成员 | 基类 protected 成员 | 基类 private 成员 |
---|---|---|---|
public | 在派生类中仍为 public | 仍为 protected | 不可访问 |
protected | 降级为 protected | 仍为 protected | 不可访问 |
private | 降级为 private | 降级为 private | 不可访问 |
【注意】1.友元关系不能够被继承。即基类的友元函数不能访问派生类的非public成员。
2.静态成员可以被继承,被派生类继承的静态成员和基类的静态成员公用相同的内 存空间。
3. 基类和派生类之间的相互赋值
- 派生类的对象可以直接赋值给基类的对象/指针/引用(向上转型安全)。
- 基类的对象不能直接赋值给派生类的对象/指针/引用。(向下转型危险)
- 如果基类是派生类的引用,或者基类类型的指针是指向派生类的,那么可以通过强制类型转换的方式将基类类型的引用,或者基类类型的指针,赋值给派生类的引用(或指针)。
4. 同名成员的重定义行为
当派生类中存在和基类中相同名称的成员变量或者成员函数(不要求相同的参数),派生类中的成员变量或者成员函数就与基类中的构成了重定义。依据就近原则,派生类会优先使用自己的这些重定义的成员。
#include <iostream>
#include <string>
using std::cout;
using std::endl;
void Test3()
{
class A
{
public:
A (int n = 0)
:_n(n)
{ }
void function() {cout << "A:" << _n << endl; };
protected:
int _n;
};
class B : public A
{
public:
B (int n = 0)
:A(n)
,_n(n)
{ }
void function()
{
++_n;
cout << "B:" << _n << endl; //优先使用派生类里重定义的成员变量
cout << "A:" << A::_n << endl; //需要使用范围限定符才可以显式使用基类的成员变量
}
protected:
int _n;
};
B b(10);
b.function(); //优先调用派生类里冲定义的成员函数
b.A::function(); //需要使用范围限定符才可以显式调用基类的成员函数
}
int main()
{
Test3();
return 0;
}
5. 派生类的默认函数
- 派生类会在构造函数里调用基类的构造函数,保证基类的对象比派生类对象更早实例化。
- 派生类会在拷贝构造函数里面调用基类的拷贝构造,完成属于基类那一部分成员的拷贝。
- 派生类会在赋值运算符重载里调用基类的赋值运算符重载。
- 派生类会在析构函数里面调用基类的析构函数,但是需要等派生类对象先完成析构。
#include <iostream>
#include <string>
// #include <windows.h>
using std::string;
using std::cout;
using std::endl;
class Person
{
public:
Person(string name = "", unsigned int age = 0, string sex = "")
:_name(name)
,_age(age)
,_sex(sex)
{ }
Person (const Person& p)
:_name(p._name)
,_age(p._age)
,_sex(p._sex)
{ }
Person& operator=(const Person& p)
{
if (this != &p)
{
_name = p._name;
_age = p._age;
_sex = p._sex;
}
return *this;
}
~Person()
{ }
void InfoMessage()
{
cout << "name:" << _name << endl;
cout << "sex:" << _sex << endl;
cout << "age:" << _age << endl;
}
protected:
string _name;
string _sex;
unsigned int _age;
};
/**
* @brief 学生类,继承了Person类
*
*/
class Student : public Person
{
public:
Student(string name = "", string sex = "", unsigned int age = 0, string stuId = "")
:Person(name, age, sex) //显式调用基类的构造函数完成基类部分的构造
,_stuId(stuId)
{ }
Student(const Student& s)
:Person(s) //const Student& ----> const Person&,调用基类的构造函数完成基类部分的构造
,_stuId(s._stuId)
{ }
Student& operator=(const Student &s)
{
if (this != &s)
{
Person::operator=(s); //const Student& ----> const Person&, 调用基类的复制运算符
_stuId = s._stuId;
}
return *this;
}
~Student()
{
//编译器会在派生类完成析构之后,默认调用基类的析构函数,确保先析构派生类,再析构基类
}
inline void InfoMessage()
{
cout << "stdId:" << _stuId << endl;
Person::InfoMessage();
}
private:
string _stuId; //学号
};
int main()
{
Student s("Anna", "female", 22, "0123456");
s.InfoMessage();
return 0;
}
6. 多继承
6.1 定义
- 单继承:一个派生类只有一个基类。
- 多继承:一个子类有多个基类。
- 菱形继承:多继承的一种,一个子类有多个基类,其中一些基类又具有共同的基类。
6.2 菱形继承存在的问题
- 数据冗余性问题
- 二义性问题
#include <iostream>
class A
{
public:
int _a = 0;
};
class B : public A
{
public:
int _b = 0;
};
class C : public A
{
public:
int _c = 0;
};
class D : public B, public C //D类属于多继承的情况
{
public:
int _d = 0;
};
int main()
{
D d;
// d._a = 1; //err,不明确这个_a是继承自类B还是类C
d.B::_a = 1; //明确指定这个_a来自哪个基类才可以,但是存在数据冗余
d.C::_a = 1; //明确指定这个_a来自哪个基类才可以,但是存在数据冗余
d._b = 2;
d._c = 3;
d._d = 4;
return 0;
}
-
内存布局
6.3 虚继承
使用关键字virtual可以解决菱形继承中的数据冗余和二义性问题。
#include <iostream>
#include <cstdint>
class A
{
public:
int _a = 0;
};
class B : virtual public A //此时A是B的虚基类
{
public:
int _b = 0;
};
class C : virtual public A //此时A是C的虚基类
{
public:
int _c = 0;
};
class D : public B, public C //D类属于多继承的情况
{
public:
int _d = 0;
};
int main()
{
D d;
std::cout << sizeof(d) << std::endl; //根据不同的编译器会有不同的结果
d._a = 1;
d._b = 2;
d._c = 3;
d._d = 4;
return 0;
}
使用虚继承之后,每个继承虚基类的类都有一个虚指针,虚指针指向虚基表,虚基表里记录了虚指针到虚基类共享成员的偏移量。
使用虚继承后的菱形继承内存分布: