类的默认成员函数
默认成员函数就是我们没有显式实现,编译器会自动生成的成员函数称为默认成员函数。默认成员函数有默认构造函数,拷贝构造函数,析构函数,复制赋值运算符,移动构造函数,移动赋值运算符。对于移动构造和移动赋值这两个默认成员函数后续再详细讨论,本文我们主要讨论构造函数,析构函数,拷贝构造函数,复制赋值符成员函数,和取地址运算符重载函数。
构造函数
构造函数是特殊的成员函数,构造函数虽然名称叫构造,但是构造函数的主要的作用是对对象实例化时初始化对象。平时定义局部对象的空间在是栈帧创建时,空间就开好了。
构造函数的特点:
①构造函数的函数名与类名相同
②构造函数无返回值(不需要写void表示无返回值)
③对象实例化时会调用对应的构造函数完成对象初始化
④构造函数可以有多个,这些构造函数构成重载关系,值得一提的是拷贝构造函数也是构造函数与其他构造函数构成重载关系。
⑤如果类中没有显示定义一个默认的构造函数,编译器将会自动生成一个构造函数。
⑥无参的构造函数,全缺省的构造函数以及编译器自动生成的构造函数都称为默认构造函数。但这三个函数实际上只能存在一个,这三个构造函数不能同时存在。无参构造函数和全缺省构造函数虽然构成函数重载,但是调用时会存在歧义。不传实参就可以调用的构造就叫默认构造函数。
⑦当我们没有显示写默认构造函数时,编译器会自动生成一个默认构造函数,该构造函数对于自定义类型会调用这个自定义类型的默认构造函数如果自定义类型没有默认构造函数那么编译将会报错(如果没有默认构造我们可以走初始化列表调用其构造函数),然而对于内置类型可能会做处理也可能不会做处理(取决于编译器)。
需要注意的是:内置类型有int,double,float,char等以及指针(指针如int,char*等,以及指向一个自定义类型的指针也当成内置类型),自定义类型就是我们使class/struct等关键字自己定义的类型*
需要注意的是:当显示定义了默认构造函数(也就是不传参就可调用的构造函数),编译器便不会再生成了
class Date
{
public:
//默认构造函数
Date()
{
_year = 1;
_month = 1;
_day = 1;
}
//全缺省的默认构造函数,与上述中的午餐的默认构造函数不能同时存在
//Data(int year = 1, int month = 1, int day = 1)
//{
// _year = year;
// _month = month;
// _day = day;
//}
//带参的构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void print()
{
cout << _year << "年" << _month << "月" << _day << "日";
}
private:
int _year;
int _month;
int _day;
};
int main()
{
//如果通过无需传参的构造函数创建对象时,对象后面不用跟括号
//否则编译器无法区分这里是函数声明还是实例化对象
Data d1;
d1.print();
return 0;
}
class Date
{
public:
//默认构造函数
Date()
{
_year = 1;
_month = 1;
_day = 1;
}
private:
int _year=1;//C++11标准,这个缺省值会给初始化初始化列表使用
int _month=1;
int _day=1;
};
#include<iostream>
using namespace std;
typedef int STDataType;
class Stack
{
public:
Stack(int n = 4)
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (nullptr == _a)
{
perror("malloc申请空间失败");
return;
}
_capacity = n;
_top = 0;
}
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
// 使用两个Stack实现队列
class MyQueue
{
public:
//编译器默认生成MyQueue的构造函数调用了Stack的构造,完成了两个成员的初始化
private:
Stack pushst;
Stack popst;
};
可以这么理解,在类中的成员变量看似定义,实际上在类中只是声明。
真正的定义发生在对象实例化时,即内存分配。初始化列表紧随实例化之后,用于对普通成员变量、自定义类型的成员变量(调用其构造函数)、const 成员和引用成员进行初始化
对于简单的数据类,如存储日期的 Date 类,如果只包含基础类型且不需要特殊的初始化和资源管理,编译器生成的默认构造函数通常是足够的。
如果类中涉及到资源管理或者需要特定的初始化,显式定义构造函数是必要的,以确保对象的状态在创建时是确定的、资源是正确管理的。
析构函数
析构函数与构造函数功能相反,析构函数的主要作用是释放对象所占用的资源,而不是直接销毁对象本身。析构函数在对象的生命周期结束时被自动调用,通常用于执行清理工作。比如局部对象是存在栈帧的,函数结束栈帧销毁,这个对象本身也就释放了,不需要我们管,而析构函数释放的资源是这个对象中的内部资源,C++规定对象在销毁时会自动调用析构函数,完成对象中资源的清理释放工作。
析构函数是否需要显示的写,取决于是否有资源要释放,如存储日期的 Date 类,如果只包含基础类型且不需要特殊的处理和资源清理等,默认生成的就够用了。
①析构函数名是~ 在加上类名(如Date类,其析构函数名位~Date())
②析构函数没有参数也没有返回值(不需要加void)
③一个类只能显示定义一个析构函数,因此析构函数不能重载。当一个类中未显示定义析构函数时,编译器会自动生成一个析构函数。编译器自动生成的析构函数对于内置类型不做处理,对于自定义类型会调用这个自定义类型的析构函数完成资源的清理释放。值得一提的是我们就算自己显示写了析构函数编译器,最后也会调用自定义类型的析构函数,因此,对于自定义类型无论是否显示写,都会自动调用自定义类型的析构函数。
④对象生命周期结束之后,会自动调用对象的析构函数。
⑤如果该类中没有申请资源(如在堆上开辟空间),可以不写析构函数,直接使用编译器默认生成的析构函数就够了如Date类,如果默认生成的析构就可以用,也就不需要显示写析构(假如通过Stack实现队列MyQueue),这是由于编译器自动生成的析构函数会调用自定义类型的析构函数,而这个栈实现了析构函数,因此不需要显示写析构。有资源申请时,⼀定要自己写析构,否则会造成资源泄漏,如Stack。
⑥一个局部域的多个对象,C++规定后定义对象的先析构。
代码示例如下:
#include<iostream>
using namespace std;
typedef int STDataType;
class Stack
{
public:
//默认构造函数
Stack(int n = 4)
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (nullptr == _a)
{
perror("malloc申请空间失败");
return;
}
_capacity = n;
_top = 0;
}
~Stack()
{
delete _a;
_a = nullptr;
_top = _capacity = 0;
}
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
// 使用两个Stack实现队列
class MyQueue
{
public:
//编译器默认生成MyQueue的构造函数调用了Stack的构造,完成了两个成员的初始化
//默认生成的析构函数够用了,默认生成的队友自定义类型会调用自定义类型的析构函数,对于内置类型不做处理
//也就是说默认生成的析构函数让pushst,popst分别调用析构函数完成自身内部资源的释放
private:
Stack pushst;
Stack popst;
};
int main()
{
MyQueue mq;
return 0;
}
拷贝构造函数
如果⼀个构造函数的第⼀个参数是⾃⾝类类型的引⽤,且任何额外的参数都有默认值,则此构造函数也叫做拷贝构造函数,也就是说拷贝构造是⼀个特殊的构造函数。拷贝构造函数与默认构造函数构成重载。
拷贝构造函数的特点:
①拷贝构造函数是构造函数的⼀个重载。
②拷贝构造函数的参数只有⼀个且必须是类类型对象的引用,使用传值方式编译器直接报错,这是由于语法逻辑上会引发无穷递归调用。示意图如下所示
③C++规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参和传值返回都会调用拷贝构造完成。
④若未显式定义拷贝构造,编译器会自动生成拷贝构造函数。自动生成的拷贝构造对内置类型成员变量会完成值拷贝/浅拷贝(一个字节一个字节的拷贝),对自定义类型成员变量会调用他的拷贝构造。
⑤像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成的拷贝构造就可以完成需要的拷贝,所以不需要我们显示实现拷贝构造。像Stack这样的类,虽然也都是内置类型,但是_a指向了资源,编译器自动生成的拷贝构造完成的值拷贝/浅拷贝不符合我们的需求,所以需要我们自己实现深拷贝(对指向的资源也进行拷贝)。像MyQueue这样的类型内部主要是自定义类型Stack成员,编译器自动生成的拷贝构造会调用Stack的拷贝构造,也不需要我们显示实现MyQueue的拷贝构造。这里还有⼀个小技巧,如果⼀个类显示实现了析构并释放资源,那么他就需要显示写拷贝构造,否则就不需要。
⑥传值返回会产生⼀个临时对象调用拷贝构造,传值引用返回,返回的是返回对象的别名(引用),没有产生拷贝。但是如果返回对象是一个当前函数局部域的局部对象,函数结束就销毁了,那么使用引用返回是有问题的,这时的引用相当于一个野引用,类似一个野指针一样。传引用返回可以减少拷贝,但是一定要确保返回的对象在当前函数结束后还在,才能用引用返回。
举例说明
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//Date(Date d)// 编译报错:error C2652: “Date”: ⾮法的复制构造函数: 第⼀个参数不应是“Date”
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
Date(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;
};
void Func1(Date d)
{
cout << &d << endl;
d.Print();
}
// Date Func2()
Date& Func2()
{
Date tmp(2024, 7, 5);
tmp.Print();
return tmp;//会造成野引用,与野指针类似
}
int main()
{
Date d1(2024, 7, 5);
// C++规定自定义类型对象进行拷贝行为必须调用拷贝构造,传值传参要调用拷贝构造所以这里的d1传值传参给d要调用拷贝构造完成拷贝,传引用传参可以较少这里的拷贝
Func1(d1);
cout << &d1 << endl;
// 这里可以完成拷⻉,但是不是拷⻉构造,只是⼀个普通的构造
Date d2(&d1);
d1.Print();
d2.Print();
//这样写才是拷贝构造,通过同类型的对象初始化构造,而不是指针
Date d3(d1);
d2.Print();
Date d4 = d1;//这是拷贝构造
d2.Print();
// Func2返回了⼀个局部对象tmp的引用作为返回值
// Func2函数结束,tmp对象就销毁了,相当于了⼀个野引用
//Date ret = Func2();//可能会崩溃
ret.Print();
return 0;
}
赋值运算符重载
运算符重载
当运算符被用于类类型的对象时,C++语言允许我们通过运算符重载的形式指定新的含义。C++规定类类型对象使用运算符时,必须转换成调用对应运算符重载,若没有对应的运算符重载,则会编译报错。
运算符重载是具有特名字的函数,他的名字是由operator和后面要定义的运算符共同构成。和其他函数⼀样,它也具有其返回类型和参数列表以及函数体。
重载运算符函数的参数个数和该运算符作⽤的运算对象数量⼀样多。一元运算符有⼀个参数,二元运算符有两个参数,二元运算符的左侧运算对象传给第⼀个参数,右侧运算对象传给第二个参数。
如果⼀个重载运算符函数是成员函数,则它的第⼀个运算对象默认传给隐式的this指针,因此运算符重载作为成员函数时,参数比运算对象少⼀个。运算符重载以后,其优先级和结合性与对应的内置类型运算符保持⼀致。不能通过连接语法中没有的符号来创建新的操作符:比如operator@等
① .* ② :: ③ sizeof ④ ?: ⑤ . 注意以上5个运算符不能重载。
载操作符符至少有一个类类型参数,不能通过运算符重载改变内置类型对象的含义,如: int operator+(int x, int y)
说明:这是由于+是个二元操作符,只能传两个参数,第一个参数是该类类型*this已经占了一个,参数太多了。
⼀个类需要重载哪些运算符,是看哪些运算符重载后有意义,比如Date类重载operator-(两个日期类相减)就有意义,但是重载operator+(两个日期相加)就没有意义。
重载++运算符时,有前置++和后置++,运算符重载函数名都是perator++,无法很好的区分。因此C++规定,后置++重载时,增加⼀个int形参,跟前置++构成函数重载,方便区分。
重载<<和>>时,需要重载为全局函数,因为重载为成员函数,this指针默认抢占了第⼀个形参位置,第⼀个形参位置是左侧运算对象,调用时就变成了 对象<<cout,不符合使用习惯和可读性。重载为全局函数把ostream/istream对象cout/cin放到第⼀个形参位置就可以了,第二个形参位置当类类型对象。重载<<和>>时需要再类中奖函数声明成友元函数,以此来访问Date类中的私有成员变量。
赋值运算符重载
赋值运算符重载是⼀个默认成员函数,用于完成两个已经存在的对象直接的拷贝赋值。特别注意:赋值运算符针对的是两个已经存在的对象,拷贝构造用于一个对象拷贝初始化给另一个要创建的对象。
赋值运算符的特点:
①赋值运算符重载是运算符重载,规定必须重载为成员函数。赋值运算重载的参数建议写成const 当前类类型引用,否则会传值传参会有拷贝。
②赋值运算符重载函数有返回值,且建议写成当前类类型引用(减少拷贝),引用返回可以提高效率,有返回值目的是为了支持连续赋值场景。
③如果没有显式实现,编译器会自动生成⼀个默认赋值运算符重载函数,默认赋值运算符重载行为跟默认构造函数类似,对内置类型成员变量会完成值拷贝/浅拷贝(⼀个字节⼀个字节的拷贝),对自定义类型成员变量会调用他的拷贝构造。
④像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成的赋值运算符重载就可以完成需要的拷贝,所以不需要我们显示实现赋值运算符重载。像Stack这样的类,虽然也都是
内置类型,但是_a指向了资源,编译器⾃动⽣成的赋值运算符重载完成的值拷贝/浅拷贝不符合我们的需求,所以需要我们自己实现深拷贝(对指向的资源也进行拷贝)。像MyQueue这样的类型内部主要是自定义类型Stack成员,编译器自动生成的赋值运算符重载会调用Stack的赋值运算符重载,也不需要我们显示实现MyQueue的赋值运算符重载。如果⼀个类显示实现
了析构并释放资源,那么他就需要显示写赋值运算符重载,否则就不需要显示的写了。
实现一个较为完整的日期类
实现Date类
class Date
{
public:
//默认构造函数
Date(int year=1,int month=1,int day=1)
{
_year = year;
_month = month;
_day = day;
}
//拷贝构造函数
Date(const Date& d)
{
this->_day = d._day;
this->_month = d._month;
this->_year = d._year;
}
//获取天数
int Getmonthday(int year,int month)
{
static int monthday[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
if (month == 2 && (year % 100 != 0 && year % 4 == 0) || (year % 400 == 0))
{
return monthday[month] + 1;
}
return monthday[month];
}
void Print()
{
//打印的时候需要判断当前的日期是否合法
cout << _year << "年" << _month << "月" << _day << "日" <<" ";
if (_month > 12 || Getmonthday(_year,_month))
{
cout << "日期有误" << endl;
return;
}
cout << endl;
}
//赋值运算符重载
Date& operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
//日期比较
bool operator<(const Date& d) const
{
if (_year < d._year)
{
return true;
}
if (_year == d._year && _month < d._month)
{
return true;
}
if (_year == d._year && _month == d._month && _day < d._day)
{
return true;
}
else
{
return false;
}
}
bool operator==(const Date& d) const
{
return _year == d._year && _month == d._month && _day == d._day;
}
bool operator<=(const Date& d) const
{
return *this == d || *this < d;
}
bool operator>(const Date& d) const
{
return !(*this < d || *this == d);
}
bool operator>=(const Date& d) const
{
return !(*this < d);
}
//日期+天数,返回一个日期
Date operator+(int day) const
{
//Date tmp(*this);//拷贝构造tmp
//tmp._day += day;
//while (tmp._day > Getmonthday(tmp._year, tmp._month))
//{
// tmp._day -= day;
// tmp._month += 1;
// if (tmp._month == 13)
// {
// tmp._year += 1;
// tmp._month = 1;
// }
//}
//return tmp;
//复用+=重载操作符
Date tmp(*this);//拷贝构造
tmp += day;
return tmp;
}
//日期+=天数,返回一个日期
Date& operator+=(int day)
{
if (day < 0)
{
*this -= (-day);
}
else
{
this->_day += day;
while (this->_day > Getmonthday(this->_year, this->_month))
{
this->_day -= Getmonthday(this->_year, this->_month);
this->_month += 1;
if (this->_month > 12)
{
this->_month = 1;
this->_year += 1;
}
}
}
return *this;
}
bool operator!=(const Date& d) const
{
return !(*this == d);
}
//日期-=天数,返回一个日期
Date& operator-=(int day)
{
if (day < 0)
{
*this += (-day);
}
else
{
this->_day -= day;
while (this->_day <= 0)
{
this->_month--;
if (this->_month == 0)
{
this->_year--;
this->_month = 12;
}
this->_day += Getmonthday(this->_year, this->_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);
*this += 1;
return tmp;
}
Date& operator--()//前置--
{
*this -= 1;
return *this;
}
Date operator--(int)//后置--
{
Date tmp(*this);
*this -= 1;
return tmp;
}
//日期-日期,返回天数
int operator-(const Date& d) const //后面加个const表示不能改变this指向的内容
{
int day = 0;
Date min(*this);//拷贝构造
Date max(d);//拷贝构造
if (min > max)
{
min = d;
max = *this;
}
while (min != max)
{
min++;
day++;
}
return day;
}
//Date类不需要自己写析构函数,默认的就够用了
~Date()//析构函数无参,也无返回值,不构成重载
{
cout << "调用析构函数" << "~Date()" << endl;
}
private:
int _year;
int _month;
int _day;
};
取地址运算符重载
const成员函数
用const修饰的成员函数称之为const成员函数,const修饰成员函数放到成员函数参数列表的后面。这个const实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
class Date
{
1
2
3
4
5
比特就业课
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// 这个Print其实就如同void Print(const Date* const this)
void Print()
{
cout<<"void Print()"<<endl;
cout << _year << "-" << _month << "-" << _day << endl;
}
void Print() const
{
cout<<"void Print() const"<<endl;
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
Date类的普通成员函数隐含的这个指针形式是Date* const this(不能修改this的指向但可修改指向的内容),而const修饰成员函数隐藏的指针形式是为const Date* const this(不能修改this指向也不能修改指向的内容)
int main()
{
Date d1;
const Date d2;
d1.Print();//调用的是void Print()这个成员函数
d2.Print();//调用的是void Print()const这个成员函数
return 0;
}
说明:非const对象也可以调用const成员函数是⼀种权限的缩小,但是const对象不能调用非const成员函数(权限不能放大)
如果Date类中只有一个void Print() const的const成员函数,d1.Print()调用的是void Print() const函数
&运算符重载
取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载,⼀般这两个函数编译器自动生成的就可以够我们用了,不需要去显示实现。
除非一些很特殊的场景,比如我们不想让别人取到当前类对象的地址,就可以自己实现一份,胡乱返回一个地址。
示例:不让外边取到当前类对象的地址
class Date
{
public :
Date* operator&()
{
//return this;
return nullptr;
}
const Date* operator&()const
{
//return this;
return nullptr;
}
private :
int _year ; // 年
int _month ; // ⽉
int _day ; // ⽇
};
总结:本文主要介绍了构造函数、析构函数、拷贝构造函数、赋值运算符重载函数以及取地址运算符重载函数的特点,并且实现了一个较为完整的日期类 Date。其中构造函数、析构函数、拷贝构造函数和赋值运算符重载函数都是默认成员函数。