十四、继承与组合(Inheritance Composition)
十四、继承与组合(Inheritance & Composition)
引言
- C++最引人注目的特性之一是代码复用。
- 组合:在新类中创建已有类的对象。
- 继承:将新类作为已有类的一个类型来创建。
14.1 组合的语法
Useful.h
//C14:Useful.h
#ifndef USEFUL_H
#define USEFUL_H
class X{int i;
public:X(){i = 0;}void set(int ii){i = ii;}int read() const {return i;}int permute(){return i = i * 47;}
};
#endif
Composition,cpp
#include "Useful.h"
class Y{int i;
public:X x;//嵌入对象,子对象Y(){ i = 0;}void f(int ii) {i = ii;}int g() cosnt{return i;}
};void main()
{Y y;y.f(47);y.x.set(37);
}
这里
Y y;
语句执行的时候,y
里面的x
是利用构造函数进行初始化的
14.2 继承的语法
Useful.h
//C14:Useful.h
#ifndef USEFUL_H
#define USEFUL_H
class X{int i;
public:X (){i = 1;}void set(int ii){i = ii;}int read() const{return i;}int permute() {return i = i*47;}
};
#endif
Inheritance.cpp
//C14:Inheritance.cpp
#include "Useful.h"
#include <iostream>
using namespace std;
class Y:public X
{int i;//不是X的i
public:Y(){i = 2;}int change(){ i = permute();//调用不同名称的函数return i;}void set(int ii){i = ii;X::set(ii);//调用同名函数,需要加::}
};int main(){cout << "sizeof(X) = " << sizeof(X) << endl;cout << "sizeof(Y) = " << sizeof(Y) << endl;Y D;//X::i = 1,Y::i = 2D.change();//return Y::i = 47(X::i = 47)D.read(); //X::i = 47D.permute();//X::i = 47 * 47D.set(15);//Y::i = 12,X::i = 12return 0;
}
这里对y里面的x初始化也是通过X的无形参构造函数。
输出
sizeof(X) = 4
sizeof(Y) = 8
-
Y
继承自X
,这意味着Y
内将包含一个X
类型的子对象,就像在Y
内部直接创建了一个X
成员对象一样。无论是成员对象还是基类所占的存储都称为子对象。 -
Y
是X
的派生类,X
是基类。派生类继承基类的属性,这种关系称为继承。 -
X
的所有私有成员在Y
中仍然是私有的(因此Y
里面不能访问X
的私有成员,只能通过X
的函数)。通过public
继承,基类的所有公有成员在派生类也保持公有(后面还会有private
继承、protected
继承),也就是说public
继承,X
中的私有在Y
中仍私有,公有仍公有,protected
仍protected
。protected
是指派生类可以访问,外部代码不可以访问。
-
将一个类用作基类相当于声明了一个该类的(未命名)对象。因此。必须先定义这个类才能将其用作基类。
class X; class Y:public X{………… };
14.3 构造函数初始化列表
- 在组合和继承中,确保**子对象被正确初始化**非常重要。
- 构造函数和析构函数不会被继承(赋值运算符也不会被继承)。因此派生类的构造函数无法直接初始化基类的成员。
- 新类的构造函数无法访问子对象的私有数据元素。
- 如果不使用默认构造函数,该如何初始化子对象的私有数据元素。
解决方法
- 在构造函数初始化列表中调用子对象的构造函数。
- 构造函数初始化列表允许显式调用成员对象的构造函数。其原理是:在进入新类构造函数的函数体之前,所有成员对象的构造函数都会被调用。
- 内置类型的变量也可以在构造函数初始化列表中初始化。而且初始化列表会自动帮忙初始化(避免垃圾值)。
注意:在非虚继承(普通继承) 中,派生类只需要构造它的直接基类,间接基类会自动由中间类网上构造,构造链自动完成。
示例1
#include <iostream>
using namespace std;
class X{int a;
public:X(int i):a(i){cout << "Constructor X:" << a << endl;}
};
class Y{int b;
public:Y(int i,int j):b(i),x(j){cout << "Constructor Y:" << b << endl;}X x;
};int main(){Y y(1,2);return 0;
}
该实例中
x
是y
的成员
输出
Constructor X:2
Constructor Y:1
示例2
#include <iostream>
using namespace std;
class X {int a;
public:X(int i = 7) :a(i) { cout << "Constructor X:" << a << endl; }
};
class Y {int b;
public:Y(int i) :b(i) { cout << "Constructor Y:" << b << endl; }X x;
};int main() {Y y(1);return 0;
}
该实例中
x
是y
的成员
输出
Constructor X:7
Constructor Y:1
示例3
#include <iostream>
using namespace std;
class X{int a;
public:X (int i):a(i){cout << "Constructor X:" << a << endl;}
};
class Y:public X
{int b;
public:Y(int i,int j):b(i),X(j){cout << "Constructor Y:" << b << endl;}
}int main(){Y y(1,2);return 0;
}
输出
Constructor X:2
Constructor Y:1
示例4
#include <iostream>
using namespace std;
class X {int a;
public:X(int i = 9) :a(i) { cout << "Constructor X:" << a << endl; }
};
class Y :public X
{int b;
public:Y(int i, int j) :b(i){ cout << "Constructor Y:" << b << endl; }
};int main() {Y y(1, 2);return 0;
}
输出
Constructor X:9
Constructor Y:1
14.4 组成与继承结合
- 当创建一个派生类对象时,可能会创建以下对象:基类对象、成员对象和派生类对象本身。构造顺序自下而上:首先构造基类、然后构造成员对象,最后构造派生类自身。
- 基类或成员对象构造函数的调用顺序以它们在派生类中声明的顺序为准,而不是它们在初始化列表中出现的顺序。
- 默认构造函数可以被隐式调用。
示例
#include <iostream>
using namespace std;
class X{int a;
public:X(int i = 0):a(i){cout << "Constructor X:" << a << endl;}
};
class Y:public X{int b;X x1,x2;
public:Y(int i,int j,int m,int n):b(i),x2(j),x1(m),X(n){cout << "Constructor Y:" << b << endl;}
};
int main(){Y y(1,2,3,4);return 0;
}
输出
Constructor X:4
Constructor X:3
Constructor X:2
Constructor Y:1
14.5 名字隐藏
先介绍下面三种相关机制
- 重载(Overload):发生在同一作用域(类)内
- 重新定义(Redefining):继承关系中的普通成员函数
- 重写(Overriding):继承关系中的虚成员函数
名字隐藏:在继承关系中 ,如果派生类定义了一个与基类中同名的成员(不论是函数还是变量),那么这个基类的同名成员(函数)就会被“隐藏”,即使参数不同也无法通过派生类对象直接访问。上面三种机制中,重载并非是名字隐藏,其余两个是名字隐藏。
概念 | 英文术语 | 适用情形 | 是否是名字隐藏 | 说明 |
---|---|---|---|---|
重载 | Overload | 同一个类或者同一作用域 | 否 | 函数名相同,但参数不同(返回值不同不算重载啊) |
重新定义 | Redifining | 继承中的普通函数 | 是 | 派生类中定义了同名函数(或变量),会隐藏基类中所有同名函数(变量),需要显示访问 |
重写 | Overriding | 继承中的虚函数 | 是 | 派生类用virtual 修饰的函数覆盖基类中的虚函数 |
虚函数会在后续的章节继续提到
- 在派生类中,只要重新定义了基类中重载的函数名(假设基类有重载函数),基类中 该名字的其他版本 在 派生类中都会被自动隐藏。
示例
//C14:NameHidding.cpp
#include <iostream>
#include <string>
using namespace std;class Base{
public:int f() const{cout << "Base::f()" << endl;return 1;}int f(string) const {return 1;}void g(){}
};class Derived1:public Base
{
public://重新定义void g() const{};
};class Derived2:public Base{
public://重定义int f() const {cout << "Derived2::f()" << endl;return 2;}
};class Derived3:public Base{
public://改变return的类型void f() const{cout << "Derived3::f()" << endl;}
};class Derived4:public Base{
public://改变return的类型int f(int) const{cout << "Derived4::f()" << endl;return 4;}
};void main(){string s("hello");Derived1 d1;int x = d1.f();d1.f(s);Derived2 d2;x = d2.f();//!d2.f(s);//string版本被隐藏Derived3 d3;//!x = d3.f();return int 版本被隐藏Derived4 d4;//!x = d4.f();f()版本被隐藏x = d4.f(1);
}
输出
Base::f()
Derived2::f()
Derived4::f()
14.6 不会自动继承的函数
以下函数不会被自动继承
- 构造函数
- 析构函数
- 赋值运算符函数
继承和静态成员函数(Inheritance and static member funcitons)
- 静态成员函数可以被继承到派生类中。
- 如果在派生类中重新定义了一个静态成员函数,那么基类中所有同名的重载函数也会被隐藏。
- 静态成员函数只能访问静态数据成员。
- 静态成员函数不能是是
virtual
(虚函数)。 - 静态成员函数没有
this
指针。
14.7 选择组合还是继承
- 共同点:组合(Composition)和继承(Inheritance)都会在新类中放置子对象(subojects)。它们都使用构造函数初始化列表(intializer list) 来构造这些子对象。
- 当我们希望在新类中包含某个已有类的功能(作文数据成员),但不想继承它的接口时,通常使用组合。
- 当我们希望新类具有与现有类完全相同的接口时,使用继承。这被称为子类型化。
14.8 基类的子类型化
-
如果一个派生类继承自一个
public
的基类,那么该派生类就继承了基类的所有成员,但只能访问基类中的public
和protected
成员。派生类就是基类的子类型,基类是派生类的超类型从外部使用者的角度来看,这个派生类就具有与基类相同的
public
接口(可以再加自己的新接口),因此它可以在需要基类对象的地方替代使用,这就是**子类型(subtyping)**的概念。 -
通过子类型化,当我们使用指针或引用操作派生类对象时,它可以被当作基类对象来处理(即:可以指向或引用基类)
#include <iostream> using namespace std; class Base { public:void speak() { cout << "Base speaking" << endl; } };class Derived : public Base { public:void shout() { cout << "Derived shouting" << endl; } };int main() {Derived d;// 子类型化:Base* 指向 Derived 对象Base* ptr = &d;ptr->speak(); // OK:Base 的函数可调用// ptr->shout(); // 错误:Base* 看不到 Derived 的接口// 同样适用于引用Base& ref = d;ref.speak(); // OK }
这在后续还会提到。
14.9 继承的访问控制
- 访问说明符:
public
private
protected
- 访问说明符 用于控制派生类对基类的成员的访问,以及从派生类到基类的指针和引用转换的权限。(无论哪种访问,基类的所有成员都会被继承,派生类的内存里都会有它们,只是能不能访问的差别。)
下面展示三种访问控制的不同
public
继承
如果一个派生类使用public
继承:
- 派生类的成员函数及其类内部可以访问其基类中的
public
和protected
成员。private
不可以访问。 - 派生类的对象可以访问其基类中的
public
成员。
说明:
public
继承会让基类的public
成员在派上类里面仍是public
,protected
成员仍是protected
。- 可以形成”子类型关系“,即能用
Base*
指向Derived
。
示例
#include <iostream>
using namespace std;
class employee {
public: void print() {}
protected: short number;
private: string name;
};class manager :public employee {
public:void meeting(int n) {print(); //oknumber = n;//ok//name = "Jhoo" //error}
private:short level;
};
int main() {employee E;manager M;E.print();//ok//E.number = 3; //error//M.name = "Jhoo"; //error//M.number = 3; //errorM.print(); //okM.meeting(1); //okreturn 0;
}
可见protected
和 private
的区别就是:
- 派上类里面可以访问
protected
,而不可以访问private
,而派生类的对象两个都不可以访问
private
继承
如果一个类使用private
说明符继承:
- 派生类的成员函数及其类内部可以访问其基类中的
public
和protected
成员。private
不可以访问。 - 派生类的对象不能访问基类中的任何成员。
说明:
private
继承会把基类的public
和protected
成员变成派生类中的private
成员。- 所以 只有派生类内部能访问 基类的
public
和protected
成员,类外(包括派生类对象)都不能访问。 - 同时,不会形成”子类型关系“,即不能用
Base*
指向Derived
。
示例
#include <iostream>
using namespace std;
class employee {
public: void print() {}
protected: short number;
private: string name;
};class manager :private employee {
public:void meeting(int n) {print(); //oknumber = n;//ok//name = "Jhoo" //error}
private:short level;
};
int main() {employee E;manager M;E.print();//ok//E.number = 3; //error//M.name = "Jhoo"; //error//M.number = 3; //error//M.print(); //errorM.meeting(1); //okreturn 0;
}
protected
继承
如果一个类使用protected
说明符进行继承:
- 派生类的成员函数及其类内部可以访问其基类中的
public
和protected
成员。private
不可以访问。 - 派生类的对象不能访问基类中的任何成员。
说明
protected
继承会将基类的public
和protected
成员变成派生类中的protected
;- 所以,只有派生类的内部能用,类外部(包括对象)都无法访问;
- 同时不形成“子类型关系”,不能用
Base*
指向Derived
。
protected
继承和private
继承当派生类再次被继承时,会体现出来差别
总结
三种继承方式对比
方面 | public 继承 | protected 继承 | private 继承 |
---|---|---|---|
基类的public 成员在派生类中变成 | public | protected | private |
基类的proitected 成员在派生类中变成 | protected | protected | ptivate |
基类的private 成员在派生类中变成 | 不可访问 | 不可访问 | 不可访问 |
派生类成员函数能访问哪些基类成员 | public 和protected | public 和protected | public 和protected |
派生类对象能访问哪些基类成员 | public | 无法访问任何成员 | 无法访问热河成员 |
是否支持子类型转换(Base* = new Derived ) | 是 | 不支持 | 不支持 |
适用场景 | 接口继承、支持多态、面向对象设计 | 实现复用,不暴露接口 | 强封装 |
类中成员访问权限对比
访问标识符 | 类中是否可以访问 | 派生类是否可以访问 | 类外部(对象)是否可以访问 | 是否可以继承 |
---|---|---|---|---|
public | 可以 | 可以 | 可以 | 可以 |
protected | 可以 | 可以 | 不可以 | 可以 |
private | 可以 | 不可以 | 不可以 | 可以(但不可见) |
示例
Example1
#include <iostream>
using namespace std;
class Location {
public:void InitL(int xx, int yy) {X = xx;Y = yy;}void Move(int xOff, int yOff) {X += xOff;Y += yOff;}int GetX() { return X; }int GetY() { return Y; }
private:int X, Y;
};
class Rectangle :public Location {
public:void InitR(int x, int y, int w, int h);int GetH() { return H; }int GetW() { return W; }
private:int W, H;
};void Rectangle::InitR(int x, int y, int h, int w)
{InitL(x, y);W = w;H = h;
}int main() {Rectangle rect;//派生类的对象rect.InitR(2, 3, 20, 10);rect.Move(3, 2);cout << rect.GetX() << "," << rect.GetY() << "," << rect.GetH() << "," << rect.GetW() << endl;return 0;
}
输出
5,5,20,10
Example2
这里在Example1里面添加一个V
类。
class Location {
public:void InitL(int xx, int yy) {X = xx;Y = yy;}void Move(int xOff, int yOff) {X += xOff;Y += yOff;}int GetX() { return X; }int GetY() { return Y; }
private:int X, Y;
};
class Rectangle :public Location {
public:void InitR(int x, int y, int w, int h);int GetH() { return H; }int GetW() { return W; }
private:int W, H;
};void Rectangle::InitR(int x, int y, int h, int w)
{InitL(x, y);W = w;H = h;
}
// 派生类
class V : public Rectangle {
public:void Function() {Move(3, 2); // 来自基类的函数}
};
-
如果这里的
clas V :public Rectangle
继承改为private
继承,那么Move(3,2)
还能用吗?解答:
- 如果
Rectangle
是以public
或protected
方式继承Location
,
那么类V
无论使用哪种继承方式(public
/protected
/private
),都可以访问Move()
函数。 - 如果
Rectangle
是以private
方式继承Location
,
那么类V
无论如何继承Rectangle
,都无法访问Move()
函数。 - 因为:
public
和protected
继承会让基类的public
/protected
成员保留可见性(对类内仍可访问); - 而private
继承会将基类的public
/protected
成员都变为private
,对子类完全不可见。
- 如果
Example3
这里将Example1 的public
继承改为private
继承
#include <iostream>
using namespace std;
class Location {
public:void InitL(int xx, int yy) {X = xx;Y = yy;}void Move(int xOff, int yOff) {X += xOff;Y += yOff;}int GetX() { return X; }int GetY() { return Y; }
private:int X, Y;
};
class Rectangle :private Location {
public:void InitR(int x, int y, int w, int h);int GetH() { return H; }int GetW() { return W; }
private:int W, H;
};void Rectangle::InitR(int x, int y, int h, int w)
{InitL(x, y); //OKW = w;H = h;
}
那么Example1里面的主函数需要修改
int main() {Rectangle rect;//派生类的对象rect.InitR(2, 3, 20, 10);rect.Move(3, 2);cout << rect.GetX() << "," << rect.GetY() << "," << rect.GetH() << "," << rect.GetW() << endl;return 0;
}
rect.GetX()
和 rect.GetY()
以及rect.Move(3,2)
会报错,因为它们在Rectangle
里面已经是private
。所以Rectangle
对象无法访问它们。
所以我们将Exampel1修改成如下
#include <iostream>
using namespace std;
class Location {
public:void InitL(int xx, int yy) {X = xx;Y = yy;}void Move(int xOff, int yOff) {X += xOff;Y += yOff;}int GetX() { return X; }int GetY() { return Y; }
private:int X, Y;
};
class Rectangle :private Location {
public:void InitR(int x, int y, int w, int h);void Move(int xOff, int yOff) {Location::Move(xOff,yOff);}int GetX() {return Location::GetX();}int GetY() {return Location::GetY();}int GetH() { return H; }int GetW() { return W; }
private:int W, H;
};void Rectangle::InitR(int x, int y, int h, int w)
{InitL(x, y); //OKW = w;H = h;
}int main() {Rectangle rect;//派生类的对象rect.InitR(2, 3, 20, 10);rect.Move(3, 2);cout << rect.GetX() << "," << rect.GetY() << "," << rect.GetH() << "," << rect.GetW() << endl;return 0;
}
这里在 Rectangle
里面重新定义Move()
、GetX()
、GetY()
。
输出
5,5,20,10
Example4
class Base {
public: void f1() {}
protected: void f3() {}
};
class Derived1 : protected Base {};class Derived2 : public Derived1 {public:void fun() {f1(); //okf3(); //ok}
};
int main() {Derived1 d;//d.f1(); //error//d.f3(); //errorreturn 0;
}
这里如果将class Derived2 : public Derived1
改为private
或者protected
,那么
void fun() {f1(); //okf3(); //ok
}
正确吗?
是正确的,因为,private
或者protected
仅仅是改变了Derived1
的成员在Derived1
里面是什么访问权限(public
/ protectecd
/ private
),而基类的public
和 protected
成员在派生类的内部是都可以访问的。
14.10 运算符重载与继承
- 除了赋值运算符(
=
)之外,其他运算符会自动被继承到派生类中。
14.11 多重继承
- 多重继承是指:一个派生类可以拥有多个直接基类。
class A{//……
}
class B{//……
}
class C:access A,access B{};
”access“是占位词,代表public
、 protected
、 private
中任意的一种访问方式。
Base classes: A B↖ ↗C ← Derived class
示例
#include <iostream>
using namespace std;
class B1 {
public:B1(int i) {b1 = i;cout << "Constructor B1:" << b1 << endl;}void Print() { cout << b1 << endl; }
private:int b1;
};class B2 {
public:B2(int i){b2 = i;cout << "Constructor B2:" << b2 << endl;}void Print() { cout << b2 << endl; }
private:int b2;
};class B3 {
public:B3(int i) {b3 = i;cout << "Constructor B3:" << b3 << endl;}int Getb3() { return b3; }
private:int b3;
};class A :public B2, public B1//多重继承
{
public:A(int i, int j, int k, int l);void Print();
private:B3 bb;int a;
};A::A(int i, int j, int k, int l) :a(l), bb(k), B2(j), B1(i) {cout << "Constructor A:" << a << endl;
}void A::Print() {B1::Print();B2::Print();cout << bb.Getb3() << endl << a << endl;
}
int main() {A aa(1, 2, 3, 4);aa.Print();return 0;
}
输出
Constructor B2:2
Constructor B1:1
Constructor B3:3
Constructor A:4
1
2
3
4
14.12 增量式开发
- 增量式开发:在不破坏已有代码的前提下添加新代码。
- 继承和组合的一个优点是:它们支持增量式开发。
歧义问题:
- 歧义1:当多个基类中拥有同名成员函数时(多重继承情况下),可能会发生名字冲突。
- 歧义2:如果一个派生类有两个基类,而这两个基类又都继承自一个类,那么就可能触发歧义(即,一个类在继承链中被“继承了两次”)。也就是”菱形继承问题“。
消除歧义1
歧义1:
当多个基类中拥有同名成员函数时(多重继承情况下),可能会发生名字冲突。
歧义1示例
class A{
public:void f(){}
};
class B{
public:void f(){}void g(){}
};
class C:public A,public B{
public: void g(){}
};
void main(){C c;//c.f(); //error:编译器不知道式A还是B的f()c.g(); //okc.B::g(); //ok
}
解决方法:
- 使用作用域解析运算符
::
(比如c.A::f();
) - 在派生类中定义一个新的函数(以覆盖或隐藏同名函数)
方法一:使用作用域解析运算符::
class A{
public:void f(){}
};
class B{
public:void f(){}void g(){}
};
class C:public A,public B{
public: void g(){}
};
void main(){C c;c.A::f(); //okc.g(); //okc.B::g(); //ok
}
方法二:在派生类中定义一个新的函数
class A {
public:void f() {}
};
class B {
public:void f() {}void g() {}
};
class C :public A, public B {
public:void g() {}void f() { A::f(); }
};
void main() {C c;c.f(); //okc.g(); //okc.B::g(); //ok
}
上述讲的只是第一种歧义:当多个基类中拥有同名成员函数时(多重继承情况下),可能会发生名字冲突。
接下来讲述第二种歧义:如果一个派生类又有两个基类,且这两个基类又都继承自同一个类,就可以出现歧义。(即,同一个类被继承了两次)
消除歧义2
歧义2:
如果一个派生类有两个基类,且这两个基类又都继承自同一个类,就可能出现歧义。(即,同一个类被继承了两次)
歧义2示例
class A
{public:void f(){};
};class B:public A{//……
};class C:public A{//……
};class D:public B,public C{//……
};void main(){D d;//A,B,A,C,Dd.f(); //error
}
解决方法:
- 使用作用域解析运算符
::
- 在派生类中定义一个新函数来隐藏或重写冲突函数
- 使用虚基类(virtual base class) 避免重复继承
方法一:使用作用域解析运算符::
class A
{public:void f(){};
};class B:public A{//……
};class C:public A{//……
};class D:public B,public C{//……
};void main(){D d;//A,B,A,C,Dd.B::f(); //ok
}
方法二:在派生类中重新定义一个新的函数
class A
{public:void f(){};
};class B:public A{//……
};class C:public A{//……
};class D:public B,public C{
public:void f(){}
};void main(){D d;//A,B,A,C,Dd.f(); //ok
}
方法三:使用虚基类
- 关键字
virtual
只作用于其后紧跟的基类。
class D:virtual public A,public B,virtual public C{//…………
};
上述中A和C都是虚基类,B不是虚基类
class A
{public:void f(){};
};class B:virtual public A{//……
};class C:virtual public A{//……
};class D:public B,public C{//……
};void main(){D d;//A,B,A,C,Dd.f(); //ok
}
采用虚基类后,A,B,C,D的关系就变为
虚基类与非虚基类的内存比较
-
虚基类
-
非虚基类
虚基类只存在一份,而非虚基类会重复拷贝。
虚基类的构造函数
-
它只会被调用一次。
-
它是由最底层派生类的构造函数调用的(可以是显示也可以是隐式调用)。
-
它会在非基类的构造函数之前被调用。
-
虚基类的构造函数会出现在所有派生类的构造函数的成员初始化列表中。
-
如果没有显示调用,则会自动调用它的默认构造函数。
下面解释显示和隐式调用
//显示调用
class A {
public:A(int x) { cout << "A(" << x << ")\n"; }
};class B : virtual public A {
public:B() : A(1) { cout << "B\n"; }
};
//隐式调用
class A {
public:A() { /*...*/ } // 默认构造函数
};class B : virtual public A {
public:B() { // 隐式调用 A()}
};
下面解释最底层派生类
最底层派生类:最终的创建那个类对象,那个类就是最底层派生类,也就是最终构造函数调用者。
示例
#include <iostream>
using namespace std;
class A {
public:A(int i) { cout << "A" << i << endl; }
};class B : virtual public A {
public:B(int i = 1):A(i) { cout << "B\n"; }
};class C : virtual public A {
public:C(int i = 2):A(i) { cout << "C\n"; }
};class D : public B, public C {
public:D(int i = 3):A(i) { cout << "D\n"; }
};
现在看两种对象的构造
int main(){B b;return 0;
}
此时构造的是 B
, B
是最底层派生类,所以它负责构虚基类A
。
输出
A1
B
int main(){D d;return 0;
}
此时构造的是 D
,D
是最底层派生类,所以它负责构造虚基类A
,即使 B
和C
也继承了 A
。
输出
A3
B
C
D
-
A
只被调用一次,由D
构造; -
B
和C
如果重新定义了A
的初始化,那也会被忽略
依旧是对最底层派生类的解释
假设有如下图的类
最底层派生类:
E e;//E是最底层派生类
D d;//D是最底层派生类
B b;//B是最底层派生类
C c;//C是最底层派生类
构造函数:
B(……):A(……){……}
C(……):A(……){……}
D(……):B(……),C(……),A(……){……}
E(……):D(……),A(……){……}
E
不需要构造B
和C
,是因为它们不是E
的直接基类;
但E
必须构造A
,是因为A
是一个虚基类,虚基类总是由“最底层派生类”负责构造,即使它不是直接基类。
示例
#include <iostream>
using namespace std;
class A {
public:A(const char* s) { cout << "Class A:" << s << endl; }~A() {}
};class B :virtual public A
{
public:B(const char* s1, const char* s2) :A(s1) {cout << "Class B:" << s2 << endl;}
};class C :virtual public A
{
public:C(const char* s1, const char* s2) :A(s1) {cout << "Class C:" << s2 << endl;}
};class D :public B, public C
{
public:D(const char* s1, const char* s2, const char* s3, const char* s4) :C(s2, s3), B(s4, s2), A(s1) {cout << "Class D:" << s4 << endl;}
};
void main() {D* ptr = new D("str1", "str2", "str3", "str4");delete ptr;
}
输出
Class A:str1
Class B:str2
Class C:str3
Class D:str4
这里先构造
B
再C
的原因是class D:public B,public C
,先继承B
再继承C
示例(A
不是虚函数)
#include <iostream>
using namespace std;
class A {
public:A(const char* s) { cout << "Class A:" << s << endl; }~A() {}
};class B :public A
{
public:B(const char* s1, const char* s2) :A(s1) {cout << "Class B:" << s2 << endl;}
};class C :public A
{
public:C(const char* s1, const char* s2) :A(s1) {cout << "Class C:" << s2 << endl;}
};class D :public B, public C
{
public:D(const char* s1, const char* s2, const char* s3, const char* s4) :C(s2, s3), B(s4, s2) {cout << "Class D:" << s4 << endl;}
};
void main() {D* ptr = new D("str1", "str2", "str3", "str4");delete ptr;
}
输出
Class A:str4
Class B:str2
Class A:str2
Class C:str3
Class D:str4
这里
D
就不需要再构造A
了,简洁基类会自动构造。
14.13 向上转型(Upcasing)
- 继承最重要的方面,并不是它为了新类提供了成员函数。
- 更关键的是:它表达了新类和基类之间的关系。
- 这种关系可以总结为这样的一句话:新类是已有类的一种类型。
向上转型(Upcasting): 将一个 派生类 类型的引用或指针转换为
其 基类 的引用或指针,就叫做“向上转型”。
示例
class Instrument {
public:void play() const {}
};
//Wind是Instrument的派生类
class Wind :public Instrument {};
void tune(Instrument& i) { i.play(); }
void main() {Wind flute;tune(flute); //向上转型(Upcasting)Instrument* p = &flute;//UpcastingInstrument& l = flute;//Upcasting
}
- 当通过指针或引用(指向或引用基类)来操作派生类对象时,派生类对象可以被当作其基类对象来处理。
在第十五章还会接着讲述“Upcasting” 。
14.14 总结
- 继承和组合
- 多重继承
- 访问控制
- 向上转型
相关文章:
十四、继承与组合(Inheritance Composition)
十四、继承与组合(Inheritance & Composition) 引言 C最引人注目的特性之一是代码复用。组合:在新类中创建已有类的对象。继承:将新类作为已有类的一个类型来创建。 14.1 组合的语法 Useful.h //C14:Useful.h #ifndef US…...

ValueError: Caught ValueError in DataLoader worker process 0.
参考链接: https://stackoverflow.com/questions/1841565/valueerror-invalid-literal-for-int-with-base-10 它提示我有个地方值错误空字符 果然因为格式处理没有传进去东西,找下原因,让它正常处理 原来是相对路径的.影响了程序运行 将v…...

【数据结构】——链表OJ(下)
前面我们已经刷了几道单链表的题目,下面我们继续看几道题目。 一、相交链表 这道题题目的要求是很好理解的,就是现在我们有两个链表,然后我们就相办法进行判断,这两个链表是否是相交的,那么链表的相交其实就是有没有共…...

Adobe Acrobat pro在一份PDF中插入空白页
在Adobe Acrobat pro中先打开我们的PDF文件; 用鼠标点击需要插入空白页处的上一页; 然后如下图操作: 默认会在光标处的下一页插入一张空白页,你也可以修改插入页的页码或者向前一页插入...

java-----异常
对于Error:表示系统级错误或者资源耗尽的状况,像OutOfMemoryError、StackOverflowError等。这类错误是程序无法处理的,通常也不应该尝试去处理。 对于Exception:表示程序可以处理的异常。它又能细分为: 受检查异常&a…...

[工具]B站缓存工具箱 (By 郭逍遥)
📌 项目简介 B站缓存工具箱是一个多功能的B站缓存工具,包含视频下载、缓存重载、文件合并及系统设置四大核心功能。基于yutto开发,采用图形化界面操作,极大简化B站资源获取与管理流程。 工具可以直接将原本缓存的视频读取&#…...
《内网渗透测试:绕过最新防火墙策略》
内网渗透测试是检验企业网络安全防御体系有效性的核心手段,而现代防火墙策略的持续演进(如零信任架构、AI流量分析、深度包检测)对攻击者提出了更高挑战。本文系统解析2024年新型防火墙的防护机制,聚焦协议隐蔽隧道、上下文感知绕…...
python_竞态条件
好的,我们通过一个具体的例子来说明在多线程环境中,可变对象和不可变对象的行为差异,以及不可变对象如何避免竞态条件(race condition)。 1. 竞态条件(Race Condition) 竞态条件是指在多线程环…...
聊聊JetCache的CachePenetrationProtect
序 本文主要研究一下JetCache的CachePenetrationProtect CachePenetrationProtect com/alicp/jetcache/anno/CachePenetrationProtect.java Documented Retention(RetentionPolicy.RUNTIME) Target({ElementType.METHOD, ElementType.FIELD}) public interface CachePenetr…...
【实战】基于 ABP vNext 构建高可用 S7 协议采集平台(西门子 PLC 通信全流程)
🚀🔧【实战】基于 ABP vNext 构建高可用 S7 协议采集平台(西门子 PLC 通信全流程)📊 📑 目录 🚀🔧【实战】基于 ABP vNext 构建高可用 S7 协议采集平台(西门子 PLC 通信全…...
数据结构:树(Tree)
目录 为什么需要树? 🌱 基本的树结构定义 什么是树? 树的术语 🌿 常见基本树的变体 🌳 二叉搜索树(BST) 🌲 自平衡二叉搜索树 1. AVL树(Adelson-Velsky and La…...

自动化测试与功能测试详解
🍅 点击文末小卡片,免费获取软件测试全套资料,资料在手,涨薪更快 什么是自动化测试? 自动化测试是指利用软件测试工具自动实现全部或部分测试,它是软件测试的一个重要组成 部分,能完成许多手工测试无…...
java中的Optional
在 Java 8 中,Optional 是一个用于处理可能为 null 的值的容器类,旨在减少空指针异常(NullPointerException)并提升代码的可读性。以下是 Optional 的核心用法和最佳实践: 1. 创建 Optional 对象 1.1 常规创建方式 Op…...
Qt事件循环机制
受事件循环机制影响,按钮的样式表改变了可能不会立即刷新。 需要使用 update() 或 repaint() 或者调用 QApplication::processEvents() 强制处理所有待处理的事件,从而确保界面更新。 在 Qt 中,事件循环(Event Loop)是…...
深入理解 OAuth 2.0:技术核心与实战场景
在互联网应用日益复杂的今天,如何安全、高效地实现第三方应用授权访问资源,成为开发者面临的重要问题。OAuth 2.0 凭借其灵活、安全的授权机制,成为解决这一问题的主流方案。本文将深入剖析 OAuth 2.0 的技术重点,并结合具体使用场…...
Rust 环境变量管理秘籍:从菜鸟到老鸟都爱的 dotenv 教程
前言 写代码的你,是否遭遇过这些灵魂拷问: “我现在在哪个环境?开发?测试?还是直接在生产线上裸奔?”“少写一个 .env,测试脚本在数据库里上演清空大法,客户当场破防。”“每次手动设置 RUST_ENV,命令敲到一半就开始怀疑人生,还怕输错一个字符引发灭世级事故。”别慌…...

CSS经典布局之圣杯布局和双飞翼布局
目标: 中间自适应,两边定宽,并且三栏布局在一行展示。 圣杯布局 实现方法: 通过float搭建布局margin使三列布局到一行上relative相对定位调整位置; 给外部容器添加padding,通过相对定位调整左右两列的…...

OpenCV 的 CUDA 模块中用于将多个单通道的 GpuMat 图像合并成一个多通道的图像 函数cv::cuda::merge
操作系统:ubuntu22.04 OpenCV版本:OpenCV4.9 IDE:Visual Studio Code 编程语言:C11 算法描述 在 OpenCV 的 CUDA 模块中,cv::cuda::merge 函数用于将多个单通道的 GpuMat 图像合并成一个多通道的图像。该函数是 cv::merge 的 GP…...

计网实验笔记(一)CS144 Lab
Lab0 ByteStream : 实现一个在内存中的 有序可靠字节流Lab1 StreamReassembler:实现一个流重组器,一个将字节流的字串或者小段按照正确顺序来拼接回连续字节流的模块Lab2 TCPReceiver:实现入站字节流的TCP部分。Lab3 TCPSender:实…...
Blog Contents
目录 Python Financing Medical Logistics Tool(IT & AI) 持续更新~ Python # Name URL 1 Python | Dashboard制作 Python | Dashboard制作-CSDN博客 2 Python | AKShare获取A股数据 Python | AKShare获取A股数据-CSDN博客 3 Python | A股指标对比 Python | A股…...

什么是ERP?ERP有哪些功能?小微企业ERP系统源码,SpringBoot+Vue+ElementUI+UniAPP
什么是ERP? ERP翻译过来叫企业资源计划,通俗的讲,应该叫企业的全面预算控制,其通常包括三个部分:工程预算、投资预算和经营预算(即产销存预算)。之所以做预算控制,是因为企业运作的…...

dockerfile: PaddleOCR hubserving api 服务
前言 目前 OCR 有比较成熟的方案,想着直接通过 docker 部署一个提供 api 接口服务,查看了一些开源方案,最终发现还是 PaddleOCR 比较好用。 本篇不介绍 PaddleOCR 的详细使用方式,只介绍一下构建镜像的 dockerfile 需要注意的事…...
【速写】TRL:Trainer的细节与思考(PPO/DPO+LoRA可行性)
序言 问题缘起来自发现PPOTrainer里并没有跟SFTTrainer类似的peft_config参数,而SFTTrainer在带和不带peft_config参数的情况下分别对应高效微调和全量微调。自然就会想到是否可以把PPO和PEFT结合,但是目前peft包和trl包上似乎还是存在这种兼容性的问题…...

Vue3+uniapp 封装axios
1.第一步在项目根目录新建utils文件夹,里边新建两个文件request.js和uni-api-promisify.js 2.request.js 代码 要安装axios import axios from axios import { showToast } from /utils/uni-api-promisify// 创建axios实例 const service axios.create({baseURL:…...

QEMU模拟32位ARM实现自定义系统调用
实现自定义系统调用 如何使用 QEMU 模拟32位 ARM 环境参考:使用Qemu模拟32位ARM系统 修改linux内核源码 使用 linux-4.4.240 源码,下载链接:下载链接 在 arch\arm\include\uapi\asm\unistd.h 文件下新增系统调用 sys_test: /…...

MySQL——数据类型表的约束
目录 数据类型 数值类型 tinyint类型 bit类型 float类型 decimal类型 字符类型 char类型 varchar类型 日期和时间类型 选择类型 表的约束 null default comment zerofill primary key auto_increment unique key foreign key 数据类型 在MySQL中的数据类…...

# YOLOv2:目标检测的升级之作
YOLOv2:目标检测的升级之作 在目标检测领域,YOLO(You Only Look Once)系列算法以其高效的速度和创新的检测方式受到了广泛关注。今天,我们就来深入探讨一下 YOLOv2,看看它是如何在继承 YOLOv1 的基础上进行…...

【爬虫】DrissionPage-1
官网地址:DrissionPage官网 小需求采集,我喜欢,我要学。 1 介绍 这是用python编写的爬虫自动化工具,将Selenium 和 Requests 的功能巧妙地整合在一起,提供了统一又简单的操作接口。开发者可以在浏览器模式࿰…...

Oracle OCP认证考试考点详解083系列15
题记: 本系列主要讲解Oracle OCP认证考试考点(题目),适用于19C/21C,跟着学OCP考试必过。 71. 第71题: 题目 解析及答案: 关于在 Oracle 18c 及更高版本中基于 Oracle 黄金镜像的安装,以下哪…...
java刷题基础知识
List<int[]> merged new ArrayList<int[]>(); return merged.toArray(new int[merged.size()][]); 表示一个存储 int[] 类型元素的列表,list灵活支持扩展,因为不知道最后有几个区间,所以用list,最后toArray返回成数组…...