C语言指针

前言:

        对于初学者来说,指针是一个难点,原因就是,它与内存息息相关,同时指针也是C语言最为重要的一个内容,基本是贯穿C语言的学习,C语言和数组,函数,结构体,动态内存,还有之后的io,线程等等,一系列相关的内容.指针的重要性不言而喻.在学习指针之前,内存是一大前提.

在 C 语言中,内存管理是一个重要的概念,而指针在内存操作中起着关键作用。

一、C 语言中的内存划分

  1. 栈(Stack)

    • 存储局部变量、函数参数等。
    • 内存的分配和释放由编译器自动管理,当函数被调用时,为局部变量在栈上分配内存,函数返回时自动释放这些内存。
    • 特点是先进后出(Last In First Out,LIFO),空间相对较小,效率高。
    • 例如:
     void function() {
         int localVariable = 5; // localVariable 存储在栈上
     }

  1. 堆(Heap)

    • 用于动态分配内存,通过malloccallocrealloc等函数进行分配,使用free函数释放。
    • 程序员需要手动管理堆内存的分配和释放,否则可能导致内存泄漏或悬空指针等问题。
    • 空间相对较大,灵活性高。
    • 例如:
     int *ptr = malloc(sizeof(int)); // 在堆上分配一个整数大小的内存空间
     free(ptr); // 释放堆上的内存

  1. 全局数据区(Data Segment)

    • 存储全局变量和静态变量。
    • 程序启动时分配内存,程序结束时释放。
    • 分为已初始化数据区和未初始化数据区(通常也称为 BSS 段)。已初始化的全局变量和静态变量存储在已初始化数据区,未初始化的全局变量和静态变量存储在 BSS 段,BSS 段在程序加载时会被初始化为零。
    • 例如:
     int globalVariable = 10; // 全局变量存储在全局数据区
     static int staticVariable = 20; // 静态变量也存储在全局数据区

  1. 代码区(Code Segment)

    • 存储程序的机器代码。
    • 只读区域,程序在执行时从这里读取指令。
    • 例如:

     int main() {
         return 0;
     }

函数main的代码存储在代码区。

二、指针与内存的关系,及指针的解释和初步认知

      

  内存是电脑上重要的存储器,计算机中所有程序运行时所需的数据都在内存中存储。所以为了有效的使用内存,就把内存划分成一个个小的存储单元,每个存储单元的大小为1个字节。
为了能够有效的访问到内存中的存储单元,就给每个存储单元进行了编号,这些编号称为存储单元的地址

    指针和普通变量一样,它也是一个变量,就好像int类型存储的是整型数字的变量,char类型存储的是字符类型的变量,而指针存储的地址仅此而已.假如我定义了一个变量 譬如:int a = 10 ;则在内存中就会开辟一个空间,这个空间有一个别名,而这个别名就做我定义的这个变量的地址,我在定义一个指针,int* ptr = &a;这个指针存储的就是a的地址,既然存储的是地址,所以我们就要用&来获取这个a的地址.但是我们要通过指针来获取地址所代表的内容,这里一般叫做指向 就要用的(*),来获取她指向的内容,例如:*ptr就等于a.任何变量在内存中都是有地址的,哪怕指针也不来例外,我上述定义的指针他也是有地址的,也是可以别被其他指针所指向,这就是二级指针!,以此类推.初学者,可能不知道指针有什么用处,为什么要定义指针,接着看看......

  1. 指针存储内存地址:指针是一个变量,它存储的是另一个变量的内存地址。通过指针,可以间接访问所指向的内存位置。
     int num = 5;
     int *ptr = # // ptr 存储了 num 的内存地址
  1. 指针操作内存

    • 可以通过指针来读取和修改所指向的内存中的值。
    • 例如:

     *ptr = 10; // 通过指针修改 num 的值为 10

  1. 动态内存分配与指针
    • 使用指针进行动态内存分配,可以在运行时根据需要分配内存空间。
    • 例如:
     int *dynamicPtr = malloc(sizeof(int)); // 在堆上分配内存,并将地址赋给指针
     *dynamicPtr = 20;
     free(dynamicPtr); // 释放动态分配的内存

        指针也就是内存地址,指针变量是用来存放内存地址的变量,在同一CPU构架下,不同类型的指针变量所占用的存储单元长度是相同的,而存放数据的变量因数据的类型不同,所占用的存储空间长度也不同。

        有了指针以后,不仅可以对数据本身进行操作,也可以对存储数据的变量地址进行操作。


        指针描述了数据在内存中的位置,标示了一个占据存储空间的实体,在这一段空间起始位置的相对距离值。在 C/C++语言中,指针一般被认为是指针变量,指针变量的内容存储的是其指向的对象的首地址,指向的对象可以是变量(指针变量也是变量),数组,函数等占据存储空间的实体。
指针相对于一个内存单元来说,指的是单元的地址,该单元的内容里面存放的是数据。在 C 语言中,允许用指针变量来存放指针,因此,一个指针变量的值就是某个内存单元的地址或称为某内存单元的指针。

总结:指针就是变量,用来存放地址的变量(存放在指针中的值都被当成地址处理).
这里有两个问题:

1. 一个存储单元有多大?
2. 如何编址?


        经过仔细的计算和权衡,发现将一个字节给一个对应的地址是比较合适的.


        在计算机中,当处理器需要在内存中存取数据的时候,就必然会面临一个问题就是寻址,寻址的前提就是建立地址。在计算机的物理结构中,一根地址线可以有0和1两个信号,那么通过这0和1的两个电信号, 32根地址线一共组合起来就可以形成2^32个电信号的组合,通过它们我们就可以建立起2^32个地址,用于处理器识别和访问。在这里,特别强调一个东西就是,在内存中,数据存储的最小单位是1个字节。为什么要强调这个,是因为它决定了我们的总的内存大小。由于在内存中,数据存储的最小单位是1个字节,而我们现在有2^32个地址,即我们需要2^32个字节大小的存储空间(2^32/1024/1024/1024),即4G大小,这也就是为什么32位地址线的内存大小为4G的原因了。那么32根地址线产生的地址就会是:


        同样的方法,那64位机器,如果给64根地址线,那能编址多大的空间?
这是我们就明白:

        在32位的机器上,地址是32个0或1组成的二进制序列,那地址就得用4个字节的空间来存储,所以一个指针变量的大小是4个字节。

        在64位的机器上,地址是64个0或1组成的二进制序列,那地址就得用8个字节的空间来存储,所以
一个指针变量的大小是8个字节。

三,指针的运算

三、指针的算术运算

  1. 指针与整数的加减:

    • 当一个指针加上或减去一个整数 n 时,指针会根据其所指向的数据类型的大小在内存中向前或向后移动 n 个位置。
    • 例如,假设有一个指向整数的指针int *ptr,如果ptr指向地址为 1000 的内存位置,并且整数在内存中占用 4 个字节,那么ptr + 1将指向地址为 1004 的位置,ptr - 2将指向地址为 992 的位置。
    • 代码示例:
     int arr[5] = {1, 2, 3, 4, 5};
     int *ptr = arr;
     ptr++; // 现在 ptr 指向 arr[1]

  1. 两个指针相减

    • 当两个指向同一数组中元素的指针相减时,结果是两个指针之间的元素个数。
    • 例如,如果有两个指针ptr1ptr2分别指向数组中的不同元素,那么ptr2 - ptr1将得到它们之间的距离(以数组元素个数为单位)。
    • 代码示例:

     int arr[5] = {1, 2, 3, 4, 5};
     int *ptr1 = arr;
     int *ptr2 = arr + 3;
     int distance = ptr2 - ptr1; // distance 的值为 3

指针的比较运算

  1. 相等性比较

    • 可以使用==!=运算符来比较两个指针是否相等或不相等。两个指针相等当且仅当它们指向同一个内存地址。
    • 例如:

     int num = 5;
     int *ptr1 = #
     int *ptr2 = ptr1;
     if (ptr1 == ptr2) {
         printf("The pointers are equal.\n");
     }

  1. 大小比较

    • 在某些情况下,可以使用<><=>=运算符来比较两个指针的大小。但这种比较只有在两个指针指向同一数组中的元素时才有意义。比较的结果是根据指针在内存中的地址大小来确定的。
    • 例如:

     int arr[5] = {1, 2, 3, 4, 5};
     int *ptr1 = arr;
     int *ptr2 = arr + 2;
     if (ptr1 < ptr2) {
         printf("ptr1 is before ptr2 in memory.\n");
     }

指针运算的注意事项

  1. 指针运算的有效性:

    • 指针运算只有在指针指向有效的内存区域并且运算结果也指向有效内存区域时才是有意义的。对指向无效内存区域的指针进行运算可能导致未定义的行为。
    • 例如,对一个野指针(未初始化的指针或指向已释放内存的指针)进行运算是非常危险的。
  2. 数据类型的影响:

    • 指针运算的结果取决于指针所指向的数据类型的大小。不同数据类型在内存中占用的空间不同,因此相同的指针运算操作可能会产生不同的结果。
    • 例如,对指向字符类型的指针和指向整数类型的指针进行相同的加法运算,结果会因数据类型的大小不同而不同。

四,指针与数组

        一、数组名是什么?

因此,可以得出结论:数组名在这里是首元素的地址

一、数组名与指针的关系

  1. 数组名在大多数情况下会被转换为指向数组第一个元素的指针。

    • 例如,定义一个整数数组int arr[5],那么arr可以看作是一个指向arr[0]的指针,即int *类型。
    • 这意味着可以使用指针的方式来访问数组元素,例如*(arr + 1)等同于arr[1]
  2. 但数组名和指针也有一些不同之处:

    • 数组名代表一块连续的内存区域,其大小在编译时就确定了,并且不能被重新赋值。而指针是一个变量,可以被重新赋值指向不同的内存地址。
    • 例如:
     int arr[5];
     int *ptr;

     ptr = arr; // 合法,将数组名(指向数组首元素的指针)赋值给指针变量
     arr = ptr; // 非法,不能将指针赋值给数组名

二、通过指针遍历数组

  1. 可以使用指针来遍历数组,这在一些情况下比使用下标更加高效。
    • 例如:

     int arr[5] = {1, 2, 3, 4, 5};
     int *ptr = arr;
     for (int i = 0; i < 5; i++) {
         printf("%d ", *(ptr + i));
     }

  1. 也可以使用指针的自增操作来遍历数组:
    • 例如:

     int arr[5] = {1, 2, 3, 4, 5};
     int *ptr = arr;
     while (ptr < arr + 5) {
         printf("%d ", *ptr);
         ptr++;
     }

三、指针与多维数组

  1. 对于二维数组,数组名同样可以看作是一个指向一维数组的指针。

    • 例如,定义一个二维数组int arr[3][4]arr可以看作是一个指向包含 4 个整数的一维数组的指针,即int (*)[4]类型。
    • 可以使用指针的方式来访问二维数组的元素,例如*(*(arr + 1) + 2)等同于arr[1][2]
  2. 对于多维数组,可以使用多个指针的组合来遍历和访问元素。

    • 例如:

     int arr[2][3][4];
     int (*ptr1)[3][4] = arr;
     for (int i = 0; i < 2; i++) {
         int (*ptr2)[4] = *(ptr1 + i);
         for (int j = 0; j < 3; j++) {
             int *ptr3 = *(ptr2 + j);
             for (int k = 0; k < 4; k++) {
                 printf("%d ", *(ptr3 + k));
             }
             printf("\n");
         }
         printf("\n");
     }

Tips:
        1. arr[i]可以理解为数组中首元素的地址偏移i个元素的大小,然后取出该地址中的内容
        2. 在上面的代码中, arr可以当作p来使用, p也可以当作arr来使用,其实两者完全不同从值来看, arr与p是相同的类型不同, p的类型为int *,arr是数组名,类型为int [7]
// 打印数组元素
for (int i = 0; i < size;i++) {
        printf("%d --- %d-----%d\n",arr[i],*(p+i),*(arr+i));
        }


p是变量,可以修改;arr是常量,不能修改,arr是地址

四 例外

        1. &数组名中,数组名不是首元素的地址,数组名表示整个数组, &数组名,取出的是整个数组的地址。这个概念可能有点抽象,但二维数组和指针的关系,是尤为重要的.

#include <stdio.h>
int main() {
int arr[10] = { 0 };
printf("arr-->%p\n", arr);
printf("arr+1-->%p\n", arr + 1);
printf("&arr[0]-->%p\n", &arr[0]);
printf("&arr[0]+1-->%p\n", &arr[0] + 1);
printf("&arr-->%p\n", &arr);
printf("&arr+1-->%p\n", &arr + 1);
return 0;
}

        arr+1与&arr[0]+1指的是数组中首元素的地址偏移1个元素的大小,在数组中每个元素为int类型,所以是4个byte。&arr中, arr代表整个数组, arr的类型为int [10],当&arr+1,指的是偏移整个数组,整个数组的大小为40个字节。
        2. sizeof(数组名) 数组名表示整个数组, sizeof(数组名)计算的是整个数组的大小。

一个简单的小问题,可以思考一下:

存在如下数组:
        char a[5] = {10,20,31,32,40};
        *(((char *)(&a + 1)) - 3) - 2
        表达式输出结果是?

五,指针数组

一、初始指针数组
         指针数组是指针还是数组?
当然是数组,并且是存放指针的数组。
#include <stdio.h>
int main() {
int a = 1;
int b = 2;
int c = 3;
int* pa = &a;
int* pb = &b;
int* pc = &c;
/*
整型数组--->存放整型
字符数组--->存放字符
指针数组--->存放指针
*/
int* arr[3] = { pa,pb,pc }; // 指针数组
// 遍历指针数组
for (int i = 0; i < 3;i++) {
printf("%p:%d\n",arr[i],*arr[i]);
}
 return 0;
}

图解:

二、指针数组工程的用法(模拟linux底层的内核代码)


    指针数组以NULL作为结束标志符


 
#include <stdio.h>
int main(){
int a = 10,b = 20,c = 30;
int* p_array[] = {&a,&b,&c,NULL};
int i = 0;
for(i = 0;p_array[i] != NULL;i++)
{
printf("%p\n",p_array[i]);
} 
return 0;
}

#include <stdio.h>
int main()
{
char a[] = {"Hello"};
char b[] = {"FeiFan"};
char c[] = {"Education"};
1 2 3 4 5 6 7三、二级指针保存指针数组的地址
/*
char* pa=a;
char* pb=b;
char* pc=c;
*/
char* q[] = {a,b,c,NULL};
int i = 0;
char* t = NULL;
for(i = 0;q[i] != NULL;i++)
{
for(t = q[i];*t != '\0' ;t++)
{
printf("%c ",*t);
} p
rintf("\n");
}
return 0;
}

三、二级指针保存指针数组的地址
 
char a = 65,b = 66,c=67;
char* arr[]={&a,&b,&c,NULL};
// arr 等价于 &arr[0]
// 因为arr[0]为&a,类型为char*
// 所以&arr[0]应该使用char**类型来保存
char** p = arr;
// 结论: 指针数组的首地址应该使用二级指针变量来保存
float scores[]={75.5,98.2,67.2,64.8,80.7};
float* q = scores;
// scores[i] <====>*(scores+i) <====>*(q+i) <====
char a = 65,b = 66,c=67;
char* arr[]={&a,&b,&c,NULL};
char** p = arr;
// 获取地址
arr[i] <===>*(arr+i)<===>*(p+i)<===>p[i]
// 获取数据

六,指针和二维数组


一、二维数组的基本概念


可以将二维数组看成是一维数组,只不过数组中的元素是一维数组。
定义方式: 数据类型 数组名[行数][列数]
例如:
        int arr[2][3]={{1,2,3},{4,5,6}};
// 说明
<1>数组名: arr
<2>数组中元素的个数: 2行*3列 = 6个
<3>数据中元素的获取方式: 数组名[行索引][列索引], arr[0][1]
<4>二维数组在内存中也是连续分配空间的

二、指针数组和数组指针


指针数组是数组
数组指针是指针


1. 指针数组
        本质是数组,数组中的每个元素是指针类型
2. 数组指针
int a = 1,b = 2,c = 3;
int* pa = &a;
int* pb = &b;
int* pc = &c;
int* arr[3] = { pa,pb,pc }; // 指针数组

2. 数组指针

        本质是一个指针变量,这个变量可以用来保存数组类型的地址
        int arr[2][3]={{1,2,3},{4,5,6}};
        前面说过,二维数组可以看作是一维数组,所以上面的arr可以看作是有2个元素的一维数组,数中的每个元素又是一个包含3个元素的一维数组。
int num = 10; //变量num的类型为【int】使用指针变量保存num的地址
int* p_num = #
int nums[3]={1,2,3};//(数组)变量nums的类型为【int [3]】 (去掉变量名, 剩下的就类型)使用指针变量保存nums的地址
int (*p_nums)[3]=&nums;//&nums,表示获取整个数组的地址, 变量p_nums的类型为【int (*)[3]】 ,表示数组指针。
 

int arr[2][3]={{1,2,3},{4,5,6}};
        前面说过,二维数组可以看作是一维数组,所以上面的arr可以看作是有2个元素的一维数组,数组中的每个元素又是一个包含3个元素的一维数组。

arr[0],arr[1]可以理解成数组名,arr[0]与arr[1]的类型为【int [3]】
1.arr是二维数组的数组名, 代表是数组中首元素的地址, 所以
2.arr <=====> &arr[0]
3.arr[0]的类型为【int [3]】 ,因此 &arr[0]的类型为【int (*)[3]】
4.int (*p)[3]=&arr[0];
或 int (*p)[3]=arr;

总结:

三,实例

#include <stdio.h>
int main()
{
int arr1[3]={1,2,3};
int arr2[2][3]={{1,2,3},{4,5,6}};
printf("arr1=%p,arr2=%p\n",arr1,arr2);
printf("arr1+1=%p\n",arr1+1);
printf("arr2+1=%p\n",arr2+1);
printf("&arr1+1=%p\n",&arr1+1);
printf("&arr2+1=%p\n",&arr2+1);
printf("arr2[0]+1=%p\n",arr2[0]+1);
return 0;
}

五,指针和函数

在 C 语言中,指针和函数有着密切的关系,主要体现在以下几个方面:

五、函数指针

  1. 定义:函数指针是指向函数的指针变量。它的定义形式为 “返回类型 (* 指针变量名)(参数列表)”。例如,“int (*funcPtr)(int, int)” 定义了一个指向接受两个整数参数并返回一个整数的函数的指针。
  2. 赋值:可以将一个函数的地址赋值给函数指针。例如,“funcPtr = someFunction”,其中 “someFunction” 是一个具有相同返回类型和参数列表的函数。
  3. 调用:通过函数指针可以调用函数。例如,“int result = funcPtr (3, 4)”,这将调用函数指针 “funcPtr” 所指向的函数,并传入参数 3 和 4,然后将返回值赋给 “result”。
  4. 用途
    • 函数指针可以作为参数传递给其他函数,使得函数具有更高的灵活性和可扩展性。例如,可以定义一个函数,它接受一个函数指针作为参数,并在内部调用这个函数指针所指向的函数。
    • 函数指针可以用于实现回调函数。在某些情况下,一个函数需要在特定的事件发生时调用另一个函数,这时可以使用函数指针来实现回调机制。

二、指针作为函数参数

  1. 传递变量地址:可以将指针作为函数的参数,传递变量的地址给函数。这样,函数可以通过指针间接访问和修改调用者传入的变量。
    • 例如:
   void increment(int *num) {
       (*num)++;
   }

   int main() {
       int value = 5;
       increment(&value);
       printf("Value after increment: %d\n", value);
       return 0;
   }

  1. 传递数组:当数组作为函数参数传递时,实际上传递的是数组的首地址,也就是一个指针。函数可以通过这个指针来访问和操作数组中的元素。
    • 例如:

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

   int main() {
       int array[] = {1, 2, 3, 4, 5};
       printArray(array, 5);
       return 0;
   }

  1. 传递动态分配的内存:指针可以用于传递动态分配的内存给函数,函数可以在内部对这块内存进行操作,而不会影响到调用者的其他内存区域。
    • 例如:

   void fillArray(int *arr, int size) {
       for (int i = 0; i < size; i++) {
           arr[i] = i * 10;
       }
   }

   int main() {
       int *dynamicArray = (int *)malloc(sizeof(int) * 5);
       fillArray(dynamicArray, 5);
       for (int i = 0; i < 5; i++) {
           printf("%d ", dynamicArray[i]);
       }
       free(dynamicArray);
       return 0;
   }

总之,指针和函数的结合使用为 C 语言编程提供了强大的功能和灵活性,但也需要谨慎使用,以避免出现错误和内存泄漏等问题。

六.特殊指针

在 C 语言中,有一些特殊的指针类型,它们具有特定的用途和特点。以下是对几种特殊指针的介绍:

一、空指针(NULL pointer)

  1. 定义:空指针是一个特殊的值,它表示指针不指向任何有效的内存地址。在 C 语言中,通常用宏定义NULL来表示空指针。例如,“int *ptr = NULL;” 定义了一个指向整数的空指针。
  2. 用途
    • 空指针常用于表示函数的错误返回值或未初始化的指针。当一个函数返回一个指针类型的值时,如果出现错误情况,可以返回空指针来表示失败。
    • 在使用指针之前,应该检查指针是否为NULL,以避免访问无效的内存地址,从而导致程序崩溃。
    • 例如:
   int *findElement(int *arr, int size, int target) {
       for (int i = 0; i < size; i++) {
           if (arr[i] == target) {
               return &arr[i];
           }
       }
       return NULL;
   }

   int main() {
       int array[] = {1, 2, 3, 4, 5};
       int *result = findElement(array, 5, 6);
       if (result == NULL) {
           printf("Element not found.\n");
       } else {
           printf("Element found at address %p.\n", result);
       }
       return 0;
   }

二、void 指针(void pointer)

  1. 定义:void 指针也称为通用指针,它可以指向任何类型的数据。void 指针的定义形式为 “void *”。
  2. 用途
    • void 指针的主要用途是在不知道具体数据类型的情况下传递指针。例如,可以使用 void 指针来传递不同类型的参数给一个通用的函数。
    • 在使用 void 指针时,需要进行类型转换才能正确地访问所指向的数据。例如,“int *ptr = (int *) voidPtr;” 将 void 指针转换为指向整数的指针。
    • 例如:

   void printData(void *data, int size, char type) {
       if (type == 'i') {
           int *intData = (int *)data;
           for (int i = 0; i < size; i++) {
               printf("%d ", intData[i]);
           }
       } else if (type == 'c') {
           char *charData = (char *)data;
           for (int i = 0; i < size; i++) {
               printf("%c ", charData[i]);
           }
       }
       printf("\n");
   }

   int main() {
       int intArray[] = {1, 2, 3, 4, 5};
       printData(intArray, 5, 'i');
       char charArray[] = {'a', 'b', 'c', 'd', 'e'};
       printData(charArray, 5, 'c');
       return 0;
   }

三、常量指针和指针常量

  1. 常量指针(pointer to constant)
    • 定义形式为 “数据类型 *const 指针变量名”。常量指针指向的内容是常量,不能通过该指针修改所指向的内容,但指针本身可以被重新赋值指向其他地址。
    • 例如:

   int num = 10;
   int num2 = 20;
   const int *ptr = &num;
   // *ptr = 15; // 错误,不能通过常量指针修改所指向的内容
   ptr = &num2; // 合法,可以重新赋值指针指向其他地址

  1. 指针常量(constant pointer)
    • 定义形式为 “数据类型 *const 指针变量名”。指针常量是指指针本身是常量,不能被重新赋值指向其他地址,但可以通过该指针修改所指向的内容。
    • 例如:

        

   int num = 10;
   int *const ptr = &num;
   *ptr = 15; // 合法,可以通过指针常量修改所指向的内容
   // ptr = &num2; // 错误,不能重新赋值指针常量指向其他地址

        这些特殊指针在 C 语言中具有特定的用途,可以帮助程序员更加灵活地处理不同类型的数据和实现各种编程需求。但在使用时需要注意正确的类型转换和避免出现错误的内存访问。

七,野指针

在 C 语言中,野指针(wild pointer)是指未初始化或已被释放但仍被错误使用的指针。野指针可能会导致严重的程序错误,如内存访问冲突、数据损坏甚至程序崩溃。

一、野指针的产生原因

  1. 未初始化的指针:
    • 当定义一个指针变量但没有对其进行初始化时,它的值是不确定的,可能指向任何内存地址。如果在这种情况下使用该指针,就会产生野指针。
    • 例如:
     int *ptr;
     // *ptr = 10; // 错误,ptr 是未初始化的野指针,可能指向任何地址,访问它会导致未定义行为

  1. 指针指向的内存被释放后继续使用:
    • 当使用动态内存分配函数(如malloccallocrealloc)分配内存,并通过指针访问这块内存后,如果使用free函数释放了这块内存,而该指针没有被设置为NULL或重新赋值,那么这个指针就变成了野指针。
    • 例如:

     int *ptr = malloc(sizeof(int));
     *ptr = 10;
     free(ptr);
     // *ptr = 20; // 错误,ptr 现在是野指针,因为它指向的内存已经被释放

二、野指针的危害

  1. 内存访问错误:

    • 野指针可能指向无效的内存地址,访问这样的地址可能导致程序崩溃或产生不可预测的结果。例如,可能会读取到错误的数据,或者试图写入数据到受保护的内存区域,从而引发内存访问冲突。
    • 例如:如果野指针指向的内存地址已经被其他程序或数据占用,对其进行写入操作可能会破坏其他数据,导致程序出现错误。
  2. 数据损坏:

    • 如果程序错误地使用野指针进行写入操作,可能会覆盖其他重要的数据,导致数据损坏。这可能会影响程序的正确性和稳定性,甚至导致数据丢失。
    • 例如:如果野指针指向的内存被多次错误地写入,可能会破坏程序的关键数据结构,导致程序无法正常运行。
  3. 难以调试:

    • 由于野指针的错误通常是在运行时出现,而且错误的表现可能不明显,因此很难确定问题的根源。这使得调试程序变得非常困难,可能需要花费大量的时间和精力来找出问题所在。
    • 例如:程序可能在运行一段时间后才出现错误,或者错误只在特定的输入或环境下出现,这增加了调试的难度。

三、避免野指针的方法

  1. 初始化指针:
    • 在定义指针变量时,应该立即对其进行初始化。如果不确定指针应该指向哪里,可以将其初始化为NULL
    • 例如:

     int *ptr = NULL;

  1. 检查指针是否为NULL
    • 在使用指针之前,应该检查它是否为NULL。如果指针为NULL,则表示它不指向任何有效的内存地址,避免对其进行访问。
    • 例如:

     int *ptr = NULL;
     if (ptr!= NULL) {
         // 安全地使用指针
     }

  1. 释放内存后将指针设置为NULL
    • 当使用free函数释放动态分配的内存后,应该立即将指针设置为NULL,以避免继续使用已释放的内存。
    • 例如:

     int *ptr = malloc(sizeof(int));
     free(ptr);
     ptr = NULL;

  1. 小心指针的赋值和传递:
    • 在进行指针赋值或传递时,要确保指针的有效性。避免将未初始化的指针或已释放的指针传递给其他函数。
    • 例如:在函数参数中使用指针时,应该明确指针的有效性,并在函数内部进行适当的检查。

        野指针是 C 语言编程中一个常见的问题,需要引起足够的重视。通过正确的初始化、检查和管理指针,可以有效地避免野指针的出现,提高程序的稳定性和可靠性。

八,指针的用处

一、高效地操作内存

  1. 直接访问内存地址:指针可以直接存储内存地址,通过指针能快速地访问特定的内存位置,无需通过间接的方式。这在需要对内存进行底层操作时非常有用,比如操作硬件寄存器或实现高效的数据结构。
  2. 动态内存分配:使用指针可以进行动态内存分配,如通过malloccalloc等函数在运行时根据实际需求分配所需大小的内存空间,提高程序的灵活性和适应性。

二、参数传递

(这点尤为重要,....特别提及,形参只是实参的拷贝,为什么说?因为在参数传递的时候,只是在内存开辟了一个新的参数,只不过里面存储的内容是一样的,但是参数的本身的地址已经改变,所以在使用指针的时候,切记要记住指针指向的哪个变量,这也是二级指针需要注意的点,因为既然本身的地址已经改变,那干脆直接传入一个为保存指针的参数,不就好了,这就是二级指针)!!!这个问题很重要.

  1. 传递大型数据结构:当需要将大型数据结构(如数组、结构体等)作为参数传递给函数时,传递指针比传递整个数据结构更高效。因为传递指针只需要传递一个地址,而不是复制整个数据结构,减少了时间和空间开销。
  2. 允许函数修改外部变量:通过传递变量的地址(指针)给函数,函数可以直接修改外部变量的值,实现了在函数内部对外部数据的修改,增加了程序的灵活性和功能性。

三、实现复杂数据结构

  1. 链表、树和图等数据结构:指针在实现诸如链表、树和图等复杂数据结构中起着关键作用。通过指针可以将不同的数据节点连接起来,实现灵活的数据存储和访问方式。
  2. 高效的数据操作:使用指针可以方便地进行数据的插入、删除和遍历等操作,提高对复杂数据结构的操作效率。

总之,指针在 C 语言中提供了强大的功能和灵活性,使程序员能够更高效地操作内存、实现复杂的数据结构以及进行灵活的参数传递。但同时也需要谨慎使用,以避免出现内存错误和野指针等问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值