C语言学习笔记《带你学C带你飞》P21-P40

21、指针

  • 通过变量名访问变量

  • 定义指针变量

    • 类型名 *指针变量名
    • char *pa;定义一个指向字符型的指针变量
    • int *pb;定义一个指向整型的指针变量
  • 取地址运算符和取值运算符

    • 获取某个变量的地址,可以使用取地址运算符&

      char *pa=&a;

      int *pb=&f;

    • 如果需要访问指针变量指向的数据,可以使用取值运算符*

      printf(“%c,%d”,*pa,*pb)

  • 避免访问未初始化的指针(野指针)

#include<stdio.h>
int main(){
	/*
	要初始化才对
	int a;
	int *pa=&a;
	*pa=123
	但是字符串可以int *ps="haha"
	*/
    int *a;        

    *a=123;

    return 0;

}

22、指针和数组

  • 数组名是数组第一个元素的地址

  • 指向数组的指针

    • char *p;
    • p=a;或p=&a[0];
  • 指针的运算

    • 当指针指向数组元素的时候,可以对指针变量进行加减运算,这样做的意义相当于指向距离指针所在位置向前或向后第n个元素。
    • 对比标准的下标访问数组元素,使用指针进行间接访问的方法叫作指针法
    • p+1并不是将地址加1,而是指向数组的下一个元素。(根据定义的类型长度加1个单位的int、char。。。)

23、指针数组和数组指针

  • 数组名只是一个地址,而指针是一个左值。

    #include <stdio.h>
    
    //s++ s必须是lvalue(左值)
    //C语言的术语lvalue 指用于识别或定位一个存储位置的标识符。
    //(注意∶左值同时还必须是可改)
    int main(){
        int count = 0;
        char str[] = "abcdefg";
        char *target = str;  //str固定不可改变 
        //不能直接用*str++,需定义一个临时指针
        while(*target++ != '\0'){
            count++;
        }
        printf("总共有 %d 个字符\n",count);
        return 0;
    }
    
  • 指针数组

    • int *p1[5] :指针数组 ,是数组, 数组类型为整型指针int *
  • 数组指针

    • int (*p2)[5] :数组指针, 是指针, 指向一个类型为int的数组*

24、指针和二维数组

  • 数组array[4][5]

    • array是指向二维数组第一行子数组的指针
    • 解引用:*(array+1) 【array+1的解引用,将一个地址的值取出来称为解引用】
    • *(array+1)==array[1]==&array[1][0] 指向数组第二行第一个元素
    • **(array+1)==array[1][0]
    • *(array+1)+3==&array[1][3] 指向数组第二行第4个元素
  • 结论

    • *(array+i)==array[i]
    • *(*(array+i)+j)==array[i][j]
    • *(*(*(array+i)+j)+k)==array[i][j][k]
  • 示例代码

    #include <stdio.h>
    
    int main(){
        int array[4][5] = {0};
    
        //array是指向包含5个元素的数组的指针,即指向二维数组中的第一个数组
        printf("sizeof int : %d\n",sizeof(int)); //sizeof int : 4
        printf("array : %d\n",array); //array : 6421952
        printf("array + 1 : %d\n",array + 1);//array + 1 : 6421972 [与上方相差4*5=20字节]
    
        int i,j,k=0;
        for (i=0;i<4;i++){
            for (j=0;j<5;j++){
                array[i][j] = k++;
            }
        }
    
        //*(array+1)==array[1]表示地址,指向数组第二行第一个元素
        printf("*(array + 1)    :%d\n",*(array + 1));       //地址:6421956
        printf("array[1]        :%d\n",array[1]);           //地址:6421956
        printf("&array[1][0]    :%d\n",&array[1][0]);       //地址:6421956
        printf("**(array + 1)   :%d\n",**(array + 1));      //值:5
    
        printf("*(*(array+1)+3) :%d\n",*(*(array+1)+3));    //值:8
        printf("array[1][3]     :%d\n",array[1][3]);        //值:8
    
        putchar('\n');
    
        int(*p)[5] = array;
        printf("**(p+1)          :%d\n",**(p+1));           //5
        printf("**(array+1)      :%d\n",**(array+1));       //5
        printf("array[1][0]      :%d\n",array[1][0]);       //5
        printf("*(*(p+1)+2)      :%d\n",*(*(p+1)+2));       //7
        printf("*(*(array+1)+2)  :%d\n",*(*(array+1)+2));   //7
        printf("array[1][2])     :%d\n",array[1][2]);       //7
    
        return 0;
    }
    

25、void指针和NULL指针

  • void指针 称为通用指针,可以指向任意类型的数据。也就是说任何类型的指针都可以赋值给void指针。

    #include <stdio.h>
    
    int main(){
    
        int a=1024;
        int *pi=&a;
        char *ps = "abc";
        void *pv;
    
        pv = pi;
        printf("pi: %p, pv: %p \n", pi,pv); 
        //pi: 000000000061FE04, pv: 000000000061FE04 
        
        printf("*pv: %d\n", *(int *)pv); 
        //*pv: 1024 [解引用需强转]
    
        pv = ps;
        printf("ps: %p, pv: %p \n", ps,pv);
        //ps: 0000000000404000, pv: 0000000000404000 
        
        printf("*pv: %s\n", (char *)pv); 
        // *pv: abc
    
        return 0;
    }
    
  • NULL指针 用于指针和对象

    • #define NULL ((void *)0)
    • 当你还不清楚要将指针初始化为什么地址时,请将它初始化NULL;在对指针进行解引用时,先检查该指针是否为NULL。这种策略可以为你今后编写大型程序节省大量的调试时间。
    • NULL不是NUL,NUL是空字符\0
    • NULL用于指针和对象,表示指向一个不被使用的地址;而’\0’表示字符串的结尾。

26、指向指针的指针

  • 指向指针的指针
#include <stdio.h>

int main(){
    int num = 123;
    int *p = &num;
    int **pp = &p;

    printf("num  : %d\n", num);
    printf("*p   : %d\n", *p);
    printf("**pp : %d\n", **pp);
    printf("&p   : %p, pp : %p\n", &p,pp);
    printf("&num : %p, p  : %p, *pp*: %p\n",&num, p, *pp);

    /*输出结果
    num  : 123
    *p   : 123
    **pp : 123
    &p:   000000000061FE08, pp : 000000000061FE08
    &num: 000000000061FE14, p  : 000000000061FE14, *pp*: 000000000061FE14
    */

    return 0;
}
  • 指针数组和指向指针的指针

    • 好处:

      • 避免重复分配内存

      • 只需进行一次修改

    • 代码的灵活性和安全性都有了显著地提高

#include <stdio.h>

int main(){

    //指针数组
    char *cBooks[] = {
        "《C程序设计语言》",
        "《C专家编程》",
        "《C和指针》",
        "《C陷阱与缺陷》",
        "《C Primer Plus》",
        "《带你学C带你飞》"
    };

    //使用指向指针的指针指向指针数组
    char **pp;
    char **list[4];

    pp = &cBooks[5]; 
    list[0] = &cBooks[0];
    list[1] = &cBooks[1];
    list[2] = &cBooks[2];
    list[3] = &cBooks[3];

    printf("*pp : %s\n",*pp);

    int i;
    for(i=0;i<4;i++){
        printf("*list[%d] = %s \n",i,*list[i]);
    }

    /*输出结果
    *pp : 《带你学C带你飞》
    *list[0] = 《C程序设计语言》 
    *list[1] = 《C专家编程》
    *list[2] = 《C和指针》
    *list[3] = 《C陷阱与缺陷》
    */

    return 0;
}

27、常量和指针

  • 常量:1,‘a’,3.14

    • #define A 10
    • 使用const修饰:const int a = 10【赋予变量常量性质,只读,不可修改】
  • 指向常量的指针

    • 指针可以修改为指向不同的常量
    • 指针可以修改为指向不同的变量
    • 可以通过解引用来读取指针指向的数据
    • 不可以通过解引用来修改指针指向的数据
  • 常量指针

    • 指向非常量的常量指针:
      • 指针自身不可被修饰
      • 指针指向的值可以被修改
    • 指向常量的常量指针:
      • 指针自身不可以被修改
      • 指针指向的值也不可以被修改
  • 指向 “指向常量的常量指针” 的指针

28、函数

  • 目前在开发中遇到的问题:随着程序规模的变大

    • main函数变得相当冗杂
    • 程序复杂度不断提高
    • 代码前后关联度高,修改代码往往牵一发而动全身
    • 变量的命名都成了问题
    • 为了在程序中多次实现某功能,不得不重复多次写相同的代码
  • 函数的定义;

    • 类型 函数名 ( 参数列表 ) { 函数体 }
    • 类型默认是整形
    #include <stdio.h>
    
    void print_C();
    
    void print_C(){
        printf(" ####### \n");
        printf("##     ##\n");
        printf("##       \n");
        printf("##       \n");
        printf("##       \n");
        printf("##     ##\n");
        printf(" ####### \n");
    
    }
    
    int main(){
        print_C();
        return 0;
    }
    
  • 函数的声明

    • 所谓声明(Declaration),就是告诉编译器我要使用这个函数,你现在没有找到它的定义不要紧,请不要报错,稍后我会把定义补上。
    • 写在主函数main前面

29、参数和指针

  • 形参和实参

    • 形参 形式参数
    • 实参 实际参数
  • 传值和传址

    #include <stdio.h>
    
    void swap(int x, int y);
    void swapPoint(int *a,int *b);
    
    int main()
    {   
        //传值
        int x = 3 , y = 5;
        printf("1交换前: x = %d y = %d\n",x ,y);
        swap(x, y);
        printf("1交换后: x = %d y = %d\n",x ,y); 
    
        /*
        1交换前: x = 3 y = 5
        1交换前: x = 3 y = 5
        1交换后: x = 5 y = 3
        1交换后: x = 3 y = 5
        */
        printf("\n");
    
        //传址
        int a = 3, b = 5;
        printf("2交换前: a = %d b = %d\n",a ,b);
        swapPoint(&a, &b);
        printf("2交换后: a = %d b = %d\n",a ,b); 
    
        /*
        2交换前: a = 3 b = 5
        2交换前: a = 3 b = 5
        2交换后: a = 5 b = 3
        2交换后: a = 5 b = 3
        */
    
    }
    
    void swap(int x ,int y){
        int temp;
        printf("1交换前: x = %d y = %d\n",x ,y);
        temp = x;
        x = y;
        y = temp;
        printf("1交换后: x = %d y = %d\n",x ,y);
    
    }
    
    void swapPoint(int *a ,int *b){
        int temp;
        printf("2交换前: a = %d b = %d\n",*a ,*b);
        temp = *a;
        *a = *b;
        *b = temp;
        printf("2交换后: a = %d b = %d\n",*a ,*b);
    
    }
    
    
  • 可变参数

    • #include <stdarg.h>

      • va 即 variable-argument

      • va_list

      • va_start

      • va_arg

      • va_end

    #include <stdio.h>
    #include <stdarg.h>
    
    //可变参数函数
    int sum(int n, ...);//n 表示参数数量
    
    int sum(int n, ...){//... 表示参数不确定
    
        int i , sum = 0;
        va_list vap; //定义参数列表
    
        va_start(vap, n); //初始化参数列表
    
        for(i = 0; i < n; i++){
            sum += va_arg(vap, int); //va_arg 获取后面每一个参数的值
        }
    
        va_end(vap); //关闭参数列表
    
        return sum;
    }
    
    int main(){
    
        printf("sum = %d\n" ,sum(3, 1, 2, 3));
        printf("sum = %d\n" ,sum(4, 10, 20, 30, 40));
        printf("sum = %d\n" ,sum(2, 11, 22));
    
    }
    

30、指针函数和函数指针

  • 指针函数

    • int *f();
    • 使用指针变量作为函数的返回值,就是指针函数。
    • 不要返回局部变量的指针
    #include <stdio.h>
    
    char *getWord(char);
    
    //指针函数
    //返回类型char * 为字符串
    char *getWord(char c){
        switch(c){
            case 'A' : return "Apple";
            case 'B' : return "Bnana";
            default: return "None";
        }
    }
    
    int main(){
        char input;
        printf("请输入一个字符:");
        scanf("%c",&input);
        printf("%s\n",getWord(input));
    
        return 0;
    }
    
    • 字符数组只要指名开头地址。结尾是\0自动判断
  • 函数指针

    • int (*fp)()
    #include <stdio.h>
    
    int squre(int);
    
    int squre(int num){
        return num * num;
    }
    
    int main(){
        int num;
    
        //函数指针
        int (*fp)(int);
    
        printf("请输入一个整数:");
        scanf("%d", &num);
    
        fp = squre;
        //或者 fp = &squre 【函数名相当于函数的地址】
    
        printf("%d * %d = %d \n", num, num, (*fp)(num));
        //或者 printf("%d * %d = %d \n", num, num, fp(num));
    
        return 0;
    }
    
    
  • 函数指针作为参数

    #include <stdio.h>
    
    int add(int , int);
    int sub(int , int);
    //函数指针作为参数
    int calc(int (*fp)(int,int) , int, int);
    
    int add(int a,int b){
        return a + b;
    }
    
    int sub(int a,int b){
        return a - b;
    }
    
    int calc(int (*fp)(int,int), int a, int b){
        return (*fp)(a, b);
    }
    
    int main(){
    
        printf("3 + 5 = %d\n", calc(add, 3, 5));
        printf("3 - 5 = %d\n", calc(sub, 3, 5));
        return 0;
    }
    
  • 函数指针作为返回值

    • 比如函数的名字叫select,它本身有两个参数,返回返回值是一个函数指针,这函数指针也有两个参数,并且其返回值为整型。
    • 现在让用户输入一个表达式,然后程序根据用户输入的运算符来决定调用add还是sub函数进行运算。
    #include <stdio.h>
    
    //函数指针作为返回值
    
    int add(int , int);
    int sub(int , int);
    int calc(int (*)(int,int) , int, int);
    
    //一个函数名为select参数是(char)的函数,
    //select函数的返回值是一个返回整型并且带有两个参数的函数指针
    int (*select(char))(int,int); //int (*)(int,int)
    
    int add(int a,int b){
        return a + b;
    }
    
    int sub(int a,int b){
        return a - b;
    }
    
    int calc(int (*fp)(int,int), int a, int b){
        return (*fp)(a, b);
    }
    
    int (*select(char op))(int,int){
        switch(op){
            case '+' : return add;
            case '-' : return sub;
        }
    }
    
    int main(){
        int a ,b;
        char op;
        int (*fp)(int,int);
    
        printf("请输入一个式子(如 1+3):");
        scanf("%d%c%d", &a, &op, &b);
    
        fp = select(op);
        printf("%d %c %d = %d \n",a, op, b, calc(fp, a, b));
    
        return 0;
    }
    

31、局部变量和全局变量

  • 不同函数的变量无法相互访问

  • 在函数里面定义的 局部变量

  • 在函数外边定义的 全局变量

    • 有时候,可能需要在多个函数中使用共同的一个变量,那么就会用到全局变量。因为全局变量可以被本程序中其他函数所共用的

    • 如果不对全局变量进行初始化,它会自动初始化为0

    • 如果在函数内部存在一个与全局变量同名的局部变量,并不会报错,而是在函数中屏蔽全局变量(也就是说在这个函数中,全局变量不起作用)

    • extern关键字 告诉程序变量在后面定义了,不要报错

    • 不要大量使用全局变量,因为

      1、全局变量从被定义开始,直到程序退出才能被释放,会占用更多的内存

      2、污染命名空间,虽然局部变量会屏蔽全局变量,但这样一来也会降低程序的可读性,人们往往很难一下子判断出每个变量的含义和作用范围

      3、提高了程序的耦合性,牵一发而动全身,时间久了,代码长了,你都不知道全局变量被哪些函数修改过。

32、作用域和链接属性

  • 当变量被定义在程序的不同位置时,它的作用范围是不一样的,变量作用范围就是作用域。

  • c语言编译器可以确认4种不同类型的作用域:

    • 代码块作用域(block scope)

      • 在代码块中定义的变量,具有代码块作用域。作用范围是从变量定义的位置开始,到标志该代码块结束的右大括号( } )处。
      • 尽管函数的形式参数不在大括号内定义,但其同样具有代码块作用域,隶属于包含函数体的代码块。
    • 文件作用域(file scope)

      • 任何在代码块之外声明的标识符都具有文件作用域,作用范围是从它们的声明位置开始,到文件的结尾处都是可以访问的。
      • 另外,函数名也具有文件作用域,因为函数名本身也是在代码块之外。
    • 原型作用域(prototype scope)

      • 原型作用域只适用于那些在函数原型中声明的参数名。函数在声明的时候可以不写参数的名字(但参数类型是必须要写上的),其实函数原型的参数名还可以随便写一个名字,不必与形式参数相匹配(当然,这样做没有任何意义!)。
      void func(int a,int b,int c);
      void func(int d,int e,int f){
      	…
      }
      
    • 函数作用域(function scope)

      • 函数作用域只适用于goto语句的标签,作用将goto语句的标签限制在同一个函数内部,以及防止出现重名标签。
  • 定义和声明

    • 当一个变量被定义的时候,编译器为变量申请内存空间并填充一些值
    • 当一个变量被声明的时候,编译器就知道该变量被定义在其他地方
    • 声明通知编译器该变量名及相关的类型已存在,不需要再为此申请内存空间。
    • 局部变量既是定义又是声明
    • 定义只能来一次,否则就叫做重复定义某个同名变量;而声明可以有很多次
  • 链接属性

    • .c编译.o,.o链接lib库文件,形成可执行文件
    • external(外部的) 多个文件中声明的同名标识符表示同一个实体
    • internal(内部的) 单个文件中声明的同名标识符表示同一个实体static
    • none(无) 声明的同名标识符被当做独立不同的实体
    • 只有具有文件作用域的标识符(函数名、全局变量)才能拥有external或internal的链接属性,其它作用域的标识符都是none属性
    • 默认情况下,具备文件作用域的标识符拥有external属性。也就是说该标识符允许跨文件访问。对于external属性的标识符,无论在不同文件中声明多少次,表示的都是同一个实体
    • 使用static关键字可以使得原先拥有external属性的标识符变为internal属性,限制(函数、全局变量)只能在当前文件中被调用。这里有两点需注意:
      • 使用static关键字修改链接属性,只对具有文件作用域的标识符生效(对于拥有其他作用域的标识符是另一种功能)
      • 链接属性只能修改一次,也就是说一旦将标识符的链接属性变为internal,就无法变回external了

33、生存期和存储类型

  • c语言的变量拥有两种生存期

    • 静态存储期(static storage duration)
    • 自动存储期(automatic storage duration)
  • 具有文件作用域的变量属于静态存储期,函数也属于静态存储期。属于静态存储期的变量在程序执行期间将一直占据存储空间,直到程序关闭才释放。

  • 具有代码块作用域的变量一般情况下属于自动存储期。属于自动存储期的变量在代码块结束时将自动释放存储空间。

  • 存储类型

    • 存储类型其实是指存储变量值的内存类型,C语言提供了5种不同的存储类型:
    • auto
    • register
    • static
    • extern
    • typedef
  • 自动变量(auto)

    • 在代码块中声明的变量默认的存储类型就是自动变量,使用关键字auto来描述,可省略
    #include<stdio.h>
    int main(){
        auto int i,j,k; //由于这是默认的存储类型,所以不写auto是完全没问题的。
        return 0;
    }
    
    
    #include<stdio.h>
    int i;
    int main(){
        auto int i; //用于区分,并且强调这里的i覆盖了全上一个i
    	return 0;
    }
    
    
  • 寄存器变量(register)

    • 将一个变量声明为寄存器变量,那么该变量就有可能被存放于CPU的寄存器中。
    • 寄存器变量和自动变量在很多方面都是一样的,都拥有代码块作用域、自动存储期和空连接属性
    • 不过将变量声明为寄存器变量,那么你就没有办法通过取址运算符获得该变量的地址。
  • 静态局部变量(static)

    • 使用static来声明局部变量,那么就可以将局部变量指定为静态局部变量。
    • static使得局部变量具有静态存储期,所以它的生存期与全局变量一样,直到程序结束才释放
    • 作用于文件作用域的static和extern,static关键字使得默认具有external链接属性的标识符变成internal链接属性,而extern关键字是用于告诉编译器这个变量或函数在别的地方已经定义过,先去别的地方找找,不要急着报错。
  • static和extern

    • 作用于文件作用域的static和extern,static关键字使得默认具有external链接属性的标识符变成internal链接属性,
    • 而extern关键字是用于告诉编译器这个变量或函数在别的地方已经定义过了,先去别的地方找找,不要急着报错。
  • typedef:为数据类型定义别名

34、递归

  • 递归可以解决的问题

    • 汉诺塔
    • 谢尔宾斯基三角形
    • 目录树的索引
    • 女神的自拍
  • 函数调用本身

  • 递归必须有结束条件,否则程序将崩溃

  • 递归求阶乘

    #include <stdio.h>
    
    long func(int n);
    long factorial(int n);
    
    //非递归
    long func(int n)
    {
        long result;
        for (result = 1; n > 1; n--)
        {
            result *= n;
        }
        return result;
    }
    
    //递归
    long factorial(int n)
    {
        long result;
        if (n > 0)
        {
            result = n * factorial(n - 1);
        }
        else
        {
            result = 1;
        }
        return result;
    }
    
    int main()
    {
        int n;
        printf("请输入一个整数:");
        scanf("%d", &n);
    
        printf("非递归:%d 的阶乘为: %d\n", n, func(n));
        printf("递归:%d 的阶乘为: %d\n", n, factorial(n));
    
        return 0;
    }
    
  • 实现递归满足两个条件

    • 调用函数本身
    • 设置了正确的结束条件
  • 慎用递归:普通程序员用迭代,天才程序员用递归,但我们宁可做普通程序员

35、汉诺塔

  • 递归求解汉诺塔,简单分为三个步奏:

    • 将前63个盘子从X移动到Y上
    • 将最底下的第64块盘子从X移动到Z上
    • 将Y上的63个盘子移动到Z上
  • 问题一:将X上的63个盘子借助Z移到Y上

    • 将前62个盘子从X移动到Z上。
    • 将最底下的第63个盘子移动到Y上。
    • 将Z上的62外盘子移动到Y上。
  • 问题二:将Y上的63个盘子借助X移到Z上

    • 将前62个盘子从Y移动到X上。
    • 将最底下的第63个盘子移动到Z上。
    • 将X上的62个盘子移动到Y上。
  • 代码

    #include <stdio.h>
    
    void hanoi(int n, char x, char y, char z);
    
    void hanoi(int n, char x, char y, char z)
    {
        if (n == 1)
        {
            printf("%c --> %c \n", x, z);
        }
        else
        {
            hanoi(n - 1, x, z, y);
            printf("%c --> %c \n", x, z);
            hanoi(n - 1, y, x, z);
        }
    }
    
    int main(void)
    {
        int n;
        printf("请输入汉诺塔的层数:");
        scanf("%d", &n);
    
        hanoi(n, 'X', 'Y', 'Z');
        return 0;
    }
    

36、快速排序

  • 分治法:大事化小,小事化了

  • 快速排序

    • 基本思想:通过一趟排序将待排序数据分割成独立的两部分,其中一部分所有元素均比另一部分的元素小,然后分别对两部分继续进行排序,重复上述步骤直到排序完成。

在这里插入图片描述

  • 代码

    #include <stdio.h>
    
    void quick_sort(int array[], int left, int right)
    {
        int i = left, j = right;
        int temp;
        int pivot;
    
        pivot = array[(left + right) / 2];
    
        while (i <= j)
        {
    
            //从左到右找到大于等于基准点的元素
            while (array[i] < pivot)
            {
                i++;
            }
            //从右到左找到小于等于基准点的元素
            while (array[j] > pivot)
            {
                j--;
            }
            //如果 i <= j,则互换
            if (i <= j)
            {
                temp = array[i];
                array[i] = array[j];
                array[j] = temp;
                i++;
                j--;
            }
        }
    
        if (left < j)
        {
            quick_sort(array, left, j);
        }
        if (i < right)
        {
            quick_sort(array, i, right);
        }
    }
    
    int main()
    {
    
        int array[] = {2, 65, 4, 34, 22, 66, 43};
        int i, length;
    
        length = sizeof(array) / sizeof(array[0]);
        quick_sort(array, 0, length - 1);
    
        printf("排序后的结果是:\n");
        for (i = 0; i < length; i++)
        {
            printf("%d ", array[i]);
        }
    
        return 0;
    }
    

37、动态内存管理1

  • 4个内存管理函数【库stdlib.h】

    • malloc 申请动态内存空间
    • free 释放动态内存空间
    • calloc 申请并初始化一系列内存空间
    • realloc 重新分配内存空间
  • malloc

    • 函数原型 void *malloc(size_t size);
    • malloc函数向系统中申请分配size个字节的内存空间,并返回一个指向这块空间的指针。
    • 如果函数调用成功,返回一个指向申请的内存空间的指针,由于返回的类型是void指针(void *),所以它可以被转换成任何类型的数据;如果函数调用失败,返回值是NULL。另外,如果size参数设置为0,返回值也可能为NULL,但并不意味着函数调用失败。
    #include <stdio.h>
    #include <stdlib.h>
    
    int main()
    {
        //局部变量存放在栈上
    
        int *ptr;
    
        //申请动态内存存放在堆上,如果不释放堆上的内容资源,那么它将永远存在,知道程序被关闭
        ptr = (int *)malloc(sizeof(int));
    
        if (ptr == NULL)
        {
            printf("分配内存失败\n");
            exit(1);
        }
    
        printf("请输入一个整数:");
        scanf("%d", ptr);
        printf("你输入的整数是:%d\n", *ptr);
        
        free(ptr);
        printf("你输入的整数是:%d\n", *ptr); //输出的值没有任何意义
        
        return 0;
    }
    
  • free

    • 函数原型:void free(void *ptr);
    • free函数释放ptr参数指向的内存空间。该内存空间必须是由malloc、calloc或realloc函数申请的。否则,该函数将导致未定义行为。如果ptr参数是NULL,则不执行任何操作。注意:该函数并不会修改ptr的值,所以调用后仍然指向原来的地方(变为非法空间)。
    • 在上面程序中添加 free(ptr);
  • 内存泄漏:

    • 内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
    • 一个吃满内存的死循环
    #include<stdio.h>
    #include<stdlib.h>
    
    int main()
    {
        while(1)
        {
            malloc(1024);
        }
        return 0;
    }
    
    • c语言没有垃圾回收机制
  • 导致内存泄露主要两种情况:

    • 隐式内存泄露(用完内存块没有及时使用free函数释放))
    • 丢失内存块地址
    #include<stdio.h>
    #include<stdlib.h>
    
    int main()
    {
        int *ptr;
        int num=123;
        ptr=(int *)malloc(sizeof(int));
        
        if(ptr == NULL)
        {
            printf("分配内存失败!\n");
            exit(1);
        }
        printf("请输入一个整数:\n");
        scanf("%d",ptr);
        
        printf("请输入的整数是:%d\n",*ptr);
    
        ptr=&num;//ptr又指向了别处,原来malloc申请的内存块丢失,free不能释放原来的动态内存。
    
        free(ptr);//free不能释放num所在的内存空间
    
        return 0;
    }
    

38、动态内存管理2

  • malloc还可以申请一块任意尺寸的内存空间

    #include<stdio.h>
    #include<stdlib.h>
    
    int main()
    {
        int *ptr=NULL;
        int num,i;
        printf("请输入待录入的整数的个数:");
        scanf("%d",&num);
        ptr=(int *)malloc(num*sizeof(int));
        for(i=0;i<num;i++)
        {
            printf("请录入第%d个数:\n",i+1);
            scanf("%d",&ptr[i]);
        }
        printf("你录入的整数是:\n");
        for(i=0;i<num;i++)
        {
            printf("%d ",ptr[i]);
    
        }
        free(ptr);
        return 0;
    }
    
  • 初始化内存空间

    • 以mem开头的函数被编入字符串标准库,函数的声明包含在string.h这个头文件中

      memset :使用一个常量字节填充内存空间

      memcpy :拷贝内存空间

      memmove :拷贝内存空间

      memcmp :比较内存空间

      memchr : 在内存空间中搜索一个字符

  • memset 示例

    #include<stdio.h>
    #include<stdlib.h>
    #include<string.h>
    
    #define N 10
    
    int main()
    {
        int *ptr=NULL;
        int i;
    
        ptr = (int *)malloc(N*sizeof(int));
        if(ptr==NULL)
        {
            exit(1);
        }
    
        memset(ptr, 0, N*sizeof(int));
    
        for(i=0;i<N;i++)
        {
            printf("%d ",ptr[i]);
        }
        free(ptr);
        return 0;
    }
    
  • calloc 申请并初始化一系列内存空间

    • 函数原型void *calloc(size_t nmemb, size_t size);
    • calloc函数在内存中动态地申请nmemb个长度为size的连续内存空间(即申请的总空间尺寸为nmemb *size),这些内存空间全部被初始化为0。
  • calloc函数与malloc函数的一个重要区别是:

    • calloc函数在申请完内存后,自动初始化该内存空间为零;
    • malloc函数不进行初始化操作,里面数据是随机的。
  • 所以下面两种写法是等价的:

    //calloc()分配内存空间并初始化
    
    int *ptr=(int *)calloc(8, sizeof(int));
    
    //malloc()分配内存空间并用memset()初始化
    
    int *ptr=(int *)malloc(8 * sizeof(int));
    memset(ptr, 0, 8*sizeof(int));
    
  • memcpy示例

    #include<stdio.h>
    
    #include<stdlib.h>
    
    #include<string.h>
    
    int main(){
        int *ptr1=NULL;
        int *ptr2=NULL;
    	
        //第一次申请的内存空间
        ptr1=(int *)malloc(10 *sizeof(int));
    
        //进行若干操作后发现ptr1申请的内存空间不够用
        
        //第二次申请的内存空间
        ptr2=(int *)malloc(20 *sizeof(int));
    	
        //将ptr1的数据拷贝到ptr2中
        memcpy(ptr2, ptr1, 10);
    
        free(ptr1);
    
        //对ptr2进行若干操作
    
        free(ptr2);
    
        return 0;
    }
    
  • realloc 重新分配内存空间

    • 函数原型 void *realloc(void *ptr, size_t size);
    • 以下几点需要注意;
      • realloc函数修改ptr指向的内存空间大小为size字节
      • 如果新分配的内存空间比原来的大,则旧内存块的数据不会发生改变;如果新的内存空间大小小于旧的内存空间,可能会导致数据丢失,慎用!
      • 该函数将移动内存空间的数据并返回新的指针
      • 如果ptr参数为NULL,那么调用该函数就相当于调用 malloc(size)
      • 如果size参数为0,并且ptr参数不为NULL,那么调用该函数就相当于调用 free(ptr)
      • 除非ptr参数为NULL,否则ptr的值必须由先前调用malloc、calloc或realloc函数返回
    #include <stdio.h>
    #include <stdlib.h>
    
    int main(void)
    {
        int i, num;
        int count = 0;
        int *ptr = NULL; //注意,这里必须初始化为NULL
    
        do
        {
            printf("请输入一个整数(输入-1结束): ");
            scanf("%d", &num);
            count++;
    
            ptr = (int *)realloc(ptr, count * sizeof(int));
            if (ptr == NULL)
            {
                exit(1);
            }
    
            ptr[count - 1] = num;
        } while (num != -1);
    
        printf("输入的整数分别是:");
        for (i = 0; i < count; i++)
        {
            printf("%d ", ptr[i]);
        }
        putchar('\n');
        free(ptr);
    
        return 0;
    }
    

39、C语言的内存布局

  • C语言的内存布局规律

    • 代码
    #include <stdio.h>
    #include <stdlib.h>
    
    int global_uninit_var;
    int global_init_var1 = 520;
    int global_init_var2 = 880;
    
    void func(void);
    
    void func(void)
    {
        ;
    }
    
    int mian(void)
    {
        int local_var1;
        int local_var2;
    
        static int static_uninit_var;
        static int static_init_var = 456;
    
        char *str1 = "abcdefg";
        char *str2 = "hijklmn";
    
        int *malloc_var = (int *)malloc(sizeof(int));
    
        printf("addr of func -> %p \n", func);
        printf("addr of str1 -> %p \n", str1);
        printf("addr of str2 -> %p \n", str2);
    
        printf("addr of global_init_var1 -> %p \n", &global_init_var1);
        printf("addr of global_init_var2 -> %p \n", &global_init_var2);
        printf("addr of static_init_var -> %p \n", static_init_var);
        printf("addr of static_uninit_var -> %p \n", static_uninit_var);
        printf("addr of global_uninit_var -> %p \n", global_uninit_var);
        printf("addr of malloc_var -> %p \n", malloc_var);
        printf("addr of local_var1 -> %p \n", local_var1);
        printf("addr of local_var2 -> %p \n", local_var2);
    
        return 0;
    }
    

    在这里插入图片描述

  • 根据内存地址从低到高分别划分为:

    • 代码段(Text segment)
    • 数据段(Initialized data segment)
    • BSS段(Uninitialized data segment)
    • 栈(Stack)
    • 堆(Heap)
  • 代码段

    • 代码段(Text segment)通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。
  • 数据段

    • 数据段(Initialized data segment)通常用来存放已经初始化的全局变量和局部静态变量。
  • BSS段

    • BSS段(Bss segment / Uninitialized data segment)通常是指用来存放程序中未初始化的全局变量的一块内存区域。BSS是英文Block Started by Symbol的简称,这个区段中的数据在程序运行前将被自动初始化为数字0。
    • 堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩展或缩小。当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上;当利用free等函数释放内存时,被释放的内存从堆中被剔除。
    • 大家平时可能经常听到堆栈这个词,一般指的就是这个栈。栈是函数执行的内存区域,通常和堆共享同一片区域。
    • 存放局部变量,函数参数,函数返回值
  • 堆和栈的区别

    • 申请方式:

      • 堆由程序员手动申请

      • 栈由系统自动分配

    • 释放方式:

      • 堆由程序员手动释放

      • 栈由系统自动释放

    • 生存周期:

      • 堆的生存周期由动态申请到程序员主动释放为止,不同函数之间均可自由访问
      • 栈的生存周期由函数调用开始到函数返回时结束,函数之间的局部变量不能互相访问
    • 发展方向:

      • 堆和其它区段一样,都是从低地址向高地址发展
      • 栈则相反,是由高地址向低地址发展

40、高级宏定义

  • c语言三大预处理功能

    • 宏定义:作用替换
    • 文件包含:include
    • 条件编译
  • 不带参数的宏定义

    • #define PI 3.14

    • 为了和普通的变量进行区分,宏的名字通常我们约定是全部由大写字母组成

    • 宏定义只是简单的进行替换,并且由于预处理是在编译之前进行,而编译工作的任务之一就是语法检查,所以编译器不会对宏定义进行语法检查

    • 宏定义不是说明或语句,在末尾不必加分号

    • 宏定义的作用域是从定义的位置开始到整个程序结束

    • 可以用 #undef 来终止宏定义的作用域

      #include<stdio.h>
      
      #define PI 3.14
      
      int main(){
          int r;
          float s;
      
          printf("请输入圆的半径;");
          scanf("%d",&r);
      
      #undef PI
      
          s=PI*r*r;
      
          printf("圆的面积是:%.2f\n",s);
          
          return 0;
      }
      
    • 宏定义允许嵌套

      #include<stdio.h>
      
      #define PI 3.14
      #define R 6371
      #define V PI*R*R*R*4/3
      
      int main(){
          printf("地球的体积是:%.2f\n",V);
          return 0;
      }
      
  • 带参数的宏定义

    • 在宏定义中的参数称为形式参数
    • 在宏定义调用中参数称为实际参数
    #include <stdio.h>
    
    //宏定义只是机械的替换
    #define MAX(x,y) (((x)>(y))?(x):(y)) //注意MAX没有空格(x,y)
    
    int main(){
        int a,b;
    
        printf("请输入两个数:");
        scanf("%d%d",&a,&b);
        printf("较大的数是:%d\n",MAX(a,b));
    
        return 0;
    }
    
    • 括号不能省
    #include<stdio.h>
    
    #define SQUARE(x) x*x
    
    int main(){
        int a;
        printf("请输入一个数:");
        scanf("%d",&a);
        printf("%d的平方是:%d\n",a,SQUARE(a));
        
        printf("%d的平方是:%d\n",a+1,SQUARE(a+1));//如果算x+1的平方会x+1*x+1,宏很傻,直接替换,不会帮你加括号 。保险做法#define SQUARE(x) ((x)*(x)),也不完美,例如计算a++则(a++)*(a++)或造成a加两次
    
        return 0;
    }
    
  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Fly-ping

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值