指针进阶详解

C指针进阶

前言

初级指针中我们知道了一些关于指针的主题:

  1. 指针就是个变量,用来存放地址,地址唯一标识一块内存空间。
  2. 指针的大小是固定的4/8个字节(32位平台/64位平台)。
  3. 指针是有类型,指针的类型决定了指针的±整数的步长,指针解引用操作的时候的权限。
  4. 指针的运算

这个章节,我们继续探讨指针的高级主题。

在这里插入图片描述

字符指针

我们都知道在指针类型中我们知道一种指针类型char*

对于char*一般的使用方法一:

int main()
{
    
    
    char ch = 'w';
    char *pc = &ch;
    *pc = 'w';
    return 0;
}

使用方法二:

int main()
{
    
    
     const   char* pstr = "hello bit.";//这里是把一个字符串放到pstr指针变量里了吗?//常量字符串,存储在常量区,一旦创建好就不能修改
    //  *pstr = 'w';
    printf("%c\n", *pstr);
    printf("%s\n", pstr);

    return 0;
}

注意:常量字符串,存储在常量区,一旦创建好就不能修改

在这里插入图片描述

理论上字符串第一个字符被改为w,实际上代码无法正常运行,直接挂掉。所以避免被修改,我们const修饰字符指针,避免其被改。

在这里插入图片描述

用const修饰后,一旦试图修改直接报错,无法正常编译。

代码 const char* pstr = “hello bit.”;,特别让人认为是把字符串hello bit放到pstr里面去了,但是本质上是把字符串的首字符放到了pstr中。

在这里插入图片描述

上面代码的意思是把一个常量字符串的首字符 h 的地址存放到指针变量 pstr 中。

接下来看一道笔试题,再来理解理解

#include <stdio.h>
int main()
{
    
    
    char str1[] = "abcdef";
    char str2[] = "abcdef";
    const char* str3 = "abcdef";
    const char* str4 = "abcdef";
    if (str1 == str2)
        printf("str1 and str2 are same\n");
    else
        printf("str1 and str2 are not same\n");

    if (str3 == str4)
        printf("str3 and str4 are same\n");
    else
        printf("str3 and str4 are not same\n");

    return 0;
}

这里最终输出的是:

分析图

在这里插入图片描述

str1数组和str2数组会在内存分别开辟空间去存储“abcdef”,所以首元素地址肯定不一样。而字符串“abcdef”是常量字符串,只会在内存常量区存储一份,不会存储两份一模一样的字符串去浪费空间。所以str3和str4存储是相同的地址。

指针数组

我们都知道整型数组,字符数组等,整型数组是用来存储整型数据的数据,字符数组是用来存储字符的数组。

int arr[5];

char arr2[5];

在这里插入图片描述

数组arr可以存放5个元素,每个元素都是整型。数组arr2可以存放5个元素,每个元素都是字符型。数组的类型就是把变量名去掉,比如int arr[5],把arr去掉就是数组的类型(int [5])

在初阶指针章节我们也学了指针数组,指针数组是一个存放指针的数组。

int* arr1[5];

数组arr包含5个元素,每个元素都是整型指针,其数组类型为int *[5]

同理可知

char *arr2[4]; //一级字符指针的数组
char **arr3[5];//二级字符指针的数组

数组arr2包含5个字符型的一级指针;数组arr3包含5个二级指针

我们都知道整型指针是指向整型的指针,字符指针是指向字符的指针,那么数组指针当是指向数组的指针,既然数组指针也是指针,那么它的使用也是存储数组的地址,然后通过解引用去操作。我们已经知道数组指针是指针了,那么如何下出数组指针呢?

我们先来看两行代码,再去分析如何书写

int *p1[10];
int (*p2)[10];
//p1, p2分别是什么?

p1毫无疑问是指针数组,那么p2其实就是我们要写出来的数组指针

为什么是数组指针是这样书写的?

首先p先和*结合,说明p是一个指针变量,然后指向一个大小为10个整型的数组,所以p是一个指针,指针,指向一个数组,叫数组指针.其类型用上述求数组类型的方法来获得,也就是去掉变量名,就可得出 int( * )[10].

注意:[]的优先级要高于 * 号的,所以必须加上()来保证p先和*结合。

如果我们不加括号就会变成int * p[10],这是因为[]的优先级要高于*号的,所以p与[]结合构成了一个数组,而数组的元素是int *,就不是数组指针而是指针数组了。

&数组名VS数组名

arr 和 &arr 分别是啥? 我们知道arr是数组名,数组名表示数组首元素的地址。 那&arr数组名到底是啥?&数组名表示得到整个数组,还有数组名参与sizeof运算时数组名不是首元素的地址而是整个数组的地址。除了&数组名和参与sizeof运算以为,其它情况下数组名就是数组首元素的地址。

代码验证

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

结果

单独打印这两个地址可以看到值是相同,但是它们意义确差个十万八千里。我们不妨给地址加1,再去看看地址的变化。

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

因为代码中的arr是首元素的地址,&arr虽然是整个数组的地址,但是和首元素的地址是一样的,所以打印出来也是一样的。当给首地址加一可以发现只能跳过一个元素指向下一个元素,而数组的地址加1直接跳过整个数组指向后面的空间

在这里插入图片描述
数组指针的使用

既然数组指针指向的是数组,那数组指针中存放的应该是数组的地址。

看代码:

#include <stdio.h>
int main()
{
    
    
    int arr[10] = {
    
     1,2,3,4,5,6,7,8,9,0 };
    int(*p)[10] = &arr;//把数组arr的地址赋值给数组指针变量p
    for (int i = 0; i < 10; i++)
    {
    
    
        printf("%d ", *(*p+i)); //打印数组中元素的值
    }
    return 0;
}

一个数组指针的使用:

#include <stdio.h>
void print_arr1(int arr[3][5], int row, int col)
{
    
    
    int i = 0;
    for (i = 0; i < row; i++)
    {
    
    
        int j = 0;
        for (j = 0; j < col; j++)
        {
    
    
            printf("%d ", arr[i][j]);
        }
        printf("\n");
    }
}

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

int main()
{
    
    
    int arr[3][5] = {
    
     1,2,3,4,5,6,7,8,9,10 };
    print_arr1(arr, 3, 5);
    print_arr2(arr, 3, 5);
    return 0;
}

上文已经介绍&地址数组名和数组名的区别,我们可以知道这里我们传入的数组名数二维数组的首元素地址是二维数组的第一行,因为二维数组是以一维数组为元素的数组,3行5列的二维数组也就意味着这个数组里面有三个一维数组,每个一维数组的元素是5个。

图解二维数组

在这里插入图片描述

数组名作为参数传递的时候有两种接受方式,第一种既然人模狗样的看似传了个数组过去,那么我们也可以用一个相应的数组来接收,注意我们虽然用数组来接收但并不代表就创建了一个数组,因为传过来是地址,理所当然要用指针接收,用数组接收只是便于理解,其次用数组接收VS会帮我们处理,就是把形参数组处理为指针。第二种就是用数组指针来接收,还原其本来面目。

数组参数、指针参数

在写代码的时候难免要把【数组】或者【指针】传给函数,那函数的参数该如何设计呢?上面的代码我们使用了简单的数组传参并设计函数的形参,接下来就来就继续介绍如何设计函数形参的设计。

一维数组传参

#include <stdio.h>
void test(int arr[])//数组接收
{
    
    }
void test(int arr[10])//数组接收
{
    
    }
void test(int *arr)//指针接收
{
    
    }
void test2(int *arr[20])//数组接收
{
    
    }
void test2(int **arr)//指针接收
{
    
    }
int main()
{
    
    
 int arr[10] = {
    
    0};
 int *arr2[20] = {
    
    0};
 test(arr);
 test2(arr2);
}

数组在这分为整型数组和整型指针数组

整型数组:

向函数传入数组名时有一下两种接收方式

  1. 数组传参数组接收,函数形参部分可创建一个数组去接收,其数组大小可以随意指定。
  2. 指针接收,数组创参本质上传的是首元素的地址,所以函数形参部分可以用指针接收。

整型指针数组:

向函数传入数组名时有一下两种接收方式

  1. 数组传参数组接收,函数形参部分可创建一个整型指针数组去接收,其数组大小可以随意指定。

  2. 指针接收,因为整型指针数组的元素类型是int *为一级指针,所以我们用二级指针接收。

    二维数组传参

void test(int arr[3][5])
{
    
    }
void test(int arr[][]) //不可以,第一个[]里面的内容可以省略,第二个不行,第二个一旦省略数组就无法确定
{
    
    }
void test(int arr[][5])
{
    
    }
void test(int *arr)//不行
{
    
    }
void test(int* arr[5])//这是个整型指针数组,无法存储
{
    
    }
void test(int (*arr)[5])//ok?
{
    
    }
void test(int **arr)//二级指针是用来接收一级指针,传过来的是二维数组第一行的地址
{
    
    }
int main()
{
    
    
 int arr[3][5] = {
    
    0};
 test(arr);
}

二维数组:

  1. 二维数组传参用二维数组接收
  2. 指针接收,二维数组的首元素的地址是二维数组第一行的地址,所以用指针接收。

一级指针传参

#include <stdio.h>
void print(int *p, int sz)
{
    
    
 int i = 0;
 for(i=0; i<sz; i++)
 {
    
    
 printf("%d\n", *(p+i));
 }
}
int main()
{
    
    
 int arr[10] = {
    
    1,2,3,4,5,6,7,8,9};
 int *p = arr;
 int sz = sizeof(arr)/sizeof(arr[0]);
 //一级指针p,传给函数
 print(p, sz);
 return 0;
}

一级指针传一级指针接收

二级指针传参

#include <stdio.h>
void test(int** ptr)
{
    
    
 printf("num = %d\n", **ptr); 
}
int main()
{
    
    
 int n = 10;
 int*p = &n;
 int **pp = &p;
 test(pp);
 test(&p);
 return 0;
}

二级指针传二级指针接收

一句话就是传啥接啥。

函数指针

我们已经学过了整型指针,字符指针,数组指针等。类比一下函数指针自然也是指针,既然是指针就是有指向的目标。函数指针根据我们之前所学过的指针可以推断出函数指针所指向的对象是函数。

首先看一段代码:

#include <stdio.h>
void test()
{
    
    
 printf("hehe\n");
}
int main()
{
    
    
 printf("%p\n", test);
 printf("%p\n", &test);
 return 0;
}

输出结果
在这里插入图片描述

%p是打印地址,由图打印函数名和&函数名,其结果都是一样的,其意义也一样。如果我们想要保存函数的地址又该如何保存呢?我们可以把它放到指针里去那么函数指针又该如何书写?

下面我们看代码:

int Add(int x, int y)
{
    
    
	return x + y;
}
//下面pfun1和pfun2哪个有能力存放test函数的地址?
int (*p)(int ,int );
int *p2();

首先,能给存储地址,就要求p或者p2是指针,那哪个是指针? 答案是:

pf可以存放。p先和*结合,说明p是指针,指针指向的是一个函数,指向的函数有两个 数,返回值类型为int。

函数指针的书写和使用

函数指针的创建:

首先p是个指针需要和*结合,而()的优先级高于 * ,所以用()把 *p括起来,P所指向的内容是函数的参数个数及其类型,返回值为int ,此时可以得出int ( * p)(int ,int ),是函数指针,存储的地址可以是&数组名和数组名因为这两者意义一样,去掉变量名就是类型,int ( * )(int ,int ),

使用函数指针的调用函数:

  • 函数指针存放的是函数的地址,我们对它解引用便可以使用函数。
int ret=(*p)(3,4);
  • 给函数指针赋值的时候直接赋数组名,使用的时候 就可以不用解引用就能使用函数
int (*p)(int ,int )=Add;
int ret3 = p(3, 4);
printf("%d\n", ret3);

注意:*没有实际含义只是说明p是指针。这就意味着无论是否给指针是&数组名赋值还是直接用数组名赋值,在使用函数指针调用函数时都可以不解引用,就可以找到函数。

在这里插入图片描述

函数指针数组

从函数指针数组之个名字就可以知道,这是个数组。数组是存放相同类型数据的容器,我们之前已经学了指针数组:
比如:

int *arr[10];
//数组的每个元素是int*
//

以此类推:
函数指针数组就是把函数的地址存到一个数组中,那这个数组就叫函数指针数组,那函数指针的数组如何定义呢?

int (*parr1[10])();

首先parr1会和[]结合,说明parr1是数组,数组的每个元素是int ( * )( )类型的函数指针,其类型就是int ( * [10]) ( )

函数指针数组的用途:转移表

例子:(计算器)

版本一

#include <stdio.h>
int add(int a, int b)
{
    
    
 return a + b;
}
int sub(int a, int b)
{
    
    
 return a - b;
}
int mul(int a, int b)
{
    
    
 return a*b;
}
int div(int a, int b)
{
    
    
 return a / b;
}
int main()
{
    
    
 int x, y;
 int input = 1;
    int ret = 0;
    do
   {
    
    
        printf( "*************************\n" );
        printf( " 1:add           2:sub \n" );
        printf( " 3:mul           4:div \n" );
        printf( "*************************\n" );
        printf( "请选择:" );
        scanf( "%d", &input);
        switch (input)
       {
    
    
       case 1:
              printf( "输入操作数:" );
              scanf( "%d %d", &x, &y);
              ret = add(x, y);
              printf( "ret = %d\n", ret);
              break;
        case 2:
              printf( "输入操作数:" );
              scanf( "%d %d", &x, &y);
              ret = sub(x, y);
              printf( "ret = %d\n", ret);
              break;
        case 3:
              printf( "输入操作数:" );
              scanf( "%d %d", &x, &y);
              ret = mul(x, y);
              printf( "ret = %d\n", ret);
              break;
        case 4:
              printf( "输入操作数:" );
              scanf( "%d %d", &x, &y);
              ret = div(x, y);
              printf( "ret = %d\n", ret);
              break;
        case 0:
                printf("退出程序\n");
 breark;
        default:
              printf( "选择错误\n" );
              break;
       }
 } while (input);
    
    return 0;
}

版本二(使用函数指针数组的实现)

#include <stdio.h>
void menu()
{
    
    
   		  printf( "*************************\n" );
          printf( " 1:add           2:sub \n" );
          printf( " 3:mul           4:div \n" );
          printf( "*************************\n" );
          printf( "请选择:" );
}

int add(int a, int b)
{
    
    
           return a + b;
}
int sub(int a, int b)
{
    
    
           return a - b;
}
int mul(int a, int b)
{
    
    
           return a*b;
}
int div(int a, int b)
{
    
    
           return a / b;
}

int main()
{
    
    
     int x, y;
     int input = 1;
     int ret = 0;
     int(*p[5])(int x, int y) = {
    
     0, add, sub, mul, div }; //转移表
     while (input)
     {
    
    
          menu();
      scanf( "%d", &input);
          if ((input <= 4 && input >= 1))
         {
    
    
          printf( "输入操作数:" );
              scanf( "%d %d", &x, &y);
              ret = (*p[input])(x, y);
         }
          else
               printf( "输入有误\n" );
          printf( "ret = %d\n", ret);
     }
      return 0;
}

版本一的计算器,使用switch语句,我们可以发现每个case后面的语句从功能的角度来看啊几乎是一模一样,这样造成了大量的代码重复,基于这点我们用函数指针数组的知识设计出了版本二的计算器。可以看到函数指针数组参放了一系列的参数和返回值一样的数组名。即函数指针,第一个数组元素存放的是0也就是NULL,第一个元素存放1,只是为了迎合菜单的设计。如此变解决了代码重复。

指向函数指针数组的指针

指向函数指针数组的指针是一个 指针 指针指向一个 数组 ,数组的元素都是 函数指针

看代码

void test(const char* str)
{
    
    
 printf("%s\n", str);
}
int main()
{
    
    
 //函数指针pfun
 void (*pfun)(const char*) = test;
 //函数指针的数组pfunArr
 void (*pfunArr[5])(const char* str);
 pfunArr[0] = test;
 //指向函数指针数组pfunArr的指针ppfunArr
 void (*(*ppfunArr)[5])(const char*) = &pfunArr;
 return 0;
}

说明:首先pfunArr与* 结合,说明pfunArr是指针,其次pfunArr与[5]j结合,说明pfunArr是一个数组,该数组有5个元素。pfunArr指向的是一个数组,其元素类型为数组除去[]就是数组中每个元素的类型。指针变量去掉变量名便是指针所指向内容的类型。

回调函数

回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个 函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数 的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进 行响应。

举个例子

#include<stdio.h>
int Add(int x,int y)
{
    
    
	return x + y;
}

text(int  (*p)(int, int))
{
    
    
	p(1,2);
}
int main()
{
    
    

	int ret=text(Add);
	printf("%d\n", ret);
	return 0;
}

此代码中的Add函数,不是直接调用而是把该函数作为参数,传递给另一个函数,另一个函数则用函数指针调用,被调用的函数被称为回调函数,即Add是回调函数。

回调函数的使用-qsort函数的使用:

qsort函数是C语言的库函数,是基于快速排序算法实现的一个排序。

void qsort (void* base, size_t num, size_t size, int (*compar)(const void*,const void*));

qsor函数第一个参数是待排序的内容的起始位置,第二个参数是待排序内容的元素个数,第三个参数是一个元素的字节大小,第四个参数是一个比较函数的指针,返回值是void,因为void可以接收任意类型。第四个参数是一个函数指针,此函数指向的两个参数均为const void*,返回值为int *,当参数e1小于e2时返回<0的数,当参数e1大于e2时返回>0的数,当参数e1等于e2时返回0的。

#include<stdio.h>
#include<string.h>
//数组排序
int Add(int x,int y)
{
    
    
	return x + y;
}

text(int  (*p)(int, int))
{
    
    
	p(1,2);
}
int main()
{
    
    

	int ret=text(Add);
	printf("%d\n", ret);
	return 0;
}
int compare(const void * e1,const void *e2)
{
    
    
	return *((int *)e1)- *((int*)e2);//void *强制类型转换成int *
}
void prints(int arr[],int len)
{
    
    
	for (int i = 0; i<len; i++)
	{
    
    
		printf("%d ", arr[i]);
	}
}

int main()
{
    
    

	int arr[] = {
    
     1,0,4,6,8,9,12,67,23 };
	int len = sizeof(arr) / sizeof(arr[0]);
	qsort(arr, len, 4, compare);
	prints(arr, len);
	return 0;
}

//结构体排序
struct Student
{
    
    
	int age;
	char name[20];
	int score;
};

void prints(struct Student  *arr,int len)
{
    
    
	for (int i = 0; i<len; i++)
	{
    
    
			printf("%s ", (arr + i)->name);
			printf("%d ", (arr + i)->age);
			printf("%d ", (arr + i)->score);
			printf("\n");
	}
}
int compare(const void * e1,const void *e2) //为结构体数组中的字符排序
{
    
    
	return strcmp(((struct Student*)e1)->name, ((struct Student*)e1)->name);
}

void  text()
{
    
    
	struct Student arr[3] = {
    
     {
    
    19,"叶秋涵",99},{
    
    17,"叶子秋",45},{
    
    18,"叶知秋",100} };
	int len = sizeof(arr) / sizeof(arr[0]);
	qsort(arr, len, sizeof(arr[0]), compare);
	prints(&arr,len);
}

int main()
{
    
    
	text();
	return 0;
}

strcmp函数

strcmp比较两个字符串的大小,一个字符一个字符比较,按ASCLL码比较。标准规定:

  1. 第一个字符串大于第二个字符串,则返回大于0的数字
  2. 第一个字符串等于第二个字符串,则返回0
  3. 第一个字符串小于第二个字符串,则返回小于0的数字
结束语:

有人考试靠实力,有人考试靠视力,而我就不一样了,全靠想象力。
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_60308100/article/details/123495729