结构体

结构体是一些值的合集,这些值称为成员变量

成员变量可以是标量、数组、指针甚至是其他结构体

结构体的声明

1
2
3
4
 struct tag
{
member-list;
};variable - list;

假如要描述一个学生

1
2
3
4
5
6
7
struct Stu 
{
char name[20]; // 姓名
int age; // 年龄
char sex[5];// 性别
char id[20]; // 学号
};

结构体变量的初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 变量的定义
struct Stu
{
char name[20]; // 姓名
int age; // 年龄
char sex[5];// 性别
char id[20]; // 学号
};

struct Stu stu1 = { "张三",18,"male","23001" }; // 一般初始化
struct Stu stu2 = { .name = "李四",.age = 19,.sex = "fmale",.id ="23002"}; // 指定初始化

struct Book
{
char name[20]; // 书名
char author[20]; // 作者名
float price; // 价格
}book1 = {"三国演义","罗贯中",6.66}; // 在声明一个结构体的同时创建并初始化了一个结构体变量

struct Book book2 = { "西游记","吴承恩",6.66 };

struct Test
{
int num;
struct Book;
};

struct Test test1 = { 1,{"红楼梦","曹雪芹",6.66} }; // 嵌套结构体初始化

结构体的特殊声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 匿名结构体类型
struct
{
int a;
int b;
int c;
}x;

struct
{
char a;
char b;
char c;
}c = {'a','b','c'};

上面两端代码在声明时省略了结构体标签

需要注意的是编译器会将多个匿名结构体当成完全不同的类型

匿名结构体如果没有对结构体类型重命名,基本上只能用一次

结构体访问操作符

结构体成员的直接访问

结构体成员的直接访问是通过点操作符(.)访问的

1
结构体变量名.成员变量名
1
2
3
4
5
6
7
8
9
10
11
12
struct Point
{
int x;
int y;
}p = { 1,2 };

int main()
{
printf("%d %d", p.x, p.y);
return 0;
}

结构体成员的间接访问

结构体成员的间接访问是通过点操作符(->)访问的

1
结构体指针变量->成员名
1
2
3
4
5
6
7
8
9
10
11
12
13
struct Point
{
int x;
int y;
}p = { 1,2 };

int main()
{
struct Point* ps = &p;
printf("%d %d\n", ps->x, ps->y);
printf("%d %d", p.x, p.y);
return 0;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
struct Student
{
char name[15]; // 姓名
int age; // 年龄
};


void print_stu_1(struct Student s)
{
printf("%s %d\n", s.name, s.age);
}

void print_stu_2(struct Student* s)
{
printf("%s %d\n", s->name,s->age);
}

int main()
{
struct Student stu1 = { "张三",18 };
struct Student* pstu1 = &stu1 ;
print_stu_1(stu1);
print_stu_2(pstu1);
return 0;
}

结构体的内存对齐

1
2
3
4
5
6
7
8
9
10
11
12
13
struct Test
{
int x;
char a;
int y;
};

int main()
{
struct Test t1 = { 1,'a',2 };
printf("%zd\n", sizeof(t1));
return 0;
}

按理来说两个 int 和 一个 char 只用 6 个字节就可以了,这里为什么分配了 12 个

字节

结构体的内存对齐规则

那么上述代码就是这么分配内存空间的

首先这是一段内存,一个方块表示一个字节

int 类型变量 x 从 0 处开始存放,存放四个字节

在存放 char 类型变量 a 时 , 需要先确定对齐数,对齐数 = 编译器默认对齐数与成

员变量大小的较小值, 在 visual studio 2026 中默认对齐数是 8 , char 类型是 1 , 此

处使用的对齐数是 1 , 对齐到 1 的整数倍开始存

同理,可推导出如何存 y

而结构体总大小为最大对其数的整数倍,这个结构体中的最大对齐数是 4 ,所以这个结构体的大小就是 12 个字节

1
2
3
4
5
6
7
8
9
10
11
12
13
struct Test2
{
int x; // 4 8 4
struct Test t; // 4 8 4
int y; // 4 8 4
};

int main()
{
struct Test2 t2 = { 0 };
printf("%zd\n", sizeof(t2));
return 0;
}

结构体的对齐值由其内部成员的最大对齐值决定, Test 中最大对齐数是 4 , 4 < 8 ,

存放 t 时要从4的整数倍开始存放

为什么存在内存对齐

  • 平台原因:

    不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只

    能在某些地址处取某些特定类型的数据,否则抛出硬件异常

  • 性能原因:

    数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问

    未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一

    次访问。

    假如现在有一个结构体声明

    1
    2
    3
    4
    5
    6
    struct My_str 
    {
    char a;
    int x;
    };

    假设都是一次性读取四个字节

    一次就能得到 a 或者 x

    未对齐的情况下需要读两次, 将 x 拼接出来

进行内存对齐实际上就是用空

间换时间的做法

如果想要节省空间,可以将占空间小的成员变量尽量排在一起
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct Test3
{
// 小的变量放在一起节省空间
char a;
char b;
int x;
};

struct Test4
{

char a;
int x;
char b;
};

struct Test3 t3 = { 0 };
struct Test4 t4 = { 0 };
printf("%zd\n", sizeof(t3));
printf("%zd\n", sizeof(t4));

修改默认对齐数

在 visual studio 2026 中,可以自行修改默认对齐数

1
#pragam

结构体传参

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
struct S
{
int data[1000];
int num;
};

//结构体传参

void print1(struct S s)
{
printf("%d\n", s.num);
}
//结构体地址传参

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

int main()
{
struct S s = { {1,2,3,4}, 1000 };
print1(s);
print2(&s);
return 0;
}

print1 和 print2 虽然功能一样,但在 print1 进行操作时,需要再创造一个 struct S s

类型的变量来接收,要造成不小的性能开销

而 print2 只需要一个指针变量大小的空间,性能上要节省不少

结构体传参时,首选传结构体的地址

如果担心传址调用会修改实参的的值使用 const 关键字就好了

1
2
3
4
5
void print2(const struct S* ps)
{
printf("%d\n", ps->num);
}

结构体实现位段

位段的声明和结构是类似的,有两个不同:

  • 位段的成员是 int unsigned int signed int char 类型, 在 C99 中可以是其他类型

  • 位段的成员名后有一个冒号和一个数字

位段的内存分配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct A
{
// 位段式结构
int _a : 2; // 分配 2 个 bit 位
int _b : 5; // 分配 5 个 bit 位
int _c : 10; // 分配 10 个 bit 位
int _d : 30; // 分配 30 个 bit 位
};

struct S
{
// 位段式结构
int a;
int b;
int c;
int d;
};



printf("%zd\n", sizeof(struct A));
printf("%zd\n", sizeof(struct S));


1
2
3
4
5
6
7
8
struct S2
{
char _a : 3;
char _b : 4;
char _c : 5;
char _d : 4;
};

位段的空间是按照四个字节(int)或一个字节(char)进行开辟的

分配原理如下:

验证

1
printf("%zd\n", sizeof(struct S2));

位段涉及诸多不确定因素,可移植程序应避免使用位段

位段的应用

这是IP数据报的格式,其中很多属性只需几个 bit 就能描述,使用位段既完成了需

求又节约了空间,网络传输的数据报大小也会较小一些,对网络的畅通是由帮助的

位段使用的注意事项

位段的成员会有共用一个字节的情况,某个成员的起始位置并不是该字节的起始位

置,那么这些位置是没有地址的。内存中每一个字节分配一个地址,一个字节内的

bit 位是没有地址的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct S2
{
char _a : 3;
char _b : 4;
char _c : 5;
char _d : 3;
};

int main()
{
struct S2 s2 = { 0 };
scanf("%c", &s2._b);
return 0;
}