C++在构造函数中使用new时需要注意哪些问题

来源:PHP编程网作者:三上悠亚头衔:网络博主
导读:本期聚焦于三上悠亚创作的《C++在构造函数中使用new时需要注意哪些问题》,敬请观看详情。在C++开发中,构造函数里使用new动态分配内存是常见操作,但如果处理不当很容易引发内存泄漏、野指针、重复释放等问题。很多开发者在编写相关代码时,容易忽略拷贝构造、赋值运算的配套实现,或者没有正确管理内存生命周期。本文将围绕构造函数中使用new的核心注意事项展开,讲解动态内存分配的正确方式、拷贝控制成员的实现要点,以及常见错误的规避方法,帮助开发者写出更健壮的C++代码,避免潜在的内存相关bug。

构造函数中使用 new 的典型场景与责任边界

在 C++ 中,构造函数承担初始化对象成员的职责。当类需要保存一段长度在运行期才能确定的数据时,开发者常常会在构造函数中使用 new 申请堆内存。例如一个自定义字符串类,需要在构造时根据传入的 C 风格字符串长度分配字符数组,并把内容复制进去。这种写法可以让对象在创建完成后立即拥有独立可用的资源,也符合对象生命周期开始即资源可用的设计目标。

一旦构造函数中出现 new,类的设计复杂度就会明显上升。因为对象不仅拥有普通成员变量,还持有了动态资源的所有权。此时必须回答几个问题:对象销毁时谁来释放内存,对象被复制时如何处理指针,对象被赋值时旧资源如何处理,以及申请内存失败时是否会让对象处于不一致状态。这些问题都直接关系到程序是否稳定。

#include <cstring>

class MyString {
private:
    char* data;
    int length;

public:
    // 构造函数中使用 new 分配字符数组
    MyString(const char* str = nullptr) {
        if (str == nullptr) {
            length = 0;
            data = new char[1];
            data[0] = '';
        } else {
            length = static_cast<int>(std::strlen(str));
            data = new char[length + 1];
            std::strcpy(data, str);
        }
    }
};

上面的示例展示了一个最基础的场景。成员指针 data 指向通过 new 分配的字符数组,length 保存有效字符数量。当传入空指针时,类仍然分配一个字符并写入空字符,这样可以让后续逻辑不必反复判断指针是否为空。这种处理方式虽然简单,却已经把动态资源管理的责任交给了类本身。

因此,构造函数中使用 new 并不可怕,可怕的是只写了申请逻辑,却没有同步设计释放、复制、赋值和异常处理策略。否则,程序可能出现内存泄漏、重复释放、悬空指针等难以排查的问题。

释放、复制与赋值:必须补齐的类成员函数

最基础的要求是析构函数。构造函数中通过 newnew[] 获得的内存,必须在对象生命周期结束时释放。对于数组形式分配的资源,要使用 delete[];对于单个对象分配的资源,要使用 delete。两者不能混用,否则会引发未定义行为。忘记释放会导致内存泄漏,而重复释放或错误释放则可能直接导致程序崩溃。

#include <cstring>

class MyString {
private:
    char* data;
    int length;

public:
    MyString(const char* str = nullptr) {
        if (str == nullptr) {
            length = 0;
            data = new char[1];
            data[0] = '';
        } else {
            length = static_cast<int>(std::strlen(str));
            data = new char[length + 1];
            std::strcpy(data, str);
        }
    }

    // 析构函数必须释放构造函数中申请的数组
    ~MyString() {
        delete[] data;
        data = nullptr;
    }
};

仅有析构函数还不够。如果类没有显式定义拷贝构造函数,编译器会生成默认的逐成员拷贝。对于普通数值成员,这种拷贝没有问题;但对于指针成员,默认拷贝只会复制指针本身的值,不会复制指针指向的内容。结果就是两个对象的指针指向同一块堆内存,当两个对象先后销毁时,同一块内存会被释放两次。

#include <cstring>

class BrokenString {
private:
    char* data;

public:
    BrokenString(const char* str) {
        data = new char[std::strlen(str) + 1];
        std::strcpy(data, str);
    }

    ~BrokenString() {
        delete[] data;
    }
};

void shallow_copy_problem() {
    BrokenString first("hello");
    BrokenString second = first; // 隐式拷贝构造只复制指针值
}

为了解决这个问题,必须实现深拷贝语义的拷贝构造函数。所谓深拷贝,就是为新对象重新分配一块足够大的内存,然后把原对象的数据复制过去。这样每个对象都拥有独立资源,互不影响。同样的问题也存在于赋值运算符中。默认赋值同样只是复制指针,而且赋值时当前对象可能已经持有旧资源,如果不先释放旧资源,就会造成内存泄漏。

#include <cstring>

class MyString {
private:
    char* data;
    int length;

public:
    MyString(const char* str = nullptr) {
        if (str == nullptr) {
            length = 0;
            data = new char[1];
            data[0] = '';
        } else {
            length = static_cast<int>(std::strlen(str));
            data = new char[length + 1];
            std::strcpy(data, str);
        }
    }

    // 深拷贝构造函数:为新对象分配独立内存
    MyString(const MyString& other) {
        length = other.length;
        data = new char[length + 1];
        std::strcpy(data, other.data);
    }

    ~MyString() {
        delete[] data;
    }
};

自定义赋值运算符时,需要特别注意自赋值场景。如果对象赋值给自己,直接释放当前资源再去读取另一个对象,就会访问已经释放的内存。常见做法是先判断 this 与参数地址是否相同,如果相同则直接返回。随后释放旧资源,再分配新资源并复制内容。虽然这种写法足够经典,但每一步都需要谨慎处理,才能保证逻辑正确。

#include <cstring>

class MyString {
private:
    char* data;
    int length;

public:
    MyString(const char* str = nullptr) {
        if (str == nullptr) {
            length = 0;
            data = new char[1];
            data[0] = '';
        } else {
            length = static_cast<int>(std::strlen(str));
            data = new char[length + 1];
            std::strcpy(data, str);
        }
    }

    MyString(const MyString& other) {
        length = other.length;
        data = new char[length + 1];
        std::strcpy(data, other.data);
    }

    // 赋值运算符需要处理自赋值、旧资源释放和深拷贝
    MyString& operator=(const MyString& other) {
        if (this == &other) {
            return *this;
        }

        delete[] data;

        length = other.length;
        data = new char[length + 1];
        std::strcpy(data, other.data);

        return *this;
    }

    ~MyString() {
        delete[] data;
    }
};

从资源管理角度看,析构函数、拷贝构造函数和赋值运算符往往是一组整体。只要类中出现了裸指针和 new,就需要认真考虑这几个函数是否应该显式实现。这也是 C++ 资源管理中最经典的规则之一。

异常安全与更稳妥的资源管理方式

除了复制和释放,异常也是构造函数中使用 new 时必须考虑的问题。new 在无法获得足够内存时会抛出 std::bad_alloc 异常。如果构造函数只申请一次资源,失败时对象没有构造完成,通常不会调用析构函数;如果构造函数中连续申请多个资源,前面的资源已经申请成功,后面的资源申请失败,就可能造成已申请资源无法释放。因此,需要在构造函数内部对可能失败的操作进行异常处理,并在重新抛出异常前清理已经获得的资源。

class ResourceHolder {
private:
    int* res1;
    int* res2;

public:
    ResourceHolder() : res1(nullptr), res2(nullptr) {
        res1 = new int(10);

        try {
            res2 = new int(20);
        } catch (...) {
            delete res1;
            res1 = nullptr;
            throw;
        }
    }

    ~ResourceHolder() {
        delete res1;
        delete res2;
    }
};

这种写法的核心目标,是让构造函数在失败路径上也能保持资源安全。即使第二个 new 抛出异常,第一个 new 分配的资源也会被释放,不会遗留在程序中。对于更复杂的类,还可以先初始化指针为空,再逐步申请资源,或者使用局部智能指针临时接管资源,等全部初始化成功后再转移到成员变量中。

如果希望进一步降低手写资源管理的复杂度,可以使用智能指针。std::unique_ptr 可以表达独占资源,离开作用域时自动释放所管理的内存;std::shared_ptr 则适合共享所有权的场景。对于数组资源,可以使用 std::unique_ptr<char[]>,它会在销毁时自动调用数组删除器。这样就不必再手写析构函数,也能避免许多由于复制和赋值带来的问题。

#include <memory>
#include <cstring>

class SafeString {
private:
    std::unique_ptr<char[]> data;
    int length;

public:
    SafeString(const char* str = nullptr) {
        if (str == nullptr) {
            length = 0;
            data.reset(new char[1]);
            data[0] = '';
        } else {
            length = static_cast<int>(std::strlen(str));
            data.reset(new char[length + 1]);
            std::strcpy(data.get(), str);
        }
    }

    // unique_ptr 会在对象销毁时自动释放数组资源
};

当然,智能指针并不是万能替代品。使用 std::unique_ptr 后,类默认不再支持复制;如果确实需要复制,仍然要显式设计拷贝语义。对于字符串、容器这类常见需求,标准库中的 std::stringstd::vector 往往比手写动态数组更安全。但在理解底层机制、维护遗留代码或实现特殊数据结构时,掌握构造函数中使用 new 的注意事项仍然非常必要。

  • 构造函数中申请的资源,必须在析构函数中释放,并且保证 newdeletenew[]delete[] 成对匹配。
  • 包含动态资源的类,应显式实现深拷贝构造函数和赋值运算符,避免默认浅拷贝导致重复释放。
  • 赋值运算符要处理自赋值,并先释放旧资源再复制新资源。
  • 构造函数中多次申请资源时,要考虑异常路径,确保已经申请的资源不会泄漏。
  • 在适合的场景中,优先使用智能指针或标准容器,把资源管理交给 RAII 机制。

总体来看,在构造函数中使用 new 的关键,不是记住某一条语法规则,而是建立完整的资源所有权意识。对象何时获得资源,何时释放资源,复制时是否共享资源,失败时如何回退,这些问题都需要在类设计之初想清楚。只要把这些边界处理完整,动态内存就可以成为可控而强大的工具,而不是程序稳定性的隐患。

C++构造函数new运算符内存泄漏拷贝构造函数赋值运算符修改时间:2026-07-09 19:45:28

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。