类的六个默认成员函数
在学习这些默认的成员函数之前,务必先来了解一下this指针,因为除了构造函数外其他的默认成员函数都有一个隐式的形参–this指针。这对于我们之后在默认成员函数的学习和使用方面有着很大的影响。
this指针
1、每个成员函数都有一个指针形参,他的名字是固定的,称为this指针,this指针是隐式的(构造函数没有this指针)。
2、编译器会对成员函数进行处理,在对象调用成员函数时,对象地址做实参传递给成员函数的第一个形参this指针。
3、this指针是成员函数的隐含的指针形参,是编译器自己处理的,我们不能再成员函数的形参中添加this指针的参数定义,也不能在调用时显示传递对象地址给this指针。
如图:编译器处理成员函数隐含的this指针:
需要注意一个问题,有时候也会被面试问到。
this指针是隐式的,不需要我们定义也不需要显式的传参,但他依旧是一个形参,因此this指针存在于栈上。
接下来我们主要学习以下4个默认成员函数。
1、构造函数
首先我们就有一个问题,在定义一个类时,我们通常会将成员变量定义为私有的,那么我们如何对这些私有的成员变量进行初始化呢???
//比如我们定义一个日期类
class Date
{
public:
void Display()
{
cout<<year<<"-"<<month<<"-"<<day<<endl;
}
private:
int _year;
int _month;
int _day;
}
//我们看到这里的成员变量是私有的,如何初始化?
//这里我们就有了构造函数这一说
注意一点:构造函数是公有的,并且只在定义对象时自动执行一次
什么样的函数才叫构造函数呢??接下来看一下构造函数所具有的几大特征
1、函数名与类名相同
2、没有返回值
3、对象构造时系统自动调用对应的构造函数
4、构造函数可以重载
5、可以在类里面定义,也可以在类外面定义
6、若类中没有给出构造函数,则C++编译器会自动产生一个缺省的构造函数,但一旦我们定义了,系统就不会自动生成了。
7、无参的构造函数和全缺省的构造函数都认为是缺省构造函数,并且缺省的构造函数只能有一个
代码展示:
class Date
{
public:
//1.1:无参构造函数
Date() //函数名与类名相同,没有返回值
{
_year = 1900;
_month = 1;
_day = 1;
}
//无参构造函数与全缺省的构造函数都是缺省构造函数,且只能存在一个。
//1.2:全缺省的构造函数
//Date(int year=1900,int month=1,int day=1)
//{
// _year = year;
// _month = month;
// _day = day;
//}
//2:带参构造函数
Date(int year,int month,int day) //可构成函数重载
{
_year = year;
_month = month;
_day = day;
}
//1和2两个构造函数构成函数重载
//函数重载:函数名相同,参数列表不同(参数个数,参数类型)
//返回值可同可不同。
void show()
{
cout<<_year<<"-"<<_month<<"-"<<_day<<endl;
}
private:
int _year;
int _month;
int _day;
};
void Test()
{
Date d1;//会调用无参构造函数
Date d2(2018,7,31);//会调用有参构造函数
}
2、拷贝构造函数
我们有时候会遇到一种情况就是当我们实例化出一个新的对象时,想要用一个同类的对象对其进行初始化,这时我们就需要用到拷贝构造函数。
拷贝构造函数的特征:
1、拷贝构造函数其实就是构造函数的一个重载。
2、拷贝构造函数的参数必须使用引用传参,使用传值方式会引发无穷递归调用。
3、若为显式的定义,系统会默认缺省的拷贝构造函数,缺省的拷贝构造函数会依次拷贝类成员进行初始化。
代码展示:
class Date
{
public:
//构造函数
Date(int year,int month,int day)
{
_year = year;
_month = month;
_day = day;
}
//拷贝构造函数
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
void Test()
{
Date d1(2018,1,1);
//以下两种方法等价
Date d2(d1);
Date d3=d1;
}
为什么拷贝构造函数必须使用引用传参?因为使用传值方式会引发无穷递归问题。
这里的无穷递归问题到底又是怎么一回事?
3、析构函数
我们知道构造函数是对象实例化时自动调用对应的构造函数,而当一个对象的生命周期结束时也会自动调用一个函数—析构函数。(其目的是为了做一些清理工作)
析构函数的几大特征:
1、函数名是在类名前加上~
2、没有参数没有返回值(包括void类型)
3、一个类有且只有一个析构函数(不能重载)
4、对象生命周期结束时,C++编译系统会自动调用析构函数
4、若类中没有给出析构函数,则C++编译器会自动产生一个缺省的析构函数(即使我们定义了,系统还是会自动生成一个,在调用时先调用用户自定义的在调用系统自动生成的)。
注意:因为我们说是在一个对象的生命周期结束的时候调用析构函数,就会想到说那么析构函数的作用就是把这个对象删掉。其实不是,析构函数并不会把一个生命周期已经结束的对象删除,只是做一些清理工作。
问题又来了,清理工作到底是什么???
代码展示:
class Array
{
public:
//有参构造函数
Array(int n)
{
_ptr = (int*)malloc(n*sizeof(int));
}
//析构函数
~Array()//没有参数和返回值
{
if(_ptr)
{
free(_ptr);//清理工作,也就是释放动态开辟的空间
_ptr = NULL;
}
}
private:
int *_ptr;
};
4、赋值运算符重载
为了增强程序的可读性,C++支持运算符的重载。
运算符重载特征:
1、operator+合法的运算符构成函数名(如:重载>运算符的函数名为 operator>)。
2、重载运算符后,不能改变运算符的优先级/结合性/操作数个数
3、.(成员访问运算符),.*(成员指针访问运算符),::(域运算符),?:(条件运算符),sizeof(长度运算符)。这五个运算符是不能被重载的。
赋值运算符的重载是对一个已经存在的对象进行拷贝赋值。
代码展示:
class Date
{
public:
//构造函数
Date()
{
_year = 1900;
_month = 1;
_day = 1;
}
//拷贝构造函数
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;
}
private:
int _year;
int _month;
int _day;
};
那么想一想,赋值运算符重载函数为什么返回值是 Date&呢???
看一下:
Date d1;
Date d3;
d3 = d1;//调用赋值运算符的重载
//走到这我们的d1已经赋给了d3
//试想。如果赋值运算符的重载返回的不是Date&,假设是void
//那么如果我们还想要将d3赋值给另外一个对象,一定是不行的
//因为在类型上都不匹配了,新实例化的对象是Date,而你在之前
//的赋值完成返回以后就把d3的类型改了,不再是Date类的对象了
所以说,之所以要返回Date&,是为了之后可以继续使用进行赋值。
另外需要注意的一点就是,拷贝构造的调用和赋值运算符重载的调用的区别。
Date d1;
//调用拷贝构造
Date d2 = d1;//此时d2还并不存在
//调用赋值运算符的重载
Date d3;//d3已经被实例化出来
d3 = d1;
//拷贝构造,顾名思义就是以拷贝的方式去构造出来,
//所以操作的对象是一个不存在的对象和一个已经存在的对象之间
//赋值是两个都已经存在的对象之间进行