结构体
概述
数组:描述一组具有相同类型数据的有序集合,用于处理大量相同类型的数据运算。
有时我们需要将不同类型的数据组合成一个有机的整体,如:一个学生有学号/姓名/性别/年龄/地址等属性。显然单独定义以上变量比较繁琐,数据不便于管理。
C语言中给出了另一种构造数据类型——结构体。
结构体变量的定义和初始化
- 先声明结构体类型再定义变量名
- 在声明类型的同时定义变量
- 直接定义结构体类型变量(无类型名)
代码示例:
#include<stdio.h>
#include<string.h>
struct student{
char name[21]; 一个汉字在windows内存中2个字节,在Linux内存中3个字节
int age;
int score;
char addr[51];
};
int main(void){
struct student stu;
/*stu.name = "张三"; */ 因为数组名是一个常量值,所以不能这样给数组名赋值
strcpy(stu.name,"张三"); 使用strcpy函数,可以给name里面的每个内存空间赋值
stu.age = 23;
stu.score = 105;
//stu.addr = "北京市丰台区";
strcpy(stu.addr,"北京市丰台区");
printf("name = %s\n",stu.name);
printf("age = %d\n",stu.age);
printf("score = %d\n",stu.score);
printf("addr = %s\n",stu.addr);
}
运行结果:
上面代码中,一行一行的赋值,太麻烦,字符串赋值还要调用函数,可以更简单一些
struct student stu = {"张三",18,100,"北京市丰台区"};
还有一种方式:定义结构体同时赋值
struct student{
char name[21];
int age;
int score;
char addr[51];
}stu = {"张三",18,100,"北京市丰台区"};
还可以在定义结构体的时候,多定义几个变量名
struct student{
char name[21];
int age;
int score;
char addr[51];
}stu1,stu2,stu3;
使用键盘输入为结构体的字段赋值
#include<stdio.h>
struct student{
char name[21];
int age;
int score;
char addr[51];
}stu;
int main(void){
struct student stu;
scanf("%s%d%d%s",stu.name,&stu.age,&stu.score,stu.addr);
这里int类型的字段要使用取地址符,字符串类型的不用,因为他本身就是一个地址
printf("name = %s\n",stu.name);
printf("age = %d\n",stu.age);
printf("score = %d\n",stu.score);
printf("addr = %s\n",stu.addr);
}
运行结果:
以上是一个结构体的初始化和使用,也就是打印一个学生的基本信息,假如我需要打印很多学生的信息呢?就需要用到结构体数组
结构体数组
类似于
int a; 这是一个整型变量
int arr[]; 这是一个整型数组
数组基本都是 类型 变量名 [] 这样的样式
所以定义一个结构体数组就是
struct student stu[3];
整体代码:
#include<stdio.h>
struct student{
char name[21];
int age;
char sex;
int score[3];
char addr[51];
};
int main(void){
struct student stu[3] = {
{"张三",18,'m',89,89,100,"河北唐山"},
{"李四",21,'m',59,60,70,"山西运城"},
{"王五",22,'f',100,100,100,"北京朝阳"}
};
for(int i = 0;i<3;i++){
printf("姓名 = %s\n",stu[i].name);
printf("年龄 = %d\n",stu[i].age);
printf("性别 = %s\n",stu[i].sex=='m'?"男":"女");
printf("语文 = %d\n",stu[i].score[0]);
printf("数学 = %d\n",stu[i].score[1]);
printf("英语 = %d\n",stu[i].score[2]);
printf("地址 = %s\n",stu[i].addr);
printf("\n");
}
}
运行结果:
结构体数组和结构体的大小
以前学习的,如果想要获取到一个变量所占用内存的大小 就用sizeof()
但是sizeof的对象如果是一个指针变量的话,不管是什么类型,多大的指针,返回结果都是4或者8
我们来试试看,sizeof函数对结构体适用不适用
代码:
#include<stdio.h>
struct student{
char name[21];
int age;
char sex;
int score[3];
char addr[51];
};
int main(void){
struct student stu[3] = {
{"张三",18,'m',89,89,100,"河北唐山"},
{"李四",21,'m',59,60,70,"山西运城"},
{"王五",22,'f',100,100,100,"北京朝阳"}
};
printf("数组占用内存大小为%d\n",sizeof(stu));
printf("数组元素占用内存大小为%d\n",sizeof(stu[0]));
printf("数组元素个数为%d\n",sizeof(stu)/sizeof(stu[0]));
}
说明sizeof函数对结构体是有用的,我们简单的计算一下sizeof计算的结构体元素大小对不对
struct student{
char name[21]; 21
int age; 4
char sex; 1
int score[3]; 12
char addr[51]; 51
89
};
我们手动计算的结果是89 sizeof计算的结果是96呢?
这是因为结构体的成员需要偏移对齐,结构体的成员在内存中存放时会根据最大类型进行偏移对齐,最大类型是int类型,也就是说,所有的内容在内存中开始存放的时候,他的起始地址一定是4的倍数,如果不是,会跳到下一个4的倍数位置上,
以上面代码为例,name占用了21个字节,下一个age存放的时候,不会从第22个字节开始存,而是跳到第24个字节开始存放,以此类推,name后面+3,sex后面+3,addr后面+1,就是89+7=96
结构体数组排序
将学生成绩按照语文成绩从高到低排序
#include<stdio.h>
struct student{
char name[21];
int age;
char sex;
int score[3];
char addr[51];
};
int main(void){
struct student stu[3] = {
{"张三",18,'m',89,89,100,"河北唐山"},
{"李四",21,'m',59,60,70,"山西运城"},
{"王五",22,'f',100,100,100,"北京朝阳"}
};
for(int i=0;i<3-1;i++){
for(int j = 0;j<3-1-i;j++){
if(stu[j].score[0]<stu[j+1].score[0]){
struct student temp = stu[j];
stu[j] = stu[j+1];
stu[j+1] = temp;
}
}
}
for(int i = 0;i<3;i++){
printf("姓名 = %s\n",stu[i].name);
printf("年龄 = %d\n",stu[i].age);
printf("性别 = %s\n",stu[i].sex=='m'?"男":"女");
printf("语文 = %d\n",stu[i].score[0]);
printf("数学 = %d\n",stu[i].score[1]);
printf("英语 = %d\n",stu[i].score[2]);
printf("地址 = %s\n",stu[i].addr);
printf("\n");
}
}
打印结果:
开辟堆空间存储结构体
首先回忆一下开辟堆空间存储int类型数据:
int* p = (int*)malloc(sizeof(int)*3);
以此类推,堆空间存储结构体的方式应该是
struct student * p = (struct student *)malloc(sizeof(struct student)*3);
这样写好像有点长了,代码阅读性不强,哪里都要加个struct,很烦人,可以给他缩写一些,就是给结构体起个别名,使用typedef
typedef struct student ss; 这样就给一个结构体类型起好了一个别名ss
使用的时候就可以写成这样:
ss* p = (ss*)malloc(sizeof(ss)*3);
代码看起来就简单多了
其实之前用到的size_t也是这样的道理,关联源码后会发现
typedef unsigned int size_t;
也可以在定义结构体的时候,直接起别名
typedef struct student{
char name[21];
int age;
int score[3];
char addr[51];
}stu;
int main(void){
stu s = {"张三",23,90,90,90,"张家村"};
printf("%s\n",s.name);
}
这样stu就直接是个别名了。
然后我们通过键盘输入给堆空间的结构体赋值,并打印出来
#include<stdio.h>
typedef struct student ss; 起个别名
struct student{
char name[21];
int age;
char sex;
int score[3];
char addr[51];
};
int main(void){
ss* p = (ss*)malloc(sizeof(ss)*3);
for(int i = 0;i<3;i++){
scanf("%s%d,%c%d%d%d%s",
p[i].name,&p[i].age,&p[i].sex,&p[i].score[0],&p[i].score[1],&p[i].score[2],p[i].addr);
}
上面的scanf函数,注意中间有一个sex字段,是char类型的,所以输入空格或者回车都有可能被这个char类型的字段接收,造成数据错误,
所以在%c占位符之前写一个逗号,在键盘输入的时候这里也写一个逗号,就不会接收错误了
for(int i = 0;i<3;i++){
printf("姓名 = %s\n",p[i].name);
printf("年龄 = %d\n",p[i].age);
printf("性别 = %s\n",p[i].sex=='m'?"男":"女");
printf("语文 = %d\n",p[i].score[0]);
printf("数学 = %d\n",p[i].score[1]);
printf("英语 = %d\n",p[i].score[2]);
printf("地址 = %s\n",p[i].addr);
printf("\n");
}
free(p);
p=NULL;
}
看一下结构体指针的大小
sizeof(ss*);
sizeof(p);
都可以
运行结果是4
结构体嵌套结构体
例如,一个学生有基本信息,和三门成绩,我们就可以把三门成绩单独定义成一个结构体
#include<stdio.h>
typedef struct student stu; 为了书写方便和阅读性强,起个别名
typedef struct score sc;
struct score{ 将分数单独写成一个结构体,三门课
int cl;
int cpp;
int cs;
};
struct student{
char name[21];
int age;
char sex;
sc s; 这里就可以直接使用结构体的别名来定义了
char addr[51];
};
int main(void){
stu stu1 = {"王二",19,'m',98,98,96,"乌鲁木齐"};
printf("%s\n%d\n%s\n%d\n%d\n%d\n%s\n",
stu1.name,stu1.age,stu1.sex=='m'?"男":"女",
stu1.s.cl,stu1.s.cpp,stu1.s.cs,stu1.addr);
取结构体的结构体的值的时候,方式类似:学生.成绩.课程
}
运行结果:
计算一下结构体中结构体的大小
#include<stdio.h>
typedef struct student student;
typedef struct score score;
struct score{
int cl;
int cpp;
int cs;
};
struct student{
char name[21];
int age;
char sex;
score s;
char addr[51];
};
int main(void){
student stu;
printf("学生结构体的大小为%d\n",sizeof(stu)); 96
printf("分数结构体的大小为%d\n",sizeof(stu.s)); 12
}
结构体的赋值
考虑以下代码的运行结果
#include<stdio.h>
typedef struct student student;
struct student{
char name[21];
int age;
int score;
char addr[51];
};
int main(void){
student stu = {"孙尚香",25,89,"巴蜀"}; 定义一个stu,并且初始化
student stu1 = stu; 定义一个stu1,将stu赋值给stu1
strcpy(stu1.name,"甘夫人"); 改变stu1.name的值,
printf("stu.name = %s\n",stu.name); 是否会影响stu.name的值
}
运行结果为
以上代码的运行过程类似于:
int a = 10;
int b = a;
b = 20;
问a等于多少?
当然是10,改变b的值,不会影响到a的值
结构体和指针
指针指向一个结构体
#include<stdio.h>
typedef struct student student; 定义一个别名就叫student
struct student{
char name[21];
int age;
int score[3];
char addr[51];
};
int main(void){
student stu = {"孙尚香",25,89,90,100,"巴蜀"};
student* p= &stu; student就相当于类型,student* p就是指针
printf("姓名 = %s\n",p->name); 指针用小箭头,变量用点
printf("年龄 = %d\n",p->age);
printf("语文 = %d\n",p->score[0]);
printf("数学 = %d\n",p->score[1]);
printf("英语 = %d\n",p->score[2]);
printf("住址 = %s\n",p->addr);
}
运行结果
结构体指针->成员
结构体变量.成员
结构体成员如果用指针表示,如何在堆空间中分配一个结构体指针
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
typedef struct student student;
struct student{
char* name; 这里使用指针类型来定义结构体的成员
int age;
int* score;
char* addr;
};
int main(void){
student* pstu = (student*)malloc(sizeof(student)*3); 创建一个结构体的指针,3个student大小,也就是4*4=12个字节大小
其中name,score,addr是指针,里面的值放的是地址
for(int i = 0;i < 3;i++){ 依次给每一个成员指针开辟空间
pstu[i].name = (char*)malloc(sizeof(char)*21);
pstu[i].score = (int*)malloc(sizeof(int)*3);
pstu[i].addr = (char*)malloc(sizeof(char)*51);
}
for(int i = 0;i<3;i++){ 通过键盘输入给每个成员赋值
scanf("%s%d%d%d%d%s",
pstu[i].name, 字符串类型不用加&,因为他的值就是首地址
&pstu[i].age, int类型的值要加&
&pstu[i].score[0],
&pstu[i].score[1],
&pstu[i].score[2],
pstu[i].addr);
}
for(int i = 0;i<3;i++){ 循环打印
printf("%s %d %d %d %d %s\n",
pstu[i].name,
pstu[i].age,
pstu[i].score[0], 指针pstu后面用[i]也代表取值,
(pstu+i)->score[1], 指针pstu+i再用箭头也代表取值,
(pstu+i)->score[2], 两个用哪个都行
(pstu+i)->addr);
}
for(int i = 0;i<3;i++){ 释放内存
free(pstu[i].name); 先释放后分配的内存,再释放pstu这样的总内存
free(pstu[i].score); 如果先释放pstu,那里面存放的地址就释放掉了,下面成员的内存都不知道在哪里了,怎么释放
free(pstu[i].addr);
}
free(pstu);
}
运行结果:
结构体做函数参数
结构体普通变量做函数参数
运行以下代码
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
typedef struct student student;
struct student{
char name[21];
int age;
int score;
char addr[51];
};
void fun01(student param){
printf("func01 %s\n",param.name);
strcpy(param.name,"卢俊义");
printf("func01 %s\n",param.name);
}
int main(void){
student stu = {"宋江",50,98,"郓城"};
fun01(stu);
printf("main %s\n",stu.name);
}
运行结果:
结果可想而知,stu是实参,param是形参,改变形参的值,不会对实参的值有影响。
如果定义结构体的时候,name使用的是指针定义方式呢?
那么初始化结构体方式就要变成下面这样
student stu = {NULL,50,98,"郓城"};
stu.name = (char*)malloc(sizeof(char)*21);
strcpy(stu.name,"宋江");
因为是指针,所以要先开辟内存空间,用name的指针指向该内存空间
然后给该空间写入数据
但是运行程序,发现结果发生了变化
不是形参不能改变实参的值吗?就改了个name为指针定义方式,为什么结果发生变化了呢?
因为name定义方式是指针,所以当stu传递给函数的时候,name是将他的地址值传递过去的,
param接收到的也是地址,用strcpy直接修改地址对应的值,导致main函数中的实参发生了变化
为了验证上面的结论,我们将给形参改变name值之前,重新给name分配一块内存,代码如下
void fun01(student param){
printf("func01 %s\n",param.name);
param.name = (char*)malloc(sizeof(char)*21); 给param.name重新指向了另外一块内存,而不是实参传过来的内存地址
strcpy(param.name,"卢俊义");
printf("func01 %s\n",param.name);
}
这样运行结果为:
结论成立!
结构体指针变量做函数参数
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
typedef struct student student;
struct student{
char name[21];
int age;
int score;
char addr[51];
};
void fun01(student* param){ 这里用一个指针对象接着
printf("func01 %s\n",param->name);
strcpy(param->name,"公孙胜");
printf("func01 %s\n",param->name);
}
int main(void){
student stu = {"吴用",50,98,"梁山"};
fun01(&stu); 传递的是地址
printf("main %s\n",stu.name);
}
运行结果;
结果显示,结构体指针作为函数的参数,函数里面修改是会影响到原来的值的
结构体数组名做函数参数
定义一个结构体数组,并且初始化,然后调用函数给他们排序
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
typedef struct student student;
struct student{
char name[21];
int age;
int score;
char addr[51];
};
void bubbleSort(student* pstu,int len){
for(int i=0;i<len-1;i++){
for(int j=0;j<len-1-i;j++){
if(pstu[j].age>pstu[j+1].age){
student temp = pstu[j];
pstu[j] = pstu[j+1];
pstu[j+1] = temp;
}
}
}
}
int main(void){
student stu[3] = { 定义结构体数组,并初始化
{"张三",50,98,"张家庄"},
{"李四",25,88,"李家庄"},
{"王五",33,99,"王家庄"}
};
bubbleSort(stu,3); 调用函数进行排序
数组作为函数参数会退化为指针,丢失元素精度,需要传递个数
for(int i = 0;i<3;i++){ 打印排序后的数组
printf("%s %d %d %s\n",stu[i].name,stu[i].age,stu[i].score,stu[i].addr);
}
}
运行结果:
const修饰结构体指针形参变量
其实和const修饰的指针变量一个意思,看下面代码
student stu1 = {"张三",23,89,"张村"};
student stu2 = {"李四",30,90,"李村"};
const student* p = &stu1; const修饰student*,可以修改p的值,
不能修改p指向的地址的值,也就是不能修改student的值
p = &stu2;//ok
p->age = 40;//err
student* const p = &stu1; const修饰p,可以修改student的值,不能修改p的值
p = &stu2;//err
p-name = "王五";//err 这一行出错不是因为p->name不能修改,而是因为p->name是数组名,是常量,不能修改
// 应该用strcpy(p->name,"王五");
p->age = 40;//ok
const student* const p = &stu1; const既修饰student* 又修饰p,student的值和p的值都不可以修改
p = &stu2;//err
p->age = 40;//err
一样的道理,const修饰的一级只读指针,可以通过二级指针进行修改
const student* const p = &stu1;
student** pp = &p;
*pp = &stu2;//OK
(*pp)->age = 99;//err
共用体(联合体)
- 联合体union是一个能在同一个存储空间存储不同类型数据的类型
- 联合体所占的内存空间等于其最大成员类型的长度倍数,
- 同一内存段可以用来存放几种不同类型的成员,但每一瞬时只有一种起作用
- 共用体变量中起作用的成员是最后一次存放的成员,在存入一个新成员后,原有的成员值会被覆盖
- 共用体变量的地址和他的各个成员的地址都是同一地址。
定义一个联合体
union var{
int a;
char b;
float c;
double d;
short f;
};
简单的使用一下
int main(void){
union var v;
v.a = 100;
printf("%d\n",v.a); 100
}
以上代码展示了如何定义一个联合体,并且简单的使用了一下,接下来,在这个联合体的基础上,展示一下他的其他特性
union var v;
v.a = 100;
v.b = 'a';
printf("%d\n",v.a);
printf("%c\n",v.b);
printf("大小:%d\n",sizeof(v));
printf("v=%p\n",&v);
printf("a=%p\n",&v.a);
printf("b=%p\n",&v.b);
运行结果:
发现给a赋值的100已经被b的值给覆盖了,
而且联合体的大小是最大元素dubble的长度8
无论是v还是v.a v.b地址都相同
如果有以下联合体,他的长度是多少
union var{
int a;
char b;
float c;
double d;
short f[6];
};
长度为16 因为最大元素类型是double,但是一个长度是8,够不了short[6]的长度,只能是8的倍数,2个8就够了
联合体的优点是,节省内存空间,如果遇到不同类型的数据,频繁的需要改变,瞬时只使用一个,这个时候就可以使用联合体
枚举
枚举是将变量的值一一列举出来,变量的值只限于列举出来的范围内
枚举类型定义:以红绿灯为例
enum color{
red,yellow,green
};
- 在枚举值表中应列出所有可用值,也成为枚举元素
- 枚举值是常量,不能在程序中用赋值语句再对他赋值
- 枚举元素本身由系统定义了一个表示序号的数值,从0开始0,1,2,3…
enum color{
red,yellow,green
}c;
int main(void){
int value;
scanf("%d",&value);
switch(value){
case red: case后面直接写枚举元素就可以
printf("红色\n");
break;
case yellow:
printf("黄色\n");
break;
case green:
printf("绿色\n");
break;
}
}
以上枚举值中,虽然没有具体描述red是几,yellow是几,但是默认的,第一个元素是0,第二个元素是1这样依次排下去
如果中间这样写
enum color{
red,yellow=20,green
};
这样red=0,yellow=20,green = 21
就是说如果中间有一个元素突然被赋值了,后面元素的值会跟在这个数后面继续加一