构造函数详解
类的6个默认的成员函数
构造函数的概念:
- 构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有一个合适的初始值,并且在对象的生命周期内只调用一次。
构造函数的特性
- 函数名与类名相同。
- 无返回值。
- 编译器自动调用对应的构造函数。
- 构造函数可以重载。
为什么要引出构造函数这一概念
- 看下面的代码,对于Date类,可以通过InitDate公有的方法给对象设置内容,但是如果每次创建对象都调用该方法设置信息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢?
- 现在我们的需求就是不通过对象去调用初始化对象的数据,我们希望当这个对象创建出来的时候,他就已经是具有一定的初始值的,那么如何做到我们现在的这个需求的呢?
- 由此,引入了构造函数这一个概念,如下所示:
#include<iostream>
using namespace std;
class Date
{
public:
void InitDate(int year, int month, int day) //进行初始化的操作
{
_year = year;
_month = month;
_day = day;
}
void PrintDate() //打印进行检测
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1, d2, d3;
d1.InitDate(2020, 4, 30);
d1.PrintDate();
d2.InitDate(2020, 4, 29);
d2.PrintDate();
d3.InitDate(2020, 4, 28);
d3.PrintDate();
}
构造函数的功能
- 需要注意的一点是,虽然构造函数叫"构造"函数,但是构造函数并不是用来构造对象的,构造函数的功能是用来完成对象的初始化的
#include<iostream>
using namespace std;
class Date
{
public:
//无参的构造函数
Date() //构造函数 ,没有返回值
{
cout << "Date()" << this << endl; //打印this是为了看当前构造的是哪一个对象
//把对象的地址打印出来
}
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
//两个构造函数形成了重载
//一个是没有参数的构造函数,一个是具有三个参数的构造函数
//创建几个对象,编译器就会调用几次构造函数
//调用构造函数的次数与对象的次数是相同的
//因为构造函数调用的时机就是在创建对象的时候调用
void InitDate(int year, int month, int day) //进行初始化的操作
{
_year = year;
_month = month;
_day = day;
}
void PrintDate() //打印进行检测
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1, d2, d3;
Date d4(2020, 4, 27);
Date d5();
//并不是创建了一个对象
//这是一个函数声明,相当于我是有一个函数名称
//为d5的函数,这个函数的返回值类型是Date类型,没有参数
d1.InitDate(2020, 4, 30);
d1.PrintDate();
d2.InitDate(2020, 4, 29);
d2.PrintDate();
d3.InitDate(2020, 4, 28);
d3.PrintDate();
d4.PrintDate();
}
如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成
- 当然,前提是,只有用户没有显示定义构造函数的时候,编译器才会去生成默认的构造函数
#include<iostream>
using namespace std;
class Date
{
public:
//无参的构造函数
//Date() //构造函数 ,没有返回值
//{
// cout << "Date()" << this << endl; //打印this是为了看当前构造的是哪一个对象
// //把对象的地址打印出来
//}
//Date(int year, int month, int day)
//{
// _year = year;
// _month = month;
// _day = day;
//}
//两个构造函数形成了重载
//创建几个对象,编译器就会调用几次构造韩素
void InitDate(int year, int month, int day) //进行初始化的操作
{
_year = year;
_month = month;
_day = day;
}
void PrintDate() //打印进行检测
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
//编译器生成的构造函数是默认的构造函数
//默认的构造函数一定是无惨的
//如果说,用户定义构造函数了,编译器就不会生成的
//如果这个时候再创建一个带有三个参数的对象的话
//就一定是不会创建成功的
//对象创建成功了,就说明是有默认的构造函数存在的
return 0;
}
无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数
#include<iostream>
using namespace std;
class Date
{
public:
//无参的构造函数
Date() //构造函数 ,没有返回值
{
cout << "Date()" << this << endl; //打印this是为了看当前构造的是哪一个对象
//把对象的地址打印出来
}
//全缺省的构造函数
Date(int year=2000, int month=1, int day=1)
{
_year = year;
_month = month;
_day = day;
}
//两个构造函数形成了重载
//创建几个对象,编译器就会调用几次构造函数
void InitDate(int year, int month, int day) //进行初始化的操作
{
_year = year;
_month = month;
_day = day;
}
void PrintDate() //打印进行检测
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
}
- 上面的代码,是会报错的,无法通过编译,报错的内容是—“Date::Date”:对重载函数的调用不明确,因为上面的代码中,有两个默认的构造函数,因为不带参数的构造函数和全缺省的构造函数都被看为默认的构造函数,所以说,现在有两个构造函数,编译器不知道到底要去调用哪个构造函数,所以说,就会报错,所以说,默认的构造函数只能存在有一个,下面的代码就可以通过编译了
#include<iostream>
using namespace std;
class Date
{
public:
//全缺省的构造函数
Date(int year = 2000, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//两个构造函数形成了重载
//创建几个对象,编译器就会调用几次构造函数
void InitDate(int year, int month, int day) //进行初始化的操作
{
_year = year;
_month = month;
_day = day;
}
void PrintDate() //打印进行检测
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
}
关于编译器生成的默认成员函数,很多人会有疑惑:在我们不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?d对象调用了编译器生成的默认构造函数,但是d对象year/month/_day,依旧是随机值。也就说在这里编译器生成的默认构造函数并没有什么用?
- 解答:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语法已经定义好的类型:如int/char…,自定义类型就是我们使用class/struct/union自己定义的类型,看看下面的程序,就会发现编译器生成默认的构造函数会对自定类型成员_t调用的它的默认成员函数
class Time
{
public:
Time()
{
cout << "Time()" << endl;
_hour = 0;
_minute = 0;
_second = 0;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year;
int _month;
int _day;
// 自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
成员变量的命名风格
// 我们看看这个函数,是不是很僵硬?
class Date
{
public:
Date(int year)
{
// 这里的year到底是成员变量,还是函数形参?
year = year;
}
private:
int year;
};
// 所以我们一般都建议这样
class Date
{
public:
Date(int year)
{
_year = year;
}
private:
int _year;
};
// 或者这样。
class Date
{
public:
Date(int year)
{
m_year = year;
}
private:
int m_year;
};
// 其他方式也可以的,主要看公司要求。一般都是加个前缀或者后缀标识区分就行。
再谈构造函数
问题:构造函数体中的语句是不是初始化?
- 赋值和初始化的区别----赋值是可以进行多次赋值的,但是初始化的话,只能进行一次的初始化操作
- 可以通过如下的方式进行验证,验证到底是初始化还是赋值
- 发现上述的代码编译成功了,所以说明构造函数体中不是初始化,而是赋值
- 上述代码中a为const类型的变量,对代码进行编译之后,发现代码无法通过编译,也就更加证实了,构造函数体内是赋值而并非是初始化,因为const类型的成员无法进行赋值的操作
构造函数体赋值
- 在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值
- 构造函数最主要是用来完成对象的初始化的工作的,但是我们在构造函数函数体内进行的赋值操作虽然说也可以把给参数的值放到对象里面去,但是构造函数函数体里面不是初始化,因为是赋值操作,那么我们如何来判断构造函数函数体里面是赋值还是初始化呢?我们可以使用一个在定义的时候必须进行初始化的东西来判断构造函数函数体里面到底是赋值还是初始化。那么很容易想到的就是引用在定义的时候必须进行初始化操作,那么我们可以在private定义的变量中引入一个引用变量,给了引用变量之后是会报错的,那么就说明构造函数函数体里面的操作其实是赋值而不是初始化的操作
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
- 虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称作为类对象成员的初始化,构造函数体中的语句只能将其称作为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值
- 所以得出结论—构造函数函数体中是赋值不是初始化
初始化列表的概念
- 构造函数具有初始化列表,并且只有工造函数具有初始化列表,别的函数都没有初始化列表
- 初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year, int month, int day)
:_year(year),
_month(month),
_day(day)
{
cout << "Date(int year, int month, int day)" << endl;
}
private:
int _year;
int _month;
int _day;
};
- 那么如何验证初始化列表是初始化而不是赋值呢?
- 因为我们是知道的,赋值是可以进行多次的,但是初始化只能初始化一次,那么我们就可以利用这个性质,在初始化列表中让一个变量出现多次,如果代码报错的话,那么就说明初始化列表是初始化而不是赋值了,如下所示,下面的代码时会报错的
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year, int month, int day)
:_year(year),
_month(month),
_day(day),
_day(day)
{
cout << "Date(int year, int month, int day)" << endl;
}
private:
int _year;
int _month;
int _day;
};
- 报错内容是_day已经初始化了
- 初始化只能初始化一次,如果多次进行初始化操作的话,代码就会报错
- 就好像是一个人不能多次出生是一样的道理
初始化列表的功能
- 初始化列表就是专门就是为了对类中各个成员变量进行初始化的操作,而初始化呢他不是可以初始化两次的,所以一个变量如果初始化两次的话就会出错。因为在对变量进行初始化的时候需要为变量开辟内存空间,如果已经为一个变量开辟了内存空间的话,就不需要为那个变量再次开辟内存空间了。 一个变量开辟两个空间肯定是有问题的
- 初始化列表并没有规定各个变量出现的次序,就算初始化列表的次序和形参出现的次序是不一样的,代码也是不会报错的,所以说,初始化列表并没有强制规定参数的次序要和形参的次序是一样的。但是一般情况下不建议这么做,防止出现像这样的问题
为什么month会是随机值呢,因为编译器先去初始化year,然后初始化year完成之后,编译器开始去初始化month,但是初始化列表给的是用day去初始化month,但是此时day并没有进行初始化的操作,所以最终看出,month为随机值。
注意事项
- (1)初始化列表中成员的出现次序,不代表其真正的初始化次序
- (2)成员变量在初始化列表中的初始化次序为其在类中的声明次序
- 建议:最好不要使用成员初始化成员
- 虽然说我们可以把_day写在_month的前面,但是我们在初始化的时候,还是先去初始化的_month然后再去初始化的_day
- 编译器是不会按照我们所给出的初始化的顺序来进行初始化的,编译器是按照形参出现的顺序来进行初始化操作的
拷贝构造函数也可以有初始化列表
#include<iostream>
using namespace std;
class Date
{
public:
Date(const Date& d)
: _year(d._year)
, _month(d._month)
, _day(d._day)
{
// _year = d._year;
// _month = d._month;
// _day = d._day;
cout << "Date(Date&):" << this << endl;
}
private:
int _year;
int _month;
int _day;
};
引用变量以及const类型的变量的注意事项
- 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
- 类中包含以下成员,必须放在初始化列表位置进行初始化:(在构造和拷贝构造的地方都需要进行初始化)
- (1)引用成员变量
- (2)const成员变量
#include<iostream>
using namespace std;
class Date
{
public:
#if 0
// 初始化列表
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{
// 构造函数体中是赋初值不是初始化
// _year = year;
// _month = month;
// _day = day;
//r = _day;
cout << "Date(int,int,int):" << this << endl;
}
#endif
// 初始化列表
// 1. 初始化列表中成员的出现次序,不代表其真正的初始化次序
// 2. 成员变量在初始化列表中的初始化次序为其在类中的声明次序
// 建议:最好不要使用成员初始化成员
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
, _r(_day)
, _a(_day)
{
// 构造函数体中是赋初值不是初始化
// _year = year;
// _month = month;
// _day = day;
//r = _day;
cout << "Date(int,int,int):" << this << endl;
}
Date(const Date& d)
: _year(d._year)
, _month(d._month)
, _day(d._day)
, _r(d._r)
, _a(d._a)
{
// _year = d._year;
// _month = d._month;
// _day = d._day;
cout << "Date(Date&):" << this << endl;
}
// d1 = d2 = d3;
Date& operator=(const Date& d)
{
cout << this << "=" << &d << endl;
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
~Date()
{
cout << "~Date():" << this << endl;
}
int _year;
int _month;
int _day;
int& _r;
const int _a;
};
- (3)类类型成员(该类没有默认构造函数)
#include<iostream>
using namespace std;
class Time
{
public:
Time(int hour, int minute, int second)
: _hour(hour)
, _minute(minute)
, _second(second)
{
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
, _t(0, 0, 0)
//, _t() //调用无参的默认的构造函数
{
cout << "Date(int,int,int):" << this << endl;
}
private:
int _year;
int _month;
int _day;
Time _t;
//如果没有上面的_t(0, 0, 0),直接给出Time_t是会报错的,原因在于
//声明了一个time类的对象t,那么这个对象t一定是需要进行初始化的
//那么编译器会默认去Time类中寻找没有参数的构造函数,但是Time类此时是没有
//显示给出无参的构造函数的,所以就会出错
//那么我们为了可以正确的创建出来这个Time类类型的对象的话,那么我们就需要给出来一个有参数的
//就好比说_t(0, 0, 0),就可通过编译了
};
int main()
{
//Date d(2019, 3, 24);
Date d;
return 0;
}
- 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。(没写并不代表没有,还是会去调用相应的函数的)
- 其实也就是说,就算你的构造函数函数体里面什么都没有,他也是会优先去执行构造函数的初始化列表,所以,尽量还是使用初始化列表来进行初始化操作。
- 调试起来的话,会首先停在构造函数函数名的位置上,并没有直接走到了函数体的内部
- 看起来构造函数没有写初始化列表,但是需要注意,没有写并不代表没有,因为如果用户没有写的话,其实编译器是会自动去加上的,只不过一般情况下成员变量是用随机值进行初始化的
- 要先初始化好,才能进入到构造函数的函数体中
#include<iostream>
using namespace std;
class Time
{
public:
Time(int hour = 0, int minute = 0, int second = 0)
: _hour(hour)
, _minute(minute)
, _second(second)
{
cout << this->_hour << endl;
}
void TestFunc()
{
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
// : _year(year)
// , _month(month)
// , _day(day)
//, _t() //调用无参的默认的构造函数
{
cout << "Date(int,int,int):" << this << endl;
}
private:
int _year;
int _month;
int _day;
Time _t;
};
- 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关,也就是说,不是按照我给的初始化列表的次序来进行初始化操作的,编译器是按照声明的次序来进行初始化的(上面已经提到过了)
提问,在初始化列表中如何知道对象已经构造好了?
- 就是在构造函数的初始化列表完成之后,去打印一下this,如果代码可以正常的通过编译,那么就说明我的对象是构造好了的
#include<iostream>
using namespace std;
class Time
{
public:
Time(int hour = 0, int minute = 0, int second = 0)
: _hour(hour)
, _minute(minute)
, _second(second)
{
cout << this<< endl; //我的意图是要去打印this,然后发现代码可以正常通过编译
//那么,也就是说,此时,我的对象已经构造好了
}
private:
int _hour;
int _minute;
int _second;
};
两个问题
在构造函数有没有把对象的地址传递过去呢?
- 对象的地址一定是传递过来了的,如果对象的地址没有传递过来的话,那儿我怎么知道当前构造的是那个对象呢?所以说对象的地址一定是已经传递过来了的
- 008560677 把t的首地址放到了exc寄存器里面,然后去调用Time类的函数
- 在构造函数调用之前,对象是不存在的,因为构造函数的目的就是用来初始化对象的,虽然对象本身是并不存在的,但是对象的空间是存在的,所以也就是说,在我们没有调用构造函数之前,我们就已经有了一段空间了,因为编译器是要进行编译的,所以他必须提前计算出这个对象需要多大的栈空间
- 定义了两个对象,所以编译器必须要在编译期间计算出这两个对象的大小,因为不可能做到一边运行一边修改,所以说,必须要提前计算好,在编译的时候是不会调用构造函数的,是在运行的时候去调用的
在构造函数的初始化列表的位置可不可以使用this指针呢?
- 在构造函数初始化列表的位置还不能使用构造函数,因为this是指向当前对象的,初始化列表的位置,当前对象还并没有构造好,所以还是不能使用的,还没有真正的区划分空间。但是在函数体里面是完全可以,因为对象已经完全的初始化完成了,是 可以正常去使用的
#include<iostream>
using namespace std;
class Time
{
public:
Time(int hour = 0, int minute = 0, int second = 0)
: _hour(hour)
, _minute(minute)
, _second(second)
{
cout << this->_hour << endl;
}
void TestFunc()
{
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
// : _year(year)
// , _month(month)
// , _day(day)
//, _t() //调用无参的默认的构造函数
{
cout << "Date(int,int,int):" << this << endl;
}
private:
int _year;
int _month;
int _day;
Time _t;
};
int main()
{
// 在编译器编译期间,已经为main分配好了栈空间
// 该空间中已经包含了函数体中的局部对象
Date d; // 在构造函数调用之前,对象是不存在的
Time t;
t.TestFunc();
return 0;
}
结论:也就是说对象的空间早就已经给好了,只不过缺的是构造函数,构造函数的功能就是把对象中各个成员变量给其初始化好就可以了
构造函数的功能
- 构造函数不仅就有初始化成员变量的功能,还具有类型转化的功能
- 因为单参的构造函数代码可读性太差,所以我们一般就把单参的构造函数的类型转换的功能给他禁止掉,用下面的关键字进行禁止
explicit关键字
- 构造函数不仅可以构造与初始化对象,对于单个参数的构造函数,还具有类型转换的作用
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year)
: _year(year)
{
cout << "Date(int,int,int):" << this << endl;
}
Date& operator=(const Date& d)
{
return *this;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d(2019);
d = 2020;
//一个是日期类型的变量,一个是整形的变量,在我们之前想的是,这时不能通过编译的
//但是没想到,这样的代码是可以通过编译的,原因在于
// 2020---> 通过单参构造函数--->临时对象
//也就是说构造函数具有类型转换的功能,本来是一个int类型,然后被转换了
// 用一个整形变量给日期类型对象赋值
// 实际编译器背后会用2020构造一个无名对象,最后用无名对象给d1对象进行赋值
//但是一般情况下,会把这种类型转换禁止掉,那么如何来禁止呢?
//禁止的方法就是在构造函数前面加上一个explicit关键字
return 0;
}
- 上述代码可读性不是很好,用explicit修饰构造函数,将会禁止单参构造函数的隐式转换。
#include<iostream>
using namespace std;
class Date
{
public:
explicit Date(int year)
: _year(year)
{
cout << "Date(int,int,int):" << this << endl;
}
Date& operator=(const Date& d)
{
return *this;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d(2019);
d = 2020; // 2020---> 通过单参构造函数--->临时对象
return 0;
}
之前说过的几个默认的构造函数,如果我么没有显示给出的话,编译器会生成默认的
#include<iostream>
using namespace std;
class Date
{
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d;
return 0;
}
- 问题来了,什么时候编译器会生成默认的?-------答案就是—>如果编译器感觉自己需要的时候就会生成,那么问题又来了,什么才是编译器需要的时候(一共有四种场景)
#include<iostream>
using namespace std;
class Time
{
public:
Time(int hour = 0, int minute = 0, int second = 0)
: _hour(hour)
, _minute(minute)
, _second(second)
{
cout << this->_hour << endl;
}
Time(Time&)
{
}
Time& operator=(Time& t)
{
return *this;
}
~Time()
{
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
// Date()
// {}
/*
Date(Date& d)
: _t(d._t)
{}
*/
private:
int _year;
int _month;
int _day;
Time _t;
};
int main()
{
Date d1; // 日期类的构造函数 // Time()
Date d2(d1); // Time(Time&)--->找个调用位置--->Date类的拷贝构造函数中
Date d3;
d3 = d1;
return 0;
}
- (1)现在有A类和B类两个类,B类中包含A类的对象,A类中有缺省的构造函数,B类中没有定义任何的构造函数,编译器此时就会生成默认的构造函数
- (2)Time(Time&)—>找个调用位置—>Date类的拷贝构造函数中
- (3)赋值也是一样的