C语言-指针进阶详解

指针基本概念

在初阶指针中我们了解到一些指针的基本概念:
1.指针就是个变量,用来存放地址,地址唯一标识一块内存

2.指针的大小是固定的4/8个字节(32位/64位平台)

3指针是有类型的,指针的类型决定了指针的+整数的步长,指针解引用时操作的权限

例如,char*类型的指针解引用,它的权限(或步长)在32位平台下只有一个字节,而int*型的指针在32位平台下的权限(步长)为4个字节。

1.字符指针

#include<stdio.h>
int main()
{
	char ch = 'w';
	char* pc = &ch;
	*pc = 'w';//这里的pc就是一个字符指针
	return 0;
}

 这里的pc就是一个字符指针,可以通过解引用pc来对w进行操作。

#include<stdio.h>
int main()
{
char arr[]="abcdef";//数组内容为字符串
//char *p="abcdef";//指针存储的内容为字符串"abcdef"的首个字母的地址
const char *p="abcdef";//abcdef为常量字符串,需要加const来修饰
return 0;
}

 这两个定义有什么区别, char arr[]='' abcdef''只是一个对数组普通的初始化,将字符串''abcdef''放数组arr中

char *p="abcdef"中,abcdef是常量字符串,其前面需要用const来修饰,那么第二个操作是不是直接将整个字符串保存在字符指针中了呢?答案是否定的。

该操作实际上是将常量字符串''abcdef''的首个字符的地址保存到字符指针*p中

而我们应该如何验证呢?

#include<stdio.h>
int main()
{
	char arr[] = "abcdef";
	const char* p = "abcdef";
	printf("%s\n",p);
	printf("%c",*p);
	return 0;
}

 若此时p中存储的是字符串"abcdef"首字符的地址,那么第一个printf打印的结果应为"abcdef"

第二个printf中对字符指针p进行*解引用的结果应该是字符串首字符a。

ef2e6e602e104f79be573c565a2cb493.png

 于是我们得到了结论:该操作实际上是将常量字符串''abcdef''的首字符的地址保存到字符指针*p中

 有这样一道面试题来练手,如下:

#include <stdio.h>
int main()
{
  char str1[] = "hello bit.";
  char str2[] = "hello bit.";
  const char *str3 = "hello bit.";
  const char *str4 = "hello bit.";
  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; 
}

 1ab25603975b46e89894b9de6cf29285.png

可以看到str1与str2是不相同的,而str3与str4是相同的,那么这究竟是什么原因呢?

 char str1[] = "hello bit.";
 char str2[] = "hello bit.";

两个数组的内容是相同的,但是开辟的并不是同一块空间 ,str1和str2都是数组名,代表了数组首元素的地址,而两个数分别开辟了不同的内存空间,所以首元素的地址不同,所以str1与str2不同而对于str3和str4,"hello bit."由const修饰,为常量字符串同时str3与str4都是字符指针,存储的是字符指针"hello bit."的首字符的地址,且其是由const修饰的常量字符串,故str3与str4指向的空间的地址相同,所以str3==str4;

2.指针数组

类比:

整型数组-存放整型的数组;

字符数组-存放字符的数组;

指针数组-存放指针的数组;

#include<stdio.h>
int main()
{
int* arr1[10];//整形指针的数组
char *arr2[4];//一级字符指针的数组
char **arr3[5];//二级字符指针的数组
return 0;
}

使用指针数组模拟实现二维数组

#include<stdio.h>
int main()
{
	int arr1[] = { 1,2,3,4,5 };
	int arr2[] = { 2,3,4,5,6 };
	int arr3[] = { 3,4,5,6,7 };
	int* arr[3] = {arr1,arr2,arr3};
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		int j = 0;
		for (j = 0; j < 5; j++)
		{
			printf("%d", arr[i][j]);
		}
		printf("\n");
	}
	return 0;
}

4252ba41bb284befa008b6a4fc10f0c4.png

用i遍历左边的数组,用j遍历右边的数组 ,结果如下图:

8d8807b8eab7469abbb9039f414e4fba.png

需要特别注意的是,指针数组模拟实现的二维数组与真正的二维数组不同的是,该指针数组中各元素地址的存放不一定是连续的。

3.数组指针

3.1数组指针概念

首先来做一个区分

整形指针-指向整型变量的指针,存放整型变量的地址的指针变量;

字符指针-指向字符变量的指针,存放字符变量的地址的指针变量;

数组指针-指向数组变量的指针,存放数组变量的地址的指针变量;

int*p1[10];//指针数组(p1先与[]结合)
int(*p2)[10];//数组指针(p2先与*运算符结合)

p2是指针指向的是数组,称为数组指针变量。

3.2对数组名的理解

数组名是数组首元素的地址,但是有两个例外,如下

1.sizeof(数组名),这里的数组名不是数组首元素的地址,数组名此时表示整个数组,sizeof(数组名)计算是整个数组的大小,单位是字节。

2.&数组名,这里的数组名表示整个数组,&数组名取出的是整个数组的地址

除此之外,所有地方的数组名都是数组首元素的地址。

#include<stdio.h>
int main()
{
int arr[10]={0};
printf("%p\n",arr);
printf("%p\n",&arr[0]);
printf("%p\n",&arr);
return 0;
}

7499578574e54ccea31012a28a968abf.png

 前两个输出都表示的是数组首元素的地址,而第三个应该表示的是整个数组的地址,但是我们可以观察到三者的输出结果是相同的,

#include<stdio.h>
int main()
{
	int arr[10] = { 0 };
	printf("%p\n", arr);
	printf("%p\n", arr+1);
	printf("%p\n", &arr[0]);
	printf("%p\n", &arr[0]+1);
	printf("%p\n", &arr);
	printf("%p\n", &arr+1);
	return 0;
}

观察运行结果

d0c0140edb49403aa8f009cce4fd94d7.png

 前两个输出+1,都只跳过了4个字节,而第三个+1跳过了40个字节,正好是数组十个元素所占的字节数,由此可见    &数组名    代表的是整个数组的地址。

再观察下图,首元素的地址和数组的地址在占用内存的情况,更加证实了我们上面的说法。

7184e40539464fb1835594375ed8cffb.png

3.3数组指针的使用

int(*p)[10]=&arr; 定义了一个数组指针,p的类型是int (*)[10](但是在监视窗口中,显示的类型为int[10]*,这大概是编译器为了方便我们理解才这样写的吧),下面是数组指针的写法。

#include<stdio.h>
int main()
{
int(*p)[10]=&arr;
return 0;
}

下面是一个使用数组指针打印二维数组的例子。

下面是打印二维数组的一种较为普通常见的写法:二维数组传参,形参是二维数组的形式

#include<stdio.h>
void Print(int arr[3][5], int r, int c)
{
	int i = 0;
	for (i = 0;i < 3; i++)
	{
		int j = 0;
		for (j = 0; j < 5; j++)
		{
			printf("%d", arr[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 };
	Print(arr,3,5);
	return 0;
}

首先,对于二维数组,我们可以这样来理解:二维数组的每一行可以理解为二维数组的一个元素,每一行又是一个一维数组,所以我们可以这样理解二维数组其实是一维数组的数组。除此之外,二维数组的数组名,代表的是首元素地址,而首元素为第一个一维数组,即为第一行的地址,也是第一行一维数组的地址 

762dbbb1e2414704993c122e66e69b45.png

在学习数组指针之后,我们可以尝试二维数组传参,形参是指针的形式。 如下:

#include<stdio.h>
void Print(int (*p)[5], int r, int c)
{
	int i = 0;
	for (i = 0; i < r; i++)
	{
		int j = 0;
		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 };
	//arr为二维数组的数组名,为首元素的地址,指针传参
	Print(arr, 3, 5);
	return 0;
}

 对于这里的*(*(p+i)+j)进行分析,p为传参所传来的二维数组首元素的地址,也就是第一行一维数组的地址,在对其解引用*(p+i),相当于某一个一维数组的数组名,同时在用另外一个数j来对这个一维数组中的其他元素进行遍历获得他们的地址也就是*(p+i)+j,最后得到这个地址后在进行解引用即可获得二维数组中的所有元素*(*(p+i)+j)

一维数组传参,形参的部分可以是数组也可以是指针,同样二维数组传参的部分可以是数组也可以是指针,下面以二维数组为例:
 

#include<stdio.h>
void test1(char arr[3][5], int r, int c)
{}
void test2(char(*p)[5], int r, int c)
{}
int main()
{
	char arr[3][5] = { 0 };
	test1(arr, 3, 5);
	test2(arr, 3, 5);
	return 0;
}

 该二维数组为3行5列,指针传参的时候传过去的是第一行的地址,第一行有五个元素,故为

char(*p)[5]。                                            补充:(p[i]==*(p+i));

4.数组参数,指针参数

4.1一维数组传参

例子如下:

#include<stdio.h>
void test(int arr[]) {}//ok?  ok
void test(int arr[10]){}//ok?   ok
void test(int *arr){}//ok?    ok 
void test2(int *arr[20]){}//ok?    ok
void test2(int **arr){}//ok?    ok
int main()
{
	int arr[10] = { 0 };
	int* arr2[20] = { 0 };
	test(arr);
	test2(arr2);
	return 0;
}

 重点讲解void test2(int **arr){},  arr2的类型是int *[20],而arr2是一个指针,将int *类型的数据传过去,是一个二级指针,用int** arr接受,合理。

4.2二维数组传参

#include<stdio.h>
void test(int arr[3][5]) {}//ok?   √
void test(int arr[][]) {}//ok?   ×
void test(int arr[][5]) {}//ok?   √
void test(int* arr) {}//ok?    ×
void test(int* arr[5]) {}//ok?    ×
void test(int (*arr)[5]) {}//ok?   √
void test(int **arr) {}//ok?    ×
int main()
{
	int arr[3][5] = {0};
	test(arr);
}

二维数组的列是不能够省略的,传参过去后,可以用数组指针来接受,而不是指针数组。

最后一个,传参过去的是数组名,是一级指针,不能够用二级指针来接收。

1.当一个函数的参数部分为一级指针时,函数能接受什么参数?

void test(char *p){}

char ch='2';

char ptr=&ch;

char arr[]="abcdef";

test(&ch);

test(ptr);

test(arr);//传过去的参数为一级指针即可。

2.当函数的参数为二级指针时,可以接受什么参数?

void test(char** p){}

int n=10;

int *p=&n;

int **p=&p;

char* arr[5];

test(&p);

test(pp);

test(arr)

5.函数指针

5.1函数指针的概念

即为指向函数的指针。

int (*)(int,int)就是函数指针的类型


#include<stdio.h>
int Add(int x, int y)
{
	return x + y;
}
int main()
{

    int (*pf)(int,int)=&Add;//pf为函数指针
	return 0;
}

5.2对函数名的李姐

#include<stdio.h>
int Add(int x, int y)
{
	return x + y;
}
int main()
{
	printf("%p\n", &Add);
	printf("%p\n", Add);
	return 0;
}

7af8dd08a0484605956c31b7a3aa252c.png

 函数名和&函数名的运算结果都是函数的地址。

5.3函数指针的使用

练习填写函数指针

给出test函数的定义,用pf表示test。

#include<stdio.h>
void test(char* pc, int arr[10])
{
}
int main()
{
	void (*pf)(char*, int[10]) = &test;
	return 0;
}

函数名的用法

#include<stdio.h>
int Add(int x, int y)
{
	return x + y;
}
int main()
{
	int (*pf)(int, int) = Add;
	int r = Add(3, 5);
	printf("%d\n", r);
	int m = (*pf)(4, 5);//int m=pf(4,5)也可以。
	printf("%d\n", m);
}

2c27b25871b541298c30ab49a16f2d6e.png

    int m = (*pf)(4, 5);也可以实现Add函数,那么是怎么实现的呢?

    int (*pf)(int, int) = Add;首先将Add存在了pf中,这时*pf相当于函数名。

下面有两段interesting的代码

int main()
{
    (*( void (*)() )0 )();
    return 0; 
}

 这一代码其实就是调用0地址处的函数。在调用前将0转换成了void(*)()类型的指针。

这一段代码是这样实现的:

1.将0强制类型转化为void(*)()类型的函数指针

2.调用0地址处的这个函数

第二段interesting的代码:

int main()
{
void(* signal(int,void(*)(int)) )(int);
}

通过分析我们知道了signal是一个函数名,参数是int型和void(*)(int)函数指针类型。

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

signal(int,void(*)int)) ,整个代码是一个函数指针,再看看去掉这一大块之后留下的内容

void(* )(int);就是上面内容的函数返回类型。

 5.4小优化

#include<stdio.h>
typedef unsigned int uint;
typedef int* prt_t;
typedef int(*parr_t)[10];//错误的写法typedef int(*)[10]parr_t;
typedef int (*pf_t)(int, int);//此时pf_t就是这个函数指针的类型
int main()
{
	uint u1;
	prt_t p1;
	int* p2;
	return 0;
}

 再对上面signal函数进行小小优化:

#include<stdio.h>
typedef void(*pf_t)(int);
int main()
{
	void(*signal(int, void(*)(int)))(int);
	pf_t signal(int, pf_t);
	return 0;
}

可以看到啊,非常的简洁啊。

6.函数指针数组

函数指针存放在数组中。

小结

创作不易,留下你的赞可好?

  • 9
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值