c++的组合/继承与多态
类与类之间,存在组合关系与继承关系。组合关系是比较简单方便的,能用就用,别滥用继承关系
1.类的组合关系
- 所谓组合关系,就是一个类中包含了其他类。具体的实现方法很简单,将其他类的对象作为当前类的成员使用,那么就构成了组合关系
class Computer //电脑类
{
Memory mMem; //内存类的对象
Disk mDisk; //硬盘类的对象
CPU mCPU; //cpu类的对象
MainBoard mMainBoard; //主板类的对象
public:
Computer()
{
}
};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 这些对象仅仅就是普通的成员变量罢了,没什么值得注意的
2.类的继承关系
继承,是面向对象中代码复用的重要手段
继承关系的特性
- 所谓继承关系,就是子类继承父类所有的特质(元素),同时可新增父类没有的特质(元素),也可重写继承得到的特质(元素)。继承关系有一些独特的性质:
- 子类重写/新增的成员函数,无法直接访问继承到的成员变量。为了克服这一点,父类中一般不用private权限,而用protected权限。如下面,Dell中的test便可直接访问mv
- 尽管子类在概念上是父类的子集,但是子类所含的特质(元素)是父类所含特质(元素)的超集,所以子类可以直接赋值给父类
class Computer
{
protected:
int mv;
public:
Computer()
{
mv = 100;
}
void change()
{
mv = 100;
}
};
class Dell : public Computer
{
public:
void test() //这个成员函数是子类新增的
{
mv = 123;
}
};
int main()
{
Dell dell;
Computer pc = dell; //可以直接用子类初始化父类
pc = dell; //也可以用子类赋值父类
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- c++支持三种继承方式,上面代码中用的是最常见的public方式,其实在工程中一般只使用public继承,不推荐其他继承方法!!
- public继承:父类成员在子类中保持原有的访问级别
- private继承:父类所有成员在子类中变为private权限
- protect继承:父类public成员在子类中变为public权限,其他成员保持不变
父子间的同名覆盖
- 子类中可以定义父类中同名的成员变量,子类中的同名成员变量将覆盖(隐藏)父类成员变量,可以用
父类名::成员变量名
来获取被覆盖(隐藏)的父类成员 - 子类中可以定义父类中同名的成员函数,不论参数是否相同,子类中的同名成员函数都会覆盖(隐藏)父类成员函数,可以用
父类名::成员函数名
来获取被覆盖(隐藏)的父类成员函数。不难得出结论,子类不能重载父类的成员函数
class Parent
{
public:
int mi;
void add(int v)
{
mi += v;
}
};
class Child : public Parent
{
public:
int mi;
void add(int a, int b)
{
mi += (a + b);
}
};
int main()
{
Child c;
c.mi = 100;
c.Parent::mi = 1000;//只能这样访问父类中的成员变量
c.add(1);
c.Parent::add(4, 5, 6);//只能这样访问父类中的函数
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
3.继承和组合类的构造与析构
当一个类是子类,同时它又包含了其他类,那么当它实例化时,如何初始化成员变量?
- 先执行父类中的构造函数:若子类初始化列表中,调用了父类中的构造函数,则执行之;初始化列表中若未调用,则执行父类中显式定义的无参构造函数
- 之后执行自己内部对象的构造函数
- 最后执行子类自己的构造函数
- 口诀:先父母,后客人,再自己
- 当对象的声明周期结束时,析构函数的触发顺序和构造顺序完全相反,即:先自己,后客人,再父母
4.重写、多态、虚函数
多态的目的,其实是用来在子类中实现良好的、可靠的函数重写
- 我们可以在子类中重写父类的成员函数,但是有时会发生一种子类退化的现象,那么这是重写就会失效
- 子类退化:当我们用一个父类类型的指针/引用,去指向一个子类对象时,该指针和引用指向的将会是一个退化为父类的子类对象
- 造成的结果就是,利用这种指针/引用,无法访问子类中独有的成员,只能访问父类中的成员,重写也会失效。根本原因是,编译器只能根据指针/引用的类型,来判断指向的对象的类型
class Parent
{
public:
void add()
{
cout << "123" << endl;
}
};
class Child : public Parent
{
public:
void add()//重写过的函数
{
cout << "abc" << endl;
}
};
int main()
{
Child c;
Parent *p = c;//父类型的指针指向了子类对象
p ->add();//由于指针类型的关系,这里访问的是父类中原本的函数,访问不了子类重写过的函数
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 由此,我们为了避免子类退化引起的重写失效,引入了多态。即使用virtual来修饰父类、子类中重写的函数,使其成为“虚函数”。这样,即使子类退化,仍然可以调用重写过的成员函数
class Parent
{
public:
virtual void add()//理论上父类子类中重写的函数都要修饰,其实只需在父类中修饰即可
{ //因为virtual属性也会被继承给子类中的add函数
cout << "123" << endl;
}
};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 值得注意的是,构造函数不能成为虚函数!只有构造函数执行后才可建立虚函数表。不过作为补偿,编译器对构造函数还是实现了多态(详见第5节继承和组合类的构造与析构),也没必要设置为虚函数了。与之相反,析构函数一定要设置为虚函数来实现多态,否则可能会内存泄漏
- 构造函数和析构函数本身可以多态,但是内部却不能发生多态行为(调用虚函数的效果仅仅为调用当前类中对应的那个成员函数),因为虚函数表的生命周期“始于构造,终于析构”
- 所以,会被重写的函数以及析构函数,必须使用在父类中使用virtual修饰,以此让它们成为虚函数,以实现多态
多态的本质,就是动态链编:让程序在运行时才去确认函数的具体调用,而不是在编译期间就确认具体的函数调用
5.安全多重继承
c++中一个子类可以继承多个父类,这也是对现实世界的一种描述,然而直接使用多重继承是不可靠的,工程中不会使用纯粹的多重继承
- 实际开发中,常常使用单继承多接口来实现多重继承
class Base
{
protected:
int mi;
public:
Base(int i)
{
mi = i;
}
};
class Interface1
{
public:
virtual void add(int i) = 0;
};
class Interface2
{
public:
virtual void multiply(int i) = 0;
};
class Derived : public Base, public Interface1, public Interface2
{
public:
Derived(int i) : Base(i)
{ }
void add(int i)
{
mi += i;
}
void multiply(int i)
{
mi *= i;
}
};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 这种方法实现的多重继承,可以完美的支持多态和重写,
6.用继承实现抽象类
所谓抽象类,就是用类表达一种概念而非实体,比如定义一个电脑类
抽象类的概念
- 抽象类有如下的特质
- 抽象类作为父类而存在,只能被继承
- 抽象类自己不能实例化,因为其实例化没有意义
- 抽象类中,通常有些成员函数不提供具体实现
- 一般来说,如果一个父类没有实例化的意义,那么我们就应该将其实现为抽象类
抽象类的实现方法
只可惜,c++中没有原生的抽象类,需要用纯虚函数来实现
- 当虚函数没有具体实现,并且我们对其赋值为0时,编译器就会认为这个虚函数是纯虚函数(若不赋值为0,链接器将报错)
- 将父类中的成员函数定义为纯虚函数(即函数没有具体的实现,只能由子类重写来实现),那么这个父类将无法被实例化,这样这个父类就成功变成抽象类了
class Shape
{
public:
virtual double area() = 0;//纯虚函数,通过给虚函数赋值0来实现
};
- 1
- 2
- 3
- 4
- 5
接口的概念
所谓接口,就是一组行为的规范(一组函数集合)
- c++中没有原生的接口,而是通过抽象类实现。接口是一种特殊的抽象类,它要满足:
- 抽象类中没有任何成员变量,只有成员函数
- 所有的成员函数都是public的、都是纯虚函数
- 如下,就实现了一个接口,该接口可以被继承为串口、usb、蓝牙等等具体的通信方式
class Channel
{
public:
virtual bool open() = 0;
virtual void close() = 0;
virtual bool send(char* buf, int len) = 0;
virtual int receive(char* buf, int len) = 0;
};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9