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 = #
*pi = 2;
printf("%d\n", num);
return 0;
}
但显然已经违反常变量的语法规则,需要限制指针的行动-->const修饰指针变量
*修饰指针变量
三种写法
1.const 放在*的左边
如const int* pi = # int const * pi = #
语法规则:指针指向的内容不能通过指针来改变,但是指针变量本身的值是可以改
*pi=?; 错误 pi=&n;正确
2.const 放在*右边
如int* const pi = #
语法规则: 指针指向的内容能通过指针来改变,但是指针变量本身的值是不可改
*pi=?; 正确 pi=&n;错误
3.const 放在*的左右两边
如const int* const pi = #
语法规则:由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 = #
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)
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修饰)!
写成下方这样程序会崩溃会报错(写入权限访问冲突):
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.二维数组传参的本质
*回顾
*打印
写法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
大致的框架
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