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个空白字节

使用联合的意义:

  1. 使用少量的内存对应若干个标识符,只要不同时使用联合的成员,就不会冲突,能大大节约内存,在早期计算机内存比较小时,该技术使用较多,现在随着计算机内存越来越大已经基本不再使用。
  2. 联合可以对一块内存进行不同格式的解释,所以在网络通信时还存在着少量的使用(使用网络协议中已经设计好的联合体)。

大端系统和小端系统:

大端系统:

​ 低位数据存储高位地址,或者说是高位数据存储在低位地址,一般大型的服务器、网络设备采用的是大端系统,所以大端格式也叫网络字节序。

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 枚举名 枚举变量;

  1. 理论上枚举变量只能使用枚举值赋值,这样可以提高代码的可读性和安全性。
  2. C语言编译器为了提高编译速度,不会检查枚举变量的赋值,全靠程序员的自觉(枚举变量就是int类型变量)。
  3. C++编译器类型检查比较严格,所以使用C++编译器编译C代码时,枚举变量只能由枚举值赋值、比较。

枚举值:

  1. 第一个枚举值的默认值是0,之后的枚举值逐渐递增+1。
  2. 可以使用=设置枚举值,没有进行设置的枚举值是上一个枚举值递增+1。
  3. 枚举值可以单独使用,这种用法可以给没有意义的字面值数据取一个有意义的名字,> 这样可以提高代码的可读取性,也可以定义匿名的枚举,只使用枚举值。
  4. 枚举值是常量,所以可以与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 宏名 表达式、操作、更复杂的标识符

定义时需要注意的问题

  1. 由于宏常量和宏表达式可能使用在表达式中,因此在定义宏常量和宏表达式的末尾不要加分号。
  2. 一般宏名全部大写以作区分 (局部变量全部小写,全局变量首字母大写,循环变量i、j、k、函数名全部小写+下划线、数组arr、字符串str、指针p)

枚举常量与宏常量的区别

相同点:它们都可以提高代码的可读性,给没有意义字面值数据取一个有意义的名字。

  1. 从安全角度来说枚举常量要比宏常量安全,因为宏常量是通过替换实现的,在替换过程中可能会导致新的错误,如:有与宏名重名和函数或变量。
  2. 但枚举常量没有宏常量方便,枚举常量只能是整型数据,而宏常量可以是任何类型的,甚至是复杂的表达式,宏常量的使用范围更广。
    总结:如果是大量的整型字面值数据建议定义为枚举常量,如果少量的或整型以外的类型的字面值数据建议定义为宏常量。

预定义的宏:

编译器预定义的宏

名称 作用
__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

定义宏函数要注意的问题

  1. 假如宏函数执行复杂的多条语句,可能会因为在if分支中缺少大括号而出现问题,可以使用大括号包括,进行保护,避免if的影响。
#define 宏名(a,b,c,...) {代码1; 代码2; ...}
  1. 可以通过加大括号解决问题1,但是如果if后面有else,也会出现问题
  2. 因此linux内核和C++开源的代码中,经常会在宏定义中使用do-while(0)来保证代码安全,除此之外还可以起到:在宏函数中定义同名变量、而不会冲突(语句块定义), 还可以在解决代码冗余问题上替换goto的效果
  3. 宏函数后面的代码不能直接换行,如果代码确定太长,可以使用续行符换行。
#define 宏名(a,b,c,...) {  \
	代码1; \
	代码2; \
	 ...   \
}
#define 宏名(a,b,c,...) do {  \
	代码1; \
	代码2; \
	...   \
} while(0)
  1. 为了防止宏函数出现二义性,对宏参数要尽量多加小括号。

二义性:就是使用宏函数的环境不同、参数不同,造成宏函数有多执行规则,会出现出乎意料的执行结果,这种宏函数的二义性,设计宏函数时要尽量杜绝。

调用宏函数要注意的问题

  1. 传递给宏函数的参数不能使用自变运算符,因为我们无法知道参数在宏代码中会被替换多少次。
  2. 宏函数没有返回值,只是个别宏函数表达式有计算结果。

普通函数与宏函数的优缺点

宏函数的优点

  1. 执行速度快,它不是真正的函数调用,而是代码替换,不会经历传参、跳转、返回值。
  2. 不会检查参数的类型,因此通用性强。

宏函数的缺点

  1. 由于它不是真正的函数调用,而是代码替换,每使用一次,就会替换出一份代码,会造成代码冗余、编译速度慢、可执行文件变大。
  2. 没有返回值,最多可以有个执行结果。
  3. 类型检查不严格,安全性低。
  4. 无法进行递归调用。

普通函数的优点

  1. 不存在代码冗余的情况,函数的代码只会在代码段中存储一份,使用时跳转过去执行,执行结束后再返回,还可以附加返回值。
  2. 安全性高,会对参数进行类型检查。
  3. 可以进行递归调用,实现分治算法。

函数的缺点

  1. 相比宏函数它的执行速度慢,调用时会经历传参、跳转、返回等过程,该过程耗费大量的时间。
  2. 类型专用,形参什么类型,实参必须是什么类型,无法通用。

适合封装成宏函数的代码

  1. 代码量少,即使多次使用也不会造成代码段过度冗余。
  2. 调用次数少,但执行次数多,也就是宏函数会在循环语句中调用。
  3. 函数的功能对返回值没有要求,也就是函数的功能不是通过返回值达到的。

封装一个mallocfree函数

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__)

条件编译:

​ 条件语句(ifswitchforwhiledo 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次方,也就是必须是124816这一类的整数
宏函数的变长参数:
#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:文件的打开模式
返回值:文件结构指针,是后续操作文件的凭证,失败则返回NULLint 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

文件操作时的局限性:

文件的内容是连续存储在磁盘上的,所以就导致需要进行以下操作:

向文件中插入数据

  1. 文件位置指针调整到要插入的位置。
  2. 把后续的数据整体向后拷贝n(要插入的数据字节数)个字节。
  3. 文件位置指针调整到要插入的位置,写入数据。

从文件中删除数据

  1. 文件位置指针调整到要删除的数据末尾。
  2. 把后续的数据整体向前拷贝n(要删除的数据字节数)个字节。
  3. 修改文件的大小。

总结:所以,在程序运行时,建议在一开始就把文件中的数据全部加载到内存中,程序在运行期间只针对这个数据内存进行增、删、改、查等操作,在程序结束之前,再把数据从内存写入到文件中

四、文件管理:

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");
	}
}

上一篇:内存管理、输入输出缓冲区、字符串

posted @   sleeeeeping  阅读(30)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· .NET10 - 预览版1新功能体验(一)
  1. 1 吹梦到西洲 恋恋故人难,黄诗扶,妖扬
  2. 2 敢归云间宿 三无Marblue
敢归云间宿 - 三无Marblue
00:00 / 00:00
An audio error has occurred, player will skip forward in 2 seconds.

敢归云间宿 - 三无Marblue

词:怀袖

曲:KBShinya

编曲:向往

策划:杨颜

监制:似雨若离RainJaded/杨颜

吉他:大牛

混音:三无Marblue

和声:雾敛

母带:张锦亮

映像:似雨若离RainJaded

美术:阿尼鸭Any-a/乙配/雨谷/今风/米可/Eluan

题字:长安

酒 泼去群山眉头

酒 泼去群山眉头

月 悬在人世沧流

空杯如行舟 浪荡醉梦里走

心 生自混沌尽头

心 生自混沌尽头

对 天地自斟自酬

诗随我 遍历春秋

行流水 走笔形生意动

见珠玉 淙淙落纸成诵

拾得浮名有几声 云深处 却空空

耳畔丝竹 清商如雾

谈笑间 却是心兵穷途

飞觞醉月无归宿 便是孤独

不如就化身为风

卷狂沙 侵天幕

吹醒那 泉下望乡 的战骨

昨日边关犹灯火

眼前血海翻覆

千万人跌落青史 隔世号呼

于是沸血重剑共赴

斩以雷霆之怒

肩背相抵破阵开路

万古同歌哭

纵他春风不度 悲欢蚀骨

此去宁作吾

挣过命途 才敢写荣枯

望 云际群龙回首

望 云际群龙回首

任 飘蓬争逐身后

叹冥顽之俦 好景尽付恩仇

收 江声随酒入喉

收 江声随酒入喉

来 提笔御风同游

不觉已 换了春秋

真亦假 泼墨腾烟沉陆

有还无 蝶影纷堕幻目

我与天地周旋久

写尽梦 便成梦

夜雨浇熄 往事残烛

生死间 谁尽兴谁辜负

管他醒来归何处 心生万物

也曾对电光火雨

抛酒樽 镇天枢

护住了 人间多少 朝与暮

烧尽了阴云冥府

烧尽了阴云冥府

且看星斗尽出

浩荡荡尘埃野马 忘怀命数

于是信步鸿蒙之轻

也领苍生之重

与诗与剑颠倒与共

沉眠斜阳中

纵他世事汹涌 万类争渡

此去宁作吾

醉得糊涂 才梦得清楚

潮水 带着叹息轻抚

潮水 带着叹息轻抚

像光阴 漫过大地上幽微草木

有情世 见众生明灭往复

天生自在 何必回顾

晦暗中双掌一拊

立此身 照前路

与某个 阔别的我 决胜负

渺渺兮身外无物

无喜无悲无怖

不过是大梦一场 各自沉浮

于是纵横万相穷通

也守心底灵通

合眼识得星沉地动

也岿然不动

敢令岁月乌有 逍遥长驻

敢归云间宿

遥祝远行人 有道不孤

点击右上角即可分享
微信分享提示