前言
虽然了解c++是面向对象的语言,由于程序规模的原因,自己写的大多数C++代码里却很少用到这些面向对象的特性,比如多态、继承之类的,于是写C++变成了写"C with Class"…
本文主要从面向对象的特性来对C++重新进行思考,主要内容:
- 面向对象提出的背景,主要是用来解决什么问题?
- C++中的具体实现机制
- C++与其他语言的比较
- 面试中问到的C++
面向对象背景
面向过程的程序设计
与“面向对象”相对的是“面向过程”程序设计,“面向过程”的特点是以过程为主,在分析问题的时候,着重分析解决该问题的步骤,并对这些步骤进行实现。
需要注意的是,以“过程”为目的方法重点在实现某些步骤或方法上,并不会对问题有进一步的抽象,换句话说,看到一个问题,脑子里最先想到的是如何去解决、实现它,这样就导致了“一千个问题就有一千种具体实现”。
举个简单的例子,计算a+b的值,由于a和b的类型不确定,可能会有以下的实现:
//1
int sum(int a, int b){
return a+b;
}
//2
float sum(float a, float b){
return a+b;
}
....
我们可以看到,实现一个简单的加法功能便需要如此多的函数来支撑,而且这些函数中的关键代码居然是重复的。因此这也引出面向过程的程序设计的一个缺点:代码重复。
所以面向过程的程序设计适合在程序规模比较小的时候使用,譬如实现一个简单功能的脚本。
而在较大规模程序设计中,面向过程的程序设计会使得程序设计变得更为困难,尤其是会有大量的重复代码出现(与以上sum程序类似,函数实现的是同一个求和功能,却不得不同多种函数进行实现),而函数之间的调用关系会变的越来越复杂、耦合。
面向对象的程序设计
在此背景下,面向对象的程序设计便被提出了,与面向过程相比,面向对象在“问题分析”的阶段便有所不同,这也是与面向过程最本质的区别:对问题进行抽象,这是面向对象最最核心的地方。
针对一个问题,面向对象将该问题进行分解,把构成问题的事务分解成对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为。 --百度百科
以上面的求和为例,具体实现的功能是对两个对象的值进行求和,我们可以把这些函数抽象成一个函数:
C sum (A a , B b){
return a+b;
}
上面的函数中,我们可以看到“求和”的基本功能并没有变,主要是对输入和返回值进行了抽象。而这也正是“面向对象”分析问题的关键,将问题分解为组成各种对象的行为以及交互。
习惯于面向过程的同学看到这里可能会有很大的疑问,道理我都懂,但这里A a , B b,到底具体怎么实现。
在具体实现的时候,不同的语言有不同的实现机制,但具体来讲,一般面向对象的语言会有“封装、继承、多态”这三种实现。
封装、继承、多态
封装
首先是封装,我们使用面向对象的方法对问题进行分析,抽象成对象后,对对象进行进一步的分析。
一般而言,一个对象可能有1. 属性 2. 行为。其中一些属性、行为可能是对象内部使用,因此实现的时候要将对象进行封装。来控制外部的访问以及隐藏内部具体实现。
譬如,将人作为一个对象后,其年龄只能因为时间而增长,不能因为外界对人这个对象的调用或者访问而影响。
继承
封装解决了对象的访问以及控制权限,而继承解决代码复用的问题。
设想一个王者荣耀的游戏场景,里面的人物有各种职业(法师、射手、坦克等),甚至具体的英雄(后裔、鲁班、亚瑟),如果用面向过程的方法来设计,设计对象就变成了每个特定的人物,针对每个人物(后裔、鲁班)都会存在一个自己的结构,而结构里都会存在很多相似的属性(如法术攻击、法术防御等),这样会导致代码的大量重复。
而使用面像对象的思想对游戏人物进行分析、抽象后,我们可以对这些人物的共性进行一个抽象,比如共性(属性:法术攻击、物理攻击等,行为:移动、攻击、被击飞等),在抽象后我们可以将其单独作为一个类,然后让具体的人物类来从它这里继承,得到具体人物对象。那么在实现的时候,我们就可以忽略这些共性属性、行为,而把重点放在其他不同特性的实现上。
如:假设抽象出的基本人物类为BaseAvatar,那么一些共有的属性为法术攻击、物理攻击等,还可能有“攻击”的行为(为了简化这里把具体参数略去),那么具体子类在继承的时候就可以忽略基本属性,把重点放在不同行为的实现上了。
class BaseAvatar{
private:
float magic_attack;//法术攻击
float physical_attack;//物理攻击
float speed;//移动速度
...
public:
virtual void attack(){}
}
假设鲁班、后裔继承了BaseAvatar,可以根据自己的攻击行为进行实现,如下:
class LuBan : public BaseAvatar{
public:
virtual void attack(){
//发射子弹
}
}
class HouYi : public BaseAvatar{
public:
virtual void attack(){
//发射箭矢
}
}
多态
多态的含义是同一条语句,在执行的时候会展示出不同的行为。
从具体实现来讲,一般在继承发生后,父类类型的指针在指向子类对象时并调用子类中的虚函数方法时,会调用子类中重新实现的虚函数方法(而不是父类的虚函数方法)。
还是以上面的王者荣耀游戏场景为例,假设抽象出的基本人物类为BaseAvatar,里面的存在一个“攻击”的行为,子类在继承的时候需要重写这个方法。
class BaseAvatar{
public:
virtual void attack(){}
}
假设鲁班、后裔继承了BaseAvatar,如下:
class LuBan : public BaseAvatar{
public:
virtual void attack(){
//发射子弹
}
}
class HouYi : public BaseAvatar{
public:
virtual void attack(){
//发射箭矢
}
}
在具体渲染攻击这个行为的时候,为了提高效率、简化运算,可能只是会将攻击的效果显示出来,并不会将攻击直接作用到对手身上(譬如鲁班在一个人的时候也可以发射子弹,遇到敌方英雄也可以发射子弹,但只是将子弹发射到敌方英雄的方向,但具体敌方英雄在收到攻击后的碰撞反应可能并不会做,如流血等)
因此在渲染攻击行为的时候,我们可以抽象出一个函数Render,并进行简化,将具体的攻击行为作为Render的参数,指的是要渲染的人物的攻击行为。
void Render(BaseAvatar * p_avatar){
...
p_avater->attack();
...
}
我们使用基类指针作为参数类型,在多态的机制下,可以通过控制传入参数的类型来使这个函数渲染出不同的效果。如:
LuBan luban;//创建鲁班
HouYi houyi;//创建后裔
Render(&luban);//渲染发射子弹
Render(&houyi);//渲染发射箭矢
如果说封装完整的包装了对象,继承解决了代码重复的问题,那么多态则使得代码变得更易扩展。