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

如何通过指针在C语言中操作变量

2023-08-275.9k 阅读

指针基础概念

在C语言中,指针是一个非常重要的概念。指针变量用于存储内存地址,通过这个地址,我们可以间接访问和操作存储在该地址的数据。简单来说,指针就像是一把钥匙,它指向内存中的某个位置,让我们能够对该位置的数据进行操作。

指针变量的声明

指针变量在使用前需要先声明,其声明语法为:类型 *指针变量名;。这里的“类型”指的是指针所指向的数据类型,它必须与实际存储在该地址的数据类型相匹配。例如:

int *ptr;
float *fptr;
char *cptr;

在上述代码中,ptr 是一个指向 int 类型数据的指针,fptr 是指向 float 类型数据的指针,cptr 是指向 char 类型数据的指针。需要注意的是,这里只是声明了指针变量,它们还没有指向任何有效的内存地址。

取地址运算符(&)

要让指针指向一个变量,我们需要使用取地址运算符 &。这个运算符用于获取变量的内存地址。例如:

#include <stdio.h>

int main() {
    int num = 10;
    int *ptr;

    ptr = &num;

    printf("The address of num is: %p\n", (void*)ptr);
    printf("The value of num is: %d\n", num);
    printf("The value of num accessed through pointer is: %d\n", *ptr);

    return 0;
}

在上述代码中,ptr = &num; 语句将 ptr 指向了变量 num 的地址。然后我们使用 printf 函数打印出 num 的地址、num 的值以及通过指针 ptr 访问到的 num 的值。%p 格式说明符用于打印地址,为了避免编译器警告,我们将 ptr 强制转换为 (void*) 类型。

解引用运算符(*)

解引用运算符 * 用于访问指针所指向的内存地址中的数据。在上面的代码中,*ptr 就表示访问 ptr 所指向的内存地址中的数据,也就是 num 的值。当 * 出现在指针变量声明中时,它表示这是一个指针变量;而当它出现在其他地方时,就是解引用运算符。

通过指针操作基本数据类型变量

操作整型变量

#include <stdio.h>

void increment(int *num) {
    (*num)++;
}

int main() {
    int num = 5;
    printf("Before increment: num = %d\n", num);

    increment(&num);
    printf("After increment: num = %d\n", num);

    return 0;
}

在上述代码中,我们定义了一个函数 increment,它接受一个指向 int 类型的指针。在函数内部,通过解引用指针并对其值进行加一操作,从而改变了主函数中 num 的值。这里需要注意的是,在 (*num)++; 语句中,括号是必要的,因为 ++ 运算符的优先级高于 * 运算符,如果不使用括号,就会变成对指针 num 本身进行加一操作,而不是对其指向的值进行加一操作。

操作浮点型变量

#include <stdio.h>

void multiply(float *num, float factor) {
    *num = *num * factor;
}

int main() {
    float num = 2.5;
    float factor = 3.0;
    printf("Before multiplication: num = %f\n", num);

    multiply(&num, factor);
    printf("After multiplication: num = %f\n", num);

    return 0;
}

此代码中,multiply 函数接受一个指向 float 类型的指针和一个 float 类型的因子。通过解引用指针,对指向的浮点数进行乘法运算,从而改变了主函数中 num 的值。

操作字符型变量

#include <stdio.h>

void convertToUpperCase(char *ch) {
    if (*ch >= 'a' && *ch <= 'z') {
        *ch = *ch - 32;
    }
}

int main() {
    char ch = 'a';
    printf("Before conversion: ch = %c\n", ch);

    convertToUpperCase(&ch);
    printf("After conversion: ch = %c\n", ch);

    return 0;
}

这里的 convertToUpperCase 函数接受一个指向 char 类型的指针。通过判断指针指向的字符是否为小写字母,如果是,则将其转换为大写字母。同样是通过解引用指针来操作字符变量。

通过指针操作数组

数组与指针的关系

在C语言中,数组名本身就是一个指针常量,它指向数组的第一个元素。例如,对于数组 int arr[5];arr 就相当于 &arr[0],是一个指向 int 类型的指针,指向数组的起始地址。我们可以通过指针来访问数组元素。

#include <stdio.h>

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

    for (int i = 0; i < 5; i++) {
        printf("arr[%d] = %d, *(ptr + %d) = %d\n", i, arr[i], i, *(ptr + i));
    }

    return 0;
}

在上述代码中,我们将指针 ptr 指向数组 arr 的起始地址。通过 *(ptr + i) 这种方式,我们可以像使用数组下标一样访问数组元素。实际上,arr[i] 在编译器内部也是被转换为 *(arr + i) 来处理的。

用指针遍历数组

#include <stdio.h>

void printArray(int *arr, int size) {
    for (int i = 0; i < size; i++) {
        printf("%d ", *(arr + i));
    }
    printf("\n");
}

int main() {
    int arr[5] = {10, 20, 30, 40, 50};
    printArray(arr, 5);

    return 0;
}

这里定义了一个函数 printArray,它接受一个指向 int 类型的指针和数组的大小。通过指针遍历数组并打印出每个元素的值。这种方式与使用数组下标遍历数组本质上是相同的,但在一些情况下,使用指针可能会更高效,尤其是在对数组进行复杂操作时。

用指针修改数组元素

#include <stdio.h>

void doubleArray(int *arr, int size) {
    for (int i = 0; i < size; i++) {
        *(arr + i) = *(arr + i) * 2;
    }
}

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    printf("Original array: ");
    for (int i = 0; i < 5; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");

    doubleArray(arr, 5);
    printf("Doubled array: ");
    for (int i = 0; i < 5; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");

    return 0;
}

doubleArray 函数中,通过指针遍历数组并将每个元素的值翻倍。这展示了如何通过指针来修改数组中的元素,再次体现了指针在操作数组时的灵活性和强大功能。

通过指针操作结构体变量

结构体指针的声明与初始化

结构体是一种用户自定义的数据类型,它可以将不同类型的数据组合在一起。当我们需要通过指针来操作结构体变量时,首先要声明结构体指针并进行初始化。

#include <stdio.h>
#include <string.h>

struct Student {
    char name[50];
    int age;
    float marks;
};

int main() {
    struct Student s1 = {"John", 20, 85.5};
    struct Student *ptr = &s1;

    printf("Name: %s\n", ptr->name);
    printf("Age: %d\n", ptr->age);
    printf("Marks: %f\n", ptr->marks);

    return 0;
}

在上述代码中,我们定义了一个 Student 结构体,然后声明了一个结构体变量 s1 并进行了初始化。接着,我们声明了一个结构体指针 ptr 并将其指向 s1。在访问结构体成员时,我们使用 -> 运算符,它是“结构体指针 -> 成员名”的形式,用于通过结构体指针访问结构体成员。

通过结构体指针修改结构体成员

#include <stdio.h>
#include <string.h>

struct Employee {
    char name[50];
    int salary;
};

void raiseSalary(struct Employee *emp, int amount) {
    emp->salary = emp->salary + amount;
}

int main() {
    struct Employee e1 = {"Alice", 5000};
    printf("Before raise: Name = %s, Salary = %d\n", e1.name, e1.salary);

    raiseSalary(&e1, 1000);
    printf("After raise: Name = %s, Salary = %d\n", e1.name, e1.salary);

    return 0;
}

这里定义了一个 Employee 结构体和一个函数 raiseSalary,该函数接受一个结构体指针和加薪的金额。在函数内部,通过结构体指针修改了结构体的 salary 成员,从而实现了对结构体成员的修改操作。

结构体数组与指针

#include <stdio.h>
#include <string.h>

struct Book {
    char title[100];
    char author[50];
    int year;
};

void printBooks(struct Book *books, int numBooks) {
    for (int i = 0; i < numBooks; i++) {
        printf("Title: %s\n", (books + i)->title);
        printf("Author: %s\n", (books + i)->author);
        printf("Year: %d\n\n", (books + i)->year);
    }
}

int main() {
    struct Book library[3] = {
        {"C Programming Language", "Brian W. Kernighan", 1988},
        {"The Mythical Man - Month", "Frederick P. Brooks Jr.", 1975},
        {"Clean Code", "Robert C. Martin", 2008}
    };

    printBooks(library, 3);

    return 0;
}

在此代码中,我们定义了一个 Book 结构体数组 library,并将其传递给 printBooks 函数。printBooks 函数使用结构体指针遍历结构体数组,并打印出每本书的信息。通过这种方式,我们可以高效地处理结构体数组,体现了指针在操作结构体数组时的便利性。

指针与函数

函数指针的声明与使用

函数指针是指向函数的指针变量。在C语言中,函数名本身就是一个指针,它指向函数的入口地址。我们可以声明一个函数指针,并将其指向某个函数,然后通过该指针来调用函数。

#include <stdio.h>

int add(int a, int b) {
    return a + b;
}

int subtract(int a, int b) {
    return a - b;
}

int main() {
    int (*funcPtr)(int, int);

    funcPtr = add;
    printf("The result of addition: %d\n", funcPtr(3, 5));

    funcPtr = subtract;
    printf("The result of subtraction: %d\n", funcPtr(8, 3));

    return 0;
}

在上述代码中,int (*funcPtr)(int, int); 声明了一个函数指针 funcPtr,它可以指向返回值为 int 类型且接受两个 int 类型参数的函数。然后我们将 funcPtr 分别指向 add 函数和 subtract 函数,并通过 funcPtr 来调用这两个函数。

函数指针作为参数传递

函数指针可以作为参数传递给其他函数,这在实现回调函数时非常有用。

#include <stdio.h>

int add(int a, int b) {
    return a + b;
}

int subtract(int a, int b) {
    return a - b;
}

void operate(int a, int b, int (*func)(int, int)) {
    printf("The result is: %d\n", func(a, b));
}

int main() {
    int num1 = 10, num2 = 5;

    operate(num1, num2, add);
    operate(num1, num2, subtract);

    return 0;
}

这里定义了一个 operate 函数,它接受两个整数和一个函数指针作为参数。在 operate 函数内部,通过传递进来的函数指针调用相应的函数,实现了不同的操作。这种方式使得代码更加灵活,我们可以根据需要传递不同的函数指针,从而执行不同的操作。

返回函数指针的函数

一个函数也可以返回一个函数指针。

#include <stdio.h>

int add(int a, int b) {
    return a + b;
}

int subtract(int a, int b) {
    return a - b;
}

int (*selectOperation(char op))(int, int) {
    if (op == '+') {
        return add;
    } else if (op == '-') {
        return subtract;
    }
    return NULL;
}

int main() {
    char op = '+';
    int (*funcPtr)(int, int) = selectOperation(op);

    if (funcPtr) {
        printf("The result is: %d\n", funcPtr(5, 3));
    } else {
        printf("Invalid operation.\n");
    }

    return 0;
}

在上述代码中,selectOperation 函数根据传入的操作符字符返回相应的函数指针。如果操作符无效,则返回 NULL。在主函数中,我们根据返回的函数指针调用相应的函数进行计算。

指针的进阶操作

多级指针

多级指针是指针的指针,也就是一个指针变量存储的是另一个指针变量的地址。例如,二级指针的声明为 类型 **指针变量名;

#include <stdio.h>

int main() {
    int num = 10;
    int *ptr1 = &num;
    int **ptr2 = &ptr1;

    printf("The value of num is: %d\n", num);
    printf("The value of num accessed through ptr1 is: %d\n", *ptr1);
    printf("The value of num accessed through ptr2 is: %d\n", **ptr2);

    return 0;
}

在上述代码中,ptr1 是一个指向 int 类型变量 num 的指针,而 ptr2 是一个指向 ptr1 的指针。通过 **ptr2 我们可以间接访问到 num 的值。多级指针在一些复杂的数据结构,如链表的链表等实现中非常有用。

动态内存分配与指针

C语言提供了 malloccallocreallocfree 等函数来进行动态内存分配和释放。这些函数返回的是 void* 类型的指针,通常需要进行类型转换。

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

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

    arr = (int*)malloc(size * sizeof(int));
    if (arr == NULL) {
        printf("Memory allocation failed.\n");
        return 1;
    }

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

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

    free(arr);

    return 0;
}

在上述代码中,我们使用 malloc 函数分配了一块能存储 sizeint 类型数据的内存空间,并将返回的 void* 指针转换为 int* 类型指针赋给 arr。然后我们对分配的内存进行初始化和使用,最后使用 free 函数释放内存。如果 malloc 分配内存失败,它会返回 NULL,我们需要进行相应的错误处理。

calloc 函数与 malloc 类似,但它会将分配的内存初始化为0。realloc 函数用于重新分配已分配内存的大小。例如:

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

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

    arr = (int*)calloc(size, sizeof(int));
    if (arr == NULL) {
        printf("Memory allocation failed.\n");
        return 1;
    }

    for (int i = 0; i < size; i++) {
        arr[i] = i * 3;
    }

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

    int newSize = 10;
    arr = (int*)realloc(arr, newSize * sizeof(int));
    if (arr == NULL) {
        printf("Memory re - allocation failed.\n");
        return 1;
    }

    for (int i = size; i < newSize; i++) {
        arr[i] = i * 4;
    }

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

    free(arr);

    return 0;
}

在这段代码中,我们首先使用 calloc 分配内存并初始化,然后使用 realloc 增加内存大小,并对新增加的内存进行初始化,最后释放内存。

指针与字符串

在C语言中,字符串通常以字符数组的形式存储,并且以 '\0' 作为结束标志。我们可以使用指针来操作字符串。

#include <stdio.h>
#include <string.h>

int main() {
    char str[] = "Hello, World!";
    char *ptr = str;

    printf("The string is: %s\n", ptr);
    printf("The length of the string is: %zu\n", strlen(ptr));

    while (*ptr != '\0') {
        printf("%c ", *ptr);
        ptr++;
    }
    printf("\n");

    return 0;
}

在上述代码中,ptr 指向字符串 str 的起始地址。我们可以使用 printf 函数直接打印指针 ptr 指向的字符串,也可以使用 strlen 函数通过指针计算字符串的长度。通过解引用指针并移动指针,我们可以逐个字符地遍历字符串。

指针操作的注意事项

野指针

野指针是指未初始化或指向已释放内存的指针。使用野指针会导致程序崩溃或产生未定义行为。例如:

#include <stdio.h>

int main() {
    int *ptr;
    // 未初始化的指针,此时ptr是野指针
    printf("%d\n", *ptr);

    return 0;
}

在上述代码中,ptr 未初始化就被解引用,这是非常危险的。为了避免野指针,在声明指针变量时,最好将其初始化为 NULL,在释放内存后,也将指针赋值为 NULL。例如:

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

int main() {
    int *ptr = NULL;
    ptr = (int*)malloc(sizeof(int));
    if (ptr != NULL) {
        *ptr = 10;
        printf("%d\n", *ptr);
        free(ptr);
        ptr = NULL;
    }

    return 0;
}

指针越界

指针越界是指指针访问了超出其合法范围的内存。在操作数组时,如果使用指针访问数组越界,同样会导致未定义行为。例如:

#include <stdio.h>

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

    // 访问越界的内存
    for (int i = 0; i < 10; i++) {
        printf("%d ", *(ptr + i));
    }

    return 0;
}

在上述代码中,我们试图访问 arr 数组范围外的内存,这是不允许的。在使用指针操作数组或其他有边界的内存区域时,一定要确保指针没有越界。

内存泄漏

内存泄漏是指程序分配了内存,但在不再使用时没有释放。这会导致内存不断被占用,最终可能导致系统资源耗尽。例如:

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

int main() {
    int *ptr;
    for (int i = 0; i < 10; i++) {
        ptr = (int*)malloc(sizeof(int));
        // 没有释放ptr指向的内存
    }

    return 0;
}

在上述代码中,每次循环都分配了内存,但没有释放,这就造成了内存泄漏。为了避免内存泄漏,在使用 malloccallocrealloc 等分配内存的函数后,一定要记得使用 free 函数释放内存。

通过深入理解和掌握指针在C语言中的各种操作,我们能够编写出更高效、灵活且强大的程序。无论是操作基本数据类型、数组、结构体,还是在函数中使用指针,以及处理动态内存分配等,指针都发挥着至关重要的作用。同时,我们也要注意指针操作中的常见问题,如野指针、指针越界和内存泄漏等,以确保程序的稳定性和正确性。