六个默认成员函数是指,构造函数,拷贝构造函数,赋值运算符重载函数,析构函数,取地址操作符重载、const
修饰的取地址操作符重载。
1.构造函数
特点:(1)函数名与类名相同
(2)可以重载
(3)构造对象时自动调用构造函数
(4)无返回值
(5)如果类中没有给出构造函数,编译器会自动产生一个缺省的构造函数,如果类中有构
造函数,编译器就不会产生缺省构造函数。
(6)全缺省的构造函数和无参的构造函数只能有一个,否则调用的时候就会产生冲突。(想想缺省参 数的使用就明白了)
(7)构造函数就是给类的对象的成员变量进行初始化,初始化的方法也有两种
列表初始化,和函数内初始化(下面有解释)
例:(使用日期类为例)无参构造函数
class Date
{
int _year;
int _month;
int _day;
public:
Date() //可以理解为变相的全缺省 定义对象时方法和全缺省相同
{
cout<<"Date(int year,int month,int day)"<<endl;
this->_year = 5;
this->_month = 6;
this->_day = 7;
}
}
缺省构造函数(可以全缺省,也可以部分缺省(遵循缺省规则))
class Date
{
int _year;
int _month;
int _day;
public:
Date(int year ,int month = 2, int day =1) //遵循缺省规则
{
cout<<"Date(int year,int month,int day)"<<endl;
this->_year = year;
this->_month = month;
this->_day = day;
}
}
初始化列表初始化成员变量
构造函数的执行可以分成两个阶段,初始化阶段和计算阶段,初始化阶段先于计算阶段。
初始化阶段:所有类类型(class type)的成员都会在初始化阶段初始化
,即使该成员没有出现在构造函数的初始化列表中。
tip(初始化列表初始化时,对象正在创建,没有this指针)
计算阶段:一般用于执行构造函数体内的赋值操作 ,如果是内部数据类型赋值(如int,char),
则效率和初始化列表初始化效率差不多,如果是定义的类 类型,可能会涉及到多次调用
所以从这方面考虑,初始化列表的效率相对于构造函数内部赋值要高。
class Date
{
int _year;
int _month;
int _day;
public:
Date(int year = 1900 ,int month = 1, int day =1)
:_year(year)
,_month(month)
,_day(day)
{
}
}
成员列表的效率问题
例
class Time{
public:
Time(int hour= 1, int minute=1, int second=1)
{
m_hour = hour;
m_minute = minute;
m_second = second;
cout << "构造时间类对象中..." << endl; //方便一会测试观察
}
private:
int m_hour;
int m_minute;
int m_second;
};
class Date
{
public:
Date(int year, int month, int day,Time t)
//:m_year(year),
// m_month(month),
// m_day(day),
//m_t(t)
{
m_year = year;
m_month = month;
m_day = day;
m_t = t;
}
void print()
{
cout << m_year << "-" << m_month << "-" << m_day << endl;
}
private:
int m_year;
int m_month;
int m_day;
Time m_t;
};
int main()
{
Time t(10,36,20);
Date d(2017,6,26,t);
d.print();
system("pause");
return 0;
}
如果使用初始化列表 给m_t初始化,只在main函数中定义对象t时调用一次Time的构造函数。
而在构造函数体内初始化则调用两次Time的构造函数,一次在main中,一次在Date的构造函数中创建m_t调用
测试:
这是使用初始化列表
这是在构造函数内部赋值。
有些成员变量必须再初始化列表中初始化,比如:
1. 常量成员变量。(常量创建时必须初始化,因为对于一个常量,我们给它赋值,是不
对的)
2. 引用类型成员变量。(引用创建时必须初始化)
3. 没有全缺省构造函数的类成员变量。(如果构造函数的参数列表中有一个类的对象,并
且该对象的类里没有缺省参数的构造函数时,要是不使用初始化列表,参数中会调用无
参或者全缺省的构造函数,而那个类中又没有。)比如创建m_t时,没有给m_t实参,如
果Time类中没有全缺省构造参数或者默认构造参数,则m_t无法在构造函数体内创建,
系统就会提示,没有默认的构造函数
———————————————————————————————————————————————————
2.拷贝构造函数
构造一个对象d1,并拷贝另一个对象d(此时构造出的d1并不调用构造函数,而是直接调用拷贝构造函数)
函数写法和使用方法(以Date类为例)
为什么传参使用传引用的方法 ?
仔细想想,如果不传引用,则形参就会调用拷贝构造来拷贝实参,在形参调用拷贝构造函数时,形参的形参又会调用拷贝构造,这样下去会陷入无限的递归。
Date(const Date& d) //函数写法
{
_year = d._year;
_month = d._month;
_day = d._day;
cout<<"拷贝"<<endl;
}
int main() //使用方法
{
Date d(12,3,4);
Date d1(d);
return 0;
}
如果用默认的拷贝构造函数,会在一些时候出现问题,比如使用动态内存开辟空间时。用通讯录的联系人创建为例
public:
Contact(char* name, int age)
{
m_name = (char *)malloc(sizeof(char)*10);
if (NULL == m_name)
{
cout << "out of memory" << endl;
exit(1);
}
strcpy(m_name,name);
m_age = age;
}
~Contact()
{
free(m_name);
m_name = NULL;
}
int main()
{
Contact c(chen,12);
Contact c1(c);
return 0;
}
当c创建后,c.m_name指向堆上的一个空间,c1对c拷贝构造,拷贝构造是传引用,所以c1.m_name指向是堆上c1.m_name所指向的空间,当c和c1生命周期结束时,会调用析构函数(释放内存,详情看下面讲解),而同一块空间
被释放两次,系统就会报错。
如果是值的拷贝,则为浅拷贝,相对的深拷贝是为对象重新分配一块空间后,再拷贝。
深浅拷贝在之后再详细讨论。
调用拷贝构造函数的3种情况:
1.当用类的一个对象去初始化该类的另一个对象时。Date d1(d)
2.当函数的形参是类的对象,调用函数时进行形参和实参的结合时。比如刚刚讲的Date d(2017,6,26,t);形参临时拷贝
3.当函数的返回值是对象,函数执行完返回调用者时。(函数运行结束后,返回的对象
会复制到一个无名对象中,然后返回的对象会消失,当调用语句执行完之后,无名对
象就消失了)
调用拷贝构造函数的两种方法:
1.代入法:
Contact c1(c);
2.赋值法:
Contact c1 = c;
____________________________________________________________________________________________
3.析构函数
定义方法
~Contact()
{
free(m_name);
m_name = NULL;
}
析构函数是一种特殊的成员函数,具有以下特点:
1. 析构函数函数名是在类名加上字符~。
2. 无参数无返回值(但有this指针)。
3. 一个类有且只有一个析构函数,所以肯定不能重载。若未显示定义,系统会自动生成
缺省的析构函数。
4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。
5. 注意析构函数体内并不是删除对象,而是做一些清理工作。(比如我们在构造函数
中动态开辟过一段空间,函数结束后需要释放,而系统自动生成的析构函数不管内
存释放,所以需要人为地写出析构函数)
注意:对象生命周期结束后,后构造的对象先释放。
———————————————————————————————————————————————————
4.赋值运算符重载函数
赋值运算符重载函数是两个已有对象一个给另一个赋值的过程。它不同于拷贝构造函数,拷贝构造函数是
用已有对象给新生成的对象赋初值的过程
类默认的赋值运算符重载函数是浅拷贝
例
Date& operator=(const Date& d)
{
if(this != &d) //=左右两个对象不为同一个对象再进行赋值操作
{
this->_day = d._day;
this->_month = d._month;
this->_year = d._year;
}
return *this;
}
如果赋值的对象中存在类似内存开辟的问题,则也要考虑到深拷贝的问题。所以此时不能使用类默认的
赋值运算符重载函数,需要手动写出深拷贝版的赋值运算符重载函数
———————————————————————————————————————————————————
5.const修饰的取地址操作符的重载
const Date * operator&() const //这里的const修饰的是默认参数this
{
return this;
}
____________________________________________________________________________________________
6.取地址操作符的重载
Date * operator&()
{
return this;
}
总结:
6个默认函数中,前4个互相联系,在不同情况下调用不同的函数,看似复杂,只要搞清楚它们各自的职责,再回头看Date类的各种函数方法,就豁然开朗了。