[笔记] 2017 - 黑马 C

详尽的视频教程笔记.

数据类型

数据类型的本质: 固定内存块大小的方式.
内存上: 数组名+1 得到下一个元素, &数组名+1 得到下一个数组的大小(无意义).

  • void 无类型, 就无法确定分配的内存大小, 因此 void 类型的变量会报错.
  1. 万能指针: ‘void *变量’ 指针可以确定大小. 很多没有返回值的内置函数完成后, 返回的就是这个 void 指针记录调用函数前的程序内存地址, 之后程序从那个地址继续运行.
  2. 编程最好明确所有东西的类型, 所以 ‘没返回值的函数’ 返回值上必须写 void.
    func(void) 表示函数拒绝接收参数, 不需要参数的函数应该写成这样
    func(void *Point), 表示函数的参数是任意类型的指针, 适用于 memcpy 可用于所有类型拷贝的函数

变量本质是一段连续内存的别名
字符格式的 \0 == 数字格式的 0, ‘\0’ == 0. “\0”是字符串

内存四区 (重要)

  • 堆: 手动使用的内存空间
    类型 新定义的指针名 = (类型 )malloc( sizeof(下一级类型 ***))
  • 栈: 程序局部变量, 分为 ‘main() 的栈’ 和 ‘调用函数的临时栈’
  • 全局区 global: 包含文字常量和全局变量还有静态变量, 多指针指向相同内容, 提高利用率.
  • 代码区

    画图 (重要)

    凡事要画出四区图来分析程序
  1. 画图分清楚谁在哪里
    char p = ‘Sx’; 在栈区的指针指向一个放在全局区的字符串
    char
    q = ‘Sx’; 在栈区的另一个指针指向同一个全局区的字符串
    由于全局区的效率策略, 第二个相同字符串不会重复创建, 只是让 p 和 q 都指向 ‘Sx’
  2. 变量的生命周期
    char * get_str(){ 返回 ‘指向 char 类型的指针’ 的地址

    下图分析1常量strchar strPoint_Str = “定义的是变量的字符串, 这个会被返回全局区地址”
    下图分析2变量 strchar str[] = “定义的是常量的数组名, 会返回函数临时栈区地址”;
    全局区有’ ‘Sx\0’, 定义不是字符串, 定义的不是字符串, 定义的不是字符串, 因此, 函数临时栈区也会拷贝一份 ‘Sx\0’
    用不着图分析 char tmp = (char )malloc()
    tmp 函数结束就释放, 堆区不会释放, 堆区的地址返回有效

    return str;}        __因为是数组不是字符串, 所以返回的是函数临时栈区的地址__
    

char *p; p = get_str() 乱码, 临时栈区已释放
char buf[]; strcpy(buf, get_str()); 不确定, 可能乱码, 也可能大函数拷贝时内部小函数的内存还在

栈是往下生长, 堆是往上生长. 栈中的数组是往上生长

字符串

char buf3[] = “abcd”; C 语言没有字符串类型, 只能用 char[] 代替. buf3 作为字符数组 有5个字节, 作为字符串有 4个字节
char str[100] = {0}; 要有结束符 0

sizeof(字符串类型) 的大小,包括’\0’;strlen(字符串类型) 长度不包括‘\0’(数字 0 和字符‘\0’等价)。

声明和定义区别

  1. 一般的情况下,把建立存储空间的声明称之为“定义”,而把不需要建立存储空间的声明称之为“声明”。
  2. 没{}的函数叫做定义, 有{}的函数叫声明
  3. 正数的原码, 反码, 补码都相同. 负数的反码是原码的取反, 补码 = 反码 +1
  4. 当一个小的数据类型赋值给一个大的数据类型,不会出错,因为编译器会自动转化。但当一个大的类型赋值给一个小的数据类型,那么就可能丢失高位。

    scanf函数与getchar函数

    getchar是从标准输入设备读取一个char。
    scanf通过%转义的方式可以得到用户通过标准输入设备输入的数据。
    gets(str)与scanf(“%s”,str)的区别:
    gets(str)允许输入的字符串含有空格
    scanf(“%s”,str)不允许含有空格

    指针

    指针指向谁,就把谁的地址赋值给指针
    放在=左边, 给内存赋值, 写内存 放在=右边, 取内存的值, 读内存

    定义

    是一个结构体, 包含三个元素
  5. 指针的名字, 一般叫 p q
  6. 指针自己的地址, 对二级指针这个属性才有意义
  7. 指针内存放的内容, 是一个 0x?????? 的数值.
  8. 二维数组是矩阵, 二级指针只是指向指针的指针
  9. 常量指针和变量指针. a[], a 是常量指针, 只能 a+1, 不能 a++, 区别于手动定义的变量指针 p可以 p++

    指针的使用

  10. 目的: 运行一个函数, 在函数内一次性改变多个 ‘传入实参的具体值’
  11. 类型 p= &XXXXX 初始化定义, 要把 ‘类型 ‘ 看成一个整体, 实际上是赋值操作 ‘新建指针自己的内存=’要指向的人’ 的地址

  12. 先声明, 再赋值地址指向, 才能用 * 修改指向的内容. 不能操作没有指向的野指针
  13. 取出: *p 相当于, 操作 ‘p 内存放的地址所在的内存’
  14. 取出格式, 一定要告诉编译器那个地址的内存是什么类型, 反向的理解是, 内存按照什么规则来解析,
    例如以%s的 char 类型解读的话, 就要遵守 char 读到\n 为止的规则, 传入 &str[x] 或 p, 输出 str 第 x 个字母之后的所有char, 直到最后的结束符.

    printf(“%s”, 一个(char )类型的 p1); 会打印直到\0
    printf(“%c”, 一个(char
    )类型的 p1); 会打印第一个 char

  • strcpy(p, ‘some string’) 方法是给 p 指向的 ‘char str[]内存’ 拷贝,
    省略 的原因是这个函数构造就是传入地址参数, 内部自行 传入的参数
  • 静态只读字符串本身是 ‘指向在静态区第一个字母的地址’ 的栈指针,const char “hello” , 所以可以 > char *p = “hello”, 但是修改静态字符串会报错, 要修改只能用 str[].
  • *p = p[], 某些情况 和 [] 等价, &的对立面是 /[]
    1. 作为函数参数时, 因为传入的是单个地址, 随意替换
    2. 参数以外的正确用法:  
        1. > char *str[] = {"hello", "hi"}
        2. > char * *p = str = &str[0];  二级指针指向的地址 = 一级指针地址
    3. 参数以外的错误用法: > char * *str =  {"hello", "hi"}
    
  • a+1 = &a[1], a = &a[]
  • char *a[3]= {“hello”, “hi”, “hei”} = 保存了 ‘三个字符串的首元素’ 的指针

通过函数改变实参的方法

要改 num, 执行修改的函数(参数 p), 调用函数实参(&num)*

  1. 众所周知, 函数内部只能复制参数生成临时变量, 无法改变传入的实参的具体值
  • 指针传递参数的用法:
    1. 函数定义:  > void func ( char *p) __注意这个是新建的指针, 其值等于一个地址值__
    2. 函数传参实际调用:  > func ( p 或者 &p[0]) __总之传入地址值)
    
  1. 因此要改变传入的实参, 只能把 &p 的地址传入函数, 函数内 ‘p = malloc 的堆地址’, 被调用函数是在heap上分配内存而非stack上
    //不要轻易改变形参的值, 要引入一个辅助的指针变量. 把形参给接过来…..
    int copy_str6(char from )
    {
    //
    (0) = ‘a’;
    char tmp = from;
    tmp = malloc;
    from = tmp
  2. 正确格式:
    1. 定义函数格式: func( int *p ){},
    2. 使用函数格式: func(&num) 这个叫做传地址, 传数组名也可以因为是个常量=首元素地址
  3. 错误使用格式: func( p ), 这个叫做传变量, 即便传的是记录地址的变量也不可以
  4. 错误使用格式: func( a[]/a[100]/*a), 传了个形参, 编译器看来还是指针变量
  5. 面试题问法: 上述传入的 sizeof, a[] = a[100] = * a = 一个指针的大小

传地址: 函数定义时的形参比调用时的形参多一个 *, 就是在’地址作为参数传入’的时刻, 新建 ‘函数临时变量指向实参’ 的关系.

高一级传入: int b; fun(int *a); fun(&b);
传变量: 平级传入. 要搞事只能 malloc在堆, 然后返回地址给 p, 释放临时变量也没事.
平级传入: func(char *p); func(p);

typedef int A[9];
A b; //相当于, 去掉 typedef, b 替换到 A 的位置

指针数组

每个元素都是指针
指针指向谁, 就把谁的地址给指针

char a[] = {}; []的优先级比

数组指针

指向数组的指针, 指针一跳是一整个数组的长度
数组名a是数组首元素的起始地址,但并不是数组的起始地址。

int a[10] = {0} //最终指向的数组实例
typedef int A[10]; // 为了设定指针类型, 先定义数组类型. int 和实例类型 int 要匹配.
A *p = NULL; //数组指针类型
p = &a //&a 是整个数组首地址, a 是数组首元素地址. 值相同, 每一跳大小不同

使用时 ( p)[i], 优先级 常用的类型定义方式*

typedef int (*Point)[10]; //定义类型MyPointer 为指向int[10]类型的指针
Point q;
q = &a;

常用的变量定义方式

int (*q)[10]; //q 是一个指针, 指向一个数组
q = &a

不论怎么变, 只要注意, 声明的数组名*q 要指向 ‘第 0 个元素地址’, 这样一跳才正常
一维数组: ‘第0个元素地址’ = (数组名 + 0个),

q = 数组名, q = &数组名
二维数组: ‘第0个元素地址’ = ‘第0行第0个元素地址’ =
(数组名 + 0行) + 0列
也就是, q = 数组名, q = 数组名

一维数组的 a 和 &a 值一样, 但是每一跳的内存大小不同
二维数组的 a =首行地址,和首行首元素地址 a 的值是一样, 然而他们的 ++, 一跳跳的内存大小不同
第 i 行首地址 = a + i = a[i]
第 i 行第 j 个元素地址 =
(a+i) +j = &a[i][j]
第 i 行第 j 个元素值 = ( (a+i) +j) = a[i][j]

sizeof()

sizeof(首元素地址) = 这个元素所在序列的大小
sizeof(数组名) = 一根指针的大小

结构体

  1. struct xxx 两个单词合起来才是结构体类型
  2. 结构体内定义的变量不能赋值, 因为没有分配空间
  3. 新建一个结构体的实例
    * > struct xxx *p, 刚刚定义的时候 p 是一个野指针
    * > p = &tmp, 指针要有指向, 栈, 堆, string 都可以
    * > p->age = 18, 指针结构体的用法
    * (&tmp)->age == ( *p).age, tmp 是正常的结构体实例名, p 是指针结构体实例名
    
  4. 初始化赋值一个新的结构体实例: > struct Student sx = {22, “Sx”, }, 赋值的属性会按照 ‘结构体属性定义时的顺序’ 赋值.

指针传参, 结构体版

  1. 定义函数 > void func(const struct Student *p){}
    __接收的是一个地址, 并把地址赋值给新建的结构体指针.
    不修改地址本身就加上 const, 还要注意 const 的位置__
    
  2. 要传的参数: > struct Student sx = {22, “sx”}
  3. 调用函数: > func( &sx ) 传入一个地址参数

结构体内, 包含指针成员

sx.name = (char )malloc( sizeof(char) (strlen(“stardustx”) + 1) )
分配的空间大小 = 每个char字的大小 * (存储内容的长度 + 结束符)
释放: 先释放 name, 再释放指针

结构体提高

定义结构体时不能赋值, 因为定义不分配内存.

Student p = (Student )malloc()
或者 > struct Student p = NULL; p = &Sx;指向一个实例
一般用 Student.age, 闲得蛋疼等价于 (&Student)->age
__指针用: p->age 用法相当于 (
p).age__

Teacher tmp[老师个数]; 等价于 > Teacher tmp = NULL; tmp = (Teacher )malloc(老师个数 * sizeof(Teacher));

p = tmp; 间接赋值是指针存在最大意义, tmp 中存放 malloc 函数返回的堆地址, 赋值给一级指针 p

9.4 typedef
typedef为C语言的关键字,作用是为一种数据类型(基本类型或自定义数据类型)定义一个新名字,不能创建新类型。

 与#define不同,typedef仅限于数据类型,而不是能是表达式或具体的值
 #define发生在预处理,typedef发生在编译阶段

链表

错误 typedef struct player{
int age;
struct player next;}; 结构体本身没定义完, 内存大小不确定. 递归定义多捞啊
正确 typedef struct player{
int age;
struct player *next;}; 指针大小是固定的, 可以指向下一个自己的类型

开始连接 ALLMight.next = &Sx;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//用指针指向, 替代新建节点=左边的名字, 有指针就不用名字了
player *head = NULL;
head = (player *)malloc(sizeof(player)); //两行建立一个节点在堆, 熟练
player *currentPlayerPoint = head; //地址 = 地址, 两个指针都指向头

while(1)
{
int data;
printf("请输入数据");
scanf("%d", &data);
if (data == -1){
break; }
player *NewPlayerPoint = NULL;
NewPlayerPoint = (player *)malloc(sizeof(player)); //两行建立一个节点
NewPlayerPoint->age = data; //新节点赋值
currentPlayerPoint->next = NewPlayerPoint;
currentPlayerPoint = NewPlayerPoint;
}

函数指针

  • 先定义类型

    typedef 返回值类型 (*pFunc) (参数);
    PFunc p2 = fun; 函数指针 p2 指向, PFunc 类型的 fun函数
    p2() 等价于 fun()

  • 直接定义

    int (*pFunc)(参数) = 函数实例名;

  • 实际使用
    创建条件[], 函数指针[], 创建一个字典, dict{ 条件: 函数指针}
    for 匹配条件, 调用对应的函数指针
  • 回调函数实现多态

    void BigFunc ( int x, int y, int(*SmallFunc)(int a, int b)){

    int x = SmallFunc(x, y);}  //里面只能用 BigFunc 的参数, SmallFunc 的参数只是声明
    

    把小函数名当做地址传入大函数, 大函数可以调用任意传入的小函数, 实现多功能大函数

    作用域

    void 外域:: 内部函数名(){}, 如果要在’外域’的外部声明内部函数, 类外定义内部函数就要这样格式
    data 区域存放静态量, 在函数没调用之前就存在了, 所以不能 > static int j = var
    static 的变量放到 const 的 data 区域, 第二次新建一个 static 的同名变量会失效, static 变量直到程序结束才释放.

全局变量要先赋值定义才能用, 和函数的模式一样
只能定义一次 > int a=1; 可以多次声明 > extern int a;
推荐定义的时候直接 a=0 , 赋值初始化

extern 普通全局变量是所有头文件和 main 文件都能连通使用
static 全局变量是只能在声明的本文件使用![屏幕快照 2018-11-09 11.46.51-sqd](media/

内存

程序执行前, 有几个内存分区已经确定

size a.out , 查看分区大小

  • text 代码区: 只读, 函数
  • data: 初始化的数据, 全局变量 extern, static 静态变量, 文字常量
  • bss: 没有初始化的数据, 全局变量, static 变量

程序运行后, 除了以上的内存分区, 还有两个分区即刻加载

  • stack: 自动管理, 递归或分配过大内存, 容易越界
  • heap: 手动申请释放, 程序结束系统自动回收所有. 不结束程序, 一直占用

mem 类函数的使用

  1. 语法: memxxx( 目标地址, 源文件地址, 拷贝多少内存)
  2. memset, 用来清空数据
  3. memcpy, 无视结束符的拷贝, strncpy 会被结束符结束拷贝. 可能会出现内存重叠错误
  4. memmove, 不会内存重叠的移动
  5. memcmp, 查看内存是否相等

代码操作内存

int p; p = (int )malloc(sizeof(int));

  1. malloc 分配 sizeof(int) 这么大的堆区内存空间, 成功的话, 返回堆区首元素地址
  2. 返回的地址是 (void )类型, 所以要转换成和指针类型匹配的 (int ) 类型
  3. 返回首元素内存地址给 *p

if(p != NULL){
free(p); p 指向的那块内存, 程序放弃使用权交还给系统
p=NULL} 把上述内存的入口删除

  1. 第一步, 不是释放 p 变量, 而是释放 ‘p指针内存放的地址’ 指向的内存. 释放的意思是用户放弃使用权, 交给系统回收
  2. 第二步, 把指针内存放的内容赋值为空
  3. 同一块内存被多个指针, 连续释放多次也会报错, 用 if(p != NULL) 判断

文件

fp 指针访问 File 文件, fp 不直接指向文件, 而是会自己调用了 fileOpen()
fp 是一个叫做 ‘句柄’ 结构体, 内部包含 {缓冲区, 文件描述符, 等} 成员属性
因此并不能直接操作 fp 指针, 而是调用文件库函数来操作 fp

  1. 打开和关闭和判断结尾
    windows 平台的打开关闭是 wb, rb

    fclose(stdout); 关闭printf 输出
    perror(“ 自定义 “); 打印库函数调用失败的原因
    fp = fopen( 路径, 读写权限 ). fopen 自己去安排 File 结构体, 完成操作, 返回地址给 fp
    feof(fp): 因为这个函数不会移动光标, 所以要__先fget(fp)读 fp, 再调用这个函数, 如果到文件结尾, 返回真. 读文件读到末尾是 EOF==-1. 类似字符串的结尾是 \n

  2. 读文件
    • fgetc(), fputc()读写单个字的函数, 都会自动移动光标
    • fgets(), fputs() 按行读写

      memset(buf, 0, sizeof(buf)) 先清空, 因为 fgets 失败的话, 缓冲区还是保留上次
      fgets( 缓存到哪buf, sizeof(缓存到哪buf), 从’fp 或者 stdout’ 读)

    • 按块读写

      fwrite( str, sizeof(str)单个块大小, 块个数, 写入文件)
      读文件的块数目= fread( 读入缓存在 buf, 1每次读一块, sizeof(buf)单个块大小, 源文件)
      避免要写的超过储存, 多用于每次写一个块, mdzz

  3. 写文件

    fputc(‘ 单个字符’, 打开的文件 fp 或者屏幕sdtout) 写文件的函数, 每次写一个字符, 自己写循环写入

    • 格式提取内容
      sscanf(“1stardustx6”, “%1d%9s%1d”, &num1, sx, &num2);, 把 参数一 中的内容, 按照第二个参数的格式分拆, 赋值给后面的参数. 因为要改参数本身, 所以后面的都是地址: 数字需要加&, 字符串本身就是首地址的指针
    • 提取后重组内容
      sprintf(buf, “%d\n”, str), 把 str 格式化为字符串且加上换行, 再赋值给 buf
    • 写入文件
      fprintf( 写入目标, “格式”, a, b, c)
  4. 文件光标移动函数

    fseek(fp, 0, SEEK_SET); 移动光标到相对于开头 0 个字节
    fseek(fp, 0, SEEK_END); 移动光标到结尾
    long size = ftell(fp); 获取光标所在位置到开头的大小

    缓冲区

    要写文件的指令实际上存在缓冲区, 有以下 4 种方法, 从缓冲区真正写到文件
    1. 缓冲区满, 自动写入, 不同 OS 缓冲区不一样
    2. 正常 fclose()
    3. fflush(), 迫不得已手动刷新
    4. 程序正常结束

程序编译步骤

C代码编译成可执行程序经过4步:
1)预处理:宏定义展开、头文件展开、条件编译等,同时将代码中的注释删除,这里并不会检查语法
2)编译:检查语法,将预处理后文件编译生成汇编文件
3)汇编:将汇编文件生成目标文件(二进制文件)
4)链接:C语言写的程序是需要依赖各种库的,所以编译之后还需要把库链接到最终的可执行程序中去

递归

1
2
3
4
5
6
7
8
9
10
11
12
13
int reverse(char *theStr){
if (*theStr ==0){
return 0;
}
// 1. 首先, 递归之上的语句会执行第一次, 然后随递归执行递归次
if (reverse(theStr+1) <0){
// 2. 第二, 把递归放在条件上, 就会一直自我循环直到满足
return -1;
}
// 3. 最后, 递归之下的语句会循环递归次.
printf("%c", *theStr);
return 0;
}