C语言 知识点 + 笔记(2w7千字 持续更新...)

目录

前言

第 1 章 C语言的流程

计算机基础

(1) 十进制转 N 进制

例1 十进制转二进制

例2 十进制转八进制

例3 十进制十六进制

(2) N进制 转十进制

例1 二进制转十进制

例2 八进制转十进制

例3 十六进制转十进制

(3) 原码反码补码

第 2 章 数据类型、运算符和表达式

一、变量

二、标识符

三、运算符

四、表达式

第 3 章 顺序结构程序设计

(1) 格式化输出函数 printf()

一、printf()函数简介

 二.常见占位符

三.转义字符

(2) 格式化输入函数 scanf() 

第 4 章 选择结构程序设计

第 5 章 循环结构程序设计

第 6 章 数组

(1) 一维数组

1. C语言数组的一些特性

2.数组初始化赋值写法

3.数组的遍历

(2) 二维数组

(3) 字符数组和字符串

(4) 常用的字符串函数

1.strlen 

2. strcmp

3. strcpy

4. strcat

 5. strchr

 6. strstr

7. atoi

8. atof

9. strtok

10. strerror

第 7 章 函数

(1) 函数的作用

(2) 函数的定义

(3) 如何定义函数 

(4) 函数的组成

(5) 数学函数库

1. pow 指数函数

2. sqrt函数 平方根函数

3. cei 上取整函数

4. floor 下取整函数

5. abs 函数

6. log 以常数e为底对数函数

7. log10函数 以10为底对数函数

8. round 四舍五入

9. 三角函数

10. 反三角函数

11. exp 函数

第 8 章 指针

一、指针的基本概念

二、指针类型和指针运算

三、多级指针

四、万能指针

五、野指针

六、悬垂指针

七、空指针

八、指针和数组

九、指针数组和数组指针

十、指针和字符数组

十一、指针和动态内存 堆和栈

十二、动态内存函数 malloc calloc relloc 和free的使用

十三、内存泄露

十四、函数返回指针

十五、函数指针

十六、回调函数

第 9 章 结构体、共用体与枚举

结构体

一、结构体的作用

二、结构体类型的定义

三、 结构体变量的定义

四、结构体变量的初始化

五、结构体变量的引用

 六、结构体数组

七、结构体指针

八、结构体内存对齐规则

共用体

1. 共用体类型的定义

2. 共用体变量的说明

3. 共用体变量的引用

4. 共用体赋值深度解析代码示例

5. 共用体赋值操作图解示例

枚举类型

1. 枚举类型的定义及引用

2. 枚举类型的说明

3. 枚举类型的引用

第 10 章 文件

一、C语言的两种文件

二、文件的打开和关闭 fopen fclose

三、文件打开模式

四、文件的顺序读写

(1) fgetc fputc

(2) fgets fputs

(3) fscanf fprintf (格式化输入 / 输出)

(4) fread fwrite (二进制输入 / 输出)

(5)sscanf sprintf 处理字符串

五、文件的定位 (随机读写)

六、文件的错误检测 ferror

ferror 用于检测文件读写出错

七、文本文件和二进制文件

文本文件(Text File)

二进制文件(Binary File)

八、文件结束判定 feof函数

九、文件缓冲区


前言

本篇以笔记为主的C语言详解,全篇一共十章内容,2万6千多字,会持续更新基础内容,争取做到更详细。多一句没有,少一句不行! 

形而上学者谓之道,形而下学者谓之器


第 1 章 C语言的流程

(1) C程序经历的六个阶段

  1. 编辑(Edit)
  2. 预处理(Preprocess)
  3. 编译(Compile)
  4. 汇编(Assemble)
  5. 链接(Link)
  6. 执行(Execute)

(2) C语言编写代码到运行

都是先编译,后链接,最后运行。(.c ---> .obj --->.exe)这个过程中注意.c和.obj文件时无法运行的,只有.exe文件才可以运行。


(3) C程序流程图


计算机基础

计算机的数据在电脑中保存是以 二进制的形式. 数据存放的位置就是 他的地址.

4.bit  是指为0 或者1 byte 是指字节, 一个字节 = 八个位(1 byte = 8 bit).


(1) 十进制转 N 进制

十进制转 二进制、八进制、十六进制

重点

万能公式 十进制 转 N进制

 x 一直 除 n(n表示n进制),取余数倒排

例1 十进制转二进制

x = 25(十进制) 转二进制

25 / 2 = 12 ... 1

12 / 2 = 6 ... 0

6 / 2 = 3 ... 0

3 / 2 = 1 ... 1

1 / 2 = 0 ... 1

得到 二进制 11001,对应十进制 25


例2 十进制转八进制

x = 25(十进制) 转八进制

25 / 8 = 3 ... 1

3 / 8 =0 ... 3

得到 八进制 31,对应十进制的 25


例3 十进制十六进制

x = 25(十进制) 转十六进制

25 / 16 =  1 ... 9

1 / 16 =0 ... 1

得到 十六进制 19,对应十进制的 25

特别注意

十六进制中 没有 10 ~ 15的阿拉伯数字,

用的是 a ,b ,c ,d ,e ,f 字母代替 数字10 ,11 ,12 ,13 ,14 ,15


(2) N进制 转十进制

重点

万能公式 N进制转十进制 

右边第一位开始,第一位的数字,从0次幂开始 ~ i次幂结束,依次递增1

n进制 转成十进制 = x * n^ i 次幂

例1 二进制转十进制

(二进制)11001

= 1 * 2^4 + 1 * 2^3 + 1 * 2^0 

= 25


例2 八进制转十进制

(八进制)31

= 3 * 8^1 + 1 * 8^0

= 25


例3 十六进制转十进制

(十六进制)19

= 1 * 16^1 + 9 * 16^0

= 25


(3) 原码反码补码

在计算机中,所有的数据都是用机器码存储的,也就是有0 和 1 组成

二进制最高是符号位

0 表示正数

1 表示负数

正数的原码,反码,补码都一样

负数的反码 = 它的原码符号位不变,其他位置取反

负数的补码 = 它的反码 + 1

负数的反码 = 它的补码 - 1

这里我们 用 16 位的short类型来举例,-1512在二进制数据中,补码怎么计算得到

0的反码 ,补码都是0

计算机是以 补码的方式来运算 的

当人类看运算结果的时候,要看他的原码


第 2 章 数据类型、运算符和表达式

一、变量

(1) 变量的概念

内存中有个存储区域,这个地方的数据可以在同一类型范围内不断变化通过变量名,可以访问这块内存区域,获取里面的值;
变量名的构成:数据类型 变量名 值
C语言中变量声明格式: 数据类型 变量名 = 值

(2) 变量的注意

全局变量
        定义在函数外部的叫全局变量,默认初始化为0

静态变量 static

        带有static开头的关键字叫静态变量,默认初始化为0

局部变量

        声明局部变量以后,要初始化赋值!定义变量时,这个变量使用的内存不一定被清空,它可能是垃圾值,运行程序会异常退出

(3) 变量的作用域

局部变量

        在函数中定义的,有效范围在 定义开始到{ }结束
全局变量

        在函数外定义,有效范围从定义位置开始,到程序结束!


二、标识符

标识符的概念

C语言中,凡是可以自己命名的地方,都叫做标识符 例如:函数名,变量名,数组名,结构体名

C语言标识符的命名规范

1.只由英文大小写字母、数字、或 _(下划线) 组成
2.第一个字符只能是英文字母或下划线,不能数字开头!!!
3.大小写英文字母代表不同的字符
4.不能是C语言的关键字

C语言 32个关键字

单击表格关键字跳转详细解释

autoshortintlong
floatdoublecharstruct
unionenumtypedefconst
unsignedsignedexternregister
staticvolatilevoidif
elseswitchcasedefault
fordowhilecontinue
breakgotosizeofreturn

数据类型的分类

三、运算符

C语言运算符优先级

优先级

运算符

名称或含义

使用形式

结合方向

说明

1

[]

数组下标

数组名[常量表达式]

左到右

--

()

圆括号

(表达式)/函数名(形参表)

--

.

成员选择(对象)

对象.成员名

--

->

结构体选择(指针)

对象指针->成员名

--

2

-

负号运算符

-表达式

右到左

单目运算符

~

按位取反运算符

~表达式

++

自增运算符

++变量名/变量名++

--

自减运算符

--变量名/变量名--

*

取值运算符

*指针变量

&

取地址运算符

&变量名

!

逻辑非运算符

!表达式

(类型)

强制类型转换

(数据类型)表达式

--

sizeof

长度运算符

sizeof(表达式)

--

3

/

表达式/表达式

左到右

双目运算符

*

表达式*表达式

%

余数(取模)

整型表达式%整型表达式

4

+

表达式+表达式

左到右

双目运算符

-

表达式-表达式

5

<< 

左移

变量<<表达式

左到右

双目运算符

>> 

右移

变量>>表达式

6

大于

表达式>表达式

左到右

双目运算符

>=

大于等于

表达式>=表达式

小于

表达式<表达式

<=

小于等于

表达式<=表达式

7

==

等于

表达式==表达式

左到右

双目运算符

!=

不等于

表达式!= 表达式

8

&

按位与

表达式&表达式

左到右

双目运算符

9

^

按位异或

表达式^表达式

左到右

双目运算符

10

|

按位或

表达式|表达式

左到右

双目运算符

11

&&

逻辑与

表达式&&表达式

左到右

双目运算符

12

||

逻辑或

表达式||表达式

左到右

双目运算符

13

?:

条件运算符

表达式1?

表达式2: 表达式3

右到左

三目运算符

14

=

赋值运算符

变量=表达式

右到左

--

/=

除后赋值

变量/=表达式

--

*=

乘后赋值

变量*=表达式

--

%=

取模后赋值

变量%=表达式

--

+=

加后赋值

变量+=表达式

--

-=

减后赋值

变量-=表达式

--

<<=

左移后赋值

变量<<=表达式

--

>>=

右移后赋值

变量>>=表达式

--

&=

按位与后赋值

变量&=表达式

--

^=

按位异或后赋值

变量^=表达式

--

|=

按位或后赋值

变量|=表达式

--

15

逗号运算符

表达式,表达式,…

左到右

--

​简记版:

括号 > 单目运算符 > 算术运算符 > 移位  > 关系> 位运算符 > 逻辑 > 三目 > 赋值 > 逗号


四、表达式

表达式的定义

表达式是一种有值的语法结构,它由运算符和常量、变量、函数调用返回值等结合而成,每个表达式一定有一个值

例 1+1 就是一个表达式,它的值为 1


第 3 章 顺序结构程序设计

什么是顺序结构

顺序结构是按照代码的书写顺序从前到后执行的结构。是C语言最简单、最基本的结构。

顺序结构的特点

1. 自上而下

2. 没有分支

3. 依次执行

//插图... 待做

(1) 格式化输出函数 printf()

一、printf()函数简介

作用:将格式化后的字符串输出(打印东西)

printf("Good job!");

函数原型

int printf ( const char * format, ... );

返回值:输出的字符总数

int a = printf("%d",123);
printf("\na = %d",a);


 二.常见占位符

占位符的使用 

%c

字符类型
%d十进制的int类型(或%i)
%ld十进制的long类型
%hd短整型short int
%f单精度浮点类型(float)
%lf双精度浮点类型(double)
%u十进制的无符号的整数
%p指针(地址)
%x十六进制整型(int、long、short)输出
 %o八进制整型(int、long、short)输出
%s字符串
%e科学计数法输出(以指数形式(e表示指数部分)输出实数)
%%输出 %


 格式修饰符

英文字母 l修饰格式字符d、u、o、x时,用于输出long型数据
英文字母 L修饰格式字符f、e、g时,用于输出long double型数据
英文字母 h修饰格式字符d、o、x时,用于输出short型数据

输出格式说明

(1)%(正整数)d 限定宽度(右对齐)

printf("%10d\n",123);

(2)%(负整数)d 限定宽度(左对齐)

printf("%-10d\n",123);

(3)%+d 显示正负号

printf("%+d\n",123);
printf("%+d\n",-567);

 

(4)%.数字f 限定小数位数

printf("num = %.2f\n",3.1415926);

 

(5)%e 科学计数法(e是浮点类型)

printf("%le\n",123450.0);


三.转义字符

 \?在书写连续多个问号时使用,(在某些编译器下 ,会将 “ ??) ” 解析成 三字母词 “ ] ” )
\' 用于表示字符常量 '
 \"用于表示一个字符产内部的双引号
\\用于表示一个反斜杠,防止它被解释为一个转义序列符
\a警告字符,蜂鸣
\b退格符
\f进纸符
\n换行
\r回车
\t水平制表符
\v垂直制表符
\dddddd表示1~3个八进制的数字。如:\031 = 十进制 25
\xdddd表示2个十六进制数字。如:\x20 = 十进制 32


(2) 格式化输入函数 scanf() 

scanf(“格式控制符”, 输入参数地址);

int scanf(“格式控制符”, 输入参数地址);

scanf 会返回输入参数个数

输入参数和格式控制符要一一对应

参数部分一定得是地址,通常都加&符。但是在输入字符串情况下不加,因为字符数组,或者字符指针名就代表了字符串存储的首地址

int a, b, c;
scanf("%d%d%d",&a,&b,&c);
// 打印 a b c 的值
printf("%d %d %d",a,b,c);

注意

格式控制符中的非格式控制符,格式控制符中的内容必须要和输入的数据一一对应,所以非格式控制符的内容也要进行输入,否则数据就会输入失败

int a;
scanf("abc%d",&a);

第 4 章 选择结构程序设计

太简单了你肯定会...好吧暂时懒得写这一块,后面会认真更新的,兄弟们

第 5 章 循环结构程序设计

太简单了你肯定会...好吧暂时懒得写这一块,后面会认真更新的,兄弟们


第 6 章 数组

(1) 一维数组

1. C语言数组的一些特性

(1) 数组的地址是首元素的地址值,而输出数组的地址可以直接写数组名

(2) 数组名通常指向其第一个元素,但&数组名指向整个数组。两者在数值上可能相同,但类型不同。对它们进行加法运算时,移动的位置也不同:前者移动到下一个元素,后者跳过整个数组

数组的定义

在程序中是一块连续的,大小固定并且里面的数据类型一致的内存空间,当某一类型数据特别多的时候,我们需要大批量操作时,就用到了数组。

定义方式 数组类型 数组名[ 数组长度 ]

//数组定义方式
int arr[10];

数组的初始化

2.数组初始化赋值写法

常用写法

//1.不给数组大小的初始化,写多少开辟多少
int arr[] = {1,2,3};
//2.给确定大小的初始化,使用越界数据会返回垃圾值
int arr[3] = {1,2,3,4};

特别注意

使用多维数组时,低维必须要给确定大小

//3.特别注意,当使用多维数组时,低维必须要给确定大小
int arr[][3] = {{1,2,3},{4,5,6}};
int arr2[][2][2] = {{{1,2},{1,2}},{3,4},{5,6}},{{7,8},{9,10}}};

反人类的写法

//给指定位置的元素赋值,阅读起来非常难受的写法
int b[5] = {[0] = 1,[2] = 22,[4] = 4444};

3.数组的遍历

通常写法

int a[4] = {1,3,5,7};
//1.正常写法
for (int i = 0; i < 4; ++i) {
    printf("%d ",a[i]);
}

变态写法

//2.变态写法
for (int i = 0; i < 4; ++i) {
    printf("%d ",i[a]);
}

指针取值

for (int i = 0; i < 4; ++i) {
    printf("%d ",*(a+i));
}

小总结:为什么数组可以使用指针的取值运算符,因为我们的数组名实际上就是地址,当你直接输出数组名的时候,输出的数组的首地址;指针就是地址,地址就是指针通常叙述时会把 指针变量 简称为 指针,实际上两者含义不同


(2) 二维数组

二维数组本质上与一维数组无异,在定义时,左边的方括号是高维的大小,右边的是低维的大小

// 这是一个两行三列的二维数组
int arr[2][3];

注:通常们初始化时,一维的大小可以省略,而高维的不可以省略 !!!


(3) 字符数组和字符串

在C语言中,是没有string类型的,我们使用字符数组来存储字符串

字符数组就是一个char类型的数组,当存储类型是一个字符串时,字符数组结尾会有一个'\0'

下列代码是字符串的三种初始化方式

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

int main( )
{
    //字符数组的 三种初始化
    // 1.字符赋值法
    char arr[5] = {'a', 'b', 'c','\0'};
    // 2.字符串赋值
    char arr2[] = "abc";//括号 {"abc"}; 可加可不加
    // 3.函数赋值
    char arr3[5];
    strcpy(arr3, "abc");

    return 0;
}

重点

字符串结尾一定有隐含的'\0',它用于标记字符串结束的位置,所以当一个字符数组长度为N时,它所能容纳 N - 1个字符,反之一个长度 为 M个字符的字符串所占 M + 1 个字节


(4) 常用的字符串函数

1.strlen 

返回字符串字符个数 (不包含隐含的'\0')

// strlen 返回字符串的长度 不包含 '\0'
int length = strlen("ABC");

2. strcmp

比较两个字符串,相同返回0,a字符串 > b字符串 返回1,a字符串 < b 字符串 返回 -1

printf("%d\n", strcmp("abc","abd"));

3. strcpy

char * strcpy(char * dest,const char * src);

把src的字符串拷贝到dst

(src 和 dst不能重叠 即:两个字符串的地址的差小于字符串的大小了,两个字符串地址太近)

char a[30] = "ABC", b[30] = "I Love programming";
// 因为C语言的字符串不能进行赋值操作,这里用拷贝
strcpy(a,b);

4. strcat

将参数dest字符串和参数src字符串拼接起来 ,dst 目标字符串的空间必须足够大。

char ch1[20] = "Hello";
char ch2[20] = "World";
strcat(ch1, ch2);
// ch1 的值 "Hello World"
printf("%s\n", ch1);

 5. strchr

char * strchr(const char * str,int val);

在参数str所指向的字符串中搜索第一次出现字符c(一个无符号字符)的位置 没找到则返回空指针

char ch1[10] = "Hello";
char * p = strchr(ch1,'l');
printf("%c\n",*p);
*p = 'A';//替换成 'A'
// ch1 "HeAlo"
printf("%s\n",ch1);

 6. strstr

strstr(const char * str,const char * SubStr)

找到子串(str)在(SubStr)中第一次出现的位置,并返回该位置的指针,如果找不到,返回空指针(NULL)。

char ch1[20] = "Hello World";
char ch2[20] = "World";
char * pStr = strstr(ch1,ch2);
printf("%s\n",pStr);

7. atoi

int atoi(const char *_Str)

把传入的字符串转成一个整型返回

int x = atoi("-666");
// x = -666
printf("%d\n",x);

8. atof

double atof(const char *_String);

把传入的字符串转成一个浮点型返回

double pi = atof("3.1415926");
printf("%.7lf\n",pi);

9. strtok

char *strtok(char *str, const char *delim);

strtok(str,c) 对字符串按照子字符串c(可以是单个字符)进行分割,返回分割后的子字符串。

注意:被delim 匹配的字符串,会置换成 '\0'

//原字符串
char str[30] = "I can speak C Language";
char delim[2] = " ";
char * tt; // 用于保存分割的字符串

/* 获取第一个标记 */
tt = strtok(str, delim);
/* 循环获取剩余标记 */
while(tt != NULL)
{
    printf("%s\n",tt);
    // 继续分割
    tt = strtok(NULL,delim);
}

10. strerror

字符串报错,常用用于文件打开失败,输出错误信息

FILE * pfile;
pfile = fopen("test.tt","r");
if(pfile == NULL)
{
    printf("打开文件失败 代码:%s",strerror(errno));
}

第 7 章 函数

C语言基本单位是函数

(1) 函数的作用

避免了重复性操作

有利于程序的模块化

(2) 函数的定义

逻辑上

能够完成特定功能的独立的代码块

物理上

能够接收数据[可以不接收]
能够对接收的数据进行处理[也可以不处理]
能够将数据处理的结果返回[也可以不返回任何值]
总结:函数是一个解决大量类似问题而设计的工具

(3) 如何定义函数 

函数的返回值 函数名 (形参列表...)
{
         函数的执行体
}

int function(int a,int b)
{
    return a + b;
}

(4) 函数的组成

一个函数包括函数头语句体两部分
函数头由三部分组成
        (1) 函数返回值类型
        (2) 函数名
        (3) 形参列表
return 表达式的含义
        (1) 终止被调函数,向主函数返回表达式的值
        (2) 如果表达式为空,则只终止函数,不向被调函数返回任何值
        (3) break 是用来终止循环和switch的,return 是用来终止函数的

函数的分类

有参函数无参函数
有返回值 无返回值
库函数 和 用户自定义函数
值传递函数地址传递函数 

普通函数 和 主函数,main()函数不include任何头文件,是C语言的一部分
一个程序必须有且只能有一个主函数
主函数可以调用普通函数,普通函数不能调用主函数
普通函数可以相互调用
主函数是程序的入口,也是程序的出口


函数的调用和函数定义的顺序

如果函数调用写在了函数定义的前面,则必须加函数前置声明
函数前置声明的作用
1.告诉编译器xxx()代表的是一个函数
2.告诉编译器xxx()所代表的函数的形参和返回值的具体情况
3.函数声明是一个语句,末尾必须加分号
4.对库函数的声明是通过#include<库函数所在的文件的名字.h>来实现的

形参和实参
形参是函数里面一个参数

实参是调用函数传入的实数个数相同 位置要一一对应 数据类型必须相互兼容


(5) 数学函数库 <math.h>

1. pow 指数函数

double pow(double a, double b);

返回值:a^b;

// 返回 x 的 a次方
int x = 2, a = 8;
int res = (int)pow(x,a);
printf("%d",res);

2. sqrt函数 平方根函数

double sqrt(double x);

返回根号x

// 返回 根号x
int x = 9;
int res = (int)sqrt(x);
printf("%d",res);

3. cei 上取整函数

double ceil(double x);

ceil(4.1) = 5

double x = 3.12;
int res = ceil(x);
// 输出 4
printf("%d",res);

4. floor 下取整函数

double floor(double x);

floor(4.1) = 4

//  返回 x 的向下取整数
double x = 3.9999;
int res = floor(x);
// 输出 3
printf("%d",res);

5. abs 函数

abs(int x) 用于整型的绝对值

fabsf 用于单精度浮点数 float 绝对值

fabs 用于双精度浮点数 double 绝对值

//  返回 x 的绝对值
int x = -32;
int res = abs(x);
// 输出 32
printf("%d",res);

6. log 以常数e为底对数函数

double log(double x);

log(9) = 2.197225

// 返回 x 的 以e为底的log(x)
double x = 3;
double res = log(x);
printf("%.2f",res);

7. log10函数 以10为底对数函数

double log10(double x);

log10(1000) = 3

double x = 1000;
double res = log10(x);
// 输出 3
printf("%.0f",res);

8. round 四舍五入

double round(double x)

round(2.3)=2

// 返回 x 的四舍五入
double x = 3.52;
double y = 3.42;
double  a = round(x);
double  b = round(y);
// 输出 a = 4  b = 3
printf("a = %.2f b = %.2f",a,b);

9. 三角函数

读者道歉信:很抱歉,等我学会了三角函数再更新这块。。。。

x的正弦值

double sin (double x);

x的余弦值

double cos (double x);

x的正切值:

double tan (double x);

10. 反三角函数

x的反正弦函值

double asin (double x);

x的反余弦值:

double acos (double x);

x的反正切值:

double atan (double x);

11. exp 函数

exp函数主要用于计算自然指数值,即e的x次方。

其中e是一个常数,约等于 2.71828.

double exp(double x)

// 返回e的x次方
int x = 3;
double res = exp(x);
printf("%.2f",res);

第 8 章 指针

一、指针的基本概念

指针的特点

        (1) 表示一些复杂的数据结构
        (2) 快速的传递数据
        (3) 使函数返回一个以上的值
        (4) 能直接访问硬件
        (5) 能够方便处理字符串
        (6) 是理解面向对象语言中引用的基础

        指针是C语言的灵魂

指针的定义
        地址
                内存单元的编号
                从零开始的非负整数
                范围32位支持最多4G(64位计算机支持128G,32个4G)

        注意(!!!):
                一个指针变量,无论它指向的变量占几个字节,
                在32位的计算机上,占4个字节;
                在64位的计算机上,占8个字节。
                一个指针占几个字节,等于是一个地址的内存单元编号有多长


        指针变量

                指针就是地址,地址就是指针
                地址就是内存单元的编号
                指针变量是存放地址的变量
                指针和指针变量是两个不同的概念
                注意:通常叙述时会把 指针变量 简称为 指针,实际上两者含义不同
                指针的本质就是一个操作受限的非负整数

指针的分类


        


二、指针类型和指针运算

指针变量的运算

        (1) 两个指针变量之间 不能相加    不能相乘    也不能相除(同一类型的指针可以相互赋值)
         (2) 若两个指针变量指向的是同一块连续空间,且是同类型,则这两个指针变量才可以相减

int a[5] = {1,2,3,4,5};
int *p = &a[0];
int *q = &a[4];
printf("p 和 q所指向的单元相隔 %d 个 单元\n",q-p);

结果: p 和 q所指向的单元相隔 4 个 单元

         (3) 指针 + n (表示往后移动  (数据类型字节大小) * n)

int i = 0;
int *p = &i;
printf("%d\n",p);
printf("%d\n",p+1);

结果: p 和 p + 1的差值一定是 4

代码案例

int a[2] = {3,9};
int * p = &a[0];
printf("*p = %d",*(p+1));

结果: *p = 9


三、多级指针

多级指针的概念

        一个指针变量指向的是另一个指针变量,我们就称它为二级指针,如此推理可以无限套娃

int i = 3;
//p 是指向变量 i 地址的指针
int * p = &i;
//q 是指向 指针p 地址的指针
int ** q = &p;
**q =666;
printf("i = %d",i);

结果:通过操作二级指针q得到, i = 666

四、万能指针

万能指针(void 类型指针)

        万能类型指针可以接收任意类型变量的内存地址 在通过万能指针修改变量时, 需要把万能指针转换为变量对应的指针的类型

int a = 10;
//1.定义万能类型指针 指向a变量地址
void * p = &a;
//2.把万能类型指针 强制转换成对应的数据类型    
*(int*)p = 666;
printf("a = %d",a);

结果:通过操作万能指针p得到, a = 666


五、野指针

野指针

        某些编程语言允许未初始化的指针的存在,而这类指针即为野指针
        例如:int * p = 100;
        指针变量指向了一个未知的空间,操作系统将0-255作为系统占用不允许访问操作, 操作野指针对应的空间可能报错

int * p;
*p = 6;//这就是野指针,指针未初始化指向有效空间,就使用了

六、悬垂指针

悬垂指针的概念       

        该指针指向曾经存在的对象,但该对象已经不再存在了,此类指针称为悬垂指针
     

常见的悬垂指针错误

        栈分配的局部变量的地址时,一旦调用的函数返回,分配给这些变量的空间将被回收,此时它们拥有的是"垃圾值"

#include <stdio.h>
int* f()
{
    //该函数结束时,分配的栈空间会被回收
    int x = 666;
    return &x;
}
int main()
{
    int * p = f();
    //此时程序出错
    printf("%d",*p);
    return 0;
}

七、空指针

空指针的概念

一个指针不指向任何数据,我们就称之为空指针,空指针用NULL表示

int * p = NULL;

八、指针和数组

指针和数组的关系

数组名本身就是个地址常量, 指针指向时不需要取地址符,直接指向数组名即可

int a[5] = {1,3,5,7,9};
int * p = a;//直接引用即可不需要加取地址符 '&'

 数组名代表数组的首地址,取值之间的语法可以相互套用

int a[5] = {1,3,5,7,9};
int * p = a;
int i = p[1];
int j = *(a+1);
printf("i = %d,j = %d\n",i);

结果: i = 3,j = 3


指针和数组的区别

(1) 赋值方式不同

 同类型指针变量可以相互赋值,数组不行,只能一个一个元素的赋值或拷贝

(2) 存储方式不同        

数组:数组在内存中是连续存放的,开辟一块连续的内存空间。数组是根据数组的下进行访问的, 数组的存储空间,不是在静态区就是在栈上。

指针:指针很灵活,它可以指向任意类型的数据。 指针p存储的是一个内存地址,这个地址指向的是某种类型变量的存储空间。 如果要访问这个变量,需要使用指针运算符(*)来解引用指针,将指针所指向的地址转换为所指向的变量的值。 指针的值也可以改变,通过指针运算符(&)获取变量的地址,然后将其赋给指针变量。

(3) 占用空间大小

数组的大小取决于数组元素的类型和元素个数

数组所占存储空间的内存:sizeof(数组名)

指针无论是什么类型,在32位平台是占4 byte ,在64位平台是占8 byte

(4) 可变性

数组的大小在定义时就已经确定,无法改变,而指针可以随时指向不同的变量,从而实现动态变化。

九、指针数组和数组指针

指针数组

//1.指针数组 是一个数组
int a = 3, b =5;
int * p[2] = {&a,&b};
*p[0] = 15;    //p[0]存储的a的地址

指针数组首先是一个数组,只不过数组的每个成员是一个指针变量。

例:int * p1[10]; // 指针数组,[ ]的优先级大于*,p是一个数组,数组的值是一个指针

数组指针

//2.数组指针 是一个指针
//定义一个二维数组
int arr[3][3] = 
{
    {2,13,4},
    {5,6,7},
    {8,9,10}
};
//定义数组指针 指向二维数组(声明时,括号一定要加!!!)
int (*p2)[3] = arr;

 数组指针首先是一个指针,这个指针指向一个数组(声明数组指针时,括号一定要加!!!)。


十、指针和字符数组

(1) 字符数组

定义方式

//省略{},省略长度值(实际上该数组长度为4 字符串默认'\0'结尾)
char arr[] = "abc";
char s[4] = {'a','b','c','\0'};

输入方式

char s[4];
scanf("%s",s);

字符数组,可以直接用scanf 输入,且不需要加&符,

因为字符数组名,就代表了整个字符串的首地址

(2) 字符指针

定义方式

char  *s = "Hello";

 输入方式

错误写法 X X X 

char *s;//这样写是错误的 !!!!!!!!!
scanf("%s",s);//这样写是错误的 !!!!!!!!!

注意:

这里的字符指针未指向有效数据空间,用scanf()输入程序必然出错!!!

正确写法 √ √ √

char a[100];
char *s = a;//字符指针 s 指向了字符数组 a
scanf("%s",s);

这里字符指针s指向了组a,分配了有效空间,这样才是正确写法,程序正常运行


十一、指针和动态内存 堆和栈

(1)栈(satck): 由系统自动分配。 例如,声明在函数中一个局部变量int b;系统自动在栈中为b开辟空间。

(2)堆(heap): 需程序员自己申请(调用malloc,realloc,calloc),并指明大小, 并由程序员进行释放。容易产生memory leak(内存泄漏).

分配方式

(1)堆都是动态分配的,没有静态分配的堆。

(2)栈有两种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由allocal 函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现


十二、动态内存函数 malloc calloc relloc 和free的使用

(1) malloc 函数

void * malloc(开辟空间大小) 不会默认初始化,比如开辟空间后,进行调用,会有一些乱码

int n = 5;
int *m = (int*) malloc(n * sizeof(int));

(2) calloc 函数

void * calloc(申请空间的个数,单个类型的大小),默认初始化为0

int n = 5;
int *c = (int*) calloc(n,sizeof(int));

 (3) realloc 函数

void * realloc(p需要调整的指针,新的大小),对于内存开辟空间大小的更改。

(1)改小:

对申请的内存空间改小,可以在原申请处减小可访问字节数,这样就做到了对使用空间的减小。

(2)改大:

1.malloc或者calloc申请得到的空间后面有足够的空供我们使用,直接开辟


2.假设realloc可连续操作的剩余空间够扩大的所需空间,会返回本来的地址 p


2,若所需的空间不够,会将原本申请的空间释放掉(还给操作系统),找一块新地盘,并把上面空间的数据复制到新的空间中,还会把p指针指向的地址改为新申请的地址

int * r = (int*) realloc(c,sizeof(int)*n*2);

注意:新开辟的空间,会有垃圾值的概率,不会进行初始化


十三、内存泄露

内存泄漏的概念

内存泄漏也称作"存储渗漏",用动态存储分配函数动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元。直到程序结束。(内存空间用完了,没有释放,上完公厕,人不走占着坑)

内存泄漏的危害

内存泄漏会因为减少可用内存的数量,导致降低计算机的性能,甚至程序崩溃。

如何防止内存泄漏

谨慎申请内存,使用后即使对内存释放,free(被释放内存指针)

int * a =(int*)malloc(sizeof(int)*10000);
//堆内存需要手动释放,否则可能会引起内存泄漏
free(a);

十四、函数返回指针

定义写法 类型名 *函数名(参数表列);

有时候我们需要指针作为返回值时,需要申请动态内存,栈内存会在函数结束时销毁

#include <stdio.h>
#include <malloc.h>

int * getPointer()
{
    //这里需要用堆内存,栈内存在函数执行后,销毁
    int * c = (int*) malloc(sizeof(int));
    *c =999;
    //static int c = 999;//静态区也不会销毁
    return c;
}
int main() {
    int * p = getPointer();
    printf("%d",*p);
    free(p);//注意释放内存
    return 0;
}

十五、函数指针

函数指针的概念

当一个指针,指向的对象是函数时,我们称它为函数指针

函数指针的作用

当我们需要,把一个函数当做参数传递时,我们可以利用指针的特性,于是就有了函数指针

#include <stdio.h>

//返回a + b的一个函数
int add(int a,int b)
{
    return a + b;
}

int main() {
    //(*p)括号一定要加!!!
    int (*p)(int,int);
    //add不需要加'&'符,因为函数名本身就代表地址
    p = add;//函数指针p,指向 函数add
    int res = p(3,4);
    printf("res = %d",res);
    return 0;
}

 注意:

如果函数指针指向的函数参数列表为空,例如void test(),这时我们定义函数指针时依然也要加上括号。赋值给函数指针时,函数只给名字! ! !否则编译器无法识别是调用还是赋值

void test()
{
    printf("test~");
}
int main() {
    int (*p)();
    p = test;
    //用函数指针执行函数
    p();    
    return 0;
}

十六、回调函数

回调函数的概念

回调函数是一种编程概念,指的是一个函数作为参数传递给另一个函数

回调函数的作用

(1) 代码逻辑分离

回调函数允许将代码逻辑分离出来,使得代码更加模块化和可维护。

(2) 异步编程

回调函数可以在某个函数执行完后被调用,通过这种方式可以将结果传递到另一个函数中进行处理,起到异步编程的作用。

(3) 代码复用

由于回调函数可以被多个地方调用,它们可以实现代码的复用。

 (4) 事件处理

回调函数可以在发生某种事件时由系统或其他函数自动调用,用于对该事件或条件进行响应

回调函数怎么写

#include <stdio.h>

//加法函数
int add(int a,int b)
{
    return a + b;
}
//减法函数
int sub(int a,int b)
{
    return a - b;
}

//计算器
int cal(int a,int b,int (*f)(int, int))
{
    return f(a,b);
}
int main()
{
    int a = 7, b = 4;
    //把函数 sub,作为参数传入,cal函数只负责返回最终结果
    int res = cal(a,b,sub);
    printf("res = %d",res);
    return 0;
}

代码解析:这里验证前面的理论,我们写了三个函数,add(加法函数),sub(减法函数),cal(计算器),我们的cal函数只负责接收两个整型然后返回计算的值,而怎么计算只需要根据我们传入的函数来决定,增加了代码复用率,更加模块化和可维护。


第 9 章 结构体、共用体与枚举

结构体

一、结构体的作用

封装数据:结构体可以将多个不同类型的数据封装成一个单独的数据类型。这使得我们可以创建一个包含多种信息的复合数据类型,而不仅仅是单个的数据类型(如整数、浮点数、字符等)。例如,我们可以创建一个结构体来表示一个学生的信息,包括姓名、年龄、学号等。

提高代码的可读性和可维护性:通过使用结构体,我们可以将相关的数据组织在一起,这有助于代码的阅读者更好地理解数据的含义和用途。此外,如果以后需要修改或扩展数据的结构,我们只需要修改结构体的定义,而不需要在代码中的多个地方进行修改。

实现数据的抽象和封装:结构体允许我们隐藏数据的实现细节,只暴露必要的接口给外部使用。这是面向对象编程的一个重要原则,可以提高代码的模块化和可重用性。

方便数据传递:当我们需要在函数之间传递多个相关的数据时,可以使用结构体作为参数。这样,我们只需要传递一个结构体变量,而不是多个单独的变量,这可以简化函数的调用和参数的传递。

支持更复杂的数据结构:结构体是构建更复杂数据结构(如链表、树、图等)的基础。通过使用结构体,我们可以定义节点类型,并在这些节点之间建立链接关系,从而构建出各种复杂的数据结构。


二、结构体类型的定义

struct 结构体名

{

         成员列表(可以是基本的数据类型,指针,数组或其他结构类型)

};

注意: 结构体大括号的结尾有分号,和语句一样;而函数和语句体的大括号没有分号

三、 结构体变量的定义

方式一

//方式1 先定义结构体类型
struct Student
{
    int age;
    float score;
    char sex;
};

void main()
{
    //再定义结构体变量    
    struct Student stu;
}

先定义结构体的类型,使用时再定义结构体变量

方式二

//方式2 
struct Student
{
    int age;
    float score;
    char sex;
} stu;//定义的同时定义变量名
void main()
{
    //定义后直接拿来使用
    stu.age = 18;
    stu.score = 66.6f;
    stu.sex = 'm';
}

定义的同时定义变量名,定义后直接拿来使用

方式三

struct
{
    int age;
    float score;
    char sex;
}stu;//起变量名,不给类型

这种结构体只能单个使用,因为没有结构体类型,无法再次创建。某些设计模式下会使用到这种定义方式


四、结构体变量的初始化

方式一

struct Score
{
    float Chinese;
    float Math;
    float English;

}t = {99,98,95};//方式1 定义时直接赋值

void main()
{    
    //定义变量同时初始化
    struct Score scorce = {100,100,99};
}

定义结构体变量时同时初始化,代码中的两种方式等价

方式二

struct Score
{
    float Chinese;
    float Math;
    float English;
}

void main()
{
    //方式二 定义结构体之后逐个赋值
    struct Score scorce;
    scorce.Chinese = 98;
    scorce.Math = 100;
    scorce.English = 88;

}

先定义结构体,然后用成员运算符 ' . ' 逐个进行赋值

方式三

struct Score
{
    float Chinese;
    float Math;
    float English;
}
void main()
{    
     //定义之后任意赋值
     struct Score test2 = {
            .English = 97,
            .Math = 96
    };   
}
定义的同时,指定元素进行赋值,没有赋值的元素默认值为 0

五、结构体变量的引用

struct Book
{
    char title[20];//一个字符串表

    char author[20];//一个字符串表示的author作者

    float value;//价格表示
};//这里只是声明 结构体的定义



void main() {
    struct Book book;//结构体变量的定义
    strcpy(book.title,"《活着》");
    strcpy(book.author,"余华");
    book.value = 30;

    printf("书名:\t%s\n",book.title);
    printf("作者:\t%s\n",book.author);
    printf("价格:\t%.2f\n",book.value);
}

结构体变量的引用,只需要在定义之后。使用 结构体变量名.成员 就可以获取到对应的值


 六、结构体数组

struct Student
{
    char name[20];
    char sex;
    int number;
}
void main() 
{
    //定义结构体数组,并且初始化
    struct Student stu[5] =
    {
            {"ZhangSan",'M',12345},
            {"Jenny",'M',12306},
            {"Mike",'W',12546},
            {"Jerry Smith",'M',14679},
            {"YuLongJiao",'W',17857}
    };  
}

结构体数组就是,由相同类型的结构体变量组成数组就是 结构体数组

 常见错误

struct Student stu1;

stu1[3]={

        {"zhaozixuan",'M',12345},...

};

定义后,再这样赋值,是错误的写法!!!


七、结构体指针

什么是结构体指针

一个指向结构体变量的指针,就是结构体指针

结构指针和其它类型的指针本质上没有区别,但唯一不同点是,结构体指针的取值和成员取值方式不同,普通结构体变量使用的是'.',结构体指针变量使用的是 -> (结构体指针运算符),它可以等价于(*p).成员

结构体指针的定义

struct Student
{
    int age;
    char sex;
    char name[100];
};

int main()
{
    /*结构体指针的创建*/
    //1.定义结构体变量
    struct Student stu = {18,'m',"Tom"};
    //2.定义结构体指针变量
    struct Student * pStu = &stu;
    return 0;
}

定义方式和普通指针一样,只需要把类型换成结构体类型即可

结构体指针的调用

struct Student
{
    int age;
    char sex;
    char name[100];
};

int main()
{
    /*结构体指针的创建*/
    //1.定义结构体变量
    struct Student stu = {18,'m',"Tom"};
    //2.定义结构体指针变量
    struct Student * pStu = &stu;
    //3.结构体指针的调用 这里我们使用的是 ->                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                
    int age = pStu->age;// pStu->age 等价于(*pStu).age                                                                                                                                                                                                                                                                            
    printf("age = %d",age);
    return 0;
}

使用结构体指针变量来获取对应的值时,通常使用 p ->xxx,它等价于( *p).xxx

因为 . 的优先级比较高我们需要给指针 *p 取值时加上括号,这样容易产生一些错误,于是我们使用 -> 更容易区别和不易犯错


八、结构体内存对齐规则

1. 对齐数(Alignment)

每个数据类型在内存中都有一个对齐数,它通常是该类型大小(以字节为单位)的整数倍。对齐数用于确定该类型的数据在内存中的起始地址。例如,char类型的对齐数通常是1,int类型的对齐数可能是4(这取决于具体的平台和编译器)。

2. 结构体成员的对齐

第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小的整数倍开始。这可能会导致在成员之间出现“填充字节”,以确保每个成员都从其对齐数的倍数地址开始。

3. 代码示例

struct stu
{
    int num; // 4个字节
    double d;// 8个字节
    int score;// 4个字节
    char name[2];//2个字节
}s;
int main( )
{    
    //输出24
    printf("%d\n",sizeof(s));
}

运行结果 24

首先我们我们计算得到,结构体的实际占用是18个字节,因为当前结构体最大类型是double (占8个字节),结构体整体大小一定是8的整数倍,所以对齐操作后得到得到 24

4. 图解内存对齐的过程

5.  修改对齐 (内存不对齐)

#include<stdio.h>
struct stu
{
    int num; // 4个字节
    double d;// 8个字节
    int score;// 4个字节
    char name[2];//2个字节
}__attribute__((packed)) s;
int main( )
{
    //输出结果 为 18
    printf("%d\n",sizeof(s));
}

运行结果 18

在某些情况下,你可能想要控制结构体的对齐方式,例如减少填充字节以节省空间。你可以使用预处理器指令或编译器特定的属性来实现这一点。例如,CLion编译器提供了__attribute__((packed))属性,它可以用来指示编译器不要添加任何填充字节


共用体

1. 共用体类型的定义

//共用体
union Data{
    short a;
    int b;
    char c;
};

共用体类型的定义和结构体类似,关键字 + 共用体类型名

共用体(union)是一种特殊的数据类型, 它允许在同一个内存位置存储不同的数据类型。

2. 共用体变量的说明

共用体的特点

共用体的所有成员共享同一块内存空间, 因此同一时间只能存储其中一个成员的值。 共用体的定义和结构体类似,使用关键字union,后面跟着成员列表。共用体(union)是一种特殊的数据类型, 它允许在同一个内存位置存储不同的数据类型。 共用体的所有成员共享同一块内存空间, 因此同一时间只能存储其中一个成员的值。

什么时候用共用体

当你知道某个变量在程序的不同部分会有不同的类型时。

当你想节省空间,并且确定不会同时需要多个类型的值时。

在处理某些硬件相关的编程任务时,例如位字段操作。

注意

由于共用体的成员共享内存,在任何时候只有一个成员的值是有效的。因此,最后一次共用体的赋值会改变之前的赋值操作

3. 共用体变量的引用

//共用体
union Data{
    short a;
    int b;
    char c;
};
int main()
{
    union Data data;
    data.a = 11;
    data.b = 12;
    data.c = 13;
    return 0;
}

共用体的引用和结构体一样,定义共用体变量后,通过成员运算符选择对应的成员,进行赋值操作或取值

4. 共用体赋值深度解析代码示例

#include <stdio.h>

//共用体
union Data{
    short a;
    int b;
    char c;
};

int main() 
{
    union Data data;
    
    // 2147483647 是int最大值
    data2.a = 2147483647;
    // 输出 a = -1    b = 65535
    printf("a = %d\n",data.a);
    printf("b = %d\n",data.b);

    return 0;
}

为什么不仅结果和我们输入的不一样,而且a b两个值差那么多 ?

short,占 2个字节(16位) 因为计算机存的是补码且倒序

所以(2147483647) 0111 1111 1111 1111 1111 1111 1111 1111

16位的short只能得到32位int的一半,从低到高位

此时 a是short类型得到的补码是 1111 1111 1111 1111,高位是1(负数),所以结果为-1

int,占 4个字节(32位),同样b的1111 1111 1111 1111,为什么是65535? 因为,int长度更长没有赋值的地方就是补0,且是倒序得到的补码如下: 0000 0000 0000 0000 1111 1111 1111 1111,高位是0,是正数,所以结果是65535

5. 共用体赋值操作图解示例


枚举类型

1. 枚举类型的定义及引用

枚举类型的定义和结构体类似,关键字 + 枚举类型名

枚举类型,它允许你为整数值分配有意义的名字

在枚举类型的定义中, 成员是不允许存在 ' ; ' (分号) 的

enum Color 
{ 
    RED, GREEN, BLUE
};

在这个例子中,Color 是一个枚举类型,它有三个成员:REDGREEN 和 BLUE。默认情况下,RED 的值为0GREEN 的值为1BLUE 的值为2,以此类推。

enum Color 
{ 
    RED, GREEN = 66, BLUE
};

注意

这里我们改变了 GREENRED还是 0GREEN = 66,此时的 BLUE 就会变成 67 然后依次递增


2. 枚举类型的说明

枚举类型在本质上仍然是整数类型,因此可以将整数赋值给枚举类型的变量,反之亦然。

枚举类型的优点

增加代码可读性:使用有意义的名称代替魔法数字(magic numbers)更便于理解。

类型安全:枚举类型提供了一种方式来限制变量可以取的值,从而增加了类型安全性。

易于维护:如果需要更改枚举成员的值或添加新的成员,只需在枚举类型的定义中进行修改,而无需在代码的多个地方进行搜索和替换。

3. 枚举类型的引用

(1) 声明后赋值使用

enum Color
{
    RED, GREEN, BLUE
};
int main( )
{   
    //方式 1 声明后赋值使用
    enum Color myColor = RED;
    printf("RED = %d\n",myColor);
    return 0;
}

(2) 直接使用 

enum Color
{
    RED, GREEN, BLUE
};
int main( )
{   
    //方式 2 直接使用成员,无需声明
    printf("RED = %d\n",RED);
    printf("GREEN = %d\n",GREEN);
    printf("BLUE = %d\n",BLUE);
    return 0;
}

第 10 章 文件

一、C语言的两种文件

程序文件
        包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境后缀为.exe)。 

数据文件
        包括程序运行时所读写的数据。本篇所涉及的就是数据文件。

二、文件的打开和关闭 fopen fclose

fopen函数

作用 创建一个指向文件的指针

FILE * open(文件路径,打开模式)

打开成功返回对应的文件指针,失败返回NULL

FILE * f = NULL;
f = fopen("C:/Users/asus/desktop/abc.txt","w");
if(f == NULL)
{
    printf("文件打开失败!");
    //打印错误信息
    printf("%s\n",strerror(errno));
    return 1;
}
else printf("文件打开成功!");

fclose函数

作用 关闭文件,缓冲区内的数据写入文件中,并释放系统所提供的文件资源

int fclose(文件指针);

关闭成功会返回 0

如果该文件已经关闭或未指向任何文件返回 -1

//关闭文件
int a = fclose(f);
printf("第一次关闭 a = %d\n",a);
a = fclose(f);
printf("第二次关闭 a = %d\n",a);

 


三、文件打开模式

打开模式作用如果指定的文件不存在
r (只读)读入数据,打开一个已经存在的文本文件出错
w (只写)写出数据,打开一个文本文件,写入数据覆盖原内容新建一个文件
a (追加)向文本文件尾添加数据新建一个文件
r+ (读写)为了读和写,打开一个文本文件出错
w+ (读写)读写数据,打开一个文本文件,写入数据覆盖原内容新建一个文件
a+ (读写)打开一个文件,在文件尾进行读写新建一个文件
rb (只读)为了读入数据,打开一个二进制文件出错
wb (只写)写出数据,新建一个二进制文件,写入数据覆盖原内容新建一个文件
ab (追加)向二进制文件尾添加数据新建一个文件
rb+ (读写)读和写,打开一个二进制文件,写入数据覆盖原内容出错
wb+ (读写)读和写,新建一个二进制文件,写入数据覆盖原内容新建一个文件
ab+ (读写)打开一个二进制文件,在文件尾进行读和写新建一个文件


四、文件的顺序读写

(1) fgetc fputc

fgetc(文件指针)

从指定文件中读取一个字符,成功读入数据时会返回该字符的ASCII值,反之则会返回EOF(-1)值。

FILE * pf = fopen("a.txt","r");
//从文件读入一个字符
int c = fgetc(pf);
printf("%c\n",c);
fclose(pf);

fputc(字符,文件指针)

把传入的字符,通过文件指针存入到对应文件中去。

该函数成功运行返回传入字符的ASCII码值,否则返回EOF(-1)值。

FILE * pf = fopen("a.txt","r+");
//把 s 写入 pf指定的文件
int num = fputc('s',pf);
printf("num = %d\n",num);

(2) fgets fputs

fgets(char *buffer, int MaxCount,文件指针);

把文件中的数据读入到内存中,每次读入 MaxCount -1 个字符(默认加'\0'),保存到buffer字符数组中。

FILE * pf = fopen("a.txt","r");
char buf[100];
//把字符读入到buf中
fgets(buf,10,pf);
printf("%s",str);
//关闭文件 此动作会让缓冲区内的数据写入文件中,并释放系统所提供的文件资源
fclose(f);

fputs(字符串,文件指针)

该函数成功运行返回一个非负值(包括 0),否则返回EOF(-1)值。

FILE * pf = fopen("a.txt","r+");
char str[100] = "海上生明月,天涯共此时";
fputs(str,pf);
//关闭文件 此动作会让缓冲区内的数据写入文件中,并释放系统所提供的文件资源
fclose(f);

(3) fscanf fprintf (格式化输入 / 输出)

fscanf

int fscanf(FILE * 文件指针, 格式化输入, 参数列表...)

传入文件指针,把文件中的内容格式化读入到内存中,第一个参数是文件指针

读取成功返回 参数个数

读取失败返回 EOF(-1)值。

 test.txt 文件内容为 "Jack 18 m"

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

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

int main() {
    Stu s = {"Tom",21,'m'};
    //创建文件指针 读入方式打开
    FILE * pf = fopen("test.txt","r");
    //格式化把文件内容读入到 s结构体变量中
    fscanf(f,"%s %d %c",&s.name,&s.age,&s.sex);
    //输出结构体内容
    printf("%s %d %c",s.name,s.age,s.sex);
    //关闭文件 此动作会让缓冲区内的数据写入文件中,并释放系统所提供的文件资源
    fclose(f);
    return 0;
}

运行结果

fprintf

int fprintf(FILE * 文件指针, 格式化输出, 参数列表...)

传入文件指针,把内存中的数据写出到指定文件中,第一个参数是文件指针

返回值为 写出的数据总长度(同 printf)

 文件初始状态为空

#include <stdio.h>

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

int main() {
    Stu s = {"Tom",21,'m'};
    //创建文件指针 写出方式打开
    FILE * f = fopen("test.txt","w");
    //格式化写出到文件
    fprintf(f,"%s %d %c",s.name,s.age,s.sex);
    //关闭文件 此动作会让缓冲区内的数据写入文件中,并释放系统所提供的文件资源
    fclose(f);
    return 0;
}

 运行结果

 


(4) fread fwrite (二进制输入 / 输出)

fread

size_t fread(void *ptr, size_t size, size_t count, FILE *stream);

ptr:指向用于存储数据的内存块的指针,文件缓冲区

size:要读取的每个数据项的大小(以字节为单位)。

count:要读取的数据项的数量。

stream:指向被读入数据文件的文件指针

fwrite

size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream);

ptr:指向要写出数据的字符指针

size:每个数据项的字节数。

count:要写入的数据项的个数。

stream:指定要写出的文件指针。

我们在C程序目录放一张图片命名为 b.jpg

#include <stdio.h>
// 我们设置读入个数一次为 1024个
#define BUFFER_SIZE 1024

int main() {
    // 打开源文件以读入
    FILE * fr = fopen("b.jpg", "rb");
    // 打开目标文件以写出
    FILE * fw = fopen("b.obj", "wb");
    // 文件缓冲区
    char buffer[BUFFER_SIZE];
    // 文件单次写出的长度
    unsigned int size;

    // 读取源文件并写入目标文件
    while ((size = fread(buffer, sizeof(char), BUFFER_SIZE, fr)) > 0) 
    {
        fwrite(buffer, sizeof(char), size, fw);
    }
    // 关闭文件
    fclose(fr);
    fclose(fw);
    printf("文件写出成功!\n");
    return 0;
}

运行结果

得到一个 b.obj  的文件


(5)sscanf sprintf 处理字符串

sscanf

int sprintf (char *__stream, const char *__format, 参数列表...)

功能:从字符串中读取格式化输入。

数据源:字符串。

用法:从字符串中读取数据并存储到变量中。

示例:sscanf(str, "%d", &num); 从字符串中读取一个整数。

#include <stdio.h>

int main() {
    char str[] = "Age: 30, Name: John Doe";
    int age;
    char name[50];

    // 使用sscanf从input字符串中读取年龄和名字
    if (sscanf(str, "Age: %d, Name: %49s", &age, name) == 2) 
    {
        printf("Age: %d\n", age);
        printf("Name: %s\n", name);
    } 
    else 
    {
        printf("读取失败~\n");
    }

    return 0;
}

sprintf

int sscanf(const char *__source, const char *__format, ...)

功能:将格式化输出写入字符串。

目标:字符串。

用法:将变量的值写入字符串中。

示例:sprintf(str, "%d", num); 将一个整数转换为字符串表示。

#include <stdio.h>

int main() {
    int age = 30;
    char name[] = "John Doe";
    char output[100];

    // 使用sprintf将年龄和名字格式化为字符串
    sprintf(output, "Age: %d, Name: %s", age, name);

    printf("格式化后的字符串: %s\n", output);
    return 0;
}

五、文件的定位 (随机读写)

fseek

int fseek(FILE *stream, long offset, int origin);

stream:指向 FILE 对象的指针,该对象标识了要操作的文件流。

offset:偏移量,表示从 whence 所指定的位置开始移动的字节数。

origin:指定了偏移量的起始位置,它是一个宏定义,可以是以下三个值之一:

SEEK_SET:等价【常量 0 】文件的开始位置。offset 是从文件开始计算的偏移量。

SEEK_CUR:等价【常量 1 】文件的当前位置。offset 是从当前位置开始计算的偏移量。

SEEK_END:等价【常量 2 】文件的结束位置。offset 是从文件末尾开始计算的偏移量(通常是一个负数,因为你要从末尾向前移动)。

返回值:

如果函数执行成功返回 0

如果发生错误,返回非零值。

//测试1 SEEK_SET 将文件指针f指向文件内容开头位置,偏移量为3,指向第 0 + 3个字符
fseek(f,3,SEEK_SET);

ftell

long ftell(FILE *_File);

传入文件指针

调用成功,将返回当前读写位置的偏移量

调用失败,将返回-1

int len = ftell(f);
//输出结果为 位置指针距离文件指针起始位置的距离
printf("%d",len);

 rewind

void __cdecl rewind(FILE *_File);

该函数可以将所传入的文件指针设置指向文件初始位置


六、文件的错误检测 ferror

ferror 用于检测文件读写出错

int ferror(FILE *_File);

ferror函数通常与文件读取和写入操作一起使用,以检测是否发生了错误。例如,由于磁盘空间不足、权限问题或其他原因而无法写入文件时,ferror可用于检测这样的错误。

返回值:0表示未出错,非0表示有错

FILE *file = fopen("example.txt", "r");  
if (file == NULL) {  
    perror("Error opening file");  
    return 1;  
}  
  
char ch;  
while ((ch = fgetc(file)) != EOF) {  
    // 处理字符  
    if (ferror(file)) {  
        perror("Error reading from file");  
        break;  
    }  
}  
 
fclose(file);

七、文本文件和二进制文件

文本文件(Text File)

文本文件是一种由一系列字符组成的文件,这些字符可以是字母、数字、标点符号等。文本文件通常用于存储人类可以理解的文本信息,如文档、源代码、网页内容等。

存储方式:

文本文件以字符为单位进行存储,每个字符占据固定的字节数(如ASCII编码中每个字符占据1个字节,UTF-8编码中英文字符占据1个字节,中文字符可能占据多个字节)。文本文件在存储时会进行编码转换,将字符转换为对应的字节序列。常见的编码方式有ASCII、UTF-8、GBK等。

处理方式:

文本文件通常使用文本编辑器或程序进行打开和编辑。在处理文本文件时,程序会按照字符编码方式读取文件中的字节序列,并将其转换回对应的字符。

应用场景:

文本文件主要用于存储人类可读的文本信息,如文档、邮件、网页等。在编程中,源代码文件通常也是文本文件,可以使用文本编辑器进行编写和修改。


二进制文件(Binary File)

二进制文件是以二进制编码形式存储的文件,它包含的是字节流,即0和1的组合,这些字节流表示的信息无法直接以文本形式展现。二进制文件通常用于存储程序、图片、音频、视频等复杂的数据结构。

存储方式:

二进制文件以字节为单位进行存储,每个字节可以表示任何数据,包括字符、数字、图像数据等。二进制文件不会进行编码转换,直接以原始的字节流形式存储数据。

处理方式:

二进制文件需要使用特定的软件或程序进行打开和解析。例如,图片文件需要使用图像查看器打开,音频文件需要使用音频播放器播放。程序在处理二进制文件时,需要按照文件的数据结构或协议来解析字节流,提取出有用的信息。

应用场景:

二进制文件主要用于存储程序、图像、音频、视频等复杂的数据结构。在编程中,编译后的程序文件通常是二进制文件,可以直接由计算机硬件执行。


八、文件结束判定 feof函数

int __cdecl feof(FILE *_File);

在文本文件可以通过ASCII是否为-1来判断文件是否读完,而feof()函数,并不是通过读取到文件的EOF来评判,这个文件是否为空。

feof()站在光标所在位置,向后看看还有没有字符。

如果文件后面有字符,返回0

如果文件后面没有字符,返回非0

FILE * f = fopen("test.txt","r");
if(f == NULL)
{
    perror("fopen");
    return 1;
}
char c;
while ((c = fgetc(f)) != EOF)printf("%c ", c);
//非 0,说明文件读到末尾
if(feof(f))printf("\nEND~");
else
{
    perror("fgetc() error");
    return 1;
}
fclose(f);

九、文件缓冲区

什么是文件缓冲区?

简单来说,就是电脑内存里的一块区域,专门用来暂时存放读写文件时的数据。当我们读取或写入文件时,数据不是直接从硬盘上读取或写入,而是先放在这个缓冲区里,这样可以减少直接操作硬盘的次数,提高数据读写效率。

举个例子,假设你正在读一个很大的文件,如果每次读一个字节就直接从硬盘上读取,那硬盘就会不停地转动来找到并读取数据,这样会很慢。但如果使用文件缓冲区,电脑会一次性从硬盘读取多个字节的数据放入缓冲区,然后直接从缓冲区中读取数据给你,这样就减少了硬盘的读写次数,提高了读取速度。

所以,文件缓冲区就像是一个“中转站”,帮助电脑更高效地处理文件读写操作。

从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上.

如果从磁盘向计算机读入数据,则磁盘文件中读取数据输入到内存缓冲区(充满缓冲区), 然后从缓冲区逐个地将数据送到程序数据区(程序变量等).缓冲区的大小根据C编译 系统决定的 

#include <stdio.h>
#include <synchapi.h>

int main() 
{
    //打开
    FILE* pf = fopen("text.txt", "wb");
    if (pf == NULL)
    {
        perror("fopen");
        return 1;
    }
    //存入
    char * a ="Information is exist";
    fprintf(pf,"%s",a);
    printf("此5秒数据在文件缓冲区内,打开文件是没有数据的\n");
    Sleep(5000);//睡眠5秒
    printf("文件内容已被刷新到缓冲区");
    fflush(pf);//此函数可以刷新缓冲区中的数据,使其存入硬盘文件中
    //关闭
    fclose(pf);
    pf = NULL;
    return 0;
}

Over~

  • 44
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值