1.类的6个默认成员函数
如果一个类中什么成员都没有,简称为空类。
空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员 函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
初始化和清理以及拷贝复制是最重要的。
析构函数不是在销毁对象,而是在清理空间。
2. 构造函数
2.1 概念
对于以下Date类:
2.2 特性
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任 务并不是开空间创建对象,而是初始化对象。
其特征如下:
1. 函数名与类名相同。
2. 无返回值。(也不需要写void)
3. 对象实例化时编译器自动调用对应的构造函数。
4. 构造函数可以重载。(这里的重载指的是函数重载)
为什么构造函数支持函数重载?因为它可能有多种初始化的方式。重载有一个限定:同一个作用域,底层用了函数名修饰规则,如果不使用函数名修饰规则,编译器在找地址的时候区分不开它们两个成员函数。
5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
6. 关于编译器生成的默认成员函数,很多人会有疑惑:不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?d对象调用了编译器生成的默认构造函数,但是d对象_year/_month/_day,依旧是随机值。也就说在这里编译器生成的默认构造函数并没有什么用??
class Date
{
public:
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d;
d.Print();
return 0;
}
解答:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型。
如:int/char/double/指针等,自定义类型就是我们使用class/struct/union等自己定义的类型
我们不写,编译器默认生成构造函数,内置类型不做处理(有些编译器也会处理,但是那是个性化行为,不是所有编译器都会处理),自定义类型会调用它的默认构造。
1.一般情况下,有内置类型成员,就需要自己写构造函数,不能用编译器自己生成的
2.全部都是自定义类型成员,可以考虑让编译器自己生成
C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。
class Date
{
public:
void Print()
{
cout << _year << " " << _month << " " << _day << endl;
}
private://这里不是初始化,这里给的是缺省值,是给编译器生成的默认构造函数用的
int _year = 2024;
int _month = 10;
int _day = 17;
};
int main()
{
Date d;
d.Print();
return 0;
}
不可以这样创建对象,会和函数声明有冲突。d1是函数名,参数列表是空,返回值是Date。
7. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。
注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。不传参就可以调用的函数就叫做默认构造函数。
3.析构函数
3.1 概念
通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没呢的?
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由 编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
3.2 特性
析构函数是特殊的成员函数,其特征如下:
1. 析构函数名是在类名前加上字符 ~。
2. 无参数无返回值类型。
3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。
析构函数对于内置类型不做处理,自定义类型会调用它的析构函数。
5. 关于编译器自动生成的析构函数,是否会完成一些事情呢?下面的程序我们会看到,编译器生成的默认析构函数,对自定类型成员调用它的析构函数。
4. 拷贝构造函数
4.1 概念
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存 在的类类型对象创建新对象时由编译器自动调用。
4.2 特征
拷贝构造函数也是特殊的成员函数,其特征如下:
1.拷贝构造函数是构造函数的一个重载形式。
2.拷贝构造函数的参数只有一个且必须是类类型对象的引用。使用传值方式编译器直接报错, 因为会引发无穷递归调用(这里编译器强制检查了,如果不强制检查会引发无穷递归的)。C++规定自定义类型传参是要调用拷贝构造的,自定义类型的传参是要调拷贝构造完成的。内置类型不用管,内置类型是直接拷贝。注:用传引用可以解决问题,用指针也可以解决问题。
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// Date(const Date& d) // 正确写法//传引用建议加const,就怕不小心改动了数据
Date(const Date& d) // 错误写法:编译报错,会引发无穷递归
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void Print()
{
cout << _year << ' ' << _month << ' ' << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(d1);
d2.Print();
return 0;
}
最上面是自定义类型传参,下面的内置类型传参。
3.若未显式定义,编译器会生成默认的拷贝构造函数。默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。自定义类型会处理,内置类型也会处理。
1.内置类型成员会完成浅拷贝/值拷贝
2.自定义成员会调用它的拷贝构造
注意:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。如果是动态开辟空间的拷贝,如果使用浅拷贝的话,程序会崩溃,问题就出现在析构的时候,对一块空间进行两次析构,并且指向同一块空间的话,一个值的修改会影响另一个。对于栈,后进先出,最后定义的对象先析构。
深拷贝的实现,简单了解一下。
4.编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗?当然像日期类这样的类是没必要的。
注意:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请 时,则拷贝构造函数是一定要写的,否则就是浅拷贝。
5.赋值运算符重载
5.1 运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其 返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
哪些日期类运算符是有意义的?
日期类的对象比较大小是有意义的,日期减日期应该是个天数,是有意义的,日期加日期没有意义。是否要重载运算符取决于这个运算符对这个类是否有意义。没意义的也可以重载,但是用到的地方少,用处不大。
友元能不用就不用,因为友元会破坏封装,一般到万不得已才用友元。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
注意:
不能通过连接其他符号来创建新的操作符:比如operator@ 重
载操作符必须有一个类类型参数
用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
.* 、:: 、sizeof 、?:、 . (.是结构体成员访问操作符)注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
5.2 赋值运算符重载
1. 赋值运算符重载格式
这个表达式如何执行?
0先赋值给了k,这个表达式还有一个返回值,这个返回值的值就是k,这个返回值又做了j的操作数,这个表达式的结果是j,j又做了i的操作数。
如果赋值运算符这样定义的话,不会支持连续赋值操作,并且赋值的时候还会调用拷贝构造。
但是这种Date返回的代价太大,可以修改一下使用传引用返回。
参数类型:const T&,传递引用可以提高传参效率
返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
检测是否自己给自己赋值
返回*this :要复合连续赋值的含义
自己给自己赋值,是可以通过的,建议加断言,避免自己给自己赋值。
2. 赋值运算符只能重载成类的成员函数不能重载成全局函数
其它的重载既可以写成全局的,也可以写成成员函数。成员变量即使是公有的也不行。复制运算符重载是默认成员函数,只能写成成员函数。但是可以声明和定义分离。任何的成员函数都可以类里面给声明,类外面给定义。默认成员函数只能是类的,因为不写会自动生成,它会有其它的一些问题。
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
int _year;
int _month;
int _day;
};
// 赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数
Date& operator=(Date& left, const Date& right)
{
if (&left != &right)
{
left._year = right._year;
left._month = right._month;
left._day = right._day;
}
return left;
}// 编译失败: // error C2801: “operator =”必须是非静态成员
原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。
3. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注 意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。默认生成的也可以完成连续赋值。Date和Myqueue不需要我们自己实现赋值重载。
默认生成赋值重载跟拷贝构造行为一样:
1.内置类型成员完成值拷贝/浅拷贝
2.自定义类型成员会调用它的复制重载
两个重载的运算符(前置++和后置++)可以构成函数重载,因为他们就是两个普通函数,只是他们两个的函数名比较特殊而已。Date& operator++();//前置++、Date& operator++(int);//后置++,增加这个int参数不是为了接受具体的值,仅仅是占位,跟前置++构成函数重载。
对于前置++和后置++,编译器会自动转化其对应的样子。
Date.h
#include<iostream>
#include<assert.h>
using namespace std;
class Date
{
//友元函数声明,加上友元就可以访问私有成员变量,友元可以定义在类中的各个位置,不考虑访问限定符
//friend void operator<<(ostream& out, const Date& d);
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& out, Date& d);
public:
Date(int year = 1, int month = 1, int day = 1);
//Date(const Date& d)
//{
// cout << "Date(const Date& d)" << endl;
// _year = d._year;
// _month = d._month;
// _day = d._day;
//}
Date(const Date& x)
{
_year = x._year;
_month = x._month;
_day = x._day;
}
void Print()const //这个地方的const修饰的是*this
{
cout << _year << " " << _month << " " << _day << endl;
}
bool operator<(const Date& x);
bool operator==(const Date& x);
bool operator<=(const Date& x);
bool operator>(const Date& x);
bool operator>=(const Date& x);
bool operator!=(const Date& x);
//加上static在类外面就可以调用,不用对象,用类域就可以调,并且没有this指针
static int GetmonthDay(int year, int month);
Date& operator+=(int day);
Date operator+(int day);
Date& operator++();//前置++
Date& operator++(int);//后置++
Date& operator-=(int day);
Date operator-(int day);
Date& operator--();
Date operator--(int);
int operator-(const Date& d);
//为什么流插入不能写成成员函数?
//因为Date对象默认占用了第一个参数,就是做了左操作数
//写出来一定是下面这个样子,但是不符合使用习惯
//d1 << cout;//转换成为d1.operator<<(cout);
private:
int _year;
int _month;
int _day;
};//void operator<<(ostream& out, const Date& d);
ostream& operator<<(ostream& out, const Date& d);
istream& operator>>(istream& in, Date& d);
Date.cpp
#include"Date.h"
Date::Date(int year , int month , int day)
{
if (month > 0 && month < 13
&& day>0 && day < GetmonthDay(year, month))
{
_year = year;
_month = month;
_day = day;
}
else
{
cout << "非法日期" << endl;
assert(false);
}
}bool Date::operator<(const Date& x)
{
if (_year < x._year)
{
return true;
}
else if (_year == x._year && _month < x._month)
{
return true;
}
else if (_year == x._year && _month == x._month && _day < x._day)
{
return true;
}
return false;
}bool Date::operator==(const Date& x)
{
if (_year == x._year && _month == x._month && _day == x._day)
{
return true;
}
return false;
}
//复用
bool Date::operator>(const Date& x)
{
return !(*this <= x);
}bool Date::operator<=(const Date& x)
{
return *this == x || *this < x;
}bool Date::operator>=(const Date& x)
{
return !(*this < x);
}
bool Date::operator!=(const Date& x)
{
return !(*this == x);
}int Date::GetmonthDay(int year, int month)
{
static int dayArr[13] = { 0,31,28,31,30,31,31,30,31,30,31,30,31 };
if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
{
return 29;
}
else
{
return dayArr[month];
}
}
//其实这里实现的是加等,如果是加的话,还要有个赋值操作
//同时加等支持连续赋值操作
//int i = 0, j = 0;
//j += i += 10;
Date& Date::operator+=(int day)
{
//用加复用的代码,不建议用复用的代码
//*this = *this + day;
//return *this;
if (day < 0)
{
return *this -= -day;
}
_day += day;
while (_day > GetmonthDay(_year, _month))
{
_day -= GetmonthDay(_year, _month);
++_month;
if (_month == 13)
{
++_year;
_month = 1;
}
}
return *this;
}Date Date::operator+(int day)
{
//用加等复用的代码,建议用复用的代码
//Date tmp(*this);
//tmp += day;
//return tmp;
Date tmp(*this);
tmp._day += day;
while (tmp._day > GetmonthDay(tmp._year, tmp._month))
{
tmp._day -= GetmonthDay(tmp._year, tmp._month);
++tmp._month;
if (tmp._month == 13)
{
++tmp._year;
tmp._month = 1;
}
}
return tmp;
}
Date& Date::operator++()
{
*this += 1;
return *this;}
Date& Date::operator++(int)
{
*this += 1;
return *this;
}Date& Date::operator-=(int day)
{
if (day < 0)
{
return *this -= -day;
}
_day -= day;
while (_day <= 0)
{
--_month;
if (_month == 0)
{
_month = 12;
--_year;
}
_day += GetmonthDay(_year, _month);
}
return *this;
}Date Date::operator-(int day)
{
Date tmp = *this;
tmp -= day;
return tmp;}
Date& Date::operator--()
{
*this -= 1;
return *this;
}Date Date::operator--(int)
{
Date tmp = *this;
*this -= 1;
return tmp;
}int Date::operator-(const Date& d)
{
Date max = *this;
Date min = d;
int flag = 1;
if (max < min)
{
max = d;
min = *this;
flag = -1;
}
int n = 0;
while (min != max)
{
++min;
++n;
}
return n * flag;
}
//void operator<<(ostream& out, const Date& d)
//{
// out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
//}
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;//返回cout,其实out就是cout
}
//如果加上const修饰,就会报错
//因为流插入要插入数据,如果加上const就不能插入数据了
//void operator<<(const ostream& out, const Date& d)
//{
// out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
//}//流提取也不能加const,因为流插入是一个对象,要修改对象里面的状态值
istream& operator>>(istream& in, Date& d)
{
int year, month, day;
in >> year >> month >> day;
if (month > 0 && month < 13
&& day>0 && day < Date::GetmonthDay(year, month))//这个地方不考虑静态的也可以用d对象去调
{
d._year = year;
d._month = month;
d._day = day;
}
else
{
cout << "非法日期" << endl;
assert(false);
}
return in;
}
text.cpp
#include"Date.h"
int main()
{
Date d1(2024, 10, 20);
//d1--;
//d1.Print();
//Date d2(d1 + 100);
//d1.Print();
//d2.Print();
//Date d3(1, 1, 2);
//d3 = d3 + 100;
//d3.Print();
//Date d1(2024, 10, 20);
//Date d2(2024, 5, 5);
//cout << d1 - d2 << endl;
//cout << d2 - d1 << endl;
//d1 << cout;//转换成为d1.operator<<(cout);
//cout << d1;//转化成为operator<<(cout, d1);
Date d2(2024, 10, 21);
Date d3(2024, 11, 11);
//刚刚定义的函数void operator<<(ostream& out, const Date& d)不支持连续赋值
//如果想支持连续赋值就要求返回值是ostream,我们会说ostream没定义啊,其实是库里面定义了
cout << d2 << d3 << d1;
cin >> d2 >> d1;
return 0;
}
比较的时候,内置类型转换成指令,自定义类型去调用它的函数。
cout<<对象,<<流插入的操作符有两个操作数,一个是cout,一个是日期类对象。cout是一个类对象,是一个ostream的类对象ostream是库定义的,是哪个库定义的,是iostream这个头文件定义的,cin是istream类型的对象。为什么内置类型能直接支持流插入呢?其实没有自动识别类型,它会转换成调用这个函数,运算符重载是让自定义类型支持运算符。
1.可以支持内置类型是库里面实现了
2.可以支持自定义类型是因为函数重载
cin是istream的对象
6.const成员
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数 隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
如果d2是const修饰的对象,发现调用普通的Print函数调不动了,其实问题就发生传参上,第一个传参是Date* this,第二个是const Date* this,想当于指针指向的内容不能发生改变,第一个是权限的平移,第二个是权限的放大。
7.取地址及const取地址操作符重载
这两个默认成员函数一般不用重新定义 ,编译器默认会生成。
取地址及const取地址操作符重载,这两个也是默认成员函数,自定义类型不能直接用相关的运算符,内置类型可以用相关的运算符。
Date* operator&()
{
cout << "Date* operator&()" << endl;
return this;
}
const Date* operator&()const
{
cout << "const Date* operator&()const" << endl;
return this;
}
Date d1;
const Date d2;
cout << &d1 << endl;
cout << &d2 << endl;
如果这样写,编译器会报错,并且我们没有写默认构造,要求const对象必须要初始化的,因为const对象只有一次初始化的机会
如果我们给了缺省值,编译器会生成默认构造,这个时候调用就可以了。其实我们不写,编译器也生成了默认构造,只是对内置类型啥都没做。
其实两个函数用的很少,什么时候用呢?
就是不想让别人取普通对象的地址
类的六个默认成员函数(默认构造函数、析构函数、拷贝构造函数、拷贝赋值运算符、赋值运算符)不能定义为全局函数。