目录
1.类的六个默认成员函数
如果一个类中什么成员都没有,简称为空类。
class A{};
空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
2.构造函数
每次类实例化出一个对象,肯定要对对象初始化,既然每次都要手动初始化,那能不能干脆直接让编译器自动调用初始化函数呢?这里构造函数其实就是这个用途。
注意:构造函数虽然叫构造,但其实是用来初始化的,可以理解成构造一个对象时自动调用的函数。
构造函数是一个特殊的成员函数,他的函数名与类名相同,是类实例化出对象时编译器自动调用的函数,并且一个对象的整个生命周期都只调用一次构造函数,这一次就是在创建对象时调用的。
其特征如下:
- 函数名与类名相同。
- 无返回值。
- 对象实例化时编译器自动调用对应的构造函数。
- 构造函数可以重载。
#include<iostream> using namespace std; class date { public: date(int year = 0, int month = 0, int day = 0) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; }; int main() { //由于没有传参,a被默认初始化成0,0,0 date a; //b被初始化成2,2,2 date b(2,2,2); //报错,前面说过对象在整个生命周期中只能调用一次构造函数, //并且这次由编译器来调用,不可自己手动调用 a.date(1, 1, 1); return 0; }
- 如果类中没有显式定义构造函数,则编译器会自动生成一个无参的构造函数。如果类中显式定义了构造函数,则编译器不会生成。
既然类中没有定义构造函数,编译器会自动生成一个,那这个自动生成的构造函数会干啥?有什么用?
答:有的编译器生成的构造函数会把成员变量初始化成0,有的编译器不做初始化,所以这里默认所以编译器生成的构造函数都不初始化。
那如果类中的一个成员变量也是一个类,成员类会不会调用它自己的构造函数?
看下面这段代码运行会有什么结果
#include<iostream>
using namespace std;
class Time
{
public:
Time()
{
cout << "Time" << endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
int _year;
int _month;
int _day;
Time _t;
};
int main()
{
Date a;
return 0;
}
下面这段呢?
#include<iostream>
using namespace std;
class Time
{
public:
Time()
{
cout << "Time" << endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
Date()
{
cout << "Date" << endl;
}
private:
int _year;
int _month;
int _day;
Time _t;
};
int main()
{
Date a;
return 0;
}
得出结论, 不论外部类的构造函数是自己写的还是编译器生成的,都会调用成员类的构造函数。
这里补充一点:C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时 可以给默认值。
class Date
{
private:
int _year = 0;
int _month = 0;
int _day = 0;
};
int main()
{
//由于Date类的成员变量在声明时给了初始值
//所以这里a初始化成了0,0,0。注意这里不是构造函数初始化的.
//相当于a是0,0,0然后再调用构造函数,
//但是编译器生成的构造函数对内置类型啥都不做,所以a依然是0,0,0
Date a;
//报错,这里不可手动初始化成1,1,1 因为这里调用的是构造函数,
//而编译器生成的构造函数没有参数,不可传参初始化
Date b(1,1,1);
return 0;
}
这里再补充一个概念:无参的构造函数和全缺省的构造函数都称为默认构造函数,而默认构造函数至多只能有一个,也可以不要。
因为编译器自动生成的构造函数是无参的构造函数,所以编译器自动生成的构造函数也是默认构造函数。
class Date
{
public:
Date(int year = 5, int month = 5, int day = 5)
{
_year = year;
_month = month;
_day = day;
}
Date()
{
_year = 1;
_month = 1;
_day = 1;
}
private:
int _year = 0;
int _month = 0;
int _day = 0;
};
int main()
{
Date a;//报错,类Date包含多个默认构造函数
return 0;
}
3.析构函数
析构函数和构造函数类似,构造函数是在对象创建时调用的,析构函数则是在对象销毁时调用的。
特性:
- 析构函数名是在类名前加上字符 ~。
- 无参数无返回值类型。
- 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:构造函数可以重载,而析构函数不能重载
- 对象生命周期结束时,C++编译系统系统自动调用析构函数。
class Date { public: Date() { cout << "Date" << endl; } ~Date() { cout << "~Date" << endl; } private: int _year = 0; int _month = 0; int _day = 0; }; int main() { Date a; return 0; }
- 关于编译器自动生成的析构函数,是否会完成一些事情呢?下面的程序我们会看到,编译器生成的默认析构函数,对自定类型成员调用它的析构函数。
class Time { public: Time() { cout << "Time" << endl; } ~Time() { cout << "~Time" << endl; } private: int _hour; int _minute; int _second; }; class Date { public: Date() { cout << "Date" << endl; } private: int _year = 0; int _month = 0; int _day = 0; Time _t; }; int main() { Date a; return 0; }
注意:如果类中有成员变量也是类的话,不论是编译器自动生成的析构函数还是自己定义的析构函数,函数内部都会调用类成员变量的析构函数。而且析构函数可以手动调用。
class Time { public: Time() { cout << "Time" << endl; } ~Time() { cout << "~Time" << endl; } private: int _hour; int _minute; int _second; }; class Date { public: Date() { cout << "Date" << endl; } ~Date() { cout << "~Date" << endl; } private: int _year = 0; int _month = 0; int _day = 0; Time _t; }; int main() { Date a; return 0; }
大家感兴趣的话,可以自己研究一下嵌套类的构造函数和析构函数 的调用顺序。
4.拷贝构造函数
拷贝构造函数是构造函数的一种,也是用来初始化的,拷贝构造一般用于用对象给对象初始化。比如说想创建一个对象B,想让B和已存在的对象A一摸一样,就可以用A来初始化B。
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型 对象创建新对象时由编译器自动调用。
特征:
1.拷贝构造函数是构造函数的一个重载形式。
class Date
{
public:
Date(int year = 2023,int month = 5,int day = 3)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& a)//这个为拷贝构造函数,是构造函数的一种重载形式
{
_year = a._year;
_month = a._month;
_day = a._day;
}
~Date()
{
cout << "~Date" << endl;
}
private:
int _year = 0;
int _month = 0;
int _day = 0;
Time _t;
};
int main()
{
Date a(2020,1,1);
Date b(a);
return 0;
}
2.拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
思考一下如果上面的拷贝构造函数Date(Date& a)换成Date(Date a)会怎么样?
所以拷贝构造函数的参数最好用引用,引用就不存在初始化对象的问题了,连构造函数都不会调用,自然也就不会陷入死循环了。
3.若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成 拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
class stack
{
public:
stack(int capacity = 4)
{
_a = (int*)malloc(sizeof(int) * capacity);
_top = 0;
_capacity = capacity;
}
~stack()
{
free(_a);
_a = nullptr;
_top = 0;
_capacity = 0;
}
void push(int x)
{
_a[_top] = x;
_top++;
}
private:
int* _a;
int _top;
int _capacity;
};
int main()
{
stack a;
a.push(1);
stack b(a);
return 0;
}
这里我没有写拷贝构造函数,如果用a来初始化b,那会调用编译器自动生成的拷贝构造函数 ,会发生什么?
可以看见a和b的内容一摸一样了,甚至连指针_a的内容也一样.这是浅拷贝,这里讲一下深拷贝浅拷贝的区别:
浅拷贝:
浅拷贝就是把变量的内容直接拷贝过去,上面就是浅拷贝。
深拷贝:
深拷贝则是把指针指向的地址的内容也拷贝。举个例子:上面的拷贝构造如果用深拷贝,那么a的_a指针和b的_a指针的内容肯定不同,他们指向两块大小相同位置不同的空间,_a里存放的是这块空间的地址,所有两个_a的内容肯定不同,但是_a所指向的这两块空间的内容相同,这就是深拷贝.
综上所述,栈的类型不能用浅拷贝,也就是说栈的拷贝构造不能由编译器来实现,必须手动实现。
4.拷贝构造函数典型调用场景:
- 使用已存在对象创建新对象
- 函数参数类型为类类型对象
- 函数返回值类型为类类型对象
5.赋值运算符重载
1.赋值运算符重载
类的第四个默认成员函数是赋值运算符重载函数,要讲这个先得讲一下运算符重载是什么,我们知道定义一个相同的函数名,但是变量、返回值、函数内容都可能不同的函数。这个过程叫函数重载,那类比一下运算符重载则是定义一个相同的运算符,但是参数、返回值、内容可能不同。赋值运算符的意思就是这个相同的运算符是赋值运算符。关键字operator运算符()。运算符重载也是一个函数,函数名是operator加上需要重载的运算符,参数是运算符的操作数。
//这是赋值运算符重载
//这个函数有两个参数,一个是this指针,一个是a;
date& operator=(date& a)
{
_year = a._year;
_month = a._month;
_day = a._day;
return *this;
}
int main()
{
date d(2020,1,1);
date b;
//这里就调用了赋值运算符重载函数,operator=,
//其中b的地址作为隐藏传参传给this指针,d被a引用
b = d;
return 0;
}
赋值运算符重载作为类的六大默认函数成员之一,如果没有显式定义,编译器也会默认生成一个。这个默认生成的和拷贝构造默认生成的都是浅拷贝,所以如果类中有涉及到动态内存的话,一定要自己写一个赋值运算符重载函数。
注意这种情况
int main()
{
date a(2020,1,1);
date b = a;
return 0;
}
这里b其实调用的是拷贝构造,并不是赋值运算符重载函数,date b = a,就等于date b(a)。
拷贝构造是在初始化的时候调用,赋值运算符重载函数是在除初始化以外的赋值的时候调用,注意这两个函数的区别。
注意:
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型参数
- 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义
- 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
- .* :: sizeof ?: . 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
2.date类基本运算符重载
1.const成员函数
如果我想比较两个日期是否相等,如果直接写a==b那肯定是错的。但是我就想这么写,有没有实现的方法?可以写一个==运算符重载函数。
bool date::operator==(date& a)
{
if (_year == a._year && _month == a._month && _day == a._day)
return true;
return false;
}
这样就能直接使用==来判断了,但是有个问题,如果有一个对象a被const修饰了,还能这样写吗?
const date a(2020,1,1);
const date b(2019,1,1);
a == b
答案是不能,因为函数内部this接收了a的地址,而this指针的类型是date* const this,也就是说可以同过this来改变a的值,b也同理,限权被放大了,所以编译器会报错,接收b的参数date& a我们可以直接在前面加个const,但是this指针是隐式接收的,我们怎么给他前面加个const?
bool date::operator==(const date& a)const
{
if (_year == a._year && _month == a._month && _day == a._day)
return true;
return false;
}
答案是在右括号外加个const,这样就相当于给*this加了个const了。const成员函数实际上这个const就是用来修饰*this的
为了避免上面的限权放大导致报错的情况,以后只要不涉及到改变对象的内容,都最好给形参加上const,
日期类除了==,还有<、 <= 、> 、>= 、!= 、+ 、+= 、- 、-= 、前置++ 、后置++ 、前置-- 、后置-- 、以及两个日期相减求相差的天数。就不一一讲了,直接把代码搬上
date.h头文件:
#include<iostream>
using namespace std;
class date
{
public:
date(int year = 2023, int month = 4, int day = 26);
date(const date& a);
bool operator==(const date& a)const;
bool operator<(const date& a)const;
bool operator<=(const date& a)const;
bool operator>(const date& a)const;
bool operator>=(const date& a)const;
bool operator!=(const date& a)const;
date& operator=(const date& a);
date operator+(int day)const;
date& operator+=(int day);
date& operator++();//前置++
date operator++(int);//后置++
date operator-(int day)const;
date& operator-=(int day);
int operator-(const date& a);
//private:
int GetMonthDay(int year, int month);
private:
int _year;
int _month;
int _day;
};
inline int MIN(int a,int b)
{
if (a < b)
return a;
return b;
}
date.cpp文件
#include"date.h"
date::date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
date::date(const date& a)
{
_year = a._year;
_month = a._month;
_day = a._day;
}
date& date::operator=(const date& a)
{
_year = a._year;
_month = a._month;
_day = a._day;
return *this;
}
bool date::operator==(const date& a)const
{
if (_year == a._year && _month == a._month && _day == a._day)
return true;
return false;
}
bool date:: operator<(const date& a)const
{
if (_year < a._year)
return true;
else if (_year == a._year && _month < a._month)
return true;
else if (_year == a._year && _month == a._month && _day < a._day)
return true;
return false;
}
bool date::operator<=(const date& a)const
{
return *this < a || *this == a;
}
bool date::operator>(const date& a)const
{
return !(*this <= a);
}
bool date::operator>=(const date& a)const
{
return !(*this < a);
}
bool date::operator!=(const date& a)const
{
return !(*this == a);
}
int date::GetMonthDay(int year, int month)
{
static int monthDayArr[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;
return monthDayArr[month];
}
date date::operator+(int day)const
{
date ret(*this);
ret += day;
return ret;
}
date& date::operator+=(int day)
{
if (day < 0)
{
return *this -= -day;
}
_day += day;
int a = GetMonthDay(_year, _month);
while (_day > a)
{
_day -= a;
++_month;
if (_month == 13)
{
++_year;
_month = 1;
}
a = GetMonthDay(_year, _month);
}
return *this;
}
date& date::operator++()//前置++
{
return *this += 1;
}
date date::operator++(int)//后置++
{
date ret(*this);
*this += 1;
return ret;
}
date date::operator-(int day)const
{
date ret(*this);
ret -= day;
return ret;
}
date& date::operator-=(int day)
{
if (day < 0)
{
return *this += -day;
}
day -= _day;
while (day >= 0)
{
--_month;
if (_month == 0)
{
--_year;
_month = 12;
}
_day = GetMonthDay(_year, _month);
day -= _day;
}
_day = -day;
return *this;
}
int date::operator-(const date& d)
{
int a = MIN(_year, d._year);
int day1 = 0, day2 = 0;
int i = a;
while (i <= _year)
{
if (i < _year)
{
day1 += (337 + GetMonthDay(i, 2));
}
else
{
int j = 1;
while (j < _month)
{
day1 += GetMonthDay(i, j);
j++;
}
day1 += _day;
}
i++;
}
i = a;
while (i <= d._year)
{
if (i < d._year)
{
day2 += (337 + GetMonthDay(i, 2));
}
else
{
int j = 1;
while (j < d._month)
{
day2 += GetMonthDay(i, j);
j++;
}
day2 += d._day;
}
i++;
}
return day1 - day2;
}
这里面有一点注意:由于前置++和后置++的运算符重载函数的函数名都是operator++,为了区分他们俩,前置++的重载函数没有显式参数,后置++的重载函数的参数需要 加个int,这个int没有实际用途,仅仅用来区分前置++和后置++。
3.流插入运算符重载
初识友元函数
如果想打印date类,能不能用cout来打印呢?答案是可以,只需要将<<操作符重载就行了,
其实cout是一个ostream类的对象,所以可以把cout传参,
//out不能加const,流插入本质上就是将out的内容改变。
ostream& operator<<(ostream& out, date& a)
{
cout << a._year << "年" << a._month << "月" << a._day << "日" << endl;
return out;
}
注意: 流插入重载不能写在类的内部,因为写在类的内部会有隐式参数this指针,而this指针默认传运算符左侧的操作数的地址,流插入的左侧只能是cout,所以会发生错误,不写在类内部就没有this指针了,但是这样会引出一个问题,就是函数没有在类域中不能访问类的成员变量,为了让类外部的函数能访问类的成员变量,引入了友元函数的概念,友元函数就是在函数声明的前面加个friend
friend ostream& operator<<(ostream& out, date& a);
友元函数放在类的内部,其相关的类外部的函数就能访问类的成员变量了。
友元函数没有公有私有之分,因为它不能被调用,所以不用区分公有私有。
class date
{
friend ostream& operator<<(ostream& out, date& a);
//其他函数就省略了,前面有
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& out, date& a)
{
cout << a._year << "年" << a._month << "月" << a._day << "日" << endl;
return out;
}
这里扩展一下:大家知道cout<<后面跟内置类型能够自动识别类型并打印,自动识别是怎么做到的?
在cplusplus.com上可以看operator<<重载函数有这么多,所以大家应该可以猜到了,所谓的自动识别类型实际上就是把所有的内置类型都给重载了一遍,不同的类型调用不同的函数,就做到了所谓的自动识别。
4.流提取运算符重载
和流插入同理,需要写在类外部,但是要在类内部写个友元函数。
class date
{
friend istream& operator>>(istream& in, date& a);
//其他函数就省略了,前面有
private:
int _year;
int _month;
int _day;
};
istream& operator>>(istream& in, date& a)
{
in >> a._year >> a._month >> a._day;
return in;
}
6.取地址操作符重载&&const取地址操作符重载
class Date
{
public :
Date* operator&()
{
return this ;
}
const Date* operator&()const
{
return this ;
}
private :
int _year ; // 年
int _month ; // 月
int _day ; // 日
};