15 指针 · 其三 · 进阶

目录

一、字符指针变量

(一)字符指针与字符串数组区别

二、数组指针变量

(一)数组指针介绍

(二)数组指针初始化

(三)数组指针的使用

三、二维数组传参的本质

四、函数指针变量

(一)函数指针介绍

1、函数的地址

2、函数指针的写法

(二)函数指针的使用

(三)两段有趣的代码

(四)typedef

五、函数指针数组

六、转移表

七、回调函数

(一)qsort 函数实践回调函数

1、qsort 函数介绍

2、qsort 函数使用

3、qsort 函数模拟实现


一、字符指针变量

        在指针类型中,有一类指针为字符指针:【char *】

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

    return 0;
}

(一)字符指针与字符串数组区别

        字符指针指向的字符串不可修改,而数组中的字符串则可以;

        如下代码演示:

int main()
{
    char a[20] = {"I like pizza!"};
    char* b = "I like pizza!";

    *a = 'A';
    *b = 'B';    

    return 0;
}

        在代码运行时,会显示错误:

        字符串放在数组里面,每个字符都作为一个元素,可以修改;

        字符串直接赋给字符指针时,是作为【常量字符串】的,而【常量字符串】不可以修改,既然不期望被修改,那就用const来限制字符指针变量,放在*的左边,这样后续若是不小心修改了该字符串,那么就会在书写的时候报错,而不是运行时报错,加强了代码的健壮性;演示如下:

        ② 【字符串数组名】赋给【字符指针变量】的是首字符地址,【字符串】直接赋给【字符指针变量】也是首字符的地址

        演示代码如下:

int main()
{
    char a[20] = { "I like pizza!" };

    char* pa = a;
    char* b = "I like pizza!";

    printf("指针变量 pa = %p\n", pa);
    printf("指针变量 b  = %p\n", b);

    return 0;
}

       在X86环境下的结果为:

        总结:

        【字符串数组名】赋值给【字符指针变量】,值为首字符的地址;

        【字符串】直接赋给【字符指针变量】时也一样,赋予的是首字符的地址,并不是整个字符串都赋过去;

        另:

        用【%s】打印字符串的时候,只需要提供首字符的地址即可,代码演示如下:

int main()
{
    char a[20] = { "I like pizza!" };

    char* pa = a;
    char* b = "I like pizza!";

    printf("%s\n", pa);
    printf("%s\n", a);
    printf("%s\n", b);

    return 0;
}

        输出结果为:

        ③ 【字符串数组】与【字符串常量】的存储空间不同

        如下代码演示:

int main()
{
    char str1[] = "hello bit.";
    char str2[] = "hello bit.";
    const char* str3 = "hello bit.";
    const char* str4 = "hello bit.";

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

        代码结果如下:

        解析:

        这里 str3 和 str4 指向的是一个同一个常量字符串;因为 C/C++ 会把【常量字符串】存储到单独的⼀个内存区域(只读数据区,可以用但是不可以改);

        当几个指针指向同一个字符串的时候,他们实际会指向同一块内存; 

        但是用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块;所以 str1 和str2 不同,str3 和 str4 相同

二、数组指针变量

(一)数组指针介绍

        数组指针——数组的指针;

        指针数组——存放指针的数组。

        类型类比:

        ① 整型指针——指向整型的指针:

int* p = &a;

                这里 * 表示 p 是指针,指向 int 型的变量;

        ② 数组指针——指向数组的指针:

int arr[20] = {0};

int (*p2)[20] = &arr;

                数组中,数组名为:arr,类型为 int [20],所以指针应该指向 int [20] 类型;

                指针应该先写(*p2),让*与p2结合,表达p2是个指针,再写指向的类型;

                所以为:int(*p2)[20] ,去掉指针名字,数组指针的类型就是int(*)[20];
               

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

        区别:

        int (*p) [10] —— 存放整个数组地址的指针变量 p;
        int*  p [10] —— 每个元素都存放整型指针的数组 

(二)数组指针初始化

        数组指针是用来存放数组的地址的,数组的地址获取方法为:【& 数组名】,表达的是整个数组的首地址

int arr[10] = {0};

int (*p)[10] = &arr;

        在调试中看到,&arr 和 p 的类型都是相同的,验证了数组指针的获取方法就是【& 数组名】

        注:

                在整型数组中,数组名为 arr ,长度为20的数组中,arr 和 &arr[0] 表示的都是 int* 类型,而 &arr 表示的是 int(*)[20] 类型,虽然前面三者的值都一样,但是类型不同,这也是为什么 arr 和 &arr[0] + 1 跳过四个字节,而 &arr+1 跳过整个数组的原因。
 

(三)数组指针的使用

        因为数组指针里面存放的是整个数组,所以解引之后的【*p】表示的是整个数组,想用到里面的元素的用法:*p [ i ] 即可,不过这个使用非常繁杂,一般不会这样用
 

三、二维数组传参的本质

        一维数组传参:

        数组名是首元素的地址,一维数组在传参的时候,其实传递的是首元素的地址,函数的参数可以写成地址,也可以写成指针;

        二维数组传参:本质是传递第一行整个一维数组的地址

        二维数组的数组名也是数组首元素的地址,而二维数组可以理解为一维数组里面的元素是数组,那么首元素就是第一个整个一维数组,后面的元素以此类推;

        所以,二维数组的数组名就是第一行整个一维数组的地址,首元素是一整个一维数组,所以传过去的是整个一维数组的地址,类型是数组指针,形参要用指针来接收。例如:int 类型的二维数组,一行有5列,那么形参就是:int(*数组名)[ 5 ]

        示例代码如下:

void test(int(*p)[5], int r, int c)
{
    for (int i = 0; i < r; i++)
    {
        for (int j = 0; j < c; j++)
        {
            printf("%d ", *(*(p + i) + j));
        }
        printf("\n");
    }
}

int main()
{
    int arr[3][5] = { {1,2,3,4,5}, {2,3,4,5,6},{3,4,5,6,7} };
    test(arr, 3, 5);

    return 0;
}

        不管是一维数组还是二维数组传参,形参可以写成数组,也可以写成指针

四、函数指针变量

        

(一)函数指针介绍

1、函数的地址

        如下代码实例:

void test()
{
    printf("hehe\n");
}

int main()
{
    printf("test:  %p\n", test);
    printf("&test: %p\n", &test);

    return 0;
}

        结果如下:

        【&函数名】和【函数名】都是函数的地址,没有区别
 

2、函数指针的写法

        函数指针的写法与数组指针的写法都非常类似,都是先让 * 与名字结合,再指向所要指向的类型,如下代码:

void test()
{
 printf("hehe\n");
}
void (*pf1)() = &test;
void (*pf2)()= test;

int Add(int x, int y)
{
 return x+y;
}
int(*pf3)(int, int) = Add;
int(*pf3)(int x, int y) = &Add;//x和y写上或者省略都是可以的

        在监视窗口中,test 函数与 Add 函数的类型如下:

        两个函数中,函数名为:test 与 Add,类型为 void () 与 int(int, int),所以指针应该指向 void () 与 int(int, int)类型;

        指针应该先写(*指针名),让*与指针名结合,表达指针名是个指针,再写指向的类型;

        所以为:【void(*pf1)( )】,【int(*pf2)(int, int)】,去掉指针名字,数组指针的类型就是【void(*)( )】,【int(*)(int, int)】

        注意:以Add函数为例子,如果 *Add不加(),则指针名会与函数的形参列表结合,变成函数名,前面的int会与*结合,变成返回类型是int*;
 

(二)函数指针的使用

        如下代码示例:

int Add(int x, int y)
{
    return x + y;
}

int main()
{
    int(*pf3)(int, int) = Add;

    printf("%d\n", (*pf3)(2, 3));
    printf("%d\n", pf3(3, 5));

    return 0;
}

        结果为:

        总结:

        pf3 == &Add == Add == *pf3

        因为函数名就是表示函数的地址,而指针变量里面存放的也是函数的地址;

        当直接调用函数的时候,是【函数名(实参,...)】,由于 *函数指针名 == 函数名,所以也可以写成【*函数指针名(实参,...)】,而指针变量里面存放的就是函数地址,也可以直接通过指针变量名来引用【指针变量名(实参,...)】,所以通过指针来引用变量时,写不写指针变量名前面的解引用的*都行,只是摆设,只要写了*号,就要和指针变量名一起带上()

(三)两段有趣的代码

        代码一:

(*(void (*)())0)();

        代码意思:
        ① 整体看来是:一次函数调用,调用地址为0的函数;
        ② 地址为0的函数:返回类型是void,没有参数
    
        代码分析:
        void(*)( ) 整体表示的是一个函数指针类型,指向的该函数返回类型是void,无参数;
        (void(*)( ))0的意思是,把0强制转化为这个函数指针类型,也就是0变成了地址;
        *((void(*)( ))0)是解引用这个地址,得到该地址的函数名;
        *((void(*)( ))0)()意思是使用这个函数,且形参列表为空;
        解引用的 * 可以省略,因为【指针名 == &函数名== 函数名 == *指针名】

        代码二:

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

        上面的代码是一次函数声明函数的名字叫:signal

        signal函数的参数有2个:
        第一个参数的类型是int
        第二个参数的类型是一种函数指针【void(*)(int)】,该函数指针指向的函数的参数是int,返回类型是void,signal函数的返回类型也是一个函数指针,类型是void(*)(int)

(四)typedef

        typedef 是用来类型重命名的,可以将复杂的类型,简单化;

        用法:

        typedef 旧类型 新类型,类似于变量的声明:变量类型 变量名,若是函数指针或者函数指针,重命名要放在*后面

        如下代码所示:

typedef unsigned int uint;
//将unsigned int 重命名为uint

typedef int* ptr_t;
//将int* 重命名为ptr_t

        数组指针与函数指针如下:

typedef int(*parr_t)[5]; 
//新的类型名必须在*的右边

typedef void(*pfun_t)(int);
//新的类型名必须在*的右边

        简化上面的代码二,如下:

typedef void(*pfv_t)(int);

pfv_t signal(int, pfv_t);

五、函数指针数组

        每个元素都存放函数指针的数组;

        例如,存放类型为 void(*)(int) 的函数指针在数组 parr 中;

 void(*parr[10])(int)

        首先parr与[10]结合,表示为数组,数组的类型为:void(*)(int);

        访问数组里面的函数:数组名[ i ](形参列表),选好函数,输入参数

        

六、转移表

        转移表:实现了一种跳转的效果,所以叫转移表;

        使用函数指针数组模拟实现计算器:

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

void menu()
{
    printf("*******************************\n");
    printf("****** 1.加法 *** 2.减法 ******\n");
    printf("****** 3.乘法 *** 4.除法 ******\n");
    printf("****** 0.退出 *****************\n");
    printf("*******************************\n");
}

int Calc(int i, int(*parr[5])(int, int))
{
    int x = 0;
    int y = 0;
    printf("请输入两个操作数:");
    scanf("%d %d", &x, &y);
    return parr[i](x, y);
}

int main()
{
    int input = 0;
    int ret = 0;
    int(*parr[5])(int, int) = {0, add, sub, mul, div};

    do
    {
        menu();
        printf("请输入想要进行的操作:");
        scanf("%d", &input);
        if (input >= 1 && input <= 4)
        {
            ret = Calc(input, parr);
            printf("计算的结果为:%d\n", ret);
        }
        else if (input == 0)
        {
            printf("退出计算器\n");
            break;
        }
        else
        {
            printf("输入错误,请输入菜单选项\n");
        }
    } while (input);

    return 0;
}

七、回调函数

        回调函数就是一个通过函数指针调用的函数。

        如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,被调用的函数就是回调函数。

        回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。(不是由接收函数指针的函数在接收参数时就直接调用了,而是由该函数中某些事件或条件再调用该函数)

(一)qsort 函数实践回调函数

1、qsort 函数介绍

        qsort 是C语言中的一个库函数,这个函数是用来对数据进行排序的,对任意类型的数据都能进行排序,是冒泡排序的一个改进版,底层是快速排序的思想;

        不同类型数组中的两两元素比较是由差异的,qsort就是使用【程序员实现数组中两两元素的比较而编写的函数】来进行排序,例如:程序员A——想使用qsort函数排序整型数据,就要提供【比较两个整型的函数】,程序员B——想使用qsort函数按结构体中的字符数据进行排序,就要提供【比较两个字符的函数】;

        虽然qsort函数限定了比较函数的两个参数是【泛型指针类型】的,但程序员使用时,已经明确知道在比较上面类型的数据了,那就将泛型指针进行强制转化成已知类型的指针,再解引用,就可以进行比较了;

2、qsort 函数使用

        qsort 的使用代码如下:

void test()
{
	int arr[] = {8, 7, 6, 4, 9, 5, 1, 3, 2};//需要排序的数组

	//库函数中qsort函数所要求传入的参数是:数组首元素地址,数组长度,元素大小,用户自定义的比较方法
	int arr_sz = sizeof(arr) / sizeof(arr[0]);
	int ele_sz = sizeof(arr[0]);

	qsort(arr, arr_sz, ele_sz, fun1);//排序

	for (int i = 0; i < arr_sz; i++)
	{
		printf("%d ", arr[i]);
	}
}


int main()
{
	test();//该函数用来测试排序整型数组

	return 0;
}

        输出的结果为:

        注意:整数比较的巧用:把需要比较的两个整数相减即可,与比较函数所期望的放回置是符合的;qsort默认是升序,若想降序,把比较函数的两个比较数据交换就行;
 

        使用 qsort 函数排序结构体类型,代码如下:

#include <string.h>

struct Stu //学⽣
{
	char name[20];//名字
	int age;//年龄
};

int sort_by_name(const void* p1, const void* p2)//此为用户实现
{
	return strcmp(((struct Stu*)p1)->name, ((struct Stu*)p2)->name); 
    //结构体类型要设置成全局变量类型
	//强制转化成struct Stu*类型,要用()括起来,不然会报错
}

void test2()
{
	
	struct Stu s[] = { {"zhangsan", 20}, {"lisi", 30}, {"wangwu", 15} };
	int sz = sizeof(s) / sizeof(s[0]);
	qsort(s, sz, sizeof(s[0]), sort_by_name);
}

int main()
{
	test2();//该函数用来测试排序结构体中的字符
	return 0;
}

        知识补充:结构体成员访问操作符:【 . 】和【->】:

        结构体指针中,访问结构体变量中的值的方法:
        ① 可以 【*(结构体指针变量名). 结构体成员名】
        ② 还可以直接使用指针:【结构体指针变量名 -> 结构体成员名】,其他类型强制转化成结构体指针类型要使用()括起来,才使用【->】

        注意:结构体的定义要全局定义!

3、qsort 函数模拟实现

        通过test函数得知,使用qsort函数要提供:数组首元素地址,数组长度,元素大小,用户自定义的比较方法;因为在qsort 函数设计当初,不知道我们要排序的时什么数组的首地址,所以要设计成 void* 类型来接收数组的首元素

        以冒泡排序为基础来模拟 qsort 函数,则要实现①排序过程,实现②元素两两交换时的大小比较,以及传递参数给比较函数,和实现③元素的两两交换

        以冒泡排序为基础来模拟 qsort 函数来排序整型,代码如下:

int fun1(const void* p1, const void* p2)//此为用户实现
{
	return (*(int*)p1 - *(int*)p2);
}


void swap(char* p1, char* p2, int ele_sz)//此为制造商实现
{
	for (int i = 0; i < ele_sz; i++)
	{
		char emp = *p1;
		*p1 = *p2;
		*p2 = emp;

		p1++;
		p2++;
	}
}

void my_qsort(void* base, int arr_sz, int ele_sz, int(*fcmp)(void*, void*))//此为制造商实现
{
	//使用的是冒泡排序为基础的排序,先确定趟数,为arr_sz - 1;然后确定一趟要比几次,arr_sz - 1 - i;
	for (int i = 0; i < arr_sz - 1; i++)
	{
		for (int j = 0; j < arr_sz - 1 - i; j++)
		{
			//比较大小,再确定交换
			if (fcmp((char*)base + j * ele_sz, (char*)base + (j + 1) * ele_sz) > 0)//元素大小为4个字节,元素转化为字符大小才能准确跳过4个字节
			{
				swap((char*)base + j * ele_sz, (char*)base + (j + 1) * ele_sz, ele_sz);//因为是字符类型,所以要每个字节每个字节地交换
			}
		}
	}
}


void test()
{
	int arr[] = {8, 7, 6, 4, 9, 5, 1, 3, 2};//需要排序的数组

	//库函数中qsort函数所要求传入的参数是:数组首元素地址,数组长度,元素大小,用户自定义的比较方法
	int arr_sz = sizeof(arr) / sizeof(arr[0]);
	int ele_sz = sizeof(arr[0]);

	//qsort(arr, arr_sz, ele_sz, fun1);//排序
	my_qsort(arr, arr_sz, ele_sz, fun1);

	for (int i = 0; i < arr_sz; i++)
	{
		printf("%d ", arr[i]);
	}
}


int main()
{
	//使用冒泡排序的方法模拟 qsort 函数的实现

	test();//该函数用来测试排序整型数组
	//通过test函数得知,使用qsort函数要提供:数组首元素地址,数组长度,元素大小,用户自定义的比较方法
	//而模拟 qsort 函数,则要实现①排序过程,实现②元素两两交换时的大小比较,以及传递参数给比较函数,和实现③元素的两两交换

	return 0;
}

        提供元素大小是为了知道【从首元素开始,跳过多少个字节才到下一个元素】;

        在整型数组的排序中,需要传递【比较的两元素地址】给模拟的qsort,我们只知道【首地址和一个元素的大小】,我们可以把首地址强制转化成 char* 类型,然后再+宽度(每个元素的大小),就可以实现跳过一个元素,又因为需要不断的跳过字节,所以可以和变量 j 联系在一起, 【j * width】来表示需要跳过的宽度;

        在交换元素时,可以确定数据的类型是 char* 类型,在交换时,一个字节一个字节地交换;

        该模拟实现qsort使用了泛型编程,泛型编程:任意类型都基本满足,通常使用【void*】和回调函数。
 


        以上内容仅供分析,若有错误,请多指正

  • 15
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值