c++类会默认生成6个成员函数:
- 构造函数
- 拷贝构造函数
- 析构函数
- 赋值操作符重载
- 取地址操作符重载
- const修饰的取地址操作符重载
例如:
class Test
{
public:
Test(); //构造函数
Test(const Test&); //拷贝构造函数
~Test(); //析构函数
Test operator=(const Test&); //赋值运算符重载
Test* operator&(); //取地址运算符重载
const Test* operator&()const; //const修饰的取地址运算符重载
}
这些函数只有在需要的时候才会产生,空类占用1个字节。
1.构造函数
类为了保护数据成员,其所有数据成员都默认是私有的(private),这也就意味着程序只能通过类里面的函数提供接口来访问数据成员。例如,下面程序用日期类创建了一个对象,并进行初始化
class Date
{
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2017,10,29);//不被允许
return 0;
}
为了解决这个问题 ,C++提供了一种特殊的成员函数~构造函数。构造函数没有类型声明(即无返回值),其函数名和类名相同,我们可以为构造函数提供一些方法完成我们我们想要的功能。例如:
class Date
{
public:
Date(int year,int month,int day)//构造函数
{
_year = year;
_month = month;
_day = day;
}
private:
int _year; //为了和非成员参数区分,一般类成员参数以m_或者_开头
int _month;
int _day;
};
int main()
{
Date d1(2017,10,29);//初始化成功
return 0;
}
使用参数列表对数据成员进行初始化:C++还提供了一种初始化数据成员的方式~初始化列表。初始化列表以冒号开头,后跟一系列以逗号分隔的初始化字段。例如:
class Date
{
public:
Date(int year,int month,int day)
:_year(year)
,_month(month)
,_day(day)
{}
private:
int _year;
int _month;
int _day;
};
构造函数的执行可以分成两个阶段,初始化阶段和计算阶段,初始化阶段先于计算阶段。
初始化阶段:所有类类型的成员都会在初始化阶段初始化,即使该成员没有出现在构造函数的初始化列表中。
计算阶段:一般用于执行构造函数体内的复制运算。
注意:
- 每个成员在初始化列表中只能出现一次。
- 初始化列表仅用于初始化类的数据成员,并不指定这些数据成员的初始化顺序,数据成员在类中定义顺序就是在参数列表中的初始化顺序。
- 尽量避免使用成员初始化成员,成员的初始化顺序最好和成员的定义顺序保持一致,对于类类型来说,使用初始化列表少了一次调用默认构造函数的过程。
下面这些必须放在初始化列表中:
- 常量成员,因为常量只能初始化不能赋值。
- 引用类型,因为引用必须在定义的时候初始化,并且不能重新赋值。
- 没有默认构造函数的类类型,因为使用初始化列表可以不必调用默认构造函数来初始化,而是直接调用拷贝构造函数初始化。
【默认构造函数】
类如果没有显式定义构造函数时,编译器会合成一个默认的构造函数,该构造函数中什么工作都不做。但是只要显式定义了,即使该构造函数什么也不做,编译器也不会为该类合成默认的构造函数。编译器生成的默认构造函数使用与成员变量初始化相同的规则来初始化成员,具有类类型的成员通过运行各自的默认构造函数来进行初始化。内置和复合类型的成员如指针、数组,只对定义在全局作用域中的对象初始化,当对象定义在局部作用域时,内置和符合类型的成员不进行初始化。在某些情况下,默认构造函数是由编译器隐式使用的。
【explicit关键字】
在C++中,当定义了只有一个参数的构造函数时,同时也定义了一种隐式的类型转换。(例如double a=3.14; int b=a;这里b=3,是因为编译器自动转换,同样的,类和对象也存在这样的转换关系)
用explicit修饰构造函数,抑制由构造函数定义的隐式转换,explicit关键字类内部的构建声明上,在类的定义体外部的定义上不再重复。
2.拷贝构造函数
如果程序想通过已有的对象复制出新的对象,就需要调用拷贝构造函数,拷贝构造函数是只有单个形参,而且该形参是对本类类型的引用(常用const)的构造函数,拷贝构造函数是特殊的构造函数,创建对象时使用已存在的同类对象来进行初始化,当我们没提供拷贝构造函数时编译器会自动创建默认的拷贝构造函数。看代码:
class Date
{
public:
Date(int year,int month,int day)
:_year(year)
,_month(month)
,_day(day)
{}
Date(const Date& date)//拷贝构造函数
{
_year = date._year;
_month = date._month;
_day = date._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2017,10,29);
Date d2(d1);//用d1拷贝构造出d2
return 0;
}
【特征】
- 它是构造函数的重载。
- 它的参数必须使用同类型对象的引用传递。否则会在栈空间一直递归的调用拷贝构造函数
- 如果没有显式定义,系统会自动合成一个默认的拷贝构造函数。默认的拷贝构造数会依次拷贝类的数据成员完成初始化。
【使用场景】
1、对象实例化对象
Date d1(1990, 1, 1);
Date d2(d1);
2、传值方式作为函数的参数
void FunTest(const Date date)
{}
3、传值方式作为函数返回值
Date FunTest()
{
Date date;
return date;
}
3.析构函数
析构函数和构造函数功能相反,在对象被销毁时,由编译器自动调用,完成类的一些资源清理和收尾工作。析构函数的语法与构造函数一样,用类名作为函数名,区别是析构函数前面有个符号~,有构造函数不同的是,析构函数没有参数。它是在对象释放前程序自动调用的,如果我们没有提供虚构函数,编译器将会自动生成一个默认的析构函数。
看一下析构函数的特点:
①一个类里面只有一个,对象被销毁的时候只调用一次。
②不能有参数,也不能有返回值,因此析构函数不能重载
③如果没有显示的给出,编译器会默认生成一个。
④在对象生命周期结束的时候,由编译器自动调用。
⑥析构函数在函数体内并不是删除对象,而是做一些清理工作。这里有一点不得不提一 下:用delete来销毁对象时,会调用其析构函数,并将所占的全局堆内存空间返回。但从销毁对象到程序退出该作用域,对象的指针还存在于栈 中,并指向对象本来的位置。显然,这种情况下调用指针是非常危险的。Win32平台下访问这种指针,结果有三种可能情况:访问违例、取得无意义值、取得其 他对象。第一种会导致进程崩溃,后两种虽然不会立即崩溃,但是可能会有不可预测的行为操作或造成对象不必要的变化,需要谨慎避免。
⑦析构顺序:利用栈的特性,先进后出,所以对后进的对象先进行析构,如果有全局对象或者静态局部对象,则它们在main函数结束或者调用exit函数时被析构。
4.赋值运算符重载
如果已经定义了多个对象,这些同类型对象之间可以相互赋值,对象赋值可以通过"="来实现,这是因为编译器提供了一个默认的赋值运算符重载,看代码:
class Date
{
public:
Date()
{}
Date(int year,int month,int day)
:_year(year)
,_month(month)
,_day(day)
{}
Date& operator=(const Date &date)//赋值运算符重载
{
_year = date._year;
_month = date._month;
_day = date._day;
return *this;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2017,10,29);
Date d2;
d2 = d1;
return 0;
}
5.取地址操作符重载
Date* operator&()
{
return this;
}
6.const修饰的取地址操作符重载
const Date* operator&()const
{
return this;
}