【C语言基础】:自定义类型(一)--> 结构体

文章目录

      • 一、内置类型与自定义类型
        • [1.1 内置类型(基本数据类型)](#1.1 内置类型(基本数据类型))
        • [1.2 自定义类型](#1.2 自定义类型)
      • 二、结构体
        • [2.1 结构体的声明](#2.1 结构体的声明)
        • [2.2 结构体变量的创建和初始化](#2.2 结构体变量的创建和初始化)
        • [2.3 结构体的特殊声明](#2.3 结构体的特殊声明)
        • [2.4 结构体的自引用](#2.4 结构体的自引用)
      • 三、结构体内存对齐
        • [3.1 对齐规则](#3.1 对齐规则)
        • [3.2 为什么存在内存对齐](#3.2 为什么存在内存对齐)
        • [3.3 修改默认对齐数](#3.3 修改默认对齐数)
      • 四、结构体传参
      • 五、结构体实现位段
        • [5.1 什么是位段](#5.1 什么是位段)
        • [5.2 位段的内存分配](#5.2 位段的内存分配)
        • [5.4 位段的跨平台问题](#5.4 位段的跨平台问题)
        • [5.5 位段使用的注意事项](#5.5 位段使用的注意事项)

书山有路勤为径,学海无涯苦作舟。
创作不易,宝子们!如果这篇文章对你们有帮助的话,别忘了给个免费的赞哟~

一、内置类型与自定义类型

在C语言中,有内置类型(也称为基本数据类型)和自定义类型(结构体)两种类型。

1.1 内置类型(基本数据类型)
  1. 整型(Integer types):用于表示整数值,包括:
  • int:通常表示整数,取决于编译器和系统架构,一般为4字节。
  • short int:短整数,通常为2字节。
  • long int:长整数,通常为4字节或8字节。
  • long long int:长长整数,通常为8字节。
  1. 字符型(Character type)
  • char:用于表示单个字符或小整数值,通常为1字节。
  1. 浮点型(Floating-point types):用于表示实数,包括:
  • float:单精度浮点数,通常为4字节。
  • double:双精度浮点数,通常为8字节。
  • long double:扩展精度浮点数,大小不定,通常大于8字节。
  1. 空类型(Void type)
  • void:表示无类型,常用于函数返回类型或指针类型。

这些内置类型是C语言提供的基本数据类型,用于表示基本数据,如整数、浮点数、字符等。

1.2 自定义类型

在C语言中,除了内置的基本数据类型外,还可以通过结构体(Structures)和枚举类型(Enums)来定义自定义类型。

  1. 结构体(Structures)

结构体是一种用户自定义的数据类型,用于组合不同类型的数据成员。它允许将多个不同类型的变量组合在一起,形成一个新的数据类型,以便更方便地操作相关数据。

  1. 枚举类型(Enums)

枚举类型是一种用户自定义的数据类型,用于定义一组相关的命名常量。它允许将一组有限的取值集合在一起,形成一个新的数据类型,以便更清晰地表示程序中的意图。

二、结构体

2.1 结构体的声明

在C语言中,定义结构体使用 struct 关键字,结构体的形式如下:

c 复制代码
struct 结构体名 {
    数据类型 成员名1;
    数据类型 成员名2;
    // 更多成员...
};

【示例】:描述⼀个学⽣

c 复制代码
struct Stu
{
	char name[20]; // 姓名
	int age;  // 年龄
	char set[5];  // 性别
	int id;  // 学号
};  // 分号不能丢
2.2 结构体变量的创建和初始化

初始化结构体变量:有几种方法可以初始化结构体变量:

  1. 按照结构体成员的顺序初始化:
c 复制代码
#include<stdio.h>

int main()
{
	struct Stu s = { "张三", 19, "男", "202201170248" };
	printf("%s\n", s.name);
	printf("%d\n", s.age);
	printf("%s\n", s.set);
	printf("%s\n", s.id);
	return 0;
}
  1. 按照指定的顺序初始化

前面在说操作符时我们讲过,可以通过点操作符来访问结构体成员,这里同样可以通过点操作符来给结构体成员进行初始化。

c 复制代码
int main()
{
	struct Stu s = { .age = 19, .id = "202201170248", .name = "张三", .set = "男" };
	printf("%s\n", s.name);
	printf("%d\n", s.age);
	printf("%s\n", s.set);
	printf("%s\n", s.id);
	return 0;
}
2.3 结构体的特殊声明

在声明结构的时候,可以不完全的声明。

匿名结构体类型

c 复制代码
struct
{
	int a;
	char b;
	float c;
}x;

struct
{
	int a;
	char b;
	float c;
}a[20], *p;

注意

  1. 匿名的结构体类型,如果没有对结构体类型重命名的话,基本上只能使用一次。
  2. p = &x; 这种写法是不合法的,编译器会把上面的两个声明当成完全不同的两个类型,所以是非法的。
2.4 结构体的自引用

结构体中的成员不仅可以是内置的数据类型,还可以是这个结构体本身,也就是结构体中包含指向相同类型结构体的指针或引用的情况。这种自引用的数据结构通常称为递归数据结构。

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

c 复制代码
struct Node
{
	int data;
	struct Node next;
};

注意:这种自引用是错误的 ,因为⼀个结构体中再包含⼀个同类型的结构体变量,这样结构体变量的大小就会无穷的大,是不合理的。即无法确定 sizeof(struct Node) 的大小。

正确的自引用方式:

c 复制代码
struct Node
{
	int data;
	struct Node* next;
};

在结构体自引用使用的过程中,夹杂了 typedef 对匿名结构体类型重命名,也容易出现引入问题。

c 复制代码
typedef struct
{
	int data;
	struct Node* next;
}Node;

这种也是错误的,因为Node是对前面的匿名结构体类型的重命名产生的,但是在匿名结构体内部提前使用Node类型来创建成员变量,这是不行的。

解决方案如下:定义结构体不要使用匿名结构体了

c 复制代码
typedef struct Node
{
	int data;
	struct Node* next;
}Node;

三、结构体内存对齐

【示例】:计算结构体的大小。

c 复制代码
struct S
{
	char c1;  // 占1个字节
	int i;  // 占4个字节
	char c2;  // 占1个字节

};

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

在代码中我们看到结构体中有两个char和一个int,那他的大小就是6个字节,但结果真的是这样吗?

运行之后发现是12个字节,这是为什么呢?

这说明结构体中的成员不是随便放的,这里面是有一定规则的,这就是结构体的内存对齐。

3.1 对齐规则

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

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

解析 :对照规则1,第一个成员对齐到和结构体变量起始位置偏移量为0,也就是图中为0的位置(char占1个字节),其余的成员变量对齐到对齐数整数倍的位置(int占4个字节,VS的默认值为8,4小于8,即这里的对齐数为4),也就是4的整数倍(图中序号4)开始存,第三个成员变量也一样(char占1个字节小于8,即对齐数是1)。最后结构体总大小是最大对期数(第一个和第三个对齐数都是1,第二个对齐数是4)的整数倍,也就是4的倍数,由于已经占了9个字节,所以下一个4的倍数就是12,这里总共浪费了6个字节的空间大小。

【练习1】

c 复制代码
struct S1
{
	char c1;  // 占1个字节
	char c2;  // 占1个字节
	int i;  // 占4个字节
};



【练习2】

c 复制代码
struct S2
{
	double d;
	char c;
	int i;
};



【练习3】

结构体中嵌套结构体

c 复制代码
struct S3
{
	double d;
	char c;
	int i;
};

struct S4
{
	char c1;
	struct S3 s3;
	double d;
};


解析 :这里就要对应规则4,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,S3中最大的对齐数是8,即要对齐到8的整数倍处。结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍,也是8的倍数。

3.2 为什么存在内存对齐
  1. 平台原因 (移植原因)

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

  2. 性能原因

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

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

设计结构体的时候,我们既要满足对齐,又要节省空间

  • 让占用空间小的成员尽量集中在一起
c 复制代码
struct S1
{
	char c1;
	int i;
	char c2;
};

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

S1 和 S2 类型的成员一模一样,但是 S1 和 S2 所占空间的大小有了一些区别(S1占12个字节,S2占8个字节)。

3.3 修改默认对齐数

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

c 复制代码
#include<stdio.h>
#pragma pack(1)  //设置默认对⻬数为1
struct S1
{
	char c1;
	int i;
	char c2;
};
#pragma pack()  //取消设置的对⻬数,还原为默认

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

结构体在对齐方式不合适的时候,我们可以自己更改默认对齐数。

四、结构体传参

【示例1】

c 复制代码
#include<stdio.h>
struct S
{
	int arr[1000];
	int num;
	double d;
};
void print1(struct S s)
{
	for (int i = 0; i < 5; i++)
	{
		printf("%d ", s.arr[i]);
	}
	printf("\n");
	printf("%d\n", s.num);
	printf("%lf\n", s.d);
}
int main()
{
	struct S s = { {1,2,3,4,5}, 100,3.14 };
	print1(s);
	return 0;
}


【示例2】

c 复制代码
struct S
{
	int arr[1000];
	int num;
	double d;
};
void print2(const struct S* ps)
{
	for (int i = 0; i < 5; i++)
	{
		printf("%d ", ps->arr[i]);
	}
	printf("\n");
	printf("%d\n", ps->num);
	printf("%lf\n", ps->d);
}
int main()
{
	struct S s = { {1,2,3,4,5}, 100,3.14 };
	print2(&s);
	return 0;
}


示例1示例2 中首选示例2 ,因为示例传参时是将结构体在拷贝一份给形参,本身这个结构体所占的空间就比较大,在拷贝一份太占用空间,不太合适,而示例2 传的是一个指针,可以通过这个指针直接访问这个结构体,不需要额外创建多余空间,当然,为了结构体内容不被修改,可以加一个const进行修饰。
原因

函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。

如果传递⼀个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降

结论

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

五、结构体实现位段

5.1 什么是位段

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

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

【示例】

c 复制代码
struct A
{
	int _a : 2;
	int _b : 5;
	int _c : 10;
	int _d : 30;
};

A就是一个位段类型。

5.2 位段的内存分配
  1. 位段的成员可以是 int 、unsigned int 、signed int 或者是 char 等类型。
  2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
  3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。

【示例】

c 复制代码
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;
}
5.4 位段的跨平台问题
  1. int 位段被当成有符号数还是无符号数是不确定的。
  2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题)。
  3. 位段中的成员在内存中从左向右分配,还是从右向左分配,标准尚未定义。
  4. 当⼀个结构包含两个位段,第⼆个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。

总结:

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

5.5 位段使用的注意事项

位段的几个成员共有同⼀个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位置处是没有地址的。内存中每个字节分配一个地址,一个字节内部的bit 位是没有地址的。

所以不能对位段的成员使用&操作符,这样就不能使用scanf直接给位段的成员输入值,只能是先输入放在一个变量中,然后赋值给位段的成员。

【示例】

c 复制代码
struct S
{
	int _a : 2;
	int _b : 5;
	int _c : 10;
	int _d : 30;
};

int main()
{
	struct S s = { 0 };
	// scanf("%d", &s._b);  // 这是错误的

	// 正确的示范
	int b = 0;
	scanf("%d", &b);
	s._b = b;
	return 0;
}
相关推荐
柏箱3 分钟前
PHP基本语法总结
开发语言·前端·html·php
学无止境\n8 分钟前
[C语言]指针和数组
c语言·数据结构·算法
进阶的架构师12 分钟前
互联网Java工程师面试题及答案整理(2024年最新版)
java·开发语言
易辰君14 分钟前
python爬虫 - 深入requests模块
开发语言·爬虫·python
木子020421 分钟前
java高并发场景RabbitMQ的使用
java·开发语言
无夜_21 分钟前
Prototype(原型模式)
开发语言·c++
看到请催我学习22 分钟前
内存缓存和硬盘缓存
开发语言·前端·javascript·vue.js·缓存·ecmascript
笃励1 小时前
Java面试题二
java·开发语言·python
jyan_敬言1 小时前
【Linux】Linux命令与操作详解(一)文件管理(文件命令)、用户与用户组管理(创建、删除用户/组)
linux·运维·服务器·c语言·开发语言·汇编·c++
FL16238631291 小时前
[C#]C# winform部署yolov11-pose姿态估计onnx模型
开发语言·yolo·c#