最近在抓代码质量这块,修改编译器告警规则入手,结合群里推荐的的
clang-tidy
来进行静态代码分析,真所谓
不看不知道,一看吓一跳
,光提示就一堆,挨个进行分析,发现了一个很有意思的告警**-Wmissing-field-initializers**。
虽然,网上有段子说程序员对warning这种一般不必care,不过,前段时间遇到一个问题,就是因为忽略了warning而导致,当然这是后话,后面可以使用一篇文章来细说。
好了,言归正传,今天聊聊遇到的这个告警~~
从一个示例说起
想必,我们会经常用到各种struct用来存储数据,然后将这些struct存储到容器中,后续进行使用,经常的用法,如下这种:
struct AdInfo {
int score;
std::string adid;
};
假如,我们现在构造一个对象,并且输出其中的内容,那么可以像如下这么操作:
#include
#include
struct AdInfo {
int32_t score;
std::string adid;
};
int main() {
AdInfo ad;
std::cout <"ad.score: " <", ad.adid: " < return 0;
}
在进行下面的内容之前,我们先思考下,上述代码会输出什么内容,想象很多人会毫不犹豫的给出答案:
ad.score: 0, ad.adid:
其实,这个答案可对可不对,当然是有前提的,即:
特定的编译器在Debug环境下,会将整形值初始化为0
在我的本地环境gcc11.2 debug下确实输出为0和空值
如果是release下,那么输出又是什么呢?
在我本地试了下,对于score的值每次都不一样(即随机值):
ad.score: 4199200, ad.adid:
ad.score: 1600677166, ad.adid:
这是因为,对于结构体或者类里面定义的成员变量,如果没有显示声明默认构造函数或者在声明的默认构造函数中对基础类型的值没有进行初始化,则在运行的时候,使用当前内存(栈或者堆)上的垃圾数据。
对于上述这种情况,最方便的莫过于使用值初始化,即:
int main() {
AdInfo ad();
std::cout <"ad.score: " <", ad.adid: " < return 0;
}
emm,编译失败,提示如下:
warning: empty parentheses were disambiguated as a function declaration [-Wvexing-parse]
从上述编译提示可以看出,对于这种形如
AdInfo ad()
编译器不确定是一个函数声明(即函数名为ad,返回类型为AdInfo)或者是一个默认初始化,所以干脆报错完事~~
为了解决上述错误,可以修改代码如下:
int main() {
AdInfo ad = AdInfo();
std::cout <"ad.score: " <", ad.adid: " < return 0;
}
也或者可以像下面这样写:
int main() {
AdInfo ad{};
std::cout <"ad.score: " <", ad.adid: " < return 0;
}
编译运行后,输出如下:
ad.score: 0, ad.adid:
初始化
根据
cppreference
,初始化可以分为以下几类:
•
aggregate initialization
•
constant initialization
•
copy initialization
•
default initialization
•
initializer list
•
list initialization
•
reference initialization
•
value initialization
•
zero initialization
本文旨在分析使用的
default initialization(即默认初始化)
和
value initialization(值初始化)
。
默认初始化
默认初始化是C++中的一种很常见的初始化方式,它根据对象的类型规定了初始化的方式,但并不为对象提供显式的初始值。
默认初始化发生在变量或对象声明时,如果没有提供任何初始值或者采用特定的初始化形式,编译器将执行默认初始化。其行为取决于变量或对象的类型和存储位置:
• 内置类型
•
对于非静态局部变量(在函数内部声明),若不显式初始化,它们不会被初始化,其值是未定义的(undefined)。这意味着这些变量可能包含垃圾值,使用它们可能导致不可预测的行为。
•
对于静态局部变量和全局变量(包括文件作用域的静态变量),若不显式初始化,它们会被初始化为该类型的零值(即零初始化,见下文)。例如,整型变量为
0
,浮点型为
0.0
,指针为
NULL
或
nullptr
。
• 类类型
•
如果类具有默认构造函数(无论用户定义还是编译器生成),默认初始化会调用该构造函数进行初始化。
•
如果类没有默认构造函数(即所有构造函数都需要参数),则不能进行默认初始化。
下面是常见的例子:
int x; // 0
double y; // 0.0
int* ptr; // nullptr
struct Point {
int x;
int y;
};
Point p; // p.x 和 p.y 的值是未定义的
class MyClass {
public:
MyClass() {
// 构造函数
}
};
MyClass obj; //调用 MyClass 的默认构造函数
好了,现在继续回到文章一开始的那个例子,对于形如**AdInfo ad;**这种,会自动调用构造函数,如果没有显式指定,则编译器会帮忙生成一个,但是对其成员变量不做特殊初始化,即仅支持默认初始化,这就是为什么这种方式下,score输出是个垃圾值的原因(adid输出为固定空值,是因为string的默认构造函数导致)。
值初始化
值初始化是一种主动请求初始化为某种特定值的方式,通常通过使用空花括号**{}**或等价的构造函数调用来实现。其行为如下:
• 内置类型
•
值初始化将变量初始化为其类型的零值,如
int
为
0
,
float
为
0.0f
,
bool
为
false
,指针为
NULL
或
nullptr
。
• 类类型
•
若类具有默认构造函数(用户定义或编译器生成),值初始化会调用该构造函数。
•
若类没有默认构造函数,值初始化会导致编译错误。
• 数组
•
数组的所有元素都将进行值初始化。
常见例子如下:
int i{}; // 值初始化为0
double d{}; // 值初始化为0.0
bool b{}; // 值初始化为false
继续回到我们一开始的例子,对于形如**AdInfo adinfo()
这种方式,编译器会报错,于是使用了
AdInfo ad = AdInfo()
这种值初始化的方式,当然了,也可以采用
AdInfo ad{}**来进行初始化,也就是说使用
= AdInfo();或者ad{}
这种值初始化的方式,可以避免我们前面遇到的问题。
好了,基于以上两个概念,继续回到正题。
目前来看,值初始化是我们所需要的,也避免了一些意想不到的问题(比如前面的score的值为一个随机值或者非预期值)。
那么,对于类来说,是不是提供了构造函数就能达到值初始化的目的呢?且看下面代码:
#include
#include
struct AdInfo {
AdInfo() {}
int32_t score;
std::string adid;
};
int main() {
AdInfo ad{};
std::cout <"ad.score: " <", ad.adid: " < return 0;
}
emm,如果输出的话,如下面这种:
ad.score: 4199200, ad.adid:
跟前面的没啥变化,显然,其并不是我们所想要的值初始化,而是执行的
默认初始化
操作,这是因为在进行ad构造的时候,调用了我们提供的构造函数而不是编译器生成的构造函数(如果我们提供了构造函数,则编译器就不会帮忙辅助生成)。
除非在上面的构造函数中对成员变量进行显式初始化,即下面这种:
struct AdInfo {
AdInfo() : score(0) {}
int32_t score;
std::string adid;
};
上面这种在
初始化列表
中进行初始化,当然了也可以在构造函数内部进行初始化,但不一定所有情况都适宜在构造函数内部进行操作,以及加上性能等情况,一般
优先使用初始化列表
进行初始化。
可能有人会提出,如果我的类中有很多,难道每个变量都要进行如此初始化么?
自C++11起有以下两种方式:
可以形如
struct AdInfo {
int32_t score = 0;
std::string adid;
};
AdInfo ad{};
也可以形如:
struct AdInfo {
AdInfo() = defalut;