目录
一、类的六个默认成员函数
二、构造函数
2.1 概念
2.2 特性
三、析构函数
3.1 概念
3.2 特性
四、拷贝构造函数
4.1 概念
4.2 特性
五、赋值运算符重载
5.1 运算符重载
5.2 赋值运算符重载特性
六、const修饰成员函数
七、取地址及const取地址操作符重载
一、类的六个默认成员函数
若一个类中什么成员都没有,其被称为空类。但是空类中并不是什么都没有,即使什么都不写,类中也会自动生成6个默认成员函数(不止6个,C++11新增两个)
class Date {}; //空类
注意:这里的"默认"和"缺省"意思相近。即不写这6个函数,编译器自动生成;若是写了,编译器则不生成
二、构造函数
2.1 概念
- 名字与类名相同,创建对象时由编译器自动调用,保证每个数据成员都有合适的初始值,在对象的生命周期内只调用一次
如:以下日期类中的成员函数Date就是构造函数。用该日期类创建对象时,编译器会自动调用该构造函数对新创建的变量进行初始化
class Date
{
public:Date(int year = 0, 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;
};
2.2 特性
- 构造函数的函数名与类名相同
- 构造函数无返回值(构造函数无返回值是真正意义上的无返回值,并不是返回值为void)
- 对象实例化时编译器自动调用对应的构造函数
- 构造函数支持重载
- 无参的构造函数、全缺省的构造函数以及编译器自动生成的构造函数都被称为默认构造函数,并且默认构造函数只能有一个(无需传参就可以调用的构造函数就是默认构造函数)
- 若类中没有显示定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,若用户显示定义了,则编译器就不再生成
既然在不写的情况下,编译器会自动生成一个构造函数,那就没有必要写构造函数了吗?这个想法是错误的
#include <iostream>
using namespace std;
class Date
{
public:void Print(){cout << _year << "年" << _month << "月" << _day << "日" << endl;}
private:int _year;int _month;int _day;
};
int main()
{Date d1; // 编译器将调用自动生成的默认构造函数对d1进行初始化d1.Print();return 0;
}
代码输出结果如下:
为什么d1对象调用了编译器自动生成的构造函数后,d1对象的_year/_month/_day依旧是随机值呢?
编译器自动生成的构造函数机制:
- 编译器自动生成的构造函数对内置类型不做处理
- 对于自定义类型,编译器会再去调用其默认构造函数
编译器自动生成的构造函数可能达不到预期的效果,所以大多数情况下都需程序员自行编写构造函数
三、析构函数
3.1 概念
- 与构造函数功能相反,析构函数负责完成对象的销毁,对象在销毁时自动调用析构函数,完成类的一些资源清理工作
当一个类对象销毁时,其中的成员变量也会随着该对象的销毁而销毁,如:用日期类创建了一个对象d1,当d1被销毁时,对象d1中的成员变量_year/_month/_day也会被自动销毁。但是这并不意味着析构函数没有什么意义。某些情况下,对象会在堆区开辟空间使用,当该对象被销毁时,其动态开辟的堆空间并不会随之被销毁,需对其进行空间释放
3.2 特性
- 一、析构函数的函数名是在类名前加上字符"~"
class Date
{
public:Date()// 构造函数{}~Date()// 析构函数{}
private:int _year;int _month;int _day;
};
- 二、析构函数无参数,无返回值(真正意义上的无返回值,并不是返回值为void)
- 三、对象生命周期结束时,C++编译器会自动调用析构函数
- 四、一个类有且只有一个析构函数。若未显示定义系统会自动生成默认的析构函数
编译器自动生成的析构函数机制:
- 编译器自动生成的析构函数对内置类型不做处理
- 对于自定义类型,编译器会再去调用它们自己的默认析构函数
- 五、先构造的后析构,后构造的先析构
因为对象是定义在函数中的,函数调用会建立栈帧,栈帧中的对象构造和析构也要符合先进后出的原则
四、拷贝构造函数
4.1 概念
- 只有单个形参,该形参是对本类类型对象的引用(常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用
#include <iostream>
using namespace std;
class Date
{
public:Date(int year = 0, int month = 1, int day = 1)// 构造函数{_year = year;_month = month;_day = day;}Date(const Date& d)// 拷贝构造函数{_year = d._year;_month = d._month;_day = d._day;}
private:int _year;int _month;int _day;
};
int main()
{Date d1(2021, 5, 31);Date d2(d1); // 用已存在的对象d1创建对象d2return 0;
}
4.2 特性
- 一、拷贝构造函数是构造函数的一个重载形式(拷贝构造函数的函数名也与类名相同)
- 二、拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用
要调用拷贝构造函数就需先传参,若传参使用传值传参,那么在传参过程中又需进行对象的拷贝构造,如此循环往复,最终引发无穷递归调用
- 三、若未显示定义拷贝构造函数,系统将生成默认的拷贝构造函数
#include <iostream>
using namespace std;
class Date
{
public:Date(int year = 0, 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(2021, 5, 30);Date d2(d1); // 用已存在的对象d1创建对象d2d1.Print();d2.Print();return 0;
}
编译器自动生成的拷贝构造函数机制:
- 编译器自动生成的拷贝构造函数对内置类型会完成浅拷贝(值拷贝)
- 对于自定义类型,编译器会再去调用它们的拷贝构造函数
代码中,并没有显示定义拷贝构造函数,但编译器自动生成的拷贝构造函数最终还是完成了对象的拷贝构造
- 四、编译器自动生成的拷贝构造函数不能实现深拷贝
深浅拷贝问题
编译器自动生成的拷贝构造函数会对内置类型完成浅拷贝。对于以下这句代码,浅拷贝实际上就是将d1的内容完完全全的复制了一份拷贝给d2,所以说浅拷贝也被称为值拷贝。
Date d2(d1);// 用已存在的对象d1创建对象d2
但某些场景下浅拷贝并不能达到预期的效果。如,如下栈(Stack)这样的类,编译器自动生成的拷贝构造函数就不能满足需求了
class Stack
{
public:Stack(int capacity = 4){_ps = (int*)malloc(sizeof(int)* capacity);_size = 0;_capacity = capacity;}void Print(){cout << _ps << endl;// 打印栈空间地址}
private:int* _ps;int _size;int _capacity;
};
类中没有显示定义拷贝构造函数,那么用已存在的对象来创建另一个对象时,将调用编译器自动生成的拷贝构造函数
int main()
{Stack s1;s1.Print();// 打印s1栈空间的地址Stack s2(s1);// 用已存在的对象s1创建对象s2s2.Print();// 打印s2栈空间的地址return 0;
}
结果打印s1栈和s2栈空间的地址相同,意味着,创建完s2栈后,对s1栈做的任何操作都会直接影响到s2栈
这种情况下,还会出现对同一块空间释放多次的问题。所以像Date这样的类,需要的就是浅拷贝,那么编译器自动生成的拷贝构造函数就够用了,不需显示定义。像Stack这样的类,浅拷贝会导致析构两次、程序崩溃等问题,需显示定义对应的拷贝构造函数
五、赋值运算符重载
5.1 运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,其目的是让自定义类型可以像内置类型一样可以直接使用运算符进行操作
d1 == d2;// 可读性高(书写简单)
IsSame(d1, d2);// 可读性差(书写麻烦)
返回值 operator运算符 (参数列表)//函数原型
注意:
- 不能通过连接其他符号来创建新的操作符,如:operator@
- 重载操作符必须有一个类类型或枚举类型的操作数
- 用于内置类型的操作符,重载后其含义不能改变
- 作为类成员的重载函数时,函数有一个默认的形参this,限定为第一个形参
- sizeof 、:: 、.* 、?: 、. 这5个运算符不能重载
示例:前置++重载、后置++重载
前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载。C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递
//前置++
Date& Date::operator++() {//...return *this;
}
//后置++
Date Date::operator++(int) {//...return tmp;
}
示例:重载运算符==
可以将该运算符重载函数作为类的一个成员函数,此时该函数的第一个形参默认为this指针
class Date
{
public:Date(int year = 0, int month = 1, int day = 1) {_year = year;_month = month;_day = day;}void Print() {cout << _year << "年" << _month << "月" << _day << "日" << endl;}bool operator==(const Date& d)// 运算符重载函数{return _year == d._year && _month == d._month&&_day == d._day;}
private:int _year;int _month;int _day;
};
也可将该运算符重载函数放在类外面,但此时外部无法访问类中的成员变量,这时可以将类中的成员变量设置为共有(public),这样外部就可以访问该类的成员变量了(也可以用友元函数或提供接口解决该问题)。并且在类外没有this指针,所以此时函数的形参必须显示的设置两个
class Date
{
public:Date(int year = 0, int month = 1, int day = 1){_year = year;_month = month;_day = day;}void Print(){cout << _year << "年" << _month << "月" << _day << "日" << endl;}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;
}
5.2 赋值运算符重载特性
class Date
{
public:Date(int year = 0, int month = 1, int day = 1)// 构造函数{_year = year;_month = month;_day = day;}Date& operator=(const Date& d)// 赋值运算符重载函数{if (this != &d){_year = d._year;_month = d._month;_day = d._day;}return *this;}void Print()// 打印函数{cout << _year << "年" << _month << "月" << _day << "日" << endl;}
private:int _year;int _month;int _day;
};
- 一、参数类型设置为引用,并用const进行修饰
赋值运算符重载函数的第一个形参默认是this指针,第二个形参是赋值运算符的右操作数。由于是自定义类型传参,若是使用传值传参,会额外调用一次拷贝构造函数,所以函数的第二个参数最好使用引用传参。其次,第二个参数,即赋值运算符的右操作数,在函数体内不会对其进行修改,所以最好加上const进行修饰
- 二、函数的返回值使用引用返回
若是只以d2 = d1这种方式使用赋值运算符,赋值运算符重载函数就没必要有返回值,因为在函数体内已经通过this指针对d2进行了修改。但是为了支持连续赋值,即d3 = d2 = d1,就需要为函数设置一个返回值了。而且返回值应该是赋值运算符的左操作数,即this指针指向的对象。和使用引用传参的道理一样,为了避免不必要的拷贝,最好还是使用引用返回。此时出了函数作用域this指针指向的对象并没有被销毁,所以可以使用引用返回
- 三、赋值前检查是否是给自己赋值
若是出现d1 = d1,就不必进行赋值操作
- 四、引用返回的是*this
赋值操作进行完毕时,应该返回赋值运算符的左操作数,而在函数体内只能通过this指针访问到左操作数,所以要返回左操作数就只能返回*this
- 五、一个类如果没有显示定义赋值运算符重载,编译器也会自动生成一个,完成对象按字节序的值拷贝
赋值运算符重载编译器也可以自动生成,并且是支持连续赋值的。但是编译器自动生成的赋值运算符重载完成的是对象按字节序的值拷贝,如d2 = d1,编译器会将d1所占内存空间的值完完全全地拷贝到d2的内存空间中去,类似于memcpy。对于日期类,编译器自动生成的赋值运算符重载函数就可以满足需求。但当遇到一些特殊的类,还是得显示定义赋值运算符函数
注意
Date d2(d1);//拷贝构造函数
Date d3 = d1;//拷贝构造函数
d1 = d3;//赋值运算符重载函数
- 拷贝构造函数:用一个已经存在的对象去构造初始化另一个即将创建的对象
- 赋值运算符重载函数:在两个对象都已经存在的情况下,将一个对象赋值给另一个对象
六、const修饰成员函数
const修饰类成员函数,实际修饰的是类成员函数隐含的this指针,表明在该成员函数中不能对this指针指向的对象进行修改
void Print()const {//cosnt修饰的打印函数cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
- const对象可以调用非const成员函数吗?
- 非const对象可以调用const成员函数吗?
- const成员函数内可以调用其他的非const成员函数吗?
- 非cosnt成员函数内可以调用其他的cosnt成员函数吗?
答案是:不可以、可以、不可以、可以
- 非const成员函数,即成员函数的this指针没有被const所修饰,传入一个被const修饰的对象,用没有被const修饰的this指针进行接收,属于权限的放大,函数调用失败
- const成员函数,即成员函数的this指针被const所修饰,传入一个没有被const修饰的对象,用被const修饰的this指针进行接收,属于权限的缩小,函数调用成功
- 在一个被const所修饰的成员函数中调用其他没有被const所修饰的成员函数,即:将一个被const修饰的this指针的值赋值给一个没有被const修饰的this指针,属于权限的放大,函数调用失败
- 在一个没有被const所修饰的成员函数中调用其他被const所修饰的成员函数,即:将一个没有被const修饰的this指针的值赋值给一个被const修饰的this指针,属于权限的缩小,函数调用成功
七、取地址及const取地址操作符重载
取地址操作符重载和const取地址操作符重载,这两个默认成员函数一般不用显示定义,使用编译器自动生成的即可
class Date
{
public:Date* operator&() { return this; }//取地址操作符重载const Date* operator&()const { return this; }//const取地址操作符重载
private:int _year;int _month;int _day;
};