C语言指针详解

发布于:2024-04-19 ⋅ 阅读:(31) ⋅ 点赞:(0)

系列文章目录

第一章 C语言基础知识

第二章 C语言控制语句

第三章 C语言函数详解

第四章 C语言数组详解

第五章 C语言操作符详解

第六章 C语言指针详解


文章目录

1. 指针

2. 指针变量

指针变量的声明

指针变量的初始化

3. 指针类型

 3.1 指针+-整数

3.2 指针的解引用

4. 野指针

4.1 野指针形成的原因

4.2 避免和处理野指针

4.3 代码示例

5. 指针算术

5.1 指针加法

5.2 指针减法

5.3 指针差值

5.4 指针比较

5.5 指针+-整数

5.6 指针-指针

5.7 指针和数组


1. 指针

指针是存储内存地址的变量,该地址指向存储在计算机内存中的某个位置的数据。指针是内存中一个最小单元的编号,也就是地址。平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量。

2. 指针变量

  • 指针变量的声明

要声明一个指针变量,需要在变量类型前加上星号(*),这表明这是一个指向该类型数据的指针。指针的类型决定了指针操作的步幅(即指针增加或减少时应移动的内存字节数)和能够访问的数据类型。

  • 指针变量的初始化

指针变量通常初始化为某个变量的地址,使用地址运算符 & 获取一个变量的地址。

#include <stdio.h>
int main()
{
  int a = 10;//在内存中开辟一块空间
  int *p = &a;//这里我们对变量a,取出它的地址,可以使用&操作符。
 //a变量占用4个字节的空间,这里是将a的4个字节的第一个字节的地址存放在p变量中,p就是一个之指针变量。 
  return 0;
}

3. 指针类型

指针有以下类型:

char  *pc = NULL; //char* 类型的指针是为了存放 char 类型变量的地址。
int   *pi = NULL; //int* 类型的指针是为了存放 int 类型变量的地址。
short *ps = NULL; //short* 类型的指针是为了存放 short 类型变量的地址。
long  *pl = NULL; 
float *pf = NULL;
double *pd = NULL;

 3.1 指针+-整数

int main()
{
    int n = 10;             // 声明一个整数变量 n 并初始化为 10
    char *pc = (char*)&n; //声明一个字符指针 pc 并初始化为指向n的地址,但将其类型转换为 char*
    int *pi = &n;           // 声明一个整数指针 pi 并初始化为指向 n 的地址

    printf("%p\n", &n);     // 打印变量 n 的地址
    printf("%p\n", pc);     // 打印字符指针 pc 指向的地址
    printf("%p\n", pc+1);   // 打印 pc+1,即 pc 向前移动一个字符的长度(1字节)
    printf("%p\n", pi);     // 打印整数指针 pi 指向的地址
    printf("%p\n", pi+1);   // 打印 pi+1,即 pi 向前移动一个整数的长度(4字节)
    return  0;
}

这段 C 语言代码演示了如何对不同类型的指针执行加法操作,并展示了指针加法如何依赖于指针所指向的数据类型的大小。

3.2 指针的解引用

假设你有一个整数变量和一个指向这个整数的指针,你可以使用解引用操作符来读取和修改这个整数的值:

#include <stdio.h>

int main() {
    int value = 10;
    int *pointer = &value;  // `pointer` 现在指向 `value` 的地址

    // 使用解引用操作符来获取指针指向的值
    printf("The value pointed to by pointer is: %d\n", *pointer);

    // 修改指针指向的内存中存储的值
    *pointer = 20;  // 改变 `value` 的值
    printf("The new value pointed to by pointer is: %d\n", *pointer);

    return 0;
}

//The value pointed to by pointer is: 10
//The new value pointed to by pointer is: 20

在这个例子中,pointer 是一个指向 value 的指针。通过使用解引用操作符 *pointer,你首先获取了 value 的原始值(10),然后修改了 value 的值(改为 20)。

4. 野指针

野指针是指向未知或无效内存区域的指针。野指针的存在通常是由于不当的内存管理和指针操作引起的,它们会导致程序行为不可预测,甚至导致程序崩溃和数据损坏。

4.1 野指针形成的原因

未初始化的指针: 未经初始化直接使用的指针。因为它们没有被明确地赋予任何地址,它们的值是不确定的,可能指向任何内存区域。

int *ptr; // 声明了一个整型指针,但未初始化
*ptr = 10; // 尝试赋值操作,但因为 ptr 指向的是随机位置,这可能导致崩溃

已释放的内存: 指向已经释放的内存的指针,通常称为悬挂指针。在释放动态分配的内存后,原指向该内存的指针仍然保留内存的地址,但该地址已不再有效。

int *ptr = malloc(sizeof(int)); // 动态分配内存
*ptr = 5; // 使用分配的内存
free(ptr); // 释放内存
*ptr = 10; // 再次使用这个指针,这是非法操作

指针运算越界如果通过指针运算(如增加或减少指针),使得指针超出其原始指向的数据结构的边界,那么这个指针就可能变成野指针。这种越界的指针可能指向任意的内存区域,其行为是未定义的。

int arr[5] = {1, 2, 3, 4, 5};
int *ptr = arr + 5; // 指向数组末尾之后的位置
*ptr = 10; // 越界操作,野指针解引用

4.2 避免和处理野指针

 初始化所有指针: 在声明指针时,最安全的做法是立即将其初始化为 NULL 或一个合法的、明确的地址。

int *ptr = NULL; // 初始化为 NULL,安全的做法

释放后置空: 在使用 free() 释放指针指向的内存后,立即将指针设置为 NULL。这样可以防止悬挂指针的问题。

free(ptr);
ptr = NULL; // 避免悬挂指针

小心处理指针运算: 在进行指针运算时,务必确保不会越界,并且操作符合逻辑。对于数组操作,确保索引值在有效范围内。

int arr[5];
for (int i = 0; i < 5; i++) {
    arr[i] = i; // 确保索引不会越界
}

4.3 代码示例

假设我们有一个整数数组和一个指针,我们想通过指针遍历数组并进行某些操作。在这个过程中,非常重要的是要确保指针不会指向数组的边界之外,这样可以避免野指针和潜在的内存错误。

#include <stdio.h>

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    int *ptr = arr; // 指针初始化,指向数组的第一个元素

    // 通过指针遍历数组
    for (int i = 0; i < 5; i++) {
        // 安全地通过指针访问数组元素
        printf("%d ", *ptr);
        ptr++; // 指针递增,移动到下一个元素
    }

    printf("\n");

    // 恢复指针到数组的起始位置
    ptr = arr;

    // 尝试超出数组范围的操作
    // 注意:以下代码是不安全的,仅用于示范如何避免
    for (int i = 0; i < 5; i++) {
        if (ptr < arr + 5) { // 确保指针没有超过数组末尾
            printf("%d ", *ptr);
            ptr++; // 安全递增指针
        }
    }

    printf("\n");
    return 0;
}

这个程序首先初始化一个指向数组第一个元素的指针。随后,通过一个 for 循环安全地遍历数组,每次迭代中,指针向前移动一个元素的位置。第一个循环简单地打印出数组的每个元素,而第二个循环在递增指针之前检查指针是否已经超出了数组的边界。

在上面的代码中,添加了一个检查 (ptr < arr + 5) 来确保在解引用指针前,指针没有超出数组的边界。这种检查是防止指针越界的一种好方法,尤其在处理边界条件时非常有用。

5. 指针算术

5.1 指针加法

直接对指针进行加法操作通常是不允许的,如 * + * 是非法的。但可以将整数值加到指针上,这种操作称为指针的递增。当你增加一个整数到指针时,实际上是将指针向前移动该整数乘以指针所指类型大小的字节数。

int arr[5] = {10, 20, 30, 40, 50};
int *p = arr;
p = p + 3;  // 将p向前移动3个int的位置,即p指向arr[3]

5.2 指针减法

与指针加法相对应,指针减法允许从指针中减去一个整数,即将指针向后移动指定的元素数量。同样,减去的整数会乘以指针指向类型的大小。

int arr[5] = {10, 20, 30, 40, 50};
int *p = arr + 4; // 指向arr的最后一个元素
p = p - 2;        // 将p向后移动2个int的位置,即p指向arr[2]

5.3 指针差值

指针之间可以进行减法运算,用来计算两个指针之间的元素个数。这种运算的结果是两个指针之间的距离,以它们所指类型的单位计算。

int arr[5] = {10, 20, 30, 40, 50};
int *p1 = arr;
int *p2 = arr + 3;
int n = p2 - p1;  // n 的值为 3,因为p2和p1之间有3个int类型的空间

5.4 指针比较

指针还可以进行比较运算,如检查两个指针是否相等(==)、不等(!=)、以及一个指针是否大于或小于另一个指针(<, >, <=, >=)。这些运算通常用于确保指针在有效范围内,或用于数组和其他数据结构的遍历。

int arr[5] = {10, 20, 30, 40, 50};
int *p1 = arr;
int *p2 = arr + 4;

if (p1 < p2) {
    printf("p1 is before p2\n");
}

5.5 指针+-整数

#define N_VALUES 5
float values[N_VALUES];  // 定义一个包含5个浮点数的数组
float *vp;               // 定义一个指向float的指针

// 指针+-整数;指针的关系运算
for (vp = &values[0]; vp < &values[N_VALUES];)
{
    *vp++ = 0;           // 将指针指向的当前元素设为0,并将指针向前移动到下一个元素
}

在这段代码中,vp 被初始化为指向数组 values 的第一个元素的地址(即 &values[0])。数组的名字(如 values)在不带下标的情况下也可以表示数组的第一个元素的地址,因此 &values[0] 可以简化为 values。

  • 指针的递增 (*vp++ = 0): 这里使用了后缀递增运算符 ++,它的含义是先对指针进行解引用赋值操作,然后指针自增。指针的自增是基于它指向的数据类型的大小进行的。因为 vp 是一个指向 float 的指针,所以每次递增都会移动一个 float 的大小(通常是 4 字节)。这个操作确保了每个数组元素都被访问并设置为 0。

  • 循环条件 (vp < &values[N_VALUES]): 这个条件检查 vp 是否还在数组 values 的有效范围内。&values[N_VALUES] 实际上指向数组最后一个元素之后的位置,这是一个常用技巧,用于确定指针是否已经处理完数组的所有元素。只要 vp 指向的地址小于这个边界,循环就继续执行。

5.6 指针-指针

int my_strlen(char *s)
{
    char *p = s;  // 初始化指针p为指向字符串s的开始位置

    // 循环直到找到字符串的终止字符'\0'
    while (*p != '\0')
        p++;  // 指针p逐字符向前移动

    // 返回p和s之间的距离,即字符串的长度
    return p - s;
}

在这个函数中,p 和 s 都是指向 char 的指针,它们指向同一个字符串的不同部分。

  • 初始化:char *p = s; 这行代码将指针 p 初始化为指向由参数 s 指定的字符串的起始位置。
  • 循环遍历字符串:while(*p != '\0') p++; 这是一个循环,它继续执行直到 *p(即 p 指向的当前字符)为 '\0',这是字符串的终止字符。在每次循环的末尾,p 递增,即向前移动到下一个字符。这种递增是按照指针指向的类型(这里是 char)进行的,对于 char 类型,每次递增移动一个字节。
  • 计算长度:return p - s; 这行代码计算指针 p 和指针 s 之间的差值,这个差值表示的是两个指针之间的元素数量,即字符串的字符数。在 C 语言中,指针减法的结果给出两个指针之间的元素数量,这里是 p 和 s 之间的 char 数量,正好是字符串的长度。

5.7 指针和数组

数组名表示的是数组首元素的地址。
#include <stdio.h>

int main()
{
    // 定义一个整型数组 arr,并初始化
    int arr[] = {1,2,3,4,5,6,7,8,9,0};
    
    // 定义一个指针 p,并初始化指向 arr 的第一个元素
    int *p = arr; // 指针存放数组首元素的地址
    
    // 计算数组 arr 的元素数量
    int sz = sizeof(arr) / sizeof(arr[0]); // sizeof(arr) 是整个数组的大小,sizeof(arr[0]) 是一个元素的大小
    
    // 遍历数组,打印每个元素的地址
    for (int i = 0; i < sz; i++)
    {
        // 打印数组元素的地址和通过指针加偏移量计算得到的地址
        // &arr[i] 是取得数组第 i 个元素的地址
        // p + i 是将指针 p 向前移动 i 个整型元素的距离
        printf("&arr[%d] = %p   <====> p+%d = %p\n", i, &arr[i], i, p + i);
    }
    return 0;
}

这段代码中的 printf 调用展示了两种获取数组元素地址的方法&arr[i] 和 p + i是等效的:

  • &arr[i] 直接使用数组索引获取第 i 个元素的地址。
  • p + i 使用基指针 p,通过加上偏移量 i(这里的 i 是自动根据指针所指向的数据类型进行缩放的,因为 p 是指向 int 的指针,所以 p + i 相当于 p + i*sizeof(int))来获取相同的地址。