C语言——指针详解 下
指针基础知识详解下
- 函数指针
函数指针变量是用来存放函数地址的,未来可以通过地址调用函数。
函数的地址就是函数名,Fun与&Fun完全等价。
- 函数指针变量的创建方法:
int Add(int x, int y)
{
return x + y;
}
int main()
{
int (*pf)(int, int) = &Add;
return 0;
}
注意:
int (*)(int, int)//这个为该函数指针的类型
- 通过函数指针调用被指针指向的函数。
#include<stdio.h>
int Add(int x, int y)
{
return x + y;
}
int main()
{
int (*pf)(int, int) = &Add;
printf("%d\n", (*pf)(2, 3));
printf("%d\n", (*pf)(10, 10));
return 0;
}
- 代码分析
( * ( void (*)() ) 0 )();
//void(*)()为函数指针类型,该函数返回类型为void,并且没有参数
//( void (*)() 0 )为强制类型转换,将0转换为void(*)()类型的函数指针变类型
//可以认为在地址0x00000000处放置着一个返回类型为void的无参函数,可以设为pF()
//最终为调用这个函数——(*pF)(void);
//调用时可类比为(*pf)(10, 10)
- typedef关键字
- typedef 是用来给类型重命名的,可以将复杂的类型简单化。
例如:将 unsigned int 改写为 uint。
#include<stdio.h>
typedef unsigned int uint;
int main()
{
unsigned int a = 10;
uint b = 100;
printf("%u %u ", a, b);
return 0;
}
- 但是对于数组指针和函数指针有些不同:
例如我们想要把数组指针类型 int( * )[ 5 ] 重命名为 parr 可以这样写:
#include<stdio.h>
typedef int(*parr)[5]; //新的类型名必须在*的右边
int main()
{
int arr[5] = { 0 };
parr pa = &arr;
//parr为类型,pa为变量名。
return 0;
}
函数指针类型的重命名也一样,例如将 void( * ) ( int ) 类型重命名为 pfv ,就可以这样写:
#include<stdio.h>
typedef void(*pfv)();
void print()
{
printf("Hello World");
}
int main()
{
pfv pf = &print;
//pfv为变量类型,pf为变量名称。
(*pf)();//函数调用
return 0;
}
输出示例:
Hello World
- 函数指针数组
数组是一个存放相同类型数据的存储空间,例如指针数组(数组的每个元素是指针变量):
int main()
{
int arr1[10] = { 0 };
int arr2[10] = { 0 };
int arr3[10] = { 0 };
int* parr[] = { arr1,arr2,arr3 };//指针数组
return 0;
}
那要把函数的地址存到⼀个数组中,那这个数组就叫函数指针数组。
- 定义:
int (*parr1[3])( );
parr1 先和 [ ] 结合,说明parr1是数组,数组的内容是 int ( * )( ) 类型的函数指针。
- 例如:
#include<stdio.h>
int add(int a, int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
int main()
{
int(*pfarr[2])(int, int) = { add,sub };
return 0;
}
- 一个数组中,只可以存放相同类型的函数指针。
- 函数指针数组的用途:转移表
- 实现简单计算器
#include <stdio.h>
//函数部分
//加减乘除这四种函数指针的类型均为 int (*pf)(int,int)
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("1.add 2.sub 3.mul 4.div 0.exit\n");
printf("输入你的选择:");
}
int main()
{
int x = 0, y = 0;//两个操作数
int input = 0;//用户输入的调用的函数ID
int ret = 0;//计算结果
//转移表
int(*p[5])(int x, int y) = { 0, add, sub, mul, div };//函数指针数组
do{
menu();
scanf("%d", &input);
if ((input <= 4 && input >= 1)){
printf("输入操作数:" );
scanf("%d %d", &x, &y);
ret = (*p[input])(x, y);//将对应下标的函数指针解引用
printf("ret = %d\n", ret);
}else if (input == 0){
printf("已退出\n");
}else{
printf("输入错误\n" );
}
} while (input != 0);
return 0;
}
- 回调函数
回调函数就是一个通过函数指针调用的函数。
如果你把函数的指针作为参数传递给另⼀个函数,当这个指针被用来调用它指向的函数时,被调用的函数就是回调函数。
- 回调函数的例子——简单计算器重制
#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("1.add 2.sub 3.mul 4.div 0.exit\n");
printf("输入你的选择:");
}
//传入函数指针
int calc(int (*pf)(int, int))
{
printf("输入两个操作数:\n");
int a = 0, b = 0, ret = 0;
scanf("%d %d", &a, &b);
//执行对应函数的操作
ret = (*pf)(a, b);
return ret;
}
int main()
{
int input = 0;
do {
menu();
scanf("%d", &input);
if (input >= 1 && input <= 4) {
//函数指针数组
int (*pfarr[])(int, int) = { 0,add,sub,mul,div };
//找到对应数组中存放函数的地址
int ret = calc(pfarr[input]);
printf("%d\n", ret);
}
else {
printf("请重新输入\n");
}
} while (input != 0);
printf("已退出\n");
return 0;
}
- 在上述代码中,函数 calc( )就是回调函数。
- qsort( )函数的应用
- qsort( )函数的使用方法
- 注意:qsort( ) 需要在头文件 <stdlib.h> 下使用
void qsort(void* base, size_t num, size_t size, int (*comper)(const void*, const void*));
// void* base -- 指针,指向待排序数组的第一个元素
// size_t num -- 是待排序数组元素的个数
// size_t size -- 是待排序数组元素的大小
// int (*comper)(const void*, const void*) --函数指针,就是两个元素比较大小的函数
-
参数顺序:首元素地址——数组元素个数——每个元素大小——比较函数指针。
-
qsort( )函数示例——排序整型数组
#include<stdio.h>
#include<stdlib.h>
//在使用qsort()函数时需要实现一个比较函数
int cmp(const void* a,const void* b)//const修饰为左定值右定址。
{
return *(int*)a - *(int*)b;//由于a和b均为void*类型,我们要根据排序数据的类型来强制类型转化a和b。
}
//若该函数返回值大于零,则会交换*a与*b两个数,其余情况不交换,即默认为升序模式。
//注意:该函数可以比较两个数的大小,并且告诉qsort()函数它的返回值是正数、零还是负数。
int main()
{
int arr[10] = { 1,9,2,8,3,7,4,6,5,10 };
int num = sizeof(arr) / sizeof(arr[0]);
qsort(arr, num, sizeof(int), cmp);
//打印数组
for (int i = 0; i < num; i++) {
printf("%d ", arr[i]);
}
return 0;
}
输出结果:
- 使用冒泡排序算法模拟实现qsort( )函数——整型变量( int )
#include<stdio.h>
//比较函数
int cmp(const void* a, const void* b)
{
return *(int*)a - *(int*)b;
}
//交换函数的写法——因为一次只能交换1B,所以要循环wide(一个元素的大小)次
void swap(char* p1, char* p2,int wide)
{
for (int i = 0; i < wide; i++) {
char tmp = *p1;
*p1 = *p2;
*p2 = tmp;
p1++;
p2++;
}
}
//类比于qsort( )函数的四个参数,采取冒泡排序算法
void bubbleSort(void* base, int sz, int wide, int (*cmp)(const void*, const void*))
{
for (int i = 0; i < sz-1; i++) {
for (int j = 0; j < sz - 1 - i; j++) {
//在这里由于不知道需要比较的具体类型,根据参数中传递的wide值(元素大小),
//把void*类型转化为char*类型
//j*wide与(j+1)*wide可以类比为*(arr+j)和*(arr+j+1)
if (cmp((char*)base + j * wide, (char*)base + (j + 1) * wide) > 0) {
swap((char*)base + j * wide, (char*)base + (j + 1) * wide, wide);
}
}
}
}
int main()
{
int arr[] = { 1,0,2,9,3,7,4,6,5,8 };
int sz = sizeof(arr) / sizeof(arr[0]);
bubbleSort(arr, sz, sizeof(int),cmp);
for (int i = 0; i < sz; i++) {
printf("%d ", *(arr + i));
}
return 0;
}
- 使用冒泡排序算法模拟实现qsort( )函数——单精度浮点型变量( float )
//用冒泡排序法模拟实现qsort函数
#include<stdio.h>
//交换函数,由于不知道交换的类型,使用1B大小的char类型乘以排序类型的大小,更容易得到正确的大小
void swap(char* a, char* b, int wide)
{
for (int i = 0; i < wide; i++) {
int tmp = *a;
*a = *b;
*b = tmp;
a++;
b++;
}
}
//该函数等价于qsort()函数
void bubbleSort(void* base, int sz, int wide, int (*cmp)(const void*, const void*))
{
for (int i = 0; i < sz - 1; i++) {
for (int j = 0; j < sz - 1 - i; j++) {
if (cmp(((char*)base + j * wide), ((char*)base + (j + 1) * wide)) > 0) {
swap(((char*)base + j * wide), ((char*)base + (j + 1) * wide), wide);
}
}
}
}
//cmp为自己创建的函数,根据需要写入相应的类型
int cmp(const void* a, const void* b)
{
if (*(float*)a - *(float*)b > 0)
return 1;
else
return 0;
}
int main()
{
float arr[4] = { 1.2f,2.4f,1.3f,2.6f };
bubbleSort(arr, sizeof(arr) / sizeof(arr[0]), sizeof(float), cmp);
for (int i = 0; i < 4; i++) {
printf("%.1f ", arr[i]);
}
return 0;
}
- strlen( ) 函数与sizeof( ) 操作符的区别
- strlen( )简介
size_t strlen ( const char* str );
注意:const在星号左边为不能修改该变量的数值,const在星号右边为不能修改该变量的地址。
统计的是从 strlen( ) 函数的参数 str 这个地址开始向后,到字符 \0 之前的字符串中字符的个数。strlen( ) 函数会⼀直向后寻找 \0 字符,直到找到为止,所以可能存在越界查找。
sizeof( )操作符 | strlen( )函数 |
---|---|
sizeof( )计算操作数所占内存的大小, 单位是字节(B) | strlen( )是库函数,需要包含头文件<string.h> |
sizeof( )不关注内存中存放的是什么数据 | srtlen( )是求字符串长度的,统计的是\0之前字符的个数,如果没有 \0,就会持续向后寻找,可能会发生越界 |
- 代码演示
#include<stdio.h>
#include<string.h>
int main()
{
char arr1[3] = { 'a', 'b', 'c'};//该字符数组末尾无\0字符
char arr2[] = "abc";
printf("%d\n", strlen(arr1));//随机值(越界查找了)
printf("%d\n", strlen(arr2));//3
printf("%d\n", sizeof(arr1));//3
printf("%d\n", sizeof(arr1));//3
return 0;
}
指针经典面试题详解
- sizeof( )操作符与strlen( )函数的输入输出问题
- 一维数组
int main()
{
int a[] = { 1,2,3,4 };
printf("%zd ", sizeof(a));// 16-数组的总大小
printf("%zd ", sizeof(a + 0));// 4/8-第一个元素的指针
printf("%zd ", sizeof(*a));// 4-第一个元素元素
printf("%zd ", sizeof(a + 1));// 4/8-第二个元素的指针
printf("%zd ", sizeof(a[1]));// 4-第一个元素元素
printf("%zd ", sizeof(&a));// 4/8-数组的总地址
printf("%zd ", sizeof(*&a));// 16-数组的总大小
//&a为数组的总地址,解引用后为整个数组,等价于sizeof(a)的数值
printf("%zd ", sizeof(&a + 1));// 4/8-数组最后一个元素之后那个元素的地址
printf("%zd ", sizeof(&a[0]));// 4/8-第一个元素的地址
printf("%zd ", sizeof(&a[0] + 1));// 4/8-第二个元素的地址
return 0;
}
需要注意的是,在x64环境下,指针变量的大小为8B,在x86的环境下,指针大小为4B。
总结:在sizeof( )操作符中,只有数组变量单独在括号中的时候才表示整个数组,其他情况均表示第数组中第一个元素的地址。
- 字符数组
代码一:
- sizeof( )操作符:
int main()
{
char arr[] = { 'a','b','c','d','e','f' };
printf("%zd\n", sizeof(arr));// 6-字符数组的大小
printf("%zd\n", sizeof(arr + 0));// 4/8-元素a的地址
printf("%zd\n", sizeof(*arr));// 1-第一个元素a的大小
printf("%zd\n", sizeof(arr[1]));// 1-第一个元素a的大小
printf("%zd\n", sizeof(&arr));// 4/8-整个数组的地址
printf("%zd\n", sizeof(&arr + 1));// 4/8-数组最后一个元素之后那个元素的地址
printf("%zd\n", sizeof(&arr[0] + 1));// 4/8-第二个元素b的地址
return 0;
}
- strlen( )函数:
int main()
{
//由于该字符数组后面没有\0,strlen( )函数会一直向后寻找
char arr[] = { 'a','b','c','d','e','f' };
printf("%zd ", strlen(arr));//随机值x
printf("%zd ", strlen(arr + 0));//随机值x
printf("%zd ", strlen(&arr));//随机值x
printf("%zd ", strlen(&arr + 1));//随机值x-6———(+1跳过了整个数组的6个元素)
printf("%zd ", strlen(&arr[0] + 1));//随机值x-1
//注:以下为两个有问题的输入方式
//printf("%zd ", strlen(*arr));//传入的是字符a的ASCII码值,该值作为地址过小,会导致程序崩溃
//printf("%zd ", strlen(arr[1]));//同理,传入的是b的ASCII码值
return 0;
}
代码二:
- sizeof( )操作符
int main()
{
//使用字符串初始化字符数组,结尾存在\0,该字符串长度为7
char arr[] = "abcdef";
printf("%zd ", sizeof(arr));// 7-数组大小
printf("%zd ", sizeof(arr + 0));// 4/8-表示首元素a的地址
printf("%zd ", sizeof(*arr));// 1-表示元素a的大小
printf("%zd ", sizeof(arr[1]));// 1-表示元素b的大小
printf("%zd ", sizeof(&arr));// 4/8-表示整个数组的地址
printf("%zd ", sizeof(&arr + 1));// 4/8-表示该数组最后一个元素之后,那个元素的地址
printf("%zd ", sizeof(&arr[0] + 1));// 4/8-表示元素b的地址
return 0;
}
- strlen( )函数
int main()
{
char arr[] = "abcdef";
printf("%zd ", strlen(arr));// 6-该字符串的长度
printf("%zd ", strlen(arr + 0));// 6-从元素a的地址到\0结束
printf("%zd ", strlen(&arr));// 6-该字符串的长度
printf("%zd ", strlen(&arr + 1));// 随机值x,因为是从数组结束之后的那个元素算起的
printf("%zd ", strlen(&arr[0] + 1));// 5-通过+1跳过了字符a,从字符b开始算起
//下面两个输入函数结果为a或b的ASCII码,值过小,会导致程序崩溃
//printf("%zd ", strlen(*arr));
//printf("%zd ", strlen(arr[1]));
return 0;
}
代码三:
- sizeof( )操作符
int main()
{
//p为一个指针变量,存放着首元素a的地址,p的类型为const char*类型
char* p = "abcdef";
printf("%d\n", sizeof(p));// 4/8-为元素a的指针
printf("%d\n", sizeof(p + 1));// 4/8-为元素b的指针
printf("%d\n", sizeof(*p));// 1-解引用后为元素a的大小
printf("%d\n", sizeof(p[0]));// 1-元素a的大小
printf("%d\n", sizeof(&p));// 4/8-指针变量p的地址,为二级指针
printf("%d\n", sizeof(&p + 1));// 4/8-为p指针变量的地址后面某个指针变量的地址
printf("%d\n", sizeof(&p[0] + 1));// 4/8-为元素b的地址
return 0;
}
- strlen( )函数
//还有部分问题尚未解决
int main()
{
char* p = "abcdef";
char arr[] = "abcdef";
printf("%d\n", strlen(p));// 6-该字符串的长度
printf("%d\n", strlen(p + 1));// 5-从b开始到\0之间字符串的长度
printf("%d\n", strlen(&p));//
printf("%d\n", strlen(&p + 1));
printf("%d\n", strlen(&p[0] + 1));
//printf("%d\n", strlen(*p));
//printf("%d\n", strlen(p[0]));
return 0;
}
- 二维数组
int main()
{
int a[3][4] = { 0 };
printf("%zd ", sizeof(a));// 48-表示整个数组的大小
printf("%zd ", sizeof(a[0][0]));// 4-表示a[0][0]元素的大小
printf("%zd ", sizeof(a[0]));
// 16-a[0]表示该二维数组第一行的数组名,单独放在sizeof内部,表示该二维数组首元素(第一行的一维数组)的大小
printf("%zd ", sizeof(a[0] + 1));
// 4/8-数组名没有单独放在sizeof内部,表示a[0][0]的地址,+1后表示a[0][1]的地址
printf("%zd ", sizeof(*(a[0] + 1)));// 4-a[0]+1表示a[0][1]的地址,解引用后表示该元素
printf("%zd ", sizeof(a + 1));// 4/8-a表示该二维数组的地址,为数组指针,+1表示第二行的地址
printf("%zd ", sizeof(*(a + 1)));// 16-表示第二排的大小
printf("%zd ", sizeof(&a[0] + 1));// 4/8-为&a[0]为第一行整个的地址,+1为第二行的地址
printf("%zd ", sizeof(*(&a[0] + 1)));// 16-为第二行数组的大小
printf("%zd ", sizeof(*a));
// 16-数组名没有单独存放在sizeof内部,所以为该二维数组首元素的地址,解引用后为首元素(为第一行的一维数组)
printf("%zd ", sizeof(a[3]));// 16-该行虽然不存在,假设为该数组的“第四行”,数组名在sizeof中表示整个数组的大小
return 0;
}
**注意:**在二维数组 arr[ i ][ j ] 中,arr[ i - 1 ]表示该数组第 i 行的数组名,一般用来表示该数组首元素的地址。
数组与指针可以相互表示,例如arr[ k ]可以用指针表示为 * ( arr + k ),arr[ m ][ n ] 可以表示为 * ( * ( arr + m ) + n )
这样我们就可以容易理解为二维数组 arr 的第 m 个元素,数组 arr[ m ] 的第 n 个元素。
- 总结——数组名的意义
- sizeof ( arr ),这里面的数组名表示整个数组,计算的是整个数组的大小。
- &arr,这里面的数组名表示整个数组,取出的是整个数组的地址。
- 除此之外所有的数组名都表示首元素的地址,包括二维数组中arr[ i - 1 ]这种第 i 行的数组名。
- 指针的运算
- 指针的运算
- 题目一:
//程序的输出结果是什么
int main()
{
int a[5] = { 1, 2, 3, 4, 5 };
int* ptr = (int*)(&a + 1);
printf("%d,%d", *(a + 1), *(ptr - 1));
return 0;
}
//题目解析:
//a在这里表示数组首元素的地址,+1后解引用为该数组第二个元素
//ptr原本为数组指针类型,+1后强制类型转化为int*,即ptr-1向前跳一个元素,即为5
答案:2 5
- 题目二:
//在x86环境下,已知结构体的大小为20B,那么该程序的输出结果为多少
struct Test
{
int num[5];
}*p = (struct Test*)0x100000;//将0x100000这个数组强制转化为结构体指针类型
int main()
{
printf("%p\n", p + 0x1);
printf("%p\n", (unsigned long)p + 0x1);
printf("%p\n", (unsigned int*)p + 0x1);
return 0;
}
//题目解析:
//p+0x1为向后跳了一个结构体单位(20B),应当是0x100000+0x20进行数学运算,结果为0x100014
//p被强制转换为unsigned long类型,p+0x1为基本的数学运算,结果为0x100001
//p被强制转换为unsigned int*类型,p+0x1应当是0x100000+0x4进行数学运算,结果为0x1000004
答案:00100014 00100001 00100004
- 题目三:
int main()
{
int a[3][2] = { (0, 1), (2, 3), (4, 5) };
int* p;
p = a[0];
printf("%d", p[0]);
return 0;
}
//题目解析:
//首先,数组中的表达式为逗号运算,计算后数组中的数字应当为{ 1,3,5,0,0,0 }
//p为指针变量,a[0]为该二维数组第一行的数组名,表示第一行首元素的地址
//p可以表示为*(a+0),那么p[0]就可以表示为*(*(a+0)+0),即为a[0][0]。
答案:1
- 题目四:
int main()
{
int a[5][5] = { 0 };
int (*p)[4];//为数组指针,类型为int (*)[4]
p = a;//将数组a的地址赋值给p,在这里需要注意类型转化
printf("%d,%p\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);
return 0;
}
//题目解析;
//a为int (*)[5],而p为int (*)[4]
//数组指针在赋值时发生了强制类型转换,变量p为四列,而a为五列
//p[4][2]表示的是 *(*(p+4)+2)
//p[4]表示每次跳过四个元素,[2]表示找到该行(四个元素为一行)的第二个元素
//p[4][2]表示的是该数组中第18个小元素,而a[4][2]表示的是第22个小元素
//&p[4][2] - &a[4][2]——地址相减,表示中间的元素个数差,得到的值为-4
//-4的补码为11111111111111111111111111111100,转化为16进制数为0xFFFF FFFC
答案:-4 FFFFFFFC
- 题目五:
int main()
{
int aa[2][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
//这里两个强制转换把数组指针转化为了int*类型的指针
int* ptr1 = (int*)(&aa + 1);
int* ptr2 = (int*)(*(aa + 1));
printf("%d,%d", *(ptr1 - 1), *(ptr2 - 1));
return 0;
}
//题目解析:
//ptr1中,先将该二维数组的地址取出+1后在强制类型转化为int*类型的指针
//*(ptr1 - 1)为向后跳一个元素之后对其解引用,得到结果为10
//ptr2中aa表示的是二维数组首元素的地址,+1后变为第二行的首元素的地址
//(即&aa[1][0]),*(ptr2 - 1)为向后跳一个整型变量,解引用可以得到为第一行最后一个元素a[0][4],结果为5
答案:10 5
- 题目六:
int main()
{
//由于每个字符串都是一个指针变量,所以此处要用指针数组
char* a[] = { "work","at","alibaba" };
//数组名为首元素的地址,而该数组首元素就是地址,所以这里要使用二级指针
char** pa = a;
pa++;
printf("%s", *pa);
return 0;
}
//题目解析:
//pa代表的是数组a中第一个元素的地址,pa++表示向后移动一个元素,pa就是字符串“at”的地址
答案:at