目录
默认成员函数是一种特殊成员函数,即便我们不定义,编译器会自己生成;我们如果定义了,编译器就不会再生成,隐含的意思:对于有些类,需要我们主动写;对于另一些类,编译器默认生成就可以用。接下来我们使用日期类来浅析几种默认成员函数。
1.构造函数
什么是构造函数?
场景:我们使用类定义出对象的时候经常需要进行初始化,但是往往又容易忘记,构造函数的产生就是为了解决这种情况。我们若在类中主动定义构造函数,编译器就不会再调用默认的构造函数;反之,若是我们没有在类中定义,则编译器会调用默认的构造函数。
其特征如下:
1.函数名与类名相同
2.无返回值
3.对象实例化时编译器自动调用对应的构造函数
4.构造函数支持重载
接下来举两个例子来看看构造函数如何使用:
//以日期类为例:
class Date
{
public:
//构造函数的功能是初始化
//方法1
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
//方法2
Date()
{
_year = 1; //年
_month = 1; //月
_day = 1; //日
}
//方法3:给定缺省值
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//用无参或者全缺省形式定义出来的构造函数都可以称作默认构造函数。从语法角度,法2和法3可以构成重载,但是因为函数调用的时候存在二义性,所以只能定义其中一种
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
}
int main()
{
Date d1(2022,9,21);
//对于无参的错误写法:Date d2(); 这样就变成了函数调用的声明了,正确写法如下:
Date d2;
}
//以数据结构栈为例
class Stack
{
public:
//使用方法3,利用缺省参数定义构造函数
Stack(int capacity = 4)
{
_a = (int*)malloc(sizeof(int)*capacity);
if(_a == nullptr)
{
perror("malloc fail!");
exit(-1);
}
_top = 0;
_capacity = capacity;
}
void Push(int x)
{
//扩容...
_a[_top++] = x;
}
private:
int* _a;
int _top;
int _capacity;
}
int main()
{
//当我们定义出对象的时候,成员变量的初始化工作已经完成了,我们无需再进行初始化
Stack st;
st.Push(1);
st.Push(2);
st.Push(3);
}
值得一提的是:C++把类型分为内置类型(基本类型)和自定义类型,内置类型一般就是语言自己提供的类型,例如:任意类型指针/int/char/short等,自定义类型例如:class/union/Stack/Queue等,但关于编译器默认生成的构造函数,编译器会对内置类型不作任何处理,自定义类型才会调用其自身的默认构造,示例如下:
class A
{
public:
A()
{
_a = 0;
cout << "A()" << endl;
}
private:
int _a;
}
class Date
{
public:
//没有定义日期类的构造函数
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
//内置类型
int _year;
int _month;
int _day;
//增加自定义类型成员
A _aa;
}
int main()
{
Date d;
//当定义出对象且没有定义构造函数的时候,_year,_month,_day这些内置类型全是随机值,而对自定义类型_aa来说,编译器会自动调用A中默认构造函数
reutrn 0;
}
2.析构函数
什么是析构函数?
与构造函数的功能相反,析构函数不是完成对象本身的销毁,局部函数的销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源清理工作
特征如下:
1.析构函数名是在类名前加上“~”
2.无参数返回值类型
3.一个类只能有一个析构函数,若未显示定义,系统会自动生成默认的析构函数,且析构函数不能重载
4.对象生命周期结束时,系统自动调用析构函数
//以数据结构栈举例:
//析构函数的利用
class Stack
{
public:
Stack(int capacity = 4)
{
_a = (int*)malloc(sizeof(int)*capacity);
if(_a == nullptr)
{
perror("malloc fail!");
exit(-1);
}
_top = 0;
_capacity = capacity;
}
void Push(int x)
{
//扩容...
_a[_top++] = x;
}
//析构函数
~Stack()
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
int* _a;
int _top;
int _capacity;
}
当对象的生命周期结束的时候,编译器会直接调用析构函数来完成对象中资源的清理,例如:释放在堆上开辟的空间等等。对于日期类而言,局部变量会随着对象生命周期的结束而销毁,所以并不需要额外定义析构函数
//思考:在函数F中,本地变量a和b的构造函数(constructor)和析构函数(destructor)的调用顺序是什么
Class A;
Class B;
void F()
{
A a;
B b;
}
//解答:构造顺序是按照语句的顺序进行构造,析构是按照构造的相反顺序进行析构,所以顺序是:a构造,b构造,b析构,a析构
//思考:设已经有A,B,C,D4个类的定义,程序中A,B,C,D析构函数调用顺序为?
C c;
int main()
{
A a;
B b;
static D d;
return 0;
}
类的析构函数调用一般按照构造函数调用的相反顺序进行调用,但是要注意static对象的存在,因为static改变了对象的生存作用域,需要等待程序结束时才会析构释放对象,全局对象先于局部对象进行构造,局部对象按照出现的顺序进行构造,无论是否为static,所以构造的顺序为 c a b d。析构的顺序按照构造的相反顺序析构,只需注意static改变对象的生存作用域之后,会放在局部 对象之后进行析构,所以顺序为 b a d c
3.拷贝构造函数
什么是拷贝构造函数?
场景:已经定义出一个对象并且被初始化后,若是还想定义出一个对象拥有跟前者一样的初始化数据,这时候就需要使用拷贝构造函数。
//对于内置类型:
int a = 9;
//若想拷贝a,仅需要:
int b = a;
//但是对于自定义类型并没有这种内置类型的赋值方法:Date d1; Date d2 = d1; 所以我们需要自己定义
特征如下:
1.拷贝构造函数是构造函数的一个重载形式
2.拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器会直接报错,因为会引发无穷递归
class Date
{
public:
//默认构造函数
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//拷贝构造函数形式(是构造函数的重载形式)
Date(Date& d)
{
_year = d._year;
_month = d._month;
_year = d._year;
}
private:
int _year;
int _month;
int _day;
}
int main()
{
Date d1(2022,9,22);
Date d2(d1); //拷贝构造
}
为什么拷贝构造函数的参数传递要用传引用传参而不使用传值传参?
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(Date& d)
{
cout << "Date 拷贝构造" << endl;
_year = d._year;
_month = d.month;
_year = d._year;
}
private:
int _year;
int _month;
int _day;
}
void Func1(Date d) //传值调用
{
cout << "Func1()" << endl;
}
void Func2(Date& d) //传引用调用
{
cout << "Func2()" << endl;
}
//调用main函数后产生如下图结果:
int main()
{
Date d1(2022,9,22);
Func1(d1);
Func2(d1);
}
可以发现,当使用传值传参的时候,形参实例化需要对d1进行拷贝,若要拷贝则需要调用拷贝构造函数;而使用传引用传参,d只是d1的别名,不会再调用拷贝构造函数,至此我们发现传值传参与传引用传参在拷贝构造函数中的区别
//再回到日期类中:
Date(Date& d);
Date(Date d);
//若使用Func1函数调用,会形成一次拷贝;若使用拷贝构造函数Date调用,则会无限递归下去,直到栈溢出
细节处理:由于拷贝构造函数并不会对值进行修改所以使用const引用来缩小权限
Date(const Date& d);
关于编译器默认生成的拷贝构造函数:
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//此处不写拷贝构造函数,观察编译器默认生成的默认拷贝构造函数的行为
private:
int _year;
int _month;
int _day;
}
int main()
{
Date d1;
//观察编译器默认生成的拷贝构造函数的结果如下图所示:
Date d2(d1);
return 0;
}
可以发现,当不定义拷贝构造函数的时候编译器对于内置类型默认拷贝构造结果是我们所想要的
区别:构造函数对内置类型不作处理,除非给定缺省值;自定义类型会去调用其本身的默认构造。而对于拷贝构造,内置类型直接按照字节拷贝;自定义类型会去调用其本身的拷贝构造函数
产生疑问:难道内置类型都不需要使用者去写拷贝构造函数吗?
关于编译器默认生成的拷贝构造函数,仅进行浅拷贝而被多次析构的问题:
//由于成员变量均为内置类型,此处不定义拷贝构造函数,使用编译器默认拷贝构造函数,观察结果
class Stack
{
public:
void CheckCapacity()
{
if (_top == _capacity)
{
_capacity = _capacity * 2;
int* tmp = (int*)malloc(sizeof(int) * _capacity);
if (tmp == nullptr)
{
perror("malloc fail");
exit(-1);
}
else
_a = tmp;
}
}
void Push(int x)
{
CheckCapacity();
_a[_top] = x;
++_top;
}
~Stack()
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
int* _a = (int*)malloc(sizeof(int)*_capacity);
int _top = 0;
int _capacity = 4;
};
//main函数调试结果如下图所示:
int main()
{
Stack st1;
st1.Push(1);
st1.Push(2);
st1.Push(3);
st1.Push(4);
Stack st2(st1);
return 0;
}
此处我们发现默认拷贝构造的结果并非我们想要的结果,仅仅只是浅拷贝,对象st1和st2中的_a维护的是同一块空间,我们希望的是st2能够对st1进行深拷贝,开辟一块新的空间。只是进行浅拷贝的后果就是,当st1与st2的生命周期结束时,会调用两次析构函数,进而对同一块空间进行两次释放,这是编译器所不允许的
需要我们主动定义拷贝构造函数:
//此时应该进行深拷贝:
Stack(const Stack& st)
{
_a = (int*)malloc(sizeof(int) * st._capacity);
if (_a = nullptr)
{
perror("malloc fail");
exit(-1);
}
memcpy(_a, st._a, sizeof(int) * st._top);
_top = st._top;
_capacity = st._capacity;
}
如何判断到底需不需要写拷贝构造函数?
结论:如果需要写析构函数的类,就需要进行写深拷贝的拷贝构造;不需要写析构函数的类(Stack...),默认生成的浅拷贝的拷贝构造就可以用(Date/MyQueue...)
4.运算符重载函数
什么是运算符重载函数?
/*场景举例:
我们可以对内置类型进行加减乘除等等操作*/
int main()
{
int a = 0, b = 1;
int c1 = a + b;
int c2 = a * b;
//如何对自定义类型进行这种操作?
Date d1(2022,9,22);
Date d2(2022,9,22);
/*情景:我们想对两个日期进行进行大小比较(d1 > d2, d1 == d2)
内置类型可以直接使用运算符进行比较,自定义类型最好写个函数去进行对比
除了比较大小的需求还有可能:d1 + 100(计算100天以后的日期),d1 - d2(计算日期差值)
但是编译器无法对日期类进行加减,此时需要函数来代替实现,但是函数的可读性不强,所以C++支持运算符重载
(运算符重载与函数重载之间没有关联)*/
return 0;
}
//换句话说:运算符重载就是为了让除了内置类型之外的自定义类型也能够使用运算符进行运算
主体:operator + 需要重载的运算符符号
函数形式:返回值类型 + 实现形式 + (参数列表)
#include<iostream>
using namespace std;
//日期类运算符重载
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
//定义出一个函数来获取每个月份的天数
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 % 400 == 0) || (year % 100 != 0 && year % 4 == 0))
return 29;
else
return monthDayArray[month];
}
}
//重载运算符+=
Date& operator+=(int day)
{
while (day > getMonthDay(_year, _month))
{
day -= getMonthDay(_year, _month);
++_month;
if (_month == 13)
{
_month = 1;
++_year;
}
}
return *this;
}
//重载运算符+
Date operator+(int day)
{
Date tmp(*this);
return tmp += day;
}
//重载运算符后置++(先用再加,也就是在用的时候其自身值并没被修改)
Date operator++(int)
{
Date tmp(*this);
tmp += 1;
return tmp;
}
//重载运算符前置++(先加再用,说明其自身值已经被修改)
Date& operator++()
{
(*this) += 1;
return *this;
}
//重载运算符<
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)
{
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;
}
//重载运算符-=
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 operator-(int day)
{
Date tmp(*this);
tmp -= day;
return tmp;
}
//重载运算符前置--
Date& operator--()
{
*this -= 1;
return *this;
}
//重载运算符后置--
Date operator--(int)
{
Date tmp(*this);
tmp -= 1;
return tmp;
}
//重载运算符d1 - d2,返回天数
int operator-(const Date& d)
{
int flag = 1;
Date longDay = *this, shortDay = d;
if (*this < d)
{
longDay = d;
shortDay = *this;
}
int gap = 0;
while (longDay > shortDay)
{
--longDay;
++gap;
}
if (*this < d)
flag = -1;
return gap * flag;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022, 9, 24);
Date d2(2022, 9, 23);
int ret = d1 - d2;
cout << ret << endl;
return 0;
}