【面向对象】构造函数与析构函数详解
构造函数与析构函数详解。
·
一、构造函数
类型
- 默认构造函数(无参/缺省构造函数)
- 单一性:
一个类
中只能
出现一个默认构造函数
- 调用时不传实参:因为调用的默认构造函数通常是
无参的
或所有形参都有缺省值
- 调用时无括号:使用默认构造函数的时候创建对象不能后面带有括号
- 编译器自动生成non-trivial(有用)的构造函数有四种情况,其他情况就算编译器产生了默认构造函数也没有实质作用。
类中有类
:类A含有类B的对象作为成员,并且类B显式定义了默认构造函数,则定义类A对象的时候编译器会产生一个默认构造函数,并在这个默认构造函数中提供了调用类B构造函数的代码
。类继承类
:类B继承于类A,且类A显式定义了构造函数,那么在生成类B对象的过程中编译器同样会产生一个默认构造函数,在默认构造函数中提供调用基类A构造函数的代码
含虚函数类
:某个类含有虚函数,那么编译器会自动产生一个默认构造函数进行虚表指针相关的初始化操作
虚继承基类
:一个类虚继承于其他类,那么同样的编译器会为该类产生默认的构造函数。
- 单一性:
- 初始化构造函数(有参构造函数)
使用实参赋值
:在创建对象时,使用实参为对象的成员属性赋值,由编译器自动调用防止隐式转换
:如果是单参构造函数,需要使用explicit
进行修饰
- 复制 / 拷贝构造函数
- 若没有显示定义复制构造函数,则系统会默认创建一个复制构造函数
- 当
类中有指针
成员时,由系统默认创建的复制构造函数会存在“浅拷贝”的风险
,因此必须显式自定义复制构造函数。 - 复制构造函数
形参为类对象本身的引用
,根据调用对象进行深拷贝而构建新的类对象 - 被调用的三种情况
// 1. 对象的赋值初始化 Complex c2(c1); Complex c2 = c1; // 2. 函数形参 void Function(Complex c){ ... } // 可以使用const & :确保实参的值不会改变,并避免复制构造函数带来的深拷贝开销 void Function(const Complex & c){ } //3. 函数返回值 Complex Func() { Complex a(4); return a; }
- 移动构造函数
- 原因:通过
移动语义
,将已构建的对象内存所有权转移
给新对象,避免了复制语义下,对旧对象的一次拷贝和销毁开销。 - 使用:移动构造函数的
形参是右值引用
,可以使用move将左值进行转换。
class Integer { private: int* m_ptr; public: Integer(Integer&& source)// 注意形参是右值引用 : m_ptr(source.m_ptr) {// 指针成员需要进行赋值 source.m_ptr= nullptr; cout << "Call Integer(Integer&& source)移动" << endl; } }; int main(int argc, char const* argv[]) { Integer a; Integer b(std::move(a));// 将a转换成右值引用 return 0; }
- 原因:通过
- 委托构造函数
- 在同一个类中一个构造函数调用另一个构造函数,从而达到简化代码的目的。
class Base { public: int value1; int value2; Base() //目标构造函数 { value1 = 1; } Base(int value) : Base() //委托构造函数 { // 委托 Base() 构造函数 value2 = value; } }; void EntrustedConstruction() { Base b(2); //首先调用Base(int value) : Base() 毫无疑问 //然后会走到base()中,先给value1复制,然后走到Base(int value) : Base() ,给value2赋值 std::cout << b.value1 << std::endl; std::cout << b.value2 << std::endl; }
- 转换构造函数
- 单参构造函数,将其他类型对象或数据转换成本类对象(参数不是本类的对象)
class Student { public: //1. 默认构造函数,没有参数或形参具有缺省值 Student(int i=2){ this->age = 20; this->num = 1000; }; // 2. 初始化构造函数,有参数和参数列表 Student(int a, int n):age(a), num(n){}; // 3. 拷贝构造函数,参数是对象 Student(const Student& s){ this->age = s.age; this->num = s.num; }; // 4. 移动构造函数,参数是右值引用 Student(const Student&& s){ this->age = s.age; this->num = s.num; }; // 3. 转换构造函数,将int型转换成类对象 Student(int r){ // this->age = r; this->num = 1002; }; ~Student(){} public: int age; int num; };
类成员初始化方式
- 类成员初始化方式
- 赋值初始化:在函数体内初始化,是在所有数据成员被
分配内存空间后
进行的 - 列表初始化:在冒号后使用初始化列表进行的,是在给数据成员
分配内存空间的同时(原地构造效率高)
,先于构造构造函数的执行 - 顺序:初始化的顺序和其在声明时的顺序是一致的,与列表的先后顺序无关
class Test { public: Test(int a, int b, int c) : _a(a) // 列表初始化 { // 赋值初始化 _b = b; _c = c; private: int _a; int _b; int _c; };
- 赋值初始化:在函数体内初始化,是在所有数据成员被
- 必须使用成员初始化表的情况
- 定义同时必须初始化
- 初始化
引用成员
- 初始化
const修饰的成员
- 初始化
- 对于
类类型的成员变量
,减少构造成本- 初始化列表只需要执行一次有参构造函数或者拷贝构造,而在函数体中初始化需要执行一次默认构造、一次赋值操作(会产生临时变量)或者多一次有参构造(取决于构造输入的是类变量函数用与初始化类的参数)
class Derived : public Base { public: // 调用基类构造函数,而它拥有一组参数时,要使用成员初始化列表 Derived() : Base("DerivedStr", 200) // 这个是正确的 { //Base::Bstr = "DerivedStr"; // 基类构造函数再此之前调用,这里赋值没有用。 //Base::_i = 200; cout << "Derived Constructor" << endl; } string Dstr; };
- 定义同时必须初始化
其他
- 构造函数的执行顺序
虚基类
的构造函数(多个虚基类则按照继承的顺序执行构造函数)。基类
的构造函数(多个普通基类也按照继承的顺序执行构造函数)。成员类对象
的构造函数(按照声明顺序)派生类
的构造函数。
- 如何禁止程序自动生成拷贝构造函数?
- 方法1:
显式定义拷贝构造函数并设置为private
,防止类外调用 - 方法2:由于
友元类可以调用类中的private成员函数
,如果该函数只声明不定义可能产生编译错误。所以可以通过将拷贝构造函数放在base基类的private中进行解决
- 方法3:使用
=delete
进行拷贝构造的定义
- 方法1:
- 空类会默认添加的构造函数
1) Empty(); // 缺省构造函数 2) Empty( const Empty& ); // 拷贝构造函数 3) Empty& operator=( const Empty& ); // 拷贝赋值运算符 4) ~Empty(); // 析构函数
- 构造函数和析构函数声明为内联函数是没有意义的
- 违反了短小和调用频繁的要求:编译器并不真正对声明为inline的构造和析构函数进行内联操作,因为编译器会在构造和析构函数中添加额外的操作(申请/释放内存,构造/析构对象等),致使构造函数/析构函数并不像看上去的那么精简。
- 构造函数中出现异常
- 在对象的构造函数中如果发生异常,对象的析构函数无法析构未构造完成的对象,所以会造成内存泄漏
用unique_ptr对象来取代类的指针成员
,便对构造函数做了强化,免除了抛出异常时发生资源泄漏的危机,不再需要在析构函数中手动释放资源- 在析构函数中抛出异常,也会导致析构不完善
- 构造函数的几种关键字
default
:为类内函数显式声明default,可以让编译器必须生成该函数的默认版本
delete
:为类内函数显式声明delete,可以让编译器阻止生成该函数的默认版本
=0
:表示该vitual函数为纯虚函数。拥有纯虚函数的类是抽象类,不能进行实例化,但是在派生类中必须进行重写定义或向下传递
。
class A { public: A() = default;// 显式要求编译器生成构造函数 A(int a){}; virtual ~A(); void* operator new() = delete;//这样不允许使用new关键字 void f1(); virtual void f2(); virtual void f3()=0; };
二、析构函数
简介
- 特点
- 析构函数每个类只能有一个,没有参数和返回值,不能重载
- 同一生命周期的对象,由于是按顺序压栈,所以先创建的后释放
- 作用
清理类内指针成员
:系统无法自动释放指针变量指向的堆空间,需要在析构函数中使用delete进行处理- 实现C++的核心思想RAII(资源获取即初始化)而存在,并清理一下程序员干的好事
- 类什么时候会析构?
- 主动:调用delete释放对象
- 被动:类对象在结束生命周期后,系统会自动调用析构函数(包括类成员对象的析构函数)
- 析构函数的释放顺序
- 调用
派生类
的析构函数; - 调用
成员类对象
的析构函数; - 调用
基类
的析构函数。
- 调用
- 基类的析构函数必须是虚的
避免内存泄漏
:指向派生类对象的基类指针被delete时,如果基类析构函数不是虚函数,将只会调用基类的析构函数,无法释放派生类的申请的内存。一般不要将基类的析构函数定义为纯虚的
,纯虚析构函数会强制子类实现自己的析构函数,否则会链接失败
- 将类的析构函数声明为private
- 作用:
只能通过new在堆上创建
,无法在栈上创建对象。因为在栈上创建的对象在生命周期结束后无法自动调用析构函数无法使用delete释放对象
,delete释放对象是通过调用析构函数实现的,当析构函数为private时,无法进行调用
- 问题
限制继承
,因为派生类无法使用析构函数,可以将析构函数设置为protected无法释放对象
,可以在类内增加一个public的destroy()
函数调用析构函数
- 作用:
参考博客
更多推荐
所有评论(0)