C++类与对象核心知识点全解析(中)【六大默认成员函数详解】
目录
1. 类的6个默认成员函数
2. 构造函数
2.1 概念
2.2 特性
3. 析构函数
3.1 概念
3.2 特性
4. 拷贝构造函数
4.1 概念
4.2 特征
浅拷贝(Shallow Copy)
深拷贝(Deep Copy)
4.3代码样例
5. 赋值运算符重载
5.1 运算符重载
5.2 赋值运算符重载
1. 赋值运算符重载格式
2.赋值运算符只能重载成类的成员函数不能重载成全局函数
3. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注 意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符 重载完成赋值。
5.3 前置++和后置++重载
5.4 流插入<<重载
6. const成员函数
7. 取地址及const取地址操作符重载
1. 类的6个默认成员函数
如果一个类中什么成员都没有,简称为空类。 空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员 函数。 默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
2. 构造函数
2.1 概念
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证 每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。
2.2 特性
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任 务并不是开空间创建对象,而是初始化对象。
其特征如下:
1. 函数名与类名相同。
2. 无返回值。
3. 对象实例化时编译器自动调用对应的构造函数。
4. 构造函数可以重载。
class Date{ public: // 1.无参构造函数 Date() {} // 2.带参构造函数 Date(int year, int month, int day) { _year = year;_month = month; _day = day; }private: int _year; int _month; int _day;};
void TestDate(){Date d1; // 调用无参构造函数Date d2(2015, 1, 1); // 调用带参的构造函数// 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明// 以下代码的函数:声明了d3函数,该函数无参,返回一个日期类型的对象// warning C4930: “Date d3(void)”: 未调用原型函数(是否是有意用变量定义的?)Date d3();}
5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦 用户显式定义编译器将不再生成。
6. 关于编译器生成的默认成员函数,很多童鞋会有疑惑:不实现构造函数的情况下,编译器会 生成默认的构造函数。但是看起来默认构造函数又没什么用?d对象调用了编译器生成的默 认构造函数,但是d对象_year/_month/_day,依旧是随机值。也就说在这里编译器生成的 默认构造函数并没有什么用??
解答:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类 型,如:int/char...,自定义类型就是我们使用class/struct/union等自己定义的类型,看看 下面的程序,就会发现编译器生成默认的构造函数会对自定类型成员_t调用的它的默认成员 函数。实际上,只要是构造函数,如果没有没有对自定义类成员进行显示初始化,就会默认调用自定义类的默认构造函数!
注意:C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在 类中声明时可以给默认值。
如:
class Date
{
private:// 基本类型(内置类型)int _year = 1970;int _month = 1;int _day = 1;// 自定义类型Time _t;
};
7. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。 注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为 是默认构造函数。
3. 析构函数
3.1 概念
通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没呢的?
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由 编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
3.2 特性
析构函数是特殊的成员函数,其特征如下:
1. 析构函数名是在类名前加上字符 ~。
2. 无参数无返回值类型。
3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
注意:析构 函数不能重载
4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。
typedef int DataType;
class Stack
{
public:Stack(size_t capacity = 3){_array = (DataType*)malloc(sizeof(DataType) * capacity);if (NULL == _array){perror("malloc申请空间失败!!!");return;}_capacity = capacity;_size = 0;}void Push(DataType data){// CheckCapacity();_array[_size] = data;_size++;}// 其他方法...~Stack(){if (_array){free(_array);_array = NULL;_capacity = 0;_size = 0;5. }}
private:DataType* _array;int _capacity;int _size;
};
析构函数主要执行以下操作:
-
释放资源:释放对象在生命周期内获取的资源,如动态分配的内存、打开的文件、网络连接等。
-
调用成员对象的析构函数:如果类包含其他类的对象作为成员,析构函数会自动调用这些成员对象的析构函数,按照它们声明的相反顺序进行。
-
执行自定义清理代码:执行你在析构函数中编写的任何清理逻辑。
对于默认析构函数,它的特点有:
- 不会释放通过new、malloc等动态分配的内存
- 不会关闭打开的文件、网络连接等资源
- 不会执行任何自定义的清理代码
- 如果类包含其他类的对象作为成员,默认析构函数会自动调用这些成员对象的析构函数,按照它们声明的相反顺序进行。
对于内置类型变量(如int、double、char、指针等),默认析构函数不会做任何特殊的释放操作,因为这些变量是自动管理的。
具体来说:
-
内置类型变量是自动存储的,它们的生命周期由其作用域决定。当对象被销毁时,这些内置类型成员变量的内存会自动被回收,不需要显式释放。
-
内置类型变量不需要调用析构函数,它们没有析构函数的概念。
如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如 Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。
析构函数需要手动清理的资源有: 动态分配的内存、关闭文件、释放互斥锁、信号量、线程等系统资源,如果类包含其他对象,且这些对象需要手动清理(如原始指针管理的对象);
4. 拷贝构造函数
4.1 概念
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存 在的类类型对象创建新对象时由编译器自动调用。
4.2 特征
拷贝构造函数也是特殊的成员函数,其特征如下:
1. 拷贝构造函数是构造函数的一个重载形式。
2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错, 因为会引发无穷递归调用。
3. 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按 字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
注意:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定 义类型是调用其拷贝构造函数完成拷贝的。
浅拷贝和深拷贝的区别:
浅拷贝(Shallow Copy)
- 定义:只复制对象的值,不复制指针指向的实际内容
- 指针处理:复制指针的值(地址),导致多个对象指向同一块内存
- 资源管理:不创建新的资源副本
- 默认行为:C++默认拷贝构造函数和赋值运算符执行浅拷贝
问题:浅拷贝可能导致多个指针指向同一资源,可能导致资源竞争、重复释放和内存泄漏。
深拷贝(Deep Copy)
- 定义:不仅复制对象的值,还复制指针指向的实际内容
- 指针处理:为新对象分配新内存,并复制原对象指针指向的内容
- 资源管理:创建资源的独立副本
- 实现方式:需要自定义拷贝构造函数和赋值运算符
- 优点:每个对象拥有自己的资源副本,避免共享问题
注意:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请 时,则拷贝构造函数是一定要写的,否则就是浅拷贝。
4.3代码样例
Time(const Time& t) // 这是Time类的成员函数
{_hour = t._hour; // 可以访问t对象的私有成员_minute = t._minute; // 可以访问t对象的私有成员_second = t._second; // 可以访问t对象的私有成员cout << "Time::Time(const Time&)" << endl;
}
注意,这里是能够访问Time类的私有成员的,因为访问权限是基于类的,而不是基于对象的。
private访问限定符限制的是"从类外部访问",而不是"从其他对象访问"。同一个类的所有成员函数(包括构造函数、拷贝构造函数等)都可以访问该类的任何实例的私有成员。
拷贝构造函数典型调用场景:
1.使用已存在对象创建新对象
2.函数参数类型为类类型对象
3.函数返回值类型为类类型对象
为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用 尽量使用引用。
5. 赋值运算符重载
5.1 运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其 返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表),这里的参数实际就是运算符的操作数
注意:
(1)不能通过连接其他符号来创建新的操作符:比如operator@
(2)重载操作符必须有一个类类型参数 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
(4)作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
(5).* :: sizeof ?: . 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
下面给出一个类外重载的“==”函数:
// 全局的operator==
class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}
//private:int _year;int _month;int _day;
};bool operator==(const Date& d1, const Date& d2)
{return d1._year == d2._year&& d1._month == d2._month&& d1._day == d2._day;
}
这里会发现运算符重载成全局的就需要成员变量是公有的,那么问题来了,封装性如何保证?
这里其实可以用我们后面学习的友元解决,或者干脆重载成成员函数。
重载成成员函数后:
class Date
{
public:Date(int year = 1900, int month = 1, int day = 1)5.2 赋值运算符重载 {_year = year;_month = month;_day = day;}// bool operator==(Date* this, const Date& d2)// 这里需要注意的是,左操作数是this,指向调用函数的对象bool operator==(const Date& d2){return _year == d2._year;&& _month == d2._month&& _day == d2._day;}
private:int _year;int _month;int _day;
};
5.2 赋值运算符重载
1. 赋值运算符重载格式
参数类型:const T&,传递引用可以提高传参效率
返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
检测是否是自己给自己赋值
返回*this :要符合连续赋值的含义
类内代码样例:
Date& operator=(const Date& d){if(this != &d){_year = d._year;_month = d._month;_day = d._day;}return *this;}
2.赋值运算符只能重载成类的成员函数不能重载成全局函数
原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现 一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值 运算符重载只能是类的成员函数。
3. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注 意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符 重载完成赋值。
注意:和拷贝构造一样,赋值重载默认是浅拷贝,如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现。
5.3 前置++和后置++重载
以日期类为例:
前置++:
前置++:返回+1之后的结果
注意:this指向的对象函数结束后不会销毁,故以引用方式返回提高效率
Date& operator++(){_day += 1;return *this;}
后置++:
Date operator++(int){Date temp(*this);_day += 1;return temp;}
前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载
C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器 自动传递
注意:后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将this保存 一份,然后给this+1 ,而temp是临时对象,因此只能以值的方式返回,不能返回引用
5.4 流插入<<重载
以日期类为例,我们想像cout<<……一样打印出年月日来,就需要对<<进行重载,参数为标准输出对象和日期类;
假如在类内实现重载,会发现重载函数的形参列表中,第一个形参必然是Date*this,其次才是输出对象,这样实现的话效果是data<<cout,显然不符合我们的预期,因此我们要在类外实现重载。
代码如下:
ostream& operator<<(ostream& out, const Date& d2)
{out << d2._year << ' ' << d2._month << ' ' << d2._day << endl;return out;}
可这里又面临日期类私有成员不可访问的问题,我们通过将函数在日期类内部进行友元声明来解决:
class Date
{friend ostream& operator<<(ostream& out, const Date& d2);
public:
……
这样,此函数就可以使用日期类的私有成员了。
6. const成员函数
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数 隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
请思考下面的几个问题:
1. const对象可以调用非const成员函数吗?
不可以。(权限扩大)
- const对象表示其状态不应该被修改
- 非const成员函数可能会修改对象的状态
- 如果允许const对象调用非const成员函数,就破坏了const的语义保护
2. 非const对象可以调用const成员函数吗?
可以。(权限缩小)
- 非const对象具有完全的访问权限
- const成员函数承诺不修改对象状态,这对非const对象是安全的
- 这是const成员函数的兼容性设计
3. const成员函数内可以调用其它的非const成员函数吗?
不可以
- const成员函数承诺不修改对象状态
- 如果它调用非const成员函数,而该函数可能会修改对象状态
- 这就破坏了const成员函数的承诺,存在逻辑矛盾
4. 非const成员函数内可以调用其它的const成员函数吗?
可以。
- 非const成员函数可以修改对象状态
- const成员函数承诺不修改对象状态
- 调用const成员函数不会影响当前函数的功能和安全性
7. 取地址及const取地址操作符重载
这两个默认成员函数一般不用重新定义 ,编译器默认会生成。
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需 要重载,比如想让别人获取到指定的内容!
class Date
{
public :Date* operator&(){return this ; }const Date* operator&()const{return this ;}
private :int _year ; // 年int _month ; // 月int _day ; // 日
};