结构体简介

1.结构体变量的创建和初始化

1.1 结构体的概念

结构体是一些值的集合,这些值称为成员变量。结构体中的每个成员可以是不同类型的变量

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

例如:描述一个学生

struct student 
{
	char name[20]; //姓名
	int age; //年龄
	char sex[5]; // 性别
	char ID[20]; // 学号
};

1.2 结构体变量的创建和初始化 

例1 

struct point
{
	int x;
	int y;
}p1; // 声明类型 同时定义变量p1
struct point p2; //定义结构体变量p2

struct point p3 = { 1, 2 }; // 初始化:定义变量的同时赋初值

例2

struct student
{
	char name[20];
	int age;
};
struct student s = { "llisi", 20 }; //初始化

例3

#include <stdlib.h>

struct point
{
	int x;
	int y;
}p1;
struct point p2;

struct node
{
	int data;
	struct point p;
	struct node* next;
}n1 = { 20, {5, 6}, NULL }; //结构体嵌套初始化
struct node n2 = { 10,{4, 5}, NULL }; //结构体嵌套初始化

例4 :不按照成员顺序的初始化(C99)

struct student
{
	char name[20];
	int age;
};
struct student n1 = { .age = 20, .name = "zhangfang" };
// 或者 : 
struct student
{
	char name[20];
	int age;
}n1 = { .age = 20, .name = "zhangfang" };

1.3 完全声明与不完全声明

不完全声明 :声明时,省略结构体名称

struct
{
	int a;
	char b;
	float c;
}x;
struct
{
	int a;
	char b;
	float c;
}a[20], *p;
// 此时 x , a[20], *p, 是全局变量

注意:

编译器会把这两个声明当成完全不同的两个类型(哪怕结构类型的成员一样),所以是非法的

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

1.4 结构体的自引用 (结构体中包含该结构本身的成员)

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

这样写对吗 ??😜

struct node
{
	int data;
	struct node next;
};

直接写成 struct node next 会造成无限套娃 ,此时结构体变量的大小就是无穷大,不合理

正确写法:

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

特例: typedef对匿名结构体类型的重命名

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

这样写 对吗??

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

解决方案:定义结构体不使用匿名结构体

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

2.结构体成员访问操作符:"  .  "和  "  ->  "

形式如下:

结构体变量.成员变量名

结构体指针.成员变量名

例:

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>

struct stu
{
	char name[20]; //姓名
	int age;
};

void print(struct stu s)
{
	printf("%s %d\n", s.name, s.age);
}

void set_stu(struct stu* p)
{
	strcpy(p->name, "lisi");
	p->age = 19;
}

int main()
{
	struct stu s = { "zhangsan", 20 };
	print(s);
	set_stu(&s);
	print(s);
	return 0;
}

3. 结构体传参

#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(const struct s* p)
{
	printf("%d\n", p->num);
}

int main()
{
	print1(s); //传结构体
	print2(&s); //传地址
	return 0;
}

两种方式,首选 print2 函数  == >>  结构体传参的时候,要传结构体的地址

因为: 函数传参的时候,参数需要压栈,造成时间和空间上的系统开销。

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

4. 结构体内存对齐

4.1 对齐规则:

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

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

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

                          VS 默认的值是8; Linux没有对齐数,对齐数就是成员自身大小

(3) 结构体总大小是最大对齐数的整数倍

            最大对齐数:结构体中每个成员变量都有一个对齐数,所有对齐数中最大的

(4) 如果嵌套结构体,嵌套的结构体成员对齐到自己成员的最大对齐数的整数倍,结构体的整体大小就是所有最大对齐数的整数倍

例:

#include <stdio.h>

int main()
{
	//   1
	struct s1
	{
		char c1;
		int i;
		char c2;
	};
	printf("%d\n", sizeof(struct s1));   // 12

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

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

	//    4
	struct s4
	{
		char c1;
		struct s3 s3;
		double d;
	};
	printf("%d\n", sizeof(struct s4));  // 32
}

内存存储示意图:

>>   可以使用 offset宏 (头文件  < stddef.h> ) 计算结构体成员相较于起始位置的偏移量

printf("%d", offset(struct s4, c1);
4.2  为什么存在内存对齐 ?

most 参考资料:

(1) 平台原因 (移植原因) :

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

(2) 性能原因:

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

总体而言,结构体的内存对齐是拿空间换取时间的做法

倘若既要满足对齐,又要节省空间,做法:让占位空间小的成员尽量集中在一起

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

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

s1 和 s2 类型的成员一样,但是s1 所占空间小于 s2所占空间 

4.3 修改默认对齐数

#pragma 这个预处理命令,可以改变编译器的默认对齐数

#include <stdio.h>

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

int main()
{
	printf("%d", sizeof(struct s)); // 6
	return 0;
}

5. 结构体实现位段

5.1 什么是位段

位段与结构体相似,只有两处不同:

(1) 位段的成员必须是 int ,unsigned int  或者 signed int  ,在C99中,位段成员的类型也可以是其他成员

(2) 位段的成员名后面有一个冒号和一个数字

例:

struct A
{
	int a : 2;
	int b : 5;
	int c : 10;
	int d : 30;
};

其中,冒号后面的数字代表所占多少bit位,(注意:数字 不能超过数据类型所占字节的限制)

位段A 所占内存:

printf("%d", sizeof(struct A)); // 8
5.2 位段的内存分配

(1) 位段的成员可以是 int , unsigned int, signed int , 或者是char 等类型(C99之后)

(2) 位段的空间按照4个空间( int )或者1给空间( char ) 的方式开辟

(3) 位段涉及很多不确定性的因素,位段是不跨平台的,注重可移植性的程序要避免使用位段

例:

struct s
{
	char a : 3;
	char b : 4;
	char c : 5;
	char d : 4;
};

struct s m = { 0 };
m.a = 10;
m.b = 12;
m.c = 3;
m.d = 4;
// 空间是如何开辟的呢?

 

 5.3 位段的跨平台问题

(1) int 位段作为有符号数还是无符号数是不确定的

(2) 位段中最大位的数目不能确定(如:16位机器最大16,32位机器最大32,如果写成27,在16位机器上会出问题)

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

(4)  当一个结构中包含两个位段 且 第二个位段成员比较大,无法容纳第一个位段剩余的位时,时是舍弃剩余的位还是利用,这是不确定的

总而言之,与结构相比,位段可以达到同样的效果,并且可以很好的节省空间,但是存在跨平台的风险

5.4 位段的应用 ----- IP数据报

5.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 = 2;
	scanf("%d", &b);
	sa.b = b;
	return 0;
}

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值