对于以下Date类:
class Date
{
public :
void Init(int month, int year, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << " " << _month << " " << _day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1, d2;
d1.Init(2023, 11, 4);
d2.Init(2022, 1, 12);
d1.Print();
d2.Print();
return 0;
}
对于Date类,可以通过Init共有方法给对象设置日期,但如果每次创建对象时都调用该方法设置信息,未免有点麻烦,于是就有了构造函数.
构造函数是一个特殊的成员函数,名字与类名相同,创建“类”类型对象时由编译器自动调用,来保证每个成员都有一个合适的初始值,并且在对象的整个声明周期内只会调用一次
构造函数是一个特殊的成员函数,名字与类名相同,但是要注意:构造函数虽然名字叫构造函数,但是构造函数的主要任务并不是开空间创建对象!(开空间是由系统自动在栈区开辟空间),而是初始化对象。
其特征如下
int main()
{
Date d1;//默认调用无参构造函数
Date d2(2015, 1, 1);//调用带参数的构造函数
Date d3();//错误写法
return 0;
}
注意
如果通过无参构造函数来创建对象时,对象后面就不要用大括号,不然编译器就会认为这是函数声明
#include
using namespace std;
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;
}
注意:
由于在系统默认生成的构造函数中对内置类型并不做处理,所以,在C++11中针对内置类型成员不初始化的缺陷打了一个补丁,即:内置类型成员变量在类中声明时可以给缺省值(默认值)
#include
using namespace std;
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 = 2023;
int _month = 1;
int _day = 5;
//自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
在经过以上处理之后,再来看对象d中各成员变量的值:
可以看到,当内置类型被给予一个缺省值时,再会使用缺省值进行初始化
在创建对象时,编译器会通过调用构造函数,给对象的每一个成员变量一个合适的初始值
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
虽然经过以上构造函数调用后,对象已经已经有了一个初始值,但是在这并不能将其称为对 对象中成员变量进行初始化,因为初始化是只能进行一次,但是构造函数体内(括号内部)是可以进行多次的赋值,那么,对象到底是在什么时候进行初始化的呢?这个时候就要引出一个概念:初始化列表。
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个“成员变量”后面跟一个放在括号中的初始值或表达式
class Date
{
public:
Date(int year, int month, int day)
:_year(year)
, _month(month)
,_day(day)
{
}
private:
int _year;
int _month;
int _day;
};
1、每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
2.、类中若包含以下成员,就必须放在初始化列表进行初始化
class A
{
public:
A(int a)
:_a(a)
{}
private:
int _a;
};
class B
{
public:
B(int a1, int b)
:_a1(a1)//调用_a1对象的A(int a)构造函数
, _rb(b)//引用变量_rb引用实体b
, _n(10)//为const修饰的常变量赋予初值10
{}
private:
A _a1;
int& _rb;
const int _n;
};
补充:
首先对于自定义类型成员,其实当自定义类型成员调用构造函数没有被显示写在初始化列表中时,自定义类型成员的构造函数也是在初始化列表进行调用的(即规定自定义成员在初始化列表调用自己的构造函数(包括默认构造和其他带参构造)),由于前面所说的构造函数只能够调用一次,当在初始化列表中已经调用过一次了,就肯定是不能在构造函数的函数体内部再次调用。
对于引用成员变量,我们都知道,引用变量只能引用一个实体,并且引用在被定义的时候就要初始化,之后在使用这个引用就相当于是在使用其所引用的实体变量。试想一下,如果不在初始化列表中及时对引用进行初始化,那么等到运行到构造函数体内,这个引用已经被初始化过了(该引用变量引用系统分配的随机变量),这个时候想初始化也已经晚了
对于const修饰的变量,和引用同理:在初始化过后就已经无法改变其值
总的来说,初始化列表就是成员变量进行定义和初始化的地方,而构造函数体内是用来进行"二次赋值"的地方,对于没有给缺省值的内置类型的成员变量,也会在初始化列表中进行初始化,只是用的是随机值,如果我们在声明变量时给了一个缺省值,那么这个成员变量就会默认用这个缺省值进行初始化
3、尽量能使用初始化列表初始化就使用初始化列表,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定是先经过初始化列表中进行初始化的
但是,有些情况是不适合在初始化列表进行初始化的
例如有些需要检查的工作,完全依赖初始化列表也不行
class A
{
public:
A(int n)
:_a((int*)malloc(sizeof(int)*n))
{}
private:
int* _a;
};
在代码中,如果构造函数初始化列表中的malloc开辟空间失败,也没有及时的提醒
正确的写法应该为:
class A
{
public:
A(int n)
:_a((int*)malloc(sizeof(int)*n))
{
if (_a == nullptr)
{
perror("malloc");
exit(-1);
}
}
private:
int* _a;
};
4、成员变量在类中声明次序就是在初始化列表中的初始化顺序,和在初始化列表中的先后次序无关
请分析以下代码运行结果
class A
{
public:
A(int a)
:_a1(a)
,_a2(_a1)
{}
void print()
{
cout << _a1 << " " << _a2 << endl;
}
private:
int _a2;
int _a1;
};
int main()
{
A a(1);
a.print();
return 0;
}
A、1 1
B、程序崩溃
C、编译不通过
D、1 随机值
正确结果应为D,原因也很简单,在初始化列表中先初始化的是_a2,再初化_a1
构造函数包含了构造函数的函数体部分和初始化列表部分,其中初始化列表才是成员变量和成员对象进行初始化和定义的地方;构造函数的主要功能就是对成员变量进行初始化,开空间是编译器负责的。
另外还有一类特殊构造函数:拷贝构造,这个构造下篇博客再进行介绍。