突破编程_C++_基础教程(结构体)

1 结构体的概念与基本使用

结构体(struct)是一种用户定义的数据类型,用于封装多个不同类型的数据成员。结构体通常用于表示具有相关属性的数据集合。C++ 的结构体是从 C 语言中演化而来的。在 C 语言中,结构体是一种将不同类型的数据组合成一个单一类型的方式,通常用于创建复杂的数据结构。C++ 继承了 C 的这一特性,并对它进行了扩展,使得结构体在 C++ 中具有更多的功能和灵活性。

1.1 结构体的定义

C++ 定义结构体的语法如下:

struct 结构体名称 {  
    成员类型1 成员名称1;  
    成员类型2 成员名称2;  
    // ...  
    成员类型N 成员名称N;  
};

这个语法结构时平时最常用的定义结构体方式,如果有特别的需要,结构体也可以定义和使用模板,可以继承其他结构体等。如下是一个结构体的定义示例:

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

上面代码定义了一个 Student 的结构体,该结构体包含三个不同类型的数据成员: name (字符串)、age (整数)和 score (浮点数)。 C++ 结构体的成员默认是 public 的,所以在结构体的外部可以直接访问这些成员(结构体可以像类一样,使用 private、protected 或 public 关键字来控制成员的访问权限,不过这样的用法不多)。
注意: C 语言定义结构体时通常会使用 typedef ,例如:

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

这是由于在 C 语言中,定义一个结构体变量需要在变量名前重复结构体的名称。使用 typedef 后,则可以直接使用别名来声明变量,使代码更加简洁。C++ 则不用这么做,直接像前面代码一样定义即可。
在完成结构体的定义后,可以创建并初始化该结构体类型的对象(也成为实例),并可以对其成员变量做访问以及赋值等操作:

#include 
#include 

using namespace std;

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

int main()
{
	// 创建 Student 结构体的对象  
	Student st;
	
	// 初始化结构体对象成员变量
	st.name = "zhangsan";
	st.age = 10;
	st.score = 98.5;

	// 访问结构体对象成员变量
	printf("st.name = %s\n", st.name.c_str());
	printf("st.age = %u\n", st.age);
	printf("st.score = %lf\n", st.score);

	return 0;
}

上面代码的输出为:

st.name = zhangsan
st.age = 10
st.score = 98.500000

1.2 结构体初始化

常见的结构体初始化方式有如下 5 种:
(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 }

1.3 结构体数组

结构体数组是一个数组,其元素是结构体类型。这表明可以在数组中存储多个相同类型的结构体对象。结构体数组在处理一组相关的结构体数据时非常有用,比如存储多个学生的信息:

#include 
#include 

using namespace std;

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

int main()
{
	// 使用列表初始化
	Student sts[3] = { {"zhangsan",10,98.5},{"lisi",11,92},{"wangwu",10,95} };
	
	// 使用基于范围的 for 循环
	for (Student& st : sts)
	{
		printf("name = %s, age = %u, score = %lf\n", st.name.c_str(), st.age, st.score);
	}

	return 0;
}

上面代码的输出为:

name = zhangsan, age = 10, score = 98.500000
name = lisi, age = 11, score = 92.000000
name = wangwu, age = 10, score = 95.000000

注意上面代码中使用的 C++11 新特性:列表初始化以及基于范围的 for 循环,很大程度的简化了代码的编写。

2 结构体与函数

实际编程中,结构体与函数经常一起使用。结构体将数据封装在一起,而函数则提供了操作这些数据的方法。两者的结合使用有助于保持代码的整洁和组织。

2.1 结构体作为函数参数

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

#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

2.2 结构体作为函数返回值

结构体也可以作为函数的返回值。这在需要从函数中返回多个值时非常有用,因为可以将这些值打包到一个结构体中,然后一次性返回它们。如下为样例代码:

#include 
#include 

using namespace std;

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

Student createStudent()
{
	Student st = { "zhangsan",10,98.5 };
	return st;
}

int main()
{
	Student st = createStudent();
	printf("name = %s, age = %u, score = %lf\n", st.name.c_str(), st.age, st.score);

	return 0;
}

上面代码的输出为:

name = zhangsan, age = 10, score = 98.500000

**使用 tuple 模板类替代结构体作为函数返回值( C++11 新特性)
使用结构体作为返回值虽然可以实现从函数中返回多个值的功能,但是毕竟还需要定义一个结构体,这对于一些简单返回的场景还是有些麻烦。典型的例如某计算函数需要返回一个错误码( int 型)以及一个错误信息( string 型),如果单独再为这两个值定义一个结构体,就显得很多余(没有其他地方还需要它)。
tuple 模板类是 C++ 标准库 中的一个组件,并且从 C++11 开始可用。它提供了一种灵活的方式来创建不同类型的组合,而不需要创建一个新的类或结构体。如下为样例代码:

#include 
#include 
#include 

using namespace std;

tuple<string, uint32_t, double> createStudent()
{
	return tuple<string, uint32_t, double>("zhangsan", 10, 98.5);
}

int main()
{
	auto st = createStudent();

	// 访问 tuple 中的元素 
	printf("name = %s, age = %u, score = %lf\n", get<0>(st).c_str(), get<1>(st), get<2>(st));

	// 解构 tuple   
	string name;
	uint32_t age;
	double score;
	tie(name, age, score) = st;
	printf("name = %s, age = %u, score = %lf\n", name.c_str(), age, score);

	return 0;
}

上面代码的输出为:

name = zhangsan, age = 10, score = 98.500000
name = zhangsan, age = 10, score = 98.500000

3 结构体高级特性

结构体还支持嵌套、位域属性、组合和方法等其他高级特性,在特定场景中,这些特性非常实用。

3.1 结构体嵌套

结构体嵌套指的是一个结构体内部定义另一个结构体。这种特性可以支持创建更复杂的数据结构,将相关的数据组织在一起,并且可以通过封装来隐藏不必要的细节。在上面结构体列表初始化的相关内容中已经给出了一种结构体嵌套与初始化的方式:

struct A
{
	int a;  
};

struct B
{
	A a;  		//结构体 A 类型的对象嵌套在结构体 B 中
	int b
};

这种形式的结构体嵌套,struct A 在外部还可以被应用定义其他对象。除此之外,还有一种内部结构体的方式来做结构体嵌套,这种情况下外部是不能使用内部结构体的:

struct Student
{
	struct Address {
		string street;
		string city;
	};
	string name;	//姓名    
	uint32_t age;	//年龄    
	double score;	//分数
	Address addr;	//住址
};

Student st;	//OK
Address a;	//错误:不能使用内部结构体

3.2 结构体位域变量

结构体中的位域变量( 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 个字节。

3.3 结构体对齐

结构体对齐是编译器在内存中布局结构体成员时遵循的一种规则。对齐的目的是提高内存访问效率,减少因内存访问不对齐而引发的性能下降或硬件异常(上面 3.3 结构体位域变量 就涉及到结构体对齐的规则)。
在大多数系统中,数据对齐通常是按字节进行的,并且某些类型的数据(如整数和浮点数)需要按特定的对齐要求进行存储。例如,一个 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 时需要谨慎考虑其对性能和可移植性的影响。

3.4 POD 类型结构体

C++中的 POD(Plain Old Data) 类型是指与 C 语言兼容的数据结构。POD 类型的数据布局与 C 语言中的布局相同,这意味着 C++ 可以直接使用 C 语言库函数来操作 POD 数据类型,而且 POD 类型在 C 和 C++ 之间的操作(主要是 memcpy 与 memset )总是安全的。
由于 C++ 是 C 的超集,加入了更多语法层面的新机制,因此编译器会为非 POD 类型(如包含虚函数或构造函数的类型)添加额外的代码和数据成员,以支持这些新特性。这使得非 POD 类型的内存布局与 POD 类型不同,从而不能直接使用 C 语言库函数来操作。
前面样例中的结构体 struct Student 由于包含 string 类型,所以不能算是 POD 类型,可以做如下调整:

struct Student
{
	char name[100];	//姓名    
	uint32_t age;	//年龄    
	double score;	//分数
};

你可能感兴趣的:(突破编程_C++_基础教程,c++,开发语言)