• 【c语言】自定义类型-结构体



    结构体:是一个自定义的类型,成员可以有一个或者多个,类型可以不同。

    结构体的声明与使用

    结构体的声明与初始化

    struct  结构体名
    {
    	成员变量;
    	....
    };
    
    //结构体初始化
    struct Stu
    {
    	char name[20];
    	int age;
    	char sex[10]};
    int main()
    {
    	struct Stu s1={"lisi",18,"man"};
    	return 0;
    }
    

    结构体还存在一中特殊声明:匿名结构体

    struct 
    {
    	成员变量;
    	...
    };
    

    这个结构体在声明时不需要写入结构体名,但是在使用时,只能使用一次
    比如下面这段代码:

    struct 
    {
    	int a;
    	int b;
    	float c;
    }x;
    struct 
    {
    	int a;
    	int b;
    	float c;
    }*p;
    int main()
    {
    	p=&x;
    	return 0;
    }
    

    这段代码在编译时会报警告,因为编译器会把这两个声明当成两个不同的类型。
    在这里插入图片描述
    结构体变量的声明与初始化:
    直接上代码:

    //在声明结构体的同时声明结构体变量并初始化
    struct stu
    {
    	int a;
    	int b;
    	float c;
    }x={1,2,1.0};
    int main()
    {
    	
    	return 0;
    }
    ///
    struct stu
    {
    	int a;
    	int b;
    	float c;
    };
    int main()
    {
    	struct 	stu x={1,2,1.0};//声明结构体变量并初始化
    	return 0;
    }
    

    对结构体数组成员初始化。

    struct Stu
    {
    	char name[20];
    	int age;
    	char sex[10]};
    int main()
    {
    	struct Stu s1={"lisi",18,"man"};
    	printf("%s %d %s",s1.name,s1.age,s1.sex);//输出结构体的值
    	return 0;
    }
    

    结构体的自引用

    若结构体中包含一个结构体本身的成员(自引用),是否可以?
    比如定义一个链表节点:

    struct Node
    {
    	int data;
    	struct Node next;
    };
    

    思考一下,这段代码正确吗?
    答案当然是“错误”的。因为以这样的方式嵌套定义结构体,那么这结构体的大小将是无穷大的。
    正确的自引用应该是下面这样的:

    struct Node
    {
    	int data;
    	struct Node*next;
    };
    

    这在后面将会学到,这其实是建立了一个链表的节点,定义一个结构体指针,让结构体指针指向结构体的数据域(data),再让被指向的结构体的指针成员(next)指向下一个结构体的数据域(data)。

    typedef关键字
    typedef关键字使用来对类型进行重命名的。
    比如:

    typedef int i;int 重命名为i 
    typedef char c;char重命名为c。
    

    或许你会有这么一个想法“int、char一个就3个字母,一个就4个字母,我为什么还有重命名?重命名它俩还要多打一个typedef,不是更麻烦吗?”

    确实,在这中情况下,确实没必要对数据类型重命名,但是在c语言中除了基本数据类型,还有自定义的数据类型。就如我们这篇文章的所讲的结构体类型,当我们定义了一个结构体类型后,要声明一个结构体变量就需要将struct 结构体名 都打出来,但是如果我们使用typedef关键字对它重命名,就能方便很多了。

    typedef struct Student
    {
    	int age;
    	char name[20];
    }S;
    int mian()
    {
    	S s1={18,"lisi"};
    	return 0;	
    }
    

    但是要注意一点,重命名的类型需要重命名完后才能使用,否则编译器过不了编译,比如:

    typedef struct Student 
    {
    	int  age;
    	S *next;
    }S;
    

    这里,提前使用了结构体struct Student类型重命名后的名字S,是不可以的,因为编译器是从上往下编译代码的,当编译到S*next时,S还没有被编译到,所以对于编译器来说,S类型是不存在的。正确的形式应该像下面这样子。

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

    结构体的内存对齐

    到这里,我们已经掌握了结构体的基本使用,那么我们再接着思考“定义的结构体的大小是多少?int是4字节,char是1字节,那么结构体呢?”
    在讨论结构体大小之前,我们先了解一下结构体的对齐规则

    对齐规则

    结构体对齐规则:是指结构体中每个成员变量的存放地址要满足一定的条件,以提高内存的访问效率。
    结构体对齐规则:

    • 结构体的第一个成员对齐到结构体变量起始位置偏移量为0的地址处
    • 结构体其余成员对齐到对齐数的整数倍的地址处
    • 对齐数=编译器默认的对齐数与该成员变量自身大小两者之间的较小值,vs中默认对齐数为8,Linux中gcc没有默认对齐数
    • 结构体总大小为最大对齐(所以成员变量都有一个对齐数,选最大的那一个)的整数倍
    • 如果结构体2嵌套了一个结构体1则这个嵌套的结构体1的对齐数=自身成员变量的最大对齐数,结构体2的大小就是最大对齐数的整数倍

    比如:

    struct Stu
    {
    	int age;
    	char a;
    	int b;
    }S;
    

    则这个结构体的大小为计算过程:
    在这里插入图片描述
    在思考的过程中,你应该已经发现了:内存对齐存在内存浪费。是的,内存对齐就是一种用空间换时间的做法。

    为什么存在内存对齐

    硬件平台限制:并不是所有硬件都能够任意读取任意地址处的数据的,某些硬件只能访问某些特定地址下的数据,如果不进行内存对齐,则cpu将不会读取到特定地址处的数据。
    性能问题:cpu在读取内存时,是按照一个固定的粒度来读取的(比如一次四字节、8字节),如果结构体不按照一定的规则进行内存对齐,那么cpu在读取一份数据的时候,可能需要读取两次、三次甚至更多次才能把一份数据读完,而进行内存对齐后,cpu则只需要读取一次就行了。

    修改默认对齐数

    #pragma,这个预处理指令可以修改默认对齐数 。

    #pragma(1);//将默认对齐数修改为1
    struct Stu
    {
    	int age;
    	char [name];
    }s;
    int main()
    {
    	printf("%zd",sizeof(struct Stu));//输出12//若把#pragma(1)注释掉,默认对齐数恢复到8,次数输出6
    	return 0;
    }
    

    当结构体的对齐数不合适的时候,我们就可以通过#pragma来修改默认对齐数。

    结构体的传参

    struct S
    {
        int age;
        char name[20];
        float i;
    };
    
    struct S s = {18,"lisi",1.0};
    
    
    void print1(struct S s)
    {
        printf("%d %s %lf\n",s.age,s.name,s.i);
    }
    
    void print2(struct S *s)
    {
        printf("%d %s %lf\n",s->age,s->name,s->i);
    }
    
    int main()
    {
        print1(s);
        print2(&s);
        return 0;
    }
    

    结构体传参也分为两类:

    • 传值调用
    • 传址调用

    在传参时,推荐使用传址调用,因为函数传参的时候,参数是需要压栈的,会有时间与空间上的系统开销。
    传值调用,如果传入的参数过大,这个开销也就会变大。
    但如果传入地址,地址大小就两种情况:4字节/8字节,所有推荐使用传址调用。

    结构体实现位段

    什么是位段

    什么是位段:是一种数据结构,它允许将数据以位(bit)的形式紧凑地存储,并允许程序员对此结构的位进行操作
    位段的声明与结构体类似:

    struct A
     {
     int _a:2;
     int _b:5;
     int _c:10;
     int _d:30;
     };
    

    这段代码中,struct A占8个字节:
    int占4个字节,所以第一次开辟空间大小为4字节,4字节=32bit,但是struct A里,四个成员共占47bit,所以4字节的空间无法存储struct A类型的数据,,需要在开辟一个Int类型的空间,所以该位段大小为8字节。

    位段的内存分配

    1. 位段的成员可以是Int家族,或者char等

    2. 位段空间的分配按照需要以4个字节或者1个字节的方式来开辟的

    3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使⽤位段

    struct A
    {
    int _a:2;
    int _b:5;
    int _c:10;
    int _d:30;
    };
    

    在这里插入图片描述

    给定空间后,空间内存按照从左到右还是从右到左使用呢?这个不确定,标准没有定义

    当剩下的空间不足以分配位段成员时,是浪费这部分空间还是继续使用?没有定义

    我们假设空间是从右往左使用,空间是被浪费的

    此时最后一个字节就完全倍浪费掉了。

    位段的跨平台问题

    1. int 位段被当成有符号数还是⽆符号数是不确定的。
    2. 位段中最⼤位的数⽬不能确定。(16位机器最⼤16,32位机器最⼤32,写成27,在16位机器会
      出问题。
    3. 位段中的成员在内存中从左向右分配,还是从右向左分配,标准尚未定义。
    4. 当⼀个结构包含两个位段,第⼆个位段成员⽐较⼤,⽆法容纳于第⼀个位段剩余的位时,是舍弃
      剩余的位还是利⽤,这是不确定的。

    总结:
    跟结构相⽐,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在

    位段使用的注意事项

    位段的有些成员的起始位置并不是字节的起始位置,就好比上图中的‘a’,‘b’,我们知道,每个字节都有自己的地址,1字节=8bit,这8bit是没有地址的,所以在使用位段时,我们不能对位段成员使用&(取地址)操作符,也不能进行scanf操作,如果需要给位段成员赋值,需要先给一个变量赋值,在通过这个变量赋值给位段成员

  • 相关阅读:
    springboot基于Android的校园综合服务App平台的设计毕业设计源码181040
    谷歌最新开源大模型 Gemma,采用与创建 Gemini 模型相同的研究和技术,专为负责任的人工智能开发而设计。
    2、IoC 浅识
    Java 1.8引入StringJoiner,用与字符串拼接
    Python 学习之路
    大数据-玩转数据-Flink定时器
    Vue虚拟DOM理解-深入且易懂
    【Linux 系统】gcc/g++使用零星整理
    java-net-php-python-jsp网络工程师在线测试系统计算机毕业设计程序
    python中scipy中uniform分布怎么用
  • 原文地址:https://blog.csdn.net/yzqy_/article/details/139445737