指针(下)

指针(下)

野指针、空指针

野指针

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

  • 野指针产生的场景:

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

      int a;
      int *p = &a; // p就是野指针
      ptf(*p); // 访问野指针,但是数据不安全
      
    2. 指针变量未初始化

      int *p = NULL; // 此时的p也是野指针
      ptf(*p);
      
    3. 指针指向的内存空间被(free函数)回收了—后面讲

    4. 指针函数中直接返回了局部变量的地址。

    5. 指针指向数组以外的地址(下标越界)

  • 何避免野指针:

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

  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 a = 10;
int *p = &a;
int **w = &p;
  • 定义格式

数据类型 **变量名 = 指针数组的数组名或者一级指针的地址

// 指针数组
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 **k = array;// 编译报错,数据类型不相符(二维数组不等于指针数组)
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:存储了所有参数的字符串形式

  4. 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
  • 结论:

    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. 函数的功能

注意:内存分配函数在申请内存时,建议用多少申请多少,可以有少量的预留量;但不能越界访问(虽然编译和运行不报错,但数据不安全)

常用函数

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)
    

    说明:

    1. realloc以原来malloc返回的内存地址开始,分配总共20个字节的内存空间

    2. 如果原来的内存空间后有20个连续空间,就扩容20-4 =16个内存单元,返回原来旧的内存首地址。

    3. 如果原来的内存空间后不够20个连续内存空间,就重新找一个内存地址开始,申请20个内存单元。并将原来的数据拷贝到新的内存中,回收旧的内存单元,并返回新的内存首地址。

free

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

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

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

    • 函数名:free
  • 形式参数:

    • void *ptr:calloc,malloc.realloc的返回值
  • 返回值类型:void:没有返回值

  • 注意:

    1. 堆内存中的指针才需要回收,栈中系统会自动回收

    2. 堆内存不能重复回收,运行会报错

说明:

  1. 堆的内存空间相比较栈要大很多
  2. 内存分配函数返回的指针变量可以参与运算(只读),但不能被修改(p++或者p+=i 是错误的)

voidvoid*的区别

  • 定义:

    • 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在使用前已经强制类型转换了,数据类型明确了,访问的内存单元明确了。
        }
        
      • 说明:

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

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

扩展:形式参数和实际参数的对应关系

//形式参数和实际参数的对应关系
#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;
}
  • 24
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值