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

C++类缺省函数的默认行为

2021-08-177.7k 阅读

C++类缺省函数的概念

在C++中,类的缺省函数(default functions)是指编译器会自动为类生成的一些特殊成员函数。这些函数在程序员没有显式定义时,由编译器隐式提供。它们在类的对象生命周期管理、对象间交互等方面起着关键作用。C++主要的缺省函数包括默认构造函数、默认析构函数、拷贝构造函数、拷贝赋值运算符重载函数以及C++11引入的移动构造函数和移动赋值运算符重载函数。

默认构造函数

默认构造函数是一种特殊的构造函数,它没有参数。当程序员没有为类定义任何构造函数时,编译器会为该类生成一个默认构造函数。这个默认构造函数的作用是对类对象的成员变量进行默认初始化。

默认构造函数的默认行为

  1. 基本数据类型成员:对于基本数据类型(如intdoublechar等)的成员变量,默认构造函数不会对其进行初始化。这意味着这些成员变量在对象创建时会处于未定义状态。例如:
class MyClass {
    int num;
public:
    // 编译器会生成默认构造函数
};

int main() {
    MyClass obj;
    // num此时的值是未定义的
    return 0;
}
  1. 类类型成员:如果类中有其他类类型的成员变量,默认构造函数会调用这些成员变量的默认构造函数。例如:
class InnerClass {
public:
    InnerClass() {
        // 自定义默认构造函数
    }
};

class OuterClass {
    InnerClass inner;
public:
    // 编译器生成的默认构造函数会调用InnerClass的默认构造函数
};

int main() {
    OuterClass outer;
    return 0;
}
  1. 数组成员:对于数组类型的成员变量,如果数组元素是基本数据类型,同样不会进行初始化;如果数组元素是类类型,则会调用每个元素的默认构造函数。例如:
class ElementClass {
public:
    ElementClass() {
        // 自定义默认构造函数
    }
};

class ArrayClass {
    int basicArray[5];
    ElementClass classArray[3];
public:
    // 编译器生成的默认构造函数
};

int main() {
    ArrayClass arrObj;
    // basicArray中的元素未初始化,classArray中的每个元素会调用ElementClass的默认构造函数
    return 0;
}

默认析构函数

默认析构函数用于在对象生命周期结束时清理对象所占用的资源。当程序员没有为类定义析构函数时,编译器会生成一个默认析构函数。

默认析构函数的默认行为

  1. 基本数据类型成员:对于基本数据类型的成员变量,默认析构函数无需执行任何操作,因为它们的内存会随着对象的销毁自动被释放。例如:
class MyClass {
    int num;
public:
    // 编译器会生成默认析构函数
};

int main() {
    {
        MyClass obj;
    }
    // 离开作用域,默认析构函数被调用,num的内存自动释放
    return 0;
}
  1. 类类型成员:如果类中有其他类类型的成员变量,默认析构函数会调用这些成员变量的析构函数。例如:
class InnerClass {
public:
    ~InnerClass() {
        // 自定义析构函数
    }
};

class OuterClass {
    InnerClass inner;
public:
    // 编译器生成的默认析构函数会调用InnerClass的析构函数
};

int main() {
    {
        OuterClass outer;
    }
    // 离开作用域,默认析构函数被调用,inner的析构函数被调用
    return 0;
}
  1. 动态分配内存的成员:如果类中有成员变量是通过动态分配内存(如new操作符)获得的,默认析构函数不会自动释放这些内存。这会导致内存泄漏。例如:
class MemoryClass {
    int* data;
public:
    MemoryClass() {
        data = new int[10];
    }
    // 编译器生成的默认析构函数不会释放data指向的内存
};

int main() {
    {
        MemoryClass memObj;
    }
    // 离开作用域,data指向的内存未释放,造成内存泄漏
    return 0;
}

拷贝构造函数

拷贝构造函数用于创建一个新对象,该对象是另一个已存在对象的副本。当程序员没有为类定义拷贝构造函数时,编译器会生成一个默认拷贝构造函数。

默认拷贝构造函数的默认行为

  1. 按成员拷贝:默认拷贝构造函数会对类的每个成员变量进行逐位拷贝(bit - by - bit copy)。对于基本数据类型成员,这种拷贝方式直接复制其值。例如:
class MyClass {
    int num;
public:
    // 编译器生成默认拷贝构造函数
};

int main() {
    MyClass obj1;
    obj1.num = 10;
    MyClass obj2(obj1);
    // obj2.num的值也为10,通过默认拷贝构造函数按成员拷贝
    return 0;
}
  1. 类类型成员:对于类类型的成员变量,默认拷贝构造函数会调用该成员变量的拷贝构造函数。例如:
class InnerClass {
public:
    InnerClass() {}
    InnerClass(const InnerClass& other) {
        // 自定义拷贝构造函数
    }
};

class OuterClass {
    InnerClass inner;
public:
    // 编译器生成的默认拷贝构造函数会调用InnerClass的拷贝构造函数
};

int main() {
    OuterClass outer1;
    OuterClass outer2(outer1);
    // outer2.inner是通过调用InnerClass的拷贝构造函数从outer1.inner拷贝而来
    return 0;
}
  1. 潜在问题 - 浅拷贝:当类中有成员变量是指针类型且指向动态分配的内存时,默认拷贝构造函数的按成员拷贝会导致浅拷贝问题。两个对象的指针成员会指向同一块内存,当其中一个对象销毁时释放了这块内存,另一个对象的指针就会成为野指针。例如:
class MemoryClass {
    int* data;
public:
    MemoryClass() {
        data = new int[10];
    }
    // 编译器生成的默认拷贝构造函数会导致浅拷贝
};

int main() {
    MemoryClass memObj1;
    MemoryClass memObj2(memObj1);
    // memObj1.data和memObj2.data指向同一块内存
    delete[] memObj1.data;
    // memObj2.data现在是野指针
    return 0;
}

拷贝赋值运算符重载函数

拷贝赋值运算符重载函数(operator=)用于将一个已存在对象的值赋给另一个已存在对象。当程序员没有为类定义拷贝赋值运算符重载函数时,编译器会生成一个默认的版本。

默认拷贝赋值运算符重载函数的默认行为

  1. 按成员赋值:默认拷贝赋值运算符重载函数会对类的每个成员变量进行逐位赋值。对于基本数据类型成员,直接赋值其值。例如:
class MyClass {
    int num;
public:
    // 编译器生成默认拷贝赋值运算符重载函数
};

int main() {
    MyClass obj1, obj2;
    obj1.num = 10;
    obj2 = obj1;
    // obj2.num的值变为10,通过默认拷贝赋值运算符按成员赋值
    return 0;
}
  1. 类类型成员:对于类类型的成员变量,默认拷贝赋值运算符重载函数会调用该成员变量的拷贝赋值运算符。例如:
class InnerClass {
public:
    InnerClass() {}
    InnerClass& operator=(const InnerClass& other) {
        // 自定义拷贝赋值运算符
        return *this;
    }
};

class OuterClass {
    InnerClass inner;
public:
    // 编译器生成的默认拷贝赋值运算符重载函数会调用InnerClass的拷贝赋值运算符
};

int main() {
    OuterClass outer1, outer2;
    outer2 = outer1;
    // outer2.inner是通过调用InnerClass的拷贝赋值运算符从outer1.inner赋值而来
    return 0;
}
  1. 潜在问题 - 浅拷贝与自我赋值:和默认拷贝构造函数类似,当类中有指针类型且指向动态分配内存的成员变量时,默认拷贝赋值运算符重载函数会导致浅拷贝问题。同时,它也没有处理自我赋值的情况。例如:
class MemoryClass {
    int* data;
public:
    MemoryClass() {
        data = new int[10];
    }
    // 编译器生成的默认拷贝赋值运算符重载函数会导致浅拷贝且未处理自我赋值
};

int main() {
    MemoryClass memObj1, memObj2;
    memObj2 = memObj1;
    // memObj1.data和memObj2.data指向同一块内存,浅拷贝问题
    memObj1 = memObj1;
    // 默认版本未处理自我赋值,可能导致错误
    return 0;
}

C++11 引入的移动构造函数和移动赋值运算符重载函数

C++11引入了移动语义,通过移动构造函数和移动赋值运算符重载函数来提高对象在资源所有权转移时的效率。当程序员没有定义这两个函数时,编译器也会生成默认版本。

移动构造函数

移动构造函数用于从一个临时对象(右值)“窃取”资源,而不是像拷贝构造函数那样进行复制。

默认移动构造函数的默认行为

  1. 基本数据类型成员:对于基本数据类型成员,默认移动构造函数会执行和拷贝构造函数相同的操作,即按位拷贝。因为基本数据类型的复制成本较低。例如:
class MyClass {
    int num;
public:
    // 编译器生成默认移动构造函数
};

int main() {
    MyClass obj1;
    obj1.num = 10;
    MyClass obj2(std::move(obj1));
    // obj2.num的值为10,基本数据类型按位拷贝
    return 0;
}
  1. 类类型成员:对于类类型的成员变量,如果该成员类型有定义移动构造函数,默认移动构造函数会调用其移动构造函数;否则调用拷贝构造函数。例如:
class InnerClass {
public:
    InnerClass() {}
    InnerClass(const InnerClass& other) {
        // 自定义拷贝构造函数
    }
    InnerClass(InnerClass&& other) noexcept {
        // 自定义移动构造函数
    }
};

class OuterClass {
    InnerClass inner;
public:
    // 编译器生成的默认移动构造函数会调用InnerClass的移动构造函数
};

int main() {
    OuterClass outer1;
    OuterClass outer2(std::move(outer1));
    // outer2.inner是通过调用InnerClass的移动构造函数从outer1.inner移动而来
    return 0;
}
  1. 指针类型成员:当类中有指针类型且指向动态分配内存的成员变量时,默认移动构造函数会进行浅拷贝,将源对象的指针直接赋值给目标对象,并将源对象的指针置为nullptr。这实现了资源的“窃取”。例如:
class MemoryClass {
    int* data;
public:
    MemoryClass() {
        data = new int[10];
    }
    // 编译器生成的默认移动构造函数会进行浅拷贝并置源指针为nullptr
};

int main() {
    MemoryClass memObj1;
    MemoryClass memObj2(std::move(memObj1));
    // memObj2.data指向原memObj1.data指向的内存,memObj1.data变为nullptr
    return 0;
}

移动赋值运算符重载函数

移动赋值运算符重载函数(operator=)用于将一个临时对象(右值)的资源“移动”给另一个已存在对象。

默认移动赋值运算符重载函数的默认行为

  1. 基本数据类型成员:对于基本数据类型成员,默认移动赋值运算符重载函数会执行和拷贝赋值运算符相同的操作,即按位赋值。例如:
class MyClass {
    int num;
public:
    // 编译器生成默认移动赋值运算符重载函数
};

int main() {
    MyClass obj1, obj2;
    obj1.num = 10;
    obj2 = std::move(obj1);
    // obj2.num的值为10,基本数据类型按位赋值
    return 0;
}
  1. 类类型成员:对于类类型的成员变量,如果该成员类型有定义移动赋值运算符,默认移动赋值运算符重载函数会调用其移动赋值运算符;否则调用拷贝赋值运算符。例如:
class InnerClass {
public:
    InnerClass() {}
    InnerClass& operator=(const InnerClass& other) {
        // 自定义拷贝赋值运算符
        return *this;
    }
    InnerClass& operator=(InnerClass&& other) noexcept {
        // 自定义移动赋值运算符
        return *this;
    }
};

class OuterClass {
    InnerClass inner;
public:
    // 编译器生成的默认移动赋值运算符重载函数会调用InnerClass的移动赋值运算符
};

int main() {
    OuterClass outer1, outer2;
    outer2 = std::move(outer1);
    // outer2.inner是通过调用InnerClass的移动赋值运算符从outer1.inner移动而来
    return 0;
}
  1. 指针类型成员:当类中有指针类型且指向动态分配内存的成员变量时,默认移动赋值运算符重载函数会先释放目标对象的原有资源(如果有),然后进行浅拷贝,将源对象的指针赋值给目标对象,并将源对象的指针置为nullptr。例如:
class MemoryClass {
    int* data;
public:
    MemoryClass() {
        data = new int[10];
    }
    ~MemoryClass() {
        delete[] data;
    }
    // 编译器生成的默认移动赋值运算符重载函数会释放目标原有资源,进行浅拷贝并置源指针为nullptr
};

int main() {
    MemoryClass memObj1, memObj2;
    memObj2 = std::move(memObj1);
    // memObj2.data指向原memObj1.data指向的内存,memObj1.data变为nullptr,memObj2原有的data资源被释放
    return 0;
}

在实际编程中,了解这些缺省函数的默认行为非常重要。如果类的成员变量涉及动态资源管理(如动态分配内存、文件句柄、网络连接等),程序员通常需要显式定义这些特殊成员函数,以避免浅拷贝、内存泄漏等问题,确保类的行为符合预期,并提高程序的性能和健壮性。同时,移动语义的合理运用可以显著提升程序在对象资源转移场景下的效率。通过深入理解这些缺省函数的默认行为,开发者能够编写出更高效、更安全的C++代码。例如,在编写一个管理动态数组的类时,如果只依赖默认的拷贝构造函数和拷贝赋值运算符,可能会在对象拷贝和赋值过程中出现内存问题。但通过显式定义这些函数,采用深拷贝的方式,可以保证每个对象都有自己独立的动态数组副本,避免数据混乱和内存泄漏。同样,在涉及大量临时对象传递的场景中,合理利用移动构造函数和移动赋值运算符,可以减少不必要的资源复制,提升程序性能。总之,对C++类缺省函数默认行为的掌握是C++高级编程的重要基础。