C语言——指针进阶

目录

前言

一. 字符指针

二. 指针数组

三. 数组指针

3.1 何为数组指针?

3.2 &数组名与数组名

四. 数组参数、指针参数

4.1 一维数组传参

4.2 二维数组传参

4.3 一级指针传参

4.4 二级指针传参

五. 函数指针

六. 函数指针数组

七. 指向函数指针数组的指针

八. 回调函数

九. 指针与数组的笔试题

十. 指针笔试题


前言

各位小伙伴,大家好久不见,想来大家都是刚刚开学,祝大家新学期新气象,咱们的编程技术更上一层楼,咱们话不多说,直接步入今天的正题,指针进阶。

在之前的指针初阶之中我们已经大概的对于指针有了些许了解,例如:

1. 指针也是一种变量

2. 指针变量的大小根据不同的环境为4/8个字节(32位平台/68位平台)

3. 指针变量大小一样,但是拥有不同的类型,解引用时可操作的权限

4. 指针的运算

那今天,我们就需要更加深入的了解指针。

一. 字符指针

我们经常会看到对于一个字符串或者字符数组的传址输入,那我们就先来介绍字符指针(char*),一般我们对于字符指针的使用都是如下:

int main()
{
	char s = 'w';
	char* ps = &s;
	//此时*pc='w'
	return 0;
}

又或者

int main()
{
	char* str = "hello world";
	printf("%s\n",str);
	return 0;
}

当我们运行第二个代码的时候,我们会发现我们打印出来的并不是一个字符“h”而是整个字符串“hello world”,很多小伙伴就会非常的疑惑?难道我们在str这个地址存入了一整句话吗?

但实际上我们并不能存入如此多的字符进去,我们实际上只是将首元素的地址存入str当中,如下图

 而根据这个知识点就很有可能出一道面试题:

#include <stdio.h>
int main()
{
	char arr1[] = "hello world";
	char arr2[] = "hello world";
	const char* arr3 = "hello world";
	const char* arr4 = "hello world";
	if (arr1 == arr2)
	{
		printf("arr1 and arr2 are same.\n");
	}
	else
	{
		printf("arr1 and arr2 are not same.\n");
	}
	if (arr3 == arr4)
	{
		printf("arr3 and arr4 are same.\n");
	}
	else
	{
		printf("arr3 and arr4 are not same.\n");
	}
	return 0;
}

很多小伙伴一看这个题目便会一头雾水,这上面四个不是一模一样吗?肯定会打印两个same,但是结果并不是这样

我们发现arr1和arr2并不是我们预料之中的结果,为什么呢?arr1和arr2是两个不同的字符串,开辟的空间并不相同,可能一个的地址在0x11223344,而另一个可能就在0x55667788,if语句中的arr1和arrr2是两个首元素地址,那他们两个肯定不会相等,但是arr3和arr4两者是去出hello world这个字符串的地址,他们并不会开辟一个新的空间,所以arr3与arr4的地址就是同一个hello world的首元素地址。

二. 指针数组

数组我们在之前早就已经学过,而数组指针顾名思义自然就是存放指针的数组,就例如

int *arr1[10];//存放10个整型指针的数组
char *arr2[5];//存放5个字符型指针的数组
char **arr3[3];//存放3个二级指针的数组

三. 数组指针

3.1 何为数组指针?

数组指针就是指向数组的指针吗?指向整个数组吗?答案是数组指针也是指针,下面根据一个例子来学习分辨什么是数组指针?

int main()
{
	int* arr[10];//这是指针数组
	int(*arr)[10];//那这个加上一个括号是要干嘛?
	return 0;
}

第一个正是我们学习的指针数组,那第二个加个括号岂不是多此一举,并非如此,数学当中加减乘除我们是不是有不同的优先级,那在我们计算机当中也是有不同的优先级,

根据优先级来看,我们发现[]操作符的优先级远高于*操作符,那arr就一定会先与[]结合,我们为了避免他们先结合,我们可以将*和arr用()括起来这样arr就会先与*结合,说明arr是一个指针变量,指向的是一个大小为10个整型的数组,所以arr是一个数组指针。

3.2 &数组名与数组名

数组的名字到底代表的是什么意思呢?

int main()
{
	int arr[10];
	printf("%p\n", arr);
	printf("%p\n", &arr);
	return 0;
}

 打印结果:

我们发现数组名与取地址数组打印出来的结果是一样的,难道这意味着数组名和取地址数组名一样吗?再看下面的这段代码:

#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的地址一样,但是当分别加一的时候一个加了4位而另一个却加了40位,由此我们发现其实&arrarr,虽然值是一样的,但是意义应该不一样的。 实际上: &arr 表示的是数组的地址,而不是数组首元素的地址。上面&arr 的类型是: int(*)[10] ,是一种数组指针类型 数组的地址+1,跳过整个数组的大小,所以 &arr+1 相对于 &arr 的差值是40

四. 数组参数、指针参数

当我们编写一个自定义函数的时候,我们一定会传入参数,当我们需要传入数组参数或者指针参数时我们又应该如何解决呢?

4.1 一维数组传参

#include <stdio.h>
void test(int arr[])//ok?
{}
void test(int arr[10])//ok?
{}
void test(int *arr)//ok?
{}
void test(int **arr)//ok?
{}
int main()
{
    int arr[10] = {0};
    test(arr);
    return 0;
}

上述四种方式都是可以数组传参的,第一个与第二个直接传入数组,第三个传入数组地址,第四个则是将数组名看作为一个一级指针地址,数组的每个元素都是一个int*类型,传入参数是一个数组名,那么我们就可以使用一个二级指针来接收,即是int **arr。

4.2 二维数组传参

void test(int arr[3][5])//ok?
{}
void test(int arr[][])//ok?
{}
void test(int arr[][5])//ok?
{}
//总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。
//因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
//这样才方便运算。
void test(int *arr)//ok?
{}
void test(int* arr[5])//ok?
{}
void test(int (*arr)[5])//ok?
{}
void test(int **arr)//ok?
{}
int main()
{
 int arr[3][5] = {0};
 test(arr);
}

其实无论是几维数组我们都可以将第一维省略,所以第二种并不可以这样传参,下面第一种和最后一种与一维数组其实道理相差不大,但是第二种与第三种就涉及了上面刚刚讲到的,我们传入的是数组指针并不是指针数组。

4.3 一级指针传参

我们先运用指针传参写出一个printf函数:

#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;
}

当我们将函数参数设置为一级指针的时候,这个函数可以接受什么样的参数呢?

例如:数组首元素地址,数组名,字符串首元素地址很多很多参数都可以传入一级指针。

4.4 二级指针传参

#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 print(int a)
{
	printf("%d\n", a);
}
int main()
{
	int a = 10;
	print(a);
	printf("%p\n", print);
	printf("%p\n", &print);
	return 0;
}

 我们发现函数也是有自己的指针的(即地址),那当我们想要保存函数的地址有应当如何去实现呢?

void test()
{
 printf("hehe\n");
}
//下面pfun1和pfun2哪个有能力存放test函数的地址?
void (*pfun1)();
void *pfun2();

答案是pfun1是可以存放的,pfun1与*优先结合,意味着是一个指针,指向的是一个函数,无参数,返回类型是void类型。在C陷阱与缺陷中有两段非常有意思的代码:

//代码1
(*(void (*)())0)();
//代码2
void (*signal(int , void(*)(int)))(int);

代码1咱们一步一步进行解读:

1.void (*)()--函数指针类型;


2.(void (*)())0--对0进行强制类型转换,被解释为一个函数地 址;


3.(*(void (*)())0)--对0地址进行了解引用操作;


4.(*(void (*)())0)()--调用0地址处的函数

代码2我们看上去十分的复杂但是我们只要稍加修改这个代码就会变得一目了然

typedef void(*pfun_t)(int);
pfun_t signal(int, pfun_t);

用typedef重新定义一个返回类型为void,参数类型是整型的函数指针,而代码二中就是一个函数指针中的一个参数也是一个函数指针,是不是十分的有趣。

六. 函数指针数组

上面我们讲了指针数组就是存放指针的数组,那函数指针数组莫不是就是存放函数指针的数组吗?但是又有一个问题那就是函数指针数组又该如何定义呢?下面我们一起来看看。

int (*parr1[10])();
int *parr2[10]();
int (*)() parr3[10];

正确的是parr1,parr1与[]操作符优先结合就是数组,那数组类型是什么?int (*) ()就是这个数组的元素,一个函数指针。

可能有小伙伴就要问了,这个函数指针数组都要把我绕昏了,这玩意有什么用处呢?

函数指针数组的用途:转移表,下面我们就来用计算器来举例:

       #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)
       {
比特就业课-专注IT大学生就业的精品课程 比特主页:https://m.cctalk.com/inst/
        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>
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)
     {
          printf( "*************************\n" );
          printf( " 1:add           2:sub \n" );
          printf( " 3:mul           4:div \n" );
          printf( "*************************\n" );
          printf( "请选择:" );
      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;
}

我们可以发现使用了函数指针数组使得代码看起来并没有那么冗杂。

七. 指向函数指针数组的指针

有的小伙伴看到这个地方就不禁非常的疑惑,最开始一个指针数组,后来来一个函数指针数组,现在又来一个函数指针数组指针,这在俄罗斯套娃呢?

指向函数指针数组的指针是一个 指针 指针指向一个 数组 ,数组的元素都是 函数指针
下面我们来看看如何定义这个指针。
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;
}

八. 回调函数

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

最后一个参数也就是一个函数,下面展示一下qsort函数的使用,并模拟实现qsort快排。

qsort函数的使用:

#include <stdio.h>
//qosrt函数的使用者得实现一个比较函数
int int_cmp(const void * p1, const void * p2)
{
  return (*( int *)p1 - *(int *) p2);
}
int main()
{
    int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
    int i = 0;
    
    qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof (int), int_cmp);
    for (i = 0; i< sizeof(arr) / sizeof(arr[0]); i++)
   {
       printf( "%d ", arr[i]);
   }
    printf("\n");
    return 0;
}

   使用qsort的方式自定义一个冒泡排序:

 

#include <stdio.h>
int int_cmp(const void * p1, const void * p2)
{
  return (*( int *)p1 - *(int *) p2);
}
void _swap(void *p1, void * p2, int size)
{
    int i = 0;
    for (i = 0; i< size; i++)
   {
        char tmp = *((char *)p1 + i);
       *(( char *)p1 + i) = *((char *) p2 + i);
       *(( char *)p2 + i) = tmp;
   }
}
void bubble(void *base, int count , int size, int(*cmp )(void *, void *))
{
    int i = 0;
    int j = 0;
    for (i = 0; i< count - 1; i++)
   {
       for (j = 0; j<count-i-1; j++)
       {
            if (cmp ((char *) base + j*size , (char *)base + (j + 1)*size) > 0)
           {
               _swap(( char *)base + j*size, (char *)base + (j + 1)*size, size);
           }
       }
   }
}
int main()
{
    int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
    //char *arr[] = {"aaaa","dddd","cccc","bbbb"};
    int i = 0;
    bubble(arr, sizeof(arr) / sizeof(arr[0]), sizeof (int), int_cmp);
    for (i = 0; i< sizeof(arr) / sizeof(arr[0]); i++)
   {
       printf( "%d ", arr[i]);
   }
    printf("\n");
    return 0;
}

注意:里面有一个void*类型,它并不是跟void一样的意义,它的含义是传入参数不知道是什么类型,可能是整型也有可能是字符型,因此这里才使用void*类型。

九. 指针与数组的笔试题

//一维数组
int a[] = {1,2,3,4};
printf("%d\n",sizeof(a));
printf("%d\n",sizeof(a+0));
printf("%d\n",sizeof(*a));
printf("%d\n",sizeof(a+1));
printf("%d\n",sizeof(a[1]));
printf("%d\n",sizeof(&a));
printf("%d\n",sizeof(*&a));
printf("%d\n",sizeof(&a+1));
printf("%d\n",sizeof(&a[0]));
printf("%d\n",sizeof(&a[0]+1));
//字符数组
char arr[] = {'a','b','c','d','e','f'};
printf("%d\n", sizeof(arr));
printf("%d\n", sizeof(arr+0));
printf("%d\n", sizeof(*arr));
printf("%d\n", sizeof(arr[1]));
printf("%d\n", sizeof(&arr));
printf("%d\n", sizeof(&arr+1));
printf("%d\n", sizeof(&arr[0]+1));
printf("%d\n", strlen(arr));
printf("%d\n", strlen(arr+0));
printf("%d\n", strlen(*arr));
printf("%d\n", strlen(arr[1]));
printf("%d\n", strlen(&arr));
printf("%d\n", strlen(&arr+1));
printf("%d\n", strlen(&arr[0]+1));
char arr[] = "abcdef";
printf("%d\n", sizeof(arr));
printf("%d\n", sizeof(arr+0));
printf("%d\n", sizeof(*arr));
printf("%d\n", sizeof(arr[1]));
printf("%d\n", sizeof(&arr));
printf("%d\n", sizeof(&arr+1));
printf("%d\n", sizeof(&arr[0]+1));
printf("%d\n", strlen(arr));
printf("%d\n", strlen(arr+0));
printf("%d\n", strlen(*arr));
printf("%d\n", strlen(arr[1]));
printf("%d\n", strlen(&arr));
printf("%d\n", strlen(&arr+1));
printf("%d\n", strlen(&arr[0]+1));
char *p = "abcdef";
printf("%d\n", sizeof(p));
printf("%d\n", sizeof(p+1));
printf("%d\n", sizeof(*p));
printf("%d\n", sizeof(p[0]));
printf("%d\n", sizeof(&p));
printf("%d\n", sizeof(&p+1));
printf("%d\n", sizeof(&p[0]+1));
printf("%d\n", strlen(p));
printf("%d\n", strlen(p+1));
printf("%d\n", strlen(*p));
printf("%d\n", strlen(p[0]));
printf("%d\n", strlen(&p));
printf("%d\n", strlen(&p+1));
printf("%d\n", strlen(&p[0]+1));
//二维数组
int a[3][4] = {0};
printf("%d\n",sizeof(a));
printf("%d\n",sizeof(a[0][0]));
printf("%d\n",sizeof(a[0]));
printf("%d\n",sizeof(a[0]+1));
printf("%d\n",sizeof(*(a[0]+1)));
printf("%d\n",sizeof(a+1));
printf("%d\n",sizeof(*(a+1)));
printf("%d\n",sizeof(&a[0]+1));
printf("%d\n",sizeof(*(&a[0]+1)));
printf("%d\n",sizeof(*a));
printf("%d\n",sizeof(a[3]));

 大家可以自己自己琢磨琢磨,我们会发现数组名的意义:

1. sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小。
2. &数组名,这里的数组名表示整个数组,取出的是整个数组的地址。
3. 除此之外所有的数组名都表示首元素的地址。

十. 指针笔试题

最后还有几道指针的笔试题,帮助我们巩固一下对于指针进阶的了解。

试题一:

int main()
{
    int a[5] = { 1, 2, 3, 4, 5 };
    int *ptr = (int *)(&a + 1);
    printf( "%d,%d", *(a + 1), *(ptr - 1));
    return 0;
}
//程序的结果是什么?

答案是2,5,因为一个*a就是1的地址,加上1就是a[1]的地址,指向的对象就是2,第二个ptr将&arr+1这个时候就指向a数组结尾的位置也就是a[4]的元素也就是5后面的一个元素,但是是未知的,减一,那指向的也就是5。

试题二:

int main()
{
    int a[5][5];
    int(*p)[4];
    p = a;
    printf( "%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);
    return 0;
}

答案是:FFFFFFFFFFFFFFFC,-4,

试题三:

int main()
{
 char *c[] = {"ENTER","NEW","POINT","FIRST"};
 char**cp[] = {c+3,c+2,c+1,c};
 char***cpp = cp;
 printf("%s\n", **++cpp);
 printf("%s\n", *--*++cpp+3);
 printf("%s\n", *cpp[-2]+3);
 printf("%s\n", cpp[-1][-1]+1);
 return 0;
}

答案是:POINT
ER
ST
EW

这就是所有内容,如果有不明白的地方,欢迎在评论区留言,咱们下次再见。

 

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Vex小摆子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值