最近在抓代码质量这块,修改编译器告警规则入手,结合群里推荐的的clang-tidy来进行静态代码分析,真所谓不看不知道,一看吓一跳,光提示就一堆,挨个进行分析,发现了一个很有意思的告警**-Wmissing-field-initializers**。
虽然,网上有段子说程序员对warning这种一般不必care,不过,前段时间遇到一个问题,就是因为忽略了warning而导致,当然这是后话,后面可以使用一篇文章来细说。
好了,言归正传,今天聊聊遇到的这个告警~~
从一个示例说起
想必,我们会经常用到各种struct用来存储数据,然后将这些struct存储到容器中,后续进行使用,经常的用法,如下这种:
struct AdInfo {
int score;
std::string adid;
};
假如,我们现在构造一个对象,并且输出其中的内容,那么可以像如下这么操作:
#include <iostream>
#include <string>
struct AdInfo {
int32_t score;
std::string adid;
};
int main() {
AdInfo ad;
std::cout << "ad.score: " << ad.score << ", ad.adid: " << ad.adid << std::endl;
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.score << ", ad.adid: " << ad.adid << std::endl;
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.score << ", ad.adid: " << ad.adid << std::endl;
return 0;
}
也或者可以像下面这样写:
int main() {
AdInfo ad{};
std::cout << "ad.score: " << ad.score << ", ad.adid: " << ad.adid << std::endl;
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 <iostream>
#include <string>
struct AdInfo {
AdInfo() {}
int32_t score;
std::string adid;
};
int main() {
AdInfo ad{};
std::cout << "ad.score: " << ad.score << ", ad.adid: " << ad.adid << std::endl;
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;
int32_t score;
std::string adid;
};
AdInfo ad{};
当然了,这两种方式各有特点,需要依据自己的需求来选择。
可读&可维护
好了,截止到此,我们已经大致了解了什么是默认初始化,什么是值初始化,现在继续回到本文的重点,即用哪种初始化方式可以解决告警-Wmissing-field-initializers
。
如果在进行对象初始化的同时,对某个变量进行初始化赋值,可以像下面这么操作:
AdInfo ad{123};
但是编译报警如下:
warning: missing initializer for member ‘AdInfo::adid’ [-Wmissing-field-initializers]
如果想要解决此报警,可以像如下这样:
AdInfo ad{123, ""};
也就是说需要把所有的成员变量全部初始化一遍,且先后顺序与变量声明完全一致,如果不一致,要么编译失败(类型不一致),要么赋值非我们预期。
也可以像下面这样:
struct AdInfo {
int32_t score = 0;
std::string adid{};
};
AdInfo ad{123};
确实,使用如上两种初始化方式,告警消失了,但是可读性却依然很差,严格依赖变量声明顺序,稍不注意,emm...
为了满足既要
(告警消除)又要
(可维护),可以使用显式指定初始化成员的方式,即:
struct AdInfo {
int32_t score;
std::string adid{};
};
AdInfo ad{.scroe{123}};
结语
本文从一次编译警告开始,探讨了默认初始化和值初始化的异同。同时,分析了在对象初始化过程中同时进行变量赋值初始化的情况,以消除 -Wmissing-field-initializers 警告,并确保代码的可读性和可维护性,为后续开发过程中的初始化工作提供了一定帮助。
以上