【C语言】自定义类型:结构体

发布于:2025-08-07 ⋅ 阅读:(11) ⋅ 点赞:(0)

一、结构体的基本概念

C语言已经提供了内置类型,如char,short,float,double等,这些内置类型无法描述一个复杂的对象。因此C语言提供了结构体这种自定义的数据类型。它允许我们将不同类型的数据组合在一起,形成一个新的自定义数据类型。这一特性极大地增强了 C 语言处理复杂数据的能力,在实际编程中有着广泛的应用。

1.1结构体的声明

结构体使用关键字 struct 来定义

例如,想要描述一个学生:

struct student
{
  char name[20]; // 名字
  int age; //年龄
  double height; //身高
  double weight; //体重
  char id[16]  //学号
};//分号不可以少

结构体是一些值的集合,这些值称为成员变量。结构体中的每个成员可以是不同的数据类型,如标量,数组,指针甚至是其他结构体。

1.2 结构体变量的创建

1.2.1 在结构体声明同时创建

struct student
{
  char name[20]; 
  int age; 
  double height ;
  double weight; 
  char id[16]  
} s1,s2;

结构体声明时,创建变量是可选选项。此时创建的s1,s2是全局变量

1.2.2 在main函数中创建

#include <stdio.h>
struct student
{
	char name[20];
	int age;
	double height;
	double weight;
	char id[16]
};
int main( )
{
	struct student s3;
	struct student s4;

	return 0;
}

此时创建的变量是局部变量

1.2.3 匿名结构体

在声明结构体时,可以省略标签(tag),形成匿名结构体:

// 匿名结构体类型
struct
{
    int a;
    char b;
    float c;
} x; // 直接创建变量x

struct
{
    int a;
    char b;
    float c;
} a[20], *p; // 创建数组a和指针p

匿名结构体的特点是:如果没有对其重命名,基本上只能使用一次。

需要注意的是,编译器会把两个成员列表相同的匿名结构体视为不同的类型,因此下面的赋值是非法的:

p = &x; // 错误:编译器认为 p和 x的类型不同

1.3 结构体变量的初始化

1.3.1 按照默认顺序

struct student s3 = {"daisy",20,170,50,"2410"};

如果结构体成员变量中包含结构体:

#include <stdio.h>

struct S
{
	char c;
	int n;
};

struct A
{
	struct S s;
	int* p;
	char arr[10];
};

int main() 
{
	struct A a = { {'1',1},NULL,"hhhh"};
}

1.3.2 指定成员初始化

struct student s3 = {.age = 30,.weight=60,.name="Rare",.height=180,.id="2410"};

1.4 结构体成员访问

  • 使用操作符.可以直接访问结构体中的成员
    在这里插入图片描述

例如:打印结构体成员:

#include <stdio.h>

struct student
{
	char name[20]; // 名字
	int age; //年龄
	double height; //身高
	double weight; //体重
	char id[16];  //学号
};

int main() 
{
	struct student s1 = {"daisy",20,170,50,"2410"};
	printf("%s %d %f %f %s\n",s1.name, s1.age,s1.height,s1.weight,s1.id);
}

在这里插入图片描述

  • 使用操作符->可以间接访问成员,一般用于访问指针变量指向的内容
#include <stdio.h>
struct Point
{
 int x;
 int y;
};
int main()
{
 struct Point p = {3, 4};
 struct Point *ptr = &p;
 ptr->x = 10;
 ptr->y = 20;
 printf("x = %d y = %d\n", ptr->x, ptr->y);
 return 0;
}

1.5 结构体的自引用

结构体可以包含指向自身类型的指针,这在数据结构中非常常见(如链表、树等)。

错误的自引用方式

struct Node
{
    int data;
    struct Node next; // 错误:会导致结构体大小无限大
};

正确的自引用方式

struct Node
{
    int data;//数据域
    struct Node* next; // 指针域
    //正确:使用指针
};

使用typedef对结构体重命名时,需要注意自引用的正确写法:

错误写法

typedef struct
{
    int data;
    Node* next; // 错误:此时Node尚未定义
} Node;

正确写法

typedef struct Node
{
    int data;
    struct Node* next; // 正确:使用struct Node
} Node;

二、结构体内存对齐

计算结构体的大小是C语言中的一个重要考点,涉及到内存对齐规则。

2.1 对齐规则

  1. 结构体的第一个成员对齐到与结构体变量起始位置偏移量为0的地址处。
  2. 其他成员变量要对齐到某个"对齐数"的整数倍的地址处。
    对齐数=编译器默认的一个对齐数 与 该成员变量大小的较小值.
    • VS中默认对齐数为8
    • Linux中gcc没有默认对齐数,对齐数就是成员自身的大小
  3. 结构体总大小为最大对齐数(结构体中所有成员对齐数的最大值)的整数倍。
  4. 如果嵌套了结构体,嵌套的结构体成员对齐到自己的最大对齐数的整数倍处,结构体的整体大小是所有最大对齐数(含嵌套结构体的)的整数倍。

2.2 示例分析

练习1

struct S1
{
    char c1; // 大小1,对齐数1
    int i;   // 大小4,对齐数4(VS环境)
    char c2; // 大小1,对齐数1
};
printf("%d\n", sizeof(struct S1)); // 输出结果:12

在这里插入图片描述

分析:

  • c1从偏移0开始,占用1字节(0)
  • i需要对齐到4的倍数,从偏移4开始,占用4字节(4-7)
  • c2从偏移8开始,占用1字节(8)
  • 总大小需为最大对齐数4的倍数,因此总大小为12

练习2

struct S2
{
    char c1; // 1字节
    char c2; // 1字节
    int i;   // 4字节
};
printf("%d\n", sizeof(struct S2)); // 输出结果:8

在这里插入图片描述

分析:

  • c1在0,c2在1,共占用2字节
  • i对齐到4的倍数,从4开始,占用4字节(4-7)
  • 总大小8,是最大对齐数4的倍数

练习3

struct S3
{
    double d; // 8字节
    char c;   // 1字节
    int i;    // 4字节
};
printf("%d\n", sizeof(struct S3)); // 输出结果:16

在这里插入图片描述

分析:

  • d在0-7(8字节)
  • c在8(1字节)
  • i需要对齐到4的倍数,从12开始(12-15)
  • 总大小16,是最大对齐数8的倍数

练习4(嵌套结构体)

struct S4
{
    char c1;   // 1字节
    struct S3 s3; // 16字节(最大对齐数8)
    double d;  // 8字节
};
printf("%d\n", sizeof(struct S4)); // 输出结果:32

在这里插入图片描述

分析:

  • c1在0(1字节)
  • s3需要对齐到自身最大对齐数8的倍数,从8开始,占用16字节(8-23)
  • d对齐到8的倍数,从24开始,占用8字节(24-31)
  • 总大小32,是最大对齐数8的倍数

2.3 内存对齐的原因

  1. 平台原因(移植性)
    某些硬件平台只能访问特定地址的数据,否则会抛出异常。

  2. 性能原因
    对齐的内存访问只需一次操作,未对齐的可能需要两次。例如,访问未对齐的8字节数据可能需要读取两个8字节块并拼接。

内存对齐是"空间换时间"的策略。设计结构体时,应将占用空间小的成员集中在一起,以减少内存浪费(如S2比S1更节省空间)。

2.4 修改默认对齐数

可以使用#pragma pack预处理指令修改默认对齐数:

#include <stdio.h>
#pragma pack(1) // 设置默认对齐数为1
struct S
{
    char c1; // 1字节
    int i;   // 4字节
    char c2; // 1字节
};
#pragma pack() // 取消设置,还原默认

int main()
{
    printf("%d\n", sizeof(struct S)); // 输出6(1+4+1)
    return 0;
}

当对齐方式不合适时,可通过此指令优化内存使用。

三、结构体传参

结构体传参有两种方式:传值和传地址。

struct S
{
    int data[1000];
    int num;
};

struct S s = {{1,2,3,4}, 1000};

// 传值
void print1(struct S s)
{
    printf("%d\n", s.num);
}

// 传地址
void print2(struct S* ps)
{
    printf("%d\n", ps->num);
}

int main()
{
    print1(s);  // 传结构体
    print2(&s); // 传地址
    return 0;
}

推荐使用传地址方式,原因是:

  • 函数传参时参数需要压栈,结构体过大时,传值会导致大量的时间和空间开销
  • 传地址仅传递4或8字节(指针大小),效率更高
  • 若需要修改结构体内容,传地址是必要的

四、结构体实现位段

位段(bit-field)是结构体的一种特殊形式,用于节省内存空间,尤其适合存储只需要少量bit位的数据。

4.1 位段的声明

位段的声明与结构体类似,但有两个区别:

  1. 位段成员通常是intunsigned intsigned intchar类型
  2. 成员名后有一个冒号和数字(表示占用的bit位数)
struct A
{
    int _a:2;  // 占用2个bit位
    int _b:5;  // 占用5个bit位
    int _c:10; // 占用10个bit位
    int _d:30; // 占用30个bit位
};
printf("%d\n", sizeof(struct A)); // 输出8

分析:

  • 前三个成员共占用2+5+10=17个bit位
  • 加上_d的30位,总共47位
  • 位段以4字节(int)或1字节(char)为单位开辟空间,因此需要8字节(64位)

4.2 位段的内存分配

存在矛盾:

  1. 位段空间按4字节(int)或1字节(char)的方式开辟
  2. 位段成员在内存中的分配方向(左到右或右到左)未定义
  3. 当剩余空间不足时,是否利用剩余空间未定义

示例:

struct S
{
    char a:3; // 3位
    char b:4; // 4位
    char c:5; // 5位
    char d:4; // 4位
};
struct S s = {0};
s.a = 10; // 二进制1010(只取低3位:010)
s.b = 12; // 二进制1100
s.c = 3;  // 二进制11
s.d = 4;  // 二进制100

在这里插入图片描述

在VS环境中的内存分配(每次分配1字节,分配方向为从右到左):

  • 第一个字节:a占3位(010),b占4位(1100),共7位,剩余1位未使用
  • 第二个字节:c占5位(00011)
  • 第三个字节:c剩余0位,d占4位(0100)

4.3 位段的跨平台问题

位段存在以下跨平台问题,需谨慎使用:

  1. int位段的符号性不确定(有符号/无符号)
  2. 位段最大位数不确定(16位机器最大16,32位机器最大32)
  3. 成员分配方向(左到右/右到左)未定义
  4. 剩余空间处理方式不确定

4.4 位段的应用场景

位段适合存储不需要完整字节的数据,例如网络协议中的数据报格式:
在这里插入图片描述

使用位段可以精确控制每个字段的位数,显著减少数据报大小,提高网络传输效率。

4.5 位段使用注意事项

位段成员没有地址,因此不能使用&操作符,也不能直接用scanf输入:

struct A
{
    int _a:2;
    int _b:5;
};

int main()
{
    struct A sa = {0};
    // scanf("%d", &sa._b); // 错误:不能取位段成员地址
    
    // 正确方式
    int b = 0;
    scanf("%d", &b);
    sa._b = b;
    return 0;
}

结构体是C语言中实现复杂数据类型的基础,掌握结构体的声明、初始化、内存对齐规则和传参方式,对于编写高效、可维护的代码至关重要。位段作为结构体的特殊形式,在需要节省内存的场景(如嵌入式系统、网络协议)中非常有用,但需注意其跨平台限制。

通过合理设计结构体,我们可以将相关数据组织在一起,提高代码的可读性和效率。理解内存对齐原理,能帮助我们写出更节省内存的程序;而正确的传参方式,则能提升程序性能。这些知识不仅是C语言学习的重点,也是后续学习数据结构(如链表、树、图)的基础。


网站公告

今日签到

点亮在社区的每一天
去签到