突破编程_C++_面试(基础知识(8))

面试题20:什么内存对齐

以结构体为例来说明内存对齐:
结构体对齐是编译器在内存中布局结构体成员时遵循的一种规则。对齐的目的是提高内存访问效率,减少因内存访问不对齐而引发的性能下降或硬件异常。
在大多数系统中,数据对齐通常是按字节进行的,并且某些类型的数据(如整数和浮点数)需要按特定的对齐要求进行存储。例如,一个 4 字节的整数可能需要存储在 4 字节对齐的地址上。
编译器通常会在结构体成员之间插入填充字节以确保正确的对齐。填充字节的具体数量取决于成员的类型、大小以及编译器的对齐规则。
结构体对齐的规则可以因编译器、目标平台和编译器设置的不同而有所差异。然而,有一些常见的对齐规则:
(1)默认对齐规则:编译器通常会按照成员的顺序和它们的大小来布局结构体。它会尝试将每个成员对齐到其类型所需的对齐边界上。
(2)指定对齐:可以使用 #pragma pack 指令或 [[gnu::packed]] 属性(取决于编译器)来指定结构体的对齐方式。例如,使用 #pragma pack(1) 可以告诉编译器按照 1 字节对齐来布局结构体,从而消除所有的填充字节。
(3)最大对齐规则:在某些情况下,编译器可能会根据结构体中最大对齐要求的成员来对齐整个结构体。这意味着结构体的大小可能是其最大对齐成员的整数倍。
(4)平台和编译器特定的对齐:不同的硬件平台和编译器可能有不同的默认对齐规则。例如,某些平台可能要求双字( double-word )类型(如 double 或 long long )按 4 字节或 8 字节对齐。
如下为样例代码:

#include 

struct A
{
	int64_t val1;
	char val2;
	int val3;
	int val4;
};

int main() {
	
	A a = { 1,0x02,3,4 };
	size_t len = sizeof(A);
	char* tmp = new char[len] {};
	memcpy(tmp,&a, len);

	delete[] tmp;
	tmp = nullptr;
	return 0;
}

对于 struct A ,具体的分配空间方式如下:
(1)为第一个成员 val1 分配空间,其起始地址跟结构的起始地址相同(恰好偏移量 0 且为 sizeof(int64_t) 的倍数),该成员变量占用 sizeof(int64_t)=8 个字节。
(2)为第二个成员 val2 分配空间,这时下一个能够分配的地址对于结构的起始地址的偏移量为8,是 sizeof(char) 的倍数,因此把 val2 存放在偏移量为 8 的地方,该成员变量占用 sizeof(char)=1 个字节。
(3)为第三个成员 val3 分配空间,这时下一个能够分配的地址对于结构的起始地址的偏移量为 9 ,不是 sizeof(int)=4 的倍数,为了知足对齐方式对偏移量的约束问题,编译器会自动填充 3 个字节,此时分配的地址对于结构的起始地址的偏移量为12,该成员变量占用 sizeof(int)=4 个字节。
(4)为第四个成员 val4 分配空间,这时下一个能够分配的地址对于结构的起始地址的偏移量为 16 ,正好是 sizeof(int)=4 的倍数,因此把 val4 存放在偏移量为 16 的地方,该成员变量占用 sizeof(int)=4 个字节。
(5)这时整个结构的成员变量已经都分配了空间,总的占用的空间大小为:8+1+3+4+4=20,不是结构中占用最大空间的类型所占用的字节数( sizeof(double)=8 )的倍数,所以编译器还要填充 4 个字节,因此整个结构的大小为:sizeof(A)=8+1+3+4+4+3=20。
如下为调试中打印出来的内存字节(变量 tmp ):

-		tmp,24	0x00000281e0a73900 "\x1\0\0\0\0\0\0\0\x2ÌÌÌ\x3\0\0\0\x4\0\0\0ÌÌÌÌ"	char[24]
		[0]	1 '\x1'	char
		[1]	0 '\0'	char
		[2]	0 '\0'	char
		[3]	0 '\0'	char
		[4]	0 '\0'	char
		[5]	0 '\0'	char
		[6]	0 '\0'	char
		[7]	0 '\0'	char
		[8]	2 '\x2'	char
		[9]	-52 'Ì'	char
		[10]	-52 'Ì'	char
		[11]	-52 'Ì'	char
		[12]	3 '\x3'	char
		[13]	0 '\0'	char
		[14]	0 '\0'	char
		[15]	0 '\0'	char
		[16]	4 '\x4'	char
		[17]	0 '\0'	char
		[18]	0 '\0'	char
		[19]	0 '\0'	char
		[20]	-52 'Ì'	char
		[21]	-52 'Ì'	char
		[22]	-52 'Ì'	char
		[23]	-52 'Ì'	char

注意其中 [9] 至 [11] 以及 [20] 至 [23] 是编译器填充的字节。
使用 #pragma pack(n) 可以指定一个对齐值 n,它告诉编译器按照 n 字节的边界进行对齐。这意味着编译器不会在成员之间插入更多的填充字节,除非这样做是为了确保下一个成员按照其类型所需的对齐要求进行存储。
例如,如果需要让结构体的成员紧密排列,不插入任何填充字节,可以使用 #pragma pack(1):

#include 

#pragma pack(push, 1) // 保存当前对齐设置,并设置对齐为1字节  
struct A
{
	int64_t val1;
	char val2;
	int val3;
	int val4;
};
#pragma pack(pop) // 恢复之前的对齐设置  

int main() {
	
	A a = { 1,0x02,3,4 };
	size_t len = sizeof(A);
	char* tmp = new char[len] {};
	memcpy(tmp,&a, len);

	// A 的大小将是 17 字节,而不是默认的可能是 24 字节(取决于平台和编译器)

	return 0;
}

需要注意的是,过度使用 #pragma pack 并将其设置得很小(如 1 )可能会导致内存访问性能下降,因为数据可能不会按照处理器最优的方式对齐。此外,某些硬件平台可能要求特定的对齐方式,因此在使用 #pragma pack 时需要谨慎考虑其对性能和可移植性的影响。

面试题21:结构体初始化由哪些方式

(1)默认初始化
当声明一个局部结构体变量时,如果没有显式地初始化它,那么它的成员将进行默认初始化。(通常是随机值,取决于内存的内容):

struct A
{
	int64_t val1;
	int64_t val2;
} ;

A a;

变量 a 的值为:

a = {val1=-3689348814741910324 val2=-3689348814741910324 }

(2)聚合初始化
聚合初始化使用花括号 {} 来初始化结构体的成员。:

struct A
{
	int64_t val1;
	int64_t val2;
} ;

A a = {1, 2}; 

变量 a 的值为:

a = {val1=1 val2=2 }

(3)指定成员初始化
在C++11及以后的版本中,可以使用指定成员初始化来明确指定要初始化的成员。这个特性适用于仅需要初始化部分成员变量:

struct A
{
	int64_t val1;
	int64_t val2;
} ;

A a = { a.val1 = 1 };

变量 a 的值为:

a = {val1=1 val2=0 }

(4)构造函数初始化:
如果结构体有定义构造函数,那么可以使用构造函数来初始化结构体的成员。构造函数可以接收参数,并可以使用这些参数来初始化成员变量:

struct A
{
	A(int64_t val1In, int64_t val2In):val1(val1In), val2(val2In) {}
	int64_t val1;
	int64_t val2;
} ;

A a(1, 2);

变量 a 的值为:

a = {val1=1 val2=2 }

(5)列表初始化:
列表初始化是 C++11 新标准中引入的一种新的初始化语法,它结合了聚合初始化和指定成员初始化的特点:

struct A
{
	int64_t val1;
	int64_t val2;
};

A a{ 1, 2 };

变量 a 的值为:

a = {val1=1 val2=2 }

面试题22:如何将结构体作为函数的参数传递

使用结构体作为函数参数时,可以选择 按值传递 或 按引用传递 。按值传递会导致结构体的复制,这可能会消耗更多的资源,特别是当结构体很大时。按引用传递则避免了复制,但需要注意的是,如果再函数体内修改了引用的结构体,那么原始的结构体也会被修改。如果不希望原始结构体被修改,可以使用常量引用(这是最常用的结构体传参方式):

#include 
#include 

using namespace std;

struct Student
{
	string name;	//姓名    
	uint32_t age;	//年龄    
	double score;	//分数
};

// 使用常量结构体引用作为函数参数
void printStudent(const Student& st)
{
	printf("name = %s, age = %u, score = %lf\n", st.name.c_str(), st.age, st.score);
}

int main()
{
	Student st = { "zhangsan",10,98.5 };
	printStudent(st);

	return 0;
}

上面代码的输出为:

name = zhangsan, age = 10, score = 98.500000

面试题23:什么是结构体的位字段

结构体中的位字段变量( bit-field )是一种特殊的成员,用于在结构体中存储固定位数的数据。位域变量允许控制每个成员在内存中的确切位数,这在某些特定应用场景(如硬件编程、网络通信等)中非常有用,因为它可以实现更紧凑的数据存储和更精确的内存布局。
位域变量的声明方式是在结构体的成员声明中指定一个冒号和一个整数,这个整数表示该成员应该占用的位数。如下为样例代码:

#include 
#include 
#include 

using namespace std;

struct A {
	uint32_t val1 : 1; // 占用 1 个位  
	uint32_t val2 : 2; // 占用 2 个位  
	uint32_t val3 : 3; // 占用 3 个位   
	// 注意:编译器可能会插入填充位以满足内存对齐要求 ,不同编译器的 sizeof(A) 可能不一样
};

int main() {
	A a;

	// 设置位域变量的值  
	a.val1 = 1; // 二进制:1 
	a.val2 = 2; // 二进制:10  
	a.val3 = 4; // 二进制:100  

	printf("val1 = %u, val2 = %u, val3 = %u\n", a.val1, a.val2, a.val3);
	printf("sizeof(A) = %u\n", sizeof(A));

	return 0;
}

上面代码的输出为:

val1 = 1, val2 = 2, val3 = 4
sizeof(A) = 4

注意,上面结构体 A 的理论大小为 6 个字节 ,不到 1 个字节,但是经过内存对齐,结构体 A 的实际大小为 4 个字节。

你可能感兴趣的:(突破编程_C++_面试,c++,面试)