C语⾔中的复杂数据类型,你掌握了哪些?
⽬录
⽂章⽬录
复杂数据类型
对于任何⼀门能够处理实际应⽤的编程语⾔,基本数据类型都是不够⽤的,只能存放线性数据的数组也是不够⽤的,例如⼀个学⽣拥有姓名、性别、年龄等内容,这样的内容必须⽤复杂数据类型来描述,对于⾼级语⾔来说复杂数据类型可以⽤类描述,对于C这样的⾯向过程语⾔,C标准提供了结构体、共⽤体这样的复杂数据类型来描述。
结构体
结构体这种数据类型有很多称呼,有的称关联数组,有的称为构造型数组,在结构体中可以定义成员名称并设置值,有的书本称为值对,把成员名称为“键”,成员值称为“键值”,⼀个结构体的定义格式如下:
struct 类型名称
{
属性1;
属性2;
…
};
注意结构体最后有⼀个分号,定义了结构体后就可以使⽤结构体类型定义变量了,例如:
#include<stdio.h>
struct Student
{
char name[20];
int age;
int sex;
};
int main(int argc,char* argv[])
{
struct Student s1={"xiaoming",5,0};
printf("s1:%s,%d,%d",s1.name, s1.age, s1.sex);
return0;
}
定义结构体和⽤结构体声明变量时都必须书写关键字struct,结构体类型可以在函数外声明也可以在函数内声明,在函数内声明的结构体类型外部不能识别,因此没有通⽤性,⼤部分情况下我们都会在⽂件顶部声明结构体。如果想节省代码,可以在定义时同时声明变量,例如:
struct Student
{
char name[20];
int age;
int sex;
} s1,s2;
甚⾄可以没有名称,例如:
struct
{
char name[20];
int age;
int sex;
} s1,s2;
这种匿名⽅式不能在其它地⽅定义变量,但可以使⽤这种⽅式定义别名,关于别名我们在后⾯的章节中讲解。定义结构体时除了不要忘记最后的分号还要记住不能在结构体内部为成员指定初始值,初始值只能在定义变量时指定,⽐如这个例⼦中的:
struct Student s1={“xiaoming”,5,0};
当然我们可以选择定义变量后再通过s1.name=”xiaoming”初始化,显然这种⽅式⽐较⿇烦,适合使⽤键盘输⼊或⽤代码读⼊数据的场景,为什么不能在结构体中初始化变量呢?因为结构体是⼀个原始数据类型,它不像⾼级语⾔的类有构造函数,因此不能在定义时进⾏初始化。
结构体指针和成员的引⽤
在讲结构体指针之前我们必须明确⼀个概念,那就是C标准对结构体的设定,初学者很容易认为结构体就是⾼级语⾔中的类模板,或者跟数组⼀样是⼀个数据集合,也容易认为结构体变量名是第⼀个成员的地址或者是数据集合⼊⼝地址,这样就犯了原始性的概念错误,产⽣这样的错觉⼀是因为结构体拥有成员,很容易将它看作数据集合或类模板,⼆是被关联数组、构造型数组这样的名称迷惑了,实际上C标准并不将结构体看作为数据集合,更不会引⼊⾯向对象的概念将它当作类模板,⽽是将结构体定义为⼀个原始的复杂数据类型,在很多⽅⾯处理结构体的⽅式和基本数据类型⼀样,如下:
1. 结构体名称仅代表数据类型
假如我们⽤struct Student声明⼀个结构体变量s1,s1的类型为struct Student,这个类型如同int,float,仅⽤于解释s1所占的内存区域的数据类型
2. 除⾮使⽤const,否则⽤结构体类型声明的是变量⽽不是常量,这个变量可以被赋值,且赋值时也是按值传递的,我们⽤下⾯的代码来
验证⼀下:
#include<stdio.h>
#include<string.h>
struct Student
{
char name[20];
int age;
int sex;
};
int main(int argc,char* argv[])
{
struct Student s1={"xiaoming",5,0};
struct Student s2=s1;
strcpy(s2.name,"xiaohong");
s2.age=4;
s2.sex=1;
printf("s1:%s,%d,%d\n",s1.name, s1.age, s1.sex);
printf("s1:%s,%d,%d",s2.name, s2.age, s2.sex);
return0;
}
从结果可以看出,修改s2后s1并未发⽣变化,同样将结构体变量作为函数参数时也是按值传递的,修改函数参数并不会对原来的变量内容产⽣影响,如果觉得克隆结构体花费的代价较⼤,可以将&s1传⼊函数。
3. 结构体名称和变量名都不表⽰地址
有⼈会想,基本数据类型变量名表⽰某内存的值,数组名表⽰⾸元素的地址,那么结构体名和结构体变量名表⽰什么呢?和基本数据类型⼀样,结构体名和int,float⼀样时数据类型,变量名则是某段内存的值,它们都不表⽰地址,要说区别,那就是基本数据类型变量和数组名都可以通过printf()输出,⽽结构体变量却不能输出,因为printf()有⽀持基本数据类型和指针的格式符,但不提供输出结构体的格式符,如果⽤printf(“%d”, s1)输出结构体变量名会得到什么结果呢?printf()会将结构体所在内存的前⼏个字节当作整数输出,⽽且还会给出类型不匹配的警告,因此⽤printf()输出结构体本⾝是没有意义的。在C语⾔中,结构体变量只能⽤于赋值、取址,⽤sizeof()求⼤⼩,或者通过它访问其成员,尝试把结构体变量转化为整数、字符串、地址等操作均会失败,⽽结构体名如同int, float ⼀样只能⽤于类型判断或⽤sizeof()计算⼤⼩。
了解结构体的概念后我们来看看结构体指针,当我们声明⼀个结构体指针时,指针类型为结构体类型加
上*号,其值是⽤&号对结构体变量取址,例如:
struct Student *p=&s1;
这和基本数据类型的声明⽅式是⼀样的,对于存放结构体的内存来说,理论上各成员在内存中应该是连续存放的,但在编译器的具体实现中成员之间可能存在缝隙,因此结构体所占空间⼤⼩不⼀定是各个成员⼤⼩的和,它的实际⼤⼩应该⽤sizeof()求出,也因为这个原因,我们不能通过指针位移来获取成员值。但是即便成员之间有间隙,结构体所在内存的起始位置仍然存放第⼀个成员的内容,因此结构体变量的地址和第⼀个成员的地址相同,但是类型不⼀样,在上⾯这个例⼦中,第⼀个成员为字符串,对于字符串来说,数组名的地址、数组名和第⼀个字符的地址也是相同的,不同的也是数据类型,因此对于上⾯的例⼦,&s1, p, &p.name, p.name以及&p.name[0]的地址都是相同的,⽤⼀段代码进⾏测试:
#include<stdio.h>
#include<stdlib.h>
struct Student
{
char name[20];
int age;
int sex;
};
int main(int argc,char* argv[])
{
struct Student s1={"xiaoming",5,0};
struct Student *p=&s1;
printf("&s1=%p,p=%p,&p.name=%p,p.name=%p,&p.name[0]=%p\n",&s1,p,&s1.name,s1.name,&s1.name[0]);
printf("s1=%d",s1);//直接输出s1看看结果时什么
return0;
}
上⾯代码输出结果验证了我们的猜测,但如果只输出结构体变量名,结果却不同,且没有意义。定义了结构体变量后,就可以通过成员名进⾏访问了,例如s1.name, s1.age,前⾯说过由于结构体成员之间有内存间隙,因此不能通过指针位移来访问成员的地址,要使⽤结构体指针直接访问结构体成员,C标准提供了专门的访问⽅法,那就是⽤符号“->”来访问,例如p->name, p->age,这⽐使⽤(*p).name,
(*p).age间接访问看上去要简洁,很多编辑器使⽤.和->引⽤结构体变量的成员时都会提⽰成员列表。
讲到这⾥可以看到结构体⽆论从概念设计、内存储存⽅式以及访问⽅式上都和数组⼤相径庭,它们之间没有任何相通之处,将数组的任何⽤法套⽤在结构体上都是不适⽤的。对于结构体的成员来说,成员可以是基本数据类型、数组或者另外⼀个结构体,如果成员为数组,数组名也是常量,这⾥ s1.name和name都是常量,不能通过s1.name=”xxx”来尝试修改学⽣的名称。
结构体数组
⼀个数组的元素可以是基本数据类型或地址,现在有了结构体,数组元素也可以是结构体类型,定义结构体数组和定义普通数组没有多⼤区别,但初始化时你会发现数组和结构体都使⽤{}进⾏初始化,因此初始化⼀个结构体数组可以写成{{},{}…}的形式,为了简化输⼊,可以省略结构体的{},下⾯使⽤代码来演⽰这两种初始化的形式:
char name[20];
int age;
int sex;
};
int main(int argc,char* argv[])
{
struct Student students1[2]={{"xiaoming",5,0},{"xiaohong",6,1}};
struct Student students2[2]={"xiaoming",5,0,"xiaohong",6,1};
printf("%s\n",students1[0].name);
printf("%s",students2[0].name);
return0;
}
这种简化的书写⽅式类似⽤⼀维数组初始化⼆维数组,但要明⽩数组中存放的是什么,内容不能有任何偏差,数组元素可以⽤p++或p++进⾏遍历,结合结构体的专⽤访问⽅式,使⽤p->name访问结构体成员⽐(*p).name更简洁,例如:
#include<stdio.h>
#include<stdlib.h>
struct Student
{
char name[20];
int age;
int sex;
};
int main(int argc,char* argv[])
{
struct Student students[2]={{"xiaoming",5,0},{"xiaohong",6,1}};
struct Student *p=students;
while(p<students+2)puts(p++->name);
return0;
}
构建链表
从数据结构上看,现在我们有了描述线性结构的数组和描述散列结构的结构体,但对于描述更复杂的数据类型仍然显得⼒不从⼼,例如环形结构、树形结构等,另外数组和结构体这样的原⽣数据类型⼀旦定义后其长度不能更改,⽽实际应⽤需要的数据结构复杂多变,长度随时可以变化,为了实现这些需求,我们需要基于已有的原⽣结构创建更加复杂的数据结构,结构体在其中扮演着关键⾓⾊。在复杂的数据结构中,每个元素被看作⼀个节点,节点跟节点之间的关系称为关系链,我们⽤结构体描述节点,⽤结构体中包含的指针描述节点之间的关系。单链表和双链表是最简单的数据结构,对于单链表,让节点末
尾的指针指向另⼀个节点的地址从⽽将多个节点串联起来;双链表是基于单链表的延申,⼀个节点包含两个指针,头部指针指向前⼀个节点,尾部指针指向后⼀个节点,从⽽可以双向遍历节点。链表虽然也是线性结构,但⽐数组在结构⽅⾯有优势,它的长度可以随时修改,从中间添加删除某个节点也不会导致其它元素整体移动,缺点是节点在内存中的存放不是连续的,结构越复杂内存越零碎,下例创建⼀个包含10个元素的单链表:
int num;
struct Node *next;
};
const int SIZE=sizeof(struct Node);//求出⾃定义类型占⽤内存⼤⼩
int main(int argc,char* argv[])
{
int len=10;//元素个数
int i=0;//元素索引
struct Node *head,*p,*next;//创建单链表下需要的各种指针
p=head=malloc(SIZE);//创建头元素
p->num=i++;//初始化头元素
p->next=NULL;
//创建其它元素
while(i<len)
{
//创建并初始化下⼀个节点
next=malloc(SIZE);
next->num=i;
c++strcpy函数用法next->next=NULL;
p->next=next;//将上⼀个节点链接到下⼀个节点
p=next;//跳转到下⼀个节点
i++;
}
//输出链表
p=head;
do
{
printf("node%d\n",p->num);
p=p->next;
}while(p!=NULL);
}
这⾥将Node类型的长度定义为常数SIZE以便后⾯申请动态空间使⽤,先创建头元素head然后再创建其余的元素,虽然代码可以继续简化,例如可以不声明索引i,把创建头元素的代码都写⼊while循环中,但对于复杂的逻辑我们不求代码最精简,⽽是让代码阅读起来更清晰,这样便于调试。能够创建单链表就可以创建其它数据结构类型,但是由于缺乏类,⽅法不能封装到数据类型中,只能写成全局函数,我们来为Node添加⼀个⽅法test():
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论