十四、继承与组合(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…...
自主添加删除开机启动项
背景 有些程序我们需要每次开机自启动,譬如自装的第三方输入法或者网络代理软件等等,而有些程序我们不希望它每次开机自启动,但是奈何这些软件安装的时候自己就给配置好了开机自启动,咱们不知道该去哪找。 anyway,问题…...
2025年第十六届蓝桥杯软件赛省赛C/C++大学A组个人解题
文章目录 题目A题目C:抽奖题目D:红黑树题目E:黑客题目F:好串的数目 https://www.dotcpp.com/oj/train/1166/ 题目A 找到第2025个素数 #include <iostream> #include <vector> using namespace std; vector<i…...
Clickhouse 迁移到 Doris 的最佳实践
一、引言 在将数据从 Clickhouse 迁移到 Apache Doris / SelectDB Cloud 的过程中,涉及表结构迁移、查询语句迁移以及数据迁移等多个关键环节。每个环节都有其复杂性和需要注意的细节,本文将详细介绍这些内容及对应的最佳实践方法。 二、表结构迁移 &…...
thinkphp模板文件缺失没有报错/thinkphp无法正常访问控制器
省流:没有出现下图的报错可能是空路由规则的问题。 编者在编写一个新的控制器时还未建立模板文件,理应出现如下报错 但是浏览器非但没有报错,反而无法正常访问所有有问题的控制器,表现为都为空白页面,正常的控制器可…...
Spring AI 与 Hugging Face 深度集成:打造高效文本生成应用
Spring AI 与 Hugging Face 深度集成:打造高效文本生成应用 前言 在人工智能技术蓬勃发展的时代,大型语言模型(LLM)在自然语言处理领域展现出了强大的能力。Hugging Face 作为人工智能社区的重要一员,提供了丰富的模…...
异步FIFO的学习
一、参考视频 FPGA(异步FIFO原理及Verilog代码实现)_哔哩哔哩_bilibili 二、设计图 高位套圈时,格雷码和二进制不一样的地方 需要注意的问题 为什么二进制的变化位数更多,就更容易产生亚稳态呢? 格雷码 格雷码&…...
Java——API基础(String类和StringBuilder类)
一、API概述 API:应用程序编程接口(是一些包含了属性和方法的类) Java API:指的就是JDK中提供各种功能的Java类 二、String类(在lang包下,不需要导包) (一)概述 1.J…...
OpenCV图像金字塔详解:原理、实现与应用
一、什么是图像金字塔? 图像金字塔是图像处理中一种重要的多尺度表示方法,它通过对图像进行重复的平滑和降采样(或上采样)操作,生成一系列分辨率逐渐降低(或升高)的图像集合。这种结构形似金字…...
AI Agent(11):垂直行业应用
引言 本文将聚焦AI Agent在金融、医疗健康、制造业以及零售与电商四个重要垂直行业的应用。我们将分析每个行业的特定需求和挑战,探讨AI Agent如何通过专业化能力为这些行业创造价值,并展望未来发展趋势。 垂直行业AI Agent的核心价值在于将通用AI能力与行业专业知识深度结…...
FFmpeg 项目中的三大核心工具详解
FFmpeg 项目中的三大核心工具详解 FFmpeg 是一个功能强大的开源多媒体框架,能够处理几乎所有格式的音视频文件。它包含三个主要的命令行工具:ffmpeg、ffplay 和 ffprobe,这三个工具各自承担不同的功能,共同构成了 FFmpeg 项目的核心。下面将全面详细地介绍这三个工具。 1…...
【Linux网络】 HTTP cookie与session
HTTP cookie与session 引入HTTP Cookie 定义 HTTP Cookie(也称为Web Cookie、浏览器Cookie或简称Cookie)是服务器发送到用户浏览器并保存在浏览器上的一小块数据,它会在浏览器之后向同一服务器再次发起请求时被携带并发送到服务器上。通常&…...
STM32入门教程——GPIO输出
前言 本教材基于B站江协科技课程整理,适合有C语言基础、刚接触STM32的新手。它梳理了STM32核心知识点,帮助大家把C语言知识应用到STM32开发中,更高效地开启STM32学习之旅。 目录 前言 1.知识储备 1.GPIO简介 2.GPIO基本结构 1.APB2外设…...
MongoDB使用x.509证书认证
文章目录 自定义证书生成CA证书生成服务器之间的证书生成集群证书生成用户证书 MongoDB配置java使用x.509证书连接MongoDBMongoShell使用证书连接 8.0版本的mongodb开启复制集,配置证书认证 自定义证书 生成CA证书 生成ca私钥: openssl genrsa -out ca…...
FEKO许可证的安全与合规性
在电磁仿真领域,FEKO软件因其出类拔萃的性能和广泛的应用场景,赢得了全球用户的广泛赞誉。但在这背后,是什么让FEKO在众多竞争者中脱颖而出?答案是其许可证的安全与合规性。它们不仅为用户提供了坚固的保障,更确保了用…...
AI大模型学习二十、利用Dify+deepseekR1 使用知识库搭建初中英语学习智能客服机器人
一、说明 很多情况下 LLM 知识库可以让 Agent 从中定位到准确的信息,从而准确地回答问题。在一些特定领域,比如客服、检索工具等有应用。 传统的客服机器人往往是基于关键词检索的,当用户输入了关键词以外的问题,机器人就无法解决…...
kubuntu系统详解
Kubuntu 系统深度解析(从系统架构到用户体验) 一、定位与核心特性 Kubuntu 是 Ubuntu 的官方 KDE 衍生版,基于 Ubuntu 的稳定底层(Debian 技术栈),搭载 KDE Plasma 桌面环境,主打 “功能丰富、…...
【AutoGen革命】多智能体协作系统的架构设计与工程实践
目录 🌍 前言🏗️ 技术背景与价值🚨 当前技术痛点🛠️ 解决方案全景👥 目标读者画像 🧠 一、技术原理剖析🖼️ 系统架构图解💡 核心运行机制⚙️ 关键技术组件🔄 技术选型…...
西电 | 2025年拟录取研究生个人档案录取通知书邮寄通知
各位考生: 我校2025年硕士研究生录取工作已结束,根据相关工作管理规定,现将个人档案转调及录取通知书邮寄信息确认等有关事宜通知如下: 一、个人档案转调 (邮寄档案请务必使用EMS) 1.全日制考生 录取类…...
9.0 C# 调用solidworks介绍1
一、C# 与 SolidWorks 联合开发概述 SolidWorks 提供了完整的 API(应用程序接口),允许开发者使用 C# 等编程语言进行二次开发,实现自动化设计、定制功能等。 主要技术要点包括: 1. API 结构:SolidWorks API 是基于 COM 的接口,包含数百个对象和数千个方法…...
Linux复习笔记(三) 网络服务配置(web)
遇到的问题,都有解决方案,希望我的博客能为你提供一点帮助。 二、网络服务配置 2.3 web服务配置 2.3.1通信基础:HTTP协议与C/S架构(了解) HTTP协议的核心作用 Web服务基于HTTP/HTTPS协议实现客户端ÿ…...
git和gdb
git基础使用 相关概念 本地仓库:自己电脑上git客户端 远端仓库:管理员端的git服务端 多人协作:文件开源,可以多个人一起修改 前提 1.一个仓库 2.确认git有没有安装 3.把远端仓库clone 这一步执行完后我们执行ll可以看到&…...
CSRF记录
CSRF(Cross-site request forgery)跨站请求伪造:攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请求。利用受害者在被攻击网站已经获取的注册凭证,绕过后台的用户验证,达…...
嵌入式MCU和Linux开发哪个好?
MCU与Linux:十年磨剑后的选择之道 MCU和Linux到底怎么选?这是一个老生畅谈的问题。 而我更想说,这不是简单的优劣对比,而是两条不同的道路,通往不同的风景。今天,我想以一个老兵的视角,聊聊这…...
2024年北理工Python123第六章编程题整理
这章的编程题都好少,难度也不高 开始进入文件的输入输出 一、字典翻转输出 我的代码: ori_dic eval(input()) dic{} if(not isinstance(ori_dic,dict)):#验证输入格式print(输入错误) else:for item in ori_dic.keys():dic[ori_dic.get(item)]itempri…...
一、HAL库的设计理念详解:从架构到实践
HAL库的设计理念详解:从架构到实践 一、HAL库的诞生背景与核心目标 STM32 HAL库(Hardware Abstraction Layer)是STMicroelectronics在2016年推出的新一代驱动框架,旨在解决STM32系列芯片不断扩展带来的开发复杂性问题。随着STM3…...
【Python】UV:单脚本依赖管理
一、基础概念 什么是 Python 脚本 以 .py 结尾的文件,可通过 python script.py 独立执行。UV 工具优势:无需手动创建或激活虚拟环境,自动为每个脚本生成隔离环境,保证依赖互不干扰。 环境管理原理 graph LRA[系统 Python 环境] -…...
多线程(2)——Thread类及常见方法
目录 构造方法常见属性前台线程,后台线程 启动一个线程——start()中断(终止)一个线程方法1:通过变量方法2:使用 isInterrupted() ---线程内置的标志位 等待一个线程——join()join设置等待时间 获取当前线程引用---cu…...
Neo4j 入门级使用
一、集成步骤 (一)创建 Spring Boot 项目 使用 Spring Initializr 创建项目时,选择 Maven 或 Gradle 作为项目构建工具,选择合适的 Spring Boot 版本,并添加 “Spring Data Neo4j” 依赖。 (二ÿ…...
解决 CJSON 浮点数精度问题:从 `cJSON_AddNumberToObject` 到 `cJSON_AddRawToObject`
在使用 CJSON 库处理浮点数时,开发者常会遇到一个棘手问题:浮点数的小数位精度丢失。例如,数值 3.1400 可能被简化为 3.14,甚至 5.0 被显示为 5。这种默认行为在需要严格保留小数位的场景(如金融、物联网传感数据&…...
ESP32开发入门(九):HTTP服务器开发实践
一、HTTP服务器基础 1.1 什么是HTTP服务器? HTTP服务器是能够处理HTTP请求并返回响应的网络服务程序。在物联网应用中,ESP32可以作为轻量级HTTP服务器,直接接收来自客户端(如浏览器、手机APP)的请求。 1.2 ESP32作为HTTP服务器的特点 轻量…...
谱聚类,大模型
使用谱聚类将相似度矩阵分为2类的步骤如下: 1. **构建相似度矩阵**:提供的1717矩阵已满足对称性且对角线为1。 2. **计算度矩阵**:对每一行求和得到各节点的度,形成对角矩阵。 3. **计算归一化拉普拉斯矩阵**:采用对…...
K8S Ingress、IngressController 快速开始
假设有如下三个节点的 K8S 集群: k8s31master 是控制节点 k8s31node1、k8s31node2 是工作节点 容器运行时是 containerd 一、理论介绍 1)什么是 Ingress 定义:Ingress 是 Kubernetes 中的一种资源对象,它定义了外部访问集群内…...
AI边缘网关_5G/4G边缘计算网关厂家_计讯物联
AI边缘网关是边缘计算与人工智能技术深度融合的产物,作为连接终端设备与云端的桥梁,在网络边缘实现数据采集、实时分析、智能决策和协议转换,显著降低了数据传输延迟,节省了云端资源,并提升了隐私保护能力,…...
【Vue】Composables 和 Utils 区别
1. 核心功能与状态管理 • Composables 用于封装有状态的逻辑,通常结合 Vue 的响应式 API(如 ref、reactive)和生命周期钩子(如 onMounted),可管理组件内部的状态和副作用。例如,封装鼠标位置…...
右值和移动
值类别(value categories) lvalue 通常可以放在等号左边的表达式, 左值 例子 变量,函数或数据成员的名字返回左值引用的表达式,如x, x 1, cout << ’ . x 1 和 x返回的都是对x的int&. x则返回的是int字符串字面量如 “hello world” rva…...
如何在 Bash 中使用 =~ 操作符 ?
在 Bash 脚本世界中,有各种操作符可供我们使用,使我们能够操作、比较和测试数据。其中一个操作符是 ~ 操作符。这个操作符经常被忽视,但功能非常强大,它为我们提供了一种使用正则表达式匹配字符串模式的方法。 ~ 操作符语法 语法…...
消息队列RocketMQ-docker部署保姆级教程(从0到1)(2)
目录 引言 1. 准备工作 1.1 准备虚拟机 1.2 将虚拟机的ip设置为静态ip地址 1.3 什么是nat网络 1.4 测试网络 2. 准备docker环境 2.1 卸载旧docker(如果有) 2.2 安装依赖包 2.3 添加 Docker 官方仓库(国内推荐使用阿里云镜像…...
《算法导论(第4版)》阅读笔记:p32-p38
《算法导论(第4版)》学习第 12 天,p32-p38 总结,总计 7 页。 一、技术总结 1.analyzing algorithms (1)running time(运行时间) worst-case running time, average-case running time,best-case running-time。 2.order of growth/rate …...
《Effective Python》第1章 Pythonic 思维详解——深入理解流程控制中的解构利器match
《Effective Python》第1章 Pythonic 思维详解——深入理解流程控制中的解构利器match 引言 Python 3.10 引入了全新的 match 语句,它不仅是一个“类 switch”的语法结构,更是一种**结构化模式匹配(structural pattern matching)…...
【氮化镓】横向GaN 器件注入隔离区的电场相关载流子传输特性
文章的关键结论和发现如下: 在GaN横向功率器件中,注入隔离区的载流子传输具有明显的电场依赖性,且其泄漏电流和击穿特性主要由注入的GaN区域决定,与缓冲层和UID GaN层的性质关系不大。 载流子传输机制随电场强度变化呈现三个不同区域:低电场下为欧姆传导,符合变程跃迁(V…...
电子电器架构 --- 借力第五代架构,驱动汽车产业创新引擎
我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 钝感力的“钝”,不是木讷、迟钝,而是直面困境的韧劲和耐力,是面对外界…...
【ROS2】通信部署概述(以话题(Topic)通信为例)
写在前面 很多内容参考了赵虚左老师的ROS2教程 概念说明 工作空间:相当于一个项目,比如一台机械臂,或者一个机器人 功能包:相当于一个项目中的一台设备,如电机、雷达、相机等 大概构建流程 以话题通信机制为例 创…...
1.8 梯度
(知识体系演进逻辑树) 一元导数(1.5) │ ├─→ 多元偏导数(1.6核心突破) │ │ │ └─解决:多变量耦合时的单变量影响分析 │ │ │ ├─几何:坐标轴切片切线斜率…...
Pycharm中No Conda enviroment selected
最近在学习Anaconda,换了台新电脑重新安装PyCharm和Anaconda后,打算创建环境,创建后发现找不到刚刚在Anaconda中创建的环境,经过不断摸索后找到解决方案 将选择的路径从Anaconda目录下的python.exe换成Anaconda\Library\bin下的c…...
continue通过我们的开源 IDE 扩展和模型、规则、提示、文档和其他构建块中心,创建、共享和使用自定义 AI 代码助手
一、软件介绍 文末提供程序和源码下载 Continue 使开发人员能够通过我们的开源 VS Code 和 JetBrains 扩展以及模型、规则、提示、文档和其他构建块的中心创建、共享和使用自定义 AI 代码助手。 二、功能 Chat 聊天 Chat makes it easy to ask for help from an LLM without…...
关于读写锁的一些理解
同一线程的两种情况: 读读: public static void main(String[] args) throws InterruptedException {ReentrantReadWriteLock lock new ReentrantReadWriteLock();Lock readLock lock.readLock();Lock writeLock lock.writeLock();readLock.lock();S…...
Ubuntu网络部署LNMP环境
目录 1. 安装nginx 2. 安装mysql 3. 安装PHP 4. 配置nginx,修改默认配置文件 5. 配置PHP 1. 安装nginx apt install -y nginx#开启 systemctl start nginx#浏览器访问页面 192.168.180.200:80#nginx网页html存放路径 ls /usr/share/nginx/#查看nginx版本号 ng…...
榜单按行显示
手机芯片_SoC天梯榜_安兔兔跑分排行_安兔兔 我只关注 骁龙7Gen1,天玑7300,骁龙6Gen1,天玑900 除了 50,64,75,86 行都隐藏。 var uls document.getElementsByClassName(newrank-c); var s 50,64,75,86; var sa s.split(,); for (var i0…...
DVWA在线靶场-xss部分
目录 1. xxs(dom) 1.1 low 1.2 medium 1.3 high 1.4 impossible 2. xss(reflected) 反射型 2.1 low 2.2 medium 2.3 high 2.4 impossible 3. xss(stored)存储型 --留言板 3.1 low 3.2 medium 3.3 high 3.…...