C语言基础整理

基础细节

  • 位移运算要注意:对于左移运算,无符号数和有符号数的结果都是从最低位开始补0;对于右移运算,无符号数从最高位开始补0,有符号数从最高位开始补0还是补符号位在C语言中并没有规定,具体依赖于编译器实现。所以,建议不要对有符号数执行位移运算,而应对无符号数执行位移运算以减少出错的可能。
  • 常用掩码: ~ ( ~( 0 << n ) )可以产生一串从右边最低位起的连续n位全1、其他高位全0的序列,即000…0001111…111。当需要取出某一操作数的任意连续n位时通过左移该掩码若干位再执行与运算就可以得到结果。
  • system函数:执行另外一个程序。System返回一个整数。如果在命令行执行一个程序,那么这个程序的调用者就是操作系统,如果在代码中通过system执行一个程序,那么这个程序的调用者就是自己写的代码本身.C语言所有的库函数调用,只能保证语法是一致的,但不能保证执行结果是一致的,同样的库函数在不同的操作系统下执行结果可能是一样的,也可能是不一样的。
    例如:system(“ls -l”);执行后执行ls -l命令;
  • 定义和声明的区别:如,int a; //定义 extern int b; //声明
  • 编译的过程:由源程序文件到可执行二进制文件
    1)预编译; 2)编译; 3)链接
    预编译:gcc -E,预编译的功能:将#include包含的头文件做简单的文本替换(在C语言中#开头的语句又叫预编译指令);将代码中的注释删除;
    编译:gcc -c,编译的功能:把文本的c语言编译为二进制的指令;
    链接:C语言写的程序是需要依赖各种库的,所以编译之后还需要把库链接到最终的可执行程序中去,gcc没有单独的链接参数。
  • 头文件不一定要放在源代码文件的第一行,只要在用到之前包含它就可以了。
  • 字符数组和字符串的区别:字符串是以\0结尾的一串字符。比如:
char a[] = "hello world";
a[5] = 0;
printf("sizeof(a) = %d\n", sizeof(a));
printf("%s\n", a);

输出结果为:

sizeof(a) = 12
hello

表明字符数组中可以包含一个或多个’\0’(也即0),并且不影响字符数组的长度,但字符串中仅有一个’\0’。

  • 生成随机数:实例如下:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main() {
	unsigned int seed = time(NULL);
	srand(seed);
	int randNum = rand() % 10;//生成10以内的随机数
	while(randNum) {
		printf("%u\n", randNum);
		randNum = rand() % 10;
	}
	return 0;
}
  • 字符串:
    – 字符和对应整数之间的相互转换:例如
char s = '1';
int result = s - '0';//字符1对应的整型数字1,反过来由整型1得到字符'1'要加'0'

– scanf函数:用户通过键盘可把一个字符串写入一个char数组,不过,scanf函数并不会直接读入用户输入的空格键或者最后输入的回车键 ( 也即scanf函数默认输入空格或者回车代表输入完成 ) ,而是自动在最后补一个0(即’\0’),这样才是完整的字符串,并且scanf函数并不检查char数组的下标,一旦用户输入过多。会导致内存错误。所以scanf函数不安全,不推荐经常使用。
注:vs2013中scanf函数无法使用的解决办法
– gets函数:原型为gets(char *s),只认为回车键代表输入完成,空格只是一个普通字符而已。不过gets同样有缓冲区溢出的问题。
–fgets函数:有三个参数,第一个为char数组,第二个为这个char数组的大小,第三个参数为输入源,如果通过键盘输入则可固定写为stdin,原型为fgets(char *s, size_t len, stdin)。fgets在读取一个用户通过键盘输入的字符串时,会把最后的回车键也作为输入的字符串的一部分输入进去。不存在缓冲区溢出问题,是安全的,安全的调用为fgets(a, sizeof(a), stdin),但如果第二个参数为某一个常数,还是有溢出问题。
–puts函数:字符串输出函数。与printf不同的是,它会在最后自动加上\n。然而puts函数并不支持各种转义字符,并且只能简单地输出一个字符串,不能输出int ,double等其他类型。
–fputs函数:puts函数的文件操作版本,第一个参数是一个char数组,第二个参数是输出端,如果是在屏幕输出则可写为stdout。并且fputs函数不会在字符串最后自动加上\n。
例子:去掉一个字符串最后的\n:

char a[] = "abc\n123\n";
int index = 0;
while (a[index]) { //去掉字符串最后的'\n'
	if (a[index] == '\n' && a[index+1] == 0) {
		a[index] = 0;
		break;
	}
	index++;
}
printf("%s\n", a);

–strlen函数:输出一个字符串的有效长度。
–strcat函数:合并两个字符串。存在缓冲区溢出危险。
–strncat函数:字符串有限追加。安全。
–strcmp:字符串比较。
–strncmp:字符串有限比较。
–strcpy:字符串拷贝。
–strncpy:字符串有限拷贝。安全用法如下:

char a1[6] = { 0 };
char a2[100] = "helloworld";
strncpy(a1, a2, sizeof(a1)-1);

不会造成a1溢出。
–sprintf函数:和printf函数功能相似,唯一区别是sprintf函数把输出放到第一个参数(一个字符串)中而非输出到标准输出。
–sscanf函数:类似scanf函数,唯一区别是sscanf函数从一个char数组中读取输入。例如:

char str[] = "1+2";
int a, b;
sscanf(str, "%d+%d", &a, &b);
printf("a = %d, b = %d", a, b);

输出为:

a = 1, b = 2

–strchr:在字符串中查找指定字符,返回字符或NULL.
–strstr:在字符串中查找子串,返回子串或NULL。
–strtok:分割字符串。例如:把一个字符串按指定分隔符分开

char str[] = "12+34; 56+23; 11+22";
const char *p = strtok(str, ";");
while (p) {
	printf("%s\n", p);
	p = strtok(NULL, ";"); //接着分割剩下的子串要把第一个参数置为NULL
}

–atoi函数:字符串转为int
–atof函数:字符串转为float
–atol函数:字符串转为long
以上三个函数位于 < stdlib.h > 中
不过,C语言并没有把其他类型数据转为字符串的函数。
这可以通过sprintf函数实现:

int a = 123;
char str[100] = { 0 };
sprintf(str, "%d", a);
printf("%s\n", str);

输出为字符串123。

  • 函数
    – exit函数:C的库函数,在 < stdlib.h > 头文件中,有一个整型参数,代表进程终止,exit(0)代表进程终止。等同于main函数中return 返回的整数。
    区别return 和exit:在函数中写return只是代表函数终止了,只不过在main函数中return后整个程序就终止了。不管在程序的任何位置调用exit,进程就马上终止了,包括在子函数和main函数中调用exit。
  • 递归
    例子1:十进制到二进制的转换:
void decimal2binary(int n) {
	if (n >= 2) {
		decimal2binary(n / 2);
	}
	printf("%d", n % 2);
}

例子2:十进制到十六进制的转换:

char hex2char(int x) {
	char hex[] = "123456789abcdef";
	int len = sizeof(hex);
	printf("%d\n", len);
	for (int i = 0; i < len; i++) {
		if (x == i)
			return hex[i];
	}
}

void decimal2hex(int n) {
	if (n >= 16) {
		decimal2hex(n / 16);
	}
	putchar(hex2char(n % 16));
}

例子3:递归求字符串长度:

int charArraylength(char s[], int n) {
	if (s[n])
		return charArraylength(s, n + 1);
	else
		return n;
}

例子4:递归求0到指定自然数之间的所有自然数的和:

int sumOfNaturalNumber(int n) {
	if (n == 0)
		return 0;
	else
		return sumOfNaturalNumber(n - 1) + n;
}

例子5:求一个正整数的逆置:
例如,输入为123,输出为321;若输如为1000,输出为1

int cntOfNum(int n) {//求整数的位数
	int cnt = 0;
	while (n) {
		cnt++;
		n /= 10;
	}
	return cnt;
}

int pow_10(int n) {//求10的n次方
	int result = 1;
	for (int i = 0; i < n; i++) {
		result *= 10;
	}
	return result;
}

int rec(int n) {//逆置正整数
	int cnt = cntOfNum(n);
	int result = 0;
	for (int i = 0; i < cnt; i++) {
		result += (n % 10)*pow_10(cnt - 1 - i);
		n /= 10;
	}
	return result;
}

指针

  • 在sizeof运算下的结果和数组名不一样,指针的sizeof结果等于内存地址的字节数,不等于它所对应的那个数组的实际大小。

  • 对于一个指针变量p,p ++ 的含义是:p 先和后自增运算符++结合,得到的值再来解引用,即等价于(p++)。因为*和++的优先级相同,并且都是从右向左结合,不过后自增运算符的作用是在表达式完成后再进行自增,也即取出p的当前值后p的下标再加一个步长。例如,*p++即指先对当前的p解引用,再递增p指向下一单位地址。

  • 任何一种数据类型都可以理解为char类型的数组,例如:

unsigned a = 0x12345678;
unsigned char *p = (char *)&a;
printf("%x, %x, %x, %x\n", p[0], p[1], p[2], p[3]);

输出为:78, 56, 34, 12(说明数据在该计算机内部使用小端法表示)

  • 在ip地址上的应用:
    IPv4地址在计算机内部其实就是用一个int型数表示的,地址一共四段,每段的范围为0~255,所以每段可用一个unsigned char来表示。例如:
unsigned int a = 0;
unsigned char *p = (char *)&a;
p[0] = 123;
p[1] = 111;
p[2] = 168;
p[3] = 192;
printf("%u\n", a);

输出为3232264059

例子1:把一个int型数转换成字符串形式的ip地址:

unsigned int a = 3232264059;
unsigned char *p = (char *)&a;
unsigned char str[100] = { 0 };
sprintf(str, "%u.%u.%u.%u\n", p[3], p[2], p[1], p[0]);
printf("%s\n", str);

输出为:192.168.111.123

例子2:把一个字符串形式的ip地址转换成一个int数:

char a[] = "192.168.111.123";
unsigned b = 0;
char *p = (char *)&b;
unsigned c[4] = { 0 };
sscanf(a, "%u.%u.%u.%u", &c[0], &c[1], &c[2], &c[3]);
for (int i = 0; i < 4; i++) {
	*(p + i) = c[4-1-i];
}
printf("%u\n", b);

输出为:3232264059

  • 函数参数为数组时,数组名当做一个指针变量而非一个地址常量,在函数体内可以修改这个数组名的值。另外,就需要另一个参数代表数组的大小。不过,当一个字符串作为函数参数时,不需要传另一个参数来表明字符串的长度,因为由字符串最后的\0就可以判断该字符串的长度了。
  • 三个内存操作函数:在头文件 < string.h > 中
void *memset(void *s, int c, size_t n);
//设置一块内存区域
//第一个参数是内存首地址,第二个是要设置的值,第三个是这块要设置值的内存的大小(单位是字节)
void *memcpy(void *dest, const void *src, size_t n);
//内存拷贝,第一个参数是目标内存首地址,第二个是源内存首地址,第三个是拷贝字节数
//使用时一定要确保源内存区域和目标内存区域间没有重叠
void *memmove(void *dest, const void *src, size_t n);
//内存移动,第一个参数是目标内存首地址,第二个是源内存首地址,第三个是移动字节数

例子:字符串逆置

void swap(char *a, char *b) {
	char temp = *a;
	*a = *b;
	*b = temp;
}
char s[] = "helloworld";
char *p = s;
unsigned len = strlen(s);
unsigned left = 0;
unsigned right = len - 1;
while (left < right) {
	//swap(&s[left], &s[right]);
	swap(p + left, p + right);
	left++;
	right--;
}
//此时s中字符顺序就颠倒过来了

注意:一个小区别

char s[] = "hello";
//char *p = s;
s[2] = '6';//*(p+2) = '6';
//此时s的内容为:he6lo
//s是一个字符数组,内容可修改
char *s = "hello";
//char *p = s;
s[2] = '6';//*(p+2) = '6';
//此时会出错,因为s指向一个常量字符串,内容不可修改

这也表明,当函数的形参为一个字符数组时,若函数体内不会对其做修改,那么就不要把这个形参设成字符数组而应为常量字符指针const char *.

  • 区别字符数组和常量字符串:
char a[] = "hello";//定义了一个char的数组,同时给数组的成员赋值,只不过这种赋值方式比较简便,且最后会自动加一个\0字符。即给数组成员赋值如下:a[0] = 'h'; a[1] = 'e'; ... a[5] = '\0';
//sizeof(a)的结果为6
const char *b = "hello"; //定义了一个const char *类型的一个指针变量b,指向了常量区的一个地址。
//sizeof(b)的结果为所用操作系统中一个指针变量类型的长度
//sizeof运算符返回操作对象的数据类型的字节长度,甚至都不会对对象求值
  • 寄存器变量,例如:register int a; //在CPU寄存器中,没有地址,所以不能对它使用指针。

内存管理

  • 代码区:程序被操作系统加载到内存时所有可执行代码(二进制文件)都加载到此区,程序加载到内存时就确定了,这块内存不可在运行期间修改,只可以执行。大小固定。

  • 静态区(全局区):程序加载到内存时就确定了,程序退出时从内存消失。所有全局变量和静态变量在运行期间占用此区。进一步地,初始化为非0的全局变量和静态(全局或局部的)变量在一块区域(data区),未初始化或初始化为0的全局变量和未初始化或初始化为0的静态(全局或局部的)变量在相邻的另一块区域(.bss区)。在程序结束后由操作系统释放。可读可写。大小固定。

  • 常量区(全局区中的rodata区):存放常量、只读全局变量

  • 栈区:自动变量、函数形参、函数返回值都由编译器自动放入栈中。大小可变。函数的返回值不能是一个自动变量的地址。

  • 堆区:和栈类似,也是一种在程序运行过程中可随时修改的内存区域。是一个大容器,容量远大于栈。但在C中,申请和释放堆内存空间需手动。堆内存有一个最小单位,称为页,不过一个内存页的大小也不是固定的。

  • 总结:代码区和常量区是只读的;栈和堆是读写;代码区、常量区、静态区在程序运行期间大小固定;栈和堆根据程序的执行,其大小动态变化的。

  • 野指针问题:示例如下

char *test() {
	char *a = malloc(sizeof(char)* 10);
	memset(a, 'a', sizeof(char)* 9);
	a[9] = 0;//字符串结尾要有\0字符
	free(a);
	return a;
}
int main(int argc, char *args[]) {
	char *s = test();
	printf("%s\n", s);
	return 0;
}
//输出结果为乱码而非9个a,因为在main函数中test返回的是一个野指针
//解决方法是把test函数中的free操作挪到main函数中return 0;之前
char *test() {
	char *a = malloc(sizeof(char)* 10);
	memset(a, 'a', sizeof(char)* 9);
	a[9] = 0;
	return a;
}
int main(int argc, char *args[]) {
	char *s = test();
	printf("%s\n", s);
	free(s);
	return 0;
}
  • calloc函数:void *calloc(size_t _Count, size_t _Size);
    第一个参数是所需内存单元数量,第二个是每个内存单元的字节大小,与malloc函数不同的是,calloc函数会自动将所分配到的内存置0,如:int *p = (int *)calloc(100, sizeof(int));//分配100个int
  • realloc函数:void *realloc(void *p, size_t _NewSize);
    第一个参数p为以前用malloc或calloc分配的内存地址,第二个参数为重新分配内存的字节大小。若第一个参数为NULL,则和malloc功能一样。注:该函数不会自动清理增加的内存部分的内容,而且如果指定的地址后有连续空间,那么就会在已有地址基础上增加内存,若没有则会重新分配连续内存,并把旧内存的值拷贝到新内存同时释放旧内存。
  • 结构体的内存对齐模式:编译器在编译一个结构体的时候采用内存对齐模式,结构体总是以最大的成员作为对齐单位。从结构体的首地址开始向后依次为每个成员寻找第一个满足条件的首地址x,即x % N = 0,即每个成员必须放在能够整除自己字节大小的地址上。并且整个结构体的长度必须为结构体对齐参数的最小整数倍,不够就补空字节。不同类型成员的对齐参数N可能不同,所有成员的对齐参数N的最大值就称为结构体的对齐参数。
  • 结构体的位字段:结构体的某个字段是一个数据类型所占内存中的某几位。例如:
struct A {
	unsigned char a1:2;//该字段占用一个char字节中的前2个bit
	unsigned int a2:4;//该字段占用一个int中4个字节的前4个bit
};
//该结构体变量实际占用8个字节,但由于内存对齐,除这两个字段所占bit数外,该结构体变量的其他内存就浪费了
//所以当用多个位字段时,应考虑用相同的数据类型,如:
struct A {
	unsigned char a1:2;
	unsigned char a2:4;
};
//该结构体变量实际占用1个字节
注意,包含有不完整数据类型成员的结构体的字节对齐遵循的规则是:如果不完整类型成员是相邻元素,那么前面为一个完整数据类型开辟的内存空间没用完时可以继续存放,如果不相邻,那么即使是相同类型的,也不能接着存放而应另外开辟一个新的完整类型空间;如果相邻成员的数据类型不一样,那么更要另外开辟一个数据类型的完整空间。
  • 如果结构体里面包含有数组等复合类型,则结构体不会以整个数组的字节长度作为内存对齐单位,而是根据数组具体元素类型。如;
struct A {
	char name[10];
	int n;
};//以int的4个字节而非char数组的10个字节作为内存对齐单位,因此整个结构体对象占用16个字节
  • 结构体对象之间的赋值即对应字段之间的一一对应的拷贝,等同于如下内存拷贝:
memcpy(&a2, &a1, sizeof(a1));//a2 = a1;
//a1,a2是同一结构体类型的对象,上述语句把a1的内容拷贝给了a2
  • 当结构体中有指针成员时,该指针成员在初始化和赋值时需要提前为指针成员分配内存,不然就是在使用野指针。另外,在对象拷贝时也应注意指针成员的拷贝应该用内存拷贝函数memcpy,如:memcpy(a1.name, a2.name, 100);

文件操作

文本文件

  • fopen函数:FILE *fopen(const char *path, const char *mode);
    打开文件示例:
FILE *p = fopen("D:\\temp\\a.txt", "r");
//p这个指针程序员一般是不用来计算的,主要的用处是给c语言库函数做为参数使用的
if (p == NULL)
{
	printf("失败\n");
}
else
{
	printf("成功\n");
	char c = 0;
	while (c != EOF)//只要不是文件结尾,那么就一直循环
	{
		c = getc(p);
		//fopen返回的指针,是不能自己计算的,一定是要给c语言文件操作的库函数操作的
		//第二次调用getc的时候,getc会自动从第二个BYTE开始读取文件的内容,这个是不需要我们通过代码干预的
		printf("%c", c);
	}
	fclose(p);//fopen成功返回的指针
}

文件拷贝示例:

FILE *p1 = fopen("D:\\temp\\c.txt", "r");//用读的方式打开a.txt
FILE *p2 = fopen("D:\\temp\\d.txt", "w");//用写的方式打开b.txt
if (p1 && p2) {
	//读取p1,将读到的内容写入p2,就实现了文件的拷贝
	while (1) {
		char c = getc(p1);//从p1中读一个字节
		if (c == EOF)
			break;
		//c++;//字符简单加密
		putc(c, p2);//将p1中读到的字节写入p2
	}
	fclose(p1);
	fclose(p2);
}

读加密文件示例:

FILE *p = fopen("D:\\temp\\b.txt", "r");
if (p) {
	while (1) {
		char c = getc(p);
		if (c == EOF)
			break;
		//c--;//字符还原
		printf("%c", c);
	}
	fclose(p);
}

注:如果打开的文件,不关闭,一定消耗内存,同时一个进程同时打开的文件数是有限的;即使不关闭文件,进程在执行完成退出的时候,操作系统会自动把进程打开的文件都关闭;但不要依赖操作关闭文件,还是需要在代码中主动将文件关闭。

  • fputs函数:每次从文件中读一行到指定目标,包括行尾的回车
  • fputs函数:每次写一行到指定目标
  • feof函数:参数为fopen返回的文件指针,判断是否到文件尾。注意,宏EOF(值为-1)只对文本文件有效,对二进制文件无效。
  • sscanf函数和sprintf函数也是很有用的。
    示例:
FILE *p = fopen("D:\\temp\\a.txt", "r");
if (p) {
	while (!feof(p)) {
		char a[100] = { 0 };
		fgets(a, sizeof(a), p);//从p中读一行,包括行尾的回车
		printf("%s", a);
	}
	fclose(p);
}

行写文件示例:

FILE *p = fopen("D:\\temp\\a.txt", "w");
if (p) {
	while (1) {
		char a[100] = { 0 };
		scanf("%s", a);
		if (strcmp(a, "exit") == 0)
			break;
		//int len = strlen(a);
		//a[len] = '\n';//在字符串最后追加一个回车键
		strcat(a, "\n");
		fputs(a, p);
		//putc('\n', p);
	}
	fclose(p);
}
  • 关于fopen函数中第二个参数中的b的含义:这和Windows与Linux下文件换行符的区别有关。换行符:Windows下文本文件的每行结尾字符为\r\n,而linux下为\n。在Windows下读写文本时不写b,但在读写二进制文件时一定要写b;Linux下b是忽略的,不起作用。
FILE *p = fopen("D:\\temp\\a.txt", "wb");//b的意思是写文件的时候,不在\n前面自动加\r了
char a[100] = "hello\nworld";//字符串的结尾是'\0',但这个0不会写入文件
FILE *p = fopen("D:\\temp\\a.txt", "rb");//r的意思是自动忽略\n前面的\r
  • stat函数:
#include <sys/stat.h>
int  stat(const char * _Filename, struct stat * _Stat)
stat.st_size;//文件大小,单位:字节
//函数的第一个参数代表文件名,第二个参数是struct stat结构。

二进制文件

  • fwrite、 fread:
size_t fread ( void *buffer, size_t size, size_t count, FILE *stream) ;
size_t fwrite(const void* buffer, size_t size, size_t count, FILE* stream);
//第一个参数是buf的内存地址,第二个参数是每个单位的大小,第三个参数是多少个单位,第四个参数是fopen返回的文件指针
//只要第二个参数和第三个参数的乘积和第一个参数所指的内存区域一样大就可以。

注意:这两个函数以二进制形式对文件进行操作,不局限于文本文件
返回值:返回实际写入的数据块数目。当fread的第二个参数是1的时候,可以认为fread返回的值是读取到的单位数,正常情况下应等于第三个参数值。
示例1:

int array[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
FILE *p = fopen("D:\\temp\\a.dat", "wb"); 
//fwrite(array, 10, sizeof(int), p);
//fwrite(array, sizeof(int), 10, p);
int i;
for (i = 0; i < 10; i++)
{
	fwrite(&array[i], sizeof(int), 1, p);
}
fclose(p);
//注释掉的两种写法和下面的循环的写法得到的文件a.dat的内容是一样的

示例2:

FILE *p = fopen("D:\\temp\\a.dat", "rb");
while (!feof(p))
{
	int a[4];
	//int rec = fread(&a, sizeof(char), sizeof(a), p);//一个单位是1个字节,fread的返回值代表读取了多少个单位,而不是字节
	//当fread的第二个参数是1的时候,可以认为fread返回的是字节
	int rec = fread(&a, sizeof(a), 1, p);
	printf("rec = %d, a = %d\n", rec, a);
}
//fread返回值是读到的单位数而非字节数,例如:
fread(buf, 5, sizeof(int), p);//如果最后一次调用读取到了3个int,那么返回0,只要没读满buf,fread就返回0

示例3:从命令行输入参数实现文件拷贝

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#pragma warning(disable:4996)

#define MAX_MEM 1024 * 1024 * 64  //一次能拷贝的最大文件大小阈值设置为64M
int main(int argc, char *args[])
{
	if (argc < 3) {
		printf("参数不够,使用方法%s 源文件名 目标文件名\n", args[0]);
		return 0;
	}
	FILE *p1 = fopen(args[1], "rb");
	FILE *p2 = fopen(args[2], "wb");
	if (p1 == NULL) {
		printf("打开%s失败\n", args[1]);
		return 0;
	}
	if (p2 == NULL) {
		printf("打开%s失败\n", args[2]);
		return 0;
	}
	struct stat st = { 0 };
	stat(args[1], &st);

	int max = 0;//设置一个阈值,如果低于阈值的话,那么就分配实际大小,如果高于这个阈值那么最大就分配这个阈值的大小
	if (st.st_size < MAX_MEM) {
		max = st.st_size;
	} else {
		max = MAX_MEM;
	}
	char *buf = malloc(max);//文件多大,就分配多大的一个堆空间
	int index = 0;
	while (1) {
		int rc = fread(buf, 1, max, p1); //返回值可视为字节数
		if (rc == 0)
			break;
		fwrite(buf, 1, rc, p2);
		index++;
	}
	//index表示本次拷贝的文件大小,每循环一次最多拷贝一个阈值大小,index加1
	//因此本次拷贝的文件大小等于index值和阈值的乘积(严格说是文件大小的上限)
	printf("%d\n", index);
	fclose(p1);
	fclose(p2);
	free(buf);
	
	return 0;
}

注意以下两段代码的区别:

while (!feof(p)) {
	fread(&buf, 1, sizeof(buf), p);
}
while (fread(&buf, 1, sizeof(buf), p));

注:读取p指向的已打开的文件的内容时,上面的调用feof函数的循环在最后一次读取不成功后仍会执行一次,而下面的循环则不会,原因在于feof函数和fread函数的功能不同。

示例4:结构体文件的增删改查

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct {
	char name[20];
	int age;
} student;

int add() {//将一个结构体变量写入文件
	student st = { "张三", 30};
	FILE *p = fopen("D:\\temp\\student.dat", "ab");
	if (p == NULL) {
		return 0;
	}
	while (1) {
		memset(&st, 0, sizeof(st));//清空结构变量的内容
		printf("请输入姓名:");
		scanf("%s", st.name);
		if (st.name[0] == 0)
			break;
		printf("请输入年龄:");
		scanf("%d", &st.age);
		if (st.age == 0)
			break;
		fwrite(st, sizeof(st), 1, p);
	}
	fclose(p);
	return 0;
}

int select() {//查询
	student st = { 0 };
	FILE *p = fopen("D:\\temp\\student.dat", "rb");
	if (p == NULL) {
		return 0;
	}

	printf("请输入要查找的姓名:");
	char name[20] = { 0 };
	scanf("%s", name);
	while (fread(&st, sizeof(st), 1, p)) {
		if (strcmp(st.name, name) == 0)
			printf("name = %s, age = %d\n", st.name, st.age);
	}
	fclose(p);
	return 0;
}

int del() {//删除
	student *st = malloc(sizeof(student));
	FILE *p = fopen("D:\\temp\\student.dat", "rb");
	if (p == NULL) {
		return 0;
	}

	printf("请输入要删除的姓名:");
	char name[20] = { 0 };
	scanf("%s", name);

	int index = 0;
	while (fread(st + index, sizeof(student), 1, p)) {//把文件内容读到st指向的堆区内存中
		index++;
		st = realloc(st, (1 + index) * sizeof(student));
	}
	fclose(p);

	p = fopen("D:\\temp\\student.dat", "wb");
	int i;
	for (i = 0; i < index; i++) {
		if (strcmp(name, st[i].name) != 0)
			fwrite(st[i], 1, sizeof(student), p);
	}
	fclose(p);
	
	/*
	while (fread(&st, 1, sizeof(st), p)) {
		if (strcmp(st.name, name) == 0)
			printf("name = %s, age = %d\n", st.name, st.age);
	}
	*/
	
	free(st);
	return 0;
}

int update() {//修改
	student *st = malloc(sizeof(student));
	FILE *p = fopen("D:\\temp\\student.dat", "rb");
	if (p == NULL) {
		return 0;
	}

	printf("请输入要修改的姓名:");
	char name[20] = { 0 };
	scanf("%s", name);

	printf("请输入要修改后的姓名:");
	char name_new[20] = { 0 };
	scanf("%s", name_new);

	printf("请输入要修改后的年龄:");
	int age = 0;
	scanf("%d", &age);

	int index = 0;
	while (fread(st + index, 1, sizeof(student), p)) {
		index++;
		st = realloc(st, (1 + index) * sizeof(student));
	}
	fclose(p);

	p = fopen("D:\\temp\\student.dat", "wb");
	int i;
	for (i = 0; i < index; i++) {
		if (strcmp(name, st[i].name) == 0) {
			student st_new = { 0 };
			strcpy(st_new.name, name);
			st_new.age = age;
			fwrite(st_new, 1, sizeof(student), p);
		} else {
			fwrite(st[i], 1, sizeof(student), p);
		}
	}
	fclose(p);
	free(st);
	return 0;
}

int main() {
	printf("1:追加,2:查询,3:修改,4:删除");
	int a;
	scanf("%d", &a);
	switch(a) {
	case 1:
		add();
		break;
	case 2:
		select();
		break;
	case 3:
		update();
		break;
	case 4:
		del();
		break;
	default:
		break;
	}
	return 0;
}
  • fflush函数:将缓冲区中任何未写入的数据写入文件中。成功返回0,失败返回EOF。
int fflush(FILE * _File);

注:由于fflush是实时将缓冲区的内容写入磁盘,所以不要大量去使用,但如果是特别敏感的数据,可以通过fflush写入磁盘,防止由于电脑各种故障,内存的数据丢失。

  • 总结:所有的文件都是二进制的,文本文件是特殊的二进制文件,只不过文本文件中所有的内容都是ASCII字符;所有的文件都是基于char的数组,更严格地说,所有的文件都是基于char的流。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值