目录
第一章 计算机基础知识
1. 计算机组成
计算机由硬件系统和软件系统所组成,没有安装任何软件的计算机称为裸机
2. DOS系统
DOS,是磁盘操作系统的缩写,是个人计算机上的一类操作系统,通过命令行窗口来操作计算机。早期的Windows系统就是基于DOS演进而来的,现在DOS作为Windows系统安装后自带的程序存在
它主要应用在:a.当Windows系统异常时,可通过DOS系统修复,b. 在DOS窗口中执行某些命令,比如ping,c. 一些脚本的运行,通过DOS运行会更好
这里有一些DOS常见命令,如下:
- cd 路径,表示切换路径
- dir 目录,查看指定目录下所有文件
- md 盘符:路径/文件,新建目录或者文件
- ipconfig,查看本地IP配置信息
- rd 盘符:路径/目录名,删除空目录
- del 盘符:路径/文件名,删除文件
- ren 旧文件名 新文件名
- cls 清屏
3. window系统
Windows系统是一个图形化界面操作系统,用户与图形用户界面交互比学习复杂的命令语言更容易,优点有:a.可浏览,b.使用方便,c.多任务运行
4. 冯洛伊曼体系结构
推导过程
计算机的作用就是为了解决人的问题,而要解决问题,首先需要将数据或是问题输入到计算机当中,所以计算机必须要有输入设备。计算机解决完问题后还需要将计算结果输出显示出来,所以计算机必须要有输出设备。计算机通过输入设备得到数据,数据在计算机当中进行一系列的算术运算和逻辑运算后,通过输出设备进行输出,于是就得到了以下流程图
但是计算机当中只有算术运算功能和逻辑运算功能是不够的,还需要有控制功能,控制何时从输入设备获取数据,何时输出数据到输出设备等。对应到C语言当中,算术运算就完成一系列的加减乘除,而逻辑运算就对应于一系列的逻辑与逻辑或等,控制功能就对应于C语言当中的判断、循环以及各个函数之间的跳转等等。
而我们后人就将这个具有算术运算功能、逻辑运算功能以及控制功能的这个模块称为中央处理器,简称CPU
但是输入设备和输出设备相对于中央处理器来说是非常慢的,于是在当前这个体系整体呈现出来的就是,输入设备和输出设备很慢,而CPU很快,根据木桶原理,那么最终整个体系所呈现出来的速度将会是很慢的
所以当前这个体系结构显然是不合适的,于是我们就不让输入设备和输出设备直接与CPU进行交互,而在这中间加入了内存
内存有个特点就是,比输入设备和输出设备要快很多,但是比CPU又要慢。现在内存就处于慢设备和快设备之间,是一个不快也不慢的设备,能够在该体系结构当中就起到一个缓冲的作用。
现在该体系的运行流程就是:用户输入的数据先放到内存当中,CPU读取数据的时候就直接从内存当中读取,CPU处理完数据后又写回内存当中,然后内存再将数据输出到输出设备当中,最后由输出设备进行输出显示
于是就形成了最终的冯诺依曼体系结构
5. 计算机网络
5.1 分类
5.2 BS模型
浏览器/服务器,浏览器将请求发送给Web服务器,Web服务器对请求进行处理,将响应发回浏览器
5.3 CS模型
客户端/服务器,客户端向服务器发出请求,服务器处理请求并将响应发送给客户端
5.4 IP地址
唯一标识网络上的每一台计算机,IP地址分为IPV4和IPV6两个版本,目前使用的是IPV4,IPV4地址由4个8位二进制数组成,总共32位;IPV6是由128位二进制组成 为了理解方便,IP地址通过点分十进制来表示,即把每8位二进制转换成十进制数
5.5 HTTP
请求消息格式
四大组成:请求行 请求头 空行 请求数据
响应消息格式
四大组成:状态行、响应头、空行,响应正文
get和post请求方法的区别
区别一:GET经常用于获取数据,POST经常用于提交数据;
区别二: GET方法提交数据时将数据拼接到URL中,客户端地址栏可见(get方式没有请求体);而POST方法提交数据时将数据置于消息主体内(post方式有请求体);
区别三:不同浏览器对URL的长度有限制,所以GET请求传送的数据量是有限的,而理论上POST请求传送的数据量是不受限制的
URL格式
"http://106.13.199.209:8080/korei/login.html "
- 协议:http,主机:106.13.199.209,端口:8080,路径:/korei/login.ht
HTTP响应状态码
分类 | 含义 |
1** | 信息,服务器收到请求,需要请求者继续执行操作 |
2** | 成功,操作被成功接收并处理 |
3** | 重定向,需要进一步的操作以完成请求 |
4** | 客户端错误,请求包含语法错误或无法完成请求 |
5** | 服务器错误,服务器在处理请求的过程中发生了错误 |
6. 前端与后端
前端:技术层面-用户交互,用户使用的页面或者客户端,web,html+css+js,C++Qt客户端
后端:技术层面-处理逻辑和保存报错,C/C++ Java Python mysql oracle 达梦数据库
7. 前台与后台
前台系统:面向商业用户
后台系统:企业内部用户
8. 机器/汇编/高级语言
机器语言:利用二进制编码进行指令的编写
汇编语言:以缩写英文作为标符编译
高级语言:多种编程语言结合之后,可以对多条指令进行整合,
在操作细节以及中间过程等方面做了适当的简化
机器是无法识别高级语言的,高级语言最终还是会编译为机器语言(0和1的二进制)
9. 了解jdk和Maven
说明一下:
- java语言的软件开发工具包,其中包含JRE(Java虚拟机)
- Maven就是一款帮助程序员构建项目的工具
10.数据格式json
优点:学习成本低,操作过程简单,上手难度低
API协同工具推荐:apifox,它能让后端人员在不知道前端的情况下,自己测试自己的代码效果,发请求-返回json数据
11. REST软件架构风格(了解)
12. Nginx组件
三大作用:1. 反向代理 2.负载均衡 3.动静分离
12.1 正向代理
正向代理:代理的是客户端,让我们能正常访问目的服务器,对于服务器来说所有请求来自一台服务器
说明一下:
- 我们需要在客户端配置代理服务器,此时将代理服务器和客户端看成一个客户端
- 这样服务器就不知道是那个客户端发送的请求,通过代理服务器进行访问,这就是正向代理
12.2 反向代理
反向代理:代理的是服务器,让大量的请求均衡的访问到某一台服务器上,对于客户端来说,响应来自一台服务器
客户端对反向代理是无感知的,因为客户端不需要任何配置就可以访问,我们只需要将请求发生到反向代理服务器上,再由反向代理服务器去选择目标服务器获取数据后,再返回给客户端
12.3 负载均衡
负载均衡:是指通过某种算法将流量分发到多个服务器上,以确保系统稳定性和高效性的技术手段
常见的策略:
- 轮询(默认),就是全部遍历一遍
- 权重,就是按照一定的比例分配数据包
- ip_hash,相同的数据包让同一个处理过数据包的IP服务器继续处理(建立的映射)
12.4 动静分离
这个是已经被淘汰的技术,当时是为了加快网站的解析速度,可以把动态网页和静态网页分离由不同的服务器来解析,加快解析速度,减低原来单个服务器的压力
第二章 C/C++介绍
1. C/C++能做哪些东西
嵌入式开发 C++服务器 系统软件 开发驱动 游戏开发 偏硬件 偏底层
2.对比C/C++和JAVA
-
C/C++,JAVA都是高级语言,偏向自然语言
-
C的程序c++可以直接运行,C面向过程,C++面向对象,JAVA面向对象
- C/C++偏硬件,偏汇编语言,编译效率更高,相对JAVA运行效率偏低
3.C/C++开发环境
C/C++不能跨平台(但QT可以跨平台),所以不同的操作系统开发的C/C++程序无法直接使用,而嵌入式多在linux中运行,所以做嵌入式开发需要linux环境
推荐:在windows上安装虚拟机,再安装ubuntu
当然直接使用vs2019这种集成开发环境也是没有问题的,比较无脑,我也比较喜欢
4. ubuntu安装(了解)
4.1 VM三种网络模式(难点)
Bridged(桥接模式)
原理:
- 虚拟机直接连接到物理网络,就像局域网(LAN)中的一台真实计算机
- VMware 创建一个“桥接”,让虚拟网卡(vNIC)连接到宿主机的物理网卡(pNIC),相当于让虚拟机和物理机共享同一个网络接口
- DHCP 服务器(如路由器)自动分配 IP 地址(或手动配置),虚拟机和局域网的其他设备处于同一子网
- 数据包在物理网络中直接传输,虚拟机和其他设备可以互相访问,不需要额外的 NAT 或代理
特点:
- 虚拟机直接连接到物理网络,与宿主机(Host)处于同一网段,就像局域网中的一台独立计算机
- 虚拟机的 IP 地址由物理网络的 DHCP 服务器(如路由器)分配,或者手动配置
- 与外部网络完全互通,虚拟机可以直接访问互联网和局域网中的其他设备,其他设备也能直接访问虚拟机
NAT(地址转换模式)
原理:
- VMware 提供一个虚拟 NAT 设备(
vmnet8
),所有 NAT 模式下的虚拟机通过这个设备共享宿主机的网络连接 - 虚拟机使用私有 IP 地址(通常是
192.168.8.x
) - 宿主机作为网关,使用 NAT(网络地址转换) 技术,让虚拟机的数据流量通过宿主机访问外部网络
- 外部设备无法直接访问虚拟机,因为 NAT 只允许出站连接,入站连接默认被阻止(除非手动配置端口转发)
特点:
- 虚拟机通过宿主机的网络访问外部网络,但外部设备不能直接访问虚拟机
- VMware 提供一个虚拟 NAT 设备,为虚拟机分配一个私有 IP 地址(通常是
192.168.x.x
或10.x.x.x
) - 宿主机充当网关,负责转发虚拟机的流量到外部网络,并进行 NAT 转换
Host-Only(仅主机模式)
主机模式与NAT模式相比,没有虚拟的NAT设备
原理:
- VMware 创建一个独立的虚拟网络(
vmnet1
),仅允许虚拟机和宿主机通信,无法访问外部网络 - 所有 Host-Only 网络的虚拟机使用 VMware DHCP 服务器分配的 IP 地址(如
192.168.56.x
) - 外部设备无法访问虚拟机,也不能直接上网(除非手动配置宿主机作为网关)
特点:
- 虚拟机只能与宿主机和其他 Host-Only 网络中的虚拟机通信,无法访问外部网络(除非手动配置 NAT 或端口转发)
- VMware 创建了一个专用的虚拟网络,并由 VMware 提供 DHCP 服务器,为虚拟机分配 IP 地址
- 完全隔离,外部设备无法访问虚拟机,虚拟机也不能访问互联网(除非额外配置)
第三章 分支与循环语句
1. 悬空else问题
现象
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main()
{
int a = 0;
int b = 2;
if (a == 1)
if (b == 2)
printf("hehe\n");//不打印
else
printf("haha\n");//不打印
return 0;
}
出现这个的主要原因是:else会匹配离自己最近的那个if
解决方案
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
int a = 0;
int b = 2;
if(a == 1){
if(b == 2){
printf("hehe\n");
}
}
else{
printf("haha\n");
}
return 0;
}
编程小建议:if和else中的代码尽量写在代码块里{ }
2. 循环输入问题
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main()
{
int ch = 0;
while ((ch = getchar()) != EOF){
putchar(ch);
}
return 0;
}
- getchar:输入一个字符
- putchar:输出一个字符
对EOF的正确解释
为End Of File的缩写,通常在文本的最后存在,用这个字符来表示文本结束
ASCII代码值的范围是0~127,不可能出现-1,所以EOF在C语言中为-1,在while循环中以EOF作为文件结束标志
3. getchar缓冲区溢出问题
现象
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
char password[20] = { 0 };
printf("请输入密码:>");
scanf("%s", password);//123456
printf("请确认密码(Y/N):>");
char ch = getchar();
if (ch == 'Y')
printf("确认成功\n");
else
printf("确认失败\n");
return 0;
}
出现这个的主要原因是:回车会触发\n,则上面代码匹配的时候总是会“确认成功”
解决方案 :清除掉缓冲区中所有的\n
4. goto语句坑点
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
int main()
{
char input[20] = { 0 };
system("shutdown -s -t 60");
tag:
printf("请注意,你的电脑将在1分钟内关机,如果输入:我是猪,就取消关机\n");
scanf("%s", input);
if (strcmp(input, "我是猪") == 0)
{
system("shutdown -a");
return 0;
}
goto tag;
return 0;
}
- goto语句一般要用标记跳转的标号来配合使用,以达到跳转的目的
说明一下:
goto语句一般用不到,更多应用在:终止程序在某些深度嵌套的结构的处理过程
且goto语句不能跨函数,跨文件使用
第四章 数据类型
1. bool类型
其实C语言本身是不支持bool的,是C99才开始支持的, 为了解决:避免误用int 作为布尔值减少bug,统一不同平台的布尔表示方式,增强可移植性,让逻辑运算的语义更加清晰,
#include <stdio.h>
#include <stdbool.h>
int main()
{
bool flag = true;
if(flag){
printf("我是真的\n");
}
else{
printf("昂昂\n");
}
return 0;
}
- 注:头文件为stdbool.h
第五章 函数
1.为什么要有库函数
为了支持可移植性和提高程序的效率,所以C语言的基础库中提供了一系列类似的库函数,方便程序员进行软件开发
2.函数嵌套
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
void test3()
{
printf("hehe\n");//hehe
}
int test2()
{
test3();
return 0;
}
int main()
{
test2();
return 0;
}
- 一个函数内部有另一个函数的调用就叫做函数嵌套
3.链式访问
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
int main()
{
int len = strlen("abc");
printf("%d\n", len);
//链式访问
printf("%d\n", strlen("abc"));
return 0;
}
- 把一个函数的返回值作为另外一个函数的参数就叫做链式访问
4.回调函数
将一个函数A的地址传给另一个函数B(用函数指针接收),该函数B又通过解引用调用其他函数
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int Add(int x, int y)
{
return x + y;
}
int Sub(int x, int y)
{
return x - y;
}
int Mul(int x, int y)
{
return x * y;
}
int Div(int x, int y)
{
return x / y;
}
void menu()
{
printf("**************************\n");
printf("**** 1. add 2. sub ****\n");
printf("**** 3. mul 4. div ****\n");
printf("**** 0. exit ****\n");
printf("**************************\n");
}
int Calc(int (*pf)(int, int))
{
int x = 0;
int y = 0;
printf("请输入2个操作数>:");
scanf("%d %d", &x, &y);
return pf(x, y);
}
int main()
{
int input = 0;
//计算器-计算整型变量的加、减、乘、除
//a&b a^b a|b a>>b a<<b a>b
do {
menu();
int ret = 0;
printf("请选择:>");
scanf("%d", &input);
switch (input)
{
case 1:
ret = Calc(Add);
printf("ret = %d\n", ret);
break;
case 2:
ret = Calc(Sub);
printf("ret = %d\n", ret);
break;
case 3:
ret = Calc(Mul);//
printf("ret = %d\n", ret);
break;
case 4:
ret = Calc(Div);//
printf("ret = %d\n", ret);
break;
case 0:
printf("退出程序\n");
break;
default:
printf("选择错误,重新选择!\n");
break;
}
} while (input);
return 0;
}
- Clac这一个函数就能调用多个函数,减少了代码的冗余,Clac就像一个集成器
5.函数递归
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
void print(unsigned int n)//123
{
if (n > 9)
{
print(n / 10);
}
printf("%d ", n % 10);//1 2 3
}
int main()
{
unsigned int num = 0;
scanf("%u", &num);//123
//递归 - 函数自己调用自己
print(num);//print函数可以打印参数部分数字的每一位
return 0;
}
- 函数自己嵌套自己,自己调用自己就叫做函数递归
- 而一个正确的函数递归不仅需要一个限制条件,而且还需要一个接近限制条件的条件
写递归代码的几个要求
- 不能写死递归,必须要有跳出条件
- 每次递归都应该逼近跳出条件
- 递归层次不能太深,防止栈溢出
6.字符转换函数
islower——判读小写字符
返回值描述: 是小写字符就返回非0
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <ctype.h>
int main()
{
char ch = 'a';
if (islower(ch)) {
printf("小写字母\n");
}
return 0;
}
isupper——判读大写字符
返回值描述: 是大写字符就返回非0
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <ctype.h>
int main()
{
char ch = 'A';
if (isupper(ch)) {
printf("大写字母\n");
}
return 0;
}
7.模拟实现字符串拷贝strcpy
char * strcpy ( char * destination , const char * source );
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<assert.h>
#include<string.h>
char* my_strcpy(char* des, const char* sou)
{
assert(des && sou);
char* ret = des;
while (*des++ = *sou++)
{
;
}
return ret;
}
int main()
{
char arr1[20] = { "xxxxxxxxxxxxxxxxx" };
char arr2[] = { "hello" };
printf("%s\n", my_strcpy(arr1, arr2));//链式访问
printf("%s\n", strcpy(arr1, arr2));//链式访问
return 0;
}
- strcpy拷贝的时候会把'\0'一起拷贝过去
8.模拟实现字符串的长度strlen
size_t strlen ( const char * str)
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<assert.h>
#include<string.h>
//计数器
int my_strlen1(const char* str)
{
assert(str);
int count = 0;
while (*str++ != '\0') {
count++;
}
return count;
}
int main()
{
char arr[] = "abcdefg";
printf("%d\n", my_strlen3(arr));
printf("%d\n", strlen(arr));
return 0;
}
- 字符串是以\0作为结尾的
9.模拟实现字符串比较strcmp
int strcmp ( const char * str1, const char * str2)
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<assert.h>
#include<string.h>
int my_strcmp(const char* s1, const char* s2)
{
assert(s1 && s2);//断言
while (*s1 == *s2)
{
if (*s1 == '\0')
{
//当读取到\0结束时
return 0;
}
s1++;
s2++;
}
return *s1 - *s2;
}
int main()
{
char arr1[] = "abce afd";
char arr2[] = "abce f";
if (!my_strcmp(arr1, arr2)) {
printf("两个字符串相同\n");
}
else if (!strcmp(arr1, arr2)) {
printf("两个字符串相同\n");
}
else {
printf("两个字符串不同\n");
}
return 0;
}
10.模拟实现字符串追加strcat
char *strcat (char *destination , const char* soure) ;
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<assert.h>
#include<string.h>
char* my_strcat(char* dest, const char* src)
{
char* ret = dest;
assert(dest && src);
//1. 找目标字符串中的\0
while (*dest){
dest++;
}
//2. 追加源字符串,包含\0
while (*dest++ = *src++){
;
}
return ret;
}
int main()
{
char arr1[20] = "abc ";
char arr2[] = "eeffff";
printf("%s\n", my_strcat(arr1, arr2));
printf("%s\n", strcat(arr1, arr2));
return 0;
}
- 先找尾,再追加字符串
11.模拟实现字符串查找strstr
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<assert.h>
#include<string.h>
char* my_strstr(const char* str1, const char* str2)
{
assert(str1 && str2);
if (*str2 == '\0')//如果查找的是空字符
{
return str1;
}
const char* first1 = str1;
const char* first2 = str2;
while (*str1 != '\0')
{
if (*str1 != *str2) {
str1 = ++first1;
str2 = first2;
}
else {
str1++;
str2++;
}
if (*str2 == '\0') {
return first1;
}
}
return NULL;
}
int main()
{
char arr1[] = "abbcdef";
char arr2[] = "bcd";
if (my_strstr(arr1, arr2))
{
printf("%s\n", my_strstr(arr1, arr2));
}
if (strstr(arr1, arr2)) {
printf("%s\n", strstr(arr1, arr2));
}
else {
printf("不存在\n");
}
return 0;
}
说明一下:
- 需要考虑字符串为空的情况
- 需要用另一个指针来记录它原来的位置,以便不匹配时能重新匹配
12. 模拟实现内存拷贝memcpy
void *memcpy (void *destnation, const void * source,size_t num );
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<assert.h>
#include<string.h>
void* my_memcpy(void* des, const void* cou, size_t num)
{
assert(des && cou);
void* ret = des;//记录地址
while (num--)
{
*(char*)des = *(char*)cou;
des = (char*)des + 1;
cou = (char*)cou + 1;
}
return ret;
}
int main()
{
int arr1[] = { 1,2,3,4,5,6,7,8,9,10 };
int arr2[] = { 1,2,3,4,5,6,7,8,9,10 };
int sz = sizeof(arr1) / sizeof(arr1[0]);
int i = 0;
my_memcpy(arr1 + 2, arr1, 20);
for (i = 0; i < sz; i++)
{
printf("%d ", arr1[i]);
}
printf("\n");
memcpy(arr2 + 2, arr2, 20);
//memmove(arr2 + 2, arr2, 20);
for (i = 0; i < sz; i++)
{
printf("%d ", arr2[i]);
}
return 0;
}
说明一下:
- 只要跟内存相关的都是void*,都是需要强制类型转换
- 这里的num参数代表字节数的意思
- 且memcpy是需要返回目的地址的起始地址
补充一下:
- 在C语言中memcpy是不能自己拷贝自己的(重叠的内存块),自己拷贝自己有一个更安全的库函数memmove
- 但在vs2019中memcpy是可以自己拷贝自己的,即memcpy的功能和memmove的功能相同
13.模拟实现内存移动memmove
void *memmove(void *destination,const void*source ,size_t num );
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<assert.h>
#include<string.h>
void* my_memmove(void* des, const void* cou, size_t num)
{
assert(des && cou);
void* ret = des;//记录地址
//从前开始移动
if (des < cou) {
while (num--)
{
*(char*)des = *(char*)cou;
des = (char*)des + 1;
cou = (char*)cou + 1;
}
}
//从后开始移动
else {
while (num--)
{
*((char*)des + num) = *((char*)cou + num);
}
}
return ret;
}
int main()
{
int arr1[] = { 1,2,3,4,5,6,7,8,9,10 };
int arr2[] = { 1,2,3,4,5,6,7,8,9,10 };
int sz = sizeof(arr1) / sizeof(arr1[0]);
int i = 0;
my_memmove(arr1 + 2, arr1, 20);
memmove(arr2 + 2, arr2, 20);
for (i = 0; i < sz; i++)
{
printf("%d ", arr1[i]);
}
printf("\n");
for (i = 0; i < sz; i++)
{
printf("%d ", arr2[i]);
}
return 0;
}
- memmove是可以自己拷贝自己的
14.模拟实现内存比较memcmp
int memcmp (const void * ptr1,const void * ptr2,size_t num);
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<assert.h>
#include<string.h>
int my_memcmp(const void* ptr1, const void* ptr2, size_t num)
{
assert(ptr1 && ptr2);
while (num--)
{
if(*(char*)ptr1 != *(char*)ptr2)
{
return *(char*)ptr1 - *(char*)ptr2;
}
ptr1 = (char*)ptr1 + 1;
ptr2 = (char*)ptr2 + 1;
}
return 0;
}
int main()
{
int arr1[] = { 1,2,3,4,5,6,7,8,9,10 };
int arr2[] = { 1,2,2,3,4,5,6,7,7,2 };
int sz = my_memcmp(arr1, arr2,20);
if (sz > 0) {
printf("arr1>arr2\n");
}
else if (sz < 0) {
printf("arr1<arr2\n");
}
else {
printf("arr1==arr2\n");
}
return 0;
}
说明一下:
- 不论是字符串比较,还是内存比较都是需要二者不同时,返回str1 - str2
- 返回值:成功则返回0,不成功返回str1 - str2
15. 认识strtok字符串分割
strtok
是 C 语言 <string.h>
头文件中的一个函数,用于分割字符串(tokenize)。它将字符串拆分为多个子字符串(token),以指定的分隔符进行分割。
char *strtok(char *str, const char *delim);
说明一下:
-
str
:要分割的字符串(第一次调用时传入完整字符串,后续调用传NULL
) delim
:分隔符字符串,表示用哪些字符作为分隔符- 对于返回值:找到子字符串时,返回指向子字符串的指针(该子字符串已被修改),当没找到是返回NULL
#include <stdio.h>
#include <string.h>
int main() {
char str[] = "app-le,ban-ana,or-ange"; // 要分割的字符串
char delim[] = ",-"; // 逗号作为分隔符
char *token;
// 第一次调用 strtok
token = strtok(str, delim);
while (token != NULL) {
printf("%s ", token); // 逐个打印分割出的子字符串
token = strtok(NULL, delim); // 继续分割,传 NULL
}
printf("\n");
return 0;
}
16. 认识strerror && perror函数
fopen打开文件失败会把错误信息存入errno中,可以使用strerror或perror打印这些错误信息
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <errno.h>
#include <string.h>
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
perror("fopen");
return 1;
}
fclose(pf);
pf = NULL;
return 0;
}
说明一下:
- strerror: 返回错误码,所对应的错误信息
- error: 返回错误码,所对应的错误信息(更清晰)
常见错误码介绍
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <string.h>
int main()
{
printf("%s\n", strerror(0));
printf("%s\n", strerror(1));
printf("%s\n", strerror(2));
printf("%s\n", strerror(3));
printf("%s\n", strerror(4));
printf("%s\n", strerror(5));
return 0;
}
17. 认识time函数
time函数是用来获取时间戳的,需要引用time.h的头文件
#include <stdio.h>
#include <time.h>
int main() {
time_t current_time;
time(¤t_time);// 得到当前时间戳
struct tm* time_info;// 一个结构体
// 将时间戳转换为本地时间的结构体
time_info = localtime(¤t_time);
char time_str[100];//存储打印时间
strftime(time_str,sizeof(time_str),"当前时间为:%Y-%m-%d %H:%M:%S\n",time_info);
printf("%s",time_str);
return 0;
}
- strftime就是一个格式化打印时间戳的函数
- 上面代码就能按照我们的习惯打印当前时间
struct tm的成员信息如下:
第六章 数组
1.变长数组的引入
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main()
{
int n = 10;
int arr[n] = { 0 };
return 0;
}
- C99引入了变长数组的概念,可以给[ ] 中加一个变量才可以
- C99之前是没有变长数组的,[ ] 只能加一个常量
2.数组越界问题
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("%d\n", arr[10]);
printf("%d\n", arr[11]);
printf("%d\n", arr[12]);
return 0;
}
说明一下:
- 数组越界不一定报错,但是可能会有警告
3.数组内存布局
局部变量都是在栈区上面的,而栈区的使用习惯是先使用高地址再使用低地址,在开辟空间的角度,不应该把数组认为成一个个独立的元素,要整体开辟,整体释放
4.数组传参
为了解决拷贝问题, 所有的数组,传参都会发生降维,都会降维成指向内部元素类型的指针!
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
void show(int pr[])
{
int i = 0;
for (i = 0;i < 10;i++){
printf("%d ", *(pr + i));
//printf("%d ",pr[i]);
}
}
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int i = 0;
show(arr);
return 0;
}
为了减低编程难度,指针和数组都可以通过*和[] 进行解引用,但它们的寻址方案完全不一样
5. 二维数组传参
说明一下:
- 传入的参数是二维数组的首地址
- 第二个test错误,接收时int arr[][],可以用二维数组接收,但不能省略列数
- 第四个test错误,不能用一级指针接收,用指针接收,只能用数组指针(一级)
- 第五个test错误,不能用一级指针数组接收,用数组接收,只能用二维数组
- 第七个test错误,不能用二级指针接收,用指针接收,只能用数组指针(一级)
6. 一道经典数组题
&数组名,取的是整个数组的地址,+1,就会跳过整个数组的地址
数组名,取的是整个数组的首地址,+1,就会跳过一个元素
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
int a[4] = { 1,2,3,4 };
int* ptr1 = (int*)(&a + 1);
int* ptr2 = (int*)((int)a + 1);
printf("%x,%x\n", ptr1[-1], *ptr2);
return 0;
}
- 大多数机器都是小端机,存的时候用小端,取的时候也用小端
7.函数指针数组的应用
实现计算器的加减乘除
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int Add(int x, int y)
{
return x + y;
}
int Sub(int x, int y)
{
return x - y;
}
int Mul(int x, int y)
{
return x * y;
}
int Div(int x, int y)
{
return x / y;
}
void menu()
{
printf("**************************\n");
printf("**** 1. add 2. sub ****\n");
printf("**** 3. mul 4. div ****\n");
printf("**** 0. exit ****\n");
printf("**************************\n");
}
int main()
{
int input = 0;
//计算器-计算整型变量的加、减、乘、除
//a&b a^b a|b a>>b a<<b a>b
do {
menu();
int (*pfArr[5])(int, int) = { NULL, Add, Sub, Mul, Div };
int x = 0;
int y = 0;
int ret = 0;
printf("请选择:>");
scanf("%d", &input);//2
if (input >= 1 && input <= 4)
{
printf("请输入2个操作数>:");
scanf("%d %d", &x, &y);
ret = pfArr[input](x, y);
printf("ret = %d\n", ret);
}
else if (input == 0)
{
printf("退出程序\n");
break;
}
else
{
printf("选择错误\n");
}
} while (input);//只有输入0才退出
return 0;
}
说明一下:
- 这个函数指针数组中存放的是加法函数,减法函数,乘法函数,除法函数
- 函数指针数组更像是一个跳板的作用,可以减少代码冗余
第七章 操作符
1.算数操作符
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
int a1 = 6 / 5;
printf("%d\n", a1);//1
float a2 = 6 / 5;
printf("%f\n", a2);//1.000000
float a3 = 6.0 / 5.0;
printf("%f\n", a3);//1.200000
return 0;
}
说明一下:
- 除了 % 操作符之外,其他的几个操作符都可以作用于整数和浮点数,%不能用于浮点数
- / 操作符左右两边至少有一个为小数,结果才为小数,比如6/5=1,6/5.0=1.2
2.移位操作符
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
int a = 2;
//把a的二进制位向左移动一位
int b = a << 1;
printf("b = %d\n", b);//4
printf("a = %d\n", a);//2
int c = -1;
//把c的二进制位向右移动一位
int d = c >> 1;
printf("c = %d\n", c);//-1
printf("d = %d\n", d);//-1
return 0;
}
说明一下:
- 左移操作符移位规则:左边丢弃,右边补0
- 右移操作符移位规则:
- 逻辑移位:右边丢弃,左边补0,
- 算术移位:右边丢弃,左边补原符号位,
补充一下:
- vs2019中对于右移操作符是采用的算术移位
- 对于移位运算符,不要移动负数位,这个是标准未定义的
3.逗号表达式
exp1 , exp2 , exp3 , …expN
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
int a = 1;
int b = 2;
int c = (a > b, a = b + 10, a, b = a + 1);//逗号表达式
printf("%d\n", c);//13
return 0;
}
- 逗号表达式,就是用逗号隔开的多个表达式,从左向右依次执行。整个表达式的结果是最后一个表达式的结果
4.隐式类型转换
隐式类型转化又叫做整形提升, 至于为什么会发生整形提升,解释起来很麻烦,一句话:表达式中各种长度可能小于int长度的整型值,都必须先转换为int或unsigned int, 然后才能送入 CPU 去执行运算
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
char a = 3;
char b = 127;
char c = a + b;
printf("%d\n", c); //-126
return 0;
}
- 这里的a b c三个都是char,都没有达到int,所以一定会发生整形提升
- 整形提升是按照原符号位进行提升的
5.算术转换
第一位 | long double |
第二位 | double |
第三位 | float |
第四位 | unsigned long int |
第五位 | long int |
第六位 | unsigned int |
第七位 | int |
如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另外一个操作数的类型后执行运算
比如说:一个整数乘以一个小数,结果是小数,这就发生了算术转换
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
float f = 3.14;
int num = f;//隐式转换,会有精度丢失
return 0;
}
这段代码存在潜在的问题, 且算术转换都是向着精度更高的转换
第八章 指针
1.对指针的大小的理解
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int* pa;
char* pc;
float* pf;
printf("%d\n", sizeof(pa));//4或8
printf("%d\n", sizeof(pc));//4或8
printf("%d\n", sizeof(pf));//4或8
return 0;
}
-
指针的大小跟指针的类型无关,取决于机器的是32位的,还是64位的
-
在32位的机器下指针的大小为4,在64位机器下指针的大小为8
2.指针类型的意义
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int arr[10] = { 0 };
int* p = arr;
char* pc = (char*)arr;
printf("%p\n", p);
printf("%p\n", p + 1);
printf("%p\n", pc);
printf("%p\n", pc + 1);
return 0;
}
- 指针的类型决定了指针向前或者向后走一步有多大(距离)
3.野指针问题
定义:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
int main()
{
//1.未初始化
int* p;//p野指针
*p = 20;
//2.越界访问
int arr[10] = { 0 };
int* ps = arr;
int i = 0;
for (i = 0; i <= 10; i++)
{
*ps = i;//当i = 10,ps野指针
ps++;
}
//3.指向的空间释放
int* tmp = (int*)malloc(sizeof(int) * 2);
free(tmp);
int* pt = tmp;//tmp野指针
return 0;
}
原因:指针未初始化,指针越界访问,指针指向的空间已经被释放了但还是指向这段空间
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int* test()
{
int a = 10;
return &a;
}
int main()
{
int*p = test();
*p = 20;
return 0;
}
同理这里的p指针也是野指针,因为a变量是栈上开辟的,出了作用域就被回收了,则p指针指向的这一段就是已经被释放了的空间
4. 如何规避野指针
a.指针初始化 b.小心指针越界 c.指针指向空间释放及时置 NULL
d.避免返回局部变量的地址 e.指针使用之前检查
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
//当前不知道p应该初始化为什么地址的时候,直接初始化为NULL
int* pa = NULL;
//明确知道初始化的值
int a = 10;
int* pc = &a;
int* p = NULL;
if(p != NULL)
*p = 10;
return 0;
}
- C语言本身是不会检查数据的越界行为的,但编译器还是会报警告
5. 指针-指针
前提:两个指针指向同一块空间
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
char c[5];
printf("%d\n", &arr[9] - &c[0]);//err
printf("%d\n", &arr[9] - &arr[0]);//9
return 0;
}
- 指针-指针得到的是中间的元素个数
- 而指针+指针没有意义,就像日期+日期一样没意义,所以不讨论
6.指针关系运算坑点
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int arr[5] = { 1,2,3,4,5 };
int* ps = NULL;
//允许指向数组元素的指针与指向数组最后一个元素
//后面的那个内存位置的指针比较
for (ps = arr; ps < &arr[5]; ps++)
{
*ps = 0;
}
//但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。
for (ps = &arr[4]; ps > &arr[-1]; ps--)
{
*ps = 0;
}
return 0;
}
说明一下:
- 实际在绝大部分的编译器上是可以顺利完成任务的,然而我们还是应该避免这样写,因为标准并不保证它可行
- 标准规定:允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较
但是不允许与指向第一个元素之前的那个内存位置的指针进行比较
7. 指针变量的解引用
指针变量是由 空间(左值)+内容(右值,及地址)组成
0x1234如果是赋给p指针变量的空间是不会报错的,由此推断出0x1234是赋给p指针变量的内容,指针变量进行解引用,使用的是指针变量的右值(内容,及地址)
8. 栈随机化技术
由于存在栈随机化技术 使得每次重新编译打印同一个变量的时候都地址都不一样
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int a = 10;
printf("%p\n", &a);
return 0;
}
9. 字符指针
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
char ch = 'q';
char * pc = &ch;
char* ps = "hello bit";
char arr[] = "hello bit";
*ps = 'w';//err
arr[0] = 'w';
printf("%c\n", *ps);//h
printf("%s\n", ps);//hello bit
printf("%s\n", arr);//wello bit
return 0;
}
说明一下:
-
char* ps = "hello bit";不是把字符串 hello bit放到字符指针 ps 里,而是把"hello bit"这个字符串的首字符的地址存储在了ps中
-
"hello bit"是一个常量字符串,常量字符串是不能被修改,则*ps = 'w';这个语句就是错的
一道经典题
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
char str1[] = "hello bit.";
char str2[] = "hello bit.";
const char* str3 = "hello bit.";
const char* str4 = "hello bit.";
//*str3 = 'w';
if (str1 == str2)
printf("str1 and str2 are same\n");
else
printf("str1 and str2 are not same\n");
if (str3 == str4)
printf("str3 and str4 are same\n");
else
printf("str3 and str4 are not same\n");
return 0;
}
说明一下:
- 这其实也很好理解"hello bit.",这是一个常量字符串,不能被修改,又因为str1和str2都是指向同一个常量字符串,自然也就不需要再开辟一段空间放相同的常量字符串
- srt1和str2虽然数组的内容一样,但是str1和str2中的"hello bit."是可以被修改,所以开辟了2个不同数组存放"hello bit."
- 其实str1和str2中的内容是在编译期间将"hello bit"(字符串常量区中)拷贝过去的,
str1和str2本身是数组,str3和str4本身是指针
10.函数指针传参
函数名 == &函数名
*函数指针 == 函数指针
第九章 自定义类型
1.结构体
1.1 结构体三种访问方式
说明一下:
- 结构体指针本身可以用->访问变量,*结构体指针就变成结构体对象,
- 而结构体对象就可以直接用.来访问
1.2 特殊的结构体声明
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], * p;
int main()
{
p = &x;
return 0;
}
虽然上面两个结构体的内容是相同的,但是编译器会把上面的两个声明当成完全不同的两个类型,所以是非法的
1.3 结构体自引用问题
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
struct Node
{
int data;
struct Node next;
};
int main()
{
struct Node a;
return 0;
}
说明一下:
- 结构体是不能自己嵌套自己的,因为sizeof(struct Node)是未知的
1.4 认识匿名结构体
#include <stdio.h>
int main() {
// 定义一个包含匿名结构体的变量
struct {
int x;
int y;
} point1, point2;
// 给 point1 和 point2 赋值
point1.x = 10;
point1.y = 20;
point2.x = 30;
point2.y = 40;
// 输出
printf("point1: (%d, %d)\n", point1.x, point1.y);
printf("point2: (%d, %d)\n", point2.x, point2.y);
return 0;
}
在上面的代码中,我们定义了一个没有名字的结构体,直接在 struct {}
中定义了结构体的字段 x
和 y
。然后通过 point1
和 point2
来引用它们,而point1
和 point2
就是这个匿名结构体的两个实例
结构体没有给定名字,所以我们无法在程序的其他地方再定义同样的结构体类型
主要应用场景
struct {
int id;
struct {
int x;
int y;
}; // 匿名结构体
} obj;
obj.id = 1;
obj.x = 10; // 直接访问匿名结构体的成员
obj.y = 20;
- 它主要应用在结构体嵌套结构体中,可以使代码更加简洁
1.5 结构体隐藏问题
- 在编译器中是先定义next,再typedef(最后调用),此时的结构体是匿名结构体,所以会报一堆错
1.6 结构体内存对齐
什么是内存对齐?内存对齐的原则?为什么要进行内存对齐,有什么优点?
内存对齐:编译器将程序中的每个“数据单元”安排在字的整数倍的地址指向的内存之中
内存对齐的原则:
- 结构体变量的首地址 = min(最宽基本类型大小,对齐基数)的整除
- 其他成员的地址偏移量 = min(成员大小,对齐基数)的整数倍 + 填充字节
- 总大小 = min(最宽基本类型大小,对齐基数) + 填充字节
进行内存对齐的原因:(主要是硬件设备方面的问题)
- 某些硬件设备只能存取对齐数据,存取非对齐的数据可能会引发异常;
- 某些硬件设备不能保证在存取非对齐数据的时候的操作是原子操作;
- 相比于存取对齐的数据,存取非对齐的数据需要花费更多的时间;
- 某些处理器虽然支持非对齐数据的访问,但会引发对齐陷阱(alignment trap);
- 某些硬件设备只支持简单数据指令非对齐存取,不支持复杂数据指令的非对齐存取
内存结构体内存对齐的原因
-
某些硬件设备只能存取对齐数据,存取非对齐的数据可能会引发异常;
-
某些硬件设备不能保证在存取非对齐数据的时候的操作是原子操作;
-
相比于存取对齐的数据,存取非对齐的数据需要花费更多的时间;
-
某些处理器虽然支持非对齐数据的访问,但会引发对齐陷阱(alignment trap);
-
某些硬件设备只支持简单数据指令非对齐存取,不支持复杂数据指令的非对齐存取
修改默认对齐数
说明一下:
- #pragma pack(2) 可以将默认对齐数改成2
小结
结构体的内存对齐是拿空间来换取时间的做法,所以在使用过程中让占用小的空间放在一起,这样可以让满足内存对齐时, 节省空间
1.7 结构体变量偏移量
offsetof是一个宏,能计算结构体的成员变量的偏移量,需要引入头文件stddef.h
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stddef.h>
#pragma pack(2)
struct S
{
char c1;
int i;
char c2;
};
int main()
{
printf("%d\n", offsetof(struct S, c1));
printf("%d\n", offsetof(struct S, i));
printf("%d\n", offsetof(struct S, c2));
return 0;
}
2.位段
跟结构体相比,位段可以达到同样的效果,虽然可以很好的节省空间,但是有跨平台的问题
2.1 内存分配
位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的,位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
struct S
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
int main()
{
struct S s = { 0 };
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
return 0;
}
说明一下:
- 大小端是字节序才考虑的这里不考虑
2.2 位段的跨平台问题
- int 位段被当成有符号数还是无符号数是不确定的
- 位段中最大位的数目不确定,(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题
- 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义
- 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的
2.3 位段的应用
两个比特位有四种组合:00,01,10,11
- 性别:男,女,保密,用两个比特位足够表示,char a : 2就行了,不用int a
- 有时候用位段,比定义变量,定义结构体更加节省空间
3. 枚举
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
void menu()
{
printf("*****************************\n");
printf("**** 1. add 2. sub *****\n");
printf("**** 3. mul 4. div *****\n");
printf("**** 0. exit *****\n");
printf("*****************************\n");
}
enum Option
{
EXIT,//0
ADD,//1
SUB,//2
MUL,//3
DIV,//4
};
int main()
{
int input = 0;
do
{
menu();
printf("请选择:>");
scanf("%d", &input);
switch (input)
{
case ADD://case 1:
break;
case SUB://case 2:
break;
case MUL://case 3:
break;
case DIV://case 4:
break;
case EXIT://case 5:
break;
default:
break;
}
} while (input);
return 0;
}
说明一下:
-
枚举增加代码的可读性和可维护性
- 和#define定义的标识符相比较(在预处理阶段就替换了),而枚举有类型检查更加严谨,
- 还可以防止了命名污染(封装)
- 便于调试, 使用方便,一次可以定义多个常量
4. 联合体(共用体)
联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
union Un
{
char c;//1
int i;//4
};
int main()
{
union Un u = {10};
u.i = 1000;
u.c = 100;
printf("%p\n", &u);
printf("%p\n", &(u.c));
printf("%p\n", &(u.i));
printf("%d\n", sizeof(u));//
return 0;
}
联合体的内存对齐
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
union Un
{
char a[5];//1 5
int i;//4
char c;//1
};
int main()
{
union Un u;
printf("%d\n", sizeof(u));
return 0;
}
说明一下:
- 联合的大小至少是最大成员的大小
- 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍
- 最大成员的大小是5,最大对齐数是4,所以结果是8
第十章 程序翻译
程序翻译的过程:预处理->编译->汇编->连接
1.预处理
预处理过程:头文件的展开->宏替换->去注释->条件编译
- gcc -E test.c -o test.i
证明宏替换和去注释的先后顺序
- 预处理期间先执行去注释,后进行宏替换
宏的有效范围
说明一下:
- 宏的有效范围:是从定义处往下有效,之前无效
- 在源文件的任意地方,宏都可以定义,与是否在函数内外无关
2. 编译
- gcc -S test.i -o test.s
3. 汇编
- gcc -c test.s -o test.o
4. 连接
- 把多个目标文件和连接库进行链接的
5. 预定义符号
__FILE__ | 进行编译的源文件 |
__LINE__ | 文件当前的行号 |
__DATE__ | 文件被编译的日期 |
__TIME__ | 文件被编译的时间 |
__STDC__ | 如果编译器遵循ANSI C,其值为1,否则未定义 |
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main()
{
printf("%s\n", __FILE__);
printf("%d\n", __LINE__);
printf("%s\n", __DATE__);
printf("%s\n", __TIME__);
printf("%s\n", __FUNCTION__);
int i = 0;
FILE* pf = fopen("log.txt", "a+");
if (pf == NULL)
{
perror("fopen\n");
return 1;
}
for (i = 0; i < 10; i++)
{
fprintf(pf, "%s %d %s %s %d\n",\
__FILE__, __LINE__, __DATE__, __TIME__, i);
}
fclose(pf);
pf = NULL;
//printf("%d\n", __STDC__);//不支持
return 0;
}
- 上面那些预定义符号,都是内置的
6. 宏和函数对比
属性 | #define定义宏 | 函数 |
执行速度 | 更快 | 存在栈帧的创建和销毁,所以会慢一点 |
参数类型 | 宏的参数与类型无关 | 函数的参数是与类型有关 |
调试 | 不方便调试的 | 可以逐语句调试的 |
递归 | 不能递归的 | 可以递归的 |
宏的特殊用法
说明一下:
- 宏的参数可以出现类型,但是函数做不到
- 建议写宏的时候全部大写
7. 命令行定义宏变量
- 命令行中定义符号: gcc test.c -D M=10
8. #undef
#undef是取消宏的意思,主要用于限定宏的有效范围
9. #ifdef &&#ifndef
主要用于:裁剪代码,快速实现版本维护,跨平台性等
10.#if #elif #endif
#if 多用于判断宏的真假
11. #error
#error的核心作用是可以进行自定义编译报错。还可以定制化文件名称和代码行号
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
//#define __cplusplus
int main()
{
#ifndef __cplusplus
#error 老铁,你用的不是C++的编译器哦
#endif
return 0;
}
13. #line
定制化文件名称和代码行号
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
printf("%s, %d\n", __FILE__, __LINE__);
#line 60 "hehe.h" //定制化完成
printf("%s, %d\n", __FILE__, __LINE__);
return 0;
}
说明一下:
- _FILE_当前文件的文件名,_LINE_当前代码的行号
14. #pragme
在编译中打印信息
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#define M 10
int main()
{
#ifdef M
#pragma message("M宏已经被定义了")
#endif
return 0;
}
15.特殊#
宏中使用# 就是将内容转换成为"字符串"
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <string.h>
#define TOSTRING(s) #s
int main()
{
int abc = 12345;
char str[64] = { 0 };
strcpy(str, TOSTRING(abc));
printf("%s", str);
return 0;
}
16.特殊##
宏中使用## 就是连接内容,组成新的字符串
第十一章 关键字
C90一共有32个关键字,C99比C90多了5个关键字,但主流的编译器对C99关键字支持的不是特别好,所以一般就以C90的32个关键字为标准
1. 认识auto关键字
被auto修饰的变量叫做局部变量,默认都是auto修饰的,不过一般省略
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int i = 0;
auto int j = 0;
return 0;
}
2. 认识register关键字
被register所修饰变量,会被放入CPU寄存区中,从而达到提高效率的目的
但现在的编译器已经很智能化了,它能够自主的决断是否将变量放入CPU寄存器,
注意:被register修饰的变量,不能取地址,因为这个变量已经在寄存区中了,地址是内存相关的概念
3. 认识extern关键字
被extern修饰的变量或者函数表示为声明外部属性
注意: extern int g_val = 11;是错误的,extern只能声明,不能定义,初始化,赋值等等
编程好习惯: 声明变量或函数的时候,都带上extern,比如: extern int g_val = 100;extern void show();
4.认识static关键字
被static修饰的全局变量或函数,只能在本文件内被访问,不能被外部其他文件直接访问
被static修饰的局部变量,会更改局部变量的生命周期,将其放在静态区
5.认识sizeof关键字
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
int arr[3] = { 0,1,2 };
printf("%d\n", sizeof(arr));//所有元素的地址大小-12
printf("%d\n", sizeof(&arr));//首元素的地址大小-4
return 0;
}
说明一下:
-
sizeof(数组名)是计算的所有元素的地址大小
-
sizeof(&数组名)是计算首元素的地址大小
-
sizeof + 变量或常量,有无括号都行,但是不能计算变量类型
5.1 sizeof 与strlen的区别
- sizeof是在编译期间计算的,是要计算\0的
- strlen是在运行期间计算的,是不包括\0的
6.认识bool(_Bool)关键字
C99引入了_Bool类型(你没有看错,_Bool就是一个类型,不过在新增头文件stdbool.h中,被重新用宏写成了bool,为了保证C/C++兼容性)
- 在C99中bool用宏重新封装了_Bool
7.认识double关键字
0.1在double中存储时,是会发生精度损失的,所有double中的0.1和实际中的0.1是有区别的,如果真的要做比较,
解决方案:
double中的0.1和实际的0.1的绝对值小于DBL_EPSILON,就认为它几乎等于实际的0.1
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<float.h>
#include<math.h>
int main()
{
double x = 1.0;
double y = 0.1;
printf("%.50lf\n", x);
printf("%.50lf\n", y);
if (fabs((x - 0.9) - y)< DBL_EPSILON) {
printf("you can see me!\n");
}
else {
printf("oops!\n");
}
return 0;
}
8.认识switch case关键字
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int num = 1;
int b = 1;
switch (num)
{
case 1:
// int a = 1;// error
break;
case 2:
b = 1;
break;
case 3:{
int c = 1;
}
break;
default:
break;
}
return 0;
}
说明一下:
-
在case中不能定义变量,如果要在case中定义变量必须加上代码块{}
- case匹配时,尽量把常见的情况放在前面
9.认识continue关键字
- 在for循环中continue是跳到改变循环变量的位置
- 且建议在使用for循环语句的时候采用外小内大的规则
10.认识void关键字
说明一下:
- void本身就被编译器解释为空类型,强制的不允许定义变量
- 在linux中void的大小是1,而在vs2019中void的大小是0
11.认识return关键字
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<windows.h>
int SumAdd() {
int sum = 0;
for (int i = 1; i <= 10; i++) {
sum += i;
}
return sum;
}
int main()
{
int sum = SumAdd();
printf("%d", sum);
return 0;
}
- return返回的时候,会通过寄存器的方式,返回给函数调用方,及时没有接收也一样
12.认识const关键字
在C语言中,const机制是通过编译器检查实现的,它标记const变量不能被直接修改,但并未限制const变量的地址的引用,则虽然加了const但还是被改变了
而在C++中,只要加上了const,即使用指针间接引用都不会被改变
13.认识struct关键字
说明一下:
- 空结构体的大小,在不同的编译器下是不同的
- 在 C 语言中,空结构体通常占 1 字节,但具体取决于编译器
14.认识typedef关键字
说明一下:
- 存储关键字有:auto,extern,register,static,typedef
- 存储关键字,不可以同时出现,也就是说,在一个变量定义的时候,只能有一个
左值表示空间,右值表示内容,任何一个变量名,在不同的应用场景中,代表不同的含义
15.变量内容的存入和读取
先看现象
对于:signed int b = -10;来说
对于:unsigned int d = -10;来说
而上面那段代码:关键在于到底是%d打印,还是%u打印(数据究竟是以有符号还是无符号的形式取出的)
总结
变量存储时:字面数据必须先转成补码,再放入空间当中,与变量本身类型无关
变量读取时: 一定要先看变量本身的类型,然后才决定要不要看最高符号位。如果不需要,直接二进制转成十进制。如果需要,则需要转成原码,然后才能识别。(当然,最高符号位在哪 里,又要明确大小端)
16. char类型中特殊的-128
char类型只有8个bit位
-128的原码:1000 0000 0000 0000 0000 0000 1000 0000
-128的反码:1111 1111 1111 1111 1111 1111 0111 1111
-128的补码:1111 1111 1111 1111 1111 1111 1000 0000
char类型只有8个bit位,所以-128存入char中的时候会发生截断
- -128在char中为1000 0000,他们规定这个1000 0000就当作-128
-
数据类型的取值范围 : -2^(n-1)到2^(n-1)-1,比如char就是-2^(7)到2^(7)-1
第十二章 符号
1. 自增/自减运算符
- 不管是前置++,还是后置++,都是通过寄存器来改变值的
- 注意: 在没有接收方的时候,前置++和后置++是一样的
2. 取整运算
四舍五入取整(round) 向负无穷取整(floor) 向正无穷取整(ceil) 向0取整(trunc)
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <math.h>
int main()
{
const char * format = "%.1f \t%.1f \t%.1f \t%.1f \t%.1f\n";
printf("value\tround\tfloor\tceil\ttrunc\n");
printf("-----\t-----\t-----\t----\t-----\n");
printf(format, 2.3, round(2.3), floor(2.3), ceil(2.3), trunc(2.3));
printf(format, 3.8, round(3.8), floor(3.8), ceil(3.8), trunc(3.8));
printf(format, 5.5, round(5.5), floor(5.5), ceil(5.5), trunc(5.5));
printf(format, -2.3, round(-2.3), floor(-2.3), ceil(-2.3), trunc(-2.3));
printf(format, -3.8, round(-3.8), floor(-3.8), ceil(-3.8), trunc(-3.8));
printf(format, -5.5, round(-5.5), floor(-5.5), ceil(-5.5), trunc(-5.5));
return 0;
}
补充一下:在vs2019中的取整规则是向0取整
3.负数取模(得余数)问题
定义:如果a和d是两个自然数,d非零,可以证明存在两个唯一的整数 q 和 r
满足 a = q*d + r , q 为整数,且0 ≤ |r| < |d|其中,q 被称为商,r 被称为余数
- 在C语言中:-10=(-3)*3+(-1),因为C语言中是向0取整所以商是-3,余数是-1,也叫负余数
- 在Python中: -10=(-4)*3+2,因为Python中是向负无穷取整所以商是-4,余数是2,也叫正余数
- 所以,在不同语言中,同一个计算表达式,负数“取模”结果是不同的
第十三章 动态内存管理
1. 验证C程序动态地址空间分布
代码区->字符常量区->已初始全局变量->未初始化全局变量->堆区->共享区->栈区
2. CPU怎么和内存实现交互
总线的宽度决定CPU的寻址能力
控制总线决定了CPU对其他空间的控制能力和控制方式
数据总线的宽度决定了CPU单次传输数据的传送量,也就是数据传输速度
当CPU想在内存中读取一个数字6时,
- 首先,CPU通过地址总线,在内存中找到数字6的地址
- 然后,通过控制总线知道该操作是读还是写
- 最后,通过数据总线,把数字6传输到CPU中
3. 电脑的32位和64位有什么区别
从CPU架构来说:
- 32位CPU一次最多能处理32位的数据,寄存器宽度为32位,寻址能力有限
- 而64位CPU一次最多能处理64位的数据,寄存器宽度为64位,处理速度更快,能处理更大的数据量
从内存寻址能力:
- 32位系统最多能寻址 4GB(2³²字节) 的内存,
- 而64位系统最多能寻址 16EB(2⁶⁴字节) 的内存
从软件兼容性:
- 32位操作系统只能运行 32位程序,不支持 64位程序,
- 而64位操作系统 既可以运行 64位程序,也可以兼容 32位程序
4. 内存越界问题
结论:
- 越界不一定报错
- 对于数值越界访问的检查,是一种抽查机制
- 程序退出,内存泄漏问题就不在了,被自动回收了
- 内存泄漏问题对于那些永远不会主动退出的程序,比如:操作系统,杀毒软件,服务器等,影响大
5. C中动态内存“管理”体现
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
int main()
{
char* p = (char*)malloc(sizeof(char) * 10);
printf("before:%p\n",p);
free(p);
printf("after:%p\n", p);
return 0;
}
说明一下:
- 其实释放的字节会比实际上10个字节多得多,申请的一定不止10字节
- malloc申请空间的时候,系统给你的其实更多,而多出来的那部分,记录了这次申请的更详细信息
- free的释放,相当于取消关系,使之后的p无法再使用
6. 认识realloc
void* realloc (void* ptr, size_t size);
#define _crt_secure_no_warnings 1
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* p = (int*)calloc(10, sizeof(int));
if (p == NULL)
{
perror("main");
return 1;
}
//使用
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = 5;
}
//这里需要p指向的空间更大,需要20个int的空间
//realloc调整空间
int* ptr = (int*)realloc(p, 20 * sizeof(int));
if (ptr != NULL)
{
p = ptr;
}
}
这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到 新 的空间
调整内存空间的三种情况
情况一: 原有空间之后有足够大的空间
- realloc会在原来的空间中添加40字节,这种情况下, ptr和p的地址一样
情况二: 原有空间不足
- realloc会在会另找一块空间,开辟80字节,这种情况下, ptr和p的地址不一样
情况三: 不存在合适的空间
- realloc有可能找不到合适的空间来调整大小,将会返回空指针
7. 常见动态内存错误
7.1 对空指针的解引用
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
void test()
{
int* p = (int*)malloc(INT_MAX / 4);
*p = 20;//如果p的值是NULL,就会有问题
free(p);
}
int main()
{
test();
return 0;
}
错误原因: 没有判断是否为空指针
7.2 对动态开辟空间的越界访问
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
void test()
{
int i = 0;
int* p = (int*)malloc(10 * sizeof(int));
if (NULL == p)
{
exit(EXIT_FAILURE);
}
for (i = 0; i <= 10; i++)
{
*(p + i) = i;//当i是10的时候越界访问
}
free(p);
}
int main()
{
test();
return 0;
}
错误原因: 动态内存开辟也存在越界访问的情况
7.3 对非动态开辟内存使用free释放
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
void test()
{
int a = 10;
int* p = &a;
free(p);//ok?
}
int main()
{
test();
return 0;
}
错误原因:free只能释放掉动态内存开辟的空间
7.4 使用free部分释放内存空间
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
void test()
{
int* p = (int*)malloc(100);
p++;
free(p);//p不再指向动态内存的起始位置
}
int main()
{
test();
return 0;
}
错误原因: p不再指向动态内存的起始位置,且p也没有置成空指针,
这就导致有一部分空间没被释放,且找不到这块空间了
7.5 对同一块动态内存多次释放
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
void test()
{
int* p = (int*)malloc(100);
free(p);
free(p);//重复释放
}
int main()
{
test();
return 0;
}
错误原因: 不能对同一块空间多次free
7.6 动态开辟内存忘记释放(内存泄漏)
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
void test()
{
int* p = (int*)malloc(100);
if (NULL != p)
{
*p = 20;
}
}
int main()
{
test();
while (1);
}
错误原因: 忘记释放不再使用的动态开辟的空间会造成内存泄漏
7.7 一道经典面试题
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void Test(void)
{
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);
if (str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
int main()
{
Test();
return 0;
}
上面这段代码:逻辑顺序错误,正确的应该是先判断是否是空指针,再free释放空间
8. 零长/柔性数组
C99 中规定,结构体中的最后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
struct S
{
int n;//4
int arr[0];//大小是未知
};
int main()
{
//期望arr的大小是10个整形
struct S*ps = (struct S*)malloc(sizeof(struct S)+10*sizeof(int));
if (ps == NULL)
{
perror("main");
return 1;
}
ps->n = 10;
int i = 0;
for (i = 0; i < 10; i++)
{
ps->arr[i] = i;
}
//增加
struct S* ptr = (struct S*)realloc(ps, sizeof(struct S)+20*sizeof(int));
if (ptr != NULL)
{
ps = ptr;
}
//使用
//释放
free(ps);
ps = NULL;
//struct S s = {0};
//printf("%d\n", sizeof(s));//?
return 0;
}
优点:拷贝次数少(指针解引用),方便内存释放,提高访问速度的优点
缺点: 结构中的柔性数组成员前面必须包含至少一个其他成员,且它必须在最后面
说明一下:
- sizeof 返回的这种结构大小不包括柔性数组的内存
- 包含柔性数组成员的结构用malloc()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小
- 但实际感觉用处不
第十四章 文件
一切都是站在代码的角度 看文件
1. 系统接口
1.1 open && write(字节流)
对第二个flags参数的说明:
- O_RDONLY 以只读方式打开文件,O_WRONLY 以只写方式打开文件,O_RDWR 以可读写方式打开文件,这三个就同时只能出现一个,但是可以 | 下面这些标识符
- O_CREAT 若欲打开的文件不存在则自动建立该文件,O_EXCL 如果 O_CREAT 也被设置, 此指令会去检查文件是否存在,O_APPEND:追加内容的形式写入到文件末尾
- fd是文件描述符,buf是要写入数据,count是数据的长度
- 返回写入的个数,失败返回-1
void function1 ()
{
int fd = open("tmp.txt1",O_WRONLY | O_CREAT);//以写的方式,没有就创建
if(fd == -1){
perror("打开文件失败\n");
exit(EXIT_FAILURE);
}
char buf[] = "2025-3-14";
int count = write(fd,buf,strlen(buf));
if(count == -1){
perror("写入文件失败\n");
exit(EXIT_FAILURE);
}
close(fd);// 记得关闭文件
}
1.2 lseek
使用场景是:文件上传下载中的断点续传,比如百度网盘下载
上面的write是通过文件指针向后移动,进行写入的,wirte每次都会从首位置开始写数据,
而这个lseek就可以控制文件指针的指向,使其能在任意位置开始
参数说明:
- fd :文件描述符,
- offset : 文件指针偏移量
- whence : 文件指针开始的位置,如
- SEEK_SET 参数 offset 即为新的读写位置.
- SEEK_CUR 以目前的读写位置往后增加 offset 个位移量.
- SEEK_END 将读写位置指向文件尾后再增加 offset 个位移量.
- 当 whence 值为 SEEK_CUR 海或SEEK_END 时, 参数 offet 允许负值的出现
void function2 ()
{
int fd = open("tmp2.txt",O_WRONLY | O_CREAT);//以写的方式,没有就创建
if(fd == -1){
perror("打开文件失败\n");
exit(EXIT_FAILURE);
}
char buf[] = "2025-3-14";
int count = write(fd,buf,strlen(buf));
if(count == -1){
perror("写入文件失败\n");
exit(EXIT_FAILURE);
}
lseek(fd,-2,SEEK_END);// 从到数第二个位置开始写入
write(fd,"lseek",strlen("lseek"));
// 最终结果2025-3-lseek
close(fd);// 记得关闭文件
}
1.3 read(字节流)
- fd是文件描述符,buf是自定义缓存区,count是数据的长度
- 返回实际读到的个数,失败返回-1
void function3()
{
// 写入一个将2025-3-14迟迟cool写入tmp3.txt中
int fd = open("tmp3.txt",O_RDWR | O_CREAT);//以写的方式,没有就创建
if(fd == -1){
perror("打开文件失败\n");
exit(EXIT_FAILURE);
}
char buf[] = "2025-3-14迟迟cool";
int count = write(fd,buf,strlen(buf));
if(count == -1){
perror("写入文件失败\n");
exit(EXIT_FAILURE);
}
// 再读取tmp3.txt中的数据
char buff[1024];// 存储缓冲区
ssize_t num = 0;// 因为read的返回值是实际读取的数目
while(num = read(fd,buff,1024) != 0){
printf("%s\n",buff);
}
close(fd);
}
2. 库函数
下面这些库函数底层还是封装了上面系统接口,但使用上来的确更简单了
2.1 fopen
FILE * fopen ( const char * filename, const char * mode );
对参数mode的说明
- r表示只读的方式,而r+表示读写的方式
- w表示只写的方式,而w+表示读写的方式【当文件不存在时,会创建这个文件】
- a表示追加的方式,而a+表示读写追加的方式【当文件不存在时,会创建这个文件】
而返回的FILE* 底层就是一个int ,fopen打开文件失败返回NULL
2.2 fread(文件流)
size_t fread(void *ptr, size_t size, size_t count, FILE *stream);
参数说明:
- ptr表示 缓冲区,就是读到了之后放在哪里
- size表示 一个数据的大小
- count表示 读多少个数据
- stream表示 文件流
- 返回值:成功读取的元素(
count
)的个数。 - 成功读取:如果
fread
读取了所有的count
个元素,则返回值等于count
。 - 读取失败:
- 返回值小于
count
:可能是 到达文件末尾 或 发生错误。 - 返回
0
:- 可能是文件为空 (
feof
返回真)。 - 可能是读取时发生错误 (
ferror
返回真)
- 可能是文件为空 (
- 返回值小于
feor && feeror 检查错误原因
if (fread(buffer, size, count, file) < count) {
if (feof(file)) {
printf("已到达文件末尾\n");
} else if (ferror(file)) {
perror("读取失败");
}
}
2.3 fwrite(文件流)
size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );
参数说明:
- ptr表示 写入的内容
- size表示 一个数据的大小
- count表示 读多少个数据
- stream表示 文件流
- 返回值:成功写入的元素(
count
)的个数。 - 预期返回值:如果
fwrite
成功写入所有元素,则返回值应等于count
。 - 写入失败:
- 如果返回值小于
count
,说明写入过程中出现了错误。 - 可能是磁盘已满、文件权限问题、流中断等原因。
- 如果返回值小于
ferror
检查错误
if (fwrite(data, size, count, file) != count) {
if (ferror(file)) {
perror("写入失败");
}
}
2.4 fseek
int fseek ( FILE * stream, long int offset, int origin );
参数说明:
- stream 表示文件流
- offset 表示偏移量
- origin 表示文件指针的起始位置
- SEEK_SET 文件首位置
- SEEK_CUR 文件指针当前位置
- SEEK_END 文件末尾位置
2.5 综合案例
void function4()
{
// 打开文件
FILE* fd = fopen("code.txt","w+");// 以读写的方式打开文件,没有会自动创建
if(fd == NULL){
perror("打开文件失败");
exit(EXIT_FAILURE);
}
// 写入数据
char buff[] = "姐姐疼我";
size_t count = fwrite(buff,sizeof(char),strlen(buff),fd);
if(count != strlen(buff)){
if(ferror(fd)){
perror("文件写入失败");
}
}
fseek(fd, 0, SEEK_SET);//让文件指针回到开头
// 读取数据
char buf[1024] = {0};
size_t num = 0;
while(num = fread(buf,sizeof(char),1024,fd)){
printf("%s\n",buf);// 打印数据
if(num < 1024){
if (feof(fd)) {
printf("已到达文件末尾\n");
} else if (ferror(fd)) {
perror("读取失败");
}
}
}
// 关闭文件
fclose(fd);
}
说明一下:
-
fwrite
执行后,文件指针在文件末尾,继续fread
会从当前位置开始(此时在文件末尾),导致读取不到数据 - 所以我加了fseek(fd, 0, SEEK_SET);//让文件指针回到开头
3. 文件格式化流输入输出
3.1 格式化输入-fprintf
int fprintf ( FILE * stream, const char * format, ... );
3.2 格式化输出-fscanf
int fscanf ( FILE * stream, const char * format, ... );
3.3 综合案例
void function5()
{
// 打开文件
FILE* fd = fopen("code1.txt","w+");// 以读写的方式打开文件,没有会自动创建
if(fd == NULL){
perror("打开文件失败");
exit(EXIT_FAILURE);
}
// 写入数据
int id = 1024;
char name[] = "迟迟cool";
fprintf(fd,"%d %s",id,name);
rewind(fd);// 让文件指针回到最开始
// 输出数据
int out_id = 0;
char out_name[1024] = {0};
fscanf(fd,"%d %s",&out_id,out_name);
printf("%d %s\n",out_id,out_name);
fclose(fd);
}
- 注:使用rewind(文件描述符/文件指针)也可以让文件指针回到文件起始位置
4.feof判断文件是否正确结束
在文件读取过程中,不能用feof函数的返回值直接用来判断文件的是否结束
而是应用于当文件读取结束的时候,判定是读取失败结束,还是遇到文件尾结束
将test.txt的内容拷贝给test2.txt中
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
FILE* pfread = fopen("test.txt", "r");
if (pfread == NULL)
{
return 1;
}
FILE* pfwrite = fopen("test2.txt", "w");
if (pfwrite == NULL)
{
fclose(pfread);
pfread = NULL;
return 1;
}
//文件打开成功
//读写文件
int ch = 0;
while ((ch = fgetc(pfread)) != EOF)
{
//写文件
fputc(ch, pfwrite);
}
if (feof(pfread))
{
printf("遇到文件结束标志,文件正常结束\n");
}
else if(ferror(pfread))
{
printf("文件读取失败结束\n");
}
//关闭文件
fclose(pfread);
pfread = NULL;
fclose(pfwrite);
pfwrite = NULL;
return 0;
}
5.文件缓冲区
ANSIC 标准采用“缓冲文件系统”处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上
如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)
缓冲区的大小根据C编译系统决定的
缓冲区刷新问题
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <windows.h>
//VS2013 WIN10环境测试
int main()
{
FILE* pf = fopen("test.txt", "w");
fputs("abcdef", pf);//先将代码放在输出缓冲区
printf("睡眠10秒-已经写数据了,打开test.txt文件,发现文件没有内容\n");
Sleep(10000);
printf("刷新缓冲区\n");
fflush(pf);//刷新缓冲区时,才将输出缓冲区的数据写到文件(磁盘)
printf("再睡眠10秒-此时,再次打开test.txt文件,文件有内容了\n");
Sleep(10000);
fclose(pf);
//注:fclose在关闭文件的时候,也会刷新缓冲区
pf = NULL;
return 0;
}
主要刷新缓冲区的方式:
- fflush 表示刷新缓冲区
- fclose 也会自动刷新缓冲区
补充一下:有些实际情况,需要考虑缓冲区是否刷新的问题
第十五章 函数栈帧(难点)
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int MyAdd(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int x = 0xA;
int y = 0xB;
int z = MyAdd(x, y);
printf("z = %x\n", z);
return 0;
}
1. 认识相关寄存器
寄存器名称 | 作用 |
eax | 通用寄存器,保留临时数据,常用于返回值 |
ebx | 通用寄存器,保留临时数据 |
ebp | 栈底寄存器 |
esp | 栈顶寄存器 |
eip | 指令寄存器,保存当前指令的下一条指令的地址 |
2. 认识相关汇编命令
汇编命令 | 作用 |
mov | 数据转移指令 |
push | 数据入栈,同时esp栈顶寄存器也要发生改变 |
pop | 数据弹出至指定位置,同时esp栈顶寄存器也要发生改变 |
sub | 减法命令 |
add | 加法命令 |
call | 函数调用,1. 压入返回地址 2. 转入目标函数 |
jump | 通过修改eip,转入目标函数,进行调用 |
ret | 恢复返回地址,压入eip,类似pop eip命令 |
3.栈帧创建销毁简单流程
3.1创建调用main函数
main函数也是需要创建的,它是在_tmainCRTStartup函数中调用的,
创建main( )函数的栈帧 完成状态寄存器的保存 堆栈寄存器的保存 函数内存空间的初始化
3.2 创建变量 && 形参实例化
说明一下:
- 寄存器ebp指向当前的栈帧的底部(高地址)
- 寄存器esp指向当前的栈帧的顶部(低地址)
- 第一张图片的那2条mov汇编是在main函数的栈帧中,开辟变量x和变量y,并赋值
- 第二张图片的那4条汇编做了一件事->形参实例化(且形参实例化是向从最右边开始实例化的)
3.3 调用Myadd函数->压栈
call这条汇编主要做 1. 压入返回地址 2. 转入目标函数
压入返回地址b0 53 8f 00是为了以后能找到
3.4 创建Myadd函数的栈帧->入栈
- 执行push汇编命令,esp的指向会变
- 执行mov汇编命令
- 执行sub汇编命令(开辟空间的大小和里面的代码有关 )
3.5 释放Myadd函数的栈帧->弹栈
说明一下:
- mov汇编命令: 就可以说Myadd的函数被释放了
- pop汇编命令: 会把ebp指向main函数的栈底,esp也会变
- ret汇编命令: 会把b0 53 8f 00写回eip中
3.6 Myadd函数结果返回
- add汇编命令: 会把esp+8,
- add汇编命令: 会得到eax中的值
- Myadd函数的返回值是通过寄存器来返回的
3.7 流程总结
- 调用函数,需要先形成临时拷贝,形成过程是从右向左的
- 临时空间的开辟,是在对应函数栈帧内部开辟的
- 函数调用完毕,栈帧结构被释放掉
- 临时变量具有临时性的本质:栈帧具有临时性
- 调用函数是有成本的,成本体现在时间和空间上,本质是形成和释放栈帧有成本
- 函数调用,因拷贝所形成的临时变量,变量和变量之间的位置关系是有规律的
第十六章 可变参数列表(难点)
1. 演示案例
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdarg.h>
int FindMax(int num,...)
{
va_list arg;//定义可以访问可变参数部分的变量,其实是一个char*类型
va_start(arg, num);//使arg指向可变参数部分
int max = va_arg(arg, int);//根据类型,获取可变参数列表中的第一个数据
int i = 0;
for (i = 0; i < num - 1; i++) {
int curr = va_arg(arg, int);
if (max < curr) {
max = curr;
}
}
va_end(arg);
return max;
}
int main()
{
int max = FindMax(5, 11, 22, 33, 44, 55);
printf("max=%d\n", max);
return 0;
}
说明一下:
- 参数列表中至少有一个命名参数,如果连一个命名参数都没有,就无法使用 va_start
- 如果在 va_arg 中指定了错误的类型,那么其后果是不可预测的
- 可变参数必须从头到尾逐个访问。如果你在访问了几个可变参数之后想半途终止,这是可以的,但是,如果你想一开始就访问参数列表中间的参数,那是不行的
- 实际传入的参数如果是char,short,float,编译器在编译的时候,会自动进行提升,而在函数内部使用的时候,根据类型提取数据,更多的是通过int或者double来进行
2. 可变参数的原理
2.1 va_list && va_end
- va_list : 定义可以访问可变参数部分的变量,其实是一个char*类型
- va_end : 相当于把arg指针置成空
2.2 va_start
- va_start : 使arg指向可变参数部分
2.3 va_arg
- va_arg: 根据类型,获取可变参数列表中的第一个数据
- 这里的arg指针减去4字节,再加上4字节,可以说设计的非常巧妙
这个巧妙之处在于ap先是通过+=步长,实现了指针指向的改变,*(后-步长)拿到最终值,
但这时的ap还是在+=步长之后的位置,-步长并不会改名位置
#define _INTSIZEOF(n) ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1))
- 这是一个求最小对齐数的宏,这是4的倍数
理解一:4的倍数
- 既然是4的最小整数倍取整,那么本质是:x=4*m,
- m是具体几倍对7来讲,m就是2,对齐的结果就是8,而m具体是多少,取决于n是多少
- 如果n能整除4,那么m就是n/4,如果n不能整除4,那么m就是n/4+1
- 由此产生了一种写法:4的倍数等于(n+3)/4,也就是( n+sizeof(int)-1) )/sizeof(int)
理解二:最小4字节对齐数
- 搞清楚了满足条件最小是几倍问题,那么,计算一个最小数字x,满足 x>=n && x%4==0,
- 就变成了 4字节对齐数等于((n+4-1)/4)*4 也就是((n+sizeof(int)-1)/sizeof(int))[最小几倍] * sizeof(int)[单位大小]
理解三:理解源代码中的宏
- ((n+4-1)/4)* 4,设w=n+4-1,表达式就变成了(w/4)*4,
- 其中一个数除4等价于二级制位右移2位,一个数乘4等价于二级制位左移2位
- 简洁版:(n+4-1) & ~(4-1)
- 原码版:( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) ),
3. 命令行参数
说明一下:
- main函数也是可以传参的
- 第一个参数: argc 是个整型变量,表示命令行参数的个数(含第一个参数)
- 第二个参数: argv 是个字符指针的数组,每个元素是一个字符指针,指向一个字符串。这些字符串就是命令行中的每一个参数(字符串)
- argv数组的最后一个元素存放了一个 NULL 的指针
第十七章 数据的存储
1. 为什么数据在内存中存放的是补码
因为CPU只有加法器,而使用补码,就可以将符号位和数值域统一处理(即统一处理加法和减法)且不会需要额外的硬件电路
2. 为什么会有大小端
这是因为在计算机系统中,是以字节为单位的,比如: 每个地址单元都对应着一个字节,而位数大于8位的处理器,比如:16位,32位处理器,由于寄存器宽度大于一个字节,那么必然会存在如何将多个字节安排的问题,这就导致出现的大,小端存储
3. 验证机器大小端
方式一:通过地址来判断
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main()
{
//写代码判断当前机器的字节序
int a = 1;
char* p = (char*)&a;
if (*p == 1)
{
printf("小端\n");
}
else
{
printf("大端\n");
}
return 0;
}
注:在vs2019中编辑器是采用的小端存储
方式二:通过联合体来判断
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int check_sys()
{
union U
{
char c;
int i;
}u;
u.i = 1;
return u.c;
//返回1 就是小端
//返回0 就是大端
}
int main()
{
int ret = check_sys();
if (ret == 1)
printf("小端\n");
else
printf("大端\n");
return 0;
}
说明一下:
- 联合体的地址是共用的,char c和int i的地址是一样的
4. 浮点数在计算机中从存储
4.1 案例展示
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
int n = 9;
float* pFloat = (float*)&n;
printf("n的值为:%d\n", n);// 正常
printf("*pFloat的值为:%f\n", *pFloat);
*pFloat = 9.0;
printf("num的值为:%d\n", n);
printf("*pFloat的值为:%f\n", *pFloat);// 不正常
return 0;
}
4.2 存储形式
根据国际标准IEEE(电气和电子工程协会)754,任意一个二进制浮点数都可以用下面的形式保存
说明一下:
- (1)^S表示符号位,当S=0,V为正数;当S=1,V为负数
- M表示有效数字,大于等于1,小于2
- 2^E表示指数位
单精度浮点数模型
- 最高的1位是符号位s,接着的8位是指数E,剩下的23位为有效数字M
双精度浮点数模型
- 最高的1位是符号位s,接着的11位是指数E,剩下的52位为有效数字M
4.3 对有效数字M的特殊说明
有效数字M的取值范围是[1,2),即M可以写成1.XXXX的形式,其中XXXX表示为小数部分,IEEE 754规定,在计算机内部保存M时,默认这个数的第一位总是1,因此可以被舍去,只保留后面的XXXX部分
以32位浮点数为例,比如保存1.01的时候,将1舍去, 只保存01,M就会有24位有效位,再等需要读取的时候,再把第一位的1加上去,
4.4. 对指数上标E的特殊说明
E为一个无符号整数(unsigned int),则当E为8位,它的取值范围为0~255,E为11位,它的取值范围为0~2047
又由于科学计数法中的E是可以出现负数的,所以IEEE 754规定,存入内存时E的真实值必须再加上一个中间数,
对于8位的E,这个中间数是127;对于11位的E,这个中间数是1023 ,比如,2^10的E是10,所以保存成32位浮点数时,必须保存成10+127=137,即10001001
4.5 指数E从内存中取出三种情况
E不全为0或不全为(正常情况)
指数E的计算值减去127(或1023),得到真实值,再将有效数字M前加上第一位的1
E全为0
存的时候E加上了127,但还是为0,说明这个2 ^ E中的这个E特别小 ,规定这时取的时候,有效数字M不再加上第一位的1,而是还原为0.xxxxxx的小数。这样做是为了表示±0,以及接近于0的很小的数字
E全为1
存的时候E加上127,居然全部都变成了1,说明这个2 ^ E中的这个E特别大(正负取决于符号位s)
4.6 案例分析
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
int n = 9;
float* pFloat = (float*)&n;
printf("n的值为:%d\n", n);
printf("*pFloat的值为:%f\n", *pFloat);
*pFloat = 9.0;
printf("num的值为:%d\n", n);
printf("*pFloat的值为:%f\n", *pFloat);
return 0;
}
说明一下:
- 对于第一个printf,毫无疑问结果是9,不解释
- 对于第二个printf,float* pFloat = (float*)&n;它将n的地址强制转化成float*,并赋给了pFloat
- 此时pFloat就认为这段二进制: 是float类型存入内存的二进制
- pFloat指向9并解引用,最后又是以%f打印的,所以结果为0.000000
- 对于第三个printf,*pFloat = 9.0;把9的值赋给了n,且pFloat是一个float* 的指针变量,最后又是以%d的形式打印,所以结果为1091567616
- 对于第四个printf,和第三个printf同理,不同之处是
第三个printf以浮点数存入,以%d的形式打印,
第四个printf中也是以浮点数存入,但是却是以%f,
所以结果应该为9.000000
第十八章 信号
1. 系统信号处理
众所周知当程序是一个死循环时,在终端中使用键盘上的ctrl+c 就能结束调这个进程,因为发送了SIGINT信号
下面我将使用signal函数将SIGINT与我自己写的信号处理函数进行绑定
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
void signal_handle(int id)
{
printf("我是第%d个系统信号\n",id);
exit(EXIT_SUCCESS);// 退出进程
}
int main(int argc, char const *argv[])
{
signal(SIGINT,signal_handle);
while(1){
;// 直接死循环,然后在小键盘上输入ctrl+c发送信号
}
return 0;
}
- 由于SIGINT被我绑定了其他信号处理函数,所以这里我使用exit()进行中断
2. 用户信号处理
系统提供我们自定义信号处理的函数有2个,一个是SIGUSR1和SIGUSR2,下面我将自己控制信号的发送条件,并将自定义信号绑定到自定义处理函数上
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
void signal_handle(int id)
{
printf("我是第%d个系统信号\n",id);
exit(EXIT_SUCCESS);// 退出进程
}
int main(int argc, char const *argv[])
{
signal(SIGUSR1,signal_handle);
sleep(3);// 让进程休眠3秒钟
// 0代表当前进程,其实第一个参数应该传PID的
kill(0,SIGUSR1);
// 或者可以使用
// raise(SIGUSR1);
return 0;
}