赞
踩
目录
如果一个类中什么成员都没有,我们称之为空类,但是空类中真的什么都没有嘛?其实并不是这样的,任何空类,在什么都不写的时候,编译器会默认生成以下6个默认成员函数。
默认成员函数:用户没有显示实现,编译器会默认自动生成的函数称为默认成员函数。
class person { };像这样就是一个空类,但是他会有以下6个默认成员函数,来方便我们的操作
对于以下data类,我们有如下写法:
对于Data类,我们可以使用public的成员函数init来给他初始化,但如果每次在创建对象的时候都需要初始化,未免有些太过麻烦,那么能否在创建新的对象的时候,自动将初始化信息设置进去呢?
构造函数是一个特殊的成员函数,其名字和类名相同,创建类类型的对象时,编译器会自动调用,以保证每一个对象都有一个合适的初始值,并且构造函数只会在对象声明周期中调用一次。
构造函数是特殊的成员函数,其名字虽然叫做构造,但是构造函数的功能却不是开辟空间创造对象,而是初始化对象
其特征如下:
1.函数名和类名相同
2.无返回值(注意不是void,而是返回值那里什么都不写)
3.对象实例化的时候编译器会自动调用对应的构造函数
4.构造函数可以重载
5.如果类中没有显示定义构造函数,则编译器会默认生成一个无参的构造函数,一旦用户显示定义了构造函数,编译器就不会再生成默认构造函数了。(默认构造函数只能存在一个)
注意:如果调用的是无参构造来实例化一个对象,对象后面则不能再跟一个()括号,否则编译器会认不出这是实例化对象,还是函数的声明!
关于默认构造函数,许多人都会有一个疑惑:在外面没有显示定义构造函数的时候,编译器会自动生成一个默认的无参构造函数,但是看起来默认构造函数并没有什么用处,因为这里的结果仍然是一个随机值,那是不是说明默认构造函数并没有起到任何作用呢?
解答:c++把类型分为了内置类型和自定义类型(类类型)。内置类型就是编译器提供的基本数据类型如:int,char等等,自定义类型就是我们使用的struct ,class等,默认构造函数不是没有任何作用,而是对于内置类型不做任何处理,但是对于自定义类型会去调用他自己的默认构造函数
这是类的声明和实例化:
下面是结果:
在这里我们可以清晰的看到,在实例化对象d1的时候,由于d1中包含了一个成员变量_time,但是_time又是一个自定义类型,所以编译器会默认生成一个构造函数帮我们去初始化d1的成员变量,又由于_year_month_day都是内置类型,并不会初始化,只有_time是自定义类型,会去调用Time的默认构造函数,所以_hour,_minute,_second都被初始化成了0,而其他的成员变量没有任何改变。
注意:c++11中针对内置类型成员不会初始化的缺陷,又打上了补丁,即:内置类型成员变量在类中声明的时候可以给默认值
无参的构造函数和全缺省的构造函数都被称为默认构造函数,并且默认构造函数只能有一个!注意:无参构造函数、全缺省构造函数、编译器自动生成的默认构造函数都被认为是默认构造函数。
这是正确的写法:
这是错误的写法:
因为默认构造函数只能存在一个,但是我们这里写了一个全缺省的构造函数和一个默认构造函数,就会出错了!我们一定要保证不要多写构造函数出来。
一个类可以有多个构造函数,但只能有一个默认构造函数。
通过对构造函数的学习,我们明白了一个对象是怎么来的,那么一个对象又是怎么销毁的呢?
析构函数:与构造函数的功能相反,析构函数本身不是完成一个对象本身的销毁,因为局部对象的销毁工作是有编译器完成的(函数栈帧的创建和销毁)。但是对象在销毁的时候,会自己调用析构函数,完成对象中的资源清理工作。
析构函数是特殊的成员函数,他的特点如下:
1.析构函数是在类名前加上~符号,表示和构造函数相反的意思。
2.没有参数没有返回值(与构造函数一样)。
3.一个类只能有一个析构函数,如果没有显示定义,编译器会默认生成一个析构函数。注意:析构函数不能重载。
4.对象声明周期结束的时候,会自动调用其析构函数。
析构函数的作用与构造函数类似:只会对自定义类型的成员变量进行处理,而不会对内置类型有所处理(因为内置类型在函数栈帧销毁的时候也就没有了)
在这里,我们在main中根本没有创建Time的对象,为什么在d1对象销毁的时候会调用Time的析构函数呢?因为Date类中包含了Time类的成员变量,在d1对象销毁的时候,编译器会自动调用Data的析构函数,其中_year_month_day都是内置类型( 内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可),不会有任何影响,而Time是自定义类型,而Date没有显式提供析构函数,则编译器会给Date类生成一个默认的析构函数,目的是在其内部调用Time类的析构函数,即当Date对象销毁时,要保证其内部每个自定义对象都可以正确销毁所以会去调用Time类的析构函数,于是就出现了上述结果。
main函数中并没有直接调用Time类析构函数,而是显式调用编译器为Date类生成的默认析构函数注意:创建哪个类的对象则调用该类的析构函数,销毁那个类的对象则调用该类的析构函数
如果类中没有申请资源的时候,析构函数可以不写,直接用编译器默认生成的析构函数即可,但是如果类在实例化对象的时候会申请一定的资源(比如在堆中开辟空间),则必须要显示写出析构函数,否则对象声明周期结束时,对象中的指针会被销毁,但是堆中的空间永远没有被释放,也就造成了内存泄漏。
像这样的栈,是在堆中申请的空间,如果没有我们自己写的显示的析构函数的话,在s1对象声明周期结束的时候,_arr就会被销毁,但是_arr指向的堆空间并没有被释放掉,就造成了内存泄漏。所以析构函数在我们平时的操作中仍然具有十分重要的地位。
在生活中,如果存在一个长相一模一样的你,我们会称之为双胞胎。
那我们在实例化一个对象的时候,能不能创建一个和原对象一模一样的对象呢?
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般会加const来修饰),在用已经存在的类类型对象去创建新的对象的时候会自动调用。
拷贝构造一种特殊的成员函数,其特征如下:
1.拷贝构造函数是构造函数的一个重载形式(一个类可以有多个构造函数,但只能有一个默认构造函数)
2.拷贝构造函数的形参有且只有一个,必须是类类型对象的引用(classname& 对象1),如果参数不是引用,而是普通的(classname 对象1)编译器就会报错,因为会引发无穷尽的拷贝构造调用。
首先我们来谈谈为什么拷贝构造的形参一定是类类型对象的引用,(为什么传值不行了)我们不妨看看下面的代码:
我们可以看到这里竟然报错了!这是为什么呢?因为这是普通的传值调用,我们传过去的是s1的值给s2,那么s2中的任何一个成员变量都和s1的相同(包括_datas指针!)
当s2对象销毁的时候,free了一次_datas,但是当s1对象销毁的时候,也会free一次_datas,但是这时候_datas已经被free过了,所以二次free会让编译器报错!
为了解决多次free的问题,c++在类类型传参的时候,规定了拷贝构造函数的形参必须是类类型的引用,也就是说我们在实例化一个新对象的时候,如果传入的是原对象的话,编译器就会默认调用拷贝构造函数(如果没有显示写出拷贝构造函数,编译器就会默认生成一个拷贝构造函数,也就是我们平时所见到的传值调用),那么这个时候上面所提到的无穷尽的调用拷贝构造也就能够得到解答了。
解答:
因为c++规定传对象名就是会调用拷贝构造函数,如果拷贝构造函数的形参是(classname 对象1),在函数调用前先要传参给对象1,而对象1又要传参给对象2,如此一来,就会无穷无尽的进行拷贝构造函数的调用(不过由于我们现在的编译器比较先进,在发现拷贝构造的形参不是类类型的引用的时候就会自动报错,告诉我们这样做事不对的)
正确的拷贝构造应该是这样写的:
可以看到,我们在进行深拷贝构造的时候,不仅给一些成员变量赋值了,还独立开辟了一块空间,用来存放数据(不同于s1的数据,而是另外开辟的一块空间,这样两个对象的指针就不会指向同一块空间了,那么在对象声明周期结束的时候就不会因为多次free掉同一块空间而让程序崩溃了)。
注意:以后我们在写类的时候,如果类中没有涉及到资源申请的时候,拷贝构造函数写不写都是可以的;但是一旦涉及到了资源申请的时候,一定要显示的写出拷贝构造函数,否则就是浅拷贝(传值调用),会在对象销毁自动调用析构函数的时候让程序崩溃。
为了提高程序效率,一般对象传参时,尽量使用引用类型(因为使用引用不会额外开辟一块新的空间,既减少了空间的浪费,也减少了因为拷贝数据所带来的时间消耗),返回时根据实际场景,能用引用尽量使用引用。(唯一需要注意的就是引用出来的别名的修改是会改变原始值的,所以在使用引用的时候一定要万分小心,比如加上const等等)
c++为了增强代码的可读性,引入了运算重载符,运算重载符是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型和参数列表与一般的函数类似。
函数名:关键字operator后面跟上需要重载的运算符符号
函数原型:返回值类型 operator需要重载的运算符符号 (参数列表)
注意:
1.operator后面不能跟没有意义的运算符符号,比如@是c++中没有定义的符号
2.重载操作符至少要有一个类类型的参数
3.用于内置类型的运算符,其含义不能够改变,比如内置的+就还是只能用作+,而且也只能有两个目的操作数
4.作为类成员的函数重载时,其形参看起来比操作数数目少1,因为其第一个参数为隐藏的this,例如我们重载+就是opeartor+(this,操作数)这个this是隐藏的,我们不能写出来,当我们要运算的时候直接使用this去找到第一个操作数(默认在重载操作符左边的被this指向,而在重载操作符右边的才是我们能看得到的形参)
5. .* :: sizeof ?: . 注意以上5个运算符不能重载。这个经常在笔试选择题中出
现。
像这样的,就是没有考虑到重载操作符有一个隐含的参数this,一旦这样写,该函数就是三个参数,但是==只能有两个参数,所以编译器便会报错
正确的写法应该是这样的:
下面我们来看看利用运算符重载完成的日期类
.h文件
.cpp文件
相信大家认真看完日期类的实现一定会对运算符重载有一个更加深刻的理解。
将const修饰的“成员函数”称为const成员函数,我们虽然说const修饰成员函数,但实际上,const修饰的是该成员函数的this指针,表明在该成员函数中,不能够对类中的任何成员进行修改。
const成员函数的写法和普通的不一样,一般我们在写const的时候,都是写在变量的前面,但是在这里,c++规定要写在函数的( )括号后面,
即
返回值类型 函数名 ( )const
{
}
在这里的const修饰的是this指针,(如图二),表明this指针指向的对象是不能够被修改的,也就保证了数据的安全性。(这是一种特殊的写法,我们只需要记住即可)
笔者在学习到这一章节的时候,也曾十分不理解,为什么需要const成员函数呢?我们可以来看看下面的代码:
在这里,我们在Date中写了两个print代码,一个是const成员函数,另一个则是非const成员函数,为了对应这两个print重载函数,我们又在main中创建了两个Date类的对象,不同的是d1是非const的普通对象,d2是const修饰的受限制的对象。我们分别调用print这个函数的时候,发现他们二者竟然调用的不是同一个函数!
这是d1(非const对象)在调用print函数的情况
这是d2(const修饰的对象)在调用print函数的情况
有没有看到,他们调用的print不是同一个函数!这里其实就是函数的重载在发生作用,编译器会自动识别对象的类型,在上述实验中,d1是Date类对象,d2是const Date类对象,所以在调用print函数的时候,编译器会自动识别,并且去调用那个最适合他们的重载函数。
当然你在看到这里的时候,可能会不以为然,认为const和重载函数也不过如此嘛,可是再一深思,你会发现:如果我们要求const对象只能读不能改,非const对象可以读也可以改,上述特性是不是就会起到极大的作用呢?
我们在这里将非const版本的_year_month_day都++了,于是出现了以上结果(是可以修改的)
然后我们再对const版本也进行相同的操作,会发现编译器竟然报错了:表达式必须是可修改的左值
所以我们说,const成员函数是可以用作一种重载函数,让我们在调用的时候编译器能够自动的识别出恰当的重载函数,从而保证了程序的正确运行。所以我们建议在使用const的时候要注意一点:该对象是否需要修改(是否是只读不改)?只要遵循了这一原则,我们便能够很好的使用const
我们再来思考下面的几个问题:
1. const对象可以调用非const成员函数吗?
2. 非const对象可以调用const成员函数吗?3. const成员函数内可以调用其它的非const成员函数吗?
4. 非const成员函数内可以调用其它的const成员函数吗?解答:
1.const对象可以不能调用非const成员函数,因为这样就是权限的放大(我们说c++是不允许权限的放大的)
2.非const对象可以调用const成员函数,因为这是权限的缩小
3.不能
4.可以
(以上的四道题目都是关于权限的理解,简单来说就是权限只能缩小或者平移,一旦权限放大就会出问题)
我们在前面的学习中已经知道了:c++中只有内置类型可以直接使用一系列的操作符,而自定义类型是不能够直接使用操作符的,需要我们利用操作符重载(比如我们上面的Date类)。
在平时,我们不需要自己来写&符号的重载,因为编译器已经自动生成了自定义类型的&符号,当然我们也可以显示的写出来,像这样
不过一般情况下,我们完全不需要自己写出来,因为编译器默认生成的就已经够用了,除非我们有特殊的需求(比如对const类型的对象不能让别人知道地址,非const对象才能知道地址),当然我们在平时的工作中,几乎是不会有这样奇葩的需求的。
赞
踩
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。