C++学习:六个月从基础到就业——面向对象编程:构造函数与析构函数
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;
};
初始化列表的优势:
-
性能优势: 对于类类型的成员,初始化列表直接调用其构造函数,而不是先默认构造再赋值,避免了不必要的操作。
-
常量和引用成员: 常量成员和引用成员必须在初始化列表中初始化,因为它们不能在构造函数体中赋值。
-
初始化顺序清晰: 使代码更清晰地表达初始化意图。
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); // 也调用复制构造函数
何时调用复制构造函数:
- 用一个对象初始化同类型的另一个对象
- 函数按值传递对象
- 函数按值返回对象
- 编译器生成临时对象
如果您没有定义复制构造函数,编译器会生成一个默认的复制构造函数,执行成员的逐一复制(浅复制)。对于包含动态分配内存或资源的类,这通常是不够的,需要自定义复制构造函数。
禁用复制: 如果您不希望对象被复制,可以将复制构造函数声明为私有或使用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;
};
析构函数的调用时机
析构函数在以下情况下被调用:
- 对象离开作用域时
- 动态分配的对象被删除时
- 临时对象的生命周期结束时
- 程序结束,全局或静态对象被销毁时
void example() {Resource localRes("Local"); // 构造localResResource* 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++中有六个特殊成员函数,在一定条件下会由编译器自动生成:
- 默认构造函数
- 析构函数
- 复制构造函数
- 复制赋值运算符
- 移动构造函数(C++11)
- 移动赋值运算符(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;
}
这个例子展示了如何使用构造函数和析构函数自动管理资源(数据库连接),确保资源在使用后被正确释放。
最佳实践
-
遵循RAII原则:使用构造函数获取资源,使用析构函数释放资源,避免资源泄漏。
-
优先使用初始化列表:在构造函数中使用初始化列表初始化成员变量,而不是在构造函数体内赋值,这更高效且能正确初始化常量和引用成员。
-
为管理资源的类实现"五大函数":如果类管理资源(如动态内存),确保正确实现复制构造函数、复制赋值运算符、移动构造函数、移动赋值运算符和析构函数,或者禁用不需要的操作。
-
使析构函数为虚函数:如果类将被用作基类,确保析构函数是虚函数,以确保通过基类指针删除派生类对象时能正确调用派生类的析构函数。
-
构造函数应该确保对象初始化为有效状态:如果构造过程中发生错误,应该抛出异常,而不是创建无效状态的对象。
-
析构函数不应该抛出异常:如果析构函数可能抛出异常,应该将可能抛出异常的代码包装在try-catch块中,并在捕获异常后处理或记录它。
-
防止异常逃逸构造函数:如果构造函数执行可能抛出异常的操作,应该捕获这些异常,确保释放任何已分配的资源,然后重新抛出异常。
-
使用委托构造函数避免代码重复:如果有多个功能相似的构造函数,使用委托构造函数减少代码重复。
-
优先使用
= delete
而非私有但未定义:要禁止特定操作,使用= delete
语法明确表明意图,而不是将函数声明为私有但不定义。 -
优先使用智能指针:现代C++中,优先使用
std::unique_ptr
和std::shared_ptr
等智能指针管理动态内存,而不是原始指针和手动内存管理。 -
避免在构造函数中调用虚函数:在构造函数中,派生类对象尚未完全构造,调用虚函数不会按预期调用派生类的版本。
-
记住"法则五"(Rule of Five)和"法则零"(Rule of Zero):
- “法则五”:如果需要定义任何一个特殊成员函数(析构函数、复制构造函数、复制赋值运算符、移动构造函数、移动赋值运算符),通常需要定义所有五个。
- “法则零”:如果可能,设计类使其不需要定义任何特殊成员函数,而是依赖编译器生成的默认版本。
总结
构造函数和析构函数是C++面向对象编程中的核心概念,它们管理着对象的生命周期——从创建到销毁。正确使用这些特殊成员函数对于编写健壮、无泄漏的C++程序至关重要。
在本文中,我们详细讨论了各种类型的构造函数,包括默认构造函数、带参数的构造函数、复制构造函数、移动构造函数和转换构造函数。我们还研究了析构函数的特性和用途,特别是在资源管理和继承层次结构中的作用。
C++11引入的移动语义和委托构造函数等新特性进一步增强了构造函数的功能,使我们能够编写更高效、更清晰的代码。同时,RAII模式与构造函数和析构函数紧密结合,成为C++资源管理的基石。
理解和掌握构造函数和析构函数是成为一名熟练的C++程序员的关键一步。通过遵循最佳实践,我们可以创建安全、高效、易于维护的C++代码。
在下一篇文章中,我们将深入探讨C++的访问控制与友元,这是封装和信息隐藏的重要机制。
参考资料
- Bjarne Stroustrup. The C++ Programming Language (4th Edition)
- Scott Meyers. Effective Modern C++
- cppreference.com - 构造函数
- cppreference.com - 析构函数
- C++ Core Guidelines - 构造、赋值和析构函数
- Herb Sutter. Exceptional C++: 47 Engineering Puzzles, Programming Problems, and Solutions
这是我C++学习之旅系列的第十篇技术文章。查看完整系列目录了解更多内容。
相关文章:
C++学习:六个月从基础到就业——面向对象编程:构造函数与析构函数
C学习:六个月从基础到就业——面向对象编程:构造函数与析构函数 本文是我C学习之旅系列的第十篇技术文章,主要讨论C中构造函数与析构函数的概念、特点和使用技巧。这些是C对象生命周期管理的关键组成部分。查看完整系列目录了解更多内容。 引…...
dfs二叉树中的深搜(回溯、剪枝)--力扣129、814、230、257
目录 1.1题目链接:129.求根节点到叶结点数字之和 1.2题目描述:给你一个二叉树的根节点 root ,树中每个节点都存放有一个 0 到 9 之间的数字。 1.3解法(dfs-前序遍历): 2.1题目链接:814.二叉树剪枝 2.2题目描述&…...
Python Selenium 一小时速通教程
Python Selenium 一小时速通教程 实战案例 一、环境配置(10分钟) 安装Python 确保已安装Python 3.x(官网下载)。 安装Selenium 在终端运行: pip install selenium下载浏览器驱动 Chrome:访问 ChromeDriv…...
通过GO后端项目实践理解DDD架构
最近在工作过程中重构的项目要求使用DDD架构,在网上查询资料发现教程五花八门,并且大部分内容都是长篇的概念讲解,晦涩难懂,笔者看了一些github上入门的使用DDD的GO项目,并结合自己开发中的经验,谈谈自己对…...
MybatisPlus最新版分页无法使用
在使用分页的时候发现分页拦截器关键API会报错,其实根本原因是在之前只需要导入一个mybatisplus依赖,而现在分页似乎被单独分离出来了,需要额外导入新依赖使其支持 <dependency><groupId>com.baomidou</groupId><art…...
【Android学习记录】工具使用
文章目录 一. 精准找视图资源ID1. 准备工作2. 使用 uiautomator 工具2.1. 获取设备的窗口内容2.2. Pull XML 文件2.3. 查看 XML 文件 3. 直接使用 ADB 命令4. 使用 Android Studio 的 Layout Inspector总结 二. adb shell dumpsys activity1. 如何使用 ADB 命令2. 输出内容解析…...
youtube视频和telegram视频加载原理差异分析
1. 客户侧缓存与流式播放机制 流式视频应用(如 Netflix、YouTube)通过边下载边播放实现流畅体验,其核心依赖以下技术: 缓存预加载:客户端在后台持续下载视频片段(如 DASH/HLS 协议的…...
在机器视觉检测中为何选择线阵工业相机?
线阵工业相机,顾名思义是成像传感器呈“线”状的。虽然也是二维图像,但极宽,几千个像素的宽度,而高度却只有几个像素的而已。一般在两种情况下使用这种相机: 1. 被测视野为细长的带状,多用于滚筒上检测的问…...
lwip记录
Index of /releases/lwip/ (gnu.org) 以太网(Ethernet)是互联网技术的一种,由于它是在组网技术中占的比例最高,很多人 直接把以太网理解为互联网。 以太网是指遵守 IEEE 802.3 标准组成的局域网,由 IEEE 802.3 标准规定的主要是位于 参考模…...
Redis清空缓存
尽管redis可以设置ttl过期时间进行指定key的定时删除,但是在某些场景下,比如: 测试时需要批量删除指定库下所有库下所有的数据,则会涉及到缓存清除的话题。 如下为具体的操作及说明: 场景类型操作指令清空当前库下所有…...
WPF 依赖注入启动的问题
原因是在App.xaml 设置了 StartupUri“MainWindow.xaml” 1.依赖注入后启动的主窗体存在无参构造 程序正常启动,但是主窗体界面会弹出2个窗体。 2.依赖注入后启动的主窗体存在有参构造 报错...
Arcgis经纬线标注设置(英文、刻度显示)
在arcgis软件中绘制地图边框,添加经纬度度时常常面临经纬度出现中文,如下图所示: 解决方法,设置一下Arcgis的语言 点击高级--确认 这样Arcgis就转为英文版了,此时在来看经纬线刻度的标注,自动变成英文...
【电子通识】案例:电缆的安装方式也会影响设备的可靠性?
背景 在日常生活中,我们常常会忽略一些看似微不足道的细节,但这些细节有时却能决定设备的寿命和安全性。比如,你知道吗?一根电缆的布置方式,可能会决定你的设备是否会因为冷凝水而损坏。 今天,我们就来聊聊…...
房屋装修费用预算表:45594 =未付14509 + 付清31085【时间:20250416】
文章目录 引言I 房屋装修费用预算表II 市场价参考防水搬运3000III 装修计划整体流程进度细节国补IV 付款凭证(销售单)伟星 PPR +PVC+太阳线+地漏=6500入户门设计通铺大板瓷砖 | 湿贴 3408(地)+3600(加)+5209(墙)=12217元门头铁空调引言 关注我,发送【装修记账】获取预…...
Python文件操作完全指南:从基础到高级应用
目录 一、文件基础概念 1.1 什么是文件? 1.2 文件的存储方式 文本文件 二进制文件 二、Python文件操作基础 2.1 文件操作三步曲 2.2 核心函数与方法 2.3 文件读取详解 基本读取示例 文件指针机制 2.4 文件打开模式 写入文件示例 2.5 高效读取大文件 三…...
03(总)-docker篇 Dockerfile镜像制作(jdk,jar)与jar包制作成docker容器方式
全文目录,一步到位 1.前言简介1.1 专栏传送门1.1.2 上文传送门 2. docker镜像制作一: jdk2.1 制作jdk镜像2.1.1 准备工作2.1.2 jdk镜像的Dockerfile2.1.3 基于Dockerfile构建镜像2.1.4 docker使用镜像运行容器2.1.5 进入jdk1.8容器内测试 3. docker镜像制作二: java镜像(jar包)…...
CUDA的安装
打开nvidia控制面板 找到组件 打开 CUDA Toolkit Archive | NVIDIA Developer 下载CUDA...
四六级听力调频广播有线传输无线覆盖系统:弥补单一发射系统安全缺陷,构建稳定可靠听力系统平台
四六级听力调频广播有线传输无线覆盖系统:弥补单一发射系统安全缺陷,构建稳定可靠听力系统平台 北京海特伟业科技有限公司任洪卓发布于2025年4月16日 随着英语四六级考试的规模不断扩大,听力考试部分的设备可靠性问题日益凸显。传统的无线发射系统存在…...
信创服务器-大国崛起,信创当道!
信创产业是数据安全、网络安全的基础,也是新基建的重要组成部分。在政策的推动下,2020-2022 年,中国信创服务器出货量整体呈现出快速增长的趋势,其中党政、电信、金融等领域采购频次高,单次采购量大,是中国…...
【仿Mudou库one thread per loop式并发服务器实现】SERVER服务器模块实现
SERVER服务器模块实现 1. Buffer模块2. Socket模块3. Channel模块4. Poller模块5. EventLoop模块5.1 TimerQueue模块5.2 TimeWheel整合到EventLoop5.1 EventLoop与线程结合5.2 EventLoop线程池 6. Connection模块7. Acceptor模块8. TcpServer模块 1. Buffer模块 Buffer模块&…...
冒泡与 qsort 排序策略集
今天我们要学习两种排序方法,分别是冒泡排序和qsort函数排序,冒泡排序相对qsort函数排序要简单一点,更易于理解。 1.冒泡排序 冒泡排序(Bubble Sort)是一种简单的排序算法,它通过重复遍历元素列并比较相邻元素来实现排…...
【Linux】第七章 控制对文件的访问
目录 1. 什么是文件系统权限?它是如何工作的?如何查看文件的权限? 2. 解释‘-rw-r--r--’这个字符串。 3. 使用什么命令可以更改文件和目录的权限?写出分别使用符号法和数值法将权限从 754 修改为 775 的命令。 4. 如何修改文…...
网站301搬家后谷歌一直不收录新页面怎么办?
当网站因更换域名或架构调整启用301重定向后,许多站长发现谷歌迟迟不收录新页面,甚至流量大幅下滑。 例如,301跳转设置错误可能导致权重传递失效,而新站内容与原站高度重复则可能被谷歌判定为“低价值页面”。 即使技术层面无误&a…...
socket 客户端和服务器通信
服务器 using BarrageGrab; using System; using System.Collections.Concurrent; using System.Linq; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading;namespace Lyx {class Server{private TcpListener listener;private Concurre…...
C实现md5功能
md5在线验证: 在线MD5计算_ip33.com 代码如下: #include "md5.h" #include <string.h> #include "stdio.h"/** 32-bit integer manipulation macros (little endian)*/ #ifndef GET_ULONG_LE #define GET_ULONG_LE(n,b,i) …...
【项目】CherrySudio配置MCP服务器
CherrySudio配置MCP服务器 (一)Cherry Studio介绍(二)MCP服务环境搭建(1)环境准备(2)依赖组件安装<1> Bun和UV安装 (3)MCP服务器使用<1> 搜索MCP…...
第五节:React Hooks进阶篇-如何用useMemo/useCallback优化性能
反模式:滥用导致的内存开销React 19编译器自动Memoization原理 React Hooks 性能优化进阶:从手动到自动 Memoization (基于 React 18 及以下版本,结合 React 19 新特性分析) 一、useMemo/useCallback 的正确使用场景…...
【Qt】QWidget 核⼼属性详解
🍑个人主页:Jupiter. 🚀 所属专栏:QT 欢迎大家点赞收藏评论😊 目录 🏝 一.相关概念🎨二. 核⼼属性概览🍄2.1 enabled🥭2.2geometry🌸 2.3 windowTitle&#…...
如何知道raid 有问题了
在 Rocky Linux 8 上,你的服务器使用了 RAID5(根据 lsblk 输出,/dev/sda3、/dev/sdb1 和 /dev/sdc1 组成 md127 RAID5 阵列)。为了监控 RAID5 阵列中磁盘的健康状态,并及时发现某块磁盘损坏,可以通过以下方…...
操作系统之shell实现(上)
🌟 各位看官好,我是maomi_9526! 🌍 种一棵树最好是十年前,其次是现在! 🚀 今天来学习C语言的相关知识。 👍 如果觉得这篇文章有帮助,欢迎您一键三连,分享给更…...
精益数据分析(3/126):用数据驱动企业发展的深度解析
精益数据分析(3/126):用数据驱动企业发展的深度解析 大家好!一直以来,我都坚信在当今竞争激烈的商业环境中,数据是企业获得竞争优势的关键。最近深入研究《精益数据分析》这本书,收获颇丰&…...
React 18/19 使用Ant Design全局弹窗message
react 18 及以上,拥有并发模式,不允许在渲染过程中直接触发副作用(如弹窗、网络请求等),应将其放至 useEffect 中,确保其在渲染完成后调用 useEffect(() > {message.success(操作成功!);}, …...
【spark3.2.4】--完全分布式集群搭建
一、spark-env.sh 文件配置(操作路径:$SPARK_HOME/conf/spark-env.sh) 如果还没创建: cp $SPARK_HOME/conf/spark-env.sh.template $SPARK_HOME/conf/spark-env.sh然后编辑(比如用 vim): vim…...
Web3技术下数字资产数据保护的实践探索
在这个信息爆炸的时代,数字资产已经成为我们生活中不可或缺的一部分。随着Web3技术的兴起,它以其去中心化、透明性和安全性的特点,为数字资产的管理和保护提供了新的解决方案。本文将探讨Web3技术在数字资产数据保护方面的实践探索࿰…...
灰度共生矩阵(GLCM)简介
灰度共生矩阵(GLCM)简介 1. 基本概念 灰度共生矩阵(Gray-level Co-occurrence Matrix, GLCM)是一种用于分析图像纹理特征的统计方法。它通过计算图像中特定空间关系的像素对出现的频率,来描述纹理的规律性1。 核心思想:统计图像中相距为d、方向为θ的两个像素点,分别具…...
基于javaEE+jqueryEasyUi+eclipseLink+MySQL的课程设计客房管理信息系统
1. 系统概述 1.1 系统功能概述 1)客户管理。能够增加一个客户,包括:身份证号、客户名称、出生年月、性别、联系电话、邮箱、会员类别等信息,默认会员类别为空;能够修改和删除客户信息;能够根据客户名称、联系电话查询…...
3款本周高潜力开源AI工具(多模态集成_隐私本地化)
本周聚焦 AI 技术领域,为开发者精选 3 款兼具创新性与实用性的开源项目。这些项目覆盖图像生成、智能助手、大语言模型框架等方向,通过技术突破解决开发痛点,助力开发者高效构建智能应用。 更多精彩科技推荐请点击->:更多精彩科…...
第一期第10讲
Linux常用的压缩文件扩展名有 .tar, .tar.bz2, .tar.gz 使用gzip压缩和解压缩 对单个文件压缩: gzip a.c //压缩a.c为a.c.gz gzip -d a.c.gz //解压缩为a.c 对文件夹压缩: gzip -r test //对test文件夹里的文件进行压缩,不对test进行压缩…...
计算方法在单细胞数据分析中的应用及AI拓展
单细胞技术的出现彻底革新了我们对生物系统的理解,揭示了看似同质的细胞群体内部复杂的异质性。为了从这些技术产生的大量复杂数据中提取有意义的见解,精密的计算方法是不可或缺的。 AI拓展 单细胞数据分析的核心在于处理和解释高维度数据的能力&#…...
如何配置环境变量HADOOP_HOMEM、AVEN_HOME?不配置会怎么样
以下是在不同操作系统中配置 HADOOP_HOME 和 JAVA_HOME 环境变量的方法,以及不配置可能产生的后果: 配置 HADOOP_HOME - Windows系统:下载并解压Hadoop安装包,然后右键“此电脑”,选择“属性”,点击“高级…...
【现代深度学习技术】循环神经网络03:语言模型和数据集
【作者主页】Francek Chen 【专栏介绍】 ⌈ ⌈ ⌈PyTorch深度学习 ⌋ ⌋ ⌋ 深度学习 (DL, Deep Learning) 特指基于深层神经网络模型和方法的机器学习。它是在统计机器学习、人工神经网络等算法模型基础上,结合当代大数据和大算力的发展而发展出来的。深度学习最重…...
【学习笔记】Taming 3DGS泛读
原文链接:https://arxiv.org/abs/2406.15643 代码链接:https://github.com/nullptr81/3dgs-accel 一、学习内容 1.研究背景 3DGS在新视角合成(NVS)中表现优异,但优化过程低效: 存在 1)资源需…...
SAP系统交货已完成标识
问题:交货已完成标识 现象:采购订单一直处于未完成交货状态,及交货完成标识处于非勾选状态 原因:采购订单交货完成标识勾会在两种情况下勾选, a.交货数量在容差范围内; b.手动勾选。 某些特殊情况…...
【正点原子STM32MP257连载】第四章 ATK-DLMP257B功能测试——音频测试 #ES8388 #录音测试
1)实验平台:正点原子ATK-DLMP257B开发板 2)浏览产品:https://www.alientek.com/Product_Details/135.html 3)全套实验源码手册视频下载:正点原子资料下载中心 文章目录 第四章 ATK-DLMP257B功能测试——音频…...
WPF 使用 DI EF CORE SQLITE
WPF 使用 DI EF CORE SQLITE 1.安装 nuget包 <PackageReference Include"Microsoft.EntityFrameworkCore.Sqlite" Version"9.0.4" />2.创建DbContext的实现类,创建有参构造函数 public XXContext(DbContextOptions<XXXContext> o…...
探索鸿蒙沉浸式:打造无界交互体验
一、鸿蒙沉浸式简介 在鸿蒙系统中,沉浸式是一种极具特色的设计理念,它致力于让用户在使用应用时能够全身心投入到内容本身,而尽可能减少被系统界面元素的干扰。通常来说,就是将应用的内容区巧妙地延伸到状态栏和导航栏所在的界面…...
Linux红帽:RHCSA认证知识讲解(十 三)在serverb上破解root密码
Linux红帽:RHCSA认证知识讲解(十 三)在serverb上破解root密码 前言操作步骤 前言 在红帽 Linux 系统的管理工作中,系统管理员可能会遇到需要重置 root 密码的情况。本文将详细介绍如何通过救援模式进入系统并重新设置 root 密码。…...
【网络安全】谁入侵了我的调制解调器?(一)
文章目录 我被黑了159.65.76.209,你是谁?黑客攻击黑客?交出证据三年后我被黑了 两年前,在我家里使用家庭网络远程办公时,遇到了一件非常诡异的事情。当时,我正在利用一个“盲 XXE 漏洞”,这个漏洞需要借助一个外部 HTTP 服务器来“走私”文件。为了实现这一点,我在 AW…...
阿里一面:Nacos配置中心交互模型是 push 还是 pull ?(原理+源码分析)
对于Nacos大家应该都不太陌生,出身阿里名声在外,能做动态服务发现、配置管理,非常好用的一个工具。然而这样的技术用的人越多面试被问的概率也就越大,如果只停留在使用层面,那面试可能要吃大亏。 比如我们今天要讨论的…...
MySQL 慢查询日志深入分析与工具实战(mysqldumpslow pt-query-digest)
🎯 学习目标 • ✅ 熟悉慢查询日志结构与核心字段 • ✅ 掌握日志开启与 SQL 记录机制 • ✅ 使用 pt-query-digest 工具进行分析 • ✅ 解读分析结果并提出优化建议 📂 基本概念 项目 内容说明 功能 记录执行时间超过阈值的 SQL 启动参数…...