C++学习:六个月从基础到就业——面向对象编程:构造函数与析构函数

发布于:2025-04-17 ⋅ 阅读:(101) ⋅ 点赞:(0)

C++学习:六个月从基础到就业——面向对象编程:构造函数与析构函数

本文是我C++学习之旅系列的第十篇技术文章,主要讨论C++中构造函数与析构函数的概念、特点和使用技巧。这些是C++对象生命周期管理的关键组成部分。查看完整系列目录了解更多内容。

引言

在C++面向对象编程中,对象的创建和销毁是程序运行过程中非常重要的环节。构造函数负责对象的初始化工作,确保对象在创建时处于有效状态;而析构函数负责清理对象占用的资源,防止资源泄露。恰当地使用构造函数和析构函数是编写高质量C++程序的关键。

本文将详细介绍C++中构造函数和析构函数的各种形式、特性和最佳实践,帮助你全面理解这两个重要概念。

构造函数

什么是构造函数?

构造函数是一种特殊的成员函数,在创建类的对象时自动调用,其主要职责是初始化对象的数据成员。构造函数与类同名,没有返回类型(甚至不是void)。

默认构造函数

默认构造函数是不需要参数就能调用的构造函数。它可以没有参数,也可以所有参数都有默认值。

class Example {
public:
    // 无参数的默认构造函数
    Example() {
        std::cout << "Default constructor called" << std::endl;
        data = 0;
    }
    
    // 也是默认构造函数(因为所有参数都有默认值)
    Example(int value = 0) {
        std::cout << "Parameterized constructor with default value called" << std::endl;
        data = value;
    }
    
private:
    int data;
};

当一个类没有定义任何构造函数时,编译器会自动生成一个默认构造函数,这称为"合成的默认构造函数"。这个构造函数会对内置类型成员不做初始化(保持不确定值),对于类类型成员则调用其默认构造函数。

注意: 如果您定义了任何构造函数,编译器就不会再生成默认构造函数。如果此时您仍然需要默认构造函数,需要自己定义或使用C++11引入的= default语法:

class Person {
public:
    Person() = default;  // 显式要求编译器生成默认构造函数
    Person(const std::string& name) : name(name) {}
    
private:
    std::string name;
};

带参数的构造函数

构造函数可以接受参数,以便在对象创建时进行定制化初始化:

class Rectangle {
public:
    Rectangle(double w, double h) : width(w), height(h) {
        std::cout << "Rectangle created with width " << width 
                  << " and height " << height << std::endl;
    }
    
private:
    double width;
    double height;
};

// 使用示例
Rectangle rect(5.0, 3.0);  // 调用带参数的构造函数

初始化列表

构造函数可以使用初始化列表来初始化成员变量。初始化列表位于构造函数参数列表之后,函数体之前,用冒号引导:

class Point {
public:
    // 使用初始化列表
    Point(double xCoord, double yCoord) : x(xCoord), y(yCoord) {
        std::cout << "Point created at (" << x << ", " << y << ")" << std::endl;
    }
    
private:
    double x;
    double y;
};

初始化列表的优势:

  1. 性能优势: 对于类类型的成员,初始化列表直接调用其构造函数,而不是先默认构造再赋值,避免了不必要的操作。

  2. 常量和引用成员: 常量成员和引用成员必须在初始化列表中初始化,因为它们不能在构造函数体中赋值。

  3. 初始化顺序清晰: 使代码更清晰地表达初始化意图。

class Entity {
public:
    // 常量成员和引用成员必须使用初始化列表
    Entity(int val, int& refVal) : constValue(val), reference(refVal) {}
    
    void print() const {
        std::cout << "Const value: " << constValue 
                  << ", Reference value: " << reference << std::endl;
    }
    
private:
    const int constValue;  // 常量成员
    int& reference;        // 引用成员
};

注意: 成员初始化的顺序与它们在类中声明的顺序一致,而不是初始化列表中的顺序。为避免混淆,建议初始化列表的顺序与成员声明顺序保持一致。

委托构造函数(C++11)

C++11引入了委托构造函数,允许一个构造函数调用同一个类的另一个构造函数,避免代码重复:

class Customer {
public:
    // 主构造函数
    Customer(const std::string& name, int age, const std::string& address) 
        : name(name), age(age), address(address) {
        std::cout << "Main constructor called" << std::endl;
    }
    
    // 委托构造函数
    Customer(const std::string& name, int age) 
        : Customer(name, age, "Unknown") {
        std::cout << "Delegating constructor called" << std::endl;
    }
    
    // 另一个委托构造函数
    Customer() : Customer("Anonymous", 0) {
        std::cout << "Default constructor called" << std::endl;
    }
    
private:
    std::string name;
    int age;
    std::string address;
};

在这个例子中,默认构造函数委托给带两个参数的构造函数,后者又委托给带三个参数的主构造函数。这种方式可以减少代码重复,并集中管理初始化逻辑。

复制构造函数

复制构造函数是一种特殊的构造函数,它以同类型的另一个对象作为参数,创建新对象作为参数对象的副本:

class MyString {
public:
    MyString(const char* str) {
        size = strlen(str);
        data = new char[size + 1];
        strcpy(data, str);
    }
    
    // 复制构造函数
    MyString(const MyString& other) {
        size = other.size;
        data = new char[size + 1];
        strcpy(data, other.data);
        std::cout << "Copy constructor called" << std::endl;
    }
    
    ~MyString() {
        delete[] data;
    }
    
private:
    char* data;
    size_t size;
};

// 使用示例
MyString s1("Hello");
MyString s2 = s1;  // 调用复制构造函数
MyString s3(s1);   // 也调用复制构造函数

何时调用复制构造函数:

  1. 用一个对象初始化同类型的另一个对象
  2. 函数按值传递对象
  3. 函数按值返回对象
  4. 编译器生成临时对象

如果您没有定义复制构造函数,编译器会生成一个默认的复制构造函数,执行成员的逐一复制(浅复制)。对于包含动态分配内存或资源的类,这通常是不够的,需要自定义复制构造函数。

禁用复制: 如果您不希望对象被复制,可以将复制构造函数声明为私有或使用C++11的= delete语法:

class NoCopy {
public:
    NoCopy() {}
    NoCopy(const NoCopy&) = delete;  // 禁止复制
};

移动构造函数(C++11)

C++11引入了移动构造函数,它接受一个右值引用参数,通过"偷取"源对象的资源而不是复制来构造新对象,这在处理大型对象时能显著提高性能:

class MyVector {
public:
    MyVector(size_t n) : size(n), data(new int[n]) {
        std::cout << "Constructor allocating " << size << " integers" << std::endl;
    }
    
    // 复制构造函数(深复制)
    MyVector(const MyVector& other) : size(other.size), data(new int[other.size]) {
        std::cout << "Copy constructor - deep copy of " << size << " integers" << std::endl;
        std::copy(other.data, other.data + size, data);
    }
    
    // 移动构造函数
    MyVector(MyVector&& other) noexcept : size(other.size), data(other.data) {
        std::cout << "Move constructor - stealing resources" << std::endl;
        other.size = 0;
        other.data = nullptr;  // 防止源对象的析构函数释放内存
    }
    
    ~MyVector() {
        std::cout << "Destructor releasing memory" << std::endl;
        delete[] data;
    }
    
private:
    size_t size;
    int* data;
};

// 使用示例
MyVector createVector(size_t size) {
    return MyVector(size);  // 返回临时对象
}

MyVector v1(1000000);               // 常规构造
MyVector v2 = v1;                   // 复制构造
MyVector v3 = std::move(v1);        // 移动构造 - 显式移动
MyVector v4 = createVector(1000000); // 移动构造 - 编译器优化

在这个例子中,移动构造函数接受一个右值引用(MyVector&&),并"窃取"源对象的资源,然后将源对象重置为有效但不拥有任何资源的状态。这避免了深复制的开销。

noexcept说明符: 移动构造函数通常应该标记为noexcept,表明它不会抛出异常。这对于标准库容器优化移动操作非常重要。

自动生成的移动构造函数: 如果类没有定义复制构造函数、复制赋值运算符、移动赋值运算符或析构函数,且所有非静态数据成员和基类都可移动构造,编译器会生成一个移动构造函数。否则,移动操作将回退到复制操作。

转换构造函数

接受一个参数的构造函数(或者除第一个参数外其余参数都有默认值的构造函数)会定义一个从参数类型到类类型的隐式转换:

class MyString {
public:
    // 转换构造函数 - 允许从const char*到MyString的隐式转换
    MyString(const char* str) {
        std::cout << "Converting const char* to MyString" << std::endl;
        // 初始化代码...
    }
};

void printString(const MyString& s) {
    // 函数实现...
}

// 使用示例
printString("Hello");  // 隐式转换:const char*被转换为MyString

防止隐式转换: 如果您不希望构造函数允许隐式转换,可以使用explicit关键字:

class MyString {
public:
    // 显式构造函数 - 不允许隐式转换
    explicit MyString(const char* str) {
        std::cout << "Converting const char* to MyString" << std::endl;
        // 初始化代码...
    }
};

// 使用示例
// printString("Hello");  // 错误:不允许隐式转换
printString(MyString("Hello"));  // 正确:显式转换

私有构造函数

构造函数也可以声明为私有的,这通常用于实现单例模式或防止直接创建类的实例:

class Singleton {
private:
    // 私有构造函数
    Singleton() {
        std::cout << "Singleton instance created" << std::endl;
    }
    
public:
    // 获取单例实例的静态方法
    static Singleton& getInstance() {
        static Singleton instance;
        return instance;
    }
    
    // 防止复制
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};

// 使用示例
// Singleton s;  // 错误:构造函数是私有的
Singleton& s = Singleton::getInstance();  // 正确:通过静态方法访问

聚合初始化

对于符合聚合条件的类(公共成员,没有用户定义的构造函数,没有私有或保护的非静态数据成员,没有基类和虚函数),可以使用聚合初始化:

struct Point {
    double x;
    double y;
};

// 使用聚合初始化
Point p1 = {10.0, 20.0};
Point p2{30.0, 40.0};  // C++11统一初始化语法

C++17允许从基类"继承"公共成员的初始化:

struct Point2D {
    int x;
    int y;
};

struct Point3D : Point2D {
    int z;
};

// C++17聚合初始化
Point3D p = {{1, 2}, 3};  // x=1, y=2, z=3

指定初始化器(C++20)

C++20引入了指定初始化器,允许按名称初始化聚合类型的成员:

struct Point {
    double x;
    double y;
};

// C++20指定初始化器
Point p{.x = 10.0, .y = 20.0};

这提高了代码的可读性,特别是当初始化复杂结构体时。然而,目前并非所有编译器都完全支持这一特性。

析构函数

什么是析构函数?

析构函数是在对象被销毁时自动调用的特殊成员函数,用于清理对象占用的资源。析构函数的名称是类名前加上波浪号(~),没有参数和返回值。

class Resource {
public:
    Resource(const std::string& name) : name(name) {
        std::cout << "Resource " << name << " acquired" << std::endl;
    }
    
    ~Resource() {
        std::cout << "Resource " << name << " released" << std::endl;
    }
    
private:
    std::string name;
};

析构函数的调用时机

析构函数在以下情况下被调用:

  1. 对象离开作用域时
  2. 动态分配的对象被删除时
  3. 临时对象的生命周期结束时
  4. 程序结束,全局或静态对象被销毁时
void example() {
    Resource localRes("Local");      // 构造localRes
    
    Resource* dynamicRes = new Resource("Dynamic");
    delete dynamicRes;               // 调用dynamicRes的析构函数
    
    {
        Resource blockRes("Block");  // 构造blockRes
    }                                // blockRes离开作用域,调用其析构函数
    
}                                    // localRes离开作用域,调用其析构函数

默认析构函数

如果您没有定义析构函数,编译器会生成一个默认的析构函数,它会调用每个成员的析构函数。对于大多数类来说,这已经足够了。但是,如果类管理其他资源(如动态分配的内存、文件句柄、网络连接等),则需要定义自己的析构函数来正确释放这些资源。

class DefaultDestructor {
public:
    DefaultDestructor() {
        std::cout << "DefaultDestructor constructed" << std::endl;
    }
    
    // 编译器会生成默认析构函数
};

class CustomDestructor {
public:
    CustomDestructor() {
        std::cout << "CustomDestructor constructed" << std::endl;
        data = new int[100];
    }
    
    ~CustomDestructor() {
        std::cout << "CustomDestructor destroyed" << std::endl;
        delete[] data;  // 释放动态分配的内存
    }
    
private:
    int* data;
};

虚析构函数

当通过基类指针删除派生类对象时,如果基类的析构函数不是虚函数,那么只会调用基类的析构函数,可能导致资源泄露。因此,如果一个类将被用作基类,其析构函数应该声明为虚函数:

class Base {
public:
    Base() {
        std::cout << "Base constructed" << std::endl;
    }
    
    virtual ~Base() {
        std::cout << "Base destroyed" << std::endl;
    }
};

class Derived : public Base {
public:
    Derived() {
        std::cout << "Derived constructed" << std::endl;
        data = new int[100];
    }
    
    ~Derived() override {
        std::cout << "Derived destroyed" << std::endl;
        delete[] data;
    }
    
private:
    int* data;
};

int main() {
    Base* ptr = new Derived();
    delete ptr;  // 如果~Base()不是虚函数,这里只会调用Base的析构函数
    return 0;
}

在这个例子中,如果~Base()不是虚函数,那么delete ptr只会调用Base的析构函数,而不会调用Derived的析构函数,导致data指向的内存泄露。

纯虚析构函数

虽然不常见,但析构函数也可以是纯虚函数。这使得类变成抽象类,但与其他纯虚函数不同,纯虚析构函数必须有一个定义:

class AbstractBase {
public:
    virtual ~AbstractBase() = 0;  // 纯虚析构函数
};

// 纯虚析构函数必须有定义
AbstractBase::~AbstractBase() {
    std::cout << "AbstractBase destroyed" << std::endl;
}

class Concrete : public AbstractBase {
public:
    ~Concrete() override {
        std::cout << "Concrete destroyed" << std::endl;
    }
};

纯虚析构函数的主要用途是创建一个必须被继承而不能直接实例化的基类,同时确保派生类对象能够正确销毁。

禁用析构函数

在一些特殊情况下,您可能希望禁止对象被销毁。这可以通过将析构函数声明为私有或使用= delete语法来实现:

class Immortal {
public:
    Immortal() {}
    
    // 禁用析构函数
    ~Immortal() = delete;
};

// int main() {
//     Immortal im;  // 错误:对象无法销毁
//     return 0;
// }

这种技术很少使用,通常只在非常特殊的设计中才会用到。

异常与析构函数

析构函数不应该抛出异常。如果析构函数在异常传播过程中被调用(例如栈展开期间),且析构函数本身也抛出异常,程序将调用std::terminate终止。

class NoExceptionDestructor {
public:
    ~NoExceptionDestructor() noexcept {  // 明确标记为不抛出异常
        try {
            // 可能抛出异常的清理代码
        } catch (...) {
            // 捕获并处理所有异常
            std::cerr << "Exception caught in destructor" << std::endl;
        }
    }
};

C++11引入了noexcept说明符,可以明确表示函数不会抛出异常。析构函数默认是noexcept(true)的。

构造函数和析构函数的执行顺序

多个对象的构造和析构顺序

当创建多个对象时,构造的顺序是确定的,但析构的顺序与构造顺序相反:

class Tracer {
public:
    Tracer(const std::string& name) : name(name) {
        std::cout << "Constructing " << name << std::endl;
    }
    
    ~Tracer() {
        std::cout << "Destroying " << name << std::endl;
    }
    
private:
    std::string name;
};

int main() {
    Tracer t1("First");
    Tracer t2("Second");
    return 0;
}

输出:

Constructing First
Constructing Second
Destroying Second
Destroying First

组合关系中的构造和析构顺序

在组合(包含)关系中,成员的构造顺序是它们在类中声明的顺序,而不是在构造函数初始化列表中的顺序。析构顺序则与构造顺序相反:

class Member1 {
public:
    Member1() { std::cout << "Member1 constructed" << std::endl; }
    ~Member1() { std::cout << "Member1 destroyed" << std::endl; }
};

class Member2 {
public:
    Member2() { std::cout << "Member2 constructed" << std::endl; }
    ~Member2() { std::cout << "Member2 destroyed" << std::endl; }
};

class Container {
public:
    Container() {
        std::cout << "Container constructed" << std::endl;
    }
    
    ~Container() {
        std::cout << "Container destroyed" << std::endl;
    }
    
private:
    Member1 m1;  // 先声明,先构造
    Member2 m2;  // 后声明,后构造
};

输出:

Member1 constructed
Member2 constructed
Container constructed
Container destroyed
Member2 destroyed
Member1 destroyed

继承关系中的构造和析构顺序

在继承关系中,构造顺序是先构造基类,再构造派生类。析构顺序则相反,先析构派生类,再析构基类:

class Base {
public:
    Base() { std::cout << "Base constructed" << std::endl; }
    virtual ~Base() { std::cout << "Base destroyed" << std::endl; }
};

class Derived : public Base {
public:
    Derived() { std::cout << "Derived constructed" << std::endl; }
    ~Derived() override { std::cout << "Derived destroyed" << std::endl; }
};

输出:

Base constructed
Derived constructed
Derived destroyed
Base destroyed

RAII(资源获取即初始化)

RAII是C++中一种重要的资源管理技术,它将资源的生命周期与持有资源的对象的生命周期绑定在一起。资源在对象构造时获取,在对象析构时释放,这确保了资源不会泄露。

class File {
public:
    File(const std::string& filename) : file(nullptr) {
        file = fopen(filename.c_str(), "r");
        if (!file) {
            throw std::runtime_error("Failed to open file");
        }
        std::cout << "File opened" << std::endl;
    }
    
    ~File() {
        if (file) {
            fclose(file);
            std::cout << "File closed" << std::endl;
        }
    }
    
    // 禁止复制
    File(const File&) = delete;
    File& operator=(const File&) = delete;
    
    // 读取文件内容
    std::string read() {
        // 实现文件读取...
        return "File content";
    }
    
private:
    FILE* file;
};

void processFile(const std::string& filename) {
    try {
        File f(filename);
        std::string content = f.read();
        // 处理内容...
    } catch (const std::exception& e) {
        std::cerr << "Error: " << e.what() << std::endl;
    }
    // 无论是否抛出异常,f的析构函数都会被调用,确保文件被关闭
}

RAII是实现异常安全代码的关键技术,它确保即使在异常发生时,资源也能被正确释放。

智能指针

现代C++提供了智能指针,这是RAII的具体应用,用于自动管理动态分配的内存:

#include <memory>
#include <iostream>

class Resource {
public:
    Resource(const std::string& name) : name(name) {
        std::cout << "Resource " << name << " acquired" << std::endl;
    }
    
    ~Resource() {
        std::cout << "Resource " << name << " released" << std::endl;
    }
    
    void use() {
        std::cout << "Using resource " << name << std::endl;
    }
    
private:
    std::string name;
};

void useUniquePtr() {
    std::unique_ptr<Resource> res = std::make_unique<Resource>("Unique");
    res->use();
    // 离开作用域时,res自动删除所指向的Resource对象
}

void useSharedPtr() {
    std::shared_ptr<Resource> res1 = std::make_shared<Resource>("Shared");
    {
        std::shared_ptr<Resource> res2 = res1;  // 共享所有权
        res2->use();
    }  // res2离开作用域,但资源不释放,因为res1仍然引用它
    res1->use();
    // 当res1离开作用域,没有其他共享指针引用资源时,资源被释放
}

智能指针的使用是现代C++资源管理的最佳实践,它们比原始指针更安全,能有效防止内存泄露。

特殊成员函数的自动生成

C++中有六个特殊成员函数,在一定条件下会由编译器自动生成:

  1. 默认构造函数
  2. 析构函数
  3. 复制构造函数
  4. 复制赋值运算符
  5. 移动构造函数(C++11)
  6. 移动赋值运算符(C++11)

下面的表格总结了这些特殊成员函数何时会被自动生成:

特殊成员函数 自动生成条件
默认构造函数 如果没有定义任何构造函数
析构函数 如果没有定义析构函数
复制构造函数 如果没有定义复制构造函数、移动构造函数、移动赋值运算符、析构函数
复制赋值运算符 如果没有定义复制赋值运算符、移动构造函数、移动赋值运算符、析构函数
移动构造函数 如果没有定义复制构造函数、复制赋值运算符、移动构造函数、移动赋值运算符、析构函数
移动赋值运算符 如果没有定义复制构造函数、复制赋值运算符、移动构造函数、移动赋值运算符、析构函数

注意: C++11之后,定义任何的构造函数都会阻止自动生成默认构造函数。类似地,定义了任何的移动操作会阻止自动生成复制操作,反之亦然。

显式控制特殊成员函数(C++11)

C++11引入了= default= delete语法,允许更精确地控制特殊成员函数的生成和禁用:

class ControlledClass {
public:
    // 显式要求默认构造函数
    ControlledClass() = default;
    
    // 显式要求默认复制构造函数
    ControlledClass(const ControlledClass&) = default;
    
    // 禁止复制赋值
    ControlledClass& operator=(const ControlledClass&) = delete;
    
    // 禁止移动构造
    ControlledClass(ControlledClass&&) = delete;
    
    // 禁止移动赋值
    ControlledClass& operator=(ControlledClass&&) = delete;
};

通过这种方式,可以精确控制哪些操作是允许的,哪些是禁止的,提高了代码的可读性和安全性。

实际应用示例

实现自己的字符串类

#include <iostream>
#include <cstring>
#include <algorithm>

class MyString {
private:
    char* data;
    size_t length;
    
public:
    // 默认构造函数
    MyString() : data(nullptr), length(0) {
        std::cout << "Default constructor called" << std::endl;
    }
    
    // 参数化构造函数
    MyString(const char* str) : data(nullptr), length(0) {
        std::cout << "Parameterized constructor called" << std::endl;
        if (str) {
            length = std::strlen(str);
            data = new char[length + 1];
            std::memcpy(data, str, length + 1);
        }
    }
    
    // 复制构造函数
    MyString(const MyString& other) : data(nullptr), length(other.length) {
        std::cout << "Copy constructor called" << std::endl;
        if (other.data) {
            data = new char[length + 1];
            std::memcpy(data, other.data, length + 1);
        }
    }
    
    // 移动构造函数
    MyString(MyString&& other) noexcept : data(other.data), length(other.length) {
        std::cout << "Move constructor called" << std::endl;
        other.data = nullptr;
        other.length = 0;
    }
    
    // 复制赋值运算符
    MyString& operator=(const MyString& other) {
        std::cout << "Copy assignment operator called" << std::endl;
        if (this != &other) {
            MyString temp(other);  // 复制构造临时对象
            std::swap(data, temp.data);
            std::swap(length, temp.length);
        }
        return *this;
    }
    
    // 移动赋值运算符
    MyString& operator=(MyString&& other) noexcept {
        std::cout << "Move assignment operator called" << std::endl;
        if (this != &other) {
            delete[] data;
            data = other.data;
            length = other.length;
            other.data = nullptr;
            other.length = 0;
        }
        return *this;
    }
    
    // 析构函数
    ~MyString() {
        std::cout << "Destructor called" << std::endl;
        delete[] data;
    }
    
    // 获取字符串长度
    size_t size() const {
        return length;
    }
    
    // 获取C风格字符串
    const char* c_str() const {
        return data ? data : "";
    }
    
    // 打印字符串
    void print() const {
        std::cout << "String: " << (data ? data : "(empty)") 
                  << ", Length: " << length << std::endl;
    }
};

int main() {
    // 测试默认构造函数
    MyString s1;
    s1.print();
    
    // 测试参数化构造函数
    MyString s2("Hello");
    s2.print();
    
    // 测试复制构造函数
    MyString s3 = s2;
    s3.print();
    
    // 测试移动构造函数
    MyString s4 = std::move(s3);
    s4.print();
    s3.print();  // s3现在应该是空的
    
    // 测试复制赋值运算符
    s1 = s2;
    s1.print();
    
    // 测试移动赋值运算符
    s1 = std::move(s4);
    s1.print();
    s4.print();  // s4现在应该是空的
    
    return 0;
}

这个例子实现了一个简单的字符串类,包含所有特殊成员函数。它展示了如何正确管理动态分配的内存,以及如何使用移动语义优化性能。

资源管理类

下面是一个模拟数据库连接的资源管理类的例子,展示了RAII原则的应用:

#include <iostream>
#include <string>
#include <stdexcept>

class DatabaseConnection {
private:
    std::string connectionString;
    bool connected;
    
    // 模拟连接数据库
    void connect() {
        std::cout << "Connecting to database: " << connectionString << std::endl;
        // 模拟连接操作
        connected = true;
        std::cout << "Connected successfully" << std::endl;
    }
    
    // 模拟断开连接
    void disconnect() {
        if (connected) {
            std::cout << "Disconnecting from database: " << connectionString << std::endl;
            // 模拟断开连接操作
            connected = false;
            std::cout << "Disconnected successfully" << std::endl;
        }
    }
    
public:
    // 构造函数 - 自动连接数据库
    DatabaseConnection(const std::string& connString) 
        : connectionString(connString), connected(false) {
        try {
            connect();
        } catch (const std::exception& e) {
            std::cerr << "Failed to connect: " << e.what() << std::endl;
            throw;  // 重新抛出异常
        }
    }
    
    // 禁止复制
    DatabaseConnection(const DatabaseConnection&) = delete;
    DatabaseConnection& operator=(const DatabaseConnection&) = delete;
    
    // 允许移动
    DatabaseConnection(DatabaseConnection&& other) noexcept 
        : connectionString(std::move(other.connectionString)), connected(other.connected) {
        other.connected = false;  // 确保other不会在析构时断开连接
    }
    
    DatabaseConnection& operator=(DatabaseConnection&& other) noexcept {
        if (this != &other) {
            disconnect();  // 断开当前连接
            connectionString = std::move(other.connectionString);
            connected = other.connected;
            other.connected = false;
        }
        return *this;
    }
    
    // 析构函数 - 自动断开连接
    ~DatabaseConnection() {
        try {
            disconnect();
        } catch (const std::exception& e) {
            std::cerr << "Error during disconnect: " << e.what() << std::endl;
            // 析构函数不应该抛出异常,所以我们只记录它
        }
    }
    
    // 执行查询
    std::string executeQuery(const std::string& query) {
        if (!connected) {
            throw std::runtime_error("Database not connected");
        }
        
        std::cout << "Executing query: " << query << std::endl;
        // 模拟查询执行
        return "Query results";
    }
    
    // 检查连接状态
    bool isConnected() const {
        return connected;
    }
    
    // 重新连接
    void reconnect() {
        disconnect();
        connect();
    }
};

// 使用RAII模式操作数据库
void performDatabaseOperation() {
    try {
        // 创建连接对象时自动连接
        DatabaseConnection db("server=localhost;user=root;password=secret");
        
        // 执行操作
        std::string result = db.executeQuery("SELECT * FROM users");
        std::cout << "Result: " << result << std::endl;
        
        // 函数结束时,db对象超出作用域,自动断开连接
    } catch (const std::exception& e) {
        std::cerr << "Database operation failed: " << e.what() << std::endl;
    }
}

int main() {
    performDatabaseOperation();
    return 0;
}

这个例子展示了如何使用构造函数和析构函数自动管理资源(数据库连接),确保资源在使用后被正确释放。

最佳实践

  1. 遵循RAII原则:使用构造函数获取资源,使用析构函数释放资源,避免资源泄漏。

  2. 优先使用初始化列表:在构造函数中使用初始化列表初始化成员变量,而不是在构造函数体内赋值,这更高效且能正确初始化常量和引用成员。

  3. 为管理资源的类实现"五大函数":如果类管理资源(如动态内存),确保正确实现复制构造函数、复制赋值运算符、移动构造函数、移动赋值运算符和析构函数,或者禁用不需要的操作。

  4. 使析构函数为虚函数:如果类将被用作基类,确保析构函数是虚函数,以确保通过基类指针删除派生类对象时能正确调用派生类的析构函数。

  5. 构造函数应该确保对象初始化为有效状态:如果构造过程中发生错误,应该抛出异常,而不是创建无效状态的对象。

  6. 析构函数不应该抛出异常:如果析构函数可能抛出异常,应该将可能抛出异常的代码包装在try-catch块中,并在捕获异常后处理或记录它。

  7. 防止异常逃逸构造函数:如果构造函数执行可能抛出异常的操作,应该捕获这些异常,确保释放任何已分配的资源,然后重新抛出异常。

  8. 使用委托构造函数避免代码重复:如果有多个功能相似的构造函数,使用委托构造函数减少代码重复。

  9. 优先使用= delete而非私有但未定义:要禁止特定操作,使用= delete语法明确表明意图,而不是将函数声明为私有但不定义。

  10. 优先使用智能指针:现代C++中,优先使用std::unique_ptrstd::shared_ptr等智能指针管理动态内存,而不是原始指针和手动内存管理。

  11. 避免在构造函数中调用虚函数:在构造函数中,派生类对象尚未完全构造,调用虚函数不会按预期调用派生类的版本。

  12. 记住"法则五"(Rule of Five)和"法则零"(Rule of Zero)

    • “法则五”:如果需要定义任何一个特殊成员函数(析构函数、复制构造函数、复制赋值运算符、移动构造函数、移动赋值运算符),通常需要定义所有五个。
    • “法则零”:如果可能,设计类使其不需要定义任何特殊成员函数,而是依赖编译器生成的默认版本。

总结

构造函数和析构函数是C++面向对象编程中的核心概念,它们管理着对象的生命周期——从创建到销毁。正确使用这些特殊成员函数对于编写健壮、无泄漏的C++程序至关重要。

在本文中,我们详细讨论了各种类型的构造函数,包括默认构造函数、带参数的构造函数、复制构造函数、移动构造函数和转换构造函数。我们还研究了析构函数的特性和用途,特别是在资源管理和继承层次结构中的作用。

C++11引入的移动语义和委托构造函数等新特性进一步增强了构造函数的功能,使我们能够编写更高效、更清晰的代码。同时,RAII模式与构造函数和析构函数紧密结合,成为C++资源管理的基石。

理解和掌握构造函数和析构函数是成为一名熟练的C++程序员的关键一步。通过遵循最佳实践,我们可以创建安全、高效、易于维护的C++代码。

在下一篇文章中,我们将深入探讨C++的访问控制与友元,这是封装和信息隐藏的重要机制。

参考资料

  1. Bjarne Stroustrup. The C++ Programming Language (4th Edition)
  2. Scott Meyers. Effective Modern C++
  3. cppreference.com - 构造函数
  4. cppreference.com - 析构函数
  5. C++ Core Guidelines - 构造、赋值和析构函数
  6. Herb Sutter. Exceptional C++: 47 Engineering Puzzles, Programming Problems, and Solutions

这是我C++学习之旅系列的第十篇技术文章。查看完整系列目录了解更多内容。


网站公告

今日签到

点亮在社区的每一天
去签到