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

C++编译器为类自动生成的四个缺省函数详解

2023-05-085.8k 阅读

C++编译器为类自动生成的四个缺省函数概述

在C++中,当我们定义一个类时,如果没有显式地定义某些特殊成员函数,编译器会自动为我们生成四个缺省函数。这四个函数分别是默认构造函数、析构函数、拷贝构造函数和拷贝赋值运算符重载函数。理解这四个缺省函数的行为和作用,对于编写高效、正确的C++代码至关重要。

默认构造函数

  1. 概念与作用:默认构造函数是一种特殊的构造函数,它不需要任何参数。其主要作用是在创建对象时对对象进行初始化。当我们使用类名来定义一个对象,而没有提供任何初始化值时,编译器就会调用默认构造函数。例如:
class MyClass {
    // 这里没有显式定义构造函数
    int data;
};

int main() {
    MyClass obj; // 调用默认构造函数
    return 0;
}

在上述代码中,MyClass类没有显式定义构造函数,当在main函数中定义obj对象时,编译器会自动生成一个默认构造函数来初始化obj

  1. 编译器生成的默认构造函数行为:编译器生成的默认构造函数会对类的数据成员进行默认初始化。对于基本数据类型(如intdouble等),默认初始化并不会赋予它们一个特定的值,它们的值是未定义的。对于类类型的数据成员,会调用其对应的默认构造函数进行初始化。例如:
class InnerClass {
public:
    InnerClass() {
        std::cout << "InnerClass default constructor" << std::endl;
    }
};

class OuterClass {
    InnerClass inner;
    int num;
};

int main() {
    OuterClass outer;
    return 0;
}

在这段代码中,OuterClass没有显式定义构造函数。当创建outer对象时,编译器生成的默认构造函数首先会调用InnerClass的默认构造函数来初始化inner成员,而对于num成员,由于是基本数据类型int,其值是未定义的。输出结果为:

InnerClass default constructor
  1. 何时需要显式定义默认构造函数:当我们需要对类的数据成员进行特定的初始化时,就需要显式定义默认构造函数。例如,如果我们希望MyClass类的data成员初始值为0,可以这样定义:
class MyClass {
    int data;
public:
    MyClass() : data(0) {
        // 显式初始化data为0
    }
};

析构函数

  1. 概念与作用:析构函数与构造函数相对,它在对象生命周期结束时被调用,主要用于释放对象在生命周期内分配的资源。例如,当对象动态分配了内存(使用new运算符),就需要在析构函数中使用delete运算符来释放这些内存,以避免内存泄漏。析构函数的名称与类名相同,但前面加上~符号。例如:
class Resource {
    int* ptr;
public:
    Resource() {
        ptr = new int(5);
    }
    ~Resource() {
        delete ptr;
    }
};

在上述代码中,Resource类在构造函数中动态分配了一个int类型的内存空间,并在析构函数中释放了该内存。

  1. 编译器生成的析构函数行为:编译器生成的析构函数是一个空函数。对于大多数简单类,编译器生成的析构函数就足够了,因为它们没有需要手动释放的资源。但是,对于那些在构造函数中分配了资源(如动态内存、文件句柄、网络连接等)的类,就必须显式定义析构函数来释放这些资源。例如,如果我们没有为Resource类定义析构函数,当Resource对象生命周期结束时,ptr所指向的内存就不会被释放,从而导致内存泄漏。

  2. 析构函数调用时机:析构函数在以下几种情况下会被调用:

    • 当对象离开其作用域时,例如在函数内部定义的局部对象,当函数结束时,该对象的析构函数会被调用。
    • 当使用delete运算符释放通过new运算符创建的动态对象时,该对象的析构函数会被调用。
    • 当包含对象的数组被销毁时,数组中每个对象的析构函数都会被调用。例如:
class Obj {
public:
    ~Obj() {
        std::cout << "Obj destructor" << std::endl;
    }
};

int main() {
    {
        Obj localObj; // 局部对象,离开这个作用域时调用析构函数
    }
    Obj* dynamicObj = new Obj();
    delete dynamicObj; // 释放动态对象时调用析构函数
    Obj arr[3]; // 数组对象,数组销毁时每个对象调用析构函数
    return 0;
}

上述代码的输出结果为:

Obj destructor
Obj destructor
Obj destructor
Obj destructor

拷贝构造函数

  1. 概念与作用:拷贝构造函数用于创建一个新对象,该新对象是另一个已有对象的副本。其参数是一个与该类类型相同的对象的引用。拷贝构造函数在以下几种情况下会被调用:
    • 当使用一个已有的对象来初始化一个新对象时,例如:ClassType newObj(oldObj);
    • 当函数按值传递对象时,实参对象会被拷贝构造出一个副本传递给函数形参。
    • 当函数返回一个对象时,会通过拷贝构造函数创建一个临时对象返回给调用者。例如:
class CopyClass {
    int value;
public:
    CopyClass(int v) : value(v) {}
    CopyClass(const CopyClass& other) : value(other.value) {
        std::cout << "Copy constructor called" << std::endl;
    }
};

CopyClass createObject() {
    CopyClass temp(10);
    return temp;
}

int main() {
    CopyClass obj1(5);
    CopyClass obj2(obj1); // 调用拷贝构造函数
    CopyClass obj3 = createObject(); // 调用拷贝构造函数
    return 0;
}

在上述代码中,obj2(obj1)使用obj1初始化obj2,调用了拷贝构造函数。在createObject函数返回temp对象时,也调用了拷贝构造函数创建临时对象返回给obj3。输出结果为:

Copy constructor called
Copy constructor called
  1. 编译器生成的拷贝构造函数行为:编译器生成的拷贝构造函数会执行成员逐一拷贝(member - wise copy),也称为浅拷贝。对于基本数据类型成员,它会直接复制其值。对于类类型成员,会调用其对应的拷贝构造函数。例如:
class Inner {
    int num;
public:
    Inner(int n) : num(n) {}
};

class Outer {
    Inner inner;
    int value;
public:
    Outer(int v, int n) : inner(n), value(v) {}
};

int main() {
    Outer outer1(10, 20);
    Outer outer2(outer1); // 调用编译器生成的拷贝构造函数
    return 0;
}

在上述代码中,Outer类没有显式定义拷贝构造函数。当outer2(outer1)时,编译器生成的拷贝构造函数会对outer1inner成员调用Inner类的拷贝构造函数(由于Inner类也没有显式定义,会调用编译器生成的Inner类的拷贝构造函数进行成员逐一拷贝),并直接复制value成员的值。

  1. 浅拷贝的问题与深拷贝的需求:浅拷贝在处理包含动态分配资源的类时会出现问题。例如:
class String {
    char* str;
public:
    String(const char* s) {
        str = new char[strlen(s) + 1];
        strcpy(str, s);
    }
    ~String() {
        delete[] str;
    }
};

int main() {
    String s1("Hello");
    String s2(s1); // 调用编译器生成的浅拷贝构造函数
    return 0;
}

在上述代码中,String类动态分配了内存来存储字符串。编译器生成的浅拷贝构造函数只是简单地复制了str指针的值,导致 s1s2str指针指向同一块内存。当 s1s2对象先后被销毁时,会对同一块内存调用两次delete[],这会导致程序崩溃。为了解决这个问题,我们需要定义深拷贝构造函数,即重新分配内存并复制字符串内容:

class String {
    char* str;
public:
    String(const char* s) {
        str = new char[strlen(s) + 1];
        strcpy(str, s);
    }
    String(const String& other) {
        str = new char[strlen(other.str) + 1];
        strcpy(str, other.str);
    }
    ~String() {
        delete[] str;
    }
};

拷贝赋值运算符重载函数

  1. 概念与作用:拷贝赋值运算符重载函数用于将一个已有的对象赋值给另一个同类型的对象。其函数名是operator =,参数是一个与该类类型相同的对象的引用。例如:
class AssignClass {
    int value;
public:
    AssignClass(int v) : value(v) {}
    AssignClass& operator=(const AssignClass& other) {
        if (this != &other) {
            value = other.value;
        }
        return *this;
    }
};

int main() {
    AssignClass obj1(5);
    AssignClass obj2(10);
    obj2 = obj1; // 调用拷贝赋值运算符重载函数
    return 0;
}

在上述代码中,obj2 = obj1调用了AssignClass类的拷贝赋值运算符重载函数,将obj1的值赋给obj2

  1. 编译器生成的拷贝赋值运算符重载函数行为:编译器生成的拷贝赋值运算符重载函数同样执行成员逐一拷贝(浅拷贝)。对于基本数据类型成员,直接赋值其值。对于类类型成员,调用其对应的拷贝赋值运算符进行赋值。例如:
class InnerAssign {
    int num;
public:
    InnerAssign(int n) : num(n) {}
};

class OuterAssign {
    InnerAssign inner;
    int value;
public:
    OuterAssign(int v, int n) : inner(n), value(v) {}
};

int main() {
    OuterAssign outer1(10, 20);
    OuterAssign outer2(30, 40);
    outer2 = outer1; // 调用编译器生成的拷贝赋值运算符重载函数
    return 0;
}

在上述代码中,outer2 = outer1调用了编译器生成的拷贝赋值运算符重载函数,对outer1inner成员调用InnerAssign类的拷贝赋值运算符(由于InnerAssign类没有显式定义,会调用编译器生成的InnerAssign类的拷贝赋值运算符进行成员逐一拷贝),并直接赋值value成员的值。

  1. 处理自赋值与资源管理:在实现拷贝赋值运算符重载函数时,需要考虑自赋值的情况,即obj = obj这种情况。同时,对于包含动态分配资源的类,需要进行深拷贝以避免资源泄漏。例如:
class DynamicString {
    char* str;
public:
    DynamicString(const char* s) {
        str = new char[strlen(s) + 1];
        strcpy(str, s);
    }
    DynamicString& operator=(const DynamicString& other) {
        if (this != &other) {
            delete[] str;
            str = new char[strlen(other.str) + 1];
            strcpy(str, other.str);
        }
        return *this;
    }
    ~DynamicString() {
        delete[] str;
    }
};

在上述代码中,DynamicString类的拷贝赋值运算符重载函数首先检查是否为自赋值。如果不是自赋值,先释放当前对象的str所指向的内存,然后重新分配内存并复制other对象的字符串内容。

总结四个缺省函数的要点

  1. 默认构造函数:用于对象的初始化,编译器生成的默认构造函数对基本数据类型成员不进行有意义的初始化,对类类型成员调用其默认构造函数。当需要特定初始化时需显式定义。
  2. 析构函数:用于释放对象生命周期内分配的资源,编译器生成的析构函数为空,对于有资源分配的类必须显式定义。
  3. 拷贝构造函数:用于创建对象副本,编译器生成的拷贝构造函数执行浅拷贝,对于包含动态分配资源的类需定义深拷贝构造函数。
  4. 拷贝赋值运算符重载函数:用于对象间的赋值,编译器生成的执行浅拷贝,要考虑自赋值和资源管理,对于动态分配资源的类需实现深拷贝赋值。

正确理解和合理使用这四个缺省函数,能使我们编写的C++代码更加健壮、高效,避免常见的编程错误,如内存泄漏、悬空指针等问题。在实际编程中,应根据类的具体需求,谨慎决定是否需要显式定义这些函数,以确保程序的正确性和性能。