C语言——指针详解 下

C语言——指针详解 下

指针基础知识详解下

  1. 函数指针

函数指针变量是用来存放函数地址的,未来可以通过地址调用函数。

函数的地址就是函数名,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;
}
  1. 代码分析
( * ( void (*)() ) 0 )();
//void(*)()为函数指针类型,该函数返回类型为void,并且没有参数
//( void (*)() 0 )为强制类型转换,将0转换为void(*)()类型的函数指针变类型
//可以认为在地址0x00000000处放置着一个返回类型为void的无参函数,可以设为pF()
//最终为调用这个函数——(*pF)(void);
//调用时可类比为(*pf)(10, 10)
  1. 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

  1. 函数指针数组

数组是一个存放相同类型数据的存储空间,例如指针数组(数组的每个元素是指针变量):

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;
}
  • 一个数组中,只可以存放相同类型的函数指针。
  1. 函数指针数组的用途:转移表
  • 实现简单计算器
#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;
}
  1. 回调函数

回调函数就是一个通过函数指针调用的函数。

如果你把函数的指针作为参数传递给另⼀个函数,当这个指针被用来调用它指向的函数时,被调用的函数就是回调函数。

  • 回调函数的例子——简单计算器重制
#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( )就是回调函数。
  1. 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;
}
  1. 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;
}

指针经典面试题详解

  1. 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;
}
  1. 二维数组
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 行的数组名。
  1. 指针的运算
  2. 指针的运算
  • 题目一:
//程序的输出结果是什么
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

  • 38
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值