★♛★指针(重难点)合集

1.介绍地址

内存:所有程序的运行在内存中

用Cheat Engine查看任意程序的内存(16进制):

显示大量的数据

想要定位某个数字 ,需要知道地址(类比二维坐标)

如F8的地址为00BCB900+08(偏移量),所以是00BCB908(偏移)

ctrl+G

则有

内存单元的说明:

打开计算器

点三道杠,选程序员

显然一个F8占用8bit(1111 1000)是一个byte

规定:一个内存单元是一个字节(F8),分配一个地址(00BCB908)

2.介绍&

&a:取变量a在内存中的地址(实际上取的是a的第一个字节(低位)的地址)

如int a=0x12345678 &a取的是78的存储地址

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
 int main()
{
	int a = 3;
	printf("%p", &a);
	return 0;
}

介绍指针:

格式 数据类型* 指针变量名称=&变量名称; //指针变量:存放指针(地址)的变量

指针常量:具体的地址 如0x00F85580

(*不可以省略)

这个*是解应用操作符,数据类型* 表示专门存储地址==专门存储指针,“数据类型*”称为指针变量的类型

拆解指针变量的类型:

char ch='a';
char* pch=&ch;

理解char*的含义:

拆成两部分

*:说明pch是指针变量

char:说明pch指向的对象ch是char类型的

所以建议命名指针变量的格式为“p+指针类型的首字母”

3.介绍解引用操作符(*)

只要拿到了地址(指针),就可以通过地址(指针)找到地址(指针)指向的对象

所以可以用解引用操作符来间接访问

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
 int main()
{
	 int a = 0;
	 int* pa = &a;//pa储存着a的地址
	 *pa = 1;//*解引用操作,*pa即通过pa的地址来找到a
	 //*pa=1;等同a=1;
	 printf("%d", a);
	 return 0;
}

pa向a(pa:pointer a)

结果是1

***总结:指针自身存放地址即指出地址(内存单元的编号 == 地址 == 指针)***

4.指针变量的大小

观察下列代码产生的结果并思考原因:

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
 int main()
{
	 printf("%d", sizeof(int*));
	 printf("%d", sizeof(long*));
	 printf("%d", sizeof(long long*));
	 printf("%d", sizeof(char*));
	 printf("%d", sizeof(float*));
	 printf("%d", sizeof(double*));
	 printf("%d", sizeof(short*));
	 return 0;
}

结果全是4-->4byte=32bit(32位选的是x86) 

如果是64位结果全是8

原因:

不同数据类型的指针的大小是相同的,因为指针存放地址,指针的大小取决于地址的大小

x86下打开内存查看:

00F85580-->两个十六进制字符代表1个字节-->地址一共4个字节

x64下打开内存查看:

000000AD5A7FF704-->两个十六进制字符代表1个字节-->地址一共8个字节

总结:

• 32位(x86)平台下地址是32个bit位,指针变量大小是4个字节
• 64位(x64)平台下地址是64个bit位,指针变量大小是8个字节
• 注意指针变量的大小和类型是无关的,只要指针类型的变量,在相同的平台下,大小都是相同的

疑问:指针变量的大小和类型无关,只要是指针变量,在同一个平台下,大小都是一样的,为什么还要有各种各样的指针类型呢?


 5.疑问解答:指针的解引用

观察下列代码产生的现象

#include <stdio.h>
int main()
{
  int n = 0x12345678;//十六进制存储
  int* pi = &n;
  *pi = 0;
  return 0;
}


改为char

#include <stdio.h>
int main()
{
  int n = 0x12345678;//十六进制存储
  char* pi = &n;
  *pi = 0;
  return 0;
}

改为short

#include <stdio.h>
int main()
{
  int n = 0x12345678;//十六进制存储
  short* pi = &n;
  *pi = 0;
  return 0;
}

 

发现:char* 的指针解引用就只能访问1个字节,而short*的指针的解引用能访问2个字节,而 int* 的指针的解引用就能访问4个字节(x64、x86下的结果一样)

总结:指针的类型决定了,对指针解引用的时候有多大的权限(一次能操作几个字节)

 6.指针+或-整数

#include <stdio.h>
int main()
{
  int n = 0x12345678;//十六进制存储
  int* pi = &n;
  short* ps = &n;
  char* pc = &n; 
  printf("&n=%p\n",&n);
  printf("pi+1=%p\n",pi+1);
  printf("ps+1=%p\n",ps+1);
  printf("pc+1=%p\n",pc+1);
  return 0;
}

发现:char* 类型的指针变量+1跳过1个字节, int* 类型的指针变量+1跳过了4个字节,short* 类型的指针变量+1跳过2个字节(同理+2,-1,-2……+n,-n)

总结:指针的类型决定了指针向前或者向后走一步有多大(距离)

7.特殊类型void* 指针

*定义:无具体类型的指针(或者叫泛型指针)

*注意:这种类型的指针可以用来接受任意类型地址,但是也有局限性:不能直接进行指针的+-整数和解引用的运算(*pv=? 错误 pv+1 错误),除非强制类型转换( *(int*)pv=200 )

#include <stdio.h>
int main()
{
  int n = 0x12345678;//十六进制存储
  short* ps = &n;
  return 0;
}

运行后会报警告:

但如果用:

#include <stdio.h>
int main()
{
  int n = 0x12345678;//十六进制存储
  void* pv = &n;
  return 0;
}

则没有问题

注:一般void* 类型的指针是使用在函数参数的部分,用来接收不同类型数据的地址,这样的设计可以实现泛型编程的效果

 8.const 修饰指针

const 全称 constant adj.不变的

*修饰普通变量

#include <stdio.h>
int main()
{
	const int num = 0;
	num = 20;
	printf("%d\n", num);
    return 0;
}

这样写会报错

说明const修饰的值不可改变

注:在C语言中,这里的num是常变量,num的本质还是变量,因为有const修饰,编译器在语法上不允许修改这个变量;而在C++语言中,这里的num就是常量

如果要强行改变,用指针

#include <stdio.h>
int main()
{
	const int num = 0;
	int* pi = &num;
	*pi = 2;
	printf("%d\n", num);
    return 0;
}

 

但显然已经违反常变量的语法规则,需要限制指针的行动-->const修饰指针变量

*修饰指针变量

三种写法

1.const 放在*的左边

如const int* pi = &num; int const * pi = &num;

 

 语法规则:指针指向的内容不能通过指针来改变,但是指针变量本身的值是可以改

*pi=?; 错误        pi=&n;正确

2.const 放在*右边

如int* const pi = &num;

语法规则: 指针指向的内容能通过指针来改变,但是指针变量本身的值是不可改

*pi=?; 正确        pi=&n;错误

3.const 放在*的左右两边

如const int* const pi = &num;

语法规则:由1,2推, 指针指向的内容不能能通过指针来改变,且是指针变量本身的值是不可改

9.指针运算

*指针+或-整数

37.【C语言】指针(重难点)(B)中已提到一些内容

练习:因为数组在内存中连续存放,所以可以用指针打印数组

#include <stdio.h>
int main()
{
	int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
    int*  pi = &arr[0];
    int length=sizeof(arr)/sizeof(arr[0]);
	for (int i=0;i<length;i++)
    {
		printf("%d ", *(pi+i));//注意pi不变
    }
    return 0;
}

*指针-指针(即地址-地址)

大地址-小地址 和 小地址-大地址 ,注意有正负

#include <stdio.h>
int main()
{
    int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
    printf("%d",&arr[8] - &arr);
    return 0;
}

 

总结:当两个指针指向同一个空间时,(指针-指针)的绝对值==指针之间的元素个数

进一步思考:

 求字符串长度:

1.strlen函数

strlen(数组); 统计\0之前的元素个数

#include <stdio.h>
int main()
{
    char arr[] = { "asdfghjk" };
    size_t result=strlen(arr);
    printf("%d", result);
    return 0;
}

具体见20.5.【C语言】求长度(sizeof和strlen)的两种方式

2.用指针

未遇到\0则指针++

#include <stdio.h>
int main()
{
    char arr[] = { "asdfghjk" };
    char* pi = &arr;//&数组名就是&数组名[0]
    int result = 0;
    //可以简写成while (*pi) \0的ASCI值是0
    while (*pi != '\0')//未到\0则继续循环
    {
        result++;
        pi++;//指针移动
    }
    printf("%d", result);
    return 0;
}

也可以改成指针-指针

printf("%d", pi-&arr);

  *指针(大小)关系运算 

可以用来打印数组

#include <stdio.h>
int main()
{
    int arr[] = { 0,1,2,3,4,5,6,7,8,9 };
    int* pi = &arr;
    int sz = sizeof(arr) / sizeof(arr[0]);
    while (pi < &arr[sz])//&arr[sz]越界不会产生影响
    {
        printf("%d ", *pi);
        pi++;
    }
    return 0;
}

10.野指针

*定义:指针指向的位置是不可知(随机的、没有明确限制的)

*案例

随机的(没有初始化):

int* p;
*p = 10;//非法访问

没有明确限制的(越界访问)

#include <stdio.h>
int main()
{
  int arr[10] = {0};
  int* p = &arr[0];
  for(int i=0; i<=11; i++)
  {         
    *(p++) = i;//当i>=10时,p就是野指针
  }
  return 0;
}

注意:*(p++) = i;先使用,后++ --> *p = i; p++;

*指针指向的空间释放

#include <stdio.h>
int* test()
{
  int n = 100;
  return &n;
}

int main()
{
  int* p = test();
  printf("%d\n", *p);
  return 0;
}

分析:test函数定义的n的生命周期介于test函数的 { 与 } 之间,一旦出test函数,n交换给操作系统,没有使用权限,即空间释放

11.野指针规避方法

*初始化

明确知道指针指向哪里就直接赋值地址,如果不知道指针应该指向哪里,给指针赋值NULL(空指针)

int num = 10;
int*p1 = &num;
int*p2 = NULL;
#define NULL ((void *)0) //把0转换为(void *)

内存查看p2的地址

注意:空指针不能访问(*p2=10;不允许)

*防止越界

int main()
{
  int arr[10] = {1,2,3,4,5,6,7,8,9,10};
  int* p = &arr[0];
  int i = 0;
  for(i=0; i<10; i++)
  {
    *(p++) = i;
  }
  //p已越界,把p置为NULL
  p  = NULL;
  return 0;
}

*指针变量不再使用时,及时置NULL,指针使用之前检查有效性

规则:只要是NULL指针就不去访问,同时使用指针之前可以判断指针是否为NULL

if (p2 != NULL)
{
  dosomething;
}

*避免返回局部变量的地址

见本篇:指针指向的空间释放

*assert断言

13.assert()  (assert v.断言)

*解释

assert(表达式); 如果表达式为真(返回值非零),继续执行;如果表达式为假(返回值为零),则报错

*作用

在运行时确保程序符合指定条件,如果不符合,就报错终止运行

使用前引用头文件

#include <assert.h>

*优点

报错是会在窗口显示没有通过的表达式,以及包含这个表达式的文件名和行号

#include <assert.h>
int main()
{
	int* p = NULL;
	assert(p != NULL);
	return 0;
}

 

*启用assert的开关

禁用assert:在#include <assert.h>前加#define NDEBUG,可以提高程序的运行效率,尽管assert(表达式)为假,但不会报错,继续执行,

启用assert:直接注释掉#define NDEBUG

注:NDEBUG为No Debug

14. 指针的使用和传址调用

29.【C语言】函数系列中 自定义函数 中的1.自定义详解和2.疑问解答

总结:

传址调用,可以让函数和主调函数之间建立真正的联系,在函数内部可以修改主调函数中的变量;传值调用,只是需要主调函数(main函数)中的变量值来实现计算

15.数组名的理解

*数组名就是数组首元素的地址

#include <stdio.h>
int main()
{
	int arr[3] = { 0 };
	printf("%p\n", &arr[0]);
	printf("%p\n", &arr);//&arr下面会讲例外
	printf("%p\n", arr);
	return 0;
}

*注意有两个例外

1.sizeof(数组名):这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节

详细见20.5.【C语言】求长度(sizeof和strlen)的两种方式

2.&数组名,这里的数组名表示整个数组,取出的是整个数组的地址(不等同于数组首元素的地址)

尽管上方三个打印的结果是一样的,但只要略微改动

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

 

C4-BC==8

显然&arr+1的+1 操作是跳过整个数组。因为是数组指针类型(见44.【C语言】指针(重难点)(G)

*使用指针访问一维数组

#include <stdio.h>
int main()
{
	int arr[] = { 0,1,2,3,6,3,9,5,2 };
	//计算数组的长度
	size_t sz = sizeof(arr) / sizeof(arr[0]);
	int* pi = &arr[0];
	for (int i = 0; i < sz; i++)
	{
		printf("%d ", *(pi + i));
	}
    return 0;
}

注意:*(pi+i)不能写成*pi+i!运算顺序不一样!

由于arr[i]编译器在执行时会转换为*(arr+i),且p == arr

一个大胆的猜想:arr[i]==i[arr],执行后结果正确(无论哪种写法都会转换为*(arr+i),[]只是操作符)

则有*(pi+i)==*(i+pi)==*(arr+i)==*(i+arr)==arr[i]==i[arr]==p[i]==i[p]

int*中*(pi+1)跳过4个字节

*一维数组的传参本质

#include <stdio.h>
void test(int arr[10])
{
	int sz2 = sizeof(arr) / sizeof(arr[0]);
	printf("sz2 = %d\n", sz2);
}
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int sz1 = sizeof(arr) / sizeof(arr[0]);
	printf("sz1 = %d\n", sz1);
	test(arr);
	return 0;
}

(左边是x64,右边是x86)

所以传参的时候并没有传递整个数组

以x86环境为例说明:sz2=sizeof(arr)/sizeof(arr[0]),sz2==1,即sizeof(arr)==1,回想:数组名是数组首元素的地址,那么在数组传参的时候,传递的是数组名

总结

           1.数组传参本质上是传递的是数组首元素的地址

           2.函数形参部分不会真实创建数组,那么就不需要数组的大大小

           3.函数形参部分应该用指针变量接收 int* p

           4.对于一维数组,形参既可以写成数组的方式,也可以写成指针变量的方式

以下写法均可以

void test(int arr[10])
void test(int arr[])
void test(int* arr[10])
void test(int* arr[])

16.二级指针

    *定义

之前讲的指针全是一级指针

int a = 1;
int *pa = &a;//一级指针

如果写成

int a = 1;
int *pa = &a;//pa是一级指针
int** ppa = &pa;//ppa是二级指针

二级指针定义:指向(存储)一级指针地址的指针

其实在这篇文章已经提前铺垫过了41.【C语言之外】聊聊Cheat Engine官方教程步骤6的思考

*演示

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
  int a = 1;
  int* pa = &a;//pa是一级指针
  int** ppa = &pa;//ppa是二级指针
  return 0;
}

命名的含义:ppa-->point pa-->point (point pa)

x86环境下,F11逐语句执行,运行到return 0;

打开监视窗口

现从&ppa查到&a

查看内存,输入&ppa

bc f8 8f 00 --倒着写-->00 8f f8 bc-->008ff8bc-->是&pa的结果

pa中存储着a的地址

c8 f8 8f 00--倒着写-->00 8f f8 c8-->是&a的结果

01 00 00 00--倒着写-->00 00 00 01-->是变量a的值

可以想到下面代码打印结果

printf("%d",**ppa);//二级指针需要两次解引用,因此要带两个*

就是a的值:1

17.三级以及多级指针

*三级指针的定义

类比二级指针,可以推出三级指针的定义:指向(存储)二级指针地址的指针(如int*** ppa=&ppa;)

*多级指针的定义

同理推出多级指针定义:n级指针是指向(存储)(n-1)级指针地址的指针

18.指针数组

*定义

回忆整型数组的定义:存放整型的数组

所以指针数组的定义:存放指针(地址)的数组

*代码

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
  int a = 1;
  int b = 2;
  int c = 3;
  int* arr[] = { &a,&b,&c };

  for (int i = 0; i < 3; i++)
  {
	  printf("%p\n", arr[i]);//打印指针数组
  }
  return 0;
}

 

DC-D0=C,D0-C4=C-->连续存放

19.指针数组模拟二维数组

*代码

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
	int arr1[] = { 1,2,3,4 };
	int arr2[] = { 5,6,7,8 };
	int arr3[] = { 9,10,11,12 };
	//数组名即代表数组首元素的地址
    int* arr[] = { arr1,arr2,arr3 };

  for (int i = 0; i < 3; i++)
  {
	  for (int j = 0; j < 4; j++)
	  {
		  printf("%d ", arr[i][j]);
	  }
	  printf("\n");
  }
  return 0;
}

*分析

上方代码并没有创建二维数组,但是却依靠指针数组模拟出二维数组,可以按照二维数组的形式(arr[i][j])来访问

注意:arr[i][j]等同于*(*(arr+i)+j)

二维数组文章:13.5.【C语言】二维数组

20.字符指针变量

*定义

指向字符的指针变量,用于存储字符在内存中的地址

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
	char a = 'm';
	char* pc = &a;
	return 0;
}

*简单说明

x86环境下,F11逐语句运行至return 0;

转到内存,输入&a

输入&pc

13 fc 6f 00--倒着写-->00 6f fc 13-->0x006ffc13是a的地址

   

*如果是字符串

回忆之前的内容

#include <stdio.h>
int main()
{
  char arr[]="abcdef";
  char *pc=arr;
  return 0;
}

arr数组存储着字符串,arr是数组首元素的地址

类比数组,如果是字符串

#include <stdio.h>
int main()
{
	char* pc = "abcdef";
	return 0;
}

x86环境下,F11逐语句运行至return 0;

转到内存,输入&pc

同理倒着写地址

地址框中输入0x00f07bcc 就找到了abcdef

 

arr数组是一段连续的空间,数组的内容是可以变的,所以常量字符串(char* pc = "abcdef";)(abcdef\0)也是一段连续的空间,常量字符串的内容不可以变(类比const修饰)!

const修饰见38.【C语言】指针(重难点)(C)

写成下方这样程序会崩溃会报错(写入权限访问冲突):

char* pc = "abcedf";
*pc = "abc";

*像数组一样指定访问常量字符串的字符

printf("%c","abcdef"[2]);

访问abcdef常量字符串的第二个字符c

类似于

char arr[]="abcdef";
printf("%c",arr[2]);

同理

printf("%s",pc);

类似于

char arr[]="abcdef";
printf("%s",arr);

*练习

求输出结果

#include <stdio.h>
int main()
{
	char str1[] = "abc";
	char str2[] = "abc";
	const char* str3 = "abc";
	const char* str4 = "abc";
	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;
}

分析:上方代码的==不是比较两个字符串的内容是否相等!比较字符串相等用的是strcmp函数

这里比的分别是数组首元素的地址和常量字符串首字符的地址

虽然两个数组的内容一样,但是abc字符串创建了两次,str1和str2存储的数组的首元素的地址不一样,所以not same

由于常量字符串具有内容不可以变的特点,因此abc没有必要创建两次所以str3和str4是same

下面调用内存说明

x86环境下,F11逐语句运行至return 0;

输入&str1

输入&str2

输入&str3

输入&str4

&str3和&str4都是cc 7b fa 00 ,指向地址0x00fa7bcc

20.数组指针变量 

 *定义

类比字符指针变量的定义,数组指针变量存放的是数组指针(地址)

*格式

   数据类型 (*指针变量名称)[数组元素个数]=&数组名

*例子

问题1:以下代码运行是否有错误?

#include <stdio.h>
int main()
{
	int arr1[5] = { 1,2,3,4,5 };
	int* p1 = &arr1;

    int arr2[5]={ 0 };
	int *p2[5] = &arr2;

    int arr3[5]={ 0 };
	int (*p3)[5] = &arr3;

    int arr4[5]={ 0 };
	int* (*p4)[5] = &arr4;
	return 0;
}

分析:p2的定义出了问题 ,由操作符运算优先级(见15.25【C语言】操作符的属性)可知:*p2[5]代表数组,不能为数组赋值&arr

[ ]的优先级要高于*号的,所以必须加上()来保证p先和*结合,表明p2是指向数组的指针变量(即数组指针变量),也就是定义p3的写法

问题2:p1,p3,p4的定义有什么区别

去除int *p2[5]=&arr;这一行后打开调试模式,x86环境下,F11逐语句运行至return 0;

监视arr,p1,p3,p4

打开内存

输入&p1

输入&p3

输入&p4

显然p1是整型指针,p3是数组指针(指向整个含5个int元素的数组的指针),p4是数组指针(指向含5个int*指针的数组的指针)

*利用指针打印

p-->&arr

*p-->*&arr即arr

21.二维数组传参的本质

*回顾

见13.5.【C语言】二维数组

*打印

写法1:实参,形参全是二维数组

#include <stdio.h>
void test(int a[3][5], int r, int c)
{
  int i = 0;
  int j = 0;
  for(i=0; i<r; i++)
  {
    for(j=0; j<c; j++)
    {
      printf("%d ", a[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;
}

写法2:指针

回顾:一维数组的数组名是首元素(单个,“0”维数组)的地址,可以推出:二维数组的数组名是首元素(第一行一维数组)的地址,同理三维数组的数组名是首元素(二维数组)的地址

所以可以用指针访问

对上方代码略加改动

#include <stdio.h>
void test(int (*p)[5], int r, int c)
{
    int i = 0;
    int j = 0;
    for (i = 0; i < r; i++)
    {
        for (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;
}

打印时p[i][j]有别的写法

   *(p+i)[j],*(*(p+i)+j)

总结:二维数组传参的本质:传递了地址,传递的是第一行这个一维数组的地址

22.函数指针变量

*创建

类比数组指针变量的定义:存放数组地址的指针变量,同理函数指针变量存放函数的地址

格式 函数的返回类型 (*指针变量的名称)(该函数的参数1,该函数的参数2,该函数的参数3……)=&函数名

*使用

打印函数地址很容易想到:&Add

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

int main()
{
    printf("%p", &Add);
    return 0;
}

其实printf("%p",Add);同样可以

&函数名 和 函数名 都能得到函数的地址

如果要定义函数指针变量

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

int main()
{
    int (*pf)(int x, int y) = &Add;
    int result = (*pf)(1, 2);
    printf("%d", result);
    return 0;
}

注:pf是point function的缩写

也可以写成 int (*pf) (int,int) = &Add;  int result = (pf)(1, 2);

对于函数指针来说,(*pf) 和 (pf )都可以

但绝不能写成 int *pf(int,int) = &Add;(优先级:() > *)

要把*pf括起来说明pf是指针变量

*两段代码

来自《C陷阱和缺陷》本书
1.

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

突破口是0

之前学过强制类型转换:(int)1.5 把1.5强制转换为int

在这里(……)0,括号内是void(*)()即函数指针类型,0作为函数的地址,这个函数没有参数,返回类型void

在void(*)()前加*是解引用(调用0地址处的函数)最后在(*(void (*)())0)后加()表示无参可传(这个函数没有参数)

2.一个函数的声明

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

突破口:函数的写法:function(类型1 参数1,类型2 参数2)

signal是函数名称signal(……)的内容是int和void(*)(int),第一个参数是int,第二个参数是void(*)(int) 即函数指针类型(该指针指向的函数:返回void,参数int)

把signal(int,void(*)(int))视作整体再看一遍

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

显然框架是void (* )(int);是signal函数的返回类型

补:如果知道typedef的命名规则更好理解(有关typedef见45.5【C语言】typedef

typedef void(* pf_t)(int) //用pf_t来表明void(*)(int)
pf_t signal(int,pf_t)

23.函数指针数组

*基本用法

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

int sub(int a,int b)
{
  return a-b;
}

int main()
{
  int (*padd)(int,int) = add;//&add的&可以省略
  int (*psub)(int,int) = sub;
  return 0;
}

add函数和sub函数的类型一样都是int (*)(int,int) 因此可以创建一个函数指针数组来简化输入

int (*pfarr[2])(int,int)={add,sub};

创建数组pfarr来存放add函数和sub函数的地址

*作用

写一个计算器,实现两个整数的+-*/

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

int sub(int x, int y)
{
	return x - y;
}

int mul(int x, int y)
{
	return x * y;
}

int div(int x, int y)
{
	return x / y;
}

int main()
{
	int input = 0;
	int a = 0;
	int b = 0;
	int result = 0;
	do
	{
		printf("\n0.exit 1.add 2.sub 3.mul 4.div\n请输入:");
		scanf("%d", &input);
		switch (input)
		{
		case 0:
			break;
		case 1:
		{
			printf("输入两个操作数:");
			scanf("%d %d", &a, &b);
			result = add(a, b);
			printf("%d", result);
			break;
		}
		case 2:
		{
			printf("输入两个操作数:");
			scanf("%d %d", &a, &b);
			result = sub(a, b);
			printf("%d", result);
			break;
		}
		case 3:
		{
			printf("输入两个操作数:");
			scanf("%d %d", &a, &b);
			result = mul(a, b);
			printf("%d", result);
			break;
		}
		case 4:
		{
			printf("输入两个操作数:");
			scanf("%d %d", &a, &b);
			result = div(a, b);
			printf("%d", result);
			break;
		}
		default:
			printf("重新输入!");
		}
	} while (input);
}

这样写代码会显得冗长 ,由于add,sub,mul,div的类型一样,可以用函数指针变量

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

int sub(int x, int y)
{
	return x - y;
}

int mul(int x, int y)
{
	return x * y;
}

int div(int x, int y)
{
	return x / y;
}

int main()
{
	int input = 0;
	int a = 0;
	int b = 0;
	int result = 0;
	int (*pfarr[5])(int, int) = { 0,add,sub,mul,div };
	do
	{
		printf("\n0.exit 1.add 2.sub 3.mul 4.div\n请输入:");
		scanf("%d", &input);
		if (input >= 1 && input <= 4)
		{
			result = pfarr[input](a, b);
			printf("%d", result);
		}
		else if (0 == input)
		{
			printf("退出");
		}
		else
		{
			printf("重新输入!");
		}
	} while (input);
}

所以函数指针数组的作用是精简代码

24.回调函数

*定义

  通过函数指针调用的函数

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

*基本用法

利用函数指针实现两个整数的加法

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

void function(int (*pf)(int, int))
{
	int result = pf(1, 2);
	printf("%d", result);
}

int main()
{
	function(add);
	return 0;
}

逻辑:

因此这篇文章45.【C语言】指针(重难点)(H) 里面的计算器题还有其他写法

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

int sub(int x, int y)
{
	return x - y;
}

int mul(int x, int y)
{
	return x * y;
}

int div(int x, int y)
{
	return x / y;
}

void calc(int(*pf)(int, int))
{
	int a = 0;
	int b = 0;
	int result = 0;
	printf("输入两个操作数:");
	scanf("%d %d", &a, &b);
	result = pf(a, b);
	printf("%d", result);
}

int main()
{
	int input = 0;

	do
	{
		printf("\n0.exit 1.add 2.sub 3.mul 4.div\n请输入:");
		scanf("%d", &input);
		switch (input)
		{
		case 0:
		{
			break;
		}
		case 1:
		{
			calc(add);
			break;
		}
		case 2:
		{
			calc(sub);
			break;
		}
		case 3:
		{
			calc(mul);
			break;
		}
		case 4:
		{
			calc(div);
			break;
		}
		default:
		{
			printf("重新输入!");
			break;
		}
	  }
	} while (input);
}

25.qsort库函数

quicksort 快速排序,底层是回调函数

之前写过42.【C语言】冒泡排序

可是排序时有局限性

  1.只能排整型,对于浮点数不可以

  2.不支持结构体、字符串等比较

  3.中间变量类型受限

但qsort能解决此问题

*简介

cplusplus网查询     点我跳转

定义如下:

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

一共4个参数

翻译:


base:指向在需要被排序数组中的第一个元素,转换为void*型(即base存放第一个元素的地址)

num:存放被base指向的需要被排序的数组元素的个数,size_t是无符号且必须的类型

( 其实:num等价为int sz = sizeof(arr)/sizeof(arr[0]) )

size:数组中每个元素的大小的单位为字节

compar(全称compare):指向一个比较两个元素的函数

该函数因比较两个元素被qsort反复调用,其应当遵循以下原型:

int compar (const void* p1, const void* p2);

把两个指针作为实参(转换成 const void* 型),compar函数(这个函数要自己创建)通过返回的值来确定元素的顺序(以稳定传递方式 备注:有const修饰)

返回值<0    p1指向的元素 < p2指向的元素

返回值=0    p1指向的元素 = p2指向的元素

返回值>0    p1指向的元素 > p2指向的元素

对于可以使用常规的关系运算符进行比较的类型,compar函数大致上长这样:

int compareMyType (const void * a, const void * b)
{
  if ( *(MyType*)a <  *(MyType*)b ) return -1;
  if ( *(MyType*)a == *(MyType*)b ) return 0;
  if ( *(MyType*)a >  *(MyType*)b ) return 1;
}

qsort函数没有返回值


*示例 1 比整数

#define CRT_NO_WARNINGNESS 1
#include <stdlib.h>
#include <stdio.h>
#include <stddef.h>
//qsort函数声明
void qsort(void* base,size_t num,size_t size,int (*compar)(const void*p1, const void*p2));

int cmp_int(const void* p1, const void* p2)
{
	if (*(int*)p1 > *(int*)p2)
		return 1;
	else if (*(int*)p1 < *(int*)p2)
		return -1;
	else
		return 0;
}

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

void func()
{
	int arr[10] = { 3,1,9,8,5,4,0,2,7,6 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	qsort(arr, sz, sizeof(arr[0]), cmp_int);
	print_arr(arr, sz);
}

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

注意:1.qsort默认排成升序

           2.用qsort函数要调用头文件 stdlib.h ,y=用size_t前要调用 stddef.h

           ★2.void*类型的指针不能解引用操作符,也不能+/-整数的操作,这种指针变量一般是用来存放地址的,使用之前要强制类型转换成想要的类型

即写成以下代码会报错

int cmp_int(const void* p1, const void* p2)
{
	if (*p1 > *p2)
		return 1;
	else if (*p1 < *p2)
		return -1;
	else
		return 0;
}

代码还有可以优化的地方

直接返回相减值

int cmp_int(const void* 1, const void* p2)
{
	return *(int*)p1 - *(int*)p2;
}

若要改成降序,反过来即可

return *(int*)p2- *(int*)p1;

*示例2 比结构体

     *比结构体中的数字

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

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

// cmp_stu_by_age 是用来比较2个结构体对象的
// 那么p1就指向一个结构体对象,p2也指向一个结构体对象
int cmp_stu_by_age(const void* p1, const void* p2)
{
    return (*(struct Stu*)p1).age - (*(struct Stu*)p2).age;
}

// 测试qsort函数来排序结构体数组
void func()
{
    struct Stu s[3] = { {"a person", 18}, {"c person", 25}, {"b person", 12} };
    int sz = sizeof(s) / sizeof(s[0]);

    // 调用qsort排序结构体数组
    qsort(s, sz, sizeof(s[0]), cmp_stu_by_age);

    // 打印排序后的结果
    for (int i = 0; i < sz; i++)
    {
        printf("Name: %s, Age: %d\n", s[i].name, s[i].age);
    }
}

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

年龄从小到大排列

注意:p1,p2要强制类型转换 (*(struct Stu*)p1)

反过来可以从大到小排列

return (*(struct Stu*)p2).age - (*(struct Stu*)p1).age;


     *比结构体中的字符串

上方代码稍作修改

#include <string.h>

int cmp_stu_by_name(const void* p1, const void* p2)
{
    return strcmp ((*(struct Stu*)p1).name , (*(struct Stu*)p2).name);
}

    qsort(s, sz, sizeof(s[0]), cmp_stu_by_name);

 

反过来:

return strcmp ((*(struct Stu*)p2).name , (*(struct Stu*)p1).name);

注意:字符串不能直接比较大小,要用strcmp  点击查看strcmp相关内容

***上方所有代码的cmp_int、cmp_stu_by_age、cmp_stu_by_name的函数的返回类型必须是int***

 26.自制排序函数

*分析

之前在42.【C语言】冒泡排序写过一个排序函数,可以将此自制一个类似qsort的函数

画圈的地方是需要修改的

#include <stddef.h>
void bubble_sort(void* base, size_t num,size_t width,int (*cmp)(const void*p1,const void*p2))

解释参数:

和qsort函数一样,

> base指针存放第一个元素的地址,由于不知道元素是什么类型,因此写void*

> num,width一定是unsigned类型,用size_t

> width 为一个元素所占的字节数,好让计算机知道元素的排布方式

> p1和p2为需要比较的两个元素,由于不知道元素是什么类型,因此写void*,又要确保稳定,用const修饰

> 比较完p1和p2后,要返回值,因此用int

下方图片摘自cplusplus网 点我跳转

大致的框架

void bubble_sort(void* base, size_t num, size_t width, int (*cmp)(const void* el, const void* e2))
{
	for (size_t i = 0; i < num - 1; i++)
	{
		for (size_t j = 0; j < num - 1 - i; j++)
		{
          //if判断元素
          //满足一定条件则交换,否则无动作
		}
	}
}

注:为保证i,j与width类型相同,写成size_t i = 0;  size_t j = 0;

详解if判断元素的写法 :

if (调用cmp函数) --> if (cmp()) --> if (cmp((char*)base + j * width, (char*)base + (j + 1) * width) > 0)  //由于数组的元素是连续排列的,因此需要知道每个元素具体占多少字节来访问每个元素,可以在base的基础上+j*width来移动,因此base是void*型,要强制类型转换为char*,才可一次只跳过一个字节 --写元素交换代码-->告诉两个元素的起始地址和交换的宽度-->swap((char*)base + j * width, (char*)base + (j + 1) * width, width);

*代码(一次一个字节交换)

void swap(char* p1, char* p2, size_t width)
{
	for (size_t i = 0; i < width; i++)
	{
		char tmp = *p1;
		*p1 = *p2;
		*p2 = tmp;
		p1++;
		p2++;
	}
}

 

元素1位置=base+j*width   元素2位置=base+(j+1)*width


完整代码:

#include <stddef.h>
#include <stdio.h>
void print_arr(int arr[ ], int sz)
{
	for (int i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
}

int cmp_int(const void* p1, const void* p2)
{
	return *(int*)p1 - *(int*)p2; //由小到大排序
}

void swap(char* p1, char* p2, size_t width)
{
	for (size_t i = 0; i < width; i++)
	{
		char tmp = *p1;
		*p1 = *p2;
		*p2 = tmp;
		p1++;
		p2++;
	}
}

void bubble_sort(void* base, size_t num, size_t width, int (*cmp)(const void* el, const void* e2))
{
	for (size_t i = 0; i < num - 1; i++)
	{
		for (size_t j = 0; j < num - 1 - i; j++)
		{
			if (cmp((char*)base + j * width, (char*)base + (j + 1) * width) > 0)
			{
				swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
			}
		}
	}
}

void int_func()
{
	int arr[10] = { 1,6,8,2,5,3,8,9,0,3 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	bubble_sort(arr, sz, sizeof(arr[0]), cmp_int);
	print_arr(arr, sz);
}

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

如果排序字符,只需要改动两处

int arr[10] = { 'a','c','r','1','@','q','m','+','!','3'};
printf("%c ", arr[i]);

 

查ASCII表知(显示十进制):

<<<<<<<<<

  • 9
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值