目录
1. 类的6个默认成员函数
2. 构造函数
3. 析构函数
4. 拷贝构造函数
5. 运算符重载
5.1运算符重载
5.2赋值运算符重载
5.3前置++和后置++重载
5.4日期类的实现
6. const成员函数
7. 取地址及const取地址操作符重载
1. 类的6个默认成员函数
对于一个空类,编译器会自动生成以下6个默认成员函数
2. 构造函数
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次
构造函数的主要任务不是开空间创建对象,而是初始化对象
构造函数的特性:
1️⃣函数名和类名相同
2️⃣无返回值
3️⃣对象实例化时编译器自动调用对应的构造函数
4️⃣构造函数可以重载
调用无参构造函数初始化对象时,对象后面不用跟括号,否则会和函数声明混淆;
5️⃣如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义则编译器不再自动生成
编译器生成的默认构造函数:
在上面的日期类中,我们创建了一个对象,通过默认构造函数初始化后,_year,_month,_day中仍是随机值,那编译器生成的默认构造函数的意义是什么呢?
C++中把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如int、char等;自定义类型就是我们使用关键字class/struct/union等自己定义的类型。编译器生成的默认构造函数会对自定义类型成员调用它的默认构造函数,而内置类型不作处理,C++11中,针对内置类型成员不初始化的缺陷,打了补丁:内置类型成员变量在类声明时可以给默认值
定义如下一个类:
class Time
{
public:
Time()
{
cout << "调用构造函数Time()" << endl;
_hour = 0;
_minute = 0;
_second = 0;
}
private:
int _hour;
int _minute;
int _second;
};
在日期类中添加我们自定义类型的一个成员变量_time,调试可以发现,创建一个对象时,会调用自定义类型成员变量的构造函数,完成初始化。
内置类型成员变量在类声明时可以给默认值
实际上这种方式是给内置类型的成员变量一个缺省值
内置类型的成员变量初始化:
6️⃣无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认的构造函数只能有一个,若存在多个,则调用时存在二义性。无参构造函数,全缺省的构造函数,编译器默认生成的构造函数,都可以认为是默认构造函数
默认构造函数:不传参就可以调用的构造函数
3. 析构函数
析构函数和构造函数的功能相反,对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。析构函数不是完成对对象本身的销毁,局部对象的销毁工作是由编译器完成的
析构函数的特性:
1️⃣析构函数名是在类名前加上~
2️⃣无参数无返回值类型,因此析构函数不支持重载
3️⃣一个类只能有一个析构函数,若未显式定义,则编译器会自动生成默认的析构函数
4️⃣对象的生命周期结束时,系统自动调用析构函数
5️⃣编译器生成的默认析构函数,对自定义类型成员调用它的析构函数
📖Note:
当创建的Date类对象销毁时,要保证其内部所有的成员都可以被销毁,test1函数中没有直接调用Time类的析构函数,而是调用Date类的析构函数(编译器自动生成的),Date类的析构函数中才会调用Time类的析构函数。
创建哪个类的对象则调用该类的构造函数,销毁哪个类的对象则调用该类的析构函数
6️⃣如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;有资源申请时,需要自己写析构函数,否则会造成资源泄露,比如Stack类
4. 拷贝构造函数
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用
拷贝构造函数的特性:
1️⃣拷贝构造函数是构造函数的一个重载形式
2️⃣拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值传参的方式编译器会直接报错,因为会引发无穷递归调用(一次传值传参就调用一次拷贝构造)
传值传参引发的无穷递归调用:
函数调用时,形参是实参的一份临时拷贝,在传值传参的过程中,会产生一个临时中间变量,先将d1拷贝给这个临时变量,这次拷贝会调用拷贝构造函数,而拷贝构造函数的传传参方式是传值传参,所以每次调用拷贝构造都会产生临时变量,调用拷贝构造函数,因此引发了无穷的函数调用。传参过程如下图:
3️⃣若未显式定义,编译器会自动生成默认的拷贝构造函数。默认的拷贝构造函数对内置类型的成员变量按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝;对自定义类型的成员变量通过调用其拷贝构造函数完成拷贝
class Stack
{
public:
//构造函数
Stack(size_t capacity = 10)
{
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (nullptr == _array)
{
perror("malloc failed");
return;
}
_capacity = capacity;
_top = 0;
}
//析构函数
~Stack()
{
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_top = 0;
}
}
//压栈
void Push(const DataType& data)
{
//扩容
CheckCapacity();
_array[_top] = data;
++_top;
}
private:
//扩容
void CheckCapacity()
{
//栈满
if (_top == _capacity)
{
int newcapacity = _capacity * 2;
DataType* tmp = (DataType*)realloc(_array, newcapacity * sizeof(DataType));
if (tmp == NULL)
{
perror("realloc failed\n");
return;
}
_array = tmp;
_capacity = newcapacity;
}
}
private:
//变量定义
DataType* _array;
size_t _capacity;
size_t _top;
};
如上图,当我们没有显式定义拷贝构造函数,使用系统自动生成的拷贝构造函数时,对于栈类,一次拷贝构造后,栈s1和栈s2指向了同一块内存空间
程序退出时,栈s1和栈s2需要销毁,后定义变量的先销毁,即s2先销毁,调用析构函数,将这块内存空间释放,s2再销毁,调用析构函数,也要释放这块空间,但此时这块空间已经被释放了,因此程序会崩溃
以上过程是一次浅拷贝的过程;对于栈类,需要显式定义拷贝构造函数以实现深拷贝,即使用栈s1拷贝构造栈s2时,需要开辟和s1相同大小的空间,并将s1中的值拷贝过来,这样栈s1和栈s2都拥有自己的空间,二者相互独立,不影响后续的操作
📖Note:
类中如果没有涉及资源申请时,拷贝构造函数可以不显式定义,如日期类;一旦类中涉及资源申请,则一定要显式定义拷贝构造函数,否则拷贝就是浅拷贝,如栈
总结:需要写析构函数的类,都要显式定义深拷贝的拷贝构造函数,其他类使用默认生成的拷贝构造函数即可
4️⃣拷贝构造函数典型的调用场景
- 使用已存在对象创建新对象(上述例子)
- 函数参数类型为类类型对象
- 函数返回值类型为类类型对象
总结:
为了提高程序效率,对象传参时尽量使用引用传参,返回时根据实际情况尽量使用引用返回
5. 运算符重载
5.1运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数
函数原型:返回值类型 operator操作符(参数列表)
📖Note:
- 不能通过连接其他符号来创建新的操作符,如operator@
- 重载操作符必须有一个类类型参数
- 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义
- 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this指针
- 不能重载的5个运算符:⑴ .* 、⑵ :: 、 ⑶sizeof 、⑷? : 、⑸ .
操作符==的重载
全局的operator==存在的问题:私有的成员变量不能访问;如果将成员变量设置成公有的,则封装性不能保证
解决方案:
- 直接重载成成员函数
- 定义一个公有的成员函数GetYear(),获取私有的成员变量
- 使用友元
1️⃣直接重载成成员函数
此时形参列表中只需要显式定义一个参数,因为第一个参数为隐含的this指针
//日期类
class Date
{
public:
//操作符==重载
bool operator==(const Date& d)
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
private:
int _year;
int _month;
int _day;
};
2️⃣定义一个公有的成员函数GetYear(),获取私有的成员变量
注意获取成员函数的函数需要用const修饰
//日期类
class Date
{
public:
//获取私有成员函数
int GetYear(const Date& d) const
{
return d._year;
}
int GetMonth(const Date& d) const
{
return d._month;
}
int GetDay(const Date& d) const
{
return d._day;
}
private:
int _year;
int _month;
int _day;
};
//全局的operator==
bool operator==(const Date& d1,const Date& d2)
{
return d1.GetYear(d1) == d2.GetYear(d2)
&& d1.GetMonth(d1)== d2.GetMonth(d2)
&& d1.GetDay(d1) == d2.GetDay(d2);
}
int main()
{
Date d1(2024, 2, 9);
Date d2(2024, 2, 10);
cout << (d1 == d2) << endl; //输出0
return 0;
}
3️⃣使用友元
//日期类
class Date
{
public:
//全缺省的构造函数
Date(int year = 2024, int month = 2, int day = 8)
{
_year = year;
_month = month;
_day = day;
}
//拷贝构造
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
//声明全局的operator==为友元函数
friend bool operator==(const Date& d1, const Date& d2);
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;
}
5.2赋值运算符重载
1️⃣赋值运算符重载格式:
- 参数类型:const T&,引用传参可以提高传参效率(T表示数据类型)
- 返回值类型:T&,引用返回可以提高返回的效率,有返回值的目的是为了支持连续赋值
- 检测是否自己给自己赋值
- 返回*this:要符合连续赋值的含义
//日期类
class Date
{
public:
//全缺省的构造函数
Date(int year = 2024, int month = 2, int day = 8)
{
_year = year;
_month = month;
_day = day;
}
//拷贝构造
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._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;
};
赋值运算符重载:
参数列表中,只写了一个参数,实际上有两个参数,第一个参数是隐含的this指针,赋值操作符需要区分左右操作数,第一个参数是左操作数,第二个参数是右操作数
返回值:this指针是左操作数的地址,返回*this,即左操作数的值,除了赋值重载函数的作用域,左操作数这个对象还在,因此可以采用引用返回,避免一次拷贝构造,提高效率
返回值的类型是类类型,不需要加const,因为可能会对返回值做修改
连续赋值的情况:上述代码中,先执行d5 = d4,调用赋值重载,d5=d4转换为Date& operator (&d5,&d4),把d4的值拷贝给d5,并返回左操作数d5,因此d5 = d4这个表达式的值为d5,因此接下来执行的就是d6 = d5,即把d5的值拷贝给d6,和上述同样的操作,完成了一次连续赋值
2️⃣赋值运算符只能重载成类的成员函数而不能重载成全局函数
赋值运算符如果在类内不显式实现,编译器会生成一个默认的赋值运算符重载。此时用户在类外实现的全局的赋值运算符重载会和编译器在类内生成的默认赋值运算符重载产生冲突,故赋值运算符重载只能是类的成员函数
3️⃣用户没有显式实现时,编译器会生成一个默认的的赋值运算符重载,以值的方式逐字节拷贝
📖Note:
内置类型的成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值
栈的赋值重载:拷贝构造不能完成使用一个栈初始化另一个栈的操作,因为拷贝构造使两个栈指向了同一块空间,程序运行结束时析构两次会导致程序崩溃
当我们没有显式定义赋值运算符重载时,使用编译器在类内生成的默认赋值运算符重载,以值的方式逐字节拷贝每个成员变量的值,由上图,两个栈赋值时会发生错误
以值的方式逐字节拷贝,会导致栈s1和栈s2中的指针同时指向s2指向的空间,而原来s1指向的空间并没有释放,导致了内存泄漏;同时指向的这同一块空间会在程序结束时被析构两次
解决方案:在栈类内显式定义赋值运算符重载,为两个栈开辟不同的空间,使二者的结构独立
Stack& operator=(const Stack& st)
{
if (this != &st)
{
free(_array);//释放原来空间
_array = (DataType*)malloc(sizeof(DataType) * st._capacity);
if (_array == nullptr)
{
perror("malloc failed");
exit(-1);
}
memcpy(_array, st._array, sizeof(DataType) * st._top);
_top = st._top;
_capacity = st._capacity;
}
return *this;
}
if语句的作用是避免同一个栈自己给自己赋值
释放被赋值的对象原来空间后,开辟和右操作数对应的栈相同大小的空间,并将栈中的数据拷贝过去,就实现了两个栈独立存在
返回*this是为了可以实现连续的赋值操作
5.3前置++和后置++重载
前置++:先++后使用 ===>返回值是++后的值
this指向的对象函数结束后不会销毁,因此可以使用引用返回
后置++:先使用后++ ===>返回值是++前的值
前置++和后置++都是一元运算符,为了使前置++和后置++能正确重载,C++规定:后置++运算符重载时多增加一个int类型的参数,但调用函数时该参数不用传递,由编译器自动传递。后置++要返回旧值,因此需要创建临时变量存储++前的值,最终返回的也是旧值,因为旧值存放在临时变量中,因此只能传值返回,不能引用返回
//日期类
class Date
{
public:
//构造函数
Date(int year = 2024, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//打印
void print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
//前置++
Date& operator++()
{
_day += 1;
return *this;
}
//后置++
Date operator++(int)
{
Date tmp(*this);//拷贝构造,存储++前的数据
_day += 1;
return tmp;
}
private:
int _year;
int _month;
int _day;
};
📖Note:
对于后置++来说,它比前置++多了两次拷贝,一次调用拷贝构造,一次传值返回;因此对于自定义类型的变量,尽量使用前置++
前置--和后置--的实现和++类似
//前置--
Date& operator--()
{
_day -= 1;
return *this;
}
//后置--
Date operator--(int)
{
Date tmp(*this);//拷贝构造,存储++前的数据
_day -= 1;
return tmp;
}
5.4日期类的实现
//日期类
class Date
{
public:
Date(int year = 2024, int month = 2, int day = 6)
{
_year = year;
_month = month;
_day = day;
//cout << "调用构造函数" << endl;
}
//拷贝构造函数
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
//cout << "调用拷贝构造函数" << endl;
}
//析构函数
~Date()
{
//cout << "调用析构函数" << endl;
}
//打印
void print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
//内置类型
int _year = 1 ;
int _month = 1;
int _day = 1;
};
以上是一个简单的日期类,我们已经实现类赋值运算符的重载,操作符==的重载,以下我们将实现>、>=、<、<=、+、+=、-、-=等运算符的重载
1️⃣>和>=的重载
//>的重载
bool operator>(const Date& d)
{
if (_year > d._year)
{
return true;
}
else if (_year == d._year && _month > d._month)
{
return true;
}
else if (_year == d._year && _month == d._month && _day > d._day)
{
return true;
}
return false;
}
//>=的重载
bool operator>=(const Date& d)
{
return *this > d || *this == d;
}
2️⃣<和<=的重载
//<的重载
bool operator<(const Date& d)
{
if (_year < d._year)
{
return true;
}
else if (_year == d._year && _month < d._month)
{
return true;
}
else if (_year == d._year && _month == d._month && _day < d._day)
{
return true;
}
return false;
}
//<=的重载
bool operator<=(const Date& d)
{
return *this < d || *this == d;
}
3️⃣+和+=的重载
日期类对象的假发涉及进位问题,因为存在大月小月,平年闰年问题,我们可以使用月份数组来获取某年某个月的天数
+=等重载可以引用返回,+的重载要返回临时变量中的值,只能传值返回
//月份数组
int GetMonthDay(int year, int month)
{
int MonthDayArrary[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
//闰年
if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
{
return 29;
}
else
{
return MonthDayArrary[month];
}
}
//+=的重载
Date& operator+=(int day)
{
_day += day;//日期+day天
//天数超出
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
_month++;
//月份超出
if (_month == 13)
{
++_year;
_month = 1;//年满之后,置1月份
}
}
return *this;
}
//+的重载
Date operator+(int day)
{
Date ret(*this);//拷贝构造
ret += day;
return ret;
}
4️⃣-和-=的重载:类似于+和+=,不够减则借位
//-=的重载
Date& operator-=(int day)
{
_day -= day;//日期+day天
//本月天数不够减,需要借位
while (_day < 0)
{
--_month;
//本年月数不够减,需要借位
if (_month == 0)
{
--_year;
_month = 12;//新的一年
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
//-的重载
Date operator-(int day)
{
Date ret(*this);//拷贝构造
ret -= day;
return ret;
}
+=、+、-、-=重载的完善:当某个日期加的天数是一个负数时,需要对这种情况进行处理,加一个负数就是减去这个负数的相反数(一个正数),调用减法的重载即可;当某个日期减的天数是一个负数时,相当于加去这个负数的相反数(一个正数),调用加法的重载即可;
//+=的重载
Date& operator+=(int day)
{
//加一个负数===>减其相反数
if (day < 0)
{
return *this -= abs(day);
}
_day += day;//日期+day天
//天数超出
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
_month++;
//月份超出
if (_month == 13)
{
++_year;
_month = 1;//年满之后,置1月份
}
}
return *this;
}
//+的重载
Date operator+(int day)
{
Date ret(*this);//拷贝构造
//加一个负数===>减其相反数
if (day < 0)
{
ret -= abs(day);
return ret;
}
ret += day;
return ret;
}
//-=的重载
Date& operator-=(int day)
{
//减一个负数 ===>加其相反数
if (day < 0)
{
return *this += abs(day);
}
_day -= day;//日期+day天
//本月天数不够减,需要借位
while (_day < 0)
{
--_month;
//本年月数不够减,需要借位
if (_month == 0)
{
--_year;
_month = 12;//新的一年
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
//-的重载
Date operator-(int day)
{
Date ret(*this);//拷贝构造
//减一个负数 ===>加其相反数
if (day < 0)
{
ret += abs(day);
return ret;
}
ret -= day;
return ret;
}
6. const成员函数
const成员函数:将const修饰的成员函数称为const成员函数,const修饰类成员函数,实际修饰的是该成员函数隐含的this指针,表明该成员函数不能对类的任何成员进行修改
编译器对const成员函数的处理如下:
一些const修饰变量或函数时的权限问题:
以上代码属于const修饰时的权限放大问题:把一个使用const修饰的变量作为实参传递给一个没有const修饰的形参,编译器报错
以上代码中,成员函数使用const修饰后, this指针指向的对象为const Date类型的,因此实参指向的对象可以是const Date类型的,也可以是Date类型的,因为权限平移和权限缩小都是允许的
总结:
- const对象不可以调用非const成员函数(权限放大不允许)
- 非const对象可以调用const成员函数(权限缩小允许)
- const成员函数内不可以调用其他的非const成员函数(权限放大不允许)
- 非const成员函数内可以调用其他的const成员函数(权限缩小允许)
凡是内部不改变成员变量,也就是*this对象数据的,这些成员函数都应该加const修饰
7. 取地址及const取地址操作符重载
这两个运算符一般不需要重载,使用编译器默认生成取地址的重载即可
//日期类
class Date
{
public:
//取地址重载
Date* operator&()
{
return this;
}
//const修饰的取地址操作符重载,给const对象调用
const Date* operator&() const
{
return this;
}
private:
int _year;
int _month;
int _day;
};