1、面向对象与面向过程的区别是什么?
面向过程
面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了;面向对象是把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为。
可以拿生活中的实例来理解面向过程与面向对象,例如五子棋,面向过程的设计思路就是首先分析问题的步骤:1、开始游戏,2、黑子先走,3、绘制画面,4、判断输赢,5、轮到白子,6、绘制画面,7、判断输赢,8、返回步骤2,9、输出最后结果。把上面每个步骤用不同的方法来实现。
面向对象
面向对象的设计则是从另外的思路来解决问题。整个五子棋可以分为1、黑白双方,这两方的行为是一模一样的,2、棋盘系统,负责绘制画面,3、规则系统,负责判定诸如犯规、输赢等。第一类对象(玩家对象)负责接受用户输入,并告知第二类对象(棋盘对象)棋子布局的变化,棋盘对象接收到了棋子的变化就要负责在屏幕上面显示出这种变化,同时利用第三类对象(规则系统)来对棋局进行判定。
可以明显地看出,面向过程是以事件为中心的开发方法,面向对象是以功能来划分问题,而不是步骤。同样是绘制棋局,这样的行为在面向过程的设计中分散在了多个步骤中,很可能出现不同的绘制版本,因为通常设计人员会考虑到实际情况进行各种各样的简化。而面向对象的设计中,绘图只可能在棋盘对象中出现,从而保证了绘图的统一。
2、面向对象三大特征?
面向对象方法首先对需求进行合理分层,然后构建相对独立的业务模块,最后通过整合各模块,达到高内聚,低耦合的效果,从而满足用户要求。具体而言,有三个特征:封装、继承和多态。
封装:
封装是指将客观事物抽象成类,每个类对自身的数据和方法进行保护。类可以把自己的数据和方法只让可信的对象操作,对不可信进行信息隐藏。C++中类是一种封装手段,描述客观事物的过程是封装,本质上是对客观事物的抽象。
继承:
面向对象编程 (OOP) 语言的一个主要功能就是“继承”。继承可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。
通过继承创建的新类称为“子类”或“派生类”。被继承的类称为“基类”、“父类”或“超类”。继承的过程,就是从一般到特殊的过程。要实现继承,可以通过“继承”(Inheritance)和“组合”(Composition)来实现。
继承概念的实现方式有三类:实现继承、接口继承和可视继承。
- 实现继承是指使用基类的属性和方法而无需额外编码的能力;
- 接口继承是指仅使用属性和方法的名称、但是子类必须提供实现的能力;
- 可视继承是指子窗体(类)使用基窗体(类)的外观和实现代码的能力。
多态
多态是指一个类实例的相同方法在不同情形有不同表现形式。多态机制使具有不同内部结构的对象可以共享相同的外部接口。这意味着,虽然针对不同对象的具体操作不同,但通过一个公共的类,它们(那些操作)可以通过相同的方式予以调用。
3、面向对象五大原则?
单一职责原则SRP(Single Responsibility Principle)
是指一个类的功能要单一,不能包罗万象。如同一个人一样,分配的工作不能太多,否则一天到晚虽然忙忙碌碌的,但效率却高不起来。
开放封闭原则OCP(Open-Close Principle)
一个模块在扩展性方面应该是开放的而在更改性方面应该是封闭的。比如:一个网络模块,原来只服务端功能,而现在要加入客户端功能,
那么应当在不用修改服务端功能代码的前提下,就能够增加客户端功能的实现代码,这要求在设计之初,就应当将服务端和客户端分开,公共部分抽象出来。
替换原则(the Liskov Substitution Principle LSP)
子类应当可以替换父类并出现在父类能够出现的任何地方。比如:公司搞年度晚会,所有员工可以参加抽奖,那么不管是老员工还是新员工,
也不管是总部员工还是外派员工,都应当可以参加抽奖,否则这公司就不和谐了。
依赖原则(the Dependency Inversion Principle DIP)
具体依赖抽象,上层依赖下层。假设B是较A低的模块,但B需要使用到A的功能,这个时候,B不应当直接使用A中的具体类: 而应当由B定义一抽象接口,并由A来实现这个抽象接口,B只使用这个抽象接口:这样就达到了依赖倒置的目的,B也解除了对A的依赖,反过来是A依赖于B定义的抽象接口。
通过上层模块难以避免依赖下层模块,假如B也直接依赖A的实现,那么就可能造成循环依赖。一个常见的问题就是编译A模块时需要直接包含到B模块的cpp文件,而编译B时同样要直接包含到A的cpp文件。
接口分离原则(the Interface Segregation Principle ISP)
模块间要通过抽象接口隔离开,而不是通过具体的类强耦合起来
4、什么是深拷贝和浅拷贝?
如果一个类拥有资源(堆或者是其他系统资源),当这个类的对象发生复制时,资源重新分配,这个过程就是深拷贝;反之对象存在资源,但复制过程并未复制资源的情况称为浅拷贝。
程序中,应避免浅拷贝,否则容易导致对同一资源的多次释放。
5、类成员变量的初始化顺序与声明顺序相同吗?
在C++中,类的成员变量的初始化顺序只与变量在类中的声明顺序有关,与构造函数的初始化列表无关。而且静态成员变量先于实例变量,父类成员变量先于自雷成员变量。初始化顺序与变量在内存中的次序有关,而内存中变量的排序,早在编译器就根据变量的定义次序确定了。
6、不能被继承的类如何设计?
将构造函数设为private可以阻止一个类被实例化以及被继承。
方法一:将构造函数设为私有函数。通过共有函数可以向外传递实例。但是所有实例都在堆上,需要程序员手动释放。
class finalClass1{
public:
static finalClass1* get(){
return new finalClass1;
}
static void free(finalClass1* pInstance){
delete pInstance;
pInstance = nullptr;
}
private:
finalClass1(){}
~finalClass1(){}
};
方法二:由于友元可以访问类的私有成员,可通过友元函数的性质,做如下实现:
template<typename T> class MakeFinal{
friend T;
private:
MakeFinal(){}
~MakeFinal(){}
};
class finalClass2:virtual public MakeFinal<finalClass2>{
public:
finalClass2(){}
~finalClass2(){}
}
finalClass2是不可继承的类,并且它可以在堆和栈上自由创建。当它继承时,其子类将跳过finalClass2访问MakeFinal的私有构造函数,编译器将报错。
7、多态的分类?
多态分为两种:通用的多态和特定的多态。两者的区别是前者对工作的类型不加限制,允许对不同类型的值执行相同的代码;后者只对有限数量的类型有效,而且对不同类型的值可能要执行不同的代码。
通用的多态又分为参数多态(parametric)和包含多态(inclusion);特定的多态分为过载多态(overloading)和强制多态(coercion)。
强制多态
编译程序通过语义操作,把操作对象的类型强行加以变换,以符合函数或操作符的要求。程序设计语言中基本类型的大多数操作符,在发生不同类型的数据进行混合运算时,编译程序一般都会进行强制多态。程序员也可以显示地进行强制多态的操作(Casting)。
举个例子,比如,int+double
,编译系统一般会把int转换为double
,然后执行double+double
运算,这个int->double
的转换,就实现了强制多态,即可是隐式的,也可显式转换。过载(overloading)多态
同一个名(操作符﹑函数名)在不同的上下文中有不同的类型。程序设计语言中基本类型的大多数操作符都是过载多态的。通俗的讲法,就是C++中的函数重载。参数多态
采用参数化模板,通过给出不同的类型参数,使得一个结构有多种类型。例如,模板类。包含多态
同样的操作可用于一个类型及其子类型(注意是子类型,不是子类)。包含多态一般需要进行运行时的类型检查。例如,虚函数virtual---override
机制
8、C++中如何实现多态?
C++中多态是主要通过虚函数实现的。虚函数的本质是通过基类访问派生类定义的函数。每个含虚函数的类,其实例对象内部都有一个虚函数表指针,位于实例首部。该指针被初始化为指向本类虚函数表的内存地址。不管对象类型如何变化,该类对象颞部的虚函数表指针时固定的,因此能够动态地实现多态调用。
9、虚函数使用时注意事项?
仅列举容易出错的几项:
- 1、声明中virtual关键字即可,在类外不需要,也不能使用virtual关键字。
- 2、非类成员函数,内联函数、全局函数、类成员函数中静态成员函数和构造函数都不能定义为虚函数。但很有必要将析构函数定义为虚函数,否则delete指向派生类的基类指针时,将只调用基类析构函数,造成内存泄漏。
- 3、构造函数里可以调用虚函数,由于基类构造函数调用虚函数时,子类还未构造,因此将调用基类自身的版本。子类构造函数中调用虚函数,同样是调用子类自身的版本。
- 4、使用虚函数需要维护虚函数表,产生额外的系统开销。如果是很小的类,且不需要派生其他类,则没有必要定义虚函数。