继承
需要预先学习:
1. 继承
继承(inheritance)让一个新类可以获得另一个类的成员,从而复用代码,并表达"是一种"(is-a)的关系。
学生、教师都是人,都有姓名、年龄,都会打招呼。把这些共性提取到 Person 类里,让 Student、Teacher 从它继承,各自只需写独有的部分——不必把姓名、年龄在每个类里重复一遍。
- 基类(base class,也叫父类):被继承的类
- 派生类(derived class,也叫子类):继承别人的类
“学生是一个人”——这种 is-a 关系是继承的核心:派生类对象可以当作基类对象来用。
1.1 继承的语法
继承写在类名后面的冒号处:
class Student : public Person // Student 公开继承 Person
{
// 这里只需写 Student 独有的成员
};
#include <iostream>
#include <string>
class Person
{
public:
std::string m_name;
int m_age = 0;
void say_hello() { std::cout << "你好,我是" << m_name << std::endl; }
};
class Student : public Person
{
public:
std::string m_school;
};
int main()
{
Student s;
s.m_name = "张三"; // 继承自 Person 的成员变量
s.m_age = 15;
s.m_school = "第一中学"; // Student 自己的成员变量
s.say_hello(); // 继承自 Person 的成员函数
std::cout << "就读于" << s.m_school << std::endl;
return 0;
}
编译并运行代码后,看到这样的输出:
你好,我是张三
就读于第一中学
Student 的对象同时拥有 Person 的成员和 Student 自己的成员,可以直接使用继承来的成员,就像它们本来就属于 Student 一样。
1.2 继承方式与访问权限
继承时同样要写一个访问权限限定符,它决定基类成员在派生类中的访问权限:
| 基类成员 | public 继承 |
protected 继承 |
private 继承 |
|---|---|---|---|
public |
仍是 public |
变为 protected |
变为 private |
protected |
仍是 protected |
仍是 protected |
变为 private |
private |
不可直接访问 | 不可直接访问 | 不可直接访问 |
- 无论哪种继承方式,基类的
private成员在派生类中都不可直接访问——但它们仍然存在于派生类对象里,只是必须通过基类的公开接口间接使用 public继承最常用,也最符合 is-a 的语义;protected、private继承较少使用,本章只以public继承为主
protected 与 private 的区别
在 封装 里两者看起来一样(类外部都不可访问),差别在于派生类:
private:只有本类的成员函数能访问protected:本类和派生类的成员函数都能访问
#include <iostream>
#include <string>
class Animal
{
public:
void eat() { std::cout << "吃东西" << std::endl; }
protected:
std::string m_kind = "动物"; // 派生类可以访问
private:
int m_weight = 10; // 派生类也不可访问
};
class Dog : public Animal
{
public:
void show()
{
std::cout << "我是一只" << m_kind << std::endl; // protected 成员,派生类可以访问
std::cout << m_weight << std::endl; // 错误!private 成员,派生类不可访问
}
};
int main()
{
Dog d;
d.show();
return 0;
}
编译时报错:
error: 'int Animal::m_weight' is private within this context
所以设计基类时,把"希望派生类直接使用、但不希望外界碰"的成员声明为 protected;只希望本类自己使用的成员声明为 private。
1.3 继承中的构造函数与析构函数
派生类对象在创建时,会先构造其中的基类部分,再构造派生类自己的部分——就像盖楼先打地基、再建上层。销毁时的顺序正好相反。
#include <iostream>
#include <string>
class Person
{
public:
Person(const std::string& name) : m_name(name)
{
std::cout << "Person 构造: " << m_name << std::endl;
}
~Person()
{
std::cout << "Person 析构: " << m_name << std::endl;
}
private:
std::string m_name;
};
class Student : public Person
{
public:
Student(const std::string& name, const std::string& school)
: Person(name), m_school(school) // 在初始化列表中调用基类构造函数
{
std::cout << "Student 构造: " << m_school << std::endl;
}
~Student()
{
std::cout << "Student 析构: " << m_school << std::endl;
}
private:
std::string m_school;
};
int main()
{
Student s("张三", "第一中学");
return 0;
}
编译并运行代码后,看到这样的输出:
Person 构造: 张三
Student 构造: 第一中学
Student 析构: 第一中学
Person 析构: 张三
规则:
- 创建:先基类构造函数,后派生类构造函数;销毁:先派生类析构函数,后基类析构函数
- 派生类构造函数可以在初始化列表中调用基类构造函数,把参数传过去
- 如果基类没有默认构造函数,派生类构造函数必须显式调用基类的某个构造函数,否则编译报错
- 派生类不继承基类的构造函数;如果不显式写,编译器会自动生成一个默认构造函数,它先调用基类的默认构造函数
1.4 名字隐藏
如果派生类定义了与基类同名的函数,基类的所有同名重载都会被隐藏(name hiding)——即使参数列表不同:
#include <iostream>
class Base
{
public:
void func() { std::cout << "Base::func()" << std::endl; }
void func(int) { std::cout << "Base::func(int)" << std::endl; }
};
class Derived : public Base
{
public:
void func() { std::cout << "Derived::func()" << std::endl; }
};
int main()
{
Derived d;
d.func();
d.func(1); // 错误!Base 的 func(int) 被隐藏了
return 0;
}
编译时报错:
error: no matching function for call to 'Derived::func(int)'
编译器在派生类里找到 func 这个名字后就不再向上查找,于是基类的重载统统看不见了。有两种解决办法:
- 用类名限定调用:
d.Base::func(1); - 在派生类里用
using Base::func;把基类的所有同名重载都引入进来
1.5 派生类对象的内存布局
派生类对象在内存中,先存放基类部分(基类子对象),再存放派生类新增的成员:
Student 对象的内存布局
+------------------------+
| Person 部分 | 基类子对象:m_name、m_age
+------------------------+
| m_school | Student 新增成员
+------------------------+
把派生类对象的地址赋给基类指针时,指针指向的正是最前面这段基类子对象。这个布局在 多态 里讨论虚函数表时还会用到。


