首页 > 其他分享 >★♛★指针(重难点)合集

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

时间:2024-08-29 14:56:05浏览次数:9  
标签:arr return int void 重难点 printf 合集 指针

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表知(显示十进制):

<<<<<<<<<

标签:arr,return,int,void,重难点,printf,合集,指针
From: https://blog.csdn.net/2401_85828611/article/details/141678008

相关文章

  • WEB渗透Win提权篇-提权工具合集
      提权工具合集包(免费分享): 夸克网盘分享 往期文章WEB渗透Win提权篇-提权工具合集-CSDN博客WEB渗透Win提权篇-RDP&Firewall-CSDN博客WEB渗透Win提权篇-MSSQL-CSDN博客WEB渗透Win提权篇-MYSQL-udf-CSDN博客WEB渗透Win提权篇-AccountSpoofing-CSDN博客WEB渗透Win提权篇......
  • C语言涉及问题(文件IO与数组和指针)
    一、文件IO相关1、标准出错、输入、输出三者的缓冲机制是什么?标准出错(stderr):属于不缓冲机制,数据直接写入设备标准输入(stdin)和标准输出(stdout):属于行缓冲和全缓冲,行缓冲时需要用'\n'分隔,全缓冲是缓冲区满才会写入或者输出。冲刷缓冲函数:fflush();无论是如果想将全缓冲......
  • 【专题】2024年中国AI人工智能基础数据服务研究报告合集PDF分享(附原数据表)
    原文链接:https://tecdat.cn/?p=37516随着人工智能技术的迅猛发展,AI基础数据服务行业迎来了前所未有的发展机遇。报告合集显示,2023年中国AI基础数据服务市场规模达到45亿元,且未来五年复合增长率有望达到30.4%。多模态大模型、长文本处理能力提升以及大模型小型化技术成为A......
  • 全国30多个省、市、自治区2001-2022年统计年鉴大合集(全新整理)
    文章目录数据下载地址数据指标说明项目备注数据下载地址数据下载地址点击这里下载数据数据指标说明目前已有30个省份更新至2022年(安徽、北京、福建、云南、浙江、重庆、香港、台湾、天津、广东、广西、贵州、海南、湖北、湖南、吉林、江苏、辽宁、内蒙古、宁夏、......
  • 初识C语言指针(5)
    目录1.回调函数2.qsort函数2.1qsort函数的基本参数2.2qsort函数的使用2.3qsort排序结构体类型数据结语1.回调函数什么是回调函数呢?回调函数就是⼀个通过函数指针调⽤的函数。如果你把函数的指针(地址)作为参数传递给另⼀个函数,当这个指针被⽤来调⽤其所指向的......
  • 指针(二):数组指针
    目录数组名的理解数组整个地址与数组首元素地址区别指针的形式访问数组冒泡排序二级指针指针数组指针数组模拟二维数组数组名的理解在介绍数组指针之前先通过一段代码了解一下数组名的本质是什么。#include<stdio.h>intmain(){ intarr[]={1,2,3,4,5};......
  • 指针(三):函数指针
    目录函数指针函数的地址函数指针结构函数指针数组了解函数指针数组函数指针数组结构简易计算器函数指针数组优化计算器函数指针函数的地址函数指针,也就是存放函数地址的变量,有人会问,函数也会有地址吗?我们用一个代码来验证一下吧。#include<stdio.h>voidtest(......
  • C++智能指针
    1.为什么需要智能指针大家来看下面这段程序我们new了两个arraydoubleDivision(inta,intb){ //当b==0时抛出异常 if(b==0) { throw"Divisionbyzerocondition!"; } return(double)a/(double)b;}voidFunc(){ int*array1=newint[10]; int*......
  • 【效率提升工具推荐】AI编程工具合集
    AI编程工具是指那些专门为开发和训练人工智能模型而设计的工具和框架。这些工具可以帮助开发者更高效地构建、训练和部署机器学习和深度学习模型。以下是一些常用的AI编程工具及其特点:1.TensorFlow简介:由Google开发,是最流行的开源机器学习框架之一。特点:支持广泛的机器学......
  • 深入理解指针(1)
    1.内存和地址1.1内存我们知道CPU(中央处理器)在处理数据的时候,需要的数据在内存中读取,处理后的数据也会放回内存中。那么如何提高内存空间的管理呢?其实也就是把内存分为一个个内存单元,每个内存单元的大小是一个字节,其中一个字节的大小相当于8个比特位(bit)。每一个内存单......