目录
类的6个默认成员函数
一.构造函数
1.概念
2.规定和使用
1.规定
2.特性
3.C++11补丁
4.对象为什么在用默认构造函数时后面不加括号?
5.默认构造函数
二.析构函数
1.概念
2.析构函数特性
3.析构顺序
三.拷贝构造
1.概念
2.特征
3.为什么形参要用引用而不用传值调用?
4.为什么自定义类型的拷贝需要自己写,为什么默认构造函数有时候不行,要自己写拷贝构造函数?
5.总结
四.赋值运算符重载
1.运算符重载
1.概念
2.函数格式
3.调用格式
4.注意
5.类外的操作符重载使用类访问成员变量时出现权限不够的解决方法
2.赋值运算符重载
1. 赋值运算符重载格式
2.“operator =”必须是非静态成员"
3.注意
五.日期类的实现源码
1.头文件
2.源文件
3.头文件放定义,main函数所在的源文件引用报错问题
4.<<流插入操作符重载和>>流提取操作符重载
六.友元的简单引入
七.const成员
1.概念
2.const修饰的对象为什么不能直接调用成员函数?
3.为什么类的定义的默认参数this指针经过const修饰后权限缩小,外层不是const修饰的对象仍然可以调用呐?
八.取地址及const取地址操作符重载
初始化类类型的对象,构造函数名与类名相同,没有返回值,对象实例化时编译器自动调用对应的构造函数,构造函数可以重载(也可以不传参,在函数体中初始化对象的成员,可以传参,重载看需求,构造函数可以写全缺省参数,不传参与传参之间使用更加方便).
初始化传参,在实例化对象的后面加(),无参就不加
例如:日期类,成员变量包含_year,_month,_day
~Date d1(2022,10,17) d1初始化为2022年10月17日
~Data d2;注意无参不能加(),编译器自动调用默认构造函数,不写构造函数,自动生成默认构造函数。
内置类型不初始化,自定义类型会初始化,对于构造函数来说面向需求可以写也可不写,比如:栈类需要自己写,因为地址的拷贝会导致两个对象地址一样,第二次free越界访问。
因为默认构造函数只初始化自定义类型,所以C++11补丁,内置类型成员可以在声明中给一个缺省值,在对象初始化时如果没写构造函数,内置类型的成员就会初始化成声明中的缺省值。
不传参时对象后不用写(),因为防止与函数声明冲突。
默认构造函数就是无参或者全缺省或者直接不写,自己写编译器调用自己写的,不写编译器调用自动生成的默认构造函数。
不传值,编译器实例化对象时调用默认构造函数,手动写的需要传参的构造函数的话要注意传参,不然编译器会报错(下图),因为编译器不会自动生成构造函数,已经有一个了,不写构造函数而使用自动生成的默认构造函数的话要考虑内置类型的缺省值,防止出现随机值。
析构函数功能是清理,出了作用域自动调用析构函数,防止忘记销毁出现的内存泄漏,相似于stack中的Destory
1. 析构函数名是在类名前加上字符 ~。
2. 无参数无返回值类型。
3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构
函数不能重载。
4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。
上图是判断栈是否为空,直接返回st.Empty是因为栈帧销毁时自动调用析构函数,所以不需要再保存,可以直接返回,这就是析构函数的一大特性。
如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如
Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类
注意:析构时倒着析构,与栈的出栈方式一样,栈顶入、栈顶出。
只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰,防止拷贝构造函数写反而造成值被赋值反),在用已存在的类类型对象创建新对象时由编译器自动调用。
1. 拷贝构造函数是构造函数的一个重载形式。
2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,
因为会引发无穷递归调用。
因为调用拷贝构造函数时,形参需要接受需要拷贝的对象值,所以会变成死递归(调用拷贝构造函数),因为实参并不是直接拷贝给形参,而是通过拷贝构造函数来拷贝。
例如:日期类,Date d1(2022,10,18); 拷贝构造函数:Date d2(d1); 调用拷贝构造函数时形参需要接收d1的值,再次进行拷贝构造函数 从而出现死递归,一直接收d1的值。
因为自定义类型太复杂了,这里涉及到浅拷贝和深拷贝,例如:日期类就可以用默认的(称为浅拷贝),Stack类就需要用自己写的(深拷贝,不是单纯的拷贝值),否则地址的拷贝会导致两个对象指向的地址一样。
需要写析构函数的类,都需要写深拷贝的拷贝构造。
不需要写析构函数的类,默认生成的浅拷贝就可以用。
运算符重载跟函数重载不同,运算符重载是让自定义类型用运算符(比如日期类对象的赋值用=将两个对象相连,当然实例化对象时的=意思是构造,赋值时就会调用赋值运算符重载)。
返回值类型 operator 操作符(参数列表)
例如:bool operator==(const Date& a,const Date& b);
注意:运算符重载有几个操作数就要有几个参数,this也包含在其中
例如:
返回类型 operator+(Date d1,Date d2)
编译器会报错,参数太多了,因为应当有两个,实际上有三个(this,d1,d2)。
a == b,显式调用operator==(a,b)不用显式调用是因为可读性不高
Date& operator+(Date d1,Date d2),这里返回日期类的引用类型,是因为外层有可能是连续用值,所以返回的值可以再次与其他对象互用。
1.不能通过连接其他符号来创建新的操作符:比如operator@。
2.重载操作符必须有一个类类型参数。
3.用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐
藏的this。
4. .* :: sizeof ?: . 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
1.写在类当中,有隐式this指向操作符从左到右的最左边的对象,形参只用补齐其余的。
2.在类中任意位置写入操作符重载的声明,最前面加上friend即可,friend用意为友元,可以根据这个类类型的对象在类外层调用对象的成员变量。
3.也可以在类中写成员函数,例如:int GetYear(),这里有隐式的this所以不用写。
1.参数类型:const T&,传递引用可以提高传参效率
2.返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
3.检测是否自己给自己赋值
4.返回*this :要复合连续赋值的含义
原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现
一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值
运算符重载只能是类的成员函数。
用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注
意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符
重载完成赋值。
如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必
须要实现。
//date.h
#prgram once //是防止引用头文件时被重复展开
class Date
{
public:
int GetMonthDay(int year, int month)
{
static int monthDayArray[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 monthDayArray[month];
}
}
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
// 检查日期是否合法
if (!(year >= 1
&& (month >= 1 && month <= 12)
&& (day >= 1 && day <= GetMonthDay(year, month))))
{
cout << "非法日期" << endl;
}
}
void Print() const
{
cout << _year << "/" << _month << "/" << _day << endl;
}
bool operator==(const Date& d) const;
// d1 > d2
bool operator>(const Date& d) const;
// d1 >= d2
bool operator>=(const Date& d) const;
bool operator<=(const Date& d) const;
bool operator<(const Date& d) const;
bool operator!=(const Date& d) const;
// d1 += 100
Date& operator+=(int day);
// d1 + 100
Date operator+(int day) const;
// d1 -= 100
Date& operator-=(int day);
// d1 - 100
Date operator-(int day) const;
// 前置
Date& operator++();
// 后置
Date operator++(int);
// 前置
Date& operator--();
// 后置
Date operator--(int);
// d1 - d2
int operator-(const Date& d) const;//后加const自动修饰
参数this,用于不改变参数值的类成员函数后面。
private:
int _year;
int _month;
int _day;
};
//date.cpp
#include "Date.h"
bool Date::operator==(const Date& d) const
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
// d1 > d2
bool Date::operator>(const Date& d) const
{
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 Date::operator>=(const Date& d) const
{
return *this > d || *this == d;
}
bool Date::operator<=(const Date& d) const
{
return !(*this > d);
}
bool Date::operator<(const Date& d) const
{
return !(*this >= d);
}
bool Date::operator!=(const Date& d) const
{
return !(*this == d);
}
Date& Date::operator+=(int day)
{
if (day < 0)
{
//return *this -= -day;
return *this -= abs(day);
}
_day += day;
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
_month++;
if (_month == 13)
{
++_year;
_month = 1;
}
}
return *this;
}
// d1 + 100
Date Date::operator+(int day) const
{
Date ret(*this);
ret += day;
return ret;
}
// d1 -= 100
Date& Date::operator-=(int day)
{
if (day < 0)
{
//return *this -= -day;
return *this += abs(day);
}
_day -= day;
while (_day <= 0)
{
--_month;
if (_month == 0)
{
--_year;
_month = 12;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
// d1 - 100
Date Date::operator-(int day) const
{
Date ret(*this);
ret -= day;
return ret;
}
// 前置
Date& Date::operator++()
{
*this += 1;
return *this;
}
// 后置 -- 多一个int参数主要是为了根前置区分
// 构成函数重载
Date Date::operator++(int)
{
Date tmp(*this);
*this += 1;
return tmp;
}
// 运算符重载
// 函数重载
// 前置
Date& Date::operator--()
{
*this -= 1;
return *this;
}
// 后置
Date Date::operator--(int)
{
Date tmp = *this;
*this -= 1;
return tmp;
}
// d1 - d2
int Date::operator-(const Date& d) const
{
Date max = *this;
Date min = d;
int flag = 1;
if (*this < d)
//if (d > *this)
{
max = d;
min = *this;
flag = -1;
}
int n = 0;
while (min != max)
{
++n;
++min;
}
return n*flag;
}
头文件放定义多次引用会出现重定义,.cpp编译生成obj文件,所以date.cpp和test.cpp合计出现两次定义
解决方法:1.static
2.声明和定义分离
3.代码量少、频繁调用,用内联,不进符号表,不会出现符号表重定义
// 实际上operator<<(cout, d1) 但在类中隐式this指针已经指向了cout,只用写类参数即可,但是cout不是类类型,而是ostream里的函数重载,不能使用<<操作符重载,this指针(默认是当前类的指针类型)不能接收cout,所以写成全局的流插入、流提取的操作符重载,因为多次调用、且代码量少所以使用inline内联修饰。
// 在类任意位置写入friend ostream& operator<<(ostream& out, const Date& d);即可利用友元特性单独给其开放成员变量的访问权限
inline ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}
// cin >> d1 operator(cin, d1)
inline istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
friend+函数声明给权限,函数内可以通过类类型的对象参数访问类的成员,因为原来类中的有些特定成员只能在类中访问(friend可以写在类中的任何地方)。
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数
隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
因为存在this指针的权限放大,所以将const加在类中函数定义或声明的后面,例如:void print() const。
因为这是权限的缩小,权限可以缩小,但不能放大。
总结:凡是成员函数内部不改变对象*this内容的成员函数都加const
Date* operator&()
{
return this;
}
const Date* operator&() const
{
return this;
}
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需
要重载,比如想让别人获取到指定的内容!