Bootstrap

[C编程入门] 第四章:C语言的指针和内存管理

在这里插入图片描述

💖💖⚡️⚡️专栏:C编程入门-轻松入门/系统总结⚡️⚡️💖💖
[C编程入门] 专为C语言初学者设计,提供轻松易懂的入门教程与系统的知识总结。本专栏将带你从零开始,循序渐进地掌握C语言的核心概念与编程技巧。无论你是完全没有编程背景的学生,还是希望系统学习C语言的自学者,这里都有丰富的示例和详尽的解释,帮助你打下坚实的编程基础,轻松迈入程序设计的世界。通过本专栏的学习,你将能够自信地运用C语言解决问题,并为进一步的技术提升铺平道路。

4.1 指针基础

指针是一种特殊的变量,它存储的是另一个变量的地址。

4.1.1 定义和使用指针

指针是一种变量,它的值是另一个变量的内存地址。通过指针,我们可以间接访问和修改变量的值。

// 定义一个指向整数的指针
int *p;

// 分配内存
int *q = (int *)malloc(sizeof(int));

// 初始化指针
*p = 42;
*q = 50;
  • 语法

    type *pointerName;
    
  • 示例

    int *p;
    int *q = (int *)malloc(sizeof(int));
    
  • 定义指针

    • 指针变量的类型决定了它能指向哪种类型的变量。
    • 指针名称前面的星号 * 表示这是一个指针。
    • 指针可以指向不同类型的数据,如整数、字符、结构体等。
    • 在定义指针时,星号 * 必须放在类型说明符的前面。
    • 指针本身是一个变量,需要初始化或分配内存。
    • 指针可以指向全局变量、局部变量、静态变量等不同类型的变量。
    • 指针也可以指向动态分配的内存,这将在内存管理部分详细介绍。
    • 指针可以指向不同类型的变量,但类型需要兼容,例如,一个指向整数的指针不能直接指向一个字符型变量。
  • 分配内存

    • 使用malloc函数分配内存给指针指向的变量。
    • malloc函数需要指定所需内存的大小,通常是通过sizeof操作符来确定。
    • 分配的内存需要显式地使用free函数释放,以避免内存泄漏。
    • malloc函数返回一个指向分配的内存块的指针,类型需要强制转换为所需的类型。
    • 如果malloc分配内存失败,它将返回NULL,此时程序应检查返回值并采取适当的措施。
    • 动态分配的内存通常用于未知大小的数据结构,如动态数组、链表等。
    • 动态分配的内存未经初始化,因此需要手动初始化。
    • 动态分配内存时,需要确保分配的内存足够大,以避免后续使用过程中出现溢出或未定义行为。
  • 初始化指针

    • 指针可以指向一个已存在的变量,或者指向动态分配的内存。
    • 指针也可以被初始化为空指针NULL,表示它不指向任何地址。
    • 对于指向已存在变量的指针,可以通过取址运算符&来获取变量的地址。
    • 对于指向动态分配内存的指针,可以直接使用malloc函数返回的指针。
    • 指针初始化时,如果指向的是已存在的变量,需要确保该变量已经被正确声明。
    • 指针初始化为空指针NULL是一种良好的编程习惯,可以避免野指针问题。
    • 初始化指针时,如果指向的是动态分配的内存,需要确保内存已经正确分配。

4.1.2 指针运算

指针支持一些特殊的运算,如加减运算、比较等。

// 指针算术
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;

// 移动指针
p++;

// 指针减法
int diff = p - arr;

// 指针比较
if (p == arr + 1) {
    printf("p points to the second element.\n");
}
  • 语法

    pointerName++; // 指针加1
    pointerName--; // 指针减1
    pointerName + n; // 指针加n
    pointerName - n; // 指针减n
    pointer1 - pointer2; // 指针差
    pointer1 == pointer2; // 指针比较
    
  • 示例

    int *p = arr;
    p++;
    int diff = p - arr;
    if (p == arr + 1) {
        printf("p points to the second element.\n");
    }
    
  • 指针算术

    • 指针可以进行加减运算,以移动指针指向的位置。
    • 指针加减整数时,指针会按照所指类型的大小移动相应的字节数。
    • 可以通过指针差来计算两个指针之间的元素数目。
    • 指针可以进行比较,以判断它们是否指向相同的地址。
    • 指针算术操作对于数组非常有用,可以用来遍历数组中的元素。
    • 指针算术需要确保不会越界,即指针不能超出数组的边界。
    • 指针算术还可以用于处理复杂的数据结构,如链表、树等。
    • 指针算术可以用来实现数组的高效访问,如快速移动指针到特定元素。

4.1.3 指针和数组

指针和数组有着密切的关系,指针可以指向数组的元素。

// 指针和数组
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;

// 访问数组元素
printf("%d\n", *p); // 访问第一个元素
  • 语法

    type arrayName[arraySize];
    type *pointerName = arrayName;
    
  • 示例

    int arr[5] = {1, 2, 3, 4, 5};
    int *p = arr;
    printf("%d\n", *p); // 访问第一个元素
    
  • 指针和数组

    • 指针可以指向数组的第一个元素。
    • 使用指针可以访问数组中的元素,通过解引用操作*来获取元素值。
    • 指针和数组名都可以用作循环中的索引。
    • 指针可以遍历整个数组,类似于数组索引的方式。
    • 数组名实际上是一个指向数组第一个元素的常量指针。
    • 通过指针可以修改数组中的元素。
    • 指针可以指向数组中的任何元素,不仅仅是第一个元素。
    • 指针可以指向数组的一部分,形成子数组的概念。
    • 指针和数组的结合使用可以简化数组的处理,尤其是在处理大型数组或复杂数据结构时。
    • 指针可以用来实现数组的高效遍历,如使用指针来遍历数组,可以避免重复计算数组元素的地址。

4.1.4 函数参数传递

指针可以用作函数的参数,以实现数据的传递。

// 交换两个整数
void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

int main() {
    int x = 10, y = 20;
    printf("Before swap: x = %d, y = %d\n", x, y);
    swap(&x, &y);
    printf("After swap: x = %d, y = %d\n", x, y);

    return 0;
}
  • 语法

    void functionName(type *pointerName) {
        // 使用指针
    }
    
  • 示例

    void swap(int *a, int *b) {
        int temp = *a;
        *a = *b;
        *b = temp;
    }
    
  • 函数参数传递

    • 使用指针作为函数参数可以修改原始变量的值。
    • 要传递变量的地址,需要使用取址运算符&
    • 通过解引用操作*来访问和修改指针指向的变量。
    • 指针作为函数参数可以用于实现数据的传递和修改。
    • 传递指针可以避免复制大量数据,提高效率。
    • 指针作为函数参数还可以用于返回多个值,而不必使用全局变量或结构体。
    • 使用指针作为函数参数需要小心,因为它们可以修改原始数据,可能导致意外的结果。
    • 传递指针时,需要确保指针指向的内存是有效的,不会导致未定义行为。
    • 使用指针作为函数参数时,还需要注意指针的安全性,确保不会访问非法内存。

在这里插入图片描述

4.2 内存管理

C语言提供了动态内存管理的功能,允许程序在运行时分配和释放内存。

4.2.1 分配内存

动态分配内存使用malloc函数。

// 分配内存
int *p = (int *)malloc(sizeof(int));
  • 语法

    type *pointerName = (type *)malloc(size);
    
  • 示例

    int *p = (int *)malloc(sizeof(int));
    
  • 分配内存

    • 使用malloc函数分配内存。
    • malloc函数返回一个指向分配的内存块的指针,类型需要强制转换为所需的类型。
    • 分配的内存大小需要通过sizeof操作符确定。
    • 分配的内存需要显式地使用free函数释放,以避免内存泄漏。
    • 如果malloc分配内存失败,它将返回NULL,此时程序应检查返回值并采取适当的措施。
    • 分配的内存未经初始化,因此需要手动初始化。
    • 动态分配的内存通常用于处理大小未知的数据结构,如动态数组、链表等。
    • 使用malloc分配的内存大小应该是足够的,以避免频繁的内存分配和释放操作。
    • 在动态分配内存时,还需要考虑内存碎片问题,特别是在频繁分配和释放内存的情况下。

4.2.2 释放内存

释放内存使用free函数。

// 释放内存
free(p);
  • 语法

    free(pointerName);
    
  • 示例

    free(p);
    
  • 释放内存

    • 使用free函数释放之前分配的内存。
    • 释放内存后,不应再通过该指针访问内存,否则会导致未定义行为。
    • 避免释放同一块内存多次,这会导致程序崩溃。
    • 在程序结束前,应当释放所有分配的内存,以避免内存泄漏。
    • 释放内存时,需要确保指针不是NULL,以避免尝试释放NULL指针导致的错误。
    • 释放内存后,最好将指针设置为NULL,以避免悬空指针的问题。
    • 释放内存时,还需要确保指针指向的内存确实是之前分配的内存,避免释放非法内存。
    • 释放内存后,如果后续还需要使用这块内存,需要重新分配。

4.2.3 重新分配内存

使用realloc函数可以改变已分配内存的大小。

// 重新分配内存
p = (int *)realloc(p, 2 * sizeof(int));
  • 语法

    pointerName = (type *)realloc(pointerName, newSize);
    
  • 示例

    p = (int *)realloc(p, 2 * sizeof(int));
    
  • 重新分配内存

    • 使用realloc函数可以改变已分配内存的大小。
    • realloc函数尝试保留原有内存中的数据。
    • 如果realloc失败,它可能会返回一个新的指针,原来的指针仍然有效。
    • 在使用realloc之后,需要检查返回的指针是否为NULL
    • 当内存大小减少时,可能会丢失数据。
    • 当内存大小增加时,新分配的内存未初始化。
    • realloc函数可以用于动态调整数据结构的大小,如动态数组。
    • 在使用realloc时,如果新大小为0,则内存将被释放,原来的指针将变为无效。
    • 如果新的内存大小大于旧的内存大小,realloc会尽可能地保留原有数据,但如果无法保留全部数据,则可能会丢失部分数据。
    • 使用realloc时,还需要考虑内存碎片问题,特别是在频繁调用realloc的情况下。

在这里插入图片描述

4.3 实践练习

4.3.1 练习编写指针程序

编写一个程序,使用指针遍历一个整数数组,并找出最大值。

#include <stdio.h>

int findMax(int *arr, int size) {
    int max = arr[0];
    for (int i = 1; i < size; i++) {
        if (arr[i] > max) {
            max = arr[i];
        }
    }
    return max;
}

int main() {
    int arr[5] = {10, 20, 30, 40, 50};
    int max = findMax(arr, 5);
    printf("The maximum value is: %d\n", max);
    return 0;
}
  • 查找数组中的最大值
    • 使用指针可以遍历数组并找到最大值。
    • 函数findMax接受一个整数数组的指针和数组的大小作为参数。
    • 函数内部使用一个循环遍历数组,通过比较来更新最大值。
    • 最终返回数组中的最大值。
    • 在编写此类函数时,需要确保指针指向的是有效的数组。
    • 使用指针可以提高函数的通用性,使其适用于不同大小的数组。
    • 函数还可以通过指针参数返回其他统计信息,如最小值、平均值等。
    • 在遍历数组时,还需要确保指针不会越界,即确保指针指向的地址是有效的。

4.3.2 练习编写内存管理程序

编写一个程序,动态分配一个整数数组,并使用指针访问和修改数组元素。

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

int main() {
    int *p;
    int size = 5;

    // 分配内存
    p = (int *)malloc(size * sizeof(int));

    // 初始化数组
    for (int i = 0; i < size; i++) {
        *(p + i) = i + 1;
    }

    // 打印数组
    for (int i = 0; i < size; i++) {
        printf("%d ", *(p + i));
    }
    printf("\n");

    // 修改数组
    *(p + 2) = 100;

    // 打印修改后的数组
    for (int i = 0; i < size; i++) {
        printf("%d ", *(p + i));
    }
    printf("\n");

    // 释放内存
    free(p);

    return 0;
}
  • 动态分配数组
    • 使用malloc函数动态分配一个整数数组。
    • 分配的内存需要显式地使用free函数释放,以避免内存泄漏。
    • 分配内存后,可以使用指针来初始化数组元素。
    • 使用指针遍历数组可以访问和修改数组元素。
    • 释放内存时,需要确保不再使用该指针指向的内存。
    • 在动态分配数组时,需要确保分配的内存足够大,以容纳所有数据。
    • 使用动态分配的数组可以处理大小未知的情况,如读取文件中的数据。
    • 在动态分配内存时,还需要考虑内存碎片问题,特别是在频繁分配和释放内存的情况下。
    • 在使用动态分配的数组时,还需要确保指针不会越界,即确保指针指向的地址是有效的。
4.4 小结

本章介绍了C语言中的指针和内存管理。通过这些知识,你可以更灵活地控制程序中的数据存储和访问方式。接下来,你可以继续深入到更复杂的主题,如函数、结构体、文件操作等。

;