C语言-结构、预处理及文件操作
一、结构:
什么是结构:
是一种由程序员设计的复合数据类型,它由若干个其它类型的成员组成,用于统一描述事物的各项属性。
使用各类型的变量也可以描述事物的各项属性(如:通讯录项目),但使用麻烦且容易出错,没有使用结构方便,安全性高、统一性高,同时结构也是面向对象编程的基础。
基础C语言编程思想:面向过程
设计结构:
struct 结构名 { // 结构名一般首字母大写
成员类型 成员名;
...
};
初始化结构变量:
// 顺序初始化,数据要与成员的顺序一一对应。
struct 结构名 结构变量名 = {v1,v2,v3,...};
struct 结构名 结构数组[n] = {
{v1,v2,v3,...},
{v1,v2,v3,...},
...
};
// 指定成员初始化
// 初始化顺序无所谓 没有初始化的成员会默认为0
struct 结构名 结构变量名 = {
.成员名1 = 初始化数据1,
.成员名2 = 初始化数据2,
...
};
struct 结构名 结构数组[n] = {
{.成员名1 = 初始化数据1,.成员名2 = 初始化数据2,...},
{.成员名1 = 初始化数据1,.成员名2 = 初始化数据2,...},
{.成员名1 = 初始化数据1,.成员名2 = 初始化数据2,...},
...
};
使用typedef
重定义简短的类型名:在C语言中,struct 结构名
才是完整的数据类型名,但使用时比较麻烦,可以使用typedef
给结构重定义简短的类型名。
// 结构设计完成后重定义
typedef struct 结构名 结构类型名;
typedef struct Contacts Contacts;
// 设计结构时重定义
typedef struct 结构名 {
成员类型 成员名;
...
} 结构名;
typedef struct Contact {
...
} Contact;
Contact con;
计算结构的总字节数:
1、结构变量的总字节数 >= 所有成员的字节数之和
2、结构成员的顺序会影响结构的总字节数
3、了解结构总字节数的计算规则,可以通过合理安排结构的成员顺序,从而达到节约内存的目的
4、计算机为了提高结构成员的访问速度,会在成员之间以及结构内存的末尾填充一些空闲内存,称为内存对齐、内存补齐行为
内存对齐:
假定从0字节排列结构的第一个成员,之后所有成员的起始字节数,必须是成员本身字节数的整数倍,如果不是则填充一些空闲字节,直到是为止。
struct Data {
char c; // 0
// 1 2 3 空闲字节
int i; // 4 5 6 7
double d; // 8 9 10 11 12 13 14 15
} Data;
内存补齐:
结构的总字节数必须是它最大成员字节数的整数倍,如果不是则在结构的末尾填充一些空闲字节。
struct Data {
char c; // 0
// 1 2 3 空闲字节
int i; // 4 5 6 7
double d; // 8 9 10 11 12 13 14 15
char c1; // 16
// 总字节为17,不能被4整除,所以会在末尾填充三个空闲字节
// 17 18 19 因此Date的总字节数是20
} Data;
注意:在32位系统下,内存对齐、内存补齐字节数是有上限的,超过上限按4字节计算。
// 在Windows32位系统下,超过8字节按4字节计算。
typedef struct Data {
char ch; // 0
// 1 2 3
long double num; // 4 ~ 15
short sh; // 16 17
// 18 19
} Data;
// 在Windows32位系统下,未超过8字节,按成员的实际字节对齐补齐
typedef struct Data {
char ch; // 0
// 1 2 3 4 5 6 7
double num; // 8 ~ 15
short sh; // 16 17
// 18 19 20 21 22 23
} Data;
// Windows64位系统和Linux64位系统,都按成员的字节数计算内存对齐、内存补齐
typedef struct Data {
char ch; // 0
// 1 2 3 4 5 6 7 8 9 11 12 13 14 15
long double num; // 16 ~ 31
short sh; // 32 33
// 34 35 36 37 38 39 40 41 42 42 44 45 46 47
} Data;
注意:如果结构成员是数组类型,那么计算对齐补齐时,应当选择该数组的成员类型字节数计算
struct Data {
char c; // 0
char str[20]; // 1~20
};
// Data的总字节数是21 而不是24
long类型的字节数:
Linux32系统 4字节
Linux32系统 8字节
Windows32系统 4字节
Windows64系统 4字节
注意:一般结构变量、结构数组所占用的连续内存可能较大,所以建议存储在堆内存
结构成员的位域:
早期由于计算机内存资源比较匮乏,一种节约内存的方式。
// 设计结构时重定义
typedef struct 结构名 {
成员类型 成员名:n; // 设置该成员只使用n个二进制位
...
} 结构名;
联合:union
也是一种由程序员设计的复合数据类型,使用语法与结构一模一样,与结构不同的是,结构中的成员各自拥有独立的内存,而联合中的所有成员共用一块内存(也叫共用体),所以只要有一个成员的值发生变化,其它成员的也会跟着一起变化。
设计联合:
union 联合名 {
成员类型 成员名;
...
};
*联合的总字节数:
由于联合的所有成员共用一块内存,所有成员是天然对齐的,不需要考虑内存对齐,但要考虑内存补齐。
情况1:所有联合的成员都是基本类型,则联合的总字节数就是最大成员的字节数。
union D {
char c;
int i;
double d;
};
情况2:如果联合的成员有数组类型,则联合的总字节数应该是最大成员的整数倍。
union D {
char ch[5];
int n;
}; // 总字节是8,在末尾内存补齐了3个空白字节
使用联合的意义:
- 使用少量的内存对应若干个标识符,只要不同时使用联合的成员,就不会冲突,能大大节约内存,在早期计算机内存比较小时,该技术使用较多,现在随着计算机内存越来越大已经基本不再使用。
- 联合可以对一块内存进行不同格式的解释,所以在网络通信时还存在着少量的使用(使用网络协议中已经设计好的联合体)。
大端系统和小端系统:
大端系统:
低位数据存储高位地址,或者说是高位数据存储在低位地址,一般大型的服务器、网络设备采用的是大端系统,所以大端格式也叫网络字节序。
int num = 0xa1b2c3d4;
0xbf9f3828 存储的是0xa1
0xbf9f3829 存储的是0xb2
0xbf9f382a 存储的是0xc3
0xbf9f382b 存储的是0xd4
小端系统:
低位数据存储在低位地址,或者说高位数据存储在高位地址,一般的个人计算机采用的是小端系统。
int num = 0xa1b2c3d4;
0xbf9f3828 存储的是0xd4
0xbf9f3829 存储的是0xc3
0xbf9f382a 存储的是0xb2
0xbf9f382b 存储的是0xa1
注意:数据存储的是大端格式还是小端格式是由计算机的CPU决定的。
int main() {
union Data d;
d.i = 0xa1b2c314;
if (0x14 == d.ch) {
printf("小端\n");
} else {
printf("大端\n");
}
int num = 0x10203040;
char *p = (char *)#
if (0x40 == *p) {
printf("小\n");
} else {
printf("大\n");
}
}
枚举:enum
是一种值受限的整数类型,由程序员设置它的值的范围,并且还可以给这些值取一个名字。
设计枚举:
typedef enum 枚举名 {
标识符名=枚举值1,
枚举值2,
枚举值3,
...
} 枚举名; // enum与struct、union一样,使用typedef重定义省略enum关键字
注意:使用标识符作为枚举值
定义枚举变量:
enum 枚举名 枚举变量
;
- 理论上枚举变量只能使用枚举值赋值,这样可以提高代码的可读性和安全性。
- C语言编译器为了提高编译速度,不会检查枚举变量的赋值,全靠程序员的自觉(枚举变量就是
int
类型变量)。- C++编译器类型检查比较严格,所以使用C++编译器编译C代码时,枚举变量只能由枚举值赋值、比较。
枚举值:
- 第一个枚举值的默认值是0,之后的枚举值逐渐递增+1。
- 可以使用=设置枚举值,没有进行设置的枚举值是上一个枚举值递增+1。
- 枚举值可以单独使用,这种用法可以给没有意义的字面值数据取一个有意义的名字,> 这样可以提高代码的可读取性,也可以定义匿名的枚举,只使用枚举值。
- 枚举值是常量,所以可以与switch语句配合使用,枚举值可以写在case的后面。
二、预处理:
程序员所编译C代码不能被直接编译,它需要一段程序把它先翻译一下,被翻译过程预处理,负责翻译的程序叫预处理器,被翻译的指令叫预处理指令,C代码中以#开头的都是预处理指令。
gcc -E xxx.c 查看C代码的预处理结果,显示在终端
gcc -E xxx.c -o xxx.i 把预处理的结果保存到文件中,以.i结尾的文件也被称为预处理文件。
文件包含指令:#include
#include
预处理指令的功能是导入一个头文件到当文件中,它用两中使用方法:
方法1:#include <file_name.h>
从系统指定的路径(一般默认是/usr/include
)查找并导致头文件,一般用于导入标准库、系统、第三方库的头文件。
方法2:#include "file_name.h"
从系统当前路径查找并导致头文件,如果没有再从系统指定的路径查找并导致头文件,一般用于导入自定义头文件。
注意:操作系统通过设置环境变量来指定头文件查找路径,或者设置编译器参数gcc xxx -I /path
宏替换指令:#define
定义宏名:#define 宏名 会被替换的内容
使用宏名:printf("%d\n", 宏名);
注意:在预处理阶段,预处理器会把代码使用的宏名替换成宏名后面的那段内容。
宏常量:\#define 宏名 字面值数据
(给没有意义的字面值数据,取一个有意义名字,代替它,这样可以提高代码的可读性、可扩展性,还可以方便代码扩展。)
宏表达式:#define 宏名 表达式、操作、更复杂的标识符
定义时需要注意的问题:
- 由于宏常量和宏表达式可能使用在表达式中,因此在定义宏常量和宏表达式的末尾不要加分号。
- 一般宏名全部大写以作区分 (局部变量全部小写,全局变量首字母大写,循环变量i、j、k、函数名全部小写+下划线、数组arr、字符串str、指针p)
枚举常量与宏常量的区别:
相同点:它们都可以提高代码的可读性,给没有意义字面值数据取一个有意义的名字。
- 从安全角度来说枚举常量要比宏常量安全,因为宏常量是通过替换实现的,在替换过程中可能会导致新的错误,如:有与宏名重名和函数或变量。
- 但枚举常量没有宏常量方便,枚举常量只能是整型数据,而宏常量可以是任何类型的,甚至是复杂的表达式,宏常量的使用范围更广。
总结:如果是大量的整型字面值数据建议定义为枚举常量,如果少量的或整型以外的类型的字面值数据建议定义为宏常量。
预定义的宏:
编译器预定义的宏:
名称 | 作用 |
---|---|
__FILE__ | 获取当前文件名 |
__func__ | 获取当前函数名 |
__LINE__ | 获取当前行号 |
__DATE__ | 获取当前日期 |
__TIME__ | 获取当前时间 |
__\WORDSIZE | 获取当前编译器的位数 |
适合用来显示警告、错误信息。 |
标准库预定义的宏:
// limits.h 头文件中定义的所有整数类型最大值、最小值
#define SCHAR_MIN (-128)
#define SCHAR_MAX 127
#define UCHAR_MAX 255
#define SHRT_MIN (-32768)
#define SHRT_MAX 32767
#define USHRT_MAX 65535
#define INT_MIN (-INT_MAX - 1)
#define INT_MAX 2147483647
#define UINT_MAX 4294967295U
#define LLONG_MAX 9223372036854775807LL
#define LLONG_MIN (-LLONG_MAX - 1LL)
#define ULLONG_MAX 18446744073709551615ULL
PATH_MAX
// stdlib.h 头文件定义两个结标志
#define EXIT_SUCCESS (0)
#define EXIT_FAILURE (-1)
// stdbool.h 头文件定义了bool、true、false
#define bool _Bool
#define true 1
#define false 0
// libio.h 头文件定义了NULL
#define NULL ((void*)0)
宏函数:
宏函数不是真正的函数,而是带参数的宏替换,只是使用方法像函数而已。
在代码中使用宏函数,预处理时会经历两次替换,第一次把宏函数替换成它后面的一串代码、表达式,第二次把宏函数中的参数替换到表达式中。
#define 宏名(a,b,c,...) a+b*c
定义宏函数要注意的问题:
- 假如宏函数执行复杂的多条语句,可能会因为在
if
分支中缺少大括号而出现问题,可以使用大括号包括,进行保护,避免if
的影响。
#define 宏名(a,b,c,...) {代码1; 代码2; ...}
- 可以通过加大括号解决问题1,但是如果
if
后面有else
,也会出现问题 - 因此linux内核和C++开源的代码中,经常会在宏定义中使用
do-while(0)
来保证代码安全,除此之外还可以起到:在宏函数中定义同名变量、而不会冲突(语句块定义), 还可以在解决代码冗余问题上替换goto的效果。 - 宏函数后面的代码不能直接换行,如果代码确定太长,可以使用续行符换行。
#define 宏名(a,b,c,...) { \
代码1; \
代码2; \
... \
}
#define 宏名(a,b,c,...) do { \
代码1; \
代码2; \
... \
} while(0)
- 为了防止宏函数出现二义性,对宏参数要尽量多加小括号。
二义性:就是使用宏函数的环境不同、参数不同,造成宏函数有多执行规则,会出现出乎意料的执行结果,这种宏函数的二义性,设计宏函数时要尽量杜绝。
调用宏函数要注意的问题:
- 传递给宏函数的参数不能使用自变运算符,因为我们无法知道参数在宏代码中会被替换多少次。
- 宏函数没有返回值,只是个别宏函数表达式有计算结果。
普通函数与宏函数的优缺点:
宏函数的优点:
- 执行速度快,它不是真正的函数调用,而是代码替换,不会经历传参、跳转、返回值。
- 不会检查参数的类型,因此通用性强。
宏函数的缺点:
- 由于它不是真正的函数调用,而是代码替换,每使用一次,就会替换出一份代码,会造成代码冗余、编译速度慢、可执行文件变大。
- 没有返回值,最多可以有个执行结果。
- 类型检查不严格,安全性低。
- 无法进行递归调用。
普通函数的优点:
- 不存在代码冗余的情况,函数的代码只会在代码段中存储一份,使用时跳转过去执行,执行结束后再返回,还可以附加返回值。
- 安全性高,会对参数进行类型检查。
- 可以进行递归调用,实现分治算法。
函数的缺点:
- 相比宏函数它的执行速度慢,调用时会经历传参、跳转、返回等过程,该过程耗费大量的时间。
- 类型专用,形参什么类型,实参必须是什么类型,无法通用。
适合封装成宏函数的代码:
- 代码量少,即使多次使用也不会造成代码段过度冗余。
- 调用次数少,但执行次数多,也就是宏函数会在循环语句中调用。
- 函数的功能对返回值没有要求,也就是函数的功能不是通过返回值达到的。
封装一个malloc
、free
函数:
void* _my_malloc(const char* filename, const char* func, size_t line, size_t size) {
void *ptr = malloc(size);
printf("%s %s %u %p\n", filename, func, line, ptr);
return ptr;
}
#define my_malloc(size) _my_malloc(__FILE__, __func__, __LINE__, size)
#define my_free(ptr) do {\
printf("%s %s %u %p\n", __FILE__, __func__, __LINE__, ptr);\
free(ptr);} while (0);
实现的一个通用的变量交换函数:
// 只适合数值型交换、数据可能溢出
#define SWAP(a, b) do {(a) = ((a) + (b)), (b) = ((a) - (b)); (a) = ((a) - (b));} while (0);
// 数据不溢出,只适合整型数据,并且不能是同一个值
#define SWAP(a, b) do {a = a ^ b; b = a ^ b; a = a ^ b;} while (0);
// 不能交换结构变量 浪费内存
#define SWAP(a, b) do {tong double t = a; a = b; b = t;} while (0);
// 可以交换任意类型,多一个参数
#define SWAP(a, b, type) do {type t = a; a = b; b = t;} while (0):
// 只能在GNU系列编译器下使用
#define SWAP(a, b) do {typedef(a) t = (a); (a) = (b); (b) = t;} while (0);
重要的宏函数
/**
* @TYPE: 类型名 结构体类型名
* @MEMBER: TYPE结构体类型中的一个成员名
* 经过对齐补齐之后,MEMBER结构体成员距离基准位置(起始)偏移的字节数
* 通过结构体成员内存地址 反推结构体变量的内存地址
*/
#define offsetof(type, member) ((size_t) &((type *)0)->member)
/**
* @ptr: 结构体成员指针(type类型结构体变量中member成员的位置)
* @type: 结构体类型名
* @member: 结构体成员名
* 获得结构体变量的起始位置
*/
#define container_of(ptr, type, member) ({ \
const typeof(((type*)0)->member) * _mptr = (ptr); \
(type *)((char *)_mptr - offsetof(type, member)); \
})
#define DEBUG(format, __va_args__...) printf("[%s %s %d]: "format,__FILE__,__func__,__LINE__,##__va_args__)
条件编译:
条件语句(if
、switch
、for
、while
、do while
)会根据条件选择执行哪些代码,条件编译就是预处理器根据条件选择哪些代码参与下一步的编译。
负责条件编译的预处理指令有:#if
, #ifdef
, #ifndef
, #elif
, #else
, #endif
头文件卫士:
这种固定写法,在头文件中使用,它能防止头文件被重复包含,所有的头文件都要遵循这个规则。
#ifndef _FILE_H__ // 判断_FILE_H__宏是否正在,不存在则条件为真
#define _FILE_H__ // 定义_FILE_H__宏
// 头文件卫士能保证此处不重复出现
#endif //_FILE_H__ // #ifndef的结尾
注释代码:
// 只能注释单行代码,早期的编译器不支持该用
/* 多行注释,但不能嵌套 */
#if 0|1
可注释大块代码,可以嵌套
#endif
版本、环境判断:
#if __WORDSIZE == 64
typedef long int int64_t;
#else
typedef long long int int64_t;
#endif
// 判断是否是Linux操作系统:
#if __linux__
#endif
// 判断是否是Windows操作系统:
#if __WIN32 | __WIN32__ | __WIN64__
#endif
// 判断gcc还是g++:
int main() {
#if __cplusplus
printf("你使用是g++编译器\n");
#else
printf("你使用是gcc编译器\n");
#endif
}
DEBUG宏:
专门用于调试程序的宏函数,这种宏函数在程序测试、调试、试运行阶段执行,在程序正式上线阶段不执行,这类函数会根据DEBUG宏是否定义确定执行的流程。
一些操作提示,如:xxx操作成功,xxx操作失败,分配内存的记录、释放内存的记录,这类型消息开发人员、测试人员需要看到,但用户不需要看到。
不常用的预处理指令:
#line <常整数> 设置当前代码的行号,目前没有发现它有什么用
#error "在预处理阶段提示错误信息",一旦预处理遇到它,将不再继续编译,它不能单独使用必须与条件判断系列语句配合使用
#warning "在预处理阶段提示警告信息" 不能建议单独使用,最好与条件判断系列语句配合使用。
#pragma GCC poison <标识符> 把标识符设置病毒,禁止在代码中使用
#pragma pack(n) 设置最大对齐和补齐字节数
每个系统在进行对齐和补齐都有一个最大对齐和补齐字节数n,也就是超出n字节按n字节计算,例如:linux32系统n=4,windows32 n=8
设置要求:
1、n < 系统默认的最大对齐、补齐字节数,往大了调整没有意义,速度不会提升还会导致内存浪费。
2、n必须是2的x次方,也就是必须是1、2、4、8、16这一类的整数
宏函数的变长参数:
#define func(...) __VA_ARGS__
注意:这种用法必须配合,printf
/fprintf
/sprintf
系列支持变长参数的函数使用。
在编译时定义宏:
gcc xxx.c -D ARR_LEN=3
-D ARR_LEN=3 <=> #define ARR_LEN 3 跟在代码中定义宏的效果一样
gcc xxx.c -D DEBUG
-D DEBUG <=> #define DEBUG
调试阶段打印调试信息:
#include <stdio.h>
#ifdef DEBUG
#define debug(...) printf(__VA_ARGS__)
#else
#define debug(...)
#endif
#define error(...) printf("%s %s %d %s %s:%m %s\n", __FILE__,__func__, __LINE__, __DATE__, __TIME__, __VA_ARGS__);
int main() {
int num = 10;
debug("调试信息%d\n", num);
if (1) {
error("main程序出错");
}
}
三、C语言文件读写
文件分类:
二进制文件:
把数据的补码直接写入文件,这种文件叫二进制文件。
优点:读写和写入时不需要进行转换,所以读写速度快,数据安全性高。
缺点:不能使用文本编译器打开,无法阅读。
文本文件:
把数据转换成字符串写入文件,也就是把字符的二进制写入文件,这种文件叫文本文件。
优点:能被文本编辑器打开,人类能看的懂,能够看出数据是否出错。
缺点:读写时需要转换,读写速度慢,数据有被修改的风险。
打开/关闭文件:
FILE *fopen(const char *path, const char *mode);
功能:打开文件
path:文件的路径
mode:文件的打开模式
返回值:文件结构指针,是后续操作文件的凭证,失败则返回NULL。
int fclose(FILE *stream);
功能:关闭文件
返回值:成功返回0,失败返回-1。
注意:不能重复关闭,否则会出现double free错误,为了避免出现这种错误,在文件关闭及时把文件指针赋值为NULL,及时关闭文件可以把缓冲区中的数据写入到文件中。
文件的打开模式:
模式 | 作用 |
---|---|
"r" | 以只读方式打开文本文件,如果文件不存在,或文件没有读权限则打开失败。 |
"r+" | 在"r"的基础上增加了写权限。 |
"w" | 以只写方式打开文本文件,如果文件不存在则创建,如果文件存在则清空文件的内容,如果文件存在但没有写权限,则打开失败。 |
"w+" | 在"w"的基础上增加了读权限。 |
"a" | 以只写方式打开文本文件,如果文件不存在则创建,如果文件存在则新写入的内容追加到文件末尾,如果文件存在但没有写权限,则打开失败。 |
"a+" | 在"a"的基础上增加了读权限。 |
如果要操作二进制文件,则在以上模式的基础上增加b。
文件的文本打开方式和二进制打开方式的区别:
在 UNIX/Linux 平台中,用文本方式或二进制方式打开文件没有任何区别。
在 UNIX/Linux 平台中,文本文件以\n
(ASCII 码为0x0a
)作为换行符号;而在 Windows 平台中,文本文件以连在一起的\r\n
(\r
的 ASCII 码是0x0d
)作为换行符号。
在 Windows 平台中,如果以文本方式打开文件,当读取文件时,系统会将文件中所有的\r\n
转换成一个字符\n
,如果文件中有连续的两个字节是0x0d0a
,则系统会丢弃前面的0x0d
这个字节,只读入0x0a
。当写入文件时,系统会将\n
转换成\r\n
写入。
也就是说,如果要写入的内容中有字节为0x0a
,则在写入该字节前,系统会自动先写入一个0x0d
。因此,如果用文本方式打开二进制文件进行读写,读写的内容就可能和文件的内容有出入。
因此,用二进制方式打开文件总是最保险的。
文本文件的读写:
int fprintf(FILE *stream, const char *format, ...);
功能:把若干个变量以文本格式写入到指定的文件中
stream:要写入的文件凭证,必须是fopen的返回值。
format:占位符+转义字符+提示信息
...:若干个变量
返回值:写入字符的数量
int fscanf(FILE *stream, const char *format, ...);
功能:从文件中读取数据
stream:要读取的文件
format:占位符
...:若干个变量的地址
返回值:成功读取的变量个数
二进制文件的读写:
size_t fwrite(const void *ptr, size_t size, size_t nmemb,FILE *stream);
功能:把一块内存当作数组,然后数组中的内容以二进制格式写入到文件中
ptr:数组首地址
size:数组元素的字节数
nmemb:数组的长度
stream:要写入的文件
返回值:实际写入的次数
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
功能:把二进制文件中的内容读取的数组中
ptr:要存储数据的数组首地址
size:数组元素的字节数
nmemb:数组的容量
返回值:成功读取的次数
注意:以二进制格式读写文件时,最好加上mode最好包含b。
注意:如果以fwrite
/fread
读写的字符数组,那么我们操作的依然是文本文件。
int sprintf(char *str, const char *format, ...);
功能:把若干个变量转换成字符串输出到str数组中
int sscanf(const char *str, const char *format, ...);
功能:从字符串读取若干个变量
文件位置指针:
文件位置指针它指向文件中即将要读取的数据,以"r"
、"r+"
方式打开文件,文件位置指针指向文件的开头,以"a"
、"a+"
方式打开文件,文件位置指针指向文件的末尾。(但是读取可以在任意位置进行)
读取数据时会从 文件位置指针指向 的地方开始读取,写入数据时也会写入到文件位置指针所指向的地址,并且它会随着读写操作自动移动。
注意:fprintf
/fwrite
写入数据后立即读取,之所以会失败,是因为文件位置指针指向着文件的末尾。
void rewind(FILE *stream);
功能:把文件的位置指针调整到文件的开头。
long ftell(FILE *stream);
功能:返回文件位置指针指向了文件中的第几个字节
int fseek(FILE *stream, long offset, int whence);
功能:设置文件的位置指针
stream:要设置的文件
offset:偏移值(正负整数)
whence:基础位置
SEEK_SET 文件开头
SEEK_CUR 当前位置
SEEK_END 文件末尾
whence+offset就是文件指针最终设置的位置。
返回值:成功返回0,失败返回-1。
文件操作时的局限性:
文件的内容是连续存储在磁盘上的,所以就导致需要进行以下操作:
向文件中插入数据:
- 文件位置指针调整到要插入的位置。
- 把后续的数据整体向后拷贝n(要插入的数据字节数)个字节。
- 文件位置指针调整到要插入的位置,写入数据。
从文件中删除数据:
- 文件位置指针调整到要删除的数据末尾。
- 把后续的数据整体向前拷贝n(要删除的数据字节数)个字节。
- 修改文件的大小。
总结:所以,在程序运行时,建议在一开始就把文件中的数据全部加载到内存中,程序在运行期间只针对这个数据内存进行增、删、改、查等操作,在程序结束之前,再把数据从内存写入到文件中
四、文件管理:
int remove(const char *pathname);
功能:删除文件
int rename(const char *oldpath, const char *newpath);
功能:重命名文件
int truncate(const char *path, off_t length);
功能:把文件的内容设置为length字节数
char *tmpnam(char *name);
功能:生成一个与当前文件系统不重名的文件名。
int access(const char *pathname, int mode);
功能:检查文件的权限
mode:
R_OK 读权限
W_OK 写权限
X_OK 执行权限
F_OK 文件是否存在
返回值:
检查的权限如果存在则返回0,不存在则返回-1。
实现一个mv命令:
gcc xxx.c -o MV
./MV file1 file2 效果要等同于 mv file1 file2
以读打开file1,读取数据到内存
以写打开file2,把从file1读到的数据从内存中写入到file2中
直到file1读完写完结束
删除file1
main 函数的参数:
为了获取命令行执行可执行文件时后面附加的参数信息
argc: 代表命令行参数的个数 包括./可执行文件 本身也算一个参数
argv:一个存储若干个字符串的数组,按顺序存储的是所有的命令行参数
#include <stdio.h>
#include <string.h>
#define N 1000010
char str[N];
int main(int argc, const char *argv[]) {
// for (int i = 0; i < argc; ++i) {
// printf("%s\n", argv[i]);
// }
if (strcmp(argv[0], "./MV") == 0) {
// printf("True");
FILE* frp = fopen(argv[1], "rb");
FILE* fwp = fopen(argv[2], "wb");
fread(str, sizeof str[0], sizeof str, frp);
fwrite(str, sizeof str[0], sizeof str, fwp);
fclose(frp), fclose(fwp);
frp = NULL, fwp = NULL;
remove(argv[1]);
} else {
printf("-1");
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· .NET10 - 预览版1新功能体验(一)