(嵌入式八股)第2章 C++(五)
2.41 为什么要字节对齐?
字节对齐(Memory Alignment)是指数据在内存中的存放方式,以确保数据类型的起始地址是其大小的倍数。字节对齐的主要目的是优化存储器访问速度,并减少存储器的浪费。以下是字节对齐的主要原因:
优化存储器访问速度
硬件架构通常要求数据按特定的边界进行存储。例如,32位系统通常要求32位(4字节)数据类型存储在4字节的地址上。如果数据没有对齐,处理器就需要执行额外的操作来访问不对齐的数据,这会导致性能下降。
- 例子:假设在没有字节对齐的情况下,一个4字节的数据存储在地址为 0x1001 的位置,这就需要处理器进行两次内存访问操作:一次访问低地址部分,一次访问高地址部分,才能将数据正确读取。而如果数据按照对齐要求存储(例如,存储在 0x1000 或 0x1004 处),处理器只需一次内存访问就能直接读取整个数据,显著提高存取速度。
减少存储器浪费
通过字节对齐,计算机能够更有效地使用内存。如果数据结构没有对齐,可能会导致内存中留下“空洞”或“填充”空间,从而浪费内存。
在没有字节对齐的情况下,MyStruct 的大小可能是 5 字节,因为 a 占 1 字节,b 占 4 字节,但是 a 和 b 之间的内存间隙可能是 3 字节,从而浪费了 3 字节。
但是,如果进行字节对齐,int b 会被放置在 4 字节对齐的位置,可能会在 a 后面填充 3 字节的空间来确保 b 对齐。这可能会导致结构体的大小为 8 字节,但这样可以提高内存访问的效率,避免访问不对齐的内存。
硬件架构的要求
现代处理器的设计通常要求按照特定的对齐方式访问内存。某些处理器会对不对齐的内存访问产生错误或异常,甚至无法正确执行。因此,为了兼容硬件架构,进行字节对齐是必需的。
提高并行处理效率
许多现代处理器采用了SIMD(单指令多数据)指令集或多核处理架构,要求数据在内存中的布局是对齐的,以便进行高效的并行计算。如果数据不对齐,处理器在执行并行计算时可能需要进行额外的计算或内存访问,导致性能下降。
2.42 字节对齐的具体实现
字节对齐的规则通常由编译器或编程语言的标准定义,目的是为了确保数据按照硬件的要求进行存储,进而提高程序的执行效率。具体的字节对齐实现主要遵循以下几个原则:
数据类型的大小
- 对齐要求: 在大多数系统中,数据类型的对齐要求通常是其大小的倍数。例如:int 类型在大多数系统中占用 4 字节,因此通常要求按 4 字节对齐。char 类型占 1 字节,它的对齐要求也是 1 字节。double 类型通常占 8 字节,因此需要按 8 字节对齐。
在这种情况下,char
类型的 a
可能会占用 1 字节,后面会有 3 字节的填充,使得 int
类型的 b
从 4 字节对齐的位置开始存储。
结构体和类的成员变量
结构体和类的成员变量在内存中的存放位置通常根据以下规则进行对齐:
- 成员变量的对齐要求: 每个成员变量的对齐要求通常由其数据类型的大小决定。如果成员变量的数据类型占用 N 字节,那么它通常要求按 N 字节对齐。
- 结构体整体对齐要求: 结构体或类的整体对齐要求通常是其最大成员对齐要求的倍数。也就是说,结构体或类的起始地址必须是最大对齐要求(例如,最大成员对齐要求是 8 字节,那么结构体的起始地址必须是 8 的倍数)。
char a
占 1 字节,接下来有 3 字节的填充,确保 int b
按 4 字节对齐。int b
占 4 字节,接下来可能会有 4 字节的填充,确保 double c
按 8 字节对齐。编译器控制字节对齐
编译器通常会提供一些控制字节对齐的选项或指令,允许开发者精细化控制数据的内存布局。这些选项和指令通常有以下几种形式:
- 对齐指令: 编译器提供的关键字或者编译器指令,可以显式地控制数据对齐方式。
例如,在 GCC 中可以使用 __attribute__((aligned(N))) 来设置数据对齐:
- 编译器选项: 许多编译器允许通过编译选项来控制对齐方式。例如,在 GCC 中可以使用
-fpack-struct
来禁用结构体内存对齐,从而节省内存空间:
- 默认对齐: 默认情况下,编译器通常会按照平台的要求对数据进行对齐,以便在常见的硬件架构上获得最佳性能。
2.43 struct 和 class 的区别
虽然在 C++ 中,struct
和 class
在功能上几乎相同,都可以包含成员变量、成员函数、构造函数、析构函数等,但它们之间的主要区别通常体现在访问控制和继承的默认方式上。以下是 struct
和 class
的几个关键区别:
默认访问权限
struct
: 默认的成员访问权限是 公共 (public
),这意味着结构体的成员可以直接在外部访问。class
: 默认的成员访问权限是 私有 (private
),这意味着类的成员默认不可直接访问,除非通过公共的成员函数或友元函数。
默认继承方式
struct
: 默认的继承方式是 公共继承 (public
),这意味着从struct
派生的类默认会继承基类的公共成员。class
: 默认的继承方式是 私有继承 (private
),这意味着从class
派生的类默认会将基类的公共成员变为私有成员。
使用习惯
struct
: 一般用于简单的数据结构,当不需要对成员进行访问控制时,使用struct
更为合适。class
: 更常用于实现复杂的数据类型,尤其是在实现面向对象编程时,使用class
来封装数据和提供方法。
成员变量和成员函数的默认访问权限
struct
:struct
中的成员变量和成员函数默认是 公共 (public
) 的,可以在外部直接访问。class
:class
中的成员变量和成员函数默认是 私有 (private
) 的,必须通过公共的成员函数来访问。
成员访问控制
struct
: 结构体的成员可以直接访问,不需要通过任何方法封装或访问控制。class
: 类的成员通常是私有的,需要通过公共成员函数(即 getter 和 setter)来访问或修改这些成员。
默认的构造函数和析构函数
struct
:struct
中没有自动生成默认构造函数和析构函数,除非显式声明。class
:class
中会自动生成默认的构造函数和析构函数。如果没有显式声明,编译器会生成一个默认构造函数和析构函数。
总结
2.44 static静态成员变量
静态成员变量是通过关键字 static
声明的,它是类的一部分,但与类的实例无关,多个对象共享同一个静态成员变量。下面是关于静态成员变量的详细说明:
静态成员变量的共享性
- 共享数据: 静态成员变量属于类,而不是类的实例,因此所有该类的对象都共享同一个静态成员变量。这意味着,无论创建多少个类的实例,它们都引用同一块内存位置。
在上面的例子中,m_total
是静态成员变量,s1
和 s2
是两个 Student
对象,它们共享同一份 m_total
。
静态成员变量的内存分配
- 内存位置: 静态成员变量与普通成员变量不同,它们并不随对象的创建和销毁而分配和释放内存。相反,静态成员变量在程序的全局数据区分配内存,并在程序结束时才释放内存。即使没有创建任何对象,静态成员变量也会在程序启动时分配内存。
- 普通成员变量:在对象创建时分配内存,在对象销毁时释放内存。
- 静态成员变量:在程序开始时分配内存,并在程序结束时释放内存,所有对象共享同一份内存。
静态成员变量的初始化
- 初始化要求: 静态成员变量必须在类外部进行初始化。虽然可以在类内部声明静态成员变量,但它必须在类外部单独初始化。
如果不显式初始化静态成员变量,编译器会将其默认初始化为 0(对于静态变量在全局数据区)。
静态成员变量的访问方式
- 访问方式: 静态成员变量可以通过类名或对象名进行访问。无论使用哪个方式,访问的始终是同一份内存。
- 通过类名访问: 推荐使用类名来访问静态成员变量,这样可以清晰地表明这是类的成员。
- 通过对象名访问: 尽管可以通过对象名访问静态成员变量,但这会给人一种误导,因为静态成员变量不属于对象,而是类的所有对象共享的。
总结
- 共享性: 静态成员变量是类的所有对象共享的,所有对象访问同一份内存。
- 内存分配: 静态成员变量在全局数据区分配内存,而不是在对象的栈上,且它的生命周期从程序开始到程序结束。
- 初始化: 静态成员变量必须在类外进行初始化。
- 访问方式: 静态成员变量既可以通过类名也可以通过对象名访问,但推荐通过类名来访问。
2.45 静态成员函数与普通成员函数的区别?
静态成员函数和普通成员函数之间有许多区别,主要包括调用方式、访问权限、存储方式、this
指针等方面。
调用方式
- 静态成员函数: 可以通过类名直接调用,无需创建类的对象。
- 普通成员函数: 必须通过类的对象或指针来调用。
MyClass::staticFunc()
调用了静态成员函数,不需要创建对象。obj.normalFunc()
通过对象调用普通成员函数。访问权限
- 静态成员函数: 只能直接访问静态成员变量和静态成员函数,不能访问非静态成员变量和非静态成员函数。
- 普通成员函数: 可以访问类的所有成员,包括静态成员和非静态成员。
staticFunc()
无法访问 num
(非静态成员变量),只能访问静态成员变量 staticNum
。normalFunc()
可以访问静态成员和非静态成员。存储方式
- 静态成员函数: 存储在类的命名空间中,所有对象共享同一个静态成员函数,不影响类的对象大小。
- 普通成员函数: 存储在类的对象中,每个对象都有自己的成员函数。
- 所有对象共享同一个静态成员函数
staticFunc()
。 - 每个对象有自己的普通成员函数
normalFunc()
,但每次调用时会共享相同的代码。
this
指针
- 静态成员函数: 没有
this
指针,不能访问对象的非静态成员。
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
作者简介:仅用大半年时间0基础天坑急转嵌入式开发,逆袭成功拿下华为、vivo、小米等15个offer,面试经验60+,收藏20+面经,分享自己的求职历程与学习心得。 专栏内容:最新求职与学习经验,详细讲解了嵌入式开发的学习路径、项目经验分享、简历优化技巧、面试心得及实习经验,从测评,笔试,技术面,HR面,AI面,主管面,谈薪一站式服务,助你突破技术瓶颈、打破信息差,争取更多大厂offer。