1.构造函数:一个特殊的成员函数,名字与类名相同,创建类类型对象的时候由编译器自动调用,在对象的生命周期内只调用一次,以保证每个数据成员都有一个合适的初始值。构造函数不能为虚函数。
必须在构造函数的初始化列表中进行初始化的数据成员有:
(1)常量成员:因为常量只能初始化不能赋值,所以需要在初始化列表里边初始化。
(2)引用类型:引用必须在定义的时候初始化,并且不能重新赋值,所以也要在初始化列表里初始化。
(3)没有默认构造函数的类类型:因为使用初始化列表不必调用默认构造函数来初始化,而是直接调用拷贝构造函数初始化。
2.拷贝构造函数:只有单个形参,而且该形参是对本类类型对象的引用(通常用const修饰),这样的构造函数成为拷贝构造函数。拷贝构造函数是特殊的构造函数,创建对象时使用已存在的同类对象来进行初始化,由编译器自动调用。
3.析构函数:与构造函数功能相反,在对象被销毁时,由编译器自动调用,完成类的一些资源清理和收尾工作。
析构函数必须是虚函数。将可能会被继承的父类的析构函数设置为虚函数,可以保证当我们new一个子类,然后使用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄漏。而C++默认地析构函数不是虚函数。因为虚函数需要额外的虚函数表和表指针,占用额外的内存。而对于不会被继承的类来说,其析构函数如果是虚函数就会浪费内存。因此C++默认的析构函数不是虚函数,而是只有当需要当作父类时,才设置为虚函数。
析构函数的作用:析构函数与构造函数相对应,当对象结束其生命周期,如对象所在的函数已调用完毕时,系统会西东执行析构函数。析构函数名与类名相同,只是在函数名前加一个位取反符“~”,例~Base(),以区别于构造函数。它不能带任何参数,也没有返回值。只能有一个析构函数,不能重载。如果用户没有编写析构函数,编译系统会自动生成一个缺省的析构函数(即使用户自定义了析构函数,编译器也总是会为我们合成一个析构函数,并且如果自定义了析构函数,编译器在执行时会先调用自定义的析构函数再调用合成的析构函数),它不进行任何操作。所以很多简单的类中没有使用显式的析构函数。如果类中有指针,且在使用过程中动态地申请了内存,那么最好显式构造析构函数,在销毁之前,释放掉申请的内存空间,彼岸内存泄漏。
类析构顺序:派生类本身的析构函数->对象成员的析构函数->基类的析构函数。
4.赋值运算符重载函数:对于类类型的对象我们需要对“=”进行重载,以完成类类型对象之间的赋值。
5.取址操作符重载函数:函数返回值为该类型的指针,无参数。
6.const修饰的取址运算符重载函数
需要注意的是:析构函数不能也不应该抛出异常。原因如下:C++异常处理模型最大的特点和优势就是对C++中的面向对象提供了最强大的无缝支持。那么如果对象在运行期间出现了异常,C++异常处理模型有责任清除那些由于出现异常所导致的已经失效了的对象(即对象超出了它原来的作用域),并释放对象原来所分配的资源,这就是调用这些对象的析构函数来完成释放资源的任务,所以说,析构函数已经变成了异常处理的一部分。如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作,比如释放某些资源,则这些动作不会被执行,从而造成诸如资源泄露的问题。通常异常发生时,C++的异常处理机制会调用已经构造对象的析构函数来释放资源,此时若析构函数本身也抛出异常,则前一个异常尚未处理又有新的异常产生,会造成程序崩溃的问题。
C++中不能声明为虚函数的函数?
1.构造函数:在编译期间编译器完成了虚表的创建,而虚指针在构造函数期间被初始化。如果构造函数是虚函数,那必然需要通过虚指针来找到虚函数的入口地址,但是这个时候我们还没有把虚指针初始化,因此,构造函数不能是虚函数。
2.内联函数:编译期间内联函数在调用处被展开,而虚函数在运行时才能被确定具体调用哪个类的虚函数。内联函数体现的是编译期机制,而虚函数体现的是运行期机制。
内联函数与宏定义的区别以及内联函数的优点?
宏定义在预编译的时候就会进行宏替换。内联函数在编译阶段,在调用内联函数的地方进行替换,减少了函数的调用过程,但是使得编译文件变大。因此,内联函数适合简单函数,对于复杂函数,即使定义了内联编译器可能也不会按照内联的方式进行编译。内联函数相比宏定义更安全,内联函数可以检查参数,而宏定义只是简单的文本替换。使用宏定义函数要特别注意给所有单元都加上括号。
3.静态成员函数:静态成员函数与类有关,即使没有生成一个实例对象,也可以调用类的静态成员函数。而虚函数的调用和虚指针有关,虚指针存在于一个类的实例对象中,如果静态成员函数被声明为虚函数,那么调用静态成员函数时又如何访问需指针呢。通俗讲就是,静态成员函数与类有关,而虚函数与类的实例对象有关。
4.非成员函数:虚函数的目的就是为了实现多态,多态和继承有关,所以声明一个非成员函数为虚函数没有任何意义。
5.友元函数:因为C++不支持友元函数的继承,对于没有继承特性的函数没有虚函数的说法。
构造函数和析构函数中可以,但最好不要调用虚函数。构造派生类对象是,首先调用基类构造函数初始化对象的基类部分。在执行基类构造函数时,对象的派生类部分是未初始化的。实际上,此时的对象还不是一个派生类对象。析构派生类对象时,首先析构它的派生类部分,然后按照构造顺序的逆序析构它的基类部分。因此,在运行构造函数或者析构函数时,对象都不是完整的。为了适应这种不完整,编译器将对象的类型视为在调用构造/析构函数时发生了变换,即:视对象的类型为当前构造函数/析构函数所在的类的类型。由此造成的结果是:在基类构造函数或者析构函数中,会将派生类对象当作基类对象对待。而这样一个结果会对构造函数、析构函数调用期间调用的虚函数类型的动态绑定对象产生影响,最终的结果是:如果在构造函数或者析构函数中调用虚函数,运行的都将是为构造函数或者析构函数自身类类型定义的虚函数版本。
存在虚函数的类中的析构函数要定义成虚函数。为了实现多态进行动态绑定,将派生类对象指针绑定到基类指针上,对象销毁时,如果析构函数没有定义成虚析构函数,则会调用基类的析构函数,显然只能销毁部分数据。如果要调用对象的析构函数,就需要将对象的析构函数定义为虚函数,销毁时通过虚函数表找到对应的析构函数。
虚函数与多态:多态的实现主要分为静态动态和动态多态,静态多态主要是重载,在编译的时候就已经确定;动态多态是用虚函数机制实现的,在运行期间动态绑定。举例来说就是,一个父类的指针指向一个子类的对象时,使用父类的指针去调用子类中重写了的父类中的虚函数的时候,会调用子类重写过后的函数,在父类中声明为加了virtual关键字的函数,在子类中重写的时候不需要加virtual也是虚函数。而关于虚函数的实现,在有虚函数的类中,类的最开始部分是一个虚函数表的指针,这个指针指向一个虚函数表,表中放了虚函数的地址,实际的虚函数在代码段(.text)中。当子类继承了父类的时候也会继承其虚函数表,当子类重写父类中虚函数的时候,会将其继承到的虚函数表中的地址替换为重写的函数地址。使用了虚函数,会增加访问内存开销,降低效率。