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; // 声明一个其它(当前)文件已经定义的变量,不能初始化
      • 赋值和初始化
  • 赋值:赋值就是给一个已经被定义出来的东西进行赋值

image.png

  • 初始化:在定义的时候进行赋值

2. 常量与字面量表示


  • 常量的使用:
    • 定义方式
      • C语言中没有真常量

image.png

  1. - 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. 不同类型转换:


  • 显式转换(强制转换):
    • 显示转换就是人工的进行一个转换

image.png

  • 隐式转换:

image.png

4. 有参宏和无参宏


  • 宏的使用 define
    • 有参宏:
    • image.png
    • 例题:
    • 无参宏:
      • 定义常量
        • define PI 3.14
      • 定义一组有意义的数值(不需要关注具体的值):
        • define UP 1
        • define DOWN 10
  • 宏的缺陷:
    • 有参宏: 没有类型检查,注意优先级(内联函数)

image.png

  • 定义常量的时候
    • 常量没有类型检查,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)
        1. 可能会修改其它变量的数据
        2. 在堆空间中越界修改一定出错
  • 字符串数组:
    • 定义: 特殊的数组,类型是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: 不推荐
  • 字符串和字符串数组
    • const char *str = “123454678”;
      • str 保存在栈区,是一个指针, “12345678”在常量区,str指向了常量区
    • char str[] = “123435678”;
      • str在栈区,是一个数组,”12345678”在常量区,执行完毕,str保存的是”12345678”;

8. 二维数组的使用


  • 二维数组的定义:

    • int number[][10] = {{1, 2}, {3, 4}, {5, 6}};
    • int number[10][10] = {{1, 2}, {3, 4}, {5, 6}};
    • 花括号中嵌套了几个花括号就是给几行进行了赋值
    • 保存的内容如下
      1. 1 2 0 0 0 0 0 0 0 0
      2. 3 4 0 0 0 0 0 0 0 0
      3. 5 6 0 0 0 0 0 0 0 0
      4. 0 0 0 0 0 0 0 0 0 0
      5. 0 0 0 0 0 0 0 0 0 0
      6. 0 0 0 0 0 0 0 0 0 0
      7. 0 0 0 0 0 0 0 0 0 0
      8. 0 0 0 0 0 0 0 0 0 0
      9. 0 0 0 0 0 0 0 0 0 0
      10. 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]; // 二维数组,保存了所有的字符串,能够修改

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,数组中计算的是它们之间的元素个数,指针减法一般没有意义。
  • 数组指针和指针数组*
    • 数组指针: 一个指针,指向了数组

image.png

  • 指针数组: 一个数组,保存了指针

image.png

  • 指针数组是数组,是一个数组中存着n个指针;int *p[10] 是一个指针数组
  • 数组指针是指针,是一个指向二维数组的指针;int(*p)[10] 是一个数组指针
  • image.png
  • 函数指针
    • void show(int); // 函数原型
      • void (*)(int);

image.png

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
  • 循环结构
    • 特点:根据条件循环一定次数
    • 循环的种类:
      • 入口条件循环:在进入循环的时候首先进行判断
        • 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(条件)
            • 先执行代码,在判断条件
  • break和continue
    • break: 可以用于 switch 和循环内,用于直接结束当前最内层的循环
    • continue: 只能用于循环内,用于跳过当前最内层循环的剩下部分,进入下次循环
  • 循环的嵌套

image.png

  • 死循环的形成条件
    • for(;;): 如果中间的表达式不填写数据,就是死循环
    • while(1): 表达式永远为真,只能通过break(goto return)之类的跳转语句结束
  • while 与 for 的转换

image.png

12. 使用枚举


  • 枚举的定义
    • enum DIR { UP, DOWN, LEFT, RIGHT};
    • 如果不指定起始的值,值从0开始
    • 下一个值是根据当前的值决定的,比如当前是LEFT =2,RIGHT 就是3

13. 使用结构体


  • typedef 关键字
    • typedef用于定义别名,通常和结构体以前使用
      • 由于C语言的规定,不支持直接使用结构体的名字进行变量的定义所以,使用typedef 关键字取别名可以简化类型的定义

image.png

  • 结构体的定义

image.png

  • 访问和修改
    • 使用指针: PTEST pTest = &test; pTest->a = 10;
    • 使用结构体本身: Test.a = 10;
  • 大小计算
    1. 结构体的总大小是结构体内最大类型的倍数
      • 例如最大的是double,那么结构体大小肯定是8的倍数
    2. 结构体的成员所在的偏移和成员本身的类型相关
      • 成员类型为 float, 那么它所在的便宜必须 sizoef(float) 4的倍数
      • image.png

14. 使用联合体


  • 联合体的定义:(互斥)
    • union u {int n; doube d; char c};
  • 访问和修改
    • 同一时刻,谁都能访问,但是只有一个数据是有意义的
  • 大小的计算
    • 联合体的大小由最大的类型决定,所以u的大小是8

15. 函数的定义和使用


  • 函数的定义
    • 函数的定义由三个部分组成
      1. 返回值: 就是函数调用表达式的结果,返回值必须和返回值的类型相同
      2. 函数名: 标识当前的函数,命名规则和变量相同
      3. 形参列表: 当前函数接收的参数个数和类型
    • 声明和定义
      • 声明: void func(int); 声明一个函数没有返回值并且接受一个int参数
        • 声明必须要在使用前,然后函数可以没有声明
        • 当函数的定义位置在调用之前就不需要声明了
        • 声明通常写于头文件,实现 通常位于CPP
      • 定义(实现): 通常是函数原型后加上花括号编写函数体
  • 形参和实参
    • 形参:形式参数,函数内的局部变量,是实参的拷贝
      • 直接修改形参,实参不会改变,可以使用指针间接改变
    • 实参:实际参数,传入的具体的值
    • image.png
  • 不同类型的参数传递
    • 数组的传参
      • 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 关闭文件
  • 操作文件内容
    • 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或者程序结束 就结束
    • 作用域:指针传递到哪里哪里就能访问
    • 注意
      1. 不释放堆空间的数据会导致内存泄漏
      2. 如果破坏了堆标记,会影响到整个程序内的堆空间
  • 常量数据区,保存的是所有的常量 .rdata
    • 即使程序没有运行,常量也会存在于PE文件中

19. 其它标准函数


  • clock(time): 获取当前的时间
  • rand/srand: (rand\stdlib)
    • srand通常在初始化的时候调用一次, srand((unsigned int) time(0));
    • image.png
  • _getch/ _kbhit(conio.h)
    • _getch(): 无回显输入
    • _kbhit(): 检查键盘击键,不会接受缓冲区内的输入
    • image.png
  • ctype.h
    • islower: 判断是不是小写
    • tolower: 将字母转换为小写
    • isalpha: 判断是不是字母
    • isdigit: 判断是不是数
  • limits.h 跨平台编程 process.h _beginthreadex