C++ | 谈谈构造函数的初始化列表
面对无法构造成功的一些成员变量,例如:常变量(const),看看C++是怎么做的👈
一、引入
- 我们知道,对于下面这个类A的成员变量
_a1
和_a2
属于【声明】,还没有在内存中为其开辟出一块空间以供存放,真正开出空间则是在【定义】的时候,那何时定义呢?也就是使用这个类A去实例化出对象的时候 - 这个对象的空间被开出来了,难道里面的成员变量就一定开出空间了吗?这一点我们很难去通过调试观察
class A {
public:
int _a1; //声明
int _a2;
};
int main(void)
{
A aa; // 对象整体的定义,每个成员什么时候定义?
return 0;
}
- 如果现在我在类A中加上一个const成员变量的话,初始化的时候似乎就出现了问题
const int _x;
- 在搞清楚上面的问题之前你要明白
const
修饰的变量有哪些特点
const int i;
- 可以看到我在这里定义了一个整型变量i,它前面是用
const
进行修饰的,不过编译后报出了错误说【必须初始化常量对象】,因为对于const
修饰的变量在声明的时候是必须要去进行初始化的,也就是要给到一个值
现在我们就可以来聊聊有关上面的成员变量
_x
为什么没有被初始化的原因了👇
- 之前有讲过,若是我们自己不去实现构造函数的话,类中会默认提供一个构造函数来初始化成员变量,对于【内置类型】的变量不会处理,对【自定义类型】的变量会去调用它的构造函数。那么对于这里的
_a1
、_a2
、_x
都属于内置类型的数据,所以编译器不会理睬,可是呢const
修饰的变量又必须要初始化,这个时候该怎么办呢╮(╯▽╰)╭
💬有同学说:这还不简单,给个缺省值不就好了
- 这位同学说的不错,这个办法确实是可以解决我们现在的问题,因为C++11里面为内置类型不初始化打了一个补丁,在声明的位置给到一个初始化值,就可以很好地防止编译器不处理的问题
但是现在我想问一个问题:如果不使用这个办法呢?你有其他方法吗?难道C++11以前就那它没办法了吗?
- 底下的同学确实想不出什么很好的解决办法,于是这个时候就要使用到本模块要学习的【初始化列表】了
二、初始化的概念区分
- 在了解【初始化列表】前,你要先知道初始化的真正含义是什么
概念:在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
- 上面这个Date类是我们之前写过的,这里有一个它的有参构造函数,虽然在这个构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化。构造函数体中的语句只能将其称为【赋初值】,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。
三、语法格式及使用
【初始化列表】:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式
- 下面就是它的具体用法,这样便可以通过外界传入一些参数对年、月、日进行初始化
class Date
{
public:
Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
int main(void)
{
Date d(2023, 3, 30);
return 0;
}
可以通过调试来观察一下它到底是怎么走的
接下去我再来说说这一块的难点所在,准备好头脑风暴🌊
- 还是看回到我们上面的这个类A,知道了【初始化列表】这个东西,此时就不需要再声明的部分给缺省值了,直接使用初始化列表即可。不过可以看到,对于
_a1
和_a2
我给到了缺省值,写了初始化列表后,它们还会被初始化吗?
class A {
public:
A()
:_x(1)
{}
private:
int _a1 = 1; //声明
int _a2 = 1;
const int _x;
};
也通过调试来看一下
-
可以看到,即使在初始化列表没有给到
_a1
和_a2
的初始化,还是会通过给到的默认缺省值去进行一个初始化。根据上面所学,我给出以下的结论- 哪个对象调用构造函数,初始化列表是它所有成员变量定义的位置
- 不管是否显式在初始化列表写,编译器都会为每个变量在初始化列表进行初始化
好,接下去难度升级,请问初始化列表修改成这样后三个成员变量初始化后的结果会是什么呢? 会是1、2、1吗?
class A {
public:
A()
:_x(1)
,_a2(1)
{}
private:
int _a1 = 1; //声明
int _a2 = 2;
const int _x;
};
一样通过调试来看看
- 可以观察到,最后初始化完后的结果为1、1、1,最令你困惑的应该就是这个
_a2
了,因为我在声明的时候给到了缺省值,然后初始化列表去进行定义的时候又去进行了一次初始化,最后的结果以初始化列表的方式为主 - 这里要明确的一个概念是,缺省参数只是一个备份,若是我们没有去给到值初始化的话,编译器就会使用这个初始值,若是我们自己给到了明确的值的话,不会去使用这个缺省值了【如果不清楚看看C++缺省参数】
接下去难度继续升级,请问下面这样初始化后的结果是多少?
- 可以看到对于构造函数我不仅写了【初始化列表】,而且在函数体内部还对
_a1
和_a2
进行了++和- -,那此时会有什么变化呢?
class A {
public:
A()
:_x(1)
,_a2(1)
{
_a1++;
_a2--;
}
private:
int _a1 = 1; //声明
int _a2 = 2;
const int _x;
};
如果对于上面的原理搞清楚了,那看这个就相当于是再巩固了一遍。也是一样,无论是否给到缺省值都会去初始化列表走一遍,若是构造函数内部有语句的话就会执行
四、注意事项【⭐】
清楚了初始化列表该如何使用,接下去我们来说说其相关的注意事项
- 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
- 可以看到,若是一个成员变量在初始化列表的地方出现了两次,编译器在编译的时候就会报出【xxx已初始化】
- 类中包含以下成员,必须放在初始化列表位置进行初始化:
- const成员变量
- 这个在前面已经说到过了,
const
修饰的成员变量和构造函数对于内置类型不做处理产生了一个冲突,因此祖师爷就提出了【初始化列表】这个概念
- 这个在前面已经说到过了,
- 引用成员变量
- 第二点就是对于引用成员变量,如果有点忘记了看看C++引用
- 通过编译可以看出,这个引用型成员变量
_z
需要被初始化,它必须要引用一个值
- 没有默认构造的自定义类型成员(写了有参构造编译器就不会提供默认构造)
- 此时,我又写了一个类B,将它定义出的对象作为类A的成员变量,在类B中,有一个无参的默认构造,也写了相关的初始化列表去初始化
_b
- 此时,我又写了一个类B,将它定义出的对象作为类A的成员变量,在类B中,有一个无参的默认构造,也写了相关的初始化列表去初始化
class B {
public:
B()
:_b(0)
{}
private:
int _b;
};
class A {
public:
A()
:_x(1)
,_a1(3)
,_a2(1)
,_z(_a1)
{
_a1++;
_a2--;
}
private:
int _a1 = 1; //声明
int _a2 = 2;
const int _x;
int& _z;
B _bb;
};
- 通过调试来观察就可以看到,完全符合我们前面所学的知识,若是当前类中有自定义类型的成员变量,那在为其进行初始化的时候会去调用它的默认构造函数
- 但是现在我对这个构造函数做了一些改动,将其变为了有参的构造函数,此时编译时就报出了【没有合适的默认构造函数可用】
- 我们知道默认构造有:无参、全缺省和编译器自动生成的,都是不需要我们手动去调的。可以看到若是我在这里将其改为全缺省的话,就不会出问题了,因为它属于默认构造函数
💬那对于有参构造该如何去初始化呢?
- 还是可以利用到我们的【初始化列表】
通过调试来看看编译器是如何走的
- 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化
看完了上面这一种,我们再来看看稍微复杂一些的自定义类型是否也遵循这个规则
- 也就是我们之前写过的Stack和MyQueue类
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 10) //全缺省构造
{
cout << "Stack()构造函数调用" << endl;
_array = (DataType*)malloc(capacity * sizeof(DataType));
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
_size = 0;
_capacity = capacity;
}
//....
private:
DataType* _array;
size_t _size;
size_t _capacity;
};
- 此处我们主要观察Stack类的构造函数,因为在MyQueue中我没有写构造函数,为的就是使用它默认生成的构造函数去进行初始化。对于【内置类型】不做处理,不过我这里给到了一个缺省值,对于【自定义类型】会去调用它的默认构造
class MyQueue{
public:
//默认生成构造函数
private:
Stack _pushST;
Stack _popST;
size_t _t = 1;
};
int main(void)
{
MyQueue mq;
return 0;
}
可能读者有所忘却,我们再通过调试来看一下
- 可以观察到在初始化MyQueue类的对象时,因为内部有两个Stack类型的对象,所以就会去调用两次Stack类默认构造来进行初始化
- 那此时我若是将这个默认构造(全缺省构造)改为有参构造吗,它还调得动吗?
Stack(size_t capacity)
- 可以看到,此时就报出了我们上面有类似遇到过的【无法引用默认构造函数】,为什么呢?原因就在于我们写了,编译器自动生成的也就不存在了,但是我又没有传入对应的参数
- 此时就可以使用到我们本模块所学习的【初始化列表】了,将需要定义的值放在初始化列表,相当于就是为Stack类传入了一个有参构造的参数,不过对于没有写在这里的
_t
,依旧会使用我给到的初始值1
MyQueue()
:_pushST(10)
,_popST(10)
{}
可以通过调试再来看看
- 当然,如果你觉得不想要这个固定的10作为栈容量的话,也可以将这个MyQueue的构造函数设定为有参,自己传递进去也是可以的
- 最后再来看一下无参构造,也是默认构造的一种,在这里编译器也会去走MyQueue的初始化列表进行初始化
//无参构造
MyQueue()
{}
所以可以看出,对于【内置类型】不做处理,【自定义类型】会调用它的默认构造可以看出其实就是当前类构造函数的初始化列表在起作用
在看了MyQueue类各种初始化列表的方式后,其实也可以总结出一点,无论如何不管有没有给到缺省值,只要是显式地写了一个构造函数,就可以通过调试去看出编译器都会通过【初始化列表】去进行一个初始化
- 初始化列表也不一定能完成所有的工作,要灵活运用
💬 有同学说:这初始化列表感觉也太强大了吧,那我们之后就一直使用初始化列表吧!
- 没错,初始化列表确实是C++在类和对象这一块的亮点,但是也有其无法做到的事情。比方说我们在下面这个Stack类的有参构造中使用到了初始化列表,其中数组
_a
的空间采用【malloc】的形式进行开辟,不过呢我们知道【malloc】在开辟的过程中总会有失败的可能性,所以需要检查,但是初始化列表可做不了这样的事情,这一部分我们需要放在{}
内部进行 - 不仅如此,如果大家学习过数据库相关知识的话,可以知道这个
_a
可能需要去连接一下数据库,那对于这种工作来说也是初始化列表无法完成的
class Stack {
public:
Stack(int capacity = 10)
: _a((int*)malloc(sizeof(int)))
, _top(0)
, _capacity(capacity)
{
if (nullptr == _a)
{
perror("fail malloc");
exit(-1);
}
memset(_a, 0, sizeof(int) * capacity);
}
private:
int* _a;
int _top;
int _capacity;
};
- 再举个例子,有时候会有这样的需求,我们得在构造函数中开辟出一个二维数组的空间,那二维数组我们知道需要知道其【行】和【列】,这两个参数我们可以在初始化对象的时候进行传入,此时初始化列表就起到了很好的作用,但是呢,我们开辟一个二维数组的空间也可以在初始化列表中进行吗。这很明显是做不到的
- 这一块工作交给构造函数自身的初始化部分就很好,首先开辟出一个一维数组的空间,里面存放的都是一个个指针,即指针数组,然后再为这一个个指针开辟空间即可
class AA
{
public:
AA(int row, int col)
: _row(row)
, _col(col)
{
_a = (int**)malloc(sizeof(int*) * row);
for (int i = 0; i < row; i++)
{
_a[i] = (int*)malloc(sizeof(int) * col);
}
}
private:
int** _a;
int _row;
int _col;
};
因此从上面我们可以看出,初始化列表并无法完成所有的工作,所以需要根据我们的需求来进行灵活变通
- 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
- 最后再来看第五点,你认为下面这段代码最后打印的结果会是多少呢?1 1 吗?
class A
{
public:
A(int a)
:_a1(a)
,_a2(_a1)
{}
void Print() {
cout<<_a1<<" "<<_a2<<endl;
}
private:
int _a2;
int _a1;
};
int main() {
A aa(1);
aa.Print();
}
但结果却和我们想象的不一样,_a1
是1,_a2
却是一个随机值,这是为什么呢?
- 通过调试可以发现,似乎是先初始化的
_a2
再去初始化的_a1
,对于【内置类型】我们可以知道是编译器是不会去进行初始化的,那若是一开始使用_a1
去初始化_a2
的时候,那_a2
就会是一个随机值,但是_a1
却使用传入进来的形参a进行了初始化,那它的值就是1
- 此时我们只需要让
_a1
先进行初始化即可,就不会造成随机值的现象了
现在你在翻上去把所有的调试图一幅幅看下来就可以发现出初始化列表是存在顺序的,它的顺序不是在列表中谁先谁后的顺序,而是类的成员变量声明的顺序
五、总结与提炼
最后来总结一下本文所学习的内容📖
- 面对必须在声明时期初始化的成员函数,我们引入了初始化列表这个东西,知道了祖师爷在构造函数中还做了这么个小文章😄。有了它,我们就再也不用担心成员变量不会被初始化的问题了,无论是你是否给到缺省值,编译器都会去走一遍构造函数的初始化列表,若是没有在定义处给到初始值,就会采用缺省值;若是给到了初始值就会采用这个值
- 不仅如此,初始化列表还有很多的注意事项:
- 首先就是每个成员只能初始化一次,可以不要初始化多次哦
- 其次就是对于三类成员一定要在初始化列表进行初始化:包括
const
修饰的成员变量、引用类型成员、无默认构造函数的自定义成员变量 - 然后尽量使用初始化列表初始化,因为无论如何编译器一定会走初始化列表,声明时期的缺省值其实就是给到初始化列表使用的
- 接着我们在使用初始化列表的时候也要注意使用的场景,并不是所以的工作都可以交给它来完成的,也有一些其无法完成的事情
- 最后就是初始化列表中的初始化顺序,与定义处的顺序是无关的,和声明处的顺序有关
- 初始化列表是构造函数这一块的难点,也是祖师爷面对C++某些地方缺陷设计出来的,搞懂之后就会豁然开朗了
以上就是本文要介绍的所有内容,感谢您的阅读🌹
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)