本文主要是介绍C语言程序设计 笔记代码梳理 重制版,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
前言
本篇以笔记为主的C语言详解,全篇一共十章内容,会持续更新基础内容,争取做到更详细。多一句没有,少一句不行!
形而上学者谓之道,形而下学者谓之器
形而上学者谓之道,形而下学者谓之器
第1章 C语言的流程
1.C程序经历的六个阶段
- 编辑(Edit)
- 预处理(Preprocess)
- 编译(Compile)
- 汇编(Assemble)
- 链接(Link)
- 执行(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^1 + x * n^2 + 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.变量
(1) 变量的概念
内存中有个存储区域,这个地方的数据可以在同一类型范围内不断变化通过变量名,可以访问这块内存区域,获取里面的值;
变量名的构成:数据类型 变量名 值
C语言中变量声明格式: 数据类型 变量名 = 值
(2) 变量的注意
全局变量
定义在函数外部的叫全局变量,默认初始化为0静态变量 static
带有static开头的关键字叫静态变量,默认初始化为0
局部变量
声明局部变量以后,要初始化赋值!定义变量时,这个变量使用的内存不一定被清空,它可能是垃圾值,运行程序会异常退出
(3) 变量的作用域
局部变量
在函数中定义的,有效范围在 定义开始到{ }结束
全局变量在函数外定义,有效范围从定义位置开始,到程序结束!
2.标识符
(1) 标识符的概念
C语言中,凡是可以自己命名的地方,都叫做标识符 例如:函数名,变量名,数组名,结构体名
(2) C语言标识符的命名规范
1.只由英文大小写字母、数字、或 _(下划线) 组成
2.第一个字符只能是英文字母或下划线,不能数字开头!!!
3.大小写英文字母代表不同的字符
4.不能是C语言的关键字
(3) C语言 32个关键字
auto | short | int | long |
float | double | char | struct |
union | enum | typedef | const |
unsigned | signed | extern | register |
static | volatile | void | if |
else | switch | case | default |
for | do | while | continue |
break | goto | sizeof | return |
数据类型的分类
3.运算符
(1) C语言运算符优先级
优先级 | 运算符 | 名称或含义 | 使用形式 | 结合方向 | 说明 |
1 | [] | 数组下标 | 数组名[常量表达式] | 左到右 | -- |
() | 圆括号 | (表达式)/函数名(形参表) | -- | ||
. | 成员选择(对象) | 对象.成员名 | -- | ||
-> | 结构体选择(指针) | 对象指针->成员名 | -- | ||
2 | - | 负号运算符 | -表达式 | 右到左 | 单目运算符 |
~ | 按位取反运算符 | ~表达式 | |||
++ | 自增运算符 | ++变量名/变量名++ | |||
-- | 自减运算符 | --变量名/变量名-- | |||
* | 取值运算符 | *指针变量 | |||
& | 取地址运算符 | &变量名 | |||
! | 逻辑非运算符 | !表达式 | |||
(类型) | 强制类型转换 | (数据类型)表达式 | -- | ||
sizeof | 长度运算符 | sizeof(表达式) | -- | ||
3 | / | 除 | 表达式/表达式 | 左到右 | 双目运算符 |
* | 乘 | 表达式*表达式 | |||
% | 余数(取模) | 整型表达式%整型表达式 | |||
4 | + | 加 | 表达式+表达式 | 左到右 | 双目运算符 |
- | 减 | 表达式-表达式 | |||
5 | << | 左移 | 变量<<表达式 | 左到右 | 双目运算符 |
>> | 右移 | 变量>>表达式 | |||
6 | > | 大于 | 表达式>表达式 | 左到右 | 双目运算符 |
>= | 大于等于 | 表达式>=表达式 | |||
< | 小于 | 表达式<表达式 | |||
<= | 小于等于 | 表达式<=表达式 | |||
7 | == | 等于 | 表达式==表达式 | 左到右 | 双目运算符 |
!= | 不等于 | 表达式!= 表达式 | |||
8 | & | 按位与 | 表达式&表达式 | 左到右 | 双目运算符 |
9 | ^ | 按位异或 | 表达式^表达式 | 左到右 | 双目运算符 |
10 | | | 按位或 | 表达式|表达式 | 左到右 | 双目运算符 |
11 | && | 逻辑与 | 表达式&&表达式 | 左到右 | 双目运算符 |
12 | || | 逻辑或 | 表达式||表达式 | 左到右 | 双目运算符 |
13 | ?: | 条件运算符 | 表达式1? 表达式2: 表达式3 | 右到左 | 三目运算符 |
14 | = | 赋值运算符 | 变量=表达式 | 右到左 | -- |
/= | 除后赋值 | 变量/=表达式 | -- | ||
*= | 乘后赋值 | 变量*=表达式 | -- | ||
%= | 取模后赋值 | 变量%=表达式 | -- | ||
+= | 加后赋值 | 变量+=表达式 | -- | ||
-= | 减后赋值 | 变量-=表达式 | -- | ||
<<= | 左移后赋值 | 变量<<=表达式 | -- | ||
>>= | 右移后赋值 | 变量>>=表达式 | -- | ||
&= | 按位与后赋值 | 变量&=表达式 | -- | ||
^= | 按位异或后赋值 | 变量^=表达式 | -- | ||
|= | 按位或后赋值 | 变量|=表达式 | -- | ||
15 | , | 逗号运算符 | 表达式,表达式,… | 左到右 | -- |
简记版:
括号 > 单目运算符 > 算术运算符 > 移位 > 关系> 位运算符 > 逻辑 > 三目 > 赋值 > 逗号
4.表达式
(1) 表达式的定义
表达式是一种有值的语法结构,它由运算符和常量、变量、函数调用返回值等结合而成,每个表达式一定有一个值
例 1+1 就是一个表达式,它的值为 2
第 3 章 顺序结构程序设计
1.顺序结构的介绍
什么是顺序结构
顺序结构是按照代码的书写顺序从前到后执行的结构。是C语言最简单、最基本的结构。
顺序结构的特点
1. 自上而下
2. 没有分支
3. 依次执行
#include<stdio.h>int main()
{printf("程序开始了~\n");printf("执行语句...\n");printf("程序结束\n");"return 0;
}
2.格式化输出函数 printf()
(1) printf()函数简介
作用:将格式化后的字符串输出(打印东西)
printf("Good job!");
函数原型
int printf ( const char * format, ... );
返回值:输出的字符总数
int a = printf("%d",123);
printf("\na = %d",a);
(2) 常见占位符
%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型数据 |
输出格式说明
%(正整数)d 限定宽度(右对齐)
printf("%10d\n",123);
%(负整数)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);
(3) 转义字符
\? | 在书写连续多个问号时使用,(在某些编译器下 ,会将 “ ??) ” 解析成 三字母词 “ ] ” ) |
\' | 用于表示字符常量 ' |
\" | 用于表示一个字符产内部的双引号 |
\\ | 用于表示一个反斜杠,防止它被解释为一个转义序列符 |
\a | 警告字符,蜂鸣 |
\b | 退格符 |
\f | 进纸符 |
\n | 换行 |
\r | 回车 |
\t | 水平制表符 |
\v | 垂直制表符 |
\ddd | ddd表示1~3个八进制的数字。如:\031 = 十进制 25 |
\xdd | dd表示2个十六进制数字。如:\x20 = 十进制 32 |
3.格式化输入函数 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);
注意
格式控制符中的非格式控制符(abc),输入的数据一一对应,所以非格式控制符的内容也要进行输入,否则数据就会输入失败
int a;
scanf("abc%d",&a);
第 4 章 选择结构程序设计
1.选择结构简介
第 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
char *strerror(int errnum);
字符串报错,常用用于文件打开失败,输出错误信息。errnum是一个整数,表示错误代码,通常是全局变量errno的值。当文件打开失败时,errno会被设置为相应的错误码,此时可以调用strerror(errno)来获取并打印出错误信息。
FILE * pfile;
pfile = fopen("test.tt","r");
if(pfile == NULL)
{printf("打开文件失败 代码:%s",strerror(errno));
}
(11) perror
void perror(const char *s);
perror会打印一个错误信息加一个冒号
FILE *fp = fopen("nonexistent_file.txt", "r");
if (fp == NULL) { // fopen 失败,打印错误消息 perror("Error opening file"); // 由于 fopen 失败,我们退出程序 exit(EXIT_FAILURE);
}
第 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,指向 函数addint 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 章 结构体、共用体与枚举
1.结构体
(1) 结构体的作用
封装数据:结构体可以将多个不同类型的数据封装成一个单独的数据类型。这使得我们可以创建一个包含多种信息的复合数据类型,而不仅仅是单个的数据类型(如整数、浮点数、字符等)。例如,我们可以创建一个结构体来表示一个学生的信息,包括姓名、年龄、学号等。
提高代码的可读性和可维护性:通过使用结构体,我们可以将相关的数据组织在一起,这有助于代码的阅读者更好地理解数据的含义和用途。此外,如果以后需要修改或扩展数据的结构,我们只需要修改结构体的定义,而不需要在代码中的多个地方进行修改。
实现数据的抽象和封装:结构体允许我们隐藏数据的实现细节,只暴露必要的接口给外部使用。这是面向对象编程的一个重要原则,可以提高代码的模块化和可重用性。
方便数据传递:当我们需要在函数之间传递多个相关的数据时,可以使用结构体作为参数。这样,我们只需要传递一个结构体变量,而不是多个单独的变量,这可以简化函数的调用和参数的传递。
支持更复杂的数据结构:结构体是构建更复杂数据结构(如链表、树、图等)的基础。通过使用结构体,我们可以定义节点类型,并在这些节点之间建立链接关系,从而构建出各种复杂的数据结构。
(2) 结构体类型的定义
struct 结构体名
{
成员列表(可以是基本的数据类型,指针,数组或其他结构类型)
};
注意: 结构体大括号的结尾有分号,和语句一样;而函数和语句体的大括号没有分号
(3) 结构体变量的定义
方式一
//方式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;//起变量名,不给类型
这种结构体只能单个使用,因为没有结构体类型,无法再次创建。某些设计模式下会使用到这种定义方式
(4) 结构体变量的初始化
方式一
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
(5) 结构体变量的引用
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);
}
结构体变量的引用,只需要在定义之后。使用 结构体变量名 . 成员 就可以获取到对应的值
(6) 结构体数组
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},...
};
定义后,再这样赋值,是错误的写法!!!
(7) 结构体指针
什么是结构体指针
一个指向结构体变量的指针,就是结构体指针
结构指针和其它类型的指针本质上没有区别,但唯一不同点是,结构体指针的取值和成员取值方式不同,普通结构体变量使用的是'.',结构体指针变量使用的是 -> (结构体指针运算符),它可以等价于(*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 取值时加上括号,这样容易产生一些错误,于是我们使用 -> 更容易区别和不易犯错
(8) 结构体内存对齐规则
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( )
{ //输出24printf("%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( )
{//输出结果 为 18printf("%d\n",sizeof(s));
}
运行结果 18
在某些情况下,你可能想要控制结构体的对齐方式,例如减少填充字节以节省空间。你可以使用预处理器指令或编译器特定的属性来实现这一点。例如,CLion编译器提供了__attribute__((packed))属性,它可以用来指示编译器不要添加任何填充字节
2.共用体
(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 = 65535printf("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) 共用体赋值操作图解示例
3.枚举类型
(1) 枚举类型的定义及引用
枚举类型的定义和结构体类似,关键字 + 枚举类型名
枚举类型,它允许你为整数值分配有意义的名字
在枚举类型的定义中, 成员是不允许存在 ' ; ' (分号) 的
enum Color
{ RED, GREEN, BLUE
};
在这个例子中,Color 是一个枚举类型,它有三个成员:RED、GREEN 和 BLUE。默认情况下,RED 的值为0,GREEN 的值为1,BLUE 的值为2,以此类推。
enum Color
{ RED, GREEN = 66, BLUE
};
注意
这里我们改变了 GREEN,RED还是 0,GREEN = 66,此时的 BLUE 就会变成 67 然后依次递增
(2) 枚举类型的说明
枚举类型在本质上仍然是整数类型,因此可以将整数赋值给枚举类型的变量,反之亦然。
枚举类型的优点
增加代码可读性:使用有意义的名称代替魔法数字(magic numbers)更便于理解。
类型安全:枚举类型提供了一种方式来限制变量可以取的值,从而增加了类型安全性。
易于维护:如果需要更改枚举成员的值或添加新的成员,只需在枚举类型的定义中进行修改,而无需在代码的多个地方进行搜索和替换。
(3) 枚举类型的引用
声明后赋值使用
enum Color
{RED, GREEN, BLUE
};
int main( )
{ //方式 1 声明后赋值使用enum Color myColor = RED;printf("RED = %d\n",myColor);return 0;
}
直接使用
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 1024int 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~
这篇关于C语言程序设计 笔记代码梳理 重制版的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!