类默认生成的成员函数有六个,它们分别是:构造函数、拷贝构造函数、析构函数、赋值操作符重载、取地址操作符重载和const修饰的取地址操作符重载。
一、构造函数
1、什么是构造函数?
初始化对象,有且仅在定义一个对象时自动执行一次的函数,就称为构造函数。据悉:类的数据成员是不能在声明类的时候初始化的,因为类并不是一个实体,而是一种抽象的数据类型,并不占据存储空间。
2、构造函数的特性
(1)函数名与类名相同。
(2)无返回值。
(3)实例化对象时系统会自动调用对应的构造函数。
(4)构造函数可以重载。
(5)构造函数可以在类内定义,也可以在类外定义。在类外定义时的格式:类名+“::”+函数名。
(6)如果类定义中没有给出构造函数,则C++编译器会自动生成一个缺省的构造函数;如果我们定义了一个构造函数,系统就不会生成缺省的构造函数。
(7)无参的构造函数和缺省的构造函数都认为是缺省的构造函数,所以缺省的构造函数只能有一个。
【例1】
#include<iostream>
using namespace std;
class Date
{
public:
Date() //构造函数
{}
Date(int year, int month, int day); //在类内声明一个构造函数,在类外定义
////在类内定义构造函数
//Date(int year, int month, int day) //构造函数重载
//{
// _year = year;
// _month = month;
// _day = day;
//}
void PrintfDate()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
//在类外定义构造函数
Date::Date(int year = 2018, int month = 7, int day = 30) //构造函数重载
{
_year = year;
_month = month;
_day = day;
}
int main()
{
Date date1(2018,7,30);
date1.PrintfDate();
system("pause");
return 0;
}
运行结果:
3、构造函数的类型
(1)无参构造函数和有参构造函数
当用户希望对不同的对象赋予不同的初始值时,就需要用到带参的构造函数,实现不同的初始化。因为用户不能调用构造函数,所以其对应的实参需要在定义对象的时候给定。
【例2】
#include<iostream>
using namespace std;
class Date
{
public:
//无参的构造函数
Date()
{}
//带参的构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void PrintfDate()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date date1;//调用无参的构造函数
date1.PrintfDate();
Date date2(2018, 7, 30);//调用带参的构造函数
date2.PrintfDate();
/*Date date3(); //注意:这样调用无参的构造函数是错的
date3.PrintfDate();*/
system("pause");
return 0;
}
运行结果:
释:因为无参的构造函数没有初始化,所以打印出来是随机值。
2、带缺省的构造函数
构造函数中参数的值既可以通过实参传递,也可以被指定为某些默认值。如果用户不指定实参值,编译系统就使用形参的默认值。
【例3】
#include<iostream>
using namespace std;
class Date
{
public:
//带缺省的构造函数
Date(int year = 2018, int month = 7, int day = 30)
{
_year = year;
_month = month;
_day = day;
}
//半缺省的构造函数(不常用)
Date(int year, int month = 7)
{
_year = year;
_month = month;
}
void PrintfDate()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date date1;//调缺省的构造函数
date1.PrintfDate();
Date date2(2018, 7, 30);//调缺省的构造函数
date2.PrintfDate();
system("pause");
return 0;
}
运行结果:
注:无参的构造函数和缺省的构造函数都认为是缺省的构造函数,所以缺省的构造函数不能同时存在。
3、用参数列表对数据成员初始化
C++还提供了一种初始化数据成员的方法——初始化列表。初始化列表位于构造函数参数列表之后,在函数体“{}”之前。该列表内的初始化工作发生在函数体内的任何代码被执行之前。
【例4】
class Date
{
public:
Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
{
}
private:
int _year;
int _month;
int _day;
};
注:使用初始化列表的优点
(1)如果类存在继承关系,派生类可以直接在其初始化列表里调用基类的特定构造函数以向它传递参数,因为不能在初始化对象时访问基类的数据成员。
(2)类的非静态const数据成员和引用成员只能在初始化列表里初始化,因为它们只存在初始化语义,而不存在赋值语义。
(3)类的数据成员的初始化可以采用初始化列表或函数体内赋值两种方式,但是使用初始化列表的方式效率更高。
二、拷贝构造函数
1、什么是拷贝构造函数?
创建对象时使用同类对象来进行初始化,这时所用的构造函数就是拷贝构造函数(Copy Constructor)。拷贝构造函数也是构造函数,但它只有一个参数,这个参数只能是本类的一个对象,而且采用对象的常引用形式。拷贝构造函数的作用就是将实参对象的各成员值一一赋给新的对象中对应的成员。
2、拷贝构造函数的特征
(1)拷贝构造函数其实是一个构造函数的重载。
(2)拷贝构造函数的参数必须使用引用传参,使用传参方式会引发无穷递归调用。
(3)若为显示定义,系统默认生成缺省的拷贝构造函数,缺省的拷贝构造函数会按照成员的声明顺序依次拷贝类成员进行初始化。
【例5】
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class Date
{
public:
//带缺省的构造函数
Date(int year = 2018, int month = 7, int day = 30)
{
_year = year;
_month = month;
_day = day;
}
//拷贝构造函数
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void PrintfDate()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date date1;
date1.PrintfDate();
//下面调用的两种方法都是拷贝构造函数,是等价的
Date date2(date1);//调缺省的构造函数
date2.PrintfDate();
Date date3 = date1;
date3.PrintfDate();
system("pause");
return 0;
}
运行结果:
释:
(1)为什么这里的对象可以直接访问私有成员变量?
1)在类的成员函数里可以直接访问同类对象的私有或保护的成员。
2)C++的访问限定符是以类为单位的,即:在这个单位内的成员可以互相访问。
(2)为什么拷贝构造函数的参数使用传值方式会引起无穷递归调用?如下图:
三、析构函数
1、什么是析构函数?
当一个对象的生命周期结束时,C++编译器会自动调用一个成员函数,这个成员函数即为析构函数,它的作用与构造函数相反。
2、析构函数的特征
(1)析构函数名是类名前加一个“~”字符。
(2)无参数,无返回值,所以不能重载。其实,构造函数和析构函数都没有返回值,就像刚出生的娃娃和即将离世的人,生不带来,死不带去。
(3)一个类有且只有一个析构函数,对象生命周期结束时,若未定义,C++编译系统会自动生成缺省的析构函数。
(4)调用构造函数和析构函数的顺序
(5)因为函数压栈的关系,所以先构造的后析构,后构造的先析构。如果有全局对象或者静态局部对象,则它们在main函数结束或者调用exit函数时才被析构。
(6)析构函数的作用并不是删除对象,而是在撤销对象时做一些清理工作,比如关闭打开的文件,释放开辟的动态内存等。
【例6】
class Array
{
public:
Array(int size)
{
_ptr = (int*)malloc(size*sizeof(int));
}
//这里的析构函数需要完成清理工作
~Array()
{
if (_ptr)
{
free(_ptr);//释放堆上内存
_ptr = NULL;//将指针置空
}
}
private:
int* _ptr;
};
四、赋值运算符重载
如果已经定义了两个或多个对象,则这些同类的对象之间可以相互赋值,即一个对象的值可以赋给另一个同类的对象。这里所指的对象的值是指对象中所有数据成员的值。对象之间的赋值是通过赋值运算符“=”重载实现的,即:将一个对象的值一一复制给另一对象的对应成员。
【例7】
class Date
{
public:
Date()
{}
//拷贝构造函数
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
//赋值运算符重载
Date& operator = (const Date& d)
{
if (this != &d)
{
this->_year = d._year;
this->_month = d._month;
this->_day = d._day;
}
return *this;
}
void PrintfDate()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
问:
(1)为什么operator=赋值运算符需要一个Date&的返回,使用void做返回值可以吗?
答:可以,只是返回一个引用效率更高。
(2)赋值运算符重载函数里的if条件判断是在检查什么?
五、取地址操作符重载和const修饰的取地址运算符重载
这两个默认成员函数一般不用重新定义,编译器默认会自动生成。
【例7】
class Date
{
public:
Date* operator&()
{
return this;
}
const Date* operator&()const
{
return this;
}
private:
int _year;
int _month;
int _day;
};
释:只有一种情况才会需要自己重载这两个操作符,那就是你只想让别人获取你指定的内容。