C语言总结项目和入门——结构体与共用体


六、C语言入门——结构体

  实际上,我们处理的东西往往是复杂的,通常需要很多的参数去描述,比如一个人,最起码姓名性别年龄这就3个需要描述的内容了,一个变量能不能同时描述这3个属性,或许可以,比方说用字符数组,不过处理很麻烦,因为名字是字符没有问题,但年龄怎么也应该是个int类型的吧,等过了一年,年龄++,字符串改起来可是比一个数简单加1麻烦的多。
  鉴于对实际事物描述的复杂性,C语言引入结构体的概念(面向对象编程则使用更为全面的类结构描述)。
  结构体就是一堆类型的集合,用来充分且合适的描述一个对象的特征,比如姓名,我们要一个字符数组,年龄要一个int,性别我们可以要一个bool值(C语言也是用int,因为bool不常见),现在我们自定义一个集合(类型),包含这三个属性,并给这个集合取名People,这就是用结构体描述人的基本操作。
  结构体是用户自定义的数据类型(这句很重要),通过结构体的定义,将一些本身没啥联系的单元(变量所在的地方)联系在一起,组成集合。

  • 结构体的定义

      struct People
      {
      	int age;
      	char name[50];
      	char sex;
      };
    
      struct 结构体类型名
      {
      	成员列表;
      };
    

  我们定义了一个人的类型,(结尾的分号!!)。

  有了人的类型,我们怎么用呢?
  这里我打算用类和对象的关系来说明一下结构体和其变量的关系,因为我觉得确实这两种可以有相似之处。
  我们上面定义了一个人,但这个人是虚假的,因为你不知道它叫啥,性别和年龄,因为我们只是定义,所以这个人是虚拟的,但每个人都有我们刚定义的几种属性,这意味着我们可以用这个人去描述其他所有人(不考虑什么同名同姓之类的),所以说我们刚定义的又有通用性,我把它叫框架(类、结构体),其他所有的人(对象、结构体变量)都可以用这个框架搭建出来,它们彼此之间只是赋的值不同,而结构上没有任何区别(人和人都是骨头搭建的)。换句话说,有了这个框架,我们想创建多少个人就能搞多少个人,无非就是赋不同的值,而骨架是固定。从结构到一个个具体的人这个过程,叫实例化,表示我们从一个理论的人(只有类型)为骨架创建了一个实际的人(这里只是给成员属性赋值,对于类的实例化还可以有行为属性。)
  比较抽象?我们现在来创建一个具体的人:
  struct People a;
  前面我们说过,结构体是一种用户自定义的类型,People就是我们对这种自定义的类型起的类型名,前面struct是C语言强调这个类型是结构体类型(好像现在有的版本前面不用struct了,不过还是建议写上)。struct People就是C语言对用户自定义类型的一种识别格式。上面的例子用前面的话来说就是:我们定义了一个People类型的结构体变量,叫a。
  int a;——我们定义了一个int类型的整型变量,叫a
  两者没有区别,无非是自定义类型和本来类型的区别。
加粗样式现在我们有了一个人了,是a,struct People a就是实例化,表示创建了一个可以实际操作的,真真实实的人,但是现在a的名字是什么,并不是a,a只是变量名,代表一个人的变量,这个人叫什么我们还没起呢,这里引出一个重点变量是真实存在的(在内存空间中有它的专属地方),所以说它是可以实际操作的(可以对内存空间中的值改变和读写),类型是虚假的,就像前面说的,你不能对int赋值,因为int是类型,同样的你也不能对People赋值,因为People是类型,是自定义的类型,这就是结构体和结构体变量的本质区别,结构体是类型,是虚假的不能赋值,变量是确切存在的,可以操作的。(类和对象的关系也是这样的)
  现在我们给a这个变量代表的人起名字,a是个结构体变量,它里面有结构体所包含的骨架(前面说的),这里就设计到怎么访问里面的属性。
  我们访问地址有&,*运算符,访问结构体属性也有自己的。

点运算符(.)
  想访问什么属性,点出来就好了
  访问a的名字:a.name;
  注意,结构体有的才能点出来,才能访问,你定义人这个骨架的时候都没写的东西,你怎么能指望你按照这个骨架创建出来的变量有别的东西呢
  我们给a的名字赋值:a.name = “ann”;
  a.name表示访问a中名字为name的变量,并给这个变量赋值ann,好像没啥问题,但我们name的类型是什么:数组,a.name就是char数组,数组怎么能在定义完还能直接这样赋值的呢,所以上的语句有问题的。
  但我们给年龄什么的赋值,就不会有这样由于char数组自己导致的赋值不行的问题发生了。
a.age = 15;
a.sex = ‘男’;
  上面由于我们是先定义后赋值,导致数组类型的赋值很是麻烦,我们实际上可以在定义结构体变量a的时候就给a赋值。
  struct People a = {15,“ann”,‘男’};
  注意,用大括号包含所有属性的初始值,每个属性初值之间用逗号分割,结构体里面写的顺序是什么,就按这个顺序赋值。
  我们甚至可以直接在声明结构体的时候就定义结构体变量
struct People
{
  int age;
  char name[50];
  char sex;
}a;
  跟在屁股后面就行了,a是结构体变量名。
  当然也可以赋初值

struct People
{
int age;
char name[50];
char sex;
}a= {15,"ann",'男'};

struct 结构体类型名
{
成员列表;
}变量名列表;

  重要的是,由于结构体也是变量类型,结构体里面的属性是变量,所以我们可以在结构体的属性里面定义别的结构体类型的变量,简单来说,结构体可以嵌套。

struct Favourite
{
int number;
int month;
};

struct People
{
int age;
char name[50];
char sex;
struct Favourite fav;
}a= {15,"ann",'男',{13,6}};

  这个例子中,我们添加了对人的一个描述——最喜欢的东西,我们把它也搞成结构体,类型为Favourite,由于C语言对于结构体变量定义的特殊形式,于是这个变量的定义就变成了struct Favourite fav;对变量a赋初值,前面很好理解,最后一个由于是结构体,所以初值也要用大括号括起来,就形成了大括号嵌套的形式(和结构体嵌套是同样的)
  怎么访问结构体的结构体?一级一级点出来就OK了。a.fav.month——访问a的month属性。
  只要抓住点运算符一级一级的访问属性,就能访问任意嵌套的结构体
  点运算符的优先级是最高的,所以a.age++就是我们期望的先找到age,再对age++。
  结构体变量作为变量,当然也有地址,其属性成员也有各自的地址。
  注意,结构体只能点名道姓的赋值和访问,不能说希望直接输入各种类型的值对其整体赋值。
  如:

struct People
{
int age;
char name[50];
char sex;
}a;
scanf("%d,%s,%c",&a);

  希望通过这种语句直接给a整体赋值是不可以的。
  结构体使可以相互赋值的,两个结构体a,b同类型,我想把a的值全给b,b=a就OK,数组是不可以这样的。

  • 结构体数组

  顾名思义,就是其元素都是结构体变量的数组。
  有了这个,我们就可以创建很多人了。
  定义和数组的定义一样:struct People a[5];
类型 数组名[大小](结构体就是自定义的类型

  数组中的每一个元素都是一个People类型的结构体,访问的话,当然根据层层访问的方法,首先这是一个数组,所以要先访问数组,当我们访问到里面的元素时,发现这是个结构体变量,再使用结构体变量的访问方式。
  a[1].age——访问第一个结构体变量的age属性。

  我们也可以在定义结构体的时候定义其数组

struct People
{
int age;
char sex;
}a[2] = {5,‘男’,6,‘女‘};

  并采用这种一一对应的初始化方式。

  • 结构体指针

  结构体变量是变量,当然有指向结构体的指针了。
  定义和指针的定义一样:struct People *p
  我们让它指向刚才定义的a[0]——p = &a[0]
  现在我们可以通过p访问a[0]的属性了,对于指针的问题,首先搞清楚,指针指向了什么,p指向a[0]这个结构体,所以*p当然就是a[0]了,我们访问其中的age属性,再.age就OK,综上——(*p).age。注意这里的括号是必须的,因为.运算优先级最高比*还高,不加括号就先成了p.age,这什么都不是,因为p不是结构体,怎么能用点运算符呢。
  对于结构体指针,我们还有更为方便的使用方法,我们想访问age属性,可以使用p->age这种形式,表示直接访问p指向的结构体的age属性(->箭头、指向的,是不是很形象),它的结果当然是age了,所以(*p).age和p->age是等价的,当然a[0].age也是等价的,这三种访问方法就看你怎么使用了。
  普通的指针还能直接指向对应的数组,对于结构体指针和结构体数组当然也是如此,结构体数组名代表这个数组的首地址,我们直接p = a;就把指针和数组挂钩了,p++呢,根据前面的元素是指向下一个元素,所以是指向数组中下一个结构体。
  P++的时候,p怎么知道要跳过多少个内存空间才能指向下一个元素呢?我们上一章讲过了,我们在定义p的时候就告诉它它指向的内存是多大的空间了。对于结构体也是如此。至于结构体在内存中是怎么存放的,这个还不是简单的说有多少属性就开辟多少空间,比如说我们有一个int类型,一个char[50],一个double类型的属性的结构体,我们在定义这样的结构体变量的时候,是在内存中开辟了4+50+8=62的内存空间吗?看起来好像确实是这样的,但实际上,我们用sizeof看一看。

struct People
{
	int age;
	char name[50];
	double k;
}a;

int main()
{
	printf("%d",sizeof(a));
	return 0;
}

在这里插入图片描述
  平白无故多了2个,为什么,这里涉及一个内存对齐的东西,对于初学来说用处不大,知不知道也不影响咱们编程,在对应的阶段干对应的事才是合适的。
  简单来说,内存对齐就是为了好看,加一些空的内存空间凑个整,64大家都知道是2^6,2的倍数计算机喜欢呀,62就不好看。当然这是感性的认识,内存对齐是有好处的,对于访问的速度加快,减少访问周期什么都挺关键。
  现在我们有结构体指针了,我们说说结构体当函数参数。
  我们当然可以直接把结构体变量当参数:void fun(struct People a)
  当我们传入一个结构体的时候,就会拷贝一份结构体给函数。这是我们前面所知道的,实参和形参的知识。
  重要的是,结构体不同于普通变量,它是一堆数据的集合,和数组比较像,通常一个结构体是很大的,上面的例子就有64byte,这还是很简单的一个结构体,复杂的结构体里套结构体,占的空间相当大,你现在还要拷贝一份传给函数,原本不富裕的内存更加雪上加霜。我们数组传进来的时候其实是一个指针,指针只用存储地址,通常都很小,所以从花销上来看,对于函数的参数需要结构体的时候,我们更倾向于传结构体的指针。(当然不是说直接给结构体不行)

共用体类型

  共用体和结构体很像,但却是不同的内存分配方式。

  • 共用体类型定义:

      union 共用体
      {
      	成员列表
      };
      	union Data
      {
      	int i;
      	double k;
      	char m;
      }
    

  变量定义:union Data a;当然和结构体一样的初始化和变量定义的形式都有,这里只是举个例子。
  看起来和结构体没啥差别,但共用体共用一个内存空间,结构体是各个元素的集合,每个元素是有自己的位置的,共用体是只开辟最大变量类型的空间,里面的值由全体成员共享。
  例如上面的Data,它实际上占了多大空间呢,答案是8byte,等于其最大的成员类型double的大小,假设我们对a.i = 2这样付个值,由于共用体成员共用这一片内存空间,所以一变都变,k的值就变成了2.0,char的值就是2,代表的ASCII是STX。
  共用体可以使同一存储区域的内容有多种解读形式。

枚举类型:

  枚举类型提供一个变量的几种可能。
  比如一个表示星期的变量,它的值只能有7中,取值情况是完全确定的,这时就可以用枚举来定义。

  • 枚举类型:

      enum 枚举名
      {
      	情况
      };
    
      enum Weekday
      {
      	Mon,
      	Thu,
      };
    

  这里只写2个举个例子。注意用逗号分开
  同样的,定义变量和前面的相同:enum Weekday a;
  枚举类型的变量,只能取枚举类型中有的值,如a=2.3,a=‘k’是不行的,a=Mon是OK的,a=Fri是不行的(因为我里面还没写),a=Monday是不行的(因为里面没有)。
  这个Mon只在对应枚举类型中才有,平常没有说明的话谁也不知道Mon是什么,所以int I = Mon是不行的。
  声明枚举类型的时候,编译器将里面的东西按常量处理,所以Mon这个符号就能像常量一样使用了。
  实际上,每一个枚举元素代表一个整数,开始的是0,依次往后排,a = Thu和a = 1是一样的,对于一个枚举类型的变量,它的值为2表示它取枚举类型中从开始往后数的第三个常量。
  枚举元素也可以判断,因为它们被赋予了对应的整数,Mon>Thu的结果是假。

  • typedef命名自己的变量类型

  和define很像,但差别也挺大的。就是给变量类型起别名。
  首先typedef是语句不是宏定义。
  typedef int INT;——我们把整型int重命名成了INT,以后在程序中INT i表示声明一个整型变量。
  同样的,我们对结构体什么的也可以这样搞:
typedef struct{
int num;
} Data;
  表示我们重新声明了一个Data的数据类型,是一个结构体类型。这里的分号是typedef语句的分号,和结构体定义什么的无关。
  Data a表示声明一个结构体变量a,struct什么的都不用写了。
  重定义函数指针:
  typedef int (*Point)();
  定义了一个Point的函数指针类型(无参数,返回int)
  这里使用书上的一个总结,我觉得说的很好。
在这里插入图片描述
  下一章的文件操作,会有一个FILE的数据类型,就是由这种方法定义出来的。
  我们下一章再见!

代码训练与详解

  这里我们将要综合指针和结构体的知识,完成链表结构的实现,这算是第一个比较关键的大项目了吧。
  首先,链表是一种数据结构,是动态开辟内存空间对数据进行分配的结构,前面我们说了,数组作为一个放数组的东西,它不能动态开辟,写成多少就是多少,它是静态的,这对我们放数据产生很大的困扰,更多时候我不知道我要放多少个,我希望我希望放多少就能放多少,这个数组明显是不行了,我们可以用更为高级的数据结构来实现,比如链表。

  • 首先我们来介绍一些知识:

C语言动态内存空间的分配
  我们定义变量,函数的形参传值什么的,都是编译器帮我们自动就开辟了内存空间了,我们能不能自己开辟一块内存空间,放自己想放的东西呢,当然是可以的。但是我们仍然没有办法把空间开在想开的地址上(汇编能轻松实现这个功能,这就是更底层语言的优势),地址仍是编译器选择,我们只是选择了一个自己开辟的内存。
  系统提供了很大库函数,来实现这种操作。
malloc函数——void* malloc(unsigned int size)
  这个函数在内存中开辟一块size大小的空间,返回这个开辟空间的首地址,这里void*表示这个指针没有指向任何类型,他是一个不知道指向什么类型,但确实指向了一个地方的指针,听起来比较奇怪,实际原因是手动开辟的内存没有变量属性,他就是单单一个空间,不像定义变量时开辟的内存,明确知道这是一个变量类型的区域。
  如果函数没能成功开辟空间,就返回NULL空指针。
malloc(100)——开辟内存大小时100byte的空间。

calloc函数——void* calloc(unsigned int n,unsigned int size)
  这个函数也是开辟一块空间,不同的是他是连续开辟n个大小为size的空间,这个空间的形式是不是和数组很像。
  **用这个函数可以实现动态数组,n是数组长度,size是数组元素的大小。**同样的不成功放回空指针。
calloc(50,4)——连续开辟50个大小是4byte的空间(其内存格式和int a[50]开辟的内存格式是一样的)

free函数——void* free(void *p)
  我们要知道,我们自己开辟的空间和编译器开的有什么区别。编译器开辟的空间受编译器管理,其空间的有效时间(存活时间)遵循生存周期的控制(前面讲的),而我们自己开的空间,编译器管不了,他只能乖乖听话说开一个多大的就开一个多大的,我们自己不释放这片空间(使其无效,相当于生存周期结束),编译器动也不感动这片区域,这就是自己开辟空间的好处,不受生存周期控制,跳出三界之外,不在五行之中。我们开辟的空间,只能由我们释放。
  但是别忘了,开辟完空间一定要记得手动释放(C和C++还是保留了手动释放内存空间的需求,C#什么的都还给编译器了不在用自己手动管了)。不然你不用了编译器也不会释放的,浪费空间。
  free函数就是释放空间的函数,p是使用malloc或calloc函数开辟的空间的返回值,即开辟的地址。

realloc函数——void* realloc(void* p,unsigned int size)
  对于已经用malloc或calloc函数开辟的空间,想改变其空间大小,可以用这个。
  P是待改变的空间指针,size是新空间的大小,函数会将p指针指向的地方进行相应的改变,p指针不变。
  这4个函数都在头文件stdlib.h中。

  现在我们来说说链表的事,我们这里只说最简单的单向链表
  链表就是链子,把每一个单元串起来,链表中的每一个单元叫结点,每一个结点包含2部分,首先是我们要存放的数据,还有就是指向下一个结点的指针,每个结点通过这样的链式指针一个一个的指下去,从而形成了连续不断的链表结构,除结点外,链表还要有一个头指针,用来指向第一个结点,从而可以访问整个链表,同样的,对于链表的末尾,其指针应当指向空。表示后面没有结点。
在这里插入图片描述
  链表的地址可以是不连续的,因为链表的每个元素(结点)之间是通过地址直接跳转访问,而数组的元素地址是连续的,因为数组的访问是通过地址简单的加减来访问的。
  这里的结点,很明显要包含数据和一个指针,所以结点应该是结构体。

struct Node
{
	int num;
	struct Node *p;	
};

  我们这里写一个简单的结点,用num表示数据,p指向下一个结点。
  静态链表很简单,因为他的数据是现成的,只要把它们串起来就好说了
  比如a,b,c三个结构体,里放着数据,我们来把他们串成链表
head = &a;
a.p = &b;
b.p = &c;
c,p = NULL;
  其中head是头指针,就这样一层一层的串下去,别忘了最后要置为NULL。

  对于动态的链表,就麻烦一点了。
  首先,我们要开辟一个空间来当结点,然后把数据写进去,然后再开辟一个结点,并把原来开辟的结点的指向下一个的指针指向新开的结点,逻辑很清楚,让我们来试试。
  我们可以写下这样的代码:
struct Node *p_next;
struct Node *head = malloc(sizeof(struct Node));
head->num = 0;
p_next = malloc(sizeof(struct Node));
head->p = p_next;
p_next->num = 1;
  首先我们给头结点head开了空间,然后给他赋值为0,又开了下一个空间,用p_next来指向,并成功把head的下一个指针指向了p_next,然后赋值,现在,我们应该再新建下一个结点了,但我们发现没有可以使用的指针变量了,头节点要始终指向链表的开头,所以头节点不能再变了,我们现在还有一个p_next指针,但我们开辟新的空间并用p_next接收的话,我们就完全失去了和前面节点的联系,还怎么让前节点的指针指向后节点呢?
  所以实际上链表的生成是要靠2个指针完成的,它们交替前进,一个提供新开节点的信息,另一个提供上一个节点的信息,并通过这两个节点将链表串起来。

#define LEN sizeof(struct Node)
struct Node *head;
	struct Node *p_old;//老指针 
	struct Node *p_new;//新指针 
	head = malloc(LEN);
	p_old = head;
	p_new = head; 		//三个指针指向开始节点 
	
	p_old->num = 0;		//对起始节点赋值 
	p_new = malloc(LEN);//新节点的开辟,并用新指针接收 
	p_old->p = p_new;//老指针(上一个)的p指向新指针(下一个) 
	p_new->num = 1;//新节点赋值 
	p_old = p_new;//老指针跟进——这一步很重要 
	
	p_new = malloc(LEN);//新指针再开辟,原来的变成老的节点,正好由老指针指向着

  这里两个指针的变化需要注意,总是新指针先指向开辟的空间,然后和老指针实现前面节点和新开节点的关联,然后老指针跟进,新指针再开辟,这样的一个循环过程。
  完整代码:

#include<stdio.h>
#include<stdlib.h>

#define LEN sizeof(struct Node)

struct Node
{
	int num;
	struct Node *p;	
};

struct Node* Link();
void Print(struct Node* head);

int main()
{
	Print(Link());
	return 0;
}

struct Node* Link()
{
	struct Node *head;
	struct Node *p_old;
	struct Node *p_new;
	int i;
	
	head = (struct Node*)malloc(LEN);
	p_old = head;
	p_new = head;
	p_old->num = 0;
	
	for(i=1;i<50;i++)
	{
		p_new = (struct Node*)malloc(LEN);//开辟 
		p_new->num = i;//赋值 
		p_old->p = p_new;//连接 
		p_old = p_new;//跟进
	}
	p_old->p = NULL;
	return head;
}

void Print(struct Node* head)
{
	struct Node *p_temp = head;
	for(;p_temp != NULL;p_temp = p_temp->p)
	{
		printf("%d\n",p_temp->num);
	}
}

  这里我编了两个函数,一个是链表的创建,另一个是链表的显示(这个待会会说)
  #define LEN sizeof(struct Node)——这是宏定义,表示将LEN和sizeof(struct Node)挂钩,编译器看见LEN就用sizeof(struct Node)替换,所以LEN 就是表示Node结构体的大小。
  head = (struct Node*)malloc(LEN);这里用了(struct Node*),这是强制类型转换,因为malloc返回的是void*,虽然可以用struct Node*的指针head接收,但有得时候会报错,而且为了突出开辟空间的真实类型,可以使用强制类型转换。
  p_old->p = NULL;——别忘了最后一个指针指向空。
在这里插入图片描述
显示1到49(截不全),说明是成功的。
下面我们来说说链表的操作:
链表打印:由于每一个节点存储着下一个节点的地址,这个访问就很简单了,每次访问完,让指针指向下一个,再加上最后一个指针会指向空的结束标志,就能写出上面Print函数的效果了。

int Find(struct Node* head,int num)
{
	struct Node *p_temp = head;
	for(;p_temp != NULL;p_temp = p_temp->p)
	{
		if(p_temp->num == num)
		{
			printf("FIND!--%d",p_temp->num);
			return 1;
		}
	}
	return 0;
}

  找到返回1,没有找到返回0.

链表修改:找到后就能访问对于属性了,修改就很方便。
链表插入:这个就比较麻烦了。
  首先我们找到待插入的节点,然后让前一个的节点指向这个节点,让这个节点指向后面的节点,就实现了插入。但注意的是,如果插在最后,最后指向的应当为空,如果插在最前面,需要改变head头指针的指向。

void Insert(struct Node* head,struct Node* temp,int num)
{
	struct Node *p_temp = head;
	if(num == head->num)
	{
		temp->p = head->p;
		head = temp;
		return;
	}
	
	for(;p_temp != NULL;p_temp = p_temp->p)
	{
		if(p_temp->num == num)
		{
			temp->p = p_temp->p;
			p_temp->p = temp;
			return;
		}
		if(p_temp->p == NULL)
		{
			p_temp->p = temp;
			temp->p = NULL;
		}
	}
	
}


链表删除:
  让前面的指向后面的,然后直接把中间free就OK。注意删除的是头节点和尾节点的时候。

void Del(struct Node* head,int num)
{
	struct Node *p_temp = head;
	struct Node *p_now = head;
	if(num == head->num)
	{
		head = head->p;
		free(p_temp);
		return;	
	}
	for(;p_temp != NULL;p_now = p_temp,p_temp = p_temp->p)
	{
		if(p_temp->num == num)
		{
			p_now->p = p_temp->p;
			free(p_temp);
			return;
		}
		if(p_temp->p == NULL && p_temp->p->num == num)
		{
			free(p_temp->p);
			p_temp->p = NULL;
		}
	}
	
}

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值