C语言的灵魂 - 指针

目录

一、变量的地址

举个小栗子 

运行效果

小细节 

二、 指针是什么?

三、对指针赋值

举个小栗子 

运行效果

四、通过指针操作内存变量

举个小栗子 

运行效果

再举个小栗子 

 运行效果

五、函数的参数传递 

运行效果

六、空指针 

 运行效果

七、数组的地址

运行效果

八、地址的运算

运行效果

小细节

九、指针占用内存大小

 运行效果

十、指针的其它知识 

(1)指针数组

(2)数组指针 

(3)指向指针的指针 

  (4)  传递指针给函数

(5)从函数返回指针 

(6)函数指针  

(7)函数指针数组 

(8)函数指针与回调函数

(9)const 与 指针

(10)常量指针

(11)指针常量


一、变量的地址

每一个变量都有一个内存位置,每一个内存位置都定义了可使用 & 运算符访问的地址,它表示了在内存中的一个地址

举个小栗子 

test44.c

#include <stdio.h>

int main ()
{
    int i = 10;
    int *pi;              // 定义指针变量
    pi = &i;

   printf("i 变量的地址: %p\n", &i);
   printf("pi 变量的地址: %p\n", pi);
   return 0;
}

运行效果

小细节 

  1. 在printf函数中,输出内存地址的格式控制符是%p,地址采用十六进制的数字显示,十六进制由数字 0~9、字母 A~F 或 a~f(不区分大小写)组成。
  2. test44.c 程序运行了3次,每次输出的结果不一样,原因很简单,程序每次运行的时候,向系统申请内存,系统随机分配内存。

  •  pi 是一个指针,存储变量 i 的地址
  • 指针 pi 的类型必须与变量的类型一致,因为整形的指针只能存储整形变量的指针地址

二、 指针是什么?

指针是一种特别变量,全称是指针变量,专用于存放其它变量在内存中的地址编号

指针在使用之前要先声明,语法是:

type *var_name;

type 是指针的基类型,它必须是一个有效的 C 数据类型var_name 是指针变量的名称。用来声明指针的星号 * 与乘法中使用的星号是相同的。但是,在这个语句中,星号是用来指定一个变量是指针

以下是有效的指针声明:

int    *pi;    // 一个整型的指针 
double *pd;    // 一个 double 型的指针 
float  *pf;    // 一个浮点型的指针 
char   *pc;    // 一个字符型的指针 

三、对指针赋值

  1. 不管是整型、浮点型、字符型,还是其他的数据类型的内存变量,它的地址都是一个十六进制数,可以理解为内存单元的编号
  2. 用整数型指针存放整数型变量的地址;用字符型指针存放字符型变量的地址;用双精度型指针存放双精度型变量的地址,用自定义数据类型指针存放自定义数据类型变量的地址
  3. 把指针指向具体的内存变量的地址,就是对指针赋值

举个小栗子 

test45.c

#include <stdio.h>

int main()
{
  int    i = 10;
  char   c = 'A';
  double d = 3.14;

  int    *pi = 0;  // 定义整数型指针并初始化
  char   *pc = 0;  // 定义字符型指针并初始化
  double *pd = 0;  // 定义双精度型指针并初始化

  pi=&i;  // 整型指针并指向变量i
  pc=&c;  // 字符型指针并指向变量c
  pd=&d;  // 双精度型指针并指向变量d

   // 输出指针变量的地址
   printf("pi的值是:%p\n",pi);
   printf("pc的值是:%p\n",pc);
   printf("pd的值是:%p\n",pd);
}

运行效果

四、通过指针操作内存变量

定义了指针变量,并指向了内存变量的地址,就可以通过指针来操作内存变量,在指针前加星号 * (解引用),效果与使用变量名相同。

举个小栗子 

test46.c

#include <stdio.h>

int main()
{
  int    i = 10;
  char   c = 'A';
  double d = 3.14;

  int    *pi = 0;  // 定义整数型指针并初始化
  char   *pc = 0;  // 定义字符型指针并初始化
  double *pd = 0;  // 定义双精度型指针并初始化

  pi=&i;  // 整型指针并指向变量i
  pc=&c;  // 字符型指针并指向变量c
  pd=&d;  // 双精度型指针并指向变量d

   // 输出指针变量的值
   printf("pi的值是:%d\n",*pi);  // * 号 解引用 获取 指针的值
   printf("pc的值是:%c\n",*pc);
   printf("pd的值是:%lf\n",*pd);
}

运行效果

test46.c 的代码几乎和 test45.c 一模一样,只不过在 printf 时加了个 * 号,来获取指针的值

再举个小栗子 

test47.c

#include <stdio.h>

int main()
{
  int  i = 10;

  int  *pi = 0;  // 定义整型指针并初始化

  pi = &i;  // 整型指针指向变量i

  // 通过指针操作内存变量,改变内存变量的值
  *pi=20;    // 此时 i=20;

   printf("pi的值是:%d\n",*pi);
   printf("i的值是:%d\n",i);
}

 运行效果

这也就是为什么之前讲的引用调用的时候,函数的参数要用指针来接收,因为通过指针传递方式,形参为指向实参地址的指针,当对形参的指向操作时,就相当于对实参本身进行的操作;

不了解引用调用的可以去看看这篇文章的后半段

https://blog.csdn.net/XiaoQiu__/article/details/126473551?spm=1001.2014.3001.5501

五、函数的参数传递 

test48.c

#include <stdio.h>

// 声明func函数,p是一个指针变量
void func(int *p);

int main()
{
  int a = 10;

  printf("位置一:a是一个变量 变量的地址是:%p,a的值是: %d\n",&a,a);
  func(&a);   // 调用函数,传递变量a的地址的值
  printf("位置二:a是一个变量 变量的地址是:%p,a的值是: %d\n",&a,a);
}

void func(int *p)
{
  printf("位置三:p是一个指针 指针的地址是:%p, 指向的变量的值是: %d\n",p,*p);
  *p = 20;
  printf("位置四:p是一个指针 指针的地址是:%p, 指向的变量的值是: %d\n",p,*p);
}

运行效果

  1. test48.c 演示了函数参数和指针的使用,主程序把变量 a 的地址传递给函数 func,func 函数的参数 p 是一个指针,接存放变量 a 的地址。在函数 func 中,根据指针中的地址直接操作内存,从而修改了主程序中变量 a 的值
  2. scanf 函数已经很熟悉了吧,调用scanf函数的时候,需要在变量前面加符号 &,其实就是把变量的地址传给 scanf 函数,scanf函数根据传进去的地址直接操作内存,改变内存中的值,完成了对变量的赋值

六、空指针 

  1. 在变量声明的时候,如果没有确切的地址可以赋值,为指针变量赋一个 NULL 值是一个良好的编程习惯。赋为 NULL 值的指针被称为指针
  2. 空指针就是说指针没有指向任何内存变量,指针的值是空,不能操作内存,否则可能会引起程序的崩溃,也就是段错误(Core Dump)
  3. 空指针是一个定义在标准库中的值为零的常量

test49.c

#include <stdio.h>
 
int main ()
{

   int *pi=0;  // 定义一个指针
 
   printf("pi的值是 %p\n",pi);
 
   *pi=10;  // 试图对空指针进行赋值操作,必将引起程序的崩溃
 
   return 0;
}

 运行效果

七、数组的地址

数组占用的内存空间是连续的,数组名是数组元素的首地址,也是数组的地址

 test50.c

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

int main()
{
  char name[51];
  memset(name,0,sizeof(name));
  strcpy(name,"www.baidu.com");

  printf("%p\n",name);
  printf("%p\n",&name);
  printf("%p\n",&name[0]);

  printf("%s\n",name);
  printf("%s\n",&name);
  printf("%s\n",&name[0]);
}

运行效果

test50.c 可以看出,数组名、对数组取地址和数组元素的首地址是同一回事

八、地址的运算

  1. 地址可以用加(+)和减(-)来运算,加1表示下一个存储单元的地址,减1表示上一个存储单元的地址
  2. 一般情况下,地址的运算适用于数组,对单个变量的地址运算意义不大

test51.c 

#include <stdio.h>

int main()
{
  char   c[4];   // 字符数组
  int    i[4];   // 整数数组
  double d[4];   // 浮点数组

   // 用地址相加的方式显示数组全部元素的的址
   printf("%p %p %p %p\n",c,c+1,c+2,c+3);
   printf("%p %p %p %p\n",i,i+1,i+2,i+3);
   printf("%p %p %p %p\n",d,d+1,d+2,d+3);
}

运行效果

小细节

  1. 第一行输出的每个地址的增量是1,第二行的每个地址的增量是4,第三行的每个地址的增量是8,为什么会这样?
  2. 因为数组 c 是 char 型,一个存储单元是1个字节,数组 i int 型,一个存储单元是4个字节,数组 d 是 double 型,一个存储单元是8个字节,地址加1指的是下一个存储单元,不是数学意义中的加1
  3. 指针的每一次递增,它其实会指向下一个元素的存储单元
  4. 指针的每一次递减,它都会指向前一个元素的存储单元
  5. 指针在递增和递减时跳跃的字节数取决于指针所指向变量数据类型长度,比如 int 就是 4 个字节

九、指针占用内存大小

指针也是一种内存变量,是内存变量就要占用内存空间,在C语言中,64位操作系统中任何类型的指针都占用 8 字节的内存(32位操作系统 4 字节)

test52.c 

#include <stdio.h>
int main()
{

   printf("整型指针占用的内存大小为:%d.\n",sizeof(int *));        // 输出:8
   printf("字符型指针占用的内存大小为:%d.\n",sizeof(char *));      // 输出:8
   printf("浮点型指针占的内存大小为:%d.\n",sizeof(double *));  // 输出:8

   return 0;
}

 运行效果

以上介绍的知识已经包括了指针99%的用法了,还有一些的知识点如指针的指针、函数指针等,这些概念难以理解,应用场景极少。但我还是给大家科普一下,以备不时之需!

十、指针的其它知识 

(1)指针数组

指针数组是一个存放指针的数组

指向整数的指针数组的声明:

int *p[MAX];

在这里,把 p 声明为一个数组,由 MAX 个整数指针组成。因此,p 中的每个元素,都是一个指向 int 值的指针。

test53.c

#include <stdio.h>

const int MAX = 3;//定义全局变量MAX

int main()
{
   int arr[] = { 1 , 2 , 3 };// int型数组
   int i;
   int *p[MAX];// 指针数组

   for (i = 0;i < MAX;i++) {

     p[i] = &arr[i];

   }
   for (i = 0;i < MAX;i++) {

     printf("arr[%d] = %d\n",i,*p[i]);

   }
   return 0;
}

                                          

运行效果

数组 p 就是一个指针数组,数组的每一个元素都是指针,并且在这个 p 数组中的每一个指针元素所指向的都是 int 值

(2)数组指针 

数组指针是数组还是指针?答案是指针

字符指针,就是指向字符的指针

char c = 'a';
char *pc = &a;

 整形指针,就是指向整形的指针

int i = 0;
int *pi = &i;

那么以此类推:

数组指针就是指向数组的指针

数组指针的声明:

int (*pi)[10];

下面有两个指针,分别是什么类型?

int *pi[10];//指针数组
int (*pi1)[10];//数组指针

(3)指向指针的指针 

  1. 指向指针的指针也叫二级指针,二级指针是一种多级间接寻址的形式,或者说是一个指针链。通常,一个指针包含一个变量的地址。当我们定义一个二级指针时,第一个指针包含了第二个指针的地址,第二个指针指向包含实际值的位置
  2. 上面说法是官方的说辞,按我的说法就是,二级指针其实也就是一个普通的指针变量,只是它里面保存的值是另一个一级指针的地址罢了

有一个 int 类型的变量 i , p1  是指向 i 的指针变量,p2 又是指向 p1 的指针变量,那么 p2 就是一个二级指针

二级指针的声明:

int **p2;

二级指针的定义:

int a = 10;
int *p1 = &a;
int **p2 = &p1;

 示例 test54.c

#include <stdio.h>
int main()
{
    int i = 100;
    int *p1;
    int **p2;

    // 获取 i 的地址
    p1 = &i;

    // 获取 p1 的地址
    p2 = &p1;

    printf("i的值 : %d\n", i );
    printf("i的地址 : %p\n", &i );
    printf("p1的地址 : %p\n", p1 );
    printf("p1的值 : %d\n", *p1 );
    printf("p2的地址 : %p\n", p2 );
    printf("p2的值 : %d\n", **p2);

    return 0;
}

运行效果

你请注意看 p1 和 p2 的地址,地址不同,也就是它们并不在同一块内存地址,虽然它们的内容一样,都是 i 的地址

 (4)  传递指针给函数

C 语言允许您传递指针给函数,只需要声明函数参数为指针类型即可

 示例 test55.c

#include <stdio.h>

// 函数声明
double GetAvg(int *arr,int size);

int main()
{
    // 整形数组
    int arr[5] = { 1 , 2 , 3 , 4 , 5 };
    double avg;

    avg = GetAvg(arr,5);

    printf("avg = %lf\n",avg);

    return 0;
}
double GetAvg(int *arr,int size)
{
    int i , sum = 0;
    double avg;

    for (i = 0;i < size;i++) {

      sum+=arr[i];

    }
    avg = sum / size;

    return avg;
}

运行效果

(5)从函数返回指针 

C 允许您从函数返回指针,声明一个返回指针的函数即可

声明:

int * myFunction()
{
    ...
    ...
    ...
}

C 不支持在调用函数时返回局部变量的地址,如果非要的话,就要定义局部变量为 static 变量  

示例 test56.c

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

int* GetRand();
int main()
{

   int *p;
   int i;

   p = GetRand();

   for (i = 0;i < 10;i++) {

     printf("p[%d] = %d\n",i,*(p+i) );

   }

   return 0;
}
int* GetRand()
{
    static int arr[10];
    int i;

    // 随机种子
    srand((unsigned)time(NULL));

    for (i = 0;i < 10;i++) {

      arr[i] = rand() % 51;// 产生1~50的随机数 
      printf("%d\n", arr[i] );
    }
    return arr;
}

运行效果

test56.c 会生成 10 个随机数,并使用表示指针的数组名(即第一个数组元素的地址)来返回它们 

(6)函数指针  

函数指针是指向函数的指针变量

指针变量是指向一个整型、字符型或数组等变量,函数指针是指向函数

函数指针可以像一般函数一样,用于调用函数、传递参数

函数指针变量的声明:

typedef int (*fun_ptr)(int,int); // 声明一个指向同样参数、返回值的函数指针类型

示例 test57.c

#include <stdio.h>

int max(int x,int y);

int main()
{
    int (*p)(int,int) = &max;// p 是函数指针  & 号可以省略
    int a,b,c,d;

    printf("请输入3个数字:");
    scanf("%d %d %d",&a,&b,&c);

    d = p( p(a,b) , c);// 等同于 d = max( max(a,b) , c);

    printf("最大的数字是: %d\n", d);

    return 0;
}
int max(int x,int y)
{
    return x > y ? x : y;
}

运行效果

函数也是有地址的,函数的地址就是内存中存储这个函数的位置

示例 test58.c

#include <stdio.h>

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

int main()
{

    printf("%p\n",add);
    printf("%p\n",&add);

    return 0;
}

运行效果

上面两个地址一样,取地址函数名(&add)和函数名(add)都代表了函数的地址,两者没有什么区别, 所以在 test57.c 中的函数max赋值给函数指针时 & 可以省略的原因

(7)函数指针数组 

函数指针数组是什么?

就是字面意思,存放函数指针的数组,数组的每一个元素都是一个函数指针

比如我有一个长这样的函数指针

int (*p)(int , int) =  add;

如果我们需要一个数组来存放该指针,数组类型就应该和函数指针的类型是相同的

int (*p[10])(int , int);//函数指针数组

上面就是一个函数指针数组,它的类型是int (*)(int , int),它可以存放10个这种类型的函数指针

(8)函数指针与回调函数

  1. 回调函数是由别人的函数执行时调用你实现的函数
  2. 函数指针变量可以作为某个函数的参数来使用的,回调函数就是一个通过函数指针调用的函数

示例 test59.c
 

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


void My_Arr(int *arr,int size,int(*GetRand)(void))
{
   int i = 0;
   for ( i = 0;i < size;i++) {

     arr[i] = GetRand();

   }
}
// 回调函数
int GetRand()
{
   return rand() % 51;  // 1 ~ 50 的随机数
}
int main()
{
    srand( (unsigned)time(NULL) );//随机种子   


    int i = 0;
    int arr[5];

    //GetRand不能加括号,否则无法编译,因为加上括号之后相当于传入此参数时传入了 int , 而不是函数指针
    My_Arr(arr,5,GetRand);
    for (i = 0;i < 5;i++) {

      printf("%d\n",arr[i]);

    }
    return 0;
}

运行效果

  1. test59.c 中 My_Arr 函数定义了三个参数,其中第三个参数是函数指针,通过该函数来设置数组的值
  2. test59.c 定义了回调函数 GetRand,它返回一个随机值,它作为一个函数指针传递给 My_Arr 函数
  3. My_Arr 将调用 5 次回调函数,并将回调函数的返回值赋值给数组
  4. 回调函数其实就是函数指针的一种用法罢了

(9)const 与 指针

const 是constant的缩写,意思是“恒定不变的”,它是定义只读变量的关键字。用const定义只读变量的方法很简单,就在定义变量时前面加const即可,如:

  const  double  pd = 3.1415926;

用const定义的变量的值是不允许改变的,不允许给它重新赋值,即使是赋相同的值也不可以。所以说它定义的是只读变量。这也就意味着必须在定义的时候就给它赋初值,如果程序中试图改变它的值,编译的时候就会报错

了解了 const 之后,把const 和指针联想起来,看看会发生什么?

我们知道指针是指向另一个变量的地址,我们可以通过访问这个地址来修改另一个变量的值

示例 test60.c

#include <stdio.h>
int main()
{
   int a = 10;
   int b = 20;
   int *pa = 0;

   pa = &a;// 指针自身
   *pa = 20;// 指针的指向  通过修改指针来改变a的值

   printf("a = %d\n",a);
   printf("b = %d\n",b);
   printf("pa = %d\n",*pa);
}

运行效果

接下来看看这行代码

const int *p2 = &a;//常量指针

这行代码是什么?答案是常量指针

(10)常量指针

const int *p2 = &a;//常量指针
  1. 常量指针,指针的指向不可以改变,指针自身可以改变
  2. const修饰的是p2的指向,所以不能通过修改指针来改变a的值,但可以改变p2自身的值
  3. 简单来讲,就是*p2 不能改变,p2可以
#include <stdio.h>
int main()
{
    int a = 10;

    const int *p2 = &a;
    *p2 = 100;// err 不能给const修饰的变量重新赋值

    printf("a = %d\n",a);
    printf("p2 = %d\n",*p2);


    return 0;
}

这是一个错误的程序,常量指针不能通过修改指针来改变另一个的值

 虽然常量指针不能通过修改指针来改变另一个变量的值,但可以改变自身的值啊

示例 test61.c

#include <stdio.h>
int main()
{
    int a = 10;
    int b = 20;

    const int *p2 = &a;
    // *p2 = 100;//err  尝试修改常量指针的指向

    printf("p2 = %d\n",*p2);
    p2 = &b;// 指针自身
    printf("p2 = %d\n",*p2);
    printf("a = %d\n",a);


    return 0;
}          

运行效果

接着我们再看这行代码

int* const p3 = &a; // 指针常量

这行代码是什么?答案是指针常量

(11)指针常量

int* const p3 = &a; // 指针常量
  1. 指针常量,指针的指向可以改变,指针的自身不能改变
  2. const修饰的是指针自身(p3),可以通过修改指针的指向来修改a的值,但是p3自身不能改变
  3. 简单来讲,就是*p3 能改变,p3不可以改变
#include <stdio.h>
int main()
{
    int a = 10;
    int b = 20;

    int* const p3 = &a;// 指针常量
    p3 = &b;//err 尝试给const 修饰的变量赋值

    printf("a = %d\n",a);
    printf("p3 = %d\n",*p3);

    return 0;
}

 这是一个错误的程序,指针常量的自身不能改变

 指针常量,指针的自身不能改变,但是指针的指向可以改变啊

示例 test62.c

#include <stdio.h>
int main()
{
    int a = 10;
    int b = 20;

    int* const p3 = &a;// 指针常量
    //p3 = &b;//err 尝试给const 修饰的变量赋值
    *p3 = 520;// 指针的指向  可以通过修改指针的指向来修改a的值

    printf("a = %d\n",a);
    printf("p3 = %d\n",*p3);

    return 0;
}

运行效果

接着我们再看这行代码

const int* const p4 = &a;

这行代码是什么?我也不知道它怎么称呼,只知道二者都被const修饰,p4和 *p4 均不可改变

int a = 10;
int b =20;
const int* const p4 = &a;
//*p4 = 100;//err
//p4 = &b;//err

如果这篇文章你认真从头看到这里的话,那么你肯定是一个比较有耐心的人,本篇文章难度不大,看完之后,相信你能够对指针有进一步的理解。这篇文章写了很久,也付出了很多时间,下了不少功夫,如果觉得本篇文章的内容对你有所帮助的话,可以点赞,收藏,加关注支持一下。

猜你喜欢

转载自blog.csdn.net/XiaoQiu__/article/details/126514553