文章目录
- 指针概念的回顾
- 1.字符指针
- 1.1字符指针练习题
- 2.指针数组
- 3.数组指针
- 3.1数组指针的定义
- 3.2 &数组名和数组名的区别
- 3.3数组指针的使用
- 3.4一组简单的练习题
- 4.数组和指针作为函数参数
- 4.1一维数组传参
- **总结**
- 4.2二维数组传参
- **总结**
- 4.3一级指针传参
- **总结**
- 4.4二级指针传参
- **总结**
- 5.函数指针
- 5.1什么是函数指针?
- 5.2函数指针变量的定义和使用
- 两个经典的函数指针代码问题
- 6.函数指针数组
- 6.1函数指针数组的声明
- 6.2函数指针数组的使用
- 7.指向函数指针数组的指针
- 8.回调函数
- 8.1qosrt函数的介绍
- 8.2通过冒泡排序思想模拟实现qsort函数功能
指针概念的回顾
1、通常口头语中的指针其实是变量,即指针变量,用来存放地址。地址唯一标识一块内存空间。
2、指针的大小是固定的,在32位平台下是4个字节,在64位平台下是8个字节。
3、指针是有具体类型的,指针的类型决定了指针±整数的步长,也决定了指针解引用操作的访问权限。
1.字符指针
字符指针存放的是一个字符的地址。一个字符类型的变量占一个字节,但是字符指针是占4个字节空间的。在前面指针入门文章中,已经介绍了字符指针的概念,这里主要研究的是字符指针存放字符串首字符地址的问题。
#include<stdio.h>
int main()
{
const char* str2 = "hello world";
printf("%s\n",str2);
return 0;
}
有些不了解指针的人会很好奇,为什么一个4个字节大小的指针变量却能存下12个字节大小的的字符串呢?
这是因为这里的str2字符指针变量其实保存的是字符串首字符 ‘h’ 的地址。
1.1字符指针练习题
该代码的输出结果是什么呢?
#include <stdio.h>
int main()
{
char str1[] = "hello world";
char str2[] = "hello world";
const char* str3 = "hello world";
const char* str4 = "hello world";
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;
}
这里字符数组str1 和 str2存放的内容一致,但是str1数组 和 str2数组在内存中分别开辟了各自的空间。故str2 != str1 所以第一个打印的内容为 “str1 and str2 are not same”。两个const修饰的字符指针str3 和 str4各自指向了常量字符串的首字符 ‘h’ 的地址。所以str3 == str4。第二个打印的内容为 “str3 and str4 are same”。
字符指针指向的常量字符串要用const进行修饰。因为常量字符串是不可以被修改,如果不用const进行修饰,可能会导致对字符指针解引用操作后,访问权限冲突导致程序崩溃。
2.指针数组
在指针入门文中,已经介绍了指针数组的概念。这里也为下文介绍的数组指针做一下铺垫。
int* arr[5]; //一级指针数组
char** ch[5];//二级指针数组
指针数组本质是存放指针变量的数组。
3.数组指针
3.1数组指针的定义
int* arr[10]; //这是一个指针数组
int(*pi)[10]; //这是一个数组指针
//这里*和pi先结合,表示pi是一个指针变量。
//然后与int [10]结合表示该指针变量指向一个元素个数为10的整型数组。
//所以pi是一个数组指针。
3.2 &数组名和数组名的区别
#include<stdio.h>
int main()
{
int arr[10];
printf("%p\n",arr);
printf("%p\n",&arr);
return 0;
}
通过打印结果可以看到,两个结果是一样的,但是它们真的一样吗?
#include<stdio.h>
int main()
{
int arr[10];
printf("%p\n", arr);
printf("%p\n", arr+1);
printf("%p\n", &arr);
printf("%p\n", &arr+1);
return 0;
}
数组名arr,没有放在sizeof内部和&数组名中,表示的是数组首元素的地址,即为int类型的指针变量。+1跳过4个字节。&arr表示的是整个数组,即为int()[10]类型。+1跳过整个数组,数组的大小是40个字节。
虽然,以%p的格式打印arr和&arr的结果是一样的。但是,这不代表它们的类型是一样的。
3.3数组指针的使用
通过上边的介绍可以知道,数组指针就是指向数组的指针变量,数组指针存放的是数组的地址。下面就通过代码简单演示一下数组指针的使用。
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
int(*pa)[10] = &arr;//把数组arr的地址赋值给数组指针变量p
//但是我们一般很少这样写代码
return 0;
}
下面举一个数组指针作为函数参数的使用场景
#include <stdio.h>
void print_arr1(int arr[3][5], int row, int col)
{
int i = 0;
for (i = 0; i < row; i++)
{
int j = 0;
for (j = 0; j < col; j++)
{
printf("%d ",arr[i][j]);
}
printf("\n");
}
}
void print_arr2(int(*arr)[5], int row, int col)
{
int i = 0;
for (i = 0; i < row; i++)
{
int j = 0;
for (j = 0; j < col; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
int main()
{
int arr[3][5] = { 1,2,3,4,5,6,7,8,9,10 };
print_arr1(arr, 3, 5);
//数组名arr,表示首元素的地址
//但是二维数组的首元素是二维数组的第一行
//所以这里传递的arr,其实相当于第一行的地址,是一维数组的地址
//可以数组指针来接收
print_arr2(arr, 3, 5);
return 0;
}
3.4一组简单的练习题
下面代码的类型分别是什么呢?
int arr[5];
int* parr1[10];
int(*parr2)[10];
int(*parr3[10])[5];
int main()
{
int arr[5];//整型数组
//类型是int[10];
//每个元素的类型是int
int* parr1[10];//指针数组
//类型是int*[10];
//每个元素的类型是int*
int(*parr2)[10];//数组指针
//类型是 int(*)[10];
int(*parr3[10])[5];//存放数组指针的数组
//类型是int(*[10])[5];
//每个元素的类型是int(*)[5];
return 0;
}
4.数组和指针作为函数参数
当书写C语言代码时,难免需要使用函数,设计函数时我们要如何书写呢?下面我会通过一些代码的案例来对此进行分析。
4.1一维数组传参
void test1(int arr[])//OK,可以参考分析(1)
{
;
}
void test1(int arr[5])//OK,可以参考分析(2)
{
;
}
void test1(int* arr)//OK,可以参考分析(3)
{
;
}
void test2(int *Parr[10])//OK,可以参考分析(4)
{
;
}
void test2(int **Parr)//OK,可以参考分析(5)
{
;
}
in t main()
{
int arr[5] = {0};
int* Parr[10] = {0};
test1(arr);//调用函数
test2(Parr);//调用函数
return 0;
}
此刻,让我来带你分析一个个的函数参数设计案例来逐步分析一维数组传参的知识点。
首先,主函数内已经定义两个一维数组,分别是int [ ] 类型的整型数组arr,和int* [ ] 类型的整型指针数组Parr。它们分别调用了test1和test2函数。
分析(1): 从test1函数的参数设计来进行分析,int [5] 类型数组传参,在函数设计的参数部分使用 int [ ]来接收参数是OK的。因为,int arr [5]的类型就是 int [5],而C语言中一维数组的不完全初始化创建可以省略元素个数的。所以第一个test1函数参数部分,设计成 int arr[ ] 是没有问题的。
分析(2):从上述分析已知,调用函数时,传参的数据类型为int [5],在函数的设计时,将参数设计成int arr[5]是OK的。
分析(3):在C语言中,使用数组名有两个特殊情况,情况一,当数组名单独放在sizeof()内部时,此时的数组名表示整个数组。情况二,对数组名进行取地址操作时,取出来整个数组的地址,此时的数组名表示整个数组的地址。除去这两种特殊情况外,其余情况下数组名表示首元素的地址,int类型数据的地址理应使用int* 类型的指针来存放,所以参数部分设计成int* arr是OK的
分析(4):同分析(2)所述,test2函数参数设计成int* Parr[10]是OK的。
分析(5):同分析(3)所述,此时的数组名表示首元素的地址,而一维指针数组的首元素地址的类型为 int** 的二级指针类型。所以,参数部分设计为int** Parr是OK的。
总结
一维数组传参,函数的参数可以是数组也可以是指针。需要注意的两个点,设计参数为一维数组时,可以不指定数组元素的个数。当接收的参数为一维指针数组的首元素地址时,参数部分应该设计成二级指针类型。
4.2二维数组传参
void test(int arr[3][5])//OK,可以参考分析(1)
{
;
}
void test(int arr[][])//error,可以参考分析(2)
{
;
}
void test(int arr[][5])//OK,可以参考分析(3)
{
;
}
void test(int *arr)//error,可以参考分析(4)
{
;
}
void test(int* arr[5])//error,可以参考分析(5)
{
;
}
void test(int (*arr)[5])//OK,可以参考分析(6)
{
;
}
void test(int **arr)//error,可以参考分析(7)
{
;
}
int main()
{
int arr[3][5] = {0};
test(arr);
}
接下来让我们分析每个函数参数设计是否可以进行二维数组传参。
分析(1):二维数组传参,参数设计部分使用二位数组接收。这样是OK的
分析(2):二维数组传参,可以不指定第一个[ ] 的内容,但是不能省略第二个[ ] 的内容。因为不初始化列数,这样编译器不知道该开辟多少的内存空间。所以这样设计参数是错误的。
分析(3):二维数组传参,参数部分可以省略行数,即第一个[ ]内容,所以这样设计参数是OK的。
分析(4):二维数组传参,参数部分若要设计成指针来接参数,形参和实参的类型应当匹配。这里函数调用部分传递的参数是arr,即数组名。二维数组的数组名,如果没有放在sizeof()内部和&地址数组名,表示的是首元素的地址,也就是第一行的地址,类型为数组指针 int (*arr)[5] 类型的数组指针。在函数参数部分设计成一级指针是错误的
分析(5):同上述,函数的形式参数设计成指针数组,类型不匹配。所以是错误的。
分析(6):正确的,因为形参和实参的类型匹配,指针数组传参,指针数组接收。
分析(7):二级指针作为参数,和实际传递的参数数组指针不匹配,所以是错误的。
总结
二维数组传参,函数形参的设计只能省略第一个[]的数字。因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。这样才方便运算。二维数组的数组名,除了单独放在sizeof内部和&数组名的情况下,都表示首元素的地址,即第一行的地址,类型为int(*)[](数组指针类型)。
4.3一级指针传参
#include<stdio.h>
void Print(int* pa, size_t sz)
{
size_t i = 0;
for(i = 0; i < sz; i++)
{
printf("%d ",*(pa+i));
}
printf("\n");
}
int main()
{
int arr[] = {1,2,3,4,5,6};
size_t sz = sizeof(arr) / sizeof(arr[0]);
int* pa = arr;
Print(arr,sz); //OK,可以参考分析(1)
Print(pa,sz); //OK,可以参考分析(2)
return 0;
}
分析(1):当一维数组的数组名没有单独放在sizeof()内部和&数组名时,数组名表示首元素的地址,int类型的地址,即int类型的指针。所以当函数参数为int 类型的参数时,传一维整型数组的数组名是OK的。
分析(2):将一维数组的数组名存放在整型指针变量pa中,调用Print()函数时,传入pa作为第一个参数是OK的。
总结
当函数的参数设计成一级指针变量时(假设为int* 类型的整型一级指针变量),使用者调用该函数,可以传入一级整型指针变量、一维整型数组的数组名,以及整型变量的地址。
4.4二级指针传参
我简单的描述一下二级指针的概念:二级指针即,数据类型** 变量名称。例如这里我定义一个字符型二级指针变量ppc,可以写作char* * ppc;
#include <stdio.h>
void test(int** ptr)
{
printf("num = %d\n", **ptr);
}
int main()
{
int n = 6;
int*p = &n;
int **pp = &p;
test(pp);//OK,可以参考分析(1)
test(&p);//OK,可以参考分析(2)
return 0;
}
分析(1):当函数的参数部分设计成一个二级指针变量时,直接传入一个二级指针变量是OK的。
分析(2):当函数的参数部分设计成一个二级指针变量时,直接传入一个一级指针变量的地址也是OK的。
总结
当函数的参数设计成二级指针变量时(假设为char* 类型的字符型二级指针变量),使用者调用该函数,可以传入二级字符型指针变量、一维字符型指针数组的数组名,以及一级字符指针型变量的地址。
5.函数指针
5.1什么是函数指针?
函数指针的概念:在C语言中如果在程序中定义了一个函数,那么在编译时系统就会为这个函数代码分配一段存储空间,这段存储空间的首地址称为这个函数的地址。而且函数名表示的就是这个地址。既然是地址我们就可以定义一个指针变量来存放,这个指针变量就叫作函数指针变量,简称函数指针。
请看下面代码
#include<stdio.h>
void test()
{
printf("hello world\n");
}
int main()
{
printf("%p/n",test);
printf("%p/n",&test);
return 0;
}
通过上述代码运行结果可知,定义C语言中的函数确实会有在内存中开辟一块空间用于存放函数的地址。函数名是等价于&函数名的。
5.2函数指针变量的定义和使用
函数指针变量的定义:函数的返回类型 (*函数名)(函数参数)
#include<stdio.h>
int Add(int x, int y)
{
return x + y;
}
int main()
{
int (*pa)(int, int) = Add;//也可以写成:int (*pa)(int, int) = &Add
int ret = pa(2, 3);
//int ret = (*pa)(2, 3);这样写也没问题
printf("%d\n", ret);
return 0;
}
我简单分析下上面代码函数指针定义的那行代码,int (*pa)(int, int) = Add;,这里应该先从变量名入手进行分析,首先是(*pa),变量名pt旁边跟着 *表示pa是一个指针变量。紧接着是右边的(int, int),表示这是一个函数指针变量,且指向函数的参数为两个int类型变量。(*pa)的左边是int,表示该函数的返回类型为整型类型。最后是 = Add,表示该函数指针变量指向的函数为Add函数。
#include<stdio.h>
void test()
{
printf("hello world\n");
}
int main()
{
void (*pt)() = test;//定义函数指针变量pt,用于存放test函数的地址
pt();//直接通过函数指针调用函数
return 0;
}
我简单分析下上面代码函数指针定义的那行代码,void (*pt)() = test; ,这里应该先从变量名入手进行分析,首先是(*pt),变量名pt旁边跟着 *表示pt是一个指针变量。紧接着是右边的(),表示这是一个函数指针变量,且指向函数的无参数。(*pt)的左边是void,表示该函数的返回类型为无具体类型。最后是 = test,表示该函数指针变量指向的函数为test函数。
两个经典的函数指针代码问题
以下两端代码均出自《C陷阱和缺陷》。
//代码1
(*(void (*)())0)();
//代码2
void (*signal(int , void(*)(int)))(int);
代码1解读:先从(void ()())0)这里开始解读,这里是将0地址强制类型转换成void()()的指针。那么剩下的()()可以解读成函数的一次调用。这段代码是一次函数调用,调用了0地址出的一个函数。
代码2解读:首先,从变量名signal入手。signal先和右边的(int, void()(int))结合,说明signal是一个函数。函数的参数分别是整型和返回类型为无具体类型,参数为整型的函数指针。然后再把这段解读后的代码从源代码移除,这么做方便后续的解读void (*)(int);,剩下的是一个函数指针,参数是(int),返回类型为无返回类型。也就是signal 函数的返回类型。
//这里我用typedef简化一下这段代码void (*signal(int , void(*)(int)))(int);
int main()
{
typedef void(*pf_t)(int);
//void (*signal(int , void(*)(int)))(int);
pf_t signal(int,pf_t);
return 0;
}
注意:这里typedef重名指针类型时,重命名符号需要跟在*后
6.函数指针数组
函数指针数组:顾名思义就是存放函数指针的数组。
6.1函数指针数组的声明
函数返回类型 (*数组名[元素个数])(函数参数);
int main()
{
int (*cacl[5])(int, int);//函数指针声明
return 0
}
简单解读一下这段代码:int (cacl[5])(int, int),首先从数组名入手,cacl先和[ ]结合,所以这是一个数组。数组的每个元素类型为int()(int,int),即返回类型为int,参数为两个int的函数指针。
6.2函数指针数组的使用
函数指针数组的用途:转移表
例1、两个整数运算的计算器(简单版)
#include<stdio.h>
void menu()
{
printf("***************************\n");
printf("*** 1.Add 2.Sub ***\n");
printf("*** 3.Mul 4.Div ***\n");
printf("***********0.exit**********\n");
printf("***************************\n");
}
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 test()
{
int input = 0;
do
{
int x = 0;
int y = 0;
int ret = 0;
menu();
printf("请选择:>");
scanf("%d", &input);
switch(input)
{
case 1:
printf("请输入两个整数:>");
scanf("%d %d",&x ,&y);
ret = Add(x, y);
printf("%d\n",ret);
break;
case 2:
printf("请输入两个整数:>");
scanf("%d %d", &x, &y);
ret = Sub(x, y);
printf("%d\n", ret);
break;
case 3:
printf("请输入两个整数:>");
scanf("%d %d", &x, &y);
ret = Mul(x, y);
printf("结果为:%d\n", ret);
break;
case 4:
printf("请输入两个整数:>");
scanf("%d %d", &x, &y);
ret = Div(x, y);
printf("%d\n", ret);
break;
case 0:
printf("退出成功!\n");
break;
default:
printf("选择错误,请重试!\n");
break;
}
} while (input);
}
int main()
{
test();
return 0;
}
例2、两个整数运算的计算器(函数指针数组版)
#include<stdio.h>
void menu()
{
printf("***************************\n");
printf("*** 1.Add 2.Sub ***\n");
printf("*** 3.Mul 4.Div ***\n");
printf("***********0.exit**********\n");
printf("***************************\n");
}
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 ret = 0;
int input = 1;
int x = 0;
int y = 0;
int(*cacl[5])(int, int) = { 0,Add,Sub,Mul,Div };//转移表
do
{
menu();
scanf("%d", &input);
if ((input < 5) && (input > 0))
{
printf("请输入两个整数\n");
scanf("%d %d", &x, &y);
ret = cacl[input](x, y);
printf("结果为:%d\n",ret);
}
else if(input==0)
{
printf("退出成功!\n");
}
else
{
printf("输入错误,请重试!\n");
}
} while (input);
return 0;
}
7.指向函数指针数组的指针
#include<stdio.h>
#inlcude<assert.h>
void PrintS(const char* str)
{
assert(str);
printf("%s\n",str);
}
int main()
{
//函数指针pfun
void (*pfun)(const char*) = PrintS;
//函数指针的数组pfunArr
void (*pfunArr[5])(const char* str);
pfunArr[0] = PrintS;
//指向函数指针数组pfunArr的指针ppfunArr
void (*(*ppfunArr)[5])(const char*) = &pfunArr;
return 0;
}
解读指向函数指针数组的指针的定义:void ((ppfunArr)[5])(const char) = &pfunArr;
首先,从(ppfunArr)入手,先和 * 结合说明是指针。然后,左边未解读代码如下void ([5])(const char) 。说明这是一个函数指针数组类型。函数返回值为void,参数为(const char*)。数组元素个数为5个。最后,ppfunArr指针存放的是pfunArr函数指针数组的地址。
8.回调函数
回调函数的定义:回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
下面我通过C标准库函数qsort来举例回调函数。
8.1qosrt函数的介绍
void qsort (void* base, size_t num, size_t size,int (*compar)(const void * e1,const void * e2));
首先,我们通过阅读文档来查看一下。
通过简单的阅读文档可以得知:qsort函数的返回类型为void,函数有共有四个参数,分别表示待排序数组(void * base,数组的元素个数(size_t num),数组的单个元素的大小,单位是字节(size_ t size),以及函数指针compar,返回类型是int ,参数分别是两个const修饰的无具体类型指针(const void * e1,const void e2)。C语言标准规定,使用者需要自己设计qosrt函数的第四个参数,当e1 > e2 是该函数返回一个大于0 的数,当e1 = e2时,函数返回0,当e1 < e2时,函数返回一个小于0的数。
void* 类型指针变量:void* 类型指针变量就是无具体类型指针变量,设计的初衷用来存放任意类型指针变量。void* 指针变量无法直接使用,因为void指针变量的步长未知,故解引用操作是非法的。使用void指针变量需要进行强制类型转换。
include<stdio.h>
#include<stdlib.h>
int CmpInt(const void* e1, const void* e2)//回调函数,用于排序整形数据
{
return *((int*)e1) - *((int*)e2);
}
int main()
{
int arr[] = { 1,3,5,7,9,2,4,6,8,10 };
size_t sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
for (i = 0; i < (int)sz; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
qsort(arr, sz, sizeof(arr[0]), CmpInt);
for (i = 0; i < (int)sz; i++)
{
printf("%d ",arr[i]);
}
printf("\n");
return 0;
}
8.2通过冒泡排序思想模拟实现qsort函数功能
实现思路:当要排序任意类型数据排序时,首先要清楚,具体排序什么类型数据得看使用者的需求。所以函数默认接收的参数为void类型指针。但是要如何才能合理的使用void指针呢?这就不得不提字符指针重要性。因为,设计者也不清楚使用者会排序什么类型数据,如果把要比较的两个数据都强制转化成char指针。然后再一个一个字节进行比较,便可以实现任意类型数据的比较。在通过char*指针来进行交换两个元素的内容以达到排序的目的。
int CmpInt(const void* e1, const void* e2)
{
return *((int*)e1) - *((int*)e2);
}
void Swap(char* c1, char* c2, size_t size)
{
size_t i = 0;
for (i = 0; i < size; i++)
{
char tmp = *c1;
*c1 = *c2;
*c2 = tmp;
c1++;
c2++;
}
}
void MySort(void* base, size_t num, size_t size,
int(*cmp)(const void* e1, const void* e2))
{
size_t i = 0;
size_t j = 0;
for (i = 0; i < num - 1; i++)
{
for (j = 0; j < num - i - 1; j++)
{
if (cmp((char*)base + (size * j), (char*)base + size * (j + 1)) > 0)
{
Swap((char*)base + (size * j), (char*)base + size * (j + 1), size);
}
}
}
}
int main()
{
int arr[] = { 2,4,6,8,10,1,3,5,7,9 };
size_t sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
for (i = 0; i < (int)sz; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
MySort(arr, sz, sizeof(arr[0]), CmpInt);
for (i = 0; i < (int)sz; i++)
{
printf("%d ",arr[i]);
}
printf("\n");
return 0;
}