构造函数
基本概念
- 类的默认成员函数:C++中类有六个默认成员函数,分别是:构造函数、拷贝构造函数、析构函数、赋值运算符重载、取地址运算符重载以及const修饰的取地址运算符重载;所谓”默认”,即如果我们不写,系统会自动帮我们生成
- 为了我们演示的方便,我们先定义一个简单的日期类:
class Date
{
public:
//成员函数
private:
int _year;
int _month;
int _day;
};
- 我们如果要用Date类来定义一个对象,那么我们如何对这个对象的各个变量进行初始化?构造函数可以帮我们做这件事情
- 成员变量为私有的,要对它们进⾏初始化,必须⽤⼀个公有成员函数来进⾏。同时
这个函数应该有且仅在定义对象时⾃动执⾏⼀次,这时调⽤的函数称为构造函数 - 我们说构造函数是默认成员函数,如果我们不定义,能不能为我们自动的构造一个日期类的对象并对其初始化呢?我们来验证一下:
- 编译器报错了,这是因为我们如果不自己定义构造函数,编译器为我们自动生成的是缺省的构造函数,我们只能构造对象但是不能对其进行初始化
- 一般我们都会显示的定义构造函数,因为编译器自动生成的不由我们控制
//无参的构造函数
Date()
{}
- 下面我们自定义一个构造函数
//带参的构造函数
Date(int year,int month,int day)
{
_year=year;
_month=month;
_day=day;
}
- 我们构造一个日期类的对象并对其进行初始化:
- 有时候,我们并不想传参数,想生成一个默认的日期对象并把它初始化成一个默认值,则我们可以这样定义:
Date(int year=2018,int month=4,int day=26)
{
_year=year;
_month=month;
_day=day;
}
- 如果定义了缺省的构造函数,我们就可传缺省的参数,但是注意:参数是从有往左缺省的
- 注意:Date d4();并不是全缺省构造出来的对象,也不是无参构造函数构造出来的对象;Date d5;是通过调用缺省的构造函数构造的对象,它的初始值为:2018-4-26
- 上面我们演示的全缺省构造函数,还可以半缺省构造函数
Date (int year, int month = 1)
{
_year = year ;
_month = month ;
_day = 1;
}
- 这样我们只用指定年份就可以构造一个日期对象并对它进行初始化了,但是这种我们一般不常用
- 注意:若缺省参数声明和定义分离,则可以在声明或定义中给默认参数,不能两个都给
总结:构造函数是特殊的成员函数,其特征如下
- 函数名与类名相同
- 无返回值
- 对象构造时由系统自动调用
- 构造函数可以重载
- 如果类定义中没有给出构造函数,则C++编译器⾃动产⽣⼀个缺省的构造函
数,但只要我们定义了⼀个构造函数,系统就不会⾃动⽣成缺省的构造函数 - ⽆参的构造函数和全缺省值的构造函数都认为是缺省构造函数,并且缺省的构
造函数只能有⼀个 - 系统自动生成的构造函数对于内置类型的成员变量来说,什么也没有做,所以通常情况下我们都是自定义构造函数的
深度探索构造函数
- 类的成员变量有两种初始化方式:构造函数体内赋值(如同上一小节所演示),初始化列表
- 我们首先定义一个初始化列表的构造函数:
Date(int year = 2018, int month = 4, int day = 26)
:_year(year)
,_month(month)
,_day(day)
{}
- 我们可以看到这两种初始化的方式可以达到同样的目的,那么它们两个有什么区别呢?使用哪个更高效呢?
- 我们可以通过下面定义的这个实例再探构造函数初始化:
class Time
{
public:
Time(int hour=0)
{
_hour=hour;
}
private:
int _hour;
};
class Date
{
public:
Date(int year = 2018, int month = 4, int day = 26)
:_year(year)
,_month(month)
,_day(day)
,Time _t(hour)
{}
private:
int _year;
int _month;
int _day;
Time _t;
};
- 启动单步调试:
- 我们用其构造一个日期类d,单步调试,我们发现在进入构造函数体内之前自动去调用了Time类的构造函数。所以说明,在进入函数体之前会自动的走一次初始化列表,不论我们写不写初始化列表,编译器都会自动的走一次初始化列表的
- 当然我们也可以自己写
- 那么我们在函数体内是如何对Time类对象_t 进行初始化的呢?
Date(int year = 2018, int month = 4, int day = 26,int hour=0)
{
_year = year;
_month = month;
_day = day;
_t=hour;//Time t(hour); _t=t;
}
- 我们可以看到,如果要在函数体内进行初始化的话,则需要调用两次构造函数和一次赋值运算符重载,这样的代价显然比在初始化列表中初始化来的大
- 而且在我们调试的过程中,我们要去调用两次Time类的构造函数,一次是在进入函数体之前,一次是在进入函数体之后
- 所以,我们建议使用初始化列表进行初始化,因为它比函数体内初始化更高效
总结,哪些成员必须在初始化列表中进行初始化
- 如果一个类的成员变量是自定义类型,如果它没有缺省的构造函数,则必须在初始化列表中进行初始化(_t调不到缺省的构造函数)
- 常量(初始化的时候赋值,而且不可变),初始化列表相当于对每个成员变量定义
- 引用(定义时必须进行初始化)
补充:成员变量的初始化是按照声明的顺序进行初始化,而不是在初始化列表中出现的顺序
class A
{
A(int x)
:_a2(x)
,_a1(_a2)
{
cout<<_a1<<endl;
cout<<_a2<<endl;
}
private:
int _a1;
int _a2;
};
A a1(10);
- 这个程序的输出结果:
- 因为先进行初始化的是_a1,_a2是随机值,所以_a1被初始化为随机值,_a2被初始化为10
拷贝构造函数
- 创建对象时使⽤同类对象来进⾏初始化,这时所⽤的构造函数称为拷贝构造函数,
拷贝构造函数是特殊的构造函数
Date(const Date& d)
{
_year = d._year; //为什么这里的对象可以直接访问私有成员变量?
_month = d._month;
_day = d._day;
}
Date d(2013,2,13);
Date d1(d);
- 通过上面的方式我们就可以定义一个拷贝构造函数
- 我们可能会有疑问,d是Date类构造出来的对象的引用,为什么它可以直接访问类的私有成员变量?
- 这是因为:(1)在类中,成员是可以直接访问类的私有/保护成员的;(2)C++的访问限定符是以类为单位,也就是说在这个单位内的成员是可以互相访问的
- 下面两种调用拷贝构造函数方式是等价的:
Date d(1997,2,22);
Date d1(d); //(1)
Date d2=d; //(2) 注意这不是赋值运算符重载
- 注意:拷贝构造函数必须是用一个已存在的对象去构造一个新的对象
- 思考:为什么传参要传引用而不是传值?
- 引用是对象的别名,对引用操作 等同于对这个对象操作;但是如果传值,则系统会调用拷贝构造函数对实参进行拷贝,在构造函数内部调用构造函数并且没有递归终止条件,那么将会导致无穷递归
总结,拷贝构造函数的特征:
- 拷贝构造函数其实是一个构造函数的重载
- 拷贝构造函数的参数必须使⽤引⽤传参,使⽤传值⽅式会引发⽆穷递归调⽤
- 若未显⽰定义,系统会默认缺省的拷贝构造函数。缺省的拷贝构造函数会依次拷贝类成员进⾏初始化,所以如果我们的类成员变量是内置类型,系统自动生成的拷贝构造函数也是够用的(但是注意,系统自动生成的是不受我们控制的)
赋值运算符的重载
- 对一个已经存在的对象进行拷贝赋值,和拷贝构造不同(拷贝构造:用一个已经存在的对象构造一个新的对象)
Date& operator = (const Date& d)
{
if (this != &d)
{
_year = d. _year;
_month = d. _month;
_day = d. _day;
}
return *this;
}
- operator=:是一个函数名,它有两个参数:隐含的this和d
- d1=d2,返回值是左值,对于operaor=函数来说就是隐含的this,因为this在整个类域中一直存在,所以返回引用;如果不用引用返回,则需要创建临时变量(调用拷贝构造)
- 对于赋值运算符重载来说,我们应该避免自己给自己赋值,因为这是毫无意义的
- 为了支持连续赋值,我们必须给一个返回值
析构函数
~Date()
{}
- 当⼀个对象的⽣命周期结束时, C++编译系统会⾃动调⽤⼀个成员函数,这个特殊
的成员函数即析构函数 - 析构函数执行的是清理工作,但是并不是删除一个对象(只有我们在堆上动态开辟的空间需要我们自动去释放,其他的都会有系统自动回收)
- 对于顺序表这种类,调用系统的析构函数,则并不能正确的析构(因为会有动态内存开辟)
- 所以对于系统自动生成的析构函数,如果是内置类型则什么都不做,自定义类型取决于类型本身
- 构造函数调用的顺序和析构函数是相反的,即先定义的后析构,后定义的先析构
- 析构函数在类中只有一个,同构造函数一样没有返回值,并且也没有参数
取地址运算符重载
Date* operator&()
{
return this;
}
//const修饰的取地址运算符重载
const Date* operator&() const
{
return this;
}
- 对于取地址运算符,我们有这样一个问题,如果我们定义一个类,但是不想让外面的操作来取地址,那么我们应该怎么做?
- (1)可以在取地址运算符重载中返回NULL
- (2)只声明不定义(编译器以声明为主,如果有声明无定义则会有链接错误)