在咱们刚开始学习 C 语言的时候,就被教诲过:“变量在应用前必须要初始化,很多 bug 都来自于未应用了未初始化的变量,因为某些编译器调配空间时,默认值并不为 0。”
变量的零初始化是一个很一般的需要,但正是这个看起来很常见的需要,其实有很多须要留神的问题。
对于繁多规范数据类型的零初始化,咱们在定义变量的时候间接在前面加一句 = 0 就好了。但构造呢?每个构造变量一一设置为 0?数组呢?循环设置为 0?
这些写法太啰嗦了,而且效率很低,咱们须要一种更为简洁的办法。
memset
这是 C 语言时代就有的函数,但直至今日,依然有大量的 C++ 程序员依然应用 memset 来进行变量的零初始化工作。
这是用最原始的办法循环零初始化一个数组,显得十分的蠢笨:
int a[10];
for (int i=0;i<10;I++)
a[i] = 0;
而与之绝对的,memset 只有一行就搞定了:
memset(a, 0, 10*sizeof(int));
甚至更进一步能够简化为:
memset(a, 0, sizeof(a));
编译器会帮咱们计算 a 到底是多大。
memset 能够不便的清空一个构造类型的变量或数组,而让你不须要关怀外面的细节,所以这种用法应用的十分遍及。
坑 1,对 const 数据执行 memset
void f1(char* a, size_t len)
{
// ...
memset(a, 0, len);
// ...
}
int main()
{
const char* a = "Hello World";
f1((char*)a, strlen(a));
return 0;
}
a 是一个 const char 类型的字符串指针,但 f1 函数须要的是一个 char 类型的指针,兴许 f1 函数是一个第三方库外面的函数,
你为了让程序编译通过,调用 f1 时对 a 做了强制类型转换,但其实在 f1 函数外部对 a 执行了 memset,这时候程序会就会解体。
坑 2,指定值初始化
如果咱们心愿数组 a 外面的初始化值不是 0 而是 1,上面的代码能够实现:
int main()
{char a[20];
memset(a, 1, sizeof(a));
return 0;
}
同理,你或者认为其它类型也能够这么做,譬如:
int main()
{int a[20];
memset(a, 1, sizeof(a));
return 0;
}
但你会惊奇的发现 a[0] = 0x01010101,a[1] = 0x01010101,…,这并不是你所心愿的后果。
起因很简略,memset 解决的对象是字节,也就是说只有在 char 或 unsigned char 的时候,指定值初始化才合乎你的预期。
坑 3,对带有虚函数的类或构造做 memset
咱们先看一个例子:
// 基类
class C0
{
public:
virtual void f0() = 0; // 这是一个纯虚函数};
// 派生类
class C1 : public C0
{
public:
C1()
{memset(this, 0, sizeof(C1));
}
virtual void f0()
{std::cout << "this f0" << std::endl;}
};
int main()
{
C1 c1;
C0* c0 = &c1; //
c0->f0(); // 这里会崩,因为 f0 指针曾经被清零了
return 0;
}
对于 C++ 来说,类的虚表是算在类自身占用的空间中的,C1 的构造函数中的 memset 会把虚表的指针 B_vfptr 也一并清空,那么在调用派生类的 f0 的时候就会解体。
坑 4,对带有 STL 元素的类或构造做 memset
struct ST
{
string str1;
string str2;
char str3[8];
};
// memset 会毁坏 string 的构造
int main()
{
ST s;
memset(&s, 0, sizeof(ST));
s.str1 = "hello";
s.str2 = "world";
printf("%s %s %x,%x,%x,%x,%x,%x,%x,%x\n", s.str1.c_str(), s.str2.c_str(), s.str3[0], s.str3[1], s.str3[2], s.str3[3], s.str3[4], s.str3[5], s.str3[6], s.str3[7]);
return 0;
}
这个程序在 Windows 用 VS2017 编译后的输入是:
?7 籑 7 籑 00,00,00,00,00,00,00,00
在 ubuntu 下用 g++ 7.5.0 编译后的输入是:
hello world 00,00,00,00,00,00,00,00
这个版本的后果看上去是正确的,但用 gcc 4.8.5 编译后果是会崩的
对带有 string, map, vector 等 STL 元素的类或构造应用 memset,会产生解体、内存透露 … 等一系列未知的问题,问题的严重性与编译器无关。
这个谬误是很多程序员最容易犯的谬误,而且不容易排查,所以要养成习惯:不对任何应用了 STL 容器的类或构造应用 memset 来做零初始化。
对 memset 的总结
那么咱们什么时候能够应用 memset 做零初始化呢?这里有一个规定,当初始化的对象是一个 POD 类型(Plain Old Data)时是能够用 memset 做零初始化的。
POD 用来表明 C ++ 中与 C 相兼容的数据类型,能够依照 C 的形式来解决(运算、拷贝等)。非 POD 数据类型与 C 不兼容,只能依照 C ++ 特有的形式进行初始化。
C 语言的规范数据类型:char、short、int、long、long long、float、double 这些都是 POD 类型,都能够用 memset 做零初始化。
应用 C 语言的规范数据类型定义的数组也是 POD 类型,也能够用 memset 做零初始化。
对于类和构造来说,很多货色会毁坏 POD 的约定,只应用了 C 语言的规范类型定义的类和构造还是 POD 的,前提是你没定义基类、没应用虚函数 …,
具体到底哪些 C++ 规定会毁坏 POD 约定请自行百度,但总的来说,不倡议应用 memset 对类或构造进行零初始化工作。
C++ 的定义初始化
如果你不是在函数外部须要对外部传递进来的数据指针做零初始化工作的化(这种状况下还是须要应用 memset),在 C++ 11 当前,在进行数据定义的同时做零初始化是更为优雅的一种形式。
int a[10] = {0};
这种定义的形式会让你间接取得一个曾经清零的数组 (留神并不是只有 a[0] 是 0,而是全副都已清零)。这种形式看起来比 memset 还简略、便捷,仿佛没有理由不去用它。
坑 1,指定值初始化
下面这种写法,很容易让人写下上面的代码:
int a[10] = {1};
你是不是感觉本人会失去全副元素已被初始化成 1 的数组?但答案不是这样的。
下面的写法你失去的是 a[0] = 1, a[1] = 0, a[2] = 0, … 也就是说除了 a[0] 以外,其它的数组元素被初始化为 0。
为什么是这样?因为有一条根本规定,数组初始化列表中的元素个数小于指定的数组长度时,有余的元素用默认值来初始化。
初始化列表外面只给了 1 个值,那么这个值被用来初始化 a[0],而 a[1] – a[9] 应用默认值 0 来做初始化。
坑 2,对带有 STL 元素的类或构造做零初始化
咱们去看看后面的例子,咱们用定义初始化的形式来初始化带有 STL 元素的构造会怎么样
struct ST
{
string str1;
string str2;
char str3[8];
};
int main()
{ST s = { 0};
s.str1 = "hello";
s.str2 = "world";
printf("%s %s %x,%x,%x,%x,%x,%x,%x,%x\n", s.str1.c_str(), s.str2.c_str(), s.str3[0], s.str3[1], s.str3[2], s.str3[3], s.str3[4], s.str3[5], s.str3[6], s.str3[7]);
return 0;
}
你是不是感觉这样没问题?因为你用的是 C++ 的办法来做的初始化。但答案是程序可能在 ST s = {0}; 处解体,即便不解体,也不会输入冀望的后果。
为什么会这样?认真了解一下坑 1 外面讲的根本规定,ST s = {0}; 初始化列表外面只有一个 0 (int 类型),而构造的第一个元素是 string,它没有通过 int 来进行结构的办法。
这时候编译器就会把这个 string 类型当作是一个构造来解决,执行: memset(&str1, 0, sizeof(string)),也就是说编译器又去调用了 memset 来对 str1 执行初始化,不崩才怪。
如何进行正确的零初始化?
咱们曾经探讨了很多谬误的零初始化形式,那么对于一个类或构造来说,什么才是正确的零初始化形式?
还是应用下面的例子:
咱们把 ST s = {0};
改为 ST s = {};
这下就 OK 了,因为你给了初始化列表,但初始化列表是空的,这时候编译器就会用 string 的默认值 “” 来初始化 str1, str2, 用 char 的默认值 0 来初始化数组,这正是咱们想要的。
这个办法能够延展到所有的类型定义上:
int a{}; // 这种写法跟 a = {} 是等价的
float b = {};
int c[10] = {};
动态分配的数据的初始化工作
对于动态分配的数据,你也能够采纳这种办法来做初始化工作。
int* a = new int[10] { };
string* b = new string[10] {};
咱们看看如果不做初始化会怎么样:
int* a = new int[10];
string* b = new string[10];
这个时候 b 外面的 string 还都是 “”,这是因为 string 的构造函数会默认初始化成 “”,但 int 可没有构造函数,所以 a 外面的内容就是随机的。
还有坑?
看到这的时候,是不是感觉曾经找到版本答案了?但生存中总是充斥了惊 (yi) 喜(wai)。
struct ST
{
string str1;
string str2;
char str3[8];
ST() {}
};
int main()
{ST s = {};
s.str1 = "hello";
s.str2 = "world";
printf("%s %s %x,%x,%x,%x,%x,%x,%x,%x\n", s.str1.c_str(), s.str2.c_str(), s.str3[0], s.str3[1], s.str3[2], s.str3[3], s.str3[4], s.str3[5], s.str3[6], s.str3[7]);
return 0;
}
咱们给 ST 增加了一个空的构造函数,当初失去的后果是:
hello world ffffffcc,ffffffcc,ffffffcc,ffffffcc,ffffffcc,ffffffcc,ffffffcc,ffffffcc
即便我写了 ST s = {};
str3 也没被初始化。也就是说,一个类或构造一旦你定义了本人的构造函数,编译器就不会再帮你去做初始化工作了。起因很简略,你本人的构造函数代替了编译器帮你默认生成的构造函数,而原来的初始化工作是这个默认的构造函数帮你实现的,一旦定义了本人的构造函数,那么初始化工作也就得本人来做了。