MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C++类构造函数与析构函数的调用时机与手动调用分析

2023-05-267.3k 阅读

C++ 类构造函数与析构函数的调用时机

构造函数的调用时机

  1. 对象创建时
    • 在 C++ 中,当定义一个类的对象时,构造函数会被自动调用。例如,定义一个简单的 Point 类:
class Point {
public:
    int x;
    int y;
    Point() {
        x = 0;
        y = 0;
        std::cout << "Default constructor called" << std::endl;
    }
};
  • 当在主函数中创建 Point 对象时:
int main() {
    Point p;
    return 0;
}
  • 输出结果为:Default constructor called。这里,在 Point p; 这一行,构造函数 Point() 被调用,对 p 对象进行初始化。
  1. 动态内存分配时
    • 使用 new 关键字动态分配对象时,构造函数也会被调用。例如:
class Rectangle {
public:
    int width;
    int height;
    Rectangle() {
        width = 0;
        height = 0;
        std::cout << "Rectangle default constructor called" << std::endl;
    }
};
  • 在主函数中动态分配 Rectangle 对象:
int main() {
    Rectangle* rect = new Rectangle();
    delete rect;
    return 0;
}
  • 输出为:Rectangle default constructor called。这里 new Rectangle() 调用了构造函数,为新分配的 Rectangle 对象进行初始化。
  1. 函数参数传递时
    • 当以值传递的方式将对象作为函数参数传递时,构造函数会被调用。考虑如下函数和类定义:
class Circle {
public:
    int radius;
    Circle() {
        radius = 0;
        std::cout << "Circle default constructor called" << std::endl;
    }
    Circle(const Circle& other) {
        radius = other.radius;
        std::cout << "Circle copy constructor called" << std::endl;
    }
};
void printCircle(Circle c) {
    std::cout << "Circle radius: " << c.radius << std::endl;
}
  • 在主函数中调用 printCircle 函数:
int main() {
    Circle c1;
    printCircle(c1);
    return 0;
}
  • 输出为:
Circle default constructor called
Circle copy constructor called
Circle radius: 0
  • 首先 Circle c1; 调用了默认构造函数,然后 printCircle(c1); 以值传递方式传递 c1,调用了拷贝构造函数。
  1. 函数返回对象时
    • 当函数返回一个对象时,构造函数(通常是拷贝构造函数或移动构造函数,C++11 引入移动语义后)会被调用。例如:
class Triangle {
public:
    int side1;
    int side2;
    int side3;
    Triangle() {
        side1 = 0;
        side2 = 0;
        side3 = 0;
        std::cout << "Triangle default constructor called" << std::endl;
    }
    Triangle(const Triangle& other) {
        side1 = other.side1;
        side2 = other.side2;
        side3 = other.side3;
        std::cout << "Triangle copy constructor called" << std::endl;
    }
    Triangle(Triangle&& other) noexcept {
        side1 = other.side1;
        side2 = other.side2;
        side3 = other.side3;
        other.side1 = 0;
        other.side2 = 0;
        other.side3 = 0;
        std::cout << "Triangle move constructor called" << std::endl;
    }
};
Triangle createTriangle() {
    Triangle t;
    t.side1 = 3;
    t.side2 = 4;
    t.side3 = 5;
    return t;
}
  • 在主函数中调用 createTriangle 函数:
int main() {
    Triangle t = createTriangle();
    return 0;
}
  • 输出为:
Triangle default constructor called
Triangle move constructor called
  • createTriangle 函数中 Triangle t; 调用了默认构造函数,return t; 调用了移动构造函数(因为 C++11 的返回值优化(RVO),这里优先调用移动构造函数)。

析构函数的调用时机

  1. 对象生命周期结束时
    • 对于在栈上创建的对象,当对象所在的作用域结束时,析构函数会被自动调用。例如:
class Box {
public:
    int length;
    int width;
    int height;
    Box() {
        length = 0;
        width = 0;
        height = 0;
        std::cout << "Box constructor called" << std::endl;
    }
    ~Box() {
        std::cout << "Box destructor called" << std::endl;
    }
};
  • 在主函数中定义 Box 对象:
int main() {
    {
        Box b;
    }
    std::cout << "After box scope" << std::endl;
    return 0;
}
  • 输出为:
Box constructor called
Box destructor called
After box scope
  • Box b; 定义时,构造函数被调用,当 b 所在的花括号作用域结束时,析构函数被调用。
  1. 动态分配对象被释放时
    • 使用 delete 关键字释放通过 new 分配的对象时,析构函数会被调用。例如:
class Book {
public:
    std::string title;
    Book(const std::string& t) : title(t) {
        std::cout << "Book constructor called for " << title << std::endl;
    }
    ~Book() {
        std::cout << "Book destructor called for " << title << std::endl;
    }
};
  • 在主函数中动态分配和释放 Book 对象:
int main() {
    Book* book = new Book("C++ Primer");
    delete book;
    return 0;
}
  • 输出为:
Book constructor called for C++ Primer
Book destructor called for C++ Primer
  • new Book("C++ Primer") 调用构造函数,delete book 调用析构函数。
  1. 容器中对象移除时
    • 当从容器(如 std::vectorstd::list 等)中移除对象时,析构函数会被调用。例如:
#include <vector>
class Fruit {
public:
    std::string name;
    Fruit(const std::string& n) : name(n) {
        std::cout << "Fruit constructor called for " << name << std::endl;
    }
    ~Fruit() {
        std::cout << "Fruit destructor called for " << name << std::endl;
    }
};
  • 在主函数中使用 std::vector 操作 Fruit 对象:
int main() {
    std::vector<Fruit> fruits;
    fruits.emplace_back("Apple");
    fruits.emplace_back("Banana");
    fruits.erase(fruits.begin());
    return 0;
}
  • 输出为:
Fruit constructor called for Apple
Fruit constructor called for Banana
Fruit destructor called for Apple
  • fruits.emplace_back("Apple")fruits.emplace_back("Banana") 调用构造函数,fruits.erase(fruits.begin()) 移除 Apple 对象,调用其析构函数。

C++ 类构造函数与析构函数的手动调用分析

构造函数手动调用的情况

  1. placement new 中的构造函数手动调用
    • placement new 是一种特殊的 new 语法,它允许在已分配的内存上构造对象。在这种情况下,构造函数是手动调用的。例如:
#include <new>
class Widget {
public:
    int data;
    Widget() {
        data = 0;
        std::cout << "Widget constructor called" << std::endl;
    }
    ~Widget() {
        std::cout << "Widget destructor called" << std::endl;
    }
};
  • 在主函数中使用 placement new:
int main() {
    char buffer[sizeof(Widget)];
    Widget* w = new (buffer) Widget();
    w->~Widget();
    return 0;
}
  • 输出为:
Widget constructor called
Widget destructor called
  • 这里 new (buffer) Widget(); 手动调用了 Widget 的构造函数,在预分配的 buffer 内存上构造 Widget 对象。之后手动调用析构函数 w->~Widget(); 来清理对象。
  1. 继承与委托构造函数中的特殊情况(非严格手动调用,但有手动控制部分)
    • 在继承体系中,派生类构造函数可以调用基类构造函数。例如:
class Animal {
public:
    std::string name;
    Animal(const std::string& n) : name(n) {
        std::cout << "Animal constructor called for " << name << std::endl;
    }
    ~Animal() {
        std::cout << "Animal destructor called for " << name << std::endl;
    }
};
class Dog : public Animal {
public:
    int age;
    Dog(const std::string& n, int a) : Animal(n), age(a) {
        std::cout << "Dog constructor called for " << name << " with age " << age << std::endl;
    }
    ~Dog() {
        std::cout << "Dog destructor called for " << name << " with age " << age << std::endl;
    }
};
  • 在主函数中创建 Dog 对象:
int main() {
    Dog d("Buddy", 3);
    return 0;
}
  • 输出为:
Animal constructor called for Buddy
Dog constructor called for Buddy with age 3
Dog destructor called for Buddy with age 3
Animal destructor called for Buddy
  • 这里 Dog 构造函数 Dog(const std::string& n, int a) : Animal(n), age(a) 手动调用了 Animal 构造函数 Animal(n)。虽然不是直接使用构造函数名调用,但对基类构造函数的调用有一定的手动控制成分。

析构函数手动调用的情况

  1. 配合 placement new 使用
    • 如前面提到的 placement new 的例子,当使用 placement new 在已分配内存上构造对象后,需要手动调用析构函数来清理对象。例如:
#include <new>
class Gadget {
public:
    double value;
    Gadget() {
        value = 0.0;
        std::cout << "Gadget constructor called" << std::endl;
    }
    ~Gadget() {
        std::cout << "Gadget destructor called" << std::endl;
    }
};
  • 在主函数中:
int main() {
    char buf[sizeof(Gadget)];
    Gadget* g = new (buf) Gadget();
    g->~Gadget();
    return 0;
}
  • 输出为:
Gadget constructor called
Gadget destructor called
  • 手动调用析构函数 g->~Gadget(); 清理通过 placement new 构造的 Gadget 对象。
  1. 在对象提前清理场景下
    • 有时候,可能需要在对象生命周期未正常结束时手动调用析构函数。例如,在一个复杂的数据结构中,当需要提前释放某个子对象的资源时。考虑如下场景:
class Node {
public:
    int data;
    Node* next;
    Node(int d) : data(d), next(nullptr) {
        std::cout << "Node constructor called with data " << data << std::endl;
    }
    ~Node() {
        std::cout << "Node destructor called with data " << data << std::endl;
    }
};
class LinkedList {
public:
    Node* head;
    LinkedList() : head(nullptr) {
        std::cout << "LinkedList constructor called" << std::endl;
    }
    void insert(int data) {
        Node* newNode = new Node(data);
        newNode->next = head;
        head = newNode;
    }
    void removeFirst() {
        if (head != nullptr) {
            Node* temp = head;
            head = head->next;
            temp->~Node();
            delete temp;
        }
    }
    ~LinkedList() {
        while (head != nullptr) {
            Node* temp = head;
            head = head->next;
            temp->~Node();
            delete temp;
        }
        std::cout << "LinkedList destructor called" << std::endl;
    }
};
  • 在主函数中操作 LinkedList
int main() {
    LinkedList list;
    list.insert(10);
    list.removeFirst();
    return 0;
}
  • 输出为:
LinkedList constructor called
Node constructor called with data 10
Node destructor called with data 10
LinkedList destructor called
  • removeFirst 函数中,temp->~Node(); 手动调用了 Node 的析构函数,提前清理即将被删除的节点资源。在 LinkedList 的析构函数中,也手动调用了每个节点的析构函数来清理链表中的所有节点。

手动调用构造函数和析构函数的注意事项

  1. 内存管理
    • 当手动调用构造函数(如使用 placement new)时,必须确保所使用的内存已经正确分配且大小合适。如果内存大小不足,构造函数可能会写入未分配的内存,导致未定义行为。同样,在手动调用析构函数后,对于动态分配的内存,需要正确使用 delete 操作符释放内存,否则会导致内存泄漏。例如:
#include <new>
class MyClass {
public:
    int* arr;
    MyClass() {
        arr = new int[10];
        std::cout << "MyClass constructor called" << std::endl;
    }
    ~MyClass() {
        delete[] arr;
        std::cout << "MyClass destructor called" << std::endl;
    }
};
  • 在主函数中使用 placement new 错误示例:
int main() {
    char buf[sizeof(int)];
    MyClass* obj = new (buf) MyClass();
    // 这里 buf 大小不足,构造函数 new int[10] 会导致未定义行为
    obj->~MyClass();
    return 0;
}
  1. 对象状态一致性
    • 手动调用构造函数和析构函数时,要注意保持对象状态的一致性。例如,在手动调用析构函数后,对象应该处于一个合理的可销毁状态。如果在析构函数中只是部分清理资源,而对象后续还可能被访问,会导致错误。例如:
class DatabaseConnection {
public:
    bool isConnected;
    DatabaseConnection() {
        isConnected = true;
        std::cout << "DatabaseConnection constructor called" << std::endl;
    }
    ~DatabaseConnection() {
        isConnected = false;
        std::cout << "DatabaseConnection destructor called" << std::endl;
    }
    void query(const std::string& q) {
        if (isConnected) {
            std::cout << "Executing query: " << q << std::endl;
        } else {
            std::cout << "Not connected, cannot execute query" << std::endl;
        }
    }
};
  • 在主函数中错误使用手动调用析构函数:
int main() {
    DatabaseConnection* conn = new DatabaseConnection();
    conn->query("SELECT * FROM users");
    conn->~DatabaseConnection();
    conn->query("SELECT * FROM products");
    // 这里在调用析构函数后,对象状态已改变,再次调用 query 会导致错误
    delete conn;
    return 0;
}
  • 输出为:
DatabaseConnection constructor called
Executing query: SELECT * FROM users
DatabaseConnection destructor called
Not connected, cannot execute query
  • 这种情况下,在调用析构函数后,对象不应该再被使用 query 方法,否则会产生逻辑错误。
  1. 继承体系中的注意事项
    • 在继承体系中,手动调用构造函数和析构函数时要遵循正确的顺序。当手动调用派生类构造函数时,要确保先调用基类构造函数(如在派生类构造函数初始化列表中调用)。在手动调用析构函数时,析构函数的调用顺序与构造函数相反,先调用派生类析构函数,再调用基类析构函数。例如:
class Base {
public:
    Base() {
        std::cout << "Base constructor called" << std::endl;
    }
    ~Base() {
        std::cout << "Base destructor called" << std::endl;
    }
};
class Derived : public Base {
public:
    Derived() {
        std::cout << "Derived constructor called" << std::endl;
    }
    ~Derived() {
        std::cout << "Derived destructor called" << std::endl;
    }
};
  • 在主函数中手动调用构造函数和析构函数(模拟 placement new 场景):
int main() {
    char buf[sizeof(Derived)];
    Derived* d = new (buf) Derived();
    d->~Derived();
    // 这里实际上也应该调用 Base 的析构函数,虽然在标准 placement new 场景下系统会处理,但手动模拟时要注意
    return 0;
}
  • 输出为:
Base constructor called
Derived constructor called
Derived destructor called
// 这里缺少 Base 析构函数调用输出,手动处理时应补充
  • 在实际代码中,如果手动模拟这种场景,需要确保调用派生类析构函数后,也调用基类析构函数以正确清理对象资源。

通过对 C++ 类构造函数与析构函数调用时机及手动调用的详细分析,我们可以更深入地理解对象的生命周期管理,从而编写出更健壮、高效且内存安全的 C++ 程序。无论是在简单的对象创建与销毁场景,还是复杂的继承体系和内存管理场景下,准确把握构造函数与析构函数的行为都是至关重要的。在实际编程中,要谨慎使用手动调用构造函数和析构函数的方式,遵循内存管理和对象状态维护的原则,以避免出现未定义行为和逻辑错误。同时,利用现代 C++ 的特性,如智能指针等,可以更好地管理对象生命周期,减少手动管理的复杂性和错误风险。例如,在使用智能指针时,智能指针会在其生命周期结束时自动调用所指向对象的析构函数,大大简化了资源管理。

#include <memory>
class Resource {
public:
    Resource() {
        std::cout << "Resource constructor called" << std::endl;
    }
    ~Resource() {
        std::cout << "Resource destructor called" << std::endl;
    }
};
int main() {
    std::unique_ptr<Resource> res = std::make_unique<Resource>();
    // 当 res 离开作用域时,Resource 的析构函数会自动被调用
    return 0;
}
  • 输出为:
Resource constructor called
Resource destructor called
  • 这种方式避免了手动调用析构函数可能出现的错误,提高了代码的可靠性和可维护性。但了解手动调用的原理和场景仍然是必要的,因为在一些底层库开发或特殊需求场景下,可能需要更精细的控制对象的构造和析构过程。