C语言
C语言
计算机系统组成

- 输⼊设备:
- ⿏标、键盘、⻨克⻛、扫描仪
- 输出设备:
- 显示屏、扬声器、打印机。
- 中央处理器(cpu): 运算速度⾮常快。ns 级别。
- 运算器(算逻单元):负责数据运算
- 控制器:帮助cpu获取指令交给运算器
- 寄存器:存储cpu⽤来运算的数据
- 预取器:从内存中获取程序中的指令。
- MMU:虚拟内存映射。
- 内存储器(内存):
- ROM:(read only memory)
- RAM:(随机:random access memory)
- 内存靠 “电信号” 来存储数据。 断电没!数据存储不能持久化。 优点:数据读写速度快。
- 外存储器(硬盘):
- 硬盘采⽤ “磁信号” 来存储数据。断电依然在。数据能持久化存储。 缺点:数据读写速度慢。
硬件系统

主机部分
- cpu
- 内存储器
外设部分
- 输⼊设备:读⼊。
- 输出设备:写出。
- 外存储器:持久化存储。

软件系统
系统软件
- 操作系统:
- 优秀的商业公司、开源组织,站出来,编写⼀套针对硬件的底层程序。 管理声卡、显卡、⽹卡、磁盘等等这些硬件。
- 概念:操作系统 就是管理计算机硬件与软件资源的 ⼀个计算机程序。 本质:程序!!
- 向下:
- 提供驱动程序,管理硬件。
- 向上:
- 图形界⾯:普通⽤户,⿏标点点点。
- 终端界⾯:普通⽤户、开发⼈员都可以使⽤。
- 系统调⽤:操作系统 提供给开发⼈员使⽤的函数。

- 常⽤操作系统:windows:微软、macOS:苹果、Linux、Unix
- 语⾔系统
- 计算机语⾔: C、C++、java、Python、php ....
- 机器语⾔:⼆进制(10101001)
应⽤软件
根据实际需求,来制定功能。
- ⽂件处理
- 图形处理
- 表格处理
- 实时控制
编译器和语⾔
- 编程语⾔:
- 作⽤:控制计算机硬件⼯作。
- 组成:字⺟、特殊字符。
- 每个编程语⾔有⾃⼰⼀套规则、语法。——学习⽬标!
- 编译器:
- 原因:CPU,只认识 1010010 ⼆进制码。 abc、汉字 看不懂。
- 作⽤:将⼈类易读易懂的语⾔转换成 cpu 能读懂的语⾔。 —— 编译!
- 语⾔不同,语法不同,因此使⽤的编译器不同。
- java 使⽤ javac 编译器。 不能拿来编译C语⾔。
- C语⾔ 使⽤ gcc 编译器。 不能拿来编译java语⾔。
C语⾔简述
计算机语⾔发展史
- 机器语⾔:101100110
- 汇编语⾔:助记符:abc --- 10100101。问题:硬件不同,指令不同。不同架构(cpu) 指令集不同。
- 只能⽀持某⼀种特定的硬件。—— 跨平台性差。
- 衍⽣出 B 语⾔。
- C语⾔:借助编译器将C代码,转换成各种平台使⽤的 指令。—— 跨平台。
- C++、java、Oc、Python:⾯向对象编程。程序扩展性好!
- SQL语句:⼈类更易理解。
机器⽣汇编,汇编⽣B,B⽣C,C⽣万物!!
C语⾔标准
标准简史
- 1972年C语⾔在⻉尔实验室诞⽣. 丹尼斯·⾥奇 参考B语⾔开发.
- 1970-80年代,C语⾔被⼴泛应⽤,产⽣很多不同的C语⾔版本. 程序可移植性⽐较差.
- 1983年,美国国家标准委员会(ANSI) 成⽴⼀个⼩组来制定C语⾔的标准. C语⾔⽀持哪些语法、⽀持哪些功能等等.
- 1989年,通过了C语⾔的第⼀个标准. C89标准.
- 1990年,国际标准化组织(ISO) 和 国际电⼯委员会(IEC) 将 C89标准当做国际的C语⾔标准. C90标准. C89和C90指的是同⼀个标准
- 1994年 ISO和 IEC 对 C89标准进⾏修订. C94标准. 由于并没有增加新的语法特性,还是叫做 C89或者C90.
- 1995年 ISO和IEC再次做了修正,C95 标准.
- 1999年 ISO 和 IEC 发布了C语⾔新标准. C语⾔第⼆个标准. 在该标准中,新增许多实⽤的C语⾔语法特性. 增加新的关键字、可变⻓数组等等. C99标准
- 2007年,重新修订了C语⾔.
- 2011年, 发布新的版本。新增了⼀些语法,泛型、国际化⽀持. ⽬前为⽌最新版本是 C11.
标准的影响
- 可将C语⾔的标准理解为C语⾔说明书。但,其并没有强制性约束⼒。
如:微软拿到标准,认为有些标准不合理,不⽀持。 微软认为某些特性⾮常好,但标准中没有,微软可以修改标准,新增语法.
- 如果编译器不⽀持标准,我们即使使⽤标准中的语法仍然会报错。
- 编译器版本也会影响程序。因此,编写程序之前要确定编译器版本。
- 常⻅的C/C++编译器:
- Borland C++ 宝蓝公司
- Intel C++ 英特尔编译器
- VC++ 微软公司
- g++编译器(gcc是编译套件), Linux 默认使⽤的编译器. 对标准⽀持最好.
C语⾔的优缺点
- 优点:学习成本低、运⾏速度快、功能强⼤。
- 缺点:代码实现周期⻓、可移植性差、对经验要求⾼、对平台库依赖多
C语⾔的应⽤领域
- 服务器。
- 操作系统。
- 上层应⽤。 MFC、QT
- 嵌⼊式。
- ⼈⼯智能、硬件驱动。
- 中间件。
- ⽹络攻防、数据安全。
- ⼤学必修课。
- 名企、外企。
C语⾔32个关键字
关键字:在C语言中被赋予了特定含义的英文单词,一共有32个关键字。但是不需要背,后面会挨个讲解,现在只要只有关键字的两个特点即可:
关键字全部小写
在特定的编译器中,关键字是高亮显示的
vs:蓝色或者紫色
auto break case char const continue default do double else enum
extern float for goto if int long register return short signed
sizeof static struct switch typedef union unsigned void volatile while
第一个程序HelloWorld

- ⽂本编辑HelloWorld:打开任意⼀个⽬录。编写hello world 程序
#include <stdio.h>
int main(void)
{
printf("hello world\n");
return 0;
}
- 编译 hello world 程序 ——> 得到 机器能识别的 ⼆进制码。
cd ⽬录 —— 含义:进⼊这个⽬录。

提示:快捷打开 HelloWorld.c ⽂件所在⽬录。进⼊到 HelloWorld.c ⽂件所在⽬录. 直接在 “地址栏” 中键⼊ cmd ,不需要 cd ⽬录。切换。
常⻅IDE
IDE:集 编辑器、编译器、调试器与⼀身的集合⼯具。
- Windows:
- vs(visual studio)2013、2015、2017、2019、2022
- Clion:跨平台IDE。、跟Java的IDEA、python的pycharm是同一家公司的
- Qt Creator 跨平台IDE。
- Eclipse。
- MacOS:(苹果电脑的操作系统)
- Xcode
- Clion:跨平台IDE。
- Qt Creator 跨平台IDE。
- Eclipse。
- Linux:
- vi/vim —— ⽂本编辑器。
- Clion:跨平台IDE。
- Qt Creator 跨平台IDE。
- Eclipse。
备注:大多企业开发用vs会更多一些
VS安装
- 打开微软的官方网站 https://visualstudio.microsoft.com/,下载 Visual Studio 安装包。

- 双击安装包进行安装。在 “工作负荷” 标签页中,选择 “使用 C++的桌面开发” 选项即可。

“安装位置” 中修改安装路径。路径要求:
1,不要有中文,不要有空格,不要有一些特殊符号
2,选择一个统一的文件夹进行管理

- 自动下载并安装,过程略微漫长,耐心等待。

- 安装完成,弹出 “安装完毕” 对话框。 点击确定。

- 可用微软的账号登录,没有可以注册一个。也可以点击“暂时跳过此项”。

- 根据提示选择主题

- 点击启动,环境搭建完毕
VS2022基本使⽤
- 创建项目:点击 “创建新项目”

- 选择创建 “控制台应用”,点击下一步。

- 指定项目名称。确保位置下的项目存储目录存在。 勾选 “将解决方案和项目.....” ,点击 “创建”。

右侧的解决方案管理器可以放在左侧或者右侧
创建 helloworld.c ⽂件:右键选择原文件,选择添加,选择新建项
在弹出的编辑框中,选中 “C++文件(.cpp)”,将 下方 “源.cpp” 手动改为要新创建的文件名,
如:helloWorld.c 。注意,默认 cpp 后缀名,要手动改为 .c 后缀名,然后点击 “添加”。

- 编写 helloworld.c程序
#include <stdio.h>
int main(void)
{
printf("hello world!\n");
return 0;
}
- 点击上方绿色空心三角运行

- 解决窗⼝⼀闪⽽过问题,解决方法
使⽤ 函数:在 return 0; 代码前添加system("pause");
2.修改 VS2019 ⼯具配置属性:项⽬名上,右键 —— 属性 —— 配置属性 ——连接器 —— 系统 —— ⼦系统 —— 下拉框中选择 “控制台(/SUBSYSTEM:CONSOLE)” —— 点击 “应⽤” —— 点击确定
- 修改字体: ⼯具 —— 选项 —— 环境 —— 字体和颜⾊ 。 选择字体。 Ctrl + ⿏标滚轮 放⼤、缩⼩字号
HelloWorld释义
#
: 代表引⼊头⽂件专⽤特殊字符。- include : 引⼊头⽂件专⽤关键字。
- <> : ⽤来包裹 库 头⽂件名
- stdio.h : 使⽤的 头⽂件。因为程序中使⽤了 printf() 函数。就必须使⽤该头⽂件。
- std:标准:standard
- i: input 输⼊。
- o: output 输出。
- int :main 函数返回值为 整型。 int
- main: 整个程序的⼊⼝函数。 任何.c 程序,有且只有⼀个 main 函数。
- (void) : 当前main函数没有参数。
- {} : 内部放函数体。
- printf(“helloworld\n”) :
- printf(); C语⾔向屏幕输出字符使⽤的函数。
- helloworld: 待写出的字符串内容。
- \n: 回⻋换⾏。
- return 0;
- return 返回。 C程序要求,main 函数要有返回值。借助 return 实现返回。
- 0:成功!因为 int ,返回整数。
- 注意事项
- 程序中使⽤的所有的字符,全部是 “英⽂半⻆” 字符。
- 程序中,严格区分⼤⼩写。
- “;” 代表⼀⾏结束。不能使⽤ 中⽂ “;”,必须是英⽂。
- 换行符:
\r
回到行首\n
:到下一行
操作系统 | 换行符 |
---|---|
windows | \r\n |
mac | \r |
Linux | \n |
平时写代码的时候,想要换行直接写\n
即可,C语言会根据不同的操作系统解析成对应的换行符
代码运⾏ 4 种模式
- Debug x86:以调试模式,运⾏32位程序。
- Debug x64:以调试模式,运⾏64位程序。
- Release x86:以发布模式,运⾏32位程序。
- Release x64:以发布模式,运⾏64位程序。
Debug: 调试模式。⽣成的 .exe ⽂件 ⽐ Release 模式⽣成⽂件⼤。 带有调试信息。学习用
Release:发布模式。⽣成的.exe ⽂件没有调试信息。⽂件较⼩。
热键:运⾏编写好的程序。Ctrl - F5
注释
- 单⾏注释。 // 待注释的内容
- 多⾏注释。 /* 待注释的内容 */
- 多⾏注释可以嵌套单⾏注释。 多⾏注释间不能嵌套
- 注释的快捷键:Ctrl + K,Ctrl + C/U
- 注释擦除:注释的内容,不参与运行
System函数
- 作⽤:执⾏ windows 系统中的指定的命令。
- 命令:
- pause:暂停。
- cmd:启动新的终端
- calc:唤起windows下的计算器。
- notepad: 唤起 windows下的记事本。
- mspaint: 唤起 windows下的画图⼯具。
- cls:清空 当前windows下 终端中的内容。
#include <stdio.h> // 引⼊头⽂件 stdio.h , 因为下⾯使⽤了printf() 必须添加此头⽂件。
#include <Windows.h> // 引⼊头⽂件 Windows.h, 因为下⾯使⽤ Sleep() 函数。
int main(void) // main 是程序的⼊⼝函数。 void表示没有参数。int表示返回整数。
{
printf("hello world1!\n"); // 打印 helloworld 字符串,到屏幕。 \n 换⾏之意。
printf("hello world7!\n");
//Sleep() 函数,指定程序睡眠。 默认单位:毫秒。 需要使⽤头⽂件。 #include <Windows.h>
Sleep(3000); // 使当前程序,打印完 helloworld 后,睡眠3s钟
// system("cmd"); //3s 后,再有机会执⾏。启动⼀个新终端。
// system("calc"); //3s 后,启动计算器
// system("notepad"); //3s 后,启动记事本
// system("mspaint"); //3s 后,启动画图⼯具
system("cls"); //3s 后,将当前 终端 清空。
return 0; // 因为 main返回 int 。所以这⾥有 return 0;
}
键盘录入的基本使用
int main()
{
//1.定义一个变量用来接收数据
int a;
// 2.键盘录入
scanf("%d",&a);
// 3.打印一下
printf("%d\n",a);
return 0;
}
main函数种类
main函数标准类型
无参:int main(void) { return 0; }
有参:int main(int argc, char *argv[]) { return 0; }
参1:表示给 main 函数传递的参数的总个数。
参2:是一个数组。数组的每一个元素都是 字符串 (char *)
int main(int argc, char *argv[])
{
int i = 0;
for (i = 0; i < argc; i++)
{
printf("argv[%d] = %s\n", i, argv[i]);
}
system("pause");
return EXIT_SUCCESS;
}
测试方法
- 在 终端中, 使用 gcc 编译得到 可执行文件,如 test.exe
- 不能获取带有空格的字符串!!!空格是分隔符。
gcc 09-带参数的main.c -o test.exe
test.exe aa bb cc dd ee
test.exe hello world haha xixi hoho heihei 。。。。
argc: --- 6
argv[0] = test.exe
argv[1] = aa
argv[2] = bb
argv[3] = cc
argv[4] = dd
argv[5] = ee
在 VS 中,项目名称上 右键 —— 属性 —— 配置属性——调试 —— 命令行参数 —— 写入 待测试
的命令行参数。

main函数其他类型
// 都能正常运行。但是!不是main的标准语法格式。
void main(int argc, char *argv[]);
void main(void);
int main();
int main(void);
main();
main(int argc, char *argv[]);
代码执行流程分析/gcc编译4步骤
整体过程

C语言运行分为以下4步,在VS中直接运行把中间的步骤省略了
预处理
- 找到#include后面的 <stdio.h>这个文件
- 参数:-E
- 生成的文件: xxx.i 预处理文件
- 使用命令: gcc -E xxx.c -o xxx.i
- 工具:预处理器(包含在 gcc 编译集合工具中)
- 完成的工作:
- 头文件展开。
- 展开 stdio.h 文件内容,和源码一起,放到 xxx.i 文件中。
- 不检查语法错误!可以在此阶段展开任意文件。
- 测试命令: gcc -E hello.c -o hello.i -I(大i) .(当前目录)
- 宏定义替换
- 将宏名,替换成宏值。
- #define PI 3.14 【解释】:define:创建宏。 PI :宏名 3.14:宏值。
- 替换注释:把单行、多行注释替换成空行。
- 展开条件编译:根据条件来展开代码。
// 测试案例2宏定义替换。 使用命令: gcc -E hello.c -o hello.i
#include <stdio.h>
// 定义宏
#define PI 3.14
int main(void)
{
printf("hello world\n");
// 使用宏
printf("PI = %f\n", PI);
return 0;
}
// 测试案例4展开条件编译
#include <stdio.h>
// 定义宏
// #define PI 3.14 // PI 定义与否,直接决定了下面的 ----666 是否打印。
int main(void)
{
printf("hello world\n");
// 使用宏
//printf("PI = %f\n", PI);
// 使用条件编译, 含义是:如果定义了PI ,那么就打印 -----666, 否则不打印。
#ifdef PI
printf("-------------6666\n");
#endif
return 0;
}
编译
把c文件编译成二进制文件后缀名为obj
参数:-S
生成的文件: xxx.s 汇编文件
使用命令: gcc -S xxx.i -o xxx.s
工具:编译器(包含在 gcc 编译集合工具中)
完成的工作:
- 逐行检查语法错误!【重点】 —— 编译过程整个gcc编译4步骤中,最耗时。
- 将 C 程序翻译成汇编指令。得到 .s 汇编文件。
.file "hello.c"
.text
.def __main; .scl 2; .type 32; .endef
.section .rdata,"dr"
.LC0:
.ascii "hello world\0"
.LC2:
.ascii "PI = %f\12\0"
.LC3:
.ascii "-------------6666\0"
.text
.globl main
.def main; .scl 2; .type 32; .endef
.seh_proc main
main:
pushq %rbp
.seh_pushreg %rbp
movq %rsp, %rbp
.seh_setframe %rbp, 0
subq $32, %rsp
.seh_stackalloc 32
.seh_endprologue
call __main
leaq .LC0(%rip), %rcx
call puts
movq .LC1(%rip), %rax
movq %rax, %rdx
movq %rdx, %xmm1
movq %rax, %rdx
leaq .LC2(%rip), %rcx
call printf
leaq .LC3(%rip), %rcx
call puts
movl $0, %eax
addq $32, %rsp
popq %rbp
ret
.seh_endproc
.section .rdata,"dr"
.align 8
.LC1:
.long 1374389535
.long 1074339512
.ident "GCC: (x86_64-posix-sjlj-rev0, Built by MinGW-W64 project) 8.1.0"
.def puts; .scl 2; .type 32; .endef
.def printf; .scl 2; .type 32; .endef
汇编
把预处理找到的h文件,还有编译之后产生的obj文件打包在一起,产生exe文件
参数:-c
生成的文件: xxx.o 目标文件(二进制。人类看不懂。)
使用命令: gcc -c xxx.s -o xxx.o
工具:汇编器(包含在 gcc 编译集合工具中)
完成的工作:翻译:将汇编指令翻译成对应的 二进制指令。
连接/链接
- 参数:无 (-o 不是链接阶段参数,是用来指定文件名)
- 生成的文件: xxx.exe 可执行文件(二进制。人类看不懂。)
- 使用命令: gcc xxx.o -o xxx.exe
- 工具:链接器(包含在 gcc 编译集合工具中)
- 完成的工作:
- 库引入
- 合并多目标文件
- 合并启动例程

小结
gcc 编译的 4个步骤中,每个步骤直接都可相互转换。

printf格式化输出int
#include <stdio.h>
#define PI 3.14
int main(void)
{
int a = 10;
printf("%d\n", a); // %d:格式匹配符,匹配整数。
printf("a = %d\n", a); // a = 在“”中,代表普通字符串, 原样输出。
printf("%d\n", 100);
printf("%f\n", PI); // %f:格式匹配符,匹配小数。
printf("PI = %f\n", PI); // PI = 在“”中,代表普通字符串, 原样输出。
printf("%f\n", 3.45678); // %f:格式匹配符,匹配小数。
int b = 20;
printf("%d + %d = %d\n", a, b, a+b); // +、= 在 “”中,代表普通字符串, 原样输出。
printf("%d + %d = %d\n", 3, 7, 3+7);
return 0;
}
程序调试
- 前提:程序,没有语法错误。 ——语法错误 VS 帮我检查。
- 检查 程序出现的逻辑错误!
- 核心思想:让程序一行一行的执行。
- 添加行号:工具 —— 选项 —— 文本编辑器 —— c/c++ —— 行号(勾选)
程序调试流程
添加断点。 —— 可添加多个
- 鼠标点击待添加断点行,左侧行号前灰色区域。 再次点击取消。
- 光标停止在待添加断点行的任意位置,按F9添加断点。 再次按 F9 取消断点。
调试,必须在 Debug 模式下进程。Release 模式无效。
F5 启动调试。
断点停止的位置,是尚未执行的指令。
开始调试:
- 逐语句执行。 逐语句执行下一行(F11):遇见函数,进入自定义函数内部,逐条跟踪执行。逐过程执行。
- 逐过程执行下一行(F10):遇见函数,不进入函数内部,逐条跟踪执行。
- 逐断点执行。 代码中有多断点,直接跳转到下一个断点。 —— 点击“继续”,无快捷键。
- 跳出函数。跳出当前断点所在的函数。 shift + F10
31.png
变量
变量 3 要素
- 变量名:用来在程序中使用。
- 变量类型:开辟内存空间大小。
- 变量值:存储的实际数据。
变量定义:
- 定义语法: 类型名 变量名 = 变量值(一般定义方法)。 int m = 57;
- 会开辟内存空间给变量。变量声明不会开辟内存空间。
变量声明:
- 语法:
- int a; 没有变量值的变量定义,叫做声明。
- extern int a; 添加 extern 关键字。
- 特性:
- 变量要想使用,必须 有定义。
- 编译器,在使用变量之前,必须要看到变量定义。如果没有看到,编译器会自动找寻一个变量声明。提升成定义。
- 如果 变量声明前,添加了 extern 关键字,无法提升!
变量的注意事项
- 只能存一个值
- 变量名不允许重复定义
- 一条语句可以定义多个变量
- 变量在使用之前一定要进行赋值
- 变量的作用域范围
常量
程序运行的过程中,其值永远不会发生改变、不能被修改的的数据
常量的分类
实型常量细节:
- 小数点前后,如果只有0,可以省略不写
0.93 可以写成 .93
18.0 可以写成 18.
- 科学计数法是实型常量,但是要写E
12340000 可以写成 1.2340000E7
但是写成 1.234 * 10的7次方就错误了,因为这是一个计算过程,不是最终的数字
- “hello”、‘A’、57、-10、 3.1415926(浮点常量)
- #define PI 3.14 ——【宏定义】语法:#define 宏名 宏值
- 强调:没有 “;” 结束标记。没有 “=”
- const int a = 10; 定义语法: const 类型名 变量名 = 变量值
- const 关键字: 被该关键字修饰的变量,表示为只读变量。
练习:
#define PI 3.1415926 // 定义常量
int main(void)
{
// 圆的面积 :s = PI * r * r
// 圆的周长 :L = 2 * PI * r
int r = 3; // 变量的定义。
float s = PI * r * r; // 表达式。作为变量值。
float l = 2 * PI * r;
//printf("圆的面积:%f\n", s);// 28.274334 默认显示 6 位小数。
//printf("圆的周长:%f\n", l);// 18.849556
//printf("圆的面积:%.2f\n", s);// 28.27
//printf("圆的周长:%.2f\n", l);// 18.85指定保留小数点后两位,对第3位四舍五入
float m = 3.4567891;
printf("m=%6.2f\n", m);
//共显示6位数,包含小数点,保留小数点后两位,对第3位四舍五入,不足6位用空格补齐。
printf("m=%06.2f\n", m);
//共显示6位数,包含小数点,保留小数点后两位,对第3位四舍五入,不足6位用0补齐。
return 0;
}
给你一组数据,说出下面每组数据中,分别是哪种类型的常量
0 :整形常量
0.0 :实型常量
-25 :整形常量
123 :整形常量
17.0:实型常量
‘’ :语法错误
‘ ’ :字符常量
‘a’ :字符常量
‘!’ :字符常量
‘#’ :字符常量
‘ab’:语法错误
‘题’:语法错误
‘¥’:语法错误
“” :字符串常量
“你好”:字符串常量
- 选择题:
1. 以下选项中正确的整型常量是:B
A) 12. B) -20 C) 1,000 D) 4 5 6
- 选择题:
2.以下选项中正确的实型常量是:D
A) 0 B) -31 C) 0.329×102 D) .871
- 选择题
3.以下选项中不正确的实型常量是:B
A) 2.607E-1 B) 0.8103e 2 C) -77.77 D) 456e-2
- 选择题
4. 想要进行数学计算,下面正确的写法是: C
A)“12”+ 13 B)“23”+“13” C) 13 + 23 D)“1.2”+ 3.7
标识符
变量和常量总称
硬性要求
- 不是关键字、函数名。 system、printf、int、main、return。。。
- 只能有 字母、数字、下划线组成。 a-z/A-Z/0-9 _
- abc_1/ abc_2 / _abc_1 / a_b_c_d
- 不能以数字开头。
- int a5ir = 10; ok
- int _34F = 6; ok
- float 98ti_54 = 5.4; error;
- 大小写严格区分。
- 通常 使用大写来定义常量。 #define MAX 100
- 通常 使用小写来定义变量。
- 变量名:全部小写
- 文件名:全部小写,单词之间用下划线隔开
命名规范
用英文单词,见名知意
- 大驼峰法:int HelloWorldHahaHohoHehe = 10;
- 多个单词组成变量名,每个单词首字母大写。
- 小驼峰法:int helloWorldHahaHohoHehe = 10;
- 多个单词组成变量名,首个单词的首字母小写,其余每个单词首字母大写。
- 小写+下划线:int hello_world_haha_hoho_hehe = 10;
- C 语言专用!
sizeof关键字
- sizeof 不是函数
- 求取变量、数据类型,占用的内存空间大小
- 查看 sizeof 返回的占用内存大小,需要使用 %llu 格式符或者%zu格式符
- 用法:
- sizeof(变量名) —— 返回变量大小,单位整数字节。
- sizeof(类型名) ——返回 数据类型大小, 单位整数字节。
- sizeof 变量名 —— 语法C语言支持该写法,不推荐使用。
整型
有符号整型
下表中列出的占用字节数和取值范围,是大多数情况下各种类型的取值。由于C 标准没有具体规定以上各类数据所占用的字节数。因此,在不同系统、编译器下,数据类型占用的字节数会有所不同。
比如:int 类型,在 Turbo C 环境占 2 字节,取值范围与 short 相同。 而在 Visual C 环境下是 4 字节。
再如:long 类型,相同的 gcc 编译器下,在 Windows 系统中占 4 字节,而在 Linux 系统中占 8 字节。
可以使用 sizeof 查看 数据类型 占用内存的大小。
可以引入头文件 #include <limits.h> 借助宏来查看 数据类型 在当前平台上 对应的最小、最大值。
- 获取数据类型的最小值,最大值,可用
#include <limits.h>
整型 | 名称 | 格式匹配符 | 占用的大小 | 数据范围 | 最小值宏 | 最大值宏 |
---|---|---|---|---|---|---|
short | 短整型 | %hd | 2字节 | -215 ~ 215-1 (-32768 ~ 32767) | SHRT_MIN | SHRT_MAX |
int | 整型 | %d | 4字节 | -231 ~ 231-1 (-2147483648 ~ 2147483647) | INT_MIN | INT_MAX |
long | 长整型 | %ld | windows:32/6位:4字节 Linux下:32位:4字节、64位:8字节。 | -231 ~ 231-1 (-2147483648 ~ 2147483647) | LONG_MIN | LONG_MAX |
long long | 长长整型 | %lld | 8字节 | -263 ~ 263-1 | LLONG_MIN | LLONG_MAX |
#include <stdio.h>
#include <limits.h>
int main(void)
{
printf("int大小 = %u\n", sizeof(int));
printf("int最小值:%d, 最大值:%d\n", INT_MIN, INT_MAX);
printf("short大小 = %u\n", sizeof(short));
printf("short最小值:%hd, 最大值:%hd\n", SHRT_MIN, SHRT_MAX);
printf("long大小 = %u\n", sizeof(long));
printf("long最小值:%ld, 最大值:%ld\n", LONG_MIN, LONG_MAX);
printf("long long大小 = %u\n", sizeof(long long));
printf("long long最小值:%lld, 最大值:%lld\n", LLONG_MIN, LLONG_MAX);
return 0;
}
无符号整型
整型名 | 名称 | 格式匹配符 | 占用的大小 | 数据范围 | 最小值宏 | 最大值宏 |
---|---|---|---|---|---|---|
unsigned short | 无符号短整型 | %hu | 同 short | 0 ~ 216-1 (0 ~ 65535) | 0 | USHRT_MAX |
unsigned int | 无符号整型 | %u | 同 int | 0 ~ 232-1 (0 ~ 4294967295) | 0 | UINT_MAX |
unsigned long | 无符号长整型 | %lu | 同 long | 0 ~ 232-1 (0 ~ 4294967295) | 0 | ULONG_MAX |
unsigned long long | 无符号长长整型 | %llu | 同 long long | 0 ~ 264-1 | 0 | ULLONG_MAX |
输出常量
利用printf集合占位符(格式匹配符)的方式可以输出常量
一个printf中可以同时输出多个数据,占位符和后面的数据要一一对应
格式:
printf(参数1,参数2);
参数1:输出内容的最终样式,以字符串的形式体现 (必填)
参数2:填补的内容 (选填)
占位符:
占位符 | 说明 |
---|---|
整形 | %d |
实型 (小数) | %f |
字符 | %c |
字符串 | %s |
char类型
- 字符型。其本质依然是存储数值,因此与数值型类似,也有有符号、无符号之分。
类型 | 字节数 | 格式符 | 数据范围 | 最小值宏 | 最大值宏 |
---|---|---|---|---|---|
char | 1(8 bit位) | %hhd | -27 ~ 27-1 (-128 ~ 127) | CHAR_MIN | CHAR_MAX |
unsigned char | 1(8 bit位) | %hhu | 0~28-1 (0 ~ 255) | 0 | UCHAR_MAX |
- 字符型:%c
#include <stdio.h>
#include <limits.h>
int main(void)
{
// 获取无符号数取值范围
printf("char 无符号 min = 0,max = %hhu\n", UCHAR_MAX);
// 获取有符号数取值范围
printf("char 有符号 min = %hhd,max = %hhd\n", CHAR_MIN, CHAR_MAX);
// 获取 char 占用的字节数 1
printf("char 大小 = %u\n", sizeof(char));
// 获取 unsigned char 占用的字节数 1
printf("unsigned char 大小 = %u\n", sizeof(unsigned char));
return 0;
}
ASCII码
ASCII 码 (American Standard Code for Information Interchange) 美国信息交换标准代码。char 类型数据,数值 对应一个 ASCII 码。
ASCII表说明
- 0 ~ 32 ASCII码 对应的字符都不可见。从键盘键入的所有内容都是字符。如,键入数字 7,实际是字符 ‘7’,真正存储在计算机内的是 55(字符 7 的 ASCII 码值),而如果我们键入了 35,实际上这是两个字符。真正存储在计算机内的是 51 和 53(字符 3 和 字符 5 的 ASCII 码值)。
- 用 printf 结合 格式符 %c,显示字符。 如果用 %d,就显示 其 ASCII 值了。
- 常用的 ASCII码:
字符 | ASCII 码 | 字符 | ASCII 码 |
---|---|---|---|
空('\0') | 0 | ‘\n’ | 10 |
空格 | 32 | 0 | 48 |
A | 65 | a | 97 |
‘\t’ 制表符tab键对应字符 | 9 |
ASCII值 | 控制字符 | ASCII值 | 字符 | ASCII值 | 字符 | ASCII值 | 字符 |
---|---|---|---|---|---|---|---|
0 | NUL | 32 | (space) | 64 | @ | 96 | 、 |
1 | SOH | 33 | ! | 65 | A | 97 | a |
2 | STX | 34 | " | 66 | B | 98 | b |
3 | ETX | 35 | # | 67 | C | 99 | c |
4 | EOT | 36 | $ | 68 | D | 100 | d |
5 | ENQ | 37 | % | 69 | E | 101 | e |
6 | ACK | 38 | & | 70 | F | 102 | f |
7 | BEL | 39 | ‘ | 71 | G | 103 | g |
8 | BS | 40 | ( | 72 | H | 104 | h |
9 | HT | 41 | ) | 73 | I | 105 | i |
10 | LF | 42 | * | 74 | J | 106 | j |
11 | VT | 43 | + | 75 | K | 107 | k |
12 | FF | 44 | , | 76 | L | 108 | l |
13 | CR | 45 | - | 77 | M | 109 | m |
14 | SO | 46 | . | 78 | N | 110 | n |
15 | SI | 47 | / | 79 | O | 111 | o |
16 | DLE | 48 | 0 | 80 | P | 112 | p |
17 | DCI | 49 | 1 | 81 | Q | 113 | q |
18 | DC2 | 50 | 2 | 82 | R | 114 | r |
19 | DC3 | 51 | 3 | 83 | S | 115 | s |
20 | DC4 | 52 | 4 | 84 | T | 116 | t |
21 | NAK | 53 | 5 | 85 | U | 117 | u |
22 | SYN | 54 | 6 | 86 | V | 118 | v |
23 | TB | 55 | 7 | 87 | W | 119 | w |
24 | CAN | 56 | 8 | 88 | X | 120 | x |
25 | EM | 57 | 9 | 89 | Y | 121 | y |
26 | SUB | 58 | : | 90 | Z | 122 | z |
27 | ESC | 59 | ; | 91 | [ | 123 | { |
28 | FS | 60 | < | 92 | \ | 124 | | |
29 | GS | 61 | = | 93 | ] | 125 | } |
30 | RS | 62 | > | 94 | ^ | 126 | ~ |
31 | US | 63 | ? | 95 | _ | 127 | DEL |
#include <stdio.h>
int main(void)
{
char ch = 'A'; // 定义变量 ch, 指定初值为 'A';
printf("ch = %c\n", ch); // c: character %c 用来显示字符的 格式匹配符。
ch = 'm'; // 给变量ch 赋值成 'm', 覆盖 原来的 'A';
printf("ch = %c\n", ch);
ch = 97; // 使用 范围内的数值 97 ,给 ch 赋值。
printf("ch = %c\n", ch); // 将数值97,按照字符格式打印输出。
ch = 98; // 使用 范围内的数值 98 ,给 ch 赋值。
printf("ch = %c\n", ch); // 将数值98,按照字符格式打印输出。
return 0;
}
将 大写字母,转换成 小写字母。
#include <stdio.h>
int main(void)
{
char ch = 'R'; // char 变量定义
printf("R 转换的小写为:%c\n", ch+32); //ch+32 表达式,对应格式匹配符 %c
char ch2 = 'h'; // char 变量定义
printf("h 转换的大写为:%c\n", ch2-32); //ch2-32 表达式,对应格式匹配符 %c
char ch3 = '5';
// 借助字符5, 利用 ASCII特性,打印出 字符9
printf(" 打印字符9 = %c\n", ch3+4);
return 0;
}
转义字符
‘/’: 自右向左划, 叫做 “斜杠”。
‘\’ : 自左向右划,叫做 “反斜杠” 。 ——是 转义字符。
转义字符的作用:
- 将普通字符,转换为 特殊意。
- 如: ‘\n’ : 这是一个字符。代表换行。(回车键)
- 如:‘\t’ : 这是一个字符。代表 一个制表符。(Tab键)
- ‘\b’ 同样是 1 个字符。 代表 退格符(Backspace 键)。
- ‘\0’ 是一个字符。代表 空。对应 ASCII 值 0。
- ‘\ddd’ 是一个字符。ddd 对应 3 个八进制数(没有用 0 开头)。 如:\141 对应 ASCII 值为 97,代表 ‘a’。
- ‘\xhh’ 是一个字符。x 表十六进制,hh 对应 2 个十六进制数。 如:\x41 对应 ASCII 值为 65,代表 ‘A’。
- 将特殊字符,还原成本身意。
如: \n : 这样就将 一个字符 ‘\n’,还原成 两个字符:‘\’ 和 ‘n’
\
代表 单引号。\\
代表 反斜杠。
//一个printf中打印 hello (一个tab缩进) world 换行
int main(void)
{
char ch1 = '\t'; // 实现 一个tab缩进
char ch2 = '\n'; // 实现 一个换行。
printf("hello%cworld%c", ch1, ch2);
return 0;
}
//将 特殊字符转换成本身意。
//屏幕上严格输出 如下内容:【 ‘\n’ 的值是 10】 要求显示时要有 ‘ ’
int main(void)
{
printf("'\\n'的值是 %hhd", '\n');
return 0;
}
实型(浮点型)【了解】
- 显示小数
- float : 单精度浮点型。 %f 大小:4字节。( 可以使用 sizeof() 求取 )
- 4.35 默认会被编译器理解为 double 类型。
- float v = 4.567f; 编译器理解 float。
- %f 格式匹配符,默认保留6位小数。
- double: 双精度浮点型。%lf 大小:8字节。double d = 5.68;
- 取值范围
//使用头文件 #include <float.h> 获取浮点型取值范围。
int main(void)
{
printf("float 范围:%f ~ %f\n", FLT_MIN, FLT_MAX);
printf("double 范围:%lf ~ %lf\n", DBL_MIN, DBL_MAX);
float f = 1.23456789101112131415f; // 定义有 20 位小数的 float 变量
double d = 1.23456789101112131415; // 定义有 20 位小数的 double 变量
printf("f=%.20f\n", f);
printf("d=%.20lf\n", d);
return 0;
}
精度问题
float类型:
- 精度 6~7 位:
- 整数部分 + 小数部分 <= 6 位, 准确。
- 整数部分 + 小数部分 == 7 位,可能准确,也可能不准确。
- 整数部分 + 小数部分 > 7位。不准确。
- 精度 6~7 位:
double类型
- 精度 15~16 位:
- 整数部分 + 小数部分 <= 15 位, 准确。
- 整数部分 + 小数部分 == 16 位,可能准确,也可能不准确。
- 整数部分 + 小数部分 > 16位。不准确。
- 精度 15~16 位:
不同平台(操作系统)的float、double 类型实现的精度有可能不同。以上是Windows特性
float 和 double 不存在 “无符号”类型。
显示小数时,%f 和 %lf 默认保留 6 位小数。
如需指定小数位数,使用格式符 %.nf ,n 为几,表示精确到小数点后几位,会对 n+1 位做 4 舍 5 入。
bool 类型
C语言原来没有 bool 类型。 C99标准中,新增了 bool 类型。C++ 自带 bool类型。
表示 :好、坏;真、假;对、错;是、否
取值:true —— 真 —— 1、false —— 假 —— 0
C语言使用bool 的条件:1. 编译器要支持c99标准。2. 导入 #include <stdbool.h>
bool 类型的大小。1字节。 ( sizeof() 求取 )
bool 没有专用的格式匹配符。 打印时,使用 %d 来打印。
#include <stdbool.h>
int main(void)
{
bool aa = true; // 定义bool 类型变量 aa , 初值为 true == 1
printf("aa = %d\n", aa);
aa = false; // 给 bool 类型变量 aa 赋值为 false == 0
printf("aa = %d\n", aa);
return 0;
}
进制和转换
- 计算机 只 使用 2 机制。
- 8进制、10进制、16进制。统统都是给人类用的!!!
- 存储知识
- 1 bit位 就是一个 二进制位。 存 0 或 1
- 一个字节(Byte) 1B = 8bit位。
- “内存单元” 是计算机内存存储的最小单位, 一个内存单元 == 1字节。
- 1KB = 1024B
- 1MB = 1024KB
- 1GB = 1024MB
- 1TB = 1024GB
8进制
逢 8 进 1。 借1当8。
取值: 0/1/2/3/4/5/6/7 最大值 7
格式匹配符:%o 和 %#o
表示语法: 0开头 。
16进制
逢 16 进 1。 借1当16。
取值: 0~9 10-A/a 11-B/b 12-C/c 13-D/d 14-E/e 15-F/f 最大值: F/f
格式匹配符:%x 或 %#x
表示语法: 0x开头 。
小结
使用频率最高的是 10 进制。 次高的:16进制。
int m = 0x15F4;
int n = 0345;
int k = 0101101;
- 不能直接将变量值赋值为 2进制。
- 上述的赋值,会被编译器理解为 8进制数。
#include <stdio.h>
int main(void)
{
int a = 56; // 10进制数作a变量初始化。 —— 定义。
printf("10进制:a = %d\n", a);
printf("8进制:a = %o\n", a);
printf("8进制:a = %#o\n", a); //在%和x中间添加#,可以在输出时,显示8进制前缀。
printf("16进制:a = %x\n", a);
printf("16进制:a = %#x\n", a); //在%和x中间添加#,可以在输出时,显示16进制前缀。
return 0;
}
常用的格式匹配符
%d、%c、%x、%u、%s (打印字符串)、%o、%#o、%#x、%hhd、%hd、%ld、%lld、%hhu、%hu、%lu、%llu、%f、%lf
编码和存储
无符号存储
unsigned int a = 12; // 占用 4 字节 32 个 bit位存储。
空间:00000000 00000000 00000000 00000000 ———— 4 字节。
存储:00000000 00000000 00000000 00001100
unsigned short b = 15; //占用 2 字节 16 个 bit位存储。
空间:00000000 00000000 ———— 2 字节。
存储:00000000 00001111
有符号存储
- 拿出一个二进制位存储符号。标识正、负。
选用 最高位 为符号位。正:0、负:1
有符号正数
// 采用 “源码” 存储
int a = 5;
空间:0 0000000 00000000 00000000 00000000
存储:0 0000000 00000000 00000000 00000101 --- 表示存储正数 5 。
有符号负数
- 有符号的负数,采用 “补码” 存储
- 源码:数值的二进制位直接存储。
- 反码:符号位不变,将其余数值为取反。
- 补码:反码+1。
int b = -33;
空间:0 0000000 00000000 00000000 00000000
源码:1 0000000 00000000 00000000 00100001
反码:1 1111111 11111111 11111111 11011110
补码:1 1111111 11111111 11111111 11011111 —— 负33 在计算机中实际的存储形式。
- 小结:
- int —— 4字节 —— 32bit位
- 有符号:31个数值位。 取值范围 -2^31~ 2^31-1 ——> -2147483648 ~ +2147483647
- 无符号:32个数值位。取值范围 0 ~ 2^32-1 ——> 0 ~ 4294967295
- short —— 2字节 —— 16bit位
- 有符号:15个数值位。 取值范围 -2^15 ~ 2^15-1 ——>
- 无符号:16个数值位。取值范围 0 ~ 2^16-1 ——> 0 ~ 65535
- int —— 4字节 —— 32bit位
- 因此,知道数据类型占用内存的大小,就能算出该类型 无符号数、有符号数 对应的取值范围。
数据溢出
- 如果 int 类型变量,已经取最大值(2147483647),再给这个变量 +1。就发了溢出。
上溢出:最大值+1
下溢出:最小值-1
无符号数
取值范围 0 ~ 2^32-1
最大值 + 1 ——> 0
0 -1 ——> 最大值。
// 上溢出
int main(void)
{
unsigned int a = UINT_MAX; // 取出无符号最大值
printf("最大值+1 = %d\n", a + 1); // 0
printf("最大值+2 = %d\n", a + 2); // 1
return 0;
}
//下溢出。
int main(void)
{
unsigned int a = 0://取出无符号最小值
printf("最小值-1 = %u,最大值 = %u\n",a - 1,UINT_MAX);
return 0;
}
有符号数
- 上溢出:最大值+1,上溢出 == 最小值。
int main(void)
{
int a = INT_MAX; // 取出无符号最小值
printf("最大值+1 = %d, 最小值 = %d\n", a+1, INT_MIN);
return 0;
}
- 下溢出:最小值-1,下溢出 == 最大值。
// signed 下溢出。
int main(void)
{
int a= INT_MIN;// 取出有符号最小值
printf("最小值-1 = %d,最大值 = %d\n",a-1, INT_MAX);
return 0;
}
小结
大部分编译器,对于数值溢出,采用上述处理方式。但个别编译器有可能不能。
受数值溢出影响,数据存储时要选择恰当的数据类型。389482390483209 --- int
不常用关键字(了解)
- extern:表示声明。声明没有内存空。不能提升为定义。
- const:限制一个变量为只读。—— 常量。
- Volatile:防止编译优化。
- register:定义一个寄存器变量。 没有内存地址。
输入输出函数
输出函数
- printf(); %d、%c、%x、%u、%s ....
- putchar() 函数:输出一个字符到屏幕。将 ch 对应的字符,输出到屏幕。返回实际写出的字符对应的ASCII值
putchar(97); // 97 == 'a'
putchar('\n'); // 输出换行符。
putchar('b');
putchar('\n'); // 使用评率高。 printf("\n");
putchar('abcZ'); //‘abcZ’是错误的定义,既不是 char 也不是字符串。
输入函数
scanf 函数
从标准输入设备(键盘)上按格式获取数据。
安装指定个格式匹配符,获取指定类型数据。
- 获取整数
//scanf("格式控制字符串“, ......);
参 1: 必须是字符串,且必须包含 格式说明符(占位符)
格式符 与 数据类型中使用方式一致。 常用的有 %d、%c、%f、%lf 等。
后续参数(变参):个数,直接受 参 1 格式匹配符影响。
对应 格式符的 必须是 变量地址(&变量名,可取变量地址),代表数据存放位置。
int a; // 可以定义a变量(有内存空间),也可以声明(自动提升成定义,有内存空间)
scanf("%d", &a); // &: 取变量a的地址 ——> 拿到a的内存空间。
// scanf 输入
int main(void)
{
//int a = 10;
int a; // 在scanf执行时,会提示为定义。
int ret = scanf("%d", &a); //scanf可以从键盘获取用户输入,用户根据%d 输入整数。
printf("获取的a为:%d\n", a);//\n换行
return EXIT_SUCCESS;
}
// 一次性获取多个整数
int main(void)
{
int a, b, c; // 一次性创建多个 int 类型变量。
// scanf 可以从键盘获取用户输入, 用户根据 多个%d 输入整数。
int ret = scanf("%d %d %d", &a, &b, &c);
printf("获取的a为:%d\n", a);
printf("获取的b为:%d\n", b);
printf("获取的c为:%d\n", c);
return EXIT_SUCCESS;
}
- 获取字符
int main(void)
{
char ch1, ch2, ch3; // 一定性创建多个 char 变量
printf("请输入3个字符,用空格隔分:");
scanf("%c %c %c", &ch1, &ch2, &ch3);
printf("ch1 = %c\n", ch1);
printf("ch2 = %c\n", ch2);
printf("ch3 = %c\n", ch3);
return EXIT_SUCCESS;
}
注意事项:
不要随意在scanf() 函数中添加 ‘\n’ 。 如果添加了换行符,会被scanf当成格式来约束用户输入
- printf() 中的 ‘\n’ 用来向屏幕输出 换行。
- scanf() 函数中的 ‘\n’,不能用来输出。因为这是一个输入函数。
解决 VS中使用 scanf函数 报 C4996 错误:
以下是错误描述
C4996 'scanf': This function or variable may be unsafe. Consider using scanf_s instead.
To disable deprecation, use _CRT_SECURE_NO_WARNINGS. See online help for details.
day04 C:\itcast\VSsource\day04\02-输出输出函数.c 43
解决方法:
- 在 .c 文件的 第一行 添加 #define _CRT_SECURE_NO_WARNINGS
- 在项目中设置:工程名→右键→属性→C/C++→预处理器→预处理器定义→编辑→将 _CRT_SECURE_NO_WARNINGS 加入“预处理定义” 中
键入数据时,数据个数、类型、顺序,必须与参 1 中占位符一一对应。
键入数据时,数据间的分割符,必须与 参 1 中 占位符的分割符一致。
getchar 函数
- 直接从键盘接收 一个字符, 并将得到的字符对应的ASCII返回。如用户键入多个字符,需要多次调用 getchar()读取。函数调用完成,会返回实际读到的字符 对应的 ASCII 值。用户键入字符结束,敲的回车键,对应‘\n’ 字符,也可以使用 getchar() 读取。
int main(void)
{
char ch = getchar(); // 定义char 变量 ch,接收 getchar() 函数返回值做为初值。
printf("ch的数值:%d\n", ch); // %hhd %hd
printf("ch的字符:%c\n", ch);
return EXIT_SUCCESS;
}
运算符
算数运算符
+ - * /
: 先 乘除取余,后加减。- 除法运算后,得到的结果赋值给整型变量,取整数部分(舍小数部分)。 int c = 10/20; ---》0 19 / 10=1
- 除 0 :错误操作,不允许。 printf("%d\n", 10/0);
- 对0取余:错误操作,不允许。 printf("%d\n", 123 % 0);
- 不允许对小数取模。运算数必须是整型。 35 % 3.4;
- 对负数取余,结果为余数的绝对值。 printf("%d\n", 10 % -3); ---> 1
- 取模% 10%3=1
- 两浮点数相除,结果 double 浮点数
- 整数和实数相除,自动类型转换为实型
自增自减运算符
- 前缀自增(++)、自减(--):先自增、自减,再取值。
- 后缀自增(++)、自减(--):先取值,再自增、自减。
- 只适用变量,不能用于常量、表达式。
赋值运算符
- “=”, 在计算机中,只能完成 “赋值” 操作,一定是右边赋值给左边,也叫单向赋值符。
- a += 10 // 等价于 a = a+10;
- a -= 30 // 等价于 a = a-30;
- a %= 5; // 等价于 a = a%5;
比较运算符
- 真:1(非0), 假:0
- == 判等符 ( = 不是用来判等)
- != 不等
- < 小于
- <= 小于等于
>
大于>=
大于等于- 【强调】:数学运算 中
13 < var < 16
判断, 在计算机中,要写成:var > 13 && var < 16;
逻辑运算符
- 0 为假, 非0 为真。(非0: 1、 27、-9)
- 逻辑非(!):非真为假,非假为真。
- 逻辑与(&&):同真为真,其余为假。
- 逻辑或(||):有真为真,同假为假。
运算符优先级
[] () > ++/--(后缀高于前缀) (强转) ! (逻辑非)sizeof > 算数运算符(先乘除取余,后加减) > 比较运算符 > 逻辑运算符 > 三目运算 (条件运算)> 赋值运算符 > 逗号运算符。
逗号运算符
含有“,”运算符表达式运算结果,是最后一个子表达式的结果。
int x, y, z;
int a = (x=1, y=2, z=3); // x=1, y=2, z=3 是一个逗号运算符表达式。运算结果为 a =3;
int a = (x=1, z=3, y=27); // 运算结果为 a = 27;
三目运算符
语法: 表达式1 ? 表达式2 :表达式3
表达式1 必须是一个判断表达式。
结果为真:整个三目运算,返回表达式2
结果为假:整个三目运算,返回表达式3
如果不使用(),三目运算默认的结合性,自右向左。
类型转换
隐式类型转换
- 编译器自动完成。 (小类型转大类型,同类型大小)

- 由赋值产生。
int r = 3;
float s = 3.14 * r * r;
// 3.14 默认类型double, r 为int类型。运算过程中,转换为 double 类型运算。
// 运算结束,赋值给 s 时,转换为 float
- 小类型 ——> 大类型: 没问题。
- 大类型 ——> 小类型。可能丢失数据。
- VS中 Ctrl+F7 只编译,检查语法错误,不运行。
int main(void)
{
int a = 321;
char ch = a; // 用值为 321 的a变量,给 char 类型赋值。
printf("ch = %d\n", ch); // 运行输出 65;
return EXIT_SUCCESS;
}
321: 2^8 = 256 有, 128 没有, 64 有, 32/16/8/4/2 没有, 有1
0000 0000 0000 0000 0000 0001 0100 0001 —— 321 二进制表现形式。
0000 0000 —— char 只有一个字节。
赋值后,char值为:0100 0001 --- 1 + 64 = 65
强制类型转换
- 语法:
- 强转变量: (目标类型)变量
- 强转表达式:(目标类型)表达式
int main(void)
{
float price = 3.6; // 单价
int weight = 4; // 斤数
//int sum = weight * (int)price; // 强转变量。
//printf("sum = %d\n", sum); // --- 12
int sum = (int)(weight * price); // 强转表达式。
printf("sum = %d\n", sum); // --- 14
return EXIT_SUCCESS;
}
if 分支语句
- 分支语句中,可以嵌套其他分支语句
- else 总是找它前面最近的 未配对的 if 组合使用。
- if ... else 分支语句,实现一种模糊匹配。匹配一个范围。
if (判断表达式){
判别表达式为真,执行的代码。
}
else
{
判别表达式为假,执行的代码。
}
- 多个分支逻辑
if(判断表达式1){
判别表达式1为真,执行的代码。
}
else if (判断表达式2)
{
判别表达式1为假,判断表达式2为真,执行的代码。
}
else if (判断表达式3)
{
判别表达式1为假,判断表达式2为假,判断表达式3为真,执行的代码。
}
。。。
else
{
以上所有判别表达式都为假,执行的代码。
}
switch 分支语句
精确匹配。 结构较清晰。较 if 语句执行效率略高。
缺点:不能直接判断区间,需要借助表达式运算。
switch(表达式)
{
case 1:
执行语句;
break; // 表示一个分支执行结束。跳出 switch。
case 2:
执行语句;
break; // 表示一个分支执行结束。跳出 switch。
......
case N:
case N+1://case穿透
执行语句;
break; // 表示一个分支执行结束。跳出 switch。
default:
其他情况,执行语句;(上述所有的case都不满足)
break;
}
case穿透
一个case分支,如果没有 break;它执行完本case的代码后,会继续执行下一个case 分支代码
大多数情况下,一个case分支对应一个 break;
switch (score/10)
{
case 10: // 故意让 switch 发生 case穿透
case 9:
printf("优秀\n");
break;
case 8: // 故意让 switch 发生 case穿透
case 7:
printf("良好\n");
break;
case 6:
printf("及格\n");
break;
default: // 所有case都不满足的其他情况。
printf("不及格");
break;
}
while 循环语句
语法
while (判别表达式) // 如果为真,执行循环体,如果为假,跳出循环。
{
循环体
条件控制语句
}
敲7:从 1~ 100 数数,逢 7 和 7 的倍数,敲桌子。
分析:
- 7 的倍数:num % 7 == 0
- 个位含7: num % 10 == 7
- 十位含7: num / 10 == 7
int main(void)
{
int num = 1;
while (num <= 100){
if (num % 7 == 0 || num % 10 == 7 || num / 10 == 7)
{
printf("敲桌子!\n");
}
else
{
printf("%d\n", num);
}
num++;
}
return EXIT_SUCCESS;
}
do while 循环语句
无论如何先执行一次循环体, 然后再判断循环是否应该继续。
语法:
do {
循环体
} while (判断表达式);
练习:
求 水仙花数。 一个三位数(100~999),各个位上数字的立方 和 等于本数字。
分析:
3位数:100 ~ 999 —— 如: 234、861
个位数:int a = num % 10;
十位数:int b = num / 10 % 10;
百位数:int c = num / 100;
int main(void)
{
int num = 100; // 数数从 100 开始。
int a, b, c; // 定义存储个位、十位、百位数 的变量。
do {
a = num % 10;
b = num / 10 % 10;
c = num / 100;
// 判断 这个数字是否是“水仙花数”
if (a * a * a + b * b * b + c * c * c == num)
{
printf("水仙花数:%d\n", num);
}
num++; // 不断向后数数。
} while (num <= 999);
return EXIT_SUCCESS;
}
配置 VS2019 快捷导入代码
准备 快捷导入代码的脚本文件,保存在系统目录中(位置自定义)
在 VS2019 中配置,使用上述目录中的 脚本文件。
工具 —— 代码片段管理器 —— 修改 Basic 为 Visual C++ —— 选择 上述自定义的目录位置
(不需要选择到具体脚本文件)。
在程序中使用 快捷导入代码。
#1 ---- tab 键。
for 循环
for (①初始化语句;②条件判断语句;③条件控制语句)
{
④循环体。
}
- 执行流程:1243243
① 执行初始化语句
② 执行条件判断语句,看其结果是否成立
成立:执行循环体语句
不成立:结束循环
③执行条件控制语句
④回到②继续执行条件判断语句
使用 for 循环 求 1~100 的 和。
// 1~100 求和 1+2+3+4+5+。。。+100
int main(void)
{
// 定义循环因子。
int i = 0; // 定义i,给初值。
// 定义变量 记录累加值
int sum = 0; // 初值为 0
for (i = 1; i <= 100; i++)
{
sum = sum + i;
}
// 循环结束,打印 累加结果
printf("sum = %d\n", sum);
return EXIT_SUCCESS;
}
- 循环因子 i:
在 for 循环之前定义。 在 for 循环,结束后依然能使用。
定义 在for 循环之内。 for 循环结束后,不能使用!
for (int i = 1; i <= 100; i++) // 将 i 的定义放到 for 内 表达式1上。
{
sum = sum + i;
}
// 循环结束,打印 累加结果
printf("sum = %d, i = %d\n", sum, i); // 编译器保存,“未定义标识符”
- for 循环 3 个表达式,在使用时,均可省略。 但,2 个 “ ; ” 不允许省略。
- 省略 表达式1
int i = 1; // 定义 循环因子
int sum = 0;
for (; i <= 100; i++) // 不写表达式1
{
sum = sum + i;
}
- 省略 表达式3
int i = 1; // 定义 循环因子
int sum = 0;
for ( ; i <= 100; ) // 不写表达式1, 不写表达式3
{
sum = sum + i;
i++; // 将原来的表达式3写到循环体中。
}
- 省略 表达式2
int i = 1; // 定义 循环因子
for ( ; ; ) // 不写表达式2, 相当于 for ( ; 1 ; )表达式2为 真(1)。 这是一个死循环。
{
printf("i = %d\n", i);
i++;
}
// for ( ; ; ) 死循环 相当于 while(1) {};
- for 每个表达式中, 可以含有多个算式。
int i = 0; // 定义 循环因子
int a = 0;
for (i=1, a=3; a<20, i<10; i++, a+=3) // i<10, a <20 也可以写成 i<10 && a<20
{
printf("i = %d\n", i);
printf("a = %d\n", a);
}
- 死循环
// 方法1:
for(;;){}
// 方法2:
while(1){}
// 方法3:
do{}while(1)
练习:
猜数字游戏:产生一个随机数。用户键盘键入一个数据,程序提示用户,输入的数据 > < == 随机数。用户根据提示不断变换输入,最终猜中!
生成随机数:
- 添加一个随机数种子。作用:保证随机是真正的随机。
srand(time(NULL)); // 固定写法。
// time(NULL): 获取系统当前时间。返回unsigned long long 类型。从1970年1月1日0:8:8 开始过了1709817925秒的那个时间点
// srand() 函数来生成随机数。使用 系统时间为 算法的系数。
- 添加头文件
// srand() --- <stdlib.h>
// time() --- <time.h>
- 生成随机数。
int n = rand() % 100; // 随机数范围: 0 ~ 99
int n = rand() % 100 + 13; // 随机数范围: 13 ~ 112
int n = rand() % 63 + 17; // 随机数范围: 0+17 ~ 62+17
int main(void)
{
// 播种随机数种子
srand(time(NULL));
// 生成随机数 n
int n = rand() % 100; // 范围 0-99
// 定义 num 变量,存储用户输入的数据。
int num;
// 创建 死循环,给用户猜数字。
for (;;) // while(1) 等价
{
printf("请输入猜测的数字:");
// 获取用户输入数据
scanf("%d", &num);
// 提示用户,测试方向
if (num > n)
{
printf("猜大了!\n");
}
// 如果 if分支、for、while 满足后,执行语句只有一条时,{} 可以省略。
else if (num < n)
printf("猜小了!\n"); // 语法允许,不写 {}
else
{
printf("猜中了!!!\n");
break; // 不必再循环。
}
}
printf("本尊数:%d\n", n);
return EXIT_SUCCESS;
}
嵌套 for 循环
- 外层循环执行一次, 内层循环执行 一周。
先确定内循环是做了什么事情外循环其实就是把里面的代码重复执行了N次
int i = 0; // 外层循环的循环因子
int j = 0; // 内层循环的循环因子
for (i = 0; i < 10; i++)
{
for (j = 2; j < 10; j++)
{
// 循环体
}
}
- 模拟电子表打印
int main(void)
{
int i, j, k;
for (i = 0; i < 24; i++) // 小时
{
for (j = 0; j < 60; j++) // 分钟
{
for (k = 0; k < 60; k++) // 秒
{
printf("%02d:%02d:%02d\n", i, j, k);
Sleep(980);
system("cls"); // 清屏
}
}
}
return EXIT_SUCCESS;
}
- 打印正序 9x9 乘法表
\t:制表符 长度可变的大空格,会根据前面字母的个数在后面补空格,让整体的长度达到8或者8的倍数,最少补1个,最多补8个原理:作用:打印表格数据的时候,可以对齐
abc\t 补5个
zhangsan\t 补8个
张\t补6个
1x1= 1 // 第1行, 打印1列
1x2= 2 2x2= 4 // 第2行,打印2列
1x3= 3 2x3= 6 3x3= 9 // 第3行,打印3列
1x4= 4 2x4= 8 3x4=12 4x4=16 // 第4行,打印4列
...
1x9= 9 2x9=18 3x9=27 4x9=36 5x9=45 6x9=54 7x9=63 8x9=72 9x9=81 // 第9行,9列
jxi =
第 i 行,打印 i 列。
for (i = 1; i <= 9; i++) // 外层,描述行。
{
for (j = 1; j <= i; j++) // 内层,描述每一列。
{
printf("%dx%d=%d\t", j, i, i*j);
}
printf("\n");
}
- 打印倒序 9x9 乘法表
1x9= 9 2x9=18 3x9=27 4x9=36 5x9=45 6x9=54 7x9=63 8x9=72 9x9=81 // 行
1x8= 8 2x8=16 3x8=24 4x8=32 5x8=40 6x8=48 7x8=56 8x8=64
1x7= 7 2x7=14 3x7=21 4x7=28 5x7=35 6x7=42 7x7=49
1x6= 6 2x6=12 3x6=18 4x6=24 5x6=30 6x6=36
1x5= 5 2x5=10 3x5=15 4x5=20 5x5=25
1x4= 4 2x4= 8 3x4=12 4x4=16
1x3= 3 2x3= 6 3x3= 9
1x2= 2 2x2= 4
1x1= 1
jxi = 值
for (i = 9; i >= 1; i--) // i 控制 行
{
for (j = 1; j<=i; j++)
printf("%dx%d=%d\t", j, i, i*j);
putchar('\n');
}
// 上述省略{}写法无误, 不推荐。--- 推荐下面的写法。
int main(void)
{
int i, j;
for (i = 9; i >= 1; i--)
{
for (j = 1; j <= i; j++)
{
printf("%dx%d=%d\t", j, i, j * i);
}
putchar('\n'); // printf("\n");
}
return EXIT_SUCCESS;
}
- 求整数n是否是2的幂次方
规则:任意的一个数字,不断的除以2,最终都会得到数字1
结果是1时候,就可以结束了 yes
结果无法被2再次整除了,也可以结束了,no
int main(){
int n = 123;
while(n > 1 && n % 2 == 0){
n = n / 2;
}
return n == 1;
}
- 求1-100内质数的个数
int main(){
//统计从2开始,到number-1.在这个范围之内,有多少个数字能被number整除
int countA = 0;
for(int i=2;i<=100;i++){
int count = 0;
for(int j=2;i<i ; j++){
if(number % j == 0){
//找到了一个数字能被number整除
count++:
//后面其他的数字就没有必要再次判断了
break;//33
}
//对countA进行判断
if(count == 0){
countA++;
}
}
}
prinf("质数个数:%d\", countA);
}
for 和 while 的区别
- for循环中:知道循环次数或者循环的范围
- while循环:不知道循环的次数和范围,只知道循环的结束条件
跳转语句
break
作用1:一次break,可以跳出一重循环。( for、while、do while )
作用2:防止 case 穿透。 结束 switch();
continue
作用:结束 本次 循环。continue关键字之后的代码,在这次循环中,不执行。
int main(void)
{
for (int i = 0; i < 10; i++)
{
for (int j = 0; j < 5; j++)
{
if (j == 2)
{
continue; // 只跳出(后续代码代码不执行)本次 j == 3 时的循环,
}
printf("i = %d, j= %d\n", i, j);
}
printf("\n");
}
return EXIT_SUCCESS;
}
goto
语法:
- 设定一个标签。标签名自定义,一般大写。如:ABC、 LABLE、AAA
- 使用 “goto 标签名” 跳转到标签的位置。(只函数内生效)
- goto 语法过于灵活,会打乱程序的执行逻辑,降低代码的 可读性。 后续编程中,尽量少用。
C 程序中,简单的逻辑可以使用 goto。
int main(void)
{
printf("==========1=========\n");
printf("==========2=========\n");
printf("==========3=========\n");
goto LABLE;
printf("==========4=========\n");
printf("==========5=========\n");
printf("==========6=========\n");
printf("==========7=========\n");
LABLE:
printf("==========8=========\n");
printf("==========9=========\n");
return EXIT_SUCCESS;
}
数组
概念:相同数据类型有序的、连续的存储集合。
%p:打印变量内存地址 格式匹配符 (占位符)
基本特性
- 各个元素,连续存储。
- 数组名为地址,是数组首个元素的地址。
arr == &arr[0]
- 求数组的总大小:
printf("数组的大小:%u\n", sizeof(arr));
- 求数组每一个元素的大小:
printf("数组元素的大小:%u\n", sizeof(arr[0]));
- 求数组元素的个数:
printf("数组元素的个数:%d\n", sizeof(arr)/sizeof(arr[0]));
- 数组第一个元素的下标: 0
- 数组最后一个元素的下标:
sizeof(arr)/sizeof(arr[0]) - 1
- 数组元素:arr[0]
- arr参与计算的时候,会退化为第一个元素的指针,包括赋值
- sizeof 运算的时候,不会退化,arr还是整体
- &arr获取地址的时候,不会退化
- 步长 = 数据类型 * 数组长度
// 1.定义数组
int arr[]={1,2,3,4,5,6,7,8,9,10};
//2.sizeof 运算的时候,不会退化,arr还是整体
printf("%zu\n",sizeof(arr));//40
//3.&arr获取地址的时候,不会退化,记录的内存地址第一个元素的首地址,也是数组的首地址,步长:数据类型 *数组的长度 40
//arr参与计算的时候,会退化为第一个元素的指针,记录的内存地址第一个元素的首地址,也是数组的首地址,步长:数据类型 int 4
printf(“%p\n",arr);
printf("%p\n",&arr);
printf("%p\n",arr +1);//4
printf("%p\n",&arr+1);//40
- 数组初始化
// 初始化方法1
int arr[5] = {3, 7, 2, 1, 9};
// 初始化方法2【多用】
int arr[5] = {3, 7}; // 剩余未初始化的元素,默认值 0
// 初始化方法3【多用】
int arr[5] = {0}; // 初始化一个 全部元素为 0 的数据。 ---清0 常用。
// 初始化方法4【多用】
int arr[] = {3, 7, 2, 1, 6, 9, 13}; // 编译器会自动求取数组元素个数。
// 初始化方法5
int arr[] = {0}; // 定义了只有一个元素的数组,值为 0
// 初始化方法6【多用】
int arr[10]; // 声明了一个有10个元素数组。
arr[0] = 5;
arr[1] = 6;
arr[2] = 7; // 剩余未初始化的元素,默认值 —— 随机数。
- 数组元素逆序
int main(void)
{
int arr[] = {1, 6, 8, 0, 4, 3, 9, 2}; // 变为:{2, 9, 3, 4, 0, 8, 6, 1}
// 获取数组的元素个数
int n = sizeof(arr) / sizeof(arr[0]);
int i = 0; // 从前向后
int j = n-1; // 从后向前
int tmp = 0; // 定义临时变量。
// 交换数组元素之前,打印数组的所有元素。
for (int i = 0; i < n; i++)
{
printf("%d ", arr[i]);
}
putchar('\n');
// 循环交换数组元素。
while (i < j)
{
tmp = arr[i]; // 三杯水变量交换法
arr[i] = arr[j];
arr[j] = tmp;
i++; // 不断后移
j--; // 不断前移
}
// 交换数组元素之后,打印数组的所有元素。
for (int i = 0; i < n; i++)
{
printf("%d ", arr[i]);
}
putchar('\n');
return 0;
}
冒泡排序

int main(void)
{
int i, j, tmp;
int xjp[] = {12, 2, 32, 14, 62, 54, 27, 89, 9, 10, 3};
// 求取数组元素个数。
int n = sizeof(xjp) / sizeof(xjp[0]);
// 开始排序。
for (i = 0; i < n - 1; i++) // 外层控制行
{
for (j = 0; j < n - 1 - i; j++) // 内层控制列。
{
// 相邻两两比较,三杯水交换
if (xjp[j] > xjp[j + 1])
{
tmp = xjp[j];
xjp[j] = xjp[j + 1];
xjp[j + 1] = tmp;
}
}
}
// 打印排序结果。
for (i = 0; i < n; i++)
{
printf("%d ", xjp[i]);
}
printf("\n");
return EXIT_SUCCESS;
}
二维数组
- 定义语法
int arr[行][列] = {数组元素}
int arr[2][3] =
{
{2, 5, 8}, // 第0行
{7, 9, 10} // 第1行
};
// 常规写法:
int arr[3][5] = {{2, 3, 54, 56, 7}, {2, 67, 4, 35, 9}, {1, 4, 9, 3, 78}};
// 第二种写法
1.定义3个一维数组
int arr1[3]={1,2,3};
int arr2[5]={1,2,3,4,5};
int arr3[9]={1,2,3,4,5,6,7,8,9};
//2.把三个一维数组放入到二维数组当中
//数组的数据类型,跟内部存储的元素类型保持一致
int* arr[3]={arr1, arr2,arr3};
// 预先计算每一个数组真实的长度
int len1 = sizeof(arr1)/sizeof(int)
int len2 = sizeof(arr2)/sizeof(int)
int len3 = sizeof(arr3)/sizeof(int)
//再定义一个数组,装所有数组的长度
int lenrr[3]={len1,len2,len3};
//2.把三个一维数组放入到二维数组当中
//数组的数据类型,跟内部存储的元素类型保持一致
// arr1:使用数组名进行计算的时候,退化为指向第一个元素的指针,此时不再表示数组的那个整体了
//指针---内存地址64位win 8个字节
//3.利用索引遍历arr
for(int i = 0;i<3;i++){
//i:依次表示二维数组的索引 0 1 2
for(int j = 0;j< lenArr[i]; j++){
printf("%d ",arr[i][j]);
}
printf("\n");
}
//2.利用指针遍历二维数组
//获取二维数组的指针
// 数组指针的数据类型:要跟数组内部元素的类型保持一致
//二维数组里面存储的是一维数组int[5]
int(*p)[5]= arr;
for(int i = 0;i < 3; i++){
// 遍历一维数组
for(int j = 0;j < 5;j++){
printf("%d,*(*p + j));
} // 换行
printf("\n");
// 移动二维数组的指针,继续遍历下一个-维数组
p++;
}
//1.定义三个-维数组
int arr1[5]={1,2,3,4,5};
int arr2[5]={11,22,33,44,55};
int arr3[5]={111,2222,333,444,555};
//2.把三个一维数组的内存地址,再放入到二维数组当中
int* arr[3]={arr1, arr2, arr3};
//3.获取指针
int** p= arr;
// 4.遍历数组
for(int i= 0;i < 3; i++){
for(intj= ; j< 5; j++){
printf(“%d“,*(*p + j));
}
// 换行
printf("\n");//移动指针
p++;
}
- 打印
// 自动补齐的 for 自带的 size_t 来源:
查看:方法1:右键 —— 转到定义
方法2:F12
typedef unsigned int size_t; // 给 unsigned int 起别名,叫 size_t
-----------------------------
// 打印 2 维数组方法:
int arr[3][5] = { {2, 3, 54, 56, 7}, {2, 67, 4, 35, 9}, {1, 4, 9, 3, 78} };
for (size_t i = 0; i < 3; i++) // 行
{
for (size_t j = 0; j < 5; j++) // 列
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
特性
- 数组大小
printf("数组大小:%u\n", sizeof(arr));
- 一行大小
printf("数组一行大小:%u\n", sizeof(arr[0]));
- 一个元素大小
printf("数组一个元素大小:%u\n", sizeof(arr[0][0]));
- 行数
int row = sizeof(arr) / sizeof(arr[0]); // 数组总大小 / 每行大小
- 列数
int col = sizeof(arr[0]) / sizeof(arr[0][0]); // 一行大小 / 每个元素大小
- 地址合一
数组的地址 == 数组的首元素地址 == 数组的首行地址
printf("%p\n", arr); // 数组的首地址
printf("%p\n", arr[0]); // 数组首行地址
printf("%p\n", &arr[0][0]); // 数组首元素的地址
- 数组大小
初始化
- 常规初始化
int arr[3][5] = { {2, 3, 54, 56, 7}, {2, 67, 4, 35, 9}, {1, 4, 9, 3, 78}};
- 不完全初始化
- 常规初始化
int arr[3][5] = {{2,3}, {2, 67, 4}, {1, 4, 16, 78}}; // 未被初始化的数值,为0
int arr[3][5] = {0}; // 初值全部为0的二维数组
int arr[3][5] = { 2, 3, 4, 5, 6, 7, 8, 9, 99, 2, 16, 78}; //【少见】系统自动分配行列
- 不完全指定行列初始化
int arr[][] = {1, 23, 4, 56, 7, 8}; 【错误】 // 二维数组定义,至少需要指定 列值。
int arr[][2] = { 1, 23, 4, 56, 7, 8, 10}; // 可以不指定行值。
int row = sizeof(arr) / sizeof(arr[0]);
int col = sizeof(arr[0]) / sizeof(arr[0][0]);
for (size_t i = 0; i < row; i++) // 行
{
for (size_t j = 0; j < col; j++) // 列
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
练习:求出5名学生3门功课的总成绩。(总成绩:一个学生的总成绩。一门功课的总成绩)
int main(void)
{
int scores[5][3]; // 5个学生, 3门功课
int row = sizeof(scores) / sizeof(scores[0]);
int col = sizeof(scores[0]) / sizeof(scores[0][0]);
// 获取 5 个学生 3门功课成绩
for (size_t i = 0; i < row; i++)
{
for (size_t j = 0; j < col; j++)
{
scanf("%d", &scores[i][j]);
}
}
// 一门功课的总成绩
for (size_t i = 0; i < col; i++) // 一次取出,每个学生的 一门功课
{
int sum = 0; // 累加每门功课的分数。
for (size_t j = 0; j < row; j++) // 每门功课第几个学生
{
sum += scores[j][i];
}
printf("第%d门功课总成绩:%d\n", i + 1, sum);
}
// 求每个学生的总成绩
for (size_t i = 0; i < row; i++) // 每个学生
{
int sum = 0; // 累加每个学生的成绩。
for (size_t j = 0; j < col; j++) // 每个学生的成绩
{
sum += scores[i][j]; // sum = sum + scores[i][j];
}
printf("第%d个学生的总成绩为:%d\n", i+1, sum);
}
//printf("---------------------------------\n");
//// 打印 5 个学生 3门功课成绩
//for (size_t i = 0; i < row; i++)
//{
// for (size_t j = 0; j < col; j++)
// {
// printf("%d ", scores[i][j]);
// }
// printf("\n");
//}
system("pause");
return EXIT_SUCCESS;
}
多维数组(了解)
三维数组: [层][行][列]
- 语法:
类型名 数组名[层][行][列]
int arr[3][3][4] =
{
{
{{12, 3, 4, 5}}, // 第0行
{{12, 3, 4, 5}}, // 第1行
{{12, 3, 4, 5}} // 第2行
}, // 第0层
{
{}, // 第0行
{}, // 第1行
{} // 第2行
} // 第1层
};
- 打印:
int main(void)
{
int arr[3][4][2] =
{
{
{1, 2},
{3, 4},
{5, 6},
{7, 8}
},
{
{12, 24},
{31, 49},
{5, 46},
{17, 88}
},
{
{122, 24},
{311, 419},
{15, 46},
{17, 188}
}
};
for (size_t i = 0; i < 3; i++) // 层
{
for (size_t j = 0; j < 4; j++) // 行
{
for (size_t k = 0; k < 2; k++) // 列
{
printf("%d ", arr[i][j][k]);
}
printf("\n");
}
printf("\n\n");
}
system("pause");
return EXIT_SUCCESS;
}
int arr[2][3][4] = {1,2,3,4,5,6,7,8,9};
int arr[][3][4] = {1,2,3,4,5,6,7,8,9}; // 层数,可以省略。
数组首地址 == 首层地址 == 首层首行地址 == 首元素地址。
4维、5维、6维、。。。。。N维
int arr[2
][3]short arr[2][3]
float arr[2]
long long arr[2][3][5]
字符串
- 一串字符。C语言中,一定使用 ‘\0’ 结束。
字符数组和字符串的区别
字符数组
char str1[5] = {'h','e','l','l','o'}; // 不是字符串,没有 \0
字符串
char str1[] = "abc";//在底层,实际存储的时候,c语言会把字符串"abc"转换成字符数组进行保存,并且在末尾还要再加上\0{'a','b','c',\0 };
char str2[6] = {'h','e','l','l','o','\0'}; 【麻烦】
char str3[6] = "hello"; // 自动带有 \0 结束标志。
- 内存占用大小的计算方式:
- 英文:1个字母,符号,数字占用一个字节
- 中文:在vs或者vc中,默认情况下,一个中文占用两个字节
- 结束标记:1个字节
- 数组的长度,要么不写,如果要写的话,记得要把结束标记的空间给预留出来
- 字符数组 +双引号的方式定义字符串,内容是可以发生改变的,会把底层的字符数组放在只读常量区,只读常量区特点:内容不可以修改。里面定义的字符串是可以复用的
char str1[4]='"abc";
printf("%s\n",str1);
str1[0] = 'Q';
printf("%s\n",str1);
char* str2 ="abcd";
char* str3 ="abcd";
printf("%p\n",str2);//地址完全一样
printf("%p\n",,str3);
字符串输出
- printf( “ %s ” ):打印字符串,从字符串的第一个字符顺序向后打印,打印到 ‘\0’ 结束
‘a' != “a” (‘a’,’\0’)
‘abc’ 是一个错误定义!既不是字符串,也不是有效字符。
其他格式匹配符
%Ns:显示 N个字符的字符串,不足N用空格向右填充。
printf("|%9s|\n", str);
%0Ns:显示 N个字符的字符串,不足N用0向左填充。
printf("|%09s|\n", str);
%-Ns:显示 N个字符的字符串,不足N用空格向左填充。
printf("|%-9s|\n", str);
%%: 与字符串无直接关系。
- 显示一个%, 转义字符 ’\‘ , 对%无效。转义%,使用%本身。
- 输出【10 % 3 = 1】:
printf("10 %% 3 = 1\n");
练习:键盘输入字符串,存至str[]中,统计每个字母出现的次数。
int main(void)
{
char str[1024] = { 0 }; // 定义有10个元素的字符数组,初值均为0
for (size_t i = 0; i < 11; i++)
{
scanf("%c", &str[i]); // helloworld
}
// 定义一个有26个元素的数组,初始化成 0
char count[26] = { 0 }; // 代码26个英文字符出现的次数。
for (size_t i = 0; i < 11; i++)
{
int index = str[i] - 'a'; // 提取每个字符,在 count 表中对应的下标。
count[index]++; // 向对应字符,位置++,代表该字符出现了一次。
}
// 循环遍历 count 数组,打印出每个字符,出现的次数。
for (size_t i = 0; i < 26; i++)
{
if (count[i] != 0) // 0 == \0
{
printf("%c字符,在字符串%s中,出现了%d次\n", i+'a', str, count[i]);
}
}
system("pause");
return EXIT_SUCCESS;
}
scanf 获取字符串
char str[1024] = {0}; // 定义字符串存储的空间,保证足够大。
scanf("%s", str);
注意事项:
- 用于存储字符串的空间,必须足够大!防止溢出。
- %s 遇到 空格 和 \n 终止。
- 借助 “正则表达式”, 可以获取带有空格的字符串。
scanf(“%[^\n]”, str);
char str[1024] = {0};
scanf("%s", str); --- 获取 “hello world haha xixi” 字符串
printf("%s", str); ———— 输出 hello
字符串操作函数
gets
从 (键盘) 标准输入 stdin 获取字符串。返回字符串首地址, 可以获取带有空格的字符串,不保存
\n , 将其替换为 \0
#include <stdio.h>
char *gets(char *s); // char * 等价于 char []
参数:用来存储字符串的空间地址。
返回值:返回实际获取到的字符串的首地址。
// 示例
char str[1024] = {0};
printf("获取的字符串为:%s\n", gets(str));
fgets
从 (键盘) 标准输入 stdin 获取字符串。一定会给字符串预留\0空间。 可以获取带有空格的字符
串。如果空间足够,保留\n , 如果空间不足,不保留\n。
#include <stdio.h>
char *fgets(char *s, int size, FILE *stream); // char * 等价于 char []
参1:用来存储字符串的空间地址。
参2:空间的大小。(严格对应实际空间的大小)
参3:读取字符串的位置。 —— stdin(键盘)
返回值:返回实际获取到的字符串的首地址。
// 示例:
char str[15];
int len = sizeof(str);
printf("获取到的字符串为:%s\n", fgets(str, len, stdin));
puts
将字符串输出到 屏幕 标准输出 stdout。 输出后会自动向屏幕输出 \n
// printf(“%s\n”, “hello”)
#include <stdio.h>
int puts(const char *s); // char * 等价于 char []
参:待 写入到屏幕的字符串。
返回值:
成功:0, 失败:-1
// 示例:
char str[] = "hello world";
int ret = puts(str);
printf("ret = %d\n", ret);
fputs
将字符串输出到 屏幕 标准输出 stdout。不自动添加 \n 字符。
#include <stdio.h>
int fputs(const char * str, FILE * stream); // char * 等价于 char []
参1: 待写出到屏幕的字符串。
参2: 写出的位置。—— stdout 标准输出。屏幕。
返回值:
成功:0, 失败:-1
// 示例:
char str[] = "hello world\n";
int ret = fputs(str, stdout);
printf("ret = %d\n", ret);
strlen
获取一个字符串有效字符个数(字符串的长度)。 不含\0 ( 碰到 \0 结束 )
#include <string.h>
size_t strlen(const char *s);
参:待 求长度的字符串
返回:有效字符个数
// 示例:
char str[] = "hello world\n";
printf("有效长度=%u\n", strlen(str)); //不含有 \0 长度
printf("sizeof=%u\n", sizeof(str)); //含有 \0 长度
实现 strlen 函数
int main(void)
{
char str[] = "hello world";
int i = 0;
while (str[i] != '\0')
{
i++;
}
printf("不含\\0的字符串长度为:%d\n", i);
printf("strlen = %d\n", strlen(str)) ;
system("pause");
return EXIT_SUCCESS;
}
//2.遍历字符串得到每一个字符
char* p= str;
while(1){
// 利用指针获取字符串中的每一个字符,直到遇到\0为止
char c = *p;
if(c == '\0'){
break;
}
printf("%c\n",c);
p++;//指针往后
}
字符串追加
int main(void)
{
char str1[] = "hello";
char str2[] = "world";
char str3[100];
// 循环将 str1 中的字符,依次写入到 str3 中
int i = 0;
while (str1[i] != '\0')
{
str3[i] = str1[i];
i++;
} // 循环结束,str3 =【hello】无 \0
// printf("i = %d\n", i); --- 循环结束为 5 。
int j = 0; // 循环 str2
// 循环将 str2 中的字符,接着str1的内容顺序写入到 str3 中
while (str2[j]) // while(str2[i] != 0) == while (str2[i] != '\0')
{
str3[i+j] = str2[j];
j++;
} // 循环结束,str3 =【helloworld】无 \0
// 手动添加 \0 结束标记
str3[i + j] = '\0';
printf("str3 = %s\n", str3);
system("pause");
return EXIT_SUCCESS;
}
函数
函数的作用
- 提高代码复用率。提高程序模块化组织性。
函数分类
- 系统库函数:标准C库。 libc
- 必须要引入头文件 #include <xxx.h> ---- 函数声明。
- 根据函数库函数原型,调用函数。
- 用户自定义函数:
- bubble_sort()、myPrint()
- 除了需要提供函数原型之外,还需要提供函数实现。
使用函数
函数定义
- 必须包含 “函数原型” 和 “函数体”
- 函数原型: 返回值类型 + 函数名 + 形参列表(形式参数列表。 一定包含:类型名、形参名)。
- 函数体:一对 {} 包裹函数实现。
函数声明
- 包含:函数原型( 返回值类型 + 函数名 + 形参列表) + “;”
- 要求,在函数调用之前,编译器,必须见过函数定义。否则需要函数声明。
- 如果,没有函数声明,编译默认做 “隐式声明”。
- 隐式声明:【不要依赖】
- 编译认为所有的函数,返回值都是 int 。
- 可以根据函数调用,推测函数原型。
- 隐式声明:【不要依赖】
- #include <xxx.h> 内部,包含 函数声明。
函数调用
- 包含:函数名(实参列表);
- 实参(实际参数):在调用时,传参必须严格按照形参填充。(参数个数、类型、顺序)
- 实参 在调用时,没有 类型描述符。
// 函数声明
int add(int a, int b);
// 函数定义
int add(int a, int b)
{
int ret = a + b;
return ret;
}
//函数调用
int m = 10;
int n = 20;
int ret = add(m, n);
exit 函数
return 关键字:返回当前函数调用。将返回值返回调用者。( 在底层,会调用 _exit() 函数。)
exit() 函数:退出当前程序
// 函数声明
// int test(int a, char ch);
int test(int, char); // 函数声明de 简化写法。 声明时,形参名 可以省略。
int main(void)
{
// 函数调用
int ret = test(10, 'a'); // test函数,调用结束,return 给 main
printf("test函数返回:ret = %d\n", ret);
// return 0; // 返回给调用者(启动例程)—— 作用:结束程序。
exit(0); // 结束程序。
}
// 函数定义
int test(int a, char ch)
{
printf("a = %d\n", a);
printf("ch = %c\n", ch);
// return 97; // 返回给调用者,程序不结束。
// 结束程序
exit(97); // 使用 #include <stdlib.h>
}
多文件编程
头文件守卫:为了防止头文件被重复包含。 --- head.h
#pragma once
是 VS自动生成的。 只应用于 windows系统。
#ifndef _HEAD_H_ // 习惯写成这样。
#define _HEAD_H_
。。。头文件内容:#include <xx.h>/宏定义 #define PI 3.14/函数声明/类型定义
#endif
// 示例:
#ifndef _HEAD_H_ // 标准引入“头文件守卫”
#define _HEAD_H_
// include 头文件
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>
// 函数声明
int add(int a, int b);
int sub(int a, int b);
// 宏定义
#define PI 3.14
// 类型定义
#endif
<>
包谷的是 系统库头文件。“”
包裹的是,用户自定义头文件。
// main函数所在的 .c 文件中:
#include "head.h"
指针
指针和内存单元
- 指针:地址!
- 指针变量(*p = 0x10101):用存储地址的变量!
- 内存单元:是计算机中内存最小的存储单位。 内存单元大小 —— 1字节(8bit位)。
- 每个内存单元,都有一个唯一的编号。
- 这个内存单元的编号,称为 “地址”
- &p 地址占位符
指针的作用
- 作用1:操作其他函数中的变量
int main()
{
//定义两个变量,要求交换变量中记录的值注意:交换的代码写在一个新的函数swap中
//1.定义两个变量
int a= 10;int b= 20;
// 2.调用swap函数
printf("调用前:%d,%d",a,b);
swap(&a,&b);
printf("调用后:%d,%d",a,b);
return 0;
}
void swap(int* p1 ,int* p2)
{
//此时交换的是p1指向的变量和p2指向的变量的值
int temp = *p1;
*p1 = *p2;
*p2 = temp;
}
int main(){
//调用method函数,并使用该函数中的变量a
int*p = method();
printf(“拖点时间\n”);
printf("拖点时间\n”)
printf(“拖点时间间\n”);
printf("拖点时间\n")
printf("拖点时间\n")
printf("拖点时间\n");
printf("拖点时间\n”);
printf("拖点时间\n");
printf("%d\n",*p);//不能打印的,因为method函数结束之后,那么该函数里面所有的变量也会随之消失
return 0;
}
int* method()
{
int a= 10;
return &a;
}
- 作用2:函数返回多个值
void getMaxAndMin(int arr[], int len, int* max, int* min)
{
//求数组的最大值
*max = arr[0];
for (int i=1;i< len; i++)
if(arr[i]> *max)
{
*max = arr[i];
}
}
//求数组的最小值
*min = arr[0];
for(int i=1;i<len;i++){
if(arr[i]< *min){
*min = arr[i];
}
}
}
- 作用3:函数的结果和计算状态分开
定义一个函数,将两数相除,获取他们的余数
//返回值:表示计算的状态0正常1不正常
int getRemainder(int numl, int num2, int* res)
{
if(num2 == 0)
{
// 停止
return 1;
}
*res = num1 %num2;
return 0;
}
- 作用4:方便地操作数组和函数
指针的定义和使用

- 指针变量的数据类型要跟指向变量的类型保持一致
int a = 10;
int *p = &a; //int* p --- windows int *p ---Linux int * p 依据个人习惯,这个*代表标记
// int *p = &a; 展开:
// int *p;
// p = &a;
a = 999;
printf("*p = %d\n", *p); // 间接引用 —— 右值。这个*代表解引用运算符
// 解引用、间接引用。
*p = 250; // 间接引用 —— 左值。
printf("a = %d\n", a);
- 给指针变量赋值的时候,不能把一个数值赋值给指针变量
int a=10;
int* p=&a;//正确
int a = 10;
int* p= 500;错误
- *p 作用:
- 将 p 变量的内容,取出,当成地址看待。找到该地址对应的内存空间。
- 如果做左值,存储数据到空间中。
- 如果做右值,取出空间中的内容。
- 将 p 变量的内容,取出,当成地址看待。找到该地址对应的内存空间。
- 指针类型大小。( 指针,算一种自定义数据类型。int * )与类型无关!只与当前使用的平台架构/编译器有关(32位:4字节、64位:8字节)。
int main(void)
{
printf("int *的大小:%u\n", sizeof(int*));
printf("short *的大小:%u\n", sizeof(short*));
printf("char *的大小:%u\n", sizeof(char*));
printf("long *的大小:%u\n", sizeof(long*));
printf("long long *的大小:%u\n", sizeof(long long *));
}
- 一条语句同时定义多个指针变量、普通变量
int a, b, c; // 多个普通变量
int *p1, *p2, *p3; // 多个指针变量。 每个变量都有有一个自己的 *
int a, *b, *c, d; // 定义整型变量a、d, 同时定义指针变量 b、c
野指针
- 没有使用 “有效” 的地址,给指针初始化。
int* p; //没有给 p 指定一个有效地址。
*p = 1000;
- p指针变量有值,但该值不是一个有效的地址。
int* p = 10; // 没有给 p 指定一个有效地址。
*p = 1000;
- 指针指向的空间未分配
//野指针:指针指向的空间未分配
int a= 10;
int* p1 = &a;
printf("%p\n",p1);
printf("%d\n”,*p1);
// p2 野指针
int* p2 = p1 + 10;
printf("%p\n",p2);
printf("%d\n",,*p2);
- 编程时杜绝野指针。
空指针
- NULL == 0 == \0
int* p = NULL; // #define NULL ((void *)0) // 定义一个空指针。
*p = 1000; // *p 所指向的内存空间,是一个 “无效访问区域”。
- 指针指向的空间已分配,但是被释放了
int* p2= method();
sleep(1000);//拖时间
printf("%p\n",p2);
泛型指针 (万能指针 void *)
- 无法获取数据,无法计算。可以接受任意一种变量的地址。 但是使用时【必须】借助 “强制类型转换” 具体化数据类型。
- void * 类型的大小:32位:4字节。 64位:8字节。
int main(void)
{
printf("void *的大小:%u\n", sizeof(void *));
char ch = 'R';
void* p; // 泛型指针(万能指针)
p = &ch;
printf("*p = %c", *(char *)p); // 将p变量类型,由void* 强转成 char *
return EXIT_SUCCESS;
}
//1.定义两个变量
int a = 10;
short b=20;
//2.定义两个指针
int* p1 = &a;
short* p2 = &b;
//3.输出打印
printf(“%d\n",*p1);
printf("%d\n",*p2);
//不同类型的指针之间,是不能互相赋值的
//void类型的指针打彼上而的观念
//void没有任何类型,好处可以接受任意类型指针记录的内存地址
void* p3 = p1;
Void* p4 = p2;
// 函数:用来交换两个变量记录的数据
// 修改一下函数,更具有通用性
void swap(void* pl,void* p2,int len)// 8
{
//把void类型的指针,转成char类型的指针
char* pc1 = p1;
char* pc2 = p2;
char temp =0;
//以字节为单位,一个字节一个字节的进行交换
for(inti=0;i<len; i++)
{
temp =*pc1;
*pc1 =*pc2;
*pc2 = temp;
pc1++;
pc2++;
}
}
数组指针和指针数组
- 数组指针:指向数组的指针
作用:方便的操作数组中的各种数据
举例:int* p=arr; 步长为:int(4字节)
举例:int(*p)[5]=&arr; 步长为:int乘5(20字节)
- 指针数组:存放指针的数组作用:用来存放指针
举例:int*p[5],这个数组里面存着int类型的指针
函数指针
//1.定义指针指向两个函数
void(*p1)()= method1;
int (p2)(int, int);
//2.利用函数指针去调用函数
p1();
int num = p2(10,20);
printf("%d\n"num);
void method1(){
printf("method1\n");
}
const关键字
- 修饰变量
const int a = 20; // a 为只读变量,不能修改。
//a = 200; // 不可以修改
int* p = &a;
*p = 677;
printf("a = %d\n", a); // 借助指针可以修改 const 普通变量的值。
- 修饰指针:常用在函数内,限制指针所指向的内存空间,为只读 (不允许修改)
// 方式1
const int *p; // 向后(右)作用
// 示例:
int a = 10;
int b = 20;
const int *p = &a;
*p = 500; //【失败】: 将 a 的值,改为 500, 不能改!
p = &b; //【成功】:可以修改 p变量的内容(地址)。
// 方式2
int const *p; // 向后(右)作用
作用方式同上!
指针和数组
数组名
数组名,是地址常量。不可以被修改、赋值(=、+=、-=、*= 、/=、%= 带有副作用的运算符)
int a[3] = { 1,2,3 }; // a 就是数组地址。
int b[3]; // b 是常量
b = a; // 不允许!! 因为 b 为地址常量。
指针,是变量。可以使用数组名,给指针赋值。
int *p = a; // a 就是数组地址。允许!!此时p就是数组指针
取数组元素

int arr[] = {1, 3, 5, 7, 9};
int *p = arr; // 使用数组地址,给p指针变量初始化
// 结论:
arr[0] == *(arr+0) == p[0] == *(p+0)
arr[1] == *(arr+1) == p[1] == *(p+1)
arr[2] == *(arr+2) == p[2] == *(p+2)
。。。
arr[N] == *(arr+N) == p[N] == *(p+N)
指针和数组名区别
- 指针是变量。数组名是常量。
- sizeof(指针)——> 4字节、8字节。
- sizeof(数组名)——> 数组实际的字节数。
指针的算数运算
数据类型对指针的作用
- 间接引用(解引用):指针的数据类型,绝定了从指针存储的地址开始,向后读取的字节数。(与指针本身存储空间无关)
int a = 0x12345678;
//int* p = &a;
//int* p; --- 0x12345678;
//short* p; --- 0x5678;
char* p; // --- 0x78;
p = &a;
printf("%#x\n", *p);

- 加减运算。指针的数据类型,决定了指针进行 +/- 操作时,向后/前 跳过的字节数。
int * +1 实际加过 4 字节。
short * +1 实际加过 2 字节。
char * +1 实际加过 1 字节。
long long * +1 实际加过 8 字节。
指针++操作数组

int main(void)
{
int arr[] = {1,2,3,4,5,6,7,8,9,0};
int* p = arr;
int n = sizeof(arr) / sizeof(arr[0]);
//for (size_t i = 0; i < n; i++)
//{
// printf("%d ", arr[i]);
//}
for (size_t i = 0; i < n; i++)
{
printf("%d ", *p);//或者*p++
p++; // p = p+1, 一次加过 一个 int 大小(一个数组元素)。
}
// p值随着循环,不断变化, 打印结束后,p指向一块无效的内存空间(野指针)
printf("\n");
return 0;
}
练习:使用 指针给空数组连续赋值。 再使用 “指针挪移” 方法打印出这个数组。
int main(void)
{
int arr[10];
int n = sizeof(arr) / sizeof(arr[0]);
int* p = arr; // p 指向 arr[0]
printf("p = %p\n", p);
// 使用 指针,给空数组连续赋值。
for (size_t i = 0; i < n; i++)
{
//arr[i] = i + 10;
*(p + i) = 10 + i; // *(p + i) == arr[i]; *(p + i)中i表示步长,移动多少个单位
}
// 循环结束时,p指向谁??? 依然指向 arr[0];
printf("循环结束后:p = %p\n", p);
// 使用“指针挪移”方法打印出这个数组。
for (size_t i = 0; i < n; i++)
{
printf("%d ", *p);
p++; // p = p+1, 一次加过 一个 int 大小(一个数组元素)。
}
printf("\n");
// 循环结束时,p指向谁??? 指向数组尾元素的下一个内存(野指针)。
printf("2 for 循环结束后:p = %p\n", p);
return 0;
}
指针 +- 整数
- 普通指针变量 +-整数(步长:指针移动一次的字节个数)
char *p; p+1 偏过1个字节。
short *p; p+1 偏过2个字节。
int *p; p+1 偏过4个字节。
- 在数组中,+-整数(步长:指针移动一次的字节个数)
short arr[] = {1, 3, 5, 8, 12, 17, 19};
short *p = arr+3;
p + 3; 向后(右)偏过 3 个元素。 6 个字节。
p - 2; 向前(左)偏过 2 个元素。 4 个字节。
- &数组名 + 1
&数组名 +1, 加过的是一个数组的总大小。
指针其他运算
指针 * / %:不允许!!!意义不明
指针 + 指针:不允许 error!意义不明
指针 - 指针:
- 普通变量来说,语法允许, 但,无实际意义。
- 对于数组来说,得到间隔步长
int main(void)
{
int a[] = {1,2,3,4,5,6,7,8,9,0};
int* p = a; // 保存数组首地址。
p = &a[3]; // 修改p保存的地址
printf("p-a = %d\n", p - a); // ---- 3
int* q = &a[8];
printf("q-p = %d\n", q - p); // ----- 5
return 0;
}
指针实现 strlen()
- 借助 数组 实现
int myStrlen1(char str[])
{
int i = 0;
while (str[i] != '\0')
{
i++;
}
return i;
}
- 借助 指针 实现
int myStrlen2(char str[])
{
char* p = str;
while (*p != '\0')
{
p++;
}
return p - str; // 返回元素个数。
}
指针比较运算
- 普通变量:语法允许,但无实际意义。
- 数组:地址之间可以比大小。 得到元素存储的先后顺序。
int arr[] = {1,2,3,5,6,7,8};
int* p = &arr[2];
if (p > arr) {
printf("成立\n");
}
else if (p < arr) {
printf("不成立");
}
else {
printf("==\n");
}
- 判断 NULL
int* p;
int a = 10;
// p = NULL // 初始化空指针。
p = &a;
if (p != NULL)
{
printf("p is not NULL\n");
}
else
{
printf("p is NULL\n");
}
指针数组
- 指针数组的本质,是一个二级指针。
int a = 10;
int b = 20;
int c = 30;
int *arr[] = {&a, &b, &c}; // int型指针数组,保存地址的数组。
printf("*arr[0] = %d\n", *arr[0]); // arr[0] == *(arr+0)
printf("*arr[0] = %d\n", *(*(arr + 0)));
printf("*arr[0] = %d\n", **arr); // * 结合性,自右向左。
- 二维数组,是指针数组, 是二级指针。
int a[] = { 10 };
int b[] = { 20 };
int c[] = { 30 };
int* arr[] = {a, b, c}; // 存地址,
printf("arr[0][0] = %d\n", arr[0][0]);
printf("*(*(arr+0)) = %d\n", *(*(arr+0)));
printf("**arr = %d\n", **arr); // 二级指针的简介引用。
多级指针
多级指针不能跳跃定义。 有一级才能定义二级;有二级才能定义三级;有三级,才能定义4级。。。
int a = 10; // 普通变量
int *p = &a; // 一级指针。是变量的地址。
int **pp = &p; // 二级指针。是一级指针的地址。【重点】
int ***ppp = &pp; // 三级指针。是二级指针的地址。
int ****pppp = &ppp;// 四级指针。是三级指针的地址。
......
*ppp == pp == &p;
**ppp == *pp == p == &a;
***ppp == **pp == *p == a;
指针和函数
栈帧
- 当函数被调用时,系统会在 stack 空间上申请一块内存,用来给函数调用提供空间。存储 形参 和局部变量(定义在函数内部的变量)。
- 函数调用结束时,这块内存空间,会被自动释放 (消失) 。
传值和传址
传值(值传递):函数调用期间,实参将自己的数据值, 拷贝一份给形参。
传址 (传引用):函数调用期间,实参将 自己的 “地址值” ,拷贝一份 赋值给形参。
可以在 A 栈帧中,借助地址,修改B栈帧上的变量数据。
数组做函数参数
- 数组做函数参数时,传递的不再是整个数组,而是数组的首地址(指针)。
- 定义处:arr表示的就是完整的数组
- 函数中的arr:只是一个变量,用来记录数组的首地址
int main(void)
{
int arr[] = {1, 4, 6, 7, 9, 0};
printf("main : sizeof(arr) = %u\n", sizeof(arr)); //整个数组的大小。
// 调用 test 函数,传参 数组。
test(arr); // 实参!
system("pause");
return EXIT_SUCCESS;
}
// 定义函数,用数组做参数
void test(int arr[]) // 形参
{
printf("test : sizeof(arr) = %u\n", sizeof(arr)); // 指针的大小。
printf("arr[0] = %d\n", arr[0]);
}
- 当整型数组做函数参数时,通常在函数定义中封装2个参数,数组首地址和元素个数。
// 冒泡排序
//void BubbleSort(int arr[]) 这种传参,无法在函数内,求元素个数。因为此时sizeof(arr)为计算得到的是sizeof(首地址),而不是sizeof整个数组
void BubbleSort(int arr[], int n)
{
for (int i = 0; i < n - 1; i++)
{
for (int j = 0; j < n - 1 - i; j++)
{
if (arr[j] > arr[j + 1])
{
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
int main(void)
{
int arr[] = {1, 4, 8, 12, 19, 43, 2, 7, 15};
// 获取元素个数
int n = sizeof(arr) / sizeof(arr[0]);
// 排序
BubbleSort(arr, n);
// 打印排序后结果
for (size_t i = 0; i < n; i++)
{
printf("%d ", arr[i]);
}
putchar('\n');
return 0;
}
- 数组做函数参数,也可以写成指针的形式。(本质一样)
void BubbleSort(int arr[], int n) == void BubbleSort(int *arr, int n)
指针做函数返回值
指针做函数返回值,不能返回【局部变量的地址】。
- 当函数调用结束,栈帧空间释放,局部变量的地址,无效。
数组做函数返回值,不允许!(C语言中,只能写成 指针形式)
指针和字符串
- 基本知识
char str1[] = {'h','i','\0'};【麻烦】 变量,可读可写。
char str2[] = "hi"; 变量,可读可写。
char *str3 = "hi"; 常量,只读。
char *str4 = {'h','i', '\0'}; // 错误!
char *str2 = "hello"
是一个常量。不允许修改。
int main0401(void)
{
char str1[] = "hello"; // 相当于 {'h','e','l','l','o','\0'}
char* str2 = "";
str1[0] = 'R'; // 相当于 {'h','e','l','l','o','\0'}是变量,可以随意修改。
printf("%s\n", str1);
str2[0] = 'R'; // "hello" 是字符串常量。 不能修改!
printf("%s\n", str2);
system("pause");
return EXIT_SUCCESS;
}
- 同一个字符串常量 ,可以给多个不同的指针赋值。
char* str2 = "hello"; 和 char* n = "hello";
地址值都相同。 都是字符串 “hello” 的地址。
int main(void)
{
char str1[] = "hello"; // 相当于 {'h','e','l','l','o','\0'}
char m[] = "hello";
char* str2 = "hello";
char* n = "hello";
printf("str1 = %p\n", str1);
printf("m = %p\n", m); // 数组定义的 hello 地址不同。
printf("str2 = %p\n", str2);
printf("n = %p\n", n); // 指针定义的 hello 是字符串常量,是同一个地址。
system("pause");
return EXIT_SUCCESS;
}
【结论】: 当字符串 (含有 \0 字符数组),做函数参数时,不需要提供 2个参数。因为每个字符串都有‘\0’
字符串比较(strcmp()函数)
- 比较 str1 和 str2, 如果相同返回0, 不同则依次比较ASCII码,str1 > str2 返回1,否则返回 -1
按对应的 位置,比较字符的大小。不比较ASCII 码 和。
分析:循环,依次比较两个 字符串 中 对应位字符。\0 结束。都相同 ---> 0,对应位不同, 比较字符的 ASCII 码。 str1 > str2 ---> 1, str1 < str2 ----> -1
// 数组的实现方式
int myStrcmp(char *str1, char *str2)
{
int i = 0;
while (str1[i] == str2[i]) // *(str1+i) == *(str2+i)
{
if (str1[i] == '\0')
{
return 0; // 2个字符串, 一样!
}
i++;
}
// str1 和 str2 有字符不同。
return str1[i] > str2[i] ? 1 : -1;
}
// 指针的实现方式
int myStrcmp2(char* str1, char* str2)
{
while (*str1 == *str2) // *(str1+i) == *(str2+i)
{
if (*str1 == '\0')
{
return 0; // 2个字符串,一样!
}
str1++;
str2++;
}
// str1 和 str2 有字符不同。
return *str1 > *str2 ? 1 : -1;
}
字符串拷贝(strcpy()函数)
- 将一个字符串中的所有字符,依次拷贝存放到另个一空字符数组中。
// 数组版:
void myStrcpy(char* src, char* dst)
{
int i = 0;
while (src[i] != '\0') // while(src[i] != 0) while(strc[i])
{
dst[i] = src[i];
i++;
}
dst[i] = '\0'; //main中的 dst 初始化为0, 此步可以省略。
}
// 指针版
void myStrcpy2(char* src, char* dst)
{
while (*src != '\0') // while(src[i] != 0) while(strc[i])
{
*dst = *src;
src++;
dst++;
}
*dst = '\0'; //main中的 dst 初始化为0, 此步可以省略。
}
在字符串中查找字符出现的位置(strchr()函数)
- 在字符串中查找字符出现的位置()
- “helloworld” ‘e’ ——> “elloworld”
- ‘l’ ——> “lloworld”
- ‘r’ ——> “rld”
// 指针版
char* myStrchr(char* str, char ch)
{
while (*str) //while (*str != '\0') == while (*str != 0)
{
if (*str == ch)
{
return str;
}
str++;
}
return NULL; // 在 str 中,没有找到 ch
}
// 数组版
char* myStrchr2(char* str, char ch)
{
int i = 0;
while (str[i]) //while (str[i] != '\0') == while (str[i] != 0)
{
if (str[i] == ch) // str[i] == *(str+i)
{
return &str[i];
}
i++;
}
return NULL; // 在 str 中,没有找到 ch
}
字符串去空格
- “ni chou sha ? chou ni za di !” ----> “nichousha?chounizadi!”
// 封装函数,去除字符串空格 --- 数组版
void str_no_space(char* src, char* dst)
{
int i = 0; // 遍历 src 字符串
int j = 0; // 记录 dst存储位置。
while (src[i])
{
if (src[i] != ' ') // 只有不为空格,才存储到 dst中。
{
dst[j] = src[i];
j++; // 不为空格,j后移。为空格,j不动。
}
i++;
}
dst[j] = '\0';
}
// 封装函数,去除字符串空格 --- 指针版
void str_no_space2(char* src, char* dst)
{
while (*src)
{
if (*src != ' ') // 只有不为空格,才存储到 dst中。
{
*dst = *src;
dst++; // 不为空格,指针后移。为空格,指针不动
}
src++;
}
*dst = '\0';
}
int main(void)
{
char str[] = "ni chou sha ? chou ni za di ! zai chou yi ge shi shi";
char dst[1024] = {0};
// 调用函数 去除 str中的空格,保存到 dst 中
// str_no_space(str, dst);
str_no_space2(str, dst);
printf("dst = %s\n", dst);
system("pause");
return EXIT_SUCCESS;
}
str 中 substr 出现的次数与strstr() 函数
str = “hellollolloabcllollollo ”
substr: “llo” strlen(“llo”)
写函数测试, llo 在 “hellollollollollollo ” 出现了多少次。
char *strstr(const char *haystack, const char *needle);
char *strstr(const char *str, const char *substr);
参1:原串
参2:子串
返回值:
成功:返回子串在原串中的位置(地址值)
失败:NULL
// 封装函数,统计 str字符串中,substr出现的次数
int substr_times(char* str, char* substr)
{
int count = 0; // 定义变量统计substr出现的次数。
char *p = strstr(str, substr); // ---> "llolloabclloxyzllollo";
// 循环的截取 str 串,判断剩余 str串中是否包含 substr
while (p != NULL) // while (p)
{
count++;
p += strlen(substr); // “lloabclloxyzllollo";
p = strstr(p, substr);
}
return count;
}
// 在 str字符串中,找子串 substr出现的次数
int main(void)
{
char str[] = "hell9oqqqlloabclloxyzlmlollo";
char substr[] = "llo";
int ret = substr_times(str, substr);
printf("%s 串中,%s 子串 出现 %d 次\n", str, substr, ret);
system("pause");
return EXIT_SUCCESS;
}
求字符串非空格元素个数
"ni chou sha ? chou ni za di ! zai chou yi ge shi shi"
统计这里,除空格外 字符的个数。
// 统计非空格数
int no_space_num(char* str)
{
int count = 0;
// 指针方式实现。
char* p = str;
while (*p)
{
if (*p != ' ')
{
count++;
}
p++;
}
return count;
}
int main(void)
{
char str[] = "hello ni hao ma world?";
int ret = no_space_num(str);
printf("ret = %d\n", ret);
system("pause");
return EXIT_SUCCESS;
}
字符串逆置
- “hello” ---> “olleh”
参考 “数组逆置”实现。day05
void str_inverse(char* str)
{
char* start = str; // 记录首个元素的地址
char* end = str + strlen(str)-1; // 记录最后一个元素的地址
// 循环交换字符串首尾元素
while (start < end)
{
char tmp = *start; // 三杯水交换字符元素。
*start = *end;
*end = tmp;
start++; // 首元素指针后移
end--; // 尾元素指针前移
}
}
int main(void)
{
char str[] = "this is a test";
str_inverse(str);
printf("%s\n", str);
system("pause");
return EXIT_SUCCESS;
}
判断字符串是回文
abcba --- 是回文
amkilolikma --- 是回文。
abccba ---- 是回文。
abcdba --- 不是回文。
// 判断字符串是否是回文
int str_is_abcba(char* str)
{
char* start = str;
char* end = str + strlen(str) - 1;
while (start < end)
{
if (*start != *end)
{
return 0; // 不是回文
}
start++;
end--;
}
return 1;
}
int main(void)
{
char str[] = "abcmnmcba";
int ret = str_is_abcba(str);
if (ret == 1) // 是回文
{
printf("%s 是回文!\n", str);
}
else if (ret == 0)
{
printf("%s 不是回文!\n", str);
}
system("pause");
return EXIT_SUCCESS;
}
字符串处理函数:
- 全部是标准C库函数。 使用头文件 #include <string.h>
字符串拷贝
strcpy
char *strcpy(char *dest, const char *src); // src:source dest: dst
将 src 的内容,拷贝给 dest。 返回 dest。dest空间要足够大。
strcpy 函数,不去检查 dest 是否足够大。—— 【不安全函数】
函数调用结束,返回值 和 dest 结果一致。
// 字符串拷贝
int main(void)
{
char str[] = "you will be die if you copy me!";
char dst[100] = { 0 };
char *p = strcpy(dst, str);
printf("dest = %s\n", dst);
printf("p = %s\n", p);
system("pause");
return EXIT_SUCCESS;
}
strncpy
char *strncpy(char *dest, const char *src, size_t n); // 安全
将 src 的内容,拷贝给 dest。 只拷贝 n 个字节。dest空间要足够大。 通常n与dest的空间大小一致
特性:1. n > src : 只拷贝 src 大小。2. n < src : 只拷贝 n 个字节。 不会自动添加 \0
int main(void)
{
char str[] = "hello world";
char dst[100] = { 0 };
char* p = strncpy(dst, str, sizeof(dst));
//for (size_t i = 0; i < 10; i++)
//{
// printf("%c\n", p[i]);
//}
printf("%s\n", p);
system("pause");
return EXIT_SUCCESS;
}
字符串拼接
strcat
char *strcat(char *dest, const char *src);
将 src 中内容,拼接到 dest 后。 返回拼接成功的字符串。—— 需要保证 dest 空间足够大。
函数调用结束后,dest 和 返回值结果相同。
int main(void)
{
char str[] = "hello world";
char dst[100] = "haha hoho xixi";
char *p = strcat(dst, str);
printf("p = %s\n", p);
printf("dst = %s\n", dst);
system("pause");
return EXIT_SUCCESS;
}
strncat
char *strncat(char *dest, const char *src, size_t n);
将 src 中前 n个字符,拼接到 dest 后。 返回拼接成功的字符串。—— 需要保证 dest 空间足够大。
函数调用结束后,dest 和 返回值结果相同。
int main(void)
{
char str[] = "hello world";
char dst[100] = "haha hoho xixi";
char* p = strncat(dst, str, 7);
printf("p = %s\n", p);
printf("dst = %s\n", dst);
system("pause");
return EXIT_SUCCESS;
}
字符串比较
- 字符比较可以使用 > < <= >= == != , 字符串 比较 不允许使用。
strcmp
int strcmp(const char *s1, const char *s2);
比较 s1 和 s2 两个字符串,如果相等 返回 0;
如果不相等, 进一步 比 s1 和 s2 对应位上的 ASCII码值。
s1 > s2 返回 1
s1 < s2 返回 -1
int main(void)
{
char s1[] = "helloz";
char s2[] = "helloaworld";
printf("ret = %d\n", strcmp(s1, s2)); // 不比较 ASCII 的 和
system("pause");
return EXIT_SUCCESS;
}
strncmp
int strncmp(const char *s1, const char *s2, size_t n);
比较 s1 和 s2 两个字符串的前n个字符,如果相等 返回 0;
如果不相等, 进一步 比 s1 和 s2 对应位上的 ASCII码值。(不比较 ASCII 的 和)
char s1[] = "helloz";
char s2[] = "helloaworld";
printf("ret = %d\n", strncmp(s1, s2, 5)); --- 0
字符串格式化输入、输出
sprintf
int sprintf(char *str, const char *format, ...);// ... 代表 这是一个参数可变的函数。
对应 printf记忆。 作用将 原来输出到屏幕的 “格式化字符串”, 写到 参1 的 str 中。
// printf("%d + %d = %d\n", 10, 24, 10+24);
char str[1024] = {0}; // 保证空间足够大
sprintf(str, "%d + %d = %d\n", 10, 24, 10 + 24); // 写到 str 中。不打印屏幕
puts(str);
printf("---%s", str);
sscanf
int sscanf(const char *str, const char *format, ...);
对应 scanf 记忆。 作用 将 原来 从键盘获取到的 “格式化字符串”, 从 参1 的 str 中获取。
int a, b, c;
// scanf("%d+%d=%d", &a, &b, &c); // 从键盘 stdin 读取。
char str[] = "10+20=30"; // 提供给 sscanf 参1 使用。
int ret = sscanf(str, "%d+%d=%d", &a, &b, &c);
printf("a = %d\n", a);
printf("b = %d\n", b);
printf("c = %d\n", c);
字符串查找字符、子串
strchr
char *strchr(const char *s, int c);
在 字符串s 中,找 字符 c 出现的位置。 返回 字符在字符串中的地址。
int main(void)
{
printf("%s\n",strchr("hellohehexixihoho",'i'));
system("pause");
return EXIT_SUCCESS;
}
strrchr
r: rightchar *strrchr(const char *s, int c);
自右向左,在 字符串s 中,找 字符 c 出现的位置。 返回 字符在字符串中的地址。
int main(void)
{
//printf("%s\n",strchr("hellohehexixihoho"'i'))
// printf("%s\n”,strchr("hellohehexixihoho","i");
system("pause");
return EXIT_SUCCESS;
}
strstr
char *strstr(const char *str, const char *substr);
在字符串 str 中, 找寻子串 substr 第一次出现的位置。 返回地址。
字符串分割
strtok
char *strtok(char *str, const char *delim);
参1:待拆分字符串
参2:分割符组成的字符串。 strtok("www.baidu.com", "."); // 写成 '.' 错误!
按照(参2)既定的分割符, 来拆分字符串。 www.baidu.com 按 “.” 拆分。
// 测试1:
int main(void)
{
char str[] = "www.itcast.cn"; // ---》 "www\0itcast.cn";
char *p = strtok(str, "."); // strtok调用完成,会将 分割符用 \0 替换。
printf("p = %s\n", p);
// 调用一次 strtok 分割之后,再去打印原串 str
for (size_t i = 0; i < 13; i++)
{
//printf("%c\n", str[i]);
printf("%d\n", str[i]); // 打印每个字符的 ASCII码
}
system("pause");
return EXIT_SUCCESS;
}
结论:
strtok() 函数,直接在原串上对字符串分割。 不能分割字符串常量。 char *str = “hello”;
strtok() 函数调用结束, 会将 分割符,替换成 ’\0‘
第一次用 strtok 拆分, 参1 传待拆分的原串。 第 1+ 次拆分,参1 传 NULL。
/字符串分割 strtok
int main(void)
{
char str[]="www.itcast.cn.net.com";//---》"www\0itcast.cn";
//char *str="www.itcast.cn.com.net";// 字符串常量。
// strtok调用完成,会将 分割符用\0 替换。
char *p= strtok(str,"."); // strtok调用完成,会将 分割符用 \0 替换。
printf("P = %s\n",p);
// 循环按".”拆分 剩余 字符串
while (1){
p = strtok(NULL,".");
if(p == NULL)
{
break;
}
printf("p=号s\n",p);
}
练习:
拆分 字符串 “”, 按 分割符 “. $”···
int main(void)
{
char str[]="www.itcast.cn$This is a test$for strtok";
// 调用第一次
char *p = strtok(str,". $");// 分割符有3个:'.’/'$’/’·空格
printf("p = %\n",p) ;
//后续 N+1次调用。while循环,参1传NULL
while (1){
p= strtok(NULL,". $");
if (p == NULL)
break;
printf("循环中 p = %\n",p);
}
system("pause");
return EXIT_SUCCESS;
}
字符串转换
a : 代表字符串 string。
将字符串转整数、小数、长整数、长长整型。
使用这类函数进行转换时, 要求:原串必须是可转换的字符串。
错误使用 : “abc123”、“xyac123”、“1245dke89” 不能正确转换。
atoi
#include <stdlib.h>
int atoi(const char *nptr);
double atof(const char *nptr);
long atol(const char *nptr);
long long atoll(const char *nptr);
示例:
char str1[] = "12abc3456";
int num = atoi(str1);
printf("num = %d\n", num);
char str2[] = "3.14";
double num2 = atof(str2);
printf("num2 = %lf\n", num2);
char str3[] = "34568490354";
long long num3 = atoll(str3);
printf("num3 = %lld\n", num3);
内存管理
局部变量
- 概念:定义在函数内部的变量。
- 作用域:从定义位置开始,到包裹该变量的第一个右大括号结束。( 函数作用域、块作用域。)
void test(void)
{
{
int m = 10; // 局部变量。 --- 块作用域。出了 } 不能使用。
}
printf("---m = %d\n", m);
//int i = 0;
for (size_t i = 0; i < 10; i++)
{
printf("m = %d\n", m);
}
//printf("i = %d\n", i);
}
全局变量
- 概念:定义在函数外部的变量。
- 作用域:从定义位置开始,默认到本文件内部。 其他文件如果想使用,可以通过 “声明” 的方式,将作用域导出。
static变量
static全局变量
- 定义语法:在全局变量定义之前,添加 static 关键字。 如:
static int a = 10;
- 作用域:被限制在本文件内部,不允许通过 “声明” 方式导出作用域。(java不同)
static局部变量
- 定义语法:在局部变量定义之前,添加 static 关键字。
- 作用域:从定义位置开始,到包裹该变量的第一个右大括号结束。
- 特性: s静态局部变量,只定义一次。相当于,在全局位置定义。通常用来做计数器。
void test08(void){
static int b=10;//静态局部变量
printf("%d\n”,b++);
}
int main (void){
for(size_t i=0;i<10;i++){
test08();//调用 test08 10 次。
}
system("pause");
return EXIT_SUCCESS;
}
static 函数
- 全局函数:就是 “函数”。定义语法:函数原型 + 函数体。
- static函数:
- 定义语法:static 函数原型 + 函数体。
- 特性:只能在 本文件内使用。其他文件即使声明也无法使用。
A.c 文件中有如下代码
// static 关键字能将 test09 函数限制在本文件内。 外部文件,不能访问
static void test09(void)
{
for (size_t i = 0; i < 5; i++)
{
printf("i = %d\n", i);
}
}
--------------------------------------------------------------
B.c 文件中调用上述函数。由于 static , B.c 不能使用 test09函数,会报错!
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>
// void test09(void); //声明函数
extern void test09(void); //声明函数
int main(void)
{
test09();
system("pause");
return EXIT_SUCCESS;
}
生命周期
- 局部变量:定一位置开始,函数调用结束(存储在栈stack上)。 —— 函数被调用期间。
- 全局变量:从程序启动开始(早于 main() 函数),程序终止结束 。—— 程序执行期间。
- static局部变量:从程序启动开始,程序终止结束(定义在全局位置)。—— 程序执行期间。
- static全局变量:从程序启动开始,程序终止结束。—— 程序执行期间。
- 全局函数:从程序启动开始,程序终止结束。—— 程序执行期间。
- static函数:从程序启动开始,程序终止结束。—— 程序执行期间。
命名冲突
如果全局变量和局部变量命名冲突。采用就近原则。
强烈不推荐!
内存4区模型
- 代码段:.text段。 存储程序源码(二进形式)
- 数据段:只读数据段 .rodata。 初始化数据段 .data。未初始化数据段 .bss。
- stack:栈。在其之上开辟栈帧。(较小:windows:1M--10M, Linux:8M---16M)
存储特性:后进先出 FILO (LIFO) - heap:堆。 给用户自定义提供空间。(较大:约1.3G+-)

Heap堆空间
开辟/释放 heap 空间
- 在 heap 上开辟空间
- malloc创建空间的单位是字节
- malloc返回的是void类型的指针,没有步长的概念,也无法获取空间中的数据,需要强转
- malloc返回的仅仅是首地址,没有总大小,最好定义一个变量记录总大小
- malloc申请的空间不会自动消失,如果不能正确释放,会导致内存泄露
- malloc申请的空间过多,会产生虚拟内存5
- malloc申请的空间没有初始化值,需要先赋值才能使用
- free释放完空间之后,空间中数据叫做脏数据,可能被清空,可能被修改为其他值
- ca1loc就是在malloc的基础上多一个初始化的动作
- realloc修改之后的空间,地址值有可能发生变化,也有可能不会改变,但是原本的数据不会丢失.如果内存中已经无法申请空间了,会返回NULL
- realloc修改分配的内存大小之后,无需释放原来的空间,函数底层会进行处理
- 如果内存地址没变,底层在原来空间的后面接着申请的
- 如果内存地址变了,申请一个新的大的空间,把原来的数据拷贝到新的空间当中,再把原来的空间给free掉
#include <stdlib.h>
void *malloc(size_t size); // 向 系统申请内存空间,在 heap 上。单位:字节。
参数:申请空间的大小。
返回值:
成功:heap上内存空间的首地址。
失败:NULL
// 申请成功的内存,通常拿来当成 “数组” 使用
- 释放 heap 申请的空间
void free(void *ptr);
参:就是 malloc 函数的返回值。
int main(void)
{
// int arr[10];
// 申请能存储 10 个 int 数的空间。 40字节。
int *p = (int *)malloc(sizeof(int)*10); // 强转的目的,方便阅读代码。
if (p == NULL)
{
printf("malloc error\n");
return -1; // 退出程序。 -1,非正常结束。
}
// 写 - 数据到 malloc 申请的空间
for (size_t i = 0; i < 10; i++)
{
p[i] = i + 10; // 存 数据到 malloc 申请的空间中。
//* (p+i) = i + 10;
}
// 从malloc申请的空间中,读数据。
for (size_t i = 0; i < 10; i++)
{
printf("%d ", *(p+i));
}
printf("\n");
// 释放malloc申请的空间。
free(p);
system("pause");
return EXIT_SUCCESS;
}
使用 heap空间注意事项
- 申请的heap堆内存空间连续。当成 “数组” 使用。
- free 后的空间,不会立即失效。通常将 free后的地址,置为 NULL。
- free 地址必须是mallo函数返回的地址。否则,报错!
- 如果malloc 后的地址一定会变化,通常使用临时变量 tmp 保存。
二级指针对应的 heap 空间
int **p = int *p[3] ==> [int *, int *, int *] ==> [ [1, 2, 3, 54, 5] , int *, int *]
int main(void)
{
// 给外层空间malloc申请内存
int** p = malloc(sizeof(int *) * 3);
if (p == NULL)
{
printf("malloc error!\n");
return -1;
}
//给 内层指针 申请 malloc 空间
for (size_t i = 0; i < 3; i++)
{
p[i] = malloc(sizeof(int) * 5);
if (p[i] == NULL)
{
printf("malloc p[i] error!\n");
return -1;
}
}
// 使用空间 -- 写
for (size_t i = 0; i < 3; i++)
{
for (size_t j = 0; j < 5; j++)
{
p[i][j] = i + j; // 随意初始化值。
}
}
// 使用空间 -- 读
for (size_t i = 0; i < 3; i++)
{
for (size_t j = 0; j < 5; j++)
{
printf("%d ", *(*(p + i) + j)); // p[i][j] == *(p+i)[j] == *(*
(p+i)+j)
}
printf("\n");
}
// free空间时,应该先释放 内层空间,再释放外层
for (size_t i = 0; i < 3; i++)
{
free(p[i]);
p[i] = NULL;
}
// 释放外层空间
free(p);
p = NULL;
system("pause");
return EXIT_SUCCESS;
}
以 char **p 为例
// 先申请外层指针。
char **p = malloc(sizeof(char *) * 5);
// 申请内层指针
for (int i = 0; i<5; i++)
{
p[i] = malloc(sizeof(char) * 10); // 字符串长度 <= 10个字符。
}
// 写
for (int i = 0; i<5; i++)
{
// p[i] = "hello"; 错误!
strcpy(p[i],"hello");
}
// 释放内层
for (int i = 0; i<5; i++)
{
free(p[i]);
p[i] = NULL;
}
// 释放外层
free(p);
p = NULL;
内存操作函数
- 以下 4 个函数,专门用来操作 heap 内层。 stack 由系统自动申请,自动释放。
memset
#include <string.h>
void *memset(void *s, int c, size_t n);
参1:内存首地址。
参2:置成什么。一般传 0
参3: 内存大小。单位:字节。
返回值:
成功:设置后的地址。
失败:NULL
memset(首地址,0,空间大小);
绝大多数,memset 用来将申请好的heap 内存,置 0。 —— 单位 : 字节。
memset 函数 按 “字节” 设置。
置0 ——> 每一个字节都为 0.
【了解】置1 ——> 每一个字节都为 1. —— 4字节 == 0x01010101 == 16843009
int* p = (int *)malloc(sizeof(int) * 10);
if (p == NULL)
{
printf("malloc error");
return -1;
}
// 将申请好的内存,全部置 0
memset(p, 0, sizeof(int) * 10);
// 直接打印申请好的空间内容。
for (size_t i = 0; i < 10; i++)
{
printf("%d ", p[i]);
}
free(p);
p = NULL;
memcpy
以字节为单元,内存拷贝。 strcpy ——> 只能拷贝字符串。void *memcpy(void *dest, const void *src, size_t n);
// 拷贝内存中的整型数据
int arr[10] = { 1, 2, 3, 4, 5 };
int arr2[10];
memcpy(arr2, arr, sizeof(int)*10);
for (size_t i = 0; i < 10; i++)
{
printf("%d ", arr2[i]);
}
printf("\n");
// 拷贝内存中的字符串
char str[] = "hello world";
char p[100];
//memcpy(p, str, strlen(str)+1); // 按内存拷贝
strcpy(p, str); // 字符串拷贝
strncpy(p, str, strlen(str) + 1);
printf("p = %s\n", p);
memmove
作用 完全等同于 memcpy。 以字节为单元,内存拷贝。---- 安全的!
拷贝的 src 和 dest 之前如果有重叠,memcpy由于底层实现原因,有可能出错。推荐使用
memmove。void *memmove(void *dest, const void *src, size_t n);
memcmp
以字节为单位,比较内存!
作用可以完全参照,strncmp。
规则:s1 == s2 --> 0, s1 > s2 ---> 1 s2 < s2 ---> -1int memcmp(const void *s1, const void *s2, size_t n);
int arr1[] = { 14, 56, 22, 59, 21, 49, 25, 86, 89, 66, 71, 31, 98 };
int arr2[] = { 14, 56, 29, 59, 21};
int ret = memcmp(arr1, arr2, 5 * sizeof(int));
printf("ret = %d\n", ret);
内存常见问题
申请 0字节空间
C 语言中,允许申请 0 字节的内存空间。
0 字节的空间,不能拿来使用。free 空指针
空NULL指针,反复free, 不会报错。
非空的指针,反复free,会报错!!! —— 推荐 free 后的 指针,一定 置 NULL。越界访问
不允许!!!导致程序崩溃。free ++ 后的地址
不能正常释放。
如果程序中,必须要使用 p++,定义临时变量,保存p值。以便free释放。什么是内存?软件在运行时,用来临时存储数据的操作系统会把内存按照字节划分为N多的小格子
什么是内存地址?概念:在内存中申请空间的编号32位操作系统:以32位的二进制表示64位操作系统:以64位的二进制表示书写的时候:转成十六进制
内存地址的作用?快速的操作内存中的数据
C语言中如何获取内存地址?&.变量名
数组内存地址?是第一个元素的第一个字节空间的地址,也是首地址;索引:偏移量,通过变量的首地址,就可以确定变量中存储的数据,数据=首地址+偏移量
sizeof(arr);//8 64位的操作系统当中,是以64个二进制表示内存地址值
函数内申请空间使用。
注意上述 4 点。
A函数内申请空间,A函数使用
int *p = malloc(10);
free(p); p = NULL;
A函数内申请空间,B函数使用
// A函数申请空间, B函数使用。
int* func1(void)
{
int* p = malloc(4);
*p = 456;
return p; // 返回的 heap 堆空间的地址值,函数调用结束,地址 有效。
//return &p; // 返回的 stack 栈空间的地址值,函数调用结束,地址 无效。
}
int main(void)
{
int* ret = NULL;
ret = func1();
printf("%d\n", *ret);
free(ret);
ret = NULL;
system("pause");
return EXIT_SUCCESS;
}
// A函数申请空间, B函数使用。
int* func2(int *p)
{
p = malloc(4);
return p;
}
int main(void)
{
int* ret = NULL;
ret = func2(ret);
*ret = 345; // 写
printf("%d\n", *ret); // 读
free(ret);
ret = NULL;
system("pause");
return EXIT_SUCCESS;
}
void func3(int** p) // int** 是一级指针的地址。
{
*p = malloc(4);
}
int main(void)
{
int* ret = NULL;
func3(&ret); // 传一级指针的地址。
// func3 函数,调用完成, ret指针,不再为 NULL ,而是指向一块有效的 heap 空间地址。
*ret = 789;
printf("%d\n", *ret);
free(ret);
ret = NULL;
system("pause");
return EXIT_SUCCESS;
}
结构体
结构体定义语法
- 复合类型:用户自定义类型:int [] 、int *、char **、 struct student
// 定义结构体类型
struct student {
int age; // 成员变量 —— 属性。不能被赋初值。
int num;
char name[10];
};
// 用结构体类型,定义变量。
struct student a; // 定义了一个结构体类型的变量 a。
typedef struct GirlFriend
{
成员1;
成员2;
} 别名;
以上定义了一个结构体类型 。名字叫 struct student。
struct student 的地位 等同于 int、char、short、char *、int[]、long long
通常 结构体类型定义在 全局位置。 或者 放到 xxx.h 头文件。
头文件:
// 头文件守卫
#ifndef _XXX_H_
#define _XXX_H_
4种:#include、宏定义、函数声明、类型定义 (结构体类型)。
#endif
普通结构体变量
定义语法
struct student stu1, stu2, stu3; // 一次定义3个变量。没赋初值。
struct student stu = {18, 1, "Andy"};
访问成员方法
- 使用 “.” 访问成员。
// 定义一个结构体变量,赋初值。
struct student stu = {18, 1, "Andy"};
printf("first:age = %d, name = %s, num = %d\n",stu.age, stu.name, stu.num);
stu.age = 118;
stu.num = 119;
//stu.name = "cuihua"; // name为地址常量,不能被赋值。
strcpy(stu.name, "cuihua");
printf("last:age = %d, name = %s, num = %d\n", stu.age, stu.name, stu.num);
- 普通变量使用 “->” 访问成员 ---- 不常用!
(&stu)->age = 118;
(&stu)->num = 119;
结构体指针变量
定义语法
struct student *p1, *p2, *P3; // 一次定义3个指针变量。 野指针!!!
访问成员方法
- 使用 “->” 访问成员。
- 避免野指针、空指针:
struct student stu, *p1; //一次定义两个结构体变量,一个普通变量stu,另一指针
变量p1
p1 = &stu; // 给指针初始化。
struct student *p1;
p1 = (struct student *)malloc(sizeof(struct student));
指针使用 “.” 访问成员。 ---- 不常用!
(*p1).age = 18;
strcpy((*p1).name, "cuihua");
(*p1).num = 119;
非常规定义语法(了解)
struct student {
int age;
int num;
char name[10];
}s1, *s2; // 定义结构体类型的同时,定义1个结构体变量 s1,一个指针变量s2
struct { // 匿名结构体
int age;
int num;
char name[10];
}s3, *s4; // 定义匿名结构体类型的同时,定义1个结构体变量 s3,一个指针变量s4。
// 无法再定义其他变量。
结构体数组
struct student {
int age;
int num;
char name[10];
};
struct student stu[5] = {{18, 1, "Andy"}, {19, 2, "Lucy"}, {118, 3, "李四"}};
int n = sizeof(stu) / sizeof(stu[0]);
for (int i = 0; i<n; i++)
{
printf("age=%d,num=%d,name=%s\n", stu[i].age, stu[i].num, stu[i].name);
}
- struct student *stu; // 要求 指针指向能存储3个student 元素的空间,并给3个元素赋初值。访问。
struct student *stu; // 野指针
// 得到的heap堆空间,当成数组使用。
stu = malloc(sizeof(struct student)*3); // 等价于 struct student stu[3];
// 给数组的第1个元素赋值。
stu[0].age = 11;
stu[0].num = 111;
strcpy(stu[0].name, "aaa");
// 给数组的第2个元素赋值。
stu[1].age = 22;
stu[1].num = 222;
strcpy(stu[1].name, "bbb");
// 给数组的第3个元素赋值。
stu[2].age = 33;
stu[2].num = 333;
strcpy(stu[2].name, "ccc");
//int n = sizeof(stu) / sizeof(stu[0]); // 不能求元素个数。
for (int i = 0; i < 3; i++)
{
printf("age=%d,num=%d,name=%s\n", stu[i].age, stu[i].num, stu[i].name);
}
free(stu);
stu = NULL;
结构体嵌套
struct person {
int age;
char name[10];
}; // 类型
struct student {
struct person man; // person 类型的变量,作为 student 类型成员。
int id;
char addr[100];
};
int main(void)
{
struct student stu = {{18, "zhaoliu"}, 1, "北京朝阳区"};
printf("age = %d\n", stu.man.age);
printf("name = %s\n", stu.man.name);
printf("addr = %s\n", stu.addr);
// 修改
stu.man.age = 119;
strcpy(stu.man.name, "张三丰");
strcpy(stu.addr, "武当山");
printf("\nage = %d\n", stu.man.age);
printf("name = %s\n", stu.man.name);
printf("addr = %s\n", stu.addr);
system("pause");
return EXIT_SUCCESS;
}
做函数参数、返回值
结构体变量赋值
- 主要应用于,函数调用期间,实参给形参赋值。
- 要求:结构体变量赋值时,必须类型相同、成员个数一致,顺序一致。
做参数、返回值
- 传值:结构体变量做函数参数,将结构体变量的值(实参),拷贝一份给 形参。形参、实参 共 2 份结构体。
- 传址:结构体指针变量做函数参数, 将结构体的地址值做实参,拷贝一份给形参。形参、实参 共 1 份结构体。
- 结论:结构体做函数参数、返回值时,通常采用 “传址” 方式,节省空间。
要求:
定义一个函数,修改学生中的数据
int main(){
// 1.定义一个学生
S stu;
//2.给学生赋初始值
strcpy(stu.name,"aaa");stu.age = 0;
//3.输出打印
printf("学生的初始数据为:%s,%d\n",stu.name,stu.age);
//4.调用函数修改学生中的数据
method(stu);
//5.输出打印printf("学生的信息修改为:%s,%d\n",stu.name,stu.age);
return 0;
}
// 细节:
//如果函数中写的是结构体类型的变量,相当于是定义了一个新的变量
//此时是把main函数中stu中的数据,传递给了method函数,并把stu中的数据赋值给了新的变量st
//我们在函数中,仅仅是修改了变量st中的值,对main函数中stu的值,是没有进行修改的
void method(s st)
{
printf("接收到main函数中学生的初始数据为:%s,%d\n",st.name,st.age);// aaa 8
// 修改
printf("请输入要修改的学生名字\n");
scanf("%s"st.name),printf(“请输入要修改的学生年龄\n");
scanf("%d",&(st.age));
printf("在method函数中修改之后,学生的信息为:%s,%d\n",st.name,st.age);//zhangsan 23
}
//如果要在函数中修改stu的值,此时就不要再定义新的变量了
//直接接收stu的内存地址,通过内存地址就可以修改stu中的数据了
//指针p里面记录的是main函数中stu的内存地址(stu 学生)
void method2(s*p){
printf("接收到main函数中学生的初始数据为:%s,%d\n",(*p).name,(*p).age);// aaa 0
// 修改
printf("请输入要修改的学生名字\n");
scanf("%s"(*p).name);printf("请输入变修改的学生年龄\n");
scanf("%d"&((*p).age));
printf("在method函数中修改之后,学牛的信息为:%s,%d\n",(*p).name,(*p).age);// zhangsan 23
}
void func08(struct student **m)
{
*m = malloc(sizeof(struct student));
if (NULL == *m)
{
printf("malloc error\n");
return -1;
}
//p->age = 100;
//p->num = 1;
//strcpy(p->name, "zyx");
(*m)->age = 100;
(*m)->num = 1;
strcpy((*m)->name, "zyx");
}
int main(void)
{
struct student* p = NULL; // 空指针
func08(&p);
//p->age = 100;
//p->num = 1;
//strcpy(p->name, "zyx");
printf("age=%d, name=%s, num=%d\n", p->age, p->name, p->num);
free(p);
p = NULL;
system("pause");
return EXIT_SUCCESS;
}
含有指针成员的结构体
- 申请内存:先申请外层空间,再申请内层空间。
- 释放内存:先释放内存空间,在释放外层空间。
struct student {
int age;
int num;
char *name; // 野指针。
};
int main(void)
{
struct student* p; //野指针。
// 给 p 初始化堆空间
p = malloc(sizeof(struct student));
if (NULL == p)
{
printf("malloc p error\n");
return -1;
}
// 给成员变量 name 开辟堆空间
p->name = malloc(sizeof(char) * 100);
if (NULL == p->name)
{
printf("malloc p->name error\n");
return -1;
}
// 写数据到结构体中
p->age = 100;
p->num = 10;
strcpy(p->name, "张三丰");
printf("age=%d, name=%s, num=%d\n", p->age, p->name, p->num);
// 先释放内层空间
free(p->name);
p->name = NULL;
free(p);
p = NULL;
return 0;
}
结构体内存对齐
- 确定变量位置:只能放在自己类型整数倍的内存地址上
- 最后一个补位:结构体的总大小,是最大类型的整数倍
- 把小的数据类型,写在最上面,大的数据类型,写在最下面(节约空间)
struct num{
double a;//8
char b;//1+3
int c;//4
char d;//1+7
}
typedef 关键字
- 给现有的数据类型起别名。 【注意】:不能定义新数据类型。
typedef unsigned int size_t; // 给 unsigned int 起别名叫 size_t
int a; a 是变量名。
typedef int a; a 变成了 类型名。 a b; 定义一个整型变量 b。(可读性差)
- 通常使用 typedef 定义过的类型,添加一个 “_t” 结尾。
- 定义语法:typedef 旧类型名 新类型名_t ;
typedef struct student {
int age;
int num;
char *name; // 野指针。
} stu_t; // 新类型名:stu_t;
// 定义变量
struct student stu1; // 依然可以正常使用
stu_t stu2; // 定义一个 struct student 类型的变量。
使用 typedef 的好处:
- 简化类型名。
- 便于代码的修改和维护。
typedef long long int32_t; // int 《----》 long long
struct student {
int age;
int32_t num;
char *name; // 野指针。
int32_t num1;
int32_t num2;
int32_t num3;
int32_t num4;
} stu_t;
共用体(联合体)
union test {
char ch;
short sh;
int var;
}; // 创建一个联合体类型。
特性:
- 内部所有成员变量的地址一致。等同于整个联合体的地址。
- 联合体的大小,是内部成员变量中,最大的那个成员的大小。(也受内存对齐影响)
- 修改其中一个成员的值,其他成员的值也跟着变化。
- 每次只能给一个变量进行赋值,因为第二次赋值时会覆盖原有的数据
typedef union test {
char ch;
short sh;
int var;
} test_t;
int main(void)
{
test_t obj;
obj.var = 0x87654321;
printf("&obj = %p\n", &obj);
printf("&obj.ch = %p\n", &obj.ch);
printf("&obj.sh = %p\n", &obj.sh);
printf("&obj.var= %p\n", &obj.var);
printf("sizeof(test_t) = %u\n", sizeof(test_t));
printf("var = %#x\n", obj.var);
printf("sh = %#x\n", obj.sh);
printf("ch = %#x\n", obj.ch);
obj.ch = 0xAA;
printf("var = %#x\n", obj.var);
printf("sh = %#x\n", obj.sh);
printf("ch = %#x\n", obj.ch);
system("pause");
return EXIT_SUCCESS;
}
结构体和共用体区别
- 存储方式:
- 结构体:各存各的
- 共用体:存一起,多次存会覆盖
- 内存占用
- 结构体:各个变量的总和(受内存对齐影响)
- 共用体:最大类型(受内存对齐影响)
枚举
- 语法:enum 枚举名 { 枚举常量 };
enum color {red, green, blue, black, pink, yellow};
- 枚举常量:必须是整型常量,不允许是浮点数。可以是负值。 默认值从 0 开始。后续常量较前一个+1;可以给任意一个常量赋初值,后续常量较前一个+1。
//enum color { red, green = -5, blue, black, pink = 18, yellow };
enum { red, green = -5, blue, black, pink = 18, yellow };
int main(void)
{
int flg = 2;
if (flg == blue)
{
printf("blue is -4\n");
}
else
{
printf("bule is not %d, blue=%d\n", flg, blue);
}
printf("red = %d, yellow = %d\n", red, yellow);
system("pause");
return EXIT_SUCCESS;
}
文件
系统文件
- scanf -- 键盘 -- 标准输入 -- stdin -- 0
- printf -- 屏幕 -- 标准输出 -- stdout -- 1
- perror -- 屏幕 -- 标准错误 -- stderr -- 2
以上 3 个文件,为系统文件。应用程序启动时,这3个文件被系统自动打开,程序执行结束,由系统自
动关闭 ( 隐式回收 )。
fclose(stdout); // 关闭文件。
printf("hello world\n"); // 报错!!!
文件分类
- 设备文件:(与硬件有直接关系)屏幕、键盘、网卡、声卡、显卡、扬声器 ......
- 磁盘文件:
- 文本文件:文件内容为 ASCII 码
- 二进制文件:文件内容为 二进制编码数据。
文件指针
- 普通指针
int *p; // 野指针。
p = &a; // 初始化方法1
p = malloc() // 初始化方法2
- 文件指针
FILE *fp; // 野指针。
- 文件指针,借助 “文件操作函数” 来改变 fp 为空、为野的情况!
- 举例: fopen() ---> 将 fp 变为 非野。
- 操作文件,可以使用的函数:fputc、fgetc、fputs、fgets、fread、fwrite 。。。
- 纯文本文件:r w a:windows里面的记事本能打开并且能读懂的,就是纯文本文件txt md lrc
- 二进制文件:rb wb ab:b:binary
文件操作一般步骤
- 打开文件:fopen() ——> FILE *fp;
- 读写文件:fputc、fgetc、fputs、fgets、fread、fwrite 。。。
- 关闭文件:fclose()
文件操作
打开文件
#include <stdio.h>
FILE *fp; // 野指针
FILE * fopen(const char * filename, const char * mode);
参1:待打开的文件名(访问路径)
参2:文件打开权限。(初学,值掌握前3个)
r:只读方式打开文件, 文件如果不存在,报错!存在,以只读方式打开。不能写
w:只写方式打开文件, 文件如果不存在,创建一个空文件。文件已经存在,清空并打开。要保证文件夹存在
w+:读、写方式打开文件,文件如果不存在,创建一个空文件。文件已经存在,清空并打开。
r+:读、写方式打开文件,文件如果不存在,报错!存在,以读、写方式打开。
a:以追加方式打开文件。
b:操作二进制文件使用的。(Windows)
返回值:
成功:返回打开文件的文件指针(fp) 【强调】:这个fp指针,不使用“解引用”操作数据。
失败:NULL
关闭文件
#include <stdio.h>
int fclose(FILE * stream);
参:打开的文件的fp( fopen() 返回值 )
返回值:
成功:0
失败:-1
示例:
int main(void)
{
FILE* fp;
// 打开文件
//fp = fopen("C:\itcast\test.txt", "r"); // 错误传参
//fp = fopen("C:\\itcast\\test.txt", "r"); // 正确传参
//fp = fopen("C:/itcast/test.txt", "r"); // 正确传参
//fp = fopen("C:/itcast/test2.txt", "r"); // 指定r打开,文件不存在,报错
fp = fopen("C:/itcast/test2.txt", "w"); // 指定w打开,文件不存在创建,存在,清空
if (fp == NULL)
{
perror("fopen error"); // printf("fopen error\n");
return -1;
}
// 读写文件。。。
// 关闭
int ret = fclose(fp);
printf("ret = %d, ----------finish\n", ret);
system("pause");
return EXIT_SUCCESS;
}
绝对、相对路径
绝对路径:从系统磁盘的盘符开始,找到待访问的文件的路径。
- windows下的书写方法:
- C:\Users\afei\Desktop\TTTTTT\01-复习.avi
- C:/Users/afei/Desktop/TTTTTT/01-复习.avi ---- 也 Linux 系统。
- windows下的书写方法:
相对路径:
如果在 VS 环境下,使用Ctrl+F5编译执行,文件的相对路径是相对于 day11.vcxproj 所在目
录位置。不是相对于 .c 文件。如果双击 .c 文件同级目录下的 Debug目录下的 xxx.exe文件,文件的相对路径是相对于
xxx.exe 所在的目录位置。如果 gcc 生成的xxx.exe 文件,运行。 文件的相对路径是相对于 xxx.exe 所在的目录位置。
按字符写文件 fputc
int fputc(int ch, FILE * stream); // 将指定一个字符,写入指定文件
参1:待写入的 字符
参2:打开的文件fp (fopen的返回值)
返回值:
成功:写入到文件中的那个字符的 ASCII
失败:-1
//练习:创建一个新文件,向该文件中写入 26 个大写英文字母。
int main(void)
{
char* filename = "03test.txt"; // 相对路径
int ret = 0;
char ch = 'A';
FILE* fp = fopen(filename, "w"); // 文件存在,会清空
if (NULL == fp)
{
perror("fopen error");
return -1;
}
// 循环写26个大写字符到 文件中。
while (ch <= 'Z')
{
ret = fputc(ch, fp);
ch++;
}
ret = fclose(fp);
printf("ret = %d, ----------finish\n", ret);
system("pause");
return EXIT_SUCCESS;
}
fputc 向文件中写字符时,文件读写指针(参照光标理解),会自动后移。
按字符读文件 fgetc
int fgetc(FILE * stream); // 从指定文件中,读取一个字符。
参:待读取的文件fp(fopen()的返回值)
返回值:
成功:实际读到的字符的ASCII
失败:-1

fgetc 在读取文件时,文件读写指针(参照光标理解),会自动后移。
文本文件,结尾处,系统会自动添加一个结束标记 EOF ---> -1 (#define EOF -1)
文件关闭时,系统自动添加。
// 利用 EOF 结束标记,按字符与读文件。
void read_file(void)
{
char* filename = "03test.txt"; // 相对路径
int ret = 0;
char ch = 0; // 存储读到的字符
FILE* fp = fopen(filename, "r"); // r方式打开现有文件。
if (NULL == fp)
{
perror("fopen error");
return -1;
}
// 从文件中读 字符
while (1)
{
ch = fgetc(fp);
//printf("ch = %c\n", ch);
if (ch == EOF) // 已经读到文件末尾。
{
break;
}
printf("ch = %c\n", ch); // 写到这,不读 EOF 结束标记。
}
ret = fclose(fp);
printf("ret = %d, ----------finish\n", ret);
}
feof 函数
int feof(FILE * stream); // 判断是否到达文件结尾。
参:fopen()返回值
返回值:
到达文件结尾 ---> 非0【真】
没到达文件结尾 ---> 0【假】
作用:用来判断文件是否到达结尾。既能判断文本文件,也能判断二进制文件。
特性:要想使用feof()判断到达文件结尾,在 feof() 调用之前,必须要有 读文件的函数调用。
FILE* fp = fopen("04test.txt", "r");
if (NULL == fp)
{
perror("fopen error");
return -1;
}
while (1)
{
printf("没有到达文件结尾\n");
// 没有这个fgetc函数读文件,feof函数,无法正常判断到达文件结尾。
fgetc(fp); // 一次读一个字符,读到的字符直接丢弃!
if (feof(fp))
{
break;
}
}
fclose(fp);
按行读文件 fgets
- 获取一个字符串, 以\n 作为结束标记。 自动添加 \0。空间足够大,读 \n, 空间不足,舍弃\n。一定会预留空间 存 \0
char * fgets(char *str, int size, FILE * stream);
参1:用来存储字符串的空间首地址
参2:空间大小
参3:数据来源的文件fp。
返回值:
成功:返回实际读到的字符串。
失败:NULL
// 示例:
char buf[10];
printf("%s", fgets(buf, 10, stdin)); "hello" ---> hello\n\0
printf("%s", fgets(buf, 10, stdin)); "helloworld" ---> helloworl\0
char arr[1024];
char* str;
while((str = fgets(arr,1024,fi1e))! NULL){
printf("%s", str);
}
按行写文件 fputs
写出一个字符串,到文件中。如果字符串中没有 \n, 不会写\n
int fputs(const char * str, FILE * stream);
参1:待写出的字符串首地址。
参2:写出到的文件fp。
返回值:
成功:0
失败:-1
// 示例:
char *str = “hello”;
fputs(str, stdout); ----> 不添加 \n
练习:
获取用户键盘输入,将所有内容,写入到文件。 规定:如果用户输入了 “:wq”, 终止接收用户输
入,将之前读到的数据,保存成一个文件。
从 stdin 中读入到程序中。写出到文件 fp。
feof() 在本题中用不上,用 strcmp 判断 读到的句子,是不是 “:wq\n”。
// 练习:接收用户键盘输入,写入文件。遇见 :wq 停止接收,保存成文件
int main(void)
{
// 创建文件,具备写权限
FILE* fp = fopen("05test.txt", "w");
if (NULL == fp)
{
perror("fopen error");
return -1;
}
// 创建一个空间,保存读到的数据内容
char buf[MAX] = { 0 };
// 从键盘读
while (1)
{
fgets(buf, MAX, stdin);
// 每读的一行数据,都判断是否是 ":wq\n"
if (strcmp(buf, ":wq\n") == 0)
{
break; // 用户输入了结束标志,终止读入,保存文件。
}
// 写入fp 文件中
fputs(buf, fp);
}
// 关闭文件。
fclose(fp);
system("pause");
return EXIT_SUCCESS;
}
练习:文件版四则运算
文件中有 表达式:
//四则运算.txt
10/2=
10*3=
4+3=
8-6=
读出表达式,运算,将结果写回文件。
//四则运算.txt
10/2=5
10*3=30
4+3=7
8-6=2
分析:
10/2= ---> fgets(buf, 4096, 四则运算.txt 对应的 fp) ----> “10/2=\n” ----> 10 / 2 =
strtok()、sscanf() ---> 选sscanf() 实现 ---> sscanf(buf, “%d%c%d=\n”, &a, &ch, &b) --->
a = 10, b = 2 , ch = ‘/’
switch (ch) {
case '/':
ret = a / b;
break;
case '*'
ret = a * b;
break;
...
}
fopen(“”, “w”) 清空原来只有表达式没有结果的文件。将带有结果的表达式直接覆盖。
拼接上述 字符串, 使用 sprintf() / strcat() --> “10/2=5\n10*3=30\n4+3=7\n8-6=2\n”
最终写出:char result[] “10/2=5\n10*3=30\n4+3=7\n8-6=2\n” ----fputs(result, fp)
实现
#define BUF_MAX 4096
// 写算式到文件中
void write_file06(void)
{
FILE* fp = fopen("C:/itcast/四则运算.txt", "w");
if (NULL == fp)
{
perror("fopen error");
return; // 退出函数调用。
}
fputs("10/2=\n", fp);
fputs("10*3=\n", fp);
fputs("4+3=\n", fp);
fputs("10-2=\n", fp);
fclose(fp);
}
// 从文件中读算式,提取、拆分、计算、写回
void read_file06(void)
{
// 创建存储算式的 空间
char buf[BUF_MAX] = { 0 };
// 创建空间,保存算式及运算结果
char str[BUF_MAX] = { 0 };
// 创建空间,保存 4 个带有结果的算式
char result[BUF_MAX] = { 0 };
// 定义变量,保存运算数和运算符, 结果
int a, b, ret;
char ch;
FILE* fp = fopen("C:/itcast/四则运算.txt", "r");
if (NULL == fp)
{
perror("fopen error");
return; // 退出函数调用。
}
// 循环读取文件中的算式
while (1)
{
fgets(buf, BUF_MAX, fp); // buf = "10/2=\n\0"
// 判断是否到达文件结尾。
if (feof(fp))
{
break;
}
// 将 sscanf 的返回值强转为 void
(void)sscanf(buf, "%d%c%d=\n", &a, &ch, &b); // a:10 b:2 ch:'/'
// 根据不同的运算符,做不同运算
switch (ch) {
case '/':
ret = a / b;
break;
case '*':
ret = a * b;
break;
case '+':
ret = a + b;
break;
case '-':
ret = a - b;
break;
default :
printf("运算符错误\n");
break;
}
// 拼接 ret 到 算式上
sprintf(str, "%d%c%d=%d\n", a, ch, b, ret); // 10/2=5
// 测试,是否能正常获取数据,拼接成带结果的表达式。
//printf("%s", str);
// 拼接 4 个子算式,到 一个 大空间中
strcat(result, str);
}
// 测试,拼接 4 个子算式,到 一个 大空间中,打印输出。
printf("%s", result);
fclose(fp);
// 清空原来没有结果的算式所在的文件。
fp = fopen("C:/itcast/四则运算.txt", "w");
if (NULL == fp)
{
perror("fopen error");
return; // 退出函数调用。
}
// 将既有算式,又有结果的字符串,写入到同一个文件中。
fputs(result, fp);
fclose(fp);
}
int main(void)
{
// 测试写出算式到文件。
write_file06();
getchar(); // 从键盘读一个字符,如果用户不输入,程序不继续,阻塞等。
// 测试读取算式
read_file06();
system("pause");
return EXIT_SUCCESS;
}
getchar() 函数
从键盘获取 一个字符。 返回 ASCII, 如果用户不输入,程序部向后执行。
putchar() 函数
向屏幕输出一个字符。
putchar('m');
格式化读写文件
fprintf()函数
printf ---- sprintf ---- fprintf
变参函数,参数列表中,有 “...”, 最后一个固定参数通常是一个模式描述串(包含格式匹配
符), 函数实际调用时传递的参数的个数、类型、顺序,由这个固参决定。
printf("hello");
printf("%s", "hello");
printf("%d = %d%c%d\n", 10+5, 10, '+', 5); ----> 屏幕
char buf[1024]; 内存空间 --- 缓冲区
sprintf(buf, "%d = %d%c%d\n", 10+5, 10, '+', 5) ----> buf 中
函数原型
#include <stdio.h>
int fprintf(FILE * stream, const char * format, ...); ----> 文件fp 中
测试
FILE* fp = fopen("abc", "w"); // 相对路径法
if (!fp) // NULL == fp
{
perror("fopen error");
return -1;
}
fprintf(fp, "%d%c%d=%d\n", 10, '+', 7, 10+7);
fclose(fp);
fscanf()函数
scanf ---- sscanf ----- fscanf
int m;
scanf("%d", &m); 键盘 ---> m
char str[] = "98";
sscanf(str, "%d", &m); str ----> m
FILE *fp = fopen("r");
fscanf(fp, "%d", &m); fp指向的文件中 ---> m
函数原型
int fscanf(FILE * stream, const char * format, ...);
测试:
int a, b, c;
char ch;
FILE* fp = fopen("abc", "r"); // 相对路径法
if (!fp) // NULL == fp
{
perror("fopen error");
return -1;
}
(void)fscanf(fp, "%d%c%d=%d\n", &a, &ch, &b, &c);
printf("a = %d\n", a);
printf("b = %d\n", b);
printf("c = %d\n", c);
printf("ch = %c\n", ch);
printf("%d%c%d=%d\n", a, ch, b, c);
fclose(fp);
格式化读写特性(扩展知识)
返回值
int fprintf(FILE * stream, const char * format, ...);
fsprintf() 函数的返回值:
成功:实际写入文件的字符个数。
失败:-1
int fscanf(FILE * stream, const char * format, ...);
fscanf() 函数的返回值:
成功:正确匹配的个数。
失败:-1
fscanf 读取特性
- 边界溢出问题。存储读取数据的空间,在使用之前,应该进行清空。否则会出现边界溢出异常。
清空:memset(buf, 0, sizeof(buf)); - fscanf 函数,每次在调用的同时,都会判断,下一次调用是否能成功匹配参2, 如果不匹配提前结
束读取文件( feof(fp) 为真 )
练习:文件版排序
生成随机数,写入文件。将文件内乱序随机数读出,排好序再写回文件。
// 生成随机数,写入文件
void write_rand(void)
{
FILE* fp = fopen("test03.txt", "w"); // 相对路径法
if (!fp) // NULL == fp
{
perror("fopen error");
return -1;
}
// 播种随机数种子
srand(time(NULL));
for (size_t i = 0; i < 10; i++)
{
//int num = rand() % 100; // 0 -- 99
fprintf(fp, "%d\n", rand() % 100); // 将生成的随机数写入到文件。
}
fclose(fp);
}
void BubbleSort(int* src, int len)
{
for (int i = 0; i < len - 1; i++)
{
for (int j = 0; j < len - 1 - i; j++)
{
if (src[j] > src[j + 1])
{
int temp = src[j];
src[j] = src[j + 1];
src[j + 1] = temp;
}
}
}
}
// 从文件中,读取随机数
void read_rand(void)
{
// 定义数组,存储 10 个随机数
int arr[10] = { 0 }, i = 0;
FILE* fp = fopen("test03.txt", "r"); // 相对路径法
if (!fp) // NULL == fp
{
perror("fopen error");
return -1;
}
// 循环读取文件内的随机数
while (1)
{
(void)fscanf(fp, "%d\n", &arr[i]);
i++;
if (feof(fp)) // 先存储,后判断,防止最后一个元素丢失。
break;
}
// 使用冒泡排序
BubbleSort(arr, sizeof(arr)/sizeof(arr[0]));
fclose(fp); //关闭随机数据的文件
// 清空 随机数据的文件
fp = fopen("test03.txt", "w");
if (!fp) // NULL == fp
{
perror("fopen error");
return -1;
}
for (size_t i = 0; i < 10; i++)
{
fprintf(fp, "%d\n", arr[i]); // 将排好序的数组写入到文件。
}
fclose(fp);
}
int main(void)
{
write_rand();
getchar(); // 从键盘获取一个字符,如果用户不输入,就阻塞程序,不向下执行。
read_rand();
printf("--------------finish\n");
system("pause");
return EXIT_SUCCESS;
}
按块读写文件
fgetc - fputc
fgets - fputs
fprintf - fscanf
以上3组函数,默认用来处理文本文件。
fwrite - fread 既可以处理文本文件,也可以处理二进制文件。
// 2.写出数据
//fputc 一次写一个字符,返回写出的字符
//a--- 97
int c = fputc(97,file);
printf("%c\n",c);
//fputs 一次写一个字符串,写出成功返回非负数,一般忽略返回值
//细节:因为如果写出失败,那么就会有一个E0F的错误
int n= fputs("你好你好”,file);
printf("%d\n”,n);
//fwrite 一次读多个,返回写出的字节个数
char arr[]={97,98,99,100,101};
fwrite(arr,1,5,file);
fwrite()函数
写出数据到文件中。
#include <stdio.h>
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
参1:待写出的数据的首地址。
参2:待写出数据的大小 (一次写多大)
参3:写出的个数 (写多少次) 写出数据的总大小 = 参2 x 参3
参4:文件fp
返回值:
成功:永远返回参3。 通常调用函数时,将参2传1,参3代表实际写出的字节数。
失败:0
// fwrite 函数,写入数据到文件中时,是按 二进制 写入。
测试
typedef struct student {
int age;
char name[10];
int num;
} stu_t;
// 按块写文件,fwrite
void write_struct(void)
{
// 定义结构体数组
stu_t stu[4] = {
18, "afei", 10,
20, "andy", 20,
30, "lily", 30,
16, "james", 40
};
FILE* fp = fopen("test04.txt", "w"); // 相对路径法
if (!fp) // NULL == fp
{
perror("fopen error");
return ;
}
int ret = fwrite(&stu[0], 1, sizeof(stu_t) * 4, fp);
if (ret == 0)
{
perror("fwrite error");
return ;
}
printf("ret = %d\n", ret);
fclose(fp);
}
fread()函数
- 从文件fp中读取数据。
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
参1:读取到的数据存放的地址。
参2:一次读取数据的字节数 (一次读多大)
参3:读多少次 读出数据的总大小 = 参2 x 参3
参4:文件fp
返回值:
成功:永远返回参3。 通常调用函数时,将参2传1, 参3代表实际读出的字节数。
0 : 1) 读失败
2) 到达文件结尾 == feof(fp)为真
// 2.读取数据
//英文的abc:占用1个字节
//中文:window64位当中,默认2个字节
//fread细节:
//在读取的时候,每次尽可能会把数组给装满,返回当前读取到的有效字节个数
// 文件:100个字节
// 数组长度:30
//第一到三次每次读取前面30个字节,把数组给装满,函数返回30
//第四次:读取剩余的10个字节,会把数据放在数组当中,函数返回10
// 第五次:没有数据可以被读取了,函数返回8
char arr[1024];
int n=fread(arr,1,1024,file);
printf("%d",nl);
printf("%s",arr);
char arr[4];
int n;
while((n=fread(arr,1.4,file))!= 0){
for(inti=0;i<n; i++){
printf("%c”, arr[i]);
}
}
- 测试
typedef struct student {
int age;
char name[10];
int num;
// char addr[100];
} stu_t;
// 按块读文件,fread, 一次读一个 stu_t 元素
void read_struct(void)
{
FILE* fp = fopen("test04.txt", "r"); // 相对路径法
if (!fp) // NULL == fp
{
perror("fopen error");
return;
}
stu_t s1;
// 从文件中,按块读取,
int ret = fread(&s1, 1, sizeof(stu_t), fp);
printf("ret = %d\n", ret);
printf("age=%d, name=%s, num=%d\n", s1.age, s1.name, s1.num);
fclose(fp);
}
// 按块读文件,fread, 一次读所有 stu_t 元素
void read_struct2(void)
{
FILE* fp = fopen("test04.txt", "r"); // 相对路径法
if (!fp) // NULL == fp
{
perror("fopen error");
return;
}
stu_t s1[10]; // stu_t *s1 = malloc(sizeof(stu_t) * 1024);
int i = 0;
// 从文件中,循环按块读取,
while (1)
{
int ret = fread(&s1[i], 1, sizeof(stu_t), fp);
//if (ret == 0) // 效果一样。
if (feof(fp)) // 效果一样。
{
printf("读取到文件结尾\n");
break;
}
printf("age=%d, name=%s, num=%d\n", s1[i].age, s1[i].name, s1[i].num);
i++;
}
fclose(fp);
}
练习:大文件拷贝
已知一个任意类型的文件,对该文件复制,产生一个相同的新文件。
实现思路:
- 打开两个文件, 一个 “r”, 另一个 “w”
- 从 r 中 fread,fwrite 写到 w 文件中
- 循环读,判断到达文件结尾,跳出循环。
- 关闭2个文件。
注意:在windows下,打开二进制文件(mp4、MP3、avi、jpg)读写,需要 “b”。 如:“rb”
、“wb”
int main(void)
{
// 创建一个缓存区(内存空间),用来存储读到的数据。
char buf[4096] = { 0 };
int ret = 0;
FILE* rfp = fopen("C:\\Users\\afei\\Desktop\\TTTTTT\\11-午后回顾.avi",
"rb");
if (!rfp)
{
perror("fopen error");
return -1;
}
FILE* wfp = fopen("mycopy.avi", "wb");
if (!rfp)
{
perror("fopen error");
return -1;
}
// 循环从文件中读, 写到另一个文件中。
while (1)
{
ret = fread(buf, 1, sizeof(buf), rfp);
if (ret == 0)
{
break; // 读到文件末尾。
}
printf("ret = %d\n", ret);
// 没有读到文件末尾,将实际读到的数据,“原封不动的”写入 wfp 中。
fwrite(buf, 1, ret, wfp);
}
fclose(rfp);
fclose(wfp);
system("pause");
return EXIT_SUCCESS;
}
//1.打开a.wmv文件(数据源)
FILE* file1 = fopen("c:\\Users\\1\\Desktop\la.wmv","rb");
//2.打开copy.wmv(目的地)
FILE* file2 = fopen("c:\\Users\\1\\Desktop\laaal\copy.wmv","wb");
//3.利用循环读取数据源,读完之后,再写到目的地
char arr[1024];
int n;
while((n=fread(arr,1,1024,file1))!= 0)
{
//就要把读取到的数据,再写到目的地文件当中
fwrite(arr,1,n,file2);
}
随机位置读写文件
强调:文件读写指针,在一个文件内,只有一个。读、写都使用这一个。
fseek
#include <stdio.h>
int fseek(FILE *stream, long offset, int whence); // 修改文件偏移量(文件读写指
针)
参1:文件fp
参2:偏移量(矢量:正数向后,负数向前)
参3:偏移的起始位置
SEEK_SET:文件开头位置
SEEK_CUR:当前位置
SEEK_END:文件结尾位置
返回值:成功:0 失败:-1
ftell
long ftell(FILE *stream); // 获取文件偏移量(文件读写指针位置)
返回值:从当前读写位置,到文件起始位置的偏移量。
// 借助 ftell(fp) + fseek(fp, -50, SEEK_END) 来获取文件大小。
rewind
void rewind(FILE *stream); // 回卷文件读写指针。将文件读写指针移动到起始位置。
// 相当于:fseek(fp, 0, SEEK_SET);
其他文件相关操作
Linux 和 Windows 文件区别
对于 二进制文件操作,Windows 下必须要使用 “b”, Linux下 二进制文件和 文件文件操作没区别。
windows下的回车换行 \r\n,回车 \r, 换行 \n。 Linux 下 回车换行 \n。
- 对文件指针,
先写后读可以直接操作。windows 和 Linux 一致。
先读后写。Linux无序修改。 Windows下需要在写操作之前,添加 fseek(fp, 0,
SEEK_CUR) 函数调用,获取文件读写指针,再来写。才能生效。
int main(int argc, char* argv[])
{
FILE* fp = fopen("test1.txt", "r+");
char buf[6] = { 0 };
char* ptr = fgets(buf, 6, fp);
printf("buf=%s, ptr=%s\n", ptr, buf);
fseek(fp, 0, SEEK_CUR); //获取文件读写指针。 如果没有这行。win下程序会崩溃。
int ret = fputs("AAAAA", fp);
printf("ret = %d\n", ret);
fclose(fp);
return 0;
}
获取文件状态
ftell(fp) + fseek(fp, 0, SEEK_END) 可以获取文件大小。此种方法获取文件大小,必须要打开文
件,文件打开、关闭操作,对于系统而言,系统资源消耗较大。
#include <sys/types.h>
#include <sys/stat.h>
int stat(const char *path, struct stat *buf); // status
参1:文件访问路径
参2:文件属性结构体指针(传出参数:函数调用结束时,充当函数返回值)
返回值:成功:0 失败:-1
// 示例:
struct stat buf;
stat("待打开文件", &buf);
buf.st_size 获取文件的实际大小。
获取文件大小
struct stat buf;
int ret = stat("test06.txt", &buf); // buf传出参数
printf("ret = %d\n", ret);
printf("获取文件的大小为:%d\n", buf.st_size);
删除、重命名文件
int remove(const char *pathname); // 删除
int rename(const char *oldpath, const char *newpath); // 重命名
// 重命名:
//int ret = rename("test06.txt", "哼哼哈嘿.txt");
//printf("ret = %d\n", ret);
// 删除文件
int ret = remove("哼哼哈嘿.txt");
printf("ret = %d\n", ret);
缓冲区刷新
标准输出 -- stdout -- 标准输出缓冲区。
写给屏幕的数据,都是先存入缓冲区中,由缓冲区一次性刷新到物理设备(屏幕)
标准输入 --- stdin -- 标准输入缓冲区
从键盘读取的数据,直接读到缓冲区,由缓冲区给程序提供数据。
缓冲机制:
- 行缓冲:遇到 \n 刷新缓冲区的数据到物理设备上。printf();
- 全缓冲:缓冲区存满,数据才刷新到物理设备上。文件。
- 无缓冲:缓冲区中只要有数据,立即刷新到物理设备。perror
手动刷新缓冲区的方法:
#include <stdio.h>
int fflush(FILE *stream);
参:文件fp
返回值:成功:0 失败:-1
当文件关闭时,会强制刷新缓冲区,写入磁盘。—— 隐式回收
隐式回收:关闭文件。刷新缓冲区。释放malloc申请的内存。
int main(void)
{
FILE* fp = fopen("test10.txt", "w+");
if (!fp)
{
perror("fopen error");
return -1;
}
char ch = 0;
while (1)
{
(void)scanf("%c", &ch);
if (ch == ':')
{
break;
}
fputc(ch, fp);
// 手动刷新缓冲区,写入物理磁盘。
fflush(fp);
}
fclose(fp); // 当文件关闭时,会强制刷新缓冲区,写入磁盘。
system("pause");
return EXIT_SUCCESS;
}