MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

探究 C++ 数据在内存中的存储方式

2021-03-235.7k 阅读

基本数据类型在内存中的存储

在 C++ 中,基本数据类型是构建复杂数据结构和程序的基石。了解它们在内存中的存储方式,对于编写高效、稳定的代码至关重要。

整型的存储

  1. 有符号整型
    • charchar 类型通常占用 1 个字节(8 位)。在有符号 char 中,最高位(第 7 位)用作符号位,0 表示正数,1 表示负数。例如,一个有符号 char 变量可以表示的范围是 -128 到 127。其存储方式遵循补码规则。以 -1 为例,其原码为 10000001,反码为 11111110,补码为 11111111,在内存中存储的就是补码 11111111。
    • shortshort 类型一般占用 2 个字节(16 位)。同样,最高位是符号位,能表示的范围是 -32768 到 32767。例如,short a = -1,在内存中的存储也是以补码形式,16 位的补码为 11111111 11111111。
    • intint 的大小取决于编译器和操作系统,通常在 32 位系统中占用 4 个字节(32 位),在 64 位系统中也可能占用 4 个字节(某些编译器会将其扩展为 64 位以提高性能)。以 32 位 int 为例,其表示范围是 -2147483648 到 2147483647。对于 int b = -1,32 位补码为 11111111 11111111 11111111 11111111。
    • longlong 类型在 32 位系统中占用 4 个字节,在 64 位系统中占用 8 个字节。它同样采用补码存储。
    • long longlong long 类型保证至少占用 8 个字节(64 位),能表示的范围更大。例如,long long c = -1,64 位补码为 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111。

以下是一个展示有符号整型存储的代码示例:

#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;
}
  1. 无符号整型
    • unsigned charunsigned char 同样占用 1 个字节,但没有符号位,所有 8 位都用于表示数值。因此,它能表示的范围是 0 到 255。例如,unsigned char uc = 255,在内存中存储为 11111111。
    • unsigned shortunsigned short 占用 2 个字节,能表示的范围是 0 到 65535。如 unsigned short us = 65535,内存存储为 11111111 11111111。
    • unsigned intunsigned int 在 32 位系统中占用 4 个字节,范围是 0 到 4294967295。对于 unsigned int ui = 4294967295,内存存储为 11111111 11111111 11111111 11111111。
    • unsigned longunsigned long 的大小与 long 相同,范围根据其字节数而定。在 64 位系统中,它能表示 0 到 18446744073709551615。
    • unsigned long longunsigned long long 占用 8 个字节,范围是 0 到 18446744073709551615。

代码示例展示无符号整型存储:

#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;
}

浮点型的存储

  1. floatfloat 类型通常占用 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。
  2. doubledouble 类型占用 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。

复合数据类型在内存中的存储

复合数据类型是由基本数据类型组合而成,它们在内存中的存储方式更为复杂,但也遵循一定的规律。

数组的存储

  1. 一维数组:一维数组是相同类型元素的线性集合。例如,int arr[5]; 定义了一个包含 5 个 int 类型元素的数组。在内存中,这些元素是连续存储的。假设 int 类型占用 4 个字节,数组 arr 的起始地址为 0x1000,那么 arr[0] 存储在 0x1000arr[1] 存储在 0x1004arr[2] 存储在 0x1008arr[3] 存储在 0x100Carr[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;
}
  1. 多维数组:以二维数组为例,int matrix[3][4]; 定义了一个 3 行 4 列的二维数组。在内存中,二维数组也是按行优先顺序连续存储的。假设 int 类型占用 4 个字节,matrix 的起始地址为 0x2000,那么 matrix[0][0] 存储在 0x2000matrix[0][1] 存储在 0x2004matrix[0][2] 存储在 0x2008matrix[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 的大小取决于其最大成员的大小。在这个例子中,intfloat 通常占用 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 的地址是 0x3000ptr = &num; 后,ptr 就存储了 0x3000。指针本身也占用一定的内存空间,在 32 位系统中,指针通常占用 4 个字节,在 64 位系统中,指针通常占用 8 个字节。 代码示例:

#include <iostream>

int main() {
    int num = 10;
    int *ptr = &num;
    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;objdatach 会按照内存对齐原则分配内存。假设 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 函数地址。

内存对齐与数据存储优化

内存对齐是影响数据存储效率的一个重要因素,了解并合理利用内存对齐原则,可以优化程序的性能。

内存对齐原则

  1. 结构体或类的成员变量按照其自身大小的倍数进行对齐。例如,int 类型的成员变量会从 4 的倍数地址开始存储。
  2. 结构体或类的整体大小是其最大成员大小的倍数。如果不满足,会在最后填充字节。
  3. 联合体的大小是其最大成员的大小。

内存对齐的影响

内存对齐可以提高内存访问效率。现代处理器通常以块(如 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++ 数据在内存中的存储方式,包括基本数据类型、复合数据类型、指针、引用、类和对象等,以及内存对齐原则,开发者可以编写更高效、更优化的代码,充分利用计算机的内存资源,提升程序的性能。