C语言结构体的相关知识详解(理论+代码+画图分析=秒懂)

本文详细介绍了C语言中结构体的使用,包括声明、成员访问、内存对齐规则、位段的概念及其应用,以及结构体在内存管理和性能优化中的重要性。此外,还讨论了结构体传参的最佳实践和位段的跨平台问题。
摘要由CSDN通过智能技术生成

前言:C语言虽然已经提供了很多内置类型:比如:int、char、short、long、float、double等等,但是这些类型并不能满足一些特殊场景的需求,比如,我想描述一个人的特征,需要名字、年龄、性别、身高、体重等;描述一部手机,需要型号、内存、颜色、价格、配置、品牌等;描述一瓶水,需要品牌、价格、容量等。为此C语言专门提供结构体这种自定义的数据,可以让程序员自己创造所需的数据类型。

1.结构体

结构是一些值的集合,这些值称为成员变量。结构中的每个成员可以是不同的数据类型,如:数组、指针、甚至是其他的结构体。

 1.1 结构体声明

struct test
{
	member_list;
}variable-list;

描述一个人:

struct Stduent
{
	char name[20];//姓名
	int age;//年龄
	char sex[4];//性别
	int height;//身高
	int weight;//体重
};//注意带上分号

1.2 结构体变量如何定义和初始化

写法有三种:

//代码1:变量的定义
struct Test
{
	char a;
	char b;
}t1;//声明类型的同时定义变量t1
struct Test t2;//定义结构体变量t2

//代码2:对变量初始化
struct Test t3 = { 'h','o' };


struct Phone //类型声明
{
	char brand[20];//品牌
	int capacity;//内存容量
};

struct Phone p1 = { "华为",256};//常见初始化
struct Phone p2 = { .capacity = 256,.brand = "小米" };//指定顺序的初始化


//代码3
struct Node
{
	int data;
	struct Test t;
	struct Node* next;//结构体体嵌套初始化	
}n = { 10,{'h','y'},NULL};

struct Node nn = { 100,{'z','g'},NULL };//结构体嵌套初始化

2.结构成员访问操作符

2.1 (.)- 结构体成员的直接访问

对结构体成员直接访问使用点(.)操作符。点操作符接受两个操作数。例如:

#include <stdio.h>
struct Water
{
	char brand[20];//品牌
	int capacity;//容量
	int price;//价格
};

int main()
{
	struct Water w = { "娃哈哈",596,1.5 };
	printf("%s %dml %d元\n", w.brand, w.capacity, w.price);
	return 0;
}

【运行结果】

使用方式:结构体变量.成员名

2.2 结构体成员的间接访问

有时候我们会使用指向结构体指针进行操作。这时就要使用这种箭头(->)操作符。如:

#include <stdio.h>
struct Test
{
	int a;
	int b;
};

int main()
{
	struct Test t = { 2,6 };
	struct Test* pt = &t;
	pt->a = 3;
	pt->b = 9;
	printf("a = %d, b = %d\n", pt->a, pt->b);
	return 0;
}

【运行结果】

 使用方式:结构体指针->成员名

综合使用如下:

#include <stdio.h>
#include <string.h>
struct Person
{
	char name[20];//姓名
	int age;//年龄
};

void PrintPs(struct Person p)
{
	printf("%s %d\n", p.name, p.age);
}

void set_ps(struct Person* ps)
{
	strcpy(ps->name, "王五");
	ps->age = 24;
}

int main()
{
	struct Person p = { "赵六",18 };
	PrintPs(p);
	set_ps(&p);
	PrintPs(p);
	return 0;
}

【运行结果】

2.3 结构体的特殊声明

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

如:

//匿名结构体类型
struct
{
	int n;
	char c;
	float f;
};

struct
{
	int n;
	char c;
	float f;
}n[10],*s;

可以看到上面两个结构体在在声明的时候,并没有给它类型(省略了结构体标签(tag)).

留意:如果在此基础上,这样写代码是非法的!!

s = &m;//非法

注意:

编译器会把将上述两个声明当成完全不同的两个类型,所以是非法的。

匿名的结构体,如果没有对结构体类型重命名的化,基本上只能使用一次。

2.4 结构体的自引用

结构体中可以包含一个类型为该结构本身的成员吗?

例如,定义一个链表的节点:

struct Node
{
	int date;
	struct Node n;
};

上面代码是正确吗?若是,那么 sizeof(struct Node)会是多少呢?

可以分析得出,这种写法是不行的,原因是一个结构体中再包含一个同类型的结构体变量,这样结构体变量的大小就会无穷的大 -- 不合理!

正确的自引用写法,如下:

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

当使用的typedef匿名结构体类型重命名和结构体自引用遇上时,也容易出现一些问题。比如:

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

注意:这种写法是不可行的,原因是Node是对前面的匿名结构体类型重命名产生的。所以在匿名结构体中提前使用Node类型来创建成员变量,是不可以的。

匿名的结构体类型是不能实现这种结构体自引用的效果的!!!

如何解决这个问题呢? --- 定义结构体时不要使用匿名结构体定义。

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

3.结构体内存对齐

我们前面已经学习了结构体的基本使用。现在学习一个新的知识点 -- 结构体内存对齐

学习计算结构体的大小。

3.1 对齐规则

掌握结构体的对齐规则:

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

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

        对齐数 = 编译器默认的一个对齐数 与  该成员变量大小的较小值

- VS 中默认的对齐数是 8 ;

-Linux 中 gcc没有默认对齐数,对齐数就是成员自身的大小。

3.结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大的数)的整数倍。

4.如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员的最大对齐数的整数倍地址处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。

分析下面结构体的大小:

小练1:

#include <stdio.h>
struct T1
{
	char c1;
	int i;
	char c2;
};

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

详解:

【运行结果】:

 小练2:

#include <stdio.h>
struct T2
{
	char c1;
	char c2;
	int i;
};

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

详解:

【运行结果】

小练3:

#include <stdio.h>
struct T3
{
	double d;
	char c;
	int i;
};

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

详解:

【运行结果】

 小练4:

#include <stdio.h>
struct T3
{
	double d;
	char c;
	int i;
};

//练习4 - 结构体嵌套问题
struct T4
{
	char c1;
	struct T3 t3;
	double d;
};

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

 详解:

【运行结果】

3.2  内存对齐存在的原因

主要原因 - 性能原因

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

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

如果在设计结构体的时,既要内存对齐,又想要节省空间就要:让占用空间小的成员尽量放在一起。

如上述出现过的代码:

struct T1        //占用12个字节
{
	char c1;
	int i;
	char c2;
};

struct T2        //占用8个字节
{
	char c1;
	char c2;
	int i;
};

可以看到T1 与 T2成员是一样的,但是所占用的内存大小发生了改变。

3.3 如何修改默认对齐数

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

#include <stdio.h>
#pragma pack(1) //设置默认对齐数是1

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

#pragma pack()//取消设置的对齐,还原为默认对齐数

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

详解:

【运行结果】

结论: 结构体在对齐方式不合适的时候,可以字节修改默认的对齐数。

3.4 结构体传参

#include <stdio.h>
struct T
{
	int arr[10];
	char c;
	float f;
};

//结构体传参
void Print1(struct T pt)
{
	int i = 0;
	for (i = 0; i < 6; i++)
	{
		printf("%d ", pt.arr[i]);
	}
	printf("%c ", pt.c);
	printf("%lf\n", pt.f);
}

//结构体地址传参
void Print2(struct T* pt)
{
	int i = 0;
	for (i = 0; i < 6; i++)
	{
		printf("%d ", pt->arr[i]);
	}
	printf("%c ", pt->c);
	printf("%lf\n", pt->f);
}

int main()
{
	struct T t = { {1,2,3,4,5,6},'h',2.98 };
	Print1(t);//传结构体方式
	Print2(&t);//传地址方式
	return 0;
}

【运行结果】

 上述两种传参方式更好? --- 答案是结构体传地址方式。

原因在于:

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

如果传递一个结构体对象时,结构体过大,参数 压栈的系统开销会比较大,这样会导致程序的性能下降。如果指传递地址过去,地址用指针接收,指针的大小在64位平台下只占8个字节(32位平台下,占4个字节),开销不算太大。

 结论:结构体传参时,首选结构体的地址。

4.结构体实现位段

结构体是位段存在的基础。

4.1 位段是什么

位段的作用和结构体的对齐方式作用是类似的。并且位段的声明和结构也是类似的。有两个不同点:

1.位段的成员必须是intunsigned intsigned int,在C99中位段成员类型可以选择其他类型。

2.位段的成员名后面跟着一个冒号和一个数字。

 比如:

#include <stdio.h>
//1.申请到的一块内存中,从左向右使用,还是从右向左使用,是不确定
//2.剩余的空间,不足下一个成员使用的时候,是被浪费掉,还是继续使用是未知的
struct T
{
	int x : 2;//后面的数值是以bit(位)位单位的
	int y : 6;
	int z : 10;
	int w : 30;
};

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

【运行结果】

 在这里T是一个位段类型。

位段T的内存大小是如何计算的?

4.2 位段的内存分配

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

2.位段的空间上是按照需要以4个字节(int)或1个字节(char)的方式分配的。

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

分析:

#include <stdio.h>
struct T
{
	char a : 3;
	char b : 4;
	char c : 5;
	char d : 4;
};

int main()
{
	struct T t = { 0 };
	t.a = 10;
	t.b = 12;
	t.c = 3;
	t.d = 4;
	return 0;
}

分析:

 4.3 位段跨平台问题

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

2.位段中最大位的数目是不确定的。(16位机器最大16位,32位机器最大32位,在16机器下写26会出问题)。

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

4.当一个结构体包含两个位段,第二个位段成员比较大,无法容纳第一个位段剩余位时,剩余位是被利用起来,还是丢弃,这还不确定。

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

4.4 位段使用时需要注意的点

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

所以我们不能对位段的成员进行取地址(&),这样就不能使用scanf直接给位段的成员输入值,只可以放在一个变量中,然后赋值给位段的成员。

如:

#include <stdio.h>
struct T
{
	int _a : 2;
	int _b : 5;
	int _c : 10;
	int _d : 30;
};
int main()
{
	struct T t = { 0 };
	scanf("%d", &t._a);//错误写法

	//正确写法
	int a = 0;
	scnaf("%d", &a);
	t._a = a;

	return 0;
}

看到这里,结构体的相关知识已经学习完毕,恭喜你又掌握了一个知识点。

  • 21
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值