文章目录
1.类的默认成员函数
C++的类中,存在六个默认成员函数.
什么是默认成员函数?默认成员函数,就是程序员没有显式实现,编译器自动生成的函数.C++中共有六个默认成员函数,按功能可以分为3组,每组两个函数------用于初始化和清理的构造与析构函数,用于拷贝赋值的拷贝构造与复制重载,以及用于取地址重载的两个函数.
1.1 构造函数
构造函数是用于初始化对象的?了解构造函数,始终要围绕两个问题:如果我们不去写,编译器默认生成的构造函数,函数行为是什么,能满足我们的要求吗;以及,如果我们来显式写这个构造函数,该如何写?
接下来,主要围绕这两个问题进行探讨.
1.1.1 怎么写构造函数
- 函数名:构造函数的函数名,即为类名
- 返回值:构造函数无返回值,也不需要写void
- 形参:构造函数的形参可有可无
- 函数重载:构造函数可以形成函数重载,如下图所示
这里需要注意的是,构造函数在对象创建的时候便会调用,如果不做任何处理,默认调用的是不传参的构造函数,如果想要调用传参的构造函数,则如下所示:
同样需要注意的是,自己写构造函数的重载,形数全缺省和无形参,这两种情况不能同时存在,否则编译器无法区分这两个重载的构造函数
另外,这里其实有一个问题值得思考:为什么调用不传参的构造函数时,不需要写括号呢?这里实际上是为了避免歧义,因为有两种理解方式:1.无形参的函数声明 2.实例化对象,并调用无形参构造函数
1.1.2 编译器默认生成的构造函数是怎样的
当程序员自己不写构造函数时,编译器便会自动生成一个默认的构造函数.
这个构造函数有什么特点呢?
- 没有形参
- 这个构造函数对C语言的内置类型是否初始化,怎样初始化,这是没有要求的,具体怎样完全看编译器
- 对于自定义类型(另一个自定义类型)成员变量,构造函数会要求调用该自定义类型默认的构造函数对该成员变量进行初始化.
这里,显然涉及到一个默认构造函数的概念.默认构造函数,简单来讲,就是一个可以不用传参的构造函数,共有三类:无参构造,形参全缺省构造,编译器默认生成的构造函数.当然,这三个种类的构造函数显然是不能同时存在的,最多只能存在一个.
当存在的这样的默认构造函数时,上述构造函数的特点3中的操作是可以正常进行的,但若是不存在默认构造函数,则会出错,此时就需要使用初始化列表来对该成员变量初始化.
代码示例如下:
结果输出为:
当然,无论是程序员自己写的构造函数,还是编译器默认生成的,对于自定义类型的成员变量,都会要求调用其对应的默认构造用以初始化.
此处初始化的顺序值得注意,在VS编译器下,先调用自定义类型成员变量对应的构造函数,然后再调用包含该成员变量的类的构造函数
1.2 析构函数
理解析构函数,依然从两方面入手:析构函数怎么写以及编译器默认生成的析构的函数行为如何.
1.2.1 析构函数怎么写
- 函数名:~+类名(相比于构造函数,即多一个按位取反符)
- 返回类型:无返回值,不需要写void
- 形参:无形参
- 函数重载:不能函数重载,一个类只能有一个析构函数
特别注意,析构函数会在对象销毁时,即对象生命周期结束前自动调用.
1.2.2 编译器默认生成的析构函数是怎样的
当程序员不写析构函数时,编译器会默认生成一个析构函数,这个析构函数有如下特点:
- 对于内置类型的成员变量,析构函数是否处理,如何处理,这是未定义的,由编译器决定
- 对于自定义类型的成员变量,析构函数要求调用该自定义类型自身对应的析构函数对该成员变量进行处理
特点2的示例如下:
此时,当对象d要销毁时,会首先调用d的析构函数,而后再调用d的自定义类型成员变量m的析构函数.
析构函数还有几个需要注意的点:
- 无论是否自己写析构函数,析构函数都会要求调用自定义类型成员变量自身对应的析构函数
- 调用析构函数,主要是为了进行对象中资源的清理释放,比如说释放在堆上动态开辟的空间,所以,如果一个类中没有申请资源的话,我们就没必要去写析构函数
- 多个对象销毁时,调用相应析构函数的次序:C++规定,一个局部域中的多个对象,后定义的,先析构.
1.3 构造与析构函数总结
C++中,构造与析构函数的引入,使得程序员不需要频繁地使用不同的初始化Init以及销毁Destroy函数,而是在对象创建时自动Init,对象销毁时自动Destroy,因此带来极大方便.
1.4 拷贝构造函数
1.4.1 拷贝构造怎么写
拷贝构造的特点:
- 拷贝构造是构造函数的重载,故函数的一些规范与构造函数相同,拷贝构造的形参是自定义类型的const引用。
- 在一个自定义类型对象实例化时,使用同自定义类型对象对其初始化,就会调用拷贝构造——例如,函数传值传参时,或函数传返回值时,只要是自定义类型,且非引用,就会调用拷贝构造。
- 关于拷贝构造的形参问题,必须为引用,因为如果传值时,进行的是自定义类型的拷贝,就会陷入拷贝构造的无穷递归——第一次调用拷贝构造传参时,会引发第二次拷贝构造,再传参时,又会引发第三次调用拷贝构造,以此趋于无穷。
- 拷贝构造可以有其它形参,但C++规定,其它形参必须为全缺省,否则这个函数只能说是构造函数的重载,但不能说是拷贝构造。
拷贝构造示例如下:
1.4.2 编译器如何实现拷贝构造
当我们没有显式实现拷贝构造时,编译器会自动帮我们实现,这个自动实现的拷贝构造有以下特点:
- 对于成员变量,会进行浅拷贝,即值拷贝,所以当某自定义类型的对象申请了资源时,不显式实现拷贝构造是很危险的,浅拷贝无法正确地实现资源的拷贝,而只是将同一块资源让两个对象管理。
- 对于某类型中的自定义类型成员变量,编译器会默认调用它的拷贝构造进行拷贝。
1.4.3 显式实现资源的深拷贝
对于资源的拷贝,我们要使用深拷贝,而非浅拷贝。
一个简单的代码示例如下:
特别注意:
如果申请了资源,却使用浅拷贝,会出现很大问题,以图中的类型为例。如果图中arr的拷贝构造为浅拷贝,拷贝后,将会出现两个对象可管理同一资源并会互相干扰的情况;同时在两个对象销毁时,又都会调用析构函数,这时堆区上的一块空间被free了两次,这是绝对不允许的。
1.5 赋值运算符重载
1.5.1 运算符重载
对于内置类型对象之间的运算,直接使用相应运算符即可,编译器可直接将这些简单的运算转为底层汇编指令。但对于自定义类型之间的运算,这种运算是比较复杂的,编译器无法自己实现,此时便需要运算符重载。
运算符重载本质是一个函数,在这个函数中,我们去自己定义实现——自定义类型的对象使用了这个运算符后,该进行怎样的操作。
运算符重载函数的格式如下:
运算符重载函数与一般函数基本相同,根据需要选择形参和返回值即可。特殊之处在于, 原本函数名之处,改为关键字operator+要重载的运算符(比如此处的>)
关于运算符重载,有以下几个点需要注意:
- 函数形参:运算符重载函数的形参个数与该运算符所作用的对象个数相同,如一元运算符重载有一个形参,二元运算符重载有两个形参。对于二元运算符而言,运算符左侧的对象传给第一个形参,右侧对象传给第二个形参。
- 函数返回值:运算符重载函数的返回值与重载的运算符需要的返回值相同。例如,>需要返回真假,所以返回值设为bool型;又比如,两个自定义类型对象相加,需要返回相加之后得到的自定义类型对象,所以返回值设为该自定义类型。
特别注意,运算符重载为成员函数的情况:此时,函数的形参需要比运算符作用的对象少一个,因为运算符中的一个对象已通过this指针传址传参(一元运算符无需区分,二元运算符为运算符左侧对象通过this指针传址传参)
运算符重载为全局函数和成员函数的示例如下:
以下介绍运算符重载的几个注意点:
- 原本不存在的运算符不能进行运算符重载,operator@
- 有5个运算符C++规定不能进行重载:点星操作符、域作用限定符、sizeof、?:(三目操作符)、点操作符(对象成员访问操作符)
- 运算符重载,至少要有一个形参为自定义类型。也就是说,不能通过运算符重载,改变作用于内置类型对象运算符的含义。
- 运算符重载后,其优先级与结合性保持不变。
- 对于运算符前置与后置的区分,如前置++与后置++,C++规定后置++重载时,增加一个int形参用以区分,后置–同样如此。代码示例如下:
1.5.2 赋值运算符重载
同其它的运算符重载不同,在未显式实现时,编译器会自动在类中生成赋值运算符重载。
1.5.2.1 如何显式实现赋值运算符重载
赋值运算符重载,除要满足运算符重载的基本规定外,其函数内容其实和拷贝构造差不多,最大的区别在于,拷贝构造是在对象创建时调用的,赋值运算符重载,是在对象创建后,再赋值时调用的,示例如下:
对于浅拷贝而言,拷贝构造与赋值运算符重载没啥区别;但对于深拷贝而言, 拷贝构造不需要释放原有资源(因为此时对象创建,只需申请资源即可),而赋值运算符重载中,被赋值的对象需要释放原有资源后,再重新申请资源。代码示例如下:
注意拷贝构造与赋值运算符重载在深拷贝时候的内容区别。
1.5.2.2 编译器如何自动实现赋值运算符重载
编译器自动实现的赋值运算符重载,对于内置类型的成员变量,进行浅拷贝;对于自定义类型的成员变量,调用其类型对应的赋值运算符重载。
所以,当内置类型的成员变量申请了资源时,我们必须显式实现赋值运算符重载以进行深拷贝。
1.6 取地址运算符重载
1.6.1 const成员函数
const成员函数,是const对成员函数的修饰,使得调用改成员函数,传入的this指针不能去访问修改所指向的对象。
1.6.2 取地址运算符重载
1.6.2.1 我们如何实现取地址运算符重载
该重载函数我们自己实现时,要实现两个,编译器默认实现时,也是实现两个。
至于为什么要写两个,是考虑到指针的权限问题。权限可以缩小,可以平移,但绝不能放大。
特别注意,上图中两个重载函数返回的this指针,即对象的地址,是编译器自动帮我们拿到的,函数中直接返回该地址即可。
如果两个重载函数我们只实现了一个,另外一个编译器也不会自动生成。
1.6.2.2 编译器如何实现取地址运算符重载
如上图所示,编译器默认实现的取地址运算符重载,也就是上面我们实现的两个函数。
2. 构造、析构、拷贝、赋值重载、取地址重载总结
既然这四个函数,编译器都可以帮我们主动实现,那我们究竟什么时候需要自己显式实现,什么时候不需要呢?
对于普通的构造函数,如果类中含有内置类型的成员变量,则往往需要显式写,否则,不需要显式写;而对于析构函数、拷贝构造、赋值运算符重载三者而言,一个很重要的判定依据就是,以这个类实例化出的对象会不会申请资源——如果申请了资源,则往往三者都要显式写,否则,三者都不显式写。
对于赋值重载和取地址重载而言,我们一般不需要自己显式写,使用编译器默认隐式实现的即可。