0x02 C语言
0. C语言的诞生
- 丹尼斯里奇,1972
1. 变量的类型和定义
基本数据类型(x86)
- 浮点数:float(4) double(8)
- 整数: char(1) short(2) int(4) long(4) longlong(8)
- 有符号类型和无符号类型:
- 除 char 默认是有符号的, char的实现根据不同编译器
- 可能时 unsigned 或者 signed
- 变量的定义
- 定义方式:
- 命名规范: 由字母数字下划线组成,并且不能由数字开头,不能是关键字
- 类型的名字 变量名;
- int number;
- 关键字
- auto int number; // 表示这是一个自动变量
- register int number; // 寄存器变量,通常将使用很多的数据作为寄存器变量
- static int number; // 静态变量,指挥初始化一次
- extern int number; // 声明一个其它(当前)文件已经定义的变量,不能初始化
- 赋值和初始化
- 赋值:赋值就是给一个已经被定义出来的东西进行赋值
- 初始化:在定义的时候进行赋值
2. 常量与字面量表示
- 常量的使用:
- 定义方式
- C语言中没有真常量
- 定义方式
- define 能定义出常量
- 字面量的使用:
- 字面量的类型:
- int: 123 0x123 010(八进制没有8) 123L
- char: ‘C’ ‘\n’ ‘\‘ ‘\x’十六进制字符 ‘\0’八进制字符
- const char*: “1234567” “abcdefg”,以空字符(\0)结尾的都是C风格字符串
- 123.456 => double (所有的浮点数常量默认都是都是double) 123.456f
- 字面量的类型:
3. 不同类型转换:
- 显式转换(强制转换):
- 显示转换就是人工的进行一个转换
- 隐式转换:
4. 有参宏和无参宏
- 宏的使用 define
- 有参宏:
- 例题:
- 无参宏:
- 定义常量
- define PI 3.14
- 定义一组有意义的数值(不需要关注具体的值):
- define UP 1
- define DOWN 10
- 定义常量
- 宏的缺陷:
- 有参宏: 没有类型检查,注意优先级(内联函数)
- 定义常量的时候
- 常量没有类型检查,C++推荐使用const
- 定义一组有意义的数据时,数据之间没有产生联系,推荐使用枚举
5. 输入和输出
- 格式化控制符(占位符)
- %c %d %f %lf %u %hd %s %S(wchar_t*)
- 使用printf函数
- 使用格式化输出
- printf(“%-.lf”, 10, 2, 1.1);
- printf(“%-10.2lf”, 1.1);
- %-10.2lf => %lf => double
- %-10.2lf => 不足10个字符宽度补全,否则不变
- %-10.2lf => 小数部分精度为2
- %-10.2lf => 左对齐
- 1.10XXXXXX X 表示空格
- 使用格式化输出
- 使用scanf函数
- 安全版本: 主要用于防止缓冲区溢出
- scanf_s(): 需要在字符(串)的后面加上缓冲区大小
6. 运算符和表达式
- 运算符的种类
- 算数运算符(从左到右)
- [ + - * / % ++ —]
- 前置++是先自增再运算
- 为了避免不同编译器的不同解释,不应该在表达式中使用自增\减
- [ + - * / % ++ —]
- 赋值运算符(从右到左)
- a = b = c = d = 0;
- [ += -= *= /= %= =]
- a += b c + d; => a = a + (b c + d);
- 逻辑运算符
- && || !
- 短路运算:先执行表达式的一部分,根据执行的结果确定是否执行另一部分
- && 短路运算符: 左边失败就不执行右边
- 任意一边为假就为假 a10 b10 a==b || a++
- || 短路运算符: 左边成功就不执行右边
- 任意一边为真就为真
- && || !
- 关系运算符
- [ > < = >= < == !=]
- 三目运算符
- [表达式 ? 语句一 : 语句二; ]
- 作用if else 相同,表达式成立执行语句一、否则执行语句二
- 位运算符
- [ |(or) &(and) ^(xor) ~(not) >>(ror shr sar) <<]
- 逗号运算符
- , 逗号运算符表达式的值是最后的,但是前面的所有语句都会执行
- (10 ,1.1, 100, “123”) -> “123”
- (a=10, b=a,c=a+b,++b) -> a(10) b(11) c(20) 表达式 11
- , 逗号运算符表达式的值是最后的,但是前面的所有语句都会执行
- sizeof运算符
- 获取类型的大小
- sizeof 表达式
- sizeof(类型) 推荐都加括号
- 运算符的优先级和结合性
- 表达式的种类:
- 赋值表达式: a= 10; 10
- 算数表达式: 1 + 1; 2
- 关系表达式: 1 > 2; false
- 逻辑表达式: 1 && 0: false
- 函数表达式: func(1)结果是返回值
- 算数运算符(从左到右)
7. 一维数组的使用
- 普通数组:
- 数组的特性
- 是用连续的空间存放一组类型相同的数据,数组的大小不可变
- 数组的初始化
- int number[] = {1,2,3}; 大小是由初始化的数据决定的
- int number[10]; // 没有给值,具体的值与所在位置相关
- int number[10] = {}; // 写不写0都是0
- int number[10] = {1,2,3,4}; // 结果是1234000000
- 数组的访问和修改
- number[9]; 下标从0开始,越界访问可能产生问题
- number[10] = 0; 可能会产生问题(GS)
- 可能会修改其它变量的数据
- 在堆空间中越界修改一定出错
- 数组的特性
- 字符串数组:
- 定义: 特殊的数组,类型是char
- char str[10] = “123456”;
- char str[] = “123456\0”; sizeof(str) == 7
- char str[10] = {”123456“};
- 常见的函数
- strcpy: 拷贝 strcpy_s(VS) strncpy(CRT) strncpy_s(VS)
- strstr: 字符串找字符串
- strchr: 字符串找字符
- strcmp: 字符串比较,相同返回 0,不同返回 字符1-字符2
- _strdup: 内存拷贝
- strlen: 求字符串长度,不带空字符 “123456\0”
- strcat: 字符串拼接
- sprintf: 格式化字符串
- sscanf: 将字符串作为输入缓冲区
- gets\puts: 输入\输出一行,换行
- %s 的输入使用 0x20 0x0D 0x0C 隔开
- 字符串到数字的转换
- atoi: 只能转十进制
- strtol: 推荐使用,”\x64”->100
- StrToInt: 只能转十进制
- sscanf: 不推荐
- 定义: 特殊的数组,类型是char
- 字符串和字符串数组
- const char *str = “123454678”;
- str 保存在栈区,是一个指针, “12345678”在常量区,str指向了常量区
- char str[] = “123435678”;
- str在栈区,是一个数组,”12345678”在常量区,执行完毕,str保存的是”12345678”;
- const char *str = “123454678”;
8. 二维数组的使用
二维数组的定义:
- int number[][10] = {{1, 2}, {3, 4}, {5, 6}};
- int number[10][10] = {{1, 2}, {3, 4}, {5, 6}};
- 花括号中嵌套了几个花括号就是给几行进行了赋值
- 保存的内容如下
1 2 0 0 0 0 0 0 0 0
3 4 0 0 0 0 0 0 0 0
5 6 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
数组的访问
- numebr[0][0] = 10;
- 辨别字符串数组的维度
- {“1”, “2”, “3”, “4”}
- const char* str[4] = { 0 }; // 存储的是字符串的地址,不能修改
- char str[4][2]; // 二维数组,保存了所有的字符串,能够修改
- {“1”, “2”, “3”, “4”}
9. 指针和数组:
- 指针的定义
- 在 C 语言中指针之间可以随意赋值,C语言对类型的要求并不严格
- 指向的类型* 指针的名字 = &指向的变量;
- 如果目标是常量那么指针必须也是常量
- char c; const char cc = c; C++ 中常量可以保存非常量,非常量不能保存常量值的
- 常量指针和指针常量,const 在*前面还是后面
- 常量指针: 是一个指针,指向了常量,指针可变,指向内容不可变
- const int ptr; int const ptr;
- 指针常量: 是一个常量,类型是指针,指向内容可变,指针本身不可以指向其他东西
- int * const ptr;
- 常量指针: 是一个指针,指向了常量,指针可变,指向内容不可变
- 指针的类型
- 悬空指针: 指向了被释放的空间
- 空指针: 指向了 0 的指针
- 野指针:未初始化的指针
- 指针的算术运算
- +: 指针+1,加的是 sizoef(指针类型)
- pDosHeader + pDosHeader.e_lfanew 错误的写法
- -:指针-1,数组中计算的是它们之间的元素个数,指针减法一般没有意义。
- +: 指针+1,加的是 sizoef(指针类型)
- 数组指针和指针数组*
- 数组指针: 一个指针,指向了数组
- 指针数组: 一个数组,保存了指针
- 指针数组是数组,是一个数组中存着n个指针;int *p[10] 是一个指针数组
- 数组指针是指针,是一个指向二维数组的指针;int(*p)[10] 是一个数组指针
- 函数指针
- void show(int); // 函数原型
- void (*)(int);
- void show(int); // 函数原型
10. 堆的使用:
- 堆的申请函数:
- malloc: 直接申请空间,没有初始化
- realloc: 重新申请空间,把原空间的数据拷贝到新的空间
- calloc : 申请空间并初始化 new int[10]{0} 多了初始化
- 堆的释放函数:
- free() 如果不释放会导致内存泄漏
- 内存操作函数:
- memcpy: 内存拷贝,按字节
- memset: 赋值
- memcmp: 内存比较
11. 程序的三大结构
- 顺序结构
- 特点:自上而下
- 选择结构
- 特点:多个分支只会执行一个
- 分支的种类
- if … else if …. else
- 通常用于指定的范围的判定
- 如果if不成立就执行elseif,如果都不成立就执行else
- switch.. case:
- 通常用于指定值的判定
- case: default:
- case成立就执行case,否则执行default
- 分支的执行和位置没有任何关系
- break; 如果在case中没有编写break,那么会顺序往下执行,直到switch块结束或者遇到break
- if … else if …. else
- 循环结构
- 特点:根据条件循环一定次数
- 循环的种类:
- 入口条件循环:在进入循环的时候首先进行判断
- for:for(语句1; 语句2; 语句3) { 代码块4 }
- 语句1: 通常用于初始化,只会执行1次
- 语句2:循环条件,没执行一次循环体,就要判断一次,判断次数比循环次数+1,肯定是最后一个被执行,条件一旦省略就是死循环
- 语句3:通常用于自增,执行次数和循环次数相同
- 执行顺序: 1 2 4 3 2 4 3 2 4 3 2….
- while
- while(表达式) {代码块}
- 出口条件循环: 先执行语句块再进行判断
- do while
- do { 代码块}while(条件)
- 先执行代码,在判断条件
- do while
- for:for(语句1; 语句2; 语句3) { 代码块4 }
- 入口条件循环:在进入循环的时候首先进行判断
- break和continue
- break: 可以用于 switch 和循环内,用于直接结束当前最内层的循环
- continue: 只能用于循环内,用于跳过当前最内层循环的剩下部分,进入下次循环
- 循环的嵌套
- 死循环的形成条件
- for(;;): 如果中间的表达式不填写数据,就是死循环
- while(1): 表达式永远为真,只能通过break(goto return)之类的跳转语句结束
- while 与 for 的转换
12. 使用枚举
- 枚举的定义
- enum DIR { UP, DOWN, LEFT, RIGHT};
- 如果不指定起始的值,值从0开始
- 下一个值是根据当前的值决定的,比如当前是LEFT =2,RIGHT 就是3
13. 使用结构体
- typedef 关键字
- typedef用于定义别名,通常和结构体以前使用
- 由于C语言的规定,不支持直接使用结构体的名字进行变量的定义所以,使用typedef 关键字取别名可以简化类型的定义
- typedef用于定义别名,通常和结构体以前使用
- 结构体的定义
- 访问和修改
- 使用指针: PTEST pTest = &test; pTest->a = 10;
- 使用结构体本身: Test.a = 10;
- 大小计算
- 结构体的总大小是结构体内最大类型的倍数
- 例如最大的是double,那么结构体大小肯定是8的倍数
- 结构体的成员所在的偏移和成员本身的类型相关
- 成员类型为 float, 那么它所在的便宜必须 sizoef(float) 4的倍数
- 结构体的总大小是结构体内最大类型的倍数
14. 使用联合体
- 联合体的定义:(互斥)
- union u {int n; doube d; char c};
- 访问和修改
- 同一时刻,谁都能访问,但是只有一个数据是有意义的
- 大小的计算
- 联合体的大小由最大的类型决定,所以u的大小是8
15. 函数的定义和使用
- 函数的定义
- 函数的定义由三个部分组成
- 返回值: 就是函数调用表达式的结果,返回值必须和返回值的类型相同
- 函数名: 标识当前的函数,命名规则和变量相同
- 形参列表: 当前函数接收的参数个数和类型
- 声明和定义
- 声明: void func(int); 声明一个函数没有返回值并且接受一个int参数
- 声明必须要在使用前,然后函数可以没有声明
- 当函数的定义位置在调用之前就不需要声明了
- 声明通常写于头文件,实现 通常位于CPP
- 定义(实现): 通常是函数原型后加上花括号编写函数体
- 声明: void func(int); 声明一个函数没有返回值并且接受一个int参数
- 函数的定义由三个部分组成
- 形参和实参
- 形参:形式参数,函数内的局部变量,是实参的拷贝
- 直接修改形参,实参不会改变,可以使用指针间接改变
- 实参:实际参数,传入的具体的值
- 形参:形式参数,函数内的局部变量,是实参的拷贝
- 不同类型的参数传递
- 数组的传参
- int arr[] => void show(int *) => void show(int [])
- int arr[10][10]
- void show(int (*a)[10], int size)
- void show(int [][10], int size)
- arr[10][10] 每一个元素是一个数组,数组的类型是 int[10]
- int(*p)[10]
- 基本类型的传参
- void show(想要传入的类型)
- 结构体的传参
- void show(结构体的名称 变量名)
- 不推荐使用结构体的值传递,占位置,其次不好逆向
- 函数的传参
- 考验的就是函数指针,通常用于windows编程和STL编程
- 数组的传参
- C语言函数的传参方式
- 值传递:不能够修改实参的值
- 指针传递: 可以间接修改实参的值
- 函数的递归
- 函数之间的互相调用就是函数递归
- 递归必须有结束条件,否则会导致栈溢出
- 能使用循环解决的问题,尽量不要用递归
- 内联函数
- inline: 在函数前使用inline定义内联函数
- 优点: 不会产生堆栈,加快执行速度
- 缺点: 如果内联函数内的代码过多,会导致所在函数的代码膨胀
16. 预处理指令
- 文件包含
- include 实际上进行的是赋值粘贴的操作
- 有两种方式可以防止头文件的重复包含
- pragma(once)
- ifdef XXX \ #define XXX \ #endif
- 宏定义
- define 用于定义常量,有参宏或一组有意义的名字
- 条件编译
- if #else #endif # ifdef #ifndef
- 通常用于根据不同的环境生成代码
- 特殊的宏
- LINE
- FUNC
17. 文件操作
- 打开和关闭
- 打开: fopen / fopen_s:
- r+/w+ 可读可写打开
- r 只读,存在是才打开
- w 只写,不存在就创建
- b: 二进制打开,取别在于换行 \n \r\n
- 是否是二进制读取和打开方式没有关系
- fread\fwrite
- fclose 关闭文件
- 打开: fopen / fopen_s:
- 操作文件内容
- fprintf/fscanf
- fputs/fgets
- fgetc/fputc
- fread/fwrite
- fread(读到哪里,都多少个数据块,每个数据块多大,从哪个文件读)
- fread 和 fwrite 的返回值是实际写入\读取的数据块的数量,如果读取成功,返回值和传入的第二个参数相同。
- 绝对路径和相对路径
- 绝对路径: 从根目录开始进行查找: 通常是磁盘盘符开始的
- 相对路径: 相对于当前的工作路径进行的寻址(exe所在的路径)
- . 代表当前目录
- .. 代表上层目录
18. 生存周期和作用域:
- 局部变量(栈)
- 在花括号内定义的就是局部变量,所有的局部变量都是在进入函数的时候开辟的空间
- 生存周期
- 进入花括号创建,离开函数之后销毁(关闭栈帧)
- 作用域:
- 从定义开始到花括号结束。
- 全局变量(静态数据区) .data
- 在所有花括号外面定义的就是全局变量
- 作用域
- 从定义开始到文件的结尾
- 可以使用 extern 放其它文件中的全局变量
- 注意: 全局变量的定义应该放在 cpp 中,头文件应该使用 extern
- 生存周期:
- 全局变量的初始化位于main函数之前
- 程序的开始到程序的结束
- 静态变量(静态数据区) .data
- 使用static定义的变量就是静态变量
- 生存周期:
- 生存周期是程序开始到程序结束
- 静态变量只会被初始化一次
- 作用域:
- 和定义的位置相关
- static变量不跨文件,不能在其它文件中访问
- 堆变量(堆空间)
- 堆空间的数据由编写者控制,程序中的 new\delete\malloc\free 出现问题绝大多数都是堆产生了溢出。
- 使用malloc和free系列的函数可以申请和销毁堆空间
- 生存周期
- malloc 开始 free或者程序结束 就结束
- 作用域:指针传递到哪里哪里就能访问
- 注意
- 不释放堆空间的数据会导致内存泄漏
- 如果破坏了堆标记,会影响到整个程序内的堆空间
- 常量数据区,保存的是所有的常量 .rdata
- 即使程序没有运行,常量也会存在于PE文件中
19. 其它标准函数
- clock(time): 获取当前的时间
- rand/srand: (rand\stdlib)
- srand通常在初始化的时候调用一次, srand((unsigned int) time(0));
- _getch/ _kbhit(conio.h)
- _getch(): 无回显输入
- _kbhit(): 检查键盘击键,不会接受缓冲区内的输入
- ctype.h
- islower: 判断是不是小写
- tolower: 将字母转换为小写
- isalpha: 判断是不是字母
- isdigit: 判断是不是数
- limits.h 跨平台编程 process.h _beginthreadex