C++中的 构造 & 析构函数

C++中的 构造 & 析构函数

1、引言

​ 在C++中,构造函数析构函数是类的重要成员函数,分别用于对象的创建和销毁。它们帮助自动初始化和清理对象的资源,避免内存泄漏和未初始化的问题。

  • 构造函数 用于对象创建时进行初始化,可以是默认构造函数、参数化构造函数、拷贝构造函数或移动构造函数。
  • 析构函数 用于对象销毁时清理资源,通常用于释放动态分配的内存或其他资源。
  • 构造与析构函数的配合使用 对于管理动态资源至关重要,特别是涉及到内存管理和防止内存泄漏时。

2、构造和析构的介绍

2.1.构造函数

2.1.1.定义:
  • 构造函数是类的成员函数,它在创建类对象时自动被调用,用于初始化对象的状态。
2.1.2格式:
  • 类名(){}
ClassName() {
    // 初始化代码
}
  • ClassName:构造函数的名字与类名相同。
  • 无返回类型:构造函数没有返回值,甚至没有 void
  • 没有参数(对于默认构造函数):构造函数可以没有参数,也可以有参数。
2.1.3.特点:
  • 名称:构造函数的名字与类名相同。
  • 权限:构造函数通常设置为 public 权限,以便外部能够调用。
  • 自动调用:构造函数会在对象创建时自动被调用,程序员无需显式调用。
  • 可以重载:可以有参数,所以可以有多个构造函数,允许根据不同的参数初始化对象。
  • 默认提供:如果不写构造函数 , 编译器会默认提供一个构造函数
  • 没有返回值:构造函数没有返回类型,甚至没有void
2.1.4.种类
  • 默认构造函数:没有参数的构造函数。如果程序员没有定义任何构造函数,编译器会自动提供一个默认构造函数。
  • 参数化构造函数:接受参数的构造函数,用于根据传入的参数来初始化对象。
  • 拷贝构造函数:用于通过现有对象创建一个新对象(通常用于按值传递或返回对象)。
2.1.5.功能:
  • 初始化成员变量:确保对象创建时各成员变量都有合适的初值。
  • 动态资源分配:为对象分配动态内存或其他资源。
  • 重载初始化方式:根据不同参数初始化对象,提供多种创建方式。
  • 自动调用:在创建对象时自动调用,简化代码。
  • 验证和约束:对构造参数进行验证,确保对象处于有效状态。
2.1.6.调用时机:
  • 构造函数会在对象实例化时自动调用,不需要手动调用,也不能手动调用。
  • 构造函数的目的是初始化对象的状态,因此它会在对象创建时被调用。
2.1.7.调用顺序:
  • 对于单个类对象,初始化列表中的成员变量初始化先于构造函数体。
  • 在类包含成员对象时,成员对象的构造函数先于该类的构造函数。
  • 在继承关系中,基类的构造函数先于派生类的构造函数调用,在多继承中按照继承顺序调用基类构造函数,虚拟继承中虚拟基类构造函数先调用且仅调用一次。
  • 对于对象数组,按数组元素的索引顺序调用构造函数。

2.2.析构函数

2.2.1.定义:
  • 析构函数是与类关联的一个特殊成员函数,用于在对象生命周期结束时清理资源。它的主要作用是释放对象占用的内存或执行必要的清理操作。析构函数没有返回类型,且无法接受任何参数。
2.2.2.格式:
  • ~类名(){}
~ClassName() {
    // 清理代码
}
  • ~ClassNameClassName 是类的名称,前面加上波浪号(~)表示它是析构函数。
  • 没有返回值:析构函数不返回任何值(包括 void)。
  • 没有参数:析构函数不能有参数,因此也不能重载析构函数。
2.2.3.特点:
  • 自动调用:析构函数是由编译器在对象生命周期结束时自动调用的。
  • 只调用一次:一个对象的析构函数只会被调用一次,当对象的生命周期结束时,析构函数自动执行。
  • 不能被手动调用:析构函数不能由程序员手动调用,只能在对象销毁时自动调用。
  • 不能被重载:一个类只能有一个析构函数,不能重载析构函数。
2.2.4.种类:

在 C++ 中,析构函数没有种类的区分,但它可以在不同的上下文中发挥不同的作用:

  • 普通析构函数:这是默认析构函数,当对象销毁时会自动调用,用于释放资源或进行清理。

    例如:

    class MyClass {
    public:
        ~MyClass() {
            cout << "析构函数调用" << endl;
        }
    };
    
  • 虚析构函数:当类设计为基类并且可能会通过基类指针删除派生类对象时,需要使用虚析构函数。虚析构函数确保派生类的析构函数也会被调用。

    例如:

    class Base {
    public:
        virtual ~Base() {  // 虚析构函数
            cout << "Base 类析构函数" << endl;
        }
    };
    
    class Derived : public Base {
    public:
        ~Derived() override {
            cout << "Derived 类析构函数" << endl;
        }
    };
    

    这样,当基类指针指向派生类对象并且 delete 该指针时,派生类的析构函数将被调用,确保正确释放资源。

2.2.5.功能:

析构函数的主要功能清理和释放对象在其生命周期中占用的资源,常见的功能包括:

  • 释放动态分配的内存:例如通过 new 分配的内存,需要通过析构函数释放。
  • 关闭文件:如果对象打开了文件或网络连接,析构函数可以负责关闭这些资源。
  • 释放其他资源:包括但不限于数据库连接、互斥锁、图形界面资源等。
2.2.6.调用时机:
  • 局部对象:当局部对象(栈对象)超出作用域时,析构函数会自动被调用。
  • 堆上对象:当通过 deletedelete[] 释放堆内存时,析构函数会被调用。
2.2.7.调用顺序:
  • 先调用派生类的析构函数:如果对象是派生类对象,并且继承自基类,那么派生类的析构函数首先被调用。
  • 然后调用基类的析构函数:基类的析构函数被调用,清理基类部分的资源。
  • 成员变量析构:对象的成员变量会在析构函数结束时被销毁。成员变量的析构顺序遵循它们的声明顺序,而不是在构造函数中初始化的顺序。

3、类中构造函数的调用方式

——概况

  • 在 C++ 中,确实有三种常见的调用构造函数的方式:括号法显示法隐式转换法。这些方式决定了如何初始化一个对象,下面我们来逐一解释它们的区别和特点。

3.1.括号法(Direct Initialization)

3.1.1.定义:
  • 括号法是最常见且推荐的构造对象方式。在此方式下,我们使用圆括号 () 来明确指定参数并调用构造函数。
3.1.2.特点:
  • 使用圆括号明确调用构造函数,直接传递参数。
  • 推荐使用此方式,简洁且明确。
3.1.3.示例:
class MyClass {
public:
    MyClass(int x) {
        cout << "构造函数调用,x = " << x << endl;
    }
};

int main() {
    MyClass obj(10);  // 括号法调用构造函数
    return 0;
}

输出:

构造函数调用,x = 10

​ 在此例中,MyClass obj(10); 是括号法构造函数调用,它会直接调用带一个整数参数的构造函数。

3.2.显示法(Copy Initialization)

3.2.1.定义:
  • 显示法通过使用等号 = 进行初始化。这种方式可以用于使用构造函数创建对象,特别是当你从一个已有对象创建新对象时,编译器会通过显示法来调用拷贝构造函数。
3.2.2.特点:
  • 使用等号 = 调用构造函数,适用于创建对象时,通常会发生类型转换或拷贝构造。
  • 它有时可能会涉及隐式类型转换或拷贝构造函数。
3.2.3.示例:
class MyClass {
public:
    MyClass(int x) {
        cout << "构造函数调用,x = " << x << endl;
    }
};

int main() {
    MyClass obj = 10;  // 显示法调用构造函数
    return 0;
}

输出:

构造函数调用,x = 10

​ 在此例中,MyClass obj = 10; 看似是赋值操作,但实际上它会调用构造函数来初始化 obj。显示法在 C++ 中会有隐式转换行为,例如上例中 10 被转换成 MyClass 类型,调用带有参数的构造函数。

3.3.隐式转换法(Implicit Conversion)

3.3.1.定义:
  • 隐式转换法是通过直接使用类型转换来初始化对象,通常是由编译器根据构造函数进行隐式类型转换。例如,在某些情况下,编译器会自动将给定的参数类型转换为构造函数所需要的类型。
3.3.2.特点:
  • 编译器根据构造函数自动进行类型转换并调用构造函数。
  • 经常出现在赋值或初始化时,编译器自动进行类型转换。
3.3.3.示例:
class MyClass {
public:
    MyClass(double x) {
        cout << "构造函数调用,x = " << x << endl;
    }
};

int main() {
    MyClass obj = 10;  // 隐式转换法调用构造函数
    return 0;
}

输出:

构造函数调用,x = 10

​ 在此例中,MyClass obj = 10; 通过隐式转换调用了接受 double 参数的构造函数。虽然传递的是 int 类型,但编译器会将 int 自动转换为 double 类型,然后调用构造函数。


4.初始化构造列表

4.1.定义

  • 在 C++ 中,初始化构造列表(initializer list)是一种特殊的语法,用于在构造函数的头部初始化类成员变量。它比在构造函数体内进行赋值更高效,因为成员变量在对象创建时就直接被初始化,而不是先默认初始化再被赋值。

4.2.格式

类名(构造函数实参表):成员1(初值1),成员2(初值2)
{
	构造函数函数体;
}

ClassName(Type arg1, Type arg2) : member1(arg1), member2(arg2) {
    // 构造函数体
}
  • member1(arg1):表示将构造函数的参数 arg1 用来初始化成员变量 member1
  • member2(arg2):表示将构造函数的参数 arg2 用来初始化成员变量 member2

4.3.示例

#include 
using namespace std;

class MyClass {
private:
    int x;
    double y;

public:
    // 构造函数,使用初始化构造列表来初始化成员变量
    MyClass(int val1, double val2) : x(val1), y(val2) {
        cout << "构造函数被调用" << endl;
    }

    // 打印成员变量
    void print() {
        cout << "x = " << x << ", y = " << y << endl;
    }

};

int main() {
    MyClass obj(10, 20.5); // 初始化构造列表初始化成员变量
    obj.print();  // 打印结果
    return 0;
}

输出:

构造函数被调用
x = 10, y = 20.5

4.4.优势

  • 更高效的初始化成员变量。
  • 对于常量成员和引用成员,必须通过初始化构造列表进行初始化。
  • 可以避免成员变量的默认初始化和后续赋值,节省性能开销。

4.5.必须使用初始化的场景

场景1:当形参列表 和 成员变量 名字重名的情况下
	可以使用初始化表 或者 this指针 解决
	
场景2:当 成员变量 中 有引用类型的 成员时
	这个时候 必须使用 初始化列表
	类名(type & num):val(num);
	
场景3: 当类中有const 修饰的成员变量时 
	
场景4: 当类中有成员子对象时
	
	类中 的 成员子对象 的有参构造
	
	当类中有成员子对象时,需要在构造函数的初始化中调用成员子对象的构造函数
	并传参完成对成员子对象初始化,如果没有调用成员子对象的构造函数
	默认会调用成员子对象的无参构造函数 但是如果 成员子对象 没有无参构造 则会报错

5、拷贝构造函数

在 C++ 中,拷贝构造函数(Copy Constructor)是一个特殊的构造函数,它用于创建一个新的对象,并将另一个同类型对象的值复制到这个新对象中。拷贝构造函数通常在以下几种情况下被调用:

  • 初始化一个对象,使用同类型的另一个对象。
  • 传递对象,例如作为函数参数传递时,采用值传递方式。
  • 返回对象,如函数返回一个对象时。
  • 赋值操作(赋值运算符的重载)。

5.1. 浅拷贝(Shallow Copy)

5.1.1定义:

浅拷贝是指在拷贝构造函数中,仅复制对象的每个成员变量的值。如果某个成员是指针,浅拷贝会直接复制指针的地址,而不是指针所指向的对象。

5.1.2.格式:
ClassName(const ClassName &other) {
    data = other.data;  // 直接复制指针的值(地址)
}
5.1.3.启动规则:

浅拷贝通常在以下情况下使用:

  • 对象中的成员变量是基本类型(如 int, double 等),或者没有动态分配的资源。
  • 对象的生命周期相同,避免共享资源的生命周期不同导致的问题。
5.1.4.风险:
  1. 资源共享问题: 当多个对象拥有相同的指针时,如果其中一个对象释放了资源(如内存),其他对象的指针就会变成悬挂指针,导致程序崩溃。
  2. 双重释放问题: 在析构函数中释放指针时,多个对象可能会重复释放相同的资源,导致双重释放(Double Free)错误。
5.1.5.示例:
#include 
using namespace std;

class ShallowCopyExample {
private:
    int *data;
public:
    // 构造函数
    ShallowCopyExample(int value) {
        data = new int(value);
    }

    // 浅拷贝构造函数
    ShallowCopyExample(const ShallowCopyExample &other) {
        data = other.data;  // 只复制指针,不是数据的副本
    }

    // 析构函数
    ~ShallowCopyExample() {
        delete data;
    }

    void print() {
        cout << "data = " << *data << endl;
    }
};

int main() {
    ShallowCopyExample obj1(10);  // 创建 obj1
    ShallowCopyExample obj2 = obj1;  // 浅拷贝构造函数调用
    obj2.print();  // 输出 obj2 的数据

    return 0;
}

输出:

data = 10

5.2. 深拷贝(Deep Copy)

5.2.1.定义:

深拷贝是指在拷贝构造函数中,不仅复制对象的成员变量值,而且还为指针成员分配新的内存空间,复制指针指向的内容。这样,源对象和目标对象将拥有各自独立的内存。

5.2.1.格式:
ClassName(const ClassName &other) {
    data = new int(*other.data);  // 分配新内存并复制数据
}
5.2.1.启动规则:

深拷贝通常用于以下情况:

  • 对象包含动态分配的内存或其他需要独立管理的资源。
  • 对象的生命周期可能不同,需要确保各个对象拥有独立的资源。
5.2.1.风险:
  • 内存泄漏: 如果深拷贝没有正确管理内存分配和释放,可能导致内存泄漏,特别是在析构函数没有正确释放资源时。
  • 性能开销: 深拷贝可能会有较高的开销,因为它需要为每个指针成员分配新的内存并复制数据。
5.2.1.示例:
#include 
using namespace std;

class DeepCopyExample {
private:
    int *data;
public:
    // 构造函数
    DeepCopyExample(int value) {
        data = new int(value);
    }

    // 深拷贝构造函数
    DeepCopyExample(const DeepCopyExample &other) {
        data = new int(*other.data);  // 为新对象分配内存并复制数据
    }

    // 析构函数
    ~DeepCopyExample() {
        delete data;
    }

    void print() {
        cout << "data = " << *data << endl;
    }
};

int main() {
    DeepCopyExample obj1(10);  // 创建 obj1
    DeepCopyExample obj2 = obj1;  // 深拷贝构造函数调用
    obj2.print();  // 输出 obj2 的数据

    return 0;
}

输出:

data = 10

5.3. 深拷贝与浅拷贝的区别

特性 浅拷贝 深拷贝
拷贝行为 只复制对象的成员变量值,指针成员复制的是地址。 不仅复制成员变量值,还为指针成员分配新的内存并复制数据。
内存管理 没有为指针成员分配新的内存,多个对象共享相同的资源。 为每个对象分配独立的内存,确保每个对象独立管理资源。
风险 可能出现双重释放(double free)或悬挂指针(dangling pointer)。 可能导致内存泄漏(memory leak)或性能开销较大。
适用场景 对象成员是基本类型或不涉及动态分配内存时。 对象包含动态分配的资源(如指针、动态数组等)时。
效率 更高效,因为只复制数据本身,操作简单。 较低效,因为需要分配新的内存并复制数据。

5.4. 如何避免风险

  • 避免浅拷贝问题:如果类中包含指针或动态内存,应该避免使用默认的浅拷贝构造函数,改为实现深拷贝构造函数。
  • 避免内存泄漏:对于深拷贝,确保析构函数正确释放内存,同时注意避免内存泄漏。
  • 智能指针:可以使用 C++11 引入的智能指针(std::unique_ptrstd::shared_ptr)来管理内存,避免手动管理内存的复杂性和风险。

5.5.总结

  • 浅拷贝适用于不涉及动态分配资源的类,并且会带来共享资源的问题。
  • 深拷贝用于类中含有动态内存分配的资源,确保每个对象拥有独立的资源,避免资源共享带来的问题。
  • 在编写 C++ 类时,需要根据实际情况选择合适的拷贝策略,避免深浅拷贝引起的内存管理问题。

6、static成员

静态成员可以分为静态成员变量和静态成员函数,具体如下:

静态成员变量

  • 共享性:所有对象共享同一份数据。
  • 内存分配:在编译阶段分配内存,而不是在对象创建时。
  • 初始化方式:在类内声明,类外进行初始化。

静态成员函数

  • 共享性:所有对象共享同一个函数。
  • 访问限制:静态成员函数只能访问静态成员变量,不能访问非静态成员变量和非静态成员函数。
  • 此外 由于 静态成员函数只能访问静态成员变量 所以不能使用 this 指针

6.1.静态成员变量

示例
#include 
#include 
using namespace std;

// 声明类
class Student
{
public:
    static int val_1;

private:
    static int val_2;

};

// 全局声明
int Student::val_1 = 100;
int Student::val_2 = 200;


int main()
{
    // 静态成员的访问方式

    // 1、通过对象访问
    Student S_1;
    Student S_2;

    S_1.val_1 = 110;

    cout << "S_1 val_1 = " << S_1.val_1 << endl;

    S_2.val_1 = 120;

    cout << "S_1 val_1 = " << S_1.val_1 << endl;
    cout << "S_2 val_1 = " << S_2.val_1 << endl;

    // 2、通过类名访问
    cout << "val_1 = " << Student::val_1 << endl;

    // cout << "val_2 = " << Student::val_2 << endl;


    return 0;
}

6.2.静态成员函数

示例
#include 
#include 
using namespace std;

// 声明类
class Student
{
public:
    static int val_1;

private:
    static int val_2;

public:

    int val_3;

    static void func(int val )
    {
        val_1 = val; // 可以使用的
        // val_3 = val; // 不可以使用

        cout << "调用 func " << " val_1 = " << val_1 << endl;

    }
};

// 全局声明
int Student::val_1 = 100;
int Student::val_2 = 200;

int main()
{
    // 静态成员的访问方式

    // 1、通过对象访问
    Student S_1;

    S_1.func(80);

    // 2、通过类名访问
    Student::func(90);
	
    return 0;
}

7、匿名对象

​ 在C++中,匿名对象是指那些没有显式命名的对象,通常用于临时性操作或在函数调用时不需要保留的对象。它们在程序运行过程中短暂地存在,用于表达式的计算或者函数的参数传递。匿名对象的典型特征是生命周期极短,通常在它们的作用范围结束时会被销毁,不需要程序员显式地管理它们的内存。

匿名对象的特点

  • 短生命周期
  • 简化代码
  • 自动销毁

示例代码

#include 
#include 
using namespace std;

class Student {
public:
    string name;
    int age;
    Student(string n, int a) : name(n), age(a) {}
    
    void show() {
        cout << "Name: " << name << ", Age: " << age << endl;
    }
};

void func(Student s) {
    s.show();
}

int main() {
    // 匿名对象可能的使用场景:
    // 1. 初始化类数组的时候
    Student hqyj[3] = {Student("小明", 18), Student("小红", 15), Student("小李", 25)};
    hqyj[0].show();
    hqyj[1].show();
    hqyj[2].show();
    
    // 2. 给函数传参时
    func(Student("张飞", 40));
    
    return 0;
}

你可能感兴趣的:(c++,数据结构)