深入C语言:探索结构体的奥秘

一、什么是结构体

首先我们为什么要用到结构体?

我们都已经学了很多int char …等类型还学到了同类型元素构成的数组,以及取上述类型的指针,在一些小应用可以灵活使用,然而,在我们实际应用中,每一种变量进行一次声明,再结合起来显然是不太实际的。

类如一位学生的信息管理,他可能有,姓名(char),学号(int)成绩(float)等多种数据。如果把这些数据分别单独定义,就会特别松散、复杂,难以规划,因此我们需要把一些相关的变量组合起来,以一个整体形式对对象进行描述,这就是结构体的好处。

官方来说结构体就是一些值的集合,这些值称为成员变量。结构体的每个成员可以是不同类型的变量。说到集合,数组也是集合,但是不同的是数组只能是相同类型元素的集合。 

二、结构体的使用

2.1 结构体的声明

struct tag
{
  member1;
  member2;
} variable-list;
  • struct是结构体关键字
  • tag是结构体的标签名,是自定义的,如book,student等。
  • struct tag就是结构体类型
  • member1 , member2 是结构体成员,是标准的变量定义,比如 int i; 或者 float f;,也可以定义数组char s[20]。
  • variable-list 结构变量,定义在结构的末尾,最后一个分号之前,您可以指定一个或多个结构变量,也可以省略。

2.2 结构体的基础结构和类型

2.2.1 普通结构体

(一)先定义结构体类型,再定义结构体变量

 struct   student //结构体类型 或 结构体名
{       
 int num;
 char  name[20];     //结构体成员
 char sex;
 int age; 
 float score;
 char addr[30];
 };
 struct student stu1,stu2;       //结构体变量

(二)定义结构体类型的同时定义结构体变量

 struct   data   // 结构体类型 或结构体名
 {      
  int day int month;   //结构体成员
  int year
 }time1,time2;   //结构体变量

2.2.2 嵌套结构体

结构体和函数一样可以嵌套使用,也就是说在一个结构体中可以使用另外一个结构体,也可以包含指向自己结构体类型的指针,而通常这种指针的应用是为了实现一些更高级的数据结构如链表等。

struct student
{
	int age;//年龄
	char sex[8];//性别
	int weight;//体重
	char tele[20];//电话
};
struct people
{
	int num;//序号
	struct student s;//学生
};
struct list
{
	int num;//序号
	struct list* next;//指向自己的结构体指针
};

但是结构体中不能包含一个同类型的结构体变量,因为这样结构体大小无法确定

struct node
{
	int num;
	struct node s;
	//错误定义
};

2.2.3 匿名结构体

匿名结构体是不定义结构体名称,而直接定义其成员的一种方式。这种结构体只能使用一次。并且两个匿名结构体的成员如果都相同的话,这两个匿名结构体也是不同的。

struct//匿名结构体
{
	int num;
	char name[20];
	//.....
};
struct
{
	int a;
	char b;
	float c;
}x;
struct
{
	int a;
	char b;
	float c;
}*p;
p = &x;//两种结构体不同无法赋值

2.3 结构体的初始化

(一)定义时初始化

#include <stdio.h>
#include <stdlib.h>
struct books    // 结构体类型
{
	char title[50];
	char author[50];    //结构体成员
	char subject[100];
	int book_id;
}book={"C 语言","xingaosheng","编程语言",12345};  //结构体变量的初始化
int main()
{
	printf("title : %s\nauthor: %s\nsubject: %s\nbook_id: %d\n", book.title, book.author, book.subject, book.book_id);
	return 0;
}

(二)先定义再进行初始化

typedef struct student
{
 
	int age;//年龄
	char sex[8];//性别
	int weight;//体重
	char tele[20];//电话
}stu;
struct people
{
	int num;//序号
	struct student s;//学生
};
int main()
{
	struct student s = { 20,"nan",50,"1233455" };//创建变量并初始化
	//struct student s;
	//s= { 20,"nan",50,"1233455" };错误
	stu t = { 18,"nan",45,"123444" };
	struct people p = { 1,{20,"nan",50,"1233455"} };
	//嵌套结构体的初始化
	return 0;
}

2.4 结构体的成员访问

2.4.1 直接访问

结构体成员的直接访问是通过点操作符(.)访问的。点操作符接受两个操作数。如下所⽰:

struct Point
{
	int x;
	int y;
};
int main()
{
	struct Point p = { 1,2 };
	printf("x: %d y: %d\n", p.x, p.y);
	return 0;
}

输出结果:

x:1  y:2  

2.4.2 间接访问

除了通过(.)操作符直接访问,我们也可以通过结构体地址,利用(->)操作符间接访问。

#include <stdio.h>
struct Point
{
	int x;
	int y;
};
int main()
{
	struct Point p = { 3, 4 };
	struct Point* ptr = &p;//结构体指针
	ptr->x = 1;
	ptr->y = 2;
	printf("x = %d y = %d\n", ptr->x, ptr->y);
	return 0;
}

输出结果: 

x:1  y:2  

需要注意的是,结构体是一种自定义的数据类型,是创建变量的模板,不占用内存空间结构体变量才包含了实实在在的数据,需要内存空间来存储

三、结构体数组

结构体数组:是指数组中的每一个元素都式结构体, 结构体数组常被用来表示一个拥有相同数据结构的群体。

struct stu
{
    char name[20];  //姓名
    int num;  //学号
    int age;  //年龄
    char group;  //所在小组 
    float score;  //成绩
}class[5];
//表示一个班有5个人

结构体数组在定义的时候也可以初始化

struct stu
{
    char name[20];  //姓名
    int num;  //学号
    int age;  //年龄
    char group;  //所在小组 
    float score;  //成绩
}class[5] = {
    {"Li ping", 5, 18, 'C', 145.0},
    {"Zhang ping", 4, 19, 'A', 130.5},
    {"He fang", 1, 18, 'A', 148.5},
    {"Cheng ling", 2, 17, 'F', 139.0},
    {"Wang ming", 3, 17, 'B', 144.5}
};

使用方法:

#include <stdio.h>
#include <stdlib.h>
struct stu
{
	char name[20];
	int num;
	int age;
	char group;
	float score;
} ban[5] = {
	{"xing",5,18,'c',145.0},{"ao",4,19,'a',130.5},
	{"sheng",1,18,'a',148.5},{"pei",2,17,'f',139.0},
	{"yuan",3,17,'b',144.5}
};      // 表示一个班有5个人
int main()
{
	int i, n = 0;
	float sum = 0;
	for (i = 0; i < 5; i++) {
		sum += ban[i].score;
		if (ban[i].score < 140) n++;
	}
	printf("sum=%.2f\naverage=%.2f\nn=%d\n", sum, sum / 5, n);
	return 0;
}

输出结果:

sum=707.50

average=141.50

n=2

🟥四、结构体指针(重点)

4.1 指向结构体变量的指针

4.1.1 结构体指针的定义

可以定义指向结构体的指针,方式与定义指向奇特类型变量的指针类似

定义方式:struct 结构体名*结构体指针名

struct books*struct_pointer 

 其中books是结构体名,struct_pointer为结构体指针名

定义之后可以在上述定义的指针变量中存储结构变量的地址

struct_pointer = &Book1;

为了使用指向该结构的指针访问结构的成员,必须使用->运算符

struct_pointer->title;

代码示例如下:

struct stu   // 结构体类型 或 结构体名
{
    char *name;  //姓名
    int num;  //学号
    int age;  //年龄
    char group;  //所在小组
    float score;  //成绩
} stu1 = { "Tom", 12, 18, 'A', 136.5 };
//结构体指针
struct stu *pstu = &stu1;

也可以定义结构体的同时定义结构体指针:

struct stu{
    char *name;  //姓名
    int num;  //学号
    int age;  //年龄
    char group;  //所在小组
    float score;  //成绩
} stu1 = { "Tom", 12, 18, 'A', 136.5 }, *pstu = &stu1;

注意:

  • ▶ 结构体变量名和数组名不同,数组名在表达式中会被转换为数组指针,而结构体变量名不会,无论在任何表达式中它表示的都是整个集合本身,要想取得结构体变量的地址,必须在前面加&符号,所以给p赋值只能写成。
  • ▶ 结构体和结构体变量是两个不同的概念:结构体是一种数据类型,是一种创建变量的模板,例如上面的stu,编译器不会为它分配内存空间,就像 int、float、char 这些关键字本身不占用内存一样;结构体变量(例如stu1)才包含实实在在的数据,才需要内存来存储。不可能去取一个结构体名的地址,也不能将它赋值给其他变量。

4.1.2 结构体指针的成员访问 

通过结构体指针可以获取结构体成员,一般形式为:

(*pointer).memberMane                  //pointer为结构体指针名
  pointer->memberName             // 或者 
  • 第一种写法中, .  的优先级高于  * ,(*pointer)两边的括号不能少。如果去掉括号写成*pointer.memberName,那么就等效于*(pointer.memberName),这样意义就不对了。
  • 第二种写法中,-> 是一个新的运算符,习惯称它为“箭头”,有了它,可以通过结构体指针直接取得结构体成员,
  •      这也是 -> 在C语言中的唯一用途。

4.1.3 结构体指针的使用

前面两种写法是等效的,我们通常采用第二种写法,这样更加直观。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct stu
{
	char name[20];
	int num;
	int age;
	char group;
	float score;
}stu1 = { "Tom",12,18,'A',136.5 }, * pstu = &stu1;
int main()
{
	// 读取结构体成员的值
	printf("%s的学号是%d,年龄是%d,在%c组,今年的成绩是%.1f!\n", (*pstu).name, (*pstu).num, (*pstu).age, (*pstu).group, (*pstu).score);
	printf("%s的学号是%d,年龄是%d,在%c组,今年的成绩是%.1f!\n", pstu->name, pstu->num, pstu->age, pstu->group, pstu->score);
	return 0;
}

运行结果如下:

4.2 指向结构体数组的指针

在我们想要用指针访问结构体数组的第n个数据时可以用:

struct Student
{	
	char cName[20];
 	int number;
 	char csex;  
}student1;     //结构体变量
struct Student stu1[5];   //结构体数组
struct Student*p;        // 结构体指针
p=stu[n];
(++p).number//是指向了结构体数组下一个元素的地址

结构体指针与结构体数组的联合使用:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
struct stu    //结构体类型 或结构体名
{
	char name[20];
	int num;
	int age;   //结构体成员
	char group;
	float score;
}stus[]{           //结构体数组
	{"Zhou ping", 5, 18, 'C', 145.0},
	{"Zhang ping", 4, 19, 'A', 130.5},
	{"Liu fang", 1, 18, 'A', 148.5},
	{"Cheng ling", 2, 17, 'F', 139.0},
	{"Wang ming", 3, 17, 'B', 144.5}
}, * ps;   //结构体指针
int main()
{
	//求数组长度 : sieof(结构体变量)/sizeof(结构体类型名)
	int len = sizeof(stus) / sizeof(struct stu);
	printf("Name\t\tNum\tAge\tGroup\tScore\t\n");
	for (ps = stus; ps < stus + len; ps++)
	{
		printf("%s\t%d\t%d\t%c\t%.1f\n", ps->name, ps->num, ps->age, ps->group, ps->score);
	}
	return 0;
}

输出结果如下:

4.3 结构体成员是指针类型

代码示例如下:

struct Student
{
 	char* Name;//这样防止名字长短不一造成空间的浪费
 	int number;
 	char csex;  
}student1;

注意在使用时可以很好地防止内存被浪费,但是注意在引用时一定要给指针变量分配地址,如果你不分配地址,结果可能是对的,但是Name会被分配到任意的一的地址,结构体不为字符串分配任何内存存储空间具有不确定性,这样就存在潜在的危险

代码改进如下:

struct Student
{
 	char* Name;
 	int number;
 	char csex;  
}stu,*stu;
 
stu.name=(char*)malloc(sizeof(char));//内存初始化

如果我们定义了结构体指针变量,他没有指向一个结构体,那么这个结构体指针也是要分配内存初始化的,他所对应的指针类型结构体成员也要相应初始化分配内存

struct Student
{
 	char* Name;
 	int number;
	char csex;  
}stu,*stu;
stu = (struct student*)malloc(sizeof(struct student));./*结构体指针初始化*/
  stu->name = (char*)malloc(sizeof(char));/*结构体指针的成员指针同样需要初始化*/  

五、结构体的内存对齐

在熟悉了结构体的基本应用之后,下面我们要深入讨论的就是结构体大小,如何计算结构体的大小,就需要知道它在内存中是如何储存的。

而结构体在内存中存在结构体对齐的现象。

我们先参考以下代码:

	struct S1
	{
		char c1;
		int i;
		char c2;
	};
	printf("%d\n", sizeof(struct S1));

	struct S2
	{
		char c1;
		char c2;
		int i;
	};
	printf("%d\n", sizeof(struct S2));

	struct S3
	{
		double d;
		char c;
		int i;
	};
	printf("%d\n", sizeof(struct S3));

	struct S4
	{
		char c1;
		struct S3 s3;
		double d;
	};
	printf("%d\n", sizeof(struct S4));

输出结果是:

12

8

16

32

如果直接计算结构体成员的所占的内存之和显然比这小,这是为什么呢?

C语言分配结构体内存时,遵循的是内存对齐规则,那什么是内存对齐规则呢?

内存对齐规则:

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

为什么会存在内存对齐呢?相信大部分人都会有这个疑问,其实主要有两个原因:

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

上面的代代码图示如下:

六、结构体位段

6.1 什么是位段

有些数据在存储时并不需要占用一个完整的字节,只需要占用一个或几个二进制位即可,所以C 语言有一种特别的数据结构名为位,允许我们按位对成员进行定义,指定其占用的位数,单位为比特位(bit)。一般是用来节约内存,与结构体有两个不同:

位段的实现和结构体类似,只不过位段的成员的类型只能是
unsigned int 或者int类型,char类型的也可以。
每个成员名后面要加上:和数字

代码示例如下:

struct stu
{
	int a : 4;
	int b : 2;
};

后面的数字表示bite位。位段不存在对齐。
位段不具有跨平台性:

  1. 位段中没有规定在内存使用的过程中,是从左使用还是从右使用。
  2. 不能满足下一个成员使用的空间是舍弃还是保留的问题没有规定。
  3. int位段中无符号还是有符号的问题没有规定

6.2 结构体实现位段的内存分配

那么位段的分配到底是怎么样的呢?

当一个结构体包含两个位段,第二个位段比较大,无法容纳于第一个位段剩余的位时, 是舍弃剩余的位还是利用呢?

struct S
{
	char a : 3;
	char b : 4;
	char c : 5;
	char d : 4;
};
struct S s = { 0 };
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
  1. 假设位段在一个字节内部是从高地址到低地址分配。
  2. 假设当一个结构体包含两个位段,第二个位段比较大,无法容纳于第一个位段剩余的位时, 是舍弃。

七、结构体传参

我们知道函数传参分为两种,一种是直接传参:直接传变量;一种是间接传参:通过传变量地址间接访问。

struct S1
{
	int p;
	int num;
};
 
//结构体传参
void print1(struct S1 s)
{
	printf("%d\n", s.num);
}
 
//结构体地址传参
void print2(struct S1* ps)
{
	printf("%d\n", ps->num);
}
 
int main()
{
        struct S1 s = { 1,2 };
	print1(s);		//传整个结构体
	print2(&s);		//传地址
	return 0;
}

但在结构体传参的时候,最好选择传址调用,有两个好处:
1.可以减少对空间的浪费
2.可以对里面的数据进行修改

🟥八、typedef关键字与结构体、结构体指针(重点)

8.1 使用typedef定义结构体

typedef用来定义新的数据类型,通常typedef与结构体的定义配合使用。使用typedef的目的使结构体的表达更加简练(所以说typedef语句并不是必须使用的。)

  1. struct 是用来定义新的数据类型——结构体
  2. typedef是给数据类型取别名。

定义一个名字为TreeNode的结构体类型(现在并没有定义结构体变量,并不占用内存空间):

struct TreeNode   // 结构体类型
{
        int Element;
        struct TreeNode* LeftChild;   //结构体成员
        struct TreeNode* RightChild;
};

为结构体起一个别名Node,这时Node就等价于struct TreeNode

typedef struct TreeNode Node;

将结构体的定义和typedef语句可以连在一起写:

typedef struct TreeNode   //结构体类型
{
        int Element;              //结构体成员
        struct TreeNode* LeftChild;
        struct TreeNode* RightChild;   
}Node;     // Node 是 struct TreeNode 的别名

 注意 :不要与“定义结构体类型的同时定义结构体类型变量”混淆:
使用typedef关键字定义结构体类型 定义结构体类型的同时定义结构体类型变量 

typedef struct student
{
        int age;
        int height;
}std;
std std1, std2;
//std相当于struct student	
 
struct student
{
        int age;
        int height;
}std1,std2;
struct student std3, std4;
//定义了student数据类型的结构体和std1、std2、std3、std4结构体变量

8.2 使用typedef定义结构体指针

使用typedef关键字用一个单词Node代替struct TreeNode,并定义指向该结构体类型的指针PtrToTreeNode:

struct TreeNode
{
        int Element;
        struct TreeNode* LeftChild;
        struct TreeNode* RightChild;
};
typedef struct TreeNode Node;   //用Node代替struct TreeNode
 
Node *PtrToTreeNode;            //定义指针

也可以简化如下所示:

typedef struct TreeNode
{
        int Element;
        struct TreeNode* LeftChild;
        struct TreeNode* RightChild;
}Node;                          //定义结构体并用Node代替struct TreeNode
Node *PtrToTreeNode;            //定义指针

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值