目录
如果一个类中什么成员都没有,简称为空类,当一个类是空类时,编译器会默认生成6个默认成员函数(用户没有显式实现时,编译器会生成的成员函数称为默认成员函数)。
补充知识:
C++把类型分为了两类:
- 内置类型/基本类型,语言本身定义的基础类型,int/char/double/指针类型等
- 自定义类型,用struct/class等定义的类型
在之前我们使用C语言写代码时,常常会发生初始化和销毁经常忘记调用,有些地方要考虑多种情况,写起来繁琐。
为了解决上述的问题,c++提出了编译器自动调用的构造函数和析构函数。
一、构造函数
1.1 定义
构造函数是一种特殊的成员函数,相当于我们以前在c语言中写的初始化函数,构造函数的主要任务是初始化对象,而不是开空间创建对象。
1.2 示例
现在我们仅仅知道构造函数的概念,还无法深入了解,我们先简单的写一个正确的构造函数实例,然后一起来探讨构造函数的特性。
typedef int DataType;
#include<stdlib.h>
class Stack
{
public:
Stack(int capacity = 4) //构造函数
{
_a = (DataType*)malloc(sizeof(DataType) * capacity);
if (_a == NULL)
{
perror("malloc fail");
return;
}
_top = 0;
_capacity = capacity;
}
private:
DataType* _a;
int _top;
int _capacity;
};
int main()
{
Stack s1(5);
return 0;
}
1.3 特点
1.函数名和类名相同。
2.无返回值(也不需要写void)。
3.对象实例化时编译器自动调用对应的构造函数。
4.构造函数可以重载。
有时候有多种初始化的方式,构造函数可以重载此时显得尤为重要。
在这里我们使用栈的初始化来举例:
typedef int DataType;
#include<stdlib.h>
class Stack
{
public:
//
Stack(int capacity = 4)
{
cout << "Stack(int capacity = 4)" << endl; //为了方便我们观察是否调用了
_a = (DataType*)malloc(sizeof(DataType) * capacity);
if (_a == NULL)
{
perror("malloc fail");
return;
}
_top = 0;
_capacity = capacity;
}
//假设有一组数据,我们要把这组数据插入,作为默认的初始化
Stack(DataType* a, int n)
{
cout << "Stack(DataType* a, int n)" << endl; //为了方便我们观察是否调用了
_a = (DataType*)malloc(sizeof(DataType) * n);
if (_a = NULL)
{
perror("mallic fail");
return;
}
memcpy(_a, a, sizeof(DataType) * n);
_capacity = n;
_top = n;
}
private:
DataType* _a;
int _top;
int _capacity;
};
int main()
{
Stack s1;
int a[] = { 1,2,3 };
int n = sizeof(a) / sizeof(a[0]);
Stack s2(a,n);
return 0;
}
在上述代码中,就有两个构造函数,他们的函数名相同,参数不同,构成了函数重载。
5.如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器就不再生成。
当我们没有显式定义构造函数时,编译器会自动生成一个,那么编译器会做一些什么操作呢?
我们定义一个Data类,不写构造函数,通过运行结果来看编译器的操作:
class Data
{
public:
void Print()
{
cout << _year << " " << _month << " " << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Data d1;
d1.Print();
return 0;
}
当我们不写构造函数,编译器自动生成的默认构造函数初始化后,得到的依然是随机值,为什么呢?
编译器自动生成的构造函数:
- 对于内置类型的成员不做处理。(C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。)
- 对于自定义类型的成员,会去调用它的默认构造。
针对编译器自动生成的构造函数的特点,接下来我们来探讨在什么情况下适合自己写,什么情况下适合编译器自动生成。
接下来我们通过几个例子来说明:
struct TreeNode
{
TreeNode* _left;
TreeNode* _right;
int _val;
};
//不用写构造函数
//内置类型的成员都有缺省值,且初始化符合我们的要求
class Tree
{
private:
TreeNode* _root=nullptr; //所有类型的指针都是内置类型
};
//适合写构造函数
//我们一般想要自己初始化_val的值
struct TreeNode
{
TreeNode* _left;
TreeNode* _right;
int _val;
TreeNode(int val= 0)
{
_left = nullptr;
_right = nullptr;
_val = val;
}
};
//不需要写MyQueue的构造函数
//在这里定义两个栈,栈在之前已经写过他的初始化函数了,
//这里MyQueue类中成员变量都是自定义类型,
//且自定义类型栈已经有了构造函数,会去调用栈的构造函数
class MyQueue
{
private:
Stack _pushst;
Stack _popst;
};
结论:
1.一般情况下,构造函数都需要我们自己写。2.可以使用编译器自动生成的构造函数的情况:
a.内置类型都有缺省值,且初始化符合我们的要求。
b.全是自定义类型的构造,且这些类型都定义默认构造。
6.无参的构造函数,我们没写编译器默认生成的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。
1.4 构造函数的调用
//构造函数的调用和普通函数不同
//如果构造函数无参,或者不需要传参时,不需要加括号
Data d1; //正确
//Data d1(); //错误,这样写编译器无法区分d1是函数名还是对象,会和函数声明冲突,编
译器不好识别
//如果构造函数有参,对象+参数列表
Data d2(2022, 2, 5);
二、析构函数
2.1 定义
2.2 特点
1.析构函数名是在类名的前面加~
2.无参数无返回值类型
typedef int DataType;
#include<stdlib.h>
class Stack
{
public:
//构造函数
Stack(int capacity = 4)
{
cout << "Stack(int capacity = 4)" << endl; //为了方便我们观察是否调用了
_a = (DataType*)malloc(sizeof(DataType) * capacity);
if (_a == NULL)
{
perror("malloc fail");
return;
}
_top = 0;
_capacity = capacity;
}
~Stack() //析构函数
{
cout << "~Stack()" << endl; //为了方便我们观察是否调用了
free(_a);
_a = nullptr;
_top = 0;
_capacity = 0;
}
private:
DataType* _a;
int _top;
int _capacity;
};
int main()
{
Stack s1;
}
3.析构函数不能重载
4.如果在类中没有显式定义析构函数,那么系统会自动生成默认的析构函数
系统自动生成的默认析构函数:
1.内置类型成员不做处理。
2.自定义类型会去调用它的析构函数。
针对编译器自动生成的析构函数的特点,接下来我们来探讨在什么情况下适合自己写,什么情况下适合编译器自动生成。
接下来我们通过几个例子来说明:
//有动态申请资源_a,就需要显式写析构函数释放资源
class Stack
{
public:
~Stack()
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_top = 0;
_capacity = 0;
}
private:
DataType* _a;
int _top;
int _capacity;
};
//没有动态申请的资源,不需要写析构。
class Data
{
private:
int _year;
int _month;
int _day;
};
//需要释放资源的成员都是自定义类型,会去调用它的析构函数,不需要写析构
class MyQueue
{
private:
Stack _pushst;
Stack _popst;
};
总结:
1.一般情况下,有动态申请资源,就需要显式写析构函数释放资源。2.没有动态申请的资源,不需要写析构。
3.需要释放资源的成员都是自定义类型,不需要写析构。
5.对象生命周期结束时,C++编译器会自动调用析构函数。
三、拷贝构造函数
3.1 定义
在有些场景下,我们有拷贝构造的要求。
3.2 特点
1.拷贝构造函数是构造函数的一个重载形式,函数名也是类名,只是参数不同。
2.拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错。
class Data
{
public:
void Print()
{
cout << _year << " " << _month << " " << _day << endl;
}
Data(Data d) //错误方式
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Data d1(2022, 2, 5);
Data d2(d1);
d1.Print();
return 0;
}
根据上图,我们了解了不进行传值调用的原因,那怎么解决呢? 有两种方式:
1.指针,但是使用指针传参相对繁琐,我们不推荐使用指针。
2.传引用,相对简单,推荐写。
正确的拷贝构造函数:
class Data
{
public:
Data(int year = 2023, int month = 5, int day = 5)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << " " << _month << " " << _day << endl;
}
Data(Data& d)
{
//为了防止出现下面的错误,我们一般在形参处Data前面加const,将权限降低
/*d._year = _year;
d._month = _month;
d._day = _day;*/
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
编译器生成的默认拷贝构造函数的操作:1.内置类型完成值拷贝/浅拷贝。2.自定义类型会调用它的拷贝构造函数。
针对编译器自动生成的拷贝构造函数的特点,接下来我们来探讨在什么情况下适合自己写,什么情况下适合编译器自动生成。
接下来我们通过几个例子来说明:
class Data
{
public:
Data(int year = 2023, int month = 5, int day = 5)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << " " << _month << " " << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Data d1(2022, 2, 5);
Data d2(d1);
d1.Print();
d2.Print();
return 0;
}
上面的Data类就可以不写拷贝构造函数,编译器会对内置类型的成员完成值拷贝/浅拷贝,默认生成的拷贝构造函数就可以用。
class Stack
{
public:
Stack(int capacity = 4)
{
cout << "Stack(int capacity = 4)" << endl; //为了方便我们观察是否调用了
_a = (DataType*)malloc(sizeof(DataType) * capacity);
if (_a == NULL)
{
perror("malloc fail");
return;
}
_top = 0;
_capacity = capacity;
}
~Stack()
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_top = 0;
_capacity = 0;
}
private:
DataType* _a;
int _top;
int _capacity;
};
int main()
{
Stack st1;
Stack st2(st1);
return 0;
}
上述的代码执行会造成程序奔溃,这是因为以下原因:
编译器默认生成的拷贝构造函数对内置类型进行浅拷贝,即只拷贝值,导致两个对象的_a指向同一块空间,析构了两次,对于上述情况我们需要自己写拷贝构造函数,它需要深拷贝。
总结:
类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。
4.拷贝构造函数的调用场景
- 使用已存在的对象创建新对象
- 函数参数类型为类类型对象
- 函数返回值类型为类类型对象
四、运算符重载
4.1 定义
在C++中,内置类型可以直接使用运算符,自定义类型无法直接使用运算符,所以C++引入了运算符重载这一概念,这样我们可以使用运算符来实现自定义类型的+,-等运算。
重载的运算符是带有特殊名称的函数,函数名是由关键字 operator 和其后要重载的运算符符号构成的。
函数原型:返回值类型 operator操作符(参数列表)
4.2 例子
class Data
{
public:
Data(int year = 2023, int month = 5, int day = 5)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << " " << _month << " " << _day << endl;
}
Data(Data& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
~Data()
{
_year = 0;
_month = 0;
_day = 0;
}
bool operator<(const Data& d) //<运算符重载,函数名是operator< ,在这里第一
个参数是this指针,有一个隐含的this指针来接受
&d1,第二个参数来接受d2,他是d2的引用(别
名)
{
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;
}
else
{
return false;
}
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Data d1(2023, 9, 1);
Data d2(2023, 8,1);
cout << (d1 < d2 )<< endl; //看到是自定义类型,会转换成调用d1.operator<(d2);
return 0;
}
4.3 特点
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型参数
- 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义
- 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
- .* :: sizeof ?: . 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
4.4 流插入运算符的重载
依旧是上面的日期类,我们想要用<<运算符打印一个对象,怎么办呢?
4.4.1 前绪知识
我们想要使用<<打印一个Date类型的对象d1,类似于下面的代码。
cout<<d1;
可以发现,<<有两个操作数,一个是cout,一个是Date类型的对象。
cout 也是一个类对象,他是用ostream的类去定义了一个全局的对象,在iostream中定义。
关于<<流插入运算符,需要知道以下两点:
1.<<可以直接支持内置类型是因为库里面实现了。
2.可以支持自动识别类型是因为函数重载。
4.4.2 使用<<打印一个Date对象
我们要打印Date对象,即打印年月日,对于<<重载函数,需要注意的是:
1. 它不能作为成员函数,成员函数的第一个参数是隐含的this指针,一般第一个参数是操作符的左操作数,第二个参数是操作符的右操作数,而<<的左操作数是ostream类型的对象,明显不符,因此<<重载函数不能作为成员函数,需要写成全局的函数。
2.需要考虑访问限制,在类外面无法直接访问私有的成员变量,有两种方式解决这个问题,使用成员函数返回成员变量的值或者写成友元函数。
3.对于连续的流插入运算,例如下例:
cout<<d1<<d2<<d3;
所以<<重载函数的返回值是cout,即ostream类型,由于cout出了作用域也不会销毁,所以使用引用。
使用成员函数返回成员变量的值:
int GetYear()
{
return _year;
}
int GetMonth()
{
return _month;
}
int GetDay()
{
return _day;
}
//out是cout的别名
ostream& operator<<(ostream& out, Date& d) //这里不能加const,在Date类定义时,this指针类型是Date* const this,
{
out << d.GetYear() << " " << d.GetMonth() << " " << d.GetDay() << endl; //相当于d.GetYear(&d),如果是const Date& d,会发生权限的扩大
return out;
}
写成友元函数:
friend ostream& operator<<(ostream& out, const Date& d);
//友元函数的声明,写到Date类里面,可以写到类中的任意位置,只是一个声明
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << " " << d._month << " " << d._day << endl;
return out;
}
4.4 流提取运算符的重载
流提取操作符和流插入大同小异,不同的是>>的左操作数cin是istream的一个类对象,在这里不再赘述。
friend istream& operator>>(istream& in,Date& d);
istream& operator>>(istream& in,Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
五、赋值运算符重载
在这里有两个对象d1,d2,我们想要把d2的值赋值给d1,
int main()
{
Data d1(2023, 9, 1);
Data d2(2023, 8,1);
d1 = d2;
return 0;
}
怎么办?使用=运算符重载。
5.1 注意
在上面的运算符重载的特点中,我们指出用于内置类型的运算符,其含义不能改变,当然运算符的特性也不能改变.
//对于内置类型
int x,y,z;
z = 5;
x = y =z; //支持连续赋值
//对于自定义类型,我们使用运算符重载也要满足连续赋值的特点
Data d1(2023,4,1);
Data d2(2025,4,3);
Data d3(2027,4,3);
d1 = d2 = d3; //自定义类型也要支持连续赋值
我们赋值运算符是从右向左执行的,在上述图片中,现将d3的值赋值给d2,然后把d2=d3这个表达式的值赋值给d1,d2=d3表达式的结果是d2,所以operator=函数的返回值类型应该是Data类型的,其次由于d2出了函数依然存在,所以这里我们可以使用引用返回即返回类型是Data&。
5.2 代码
由于我们要去访问私有的成员变量,在这里我们依然把函数写到日期类(在<运算符重载函数那已经写过在这里只写=运算符重载,不再写类)中。
Data& operator=(const Data& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
在一些时候可能会出现d1=d1的时候,如果我们不想要出现自己给自己赋值的情况,可以对代码进行优化。
Data& operator=(const Data& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
}
5.3 赋值重载和拷贝构造的区别
拷贝构造函数是用一个已经存在的对象初始化另一个对象。
Data d1(2023,3,5);
Data d2(d1); //使用d1去拷贝构造d2
赋值运算符重载函数是已经存在的两个对象之间的复制拷贝。
Data d1(2023,3,3);
Data d2(2025,4,4);
d1 = d2; //d1和d2都已经存在,使用d2赋值给d1
5.4 特性
1.对于内置类型的成员-值拷贝/浅拷贝(以值的方式逐字节拷贝)
六、Data类的完整实现
6.1 Date.h
#pragma once
#include<iostream>
#include<assert.h>
using namespace std;
class Date
{
friend istream& operator>>(istream& in,Date& d);
friend ostream& operator<<(ostream& out, const Date& d);
public:
Date(int year=2023, int month=6, int day=8);
void Print() const
{
cout << _year << " " << _month << " " << _day << endl;
}
bool operator<(const Date& x) const;
bool operator==(const Date& x)const;
bool operator<=(const Date& x)const;
bool operator>(const Date& x)const;
bool operator>=(const Date& x)const;
bool operator!=(const Date& x)const;
//得到每个月的天数
int GetMonthDay(int year, int month)const;
//运算符重载并没有规定必须是两个相同类型的运算数进行操作
Date& operator-=(int day);
Date operator-(int day)const;
Date& operator+=(int day);
Date operator+(int day)const;
//前置++
Date& operator++();
//后置++
//为了让后置++重载函数和前置++重载函数构成重载,在这里参数列表中增加一个int类型的参数,增加这个int类型的参数不是为了接收具体的值,仅仅是占位的作用
Date operator++(int);
Date& operator--();
Date operator--(int);
//两个日期相减
int operator-(const Date& d);
/*int GetYear()
{
return _year;
}
int GetMonth()
{
return _month;
}
int GetDay()
{
return _day;
}*/
private:
int _year;
int _month;
int _day;
};
6.2 Date.cpp
#include "Data.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) const
{
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;
}
else
{
return false;
}
}
bool Date::operator==(const Date& x) const
{
return _year == x._year
&& _month == x._month
&& _day == x._day;
}
bool Date::operator!=(const Date& x) const
{
return !(*this == x);
}
bool Date::operator<=(const Date& x) const
{
return *this == x || *this < x;
}
bool Date::operator>(const Date& x) const
{
return !(*this <= x);
}
bool Date::operator>=(const Date& x) const
{
return !(*this < x);
}
int Date::GetMonthDay(int year, int month) const
{
static int daysArr[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 daysArr[month];
}
}
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);
}
return *this;
}
Date Date::operator-(int day)const
{
Date tmp(*this);
tmp._day -= day;
while (tmp._day <= 0)
{
tmp._month--;
if (tmp._month == 0)
{
tmp._year--;
tmp._month = 12;
}
tmp._day += GetMonthDay(tmp._year, tmp._month);
}
return tmp; //不能使用引用返回,出了作用域tmp销毁
}
Date& Date::operator+=(int day)
{
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)const
{
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;
}
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 (*this < d)
{
max = d;
min = *this;
flag = -1;
}
int n = 0;
while (min != max)
{
++min;
++n;
}
return n * flag;
}
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << " " << d._month << " " << d._day << endl;
return out;
}
istream& operator>>(istream& in,Date& d)
{
int year, month, day;
in >> year >> month >> day;
if (month > 0 && month < 13 && day>0 && day <= d.GetMonthDay(year, month))
{
d._year = year;
d._month = month;
d._day = day;
}
else
{
cout << "非法日期" << endl;
assert(false);
}
return in;
}
6.3 Test.cpp
#include "Data.h"
void TestDate1()
{
Date d1(2023, 4, 8);
d1 += 100;
d1.Print();
Date d2(2026, 5, 9);
Date d3 = d2 + 1000; //在这里是拷贝构造,不是赋值,拷贝构造函数---用一个已经存在的对象初始化另一个对象,运算符重载函数---已经存在的两个对象之间复制拷贝
d2.Print();
d3.Print();
}
void TestDate2()
{
Date d1(2023, 9, 16);
++d1;
d1.Print();
d1++;
d1.Print();
}
void TestDate3()
{
Date d1(2023, 4, 35);
d1 -= 100;
d1.Print();
}
void TestDate4()
{
Date d1(2023, 5, 5);
Date d2(2000, 8, 9);
int x = (d1 - d2);
cout << x << endl;
cout << d1 << d2;
}
void TestDate5()
{
Date d1(2023, 5, 9);
d1.Print(); //d1.Print(&d1) Data*
//const Date d2(2023, 6, 6);
//d2.Print(); //d2.Print(&d1) const Data* 权限的放大
}
int main()
{
TestDate4();
return 0;
}
七、取地址及const取地址操作符重载
这两个默认成员函数一般不用重新定义,编译器会自动生成。
Date* operator&()
{
return this;
}
const Date* operator&()const
{
return this;
}
总结:对于这六个默认成员函数如果不显式定义,编译器会自动生成,其次这六个默认成员函数都不能写在全局域,否则会形成冲突。