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

C++重写拷贝构造函数的常见错误

2024-05-204.3k 阅读

浅拷贝问题

浅拷贝的定义与问题本质

在 C++ 中,默认的拷贝构造函数执行的是浅拷贝。浅拷贝意味着仅仅复制对象中成员变量的值。对于包含指针类型成员变量的类,这会引发严重问题。当两个对象通过浅拷贝共享同一内存地址的指针所指向的数据时,一旦其中一个对象析构,释放了该内存,另一个对象的指针就会变成野指针,导致程序出现未定义行为。

代码示例说明浅拷贝问题

#include <iostream>
#include <cstring>

class String {
public:
    char* str;
    int length;
    // 构造函数
    String(const char* s) {
        length = strlen(s);
        str = new char[length + 1];
        strcpy(str, s);
    }
    // 默认拷贝构造函数(执行浅拷贝)
    String(const String& other) {
        length = other.length;
        str = other.str;
    }
    // 析构函数
    ~String() {
        delete[] str;
    }
};

int main() {
    String s1("Hello");
    String s2(s1);
    std::cout << "s1: " << s1.str << ", s2: " << s2.str << std::endl;
    // s1 析构,释放内存
    return 0;
}

在上述代码中,String 类包含一个 char* 类型的指针 str 用于存储字符串。默认的拷贝构造函数进行浅拷贝,使得 s1s2str 指针指向同一块内存。当 s1 析构时,这块内存被释放,s2str 就变成了野指针。如果后续尝试访问 s2.str,程序就会崩溃。

未正确分配内存

内存分配失败情况

在重写拷贝构造函数时,为新对象的指针成员分配内存是关键步骤。然而,如果内存分配失败,程序可能会以意想不到的方式崩溃,或者导致未定义行为。C++ 中使用 new 运算符分配内存时,如果内存不足,会抛出 std::bad_alloc 异常。

示例代码展示内存分配失败问题

#include <iostream>
#include <cstring>

class BigMemory {
public:
    char* data;
    BigMemory(int size) {
        data = new char[size];
    }
    // 重写拷贝构造函数,可能存在内存分配问题
    BigMemory(const BigMemory& other) {
        data = new char[1000000000000]; // 尝试分配极大内存,很可能失败
        std::memcpy(data, other.data, 1000000000000);
    }
    ~BigMemory() {
        delete[] data;
    }
};

int main() {
    try {
        BigMemory b1(100);
        BigMemory b2(b1);
    } catch (const std::bad_alloc& e) {
        std::cerr << "Memory allocation failed: " << e.what() << std::endl;
    }
    return 0;
}

在这个 BigMemory 类的拷贝构造函数中,尝试分配一个极大的内存块。如果系统无法提供这么多内存,new 运算符会抛出 std::bad_alloc 异常。为了避免程序崩溃,应该在重写拷贝构造函数时,正确处理内存分配失败的情况。

遗漏成员变量拷贝

复杂类结构中的遗漏风险

当类的结构较为复杂,包含多个成员变量时,很容易在重写拷贝构造函数时遗漏某些成员变量的拷贝。这会导致新创建的对象部分数据缺失或处于未初始化状态,从而引发程序错误。

代码示例体现遗漏成员变量拷贝

#include <iostream>

class ComplexClass {
public:
    int a;
    double b;
    char c;
    // 构造函数
    ComplexClass(int _a, double _b, char _c) : a(_a), b(_b), c(_c) {}
    // 重写拷贝构造函数,遗漏了 c 的拷贝
    ComplexClass(const ComplexClass& other) {
        a = other.a;
        b = other.b;
    }
};

int main() {
    ComplexClass c1(10, 3.14, 'A');
    ComplexClass c2(c1);
    std::cout << "c2.a: " << c2.a << ", c2.b: " << c2.b << ", c2.c: " << c2.c << std::endl;
    return 0;
}

ComplexClass 类的重写拷贝构造函数中,遗漏了成员变量 c 的拷贝。因此,c2 对象的 c 成员变量处于未初始化状态,这可能导致程序在后续使用 c2.c 时出现错误。

递归调用拷贝构造函数

递归调用产生的原因

递归调用拷贝构造函数通常发生在拷贝构造函数内部又直接或间接地调用了自身。这可能是由于逻辑错误,例如在构造函数内部试图用正在构造的对象去初始化另一个对象,而这个初始化过程又触发了拷贝构造函数的调用。

示例代码演示递归调用问题

#include <iostream>

class RecursiveClass {
public:
    int value;
    RecursiveClass(int v) : value(v) {}
    // 错误的拷贝构造函数,导致递归调用
    RecursiveClass(const RecursiveClass& other) {
        RecursiveClass temp(other); // 这里会递归调用拷贝构造函数
        value = temp.value;
    }
};

int main() {
    RecursiveClass r1(10);
    try {
        RecursiveClass r2(r1);
    } catch (const std::exception& e) {
        std::cerr << "Exception: " << e.what() << std::endl;
    }
    return 0;
}

RecursiveClass 的拷贝构造函数中,试图通过创建一个临时对象 temp 来进行赋值,但这个临时对象的创建又调用了拷贝构造函数,从而导致无限递归调用,最终耗尽系统资源,程序崩溃。

未处理派生类成员

继承体系下的拷贝构造

在继承体系中,重写派生类的拷贝构造函数时,不仅要处理派生类新增的成员变量,还需要正确调用基类的拷贝构造函数来初始化基类部分的成员变量。如果遗漏这一步,派生类对象的基类部分将处于未初始化状态。

代码示例说明派生类拷贝构造问题

#include <iostream>

class Base {
public:
    int baseValue;
    Base(int v) : baseValue(v) {}
};

class Derived : public Base {
public:
    double derivedValue;
    // 错误的派生类拷贝构造函数,未调用基类拷贝构造
    Derived(const Derived& other) {
        derivedValue = other.derivedValue;
    }
    Derived(int b, double d) : Base(b), derivedValue(d) {}
};

int main() {
    Derived d1(10, 3.14);
    Derived d2(d1);
    std::cout << "d2.baseValue: " << d2.baseValue << ", d2.derivedValue: " << d2.derivedValue << std::endl;
    return 0;
}

Derived 类的拷贝构造函数中,只拷贝了自身新增的 derivedValue,而没有调用基类的拷贝构造函数来初始化 baseValue。这使得 d2 对象的 baseValue 处于未初始化状态,可能导致程序出现错误。正确的做法是在派生类拷贝构造函数的初始化列表中调用基类的拷贝构造函数。

异常安全问题

异常安全的重要性

在重写拷贝构造函数时,异常安全是一个关键问题。如果在拷贝过程中发生异常,对象应该保持在一个有效的状态,并且不会泄漏资源。否则,可能会导致内存泄漏或其他未定义行为。

示例代码展示异常安全问题

#include <iostream>
#include <vector>

class ExceptionUnsafe {
public:
    std::vector<int> data;
    ExceptionUnsafe(int size) {
        data.resize(size);
    }
    // 异常不安全的拷贝构造函数
    ExceptionUnsafe(const ExceptionUnsafe& other) {
        std::vector<int> temp(other.data.size());
        for (size_t i = 0; i < other.data.size(); ++i) {
            temp[i] = other.data[i];
            if (i % 2 == 0) {
                throw std::runtime_error("Simulated exception");
            }
        }
        data = temp;
    }
};

int main() {
    try {
        ExceptionUnsafe e1(10);
        ExceptionUnsafe e2(e1);
    } catch (const std::exception& e) {
        std::cerr << "Exception: " << e.what() << std::endl;
    }
    return 0;
}

在上述 ExceptionUnsafe 类的拷贝构造函数中,假设在填充临时 std::vector temp 时抛出异常。此时,e2 对象的 data 成员变量处于未完全初始化状态,而 temp 中的数据可能已经部分拷贝,这可能导致资源泄漏或对象处于无效状态。为了确保异常安全,可以采用 “拷贝并交换” 等技术。

性能问题

拷贝构造函数中的性能考量

虽然拷贝构造函数的正确性至关重要,但性能也不容忽视。不必要的深拷贝、频繁的内存分配与释放等操作都可能导致性能下降。尤其是在大规模数据或频繁拷贝的场景下,性能问题会更加突出。

代码示例体现性能问题

#include <iostream>
#include <vector>

class PerformanceInefficient {
public:
    std::vector<int> largeData;
    PerformanceInefficient(int size) {
        largeData.resize(size);
        for (int i = 0; i < size; ++i) {
            largeData[i] = i;
        }
    }
    // 性能低效的拷贝构造函数
    PerformanceInefficient(const PerformanceInefficient& other) {
        std::vector<int> temp(other.largeData.size());
        for (size_t i = 0; i < other.largeData.size(); ++i) {
            temp[i] = other.largeData[i];
        }
        largeData = temp;
    }
};

int main() {
    PerformanceInefficient p1(1000000);
    PerformanceInefficient p2(p1);
    return 0;
}

PerformanceInefficient 类的拷贝构造函数中,先创建一个临时 std::vector temp,然后逐个拷贝元素,最后赋值给 largeData。这种方式在数据量较大时会导致性能瓶颈,因为涉及大量的内存分配和元素拷贝。可以通过优化算法,例如在可能的情况下使用移动语义来提高性能。

与其他特殊成员函数的不一致

特殊成员函数的协同

C++ 中的特殊成员函数包括构造函数、析构函数、拷贝构造函数、拷贝赋值运算符和移动构造函数、移动赋值运算符。这些函数之间需要保持一致性。如果重写了拷贝构造函数,但没有正确实现其他相关的特殊成员函数,可能会导致程序出现不一致的行为。

代码示例说明特殊成员函数不一致问题

#include <iostream>
#include <cstring>

class InconsistentMembers {
public:
    char* str;
    int length;
    InconsistentMembers(const char* s) {
        length = strlen(s);
        str = new char[length + 1];
        strcpy(str, s);
    }
    // 重写拷贝构造函数
    InconsistentMembers(const InconsistentMembers& other) {
        length = other.length;
        str = new char[length + 1];
        strcpy(str, other.str);
    }
    // 未正确实现拷贝赋值运算符
    InconsistentMembers& operator=(const InconsistentMembers& other) {
        if (this == &other) {
            return *this;
        }
        // 这里没有释放原有的 str 内存
        length = other.length;
        str = new char[length + 1];
        strcpy(str, other.str);
        return *this;
    }
    ~InconsistentMembers() {
        delete[] str;
    }
};

int main() {
    InconsistentMembers i1("Hello");
    InconsistentMembers i2("World");
    i2 = i1;
    return 0;
}

InconsistentMembers 类中,重写了拷贝构造函数,但在拷贝赋值运算符中,没有释放对象原有的 str 内存,导致内存泄漏。当特殊成员函数之间不一致时,程序的行为将变得不可预测,因此在重写拷贝构造函数时,需要同时考虑其他相关特殊成员函数的正确实现。