C++细说sizeof

摘要

sizeof 的作用非常简单:求对象或者类型的大小。然而 sizeof 又非常复杂,它涉及到很多特殊情况,总结出了 sizeof 的 9 个特性:(1)sizeof 不能求得 void 类型的长度;(2)sizeof 能求得 void 类型的指针的长度;(3)sizeof 能求得静态分配内存的数组的长度!(4)sizeof 不能求得动态分配的内存的大小!(5)sizeof 不能对不完整的数组求长度;(6)当表达式作为 sizeof 的操作数时,它返回表达式的计算结果的类型大小,但是它不对表达式求值!(7)sizeof 可以对函数调用求大小,并且求得的大小等于返回类型的大小,但是不执行函数体!(8)sizeof 求得的结构体及其对象的大小并不等于各个数据成员对象的大小之和!(9)sizeof 不能用于求结构体的位域成员的大小,但是可以求得包含位域成员的结构体的大小!

概述

sizeof 是 C/C++中的关键字,它是一个运算符,其作用是取得一个对象,数据类型或者数据对象的长度, 即占用内存的大小,以 byte 为单位。其中类型包含基本数据类型不包括 void、用户自定义类型结构体、类、函数类型。数据对象是指用前面提到的类型定义的普通变量和指针变量包含 void 指针。不同类型的数据的大小在不同的平台下有所区别,但是 c 标准规定所有编译平台都应该保证 sizeof(char) 等于 1。关于 sizeof 的更多概述你可以在 msdn 总输入 sizeof 进行查询。看了上面这些,或许你看了没有多少感觉。没关系,下面我将详细列出 sizeof 的诸多特性,这些特性是造成 sizeof 是一个较刁钻的关键字的原因:

十大特性

特性 0:sizeof 是运算符,不是函数

这个特性是 sizeof 的最基本特性,后面的很多特性都是受到这个特性的影响,正因为 sizeof 不是函数,因此我们不把它所要求得长度的对象叫做参数,我本人习惯上叫做操作数, 这不严谨,但是有助于我记住 sizeof 是个操作符。

特性 1:sizeof 不能求得 void 类型的长度

是的,你不能用 sizeof(void),这将导致编译错误:illegal sizeof operand。事实上你根本就无法声明 void 类型的变量,不信你就试试 void a;这样的语句,编译器同样会报错:illegal use of type 'void'。或许你要问为什么,很好,学东西不能只知其然,还要知其所以然。我们知道声明变量的一个重要作用就是告诉编译器该变量需要多少存储空间。然而,void 是”空类型”,什么是空类型呢,你可以理解成不知道存储空间大小的类型。既然编译器无法确定 void 类型的变量的存储大小,那么它自然不让你声明这样的变量。当然了,声明 void 类型的指针是可以的!这就是特性 2 的内容。

特性 2:sizeof 能求得 void 类型的指针的长度

在特性 1 中说过,可以申明 void 类型的指针,也就是说编译器可以确定 void 类型的指针所占用的存储空间。事实上确实如此,目前,几乎所有平台上的所有版本的编译器都把指针的大小看做 8byte,不信你试试 sizeof(int*);sizeof(void*);sizeof(double*);sizeof(Person*) 等等,它们都等于 8!

特性 3:sizeof 能求得静态分配内存的数组的长度

int a[10];int n = sizeof(a);假设 sizeof(int) 等于 4,则 n= 10*4=40;特别要注意:char ch[]="abc";sizeof(ch); 结果为 4,注意字符串数组末尾有’\0’!通常我们可以利用 sizeof 来计算数组中包含的元素个数,其做法是:int n = sizeof(a)/sizeof(a[0]);非常需要注意的是对函数的形参数组使用 sizeof 的情况。举例来说,假设有如下的函数:

void fun(int array[10])
{
     int n = sizeof(array);
}
```cpp
你会觉得在 fun 内,n 的值为多少呢?如果你回答 40 的话,那么我很遗憾的告诉你,你又错了。这里 n 等于 8,事实上,不管形参是 int 的型数组,还是 float 型数组,或者其他任何用户自定义类型的数组,也不管数组包含多少个元素,这里的 n 都是 8!为什么呢?原因是在函数参数传递时,数组被转化成指针了,或许你要问为什么要转化成指针,原因可以在很多书上找到,我简单说一下:假如直接传递整个数组的话,那么必然涉及到数组元素的拷贝,当数组非常大时,这会导致函数执行效率极低!而只传递数组的地址即指针那么只需要拷贝 8byte。
### 特性 4:`sizeof` 不能求得动态分配的内存的大小
假如有如下语句:`int *a = new int[10];int n = sizeof(a);` 那么 n 的值是多少呢?是 40 吗?答案是否定的!其实 n 等于 8,因为 a 是指针,在特性 2 中讲过:在 64 位平台下,所有指针的大小都是 8byte!切记,这里的 a 与特性 3 中的 a 并不一样!很多人都认为数组名就是指针,其实不然,二者有很多区别的,要知详情,请看《c 专家编程》。通过特性 3 和特性 4,我们看到了数组和指针有着千丝万缕的关系,这些关系也是导致程序潜在错误的一大因素,关于指针与数组的关系问题我将在《C/C++刁钻问题各个击破之指针与数组的秘密》一文中进行详细介绍。
### 特性 4 说明 `sizeof` 不能求的动态分配的内存的大小
于是有人认为 `sizeof` 是编译时进行求值的,并给出理由:语句 `int array[sizeof(int)*10];` 能编译通过,而很多书上都说过数组大小是编译时就确定下来的,既然前面的语句能编译通过,所以认为 `sizeof` 是编译时进行求值的。经过进一步测试我发现这个结论有些武断!至少是有些不严谨!因为在实现了 c99 标准的编译器可以定义动态数组。那么到底 `sizeof` 是编译时求值还是运行时求值呢?最开初 c 标准规定 `sizeof` 只能编译时求值,后来 c99 又补充规定 `sizeof` 可以运行时求值。但值得注意的是,即便是在实现了 c99 标准的 DEV C++中仍然不能用 `sizeof` 求得动态分配的内存的大小!
### 特性 5:`sizeof` 不能对不完整的数组求长度
在阐述该特性之前,我们假设有两个源文件:file1.cpp 和 file2.cpp,其中 file1.cpp 中有如下的定义:
```text
int arrayA[10] = {1,2,3,4,5,6,7,8,9,10};
int arrayB[10] = {11,12,13,14,15,16,17,18,19,20};
```cpp
file2.cpp 包含如下几个语句:
```text
extern arrayA[];
extern arrayB[10];
cout << sizeof(arrayA) << endl;            //编译出错!!
cout << sizeof(arrayB) << endl;
```cpp
在 file2.cpp 中第三条语句编译出错,而第四条语句正确,并且能输出 40!为什么呢?原因就是 `sizeof(arrayA)` 试图求不完整数组的大小。这里的不完整的数组是指数组大小没有确定的数组!`sizeof` 运算符的功能就是求某种对象的大小,然而声明:`extern int arrayA[]` 只是告诉编译器 `arrayA` 是一个整型数组,但是并没告诉编译器它包含多少个元素,因此对 file2.cpp 中的 `sizeof` 来说它无法求出 `arrayA` 的大小,所以编译器干脆不让你通过编译。那为什么 `sizeof(arrayB)` 又可以得到 `arrayB` 的大小呢?关键就在于在 file2.cpp 中其声明时使用 `extern int arrayB[10]` 明确地告诉编译器 `arrayB` 是一个包含 10 个元素的整型数组,因此大小是确定的。
### 特性 6:当表达式作为 `sizeof` 的操作数时,它返回表达式的计算结果的类型大小,但是它不对表达式求值
为了说明这个问题,我们来看如下的程序语句:
```text
char ch = 1;
int num = 1;
int n1 = sizeof(ch + num);
int n2 = sizeof(ch = ch + num);
```cpp
假设 `char` 占用 1byte,`int` 占用 4byte,那么执行上面的程序之后,n1,n2,ch 的值是多少呢?我相信有不少人会认为 n1 与 n2 相等,也有不少人认为 ch 等于 2,事实这些人都错了。事实上 n1 等于 4,n2 等于 1,ch 等于 1,为什么呢?请看分析:由于默认类型转换的原因,表达式 `ch + num` 的计算结果的类型是 `int`,因此 n1 的值为 4!而表达式 `ch = ch + num`;的结果的类型是 `char`,记住虽然在计算 `ch + num` 时,结果为 `int`,但是当把结果赋值给 ch 时又进行了类型转换,因此表达式的最终类型还是 `char`,所以 n2 等于 1。n1,n2 的值分别为 4 和 1,其原因正是因为 `sizeof` 返回的是表达式计算结果的类型大小,而不是表达式中占用最大内存的变量的类型大小!对于 `n2 = sizeof(ch = ch + num)`;乍一看该程序貌似实现了让 ch 加上 num 并赋值给 ch 的功能,事实并非如此!由于 `sizeof` 只关心类型大小,所以它自然不应该对表达式求值,否则有画蛇添足之嫌了。正是因为这点,这里告诫各位,尽量不要在 `sizeof` 中直接对表达式求大小,以免出现错误,你可以将 `sizeof(ch = ch + num)`;改写成 `ch = ch + num;sizeof(ch);` 虽然多了一条语句,看似冗余了,其实好处多多:首先更加清晰明了,其次不会出现 ch 等于 1 这样的错误假设程序的逻辑本身就是要执行 `ch = ch +num`;。
### 特性 7:`sizeof` 可以对函数调用求大小,并且求得的大小等于返回类型的大小,但是不执行函数体
```text
int fun(int& num, const int& inc)
{
         float div = 2.0;
         double ret = 0;
         num = num + inc;
         ret = num / div;
         return ret;
}
```cpp
那么语句:
```text
int a = 3;
int b = 5;
cout << sizeof(fun(a,b)) << endl;
cout << a<< endl;
```cpp
输出多少呢?不同的人会给出不同的答案,我将对 `sizeof(fun(a, b))` 的值和 a 的值分别进行讨论:首先 `sizeof(fun(a, b))` 的值:其正确是 4,因为用 `sizeof` 求函数调用的大小时,它得到的是函数返回类型的大小,而 `fun(a, b)` 的返回类型是 `int`,`sizeof(int)` 等于 4。很多人把函数的返回类型和返回值的类型弄混淆了,认为 `sizeof(fun(a, b))` 的值是 8,因为函数返回值是 `ret`,而 `ret` 被定义成 `double`,`sizeof(double)` 等于 8。注意,虽然函数返回值类型是 `double`,但是在函数返回时,将该值进行了类型转换。也有人错误的认为 `sizeof(fun(a, b))` 的值是 12,它们的理由是:`fun` 内部定义了两个局部变量,一个是 `float` 一个是 `double`,而 `sizeof(float) + sizeof(double)= 4+8 =12`。这样的答案看似很合理,其实他们是错误地认为这里的 `sizeof` 是在求函数内部的变量的大小了。这当然是错误的。接下来看 a 的值:其正确答案是 3!还记得特性 6 吗?这里很类似,`sizeof` 的操作对象是函数调用时,它不执行函数体!为此,建议大家不要把函数体放在 `sizeof` 后面的括号里,这样容易让人误以为函数执行了,其实它根本没执行。既然对函数调用使用 `sizeof` 得到的是函数返回类型的大小,那么很自然能得出这样的结论:不能对返回类型为 `void` 的函数使用 `sizeof` 求其大小!原因请参考特性 1。同理,对返回类型是任何类型的指针的函数调用使用 `sizeof` 求得的大小都为 4,原因请参考特性 2。最后我们来看看这样的语句:`cout<< sizeof(fun)`;其答案是多少呢?其实它得不到答案,原因是编译就通不过!最开始,我以为能输出答案 4,因为我认为 fun 是函数名,而我知道函数名就是函数的地址,地址就是指针,于是我认为 `sizeof(fun)` 其实就是对一个指针求大小,根据特性 2,任何指针的大小都是 4。可是当我去验证时,编译器根本不让我通过!这个是为什么呢?我一时半会想不到,所以还请朋友们补充!
### 特性 8:`sizeof` 求得的结构体的大小并不等于各个数据成员对象的大小之和
结构体的大小跟结构体成员对齐有密切关系,而并非简单地等于各个成员的大小之和!比如对如下结构体两个结构体 A、B 使用 `sizeof` 的结果分别是:16,24。可以看出 `sizeof(B)` 并不等于 `sizeof(int) + sizeof(double) + sizeof(int)=16`。
```text
struct A {
    int num1;
    int num2;
    double  num3;
};

struct B {
    int num1;
    double num3;
    int num2;
};
```cpp
如果您不了解结构体的成员对齐,你会感到非常惊讶:结构体 A 和 B 中包含的成员都一样,只不过顺序不同而已,为什么其大小不一样呢?要解释这个问题,就要了解结构体成员对齐的规则,由于结构体成员对齐非常复杂,我将用专题——C/C++刁钻问题各个击破之位域和成员对齐——进行讲解,这里我只简单地介绍其规则:结构体的大小等于结构体内最大成员大小的整数倍结构体内的成员的首地址相对于结构体首地址的偏移量是其类型大小的整数倍,比如说 `double` 型成员相对于结构体的首地址的地址偏移量应该是 8 的倍数。为了满足规则 1 和 2 编译器会在结构体成员之后进行字节填充!基于上面三个规则我们来看看为什么 `sizeof(B)` 等于 24:首先假设结构体的首地址为 0,第一个成员 num1 的首地址是 0(满足规则 2,前面无须字节填充,事实上结构体绝对不会在第一个数据成员前面进行字节填充),它的类型是 `int`,因此它占用地址空间 0——3。第二个成员 num3 是 `double` 类型,它占用 8 个字节,由于之前的 num1 只占用了 4 个字节,为了满足规则 2,需要使用规则 3 在 num1 后面填充 4 个字节,使得 num3 的起始地址偏移量为 8,因此 num3 占用的地址空间是:8——15。第三个成员 num2 是 int 型,其大小为 4,由于 num1 和 num3 一共占用了 16 个字节,此时无须任何填充就能满足规则 2。因此 num2 占用的地址空间是 16——19。那么是不是结构体的总大小就是 0——19 共 20 个字节呢?请注意,别忘了规则 1!由于结构体内最大成员是 `double` 占用 8 个字节,因此最后还需要在 num2 后面填充 4 个字节,使得结构体总体大小为 24。按照上面的三个规则和分析过程,你可以很容易地知道为什么 `sizeof(A)` 等于 16。特别需要说明的是,我这里给出了三个结论性的规则,而没有阐述为什么要这样。你或许有很多疑问:为什么要结构体成员对齐,为什么要定义规则 1 等。如果你有这样的疑问,并尝试去弄清楚的话,那么我敢断言,不久的将来你必定会有大成就,至少在学习 c++上是这样。最后再提醒一点,在进行设计时,最好仔细安排结构体中各个成员的顺序,因为你已经看到了上面的结构体 B 与结构体 A 包含的成员相同,只不过顺序略有差异,最终就导致了 B 比 A 多消耗了 50%的空间,假如在工程中需要定义该结构体的数组,多消耗的空间将是巨大的。
### 特性 9:`sizeof` 不能用于求结构体的位域成员的大小,但是可以求得包含位域成员的结构体的大小
首先解释一下什么是位域:类型的大小都是以字节为基本单位的,我们知道某个类型的大小确定了该类型所能定义的变量的范围,char 类型的变量范围是-128——127,或者 0——255(unsigned char),总之它只能定义 28=256 个数!然而,要命的是 bool 类型只取值 true 和 false,按理所只用 1bit 就够了,但事实上等于 1。因此我们可以认为 bool 变量浪费了 87.5%的存储空间!这在某些存储空间有限的设备上是不合适的,为此需要提供一种能对变量的存储空间精打细算的机制,这就是位域。简单来说,在结构体的成员变量后面跟上的一个冒号+一个整数,就代表位域,请看如下的结构体:
```text
struct A
{
    bool b:1;
    char ch1:4;
    char ch2:4;
}item;
```cpp
其中 b,ch1,ch2 都是位域成员,而 i 是普通成员。该结构体的试图让 bool 类型的变量 b 只占用 1 个 bit,让 ch1 和 ch2 分别只占用 4 个 bit,以此来达到对内存精打细算的功能事实上使用位域对内存精打细算有时候能成功,有时候却未必,我将《C/C++刁钻问题各个击破之位域和成员对齐》进行论述。另外需要特别注意的是:c 语言规定位域只能用于 `int,signed int,unsigned int` 类型,C++又补充了 `char` 和 `long` 类型!你不能这样使用位域:`float f:8`;这是不能通过编译的。并且位域变量不能在函数或者全局区定义,只能在结构体,自定义类,联合中使用!基于上面的结构体,语句 `sizeof(item.b) sizeof(item.ch1)` 等对位域成员求大小的语句均不能通过编译。其原因能在本篇的概论中找到:`sizeof` 以 `byte` 为单位返回操作数的大小!那么爱学好问的你可能要问,`sizeof(A)` 能否通过编译呢?如何能,其结果又是多少呢?这是两个非常好的问题,我正是在看到 `sizeof(item.b)` 不能通过编译时想到了这两个问题,然后通过验证得出了后面的结论:对包含位域的结构体是可以使用 `sizeof` 求其大小的,但其求值规则比较复杂,不仅涉及到成员对齐,还与具体编译环境有关!在这里你只需要知道可以对包含位域的结构体使用 `sizeof` 求其大小,对于 `sizeof` 是根据什么规则来求这个大小的问题,

   转载规则


《C++细说sizeof》 吴杭沉 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
C++面试题 C++面试题
1.char c = '\72'; 中的 \72 代表一个字符,72 是八进制数,代表 ASCII 码字符 :。2.10*a++ 中 a 先进行乘法运算再自增。3.const 和 static 的作用 static 关键字:
2018-08-01
下一篇 
C++中new与malloc区别 C++中new与malloc区别
申请的内存所在位置new 操作符从自由存储区上为对象动态分配内存空间,而 malloc 函数从堆上动态分配内存。自由存储区是 C++ 基于 new 操作符的一个抽象概念,凡是通过 new 操作符进行内存申请,该内存即为自由存储区。而堆是操作
2018-07-26
  目录