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

文章详细介绍了C语言中的结构体,包括结构体的基本概念、声明、内存对齐规则以及如何通过typedef简化类型声明。文中还探讨了结构体传参的两种方式,并强调了结构体指针作为参数传递在处理大型结构体时的性能优势。
摘要由CSDN通过智能技术生成

C语言自定义类型—结构体

0. 目录

1. 结构体

1.1 结构体的基本概念

​ 结构体:是一组值的集合,与数组不同的是,结构体可以包含不同的数据类型(包括基本数据类型、数组、指针类型甚至结构体类型),我们通常把定义在结构体当中的数据称为成员变量,可以说结构体是C语言数据类型中的集大成者。

1.2 结构体的声明

声明格式:

struct tagName {
  member-list;  
}variable-list;

​ 上述格式中,我们使用关键字struct声明这是一个结构体类型,tagName作为该结构体的名字,member-list定义成员列表,即一组成员变量,例如int age;char name[20];,variable-list表示使用该结构体类型创建的变量

例如我们描述一个人的结构体类型

struct Person {
  	char name[20];
    int age;
}p1, p2;

注意:

  1. struct Person一起使用表示该结构体的类型,所以在主函数中创建变量格式为struct Person p

  2. 结构体声明时可以同时创建多个变量,如上述代码中的p1、p2,多个变量之间使用逗号分隔,且该变量为全局变量

  3. 我们可以使用typedef关键字简化后续声明类型的格式

    typedef struct Person {
      	char name[20];
        int age;
    }Person;
    
    int main() {
        Person p1; // 声明一个Person结构体类型变量
        return 0;
    }
    

1.3 结构体特殊声明

结构体可以省略tagName进行声明,该结构体称之为匿名结构体

例如:

struct {
  	int age;
}s;

易错点:

struct {
  	int age;  
}s1;

struct {
  	int age;  
}*ps;

int main() {
    ps = &s1; // 合法吗?
    return 0;
}

上述代码中,编译器一般会报警告因为这两种匿名结构体并不是相同类型,所以ps = &s1会导致类型不兼容的错误。

结论:

  1. 尽管声明的两个匿名结构体成员变量相同,这两个匿名结构体也属于不同的数据类型

  2. 由于匿名结构体缺少tagName标签名,无法定义完整类型, 因此匿名结构体只有在声明之时可以创建变量,在主函数等其他地方不可以再创建变量

1.4 结构体嵌套自身

易错点1:

struct B1 {
    int b;
    struct B1 b; // 可行么?
};

struct B2 {
  	int b;
    struct B2* b; // 可行么?
};

​ 分析:上述代码中,结构体B1与结构体B2当中成员变量都有自身结构体类型的成员,但是区别在于结构体B1中声明的是结构体B1类型的变量b,而结构体B2中声明的是指向结构体B2类型的指针变量b,事实上结构体B1无法通过编译,因为sizeof(B1)计算结构体大小时无法确定,而指针变量在32位平台上有唯一确定大小为4字节,因此结构体B2的声明方式可以通过编译,事实上C语言实现数据结构链表就是使用了第二种结构体定义方式。

易错点2:

下面两种声明方式哪种可行?

// 声明方式1
typedef struct Node {
    int data;
    Node* next;
}Node;
// 声明方式2
typedef struct Node {
    int data;
    struct Node* next;
}Node;

​ 分析:上述代码中,方案2可行但是方案1不行,在结构体声明时引用自身不得省略struct关键字

1.5 结构体变量的定义与初始化

结构体变量的定义方式有两种:

  1. 在声明时就进行定义

    struct Student {
        int age;
        char name[20];
    }stu1, stu2;
    

    该定义方式中创建的变量为全局变量

  2. 在主函数等其他地方进行定义

    struct Student {
      	int age;
        char name[20];
    };
    
    int main() {
        struct Student s1 = {20, "jack"};
        return 0;
    }
    

    该定义方式中创建的变量为局部变量

结构体变量的初始化方式也有两种

  1. 初始化方式1

    struct Student {
      	int age;
        char name[20];
    }s1 = {19, "rice"};
    

    该初始化方式顺序必须与声明顺序一致,不可以打乱顺序!

  2. 初始化方式2

    struct Student {
      	int age;
        char name[20];
    }s1 = {.name = "rice", .age = 19};
    

    该初始化方式顺序可以打乱

1.6 结构体内存对齐

1.6.1 结构体内存对齐规则

结构体的内存对齐是笔试的一大考点,下面将介绍结构体内存对齐的规则

  1. 结构体的第一个成员变量对齐到距离结构体变量地址偏移量为0处

  2. 从结构体的第二个成员变量开始,每个成员变量对齐到某个数字(对齐数)的整数倍

    对齐数:成员变量自身所占大小与平台默认对齐数中较小值即为对齐数

    默认对齐数

    • 在Linux gcc编译器环境中没有默认对齐数,对齐数按照成员变量自身所占大小来进行计算
    • 在VS编译器中默认对齐数为8字节
  3. 结构体总大小为最大对齐数(所有成员变量的对齐数的最大值)的整数倍

  4. 当结构体嵌套了结构体,内部结构体对齐到自身最大对齐数的整数倍,外部结构体的整体大小为所有成员变量(包括内部结构体)的最大对齐数的整数倍

1.6.2 结构体内存对齐相关练习题

练习题1:

//练习1
struct S1
{
    char c1;
    int i;
    char c2;
};

printf("%d\n", sizeof(struct S1)); // 12

题目分析:本题中声明结构体S1,根据规则1,第一个成员变量char c1对齐到变量地址偏移量为0处,自身占用内存空间为0号,根据规则2,第二个成员变量int i自身占用4字节、默认对齐数为8,则对齐数为4,则应当对齐到4的整数倍,占用内存空间为4号, 5号, 6号, 7号,第三个成员变量char c2自身占用1字节,默认对齐数为8,则对齐数为1,对齐到1的整数倍为8,内存占用空间为8号,根据规则3,结构体整体大小为所有成员变量最大对齐数的整数倍,最大对齐数为max(1, 4, 1) = 4,因此整个结构体占用的内存大小为12字节。

在这里插入图片描述

练习题2

//练习2
struct S2
{
    char c1;
    char c2;
    int i;
};
printf("%d\n", sizeof(struct S2)); // 8

题目分析:本题声明了结构体S2,第一个成员变量为char c1,根据规则1,内存对齐到偏移为0地址处,占用内存空间为0号,第二个成员变量为char c2,根据规则2,自身所占大小为1字节,默认对齐数为8字节,因此对齐数为1,对齐到1的整数倍即1处,因此占用内存空间为1号,第三个成员变量为int i,根据规则2,自身所占大小为4字节,默认对齐数为8,因此对齐数为4,对齐到4的整数倍即偏移量为4处,占用内存空间为4号、5号、6号、7号,最后根据规则3,整个结构体的大小为最大对齐数的整数倍,而最大对齐数为max(1, 1, 4) = 4,即结构体最终大小为8字节

在这里插入图片描述

练习题3

//练习3
struct S3
{
    double d;
    char c;
    int i;
};
printf("%d\n", sizeof(struct S3)); // 16

练习题4

//练习4-结构体嵌套问题
struct S4
{
    char c1;
    struct S3 s3;
    double d;
};
printf("%d\n", sizeof(struct S4)); // 32

注:练习题3与练习题4就留作扩展练习由读者自行完成了

1.6.3 内存对齐的原因

内存对齐的原因在大部分资料中可以总结为如下两点主要原因:

  1. 平台原因

    并不是所有的硬件平台都可以实现访问任意地址上的任意数据的,由于硬件实现电路的原因,使用内存对齐的方式更加便于进行访村操作。

  2. 性能问题

    数据结构(例如说栈)应尽可能实现在自然边界上的对齐,在32位机上,机器字长一般为32位,即一次访存读写4字节的数据,因此为减少不必要的访存次数,应当实现内存对齐。

总结:内存对齐是通过牺牲空间来换取时间的做法

1.6.4 修改默认对齐数

之前已经介绍了默认对齐数的概念,但是我们在C语言中可以通过一定方式根据实际情况灵活的修改默认对齐数

预处理指令格式:#pragma pack(int num)其中num为设置的默认对齐数

例如:

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

#pragma pack(1)//设置默认对齐数为1
struct S2
{
    char c1;
    int i;
    char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认

int main()
{
  //输出的结果是什么?
  printf("%d\n", sizeof(struct S1)); // 12
  printf("%d\n", sizeof(struct S2)); // 6
  return 0;
}

结论:

  • C语言提供灵活的方式设置默认对齐数
  • 预处理指令#pragma pack(int num)用于设置默认对齐数为num
  • 预处理指针#pragma pack()用于取消先前设置的默认对齐数

1.7 结构体传参

结构题传递参数的方式有两种

  1. 结构体变量作为参数传递
  2. 结构体指针变量作为参数传递

示例:

#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;
}

问:针对上述情景,两种传参方式哪个更好?

数传递

示例:

#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;
}

问:针对上述情景,两种传参方式哪个更好?

答:采用结构体指针传参更好,因为函数传参的时候,参数需要压栈,如果一个结构体对象过于庞大,比如此案例中结构体中含有开辟1000个int类型的数组成员变量,系统开销比较大,因此采用结构体指针传参性能更加好。

  • 9
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值