问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

深入理解C语言中的结构体

创作时间:
作者:
@小白创作中心

深入理解C语言中的结构体

引用
1
来源
1.
https://cloud.tencent.com/developer/article/2456000

在C语言中,结构体是一种强大的数据组织工具,它允许你将不同类型的数据组合成一个单一的实体。无论是在处理复杂数据、设计数据模型还是进行内存优化,结构体都能帮助你更好地管理和组织数据。本文将深入探讨C语言中的结构体。

一. 结构体的定义与基本用法

什么是结构体?结构体是一种用户自定义的数据类型,它允许我们将逻辑上相关的数据组合在一起。每个数据项称为结构体的成员。结构体的成员可以是基本数据类型(如int、float、char等),也可以是其他复合数据类型(如数组、指针、甚至其他结构体)。

1.结构体的声明

在C语言中,结构体的声明用于定义新的数据类型,这种数据类型由多个不同的数据成员组成。声明结构体的基本语法如下:

struct 结构体名称 {
    数据类型 成员1;
    数据类型 成员2;
    // 更多成员
};

示例:

#include <stdio.h>
// 声明一个结构体类型Student
struct Student {
   char name[20];//名字
   int age;//年龄
   char sex[5];//性别
   char id[20];//学号
};

在上面的代码中,Student是一个命名结构体,可以用这个类型名称创建多个结构体变量,而point是一个匿名结构体,没有显式的名称,以此无法无法使用这个结构体来创建其他的变量。

2. 结构体变量的创建和初始化

声明结构体类型后,你可以创建结构体变量并对其进行初始化。结构体变量可以是结构体类型的实例,你可以在声明时进行初始化,也可以在运行时赋值。

#include <stdio.h>
struct Stu
{
    char name[20];//名字
    int age;//年龄
    char sex[5];//性别
    char id[20];//学号
};
int main()
{
    //按照结构体成员的顺序初始化
    struct Stu s = { "张三", 20, "男", "20230818001" };
    printf("name: %s\n", s.name);
    printf("age : %d\n", s.age);
    printf("sex : %s\n", s.sex);
    printf("id : %s\n", s.id);
    //按照指定的顺序初始化
    struct Stu s2 = { .age = 18, .name = "lisi", .id = "20230818002", .sex = "女" };
    printf("name: %s\n", s2.name);
    printf("age : %d\n", s2.age);
    printf("sex : %s\n", s2.sex);
    printf("id : %s\n", s2.id);
    return 0; 
}   

3. 结构体成员访问操作符

C语言提供了两种操作符来访问结构体的成员: 点操作符(.):用于通过结构体变量访问成员。 箭头操作符(->):用于通过结构体指针访问成员。

示例:

#include <stdio.h>
struct Stu
{
    char name[20];//名字
    int age;//年龄
    char sex[5];//性别
    char id[20];//学号
};
int main()
{
    struct Stu s = { "张三", 20, "男", "20230818001" };
    struct Stu* ptr = &s;
    printf("name: %s\n", ptr->name);
    printf("age : %d\n", ptr->age);
    printf("sex : %s\n", ptr->sex);
    printf("id : %s\n", ptr->id);
    return 0; 
}   

4.结构体的特殊声明

  1. 匿名结构体

当你定义一个匿名结构体时,你只能在定义它的同时创建一个变量。这个结构体没有名字,因此无法在其他地方使用这个结构体来创建新的变量。

struct {
    int x;
    int y;
} point;

这里point是一个结构体变量,而结构体本身没有名字。

  1. 嵌套结构体

嵌套结构体就是在结构体内部定义另一个结构体。结构体可以嵌套其他结构体,包括匿名结构体。

struct Date {
    int day;
    int month;
    int year;
};
struct Person {
    char name[50];
    struct Date birthday; // 嵌套结构体
    float height;
};

在这个例子中,Person 结构体包含了 Date 结构体作为其一个成员。

3.结构体自引用

结构体自引用是指结构体中的一个或多个成员是指向相同结构体类型的指针。

struct Node {
    int value;
    struct Node* next; // 自引用:指向相同结构体类型的指针
};

在这个例子中,Node 结构体包含一个名为 next 的指针,它指向另一个 Node 结构体实例。

  1. typedef 声明

使用 typedef 关键字可以为结构体定义一个新的类型名,使结构体声明更加简洁。

typedef struct {
    char* name;
    int age;
} Person;
Person p1,p2;//创建两个结构体变量

在这个例子中,Person成为了struct { char* name; int age; }这个结构体类型的别名,可以用Person来创建多个结构体变量,如Person p1,p2;。

二、结构体内存对齐

什么是内存对齐?内存对齐是指将数据存储在内存中的特定地址上,使得数据的起始地址满足某种对齐要求。对齐的要求通常与数据类型的大小有关。例如,4字节的整数通常要求存储在4的倍数的地址上。

1.对齐规则

首先得掌握结构体的对齐规则:

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

示例:

#include <stdio.h>
struct S1
{
    char c1;//占1字节
    int i;//占4字节
    char c2;//占1字节
};
int main()
{
    printf("%d\n", sizeof(struct S1));//结果是12
    return 0;
}

内存分布:

2.为什么存在内存对齐?

  1. 平台原因 (移植原因):
  • 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
  1. 性能原因:
  • 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。假设一个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对齐成8的倍数,那么就可以 用一个内存操作来读或者写值了。否则,我们可能需要执行两次内存访问,因为对象可能被分放在两个8字节内存块中。

总体来说:结构体的内存对齐是拿空间来换取时间的做法。

那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到 :

让占用空间小的成员尽量集中在一起来

#include <stdio.h>
struct S1
{
    char c1;//占1字节
    int i;//占4字节
    char c2;//占1字节
};
struct S2//s2中占用空间小的成员集中在了一起
{
    char c1;//占1字节
    char c2;//占1字节
    int i;//占4字节
};
int main()
{
    printf("Size of S1:%d\n", sizeof(struct S1));
    printf("Size of S2:%d\n", sizeof(struct S2));
}

S1 和 S2 类型的成员一模一样,但是 S1 和 S2 所占空间的大小有了些许区别:

3.修改默认对齐数

#pragma这个预处理指令,可以改变编译器的默认对齐数。

#include <stdio.h>
#pragma pack(1) // 设置对齐数为1字节
struct MyStruct {
    char a; // 占1字节
    int b; // 占4字节
    double c;// 占8字节
};
#pragma pack()// 恢复默认对齐方式
int main() {
    printf("Size of MyStruct: %zu\n", sizeof(struct MyStruct));
    return 0;
}

运行结果:

#pragma pack(1)的效果仅限于它和随后的#pragma pack()之间的代码。一旦执行到#pragma pack(),对齐数将恢复到编译器的默认设置,但这不会改变MyStruct的定义,因为MyStruct是在#pragma pack(1)的作用下定义的。

所以,MyStruct的大小计算如下:

char a; 占用1字节
int b; 由于对齐数为1,所以紧接着char a后面,占用4字节
double c; 由于对齐数为1,所以紧接着int b后面,占用8字节
因此,MyStruct的总大小是1 + 4 + 8 = 13字节。这里没有额外的填充字节,因为对齐数被设置为1,这意味着结构体中的成员是紧挨着存放的,没有额外的填充字节。

三、结构体传参

1.按值传递和按指针传递对比

#include<stdio.h>
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;
}

上面的 print1 和 print2 函数哪个好些?

答案是:首选print2函数。

原因:函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。结论: 结构体传参的时候,要传结构体的地址。

四、结构体实现位段

1.位段的定义

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

  1. 位段的成员必须是 int、unsigned int 或signed int ,在C99中位段成员的类型也可以
    选择其他类型。
  2. 位段的成员名后边有一个冒号和一个数字。

位段在结构体中的定义方式如下:

struct bit_field_struct {
    type member_name : width;
};

type 是位段的数据类型,通常是 unsigned int 或 int。
member_name 是位段的名称。
width 是位段的宽度,表示该位段所占的位数。

2.位段的内存分配

  1. 位段的成员可以是 int unsigned int signed int 或者是 char 等类型
  2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
  3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
#include<stdio.h>
struct S {
    char a : 3;
    char b : 4;
    char c : 5;
    char d : 4;
};
int main() {
    struct S s = { 0 };
    s.a = 10;
    s.b = 12;
    s.c = 3;
    s.d = 4;
    return 0;
}
//空间是如何开辟的?

示意图:

3.注意事项

位段类型:位段的类型必须是int、unsigned int或signed int。
位段宽度:位段的宽度必须是一个非负整数常量表达式。
位段对齐:位段成员可能会跨越其类型的自然边界,这取决于具体的编译器实现。
未命名的位段:可以使用未命名的位段(如上面例子中的unsigned int : 0;)来强制下一个位段从下一个存储单元开始,这有助于对齐。
访问位段:可以使用结构体变量名和点操作符来访问位段成员,就像访问普通结构体成员一样。
位段的大小:结构体中位段的总大小可能比所有位段宽度之和要大,因为编译器可能为了对齐而添加填充位。

位段是一种节省内存的有效方式,特别是在嵌入式系统或需要大量布尔标志的情况下。然而,由于它们的实现细节和可移植性问题,使用位段时应谨慎。

总结

通过对C语言结构体的详细探讨,我们了解了结构体的声明、创建和初始化、成员访问、匿名结构体的使用、结构体自引用、内存对齐、结构体传参以及结构体实现位段。这些知识可以帮助你在C语言编程中更高效地组织和管理数据,编写出更清晰、更高效的代码。掌握这些概念对于任何C语言开发者都是必不可少的。

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号