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

C语言指针与变量值类型的关联

2022-03-124.7k 阅读

C语言指针基础概念

在C语言中,指针是一种特殊的变量类型,它存储的是内存地址。简单来说,变量在内存中占据一定的存储空间,而指针就像是一个指向这个存储空间的箭头,它的值就是该变量的内存地址。

声明指针变量的一般形式为:类型 *指针变量名。例如:

int *ptr;

这里声明了一个名为ptr的指针变量,它指向int类型的数据。注意,*在这里用于声明一个指针,而不是乘法运算符。

初始化指针变量时,通常会让它指向一个已存在的变量。例如:

int num = 10;
int *ptr = #

这里,&是取地址运算符,它获取变量num的内存地址,并将其赋值给指针ptr

指针与基本数据类型的关联

  1. 与整数类型
    • 整数类型在C语言中有charshortintlong等。以int为例,当我们声明一个int类型的指针时,它指向的是存储int类型数据的内存地址。
    • 示例代码如下:
#include <stdio.h>

int main() {
    int num = 5;
    int *ptr = &num;
    printf("变量num的值: %d\n", num);
    printf("指针ptr指向的地址: %p\n", (void *)ptr);
    printf("通过指针ptr访问的值: %d\n", *ptr);
    return 0;
}

在这段代码中,首先定义了一个int类型变量num并赋值为5。然后定义了一个int类型指针ptr并让它指向numprintf函数中,%p用于打印指针所指向的内存地址,*ptr用于通过指针访问所指向变量的值。 2. 与字符类型

  • char类型指针用于指向字符数据。例如:
#include <stdio.h>

int main() {
    char ch = 'A';
    char *charPtr = &ch;
    printf("字符变量ch的值: %c\n", ch);
    printf("字符指针charPtr指向的地址: %p\n", (void *)charPtr);
    printf("通过字符指针charPtr访问的值: %c\n", *charPtr);
    return 0;
}

这里定义了一个char类型变量ch并赋值为'A',接着定义了一个char类型指针charPtr指向ch。同样可以通过指针访问字符变量的值。

  1. 与浮点类型
    • 浮点类型包括floatdouble。以float为例:
#include <stdio.h>

int main() {
    float fNum = 3.14f;
    float *floatPtr = &fNum;
    printf("浮点变量fNum的值: %f\n", fNum);
    printf("浮点指针floatPtr指向的地址: %p\n", (void *)floatPtr);
    printf("通过浮点指针floatPtr访问的值: %f\n", *floatPtr);
    return 0;
}

此代码定义了一个float类型变量fNum并赋值为3.14,定义float类型指针floatPtr指向fNum,通过指针可以访问该浮点变量的值。

指针与数组的关联

  1. 一维数组与指针
    • 在C语言中,数组名在很多情况下可以被看作是一个指向数组首元素的指针常量。例如:
#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\n", i, arr[i]);
        printf("通过指针访问 ptr[%d]: %d\n", i, ptr[i]);
    }
    return 0;
}

这里定义了一个int类型的一维数组arr,然后定义了一个int类型指针ptr并让它指向arr的首地址。在循环中,可以看到通过数组名arr和指针ptr都能访问数组元素,ptr[i]等价于*(ptr + i),而arr[i]实际上也等价于*(arr + i),因为数组名arr在这种情况下就相当于一个指针常量指向数组首元素。 2. 二维数组与指针

  • 二维数组可以看作是数组的数组。例如int arr[3][4],可以把它看作是由3个包含4个int类型元素的一维数组组成。
  • 二维数组名同样可以被看作是一个指针,它指向第一个一维数组的首地址。其类型是指向一维数组的指针。例如:
#include <stdio.h>

int main() {
    int arr[3][4] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };
    int (*ptr)[4] = arr;
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 4; j++) {
            printf("通过二维数组名访问 arr[%d][%d]: %d\n", i, j, arr[i][j]);
            printf("通过指针访问 ptr[%d][%d]: %d\n", i, j, *(*(ptr + i)+j));
        }
    }
    return 0;
}

这里定义了一个二维数组arr,然后定义了一个指针ptrptr是一个指向包含4个int类型元素的一维数组的指针。*(*(ptr + i)+j)用于通过指针访问二维数组的元素,ptr + i指向第i个一维数组,*(ptr + i)指向该一维数组的首元素,*(ptr + i)+j指向该一维数组的第j个元素,*(*(ptr + i)+j)就是第i行第j列的元素值。

指针与结构体的关联

  1. 结构体指针的声明与使用
    • 结构体是一种用户自定义的数据类型,它可以包含不同类型的数据成员。当定义结构体指针时,它指向结构体变量的内存地址。
    • 例如,定义一个表示学生信息的结构体:
#include <stdio.h>
#include <string.h>

struct Student {
    char name[20];
    int age;
    float score;
};

int main() {
    struct Student stu = {"Alice", 20, 85.5f};
    struct Student *stuPtr = &stu;
    printf("学生姓名: %s\n", stuPtr->name);
    printf("学生年龄: %d\n", stuPtr->age);
    printf("学生成绩: %f\n", stuPtr->score);
    return 0;
}

这里定义了一个struct Student结构体类型,然后创建了一个stu结构体变量并初始化。接着定义了一个struct Student类型的指针stuPtr指向stu。使用->运算符可以通过结构体指针访问结构体成员。 2. 动态分配结构体内存与指针

  • 可以使用malloc函数动态分配结构体的内存空间,并使用指针来操作这块内存。
  • 示例代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct Student {
    char name[20];
    int age;
    float score;
};

int main() {
    struct Student *newStu = (struct Student *)malloc(sizeof(struct Student));
    if (newStu == NULL) {
        printf("内存分配失败\n");
        return 1;
    }
    strcpy(newStu->name, "Bob");
    newStu->age = 21;
    newStu->score = 90.0f;
    printf("学生姓名: %s\n", newStu->name);
    printf("学生年龄: %d\n", newStu->age);
    printf("学生成绩: %f\n", newStu->score);
    free(newStu);
    return 0;
}

在这段代码中,使用malloc函数为struct Student结构体分配内存,将返回的地址强制转换为struct Student *类型并赋值给newStu指针。然后通过指针设置结构体成员的值,最后使用free函数释放动态分配的内存。

指针与函数的关联

  1. 函数指针
    • 函数指针是指向函数的指针变量。在C语言中,函数在内存中也有一个入口地址,函数指针可以存储这个地址。
    • 函数指针的声明形式为:返回类型 (*指针变量名)(参数列表)。例如,对于一个简单的加法函数:
#include <stdio.h>

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

int main() {
    int (*funcPtr)(int, int) = add;
    int result = funcPtr(3, 5);
    printf("两数之和: %d\n", result);
    return 0;
}

这里定义了一个add函数,然后声明了一个函数指针funcPtr并让它指向add函数。通过函数指针funcPtr调用add函数,得到两数之和。 2. 指针作为函数参数

  • 当指针作为函数参数时,可以在函数内部修改调用函数中变量的值。例如:
#include <stdio.h>

void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

int main() {
    int num1 = 5, num2 = 10;
    printf("交换前: num1 = %d, num2 = %d\n", num1, num2);
    swap(&num1, &num2);
    printf("交换后: num1 = %d, num2 = %d\n", num1, num2);
    return 0;
}

在这个例子中,swap函数接受两个int类型指针作为参数,通过指针在函数内部交换了调用函数中num1num2的值。

指针类型的本质理解

  1. 指针类型决定解引用行为
    • 指针类型决定了对指针解引用(使用*运算符)时访问的内存大小和数据类型。例如,int类型指针解引用时,会按照int类型的大小(通常在32位系统上是4字节)从指针所指向的地址开始读取数据。
    • 示例代码如下:
#include <stdio.h>

int main() {
    int num = 12345;
    int *intPtr = &num;
    char *charPtr = (char *)intPtr;
    printf("int类型指针解引用的值: %d\n", *intPtr);
    printf("char类型指针解引用的第一个字节值: %d\n", *charPtr);
    return 0;
}

这里将int类型指针intPtr强制转换为char类型指针charPtr*intPtr按照int类型读取4字节数据,而*charPtr只按照char类型读取1字节数据,所以输出的值不同。 2. 指针类型与内存对齐

  • 不同的指针类型在内存中的对齐方式可能不同。内存对齐是为了提高内存访问效率,不同的数据类型在内存中的存储地址需要满足一定的对齐要求。例如,int类型可能要求地址是4的倍数,double类型可能要求地址是8的倍数。
  • 当定义指针时,指针类型也遵循相应的数据类型对齐规则。例如:
#include <stdio.h>

struct Data {
    char ch;
    int num;
};

int main() {
    struct Data data;
    char *charPtr = &data.ch;
    int *intPtr = &data.num;
    printf("char指针地址: %p\n", (void *)charPtr);
    printf("int指针地址: %p\n", (void *)intPtr);
    return 0;
}

在这个结构体Data中,char类型成员ch后面可能会有填充字节,以保证int类型成员num的地址满足int类型的对齐要求。因此,charPtrintPtr的地址会体现出这种内存对齐的差异。

多级指针

  1. 二级指针
    • 二级指针是指向指针的指针。声明二级指针的形式为:类型 **指针变量名。例如:
#include <stdio.h>

int main() {
    int num = 10;
    int *ptr = &num;
    int **ptrPtr = &ptr;
    printf("变量num的值: %d\n", num);
    printf("一级指针ptr指向的地址: %p\n", (void *)ptr);
    printf("二级指针ptrPtr指向的地址: %p\n", (void *)ptrPtr);
    printf("通过二级指针访问的值: %d\n", **ptrPtr);
    return 0;
}

这里定义了一个int类型变量num,一个指向num的一级指针ptr,以及一个指向ptr的二级指针ptrPtr**ptrPtr用于通过二级指针访问num的值。 2. 多级指针的应用场景

  • 多级指针在一些复杂的数据结构和算法中很有用。例如,在动态分配二维数组时,可能会用到二级指针。
  • 示例代码如下:
#include <stdio.h>
#include <stdlib.h>

int main() {
    int rows = 3, cols = 4;
    int **matrix = (int **)malloc(rows * sizeof(int *));
    for (int i = 0; i < rows; i++) {
        matrix[i] = (int *)malloc(cols * sizeof(int));
    }
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            matrix[i][j] = i * cols + j;
        }
    }
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            printf("%d ", matrix[i][j]);
        }
        printf("\n");
    }
    for (int i = 0; i < rows; i++) {
        free(matrix[i]);
    }
    free(matrix);
    return 0;
}

在这段代码中,使用二级指针matrix动态分配了一个二维数组。首先为每一行分配一个指针(matrix[i]),然后为每一行中的每一列分配实际的int类型数据。最后释放动态分配的内存。

指针运算的本质

  1. 指针的算术运算
    • 指针的算术运算主要包括加法、减法和比较运算。指针加法和减法的本质是基于指针所指向的数据类型的大小。
    • 例如,对于int类型指针,指针加1实际上是指针所指向的地址增加sizeof(int)个字节。
    • 示例代码如下:
#include <stdio.h>

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    int *ptr = arr;
    printf("指针ptr指向的地址: %p\n", (void *)ptr);
    ptr++;
    printf("指针ptr加1后指向的地址: %p\n", (void *)ptr);
    return 0;
}

在这个例子中,ptr++使得指针ptr指向数组arr的下一个元素,地址增加了sizeof(int)个字节(假设int类型为4字节)。 2. 指针的比较运算

  • 指针的比较运算(如==!=<>等)是基于指针所指向的内存地址。例如,可以比较两个指针是否指向同一个内存地址,或者判断一个指针是否在另一个指针之前或之后。
  • 示例代码如下:
#include <stdio.h>

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    int *ptr1 = &arr[0];
    int *ptr2 = &arr[2];
    if (ptr1 < ptr2) {
        printf("ptr1指向的地址在ptr2之前\n");
    }
    return 0;
}

在这段代码中,通过比较ptr1ptr2所指向的地址,判断ptr1是否在ptr2之前,并输出相应的信息。

指针与内存管理

  1. 动态内存分配与指针
    • 使用malloccallocrealloc等函数进行动态内存分配时,会返回一个指向分配内存起始地址的指针。例如:
#include <stdio.h>
#include <stdlib.h>

int main() {
    int *dynArray = (int *)malloc(5 * sizeof(int));
    if (dynArray == NULL) {
        printf("内存分配失败\n");
        return 1;
    }
    for (int i = 0; i < 5; i++) {
        dynArray[i] = i * 2;
    }
    for (int i = 0; i < 5; i++) {
        printf("%d ", dynArray[i]);
    }
    free(dynArray);
    return 0;
}

这里使用malloc函数为一个包含5个int类型元素的数组分配内存,返回的指针dynArray指向这块内存。可以通过指针操作数组元素,最后使用free函数释放内存。 2. 避免内存泄漏与指针

  • 内存泄漏是指动态分配的内存没有被释放,导致内存浪费。这通常是由于指针管理不当造成的。例如,丢失了指向动态分配内存的指针,就无法释放该内存。
  • 示例代码如下(错误示例,会导致内存泄漏):
#include <stdio.h>
#include <stdlib.h>

int main() {
    int *ptr = (int *)malloc(10 * sizeof(int));
    ptr = NULL; // 丢失了指向动态分配内存的指针
    // 这里无法释放之前分配的内存,导致内存泄漏
    return 0;
}

为了避免内存泄漏,在释放内存后应该将指针设置为NULL,以防止悬空指针(指向已释放内存的指针)。例如:

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

int main() {
    int *ptr = (int *)malloc(10 * sizeof(int));
    free(ptr);
    ptr = NULL;
    return 0;
}

这样,当再次使用ptr时,可以通过检查ptr是否为NULL来避免访问已释放的内存。

指针的常见错误与陷阱

  1. 未初始化指针
    • 使用未初始化的指针是非常危险的,因为它可能指向任意内存地址,导致程序崩溃或数据损坏。例如:
#include <stdio.h>

int main() {
    int *ptr;
    printf("%d\n", *ptr); // 未初始化指针,行为未定义
    return 0;
}

在这个例子中,ptr未初始化就进行解引用操作,这是一个严重的错误,程序可能会崩溃。 2. 悬空指针

  • 悬空指针是指指向已释放内存的指针。例如:
#include <stdio.h>
#include <stdlib.h>

int main() {
    int *ptr = (int *)malloc(sizeof(int));
    *ptr = 10;
    free(ptr);
    printf("%d\n", *ptr); // 悬空指针,行为未定义
    return 0;
}

这里在释放ptr指向的内存后,又试图通过ptr访问已释放的内存,这是一个悬空指针错误。 3. 指针越界

  • 指针越界是指指针访问了不属于它所指向对象的内存区域。例如,在数组中,访问超过数组边界的元素:
#include <stdio.h>

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    int *ptr = arr;
    printf("%d\n", ptr[10]); // 指针越界,行为未定义
    return 0;
}

在这个例子中,ptr[10]访问了数组arr边界之外的内存,这是一个指针越界错误,可能导致程序崩溃或数据损坏。

通过深入理解C语言指针与变量值类型的关联,以及掌握指针的各种特性和注意事项,可以编写出高效、稳定且安全的C语言程序。在实际编程中,要时刻注意指针的正确使用,避免常见的错误和陷阱。