MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C 语言指针用法详解

2022-04-191.2k 阅读

指针基础概念

在 C 语言中,指针是一个重要且强大的概念。简单来说,指针是一个变量,其值为另一个变量的内存地址。内存地址就像是房子的门牌号,通过它我们可以准确地找到存储数据的地方。

例如,我们定义一个整型变量 num

int num = 10;

这里,num 是一个整型变量,它在内存中占据一定的空间,存放数值 10。如果我们想要获取 num 的内存地址,可以使用取地址运算符 &

int num = 10;
int *ptr; // 定义一个指针变量 ptr
ptr = # // 将 num 的地址赋给 ptr

在上述代码中,int *ptr 定义了一个名为 ptr 的指针变量,它指向 int 类型的数据。注意,* 在这里用于声明 ptr 是一个指针,而不是乘法运算符。ptr = &num 这行代码将 num 的内存地址赋给了 ptr,此时 ptr 就指向了 num

指针的声明与初始化

  1. 声明指针 声明指针的一般形式为:
type *pointer_name;

其中,type 是指针所指向的数据类型,它可以是基本数据类型(如 intcharfloat 等),也可以是自定义的数据类型(如结构体、联合体等)。pointer_name 是指针变量的名称。

例如,声明一个指向字符型数据的指针:

char *charPtr;
  1. 初始化指针 指针在使用前最好进行初始化,否则它可能指向一个不确定的内存位置,这会导致未定义行为。初始化指针就是让它指向一个已分配内存的变量。
int num = 5;
int *ptr = # // 声明并初始化指针

也可以先声明指针,后进行初始化:

int num = 5;
int *ptr;
ptr = #

通过指针访问数据

一旦指针指向了某个变量,我们就可以通过指针来访问该变量的值。这需要使用指针运算符 *,也称为解引用运算符。

int num = 10;
int *ptr = #
printf("通过变量直接访问: %d\n", num);
printf("通过指针访问: %d\n", *ptr);

在上述代码中,*ptr 表示访问 ptr 所指向的内存地址中的值,也就是 num 的值。因此,通过 *ptr 我们可以像直接使用 num 一样获取其值。

我们还可以通过指针来修改所指向变量的值:

int num = 10;
int *ptr = #
*ptr = 20; // 通过指针修改 num 的值
printf("修改后的值: %d\n", num);

这里,*ptr = 20 实际上是将 num 的值修改为 20,因为 ptr 指向 num 的内存地址。

指针与数组

  1. 数组名与指针的关系 在 C 语言中,数组名可以看作是一个指向数组首元素的常量指针。例如:
int arr[5] = {1, 2, 3, 4, 5};
int *ptr = arr; // 数组名 arr 作为指针使用

这里,arr 指向数组 arr 的首元素 arr[0] 的地址,ptr 也指向 arr[0]。因此,通过指针 ptr 可以像使用数组下标一样访问数组元素。

  1. 通过指针访问数组元素
int arr[5] = {1, 2, 3, 4, 5};
int *ptr = arr;
for (int i = 0; i < 5; i++) {
    printf("arr[%d] = %d\n", i, *(ptr + i));
}

在上述代码中,*(ptr + i) 等价于 arr[i]ptr + i 表示从 ptr 所指向的地址开始,偏移 iint 类型的大小(在 32 位系统中,int 通常占 4 个字节),然后通过解引用运算符 * 获取该地址处的值。

  1. 指针运算 指针可以进行一些算术运算,常见的有:
  • 指针与整数的加法:如 ptr + n,表示从 ptr 当前指向的地址向后偏移 n 个所指向数据类型的大小。
  • 指针与整数的减法:如 ptr - n,表示从 ptr 当前指向的地址向前偏移 n 个所指向数据类型的大小。
  • 两个指针相减:两个指针相减的结果是它们之间相隔的元素个数,前提是这两个指针指向同一个数组中的元素。
int arr[5] = {1, 2, 3, 4, 5};
int *ptr1 = &arr[0];
int *ptr2 = &arr[3];
printf("ptr2 - ptr1 = %d\n", ptr2 - ptr1);

在上述代码中,ptr2 - ptr1 的结果为 3,因为 ptr2 指向 arr[3]ptr1 指向 arr[0],它们之间相隔 3 个 int 类型的元素。

多级指针

  1. 二级指针 二级指针是指向指针的指针。例如:
int num = 10;
int *ptr1 = &num;
int **ptr2 = &ptr1;

在上述代码中,ptr1 是一个指向 int 类型变量 num 的指针,而 ptr2 是一个指向 ptr1 的指针,即二级指针。

要通过二级指针访问 num 的值,需要进行两次解引用:

int num = 10;
int *ptr1 = &num;
int **ptr2 = &ptr1;
printf("通过二级指针访问: %d\n", **ptr2);

这里,*ptr2 得到 ptr1,再对 ptr1 进行解引用 *ptr1 就得到 num 的值。

  1. 多级指针的应用场景 多级指针在处理复杂的数据结构,如链表的链表、树结构等时非常有用。例如,在实现一个双向链表的插入操作时,可能会用到二级指针来方便地修改链表节点的指针。

指针与函数

  1. 函数指针 函数指针是指向函数的指针变量。每个函数在内存中都有一个入口地址,函数指针可以存储这个地址。函数指针的声明形式如下:
return_type (*pointer_name)(parameter_list);

其中,return_type 是函数的返回类型,pointer_name 是函数指针变量的名称,parameter_list 是函数的参数列表。

例如,定义一个简单的加法函数和指向它的函数指针:

int add(int a, int b) {
    return a + b;
}
int main() {
    int (*funcPtr)(int, int);
    funcPtr = add;
    int result = funcPtr(3, 5);
    printf("结果: %d\n", result);
    return 0;
}

在上述代码中,int (*funcPtr)(int, int) 声明了一个函数指针 funcPtr,它指向返回值为 int 类型,接受两个 int 类型参数的函数。funcPtr = addfuncPtr 指向 add 函数。然后可以通过 funcPtr 来调用 add 函数。

  1. 指针作为函数参数 将指针作为函数参数可以实现对调用函数中变量的修改,而不仅仅是传递变量的值。例如:
void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}
int main() {
    int num1 = 5, num2 = 10;
    swap(&num1, &num2);
    printf("num1 = %d, num2 = %d\n", num1, num2);
    return 0;
}

在上述代码中,swap 函数接受两个 int 类型的指针参数。通过指针,函数可以直接访问并修改调用函数中的变量 num1num2 的值,从而实现交换操作。

动态内存分配与指针

  1. malloc 函数 malloc 函数用于在堆内存中动态分配指定字节数的内存空间。其原型为:
void *malloc(size_t size);

size 是要分配的字节数,返回值是一个指向分配内存起始地址的指针。如果分配失败,返回 NULL

例如,动态分配一个 int 类型大小的内存空间:

int *ptr = (int *)malloc(sizeof(int));
if (ptr != NULL) {
    *ptr = 10;
    printf("分配的值: %d\n", *ptr);
    free(ptr); // 释放内存
} else {
    printf("内存分配失败\n");
}

在上述代码中,malloc(sizeof(int)) 分配了一个 int 类型大小的内存空间,并返回一个指向该空间的指针。我们将其强制转换为 int * 类型并赋值给 ptr。然后可以通过 ptr 来访问和修改这块内存。最后,使用 free(ptr) 释放这块动态分配的内存,以避免内存泄漏。

  1. calloc 函数 calloc 函数用于在堆内存中分配指定数量的指定大小的内存块,并将它们初始化为 0。其原型为:
void *calloc(size_t num, size_t size);

num 是要分配的内存块数量,size 是每个内存块的大小。返回值与 malloc 类似。

例如,分配一个包含 5 个 int 类型元素的数组:

int *arr = (int *)calloc(5, sizeof(int));
if (arr != NULL) {
    for (int i = 0; i < 5; i++) {
        printf("arr[%d] = %d\n", i, arr[i]);
    }
    free(arr);
} else {
    printf("内存分配失败\n");
}

在上述代码中,calloc(5, sizeof(int)) 分配了 5 个 int 类型大小的内存块,并将它们初始化为 0。

  1. realloc 函数 realloc 函数用于重新分配已经动态分配的内存空间的大小。其原型为:
void *realloc(void *ptr, size_t size);

ptr 是指向已分配内存的指针,size 是新的大小。如果重新分配成功,返回指向新内存块的指针,可能与原来的 ptr 相同,也可能不同;如果失败,返回 NULL,原来的内存块保持不变。

例如,动态扩展一个已分配的数组:

int *arr = (int *)malloc(3 * sizeof(int));
if (arr != NULL) {
    arr[0] = 1;
    arr[1] = 2;
    arr[2] = 3;
    int *newArr = (int *)realloc(arr, 5 * sizeof(int));
    if (newArr != NULL) {
        arr = newArr;
        arr[3] = 4;
        arr[4] = 5;
        for (int i = 0; i < 5; i++) {
            printf("arr[%d] = %d\n", i, arr[i]);
        }
        free(arr);
    } else {
        printf("重新分配内存失败\n");
    }
} else {
    printf("内存分配失败\n");
}

在上述代码中,首先使用 malloc 分配了一个包含 3 个 int 元素的数组,然后使用 realloc 将其扩展为包含 5 个 int 元素的数组。如果 realloc 成功,arr 将指向新的内存块,我们可以继续使用它。

指针与结构体

  1. 结构体指针 可以定义指向结构体的指针。例如:
struct Student {
    char name[20];
    int age;
};
int main() {
    struct Student stu = {"Alice", 20};
    struct Student *stuPtr = &stu;
    printf("姓名: %s, 年龄: %d\n", stuPtr->name, stuPtr->age);
    return 0;
}

在上述代码中,struct Student *stuPtr = &stu 定义了一个指向 stu 结构体变量的指针 stuPtr。通过 stuPtr->namestuPtr->age 可以访问结构体成员,-> 运算符称为结构体指针运算符。

  1. 动态分配结构体内存
struct Point {
    int x;
    int y;
};
int main() {
    struct Point *pointPtr = (struct Point *)malloc(sizeof(struct Point));
    if (pointPtr != NULL) {
        pointPtr->x = 10;
        pointPtr->y = 20;
        printf("点的坐标: (%d, %d)\n", pointPtr->x, pointPtr->y);
        free(pointPtr);
    } else {
        printf("内存分配失败\n");
    }
    return 0;
}

这里,使用 malloc 动态分配了一个 struct Point 结构体大小的内存空间,并通过指针 pointPtr 来访问和修改结构体成员。

指针的常见错误与注意事项

  1. 未初始化指针 使用未初始化的指针会导致未定义行为。例如:
int *ptr;
printf("%d\n", *ptr); // 未初始化指针,错误行为

在使用指针前,一定要确保它指向一个有效的内存地址。

  1. 野指针 野指针是指向一个已释放内存或未分配内存区域的指针。例如:
int *ptr = (int *)malloc(sizeof(int));
*ptr = 10;
free(ptr);
printf("%d\n", *ptr); // ptr 成为野指针,错误行为

在释放内存后,应立即将指针设置为 NULL,以避免成为野指针:

int *ptr = (int *)malloc(sizeof(int));
*ptr = 10;
free(ptr);
ptr = NULL;
  1. 内存泄漏 忘记释放动态分配的内存会导致内存泄漏。例如:
while (1) {
    int *ptr = (int *)malloc(sizeof(int));
    // 未释放内存,每次循环都会导致内存泄漏
}

一定要在不再需要动态分配的内存时,使用 free 函数进行释放。

  1. 指针类型不匹配 在进行指针操作时,要确保指针类型与所指向的数据类型匹配。例如:
int num = 10;
char *ptr = (char *)&num; // 指针类型不匹配,可能导致错误

这种类型不匹配可能会导致数据访问错误。

总之,指针是 C 语言中非常强大但也容易出错的特性。在使用指针时,需要严格遵循规则,仔细处理内存分配和释放,以避免各种潜在的错误。通过深入理解指针的概念和用法,可以编写出高效、灵活的 C 语言程序。在实际编程中,要不断练习和实践,才能熟练掌握指针的使用技巧,充分发挥 C 语言的优势。无论是简单的变量访问,还是复杂的数据结构操作,指针都能为我们提供强大的支持。例如,在操作系统内核开发、嵌入式系统编程等领域,指针的高效使用是必不可少的。希望通过本文的详细介绍,读者能对 C 语言指针的用法有更深入的理解和掌握。同时,要始终牢记指针使用过程中的注意事项,养成良好的编程习惯,减少错误的发生。在进一步学习 C 语言的过程中,还会遇到更多与指针相关的高级应用,如指针数组、数组指针等,这些都是深入掌握 C 语言编程的重要内容。只有不断探索和实践,才能在 C 语言编程领域取得更好的成绩。