文章目录
初识篇介绍了部分类和对象的概念以及类对象相关问题,今天继续学习类的六个默认成员函数。
类的6个默认成员函数
前面提到C++会使用大量的类,如果一个类中什么成员都没有,简称为空类。
class Date
{};
那空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
构造函数
构造函数的概念
以一个日期类演示
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;
};
int main()
{
Date d1;
d1.Init(2022, 7, 5);
d1.Print();
Date d2;
d2.Init(2022, 7, 6);
d2.Print();
return 0;
}
对于Date类,可以通过 Init 公有方法给对象设置日期,但如果每次创建对象时都调用该方法设置
信息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢?答案是构造函数
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。
构造函数的特性
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
其特征如下:
一:函数名与类名相同。
二: 无返回值。
- 无需任何类型(连void也不需要,单纯只有函数名)
三:对象实例化时编译器自动调用对应的构造函数。
- 创建对象时编译器自动调用。
四: 构造函数可以重载。
- 所以你可以有多种初始化方式,例如拷贝构造也是构造函数。
class Date
{
public:
// 1.无参构造函数
Date()
{}
// 2.带参构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
void TestDate()
{
Date d1; // 调用无参构造函数
Date d2(2015, 1, 1); // 调用带参的构造函数
// 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
// 以下代码的函数:声明了d3函数,该函数无参,返回一个日期类型的对象
// warning C4930: “Date d3(void)”: 未调用原型函数(是否是有意用变量定义的?)
//Date d3();错误示范,被认为时函数
}
五: 如果类中没有显式定义构造函数(自己没有写),则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。如下:
class Date
{
public:
/*
// 如果用户显式定义了构造函数,编译器将不再生成
Date(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;
};
int main()
{
Date d1;
// 将Date类中构造函数屏蔽后,代码可以通过编译,因为编译器生成了一个无参的默认构造函数
// 将Date类中构造函数放开,代码编译失败,因为一旦显式定义任何构造函数,编译器将不再生成
//此时,没有无参的构造函数,编译器就会报错
//解决方案:使用全缺省的构造函数
return 0;
}
六: 那是不是意味着我们自己就不需要写构造函数了,直接用编译器默认生成的就可以了?
当然不是,就以这个日期类来说。使用编译器自动生成的构造函数并没有对我们的数据初始,仍然是随机值。
这里就不得不提编译器的机制了
编译器自动生成的构造函数机制:
1、编译器自动生成的构造函数对内置类型不做处理。
2、对于自定义类型,编译器会再去调用它们自己的默认构造函数。
嗯。。。。挺bug的,所以在C++11中对此进行了补充:
C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。
七: 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。(也就是说有全缺省的就不需要无参的了,也建议使用全缺省的构造函数)
- 注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数(也可以理解为不需要传参的构造函数就是默认构造函数)。
析构函数
析构函数的概念
通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没呢的?
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由
编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
- 这里的资源一般指独立申请的空间(如在堆区申请的空间)。
class Date
{
public:
Date()// 构造函数
{}
~Date()// 析构函数
{}
private:
int _year;
int _month;
int _day;
};
析构函数的特性
析构函数是特殊的成员函数,其特征如下:
一: 析构函数名是在类名前加上字符 ~。
二: 无参数无返回值类型。
三: 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载。
四: 对象生命周期结束时,C++编译系统系统自动调用析构函数。
五: 关于编译器自动生成的析构函数,是否会完成一些事情呢?
其实关于编译器默认生成的构造函数和析构函数对内置类型和自定义类型的处理方式一致
- 对内置类型不处理。
- 对自定义类型调用它的析构函数。
所以编译器生成的默认析构函数,对自定类型成员调用它的析构函数,虽然对内置类型不处理,但内置类型大都会随函数调用结束而销毁,所以问题不大。
六: 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类其动态开辟的栈并不会随之被销毁,需要我们对其进行空间释放,否则就会造成内存泄漏,这时就需要手写析构函数去释放空间了。
拷贝构造函数
拷贝构造
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
也就是说创建对象时,创建一个与已存在对象一某一样的新对象。
class Date
{
public:
Date(int year = 2024, int month = 4, int day = 20)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
void Test()
{
Date d1;//构造
Date d2(d1);//拷贝构造
}
拷贝构造特性
拷贝构造函数也是特殊的成员函数,其特征如下:
一: 拷贝构造函数是构造函数的一个重载形式。
二: 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
- 一传参就需要调用拷贝构造,一拷贝构造就得需要参数,逻辑上就会形成死递归,所以编译器会直接报错。
所以正确的写法如下:
class Date
{
public:
Date(int year = 2024, int month = 4, int day = 20)
{
_year = year;
_month = month;
_day = day;
}
//Date(const Date d)错误写法
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
void Test()
{
Date d1;//构造
Date d2(d1);//拷贝构造
}
为了防止不小心修改引用的值,一般会加上const
三: 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。对于日期类这种按值拷贝就能达到效果的,默认生成的构造函数就能完成。(拷贝构造函数对内置类型会处理)。
四: 编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,对于日期类就不需要自己实现了,但是对于需要有独立空间的呢?类如栈(stack)
// 这里会发现下面的程序会崩溃掉?这里就需要我们以后讲的深拷贝去解决。
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 10)
{
_array = (DataType*)malloc(capacity * sizeof(DataType));
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
_size = 0;
_capacity = capacity;
}
void Push(const DataType& data)
{
_array[_size] = data;
_size++;
}
~Stack()
{
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
size_t _size;
size_t _capacity;
};
int main()
{
Stack s1;
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Push(4);
Stack s2(s1);//调用默认生成的拷贝构造函数。
return 0;
}
上面的栈我们并没有自己去实现拷贝构造函数,而是使用默认生成的。当用s1去初始化s2时代码运行后程序会崩掉。
原因: stack的成员变量中_a是指向一块存放数据的空间,当使用默认生成的拷贝构造函数进行值拷贝就会出现以下问题:
调试发现s2中的_a指向的和s1中的_a是同一块空间,当程序结束时会自动调用析构函数释放资源,分别去释放s1和s2,而他们的_a都是同一块空间,导致同一块空间被释放两次,导致程序崩溃。
所以类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。
五: 拷贝构造函数典型调用场景:
- 使用已存在对象创建新对象
Date d1;//构造
Date d2(d1);//拷贝构造
- 函数参数类型为类类型对象
Date(const Date& d);
- 函数返回值类型为类类型对象
Date& operator+=(int day);
一般对象传参时,尽量使用引用类型,因为传值的代价是比较大的,返回时根据实际场景,能用引用尽量使用引用。
小小总结一下:
- 拷贝构造是构造函数的重载。
- 拷贝构造函数的参数只能有一个而且必须是引用。
- 拷贝构造会处理内置类型,但是为浅拷贝,不涉及资源管理的类可以直接使用默认生成的拷贝构造。
- 如果涉及资源管理需要自己写拷贝构造,相对应的也要自己写析构函数释放资源。
赋值运算符重载
运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
注意:
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型参数
- 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
- 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐
藏的this指针(第一个参数规定为运算符的左操作数) .* :: sizeof ?: .
注意以上5个运算符不能重载。
示例:判断两个日期是否相等。
在全局实现
// 全局的operator==
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//private:
int _year;
int _month;
int _day;
};
// 这里会发现运算符重载成全局的就需要成员变量是公有的,那么问题来了,封装性如何保证?
// 这里其实可以用我们后面学习的友元解决,或者干脆重载成成员函数。
bool operator==(const Date& d1, const Date& d2)
{
return d1._year == d2._year
&& d1._month == d2._month
&& d1._day == d2._day;
}
void Test()
{
Date d1(2018, 9, 26);
Date d2(2018, 9, 27);
cout << (d1 == d2) << endl;
}
int main()
{
Test();
return 0;
}
在类里实现
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// bool operator==(Date* this, const Date& d2)
// 这里需要注意的是,左操作数是this,指向调用函数的对象
bool operator==(const Date& d2)
{
return _year == d2._year
&& _month == d2._month
&& _day == d2._day;
}
private:
int _year;
int _month;
int _day;
};
赋值运算符重载
赋值运算符重载也是默认成员函数
特点:
一: 赋值运算符重载格式。
- 参数类型:const T&,传递引用可以提高传参效率。
- 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值。
- 为了支持连续赋值,即d3 = d2 = d1,我们就需要为函数设置一个返回值了,而且很明显,返回值应该是赋值运算符的左操作数,即this指针指向的对象。
- 检测是否自己给自己赋值。
- d1=d1这种自己给自己赋值就没必要了。
- 返回*this :要符合连续赋值的含义。
二: 赋值运算符只能重载成类的成员函数不能重载成全局函数
三: 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。
注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符
重载完成赋值。
- 默认生成的也为值拷贝,就会可能会涉及和上面拷贝构造一样的情况。
- 所以,无论拷贝构造还是赋值重载,如果类中未涉及到资源管理,拷贝构造,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现。
再来区分一下以下代码,d3,d4会带用什么函数呢?
int main()
{
Date d1(2024, 4, 21);//构造
Date d2(d1);//拷贝构造
Date d3 = d1;//拷贝构造
Date d4;//构造
d4 = d1;//赋值重载
return 0;
}
可能会有人认为d3会调用赋值重载,其实不然,d3仍是调用拷贝构造。原因如下:
拷贝构造函数:用一个已经存在的对象去构造初始化另一个即将创建的对象。
赋值运算符重载函数:在两个对象都已经存在的情况下,将一个对象赋值给另一个对象。
前置++和后置++
前置
以日期类为例:日期类++则天数++,前置++即返回++后的值,可以使用引用返回。
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// 前置++:返回+1之后的结果
// 注意:this指向的对象函数结束后不会销毁,故以引用方式返回提高效率
Date& operator++()
{
_day += 1;
return *this;
}
private:
int _year;
int _month;
int _day;
};
后置
后置++则需要返回++前的值,所以先用一个临时变量保存这个值,出了作用域就会销毁,所以不能引用返回。而前置++,后置++的形参类型,顺序,个数都相同,所以为了构成重载,C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递。重载时参数只能是int,不能是别的类型。
后置–也是如此。
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// 后置++:
// 前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载
// C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递
// 注意:后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将this保存一份,然后给this + 1
// 而temp是临时对象,因此只能以值的方式返回,不能返回引用
Date operator++(int)
{
Date temp(*this);
_day += 1;
return temp;
}
private:
int _year;
int _month;
int _day;
};
const成员函数
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
- this指针为Date*const this
看看下面的代码
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << "Print()" << endl;
cout << "year:" << _year << endl;
cout << "month:" << _month << endl;
cout << "day:" << _day << endl << endl;
}
void Print() const
{
cout << "Print()const" << endl;
cout << "year:" << _year << endl;
cout << "month:" << _month << endl;
cout << "day:" << _day << endl << endl;
}
void test()
{
this->Print();
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
int main()
{
Date d1(2024, 4, 21);
d1.Print();
const Date d2(2024, 4, 21);
d2.Print();
return 0;
}
其中Print函数有const版和非const版,d1为非const对象,d2为const对象,可以屏蔽掉其中一个Print函数看看会有什么效果。
这里再用四个问题来加深这个现象(以上面的Date类为例)
-
const对象可以调用非const成员函数吗?
不可以:这里的能否调用的本质问题是const的权限问题。
const对象的内容是不能修改的,而非const对象的内容是可以修改:所以,当const对象调用非const成员函数时,会将const对象传给非const成员函数,非const成员函数会用Date* this指针接受const对象,导致const对象(接受指针类型应为:const Date* this)失去了const的限制,变得可以修改,是一种权限的放大,所以是不合理的。 -
非const对象可以调用const成员函数吗?
可以:
非const对象调用const成员函数时,会将非const对象传给const成员函数,const成员函数会用const Date* this指针接受非const对象,导致非const对象内容不可以修改,是一种权限的缩小,是合理的。
- 成员函数的指针本来是Date* const this,这里只是为了方便演示写的Date*this。const对象的指针实际是const Date * const this。
- const成员函数内可以调用其它的非const成员函数吗?
不可以,原因与上面一致。 - 非const成员函数内可以调用其它的const成员函数吗?
可以,原因与上面一致
总结:
- 一般的输出型函数可以加const保证其内容不被修改,而const对象也能调用。
取地址及const取地址操作符重载
这两个默认成员函数一般不用重新定义 ,编译器默认会生成。
class Date
{
public:
Date* operator&()
{
return this;
}
const Date* operator&()const
{
return this;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需
要重载,比如想让别人获取到指定的内容!如
总结:
以上就是六大默认成员函数的介绍,这些函数最大的特点就是不写编译器会自动生成一个,但有些场景却并不适用,需要自己实现一个。而且类如构造,拷贝构造,析构都是自动调用。