类的默认成员函数
如果一个类中什么成员都没有,简称为空类。空类中什么都没有吗?并不是的,任何一个类在我们不写的情况下,都会自动生成下面6个默认成员函数。
1.构造函数
通过Date类来说明:
对于Date类,可以通过SetDate公有的方法给对象设置内容,但是如果每次创建对象都调用该方法设置信息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢?
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有 一个合适的初始值,并且在对象的生命周期内只调用一次。
#include<iostream>
using namespace std;
class Date
{
public:
void SetDate(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Display()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.SetDate(2021, 1, 1);
d1.Display();
Date d2;
d2.SetDate(2021, 5, 1);
d2.Display();
return 0;
}
对于Date类,可以通过SetDate公有的方法给对象设置内容,但是如果每次创建对象都调用该方法设置信息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢?
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有 一个合适的初始值,并且在对象的生命周期内只调用一次。
构造函数是特殊的成员函数,需要注意的是,构造函数的虽然名称叫构造,但是需要注意的是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
其特征如下:
- 函数名与类名相同。
- 无返回值。
- 对象实例化时编译器自动调用对应的构造函数。
- 构造函数可以重载。
#include<iostream>
using namespace std;
class Date
{
public:
Date()//无参构造函数
{
cout << "Date()" << endl;
}
Date(int year, int month, int day)//有参构造函数
{
cout << "Date(int year, int month, int day)" << endl;
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date a;//当调用无参构造时后面不用加括号
Date b(2021, 5, 14);
return 0;
}
上述Date类的的有参和无参构造函数构成了重载,构造函数虽然可以重载,但是对于同一个定义的对象只能使用一个,否则有歧义。
当然如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
编译器生成的默认构造函数做了啥,他是有区别对待的:
对于成员变量中的基本类型,他什么也不会做。
对于成员变量中的自定义类型,他会去调用他会去调他的默认构造函数(不用参数就可以调用的:无参构造函数、全缺省构造函数、编译器生成的构造函数)
如下:
#include<iostream>
using namespace std;
class A
{
public:
A()
{
cout << "A()"<<endl;
}
private:
int a;
};
class Date
{
private:
int _year;
int _month;
int _day;
A a;
};
int main()
{
Date a;
return 0;
}
2.析构函数
析构函数:与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作,析构函数是特殊的成员函数。
其特征如下:
- 析构函数名是在类名前加上字符 ~。
- 无参数无返回值。
- 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
- 对象生命周期结束时,C++编译系统系统自动调用析构函数。
#include<iostream>
using namespace std;
class A
{
public:
void create(int n)
{
cout << "create(int a)" << endl;
_a = (int*)malloc(sizeof(int)*n);
}
~A()
{
if (_a)
{
cout << "~A()" << endl;
free(_a);
}
}
public:
int* _a;
};
int main()
{
A a;
//a.create(1);
return 0;
}
当解除a.create的屏蔽后
想Date类,没有要释放的的资源,所以不需要自己写析构函数,编译器自动生成的就够了。但是只要我们的类中动态开辟了空间如stack这种,一定要自己实现析构函数。
构造函数和析构函数的最大特点就是会自动调用
class A
{
public:
A(int a = 1)
{
cout <<this<< ":构造函数" << endl;
}
~A()
{
cout <<this<< ":析构函数" << endl;
}
private:
int _a;
};
int main()
{
A a1;
A a2;
cout << "&a1:" << &a1 << " &a2:" << &a2 << endl;
}
从上面这代码和运行结果看出来什么?
定义对象性时构造函数和析构函数的最大特点就是会自动调用,并且后定义的先析构,构造函数在定义时调用,析构函数在生命周期结束时调用。
3.拷贝构造函数
在创建对象时,可否创建一个与一个对象一某一样的新对象呢?
比如:
int a=10;
int b=a;
就像上述代码这样在创建时可以用同类型去初始化
所以有了拷贝构造函数
构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
特征
拷贝构造函数也是特殊的成员函数,其特征如下:
- 拷贝构造函数是构造函数的一个重载形式。
- 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用。
- 若未显示定义,系统生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝。
来一段代码演示:(暂时不考虑日期是否正确)
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 2021, int month = 5, int day = 14)
{
_year = year;
_month = month;
_day = day;
}
Date(const 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;
};
int main()
{
Date d1(2021, 5, 15);
Date d2(d1);
Date d3 = d1;
d1.print();
d2.print();
d3.print();
return 0;
}
结果:
从这里看到
Date d2(d1);
Date d3 = d1;
这两句代码效果一样
看一下上述代码拷贝构造函数的结构
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
定义规定参数就只能有一个,这没问题。
但是为什么他的参数是引用类型的呢?如果就是普通的自定义类型呢
思考一下
是不是会有下面这种情况
所以这里我们不妨试试用引用类型,为什么用可以用引用呢?
引用就是一个变量的别名,所以用引用时在上图中传参时就不会调用拷贝构造,这里就直接相当于把实参(因为引用定义的时候就是相当于那个数,只是名字不同)直接拿过来,而不会产生临时变量来存储实参
当然这里加const是为了安全考虑,前面讲引用的文章也讲过,引用传参时若不改变实参的值就尽量加const,不然有时候误操作或者不小心写错代码会改变实参的值。
当然拷贝构造函数我们不写编译器也会自动生成,向上面这种日期类编译器自动生成的就够了,编译器生成的拷贝构造能完成值拷贝也称浅拷贝。
但是当我们在写像stack这种类时就得自己实现了,举个栗子:
class Stack
{
public:
Stack(int capacity=1)
{
_size = 0;
_capacity=capacity;
_a = (int*)malloc(sizeof(int)*_capacity);
}
private:
int _size;
int _capacity;
int *_a;
};
int main()
{
Stack s1;
Stack s2(s1);
}
从调试中能不能看到什么问题
发现s1和s2两个对象中的_a也值变一样了,那么当我们改变s2的的时候,s1是不是也会受到改变,这就会出问题了。
所以像这种类型的类千万不能用编译器默认生成的拷贝构造函数,得自己写,自己实现深拷贝。
总之,视情况而定。
4.赋值运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
注意:
1.不能通过连接其他符号来创建新的操作符:比如operator@
2.重载操作符必须有一个类类型或者枚举类型的操作数
3.用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不 能改变其含义
4.作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的
操作符有一个默认的形参this,限定为第一个形参
5.(.* 、:: 、sizeof 、?: 、.) 注意以上5个运算符不能重载。
当运算符重载在全局:
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 main()
{
Date d1(2021, 5, 15);
Date d2(2021, 6, 16);
cout << (d1 == d2) << endl;
}
显然上面这段代码会有错误,因为在在类外是不能直接访问私有域的数据的,若是将私有域改为公有这样到时可以解决问题了,那么问题来了,封装性如何保证?
所以,我们可以将运算符重载放在类里面。
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
bool operator==(const Date& d1, const Date& d2)
{
return d1._year == d2._year&& d1._month == d2._month&& d1._day == d2._day;
}
private:
int _year;
int _month;
int _day;
};
void main()
{
Date d1(2021, 5, 15);
Date d2(2021, 6, 16);
cout << (d1 == d2) << endl;
}
但是这样编译器又会报错了
为什么?
this指针,编译器会自动向类里面的函数添加一个隐含的this指针,前面也有讲过,就不在这里细讲了。
所以优化后会有三个参数,与函数调用的参数数量不符。
this指针指向的就是这个类,所以在类里重载时只需要另一个类传过来就行了,用引用传参可以减少拷贝。所以改一下有:
bool operator==( Date& d2)
{
return (*this)._year == d2._year&& (*this)._month == d2._month&& (*this)._day == d2._day;
}
*this就是当前类。
运行结果
调用时注意参数位置就行了。
赋值运算符主要有四点:
- 参数类型
- 返回值
- 检测是否自己给自己赋值
- 返回*this
- 一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝。
若是遇到下面这种情况呢?
当我们重载了=用来赋值
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void operator=(const Date& d2)
{
this->_year = d2._year;
this->_month = d2._month;
this->_day = d2._day;
}
void print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
void main()
{
Date d1(2021, 5, 15);
Date d2(2021, 6, 16);
Date d3, d4;
d2 = d1;
d4 = d3 = d2;
d1.print();
d2.print();
}
d4 = d3 = d2;
向上面这种连续赋值,我们就得改代码了,若是重载后的运算符是无返回值类型,那么上面这个语句就会出错,得返回一个和d3、d4一样类型的值。
Date operator=(const Date& d2)
{
this->_year = d2._year;
this->_month = d2._month;
this->_day = d2._day;
return d2;
}
直接返回d2可以,那这里是不是就要调拷贝构造函数了,还是像前面常用的引用一样,直接传引用就不用拷贝构造了。改为
Date& operator=(const Date& d2)
{
this->_year = d2._year;
this->_month = d2._month;
this->_day = d2._day;
return *this;
}
但是前面将引用的时候讲过,引用返回的时候要保证这个值的生命周期,这里可以成功返回码?
就算出来这个作用域销毁了this,但是Date&确已是d3的别名了,d3要销毁也得等程序结束了。
赋值运算符重载(d2=d1这种)和拷贝构造一样,不写的话编译器也会自动生成,他会完成值拷贝,但是像stack这样的类就不能用默认生成的,跟默认生成的拷贝构造一样,他只会完成浅拷贝,会影响到他前面的对象,要用深拷贝就得自己去实现了。