继承与派生定义
C++ 中的继承是类与类之间的关系:继承(Inheritance)可以理解为一个类从另一个类获取成员变量和成员函数的过程。派生(Derive)和继承是一个概念,只是站的角度不同,可以理解为被另一个类获取成员变量和成员函数;派生类除了拥有基类的成员,还可以定义自己的新成员,以增强类的功能。
例如: Quaternion类继承了QuadWord类,当创建一个Quaternion类型的对象时,该类的成员函数不包括查询四元数值的函数;此时可调用其基类QuadWord的成员函数x(),y(),z(),w()以获取四元数成员值。
Quaternion.h
class Quaternion : public QuadWord {
public:
Quaternion() {}
Quaternion(const tfScalar& x, const tfScalar& y, const tfScalar& z, const tfScalar& w)
: QuadWord(x, y, z, w)
{}
Quaternion(const Vector3& axis, const tfScalar& angle)
{
setRotation(axis, angle);
}
}
QuadWord.h
class QuadWord{
public:
TFSIMD_FORCE_INLINE QuadWord() {}
TFSIMD_FORCE_INLINE QuadWord(const tfScalar& x, const tfScalar& y, const tfScalar& z,const tfScalar& w)
{
m_floats[0] = x, m_floats[1] = y, m_floats[2] = z, m_floats[3] = w;
}
TFSIMD_FORCE_INLINE const tfScalar& x() const { return m_floats[0]; }
TFSIMD_FORCE_INLINE const tfScalar& y() const { return m_floats[1]; }
TFSIMD_FORCE_INLINE const tfScalar& z() const { return m_floats[2]; }
TFSIMD_FORCE_INLINE const tfScalar& w() const { return m_floats[3]; }
protected:
double m_floats[4];
}
geometry_msgs::PoseStamped cmd_pos;
tf::Quaternion quaternion = tf::createQuaternionFromYaw(theta);
cmd_pos.pose.orientation.x = quaternion.x(); // 取四元素对应的位置x值
cmd_pos.pose.orientation.y = quaternion.y(); // 取四元素对应的位置y值
cmd_pos.pose.orientation.z = quaternion.z(); // 取四元素对应的位置z值
cmd_pos.pose.orientation.w = quaternion.w(); // 取四元素对应的位置w值
本例中,QuadWord 是基类,Quaternion 是派生类。Quaternion 类继承了QuadWord 类的成员,这些继承过来的成员,可以通过子类对象访问,就像自己的一样。
class Quaternion : public QuadWord
这就是声明派生类的语法。class 后面的“Quaternion”是新声明的派生类,冒号后面的“People”是已经存在的基类。在“QuadWord”之前有一关键宇 public,用来表示是公有继承。
由此总结出继承的一般语法为:
class 派生类名:[继承方式] 基类名{
派生类新增加的成员
};
继承与派生方式
继承方式/基类成员 | public成员 | protect成员 | private成员 |
---|---|---|---|
public继承 | public | protect | 不可见 |
protect继承 | protect | protect | 不可见 |
private继承 | private | private | 不可见 |
通过表格可以发现:
(1)基类成员在派生类中的访问权限不得高于继承方式中指定的权限。也就是说,继承方式中的 public、protected、private 是用来指明基类成员在派生类中的最高访问权限的。
(2) 不管继承方式如何,基类中的 private 成员在派生类中始终不能使用(不能在派生类的成员函数中访问或调用)。在派生类中访问基类 private 成员的唯一方法就是借助基类的非 private 成员函数,如果基类没有非 private 成员函数,那么该成员在派生类中将无法访问。
(3) 如果希望基类的成员能够被派生类继承并且毫无障碍地使用,那么这些成员只能声明为 public 或 protected;只有那些不希望在派生类中使用的成员才声明为 private。
(4) 如果希望基类的成员既不向外暴露(不能通过对象访问),还能在派生类中使用,那么只能声明为 protected。
注:由于 private 和 protected 继承方式会改变基类成员在派生类中的访问权限,导致继承关系复杂,所以实际开发中我们一般使用 public。
改变继承权限: 使用 using 关键字可以改变基类成员在派生类中的访问权限,例如将 public 改为 private、将 protected 改为 public,不能改变 private 成员的访问权限。
C++继承时的名字遮蔽
如果派生类中的成员(包括成员变量和成员函数)和基类中的成员重名,那么就会遮蔽从基类继承过来的成员。所谓遮蔽,就是在派生类中使用该成员(包括在定义派生类时使用,也包括通过派生类对象访问该成员)时,实际上使用的是派生类新增的成员,而不是从基类继承来的;但是,基类中的重名成员函数仍然可以访问,不过要加上类名和域解析符。
基类成员函数和派生类成员函数不构成重载: 基类成员和派生类成员的名字一样时会造成遮蔽,对于成员函数要引起注意,不管函数的参数如何,只要名字一样就会造成遮蔽。换句话说,基类成员函数和派生类成员函数不会构成重载,如果派生类有同名函数,那么就会遮蔽基类中的所有同名函数,不管它们的参数是否一样。
C++基类和派生类的构造函数
基类的成员函数可以被继承,可以通过派生类的对象访问,但这仅仅指的是普通的成员函数,类的构造函数不能被继承。在设计派生类时,对继承过来的成员变量的初始化工作也要由派生类的构造函数完成,但是大部分基类都有 private 属性的成员变量,它们在派生类中无法访问,更不能使用派生类的构造函数来初始化。解决这个问题的思路是:在派生类的构造函数中调用基类的构造函数。
例如: 在上一个例子中,QuadWord(x, y, z, w)就是调用基类的构造函数,并将 x,y,z,w作为实参传递给它。
Quaternion(const tfScalar& x, const tfScalar& y, const tfScalar& z, const tfScalar& w)
: QuadWord(x, y, z, w)
{}
如果派生类里有新增成员变量需要赋值,可以在基类构造函数后使用参数列表对其赋值,它们中间以逗号隔开,但是不管它们的顺序如何,派生类构造函数总是先调用基类构造函数再执行其他代码(包括参数初始化表以及函数体中的代码);
假设Quaternion类中,新增一个私有成员变量axis_需要通过构造函数在初始化的时候赋值,此时就可以写作:
Quaternion(const tfScalar& x, const tfScalar& y, const tfScalar& z, const tfScalar& w, const tfScalar& axis)
: QuadWord(x, y, z, w),axis_(axis)
{}
或:
Quaternion(const tfScalar& x, const tfScalar& y, const tfScalar& z, const tfScalar& w, const tfScalar& axis)
: axis_(axis),QuadWord(x, y, z, w)
{}
基类构造函数总是被优先调用,这说明创建派生类对象时,会先调用基类构造函数,再调用派生类构造函数,如果继承关系有好几层的话,例如:
A --> B --> C
那么创建 C 类对象时构造函数的执行顺序为:
A类构造函数 --> B类构造函数 --> C类构造函数
构造函数的调用顺序是按照继承的层次自顶向下、从基类再到派生类的。注意派生类构造函数中只能调用直接基类的构造函数,不能调用间接基类的。
注:通过派生类创建对象时必须要调用基类的构造函数,定义派生类构造函数时最好指明基类构造函数;如果不指明,就调用基类的默认构造函数(不带参数的构造函数);如果没有默认构造函数,那么编译失败。如果类中已经显式定义了构造函数(带参数),编译器不会再生成默认的构造函数,此时如果不指明构造函数(带参数),编译就会出错。
C++多继承
多继承的语法也很简单,将多个基类用逗号隔开即可。例如已声明了类A、类B和类C,那么可以这样来声明派生类D:
class D: public A, private B, protected C{
//类D新增加的成员
}
多继承形式下的构造函数和单继承形式基本相同,只是要在派生类的构造函数中调用多个基类的构造函数。以上面的 A、B、C、D 类为例,D 类构造函数的写法为:
D(形参列表): A(实参列表), B(实参列表), C(实参列表){
//其他操作
}
基类构造函数的调用顺序和和它们在派生类构造函数中出现的顺序无关,而是和声明派生类时基类出现的顺序相同。仍然以上面的 A、B、C、D 类为例,即使将 D 类构造函数写作下面的形式:
D(形参列表): B(实参列表), C(实参列表), A(实参列表){
//其他操作
}
那么也是先调用 A 类的构造函数,再调用 B 类构造函数,最后调用 C 类构造函数。
命名冲突: 当两个或多个基类中有同名的成员时,如果直接访问该成员,就会产生命名冲突,编译器不知道使用哪个基类的成员。这个时候需要在成员名字前面加上类名和域解析符::,以显式地指明到底使用哪个类的成员,消除二义性。
C++虚继承和虚基类
多继承时很容易产生命名冲突,比如典型的是菱形继承:
A 派生出类 B 和类 C,类 D 继承自类 B 和类 C,这个时候类 A 中的成员变量和成员函数继承到类 D 中变成了两份,一份来自 A–>B–>D 这条路径,另一份来自 A–>C–>D 这条路径。在一个派生类中保留间接基类的多份同名成员,虽然可以在不同的成员变量中分别存放不同的数据,但大多数情况下这是多余的:因为保留多份成员变量不仅占用较多的存储空间,还容易产生命名冲突。假如类 A 有一个成员变量 a,那么在类 D 中直接访问 a 就会产生歧义,编译器不知道它究竟来自 A -->B–>D 这条路径,还是来自 A–>C–>D 这条路径。
虚继承: 为了解决多继承时的命名冲突和冗余数据问题,C++ 提出了虚继承,使得在派生类中只保留一份间接基类的成员。在继承方式前面加上 virtual 关键字就是虚继承,虚继承的目的是让某个类做出声明,承诺愿意共享它的基类。其中,这个被共享的基类就称为虚基类(Virtual Base Class),在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含一份虚基类的成员。
虚继承构造函数: 在最终派生类 D 的构造函数中,除了调用 B 和 C 的构造函数,还调用了 A 的构造函数,这说明 D 不但要负责初始化直接基类 B 和 C,还要负责初始化间接基类 A。而在以往的普通继承中,派生类的构造函数只负责初始化它的直接基类,再由直接基类的构造函数初始化间接基类,用户尝试调用间接基类的构造函数将导致错误。
现在采用了虚继承,例如虚基类 A 在最终派生类 D 中只保留了一份成员变量 m_a,如果由 B 和 C 初始化 m_a,那么 B 和 C 在调用 A 的构造函数时很有可能给出不同的实参,这个时候编译器就会犯迷糊,不知道使用哪个实参初始化 m_a。
为了避免出现这种矛盾的情况,C++ 干脆规定必须由最终的派生类 D 来初始化虚基类 A,直接派生类 B 和 C 对 A 的构造函数的调用是无效的。