【C进阶】 第九章 - 指针进阶

字符指针

看下面的程序

void t() {
	char* a = "hello";
	*a = 'x';
	printf("%c", a);
}

     

程序想把a的指向的值 ‘h’改成‘a' ,但结果引发异常,这是因为右边是常量,常量存放在在内存的常量区,是只读的。a指向这个常量的首位想改变*a,但常量是无法改变的,因此报错。

这里可以在指针变量前面声明这是常变量,预防异常。

void t1() {
	const char* a = "hello";
	const char* b = "hello";
	if (a == b)	printf("aEqualb.\n");
	char c[] = "hello";
	char d[] = "hello";
	if (c == d)	printf("cEquald.");
}

结果输出

aEqualb.

同样的常量字符串只在内存保存一份,指针共同指向这一块内存。

字符数组,在内存开辟2个数组,存的虽然是同样的内容,但是地址不是相同的。

即栈区:a,b,c,d 。其中:a,b指向的常量区的字符串"hello"。

指针数组跟数组指针

void t3() {
	int* p1[10];//*与int先结合,为指针数组 存放指针(地址)的数组
	int(*p2)[10]; //数组指针,指向数组的指针 跟 int* p一样?
	int a[20] = { 0 };
	p2 = a;
	printf("%d\n", *(p2 + 15));
	printf("%d", **(p2+15));
}

输出

12122996
0

第一个输出p2指向的数组首地址,第二个输出p2指向数组首地址指向的值。

数组首地址与数组地址的区别

	int arr[2] = { 0 };
	printf("%d\n", arr);
	//printf("%d\n", &arr[0]);
	printf("%d\n\n", &arr);
	printf("%d\n", arr+1);
	//printf("%d\n", &arr[0]+1);与上一条等效
	printf("%d", &arr+1);

输出

7338352
7338352
7338356
7338360

可以看到首地址跟数组地址是一样的,但是+1时,往后一步的距离是不同的。数组首地址往后一个元素大小为一步(52+4=56),数组按数组大小往后一步(52+2*4=60)。

差异的根本就是因为指针类型的不同。

arr,&arr[0](数组的首地址)的类型是int * ,&arr(数组的地址)的类型是int*[ ]

数组指针的写法

   int (*p) [10] = &arr 

让p 与*先结合说明这是个指针p,再让 int 与 [ ] 结合说明他的类型为数组。

int (*)[10] 即数组类型指针

再看一段代码

int arr[5];
int *parr1[10];
int (*parr2)[10];
int (*parr3[10])[5];

第一条指 int类型数组

第二条指 int指针类型数组

第三条指 in型数组指针

第四条 ,parr3与[10]结合为数组,再与*结合成指针数组:*parr3[10]  。

                数组里面是什么类型的指针?把parr3[10]去掉 → int (*)  [5] 大小5的int型数组指针

                大小为5的int型数组指针数组,大小为10 ,每个元素类型为 int (*)  [5]

数组传参

形参的形式:

数组形式

 数组名

        除2个情况外,数组名都表示首地址。

        1.sizeof(arr) arr表示整个数组

        2.&arr  arr表示整个数组的地址

把数组地址作为参数,且不计算数组大小,打印每个元素

//不传数组大小,打印每个元素
void t333(int (*arr)[10]) {
	int* p = *arr;
	while (p < arr+1) {
		printf("%d", *p);
		p++;
	}
}

        

        一维数组传参不需要写大小,写多少都不影响。这是因为数组传参时,本质上传的指针,而其读取范围只跟实参数组本来大小有关,跟形参无关。 

a[10];
void a(arr[]){
}
void a(arr[10]){
}
void a(arr[100]){
}

指针形式

void (int* p){
}

二维数组的传参

	int a[3][5] = { { 1,2,3 },{2,3,4,5} };
	test(a);

因为数组名表示的是数组的首地址,所以a表示,a[3][5]的首地址,a[0]即表示第一行数组(所有数)的指针。其类型表示为 int (*p)[5]  

下面的程序,传入二维数组的列指针p,输出每个元素

p+i表示第i行数组地址,  *(p+i)表示i行第0个 ,  *(p+i)+j表示i行第j个的地址

void test(int (*p)[5]) {
	int i = 0, j = 0;
	for (i=0; i < 3; i++) {
		for (j=0; j < 5; j++) {//p+i表示第i行数组地址,*(p+i)表示i行第0个,*(p+i)+j表示i行第j个的地址
			printf("%d", *(*(p + i)+ j));
			//printf("%d",p[i][j]);编译器最终把这个写法转为上一条写法
		}
		printf("\n");
	}
}

 输出

12300
23450
00000

可以对地址变量取地址,但不能对地址取地址


void te(char** a)
{

}
int main(){
    char d = 'a';
	char* w = &d;
	char** s = &w;

	te(s);
	te(&(&d));//错误
}

二维数组指针传参不同形式

void t0(int* p) {
	//报错,因为是按一个int类型的指针传进来的 
	//printf(p[1][2]);

	printf("t0函数:\n%d\n", *(p + 2));//拿到3,也就是[0][2]的位置
	printf("%d\n", *(p + 5));//拿到6,说明数组元素隔了一定字节大小放置
	//printf("%d\n", *(p + 6));
}
void t3(int(*p)[5]) {
	//每一行5个
	printf("t3函数\n%d\n",p[0][2]);//不报错,因为类型是一行的指针


	printf("%d\n", **(p + 1));//2次解引用拿一个int,等价于上面的*(p+5)
}
void t33(int(*p)[3][5]) {
	//传的是一整个数组,是3行五列
}
int main() {
	//t();

	int arr[3][5] = { {1,2,3 }, {6,5,4} };
	t0(arr);//传arr第一行,但是被转成了int*
	t3(arr);//传arr第一行,解引用可以得到int*
	t33(&arr);//可以传整个数组
}

输出

t0函数:
3
6
t3函数
3
6


函数指针

        函数的地址同样可以用取地址符号拿到

int Add(int c,int a){

}

&Add;

其中&Add也可以写成Add

函数地址的类型, 首先指针 *pf,参数类型为int ,int ,返回类型为int

所以 函数指针变量: int (*pf)(int,int) =Add

通过地址找/调用函数,同样可以通过解引用实现:

 int num = (*pf) (3,233);

可以写成

 int num = pf (3,233);

因为,pf就等于*pf ,同&Add等于Add。

再看个例子:

void (*signal(int , void(*)(int)))(int);

函数名+参数

signal(int , void(*)(int))

去掉后就是函数的类型,返回值为函数指针类型

void (*)(int);
typedef void (*tf) (int);//重命名,但名字要在*后面
tf signal(int, tf);

函数指针数组

        名字后面加方块就构成数组

    void t1(int a) {

    }

    void t2(int b) {

    }	
    void (*a1)(int) = t1;
    void (*a2)(int) = t2;
    void (*a[])(int) = { a1,a2 };
	//void (*)(int);去掉函数名,方块就是类型

指向函数指针数组的指针

再看看如何表示指向函数指针数组的指针

int Arr1[3]={1,2,3};
int* Arr2[3];

指向Arr1的指针: 先声明这个指针(*pArr1),指向的是数组,(*pArr1)[3],再去掉名字Arr1得到类型,即 int (*pArr1) [3]

指向Arr2的指针:同样,声明(*pArr2),指向的是数组,(*pArr1)[3],去名得类型,即int* (*pArr2)[3] 。

int (*pArr1) [3] =&Arr1;
int* (*pArr2) [3] =&Arr2;

 同样的函数指针数组,现有个函数指针数组 ,要表示指向函数指针数组的指针

void (*pfArr[4])(int);

指向ppfArr的指针: 先声明这个指针(*ppfArr),指向的是数组,(*ppfArr)[4],去名得类型,

即:void(*(*ppfArr)[4])(int)

void(*(*ppfArr)[4])(int)=&pfArr;

当然可以通过解引用拿到ppfArr数组的地址,再通过方块[]/解引用方式访问每一个元素。

回调函数

        下面用加减函数案例做对比

int Add(int x, int y) {
	return x + y;
}

int Sub(int x, int y) {
	return x - y;
}
int main() {
	int input;
	int x, y;
	int ret = 0;
	scanf("%d", &input);
	switch (input)
	{
	case 1:
		scanf("%d %d", &x, &y);
		ret = Add(x, y);
		printf("%d\n", ret);
		break;
	case 2:
		scanf("%d %d", &x, &y);
		ret = Sub(x, y);
		printf("%d\n", ret);
		break;
	default:
		break;
	}
}

        根据每次输入,判断要执行什么函数,但是重复的函数有很多。特别是当同类函数越多的时候,比如再增加乘除运算的函数,重复代码也越多。这里就可以用回调函数减少代码量。(用上面的函数指针数组也可以)

int Add(int x, int y) {
	return x + y;
}
int Sub(int x, int y) {
	return x - y;
}
void ret(int (*pf)(int ,int )) {
	int ret = 0;
	int x, y;
	scanf("%d %d", &x, &y);
	ret=(*pf)(x, y);//可以把*号去掉
	printf("%d\n", ret);
}

int main() {
	int input;
	scanf("%d", &input);
	switch (input)
	{
	case 1:
		ret(&Add);
		break;
	case 2:
		ret(&Sub);
		break;
	default:
		break;
	}
}

void*

int a=0;
float* pa=&a;

生成时会警报        

函数指针.c(58,1): warning C4133: “初始化”: 从“int *”到“float *”的类型不兼容
	int a = 0;
	//float* pa = &a;
	void* pb = &a;

用void* 就不会报错,这是因为void* 是不确定指向类型的指针,任何类型指针都能存进void*里。但是因为void*没有访问权限,解引用/跟操作数+-运算会有问题,因此解引用需要先转换类型。

函数指针的应用:

        实现类似,可以排序任意类型的库函数qsort的排序函数bubble sort。把比较规则封装成函数,每次只要改变函数指针,就能排序不同类型的数据。

int cmp_by_int(const void* e1, const void* e2) {
	return (*(int*)e1 - *(int*)e2);
}
int cmp_by_short(const void* e1, const void* e2) {
	return (*(short*)e1 - *(short*)e2);
}
int cmp_by_short_reverse(const void* e1, const void* e2) {
	return (*(short*)e2 - *(short*)e1);
}

//按一个字节去交换
void Swap(char* b1, char* b2,int width) {
	//交换[宽度]次
	while (width--) {
		char tmp = *b1;
		*b1 = *b2;
		*b2 = tmp;
		//8 : 08 00 00 00
		//7 : 07 00 00 00
		b1++;
		b2++;
	}
}


//数组地址,元素个数,类型大小(宽度),比较规则,利用回调函数回去找规则
void bSort(void* base,int n,int width,int (*cmp)(const void* e1,const void* e2)){
	for (int i = 0; i < n-1;i++) {
		//j从0开始
		for (int j = 0; j < n-1-i ;j++) {
			//这里只要得到,元素的首地址传给比较函数,不需要元素的整个地址
			//如int不用传int*
			//if(arr[j]-arr[j+1]>0)比较arr[j],arr[j+1]
			if (cmp((char*)base + width*j, (char*)base + width*(j+1))>0) {
				Swap((char*)base + width * j, (char*)base + width * (j + 1),width);
			}
		}
	}
}

int main()
{
	int arr[] = { 1,4,2 ,5,3,66,61,6};
	bSort(arr, sizeof(arr)/sizeof(arr[0]), sizeof(int), cmp_by_int);
	//debug测试没有问题,顺序排序
	short arr2[] = { 1,4,2 ,5,3,6,21,66 };
	bSort(arr2, sizeof(arr2) / sizeof(arr2[0]), sizeof(short), cmp_by_short);
	//debug里看
	bSort(arr2, sizeof(arr2) / sizeof(arr2[0]), sizeof(short), cmp_by_short_reverse);
	return 0;
}

练习

一、

int a[3][4] = { 0 };
//&a:整个数组的地址;a:第一行的地址;a[0]第一行第一个的地址;a[0][0]第一行第一个地址的值
// 只有上面名字这些被sizeof操作时,才会被当作整个数组的大小
// 
//&a+1:跳过整个数组的下一个地址;a+1:第二行的地址;
//a[0]+1第一行第二个的地址;a[0][1]第一行第二个地址的值
// 
//*(&a+1):跳过整个数组的下一个地址的值(随机值); *(a+1):第一行第二个的地址;
//*(a[0]+1)第一行第二个地址的值;*(a[0][1])不是地址不能解引用

注意a是首行地址,a[0]/*a是第一行第一个的地址

二、

	int a[4] = { 1,2,3,4 };
	int* p1 = (int*)(&a + 1);
	//按数组跳
	int* p2 = (int*)((int)a + 1);
	//把地址加一个字节
	//假设		01 00 00 00 02 00 00 00
	//p2指的就是   ↑01后的第一个00
	printf("%0X,%0X\n", *(p1 - 1), *p2);
	//p1是int*,步长为4,往前走一个int拿到a[3]
	//p2存的是 00 00 00 02  即0x02000000
	printf("%d,%d", *(p1 - 1), *p2);
	//转成10进制打印

三、

int main()
{
  int a[5][5];
  int(*p)[4];
  p = a;
  printf( "%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);
  return 0;
}

结果

FFFFFFFC,-4

图解:

0x00CFF960 -0x00CFF960 

按地址打印就是FFFFFFFC,按%d打印就是-4

四、

int main()
{
char *c[] = {"ENTER","NEW","POINT","FIRST"};
char**cp[] = {c+3,c+2,c+1,c};
char***cpp = cp;
printf("%s\n", **++cpp);
printf("%s\n", *--*++cpp+3);
printf("%s\n", *cpp[-2]+3);
printf("%s\n", cpp[-1][-1]+1);
return 0;
}

图解:

 cpp[-2] 等价于*(cpp-2)

结果

POINT
ER
ST
EW

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值