面向对象设计核心的思想便是抽象、封装、继承、多态和接口。继承机制是面向对象设计过程中动态性和模块性的来源,而多态性则是很多功能灵活性的来源。
面向对象设计核心是抽象的思维,但是”重复两次的代码就可能有坏味道“,如果仅仅通过抽象将不同的功能模块具象成对象,但是不同对象间很可能存在着很强的相似性,如果独立成类,必然会造成重复,这便是继承机制出现的目的:复用。不过继承虽好,但是继承是强耦合关系,除非需求确实明确需要。
接着说说最常见的关于C++类的问题,即为何所有子类对象都可用父类类型进行强制转化和使用?其实这应该是涉及到符号表的原理。通过一组代码实例来演示类之间的类型转换
#include <iostream>
#include <cstdio>
using namespace std;
class A
{
public:
void foo() {
printf("get inside A::foo\n");
}
virtual void fun() {
printf("get inside A::fun\n");
}
protected:
void protected_func() {
printf("get inside A::protected_func\n");
}
};
class B: public A
{
public:
using A::protected_func;
void foo() {
protected_func();
printf("get inside child B::foo\n");
}
void new_foo()
{
printf("get inside child B::new_foo\n");
}
void fun() {
printf("get inside child B::fun\n");
}
};
class C: protected A
{
public:
using A::protected_func;
void new_fun()
{
printf("get inside child C::new_fun\n");
}
};
int main() {
printf("********用父类A类型可以访问任何子类***************************\n");
printf("1. 用父类A类型约束子类B的对象,可以看到访问的是A的函数\n");
((A*)new B())->foo();
printf("\n");
printf("2. virtual多态性:用父类A类型约束子类B的对象,但访问到的是子类B定义的virtual fun函数\n");
((A*)new B())->fun();
printf("\n");
//(new A())->protected_func();
printf("*********用低等级的子类类型可以访问高位的父类对象吗?************\n\n");
printf("1. 用子类B类型约束父类A的对象,确实可以访问子类B的函数\n");
((B*)new A())->new_foo(); //调用B类的foo()函数
printf("\n");
printf("2. 在子类中通过using A::protected显式声明的机制,更改A类某些属性的可见性\n");
((B*)new A())->foo(); //通过using A::protected将A类本外部不可见的protected_func()在子类B中更改了可见性
printf("\n");
printf("3. 即便是父类A中没有的元素new_func(),通过显式的类型转换依旧可以访问到子类C中才定义的new_fun()\n");
C* c = (C*) new A();
c->new_fun();
//c->foo(); // A::foo不可访问,因为“C"类使用”protected”从“A”继承
printf("\n");
return 0;
}
从上面的运算过程可以看到其实关于类的函数信息从来就是绑定在符号表中的,而并不占用类具体实例对象的内存空间。如下验证程序
#include <iostream>
#include <cstdio>
using namespace std;
class A
{
public : int a, b;
public:
void foo() {
printf("get inside A::foo\n");
}
protected:
void protected_func() {
printf("get inside A::protected_func\n");
}
};
int main() {
A a;
cout<<"A类实例对象的size: "<<sizeof(a)<<endl;
return 0;
}
运行结果如下:
可以看到显然A a
实例对象在内存空间中只保留了两个属性值的空间,而函数信息并没有跟随在对象中,所以关于类的信息在符号表中的样子很可能是这样的,其中可以看到关于函数的信息是写死在符号表中的,从而才可以不占用实例对象的内存空间。当然也正是因为这个原因,父类类型强制转换任何子类对象都可以正常运行父类接口,但是反向并不一定成立,需要在一些特殊情况下才可以实现(比如在子类使用的函数不需要调用在子类中新定义的属性值)。
Q: c++ 中为什么存在public protect private 三种访问权限?
A:1. 抽象封装是模块化编程的基础,如同房间内部的装饰家居被内聚化,但是再好的房子必须要留门让外部对象接触,否则也就成了黑盒子,称为信息孤岛,那么完全没有存在的必要。这些留给外部对象接触的接口就是public
,否则所做的工作别人没法用,将是毫无意义的。
2 . 如果我们不想让别人知道内部的实现细节,那么就是private,它也是封装特征的具体语法实现;
3 . 至于protected的出现,则是因为抽象封装机制之后还有继承机制,三者存在一个交叉地带,即不希望外部对象可以访问,但是又不想像private那样吝啬的一毛不拔,想要作为传家宝传递给子类,这便是protected出现的原因。
因为有需求,所以才有这样的设计!
多态:一个接口,多种不同的实现方式。程序在运行时才决定调用的函数是什么,优点类似于“延迟绑定”的机制,但是相比于动态链接中延迟绑定出于加速程序加载速度的目的,多态是为程序提供更自由的编程空间。 多态与非多态的区别:函数地址是编译时绑定还是运行时绑定。
C++支持两种多态性:编译时多态性,运行多态性。
编译时多态性是通过重载函数实现(配合函数名修饰规则实现addressable),可以参考不同形参的构造函数。
运行时多态性是通过虚函数实现。
至于为什么在有了继承机制的情况下,还要推出virtual多态机制,其实这便需要结合设计模式中的里氏代换原则来解释了。
里氏代换原则:如果一个软件实体使用的是一个父类的话,那么一定适用于其子类,而且该软件实体察觉不出父类对象和子类对象的区别。也就是说,在软件里面,把父类double替换成它的子类,程序的行为没有变化。简单地说,子类必须可以无差别地替换父类的功能。
举例子如下,下面给出代码系统初始的情况,定义一个father类供客户端使用
class father{
public:
operationA(){...}
operationB(){...}
}
int main(){
father tempObject = new fathter();
/*to-do something using tempObject***/
tempObject.OperationA();
tempObject.OperationB();
/*end **/
//...
}
假设后来operationA()
对应的算法需要修改,这是显然可以通过创建一个子类child
继承类father
并重写operationA()
即可,但是在继承机制下有一个明显的弊端,就是一旦更新了新的子类child
,那么client
必须得知道新的子类的信息,这样才能启用新添加的方法
child tempObject = new child();
//...
但是假设对于一个算法库而言,如果每次更新都要求用户重新学习一些新添加的信息并修正以前已经在运行的代码是不合理的,但是如果依旧在client
中采用father tempObject = new father()
的声明,新修改的operationA()
并不能启用,这该如何是好?
答案是明显的,既然关于类的函数地址信息一开始已经在符号表中被固定死,类的实例对象所占内存空间只是那些属于当前对象的属性值集合。那么完全可以在类的实例对象中引入一个域专门用来指向想要使用的子类函数(比如child::operationA()
)。但是遵循着同样的对象不能在虚拟地址空间中存在两处,显然多态机制作为继承机制的补充,必然要在继承机制做出修改的。所以C++多态是通过虚函数关键词virtual
实现的,虚函数允许子类重新定义成员函数,子类override父类的函数实现。所以在类的实例对象中添加了一个隐含的属性值:虚表指针,专门用来指向当前对象所属具体类型的虚函数地址信息集合。
#include <iostream>
#include <cstdio>
using namespace std;
class A
{
public : int a, b;
public:
void foo() {
printf("get inside A::foo\n");
}
void virtual fun(){
printf("get insider A::virtual::fun\n");
}
void virtual lol(){
printf("get insider A::virtual::lol\n");
}
protected:
void protected_func() {
printf("get inside A::protected_func\n");
}
};
int main() {
A a;
cout<<"A类实例对象的size: "<<sizeof(a)<<endl;
return 0;
}
运行结果如下
该程序相比于此前的程序只是在A类中添加了两个虚函数,则可以看到在具体的实例对象所占用空间中多出了4,便是用来存放虚表指针的。
所以可以想象到引入虚表指针后类的符号表运行机制如下
在C++中,一旦某个函数在基类中被声明为virtual
,那么在所有的派生类中该函数都是virtual
,而不需要再显式地声明为virtual
。
纯虚函数
将函数定义为纯虚函数(方法:virtual ReturnType Function()= 0;
)如virtual void func()=0
;,则编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。纯虚函数是一种特殊的虚函数,它只有声明,没有具体的定义。抽象类中至少存在一个纯虚函数;存在纯虚函数的类一定是抽象类。
Abstract 抽象类
在C++中,我们可以把只能用于被继承而不能直接创建对象的类设置为抽象类(Abstract Class)。但是abstract并非C++的关键字。在很多情况下,基类本身生成对象是不合情理的。为了解决这个问题,引入了纯虚函数的概念,将函数定义为纯虚函数,则编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,由于抽象类包含了没有定义的纯虚函数,它不能生成对象。
Q:为什么要用纯虚函数?
在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。为了解决这个问题,方便使用类的多态性,引入了纯虚函数的概念,将函数定义为纯虚函数,则编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。