前言
类与对象(二)
如果在类的声明中未显式提供某个成员函数的定义,编译器会自动生成一个默认实现。 这包括默认构造函数、默认析构函数、默认拷贝构造函数、默认拷贝赋值运算符以及默认移动构造函数和移动赋值运算符。
我们主要将讲解一下构造函数,析构函数,拷贝构造函数和默认拷贝赋值运算符。
构造函数用来初始化对象的成员变量
主要特性:
与类同名
没有返回类型
在对象创建时自动调用
可以重载
class Date{
public:
// 1.无参构造函数
Date(){
_year = 0;
_month = 0;
_day = 0;
}
// 2.带参构造函数
Date(int year, int month, int day){
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main() {
Date d1; // 调用无参构造函数,不需要跟括号
Date d2(2015, 1, 1); // 调用带参的构造函数
// 这不是在创建一个对象,而是声明一个函数 d3,该函数没有参数并返回一个
//Date d3(); warning C4930 : “Date d3(void)” : 未调用原型函数(是否是有意用变量定义的 ? )
return 0;
}
类中没有显式定义构造函数,则C++编译器自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
class Date{
public:
// 如果用户显式定义了构造函数,编译器将不再生成
//Date(int year, int month, int day){
//_year = year;
//_month = month;
//_day = day;
//}
void Print(){
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main(){
// 无自定义构造函数,编译器自动生成并调用默认构造函数,这时程序能够运行
// 如果显式定义了构造函数,编译器将不会自动生成默认构造函数,而是自动调用自定义的构造函数
// 证据是,当我们显式定义一个有参的构造函数,并在创建对象时不传参,
// 编译器会报错 ,“Date”: 没有合适的默认构造函数可用
Date d1;
return 0;
}
编译器在遇到内置类型时,自动生成的默认构造函数(没有显式定义构造函数的情况下,这符合第五点)不对其初始化;遇到自定义类型时调用该类型中的显式定义构造函数,如果没有,也会像内置类型那样,自动生成默认构造函数并调用,但不对内置类型初始化。
对于内置类型,编译器生成的默认构造函数通常不包含任何实际的初始化代码,这意味着内置类型的成员变量将包含未定义的值,即取决于存储它们的内存的初始状态(内置类型的成员变量是在栈上或堆上分配内存的,而这块内存的初始值是未定义的,即它们可能包含任意的数值)。
对于自定义类型:
如果类中没有任何构造函数,编译器会生成一个默认构造函数,对所有成员变量执行它们各自的默认构造函数。对于基本数据类型成员,执行与内置类型相同的处理,即保留未初始化的值。
如果类显式声明了其他构造函数(无论是默认构造函数还是带参数的构造函数),编译器将不再生成默认构造函数。此时,如果你确实需要一个默认构造函数,你需要显式提供它。
示例:
#include
class Example {
public:
// 默认构造函数
Example() {
std::cout << "默认构造函数被调用" << std::endl;
// 对于基本数据类型,保留未初始化的值
}
private:
int intValue;
double doubleValue;
};
int main() {
// 对于自定义类型 Example,会调用默认构造函数
Example obj;
return 0;
}
在上述例子中,Example
类包含两个基本数据类型成员变量。默认构造函数将被调用,并且对于 int
和 double
类型的成员变量,它们将包含未初始化的值。
C++11 允许在类的声明中直接进行成员变量的初始化,这被称为默认成员初始化。
在没有显式提供构造函数的情况下,成员变量 intValue 将会被默认初始化为 42。
class Example {
public:
int intValue = 42; // 默认成员初始化
};
自定义的无参构造函数和全缺省构造函数,以及编译器自动生成的默认构造函数都是默认构造函数。
默认构造函数的意思是,在创建对象时不需要任何参数。而无参构造函数和全缺省构造函数,以及编译器自动生成的默认构造函数,它们都不需要任何参数就可以创建对象,因此它们都是默认构造函数。
但要注意,因为它们都不需要参数,所以它们不会同时出现,否则编译器不知道要调用哪个函数。
class Date{
public:
Date(){
_year = 1900;
_month = 1;
_day = 1;
}
Date(int year = 1900, int month = 1, int day = 1){
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
// 以下测试函数能通过编译吗?
int main() {
// “Date::Date” : 对重载函数的调用不明确
//Date d1;
}
析构函数是在对象生命周期结束时被调用的特殊成员函数。它的主要作用是进行对象的清理和资源释放工作。
在C++中,每个类都可以有一个析构函数,其名称与类名相同,前面加上波浪号(~)。
无参数无返回值类型。
一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载。
对象生命周期结束时自动调用析构函数。
class Date {
public:
Date() {
_year = 1900;
_month = 1;
_day = 1;
}
~Date() {
cout <<"对象生命周期结束时自动调用"<< "\n";
}
private:
int _year;
int _month;
int _day;
};
int main() {
Date d1;
}
当一个类没有显式定义析构函数时,C++编译器会自动生成一个默认的析构函数。这个默认的析构函数会执行基本的清理工作,但对于动态分配的内存或其他资源的释放,它可能不会进行额外的操作。
#include
class Example {
public:
// 没有显式定义析构函数
// 其他成员函数和变量
void someFunction() {
std::cout << "Executing some function\n";
}
};
int main() {
// 创建对象
Example obj;
// 调用成员函数
obj.someFunction();
// 对象超出作用域,析构函数被调用
return 0;
}
默认的析构函数通常足够处理大多数情况,尤其是对于没有动态资源管理的简单类。然而,如果类涉及到动态分配的内存等复杂的操作,通常建议显式定义析构函数以确保这些资源能够被正确释放。
拷贝构造函数是一种特殊的构造函数,用于创建一个对象,该对象是已有对象的精确副本。
拷贝构造函数通常在以下情况下调用:
拷贝构造函数的基本语法如下:
class MyClass {
public:
// 拷贝构造函数
MyClass(const MyClass& other) {
// 执行拷贝操作,创建一个对象的副本
}
// 其他成员函数和变量
};
可以将拷贝构造函数看作构造函数的重载。
拷贝构造函数的参数是一个对同类型对象的引用,并且通常是 const
引用,以确保不修改原始对象。在函数体内,你需要编写适当的代码来实现对象的拷贝。
同时拷贝构造函数不能通过传值的方式定义,因为这样会引发无限递归的拷贝构造函数调用。
class MyClass {
public:
// 错误的拷贝构造函数,传值方式
MyClass(MyClass another) {
// 这里的传值方式将调用拷贝构造函数,导致无限循环
}
//正确方式 MyClass(const MyClass& another)
};
通过值传递方式定义拷贝构造函数时,传递的对象 another 会触发拷贝构造函数,而这个拷贝构造函数又传递了一个值,然后再次触发拷贝构造函数,导致无限递归调用。
如果你没有显式提供拷贝构造函数,C++ 编译器会为你生成一个默认的拷贝构造函数。 这个默认的拷贝构造函数执行的操作是按位拷贝(浅拷贝),即将一个对象的每个成员变量的值复制给另一个对象的对应成员变量。
class Date{
public:
Date(int year = 1900, int month = 1, int day = 1){
_year = year;
_month = month;
_day = day;
}
void Print() {
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main(){
Date d1;
Date d2(d1);// 调用默认拷贝构造函数,创建对象d2
d1.Print();
d2.Print();
return 0;
}
如果类中包含了动态分配的资源(比如使用 new 分配的内存),默认的拷贝构造函数执行的是浅拷贝,这可能导致两个对象共享相同的资源,而不是创建资源的副本。
而且当对象的生命周期结束时,析构函数清理动态分配的资源,但由于两个对象共享相同的资源,会将已经清理的空间再次清理,导致程序崩溃。
所以当中包含了动态分配的资源时,拷贝构造函数要由我们自己定义。这就是深拷贝。
class Date {
public:
// 构造函数
Date(const char* dateString) {
// 假设 dateString 是通过 new 分配的内存
data = new char[strlen(dateString) + 1];
strcpy(data, dateString);
}
// 自定义的深拷贝构造函数
Date(const Date& other) {
// 分配新的内存
data = new char[strlen(other.data) + 1];
// 复制原始对象的数据到新分配的内存中
strcpy(data, other.data);
}
// 析构函数
~Date() {
// 释放动态分配的内存
delete[] data;
}
// 打印日期
void printDate() const {
std::cout << "Date: " << data << std::endl;
}
private:
char* data;
};
int main() {
// 创建日期对象
Date date1("2022-01-01");
// 使用深拷贝创建另一个日期对象
Date date2 = date1;
// 打印两个日期对象
date1.printDate();
date2.printDate();
return 0;
}
调用拷贝构造函数的三种情况
对象的初始化: 当一个对象通过另一个对象进行初始化时,拷贝构造函数会被调用。
MyClass obj1; // 调用默认构造函数
MyClass obj2 = obj1; // 调用拷贝构造函数
//或者 MyClass obj2(obj1);
传递对象给函数: 当对象作为参数传递给函数时,拷贝构造函数会被调用。
void someFunction(MyClass param) {
// 在函数体内使用 param
}
MyClass obj3;
someFunction(obj3); // 调用拷贝构造函数
从函数返回对象: 当一个函数返回一个对象时,拷贝构造函数会被调用,用于创建返回对象的副本。
MyClass createObject() {
MyClass obj;
return obj; // 调用拷贝构造函数
}
MyClass obj4 = createObject(); // 调用拷贝构造函数
拷贝赋值运算符用于将一个已经存在的对象的值赋给另一个已经存在的对象。这个运算符通常用于确保对象之间的深度拷贝,特别是在涉及到动态分配的资源时。
拷贝赋值运算符的一般形式如下:
class MyClass {
public:
// 拷贝赋值运算符
MyClass& operator=(const MyClass& other) {
// 检查是否是自赋值
if (this != &other) {
// 执行深拷贝操作,复制资源
// 注意:需要释放当前对象可能持有的资源
}
return *this; // 返回当前对象的引用
}
// 其他成员函数和变量
};
拷贝赋值运算符返回一个对当前对象的引用,这样可以支持链式赋值操作(例如 a = b = c
)。在实现拷贝赋值运算符时,需要注意避免自赋值,以免在释放资源时导致错误。
如果在类中没有显式定义拷贝赋值运算符,编译器会自动生成一个默认的拷贝赋值运算符。这个默认生成的版本会按字节拷贝对象的每个成员变量,即浅拷贝。
例子:
class MyClass {
public:
// 构造函数,成员初始化列表
MyClass(int val) : value(val) {}
// 打印数据
void printData() const {
std::cout << "Value: " << value << std::endl;
}
private:
int value;
};
int main() {
// 创建对象
MyClass obj1(42);
// 使用默认生成的拷贝赋值运算符进行赋值
MyClass obj2(0);
obj2 = obj1;
// 打印两个对象的数据
obj1.printData();
obj2.printData();
return 0;
}
同样的,如果类需要管理动态分配的资源,需要显式提供拷贝构造函数和拷贝赋值运算符以确保正确的资源复制。
拷贝构造函数和拷贝赋值运算符的一些区别:
时机不同: 拷贝构造函数在对象的创建和复制时被调用,而拷贝赋值运算符在对象已经存在的情况下进行赋值时被调用。
用途不同: 拷贝构造函数通常用于对象的初始化和创建副本(类类型传参和函数返回类类型时),而拷贝赋值运算符用于对象的赋值操作。
返回类型不同: 拷贝构造函数没有返回类型,而拷贝赋值运算符返回当前对象的引用(链式赋值操作)。