《C语言笔记》第8章 自定义数据类型

8.1 结构体

8.1.1概念

用户自定义的新数据类型,在结构体中可以包含若干个不同数据类型和不同意义的数据项,从而反映一定的信息。

8.1.2定义

struct 结构体名
{
数据类型 结构体名1;
数据类型 结构体名2;
数据类型 结构体名3;

};
注意:
1)在大括号中的内容也称“成员列表”或“域表”;
2)每个成员名的命名规则与变量名相同;
3)数据类型可以是基本变量类型也可以是一个结构体类型;定义时不分配空间,定义变量才分配空间;
4)用“;”结束。
变量定义方法:
1)struct 结构体名
{
成员列表;
};
struct 结构体名 变量名;
例子:

struct stuff{  
        char job[20];  
        int age;  
        float height;  
};  
struct stuff Huqinwei;  

2)struct 结构体名
{
成员列表;
} 变量名;
3)struct //无结构体名
{
成员列表;
} 变量名;
例子:

struct{  
        char job[20];  
        int age;  
        float height;  
}Huqinwei;  

8.1.3结构体变量的使用

结构体变量是不同数据类型的若干数据的集合体。一般情况下不能把它作为一个整体参加数据处理,而参加各种类型运算和操作的是结构变量的各个成员数据。
结构体变量的成员用以下一般形式表示:
结构体变量名.成员名。
注意:
1>不能对结构体类型变量作为一个整体加以引用,而是对结构体类型变量的各个成员分别引用。
2>如果结构体成员本身又是一个结构体类型,则要运用若干运算符一级一级引用。
3>对成员变量可以像普通变量一样进行各种运算。
4>数组是不能相互赋值的,而结构体可以相互赋值。

8.1.4结构体变量的初始化

与其他类型的变量一样,也可以给结构体变量赋值,这称为结构体的初始化,一种是在定义结构体变量的时候进行初始化,语法结构如下:
struct 结构体 变量名={初始数据表};
另一种是在定义结构体的时候进行初始化。
struct 结构体名
{
类型 成员名;
类型 成员名;
……
}
变量名 ={初始数据表};

8.1.5结构体数组

结构体数组和普通类型的数组类似这里就不一一介绍了。

8.1.6结构体指针

介绍结构体指针之前,先给大家看一个小程序:

#include <stdio.h>  
#include <string.h>  
#include <malloc.h>  
  
struct Man  
{  
    char name[10];  
};  
  
int main()  
{  
    	struct Man N;  
    	N.name = "qiang";  
    	printf("%s\n",N.name);  
}  

这段程序很简单,就是给结构体成员赋值,这里结构体成员是个数组,大家看看这种赋值方式有没有错,我们编译一下:
这里写图片描述
13行报错,就是赋值那行,报错原因是“字符分配的类型是不兼容的类型”我们看看这句N.name = “qiang”,右边是字符串常量,这里其实是字符串的首地址,就是一个地址,我们以前 char a[] = "qiang"没错啊,为什么这里报错了,我们看看左值,N.name, name 是数组名,是代表数组的首地址啊,但是我们要记住,这里name是个地址常量,是不能给常量赋值的,所以会报错,那我们如何给一个结构体中的字符数组赋值呢?我们这里用strcpy(N.name,“qiang”) ! 当然我们N.name[1] = ‘q’,这样是可以的。

8.1.6.1指向结构体类型变量的使用

首先让我们定义结构体:

{  
    char name[20];  
    long number;  
    float score[4];  
};  

再定义指向结构体类型变量的指针变量:

struct stu *p1, *p2 ;

定义指针变量p1、p2,分别指向结构体类型变量。引用形式为:指针变量→成员;这里我们要注意,非结构体指针引用类型是 结构体类型变量 . 成员;
下面我们看一个例子:
对指向结构体类型变量的正确使用。 输入一个结构体类型变量的成员,并输出:

#include <stdlib.h>   
#include <stdio.h>  
  
struct data   
{  
    int day,month,year;  
};  
struct stu   
{  
    char name[20];  
    long num;  
    struct data birthday; /*嵌套的结构体类型成员*/  
};  
int main()   
{  
    struct stu *student; /*定义结构体类型指针*/  
    student = malloc(sizeof(struct stu)); /*为指针变量分配安全的地址*/  
    printf("Input name,number,year,month,day:\n");  
    scanf("%s",student->name); /*输入学生姓名、学号、出生年月日*/  
    scanf("%ld",&student->num);  
    scanf("%d%d%d",&student->birthday.year,&student->birthday.month,  
            &student->birthday.day);  
    printf("\nOutputname,number,year,month,day\n");  
/*打印输出各成员项的值*/  
    printf("%8s    %5ld  %d//%d//%d\n",student->name,student->num,  
        student->birthday.year,student->birthday.month,  
        student->birthday.day);  
}  

执行结果如下:
这里写图片描述
程序中使用结构体类型指针引用结构体变量的成员,需要通过C提供的函数malloc()来为指针分配安全的地址。函数sizeof()返回值是计算给定数据类型所占内存的字节数。指针所指各成员形式为:
student->name
student->num
student->birthday.year
student->birthday.month
student->birthday.day

8.1.6.2指向结构体类型数组的指针的使用

定义一个结构体类型数组,其数组名是数组的首地址,这一点前面的课程介绍得很清楚。定义结构体类型的指针,既可以指向数组的元素,也可以指向数组,在使用时要加以区分。
上个例子中定义了结构体类型,根据此类型再定义结构体数组及指向结构体类型的指针

struct data  
{  
	intday,month,year;  
};  
struct stu/*定义结构体*/  
{  
	char name[20];  
	long num;  
	struct data birthday;/*嵌套的结构体类型成员*/  
};  
struct stustudent[4],*p;   /*定义结构体数组及指向结构体类型的指针*/  

使p=student,此时指针p就指向了结构体数组student。
p是指向一维结构体数组的指针,对数组元素的引用可采用三种方法。

1)地址法
student+i和p+i均表示数组第i个元素的地址,数组元素各成员的引用形式为:
(student+i)->name、(student+i)->num和(p+i)->name、(p+i)->num等。student+i和p+i与&student[i]意义相同。

2)指针法
若p指向数组的某一个元素,则p++就指向其后续元素。

3)指针的数组表示法
若p=student,我们说指针p指向数组student,p[i]表示数组的第i个元素,其效果与student[i]等同。对数组成员的引用描述为:p[i].name、p[i].num等
指向结构体数组的指针变量的使用:

#include <stdio.h>  
#include <malloc.h>  
struct data/*定义结构体类型*/  
{
	int year,month,day;  
};  
struct stu/*定义结构体类型*/  
{
	char name[20];  
	long num;  
	struct data birthday;  
};  
int main()  
{
	int i;  
	struct stu *p,student[4]={
		{"liying",1,1978,5,23},
		{"wangping",2,1979,3,14},  
		{"libo",3,1980,5,6},
		{"xuyan",4,1980,4,21}};  
	/*定义结构体数组并初始化*/  
	p = student;/*将数组的首地址赋值给指针p,p指向了一维数组student*/  
	printf("Outputname,number,year,month,day\n");  
	for(i = 0;i < 4;i++)/*采用指针法输出数组元素的各成员*/  					        
		printf("%8s %6ld   %d//%d//%d\n",(p+i)->name,(p+i)->num,  
				(p+i)->birthday.year,(p+i)->birthday.month, (p+i)->birthday.day);  
	return 0;  
}

执行结果如下:
这里写图片描述

附:模拟时钟程序

分析:我们知道时间有时 分 秒 组成,这里用结构体表示
代码如下:

#include <stdio.h>  
#include <unistd.h>  
#include <malloc.h>  
#include <string.h>  
typedef struct Clock  
{
	int hour;  
	int minute;  
	int second;  
}Clock;  
update(Clock *p)  
{
	p->second++;  
	if(p->second == 60)  			  
	{	 
		p->second = 0;
		p->minute++;	
	}  
	if(p->minute == 60)  		
	{					
		p->minute = 0;  	
		p->hour++;  
	}  
	if(p->hour == 24)  
		p->hour = 0;  
}  
Display(Clock *p)  
{
	printf("\r%02d:%02d:%02d",p->hour,p->minute,p->second);//%02d中0 输出数值时指定左面不使用的空位置自动填0,达到00:00:00效果  
	fflush(stdout);//printf属于行缓冲,遇到\n或程序结束才会输出,这里没有\n,所以用fflush刷新;  
}  
int main()  
{
	Clock *clock;  
	clock = (Clock *)malloc(sizeof(Clock));  
	memset(clock,'\0',sizeof(Clock));//时钟初始化  
	while(1)  
	{	
		sleep(1);  	
		update(clock);  	
		Display(clock);  		
	}  
	free(clock);  
	return 0;  
}

执行结果如下:
这里写图片描述
这里是个动态效果,大家可以打印出来看一下。

8.1.7指针和结构类型的关系

可以声明一个指向结构类型对象的指针。
例:

struct MyStruct 
{ 
 int a; 
 int b; 
 int c; 
} 
MyStruct ss={20,30,40};
//声明了结构对象ss,并把ss的三个成员初始化为20,30和40。 
MyStruct*ptr=&ss;
//声明了一个指向结构对象ss的指针。它的类型是MyStruct*,它指向的类型是MyStruct。 
int*pstr=(int*)&ss;
//声明了一个指向结构对象ss的指针。但是它的类型和它指向的类型和ptr是不同的。 

请问怎样通过指针ptr来访问ss的三个成员变量?
答案:
ptr->a;
ptr->b;
ptr->c;
又请问怎样通过指针pstr来访问ss的三个成员变量?
答案:
*pstr;//访问了ss的成员a。
*(pstr+1);//访问了ss的成员b。
*(pstr+2)//访问了ss的成员c。
虽然我在我的MSVC++6.0上调式过上述代码,但是要知道,这样使用pstr来访问结构成员是不正规的,为了说明为什么不正规,让我们看看怎样通过指针来访问数组的各个单元:
例:

int array[3]={35,56,37}; 
int*pa=array; 

通过指针pa访问数组array的三个单元的方法是:
*pa;//访问了第0号单元
*(pa+1);//访问了第1号单元
*(pa+2);//访问了第2号单元

从格式上看倒是与通过指针访问结构成员的不正规方法的格式一样。
所有的C/C++编译器在排列数组的单元时,总是把各个数组单元存放在连续的存储区里,单元和单元之间没有空隙。但在存放结构对象的各个成员时,在某种编译环境下,可能会需要字对齐或双字对齐或者是别的什么对齐,需要在相邻两个成员之间加若干个"填充字节",这就导致各个成员之间可能会有若干个字节的空隙。

所以,在例中,即使pstr访问到了结构对象ss的第一个成员变量a,也不能保证(pstr+1)就一定能访问到结构成员b。因为成员a和成员b之间可能会有若干填充字节,说不定*(pstr+1)就正好访问到了这些填充字节呢。这也证明了指针的灵活性。要是你的目的就是想看看各个结构成员之间到底有没有填充字节,嘿,这倒是个不错的方法。
通过指针访问结构成员的正确方法应该是象使用指针ptr的方法。

8.2联合体(共用体)

8.2.1 联合体union的基本特性

union,中文名“联合体、共用体”,在某种程度上类似结构体struct的一种数据结构,共用体(union)和结构体(struct)同样可以包含很多种数据类型和变量。不过区别也挺明显:结构体(struct)中所有变量是“共存”的——优点是“有容乃大”,全面;缺点是struct内存空间的分配是粗放的,不管用不用,全分配。而联合体(union)中是各变量是“互斥”的——缺点就是不够“包容”;但优点是内存使用更为精细灵活,也节省了内存空间。

8.2.2双刃剑——多种访问内存途径共存

//example  
#include<stdio.h>  
union var{  
        long int l;  
        int i;  
};  
main(){  
        union var v;  
        v.l = 5;  
        printf("v.l is %d\n",v.i);  
        v.i = 6;  
        printf("now v.l is %ld! the address is %p\n",v.l,&v.l);  
        printf("now v.i is %d! the address is %p\n",v.i,&v.i);  
}  

结果:
这里写图片描述
所以说,管union的叫共用体还真是贴切——完全就是共用一个内存首地址,并且各种变量名都可以同时使用,操作也是共同生效。如此多的access内存手段,确实好用,不过这些“手段”之间却没法互相屏蔽——就好像数组+下标和指针+偏移一样。
上例中我改了v.i的值,结果v.l也能读取,那么也许我还以为v.l是我想要的值呢,因为上边提到了union的内存首地址肯定是相同的,那么还有一种情况和上边类似:一个int数组变量a,一个long int(32位机中,long int占4字节,与int相同)变量b,我即使没给int变量b赋值,因为数据类型相同,我使用int变量b也完全会拿出int数组a中的a[0]来,一些时候一不小心用上,还以为用的就是变量b呢。
这种逻辑上的错误是很难找出来的(只有当数据类型相去甚远的时候稍好,出个乱码什么的很容易发现错误)。

8.2.3联合体union和大小端

(big-endian、little-endian)

#include<stdio.h>  
union var{  
        char c[4];  
        int i;  
};  
  
int main(){  
        union var data;  
        data.c[0] = 0x04;//因为是char类型,数字不要太大,算算ascii的范围~  
        data.c[1] = 0x03;//写成16进制为了方便直接打印内存中的值对比  
        data.c[2] = 0x02;  
        data.c[3] = 0x11;  
//数组中下标低的,地址也低,按地址从低到高,内存内容依次为:04,03,02,11。总共四字节!  
//而把四个字节作为一个整体(不分类型,直接打印十六进制),应该从内存高地址到低地址看,0x11020304,低位04放在低地址上。  
        printf("%x\n",data.i);  
}  

结果:
11020304
证明32位linux是小端(little-endian)

8.2.4联合体union所占内存空间大小

前边说了,首先,union的首地址是固定的,那么,union到底总共有多大?根据一些小常识,做个不严谨不高深的基础版验证吧。
根据:分配栈空间的时候内存地址基本上是连续的,至少同类型能保证在一起,连续就说明,我如果弄三个结构体出来,他们三个地址应该连着,看一下三个地址的间隔就知道了。

#include<stdio.h>  
union sizeTest{  
        int a;  
        double b;  
};  
void main(){  
        union sizeTest unionA;  
        union sizeTest unionB;  
        union sizeTest unionC;  
  
        printf("the initial address of unionA is %p\n",&unionA);  
        printf("the initial address of unionB is %p\n",&unionB);  
        printf("the initial address of unionC is %p\n",&unionC);  
}  

结果:
the initial address of unionA is 0xbf9b8df8
the initial address of unionB is 0xbf9b8e00
the initial address of unionC is 0xbf9b8e08
很容易看出,8,0,8,这间隔是8字节,按double走的。怕不保险,再改一下,把int改成数组,其他不变:

union sizeTest{  
        int a[10];  
        double b;  
};  

打印结果:
the initial address of unionA is 0xbfbb7738
the initial address of unionB is 0xbfbb7760
the initial address of unionC is 0xbfbb7788
88-60=28
60-38=28
算错了?我说的可是16进制0x。那么0x28就是40个字节,正好是数组a的大小。似乎忘了一个功能——sizeof()用sizeof直接看,就知道union的大小了
printf(“the sizeof of unionA is %d\n”,sizeof(unionA));
printf(“the sizeof of unionB is %d\n”,sizeof(unionB));
printf(“the sizeof of unionC is %d\n”,sizeof(unionC));
printf(“the sizeof of union is %d\n”,sizeof(union sizeTest));

8.2.5联合体union适用场合

有了前边那个验证,基本可以确认,union的内存是照着里边占地儿最大的那个变量分的。也就可以大胆的推测一下,这种union的使用场合,是各数据类型各变量占用空间差不多并且对各变量同时使用要求不高的场合(单从内存使用上,我觉得没错)。
像上边做的第二个测试,一个数组(或者更大的数组int a[100]),和一个或者几个小变量写在一个union里,实在没什么必要,节省的空间太有限了,还增加了一些风险(最少有前边提到的逻辑上的风险)。所以,从内存占用分析,这种情况不如直接struct。
不过话说回来,某些情况下虽然不是很节约内存空间,但是union的复用性优势依然存在啊,比如方便多命名,这种“二义性”,从某些方面也可能是优势。这种方法还有个好处,就是某些寄存器或通道大小有限制的情况下,可以分多次搬运。

8.2.6本质&进阶

根据union固定首地址和union按最大需求开辟一段内存空间两个特征,可以发现,所有表面的定义都是虚的,所谓联合体union,就是在内存给你划了一个足够用的空间,至于你怎么玩它不管!(何止是union和struct,C不就是玩地址么,所以使用C灵活,也容易犯错)

没错,union的成员变量是相当于开辟了几个接口(即union包含的变量)!但是,没开辟就不能用了?当然也能用!
写个小测试:

#include<stdio.h>  
union u{  
        int i;  
        double d;//这个union有8字节大小  
};  
void main(){  
        union u uu;  
        uu.i = 10;  
        printf("%d\n",uu.i);  
  
        char * c;  
        c = (char *)&uu;//把union的首地址赋值、强转成char类型  
        c[0] = 'a';  
        c[1] = 'b';  
        c[2] = 'c';  
        c[3] = '\0';  
        c[4] = 'd';  
        c[5] = 'e';  
//最多能到c[7]  
        printf("%s\n",c);//利用结束符'\0'打印字符串"abc"  
        printf("%c %c %c %c %c %c\n",c[0],c[1],c[2],c[3],c[4],c[5]);  
}  

一个例子了然,我的结构体只定义了int和double“接口”,只要我获得地址,往里边扔什么数据谁管得到?这就是C语言的强大,这就是union的本质——只管开辟一段空间。有些东西,熟悉编译原理和编译器工作过程的话,解决会更容易点,虽然我现在这方面技能不太强,不过一般问题也足够分析了。

8.3枚举

枚举类型声明为一组相关的符号常数定义了一个类型名称。枚举用于“多项选择”场合,就是程序运行时从编译时已经设定的固定数目的“选择”中做出决定。
枚举类型(也称为枚举)为定义一组可以赋给变量的命名整数常量提供了一种有效的方法。例如,假设您必须定义一个变量,该变量的值表示一周中的一天。该变量只能存储七个有意义的值。若要定义这些值,可以使用枚举类型。枚举类型是使用 enum 关键字声明的。

enum Days { Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday };
默认情况下,枚举中每个元素的基础类型是 int。可以使用冒号指定另一种整数值类型。
如果不为枚举数列表中的元素指定值,则它们的值将以 1 为增量自动递增。在前面的示例中,Days.Sunday 的值为 0,Days.Monday 的值为 1,依此类推。创建新的 Days 对象时,如果不显式为其赋值,则它将具有默认值 Days.Sunday (0)。创建枚举时,应选择最合理的默认值并赋给它一个零值。这便使得只要在创建枚举时未为其显式赋值,则所创建的全部枚举都将具有该默认值。枚举中大小写敏感,但是建议不要这样。

8.4位域

有些信息在存储时,并不需要占用一个完整的字节, 而只需占几个或一个二进制位。例如在存放一个开关量时,只有0和1 两种状态, 用一位二进位即可。为了节省存储空间,并使处理简便,C语言又提供了一种数据结构,称为“位域”或“位段”。所谓“位域”是把一个字节中的二进位划分为几个不同的区域, 并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。 这样就可以把几个不同的对象用一个字节的二进制位域来表示。

8.4.1位域的定义和位域变量的说明

位域定义与结构定义相仿,其形式为:
  struct 位域结构名
  { 位域列表 };
  其中位域列表的形式为: 类型说明符 位域名:位域长度
  例如:
struct bs
{
 int a:8;
 int b:2;
 int c:6;
};
位域变量的说明与结构变量说明的方式相同。 可采用先定义后说明,同时定义说明或者直接说明这三种方式。例如:
struct bs
{
 int a:8;
 int b:2;
 int c:6;
}data;
说明data为bs变量,共占两个字节。其中位域a占8位,位域b占2位,位域c占6位。对于位域的定义尚有以下几点说明:
1>一个位域必须存储在同一个字节中,不能跨两个字节。如一个字节所剩空间不够存放另一位域时,应从下一单元起存放该位域。也可以有意使某位域从下一单元开始。例如:

struct bs
{
 unsigned a:4
 unsigned :0 /*空域*/
 unsigned b:4 /*从下一单元开始存放*/
 unsigned c:4
}

在这个位域定义中,a占第一字节的4位,后4位填0表示不使用,b从第二字节开始,占用4位,c占用4位。
2>由于位域不允许跨两个字节,因此位域的长度不能大于一个字节的长度,也就是说不能超过8位二进位。
3>位域可以无位域名,这时它只用来作填充或调整位置。无名的位域是不能使用的。例如:

struct k
{
 int a:1
 int :2 /*该2位不能使用*/
 int b:3
 int c:2
};

从以上分析可以看出,位域在本质上就是一种结构类型, 不过其成员是按二进位分配的。

8.4.2位域的使用

位域的使用和结构成员的使用相同,其一般形式为: 位域变量名·位域名 位域允许用各种格式输出。

void main()
{
 struct bs
 {
  unsigned a:1;
  unsigned b:3;
  unsigned c:4;
 } bit,*pbit;
 bit.a=1;
 bit.b=7;
 bit.c=15;
 printf("%d,%d,%d\n",bit.a,bit.b,bit.c);
 pbit=&bit;
 pbit->a=0;
 pbit->b&=3;
 pbit->c|=1;
 printf("%d,%d,%d\n",pbit->a,pbit->b,pbit->c);
}

上例程序中定义了位域结构bs,三个位域为a,b,c。说明了bs类型的变量bit和指向bs类型的指针变量pbit。这表示位域也是可以使用指针的。
程序的9、10、11三行分别给三个位域赋值。( 应注意赋值不能超过该位域的允许范围)程序第12行以整型量格式输出三个域的内容。第13行把位域变量bit的地址送给指针变量pbit。第14行用指针方式给位域a重新赋值,赋为0。第15行使用了复合的位运算符"&=", 该行相当于: pbit->b=pbit->b&3位域b中原有值为7,与3作按位与运算的结果为3(111&011=011,十进制值为3)。同样,程序第16行中使用了复合位运算"|=", 相当于: pbit->c=pbit->c|1其结果为15。程序第17行用指针方式输出了这三个域的值。

已标记关键词 清除标记
适用人群 使用苹果Mac电脑、有一定项目管理基础、希望进行项目管理实践,与优秀的人在专属学习社群一起进步的朋友。 课程概述 参加本课程,您将可以—— ★拓展高价值人脉:在学习社群结识志同道合的小伙伴 ★将知识活学活用到你的日常 ★借助OmniPlan管理利器实现技能快速跃迁。 总课时数:60+  更新周期:每周至少一次,每周不少于5课时,约在两个月内连载结束。 课程特色: ★强大团队与教学设计:东东老师从事教育投资与管理工作15年,目前负责四家不同类型教育机构,具有丰富教育与项目管理实战经验,以及丰富的员工培训经验。老师及幕后教学团队将职场干货融入到在线课堂,根据学员的不同起步设计教学,尽量满足不同层次学员需求,从初级入门,到中级进阶,到高级技巧,应有尽有,全网唯一最新最全面最深入的OmniPlan项目管理课程。 ★ 答疑解惑与打卡作业:报名即送30天训练营,教学助理全程指导,每天集中答疑,更可微博直接@东东研习社,提交打卡作业和问题。 ★ 内容全面人人可学:以macOS系统下项目管理软件OmniPlan为主,从头开始进行项目管理模拟实战,既有全面基础操作,又有完整实战案例,无基础者从零无痛入门,有基础者可通过实践进行技能提升。 ★ 学以致用人生致胜:学会老师分享的方法和技巧,将大大提高项目管理的效率,并可应用于日常生活与工作,快速提升个人专业技能。
相关推荐
©️2020 CSDN 皮肤主题: 酷酷鲨 设计师:CSDN官方博客 返回首页
实付 19.90元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值