10. 运算符补充
10.1 位运算
位运算符:<< >> & | ~ ^
-
<< >> :按位左移右移,移出去的数直接丢掉,符号不动
-
&:按位与
-
|: 按位或
-
~:按位非
-
^:按位异或
位运算对应CPU最底层的门电路,运算速度非常快,这也是位运算的优点
利用位运算符输出一个整数的二进制形式
int main(){
unsigned a = 0XFFFFFFFF;
unsigned length = sizeof(a)*8;
unsigned background = 1<<(length-1);
for (int i = 0; i < length; ++i) {
unsigned shifted_a = a<<i;
unsigned temp = shifted_a & background;
unsigned result = temp>>(length-1);
printf("%u ",result);
}
return 0;
}
10.2 三元表达式
表达式1?表达式2:表达式3
- 如果表达式1取非0,则整个式子替换成表达式2
- 如果表达式1取0,则整个式子替换成表达式3
三元表达式可以嵌套使用
运算符优先级
- 三目 > 赋值类 > 逗号
- 其他所有运算符 > 三目
10.3 逗号表达式
- 逗号运算符是C语言优先级最低,即用逗号分隔的若干个子表达式
- 效果:从左至右依次执行各个子表达式
- 完成替换值:所有子表达式运算完后,最右侧的子表达式
10.4 switch语句
- 如果没有break,在执行完成对应case后会接着执行
- 注意巧用break
10.5 维持terminal(阻退)
- 思考:编译出来的 xxx.exe 文件是不是从来没双击打开过?为什么
- 因为双击打开意味着将打开一个独立窗口,程序结束则窗口自动关闭,无法查看输出
- 所以通过终端执行 .xxx.exe 来启动,程序结束后终端还在
两种解决方式:
- 两个getchart()
- system(“pause”);
11. 指针
11.1 地址和指针
注意,指针的值是一个地址
- 程序运行时,数据存放在内存中(PCB)
int n=4; //&:取变量指针,包括地址和解释方式
int *p=&n; //*:间接引用符号,表示按照该指针指向类型解读该指针记录的地址上的数据
*p=10; //int *:数据类型,表示指向int的指针
printf("%d",*p);
11.2 指针的四个性质
- 数据 = 地址 + 类型:变量名同时帮我们维护了这两者,我们直接按名用就行,但指针是对这两个信息进行解构,使对数据的处理更加灵活:
- 指针本身是一个变量,其值是一个地址,如果要打印指针的值,即打印地址的值,占位符应该是%p,表示地址
- 指针本身的类型,比如 i n t ∗ p ; int *p; int∗p;其指针本身的类型是 i n t ∗ int * int∗,指针p认为它所指向的地址存放的是一个int类型数据
- 指向的类型,比如 i n t ∗ p ; int *p; int∗p;其指针指向的类型是是 i n t int int
- 指向的值,*运算符会按照指针记录的地址和指针类型指明的类型表示方式,解读出该位置的值
- 指针通过值记录一个数据的地址类型表示解读方式实现了间接访问数据
11.3 指针传参
- 参数占用空间大,且函数对参数的使用过程中不进行修改,那么靠拷贝传参空间复杂度过大
- 拷贝传参,形参仅在函数内部有效,无法对实际参数造成影响
指针传参的特点
- 指针传参:函数的参数是指针,调用时给它传递的参数是指针
- 实际上指针传参和按值传参并无本质区别,就是指针的按值传参
11.4 二级指针
指针的本质也是一个变量,也有自己存放的地址,二级指针与一级指针的关系=一级指针与变量的关系
11.5 指针与数组
数组的特性
- 元素类型相同
- 内存地址连续(顺序存储)
- 下标的本质是偏移量
指针的特性
- 记录数据地址
- 记录数据类型
指针偏移运算
- 指针偏移运算以其指向的数据类型大小为单位
- 指针运算的结果:
- ①地址按其类型为单位变更
- ②仍然得该类型指针
- *(p +i)等价于 p[i],因此数组索引方式 a[i] 本质就是指针运算 + 间接引用
需要时刻注意,指针表示单元格数,不是字节数
12. 动态内存分配
12.1 C程序内存模型
- 内存模型:描述程序在运行时,如何组织和利用自己的内存空间
12.2 malloc与free
12.2.1 动态内存分配
- 发生在堆区:即堆区内存就是设计来给程序动态地 申请-使用-归还 的
- malloc:申请一块指定大小的堆区内存,若成功返回指向其首地址的指针
- free:归还指针所指向的堆区内存
- 使用malloc和free需要包括头文件
#include <stdlib.h>
//用malloc申请4字节的堆区内存,得到一个void*指针,强转成int*
int *ptr;
ptr = (int *)malloc(4);
*ptr = 10;
printf("%d",*ptr);
free(ptr);
//通过malloc和sizeof申请一个动态数组
int *ptr;
ptr = (int*)malloc(3*sizeof(int));
ptr[0]=1;
*(ptr+1)=2;
ptr[2]=3;
printf("%d",*ptr);//输出1
free(ptr);
堆区内存碎片
malloc分配内存空间的过程是随机散乱的,可能在堆区产生若干内存碎片
12.2.2 动态内存和数组
大多数情况下,申请到的动态堆内存都是用于实现类似动态数组
动态内存与数组的异同:
相同点:
- 数组和动态内存得到的空间都是完整的、连续的、定长的(分配后无法改变长度)
不同点:
- 数组是变量,指定类型,由编译器维护生命周期和类型
- 堆内存是自行管理、按需分配的,没有类型的限制,需要申请者通过指针来解释地址存的内容
小端机器的特点
高位地址存放高位数据,低位地址存放低位数据
12.3 内存溢出和内存泄漏
内存溢出——内存空间不足
内存泄漏——malloc但是不free,增大了内存溢出的概率“
“借了东西为什么不还~~”——《你的背包》
- 申请到内存堆空间,一定要找地方存起来,如果存放分配空间的地址被覆盖,原来申请的空间就找不到了,造成内存泄漏
13. 指针综合
13.1 数组类型与数组名
数组名支持的操作
数组名与指针的差异
- 表明数组名一定不是指针,因为指针存放地址,占用8字节,而数组名占用空间就是数组所占内存字节数
编译器对数组名的处理——数组名a其实通常为&a[0]
int a[5]={1,2,3,4,5};
- 回忆数组本质:自定义数据类型,如 int[5]表示“元素个数为5的整型数组类型
- 数组名(如a)就是数组类型(如int [5])的一个变量,但编译器对数组名会有一特殊处理:
- 除以下两种情况外,数组名 a都会被偷偷转变为 &a[0]:
-
- sizeof(a)被转化为sizeof(int [5])
-
- &a——&运算符作用于数据上相当于取得一个指针(地址),作用在a上得到四个属性为:
因此,&a其实是得到一个数组指针
13.2 数组指针与指针数组
13.2.1 数组指针——是个指针【int (*p)[5]】(本节进入指针的深水区,注意仔细体会)
- 对数组名取地址得到一个数组指针——指向数组的指针
int a[5]={1,2,3,4,5},b[5]={6,7,8,9,0};
int (*p)[5]=&a;
printf("%d,",(*p)[2]);//3
p=&b;
printf(" %d",(*p)[2]);//8
上述p的三种错误写法:
- p[2]
- *(p+2)
- *p[2]
p指向的是一个int [5]地址的指针
注意:
p
[
]
p[\ \ ]
p[ ]、
p
.
x
p.x
p.x和
p
−
>
x
p->x
p−>x中的
[
]
[\ \ ]
[ ]操作、
.
\ .
.操作和
−
>
\ ->
−>操作的优先级均高于*和&,因此
∗
p
[
2
]
=
∗
(
p
[
2
]
)
*p[2]=*(p[2])
∗p[2]=∗(p[2]),而
p
[
2
]
p[2]
p[2]是错误写法
t
e
m
p
l
a
t
e
(
∗
p
)
[
N
]
template\ (*p)[N]
template (∗p)[N]含义是,p是一个指向
t
e
m
p
l
a
t
e
[
N
]
template\ [N]
template [N]类型的指针:
t
e
m
p
l
a
t
e
(
∗
)
[
N
]
template\ (*)[N]
template (∗)[N]
13.2.2 指针数组——是个数组【int *p[5]】
- 定义一个数组,存储指定数量个指针数据:存储指针的数组
注意:区分指针数组和数组指针定义的核心关键:后缀运算符优先级大于前缀运算符
t
e
m
p
l
a
t
e
∗
p
[
N
]
template *p[N]
template∗p[N]含义是,p是一个存储了N个
t
e
m
p
l
a
t
e
∗
template\ *
template ∗类型指针的数组
13.3 指针常量和常量指针
- 此处常量指的是不变量(const)
- 指针常量和常量指针都是指针
- 区别在于是自己不能被赋值,还是其间接引用不能被赋值
- 先读在前,在前不变
13.4 字符串与字符数组
三大常考点:
- 字符串由char*表示
- 字符串结束符是’\0’
- 不能修改字符串常量的内容
13.4.1 字符串
- 字符串:一串连续的字符(字节)
- 双引号内部的字符串,其实是字符串常量,存放在常量区(被操作系统限定),不可以修改 ,如果修改会报运行时错误
尝试修改的示例:
Process finished with exit code -1073741819
遇到 “Process finished with exit code -1073741819 (0xC0000005)” 错误通常表示程序发生了访问违例(Access Violation)错误。这种错误通常是由于访问无效的内存地址或访问未初始化的变量导致的。
13.4.2 字符数组
- 字符数组:连续排列的字符变量,存放在栈区
- 初始化时,可以将常量区的字符串常量copy到字符数组中进行初始化,这种初始方法可以不考虑字符串末尾的’\0’
- 另一种初始化方法是对字符数组元素进行逐个赋值,但是这种方法需要自己补充’\0’,如果没有’\0’就只是一个单纯的字符数组,并不是字符串
字符数组与字符串的区别
- 字符串必须有’\0’作为结尾
- 字符数组可以存放字符串,但是也可以不存’\0’,做一个单纯的字符数组
13.5 字符串库函数
引入头文件:
#include <string.h>
- 首先明确,C语言没有一个类型叫“字符串”,字符串就是用char*表示的
- 以下函数的参数都是一个或两个 char* 数据
sizeof
对与字符数组,计算长度时如果末尾有’\0’,需要计入sizeof,有两种情况:
char a[]="a";//sizeof为2,strlen为1
char b[]={'a'};//sizeof为1,不是字符串strlen为0
strcpy
- strcpy:s1=>“bb\0aa\0”
- 延申1:malloc获得的堆区内存完全可以当字符数组用,用char*操作即可
- 延申2:如果strcpy,strcat等函数中的两个字符串有重叠怎么办?
【内存重叠之strcpy/strncpy/strcat/strncat和memcpy】
【strcpy的实现,以及内部重叠问题的解决。memcpy和memove的区别。】
13.6 结构体指针
访问结构体成员的两种方式
- 直接使用结构体数据(如变量)的成员,使用**.运算符**
- 若使用指针指向一个结构体数据,可使用**->运算**符访问其成员
- (*p->p).a.f是从堆区中申请的,没有进行初始化操作,因此应该是随机数
14. 文件输入输出
14.1 文本文件
14.1.1 什么是文件
- 任何外部数据源都是文件(于操作系统而言)
- 和运行的程序(内存数据)不同,文件中的数据是持久化的,断电也存在
- 但本质仍是数据(二进制字节块/字节流)
14.1.2 标准IO与文件IO
- 我们熟悉使用 printf 和 scanf 等库函数用以控制台IO
- 控制台本质也是一个外部数据源,换言之文件I0和控制台I0是类似的
- 即学习文件IO就是学习一系列C语言文件操作的函数的用法/效果
14.1.3 文本文件IO
- 通过 fopen 函数用指定模式打开指定路径的文件,获得一个 FILE*指针
- 文件打开模式包括:①权限 ②方式,以字符串的形式传递给 fopen 函数
14.1.3.1 文件打开与检查:fopen
- 文件是外存上的实体,在C程序中用一个结构体指针(FILE*)(文件指针)表示它
- 类比:同学们(文件)把学生卡(文件指针)都交给老师进行抽奖
14.1.3.2 文件路径
- 相对路径:以程序执行路径(编译后生成的.exe文件所在路径)为基准的路径
- 绝对路径:以操作系统根目录为基准的路径
14.1.3.3 文件打开模式
- r:read,读
- w:write,写
- a:appand,追加
读写权限
读写方式
14.1.3.4 从文本文件中读:fscanf
- 从文本文件中读内容等效于从控制台中读取用户输入的相同内容
- 只不过,使用 fscanf函数并指定文件指针
14.1.3.5 向文本文件中写:fprintf
- 往文本文件中写内容等效于往控制台输出内容
- 只不过,使用 加fprintf函数并指定文件指针
14.1.3.6 判断是否读取至文件末尾:feof
- 文件指针随着读写而在文件中不同位置前后移动
- 在读文件时,文件指针移动到文件末尾就意味读取完成了文件所有内容
- 使用专门的函数 feof 进行此判断
14.1.3.7 往文本文件中追加
- 往文本文件中追写内容需要在打开模式字符串中包含字母
- 会从文件的原有内容的最未尾后继续写入
- 追写模式实质是打开文件后,将文件指针置于文件未尾
其他注意点
14.1.4 二进制文件IO
- 文本文件:对数据进行ASCII编码后,按字节逐字符写入/读取
- 二进制文件:直接将内存二进制数据整体搬入文件,要解读需要知道其特定格式
二进制文件读写:fread、fwrite
- 直接把二进制文件读写理解成字节块搬家即可,fwrite搬进去,fread搬出来
调换两行,会导致读出来的东西与写入顺序不一致。