重新认识 sizeof

关于因没完全理解 sizeof 而引发了血案后尝试完全理解 sizeof 这回事

事件的起因

由于业务上的需要,为了还原应用本身的 IO 复用行为,以 select 函数为例,其函数原型为:

1
2
int select(int nfds, fd_set *restrict readfds, fd_set *restrict writefds,
           fd_set *restrict errorfds, struct timeval *restrict timeout);

其参数 timeout 指 select 函数本身的超时时间,从逻辑上讲,在 timeout 不为 0 的情况下,可以把单次 select 调用拆分为多次调用,例如 timeout 为 5s 可以拆分为 5 次 timeout 为 1s 的 select;若返回值为 0,那么每次重新调用 select 函数前都需要将参数 fds 重置为第一次调用前的值,如何重置呢?调用 FD_COPY 函数,它封装了 bcopy 函数,其函数原型为:

1
void bcopy(const void *src, void *dest, size_t n);

那么对于 FD_COPY 函数,参数 n 是如何得到的呢?通过源码可知是由 sizeof(*src) 得到的,通常情况下这并不会有什么问题,然而当 src 是由 malloc 动态分配时,灾难就发生了。

sizeof 是什么?

sizeof 并不是一个函数,而是一个 一元操作符(unary operator),用它可以得到一个表达式或 数据类型 的存储大小,例如在 64 位机器上:

1
2
3
4
5
sizeof(char)   // 1 Bytes
sizeof(int)    // 4 Bytes
sizeof(long)   // 8 Bytes
sizeof(double) // 8 Bytes
sizeof(char *) // 8 Bytes

那么对于 bcopy 中使用 sizeof(*src) 得到的大小是多少呢?有人会说应该是指针指向的那块内存地址的大小,实际上 sizeof 只关心数据的类型,并不关心数据本身在内存中的具体存储大小,以下是实际的测试结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 测试环境:
// Ubuntu 22.04, gcc-11
#include <stdio.h>

int main(void) {
  char *a = "a";
  char *b = "bb";
  char *str1 = "abcd";
  char str2[4] = "abcd";
  printf("%lu %lu %lu %lu\n", sizeof(*a), sizeof(*b), sizeof(*str1),
         sizeof(*str2));

  return 0;
}

// 输出为:
// 1 1 1 1

可以看到变量 a, b, str1, str2 的类型大小都为 1 字节,显然其类型全为 char,sizeof 得到的值只与其类型有关。

开头我们提到重复调用 select 函数需要重置 fds,而 fds 是一个结构体,那么对结构体求 sizeof 的值是什么呢?

结构体的类型大小

我们先定义一个简单的结构体进行测试:

1
2
3
4
struct int_set {
  int set[32];
};
// sizeof(struct int_set) 为 128 Bytes

int_set 结构体中只有一个容量为 32 的数组成员变量,因此其类型大小为 32 * sizeof(int) 即 128 Bytes,那么成员变量稍多的结构体如何呢?

1
2
3
4
5
6
struct student {
  int num;
  char name;
  int sex;
};
// sizeof(struct student) 为 12 Bytes

结构体 student 有 3 个成员变量,分别为 2 个 int 类型和 1 个 char 类型,2 * sizeof(int) + sizeof(char) 应当为 9 Bytes 才对为什么输出为 12 Bytes 呢?这涉及到编译过程中的内存对齐,至于为什么要对齐我们先不深入讨论,一般来讲,有硬件兼容与性能提升这两个原因。

结构体内存对齐遵循以下 2 条规则:

  1. 成员变量的偏移量为其自身类型大小的整数倍。
  2. 结构体的总大小为最大成员变量类型大小的整数倍。

由于 int 类型比 char 类型大 3 Bytes,因此需要在 char 类型后补 3 Bytes 对齐到 4 Bytes,符合上述内存对齐规则中的第 2 条,那么我们在成员变量 name 后添加一个类型为 short 的成员变量 age,此时的内存对齐情况如何呢?

1
2
3
4
5
6
7
struct student {
  int num;
  char name;
  short age;
  int sex;
};
// sizeof(struct student) 仍为 12 Byte

short 类型为 2 Bytes,根据内存对齐的第 1 条规则,此时应当在 name 后补 1 Byte,而后紧跟 age,示意图如下:

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|            num            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| name |  1B  |     age     |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|            sex            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

好了扯远了,在实际应用过程中我们通常都是以 struct fd_set readfds; 的形式定义一个结构体变量,但已 fd_set 结构体为例,在 fd 可控的情况下我们其实不需要用满结构体成员变量的所有容量,转而使用 malloc 对内存分配做更精细化的控制,此时若我们仍用原来的方式(例如 FD_COPY)操作这样的变量就容易出现难以定位的问题。

用 malloc 定义结构体

直接进行测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
typedef struct int_set {
  int set[10];
} int_set;

int main(void) {
  int_set ints1;
  for (int i = 0; i < 10; ++i) {
    // init
    ints1.set[i] = i;
  }

  int_set *ints2 = (int_set *)malloc(sizeof(int_set));
  int_set *ints3 = (int_set *)malloc(6 * sizeof(int));
  memcpy(ints2, &ints1, 6 * sizeof(int));
  memcpy(ints3, &ints1, 6 * sizeof(int));

  for (int i = 0; i < 10; ++i) {
    printf("ints1[%d] = %d, ints2[%d] = %d, ints3[%d] = %d\n", i, ints1.set[i],
           i, ints2->set[i], i, ints3->set[i]);
  }

  return 0;
}

输出结果为:

ints1[0] = 0, ints2[0] = 0, ints3[0] = 0
ints1[1] = 1, ints2[1] = 1, ints3[1] = 1
ints1[2] = 2, ints2[2] = 2, ints3[2] = 2
ints1[3] = 3, ints2[3] = 3, ints3[3] = 3
ints1[4] = 4, ints2[4] = 4, ints3[4] = 4
ints1[5] = 5, ints2[5] = 5, ints3[5] = 5
ints1[6] = 6, ints2[6] = 0, ints3[6] = 1041
ints1[7] = 7, ints2[7] = 0, ints3[7] = 0
ints1[8] = 8, ints2[8] = 0, ints3[8] = 1937010281
ints1[9] = 9, ints2[9] = 0, ints3[9] = 1563974449 

可以看到当前环境下,使用 malloc 动态分配内存的大小小于结构体类型的大小时,就不能用 sizeof 去操作一个变量了,很「遗憾」的是 memcpy 参数大于其变量本身大小时在编译过程中编译器是不会报错的,然而后续的内存空间已经被不幸覆盖,最终在运行过程中酿成「血案」。

小结

  1. sizeof运算符 而不是函数
  2. 结构体需要内存对齐
  3. 谨慎使用 sizeof 操作 malloc 动态分配的变量

参考

  1. sizeof - Wikipedia
  2. Struct memory layout in C - Stack Overflow
  3. Mac OS X Manual Page For FD_COPY(2) - Apple Developer
updatedupdated2024-10-302024-10-30