以下为个人学习笔记整理。参考书籍《C++ Primer Plus》
# 类继承
类继承可以提供到的一些功能:
- 在已有类的基础上添加新的功能。
- 可以给类添加数据。
- 可以修改类方法的行为。
# 一个简单的基类
编写一个简单的 Worker
类:
// .h | |
#pragma once | |
#ifndef BASE_CLASS_H | |
#define BASE_CLASS_H | |
#include <string> | |
using std::string; | |
class Worker { | |
private: | |
string frist_name; | |
string last_name; | |
bool has_work; | |
public: | |
Worker(const string& fn = "none", const string& ln = "none", bool hw = false); | |
void Name() const; | |
bool HasWork() const { return has_work; }; | |
void SetWorkState(bool state) { has_work = state; }; | |
}; | |
#endif // !BASE_CLASS_H |
// .cpp | |
#include"base_class.h" | |
#include<iostream> | |
Worker::Worker(const string& fn, const string& ln, bool hw) :frist_name(fn), last_name(ln), has_work(hw) {} | |
void Worker::Name() const { | |
std::cout << last_name << ", " << frist_name; | |
} |
# 生成一个派生类
创建一个类 Engineer
,继承自 Worker
。
class Engineer : public Worker { | |
//... | |
}; |
上述代码完成了如下工作:
- 派生类对象存储了基类的数据成员。(派生类继承了基类的实现)
- 派生类对象可以使用基类的方法。(派生类继承了基类的接口)
因此 Engineer
可以记录姓名(使用属性)以及方法( Name()、HasWork()、SetWorkState()
)
# 需要在继承特性中加入些什么呢🤔?
- 派生类需要自己的构造函数。
- 派生类可以根据需要添加额外的数据成员和成员函数。
# 让我们来加个码代码的接口:
现在 Engineer
出了名字和工作外,还有了 title
以及 Code
的能力~
// .h | |
class Engineer : public Worker { | |
private: | |
string title; // Engineer title | |
public: | |
Engineer(const string& fn, const string& ln, bool hw = false, const string& t = "none"); | |
void Code() const; | |
}; | |
//.cpp | |
// 构造函数内别忘了初始化基类「Worker」。 | |
Engineer::Engineer(const string& fn, const string& ln, bool hw, const string& t) :title(t),Worker(fn,ln,hw) {} | |
void Engineer::Code() const { | |
Name(); | |
std::cout << "[" << title << "]: coding..."; | |
} |
但是有一点需要注意: Engineer
类中不能直接访问 Worker
的私有成员:
# 构造函数:访问权限考虑
由于派生类不能直接访问基类的私有成员,而必须通过基类提供的方法进行访问。
因此,初始化构造函数时,对于基类成员的初始化必须用构造函数列表的形式:
Engineer::Engineer(const string& fn, const string& ln, bool hw, const string& t) :title(t),Worker(fn,ln,hw) {} |
如果不显式的指明基类的构造函数,编译器会默认使用基类的默认构造函数:
Engineer::Engineer(const string& fn, const string& ln, bool hw, const string& t) :title(t){} | |
Engineer::Engineer(const string& fn, const string& ln, bool hw, const string& t) :title(t)Worker(){} |
有关派生类构造函数的要点:
- 构造函数首先会创建基类对象。
- 派生类构造函数应通过成员初始化列表将基类信息传递给基类构造函数。
- 派生类构造函数可以初始化派生类的新增数据成员。
# 析构函数
值得注意的时,析构函数的调用顺序正好和构造函数相反:先调用派生类的析构函数,然后在调用基类的析构函数。
# 使用派生类
要使用派生类,程序必须能够访问基类声明,因此最好把派生类和基类的声明放在同一个头文件内。
# 派生类和基类之间的特殊关系
- 派生类可以使用基类的方法,前提是方法不能是私有的。
- 基类指针可以在不进行显式转换的情况下指向派生类对象,基类引用也是如此。但这种关系是单向的,不可用将基类对象的地址赋给派生类引用和指针:
Engineer ee { "ming", "xiao", true,"game-engineer" }; | |
Worker* w = ⅇ // ok | |
w->Name(); | |
Worker& wk = ee; // ok | |
wk.Name(); | |
Worker kk{ "ming", "hong", true, }; | |
Engineer* eg = &kk; // fail | |
Engineer& egr = kk; // fail |
- 需要注意的是,基类指针和基类引用只能调用基类方法,和访问基类成员。
# 继承:is-a 关系
# 多态公有继承
有时候,我们会希望派生类的某些功能和基类相似,但是又有一些不同之处。这种复杂的行为称为多态 —— 具有多种形态,即:同一种函数在不同的调用对象上表现出了不同的行为。有以下两个机制可以用于实现多态公有继承:
- 在派生类中重新定义基类的方法。
// base | |
class Worker { | |
private: | |
string frist_name; | |
string last_name; | |
bool has_work; | |
public: | |
Worker(const string& fn = "none", const string& ln = "none", bool hw = false); | |
void Name() const; | |
bool HasWork() const { return has_work; }; | |
}; | |
// extend | |
class Engineer : public Worker { | |
private: | |
string title; // Engineer title | |
protected: | |
public: | |
Engineer(const string& fn, const string& ln, bool hw = false, const string& t = "none"); | |
void Code() const; | |
void Name() const; // 重新定义一个 Name 函数 | |
}; |
- 使用虚方法 ——
virtual
,该关键字只能用于类声明,不能用在类定义。
// base | |
class Worker { | |
private: | |
string frist_name; | |
string last_name; | |
bool has_work; | |
public: | |
Worker(const string& fn = "none", const string& ln = "none", bool hw = false); | |
void Name() const; | |
bool HasWork() const { return has_work; }; | |
virtual void SetWorkState(bool state) { has_work = state; }; | |
}; | |
// extend | |
class Engineer : public Worker { | |
private: | |
string title; // Engineer title | |
protected: | |
public: | |
Engineer(const string& fn, const string& ln, bool hw = false, const string& t = "none"); | |
void Code() const; | |
void Name() const; | |
// 可以通过 BaseClass::Func () 的方式调用父类的方法。 | |
virtual void SetWorkState(bool state) { Worker::SetWorkState(state); title = ""; }; | |
}; |
# 虚析构函数
虚析构函数的作用是能够保证对象在调用析构函数时,按照正常的顺序调用父类的析构函数。否则只会调用类对象本身的析构函数,这样往往会在基类构造函数定义了某些 new
创建的对象时出现问题。
class Worker { | |
public: | |
virtual ~Worker(); | |
}; | |
class Engineer : public Worker { | |
}; |
如果要在派生类中重新定义基类的方法,应该把其声明为虚的。
# 静态联编和动态联编
程序在调用函数时,如何确定执行那一段代码块呢?这个问题就得问问编译器了。将源代码中的函数调用解释为执行特定函数的代码块被称为函数名联编(binding)。如何确定这种联编呢?一种方式是在编译过程中进行的,称为静态联编(static binding)。另一种是在程序运行过程中进行的,被称为动态联编(dynamic binding)。
# 指针和引用类型的兼容
在 C++ 中,动态联编主要是通过指针和引用的方式,这种动态的行为,某种程度上是由继承控制的。
C++ 通常情况下是不允许把一种类型的「指针」或是「引用」赋值给另一种类型,然而对于派生类对象和基类对象则例外。
- 将派生类引用或者指针转为基类引用或者指针的方式被称为「向上强制转换(upcasting)」,这种转换方式不需要显式调用。因为派生类本身就是向上「兼容」的。
- 相反,从基类指针或是引用转为派生类的指针或是引用的方式被称为「向下强制转换(downcasting)」,这种方式必须使用显式的类型转换。因为基类到派生类的过程是不向下兼容的,派生类的功能往往比基类多。这种转换往往伴随着一些风险。
# 虚成员函数和动态联编
只有被定义为虚函数的方法才会被动态联编,因而才能实现多态的效果。
# 为什么会有两种不同的联编方式❓
- 效率:静态联编的效率比较高。
- 概念模型:某些函数虽然会被重写,但是其本身并不希望实现多态效果,这时候,默认的静态联编是一个很好的替代方案。
# 虚函数的工作原理
C++ 定义了虚函数的行为,但具体的实现需要让编译器作者来完成。
通常情况下,编译器对于虚函数的处理方法是:为每个对象添加一个隐藏成员。该成员中保存了一个指向函数地址数组的指针。这种数组被称之为虚函数表(virtual function table,vbl)。
例如:基类对象包含了一个保存所有该类内的虚函数声明地址的数组,同样,每个派生类也有这么一个对象。不同之处在于:如果存在派生类也定义的虚函数,那么虚函数地址所存储的虚函数将是派生类而非基类的。
因此如果一个基类指针指向的派生类对象调用一个在派生类和基类同时定义的虚函数,编译器会选择派生类所定义的虚函数进行执行。
复杂的功能往往伴随性一定的成本,虚函数也是如此:
- 每个对象都变大了,需要额外的空间用来存储虚函数表。
- 对于每个类,编译器都需要创建一个虚函数表。
- 每个虚函数的调用,都需要去虚函数表执行一次查询操作。
# 关于虚函数注意事项
- 在基类方法的声明中使用关键字
virtual
可以使得该方法在基类以及所有的派生类中都为虚函数。 - 如果使用指向对象的引用或指针来调用虚方法,程序将使用调用对象类型所定义的虚方法,而不适用指针或引用对象本身的类型,该特性也被称为「多态」。
- 如果定义的类将被用作基类,则应该将那些需要在派生类中重新定义的方法声明为虚的。
# 构造函数
构造函数不能是虚的,原因也很简单,派生类并不需要重新定义基类的构造函数。
# 析构函数
虚构函数应该定义为虚的,除非该类不用做基类。原因见上述虚析构函数。此外,就算该类不是基类,也可以定义虚构造函数,只是会有一些性能开销。
# 友元
友元函数不能是虚函数,因为友元函数本身就不是类成员,不能被继承,也就没有任何意义。
# 派生类没有重新定义虚函数
- 如果派生类没有重新定义虚函数,将会使用基类的版本。
- 如果派生类位于派生链中,则将使用最新的虚函数版本(多继承情况下派生类的派生类可能会使用派生类本身的虚函数而非基类)。
# 重新定义将隐藏方法
重载虚函数可能会导致虚函数被覆盖(隐藏):
class Worker { | |
public: | |
virtual void SetWorkState(bool state) { has_work = state; }; | |
virtual ~Worker(); | |
}; | |
class Engineer { | |
public: | |
virtual void SetWorkState() {}; | |
}; | |
Engineer e; | |
e.SetWorkState(); // ok | |
e.SetWorkState(false); // fail |
这里还有一个注意事项:
- 如果重新定义继承的方法,应该保证与原来的原型完全相同。
- 如果返回类型是基类引用或者指针,修改为派生类引用或者指针是被允许的,这种新特性被称为返回类型协变(covariance of return type)。
class Worker { | |
public: | |
virtual Worker& SetWorkState(bool state); | |
virtual ~Worker(); | |
}; | |
class Engineer { | |
public: | |
virtual Engineer& SetWorkState(bool state); // is ok | |
}; |
# 访问控制:protected
protected
关键字和 private
有点类似,表示在类外不能直接访问 protected
成员。两者区别在于:
- 派生类对象可以直接访问基类的
protected
成员。
# 汇总:
public
:类内类外都可访问。protected
:类内可,类外不可。private
:类内类外都不可。
# 抽象基类
抽象类要求必须至少有一个虚函数是纯虚函数。并且所有的派生类都必须实现纯虚函数。因此纯虚函数更像是一个「接口的约定」。
# 纯虚函数的声明:
virtual xxx FuncName(XX x) = 0; |
# 继承和动态内存分配
派生类本身如果定义了 new
创建的对象,那么派生类本身必须显式实现「析构函数」「赋值构造函数」「复制构造函数」来回收内存。
并且如果基类同样定义了 new
创建的对象,还需要通过基类提供的方式一并进行处理(析构函数除外,编译器会帮你主动调用基类的析构函数)。