- OOP与C++。
面向对象编程(OOP)是一种相对于过程编程比较特殊的设计程序的概念方法。
C++是针对C语言的一些问题进行了改进使其更加容易的实现OOP思想。
使用C++是否等同于使用了OOP思想那?或是OOP就限定了使用的编程语言那?
可以这样解释,OOP是一种编程风格,从某种程度来讲,它试用于任何一种语言中。当然也可以将OOP思想融合到常规的C语言中,例如:
我们在C语言的头文件中包含结构原型和操作该结构的函数原型,便利用的OOP思想。
那么既然OOP思想可以通过任何语言实现,为什么还要使用C++或是将C++称之为面向对象语言(面向对象语言不仅仅有C++,还有JAVA,C#,.net等等)。因为C++针对OOP提供了相应的机制,包括类和对象,封装和数据隐藏,多态和继承等等。
打个不是十分恰当的比喻来说吧:
如果我是一个雕刻师(OOP),随便拿一把小刀就可以雕刻的(C等被称为过程的语言),但是用小刀总是不如用雕刻的专业刀具(C++等面向对象语言)使用的得心应手。但是一个普通人(没有使用或者不懂得OOP思想)即使拿着上好的专业雕刻刀,也做出好的雕塑,甚至有时还不如拿小刀刻出玩意更好些。
所以可以这样说,提到OOP我们不应该也不可以立马想到特定的语言,如C++,JAVA等等。虽然这些语言针对面向对象,但是思想与具体实现并没有必要联系。
但是提到C++我们不得不想到使用OOP思想。
- 对象和类。
按照之前教科书和我曾经那让人发狂的老师的解释两者关系如下:
对象是类的实例化,类是对象集合的抽象。
简单倒是简单,但是怎和linux软件中的循环依赖一样啊,相互解释和定义。不过现在看来这两句话倒是简单和明了,很是精炼。
其实从程序角度来讲:
类相当于我们自己定义的一个特殊的类型,它不仅仅声明了数据,还声明了在这组数据上的操作,并且对这些操作和数据的可见性按照,private,public,protect,进行了划分。相当于ADT。
而对象则是我们使用这个类型(我们自己或函数库提供的类)定义的变量。
假设我们定义了类 CLASS Employee;那么当我们在程序中使用如下语句进行变量声明时:
则emp_1被称为Employee类的对象。而类的是对象集合的抽象如何体现那,这个就体现在对Employee这个类进行设计时了。如何通过对程序关心的问题,和程序中针对职员(Employee)关注的问题点和业务需求来进行确定,对外提供的公共接口,内部数据(保存相应数据),内部接口(操作私有数据),保护数据,友元函数确定等等。这都是需要对业务和操作对象集合进行理解和抽象进行的。
- 类的构造函数和析构函数。
C++的设计目的是将类作为标准的类型一样使用,但是在设计类时,基于对数据隐藏和保护的原则,我们基本都会将类的数据设为保护(private)。而保护的类型的类成员外部是无法访问和修改的。那类对象的初始化就有了问题,因为无法访问,那么就会造成无法赋初始值。而我们都知道使用没有经过初始化的变量会产生不可预计错误,因为我们不知道当前变量的值。这样就不得不提供一种机制来实现类对象的初始化,这个机制就是构造函数。而析构函数的引入则是为了实现,类似于C语言中局部(栈)变量的自动释放机制。这在类数据成员或构造函数中使用了new开辟堆空间时,体现的尤为突出。
- 构造函数和析构函数需要关注的细节。
- 如果没有显示提供构造函数和析构函数,则编译器会提供默认的构造函数和析构函数。注意默认的构造函数并不对类数据成员进行初始化。析构函数则会在对象生命期结束后释放数据内存。
- 如果在希望对类对象的初始化方式有所变化,则应该改提供相应的可重载的构造函数。
- 默认构造函数没有参数,如果提供参数则所有参数必须提供默认值。
- 如果构造函数使用了new或者new[]来初始化对象成员,则析构函数必须提供对性的delete或delete[]操作来完成内存释放。而且new与delete,new[]与delete[]必须严格成对使用。
- 在使用了new的构造函数时,需要同时显示的那个复制构造函数,以保证正确开辟空间,存储数据。同时也应该提供显示的赋值重载函数,以提供深度的复制,(开辟空间,而不是仅仅给指针赋值)。
- 构造函数和析构函数的调用。
构造函数在对象被创建时调用,通过其初始化方式调用对应的构造函数(这个由语言实现自动的重载)。
析构函数则跟踪对象的,在对象生命周期结束后按照一定次序进行释放。
- 操作符重载和限制。
操作符的重载,使得我们可以像使用标准数据类型一样使用类对象。通过重载操作符将一些常用操作得以简单明了的进行实现。(体现在对对象进行编程时的便利和语义明确)。
但是对于操作符重载有以下限制:
-
- 重载后的操作符必须至少有一个操作数使用户定义的类型,浙江纺织用户为标准类型重载操作符。例如:不能将减法操作符(-)重载为计算两个double值的和而不是它们的差。虽然这种限制将对创造性有所影响,但可以确保程序正常运行。
- 使用操作符时,不能违反操作符原来的句法规则。例如不能将求模操作符(%)重载成使用一个操作数。同样,不能修改操作符的优先级。即重载后的操作符与其被重载之前的具有相同的优先级。
- 不能定义新的操作符。例如不能定义 operator **()函数来表示求幂。
- 不能重载以下操作符:
-
sizeof—sizeof操作符
-
.—成员操作符
-
.*---成员指针操作符
-
::--作用域解析操作符
-
?:--条件操作符
-
typeid ---一个RTTI操作符
-
const_cast—强制类型转换操作符
-
reinterpret_cast——强制类型转换操作符
-
- 以下操作符必须通过成员函数进行重载
-
= ——赋值操作符
-
() ——函数调用操作符
-
[] ——下标操作符
-
-> ——通过指针访问类成员的操作符
- 类的隐式成员函数
默认构造函数,如果没有定义。
复制构造函数,如果没有定义。
赋值构造函数,如果没有定义。
默认析构函数,如果没有定义。
地址操作符,如果没有定义。
以上五个隐式的成员函数,在没有编写时,则由编译器生成。本来这种实现方式,可以减少程序员的工作量,而且也使得类的操作更像标准数据类型。但是这种隐式提供的函数,在程序员在类数据成员中使用指针相关数据类型时,会有潜在危险。所以要明白以上五个隐式函数有什么功能,何时被调用。
-
- 默认构造函数。
-
如果没有提供任何构造函数,C++将创建默认构造函数。例如:加入定义了KLunk类,但没有提供任何构造函数,则编译器将提供如下默认构造函数:
-
Klunk::Klunk(){}//编译器提供的默认构造函数
-
也就说,编译器将提供一个不接受任何参数,也不执行任何操作的构造函数,这是因为创建对象时总是会调用构造函数。而这种默认构造函数使得类对象类似一个常规的自动变量,也就说它的值在初始化时是未知的。
-
- 复制构造函数
-
复制构造函数的调用发生在新建一个对象并将其初始化为同类现有对象时,复制构造函数都将被调用。
-
每当程序生成了对象副本时,编译器都将使用复制构造函数。
-
默认复制构造函数实现的功能如下:逐个复制非静态成员(成员复制也称为浅复制),复制的是成员的值。当成员包括与指针相关的类型,使用默认复制构造函数需要注意,因为在逐个复制非静态成员值时,会将指针值复制,而被复制的指针指向的空间很可能在析构函数调用后被释放,将出现指针指向错误。
-
- 赋值操作符。
-
再将已有的一个对象赋给另一个对象时,将使用重载的赋值操作符。
-
赋值操作符的功能与复制构造函数相似,其隐式实现也对成员进行逐个复制。如果成员本身就是类对象,则程序将使用为这个类定义的赋值操作符来复制该成员,但静态数据成员不受影响。
-
- 默认析构函数和地址操作符
-
默认析构函数不执行任何的操作。
-
地址操作符则返回调用对象的指针(This指针)。
- 类的继承机制
-
- 派生类和基类之间的关系
- 派生类可以使用基类的方法,条件是方法不是私有的。
- 基类指针可以在不进行显示类型转换的情况下指向派生类对象;基类引用可以在不进行显示类型转换的情况下引用派生类对象。
- 多态公有继承。
- 派生类和基类之间的关系
-
-
当在继承关系时,有可能出现,派生类对于基类的方法是不相同的。也就是说,方法的行为取决于调用该方法的对象。这种较复杂的行为称为多态。
-
有两种机制实现多态公有继承:
-
在派生类中重新定义基类的方法。
-
这种方法只能通过派生类对象调用才能使用。
-
使用虚方法(即虚函数)。
-
这种方法提供了通过类型指针调用方法,但是调用实际对象的具体方法的实现。
使用虚函数的注意点:
构造函数不能是虚函数。
析构函数应当是虚函数,除非类不用做基类。
友元不能是虚函数,因为友元不是类成员,只有成员才能是虚函数。
需要重新定义隐藏的方法,以减少潜在危险。
-
-
- 抽象基类
-
-
当我们需要产生has-a关系的继承时,总会出现各种问题。比如说,椭圆类与圆类。我们可这样理解椭圆中有一个特别的(has-a)情况,长半轴与短半轴一致。这种看似简单改变,将对我们的操作产生很多影响,首先用于记录椭圆的数据必须包括,中心坐标,长半轴,短半轴及方向角。而记录圆则只需,中心坐标,半径长度。其次在计算面积等等方面都会有比较明显的区别,如果从椭圆类继承出派生类圆,则这种继承并没有减少我们的工作,也没有使方法更通用。所以从椭圆类继承派生类圆是不明智。
-
但椭圆与圆又有很多共同点,则这时候C++通过抽象基类这一概念实现,抽象出椭圆与圆的共性够成一个基类,在从基类中派生出椭圆和圆。
-
而抽象基类则必须包括纯虚函数,只有包含纯虚函数时,则不能创建该类的对象,这是因为这个类是抽象的,而抽象概念是不存在实体的(即对象)。抽象基类必须包括纯虚函数,而包含纯虚函数的类只能用作基类。