41、内联函数和一些鲜为人知的技巧
-
内联函数 解决程序中函数调用的效率问题。(但会增加编译时间)
- 定义函数前加上inline关键字
- 内联函数执行过程是在主函数中展开,而不是主函数-子函数-返回主函数。
- 内联函数虽然节省了函数调用的时间消耗,但由于每一个函数出现的地方都要进行替换,因此增加了代码编译的时间。另外,并不是所有的函数都能够变成内联函数。
- 现在的编译器很聪明,不写inline,也会自动将一些函数优化成内连函数
- 总结:编译器比你更了解哪些函数应该内联哪些不能内联,所以这个知识点你只需要知道就好……
#include<stdio.h>
inline int square(int x);
inline int square(int x)
{
return x*x;
}
int main()
{
int i=1;
while(i<=100){
printf("%d的平方是:%d\n",i-1,square(i++));
}
return 0;
}
-
#和## 是两个预处理运算符
- 在带参数的宏定义中,#运算符后面应该跟一个参数,预处理会把这个参数转化为一个字符串。
#include<stdio.h>
#define STR(s) # s
int main(){
printf("%s\n",STR(FISHC));
return 0;
}
#include<stdio.h>
#define STR(s) # s
int main(){
printf(STR(Hello %s num=%d\n),STR(FISHC),520);
return 0;
}
- ##运算符被称为记号连接运算符,比如可以使用##运算符连接两个参数
#include<stdio.h>
#define TOGETHER(x,y) x ## y
int main(){
printf("%d\n",TOGETHER(2,50));
return 0;
}
-
可变参数
- 带参数的宏定义也可以使用可变参数
- #define SHOWLIST(…) printf(#__VA_ARGS__)
- 其中…表示可变参数,__VA_ARGS__在预处理中被实际的参数集所替换(就像参数列表)(两边是两个下划线哦)
#include <stdio.h>
#define SHOWLIST(...) printf(#__VA_ARGS__)
#define PRINT(format, ...) printf(#format, ##__VA_ARGS__)
int main(void)
{
SHOWLIST(FishC, 520, 3.14\n);
PRINT(num = % d\n, 520);
PRINT(Hello FishC !\n);
return 0;
}
42、结构体
-
结构体声明 struct 结构体名
{
结构体成员1;
结构体成员2;
结构体成员3;
...
};//这里有一个分号
-
示例 struct Book
{
char title[128];
char author[40];
float price;
unsigned int date;
char publisher[40];
};
-
定义结构体类型变量
- struct 结构体名称 结构体变量名;
- 或者:在声明结构体时定义
struct 结构体名{
...
} 变量名;//不过这里是全局变量
//注意:如果
typedef struct 结构体名称{
...
}简称; //或typedef struct 结构体名 简称;
//使用typedef给结构体定义了一个简称,并不是变量
- 结构体可以放在函数外(全局),可以放在函数内(局部)
-
访问结构体变量
- 要访问结构体成员,我们需要引入一个新的运算符——点号( . )运算符。比如book.title就是引用book结构体的title成员,它是一个字符数组;而book.price则是引用book结构体的price成员,它是一个浮点型的变量。
#include<stdio.h>
struct Book
{
char title[128];
char author[40];
float price;
unsigned int date;
char publisher[40];
} book;
int main(){
printf("请输入书名:");
scanf("%s",book.title);
printf("请输入作者:");
scanf("%s",book.author);
printf("请输入售价:");
scanf("%f",&book.price);
printf("请输入出版日期:");
scanf("%d",&book.date);
printf("请输入出版社:");
scanf("%s",book.publisher);
printf("\n===========数据录入完毕========\n");
printf("书名:%s\n作者:%s\n售价:%.2f\n出版日期:%d\n出版社:%s\n",book.title,book.author,book.price,book.date,book.publisher);
}
-
初始化一个结构体变量(按顺序) stuct Book book={
"C语言",
"小明",
19.9,
20220420,
"出版社"
};
-
c99新特性:初始结构体的指定成员值
-
其语法和数组指定初始化元素类似,不过结构体指定初始化成员使用点号( . )运算符和成员名。 -
比如可以让程序只初始化Book的price成员:struct Book book={.price = 19.9}; -
还可以不按结构体声明的成员顺序进行初始化: struct Book book={ .price = 19.9, .title = “C语言” , .date = 20220420}; -
结构体的长度与内存对齐 #include<stdio.h>
int main(){
struct A
{
char a;
int b;
char c;
} a={'x',520,'o'};
printf("size of a=%d\n",sizeof(a));
return 0;
}
43、结构体数组和结构体指针
-
结构体嵌套 #include <stdio.h>
struct Date
{
int year;
int month;
int day;
} date;
struct Book
{
char title[128];
char author[40];
float price;
struct Date date;
char publisher[40];
} book = {
"C语言",
"小明",
19.9,
{2022, 4, 20},
"出版社"};
int main(void)
{
printf("书名:%s\n作者:%s\n售价:%.2f\n出版社:%s\n",
book.title, book.author, book.price, book.publisher);
printf("日期:%d-%d-%d", book.date.year, book.date.month, book.date.day);
return 0;
}
-
定义结构体数组
struct 结构体名称
{
结构体成员;
} 数组名[长度];
- 第二种方法是先声明一个结构体类型(比如上面Book) ,再用此类型定义一个结构体数组:
struct 结构体名称
{
结构体成员;
};
struct 结构体名称 数组名[长度];
-
初始化结构体数组 struct Book book[3]={
{"书1", "张三", 11.1, {2022, 1, 1}, "出版社1"},
{"书2", "李四", 22.2, {2022, 2, 2}, "出版社2"},
{"书3", "王五", 33.3, {2022, 3, 3}, "出版社3"}
};
-
结构体指针
-
struct Book *pt; pt=&book; -
通过结构体指针访问成员的两种方法
- (*结构体指针).成员名 【例如: (*pt).title;注意: *优先级低于点. 不加括号会先运行 pt.title;】
- 结构体指针->成员名 【如:pt->title,注意:箭头->用于指针;点. 用于对象】
44、传递结构体变量和结构体指针
-
传递结构体变量
- 两个相同结构体类型的结构体变量可以赋值。book1=book2;
#include <stdio.h>
struct Date
{
int year;
int month;
int day;
};
struct Book
{
char title[128];
char author[40];
float price;
struct Date date;
char publisher[40];
};
struct Book getInput(struct Book book)
{
printf("请输入书名:");
scanf("%s", book.title);
printf("请输入作者:");
scanf("%s", book.author);
printf("请输入售价:");
scanf("%f", &book.price);
printf("请输入出版日期:");
scanf("%d-%d-%d", &book.date.year, &book.date.month, &book.date.day);
printf("请输入出版社:");
scanf("%s", book.publisher);
return book;
}
void printBook(struct Book book)
{
printf("书名:%s\n作者:%s\n售价:%.2f\n出版日期:%d-%d-%d\n出版社:%s\n", book.title, book.author, book.price, book.date.year, book.date.month, book.date.day, book.publisher);
}
int main()
{
struct Book book1, book2;
printf("请输入第一本书的信息");
book1 = getInput(book1);
printf("请输入第二本书的信息");
book2 = getInput(book2);
printf("打印第一本书的信息");
printBook(book1);
putchar('\n');
printf("打印第二本书的信息");
printBook(book2);
return 0;
}
- 提高执行效率,函数可以不用结构体传址,而是传递指向结构体变量的指针。
#include <stdio.h>
struct Date
{
int year;
int month;
int day;
};
struct Book
{
char title[128];
char author[40];
float price;
struct Date date;
char publisher[40];
};
void getInput(struct Book *book);
void printBook(struct Book *book);
void getInput(struct Book *book)
{
printf("请输入书名:");
scanf("%s", book->title);
printf("请输入作者:");
scanf("%s", book->author);
printf("请输入售价:");
scanf("%f", &book->price);
printf("请输入出版日期:");
scanf("%d-%d-%d", &book->date.year, &book->date.month, &book->date.day);
printf("请输入出版社:");
scanf("%s", book->publisher);
}
void printBook(struct Book *book)
{
printf("书名:%s\n作者:%s\n售价:%.2f\n出版日期:%d-%d-%d\n出版社:%s\n", book->title, book->author, book->price, book->date.year, book->date.month, book->date.day, book->publisher);
}
int main()
{
struct Book book1, book2;
printf("请输入第一本书的信息\n");
getInput(&book1);
putchar('\n');
printf("请输入第二本书的信息\n");
getInput(&book2);
printf("打印第一本书的信息\n");
printBook(&book1);
putchar('\n');
printf("打印第二本书的信息\n");
printBook(&book2);
return 0;
}
-
动态申请结构体
int main(){
struct Book *book1,*book2;
book1=(struct Book *)malloc(sizeof(struct Book));
book2=(struct Book *)malloc(sizeof(struct Book));
if(book1==NULL||book2==NULL){
printf("内存分配失败!\n");
exit(1);
}
printf("请输入第一本书的信息");
getInput(book1);
printf("请输入第二本书的信息");
getInput(book2);
printf("显示第一本书的信息");
printBook(book1);
printf("显示第二本书的信息");
printBook(book2);
free(book1);
free(book2);
return 0;
}
45、单链表1
-
单链表
-
单链表插入元素(头插法) #include <stdio.h>
#include <stdlib.h>
struct Book
{
char title[128];
char author[40];
struct Book *next;
};
void getInput(struct Book *book)
{
printf("请输入书名:");
scanf("%s", book->title);
printf("请输入作者:");
scanf("%s", book->author);
}
void addBook(struct Book **library)
{
struct Book *book, *temp;
book = (struct Book *)malloc(sizeof(struct Book));
if (book == NULL)
{
printf("内存分配失败!\n");
exit(1);
}
getInput(book);
if (*library != NULL)
{
temp = *library;
*library = book;
book->next = temp;
}
else
{
*library = book;
book->next = NULL;
}
}
void printLibrary(struct Book *library)
{
struct Book *book;
int count = 1;
book = library;
while (book != NULL)
{
printf("-----------Book%d-----------\n", count);
printf("书名:%s\n", book->title);
printf("作者:%s\n", book->author);
book = book->next;
count++;
}
}
void releseLibrary(struct Book *library)
{
struct Book *temp;
while (library != NULL)
{
temp = library->next;
free(library);
library = temp;
}
}
int main()
{
struct Book *library = NULL;
int ch;
while (1)
{
printf("是否录入书籍信息(Y/N):");
do
{
ch = getchar();
} while (ch != 'Y' && ch != 'N');
if (ch == 'Y')
{
addBook(&library);
}
else
{
break;
}
}
printf("是否打印书籍信息(Y/N):");
do
{
ch = getchar();
} while (ch != 'Y' && ch != 'N');
if (ch == 'Y')
{
printLibrary(library);
}
releseLibrary(library);
return 0;
}
46、单链表2
-
单链表插入元素(尾插法)
void addBook2(struct Book **library)
{
struct Book *book, *temp;
book = (struct Book *)malloc(sizeof(struct Book));
if (book == NULL)
{
printf("内存分配失败!\n");
exit(1);
}
getInput(book);
if (*library != NULL)
{
temp = *library;
while (temp->next != NULL)
{
temp = temp->next;
}
temp->next = book;
book->next = NULL;
}
else
{
*library = book;
book->next = NULL;
}
}
void addBook2(struct Book **library)
{
struct Book *book;
static struct Book *tail;
book = (struct Book *)malloc(sizeof(struct Book));
if (book == NULL)
{
printf("内存分配失败!\n");
exit(1);
}
getInput(book);
if (*library != NULL)
{
tail->next = book;
book->next = NULL;
}
else
{
*library = book;
book->next = NULL;
}
tail = book;
}
-
搜索单链表,比如输入书名或作者,就可以找到相关的节点数据。
struct Book *searchBook(struct Book *library, char *target)
{
struct Book *book;
book = library;
while (book != NULL)
{
if (!strcmp(book->title, target) || !strcmp(book->author, target))
{
break;
}
book = book->next;
}
return book;
}
void printBook(struct Book *book)
{
printf("书名:%s", book->title);
printf("作者:%s", book->author);
}
int main()
{
struct Book *library = NULL;
struct Book *book;
char input[128];
int ch;
while (1)
{
printf("是否录入书籍信息(Y/N):");
do
{
ch = getchar();
} while (ch != 'Y' && ch != 'N');
if (ch == 'Y')
{
addBook(&library);
}
else
{
break;
}
}
printf("是否打印书籍信息(Y/N):");
do
{
ch = getchar();
} while (ch != 'Y' && ch != 'N');
if (ch == 'Y')
{
printLibrary(library);
}
printf("请输入查找的书名或作者");
scanf("%s", input);
book = searchBook(library, input);
if (book == NULL)
{
printf("很抱歉,未找到");
}
else
{
do
{
printf("已找到符合条件的图书...");
printBook(book);
} while ((book = searchBook(book->next, input)) != NULL);
}
releseLibrary(library);
return 0;
}
47、单链表3
-
单链表插入节点(中间插入) #include <stdio.h>
#include <stdlib.h>
struct Node
{
int value;
struct Node *next;
};
void insertNode(struct Node **head, int value)
{
struct Node *previous;
struct Node *current;
struct Node *new;
current = *head;
previous = NULL;
while (current != NULL && current->value < value)
{
previous = current;
current = current->next;
}
new = (struct Node *)malloc(sizeof(struct Node));
if (new == NULL)
{
printf("内存分配失败!\n");
exit(1);
}
new->value = value;
new->next = current;
if (previous == NULL)
{
*head = new;
}
else
{
previous->next = new;
}
}
void printNode(struct Node *head)
{
struct Node *current;
current = head;
while (current != NULL)
{
printf("%d ", current->value);
current = current->next;
}
printf("\n");
}
int main()
{
struct Node *head = NULL;
int input;
while (1)
{
printf("请输入一个整数(输入-1表示结束):");
scanf("%d", &input);
if (input == -1)
{
break;
}
insertNode(&head, input);
printNode(head);
}
return 0;
}
-
单链表删除节点 void deleteNode(struct Node **head, int value)
{
struct Node *previous;
struct Node *current;
current = *head;
previous = NULL;
while (current != NULL && current->value != value)
{
previous = current;
current = current->next;
}
if (current == NULL)
{
printf("找不到匹配的节点");
return;
}
else
{
if (previous == NULL)
{
*head = current->next;
}
else
{
previous->next = current->next;
}
free(current);
}
}
int main()
{
struct Node *head = NULL;
int input;
while (1)
{
printf("请输入一个整数(输入-1表示结束):");
scanf("%d", &input);
if (input == -1)
{
break;
}
insertNode(&head, input);
printNode(head);
}
printf("开始测试删除整数。。。\n");
while(1){
printf("请输入一个整数(输入-1表示结束):");
scanf("%d",&input);
if(input==-1){
break;
}
deleteNode(&head,input);
printNode(head);
}
return 0;
}
48、内存池
49、基础typedef
-
typedef是C语言最重要的关键字之一 -
世界上第一门高级编程语言 Fortran -
typedef基本功能:给数据类型(包括结构体)起别名
- typedef A B;//给A取别名B
- typedef A B,C;//给A取别名B/C。可以取多个别名
-
define也可以 #define B A -
区别
- define是直接替换
- typedef是对类型的封装。用来起别名。可以对指针类型取别名。
-
给结构体取别名 #include <stdio.h>
#include <stdlib.h>
typedef struct Date
{
int year;
int month;
int day
} DATE, *PDATE;
int main(void)
{
struct Date *date;
date = (PDATE)malloc(sizeof(DATE));
if (date == NULL)
{
printf("内存申请失败\n");
exit(1);
}
date->year = 2022;
date->month = 4;
date->day = 22;
printf("%d%d%d\n", date->year, date->month, date->day);
return 0;
}
50、进阶typedef
-
使用typedef目的一般有两个
- 给变量起一个容易记住且意义明确的别名
- 简化一些比较复杂的类型声明。
-
一些比较复杂的声明语句
-
int (*ptr)[3]; :数组指针, ptr是一个指针,指向三个整型元素的数组(数组起始地址) #include <stdio.h>
typedef int (*PTR_TO_ARRAY)[3];
int main(){
int array[3] = {1,2,3};
PTR_TO_ARRAY ptr_to_array = &array;
int i;
for(i=0;i<3;i++){
printf("%d\n",(*ptr_to_array)[i]);
}
return 0;
}
-
int (*fun)(void); :函数指针,指向一个参数是void,返回值为int 的函数 #include <stdio.h>
typedef int (*PTR_TO_FUN)(void);
int fun(void)
{
return 123;
}
int main(void)
{
PTR_TO_FUN ptr_to_fun = &fun;
printf("%d\n", (*ptr_to_fun)());
return 0;
}
-
int *(*array[3])(int); :
- (*array[3])是一个指针数组并用A替换,则int *A(int)是一个指针函数
#include <stdio.h>
typedef int *(*PTR_TO_FUN)(int);
int *funA(int num)
{
printf("%d\t", num);
return #
}
int *funB(int num)
{
printf("%d\t", num);
return #
}
int *funC(int num)
{
printf("%d\t", num);
return #
}
int main(void)
{
PTR_TO_FUN array[3] = {&funA, &funB, &funC};
int i;
for (i = 0; i < 3; i++)
{
printf("addr pf num: %p\n", (*array[i])(i));
}
return 0;
}
-
void (*funA(int, void (*funB)(int)))(int);
51、共用体
- 在进行某些算法的C语言编程的时候,需要使几种不同类型的变量存放到同一段内存单元中。也就是使用覆盖技术,几个变量互相覆盖。这种几个不同的变量共同占用一段内存的结构,在C语言中,被称作“共用体”类型结构,简称共用体,也叫联合体。
union 共用体名称
{
共用体成员1;
共用体成员2;
共用体成员3;
...
};
- 共用体所有成员共享同一个内存地址。地址长度足以容纳最大成员的长度。也会受内存对齐影响
#include <stdio.h>
#include <string.h>
typedef int *(*PTR_TO_FUN)(int);
union Test
{
int i;
double pi;
char str[10];
};
int main()
{
union Test test;
test.i = 520;
test.pi = 3.14;
strcpy(test.str, "FishC.com");
printf("addr of test.i:%p\n", &test.i);
printf("addr of test.pi:%p\n", &test.pi);
printf("addr of test.str:%p\n", &test.str);
printf("test.i:%d\n", test.i);
printf("test.pi:%.2f\n", test.pi);
printf("test.str:%s\n", test.str);
printf("size of int:%d\n", sizeof(int));
printf("size of double:%d\n", sizeof(double));
printf("size of test.str:%d\n", sizeof(test.str));
printf("size of test:%d\n", sizeof(test));
return 0;
}
-
定义共用体类型变量
- 定义共用体跟定义结构体的语法相似,可以先声明一个共用体类型,再定义共用体变量:
union data
{
int i;
char ch;
float f;
};
union data a,b,c;
union data
{
int i;
char ch;
float f;
} a, b, c;
union {
int a;
char b;
} a,b,c;
-
初始化共用体 union data a = {520};
union data b = a;
union data c = {.ch = 'c'};
-
拓展:https://blog.csdn.net/xy010902100449/article/details/48292527
- 共用体常用来节省内存,特别是一些嵌入式编程,内存是非常宝贵的!
- 共用体也常用于操作系统数据结构或硬件数据结构!
- union 在操作系统底层的代码中用的比较多,因为它在内存共享布局上方便且直观。所以网络编程,协议分析,内核代码上有一些用到 union 都比较好懂,简化了设计。
- 共用体(union)是一种数据格式,它能够存储不同类型的数据,但是只能同时存储其中的一种类型。
52、枚举类型
-
如果一个变量只有几种可能的值,那么就可以将其定义为枚举(enumeration)类型 -
声明枚举类型 -
enum 枚举类型名称 {枚举值名称, 枚举值名称…}; -
定义枚举变量
-
代码实例 #include <stdio.h>
#include <time.h>
int main()
{
enum Week
{
sun,
mon,
tue,
wed,
thu,
fri,
sat
};
enum Week today;
struct tm *p;
time_t t;
time(&t);
p = localtime(&t);
today = (enum Week)p->tm_wday;
switch (today)
{
case mon:
case tue:
case wed:
case thu:
case fri:
printf("工作日\n");
break;
case sat:
case sun:
printf("休息日\n");
break;
default:
printf("Error\n");
}
return 0;
}
53、位域
-
单片机(Microcontrollers)是一种集成电路芯片
-
位域,或称位段、位字段。
- 位段,C语言允许在一个结构体中以位为单位来指定其成员所占内存长度,这种以位为单位的成员称为“位段”或称“位域”( bit field) 。利用位段能够用较少的位数存储数据,节约空间
- 允许把一个字节拆开来使用,把一个字节中的二进制位划分为一个区域并指定每一个区域的位数,每一个区域可以命名然后在程序中单独进行使用
- 例如,对一个字节划分为几个部分并命名,这几部分就是位域
-
使用位域的做法是 在结构体定义时,在结构体或成员后面使用冒号( : )和数字来表示该成员所占的位byte数。
#include <stdio.h>
int main()
{
struct Test
{
unsigned int a : 1;
unsigned int b : 1;
unsigned int c : 2;
};
struct Test test;
test.a = 0;
test.b = 1;
test.c = 2;
printf("a=%d,b=%d,c=%d\n", test.a, test.b, test.c);
printf("size of test=%d", sizeof(test));
return 0;
}
-
无名位域
- 位域成员可以没有名称,只要给出数据类型和宽度即可。
struct Test{
unsigned int x:100;
unsigned int :100
}
54、位操作
-
c语言并没有规定一个字节有几位(一般是8位),只是规定“可寻址的数据存储单位,其尺寸必须可以容纳运行环境的基本字符集的任何成员”。一般是由编译器规定在limits.h中 -
逻辑位运算符 -
按位取反( ~ )
- 逻辑位运算符中优先级最高的是按位取反运算符,它的运算符是一个~符号,作用是将1变成0,将0变成1:
-
按位与( & )
- 优先级第二高的是按位与运算符,它的运算符是一个&符号(逻辑与是两个&符号)
-
按位异或 ( ^ )
- 优先级排第三的是按位异或运算符,它的运算符是一个^符号,只有当两个操作数对应的二进制位不同时,它的结果才为1,否则为0:
-
按位或( | ) -
逻辑位运算符中优先级最低的是按位或运算符,它的运算符是一个|符号(逻辑或是两个|符号) -
和赋值号结合
- 这四个运算符,除了按位取反只有一个操作数之外,其它三个都可以跟赋值号(=)结合到一块,使得代码更加简洁
-
代码示例 #include <stdio.h>
int main()
{
int mask = 0xFF;
int v1 = 0xABCDEF;
int v2 = 0xABCDEF;
int v3 = 0xABCDEF;
v1 &= mask;
v2 |= mask;
v3 ^= mask;
printf("v1=0x%X\nv2=0x%X\nv3=0x%X\n", v1, v2, v3);
return 0;
}
55、移位和位操作的应用
-
C语言除了提供四种逻辑位运算符之外,还提供了可以将某个变量中所有的二进制位进行左移或右移的运算符——移位运算符。 -
左移位运算符 << -
左边操作数是即将被移位的数据,右边是要移动的位数。移出的数据扔掉,移动后空位用0填充 -
右移位运算符 >> -
左边操作数是即将被移位的数据,右边是要移动的位数。移出的数据扔掉,移动后空位用0填充 -
和赋值号结合 #include <stdio.h>
int main()
{
int value = 1;
printf("----------左移---------\n");
while (value < 1024)
{
value <<= 1;
printf("value=%d\n", value);
}
printf("\n");
printf("----------右移---------\n");
value = 1024;
while (value > 1)
{
value >>= 2;
printf("value = %d\n", value);
}
return 0;
}
-
一些未定义行为
- 左移、右移运算符右边的操作数如果是为负数,或者右边的操作数大于左边操作数支持的最大宽度,那么表达式的结果均是属于“未定义行为”。
- 左边的操作数是有符号还是无符号数其实也对移位运算符有着不同的影响。无符号数肯定没问题,因为这时候变量里边所有的位都用于表示该数值的大小。但如果是有符号数,那就要区别对待了,因为有符号数的左边第一位是符号位,所以如果恰好这个操作数是个负数,那么移动之后是否覆盖符号位的决定权还是落到了编译器上。
56、打开和关闭文件
-
文件
- “计算机文件(或称文件、电脑档案、档案),是存储在某种长期储存设备或临时存储设备中的一段数据流,并且归属于计算机文件系统管理之下。所谓“长期储存设备”一般指磁盘、光盘、磁带等。而“短期存储设备”一般指计算机内存。需要注意的是,存储于长期存储设备的文件不一定是长期存储的,有些也可能是程序或系统运行中产生的临时数据,并于程序或系统退出后删除。”——维基百科
-
linux/unix
- 万物皆文件 Everything is a file
- KISS原则 Keep it simple,stupid.
-
文本文件和二进制文件
- 文本文件:由一些字符的序列组成的文件
- 二进制文件:通常指除了文本文件以外的
- 从本质上来说,文本文件也是属于二进制文件的,只不过它存放的是相应的字符编码值。。
-
打开和关闭文件
- 读:从文件中获取数据
- 写:将数据写入到文件里
- 在完成读写操作后,必须将其关闭
-
fopen(路径,模式),用于打开文件并返回函数指针
-
路径参数可以是相对路径(./a.txt),也可以是绝对路径(/home/user1/a.txt)。如果只给出文件名(a.txt)则表示该文件在当前文件夹。 -
如果文件打开成功,返回一个指向FILE结构的文件指针; -
如果文件打开失败,返回NULL并设置errno为指定的错误 -
文件的打开模式
模式 | 描述 |
---|
“r” | 1 、以只读的模式打开一个文本文件,从文件头开始读取; 2、 该文本文件必须存在 | “w” | 1、以只写的模式打开一个文本文件,从文件头开始写入 ;2、如果文件不存在则创建一个新的文件; 3、如果文件已存在则将文件的长度截断为 0(重新写入的内容将覆盖原有的所有内容) | “a” | 1、以追加的模式打开一个文本文件,从文件末尾追加内容; 2、如果文件不存在则创建一个新的文件 | “r+” | 1、以读和写的模式打开一个文本文件,从文件头开始读取和写入; 2、该文件必须存在; 3、该模式不会将文件的长度截断为 0(只覆盖重新写入的内容,原有的内容保留) | “w+” | 1、以读和写的模式打开一个文本文件,从文件头开始读取和写入; 2、如果文件不存在则创建一个新的文件; 3、如果文件已存在则将文件的长度截断为 0(重新写入的内容将覆盖原有的所有内容) | “a+” | 1、以读和追加的模式打开一个文本文件; 2、如果文件不存在则创建一个新的文件; 3、读取是从文件头开始,而写入则是在文件末尾追加 | “b” | 1、与上面 6 中模式均可结合(“rb”, “wb”, “ab”, “r+b”, “w+b”, “a+b”); 其描述的含义一样,只不过操作的对象是二进制文件 |
- 打开方式要区分开主要是因为换行符。C语言换行符\n,unix系统\n,windows用\r\n,mac用\r。如果在windows系统以文本文件打开,读会将\r\n自动转换为\n,写会\n转换为\r\n,但是以二进制模式打开则不会做转换。如果在unix系统二者是一样的。
-
代码示例 #include <stdio.h>
#include <stdlib.h>
int main(void)
{
FILE *fp;
int ch;
if ((fp = fopen("hello.txt", "r")) == NULL)
{
printf("打开文件失败!\n");
exit(EXIT_FAILURE);
}
while ((ch = getc(fp)) != EOF)
{
putchar(ch);
}
fclose(fp);
return 0;
}
57、读写文件1
-
顺序读写和随机读写 -
读单个字符
-
fgetc 函数返回来自stream(流)中的下一个字符
-
函数用于从文件流中读取下一个字符并推进文件的位置指示器(用来指示接下来要读写的下一个字符的位置) 。 -
函数原形: #include <stdio.h>
int fgetc( FILE *stream );
-
参数:stream,该参数是一个FILE对象的指针,指定一个待读取的文件流 -
返回值:
- 将读取到的unsigned char类型转换为int并返回
- 如果到达文件尾或者发生错误时返回EOF.
-
备注:
- fgetc 函数和 getc 函数两个的功能和描述基本上是一模一样的,它们的区别主要在于实现上:fgetc 是一个函数;而 getc 则是一个宏的实现。
- 一般来说宏产生较大的代码,但是避免了函数调用的堆栈操作,所以速度会比较快。
- 由于 getc 是由宏实现的,对其参数可能有不止一次的调用,所以不能使用带有副作用(side effects)的参数。所谓带有副作用的参数就是指 getc(fp++) 这类型的参数,因为参数在宏的实现中可能会被调用多次,所以你的想法是 fp++,而副作用下产生的结果可能是 fp++++++。
-
写单个字符
-
fputc 函数用于将一个字符写入到指定的文件中并推进文件的位置指示器(用来指示接下来要读写的下一个字符的位置)。
-
备注:
- fputc 函数和 putc 函数两个的功能和描述基本上是一模一样的,它们的区别主要在于实现上:fputc 是一个函数;而 putc 则是一个宏的实现。
-
代码示例: #include <stdio.h>
#include <stdlib.h>
int main(void)
{
FILE *fp1, *fp2;
int ch;
if ((fp1 = fopen("hello.txt", "r")) == NULL)
{
printf("打开文件失败!\n");
exit(EXIT_FAILURE);
}
if ((fp2 = fopen("world.txt", "w")) == NULL)
{
printf("打开文件失败!\n");
exit(EXIT_FAILURE);
}
while ((ch = fgetc(fp1)) != EOF)
{
fputc(ch, fp2);
}
fclose(fp1);
fclose(fp2);
return 0;
}
-
读写整个字符串
-
fgets 函数用于从指定文件中读取字符串
-
fgets 函数最多可以读取 size - 1 个字符,因为结尾处会自动添加一个字符串结束符 ‘\0’。当读取到换行符(‘\n’)或文件结束符(EOF)时,表示结束读取(‘\n’ 会被作为一个合法的字符读取,EOF不会)。 -
函数原型: #include <stdio.h>
char *fgets(char *s, int size, FILE *stream);
-
参数:
- s :字符型指针,指向用于存放读取字符串的位置
- size : 指定读取的字符数(包括最后自动添加的 ‘\0’)
- stream : 该参数是一个 FILE 对象的指针,指定一个待操作的数据流
-
返回值:
- 如果函数调用成功,返回 s 参数指向的地址。
- 如果在读取字符的过程中遇到 EOF,则 eof 指示器被设置;如果还没读入任何字符就遇到这种 EOF,则 s 参数指向的位置保持原来的内容(s不变),函数返回 NULL。
- 如果在读取的过程中发生错误,则 error 指示器被设置,函数返回 NULL,但 s 参数指向的内容可能被改变。
-
fputs 函数用于将一个字符串写入到指定的文件中,表示字符串结尾的 ‘\0’ 不会被一并写入。
-
feof 函数用于检测文件的末尾指示器(end-of-file indicator)是否被设置。
-
函数原型: #include <stdio.h>
int feof(FILE *stream);
-
参数:stream, 该参数是一个 FILE 对象的指针,指定一个待检测的文件流 -
返回值:
- 如果检测到末尾指示器(end-of-file indicator)被设置,返回一个非 0 值;
- 如果检测不到末尾指示器(end-of-file indicator)被设置,返回值为 0。
-
feof 函数仅检测末尾指示器的值,它们并不会修改文件的位置指示器。 -
文件末尾指示器只能使用 clearerr 函数清除。 -
代码示例: #include <stdio.h>
#include <stdlib.h>
#define MAX 1024
int main()
{
FILE *fp;
char buffer[MAX];
if ((fp = fopen("hello.txt", "w")) == NULL)
{
printf("打开文件失败!\n");
exit(EXIT_FAILURE);
}
fputs("hello 1\n", fp);
fputs("hello 2\n", fp);
fputs("hello 3\n", fp);
fclose(fp);
if ((fp = fopen("hello.txt", "r")) == NULL)
{
printf("打开文件失败!\n");
exit(EXIT_FAILURE);
}
while (!feof(fp))
{
fgets(buffer, MAX, fp);
printf("%s", buffer);
}
fclose(fp);
return 0;
}
58、读写文件2
-
格式化读写文件
- fscanf
- fprintf
- 和scanf、printf相似,只不过是从文件读取、输出到文件
- 拓展 为什么scanf中用&取地址符,而printf不用。因为scanf本来就是一个函数,用取地址后就能将接受的数据存在这个地址里,在scanf函数外也能用。指针在函数内就是通过访问所指向地址的值来进行改写,并且能延续到函数外。
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main()
{
FILE *fp;
struct tm *p;
time_t t;
time(&t);
p = localtime(&t);
if ((fp = fopen("date.txt", "w")) == NULL)
{
printf("打开文件失败!\n");
exit(EXIT_FAILURE);
}
fprintf(fp, "%d-%d-%d", 1900 + p->tm_year, 1 + p->tm_mon, p->tm_mday);
fclose(fp);
int year, month, day;
if ((fp = fopen("date.txt", "r")) == NULL)
{
printf("打开文件失败!\n");
exit(EXIT_FAILURE);
}
fscanf(fp, "%d-%d-%d", &year, &month, &day);
printf("%d-%d-%d", year, month, day);
fclose(fp);
return 0;
}
-
以二进制方式读写文本文件 #include <stdio.h>
#include <stdlib.h>
int main()
{
FILE *fp;
if ((fp = fopen("text.txt", "wb")) == NULL)
{
printf("打开文件失败!\n");
exit(EXIT_FAILURE);
}
fputc('5', fp);
fputc('2', fp);
fputc('0', fp);
fputc('\n', fp);
fclose(fp);
return 0;
}
-
二进制读写文件
-
fread 函数用于从指定的文件中读取指定尺寸的数据。
-
fwrite 函数用于将指定尺寸的数据写入到指定的文件中。
-
代码示例 #include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct Date
{
int year;
int month;
int day;
};
struct Book
{
char name[40];
char author[40];
char publisher[40];
struct Date date;
};
int main()
{
FILE *fp;
struct Book *book_for_write, *book_for_read;
book_for_write = (struct Book *)malloc(sizeof(struct Book));
book_for_read = (struct Book *)malloc(sizeof(struct Book));
if (book_for_write == NULL || book_for_read == NULL)
{
printf("内存分配失败");
exit(EXIT_FAILURE);
}
strcpy(book_for_write->name, "带你学c带你飞");
strcpy(book_for_write->author, "小甲鱼");
strcpy(book_for_write->publisher, "清华大学出版社");
book_for_write->date.year = 2017;
book_for_write->date.month = 11;
book_for_write->date.day = 11;
if ((fp = fopen("file.txt", "w")) == NULL)
{
printf("打开文件失败!\n");
exit(EXIT_FAILURE);
}
fwrite(book_for_write, sizeof(struct Book), 1, fp);
fclose(fp);
if ((fp = fopen("file.txt", "r")) == NULL)
{
printf("打开文件失败!\n");
exit(EXIT_FAILURE);
}
fread(book_for_read, sizeof(struct Book), 1, fp);
printf("书名:%s\n", book_for_read->name);
printf("作者:%s\n", book_for_read->author);
printf("出版社:%s\n", book_for_read->publisher);
printf("出版日期:%d-%d-%d\n", book_for_read->date.year, book_for_read->date.month, book_for_read->date.day);
fclose(fp);
return 0;
}
59、随机读写文件
-
ftell返回给定流 stream 的当前文件位置。 #include <stdio.h>
#include <stdlib.h>
int main()
{
FILE *fp;
if ((fp = fopen("hello.txt", "w")) == NULL)
{
printf("文件打开失败!\n");
exit(EXIT_FAILURE);
}
printf("%ld\n", ftell(fp));
fputc('F', fp);
printf("%ld\n", ftell(fp));
fputs("ishC\n", fp);
printf("%ld\n", ftell(fp));
fclose(fp);
return 0;
}
-
rewind移动到文件头
rewind(fp);
fputs("AA",fp);
-
fseek用于设置文件流的位置指示器
-
函数原型 #include<stdio.h>
int fseek(FILE *stream,long int offset,int whence);
-
参数
参数 | 含义 |
---|
stream | 该参数是一个FILE对象的指针,指定一个待操作的文件流 | offset | 指定从whence参数的位起置偏移多少个字节 | whence | 这是表示开始添加偏移 offset 的位置。它一般指定为下列常量之一:【SEEK_SET: 文件的开头】;【SEEK_CUR: 文件指针的当前位置】;【SEEK_END :文件的末尾】 |
-
返回值:如果成功,则该函数返回零,否则返回非零值。 -
代码示例 #include <stdio.h>
#include <stdlib.h>
#define N 4
struct Stu
{
char name[24];
int num;
float score;
} stu[N], sb;
int main()
{
FILE *fp;
int i;
if ((fp = fopen("score.txt", "w")) == NULL)
{
printf("打开文件失败!\n");
exit(EXIT_FAILURE);
}
for (i = 0; i < N; i++)
{
printf("请开始录入成绩(格式:姓名 学号 成绩)");
scanf("%s %d %f", stu[i].name, &stu[i].num, &stu[i].score);
}
fwrite(stu, sizeof(struct Stu), N, fp);
fclose(fp);
if ((fp = fopen("score.txt", "rb")) == NULL)
{
printf("打开文件失败!");
exit(EXIT_FAILURE);
}
fseek(fp, sizeof(struct Stu), SEEK_SET);
fread(&sb, sizeof(struct Stu), 1, fp);
printf("%s(%d)的成绩是:%.2f\n", sb.name, sb.num, sb.score);
fclose(fp);
return 0;
}
-
可移植性问题
- 对于以二进制模式打开的文件,fseek在某些操作系统中可能不支持SEEK_END位置。
- 对于以文本模式打开的文件,feek函数的whence参数只能取SEEK_SET才是有意义的,并且传递给offset参数的值要么是0,要么是上一次对同一个文件调用ftell函数获取的返回值。
60、标准流和错误处理
-
文件流
- 标准输入 stdin
- 标准输出 stdout
- 标准错误输出 stderr
-
标准错误输出 stderr 代码示例 #include <stdio.h>
#include <stdlib.h>
int main()
{
FILE *fp;
if ((fp = fopen("压根都不存在的文件.txt", "r")) == NULL)
{
fputs("打开文件失败!\n", stderr);
exit(EXIT_FAILURE);
}
fclose(fp);
return 0;
}
-
重定向
-
错误处理
- 检测错误指示器,使用ferror函数
- 使用clearerr函数可以人为的清除文件末尾指示器和错误指示器状态
#include <stdio.h>
#include <stdlib.h>
int main()
{
FILE *fp;
int ch;
if ((fp = fopen("0_hello.txt", "r")) == NULL)
{
fputs("打开文件失败!\n", stderr);
exit(EXIT_FAILURE);
}
while (1)
{
ch = fgetc(fp);
if (feof(fp))
{
break;
}
putchar(ch);
}
fputc('c', fp);
if (ferror(fp))
{
fputs("出错了,这条消息输出到错误输出流\n", stderr);
}
clearerr(fp);
if (feof(fp) || ferror(fp))
{
printf("123456");
}
fclose(fp);
return 0;
}
- ferror函数只能检测是否出错,但无法获取错误原因。不过,大多数系统函数在出现错误时会将错误原因就在errno中。
- perror函数可以直观的打印出错误原因。会自己加"冒号空格错误原因"。例如perror(“错误原因是”);输出为"错误原因是: Bad file descriptor"
- strerror函数直接返回错误码对应的错误消息,参数是errno。
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
int main()
{
FILE *fp;
if ((fp = fopen("压根都不存在的文件.txt", "r")) == NULL)
{
printf("打开文件失败,原因(错误代码)是:%d\n",errno);
perror("打开文件失败,原因是");
fprintf(stderr,"出错,原因竟是 -> %s <- 这个!",strerror(errno));
exit(EXIT_FAILURE);
}
fclose(fp);
return 0;
}
61、IO缓冲区
-
IO缓冲区 #include <stdio.h>
#include <stdlib.h>
int main()
{
FILE *fp;
if ((fp = fopen("12_output.txt", "w")) == NULL)
{
perror("打开文件失败,原因");
exit(EXIT_FAILURE);
}
fputs("123456\n", fp);
getchar();
fclose(fp);
return 0;
}
-
标准IO提供三种类型的缓冲模式
- 按块缓存:也称为全缓存,在填满缓冲区后才进行实际的设备读写操作;
- 按行缓存:是指在接收到换行符\n之前,数据都是先缓存在缓冲区的;
- 不缓存:允许直接读写设备上的数据
-
fflush函数可以强制刷新缓冲区 -
setvbuf 函数定义流 stream 应如何缓冲。
-
函数原型 #include<stdio.h>
int setvbuf(FILE *stream, char *buffer, int mode, size_t size)
-
参数
参数 | 含义 |
---|
stream | 该参数是指向 FILE 对象的指针,该 FILE 对象标识了一个打开的流。 | buf | 1、指定一个用户分配的缓冲区 2、如果该参数为NULL,那么函数会自动分配一个指定尺寸的缓冲区 | mode | 指定数据流的缓存模式:【_IOFBF:按块缓存】【_IOLBF:按行缓存】【_IONBF:不缓存】 | size | 指定缓冲区的尺寸(字节) |
-
返回值:如果成功,则该函数返回 0,否则返回非零值。 -
模式 描述
- _IOFBF 全缓冲:对于输出,数据在缓冲填满时被一次性写入。对于输入,缓冲会在请求输入且缓冲为空时被填充。、
- IOLBF 行缓冲:对于输出,数据在遇到换行符或者在缓冲填满时被写入,具体视情况而定。对于输入,缓冲会在请求输入且缓冲为空时被填充,直到遇到下一个换行符。
- _IONBF 无缓冲:不使用缓冲。每个 I/O 操作都被即时写入。buffer 和 size 参数被忽略。
-
代码示例 #include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
char buff[1024];
memset(buff, '\0', sizeof(buff));
setvbuf(stdout, buff, _IOFBF, 1024);
fprintf(stdout, "welcome\n");
fflush(stdout);
fprintf(stdout, "输入任意字符后才会显示该行字符\n");
getchar();
return 0;
}
|