C++类和对象详解:构造函数、析构函数与拷贝构造函数
C++类和对象详解:构造函数、析构函数与拷贝构造函数
C++类和对象是C++编程中的核心概念,其中构造函数、析构函数和拷贝构造函数是类的三个重要成员函数。本文将详细介绍这些函数的功能、特性以及使用场景。
1. 类的6个默认成员函数
默认成员函数是用户没有显式实现时,编译器会生成的成员函数。这些函数包括构造函数、析构函数、拷贝构造函数、赋值运算符、移动构造函数和移动赋值运算符。
2. 构造函数
构造函数是一个特殊的成员函数,用于初始化对象的成员变量。它在创建对象时由编译器自动调用,并且在整个对象生命周期内只调用一次。
2.1 构造函数的特性
2.1.1 函数名与类名相同
2.1.2 无返回值
2.1.3 对象实例化时编译器自动调用对应的构造函数
目前来说,我们将构造函数放在公有区域内,以便编译器调用。正如前面所说的一样,构造函数将在对象创建时自动调用。注意,构造函数无返回值,不用写成void。
2.1.4 构造函数可以重载
我们当然希望对象可以根据需要进行初始化:
#include <iostream>
using namespace std;
class Date
{
public:
Date()
{
cout << "Date()" << endl;
_year = 2025;
_month = 3;
_day = 10;
}
Date(int year, int month, int day)
{
cout << "Date(int year, int month, int day)" << endl;
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(2025, 3, 10);
return 0;
}
注意构造函数的调用方式,此时我们有显示定义构造函数:
- 当调用无参构造函数时:
Date d1; // right
Date d1(); // error
为什么调用无参构造函数不能像第二行一样加括号呢?因为,第二行的代码就与无参函数的声明起冲突了,编译器无法识别这是在创建一个对象然后再选择去调用该类对象的构造函数,还是在声明一个函数名为 d1 返回值为一个 Date 类且无参的函数。
- 当调用有参构造函数时
直接在对象名后面进行传参。
2.1.5 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成
用户有定义构造函数之后编译器当然要选择不再生成构造函数,不然当编译器自动生成的无参构造函数与用户显式定义的无参或全缺省函数冲突时,编译器该调用哪个构造函数呢?
2.1.6 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。 注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为 是默认构造函数
无参的构造函数和全缺省的构造函数虽然语法上构成重载,但是当创建对象不传参的时候编译器就无法抉择该调用哪一个构造函数,此时可以选择去掉显式定义的无参构造函数。
2.1.7 C++编译器自动生成的无参的默认构造函数的特性
创建对象时,该构造函数:
- 对于内置类型,如int、double、char....等,不进行初始化(有的编译器会进行初始化)
- 对于自定义类型,如类、联合体、枚举,去调用它的默认构造函数从而完成初始化
class Time
{
public:
Time()
{
_hour = 17;
_minute = 14;
_second = 15;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
int _year;
int _month;
int _day;
Time _time;
};
int main()
{
Date d1;
return 0;
}
如上,Date类中没有显式定义构造函数,编译器自动生成一个Date类的无参的默认构造函数,该函数对内置类型不进行初始化操作,而调用了Time类中的默认构造函数从而完成了 _time对象的初始化操作。当然,想要得到我们想要的初始化操作,自定义类型的默认构造函数还得我们自己写,不然,一个编译器自动生成的Date类的无参的默认构造函数调用一个编译器自动生成的Time类的无参的默认构造函数,实际上并不会对自定义类型进行相应的初始化操作。
2.1.8 C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在 类中声明时可以给默认值
class Date
{
private:
int _year = 2025;
int _month = 3;
int _day = 10;
};
int main()
{
Date d1;
return 0;
}
3. 析构函数
构造函数是一个特殊的成员函数,用于清理对象中的资源(动态内存、文件、网络连接等)。它在对象销毁时由编译器自动调用。
3.1 析构函数的特性
3.1.1 析构函数名是在类名前加上字符~
3.1.2 无参数无返回值类型
3.1.3 对象生命周期结束时,C++编译系统自动调用析构函数。
我们将析构函数放在公有区域内,以便编译器调用。正如前面所说的一样,析构函数将在对象销毁时自动调用。注意,构造函数无参,无返回值,不用写成void。
3.1.4 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
析构函数不能被重载,这是因为它没有参数且无返回值。如果一个类中有多个析构函数,可能会导致同一块内存被多次释放、编译器无法确定该调用哪一个析构函数等未定义行为。
3.1.5 C++编译器自动生成的析构函数的特性
对象销毁时,该析构函数:
- 对于内置类型,如int、double、char....等,不进行资源清理,因为系统会自动回收其内存
- 对于自定义类型,如类、联合体、枚举,去调用它的析构函数从而完成相应的资源清理
class Time
{
public:
~Time()
{
cout << "~Time()" << endl;
}
private:
int _hour = 18;
int _minute = 57;
int _second = 39;
};
class Date
{
private:
int _year = 2025;
int _month = 3;
int _day = 10;
Time _time;
};
int main()
{
Date d1;
return 0;
}
Date类中没有显示定义析构函数,编译器自动生成一个Date类的析构函数,该函数不会清理d1中的内置类型变量。又因为只有Time类的析构函数才能完成对象_time中的资源清理工作,所以Date类的析构函数转而去调用了Time类的析构函数。当然,Time类中的析构函数也没有对_time中的内置类型变量进行清理工作。
3.1.6 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如 Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类
4. 拷贝构造函数
拷贝构造函数是一个特殊的成员函数,用于创建一个新的对象,并将其初始化为另一个同类型对象的副本。它在用已存在的类类型对象创建新对象时由编译器自动调用。
4.1 拷贝构造函数的特性
4.1.1 拷贝构造函数是构造函数的一个重载形式
4.1.2 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错, 因为会引发无穷递归调用
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& date)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2025, 3, 11);
Date d2(d1);
return 0;
}
注意拷贝构造函数的书写:
- 函数名与类名相同
- 无返回值
- 有且仅有一个形参
- 拷贝构造函数参数是对该类型的引用,并且最好加上const修饰
引用的目的:我们知道,当用一个对象去创建另一个对象的时候,编译器就会自动调用拷贝构造函数。在这里,我们用d1去创建d2时,编译器会自动调用拷贝构造函数。一旦调用,实参d1就要去传参给形参date,如果形参是类类型,那么形参会是实参的一份临时拷贝,在这里编译器又会自动调用拷贝构造函数以实现形参date对实参d1的拷贝操作,即用实参d1去创建形参date,因为这又是用一个对象去创建另一个对象的行为,那么编译器又会自动调用拷贝构造函数,如此循环往复,无穷递归也。所以使用引用接收实参,就不存在对象的创建啦~比特就业课
加上const修饰的目的:引用与引用实体共用同一块空间,加上const修饰以防止引用对象被篡改。
4.1.3 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数按内存存储的字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝
class Time
{
public:
Time()
{
_hour = 14;
_minute = 40;
_second = 30;
}
Time(const Time& time)
{
cout << "Time(const Time& time)" << endl;
_hour = time._hour;
_minute = time._minute;
_second = time._second;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
int _year = 2025;
int _month = 3;
int _day = 11;
Time _time;
};
int main()
{
Date d1;
Date d2(d1);
return 0;
}
Date类中没又显示定义拷贝构造函数,当用d1创建d2时,编译器会自动生成并调用一个Date类的拷贝构造函数,此时,将d1中的内置类型拷贝到d2的内置类型当中去。对于Date类中的Time类型,上述生成的拷贝构造函数会去调用Time类中的拷贝构造函数从而实现将自定义类型的拷贝。需要注意的是,显示定义了一个拷贝构造函数而不定义一个构造函数的话,编译器不会再生成一个无参的构造构造函数,此时就不能单独创建对象。
//error
class Time
{
public:
Time(const Time& time)
{
cout << "Time(const Time& time)" << endl;
_hour = time._hour;
_minute = time._minute;
_second = time._second;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
int _year = 2025;
int _month = 3;
int _day = 11;
Time _time;
};
int main()
{
Date d1;
Date d2(d1);
return 0;
}
在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。
4.1.4 类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请 时,则拷贝构造函数是一定要写的,否则就是浅拷贝
没有涉及资源申请的时候,无非就是对内置类型的拷贝,不会出现什么错误。但是,比如说,当涉及到动态内存分配的时候,拷贝构造函数就必须自己写了。这是因为,默认的拷贝构造函数是将值进行复制,这就会导致,创建的新对象中的对应指针变量会与原对象中的对应指针变量指向同一块动态分配的内存,那么,对象销毁时,这块内存就会被释放两次,这是不允许的!涉及资源申请的拷贝构造函数我们后续再进行讲解。
4.1.5 拷贝构造函数典型调用场景
- 使用已存在对象创建新对象
- 函数参数类型为类类型对象
- 函数返回值类型为类类型对象
class Date
{
public:
Date(int year, int minute, int day)
{
cout << "Date(int,int,int):" << this << endl;
}
Date(const Date& d)
{
cout << "Date(const Date& d):" << this << endl;
}
~Date()
{
cout << "~Date():" << this << endl;
}
private:
int _year;
int _month;
int _day;
};
Date Test(Date d)
{
Date temp(d);
return temp;
}
int main()
{
Date d1(2022,1,13);
Test(d1);
return 0;
}
为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用尽量使用引用。