继承和组合¶
继承语法¶
C++是面向对象的语言,面向对象最重要的就是继承。
一个类继承另一个类,用冒号:
表示继承。
三种继承: public,私有的,受保护的。
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;}
};
Y有两个i,一个是Y自己的,一个是继承的。但是继承的那个是私有的i,Y不能直接使用。
Y也可以自己添加成员函数,名字可以和X的一样:那么就是不要父类X的,而是用自己的函数。重定义。也可以使用X的, 不过要写上X::
class Y : public X {
int i;
public:
Y() {i = 0;}
int change() {
i = permute();
return i;
}
//重定义
void set(int ii) {
i = ii;
X::set(ii);
}
};
一个类继承另一个类。会继承数据成员和成员函数,根据需要添加新的功能,自己的成员,或者把继承下来的重定义。
使用继承效率比较高。拿来直接用。
继承中的构造和析构¶
- 构造函数的初始化表达式列表
- 构造函数和析构函数调用的次序
- 自动调用构造函数
- 自动调用析构函数
C++可以在初始化列表中初始化,值用小括号括起来。
没有写初始化列表,C++会默认的初始化。全是默认值。
在构造函数中对成员变量进行初始化有点晚了,因为C++会默认的有初始化,相当于初始化了两遍。
两个类
Y 父类
X 继承 Y
创建X的对象的时候要先把Y创建了,调用X构造函数也要调用Y的构造函数。
在X的初始化列表里调用Y的构造函数,把Y构造出来,Y在初始化列表的位置可以随便写,顺序不固定,一般习惯写前面。
如果在X的初始化列表里没有写Y, 则自动的调用Y的没有参数的构造函数(默认的构造函数)
基类 超类 父类, 派生类 子类
class Y {
int i;
public:
Y(int ii) : i(ii) {}
};
class X : public Y {
int i;
float f;
char c;
char* s;
public:
X():Y(10),i(7),f(1.4),c('x'),s("howdy") {
std::cout << "X()" << std::endl;
}
};
void test() {
X x;
}
高级:
typedef int Member1;
typedef int Member2;
typedef int Member3;
typedef int Member4;
class Base1 {
public:
Base1(int) {
std::cout << "Base1 constructor \n";
}
~Base1() {
std::cout << "Base1 destructor \n";
}
};
class Derived1 : public Base1 {
Member1 m1;
Member2 m2;
public:
Derived1(int) : m2(1),m1(2),Base1(3) {
std::cout << "Derived1 constructor \n";
}
~Derived1() {
std::cout << "Derived1 destructor \n";
}
};
Derived1的构造函数的初始化列表顺序可以随便写,但是执行调用的时候会先调用Base1的构造函数,然后调用m1,再调用m2。因为成员变量Member1定义在Member2前面。
class Derived2 : public Derived1 {
Member3 m3;
Member4 m4;
public:
Derived2(int) : m3(1),Derived1(2),m4(2) {
std::cout << "Derived2 constructor \n";
}
~Derived2() {
std::cout << "Derived2 destructor \n";
}
};
调用
void test() {
Derived2 d2(1);
}
只调用了Derived2 d2
。因为存在继承则会调用父类Derived1构造方法,Base1构造方法,初始化Derived1数据成员,Base1数据成员。这一行代码,会调用很多东西。
最先调用Base1构造方法,然后调用Derived1构造函数,然后调用Derived2自己的。
析构函数的调用和构造方法的顺序是完全相反的。
这些都是自动的。
同名的成员函数¶
子类和父类有同名的成员函数,子类调用函数的时候,调用自己的函数不会调用父类的。不会调用自己的还调父类的。
如果子类没有该函数 父类有该函数,子类调用函数的时候就会调用父类的。
构造函数会都调 调用父类的 自己的, 普通函数只调一个。
继承的其它要点¶
1、名字隐藏¶
基类¶
class Base {
public:
//三个成员函数
int f() const {
std::cout << "Base::f()\n";
return 1;
}
int f(std::string) const {
return 1;
}
void g() {}
};
子类¶
继承基类
- 子类
Derived2
继承base类
重写成员函数g()
- 子类
Derived2
继承base类
把f重写 重新定义
Derived2
调用f的话。是调用的自己的。
调用f(字符串),方法就会报错,因为名为f
的所有函数都隐藏了。
- 子类
Derived3
继承base类
Derived3
重定义f,并且返回值类型改为void
,即使不一样,这也会把重名称都隐藏。有返回值的,带参数的都隐藏了。
- 子类
Derived3
继承base类
重定义f,参数类型变了。和基类的参数类型都不一样,但是名称一样,还是会隐藏所有名字为f的函数。只能用自己的函数f,不能用基类的函数f了。
这些函数隐藏针对的都是非虚函数。
2、如果是虚函数重新定义¶
在多态中用
class Base {
public:
int virtual f() const {
std::cout << "Base::f()\n";
return 1;
}
int virtual f(std::string) const {
return 1;
}
void g() {}
};
3、非自动继承的函数¶
- 可以自动继承
- 数据成员
-
函数成员
-
不可以继承。自己用自己的。基类有,子类也不可以使用。
- 构造函数
- 析构函数
-
重载
=
运算符 -
其它的重载运算符 可以继承
子类可以添加自己的数据成员,函数成员,构造函数,析构函数。
4、公有继承,私有继承,保护继承¶
一般是用公有继承
继承相当于把基类中的所有成员拷贝下来
- 公有继承
原来是私有的还是私有的 原来是受保护的还是受保护的 原来是公有的还是公有的。
- 私有继承
子类拷贝基类成员,成员都变成私有的。 如果不写哪种继承默认是私有继承。
- 保护继承
相当于子类拷贝基类成员,成员都变成受保护的。
私有继承,保护继承没有用。仅做了解。
5、多重继承¶
class Z : public X, public Y, public Base {
};
多继承比较复杂,容易出问题。
继承中的向上类型转换¶
- 向上类型转换
- 基类与新类之间的关系:新类属于原有类的类型
- 向上类型转换和拷贝构造函数
继承:倒立的树形图表示,不断的继承,最上面的类叫根。
把子类型向上转为它的基类。
例:
- 乐器类
-
成员函数play 参数是一个枚举 中音、高音、低音
-
长笛 继承 乐器
- 创建一个函数 参数是乐器,调用乐器去play。
测试
创建一个乐器对象,调用函数参数传乐器对象
创建一个长笛对象,调用函数 参数传长笛对象。C++会自动的向上转型,把长笛转变为乐器类型。
可以利用这个特点, 写一些通用的函数。例:上面的play就是通用的函数,长笛,钢琴,大鼓都可以play。
向上类型转换和拷贝构造函数¶
class Parent {
int i;
public:
Parent(int ii) : i(ii) {
cout << "Parent(int ii)\n";
}
//拷贝构造函数
Parent(const Parent& b) : i(b.i) {
cout << "Parent(const Parent& b)\n";
}
//没有参数的构造函数
Parent() : i(0) {
cout << "Parent()\n";
}
//运算符重载
friend ostream& operator<<(ostream& os, const Parent& b) {
return os << "Parent: " << b.i << endl;
}
};
class Member {
int i;
public:
Member(int ii) : i(ii) {
cout << "Member(int ii)\n";
}
//拷贝构造函数
Member(const Member& m) : i(m.i) {
cout << "Member(const Member& m)\n";
}
//运算符重载
friend ostream& operator<<(ostream& os, const Member& m) {
return os << "Member: " << m.i << endl;
}
};
class Child : public Parent {
int i;
Member m;
public:
//构造函数,调用child的构造函数,所有的i的值都变为传进来的值
Child(int ii) : Parent(ii),i(ii),m(ii) {
cout << "Child(int ii)\n";
}
friend ostream& operator<<(ostream& os, const Child& c) {
return os << Parent(c)
<< c.m
<< "Child: " << c.i << endl;
}
};
测试
void test() {
Child c(2); //调用Child的构造函数 Parent的i为2,Child的i为2,Member的i为2.
cout << c; //会输出三个2
Child c2 = c; //调用拷贝构造函数
cout << c2;//还是三个2 因为没有写Child的拷贝构造函数,只写了parent和Member的拷贝构造函数,会调用C++默认添加的拷贝构造函数。
}
如果要写Child的拷贝构造函数的话,不能随便写,否则可能出错。
在使用Child类的时候,用到向上转型时,则Child的拷贝构造函数也要构造父类Parent。
组合¶
两种常用的程序设计方法
- 组合与继承的选择
判断类和类之间的关系
- has-a 有一个 则组合,
- is-a。 是一个 则继承。
组合的例子¶
汽车发动机
成员方法:
- 启动
- 后退
- 停止
汽车轮胎
- 充气
汽车车窗
- 打开
- 关闭
汽车车门
- 车门有车窗
- 打开车门
- 关闭车门
汽车 组合
- 发动机
- 4个轮胎
- 两个门
上面的组合还可以加上继承
- 新型的发动机(继承发动机)
- 新型的轮胎(继承轮胎)
汽车 (既有组合 又有继承)
继承的例子¶
- 图形形状
- 填充颜色
- 线的颜色
- 线的宽度
- 画图形
- 圆 继承形状
- 添加 半径
- 长方形 继承图形形状
- 宽度
- 高度
- 三角形 继承图形形状
- 圆角矩形 继承圆和矩形 双继承 C++可以多继承
组合和继承的联合¶
例¶
C继承B(继承), C里有A(组合)。既有继承又有组合。
class A {
};
class B {
};
class C : public B {
A a;
};
再论继承和组合¶
- 根据是否需要向上类型转换决定继承还是组合
- 是 --> 继承
- 否 --> 组合
- 可以使用组合代替继承,消除多重继承
有一个类简单或者复杂的类A,使用A的时候有下面几种
- 可以直接使用
- 做一个新的类B继承它
- 使用组合。在C类中有类A。做为C的成员。
例:之前做的堆栈stack,保存数据是任何数据,因为使用的万能指针。但是保存字符串的时候不方便,所以自己写一个字符串堆栈StringStack。
有两种方法:继承或组合。
继承¶
把要修改的地方重新定义。把继承下来的方法重新定义, 原方法就隐藏了。
这里修改的主要是返回值类型,和参数类型,之前是void* 现在改为string。
重写的方法里调用父类原方法。
并且子类可以在析构函数中实现delete,因为是string*,字符串指针,而父类delete要delete void*
万能指针,delete万能指针容易出问题,所以要由客户端自己delete。
子类比父类优点:
- 不需要转型,直接就是字符串类型
- 不需要delete。析构函数中就已经delete了。
如果需要向上类型转换的话(StringStack转为Stack),则使用继承。
因为上面的没有用到转换,所以应该使用组合而不是继承。
组合¶
新建一个class类 特殊的堆栈 字符串堆栈
内部有一个Stack通用的堆栈
同样的方法再实现一次,把原来的方法包装一下,然后把返回值参数类型由万能指针转成字符串类型,方便使用。
有A,B,C,X,Y,Z六个class。
新建一个class继承X,Y,Z。多继承。
多继承太复杂,容易出错,所以避免使用多重继承,用组合代替继承。
只用单继承X,另外两个使用组合,类中包含Y,Zclass。