0. sizeof运算符
是的,没有看错,这个像函数一样的sizeof
,是运算符(参考【深度C++】之“表达式与运算符”)。
sizeof运算符返回一条表达式或一个类型名字所占的字节数,它满足右结合律,它的结果是一个size_t类型的常量表达式。
分析一下上面的定义:
sizeof
是一个运算符sizeof
的输入需要一条表达式或类型名字- 返回的结果是输入的字节数,类型是
size_t
- 满足右结合律
它的使用如定义所述,有两种形式
sizeof(type) // sizeof(expr)也可以,可以理解为下面的用法添加括号
sizeof expr // 只能添加表达式,不可以使用类型名
概括来说,sizeof的使用,一共有以下7种情况:
- sizeof与基本内置类型
- sizeof与类或结构体(空类、数据对齐、静态成员、虚函数、继承)
- sizeof与联合体
- sizeof与枚举
- sizeof与数组
- sizeof与引用
- sizeof与指针
注:不同平台的结果是有差异的,以下代码及结果均实验于Intel Core i7,64位, 操作系统MacOS10.15.5,编译器Apple clang version 11.0.3 (clang-1103.0.32.29)。
1. sizeof与基本内置类型
见示例:
int main() {
cout << "bool: " << sizeof(bool) << endl; // 1
cout << "short: " << sizeof(short) << endl; // 2
cout << "int: " << sizeof(int) << endl; // 4
cout << "long: " << sizeof(long) << endl; // 8
cout << "long long: " << sizeof(long long) << endl; // 8
cout << "char: " << sizeof(char) << endl; // 1
cout << "wchar_t: " << sizeof(wchar_t) << endl; // 4
cout << "char16_t: " << sizeof(char16_t) << endl; // 2
cout << "char32_t: " << sizeof(char32_t) << endl; // 4
cout << "float: " << sizeof(float) << endl; // 4
cout << "double: " << sizeof(double) << endl; // 8
cout << "long double: " << sizeof(long double) << endl; // 16
return 0;
}
编者认为不必背上面的值,各个编译器不太一样,知道用法即可;当然一些常用的情况简单记下就好。
2. sizeof与自定义数据类型
2.1 结构体
结构体与类一样,请看下节2.2。
2.2 类
结构体和类都是C++用来实现抽象数据类型的方式,sizeof运算符作用在它们身上效果一样。
值得一提的是,这一节的内容,只要理解了C++类内存布局,就不难算出sizeof
的结果;但是内存布局倾向于微观的内存情况,这里倾向于了解sizeof的用法与结果。关于内存布局的相关内容,参考【深度C++】之“类内存布局”。
2.2.1 基本用法
class NullClass {
};
class MyClass {
int i;
char c;
double d;
};
int main() {
MyClass my_class;
cout << sizeof(NullClass) << endl; // 1
cout << sizeof(MyClass) << endl; // 16
// 单独使用变量名是一条表达式,表达式的结果是变量本身,16
cout << sizeof(my_class) << endl;
return 0;
}
说明:
- sizeof一个普通的类,只需累加各个数据成员的大小并考虑内存对齐,不计算函数成员。(这里的普通的类指的是不含2.2.3 - 2.2.5情况的类)
- sizeof空类得到1;
这里sizeof(MyClass)
输出16而不是13是因为编译器自动进行了内存对齐。
2.2.1.1 为什么空类是1
至于为什么空类得到1,考虑即使我们定义了一个空类的情况下,我们也可以将该类实例化一个对象,并且还可以使用指针或引用指向或引用它。
假若一个空类A真的为0个字节,它在地址854616
位置;那么下一个对象在实例化的时候,他的地址就可以覆盖这个空类A的854616
位置,因为类A的大小为0。所以在使用指针或引用访问A的时候,就出现了问题。
因此必须给空类一个大小;至于这个大小是多少,C++规定为1字节。
2.2.2 内存对齐
现代计算机在处理数据时,按照某个“单位”来处理。32位机器,每次处理32位、4字节的二进制数据,64位同理。
内存对齐指的是计算机系统对基本数据类型合法地址做出了一些限制,要求某种类型对象的地址必须是某个值的倍数。
读者只需知道sizeof
作用于结构体或类的时候,得出的结果并不是简单的将所有非静态数据成员的字节数累加在一起即可,详细内容,请参考【深度C++】之“内存对齐”。
2.2.3 静态成员
参考【深度C++】之“static”中的第4条用法,我们得知静态成员属于类本身,不属于每个实例化出来的对象,因此使用sizeof运算符作用于类时,不考虑静态成员的大小。
从C++内存布局上也可得知,static成员变量存储在静态全局区,每次实例化一个类时并不包含其内容,因此使用sizeof也不会计算它的大小。
class MyClass {
static int i;
char c;
public:
MyClass() = default;
};
int main() {
cout << sizeof(MyClass) << endl; // 1
return 0;
}
2.2.4 虚函数
当成员函数包含虚函数的时候,sizeof
这个类或结构体,通常会增加一个指针长度的大小;32位机器增加4,64位机器增加8。
这是因为C++编译器在实现虚函数的时候,使用了虚表,编译器隐式的在具有虚函数的类或结构体中,添加了一个指向虚表的指针(vfptr),该指针总是在类内存的起始位置。
在计算数据对齐的第一步对齐单元k时,要把指向虚表的指针的大小也考虑进去。
// 64位机器,#pragma pack参数n默认8
class MyClass {
char c;
public:
MyClass() = default;
virtual void test();
};
int main() {
cout << sizeof(MyClass) << endl; // 16
// 比较机器字节8、pack参数8、指向虚表的指针8,得到k = 8
// 8(vtblptr) + 1(char) + 7(浪费) = 16
return 0;
}
2.2.5 继承
关于继承情况下的sizeof
,要深刻理解类内存布局相关内容,请参考【深度C++】之“类内存布局”。
有继承和虚函数情况下的类,在计算对齐参数k时,要考虑虚表指针和从父类到子类的所有的成员变量。接下来将他们按规定衔接。然后考虑多继承情况下每个间接基类的多个虚表指针以及子类复用主要基类的虚表指针,虚继承情况下子类父类分开的虚表指针和只有一个公共父类,最终得出sizeof
的值。
2.3 联合体
联合体(union) 是一种特殊的类,一个union
可以有多个数据成员,但是任意时刻只有一个数据成员可以有值。
当我们给union
的某个成员赋值之后,该union
的其他成员就变成未定义的状态了。
分配给union
对象的存储空间至少要能容纳它的最大的数据成员。
因此sizeof
一个联合体的结果,也是union
中数据成员最大的那个。
union U {
char c;
int i;
double d;
};
int main() {
cout << sizeof(U) << endl; // 8
return 0;
}
2.4 枚举
枚举类型是我们可以将一组整型常量组织在一起,定义一种新的类型,类型的取值只能是枚举类型中出现的整型常量。
实际上enum
是由某种整数类型表示的,默认是int
。在C++11新标准中,我们可以在enum
的名字后加上冒号以及我们想在该enum
中使用的类型:
enum EIntVal: unsigned long long {
char_typ = 255,
short_typ = 65535,
int_typ = 65535,
long_typ = 4294967295UL,
long_long_typ = 18446744073709551615ULL
};
int main() {
EIntVal echar = char_typ;
cout << sizeof(echar) << endl;
cout << sizeof(EIntVal) << endl;
cout << sizeof(EIntVal::char_typ) << endl;
cout << sizeof(EIntVal::short_typ) << endl;
cout << sizeof(EIntVal::int_typ) << endl;
cout << sizeof(EIntVal::long_typ) << endl;
cout << sizeof(EIntVal::long_long_typ) << endl;
// 上述输出全是8
return 0;
}
3. sizeof与复合类型
3.1 数组
sizeof运算符作用于数组,将得到整个数组所占空间的大小;等价于对数组中所有元素各执行一次sizeof运算符并将所得结果求和。
sizeof不会把数组转换成指针来处理。
int ary[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
cout << "array: " << sizeof(ary) << endl; // 40
注意上述的写法,ary
是一个标识符,直接书写ary是一个表达式,并不是数组的类型,因此也可以使用sizeof ary
的方法。
那么怎么用sizeof(type)
,输入数组的类型,得到数组的大小呢?
首先,我们需要知道数组的类型是什么。参考【深度C++】之“数组”我们得知,数组是一种复合类型。既然是复合类型,和简单的int ary
相比,只多了一个[]
,不妨试试如下代码:
cout << "array: " << sizeof(int [10]) << endl; // 40
编译通过。
既然执行sizeof能得到数组的整体大小,因此可以用数组的大小除以单个元素的大小得到数组的长度:
int ary[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
cout << "array's length: " << sizeof(ary) / sizeof(*ary); // 10
注意上述代码,若将ary
传递给一个函数后:
void sizeof_ary(int ary[]) {
cout << "array: " << sizeof(ary) << endl;
// 此时输出指针的大小
}
此时输出的是指针的大小。
3.2 引用
引用就是被引用对象的别名,因此对引用使用sizeof
得到被引用对象所占空间的大小。
int ary[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 这是一个引用,引用的是包含10个int类型的数组
int (&rary)[10] = ary;
cout << sizeof(rary) << endl; // 40
3.3 指针
指针是一个对象,与引用不同,它有自己的存储空间。因此sizeof
指针得到的就是指针的大小。
这个值在32位电脑上为4byte,64位电脑上为8byte。
int a = 10;
int *pa = &a;
cout << sizeof(pa) << endl; // 输出8
4. 总结
sizeof运算符作用于表达式或一个类型的名字,得到它们所占的字节数。
基本内置类型需要考虑不同的编译器。
自定义数据类型,sizeof结构体和类最复杂,要考虑内存对齐,不包含静态成员,还有虚函数添加的虚表指针,各种继承包裹父类的情况。空类的结果是1。sizeof联合体得到的结果是联合体中最大的类型。sizeof枚举默认是int,程序员也可以自己定义枚举中整型的类型。
复合类型里,sizeof数组会得到整个数组的大小。sizeof指针会得到指针变量得大小。sizeof引用会得到引用源对象的大小。