探究 C++ 数据在内存中的存储方式
基本数据类型在内存中的存储
在 C++ 中,基本数据类型是构建复杂数据结构和程序的基石。了解它们在内存中的存储方式,对于编写高效、稳定的代码至关重要。
整型的存储
- 有符号整型
- char:
char
类型通常占用 1 个字节(8 位)。在有符号char
中,最高位(第 7 位)用作符号位,0 表示正数,1 表示负数。例如,一个有符号char
变量可以表示的范围是 -128 到 127。其存储方式遵循补码规则。以 -1 为例,其原码为 10000001,反码为 11111110,补码为 11111111,在内存中存储的就是补码 11111111。 - short:
short
类型一般占用 2 个字节(16 位)。同样,最高位是符号位,能表示的范围是 -32768 到 32767。例如,short a = -1
,在内存中的存储也是以补码形式,16 位的补码为 11111111 11111111。 - int:
int
的大小取决于编译器和操作系统,通常在 32 位系统中占用 4 个字节(32 位),在 64 位系统中也可能占用 4 个字节(某些编译器会将其扩展为 64 位以提高性能)。以 32 位int
为例,其表示范围是 -2147483648 到 2147483647。对于int b = -1
,32 位补码为 11111111 11111111 11111111 11111111。 - long:
long
类型在 32 位系统中占用 4 个字节,在 64 位系统中占用 8 个字节。它同样采用补码存储。 - long long:
long long
类型保证至少占用 8 个字节(64 位),能表示的范围更大。例如,long long c = -1
,64 位补码为 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111。
- char:
以下是一个展示有符号整型存储的代码示例:
#include <iostream>
#include <limits>
void printBinary(int num, int bits) {
for (int i = bits - 1; i >= 0; --i) {
std::cout << ((num >> i) & 1);
}
std::cout << std::endl;
}
int main() {
char char_num = -1;
short short_num = -1;
int int_num = -1;
long long_num = -1;
long long long_long_num = -1;
std::cout << "char (-1) binary: ";
printBinary(static_cast<unsigned char>(char_num), 8);
std::cout << "short (-1) binary: ";
printBinary(static_cast<unsigned short>(short_num), 16);
std::cout << "int (-1) binary: ";
printBinary(static_cast<unsigned int>(int_num), 32);
std::cout << "long (-1) binary: ";
printBinary(static_cast<unsigned long>(long_num), sizeof(long) * 8);
std::cout << "long long (-1) binary: ";
printBinary(static_cast<unsigned long long>(long_long_num), sizeof(long long) * 8);
return 0;
}
- 无符号整型
- unsigned char:
unsigned char
同样占用 1 个字节,但没有符号位,所有 8 位都用于表示数值。因此,它能表示的范围是 0 到 255。例如,unsigned char uc = 255
,在内存中存储为 11111111。 - unsigned short:
unsigned short
占用 2 个字节,能表示的范围是 0 到 65535。如unsigned short us = 65535
,内存存储为 11111111 11111111。 - unsigned int:
unsigned int
在 32 位系统中占用 4 个字节,范围是 0 到 4294967295。对于unsigned int ui = 4294967295
,内存存储为 11111111 11111111 11111111 11111111。 - unsigned long:
unsigned long
的大小与long
相同,范围根据其字节数而定。在 64 位系统中,它能表示 0 到 18446744073709551615。 - unsigned long long:
unsigned long long
占用 8 个字节,范围是 0 到 18446744073709551615。
- unsigned char:
代码示例展示无符号整型存储:
#include <iostream>
#include <limits>
void printBinary(unsigned num, int bits) {
for (int i = bits - 1; i >= 0; --i) {
std::cout << ((num >> i) & 1);
}
std::cout << std::endl;
}
int main() {
unsigned char uchar_num = 255;
unsigned short ushort_num = 65535;
unsigned int uint_num = 4294967295;
unsigned long ulong_num = 18446744073709551615UL;
unsigned long long ullong_num = 18446744073709551615ULL;
std::cout << "unsigned char (255) binary: ";
printBinary(uchar_num, 8);
std::cout << "unsigned short (65535) binary: ";
printBinary(ushort_num, 16);
std::cout << "unsigned int (4294967295) binary: ";
printBinary(uint_num, 32);
std::cout << "unsigned long (18446744073709551615) binary: ";
printBinary(ulong_num, sizeof(unsigned long) * 8);
std::cout << "unsigned long long (18446744073709551615) binary: ";
printBinary(ullong_num, sizeof(unsigned long long) * 8);
return 0;
}
浮点型的存储
- float:
float
类型通常占用 4 个字节(32 位),遵循 IEEE 754 标准。这 32 位被分为三个部分:1 位符号位(S)、8 位指数位(E)和 23 位尾数位(M)。符号位 0 表示正数,1 表示负数。指数位存储的是实际指数加上一个偏移量(127),尾数位表示小数部分。例如,对于数字 1.5,其二进制表示为 1.1。规范化后为 1.1×2^0,符号位 S = 0,指数 E = 0 + 127 = 127(二进制为 01111111),尾数 M = 10000000000000000000000,所以在内存中的存储为 0 01111111 10000000000000000000000。 - double:
double
类型占用 8 个字节(64 位),同样遵循 IEEE 754 标准。其中 1 位符号位,11 位指数位,52 位尾数位。指数的偏移量是 1023。由于double
有更多的位用于表示指数和尾数,它能表示的范围更大,精度更高。例如,对于一个较大的浮点数,其存储会根据规则在这 64 位中分配。
代码示例展示浮点数存储:
#include <iostream>
#include <limits>
void printBinary(unsigned long long num, int bits) {
for (int i = bits - 1; i >= 0; --i) {
std::cout << ((num >> i) & 1);
}
std::cout << std::endl;
}
int main() {
float f_num = 1.5f;
double d_num = 1.5;
unsigned long long f_bits = *reinterpret_cast<unsigned long long*>(&f_num);
unsigned long long d_bits = *reinterpret_cast<unsigned long long*>(&d_num);
std::cout << "float (1.5) binary: ";
printBinary(f_bits, 32);
std::cout << "double (1.5) binary: ";
printBinary(d_bits, 64);
return 0;
}
字符型的存储
char
类型主要用于存储单个字符,它占用 1 个字节。在内存中,字符是以其对应的 ASCII 码值存储的。例如,字符 'A' 的 ASCII 码值是 65,在内存中存储为 01000001。当使用 char
进行数学运算时,实际操作的是其 ASCII 码值。例如,char c = 'A'; c = c + 1;
,此时 c
的值变为 'B',因为 65 + 1 = 66,'B' 的 ASCII 码值是 66。
复合数据类型在内存中的存储
复合数据类型是由基本数据类型组合而成,它们在内存中的存储方式更为复杂,但也遵循一定的规律。
数组的存储
- 一维数组:一维数组是相同类型元素的线性集合。例如,
int arr[5];
定义了一个包含 5 个int
类型元素的数组。在内存中,这些元素是连续存储的。假设int
类型占用 4 个字节,数组arr
的起始地址为0x1000
,那么arr[0]
存储在0x1000
,arr[1]
存储在0x1004
,arr[2]
存储在0x1008
,arr[3]
存储在0x100C
,arr[4]
存储在0x1010
。 代码示例:
#include <iostream>
int main() {
int arr[5] = {1, 2, 3, 4, 5};
for (int i = 0; i < 5; ++i) {
std::cout << "arr[" << i << "] address: " << &arr[i] << " value: " << arr[i] << std::endl;
}
return 0;
}
- 多维数组:以二维数组为例,
int matrix[3][4];
定义了一个 3 行 4 列的二维数组。在内存中,二维数组也是按行优先顺序连续存储的。假设int
类型占用 4 个字节,matrix
的起始地址为0x2000
,那么matrix[0][0]
存储在0x2000
,matrix[0][1]
存储在0x2004
,matrix[0][2]
存储在0x2008
,matrix[0][3]
存储在0x200C
,接着matrix[1][0]
存储在0x2010
,以此类推。 代码示例:
#include <iostream>
int main() {
int matrix[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 4; ++j) {
std::cout << "matrix[" << i << "][" << j << "] address: " << &matrix[i][j] << " value: " << matrix[i][j] << std::endl;
}
}
return 0;
}
结构体的存储
结构体是用户自定义的数据类型,它可以包含不同类型的成员。结构体的存储遵循内存对齐原则。例如:
struct MyStruct {
char c;
int i;
short s;
};
假设 char
占用 1 个字节,int
占用 4 个字节,short
占用 2 个字节。如果不进行内存对齐,MyStruct
理论上应该占用 1 + 4 + 2 = 7 个字节。但为了提高内存访问效率,编译器会进行内存对齐。通常,结构体的成员会按照其自身大小的倍数进行对齐。在这个例子中,c
占用 1 个字节,接着为了对齐 int
,会在 c
后面填充 3 个字节,i
占用 4 个字节,s
占用 2 个字节,最后为了使整个结构体的大小是最大成员大小(这里是 int
的 4 个字节)的倍数,会再填充 2 个字节。所以 MyStruct
实际占用 12 个字节。
代码示例展示结构体大小和内存对齐:
#include <iostream>
struct MyStruct {
char c;
int i;
short s;
};
int main() {
std::cout << "Size of MyStruct: " << sizeof(MyStruct) << " bytes" << std::endl;
return 0;
}
联合体的存储
联合体也是用户自定义的数据类型,它允许不同类型的成员共享同一块内存空间。例如:
union MyUnion {
int i;
float f;
char c[4];
};
MyUnion
的大小取决于其最大成员的大小。在这个例子中,int
和 float
通常占用 4 个字节,char[4]
也占用 4 个字节,所以 MyUnion
的大小是 4 个字节。当给联合体的一个成员赋值时,会覆盖其他成员的值。例如:
#include <iostream>
union MyUnion {
int i;
float f;
char c[4];
};
int main() {
MyUnion u;
u.i = 10;
std::cout << "u.i: " << u.i << std::endl;
std::cout << "u.f: " << u.f << std::endl;
std::cout << "u.c: ";
for (int i = 0; i < 4; ++i) {
std::cout << static_cast<int>(u.c[i]) << " ";
}
std::cout << std::endl;
u.f = 3.14f;
std::cout << "u.i: " << u.i << std::endl;
std::cout << "u.f: " << u.f << std::endl;
std::cout << "u.c: ";
for (int i = 0; i < 4; ++i) {
std::cout << static_cast<int>(u.c[i]) << " ";
}
std::cout << std::endl;
return 0;
}
指针和引用在内存中的存储
指针和引用是 C++ 中用于间接访问数据的重要工具,它们在内存中的存储方式也有独特之处。
指针的存储
指针是一个变量,它存储的是另一个变量的地址。例如,int *ptr;
定义了一个指向 int
类型的指针。假设 int
变量 num
的地址是 0x3000
,ptr = #
后,ptr
就存储了 0x3000
。指针本身也占用一定的内存空间,在 32 位系统中,指针通常占用 4 个字节,在 64 位系统中,指针通常占用 8 个字节。
代码示例:
#include <iostream>
int main() {
int num = 10;
int *ptr = #
std::cout << "num address: " << &num << std::endl;
std::cout << "ptr value (num's address): " << ptr << std::endl;
std::cout << "ptr address: " << &ptr << std::endl;
std::cout << "Size of ptr: " << sizeof(ptr) << " bytes" << std::endl;
return 0;
}
引用的存储
引用是变量的别名,它在内存中并没有独立的存储空间。例如,int &ref = num;
这里 ref
就是 num
的别名,它们共享同一块内存空间。当对 ref
进行操作时,实际上就是对 num
进行操作。
代码示例:
#include <iostream>
int main() {
int num = 10;
int &ref = num;
std::cout << "num address: " << &num << std::endl;
std::cout << "ref address: " << &ref << std::endl;
ref = 20;
std::cout << "num value: " << num << std::endl;
return 0;
}
类和对象在内存中的存储
类是 C++ 面向对象编程的核心,对象是类的实例。了解类和对象在内存中的存储方式,对于理解面向对象编程的底层机制非常重要。
类的非静态成员存储
当定义一个类时,其非静态成员变量在对象创建时会分配内存空间。例如:
class MyClass {
public:
int data;
char ch;
};
当创建 MyClass
的对象时,如 MyClass obj;
,obj
的 data
和 ch
会按照内存对齐原则分配内存。假设 int
占用 4 个字节,char
占用 1 个字节,obj
总共会占用 8 个字节(data
4 个字节,ch
1 个字节,为了内存对齐填充 3 个字节)。
类的静态成员存储
静态成员变量属于类,而不是某个具体的对象。它们在程序的全局数据区分配内存,只有一份实例,被所有对象共享。例如:
class MyClass {
public:
static int staticData;
};
int MyClass::staticData = 0;
staticData
在程序启动时就分配内存,无论创建多少个 MyClass
的对象,都共享这一份 staticData
。
成员函数的存储
成员函数并不存储在每个对象中,而是所有对象共享一份代码。编译器会为类的成员函数生成独立的代码段,通过对象的地址来访问成员函数。例如,对于 MyClass
类的成员函数 void printData() { std::cout << data << std::endl; }
,所有 MyClass
对象都共享这一份 printData
函数的代码。
虚函数和虚表的存储
当类中包含虚函数时,会引入虚表机制。每个包含虚函数的类都会有一个虚表,虚表是一个函数指针数组,存储了类中虚函数的地址。每个对象会有一个隐藏的虚表指针(vptr),指向类的虚表。例如:
class Base {
public:
virtual void virtualFunction() { std::cout << "Base virtual function" << std::endl; }
};
class Derived : public Base {
public:
void virtualFunction() override { std::cout << "Derived virtual function" << std::endl; }
};
Base
类有一个虚表,vptr
指向这个虚表,虚表中存储了 virtualFunction
的地址。Derived
类继承自 Base
,也有自己的虚表,虚表中 virtualFunction
的地址被重写为 Derived
类的 virtualFunction
函数地址。
内存对齐与数据存储优化
内存对齐是影响数据存储效率的一个重要因素,了解并合理利用内存对齐原则,可以优化程序的性能。
内存对齐原则
- 结构体或类的成员变量按照其自身大小的倍数进行对齐。例如,
int
类型的成员变量会从 4 的倍数地址开始存储。 - 结构体或类的整体大小是其最大成员大小的倍数。如果不满足,会在最后填充字节。
- 联合体的大小是其最大成员的大小。
内存对齐的影响
内存对齐可以提高内存访问效率。现代处理器通常以块(如 4 字节、8 字节)的方式访问内存。如果数据存储地址符合对齐要求,处理器可以一次读取一个完整的块,而不需要进行额外的处理。否则,可能需要多次读取和合并数据,降低效率。
手动控制内存对齐
在 C++ 中,可以使用 #pragma pack
指令来手动控制内存对齐。例如,#pragma pack(1)
可以取消内存对齐,让结构体成员紧密排列,以节省内存空间,但可能会降低访问效率。#pragma pack()
则恢复默认的对齐方式。
#include <iostream>
#pragma pack(1)
struct NoAlignStruct {
char c;
int i;
short s;
};
#pragma pack()
struct AlignStruct {
char c;
int i;
short s;
};
int main() {
std::cout << "Size of NoAlignStruct: " << sizeof(NoAlignStruct) << " bytes" << std::endl;
std::cout << "Size of AlignStruct: " << sizeof(AlignStruct) << " bytes" << std::endl;
return 0;
}
通过深入了解 C++ 数据在内存中的存储方式,包括基本数据类型、复合数据类型、指针、引用、类和对象等,以及内存对齐原则,开发者可以编写更高效、更优化的代码,充分利用计算机的内存资源,提升程序的性能。