为什么用 memset 初始化 int 数组时值为 16843009?

这几天写代码的时候,我发现了一个问题。不管程序输入的是什么,Int 数组最终输出的数字都是一个不变的数字——16843009。查阅资料,发现是在用 memset 函数时出的问题,却突然一时想不明白。后来才发现,原来是很简单的事情。

然而还是水了一篇文

问题

这里附上测试的代码

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include <string.h>

using namespace std;

int main(){
int i[10];
memset(i, 1, sizeof(i));
for(int j = 0; j < 10; ++j)
cout << i[j] << "";
return 0;
}

输出是

1
16843009 16843009 16843009 16843009 16843009 16843009 16843009 16843009 16843009 16843009

原因

首先我们需要了解一下 memset。
memset 有三个参数,分别为:传入的数组(指针),要替换为的值,要修改的 字节数

memset 来自 string.h 头文件,一般来说,当我们用 memset 操作 char 数组时,非常方便。

1
2
char str[] = "Hello, world!";
memset(str, '-', 5);

此时数组 str 的值会变成

1
-----, world!

因为 memset 将此字符串的前五个字符每个设置为 '-'。

而当我们使用

1
2
int i[10];
memset(i, 0, sizeof(i));

之类的语句来初始化数组时也不会出问题,然而为什么把值换成 1 就不行了呢。

原因就在于 memset 是按字节修改数组的,就像上面加粗提示的那样。这里我们可以用一个示意图来理解。

Int 类型变量的占用空间是 4 个字节,一个字节(Byte)是 8 个比特(bit),即一个字节可以放 8 个 01(二进制)。

而将一个字节设置为 1,而一个字节有 8 个比特,这个字节的二进制数据就是0000 0001。一个字节一个字节设置后,一个 Int 的数据就会变成0000 0001 0000 0001 0000 0001 0000 0001,而这个转换为十进制就是 16843009。

这就是为什么我们会得到 16843009 的输出,以及为什么我们不应该用 memset 以非零的数来初始化 Int 等类型的数据。

补充:事实上 memset 也可以初始化每个数据为 -1

为什么呢,因为 -1 的二进制表达全是 1,若是在一个字节内存储 -1,它的二进制值就是1111 1111。当我们把 Int 的 4 个字节都设为 -1 时,就变成1111 1111 1111 1111 1111 1111 1111 1111,而这恰好还是 -1 的值。

(为什么是 1111 1111?参考资料:维基百科:补码

替代方案

不能用 memset,我们总得用一个方法来初始化数组的。
用什么呢,有人可能想到这样写:

1
int i[10] = {1};

然而这是行不通的,因为这样做只能让数组的第一个数变成 1。

一般来说我们能想到的最简单的方法是循环赋值,然而这样会增加代码长度,降低代码可读性。

这里介绍一个新函数:fill

fill 的头文件是<algorithm>

从 fill 的定义

1
2
template <class ForwardIterator, class T>
void fill (ForwardIterator first, ForwardIterator last, const T& val);

我们可以看出,参数分别是:第一个元素的迭代器,最后一个元素的迭代器,要赋的值。对于普通的数组,迭代器在这里可以理解为指向元素的指针。

那么文章最开头的代码可以改为:

1
2
int i[10];
fill(i, i+10, 1);

注意:这里第二个参数的指针要指向最后一个值的后面,因为这种函数要的范围是左闭右开,即 [first,second)。这里 i+10 即 i[10] 的开头(虽然事实上我们不能这样写)。

值的留意的是,fill 事实上等价于:

1
2
3
4
5
6
7
8
template <class ForwardIterator, class T>
void fill (ForwardIterator first, ForwardIterator last, const T& val)
{
while (first != last) {
*first = val;
++first;
}
}

也就是说这和我们循环赋值数组一样,时间复杂度都为 O(n)
(来源:http://www.cplusplus.com/reference/algorithm/fill/

类似功能的函数是 fill_n,具体来说 fill_n 为前 n 个元素每个都赋值。有兴趣的同学可以了解下,这里不再作介绍。
(参考资料:http://www.cplusplus.com/reference/algorithm/fill_n/