C语言结构体:自定义数据类型的魅力

        前言:C语⾔已经提供了内置类型,如:char、short、int、long、float、double等,但是只有这些内置类型还是不够的。

假设我想描述学⽣,描述⼀本书,这时单⼀的内置类型是不⾏的。描述⼀个学⽣需要名字、年龄、学号、⾝⾼、体重等;描述⼀本书需要作者、出版社、定价等。C语⾔为了解决这个问题,增加了结构体这种⾃定义的数据类型,让程序员可以⾃⼰创造适合的类型。

一、初识结构体

1.结构体的声明

结构体的语法模型如下:

        

struct tag //结构体的名称
{
    
 member-list;   //成员列表
} variable-list ;  //变量列表

        

温馨提示:结尾的分号不要忘记

形如:描述一个学生结构体

struct stu
{
	char name[20];   //名字
	int age; //年龄
	int high; //身高
	double weight;//体重
	char id[16]; //学号
}s4,s5,s6;   //  (全局)结构体变量

1.定义 stu  为结构体的名称

        

2. 通过下列数据类型,描述结构体成员变量:

        

    char name[20];   //名字
    int age; //年龄
    int high; //身高
    double weight;//体重
    char id[16]; //学号

        

3. s4  s5  s6    为结构体变量,此时的结构体变量为全局变量

2.结构体变量的定义

      声明了一个结构体后,如何进行对结构体变量的定义呢?有以下两种方式可以进行定义。  

        

方法一:声明结构体的时候进行定义结构体变量

        

struct Point
{
  int x;
  int y;
}p1;

此时的 p1 就为结构体Point 的变量,注意此时的p1为全局变量

        

方法二:直接定义结构体变量

        

struct Point
{
  int x;
  int y;
};

struct Point p1;   //此时为全局变量

int main()
{
    struct Point p2;  //此时为局部变量
    return 0;
}

通过 struct + 结构体名  + 变量名  的方法直接定义

        

3.结构体变量的初始化

        如何对结构体变量进行初始化呢? 其实类似于数组的初始化,但也有一定的区别

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

int main()
{
    //按照顺序对结构体进行初始化
	struct stu s1 = {"wang",20,180,70,"001"}; 

    //自定义对结构成员进行初始化
	struct stu s2 = {.age=21,.high=186,.name="li",.id="002",.weight=75};   
     	
	return 0;
}

 对结构体初始化可以分为两种形式:

        

①按照结构体中的成员变量,通过 {} 依次进行赋值,其中要用 的形式分隔各个变量 

        

②通过 · + 成员变量名 的方式,可以对结构体变量以自定义的形式进行赋值操作

        

若结构中还含有结构体如何进行赋值呢? 

struct S
{
	char name[20];
	int age;
};
    
struct B
{
	struct S s;
	int* p;
};
    
int main()
{	
	struct B b1 = { .p=NULL ,.s = {"zhang",20} };
    
	struct B b2 = { {"zhang",20} , NULL };
	return 0;
}

对于这种嵌套结构体而言,对结构体中的成员结构体,通过 {} 单独进行赋值

4.结构体的访问

        针对于结构体的访问,也有如下两种方式:

        

 方法一:通过 · 直接进行访问

#include<stdio.h>


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


int main()
{

	struct stu s1 = {"wang",20,180,70,"001"}; 

	printf("%s %d %d %.1f %s", s1.name, s1.age, s1.high, s1.weight, s1.id);

	return 0;
}

        

方法二:利用 结构体指针 进行间接访问

        

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

        

5.结构体传参

        

 ①通过以 传值 的形式进行传递参数

void print1(struct S tmp)
{
	for (int i = 0; i < 5; i++)
	{
		printf("%d ", tmp.arr[i]);
	}
	printf("\n%d ", tmp.n);
	printf("\n%lf ", tmp.d);
}

int main()
{

	struct S s ={ {1,2,3,4}, 100, 9.9};
	print1(s);
	return 0;
}

        

②通过以 传址 的形式进行传递参数        

void print2(const struct S* ps)
{
	for (int i = 0; i < 5; i++)
	{
		printf("%d ", ps->arr[i]);
	}
	printf("\n%d ", ps->n);
	printf("\n%lf ", ps->d);
}

int main()
{

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

	print2(&s);

	return 0;
}

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

        

分析:

              函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。如果传递⼀个结构体对象的时候,结构体过⼤,参数压栈的的系统开销⽐较⼤,所以会导致性能的下降。

        

所以,结构体传参的时候,要传结构体的地址

二、深入结构体

1.结构体的特殊声明

        在 C 语言中,匿名结构体(Anonymous Struct)是指没有显式命名的结构体类型,通常嵌套在其他结构体或联合体中使用。

        匿名结构体的定义形式为:在结构体关键字struct后直接定义成员,不指定结构体名称

#include <stdio.h>

// 外层结构体包含匿名结构体
struct Student 
{
    int id;
    int age;
    // 匿名结构体(无名称)
    struct 
    {
        char first_name[20];
        char last_name[20];
    };  // 注意:匿名结构体后直接跟分号,无需名称
    
};

int main()
{
    struct Student s = {
        .id = 1001,
        .first_name = "Zhang",  // 直接访问匿名结构体的成员
        .last_name = "San",
        .age = 20
    };

    // 直接通过外层结构体变量访问匿名结构体的成员
    printf("Student: %s %s, ID: %d, Age: %d\n",  s.first_name, s.last_name, s.id, s.age);
    return 0;
}

温馨提示:

  • 匿名结构体必须嵌套在其他结构体或联合体中(不能单独定义匿名结构体变量)。
  • 匿名结构体的成员可以被外层结构体 / 联合体 “直接访问”,无需通过结构体名称中转。

        

2.结构体的自引用

        思考在结构中包含⼀个类型为该结构本⾝的成员是否可以呢? 答案是可以的。

比如,定义一个链表的结点:

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

但这段代码是否正确呢? 如果正确,sizeof(struct Node) 的大小是多少。

        

仔细分析,其实是不⾏的。

因为⼀个结构体中再包含⼀个同类型的结构体变量,这样结构体变量的⼤⼩就会⽆穷的⼤,是不合理的。

        

正确写法,通过结构体指针,存储下一个节点的信息:

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

        

其中为了简化结构体变量的定义,可以通过typedef的形式,给结构体重新命名。

typedef struct Node
{
	int data;
	struct Node* next;  //结构体自引用
}Node;

对这段代码的理解是:定义了一个结构体类型struct Node,重命名为Node。

        

这样的写法也等价于:

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

3.结构体内存对⻬

我们已经掌握了结构体的基本使⽤了,现在我们深⼊讨论⼀个问题:计算结构体的⼤⼩。

在计算结构体大小前,首先得了解规则:

        

    1. 结构体的第⼀个成员对⻬到和结构体变量起始位置偏移量为0的地址处

        
    2. 其他成员变量要对⻬到某个数字(对⻬数)的整数倍的地址处。

        
        对⻬数 = 编译器默认的⼀个对⻬数 与 该成员变量⼤⼩的较⼩值。
        - VS 中默认的值为 8
        - Linux中 gcc 没有默认对⻬数,对⻬数就是成员⾃⾝的⼤⼩

        
   3. 结构体总⼤⼩为最⼤对⻬数(结构体中每个成员变量都有⼀个对⻬数,所有对⻬数中最⼤的)的整数倍。

        
   4. 如果嵌套了结构体的情况,嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,结构体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体中成员的对⻬数)的整数倍。

重点知识:结构体变量起始位置偏移量的理解

       如下图所示,若开辟了一块内存,结构体变量从某个起始地址开始存放,距离该地址差值即为偏移量

        

        

通过了解上述规则我们可以练习一下试题:(温馨提示以下:代码在vs2022编译器下,编译的结果)

        

练习一

struct S1
{
  char c1;//第一个成员 存放在偏移量为0的地址处  
  int i;  // 本身大小为4  vs默认大小8   对齐数为4
  char c2;// 本身大小为1  vs默认大小8   对齐数为1
};

int main()
{
    printf("%d\n", sizeof(struct S1));
    return 0;
}

解析:

        ①char c1 变量为第一个成员,对齐到起始位置偏移量为0的地址处

        

        ②int i 变量本身大小为4个字节 与 vs默认对齐数8  的最小值为4 所以对齐数为4 ,再根据规则存放到对齐数的整数倍偏移的             位置处。

        

        ③char c2 变量本身大小为1个字节 与 vs默认对齐数8 的最小值为1 所以对齐数为1,再根据规则存放到对齐数的整数倍偏的             位置处。

        

  如下图分布所示:

        

所以大小为9个字节吗 ?根据打印结果来看,显然不是。

        

因为结构体的总大小为成员变量中对齐数的整数倍,成员变量中最大的对齐数为4,故而总大小为12个字节。

       

        

练习二:含结构体

struct S1
{
	char c1;
	int i;
	char c2;
};


struct S2
{
  char c1;    //对齐到偏移量为0的地址处
  struct S1 s1;  //结构体S1中成员最大的对齐数为4  vs默认8 对齐数为4
  double d;  //  本身大小为8  vs默认8  对齐数为8
};


int main()
{
    printf("%d\n", sizeof(struct S2));
    return 0;
}

如图所示分配内存:

        

最终结果为:为结构体的总大小为成员变量中对齐数的整数倍,成员变量中最大的对齐数为8,故而总大小为24个字节。

        

4.修改默认对齐数

        在vs环境下。可以根据需要修改默认对齐数,以实现合适的内存对齐结构

#include <stdio.h>

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

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

此时该结构体的内存大小为6个字节。

        

5.为什么出现内存对齐

1. 平台原因 (移植原因):
        不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

        

2. 性能原因:
        数据结构(尤其是栈)应该尽可能地在⾃然边界上对⻬。原因在于,为了访问未对⻬的内存,处理器需要作两次内存访问;⽽对⻬的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对⻬成8的倍数,那么就可以⽤⼀个内存操作来读或者写值了。否则,我们可能需要执⾏两次内存访问,因为对象可能被分放在两个8字节内存块中。

        

总而言之:结构体的内存对⻬是拿空间来换取时间的做法

三、利用结构体实现位端

        

1.什么是位端

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

        

        ①位段的成员必须是 int、unsigned int 或signed int ,在C99中位段成员的类型也可以选择其他类型。

        

        ②位段的成员名后边有⼀个冒号和⼀个数字,其中冒号后面跟着的数字表示,改位段所需开辟的bit位

比如:

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

        

2.位端的内存分配
        

对于位端的内存,有如下三点警示:

        

1. 位段的成员可以是 int、 unsigned int、 signed int 或者是 char 等类型

        
2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的⽅式来开辟的。

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

        

事实上对于位端的内存开辟在不同的编译器上是有差异的,就拿vs2022这个环境下为例讲解一下

        

struct S
{
   char a:3;  //3个bit位
   char b:4;  //4个bit位
   char c:5;  //5个bit位
   char d:4;  //4个bit位
};
 
int main()
{
   struct S s = {0};
   s.a = 10;
   s.b = 12;
   s.c = 3;
   s.d = 4;
   return 0;
}

针对于位段的内存开辟,在vs的环境下有如下规则:

        

①:按照4个字节(int 型变量) 或则 1个字节 (char 型变量)来开辟空间,然后从右向左填充bit位大小的空间。

        

②:若填充所需的bit位空间不足,则需要再另外开辟空间。

        

所以上述代码的空间开辟如下图所示:

        

总共需要3个字节大小。

        

根据上述赋值操作,如下图所示:

        

        

3.位段的跨平台问题

1. int 位段被当成有符号数还是⽆符号数是不确定的。

        
2. 位段中最⼤位的数⽬不能确定。(16位机器最⼤16,32位机器最⼤32,写成27,在16位机器会出问题。

        
3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。

        
4. 当⼀个结构包含两个位段,第⼆个位段成员⽐较⼤,⽆法容纳于第⼀个位段剩余的位时,是舍弃剩余的位还是利⽤,这是不确定的。

        

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

        

4.位段的应用

        下图是⽹络协议中,IP数据报的格式,我们可以看到其中很多的属性只需要⼏个bit位就能描述,这⾥使⽤位段,能够实现想要的效果,也节省了空间,这样⽹络传输的数据报⼤⼩也会较⼩⼀些,对⽹络的畅通是有帮助的。

        

5.位段的注意事项

                

        位段的⼏个成员共有同⼀个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位置处是没有地址的。内存中每个字节分配⼀个地址,⼀个字节内部的bit位是没有地址的。所以不能对位段的成员使⽤&操作符,这样就不能使⽤scanf直接给位段的成员输⼊值,只能是先输⼊放在⼀个变量中,然后赋值给位段的成员。

struct A
{
  int _a : 2;
  int _b : 5;
  int _c : 10;
  int _d : 30;
};
    
int main()
{
  struct A sa = {0};
  //这是错误的
  scanf("%d", &sa._b);

  //正确的⽰范
  int b = 0;
  scanf("%d", &b);
  sa._b = b;
  return 0;
}

既然看到了这里了,不妨给个一键三连吧!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值