初识类和对象,初步了解了类的基本知识,这篇文章主要涉及类的默认成员函数。
每个类在未显示定义的情况下,编译器都自动生成6个默认的成员函数,空类也是如此。
1.构造函数
构造函数:名字与类名相同,创建类对象时由编译器自动调用,并在对象的声明周期内只调用一次。其主要任务是:初始化对象,而不是开辟空间创建对象。
函数的特征有:
-
函数名与类名相同
-
没有返回值类型
-
对象实例化时,编译器自动调用构造函数
-
构造函数可以重载
注意:在调用无参构造函数创建对象时,不能跟括号,否则就成了函数声明。
-
类中没有显式定义构造函数,编译器会自动生成一个默认的构造函数,若类中有显式定义构造函数,编译器不再生成。
-
无参构造函数和全缺省构造函数都可以认为是默认构造函数。即:
-
编译器默认生成的构造函数,而对象依然是随机值,那么默认构造函数有什么用呢?
实例化Date类对象时,Date类默认的构造函数会调用自定义类型的构造函数。
int main()
{
Date d1;
return 0;
}
反汇编代码:
而在调用的默认无参构造函数中:又再调用了Time类构造函数。
构造类对象顺序:
- 全局对象先于局部对象构造
- 静态对象先于普通对象构造
析构类对象顺序:按照上述顺序反着来。因为是栈是后进先出的。
2.析构函数
析构函数:析构函数是 对象在销毁时自动调用析构函数,完成类的一些资源清理 。而不是完成对象的销毁。特殊的成员函数之一。
其特性有:
-
析构函数名是在类名前加上字符
~
-
无参数,无返回值类型
-
一个类只有一个析构函数。若未显式定义,编译器会自动生成默认的析构函数。析构函数不能重载。
-
对象生命周期结束时,编译器自动调用析构函数。
查看其反汇编:对象生命周期结束,调用了析构函数
-
和构造函数一样,默认的析构函数会对自定义类型成员调用它的析构函数。
注意:如果类中涉及到资源管理,就需要显式提供析构函数,在析构函数中将对象的资源释放掉。否则就会造成内存泄漏。
3.拷贝构造函数
拷贝构造函数:用已经存在的对象创建新对象时,编译器会自动调用拷贝构造函数完成新对象的初始化工作。特殊的成员函数之一。
其特性有:
-
是构造函数的一个重载形式。
-
参数只有一个且必须使用引用传参。
为什么要使用引用传参?
即:
所以拷贝构造函数,要使用引用进行传参。 -
若未显示定义拷贝构造函数,编译器会生成默认的拷贝构造函数。
默认的拷贝构造函数对象按内存字节序完成拷贝。 -
即使编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要我们自己实现。
测试代码:
class String
{
public:
String(const char *str)
{
_str = (char *)malloc(strlen(str) + 1);
assert(_str);
strcpy(_str, str);
}
~String()
{
free(_str);
}
private:
char *_str;
};
void TestClass()
{
String s1("Hello");
String s2(s1);
}
该代码运行时会崩溃。
在拷贝构造时,s1和s2共用了一块堆空间。
所以这份代码有问题:s1已经是野指针了,而再次进行了释放。
而这里的拷贝构造是浅拷贝:将一个对象中的内容原封不动拷贝到新对象中。
如果多个对象共同使用同一份资源,在销毁时,会被多次释放,导致出错。
注意:如果类中涉及到资源管理时,拷贝构造函数必须要自己实现。
4.赋值运算符重载
为什么会有运算符重载呢?先看这样一段代码:
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
void TestClass()
{
Date d1(2020, 10, 1);
d1 + 10;//会报错
}
这段代码中,自定义类型进行操作符运算时,编译器并不知道如何进行运算,所以是会报错的。
当然,使用函数也可以做到具体的成员变量相加,如:
Date DateAdd(int days)
{
Date temp(*this);
temp._day += days;
return temp;
}
结果:
但是C++编译器提供了运算符的重载,进一步提高了代码可读性。
运算符如何重载:使用关键字operator
后跟需要重载的符号。
例:
Date operator+(int days)
{
Date temp(*this);
temp._day += days;
return temp;
}
测试结果为:
这样的代码比函数实现更简洁,可读性更高。
使用运算符重载需要注意的地方:
- 不能用其来创建新的操作符。
- 重载操作符必须要有一个类类型或者枚举类型的操作数
- 内置类型的操作符,不能改变其含义 。如上述的+操作符,没有改变其含义
- 5个运算符不能重载:
.*
,::
,sizeof
,?:
,.
赋值运算符重载:operator=
代码实现:
Date& operator=(const Date& d)
{
if (this != &d)
{
this->_year = d._year;
this->_month = d._month;
this->_day = d._day;
}
return *this;
}
涉及到了几个问题:参数类型,返回值,如何选择?
自己给自己赋值没有必要,所以要做检查。
一个类中若是没有显式定义赋值运算符重载,编译器会自动生成一个,完成对象字节序的拷贝。
和拷贝构造函数同理,若类中涉及到了资源管理,一定要显式定义赋值运算符重载,编译器生成的赋值运算符重载是浅拷贝方式的。
其它运算符重载的实现:
//运算符+=重载
Date& operator+=(int days)
{
_day += days;
return *this;
}
//运算符+重载
Date operator+(int days)
{
Date temp(*this);
temp._day += days;
return temp;
}
//运算符-=重载
Date& operator-=(int days)
{
_day -= days;
return *this;
}
//运算符-重载
Date operator-(int days)
{
Date temp(*this);
temp._day -= days;
return temp;
}
// 前置++
Date& operator++()
{
_day++;
return *this;
}
//后置++
Date operator++(int)//必须要加int,语法规定
{
Date temp(*this);
_day++;
return temp;
}
// 前置--
Date& operator--()
{
_day--;
return *this;
}
//后置--
Date operator--(int)//必须要加int,语法规定
{
Date temp(*this);
_day--;
return temp;
}
//运算符>重载
bool operator>(const Date& d)const//加const,让this指针的成员变量不能被修改
{
return (_year < d._year) ||
(_year == d._year && _month < d._month) ||
(_year == d._year && _month == d._month && _day < d._day);
}
//运算符>=重载
bool operator>=(const Date& d)const//加const,让this指针的成员变量不能被修改
{
return (_year <= d._year) ||
(_year == d._year && _month <= d._month) ||
(_year == d._year && _month == d._month && _day <= d._day);
}
//运算符<重载
bool operator<(const Date& d)const//加const,让this指针的成员变量不能被修改
{
return (_year > d._year) ||
(_year == d._year && _month > d._month) ||
(_year == d._year && _month == d._month && _day > d._day);
}
//运算符<=重载
bool operator<=(const Date& d)const//加const,让this指针的成员变量不能被修改
{
return (_year >= d._year) ||
(_year == d._year && _month >= d._month) ||
(_year == d._year && _month == d._month && _day >= d._day);
}
//运算符==重载
bool operator==(const Date& d)const//加const,让this指针的成员变量不能被修改
{
return _year == d._year && _month == d._month && _day == d._day;
}
//运算符!=重载
bool operator!=(const Date& d)const
{
return !(*this == d);
}
//在成员函数中,不需要修改任何成员变量时,一般加上const。避免误修改数据
可以看到运算符都是有返回值的,返回bool值,返回对象,或者引用。
具体的返回值类型要根据运算符本身而定,不能改变运算符本身的含义。
那么返回对象还是引用呢?
一般来说能返回引用尽量返回引用。
原因:
- 允许连续赋值
- 降低了函数压栈的开销。
一:如连续赋值d1 = d2 = d3 和(d1 = d2) = d3
情况下:
d1 = d2 = d3
,返回对象
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_ay = d._day;
cout << "拷贝构造函数" << "---" << this << endl;
}
~Date()
{
cout << "析构函数" << "---" << this << endl;
}
Date operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
void TestClass()
{
Date d1;
Date d2;
Date d3(2020, 10, 1);
d1 = d2 = d3;
}
其结果为:可以完成赋值
输出调用了两次拷贝构造函数和析构函数。
返回引用时:
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
cout << "拷贝构造函数" << "---" << this << endl;
}
~Date()
{
cout << "析构函数" << "---" << this << endl;
}
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
同样可以完成赋值:
输出为:只调用了3次析构函数。
2. (d1 = d2) = d3
,
返回对象时:
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
cout << "拷贝构造函数" << "---" << this << endl;
}
~Date()
{
cout << "析构函数" << "---" << this << endl;
}
Date operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
void TestClass()
{
Date d1;
Date d2(2020,10,6);
Date d3(2020, 10, 1);
(d1 = d2) = d3;
}
结果:赋值未完成
输出:
返回引用时:
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
cout << "拷贝构造函数" << "---" << this << endl;
}
~Date()
{
cout << "析构函数" << "---" << this << endl;
}
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
void TestClass()
{
Date d1;
Date d2(2020,10,6);
Date d3(2020, 10, 1);
(d1 = d2) = d3;
}
结果:赋值成功
输出:
可以很明显的看到:
- 在连续赋值情况下,返回对象可能不会赋值成功
- 多调用了两次拷贝构造函数和析构函数
为什么会多调用两次拷贝构造函数和析构函数?
答:return要将返回对象保存在外部存储单元中,所以就需要进行实例化,调用拷贝构造函数。在语句完成之后,这个语句就只是返回的临时对象,而不是this所指的对象,所以在(d1 = d2 ) = d3
情况下,赋值不会成功。
所以,在保持运算符本身含义不改变的情况下,能返回引用就返回引用。
优点:
1. 避免连续赋值造成错误。
2. 减少调用多余函数的压栈开销。
灵魂拷问
- const对象可以调用非const成员函数吗?
- 非const对象可以调用const成员函数吗?
- const成员函数内可以调用其它非const成员函数吗?
- 非const成员函数内可以调用其它const成员函数吗?
答:
- 不可以。非const成员函数,可能会修改其成员变量,不安全,编译器不会调用。
- 可以。const成员函数中,不能修改其成员变量,非const对象可修改成员变量也可不修改成员变量。
- 不可以。同1。
- 可以。const成员函数中,不能修改其成员变量,非const成员函数可修改成员变量也可不修改成员变量。
5.对象取地址和const对象取地址:
Date* operator&()
{
return this;
}
const Date* operator&()const
{
return this;
}
一般情况下,不用我们对&进行重载。