野指针、空指针

  • 野指针、空指针

    野指针

    • 定义:访问了一个已经销毁或者访问受限的内存区域外的指针,这个指针就被称为野指针。

    • 野指针产生的场景:

      1.变量未初始化,通过指针访问该变量

      int a;
      int *p = &a; // p就是野指针
      ptf(*p); // 访问野指针,但是数据不安全
      

​ 2.指针变量未初始化

int *p = NULL; // 此时的p也是野指针
ptf(*p);
  1. 指针指向的内存空间被(free函数)回收了—后面讲
  2. 指针函数中直接返回了局部变量的地址。
  3. 指针指向数组以外的地址(下标越界)
  • 如何避免野指针:

写代码要养成两个习惯(通过编码规范避免)

  1. 指针变量幻及时初始化,如果暂时没有对应的值,建议赋值为NULL;

  2. 数组操作(遍历,指针运算)时,注意数组的长度,避免越界

  3. 指针指向的内存空间被回收,建议给这个指针变量赋值为NULL;

    int *p = (int*)malloc(10);// 动态内存分配
    free(p);// 动态内存释放
    p = NULL;// p不指向任何空间
    
    1. 指针变量使用之前要检查它的有效性(以后开发中要做非空校验)

      int *p = NULL;
      if(!p)
      {
          return -1;
      }
      

    说明:NULL是空常量,它的值是0,这个NULL一般存放在内存中的0x00000000位置,这个地址只能存放NULL,不能被其他程序修改。

    空指针

空指针,又被称作悬空指针:当一个指针的值是NULL,这个指针被称为空指针;对空指针访问会运行报错(段错误)

二级指针

  • 定义:二级指针,又被称作多重指针,引用一级指针的地址,此时这个指针变量就得定义成二级指针。

  • 定义格式:

    数据类型 **变量名 = 指针数组的数组名或者一级指针的地址
    
    // 指针数组
    int array = {1,2,3};
    int *arr = {&array[0],&array[1],&array[2]};// 指针数组
    // 一级指针
    int a = 10;
    int *p = &a; // 一级指针
    
  • 举例:

    // 字符型指针数组
    char *arr[3] = {"abc","aaa034","12a12"};// 等效于:char arr[3][6] = {"abc","aaa034","12a12"}
    // 定义二级指针并赋值(指针需要跟依赖的源同类型)
    char **p = arr;// 正确
    int array[2][3] = {{1,2,3},{11,22,33}}
    
    int a = 90;
    int *p = &a;// 一级指针
    int **k = &p;// 正确,二级指针
    
  • 结论

    1. 二级指针和指针数组是等效,和二维数组不等效
    2. 二维数组和数组指针是等效,和二级指针不等效
  • 二级指针的用法:

    1. 如果是字符的二级指针,可以像遍历字符串数组一样遍历它
    2. 如果是其他的二级指针,就需要解引用两次访问它所指向的数据

案例:

/**
* 二级指针案例:使用指向指针数据的指针变量。
*/
#include <stdio.h>
void fun1()
{
    char *name[]={"Follow me","BASIC","Great Wall","FORTRAN","Computer design"};
    // 定义一个二级指针
    char **p;
    // 定义循环变量
    int i = 0;
    // 遍历指针数组
    do
    {
        p = name + i;
        printf("%s\n",*p);
        i++;
    }while(i < 5);
    printf("\n");
}
void fun2()
{
    int arr1[5] = {11,12,13,14,15};
    // 创建一个指针数组
    int *arr[] = {&arr1[0],&arr1[1],&arr1[2],&arr1[3],&arr1[4]};
    int **p = arr,i = 0;
    // 遍历
    for(;i < 5; i++)
    {
        // printf("%5d",**(p+i));
        printf("%5d",**p);
        p++;
    }
    printf("\n");
}
int main()
{
    fun1();
    fun2();
}

main函数的原型

  • 定义:main函数有多种定义格式,main函数也是函数,函数相关的结论对main函数也有效(也可以定义main函数的函数指针)。

  • main函数的完整写法:

    int main(int argc,char *argv[]){}
    int main(int argc,char **argv){}
    
  • 扩展写法:

    int main(){}
    int main(void){}
    void main(){}
    main(){} ---- int main(){}
    void main(void){}
    int main(int a){}
    int main(int a,int b,int c){}
    ...
    
  • 说明:

    1. argc,argv是形参的名称,它们俩可以修改
    2. main函数的扩展写法有些编译器不支持,编译报警告
    3. argc和argv的常规写法:
  • argc:存储了参数的个数

  • argv:存储了所有参数的字符串形式

  1. main函数是系统通过函数指针的回调形式调用的
  • 注意:如果一个函数没有写返回值类型,这个函数的默认返回类型是int。

案例:

/**
* main函数
*/
#include "stdio.h"
int main(int argc,char **argv)
{
    int k;
    for (k=1;k < argc;k++)
        printf("%s\n",argv[k]);
}

常量指针与指针常量

常量:分为字面量和只读常量,字面量(就是我们平时直接操作的如:printf(12)|printf(“hello”));只读常量使用关键字 const 修饰,凡是被这个关键字修饰的变量,一旦赋值,值就不能改变。

语法:

// 字面量举例,字面量是一种匿名的常量
printf(12);
printf("请输入一个数:\n");
// 只读常量
const int a = 10;
a = 21;// 编译错误,因为此时这个变量是只读常量,所以不可更改其值

常量指针

定义:常量的指针,本质是一个指针,指针指向的数据不能改变。

定义格式:

const 数据类型 *变量名;

举例:

const int *p; // p就是常量指针
  • 结论

    1. 常量指针指向的数据不能被改变(不能解引用间接修改数据)
    2. 常量指针的地址可以改变。
  • 应用场景:作为形式参数,实际参数需要给一个常量。

  • 案例:

    #include <stdio.h>
    /* 常量指针 */
    void test1()
    {
        // 定义变量
        int a = 10;
        // 定义常量指针
        const int *p = &a;
        // *p = 100; // 编译报错,常量的值不能修改(常量指针指向地址空间的数值不能修改)
        printf("%d\n",*p);// 10
        int b = 90;
        p = &b; // 编译通过,常量的地址可以修改(常量指针指向的地址空间可以发生改变)
        printf("%d\n",*p);// 90
    }
    int main()
    {
        test1();
    }
    

    指针常量

  • 定义:指针的常量,指针的指向不能改变

  • 定义格式:

    数据类型* const 变量名;
    

    举例:

    int* const p; // 指针常量
    
  • 结论:

    1. 指针常量的指向不能被改变(不能给指针变量重新赋地址值)
    2. 指针常量指向的数据可以改变
  • 注意:指针常量在定义时就要赋值;不能先定义后赋值,否则编译报错。

  • 案例:

    /**
    * 常量指针与指针常量
    */
    #include <stdio.h>
    /* 指针常量 */
    void test2()
    {
        // 定义变量
        int a = 10;
        // 定义指针变量
        int* const p = &a;
        // 错误写法:先定义,后赋值(编译报错)
        // int* const p;
        // p = &a;
        // 间接取数据
        pirntf("%d\n",*p);
        // int b = 200;
        // p = &b;// 编译报错,地址不能改变
        *p = 200;
        printf("%d\n",*p);// 200
    }
    int main()
    {
        test2();
    }
    

    常量指针常量

  • 定义语法:

    const 数据类型* const 变量名;
    

    举例:

    const int* const p; // 常量指针常量
    
  • 作用:p的指向不能被改动(地址),p指向的数据不能被改(地址对应内存中存放的数据)

动态内存分配

我们要想实现动态内存分配,就需要学习标准C提供的函数库:

  1. 函数所属的库文件
  2. 函数的原型-函数的声明
  3. 函数名
  4. 形参
  5. 返回值类型
  6. 函数的功能
  • 注意:内存分配函数在申请内存时,建议用多少申请多少,可以有少量的预留量;但不能越界访问(虽然编译和运行不报错,但数据不安全)

常用函数

malloc

  • 头文件: #include <stdlib.h>

  • 函数功能:C 库函数 void *malloc(size_t size) 分配所需的内存空间,并返回一个指向它的指针。

  • 函数原型:

  • 函数名:malloc

    形式参数:size_t size:内存块的大小,以字节为单位。本质上就是一个 unsigned int

    返回值类型:void* :该函数返回一个指针,指向已分配大小的内存,如果请求失败,返回NULL。

  • 举例:

    int *p = (int*)malloc(4);
    
  • 说明:

    1. malloc函数分配的内存没有默认值,是不确定数,大概率是0;
    2. malloc函数申请的内存空间连续。

calloc

  • 头文件: #include <stdlib.h>

  • 函数功能:C库函数 void * calloc(size_t nitems,size_t size) 分配所需的内存空间,并返回一个指向它的指针。

    ​ malloc和calloc之间不同点事,malloc不会设置内存为零,而calloc会设置内存为零。

  • 函数原型: void *calloc(size_t nitems,size_t size)

    • 函数名:calloc
    • 形式参数:
      • size_t nitems:申请多少个
      • size_t size:一个占几个内存单元(一个内存单元 等于 一个字节)
  • 返回值类型:void*:该函数返回一个指针,指向已分配大小的内存。如果请求失败,返回NULL。

  • 举例:

int *p = (int*)calloc(3,4); // p指向的空间的大小是12个字节
if(!p) printf("内存申请失败!\n");
  • 说明:

    1. calloc函数分配的内存有默认值,每个内存单元都是0
    2. calloc函数申请的内存空间连续
    3. calloc大多时候为数组中的元素申请内存

    转存栈中数组中的数据:

    int arr[3] = {10,20,30}; // 在栈区
    int *p = (int*)calloc(3,4); // 申请内存,在堆区
    if(!p) puts("内存申请失败!");
    // 转存
    for(int i = 0;i < 3; i++)
        p[i] = arr[i];
    // 遍历
    for(int i = 0;i < 3; i++)
        printf("%d,",p[i]);
    printf("\n");
    // p使用完,记得释放内存
    free(p);
    p = NULL; // 内存回收后,建议置空
    

realloc

  • 头文件: #include <stdlib.h>

  • 函数功能:尝试重新调整之前调用malloc或calloc所分配的ptr所指向的内存块的大小。

  • 函数原型: void *realloc(void *ptr,size_t size)

  • 函数名:realloc

  • 形式参数:

    • void *ptr:是malloc或者calloc的返回值
    • size_t size:重新分配后的内存大小
  • 返回值:void*:该函数返回一个指针,指向已分配大小的内存。如果请求失败,返回NULL。

  • 案例:

    int *p = (int*)malloc(4);
    int *w = (int*)realloc(p,20);
    // int *q = (int*)realloc(p,0); // 等效于free(p)
    

free

  • 头文件: #include <stdlib.h>

  • 函数功能:释放之前调用 malloc、calloc、realloc所分配的内存空间,是访问完记得使用NULL置空。

  • 函数原型: void free(void *ptr)

    • 函数名:free

    • 形式参数:

    • void *ptr:calloc,malloc.realloc的返回值

    • 返回值类型:void:没有返回值

  • 注意:

    • 堆内存中的指针才需要回收,栈中系统会自动回收
    • 堆内存不能重复回收,运行会报错
  • 说明:

    1. 堆的内存空间相比较栈要大很多

    2. 内存分配函数返回的指针变量可以参与运算(只读),但不能被修改(p++或者p+=i 是错误的)

      //形式参数和实际参数的对应关系
      #include <stdio.h>
      //void test(int arr[]) //可以
      //void test(int arr[2]) //可以
      //void test(int * arr) //可以
      //void test(int *arr[]) //不可以,编译报错
      //void test(int ** arr) //不可以
      //void test(int (*arr)[]) //不可以
      //---------------------------------
      //void test2(int * arr[]) //可以
      //void test2(int ** arr) //可以
      void test2(int (*arr)[]) //不可以
      {
          printf("test执行了\n");
      }
      int main()
      {
          /*
      int arr[3]={0};
      test(arr);
      */
          int *arr[3] = {0};
          test2(arr);
          return 0;
      }
      
      //形式参数和实际参数的对应关系
      #include <stdio.h>
      //void test(int arr[2][3]) //可以
      //void test(int arr[][3]) //可以
      //void test(int arr[2][]) //不可以
      //void test(int arr[][]) //不可以
      //void test(int (*arr)[]) //可以
      //void test(int(*)arr[]) //语法错误
      //void test(int(*arr)[3])
      //void test(int *arr[]) //不可以
      //----------------------------------------
      //void test2(int *arr[]) //可以
      //void test2(int arr[][1]) //不可以
      //void test2(int **arr) //可以
      void test2(int(*arr)[]) //不可以
      {
          printf("test\n");
      }
      int main()
      {
          /*
      int arr[2][3] = {0};
      test(arr);
      */
          /*
      int a = 20;
      int *arr[] = {&a};
      test2(arr);
      */
          int** p;
          test3(p);
          return 0;
      }
      

void与void*的区别

  • 定义:

    • void:是空类型,是数据类型的一种
    • void*:是指针类型,是指针类型的一种,可以匹配任意类型的指针,类似于通配符
  • void

    • 说明:void作为返回值类型使用,表示没有返回值;作为形参,表示形参列表为空,在调用函
    • 数是不能给实参
  • 举例:

    // 函数声明
    void fun(void); // 等效于 void fun();
    // 函数调用
    fun();
    
  • void*

    • 说明:

      • void是一个指针类型,但该指针的数据类型不明确,无法通过解引用获取 内存中的数据,因为 void 不知道访问几个内存单元。

      • void*是一种数据类型,可以作为函数返回值类型,也可以作为形参类型

      • void*类型的变量在使用之前必须强制类型转换,明确它能够访问几个字节的内存空间

        int *p = (int*)malloc(4);
        double *p2 = (double*)malloc(8);
        
  • 举例:

    #include <stdio.h>
    #include <stdlib.h>
    // 函数定义
    void* fun(void* p) // 指针函数(返回值类型是指针的函数,此时返回的是不明确类型,需要外部强转)
    {
    int *p;
    // double *p;
    // long *p;
    // char *p;
    return p;
    }
    // 函数调用
    void main()
    {
    int *p;
    void* a = fun(p);// 这种接收方式,实际上没有意义
    printf("%p\n",a);// 可以正常打印,打印出一个地址
    *a = 10;// 编译报错,void*变量不能解引用访问数据
    int *w = (int*)a;
    *w = 10;// 编译和运行正常,void*变量a在使用前已经强制类型转换了,数据类型明确了,访问的内存
    单元明确了。
    }
    
  • 说明:

    • void作为返回值类型:这个函数可以返回任意类型( char,int*,double*等 )的指针。
    • void作为形参类型:这个函数在调用时,可以给任意类型( char,int*,double*等 )的指

    针。

    • 总结:
      • void* 类似于通配符,不能对void*类型的变量解引用(因为不明确内存单元的大小)。
      • void*在间接访问(解引用)前要强制类型转换,但不能太随意,否则存和去的数据类型不一致。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值