指针和内存管理(申请释放内存函数)

文章目录

    • 指针
        • 1. 内存和地址
          • 1.1 内存
          • 1.2 内存分区
          • 1.3 内存地址【重点】
        • 2. 指针格式【重点】
        • 3.指针操作相关运算符
          • 3.1 指针操作相关运算符
          • 3.2 案例代码
        • 4. 指针变量占用的内存空间
        • 5. 数组和指针【重点】
          • 5.1 数组名就是一个指针变量
          • 5.2 数组名 + 下标方式操作
        • 6. 值传递和址传递
          • 6.1 图例说明
          • 6.2 案例代码
        • 7. 二级指针
          • 7.1 二级指针基础案例
          • 7.2 指针数组
        • 8. 函数指针【重点】
          • 8.1 函数指针概述
          • 8.2 函数指针案例
        • 9. 内存管理
          • 9.1 内存的申请和释放概述
          • 9.2 内存申请函数
            • 9.2.1 malloc 函数
            • 9.2.2 calloc 函数
            • 9.2.3 realloc 函数
          • 9.3 内存释放函数
          • 9.4 内存管理案例数组扩容

指针

1. 内存和地址
1.1 内存

计算机中核心组成模块:CPU 内存

  • CPU 是执行指令,执行程序的核心控制单元
  • 内存是存储当前执行程序的指令,二进制内容
1.2 内存分区

在这里插入图片描述

1.3 内存地址【重点】

计算机中的最小存储单元:字节(byte)

	计算机操作系统将内存每一个字节进行【编号】,类似与道路街道的门牌号。内存每一个字节都有一个唯一的编号,这个编号就是【内存地址】。
	以 4GB 内存为例,内存地址范围是 0 ~ 4294967295,为了方便数据展示和管理,计算机约定内存的地址采用【十六进制】方式,地址范围 0x0 ~ 0xFFFF FFFF

0x0 ===> NULL

	0x0 是内存中编号为 0 的内存,受到系统保护,任何程序不可以读取数据 or 写入数据到编号为 0 的内存中,否则【格杀勿论】。
	NULL 常用于指针变量初始化操作!!!
		1. 避免野值,也可以称为【野指针】
		2. 可以利用 NULL 系统报错机制,提示指针变量尚未初始化。

在这里插入图片描述

2. 指针格式【重点】
数据类型 * 指针变量 = 初始化数据;
  • 数据类型:
    • 可以认为是当前指针的数据类型,可以称之为什么类型的指针,例如 int * int 类型指针
    • 数据类型可以约束当前指针可以存储哪一种数据类型变量对应的内存空间首地址,也可以认为当前指针指向的空间是对应数据类型,例如 int * ,可以存储一个 int 类型变量的地址,也可以认为当前指针指向的数据空间数据类型认为是 int 类型。
  • *:
    • 指针特色,指针变量标记
  • 指针变量:
    • 存储内存空间的首地址。
  • 初始化数据:
    • 数据必须是一个地址,通过取地址运算符或者其他方式得到地址数据,赋值给指针变量
    • 如果在尚未明确指向的空间是哪一个,通常情况下指针变量初始化为 NULL(0x0)
3.指针操作相关运算符
3.1 指针操作相关运算符
  • & 取地址运算符,可以获取对应变量在内存空间中【首地址】
  • * 取值运算符,通过指针变量存储的地址,CPU 访问地址对应的变量,取值变量数据
3.2 案例代码
#include <stdio.h>

int main(int argc, char const *argv[])
{
    // 数据类型 * 指针变量 = 初始化数据;
    /*
    定义了一个 int 类型指针变量,指针变量名为 p
    目前初始化为 NULL
    */
    int * p = NULL;

    // 定义了一个 int 类型变量,变量名为 num ,
    // 所在内存空间是【栈区】
    int num = 10;

    /*
    & 取地址操作,可以获取变量在内存空间中的首地址。
    */
    printf("&num = %p
", &num);

    // 指针变量 p 存储 num int 类型变量空间首地址
    p = &num;
    // 查看 p 指针变量存储的地址情况
    printf("p = %p
", p);

    /*
      * 取值运算符,可以通过指针变量存储的地址,访问对应内存取值存储数据
    */
    printf("num = %d
", num);
    printf("*p = %d
", *p);

    // int * p1 = "ABCD";
    // printf("*p1 = %d
", *p1); // 1,145,258,561

    return 0;
}
4. 指针变量占用的内存空间
  • 实例说明
#include <stdio.h>

int main(int argc, char const *argv[])
{
    short *p1 = NULL;
    int *p2 = NULL;
    long *p3 = NULL;
    float *p4 = NULL;
    double *p5 = NULL;
    char *p6 = NULL;

    printf("sizeof(p1) = %ld
", sizeof(p1)); // 8 字节! 
    printf("sizeof(p2) = %ld
", sizeof(p2)); // 8 字节!
    printf("sizeof(p3) = %ld
", sizeof(p3)); // 8 字节!
    printf("sizeof(p4) = %ld
", sizeof(p4)); // 8 字节!
    printf("sizeof(p5) = %ld
", sizeof(p5)); // 8 字节!
    printf("sizeof(p6) = %ld
", sizeof(p6)); // 8 字节!

    return 0;
}
5. 数组和指针【重点】
5.1 数组名就是一个指针变量
#include <stdio.h>

void print_int_array1(int arr[], int capacity);
void print_int_array2(int arr[], int capacity);

int main(int argc, char const *argv[])
{
    int arr[10] = {1, 3, 5, 7, 9, 2, 4, 6, 8, 10};

    /*
    直接展示数组名数据情况,数组名是一个指针变量,存储对应数组空间的【首地址】。
    */
    printf("arr = %p
", arr); // arr = 0x7fffd9e7ca40
    printf("&arr[0] = %p
", &arr[0]); // &arr[0] = 0x7fffd9e7ca40

    printf("arr + 5 = %p
", arr + 5);

    printf("----------------------
");
    print_int_array1(arr, 10);
    printf("----------------------
");
    print_int_array2(arr, 10);
    return 0;
}

void print_int_array1(int arr[], int capacity)
{
    // 数组操作
    for (int i = 0; i < capacity; i++)
    {
        printf("arr[%d] = %d
", i, arr[i]);
    }
}

void print_int_array2(int arr[], int capacity)
{
    // 指针操作
    for (int i = 0; i < capacity; i++)
    {
        printf("*(arr + %d) = %d
", i, *(arr + i));
    }
}
5.2 数组名 + 下标方式操作

arr[5] <==> *(arr + 5)

arr = 0x7fffd9e7ca40
arr + 5 = 0x7fffd9e7ca54

/*
发现地址多多出 20 个字节
	ca54(16) - ca40(16) ==> 20(10)
   
arr 是一个 int 类型的指针,指针存储地址对应的内存空间对于 CPU 而言是一个 int 类型。
arr + 5 操作不是在原本的地址移动 5 个字节,而是移动 5 个 int 类型数据。

arr + 5 ==> arr地址 + 5 * sizeof(int) ==> arr地址 + 20
假设 arr 地址是 0x1000 
最终 arr + 5 对应的地址 0x1020

*(arr + 5) 获取 arr + 5 对应地址的元素,等价于 arr[5] 操作。
*/

在这里插入图片描述

6. 值传递和址传递
6.1 图例说明

在这里插入图片描述

在这里插入图片描述

6.2 案例代码
#include <stdio.h>

void swap1(int n1, int n2);

void swap2(int *p1, int *p2);

int main(int argc, char const *argv[])
{
    int num1 = 10;
    int num2 = 20;

    printf("Before num1 = %d, num2 = %d
", num1, num2); // 10 20
    // 值传递,仅数值传递,swap1 函数中的任何操作对于 num1 和 num2 没有任何的影响。
    swap1(num1, num2);
    printf("After num1 = %d, num2 = %d
", num1, num2); // 10 20

    printf("------------------------------------
");

    printf("Before num1 = %d, num2 = %d
", num1, num2); // 10 20
    // 取地址 num1 和 num2 作为当前函数的参数。
    swap2(&num1, &num2);
    printf("After num1 = %d, num2 = %d
", num1, num2); // 20 10 

    return 0;
}

void swap1(int n1, int n2)
{
    int temp = n1;
    n1 = n2;
    n2 = temp;
}

void swap2(int *p1, int *p2)
{
    int temp = *p1;
    *p1 = *p2;
    *p2 = temp;
}
7. 二级指针
7.1 二级指针基础案例

在这里插入图片描述

#include <stdio.h>

int main(int argc, char const *argv[])
{
    int num = 100;
    // 一级指针,存储 int 类型变量的空间首地址
    int * p = &num;
    // 二级指针,存储 int 类型指针变量空间首地址
    int ** q = &p;

    // num 空间首地址
    printf("&num = %p
", &num);
    printf("p = %p
", p);
    // 相当于根据二级指针存储的 一级指针变量首地址,取值一级指针存储的地址
    printf("*q = %p
", *q);

    printf("-------------------------------
");

    // num 存储的数据情况
    printf("num = %d
", num);
    printf("*p = %d
", *p);
    printf("**q = %d
", **q);

    return 0;
}
7.2 指针数组
  • 首先是一个数组
  • 数组中的每一个元素都是一个指针
#include <stdio.h>

int main(int argc, char const *argv[])
{
    int num1 = 100;
    int num2 = 200;
    int num3 = 300;

    // 定义了一个 int 类型指针数组,容量为 3
    // 每一个元素都是一个 int 类型的指针。
    int * arr[3] = {NULL};

    // 将变量取地址存储操作,存储到对应的数组中
    arr[0] = &num1;
    arr[1] = &num2;
    arr[2] = &num3;

    //【重点】数组中每一个元素,存储的内容都是地址
    printf("arr[0] = %p
", arr[0]);
    printf("arr[1] = %p
", arr[1]);
    printf("arr[2] = %p
", arr[2]);

    printf("
");

    // 【重点】通过数组中的指针取值操作,获取对应变量的数据
    printf("*arr[0] = %d
", *arr[0]);
    printf("*arr[1] = %d
", *arr[1]);
    printf("*arr[2] = %d
", *arr[2]);

    printf("
");

    // 【重点】利用数组名通过累加的方式获取数组元素存储地址对应的变量数据内容
    printf("**(arr + 0) = %d
", **(arr + 0));
    printf("**(arr + 1) = %d
", **(arr + 1));
    printf("**(arr + 2) = %d
", **(arr + 2));

    printf("
");
    printf("arr[0] = %p
", arr[0]);
    printf("*arr + 5 = %p
", *arr + 5);
    
    return 0;
}
8. 函数指针【重点】
8.1 函数指针概述

函数的分类:

  • 有参数有返回值,无参数有返回值,有参数无返回值,无参数无返回值

区分函数的特征是根据【返回值类型】【参数类型】来确定的

函数名是调用当前函数的重要的名称,同时【函数名是当前函数的指针常量】,存储当前函数在内存【方法区/函数区】的空间首地址。

#include <stdio.h>

/*
计算得到两个 int 类型数据之和

@param n1 用户提供的 int 类型数据
@param n2 用户提供的 int 类型数据
@return 两个 int 类型数据之和,返回值数据类型为 int 类型
*/
int add(int n1, int n2);

int main(int argc, char const *argv[])
{
    /*
    【重点1】
        函数的关注点是函数的【返回值数据类型】【形式参数列表数据类型】
    【重点2】
        函数名是指针常量,存储当前函数在内存【方法区/函数区】首地址
    */
    printf("add = %p
", add);

    /*
    根据当前函数的【类型】,自定函数指针,存储函数空间首地址
    自定义函数指针的格式:
    	返回值数据类型:
        			int
    	形式参数列表数据类型,以及顺序:
        			(int, int) 参数名称不重要!!!
    	函数指针的名称和固定格式:
        			(* 函数指针变量名称)
        			小括号包含 * 指针变量标记是为了优化组合关系,如果没有
        			对应函数返回值数据类型从 int 转换为 int *
    
    				pf : 可以存储 add 函数空间首地址函数指针变量
    */
    int (*pf)(int, int) = add;

    /*
    pf 就是函数指针,要求
        1. 对应函数的返回值数据类型必须是 int 类型
        2. 对应函数的形式参数列表必须是(int, int)
    */

    /*
    函数指针,可以直接当做对应函数使用
    */
    int ret = pf(10, 20);
    printf("ret = %d
", ret);

    return 0;
}

int add(int n1, int n2)
{
    return n1 + n2;
}

在这里插入图片描述

8.2 函数指针案例

接口思维

  • 规范限制
  • 接口可以做什么,取决于接口对应的连接目标是什么。
  • 思考 USB 接口概念

在这里插入图片描述

#include <stdio.h>

int add(int n1, int n2);
int sub(int n1, int n2);
int mul(int n1, int n2);
int div(int n1, int n2);

/*
多功能函数,可以处理用户提供的两个 int 类型数据,处理方式由函数指针 pf 对应的函数决定。

@param n1 用户提供的 int 类型数据
@param n2 用户提供的 int 类型数据
@param pf 处理两个 int 类型数据对应的函数指针,决定当前函数最终功能。
@return 处理数据之后在最终反馈结果。
*/
int operation(int n1, int n2, int (*pf)(int, int));

int main(int argc, char const *argv[])
{
    // shift + Alt + F 对齐代码
    int (*pf)(int, int) = NULL;

    // pf 函数指针指向不同的函数,处理数据的结果不同,执行操作一致
    pf = add;
    printf("ret = %d
", pf(10, 20));

    pf = sub;
    printf("ret = %d
", pf(80, 20));

    pf = mul;
    printf("ret = %d
", pf(30, 20));

    pf = div;
    printf("ret = %d
", pf(100, 20));
    printf("--------------------------------------------
");
    // 调用函数指针作为函数参数的 operation 函数
    /*
    函数指针作为函数的参数,可以直接提供符合函数指针要求的函数名称作为实际参数,提供的数据本质是函数在内存【方法区/函数区】的空间首地址,CPU 可以根据函数指针对应的地址,访问对应的二进制可执行内存空间内容,执行目标函数
    */
    int ret = operation(20, 100, add);
    printf("ret = %d
", ret);
    ret = operation(200, 100, sub);
    printf("ret = %d
", ret);
    ret = operation(20, 10, mul);
    printf("ret = %d
", ret);
    ret = operation(1000, 100, div);
    printf("ret = %d
", ret);
    return 0;
}

int operation(int n1, int n2, int (*pf)(int, int))
{
    // 函数指针可以直接当前函数使用。
    return pf(n1, n2);
}

int add(int n1, int n2)
{
    return n1 + n2;
}
int sub(int n1, int n2)
{
    return n1 - n2;
}
int mul(int n1, int n2)
{
    return n1 * n2;
}
int div(int n1, int n2)
{
    return n1 / n2;
}
9. 内存管理
9.1 内存的申请和释放概述
  • 内存申请

    内存申请解决程序所需变量占用内存空间较大,例如: 数组,链表…,如果存储在栈区,对于整个内存空间不友好,并且栈区空间较小。可以申请【内存堆区】空间,存储对应的目标数据内容,利用指针存储对应申请空间的首地址,方便我们操作。

  • 内存释放

    通过内存申请函数在内存【堆区】开辟的空间,需要通过释放函数告知系统,当前内存已归还!

9.2 内存申请函数

在 C 语言中,申请内存的常用函数是malloccallocrealloc

需要导入头文件 stdlib.h

9.2.1 malloc 函数
  1. 函数格式:

    malloc 函数用于在堆上分配指定大小的内存空间。它的声明如下:

    void* malloc(size_t size);
    

    malloc 函数接受一个参数 size,表示需要分配的字节数。如果分配成功,它会返回一个指向分配内存的指针;如果分配失败,返回 NULL

  1. malloc 案例

在这里插入图片描述

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char const *argv[])
{
    /*
    void * malloc(size_t size);
        size_t ==> unsigned long 无符号长整型,仅支持正数 long 类型
        malloc 函数接受一个参数 size,表示需要分配的字节数。如果分配成功,它会返回一个指向分配内存的指针;如果分配失败,返回 NULL。

    void * 万能指针,可以指向任何一个空间首地址,但是系统要求,不可以通过 void * 访问读取目标空间中数据内容。在使用时需要进行【强制类型转换】,明确告知当前指针对应数据空间,按照哪一个类型进行处理。
    */
    // 当前操作只是向 CPU 申请内存堆区空间,要求空间字节数为 40 个字节。
    void *p = malloc(40);

    /*
    [小缺失] 使用 malloc 申请的内存空间,需要明确告知空间存储数据类型或者说申请的内存空间,按照哪一个类型进行分配
        (int *)p  强制类型转换,原本 p 是 void * ,强制转换为 int *
        告知 CPU 当前 p 对应地址的内存,按照 int 类型数据方式处理
    */
    int *p2 = (int *)p;

    /*
    可以通过 p2 操作申请的内存空间,CPU 通过 p2 操作对应的内存空间,空间中的每一个元素都是 int 类型
    */
    for (int i = 0; i < 40 / sizeof(int); i++)
    {
        // 方式一: 可以按照指针形式进行操作
        // *(p2 + i) = i * 2;

        // 方式二: 可以按照数组形式进行操作,当前指针形式,数组的存储数据类型为 int 类型
        p2[i] = i * 2;
    }


    for (int i = 0; i < 40 / sizeof(int); i++)
    {
        // 方式一: 可以按照指针形式进行操作
        // printf("*(p2 + %d) = %d
", i, *(p2 + i));

        // 方式二: 可以按照数组形式进行操作,当前指针形式,数组的存储数据类型为 int 类型
        printf("p2[%d] = %d
", i, p2[i]);
    }

    /*
    void free(void* ptr);
        free 函数是用于释放 malloc realloc calloc 申请的内存空间,需要提供给当前函数申请空间的【首地址】,所需参数是 void * 类型,在实际使用中,可以提供任意类型指针。

        free 操作将 p2 指向内存空间进行释放操作,告知系统当前内存使用完毕,可以其他程序申请。
    */
    free(p2);

    /*
    p 和 p2 原本都存储申请空间首地址,但是对应空间已经通过 free 释放归还给系统,为了安全操作不可以通过 p 和 p2 访问原本的数据空间,重新赋值为 NULL,防止后期使用。
    */
    p = NULL;
    p2 = NULL;

    return 0;
}
9.2.2 calloc 函数
  1. 函数格式

    calloc 函数用于在堆上分配指定数量的指定大小的内存空间,并将分配的内存全部设置为零。它的声明如下:

    void* calloc(size_t num, size_t size);
    

    calloc 函数接受两个参数 numsize,分别表示需要分配的元素个数和每个元素的大小。它会返回一个指向分配内存的指针;如果分配失败,返回 NULL

  1. calloc 案例
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char const *argv[])
{
    /*
    calloc 案例
        void* calloc(size_t num, size_t size);
        通过 calloc 函数申请内存【堆区空间】,要求申请 10 个元素,每一个元素占用内存 4 个字节,总计内存空间 40 个字节。

        同时直接当前当前申请内存空间首地址,强转为 int * , CPU 通过 p 操作对应内存空间,每一个元素都是 int 类型。
    */
    int *p = (int *)calloc(10, sizeof(int));

    for (int i = 0; i < 10; i++)
    {
        p[i] = 99;
    }

    for (int i = 0; i < 10; i++)
    {
        printf("p[%d] = %d
", i, p[i]);
    }

    free(p);
    p = NULL;

    return 0;
}
9.2.3 realloc 函数
  1. 函数格式

    realloc 函数用于重新分配已经申请的内存空间的大小。它的声明如下:

    void* realloc(void* ptr, size_t size);
    

    realloc 函数接受两个参数 ptrsize,其中 ptr 是先前使用 malloccallocrealloc 返回的指针,size 表示需要重新分配的字节数。它会返回一个指向重新分配内存的指针;如果分配失败,返回 NULL。需要注意的是,realloc 函数可能会在原地或者重新分配内存。

  1. realloc 案例

在这里插入图片描述

在这里插入图片描述

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char const *argv[])
{
    // malloc 申请 40 个字节数据空间
    int *p = (int *)malloc(10 * sizeof(int));

    printf("p = %p
", p);

    for (int i = 0; i < 10; i++)
    {
        p[i] = i * 2;
    }

    for (int i = 0; i < 10; i++)
    {
        printf("p[%d] = %d
", i, p[i]);
    }

    /*
    void* realloc(void* ptr, size_t size);
        ptr 是需要提供给当前函数的通过 malloc calloc realloc 申请的内存空间首地址
        size 是当前重新分配空间对应的空间字节数。

        返回值是新空间首地址。
            1. 地址不变
            2. 地址改变
            【必须使用原本存储空间首地址的指针变量,接受 realloc 返回值】
    */
    p = (int *)realloc(p, 20);

    printf("p = %p
", p);

    for (int i = 0; i < 5; i++)
    {
        printf("p[%d] = %d
", i, p[i]);
    }

    free(p);
    p = NULL;
}
9.3 内存释放函数
  • free 函数声明

上面的内存申请函数在使用后应该通过调用 free 函数来释放内存,以避免内存泄漏。free 函数的声明如下:

void free(void* ptr);

free 函数接受一个参数 ptr,表示要释放的内存指针*

9.4 内存管理案例数组扩容
#include <stdio.h>
#include <stdlib.h>

int *grow1(int arr[], int capacity);

/*
数组扩容,要求用户提供目标 int 类型数组的空间首地址和当前数组容量

@param arr      目标 int 类型数组空间首地址,要求当前数组空间必须是通过malloc, calloc or realloc 申请				的内存空间 
@param capacity 当前数组的容量
@return 扩容之后的 int 类型数组新地址。
*/
int *grow2(int *arr, int capacity);

int main(int argc, char const *argv[])
{
#if 0
    /*
    【静态数组】
        1. 所处区域为内存的【栈区】
        2. 空间不能变,数据不能多,容量就是个常量!
        3. 作用域范围非常的小!!!只能在当前函数中使用。
    */
    int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    // 静态数组无法进行扩容操作,会导致代码出现问题
    int *p = grow1(arr, 10);

    for (int i = 0; i < 20; i++)
    {
        printf("p[%d] = %d
", i, p[i]);
    }
#endif
    int *p = (int *)malloc(10 * sizeof(int));

    for (int i = 0; i < 10; i++)
    {
        p[i] = i * 2;
    }

    p = grow2(p, 10);

    for (int i = 0; i < 10; i++)
    {
        printf("p[%d] = %d
", i, p[i]);
    }
    printf("-----------------------------
");

    for (int i = 10; i < 20; i++)
    {
        p[i] = i * 2;
    }

    for (int i = 0; i < 20; i++)
    {
        printf("p[%d] = %d
", i, p[i]);
    }

    free(p);
    p = NULL;

    return 0;
}

/*
扩容到原本空间的 2 倍
【错误案例】

int *grow1(int arr[], int capacity)
{
    // 1. 根据原数组容量 * 2 创建新的数组
    int new_arr[capacity * 2];

    // 2. 将原数组数据移动到新数组中
    for (int i = 0; i < capacity; i++)
    {
        new_arr[i] = arr[i];
    }

    // 3. 告知函数外部,新数组空间首地址
    return new_arr;
}
*/

int *grow2(int *arr, int capacity)
{
    return (int *)realloc(arr, capacity * 2 * sizeof(int));
}