一、简述
对结构体、共用体的认识。
结构体是一种自定义的复合数据类型。
类比数组,数组也算一种复合数据类型,数组是由多个由相同数据类型的元素组成,(比如需要记录描述100个人的年龄,此时只需int arr[100]即可,不需一个一个的声明);结构体可由多中数据类型的元素组成(将不同的数据类型组合成为一个整体),比如描述一个学生信息,学生的学号使用long int类型存储、年龄用int类型存储、姓名用字符串(字符数组)存储。如果是一个学生,可以只需声明3个变量即可(long int id;int age;char* name[8]),要是100个学生,如此一个学生就得对应声明3个变量明显是不可行的;可以设想一下,能不能有一种数据类型是专门用来存储学生信息的?能,结构体就可以派上用场,结构体是一种自定义的数据类型,我们可以自己定义一种叫Student的数据类型,这种类型可以同时存放一个long int数据、int数据、字符串。如此,就可以声明Struct Student stu[100];来存放100个学生的数据了。
二、结构体的定义
struct student
{
long id;
int age;
char name[8];
float score[4];
};
三、声明结构体变量
3.1普通方式 (注意声明变量时不能省略struct关键字,C++可以省略)
struct student stu1; //定义了一个student类型的变量stu1
struct student stu1,stu2; //声明结构体变量stu1,stu2
3.2 在定义的时候声明
1 定义结构体 同时 声明变量
struct student
{
long id;
int age;
char name[8];
float score[4];
}stu1,stu2; //在结构体定义时,同时声明两个这种结构体变量stu1,stu2
2 定义结构体 同时 声明变量时直接定义(暂且把这里的定义等同于后面写的初始化)
struct student
{
long id;
int age;
char name[8];
float score[4];
/*这种方式不环保,只能用一次*/
} a={21,80,'Liang',0.5,1.5,2.5,3.5};
3 省略结构体类型名,声明变量时直接定义 ,这种方式最烂
struct
{
long id;
int age;
char name[8];
float score[4];
} t={21,80,'Liang',0.5,1.5,2.5,3.5};
4 省略结构体名,直接声明变量
struct
{
long id;
int age;
char name[8];
float score[4];
}stu1,stu2;
- 注意这里不再是定义声明结构体类型,而是直接创建结构体变量了,这个编译器会分配内存的;属于省略结构体名直接定义结构体类型的变量
- 这样的确可以省略标识符也就是结构体名,但是只能使用一次;因为这是;声明结构体的过程和定义结构体变量的过程和在了一起;并且个成员变量没有初始化的;
- 如果你想多次使用一个结构体模块,这样子是行不通的;
- 这种方式不能指明结构体类型名而是直接定义结构体变量,并且在值定义一次结构体变量时适用,无结构体名的结构体类型是无法重复使用的。
5 比较
struct student {
...
}stu1,stu2; //定义结构体同时声明变量
struct{
...
}strc1,strc2; //省略结构体名直接定义结构体类型的变量
这两种用法使结构体丧失了通用性,特别是省略结构体名的做法,
这样结构体就不能在源代码其他部分声明更多的结构体变量。在编
写大型程序的源码时,结构体定义部分通常放在头文件中,使用
时包含该头文件即可,这样一个结构体不需要程序的不同文件中
反复定义。
四、结构体的初始化
- 关于结构体初始化和存储类时期的问题;如果要初始化一个具有静态存储时期的结构体,初始化项目列表中的值必须是常量表达式;
- 注意如果在定义结构体变量的时候没有初始化,那么后面就不能全部一起初始化了,只能单个赋值了
4.1
struct student
{
long id;
int age;
char name[8];
float score[4];
}
/这样是可以的,在定义变量的时候就初始化了;
struct student stu1={//对结构体初始化
001.001,
10,
"guojiajiaoyun",
87.5,
70.5,
93,
91
};
//这种就不行了,在定义变量之后,若再要对变量的成员赋值,那么只能单个赋值了;
//这样就是不行的,只能在定义的时候初始化才能全部赋值,之后就不能再全体赋值了,只能单个赋值;
struct student stu1;
stu1={
001.001,
10,
"guojiajiaoyun",
87.5,
70.5,
93,
91
};
只能:
stu1.id = 001.001;........//单个赋值
stu1.age = 18;............//单个赋值
stu1.name = "Tom".........//单个赋值
stu1.score[0] = 87.5......//单个赋值
stu1.score[1] = 70.5......//单个赋值
stu1.score[2] = 93........//单个赋值
stu1.score[3] = 91........//单个赋值
4.2先声明结构体变量,后初始化。
struct student stu1;
stu1.id = 12345; //通过成员运算符'.'来访问结构体的成员变量
stu1.age = 20;
stu1.name="Liang";
stu1.score={0.5,1.5,2.5,3.5}
strcpy(stu1.age,"Liang"); //因为数组在非初始化时,不能直接通过数组名直接赋值,strcpy函数需要包含头文件string.h 错误的写法:stu1.name = "Liang";
4.2在声明结构体变量时同时初始化,类似于数组初始化。
struct student stu1;
等效于
struct student{
long id;
int age;
char name[8];
float score[4];
} stu1;
声明结构体变量并初始化
声明结构体变量并初始化
struct student stu1 = {12345,22,"Liang",87.5,70.5,93,91};
#include <stdio.h>
#include <string.h>
struct student
{
long int id;
int age;
char name[8];
};
int main(int argc,char* argv[])
{
struct student stu1 = {12345,22,"Liang",87.5,70.5,93,91};
printf("id=%ld age=%d name=%s \n",stu1.id,stu1.age,stu1.name);
return 0;
}
五、结构体成员变量的访问
1、可使用成员运算符'.'来访问结构体成员变量。例如stu1.age = 10;int age = stu1.id;
2、可以使用结构体指针结合"->"来访问及饿哦固体成员变量。例如struct student *sp = &stu1;sp->id = 12345;
也可以 (*sp).age = 20;注意成员运算符'.'的优先级高于指针操作符'*'。
Windows环境下
Linux环境下
六、结合使用typedef
使用typedef为自定义结构体定义别名,在声明变量时可以省略struct关键字。
用typedef定义新类型名来代替已有类型名,即给已有类型重新命名;
一般格式为:
- typedef 已有类型 新类型名;// typedef int Elem;
- typedef 已有类型 新类型名{ ... };
typedef struct student
{
long id;
int age;
char name[8];
float score[4];
}Student,*StuPtr;
typedef struct{
long id;
int age;
char name[8];
float score[4];
}STUDENT;
STUDENT stu1,stu2;
也可以这样表示定义了一个指向该结构体的指针p,其p指针可以指向s1,s2,或者任何其他的student结构体变量。
(结构体指针->成员 = 结构体变量.成员)
typedef struct student
{
long int id;
int age;
char name[8];
}
//定义了一个指向该结构体的指针p,其p指针可以指向s1,s2,或者任何其他的student结构体变量。
(结构体指针->成员 = 结构体变量.成员)
typedef struct student s1,s2;
+
typedef struct student *p;
typedef struct student* p;
||
typedef struct student s1,s2,*p;
windows环境下
linux环境下
七、结构体的大小
typedef struct
{
char addr;
char name;
int id;
}PERSON;
printf( " PERSON长度=%d字节\n " , sizeof( PERSON ) );
printf("size of struct man:%d\n",sizeof (struct man));
- 定义含有20个char元素的数组
char ss[20]={0x10,0x11,0x12,0x13,0x14,0x15,0x16,0x17,0x18,0x19,0x20,0x21,0x22,0x23,0x24,0x25,0x26,0x27,0x28,0x29};
- 定义结构体类型的指针ps指向ss数组
PERSON *ps=(PERSON *)ss;
- 打印出各个成员
printf("0x%02x,0x%02x,0x%02x\n",ps->addr,ps->name,ps->id);printf("PERSON长度=%d字节\n",sizeof(PERSON));
-
可以看到addr和name都只占一个字节,但是未满4字节,跳过2字节后才是id的值,这就是4字节对齐。结构体成员有int型,会自动按照4字节对齐。
- 把结构体成员顺序调换位置
typedef struct { char addr; int id; char name; }PERSON;
-
再把结构体成与阿奴顺序调换位置
typedef struct { int id; char addr; char name; }PERSON;
字节对齐以提高CPU效率。
比如我的机器是32位,CPU一次最大访问数据位数是32位(也就是4字节,那么默认4字节对齐)。
struct ss
{
char c;
int a;
}s1={'a',0x12345678};
int main(int argc,char* argv[])
{
printf("%d \n",sizeof(s1));
return 0;
}
结构体会有内存对齐现象,所以所有结构体成员的大小之和并不一定是结构体的大小,结构体变量占据的内存单元的个数应当大于等于其内部所有数据成员占据内存单元数的和。
一般来说,不同的编译器字节对齐机制有所不同。字节对齐的细节和具体编译器实现相关,但一般而言,满足三个准则:
1) 结构体变量的首地址能够被其最宽基本类型成员的大小所整除;
2) 结构体每个成员相对于结构体首地址的偏移量都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节;
3) 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节。
4) 最宽基本类型的概念,所谓基本类型是指像char、short、int、float、double这样的内置数据类型。“数据宽度”就是指其sizeof的大小。诸如结构体、共用体和数组等都不是基本数据类型。
结构体、共用体结合使用时。
1、同类型的结构体之间可以直接赋值
2、结构体嵌套
3、匿名结构体
匿名结构体只能再定义结构体的同时声明变量,定义之后无法再声明变量。应用场景:比如限定只有一个超级用户
但是有个例外,使用typeof关键字来获取类型,然后可以声明这种变量。
4、用结构体变量作参数----多值传递,因为函数有着副本机制,形参相当于实参的副本,当数据量很大时,效率较低。(数组最为函数参数时,会退化成为一个指针,效率高)
5、用结构体成员变量的地址求出结构体的首地址
#include <stdio.h>
#define GET_ENTRY(ptr,type,member)\
(type *)((char*)(ptr)-(unsigned long)(&(((type*)0)->member)));
struct test
{
char ch;
int num;
double db;
char arr[22];
};
int main(int argc,char* argv[])
{
struct test s1 = {'a',22,33.0,"hello world"};
printf("addr=%p num=%d \n",&s1,s1.num);
double *dptr = &s1.db;
struct test *sp = GET_ENTRY(dptr,struct test,db);
//带参宏GET_ENTRY(dptr,struct test,db)展开--》(struct test *)((char*)(dp)-(unsigned long)(&(((struct test*)0)->db)));
printf("addr=%p num=%d \n",sp,sp->num);
return 0;
}
八、结构体数组
结构体数组的定义和使用
当需要使用大量相同的结构体变量时候,可使用结构体定义数组,该数组包含与结构体相同的数据结构所组成的连续存储空间。如下例所示
struct student stu_a[50]; //声明长度为50的结构体数组stu_a
引用结构体数组中的元素的一般形式为
结构体数组名[n].成员名
同理,如果该成员是结构体变量,引用形式为
结构体数组名[n].成员名[n].子成员
九、结构体指针
结构体指针的定义方法
定义结构体指针:
struct student *p1;
或者在定义结构体时候直接声明指针:
struct student
{
int num;
char name[20];
char sex;
}*p2;
结构体指针的访问变量方法
1)p->结构体成员;
2)(*p).结构体成员;
两个特殊的结构体指针访问方法
1)p->num++
2)p++ ->num
#include <stdio.h>
#include <stdlib.h>
//定义结构体
struct student
{
int num;
char name[20];
char sex;
};
int main()
{
//定义结构体数组
struct student sarr[3] = { 1001,"wr1",'m',1003,"wr2",'m',1005,"wr3",'m' };
struct student *p;//定义结构体指针
p = sarr;//数组本身就是地址,不用取地址
int num;
num = p->num++;//num=p->num;p->num=p->num+1,优先级->的优先级高于++
printf("num=%d,p->num=%d\n",num,p->num);
//p->num上一步已经加一
num = p++->num;//num=p->num,p=p+1
printf("num=%d,p->num=%d\n", num, p->num);
return 0;
}
指针指向的是数组的首地址,指针+1=下一个数组的首地址 ,故输出结果第二行是1003
十、共用体
共用体关键字union;
共用体也叫联合体,使几个不同类型的变量共占一段内存(相互覆盖),也就是说共用体的成员共用一片内存,后赋值的成员变量的数据才是共用体的生效数据,因为前面的赋值已经被覆盖了。共用体所占内存至少能够容纳最大的成员变量所需的空间,应用场景,比如需要一种既可以存储int型数据也可以存储double型数据的变量。比如识别设备,如果是U盘我要这样读取,如果是手机我又要这样读取……
共用体的大小
共用体与结构体很多方面都很相似,比如定义、声明变量、初始化、赋值等等可以参考结构体部分。
8. 1共用体定义
union uu
{
int i;
double db;
};
8.2声明共用体变量 、初始化、赋值。(后赋值的成员变量会覆盖前面赋值的成员的数据)
九 函数的参数是结构体指针型
int get_video(char **name, long *address, int *size, time_t *time, int *alg)
{
...
}
int handle_video(char *name, long address, int size, time_t time, int alg)
{
...
}
int send_video(char *name, long address, int size, time_t time, int alg)
{
...
}
上述C语言代码定义了三个函数:get_video() 用于获取一段视频信息,包括:视频的名称,地址,大小,时间,编码算法。
然后 handle_video() 函数根据视频的这些参数处理视频,之后 send_video() 负责将处理后的视频发送出去。下面是一次调用:
char *name = NULL;
long address;
int size, alg;
time_t time;
get_video(&name, &address, &size, &time, &alg);
handle_video(name, address, size, time, alg);
send_video(name, address, size, time, alg);
从上面这段C语言代码来看,为了完成视频的一次“获取”——“处理”——“发送”操作,C语言程序不得不定义多个变量,并且这些变量需要重复写至少三遍。
虽说C语言程序的代码风格因人而异,但是“重复的代码”永远是应尽力避免的,原因本专栏已经分析多次。不管怎么说,每次使用这几个函数,都需要定义很多临时变量,总是非常麻烦的。所以,这种情况下,完全可以使用C语言的结构体语法:
struct video_info{
char *name;
long address;
int size;
int alg;
time_t time;
};
定义好 video_info 结构体后,上述三个C语言函数的参数可以如下写,请看:
int get_video(struct video_info *vinfo)
{
...
}
int handle_video(struct video_info *vinfo)
{
...
}
int send_video(struct video_info *vinfo)
{
...
}
修改后的C语言代码明显精简多了,在函数内部,视频的各个信息可以通过结构体指针 vinfo 访问,例如:
printf("video name: %s\n", vinfo->name);
long addr = vinfo->address;
int size = vinfo->size;
事实上,使用结构体 video_info 封装视频信息的各个参数后,调用这几个修改后的函数也是非常简洁的:
struct video_info vinfo = {0};
get_video(&vinfo);
handle_video(&vinfo);
send_video(&vinfo);
从上述C语言代码可以看出,使用修改后的函数只需定义一个临时变量,整个代码变得非常精简。
读者应该注意到了,修改之前的 handle_video() 和 send_video() 函数原型如下:
int handle_video(char *name, long address, int size, time_t time, int alg);
int send_video(char *name, long address, int size, time_t time, int alg);
根据这段C语言代码,我们知道 handle_video() 和 send_video() 函数只需要读取参数信息,并不再修改参数,那为什么使用结构体 video_info 封装数据,修改后的 handle_video() 和 send_video() 函数参数是 struct video_info *指针型呢?
int handle_video(struct video_info *vinfo);
int send_video(struct video_info *vinfo);
既然 handle_video() 和 send_video() 函数只需要读取参数信息,那我们就无需再使用指针型了呀?的确如此,这两个函数的参数直接使用 struct video_info 型也是可以的:
int handle_video(struct video_info vinfo)
{
...
}
int send_video(struct video_info vinfo)
{
...
}
似乎这种写法和使用 struct video_info *指针型 参数的区别,无非就是函数内部访问数据的方式改变了而已。但是,如果读者能够想到我们之前讨论过的C语言函数的“栈帧”概念,应该能够发现,使用指针型参数的 handle_video() 和 send_video() 函数效率更好,开销更小。
附录:
参考链接:
C语言结构体与结构体指针的使用_wrlovesmile的博客-CSDN博客_c语言结构体和指针
文章2
参考链接:
指向结构体变量的指针
前面我们通过“结构体变量名.成员名”的方式引用结构体变量中的成员,除了这种方法之外还可以使用指针。
前面讲过,&student1 表示结构体变量 student1 的首地址,即 student1 第一个项的地址。如果定义一个指针变量 p 指向这个地址的话,p 就可以指向结构体变量 student1 中的任意一个成员。
那么,这个指针变量定义成什么类型呢?只能定义成结构体类型,且指向什么结构体类型的结构体变量,就要定义成什么样的结构体类型。比如指向 struct STUDENT 类型的结构体变量,那么指针变量就一定要定义成 struct STUDENT* 类型。
下面将前面的程序用指针的方式修改一下:
# include <stdio.h>
# include <string.h>
struct AGE
{
int year;
int month;
int day;
};
struct STUDENT
{
char name[20]; //姓名
int num; //学号
struct AGE birthday; //生日
float score; //分数
};
int main(void)
{
struct STUDENT student1; /*用struct STUDENT结构体类型定义结构体变量student1*/
struct STUDENT *p = NULL; /*定义一个指向struct STUDENT结构体类型的指针变量p*/
p = &student1; /*p指向结构体变量student1的首地址, 即第一个成员的地址*/
strcpy((*p).name, "小明"); //(*p).name等价于student1.name
(*p).birthday.year = 1989;
(*p).birthday.month = 3;
(*p).birthday.day = 29;
(*p).num = 1207041;
(*p).score = 100;
printf("name : %s\n", (*p).name); //(*p).name不能写成p
printf("birthday : %d-%d-%d\n", (*p).birthday.year, (*p).birthday.month, (*p).birthday.day);
printf("num : %d\n", (*p).num);
printf("score : %.1f\n", (*p).score);
return 0;
}
输出结果是:
name : 小明
birthday : 1989-3-29
num : 1207041
score : 100.0
我们看到,用指针引用结构体变量成员的方式是:
(*指针变量名).成员名
注意,*p 两边的括号不可省略,因为成员运算符“.”的优先级高于指针运算符“*”,所以如果 *p 两边的括号省略的话,那么 *p.num 就等价于 *(p.num) 了。
从该程序也可以看出:因为指针变量 p 指向的是结构体变量 student1 第一个成员的地址,即字符数组 name 的首地址,所以 p 和 (*p).name 是等价的。
但是,“等价”仅仅是说它们表示的是同一个内存单元的地址,但它们的类型是不同的。指针变量 p 是 struct STUDENT* 型的,而 (*p).name 是 char* 型的。所以在 strcpy 中不能将 (*p).name 改成 p。用 %s 进行输入或输出时,输入参数或输出参数也只能写成 (*p).name 而不能写成 p。
同样,虽然 &student1 和 student1.name 表示的是同一个内存单元的地址,但它们的类型是不同的。&student1 是 struct STUDENT* 型的,而 student1.name 是 char* 型的,所以在对 p 进行初始化时,“p=&student1;”不能写成“p=student1.name”。因为 p 是 struct STUDENT* 型的,所以不能将 char* 型的 student1.name 赋给 p。
此外为了使用的方便和直观,用指针引用结构体变量成员的方式:
(*指针变量名).成员名
可以直接用:
指针变量名->成员名
来代替,它们是等价的。“->”是“指向结构体成员运算符”,它的优先级同结构体成员运算符“.”一样高。p->num 的含义是:指针变量 p 所指向的结构体变量中的 num 成员。p->num 最终代表的就是 num 这个成员中的内容。
下面再将程序用“->”修改一下:
# include <stdio.h>
# include <string.h>
struct AGE
{
int year;
int month;
int day;
};
struct STUDENT
{
char name[20]; //姓名
int num; //学号
struct AGE birthday; /*用struct AGE结构体类型定义结构体变量birthday, 生日*/
float score; //分数
};
int main(void)
{
struct STUDENT student1; /*用struct STUDENT结构体类型定义结构体变量student1*/
struct STUDENT *p = NULL; /*定义struct STUDENT结构体类型的指针变量p*/
p = &student1; /*p指向结构体变量student1的首地址, 即第一项的地址*/
strcpy(p->name, "小明");
p->birthday.year = 1989;
p->birthday.month = 3;
p->birthday.day = 29;
p->num = 1207041;
p->score = 100;
printf("name : %s\n", p->name); //p->name不能写成p
printf("birthday : %d-%d-%d\n", p->birthday.year, p->birthday.month, p->birthday.day);
printf("num : %d\n", p->num);
printf("score : %.1f\n", p->score);
return 0;
}
输出结果是:
name : 小明
birthday : 1989-3-29
num : 1207041
score : 100.0
但是要注意的是,只有“指针变量名”后面才能加“->”,千万不要在成员名如 birthday 后面加“->”。
综上所述,以下 3 种形式是等价的:
- 结构体变量.成员名。
- (*指针变量).成员名。
- 指针变量->成员名。
其中第 3 种方式很重要,通常都是使用这种方式,另外两种方式用得不多。后面讲链表的时候用的也都是第 3 种方式。
指向结构体数组的指针
在前面讲数值型数组的时候可以将数组名赋给一个指针变量,从而使该指针变量指向数组的首地址,然后用指针访问数组的元素。结构体数组也是数组,所以同样可以这么做。
我们知道,结构体数组的每一个元素都是一个结构体变量。如果定义一个结构体指针变量并把结构体数组的数组名赋给这个指针变量的话,就意味着将结构体数组的第一个元素,即第一个结构体变量的地址,也即第一个结构变量中的第一个成员的地址赋给了这个指针变量。比如:
# include <stdio.h>
struct STU
{
char name[20];
int age;
char sex;
char num[20];
};
int main(void)
{
struct STU stu[5] = {{"小红", 22, 'F', "Z1207031"}, {"小明", 21, 'M', "Z1207035"}, {"小七", 23, 'F', "Z1207022"}};
struct STU *p = stu;
return 0;
}
此时指针变量 p 就指向了结构体数组的第一个元素,即指向 stu[0]。我们知道,当一个指针指向一个数组后,指针就可以通过移动的方式指向数组的其他元素。
这个原则对结构体数组和结构体指针同样适用,所以 p+1 就指向 stu[1] 的首地址;p+2 就指向 stu[2] 的首地址……所以只要利用 for 循环,指针就能一个个地指向结构体数组元素。
同样需要注意的是,要将一个结构体数组名赋给一个结构体指针变量,那么它们的结构体类型必须相同。
下面编写一个程序:
# include <stdio.h>
struct STU
{
char name[20];
int age;
char sex;
char num[20];
};
int main(void)
{
struct STU stu[3] = {{"小红", 22, 'F', "Z1207031"}, {"小明", 21, 'M', "Z1207035"}, {"小七", 23, 'F', "Z1207022"}};
struct STU *p = stu;
for (; p<stu+3; ++p)
{
printf("name:%s; age:%d; sex:%c; num:%s\n", p->name, p->age, p->sex, p->num);
}
return 0;
}
输出结果是:
name:小红; age:22; sex:F; num:Z1207031
name:小明; age:21; sex:M; num:Z1207035
name:小七; age:23; sex:F; num:Z1207022
此外同前面“普通数组和指针的关系”一样,当指针变量 p 指向 stu[0] 时,p[0] 就等价于 stu[0];p[1] 就等价于 stu[1];p[2] 就等价于 stu[2]……所以 stu[0].num 就可以写成 p[0].num,其他同理。下面将上面的程序用 p[i] 的方式修改一下:
# include <stdio.h>
struct STU
{
char name[20];
int age;
char sex;
char num[20];
};
int main(void)
{
struct STU stu[3] = {{"小红", 22, 'F', "Z1207031"}, {"小明", 21, 'M', "Z1207035"}, {"小七", 23, 'F', "Z1207022"}};
struct STU *p = stu;
int i = 0;
for (; i<3; ++i)
{
printf("name:%s; age:%d; sex:%c; num:%s\n", p[i].name, p[i].age, p[i].sex, p[i].num);
}
return 0;
}
输出结果是:
name:小红; age:22; sex:F; num:Z1207031
name:小明; age:21; sex:M; num:Z1207035
name:小七; age:23; sex:F; num:Z1207022