概述
-
-
- 一什么是类
- 1-1 程序设计的发展
- 1-2 面向过程到面向对象
- 1-3 类的定义
- 1-4 构造函数析构函数
- 二内存管理
- 2-1 内存分布
- 2-2堆和栈
- 2-2-1 堆和栈区别
- 2-2-2 new与delete
- 三拷贝构造函数
- 四类的组合
- 4-1什么是类的组合
- 4-2类组合的构造函数
- 五继承
- 5-1 继承与派生
- 5-2 继承时的构造函数析构函数
- 5-3 重写父类方法
- 六多态与虚函数
- 6-1什么是多态
- 6-2 虚函数
- 6-3 纯虚函数与抽象类
- 一什么是类
-
一、什么是类
1-1 程序设计的发展
我们知道程序的发展经过了大概三个阶段:面向机器的程序设计、面向过程(结构)程序设计、面向对象程序设计。其中面向机器的程序设计主要采用二进制指令或者汇编语言进行程序的编写。这种方式对计算机来说是很容易理解的,但是对程序设计人员来说是很痛苦的,一般没有经过特殊的训练人员很难读懂或者设计,因此进化出了面向过程的设计方式。
面向过程这种方式跟我们正常思考问题的方式比较接近,比如我们需要洗衣服,根据我们的操作步骤来进行设计程序:取出衣服->倒水放洗衣粉->开始搓->晾衣服等等的步骤,进而对每一个步骤编写函数进行模拟实现。我们使用的语言代表性的就是C语言。的确这种方式很大程度的方便了我们的程序编写,但是随着发展,我们需要描述越来越复杂的流程,工程也越来越庞大,当我们重新开始一个项目的时候往往需要从头开始编写代码,比如洗外套我们有一个程序,洗裤子也有一个程序,但是由于代码没有重用或者共享,导致我们必须重头开始编写一个程序。如何才能加强代码的重用性、灵活性与扩展性呢?
新的问题的提出便引出了我们的面向对象程序设计(OOP),这是一种计算机编程架构,其中有一条基本的原则就是程序是由单个能够起到子程序作用的单元或者对象组合而成。这样为了整体运算,每个对象都能够接受信息、处理数据和向其他对象发送信息。每一个单元都可以运用到其他程序中。这样OOP达到了软件工程的三个主要目标:重用性、灵活性、扩展性代表就是我们的C++语言,当然C++语言也是兼容面向过程的。
1-2 面向过程到面向对象
面向对象的设计方式是怎样实现我们的代码重用与扩展呢?我们知道在我们的面向过程方式中,我们经常是这样做的:
1. 对需要进行的功能设计对应的数据结构
2. 设计需要完成这种功能的算法
这时候我们的程序结构是这样的:程序=算法+数据结构。所以当我们面对一个新问题时候往往需要重新设计对应的数据结构,重新设计完成这个功能的算法。这样代码重用性就比较低。
当我们采用面向对象方式去设计程序时候,我们经常是这样思考的:
1. 这个整体对象有哪些子对象呢
2. 每一个子对象有什么属性和功能呢
3. 这些子对象是怎样联系起来的
这样我们的设计出来的代码是这样的:对象=算法+数据结构;程序=对象+对象+对象+·······
因此我们遇到新问题是,因为已经有各种已经完成子功能的对象,所以很方便的能够将代码进行重用。
面向对象中对象是基础,而类便是我们用来实现这些对象的,因此类是面向对象的基础。类描述了一组具有相同特性和相同行为的对象。
1-3 类的定义
在C++程序中我们经常会将类的定义与其成员函数的定义分开,这样也是为了方便阅读与代码的编写
1. 类定义可以看成类的外部接口,一般写成.h文件
2. 类成员函数定义可以看成类的内部实现,一般写成.cpp文件
在代码中我们一般是这样定义一个类的
对于类的成员函数定义,我们一般使用如下格式:
返回值 类名::函数名(参数列表)
{
函数体
}
这里需要说明的是,当函数前面没有类名::时候,编译器就会认为这是一个普通函数,因为我们并没有说明这个函数是哪一个类的,同时这样也就说明了这个函数的作用域,在类作用域里面,一个类的成员函数对同一类的数据成员具有无限制的访问权限。
对类的不同成员设置不同的访问权限就是进行类的封装,这样可以增强安全性和简化编程,使用者不必了解具体的实现细节,而只需要通过外部接口,以特定的访问权限来使用累得成员。
1-4 构造函数、析构函数
构造函数是类中一个比较重要的函数,主要用来创建和初始化对象,构造函数在对象创建时由系统自动调用。构造函数与类名相同,没有返回值,默认无参形式。构造函数一般形式如下:
class 类名
{
public : 类名();
}
注意:构造函数默认是无参的,但是是可以重载的,也就是我们可以设定有参数的构造函数。
构造函数一般有三个功能:
1. 分配空间,即在内存中分类该对象的使用空间
2. 构造结构,构造整个类的结构
3. 初始化,即对类中的属性进行初始化
与构造函数相反的就是我们的析构函数,析构函数主要完成对象删除前的一些清理工作,在对象生存周期结束时候系统自动调用,然后释放对象的空间。析构函数在类名前添加~,没有返回值,没有参数,与构造函数不同,析构函数不能重构。析构函数一般形式如下:
class 类名
{
public ~类名();
}
二、内存管理
2-1 内存分布
我们知道我们类在构造之后,会在内存中创建空间来保存各类数据,当类生存周期结束之后,系统会调用析构函数回收空间,我们的类有常量、变量、函数等等,这些数据是怎样在内存中存储的呢?
C++程序的内存一般分为四个区,如图:
需要非常注意的是:堆中和栈中的数据回收是不一样的,操作堆内存,如果分配了内存,就有责任回收,否则会造成内存泄露,函数中在栈区分配的局部变量,在函数结束时,会自动回收与释放空间
2-2堆和栈
2-2-1 堆和栈区别
为了方便存储,内存区域分为堆和栈,两者具有不同的特征来存储不同的数据。
一般来说堆空间相对其他内存空间比较空间,因此会给程序带来很大的自由度来分配空间。但是也不是说堆的内存是可以随意申请的,一般使用堆的空间情况有以下几种:
- 直到运行时才能知道需要多少对象空间
- 不知道对象的生存周期到底有多长
- 直到运行时才知道一个对象需要多少内存空间
而与堆不同的是,创建程序时,编译器准确的直到栈内保存了所有的数据长度以及存在的时间。由于栈的FILO特性,栈指针下移,就会创建新的内存,上移会释放对应空间,因此是可以精准操作栈内的数据。
举例如下:
2-2-2 new与delete
malloc与free是C++/C语言标准库函数,new/delete是C++的运算符。注意一个是库函数,一个是运算符。;两个有什么区别呢?对于库函数来说,在分配堆内存时候,根本不关心这个对象是什么,只关心需要分配多少空间,而操作符来说,在分配时候内存大小跟类对象是相关的,而且在分配内存时候回调用类的构造函数。
他们都用来申请动态内存和释放内存。既然new用来申请内存,而且构造函数可以有参数,所以跟在new 后面得类类型也是可以有参数的,格式如下:
类名 *变量名 = new 类名(···);
类名 *变量名 = new 类名[元素个数];
我们发现我们可以申请类对象数组,但是必须注意的是从堆中分配类对象数组时候,只能调用无参的默认构造函数,不能调用有参的构造函数,一旦类中没有默认的构造函数不能分配类数组
前面说了,堆中申请的内存空间,需要手动回收,因此采用delete来释放空间,会调用类的析构函数。格式如下:
delete 变量名; 或 delete[] 变量名;
变量名=null 变量名=null
三、拷贝构造函数
拷贝构造函数是一种特殊的构造函数,其形参为本类的对象引用。其本质是用一个对象去构造另一个对象,或者说用另一个对象值初始化一个新的构造对象,格式如下:
class 类名
{
public 类名(形参) //构造函数
public 类名(类名 &对象名) //拷贝构造函数
}
//拷贝构造函数的实现
类名::类名(类名 &对象名)
{
函数体
}
举一个例子:
由上面例子我们发现一个有趣的现象就是:拷贝构造函数里面我们直接访问了类中的私有成员(pt.m_mx),其他情况是无法使用”.“来访问的
拷贝构造函数具有以下特点:
- 如果程序中没有为类声明一个拷贝构造函数,则编译器自己生成一个默认的拷贝构造函数
- 这个默认的拷贝构造函数执行功能是,把初始值对象中的每个数据成员值,都复制到新建立的对象中
- 在默认拷贝构造函数中,拷贝的策略是逐个成员依次拷贝
因为拷贝构造函数是将本类的对象传递进去了,这个时候就会出现一个问题。
当类的构造函数在堆上分配了一个资源时候,常见的就是字符串赋值,而拷贝构造函数是复制资源的,如果只是简单的拷贝的话,就会导致两个对象同时拥有一个资源,当一个对象释放了该资源,另一个对象再使用的时候就会出现问题。
这个问题怎么解决呢?显然拷贝构造函数不能简单的拷贝该资源,需要手动编写拷贝构造函数,不能使用默认的拷贝构造函数。需要在拷贝资源的时候将资源复制一份,这样两个对象就会指向不同的资源。这就是浅拷贝和深拷贝的区别
拷贝构造函数经常在以下进行使用
- 当用类的一个对象去初始化该类的另一个对象时
- 若函数的形参是类的对象,调用函数时,实参赋值给形参,系统自动调用拷贝构造函数
- 当函数返回值是类的对象时,系统自动调用拷贝构造函数。
四、类的组合
4-1什么是类的组合
什么是类的组合呢,简单的来说类的组合就是类的成员数据是另一个类的对象,这样就可以在已有的抽象基础上实现更加复杂的抽象
4-2类组合的构造函数
当我们的类进行组合之后,怎样对类进行初始化呢?一般来说不仅要负责对本类中的类型成员进行初始赋值,也要对对象成员进行初始话。声明格式如下:
类名::类名(对象成员所需的形参,本类成员形参):对象1(参数),对象2(参数)
{
本类初始化
}
举例如下:
当类组合之后,就会遇到一个新的问题,我们知道每一个类都有构造函数与析构函数,当类组合的时候,构造函数与析构函数的调用顺序是怎样的呢?
1. 构造函数的调用顺序
1. 调用内嵌对象的构造函数(按照内嵌时的声明顺序,先声明者先构造)
2. 调用本类构造函数
2. 析构函数的调用顺序
1. 调用本类析构函数
2. 调用内嵌对象的析构函数(按照内嵌时的声明顺序,先声明的先析构)
这里需要说明的是若调用默认的构造函数,则内嵌对象的初始化也将调用默认的构造函数
五、继承
5-1 继承与派生
既然是面向对象的,我们的显示世界就是一个对象的世界,那么我们也应该描述现实世界对象间的关系,比如继承,为了描述这种类之间的关系,C++中也是引入了继承的概念
继承是C++中一个重要的机制,该机制自动的为一个类提供来自另一个类的操作和数据结构,这使得我们只需要在新类中定义已有类没有的成分来建立新类。继承就是为了代码的重用和扩充
继承一般的定义格式如下:
class 派生类:public 父类
{
类的实现
}
既然类中的成员具有不同的访问权限,而类继承也有不同的继承方式(一般类继承分为公有继承、保护继承、私有继承),那么类继承之后,也应该具有不同的访问权限,不同的派生情况与类成员的访问权限如下:
5-2 继承时的构造函数、析构函数
既然基类的成员能够被继承,那么基类的一些特殊函数怎么办呢?
基类的构造函数是不被继承的,派生类是需要自己生命自己的构造函数。但是在构造函数时候,只需要对本类中新增成员进行初始化,对继承来的基类成员初始化,自动调用基类的构造函数完成。
派生类的构造函数形式如下:
派生类名::派生类名(基类所需的形参,本类所需的形参):基类名(参数表)
{
本类成员初始化
}
举例如下:
一旦牵扯到多各类,那么就必须说明每个类的调用顺序。对于继承情况下,构造函数的调用顺序如下:
- 调用基类的构造函数,调用顺序是按照他们被继承时的声明顺序(从左到右)
- 调用成员对象的构造函数,调用顺序按照他们在类中声明的顺序
- 派生类的构造函数
举例如下:
与构造函数相同,析构函数也不能被继承,派生类自行声明,声明方法与一般类的析构函数相同。值得注意的是,这里并不需要显示的调用基类的析构函数,系统会自动隐式调用,同时析构函数的调用顺序与构造函数相反。
举例如下:
5-3 重写父类方法
一般父类定义的方法只能操作父类的成员,当子类继承之后,调用父类的方法是只能操作继承来自父类的成员,当子类也想进行相应的操作,此时调用父类的方法已经不能够完成了,这时候需要子类重写父类的方法。
运行时编译器会根据我们的调用情况来调用父类还是子类的方法。举列子如下:
六、多态与虚函数
6-1什么是多态
现实情况下,我们经常需要实现某一种功能,就是对于同一个函数,当不同的对象去运行时候,会得到不同的结果,这样便极大的方便了我们的程序编写。
为了实现这个功能,我们需要采用C++的一种重要机制,多态。所谓的多态性就是发出同样的消息被不同的类型对象接受时可能导致完全不同的行为。
多态性的实现依靠的是一种动态绑定的技术。什么是绑定呢?绑定就是程序自身彼此关联的过程,确定程序中的操作调用与执行该操作代码间的关系。
因此绑定就有两种,静态绑定与动态绑定。静态绑定也叫做静态联编,出现在编译阶段,动态绑定也叫做动态联编,是出现在程序的执行过程,在程序运行时才确定需要调用的函数
6-2 虚函数
为了证明某个函数具有多态性,我们便用关键字virtual关键字来将其标记,使用该标记的函数我们程序虚函数。虚函数一般声明如下:
virtual 返回值 函数名(参数列表)
对于虚函数我们有两点需要说明的:
1. virtual 只能用来说明类声明中的原型,不能用在函数的实现时
2. 基类中声明的虚函数,不管派生类是否声明吗,同原型函数都自动转换为虚函数
举例如下:
虚函数是怎样实现的呢?一般虚函数的实现是通过一张虚函数表来实现的,简称为V-Table。虚函数表主要是一个类的虚函数地址表,当用父类的指针来操作一个子类时,虚函数表就是一张地图,来指明实际应该调用的函数
需要值得说明的是:
1. 编译器将虚函数表的指针存放在对象实例开头,通过对象的地址即可得到这张虚函数表,遍历其函数指针可调用相应的函数
2. 虚函数按其声明顺序放于表中,父类虚函数放在子类虚函数前面
前面说了只有将函数用virtual进行修饰编译器才会进行动态联编来进行编译,否则编译器并不知道是不是要后期编译。可以说虚函数的本质是覆盖不是重载声明
根据虚函数的特征,有几点需要说明的是:
1. 只有类成员函数才能说明为虚函数。虚函数仅适用于继承关系的类对象
2. 静态成员函数不能使虚函数,因为静态成员函数不受限于某个对象
3. 内联函数、构造函数不能使虚函数
4. 析构函数可以使虚函数,通常也就是虚函数
6-3 纯虚函数与抽象类
在虚函数的基础上我们还有一种虚函数叫做纯虚函数,就是当基类当前并不具体实现的一个成员函数,留给派生类去实现,相当于给子类保留了一个位置,以便派生类来实现。
纯虚函数一般定义如下:
Vitural 类型 函数名(参数表)=0
可以发现,将虚函数等于0就是纯虚函数。包含纯虚函数的类就是抽象类。
抽象类就是为了抽象和设计的目的而声明的,将有关的数据和行为组织在一个继承层次的结构中,保证派生类具有的要求行为。对于一个暂时无法实现的函数,可以声明为纯虚函数,留给派生类来实现。
对于抽象类必须说明
1. 抽象类只能作为基类来使用
2. 不能声明抽象类的对象,哪怕是实现了部分纯虚函数的类
特此声明:
本文章为原创文章,欢迎喜欢热爱本文章的人进行讨论,如有不足肯定大家提出。非常感谢!!!同时若转载请声明出处,谢谢!!!
最后
以上就是仁爱小虾米为你收集整理的C++类与对象详解的全部内容,希望文章能够帮你解决C++类与对象详解所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复