拿捏指针(二)

2024-10-09 20:13:12 浏览数 (2)

前言

前面我们已经讲了,C语言的第一篇《拿捏指针(一)》,接下里我们继续深入的来了解指针。

1.0 数组与指针

1.1 数组名的理解

我们之前学习了,数组知道了数组arr就是首元素的地址,但却不理解&arr和&arr[0]的区别,脑子还是有点乱,今天我们一次给它讲明白。

代码语言:javascript复制
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	printf("&arr[0] = %pn", &arr[0]);//&arr[0]表示首元素地址
	printf("&arr    = %pn", arr);//&arrb表示整个数组的地址
	printf("arr     = %pn", arr);//arr表示一维数组数组名
	return 0;
}

输出结果:

&arr[0] = 00AFF754 &arr = 00AFF754 arr = 00AFF754

从输出的结果看,&arr[0],&arr和arr这三的地址都是一样的,那么就能得出结论,数组名就是首元素的地址。

那么我们再来看看一下的代码

代码语言:javascript复制
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	printf("&arr[0] = %zd字节n", sizeof(&arr[0]));
	printf("&arr    = %zd字节n", sizeof(&arr));
	printf("arr     = %zd字节n", sizeof(arr));
	return 0;
}

输出结果:

&arr[0] = 4字节 &arr = 4字节 arr = 40字节

我们能看出来,&arr[0],&arr和arr尽管都是打印首元素的地址,但还是有所区别的

如果arr是数组⾸元素的地址,那输出应该的应该是4/8才对,为什么打印的确实40?

其实数组名就是数组⾸元素(第⼀个元素)的地址是对的,但是有两个例外:

  • sizeof(数组名),sizeof中单独放数组名,这⾥的数组名表⽰整个数组,计算的是整个数组的⼤⼩, 单位是字节
  • &数组名,这⾥的数组名表⽰整个数组,取出的是整个数组的地址(整个数组的地址和数组⾸元素 的地址是有区别的)

除此之外,任何地⽅使⽤数组名,数组名都表⽰⾸元素的地。

1.2 指针数组与数组指针

相信看到指针数组和数字指针每个人都很头疼,不知道哪个是数组指针,哪个是指针数字。我带大家来看看吧。

1.2.1 指针数组

指针数组:存放指针的数组。

指针数组是指针还是数组,我们来类比一下,就知道。

整形数组和字符串数组

整形数组是数组,字符串数组是数组,那么指针数组自然就是指针了。指针数组的每个元素都是⽤来存放地址(指针)的。

1.2.2 数组指针

数组指针是数组还是指针呢?

代码语言:javascript复制
int(*arr)[10];

p先和*结合,说明p是⼀个指针变量变量,然后指着指向的是⼀个⼤⼩为10个整型的数组。所以 p是⼀个指针,指向⼀个数组,叫数组指针

值得注意的是,[]的优先级大于*,所以必须得加括号。

代码语言:javascript复制
int arr[10] = {0};
int(*p)[10] = &arr;

所以我们知道了,int(*)[10] = &arr这两个的类型其实是相同的。

数组指针类型解析: int (*p) [10] = &arr; intp p指向的数组的元素类型 (*p)p是数组指向变量名 [10] p指向数组的元素个数


2.0 数组传参

2.1 一维数组传参的本质

一维数组arr表示首元素的地址,那么一维数组传参传的是地址还是一个数值呢?

代码语言:javascript复制
void  Print(int * arr)
{
	int sz = sizeof(arr) / sizeof(arr[0]);
	for (int i = 0; i < sz; i  )
	{
		printf("%d ", arr[i]);
	}
}
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	Print(arr);
	return 0;
}

arr是首元素地址,既然是地址我们就能用指针接收,也可以用 int arr[10]

打印结果:

1

这里我们预期的打印是1 2 3 4 5 6 7 8 9 10,可是输出的确实1,这就印证了我们的猜想,数组传参传的也是首元素的地址。

所以arr的元素个数我们还是要在main函数里初始化,在Print函数里是一个指针4个字节(32位下,如果是64位下是8个字节),当然sz=1。

2.2 二维数组传参的本质

我们知道了一维数组传参传的是首元素地址,那么二维数组也是同理的。

代码语言:javascript复制
void  Print(int(*arr)[5])
{
	for(int j = 0; j < 3;j  )
	{
		for (int i = 0; i < 5; i  )
		{
			printf("%d ", arr[j][i]);
		}
		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);
	return 0;
}

arr[3][5]我们也可以用数组的形式接收,int arr[3][]列数可以省略,而行数却不可以

输出结果

1 2 3 4 5 2 3 4 5 6 3 4 5 6 7

这⾥实参是⼆维数组,形参也写成⼆维数组的形式,那还有什么其他的写法吗

⾸先我们再次理解⼀下⼆维数组,⼆维数组起始可以看做是每个元素是⼀维数组的数组,也就是⼆维 数组的每个元素是⼀个⼀维数组。那么⼆维数组的⾸元素就是第⼀⾏,是个⼀维数组。

三行五列,第一行相当于arr[0],第二行相当于arr[2],第三行相当于arr[3];,第⼀⾏的地址是⼀维数组的类型就是数组指针类型 int [5] ,所以第⼀⾏的地址的类 int(*)[5] 那就意味着⼆维数组传参本质上也是传递了地址,传递的是第⼀ ⾏这个⼀维数组的地址。

总结:

  • 数组传参的本质是传递了数组首元素的地址,所以形参访问的数组和实参的数组是同一个数组的。
  • 形参的数组是不会单独再创建数组空间的,所以形参的数组是可以省略掉数组大小的。

实际上还可以用指针的方式打印出来

代码语言:javascript复制
void test(int(*p)[5])
{
	int i = 0;
	int j = 0;
	for (i = 0; i < 3; i  )
	{
		for (j = 0; j < 5; j  )
		{
			printf("%d ", *(*(p   i)   j));
		}
		printf("n");
	}
}

因为二维数组在内存中是连续存放的,且arr是一个数组arr[0]里面有五个元素{1,2,3,4,5}; 我们可以把它想像成一维数组。

(arr 1)是首元素的地址,(arr 1) 1是首元素第一个的地址。

2.2.1 二维数组模拟

代码语言:javascript复制
int main()
{
	int arr1[5] = { 1,2,3,4,5 };
	int arr2[5] = { 2,3,4,5,6 };
	int arr3[5] = { 3,4,5,6,7 };
	int* p[3] = {arr1,arr2,arr3};//数组名是数组⾸元素的地址,类型是int*的,就可以存放在p数组中
	for (int i = 0; i < 3; i  )
	{
		for (int j = 0; j < 5; j  )
		{
			printf("%d ", p[i][j]);
		}
		printf("n");
	}
	return 0;
}

输出结果:

1 2 3 4 5 2 3 4 5 6 3 4 5 6 7

p[i]是访问parr数组的元素,parr[i]找到的数组元素指向了整型⼀维数组,parr[i][j]就是整型⼀维数 组中的元素。 上述的代码模拟出⼆维数组的效果,实际上并⾮完全是⼆维数组,因为每⼀⾏并⾮是连续的。


3.0 二级指针

二级指针跟一级指针的原理很相似

代码语言:javascript复制
	int a = 10;
	int* pa = &a;

一级指针是将变量的地址放入一级指针变量里面,二级就是将一级的指针变量的地址放入二级的指针变量。

代码语言:javascript复制
	int a = 10;
	int* pa = &a;//一级指针
	int** ppa = &pa;//二级指针

这就是一个二级指针,我们画一个图了解。

当我们解引用的时候,*ppa访问达到的指针变量pa的内容(a的地址),*pa继续解引用访问到的是a的内容

这就是二级指针,依次类推三级指针,四级指针,五级指针都是这样的。

4.0 字符指针变量

代码语言:javascript复制
//代码1
int main()
{
	char ch = 'w';
	char* pc = &ch;
	*pc = 'w';
	return 0;
}

//代码2
int main()
{
	char* pc = "hello world";
	return 0;
}

代码2,常常被人误解,以为 是将“hello world”一整串的地址放入了pc变量中,其实只是将"h"的地址放入了pc变量,这样的字符串我们称为常量字符串。

我们来看你一段代码

代码语言:javascript复制
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 samen");
	else
		printf("str1 and str2 are not samen");
	if (str3 == str4)
		printf("str3 and str4 are samen");
	else
		printf("str3 and str4 are not samen");
	return 0;
}

打印结果:

str1 and str2 are not same str3 and str4 are same

为什么会出现这样的结果呢?

这是因为str3和str4指向的是⼀个同⼀个常量字符串。C/C 会把常量字符串存储到单独的⼀个内存区域, 当⼏个指针指向同⼀个字符串的时候,他们实际会指向同⼀块内存。但是⽤相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。所以str1和str2不同,str3和str4相同。

5.0 函数指针变量

上面我们已经学习了数组指针变量,字符串指针变量,同样函数指针变量也是类似的。

函数指针变量应该是⽤来存放函数地址的,未来通过地址能够调⽤函数的

代码语言:javascript复制
void test()
{
	printf("hehen");
}
int main()
{
	printf("test:  %pn", test);
	printf("&test: %pn", &test);
	return 0;
}

输出结果:

test: 00DE13CA &test: 00DE13CA

这里函数的取地址与数组是相似的,都是首元素的地址,&函数名的方法获得函数的地址。

如果我们需要将函数的地址存放起来,,就得创建函数指针变量咯,函数指针变量的写法其实和数组指针 ⾮常类似。

代码语言:javascript复制
void test()
{
	printf("hehen");
}
void (*pf1)() = &test;
void (*pf2)() = test;
int Add(int x, int y)
{
	return x   y;
}
int(*pf3)(int, int) = Add;
int(*pf3)(int x, int y) = &Add;//x和y写上或者省略都是可以的

调佣指针指向的函数

代码语言:javascript复制
int add(int x,int y)
{
	return x   y;
}
int main()
{
	int a = 0;
	int b = 0;
	int(*pf)(int, int) = add;//存add的地址
	printf("%dn", (*pf)(2, 3));
	printf("%dn", pf(5, 5));
	return 0;
}

输出结果:

5 10

这就是函数变量的用途。

0 人点赞