c语言(结构体,共用体,枚举,文件操作,makefile)
结构体,共用体,枚举,文件操作,makefile
1. 结构体
1.1 结构体的概念
结构体也是构造类型之一,由至少一个基本数据类型或构造类型组成的一种数据结构(集合),这种数据结构称之为结构体
1.2 结构体的定义
使用结构体之前,先定义结构体,然后使用这个结构体时作为一种数据类型(构造类型)
语法1:只声明结构体,【推荐使用】
struct 结构体类型的名称{
//声明成员变量,成员变量不要初始化(值)
int sid;
char name[32];
};
结构体变量的定义:struct结构体类型名 变量名;
结构体类型的变量的初始化:{按成员顺序从上到下依次赋值}
结构体类型的变量的访问:变量名.成员名
语法2:声明结构体时,同时声明结构体类型的变量
【注】结构类型的变量的内存空间随着定义而创建(栈),结构体变量的内存空间大小由成员变量决定的。
struct 结构体类型的名称{
//声明成员变量,成员变量不要初始化(值)
...
}变量名,...;
语法3:一次性声明结构体,因为没有结构体类型名,所以无法再定义新的结构体变量
struct {
//声明成员变量
}变量;
1.3 定义结构体时指定新类型名
语法:
typedef struct 结构体类型名{
成员变量;
}大写字母的新类型名称;
1.4 结构体变量赋值
1.4.1 从键盘输入数据
结构体中成员变量是可以取地址的
typedef struct stu
{
int sid;
char name[32];
}STU;
int main()
{
STU s;
scanf("%d %s",&s.sid,s.name);
}
1.4.2 清空数据
可以通过memset()清空结构体变量中成员的数据
memset(&结构体类型的变量名,0,sizeof(struct 结构类型名或typedef定义的新类型名));
typedef struct stu
{
int sid;
char name[32];
}STU;
int main()
{
STU s;
memset(&s,0,sizeof(STU));//先清空,再使用
scanf("%d %s",&s.sid,s.name);
}
1.4.3 结构体的成员是指针
如果结构体的成员是指针时,可以直接赋值(常量区的地址)或者可以从堆区中分配空间并向内存空间写数据(strcpy)
struct Data
{
int n;
char *name;
};
int main()
{
struct Data1 d1 = {1,"lucy"};
printf("%d %s\n",d1.n,d1.name);
struct Data d2;
d2.n=100;
//字符数组名 不能直接赋值,因为数组名是常量
//d2.name = "jack";//指针变量可以指向其他内容,可以修改的
d2.name = (char*)malloc(32);
strcpy(d2.name,"lucy");
printf("%d %s\n",d2.n,d2.name);
free(d2.name);//手动释放,否则会内存泄露
return 0;
}
结构体变量中的成员部分在栈区(结构体变量是在栈区分配空间),当结构体变量释放(自动释放,离开变量的作用域),堆空间需要手动释放
1.4.4 结构体嵌套赋值
给嵌套的结构体变量赋值时,由外向内引用,逐层引用
struct lrc_time
{
char hour;
char minute;
char second;
};
typedef struct lrc
{
struct lrc_time start_time;
char *content;
}LRC;
int main ()
{
//定义结构体变量并初始化值
LRC line1 = {{0,0,2},"abc..."};
LRC line2;
line2.start_time.hour=0;
line2.start_time.minute=1;
line2.start_time.second=15;
line2.content="bbb...";
}
1.4.5 结构体变量赋值引用
两个相同的结构体类型的变量可以相互的赋值(浅拷贝)。
浅拷贝:复制结构变量的内存空间(只复制第一层)
如:
struct user_s
{
int uid;
char *name;
};
struct goods_s
{
int gid;
float price;
char *name;
};
struct order_s
{
struct user_s user;
struct goods_s goods;
float order_price;
int num;
char pay_status;
}
int main()
{
struct user_s user1={1,"小明"};
struct user_s user2={2,"小红"};
struct goods_s goods1={201,6000.0f,"苹果"};
struct goods_s goods2={202,5000.0f,"华为"};
struct order_s order1={user1,goods1,6000.0f,1,1};
struct order_s order2=order1;
order2.user.name="小王";
order2.goods.name="小米";
}
浅拷贝:只复制结构体变量的内存空间,不会复制结构体指针指向的堆空间
深拷贝:将结构体变量的内存空间及所有层次的指针指向的空间也复制出来
1.5 结构体数组
语法:
struct 结构体类型名 变量名[个数];
引用:同数组操作。
struct POS
{
int x;
int y;
};
int main()
{
struct POS two_pos[2];
memset(&two_pos[0],0,sizeof(struct POS));
memset(&two_pso[1],0,sizeof(struct POS));
for(int i=0;i<2;i++)
{
printf("输入点坐标:x y");
scanf("%d %d",&two_pos[i].x,&two_pos[i].y);
}
}
1.6 结构体指针
结构体指针可以指向已存在的结构体变量,也可以在堆区动态申请空间
结构体指针变量 和一般的指针变量的用法相同
【注】结构体变量引用成员时,必须使用
->
替换.
语法:
struct 结构体类型名 *变量名;
结构体指针可以指向已存在的结构体变量
结构体指针的变量名 = &结构体变量;
在堆区动态申请空间
结构指针的变量名 = malloc(sizeof(struct 结构体类型名));
struct POS
{
int x;
int y;
}
int main()
{
//struct POS p1 = {0,0};
//struct POS *p2 = &p1;
//p2->x=2;
//p2->y=2;
struct POS*p1=malloc(sizeof(struct POS));
p1->x=(int*)malloc(sizeof(int));
p1->y=(int*)malloc(sizeof(int));
*(p1->x)=20;
*(p1->y)=30;
//回收堆内存,先结构体成员,再结构体
free(p1->x);
free(p1->y);
free(p1);
}
2. 结构体内存分配
结构体变量大小是它所有成员之和
2.1 结构体内存分配
规则1:以多少个字节为单位开辟内存【分配单位】
以成员占最大字节为单位分配的
1)只有一个char,按1B分配
2)只有一个short,按2B
3)只有一个int,float,按4B
4)只有一个long,double 按8B(64位操作系统)或按4B(32位操作系统)
【注意】
1)如果在结构体中出现了数组,数组可以看成多个变量的集合
2)如果出现指针的话,没有占字节数更大的类型的,以4或8字节为单位开辟内存
规则2:字节对齐
1)确认结构体的分配单位(最大的基本数据类型的大小)
2)成员变量的起始地址(编号),是数据类型大小的整数倍数
3)自动对齐,按分配单位对齐
4)如果成员变量是数组时,可以看成是多个结构体的成员变量
5)分配内存是按从上到下按顺序依次分配
字节对齐用空间来换时间,提高cpu读取数据的效率
指定对齐原则:可以通过#pragma a pack(value)设置结构的分配单位 value只能是:1 2 4 8
指定对齐值与数据类型对齐值相比取较小值
2.2 位段
在结构体中,以位为单位的成员,称之为位段(位域)
【注意】
1)不能对位段成员取地址
2)位段赋值时,不要超出位段定义的范围;
3)位段成员的类型必须指定为整型或字符型
4)一个位段必须存放在一个存储单元中,不能跨两个单元,第一个单元空间不能容纳下一个位段,则该空间不用,而从下一个单元起存放该位段。
位段的存储单元:
(1)char 存储单元是1个字节 (2)short int 存储单元是2个字节 (3)int 存储单元是4个字节 (4)long int 存储单元是4或8字节
5)位段的长度不能大于存储单元的长度
6)如一个段要从另一个存储单元开始,可以定义位值为0,而且不能有成员名
例1:
struct
{
char a:6;
short b:15;
int c:30;
}p1;//8B
例2:
struct
{
char a:3;
char :0;//下一个位段在新的存储单元中存储
char c:3;
}p2;//2B
例3:
struct
{
unsigned char a:2;
unsigned char :1;//占位
unsigned char b:2;//操作类型
unsigned char :1;//占位
unsigned char c:2;//操作类型
}p3;//1B
3. 共用体union
共用体也是一种构造类型的数据结构
定义共用体类型的方法和结构体非常相似,把struct改成union
几个不同的变量(成员)共同占用一段内存的结构
共用体中的成员占用同一段内存空间,共用体的大小是其占内存长度最大的成员的大小。
union data1_uni
{
char a;
short b;
int c;
}data1;
void test()
{
printf("%ld\n",sizeof(union data1_uni));//4
}
共用体的特点:
1.同一内存段可以用来存放几种不同类型的成员,但一瞬时只有一种起作用
2.共用体变量中起作用的成员是最后一次操作的成员,在修改新的成员数据后原有的成员的值会被覆盖
3.共用体变量的地址和他的各成员的地址都是同一地址
4.共用体变量的初始化,只初始化第一个成员
【注】对共用体成员进行操作时,原则上操作哪一个成员,则用哪一个成员
4. 枚举
将变量的值一一列举出来,变量的值只限于列举出来的值的范围内
枚举也是个构造类型的,用法同struct,union相似
【注】
1)枚举中的成员都是一个常量名,可以在程序中直接使用的,而且在定义时可以给定顺序值(第一个成员常量默认为0)
2)枚举变量仅能取枚举值所列元素(常量)
//一般情况下,枚举的成员名采用全大写字母
typedef enum color_enum
{
Gray=30,//默认第一个元素的常量值:0
Red,
Black,
Green,
Yellow,
Blue
}Color;
【注】在定义枚举类型的时候枚举元素可以用等号给它赋值,用来代表元素从几开始编号在程序中,不能再次对枚举元素赋值,因为枚举元素是常量
5. 文件操作
5.1 文件的概念
文件用来存放程序、文档、音频、视频、图片等数据
文件就是存放在磁盘上的,一些数据的集合
硬盘文件类型:存储在外部介质(如磁盘)上
1)文本文件(记事本、文本编辑器,以字符为单位存储)
2)二进制文件(图片、音频、视频、office文件,以位为单位存储,读取内容时,需要特定格式的软件)
设备文件类型:操作系统中把每一个与主机相连的输入、输出设备看作是一个文件
1)标准输入设备stdin,键盘 fgets(buf,size,stdin);
2)标准输出设备stdout,控制台(终端) fflush(stdout);
3)标准错误设备stderr,控制台(终端)
5.2 缓冲区的特点
io操作:输入(input)输出(output)操作
文件缓冲是库函数(c语言封装的函数)申请的一段内存,由库函数对其进行操作,程序员没有必要知道存放在哪里,只需要知道对文件操作的时候的一些缓冲特点即可。
1)行缓冲:写数据时遇到换行符,则刷新缓冲
int main(int argc,char const*argv[])
{
printf("hello,world");
while(1);
return 0;
}
如果行缓冲不刷新时,则不会输出结果,因为输出的内容没有遇到换行符(Linux\n
),Window(\r\n
)。
2)缓冲区满了:自动刷新缓冲区
void test()
{
char m[32]="abcdefghijklmnabcdefghijklmn";
while(1)
printf("%s",m);
return 0;
}
3)手动刷新缓冲区:fflush(stdout)
void test()
{
//缓冲区一般是8kb
char m[33]='abcdefghijklmnabcdefghijklmn\0';
printf("%s",m);
fflush(stdout);
while(1);
return 0;
}
4)程序正常结束 会刷新缓冲区
void test()
{
char m[33]='abcdefghijklmnabcdefghijklmn\0';
printf("%s",m);
return 0;
}
5.3 缓冲的分类
行缓冲:遇到换行符则刷新缓冲
全缓冲:
标准io库函数,往普通文件读写数据的,是全缓冲的
碰到换行符也不刷新缓冲区,只有缓冲区满了,才会刷新缓冲区
无缓冲:
使用系统调用的io函数操作文件时,直接与文件进行交互,中间没有缓冲区
【读写文件的流程】
应用程序空间->内核空间->驱动程序->硬盘上
应用程序和内核程序运行在不同的空间里,目的是为了保护内核。
5.4 文本文件与二进制的比较
译码
文本问价编码基于字符定长,译码容易些;
二进制文件编码是变长的,译码难一些(不同的二进制文件格式,有不同的译码方式)
空间利用率
二进制文件用一个比特来代表一个意思(位操作)
而文本文件任何一个意思至少是一个字符
二进制文件,空间利用率高
可读性
文本文件用通用的记事本工具就几乎可以浏览所有文本文件
二进制文件需要一个具体的文件解码器,比如读BMP文件,必须用读图软件
总结
文件在硬盘上存储的时候,物理上都是用二进制来存储的
标准io库函数,对文件操作的时候,不管文件的编码格式(字符编码,二进制),而是按字节对文件进行读写,所以文件又叫流式文件,即把文件看成一个字节流
5.5 文件操作的相关库函数
文件指针在程序中用来标识(代表)一个文件的,在打开文件的时候得到文件指针。对文件进行读、写、关闭等操作的时候,对文件指针进行操作即可。
FILE在stdio.h文件中的文件类型声明:
typedef struct
{
short level;//缓冲区"满"或"空"的程度
unsigned flags;//文件状态标志
char fd;//文件描述符
unsigned char hold;//如无缓冲区不读取字符
short bsize;//缓冲区的大小
unsigned char *buffer;//数据缓冲区的位置
unsigned char *curp;//指针,当前的指向
unsigned int istemp;//临时文件,指示器
short token;//用于有效性检查
}FILE;
定义文件指针的一般形式为:FILE * 指针变量标识符
打开与关闭文件:
FILE * fopen (const char * path,const char *mode);
path:打开文件的路径(相对的,绝对的)
mode:打开文件的模式(r,r+,rb,rb+,w,w+,wb,wb+,a,a+,ab+)
r开头的模式,只读或读写,文件不存在时,会报错
w开头的模式,写写或读写,文件不存在时,会自动创建。w模式会清空文件内容
a开头的模式,追加(写),文件不存在时,会自动创建。打开文件之后光标会移到内容的尾部。
+模式和r,w,a组合使用,表示扩展读写的
b模式和r,w,a组合使用,表示以二进制的格式(图片、pdf、音频)等。
打开文件成功时,返回文件指针,反正,返回NULL
int fclose(FILE*fp);关闭文件,成功返回0,反之非0(-1)
一次一个字符读和写:
int fgetc(FITL*stream);从文件中读取一个字符
返回值:1)t模式:读到文件结尾返回EOF(符号常量 -1)
2)b模式:读到文件结尾,使用feof()判断
int fputc(int c,FILE*stream);向文件中写一个字符。如果写入字节的值,反之返回EOF
一次一个字符串读和写:
char *fgets(char *s,int size,FILE *stream);
从stream所指的文件中读取字符,在读取的时候碰到换行符或者是碰到文件的末尾停止读取,或者是读取了size-1个字节停止读取,在读取的内容后面会加一个\0,作为字符串的结尾。
成功返回目的数组的首地址,即s
失败返回NULL
int fputs(const char*s,FILE *stream);
将s指向的字符串,写到stream所代表的文件中
成功返回1,失败-1 EOF
按块的方式读和写:
size_t fread(void *ptr,size_t size,size_t nmemb,FILE *stream);
从stream文件中读取数据,一块是size个字节,共nmemb块,存放到ptr指向的内存空间。
size_t fwrite(void *ptr,size_t size,size_t nmemb,FILE *stream);
将ptr指向的内存空间的nmemb块的size的大小写入到文件中。
格式化写与读:
fprintf(FILE *file,char *format,...);
fscanf(FILE *file,char *format,...);
随机读取:
rewind(FILE *file);光标位置的复位
long ftell(FILE *file);获取当前光标的位置(相对于开始偏移了多少个字节),返回字节数
int fseek(FILE *stream,long offset,int whence);移动文件流的读写位置
whence:
文件开头 SEEK_SET 0
文件当前位置 SEEK_CUR 1
文件末尾 SEEK_END 2
6. makefile
6.1 make和makefile区别
make是个命令,是个可执行程序,用来解析Makefile文件的命令
makefile是个文件,这个文件中描述了项目资源的编译规则。
采用Makefile的好处
1)简化编译程序的时候输入的命令,编译的时候只需要敲make命令就可以了
2)可以节省编译时间,提高编译效率
【自动检查源文件哪些发生了变化,只有被修改的源文件才会再次编译】
6.2 make的概念
GNU make是一种代码维护工具,make工具会根据makefile文件定义的规则和步骤,完成整个软件项目的代码维护工作。
makefile文件:在Window上的集成开发环境下,会自动生成,在Linux下需要手动编写。
make主要解决两个问题:
1)大量代码的关系维护
2)减少重复编译时间
make的命令:自动从当前工作目录下找makefile文件(GNUmakefile、makefile、Makefile)
make 编译目标
make -f makefile文件名[makefile中第一个编译目标]
检查是否存在make命令:
which make
make --version
6.3 makefile语法规则
语法:
[变量名=变量值]
[...]
编译目标: [依赖文件列表]
<tab>命令
定义变量的方式:
在makefle编译目标的上面,可以定义一些变量,变量在下方使用时,可以通过$(变量名)或${变量名}引用。一个变量占一行
依赖文件列表
多个依赖文件使用空格分隔
一个编译目标可以不需要依赖文件,只是执行目标的命令
命令
编译c文件或库文件等常用Linux命令,如:gcc,ar,rm
【注】如果存在多个命令时,每一个命令占一行
项目编译的命令
make
make main
make clean
6.4 makefile中的变量
makefile中的变量可以在编译目标内的命令中使用
在makefile中的变量分三种:
1)环境变量
在执行make命令编译makefile之前,会将系统的环境变量加载到makefile中
2)预定义变量
是makefile中特有的变量:
$@ 表示编译目标
$< 表示依赖文件列表中的第一个
$^ 表示依赖文件列表中去重之后的内容
3)自定义变量
注意:1.变量名是大小写敏感的
2.变量名一般都在makefile的头部定义(编译目标上方)
3.变量名几乎可在makefile的任何地方使用
makefile中可以使用通配符:%(表示任意多或零个字符),%在编译的目标中使用时,可能会被执行多次,每一次一个目标。
如:
cc=gcc
obj=main.o link.o
main:$(obj)
$(cc) $^ -o $@
%.o:%.c
$(cc) -c $< -o $@
clean:
rm -rf *.o main
6.5 makefile中常用的预定量
AR 归档维护程序的程序名,默认值是ar
ARFLAGS 归档维护程序的选项
AS 汇编程序的名称,默认值为as
ASFLAGS 汇编程序的选项
CC C 编译器的名称,默认为CC,软链接到/usr/bin/gcc上
CFLAGS C 编译器的选项
CPP C 预编译的名称,默认值为$(CC) -E
CPPFLAGS C 预编译的选项
CXX C++编译器的名称,默认值为g++
CXXFLAGS C++编译器的选项
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具