指针

Tutorial: 教程一 Category: C语言 Published: 2026-04-07 13:58:26 Views: 20 Likes: 0 Comments: 0
9. 指针

指针是 C 语言中一个强大而灵活的特性,它允许程序员直接操作内存地址,从而实现高效的数据处理和复杂的数据结构管理。掌握指针的概念、定义与使用方法,对于深入理解 C 语言及编写高效的 C 程序至关重要。

9.1 指针的概念

指针是一个变量,用于存储另一个变量的内存地址。通过指针,程序可以间接访问和修改存储在特定内存位置的数据。这种间接访问为动态内存管理、数据结构(如链表、树等)的实现提供了基础。

指针的基本概念
  • 内存地址:每个变量在内存中都有一个唯一的地址,可以通过运算符&获取。
  • 指针类型:指针变量必须声明为特定的数据类型,以确保正确地解引用和操作数据。
  • 解引用:通过指针访问其指向的变量的值,使用运算符*
指针的作用
  • 动态内存管理:通过指针动态分配和释放内存。
  • 高效数组和字符串操作:指针可以高效地遍历和操作数组和字符串。
  • 实现复杂数据结构:如链表、树、图等。
  • 函数参数传递:通过指针实现传址调用,提高函数的灵活性和效率。
示例
#include <stdio.h>

int main() {
    int var = 20;    // 声明一个整型变量
    int *ptr;        // 声明一个指向整型的指针变量

    ptr = &var;      // 将变量var的地址赋给指针ptr

    printf("var的值: %d\n", var);          // 输出 var的值
    printf("ptr存储的地址: %p\n", ptr);    // 输出 ptr存储的地址
    printf("*ptr的值: %d\n", *ptr);        // 输出指针ptr指向的值

    return 0;
}

输出(具体地址可能因系统而异):

var的值: 20
ptr存储的地址: 0x7ffee4b2c89c
*ptr的值: 20

详细解释

  • int *ptr; 声明了一个指向整型的指针变量ptr
  • ptr = &var; 将变量var的内存地址赋给指针ptr
  • *ptr 表示指针ptr指向的变量的值,即var的值。
9.2 指针变量的定义与使用

指针变量是存储内存地址的变量。正确地定义和使用指针变量是理解和掌握指针的关键。

指针变量的定义

语法

数据类型 *指针变量名;
  • 数据类型:指针所指向的数据类型,如intfloatchar等。
  • \*符号:表示这是一个指针变量。
  • 指针变量名:指针的名称。

示例

#include <stdio.h>

int main() {
    int a = 10;
    float b = 5.5;
    char c = 'A';

    int *ptrA;      // 指向整型的指针
    float *ptrB;    // 指向浮点型的指针
    char *ptrC;      // 指向字符型的指针

    ptrA = &a;      // ptrA存储变量a的地址
    ptrB = &b;      // ptrB存储变量b的地址
    ptrC = &c;      // ptrC存储变量c的地址

    // 输出指针的值(内存地址)
    printf("ptrA 指向的地址: %p\n", ptrA);
    printf("ptrB 指向的地址: %p\n", ptrB);
    printf("ptrC 指向的地址: %p\n", ptrC);

    // 通过指针访问变量的值
    printf("*ptrA = %d\n", *ptrA);
    printf("*ptrB = %.2f\n", *ptrB);
    printf("*ptrC = %c\n", *ptrC);

    return 0;
}

输出(具体地址可能因系统而异):

ptrA 指向的地址: 0x7ffee4b2c89c
ptrB 指向的地址: 0x7ffee4b2c8a0
ptrC 指向的地址: 0x7ffee4b2c8a1
*ptrA = 10
*ptrB = 5.50
*ptrC = A
指针的解引用

解引用是指通过指针访问其指向的变量的值,使用运算符*

示例

#include <stdio.h>

int main() {
    int num = 25;
    int *ptr = &num; // ptr指向num的地址

    printf("num的值: %d\n", num);       // 输出: 25
    printf("*ptr的值: %d\n", *ptr);     // 输出: 25

    *ptr = 30; // 通过指针修改num的值
    printf("修改后的num的值: %d\n", num); // 输出: 30

    return 0;
}

输出

num的值: 25
*ptr的值: 25
修改后的num的值: 30

详细解释

  • *ptr = 30; 通过指针ptr修改了变量num的值。
  • 解引用操作符*用于访问指针所指向的内存位置。
指针的类型

指针类型决定了指针解引用后访问的数据类型。不同类型的指针占用的内存大小可能不同,但在大多数现代系统中,所有指针类型通常占用相同的内存空间(如 64 位系统中的 8 字节)。

示例

#include <stdio.h>

int main() {
    int *ptrInt;
    double *ptrDouble;
    char *ptrChar;

    printf("int指针的大小: %zu 字节\n", sizeof(ptrInt));       // 输出: 8
    printf("double指针的大小: %zu 字节\n", sizeof(ptrDouble)); // 输出: 8
    printf("char指针的大小: %zu 字节\n", sizeof(ptrChar));     // 输出: 8

    return 0;
}

输出(在 64 位系统中):

int指针的大小: 8 字节
double指针的大小: 8 字节
char指针的大小: 8 字节
指针的运算

指针支持有限的运算操作,包括加减整数和指针的比较。指针间的加减操作涉及到指针类型的大小。

示例

#include <stdio.h>

int main() {
    int arr[5] = {10, 20, 30, 40, 50};
    int *ptr = arr; // 指向数组的第一个元素

    printf("ptr = %p\n", ptr);
    printf("ptr + 1 = %p\n", ptr + 1); // 指向下一个整数
    printf("*ptr = %d\n", *ptr);
    printf("*(ptr + 1) = %d\n", *(ptr + 1));

    return 0;
}

输出(具体地址可能因系统而异):

ptr = 0x7ffee4b2c890
ptr + 1 = 0x7ffee4b2c894
*ptr = 10
*(ptr + 1) = 20

详细解释

  • ptr + 1 指向数组的下一个元素,地址增加了sizeof(int)(通常为 4 字节)。
  • 指针运算考虑了数据类型的大小,确保指向正确的内存位置。
9.3 指针与数组的关系

指针和数组在 C 语言中有着密切的关系,理解它们之间的联系和区别对于高效地操作数据结构至关重要。

数组名与指针

在大多数情况下,数组名代表数组的首地址,可以被视为指向数组第一个元素的指针。

示例

#include <stdio.h>

int main() {
    int arr[3] = {100, 200, 300};
    int *ptr = arr; // 等同于 int *ptr = &arr[0];

    // 使用指针访问数组元素
    printf("arr[0] = %d, *ptr = %d\n", arr[0], *ptr); // 输出: 100, 100
    printf("arr[1] = %d, *(ptr + 1) = %d\n", arr[1], *(ptr + 1)); // 输出: 200, 200
    printf("arr[2] = %d, *(ptr + 2) = %d\n", arr[2], *(ptr + 2)); // 输出: 300, 300

    return 0;
}

输出

arr[0] = 100, *ptr = 100
arr[1] = 200, *(ptr + 1) = 200
arr[2] = 300, *(ptr + 2) = 300

详细解释

  • 数组名arr在表达式中通常被解释为指向数组第一个元素的指针。
  • ptr + 1 指向数组的第二个元素,*(ptr + 1)即为arr[1]
指针与数组的区别

尽管指针和数组在很多情况下表现相似,但它们在内存分配、可变性和运算方式上有显著区别。

特性数组指针
内存分配编译时分配固定大小的内存空间可以指向动态分配或不同的内存区域
大小sizeof运算符返回整个数组的大小sizeof运算符返回指针本身的大小
可变性数组名不可修改,始终指向同一内存位置指针可以修改指向不同的内存位置
运算方式支持部分指针运算,如&arr[0]支持更多指针运算,如算术和逻辑运算

示例比较

#include <stdio.h>

int main() {
    int arr[3] = {1, 2, 3};
    int *ptr = arr;

    printf("sizeof(arr) = %zu 字节\n", sizeof(arr));   // 输出数组的总大小
    printf("sizeof(ptr) = %zu 字节\n", sizeof(ptr));   // 输出指针的大小

    // 尝试修改数组名(错误)
    // arr = ptr; // 错误:数组名是不可修改的左值

    // 修改指针指向
    ptr = &arr[1];
    printf("ptr现在指向的值: %d\n", *ptr); // 输出: 2

    return 0;
}

输出(在 64 位系统中):

sizeof(arr) = 12 字节
sizeof(ptr) = 8 字节
ptr现在指向的值: 2

注意事项

  • 数组名不可赋值:数组名是常量指针,不能通过赋值改变其指向。
  • 指针的灵活性:指针可以指向不同类型的内存区域,如动态分配的内存、其他变量等。
指针作为函数参数传递数组

将数组作为函数参数时,实际上是将数组名(指针)传递给函数,这使得函数能够访问和修改原数组的内容。

示例

#include <stdio.h>

// 函数定义:打印数组元素
void printArray(int *arr, int size) {
    for(int i = 0; i < size; i++) {
        printf("arr[%d] = %d\n", i, arr[i]);
    }
}

int main() {
    int numbers[] = {10, 20, 30, 40, 50};
    int size = sizeof(numbers) / sizeof(numbers[0]);

    // 调用printArray函数
    printArray(numbers, size);

    return 0;
}

输出

arr[0] = 10
arr[1] = 20
arr[2] = 30
arr[3] = 40
arr[4] = 50

详细解释

  • printArray函数接受一个指向整型的指针arr和一个整型参数size
  • main函数中,将数组numbers传递给printArray,实际上是传递了数组的首地址。
指针与数组的高级用法
  • 指针数组:数组的每个元素都是一个指针。
  • 多维数组与指针的关系:多维数组可以通过指针进行访问和操作。

示例:指针数组

#include <stdio.h>

int main() {
    char *fruits[] = {"Apple", "Banana", "Cherry"};
    int size = sizeof(fruits) / sizeof(fruits[0]);

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

    return 0;
}

输出

fruits[0] = Apple
fruits[1] = Banana
fruits[2] = Cherry

详细解释

  • char *fruits[] 声明了一个指针数组,每个元素都是指向字符的指针(即字符串)。
  • 通过指针数组,可以方便地管理多个字符串。
9.4 函数指针与指针函数

指针在 C 语言中不仅可以指向数据,还可以指向函数。理解函数指针和指针函数的区别和用法,是实现回调函数、动态函数调用等高级编程技巧的基础。

函数指针

函数指针是一个指针变量,用于存储函数的地址。通过函数指针,可以间接调用函数,实现更灵活的函数调用机制。

函数指针的定义

语法

返回类型 (*指针变量名)(参数类型1, 参数类型2, ...);

示例

#include <stdio.h>

// 函数定义:求和
int add(int a, int b) {
    return a + b;
}

// 函数定义:求差
int subtract(int a, int b) {
    return a - b;
}

int main() {
    // 定义函数指针,指向返回int,接受两个int参数的函数
    int (*funcPtr)(int, int);

    // 将add函数的地址赋给函数指针
    funcPtr = add;
    printf("add(10, 5) = %d\n", funcPtr(10, 5)); // 输出: 15

    // 将subtract函数的地址赋给函数指针
    funcPtr = subtract;
    printf("subtract(10, 5) = %d\n", funcPtr(10, 5)); // 输出: 5

    return 0;
}

输出

add(10, 5) = 15
subtract(10, 5) = 5

详细解释

  • int (*funcPtr)(int, int); 声明了一个函数指针funcPtr,指向返回int,接受两个int参数的函数。
  • 通过将不同函数的地址赋给funcPtr,可以实现动态函数调用。
函数指针的应用
  1. 回调函数:在某些库函数中,用户可以传递自定义函数作为回调,实现自定义操作。
  2. 动态函数调用:根据程序运行时的条件,动态选择调用不同的函数。
  3. 实现多态:通过函数指针,可以实现类似面向对象编程中的多态性。

示例:回调函数

#include <stdio.h>

// 函数定义:执行操作
void executeOperation(int a, int b, int (*operation)(int, int)) {
    int result = operation(a, b);
    printf("操作结果: %d\n", result);
}

// 函数定义:乘法
int multiply(int a, int b) {
    return a * b;
}

int main() {
    int x = 6, y = 7;

    // 使用函数指针作为回调函数
    executeOperation(x, y, multiply); // 输出: 操作结果: 42
    executeOperation(x, y, add);      // 输出: 操作结果: 13

    return 0;
}

// 之前定义的add函数
int add(int a, int b) {
    return a + b;
}

输出

操作结果: 42
操作结果: 13

详细解释

  • executeOperation函数接受两个整数和一个函数指针作为参数。
  • 通过传递不同的函数(如multiplyadd),可以动态执行不同的操作。
指针函数

指针函数是指返回指针的函数。它们在返回指向变量、数组或动态分配内存的指针时非常有用。

指针函数的定义

语法

返回类型 *函数名(参数列表) {
    // 函数体
}

示例

#include <stdio.h>

// 函数定义:返回指向整型变量的指针
int* getPointer(int *ptr) {
    return ptr;
}

int main() {
    int var = 50;
    int *ptrVar = &var;

    // 调用指针函数
    int *returnedPtr = getPointer(ptrVar);
    printf("var = %d\n", *returnedPtr); // 输出: var = 50

    return 0;
}

输出

var = 50

详细解释

  • int* getPointer(int *ptr) 定义了一个返回指向整型的指针函数。
  • 函数返回传入的指针,使得调用者可以访问和修改原始变量。
指针函数的应用
  1. 访问动态分配的内存:通过指针函数返回动态分配的内存地址,便于管理内存。
  2. 操作复杂数据结构:返回指向结构体或数组的指针,便于访问和修改数据结构中的元素。
  3. 实现链式调用:通过返回指针,可以实现多个函数调用的链式操作。

示例:返回动态分配的数组

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

// 函数定义:动态分配并返回整型数组的指针
int* createArray(int size) {
    int *arr = (int*)malloc(size * sizeof(int));
    if(arr == NULL) {
        printf("内存分配失败\n");
        exit(1);
    }

    // 初始化数组
    for(int i = 0; i < size; i++) {
        arr[i] = i * 10;
    }

    return arr;
}

int main() {
    int size = 5;
    int *myArray = createArray(size);

    // 输出数组元素
    for(int i = 0; i < size; i++) {
        printf("myArray[%d] = %d\n", i, myArray[i]);
    }

    // 释放动态分配的内存
    free(myArray);

    return 0;
}

输出

myArray[0] = 0
myArray[1] = 10
myArray[2] = 20
myArray[3] = 30
myArray[4] = 40

详细解释

  • createArray函数动态分配一个整型数组,并返回其指针。
  • main函数中,调用createArray获取数组指针,并使用该指针访问数组元素。
注意事项
  • 区别指针函数与函数指针:指针函数是返回指针的函数,而函数指针是存储函数地址的指针变量。两者语法不同,易混淆。
  • 避免悬挂指针:确保指针函数返回的指针指向有效的内存,避免返回指向局部变量的指针,因为局部变量在函数返回后会被销毁。
9.5 动态内存分配与指针

动态内存分配允许在程序运行时根据需要分配和释放内存空间。通过指针,程序可以高效地管理动态内存,适应不同的数据需求。C 语言提供了一系列标准库函数用于动态内存管理,主要包括malloccallocreallocfree

9.5.1 malloc

malloc(memory allocation)函数用于在堆内存中分配指定字节数的连续内存空间,返回指向该内存块的指针。分配的内存内容未初始化,可能包含随机值。

原型

void* malloc(size_t size);
  • 参数:要分配的内存字节数。
  • 返回值:指向分配内存的指针,如果分配失败,返回NULL

示例与详细说明

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

int main() {
    int *ptr;
    int n, i;

    printf("请输入要分配的整数个数: ");
    scanf("%d", &n);

    // 使用malloc分配内存
    ptr = (int*)malloc(n * sizeof(int));
    if(ptr == NULL) {
        printf("内存分配失败\n");
        return 1;
    }

    // 初始化数组
    for(i = 0; i < n; i++) {
        ptr[i] = i + 1;
    }

    // 输出数组元素
    printf("分配的数组元素是: ");
    for(i = 0; i < n; i++) {
        printf("%d ", ptr[i]);
    }
    printf("\n");

    // 释放内存
    free(ptr);

    return 0;
}

示例输出

请输入要分配的整数个数: 5
分配的数组元素是: 1 2 3 4 5

详细解释

  • malloc(n * sizeof(int)) 分配了n个整型所需的内存空间。
  • 通过指针ptr访问和初始化分配的内存。
  • 使用free(ptr)释放动态分配的内存,避免内存泄漏。
9.5.2 calloc

calloc(contiguous allocation)函数用于在堆内存中分配指定数量的元素,每个元素具有相同的大小,并将分配的内存初始化为零。

原型

void* calloc(size_t num, size_t size);
  • 参数

    • num:要分配的元素数量。
    • size:每个元素的字节大小。
  • 返回值:指向分配内存的指针,如果分配失败,返回NULL

示例与详细说明

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

int main() {
    int *ptr;
    int n, i;

    printf("请输入要分配的整数个数: ");
    scanf("%d", &n);

    // 使用calloc分配内存,并初始化为0
    ptr = (int*)calloc(n, sizeof(int));
    if(ptr == NULL) {
        printf("内存分配失败\n");
        return 1;
    }

    // 输出数组元素
    printf("分配并初始化的数组元素是: ");
    for(i = 0; i < n; i++) {
        printf("%d ", ptr[i]); // 所有元素初始化为0
    }
    printf("\n");

    // 释放内存
    free(ptr);

    return 0;
}

示例输出

请输入要分配的整数个数: 5
分配并初始化的数组元素是: 0 0 0 0 0

详细解释

  • calloc(n, sizeof(int)) 分配了n个整型元素的内存,并将所有字节初始化为零。
  • 适用于需要初始化为零的内存分配场景。
9.5.3 realloc

realloc(reallocate)函数用于调整之前分配的内存块的大小,可以增加或减少内存的分配量。如果需要扩大内存,realloc可能会在内存中移动块的位置,以适应更大的空间。

原型

void* realloc(void *ptr, size_t new_size);
  • 参数

    • ptr:指向之前分配的内存块的指针(通过malloccallocrealloc获得)。
    • new_size:新的内存块大小,以字节为单位。
  • 返回值:指向重新分配的内存块的指针,如果分配失败,返回NULL,原内存块保持不变。

示例与详细说明

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

int main() {
    int *ptr;
    int n, new_n, i;

    printf("请输入初始要分配的整数个数: ");
    scanf("%d", &n);

    // 使用malloc分配内存
    ptr = (int*)malloc(n * sizeof(int));
    if(ptr == NULL) {
        printf("内存分配失败\n");
        return 1;
    }

    // 初始化数组
    for(i = 0; i < n; i++) {
        ptr[i] = i + 1;
    }

    // 输出初始数组
    printf("初始数组元素: ");
    for(i = 0; i < n; i++) {
        printf("%d ", ptr[i]);
    }
    printf("\n");

    printf("请输入新的要分配的整数个数: ");
    scanf("%d", &new_n);

    // 使用realloc调整内存大小
    ptr = (int*)realloc(ptr, new_n * sizeof(int));
    if(ptr == NULL) {
        printf("内存重新分配失败\n");
        return 1;
    }

    // 初始化新增的元素
    for(i = n; i < new_n; i++) {
        ptr[i] = i + 1;
    }

    // 输出重新分配后的数组
    printf("重新分配后的数组元素: ");
    for(i = 0; i < new_n; i++) {
        printf("%d ", ptr[i]);
    }
    printf("\n");

    // 释放内存
    free(ptr);

    return 0;
}

示例输出

请输入初始要分配的整数个数: 3
初始数组元素: 1 2 3
请输入新的要分配的整数个数: 5
重新分配后的数组元素: 1 2 3 4 5

详细解释

  • realloc(ptr, new_n * sizeof(int)) 调整了内存块的大小,使其能够容纳new_n个整型元素。
  • 如果扩大了内存空间,新的元素需要手动初始化。
  • 使用realloc时,务必检查返回值是否为NULL,以避免内存泄漏。
9.5.4 free

**free**函数用于释放之前通过malloccallocrealloc分配的内存,防止内存泄漏。

原型

void free(void *ptr);
  • 参数:指向要释放的内存块的指针。
  • 返回值:无。

示例与详细说明

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

int main() {
    int *ptr;
    int n, i;

    printf("请输入要分配的整数个数: ");
    scanf("%d", &n);

    // 使用malloc分配内存
    ptr = (int*)malloc(n * sizeof(int));
    if(ptr == NULL) {
        printf("内存分配失败\n");
        return 1;
    }

    // 初始化数组
    for(i = 0; i < n; i++) {
        ptr[i] = i * 2;
    }

    // 输出数组元素
    printf("数组元素: ");
    for(i = 0; i < n; i++) {
        printf("%d ", ptr[i]);
    }
    printf("\n");

    // 释放内存
    free(ptr);
    printf("内存已释放。\n");

    // 访问已释放的内存(未定义行为)
    // printf("访问ptr[0] = %d\n", ptr[0]); // 错误:ptr指向的内存已被释放

    return 0;
}

示例输出

请输入要分配的整数个数: 4
数组元素: 0 2 4 6
内存已释放。

详细解释

  • free(ptr); 释放了之前分配的内存块。
  • 释放后,指针ptr仍然指向原内存地址,但该内存已被系统回收,不能再访问或修改。
动态内存分配的注意事项
  1. 检查分配是否成功:始终检查malloccallocrealloc的返回值,确保内存分配成功。
  2. 避免内存泄漏:每次动态分配的内存都应在不再需要时通过free释放。
  3. 避免重复释放:不要多次释放同一内存块,这会导致未定义行为。
  4. 悬挂指针:释放内存后,应将指针设置为NULL,防止指针成为悬挂指针。

示例

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

int main() {
    int *ptr = (int*)malloc(sizeof(int));
    if(ptr == NULL) {
        printf("内存分配失败\n");
        return 1;
    }

    *ptr = 100;
    printf("ptr的值: %d\n", *ptr); // 输出: 100

    free(ptr);      // 释放内存
    ptr = NULL;     // 将指针设置为NULL

    // 尝试访问ptr(安全)
    if(ptr != NULL) {
        printf("ptr的值: %d\n", *ptr);
    } else {
        printf("ptr 已被释放并设置为 NULL\n"); // 输出此行
    }

    return 0;
}

输出

ptr的值: 100
ptr 已被释放并设置为 NULL

详细解释

  • 释放内存后,将指针ptr设置为NULL,避免指针悬挂。
  • 在访问指针之前,检查指针是否为NULL,确保安全。
9.6 总结

指针是 C 语言中一个强大而灵活的工具,通过理解指针的基本概念、定义与使用方法,可以实现高效的内存管理和复杂的数据结构操作。本节详细介绍了指针的概念、指针变量的定义与使用、指针与数组的关系、函数指针与指针函数,以及动态内存分配与指针相关的操作函数。以下是本节的关键点:

  • 指针的概念:理解指针用于存储变量的内存地址,及其在程序中的重要作用。
  • 指针变量的定义与使用:掌握如何声明指针变量,如何通过指针访问和修改数据。
  • 指针与数组的关系:了解数组名与指针的关系,以及如何通过指针操作数组元素。
  • 函数指针与指针函数:区分函数指针和指针函数,掌握函数指针的应用场景。
  • 动态内存分配与指针:掌握malloccallocreallocfree等动态内存管理函数,确保高效且安全地管理内存。