指针(下)

目录

1. 数组名的理解

2. 使⽤指针访问数组

 3. ⼀维数组传参的本质

4. ⼆级指针

5.指针数组

6. 指针数组模拟⼆维数组

1. 字符指针变量

2. 数组指针变量

2.1 数组指针变量是什么?

2.2 数组指针变量怎么初始化

 7. assert断⾔

8. 指针的使⽤和传址调⽤

8.1 strlen的模拟实现

8.2  传值调⽤和传址调⽤

8.2.1传值调用

8.2.2 传址调用


1. 数组名的理解

在上⼀个章节我们在使⽤指针访问数组的内容时,有这样的代码:

int arr[10] = {1,2,3,4,5,6,7,8,9,10}; int *p = &arr[0];

 这⾥我们使⽤ &arr[0] 的⽅式拿到了数组第⼀个元素的地址,但是其实数组名本来就是地址,⽽且 是数组⾸元素的地址,我们来做个测试。

#include

int main() {

int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };

printf("&arr[0] = %p\n", &arr[0]);

printf("arr = %p\n", arr);

return 0; }

 输出结果:

我们发现数组名和数组⾸元素的地址打印出的结果⼀模⼀样,数组名就是数组⾸元素(第⼀个元素)的地 址。

这时候有同学会有疑问?数组名如果是数组⾸元素的地址,那下⾯的代码怎么理解呢?

#include

int main() {

int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };

printf("%d\n", sizeof(arr));

return 0; }

输出的结果是:

40

如果arr是数组⾸元素的地址,那输出应该的应该是4/8才对。

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

sizeof(数组名),sizeof中单独放数组名,这⾥的数组名表⽰整个数组,计算的是整个数组的⼤⼩, 单位是字节。

• &数组名,这⾥的数组名表⽰整个数组,取出的是整个数组的地址(整个数组的地址和数组⾸元素的地址是有区别的)

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

这时有好奇的同学,再试⼀下这个代码:

#include

int main()

{ int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };

printf("&arr[0] = %p\n", &arr[0]);

printf("arr = %p\n", arr);

printf("&arr = %p\n", &arr);

return 0; }

三个打印结果⼀模⼀样,这时候⼜纳闷了,那arr和&arr有啥区别呢?

#include int main() {

int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };

printf("&arr[0] = %p\n", &arr[0]);//int*

printf("&arr[0]+1 = %p\n", &arr[0]+1);//+4


printf("arr = %p\n", arr);//int*

printf("arr+1 = %p\n", arr+1);//+4


printf("&arr = %p\n", &arr);

printf("&arr+1 = %p\n", &arr+1);

return 0; }

输出结果:

输出结果:

这⾥我们发现&arr[0]和&arr[0]+1相差4个字节,arr和arr+1 相差4个字节,是因为&arr[0] 和 arr 都是⾸元素的地址,+1就是跳过⼀个元素

但是&arr 和 &arr+1相差40个字节,这就是因为&arr是数组的地址,+1 操作是跳过整个数组的。

2. 使⽤指针访问数组

有了前⾯知识的⽀持,再结合数组的特点,我们就可以很⽅便的使⽤指针访问数组了

#include<stdio.h>{
int main()
{
	int arr[2] = { 0 };
	int sz = sizeof(arr) / sizeof(0);
	int *p = arr;
	for (int i = 0; i < sz; i++) {
		scanf_s("%d", p + i);
	}
	for (int i = 0; i < sz; i++) {
		printf("%d", *(p + i));
		printf("%d", *(arr + i));
		printf("%d",arr[i]);
		//arr[i]=*(arr+i);
		//p[i]=*(p+i);
	}

}

数组元素的访问在编译器处理的时候,是转换成⾸元素的地址+偏移量求出元素的地址,然后解引⽤来访问的。

 3. ⼀维数组传参的本质

#include <stdio.h>
void test(int arr[])
{
 int sz2 = sizeof(arr)/sizeof(arr[0]);
 printf("sz2 = %d\n", sz2);
}
int main()
{
 int arr[10] = {1,2,3,4,5,6,7,8,9,10};
 int sz1 = sizeof(arr)/sizeof(arr[0]);
 printf("sz1 = %d\n", sz1);
 test(arr);
 return 0;
}

输出的结果:

我们发现在函数内部是没有正确获得数组的元素个数。 这就要学习数组传参的本质了,上个⼩节我们学习了:数组名是数组⾸元素的地址;那么在数组传参 的时候,传递的是数组名,也就是说本质上数组传参本质上传递的是数组⾸元素的地址。

所以函数形参的部分理论上应该使⽤指针变量来接收⾸元素的地址。那么在函数内部我们写 sizeof(arr) 计算的是⼀个地址的⼤⼩(单位字节)⽽不是数组的⼤⼩(单位字节)。正是因为函 数的参数部分是本质是指针,所以在函数内部是没办法求的数组元素个数的。

void test(int arr[])//参数写成数组形式,本质上还是指针
{

 printf("%d\n", sizeof(arr));
}
void test(int* arr)//参数写成指针形式
{
 printf("%d\n", sizeof(arr));//计算⼀个指针变量的⼤⼩
}
int main()
{
 int arr[10] = {1,2,3,4,5,6,7,8,9,10};
 test(arr);
 return 0;
}

总结:⼀维数组传参,形参的部分可以写成数组的形式,也可以写成指针的形式

4. ⼆级指针

指针变量也是变量,是变量就有地址,那指针变量的地址存放在哪⾥?

这就是 ⼆级指针 。

对于⼆级指针的运算有:

• *ppa 通过对ppa中的地址进⾏解引⽤,这样找到的是 pa ,*ppa 其实访问的就是 pa .
 

int b = 20;

*ppa = &b;

//等价于 pa = &b;

 • **ppa 先通过 *ppa 找到 pa ,然后对 pa 进⾏解引⽤操作: *pa ,那找到的是 a .
 

**ppa = 30;

//等价于*pa = 30;

//等价于a = 30;

5.指针数组

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

我们类⽐⼀下

整型数组,是存放整型的数组(数组中的每个元素都是整形类型)

字符数组,是存放字符的数组(数组中的每个元素都是字符类型)。

指针数组,是存放字符的数组(数组中的每个元素都是指针类型)。指针数组的每个元素都是⽤来存放地址(指针)的。

指针数组的每个元素是地址,⼜可以指向⼀块区域。

6. 指针数组模拟⼆维数组

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

		}
		printf("\n");
	}
	return 0;
}

结果展示

数组名是数数组首元素的地址,类型数int*的,就可以存放在parr数组中

parr数组的画图演⽰

parr[i]是访问parr数组的元素,parr[i]找到的数组元素指向了整型⼀维数组,parr[i][j]就是整型⼀维数组中的元素。

上述的代码模拟出⼆维数组的效果,实际上并⾮完全是⼆维数组,因为每⼀⾏并⾮是连续的。

1. 字符指针变量

在指针的类型中我们知道有⼀种指针类型为字符指针 char* ;

⼀般使⽤:

int main()
{
 char ch = 'w';
 char *pc = &ch;//pc就是字符指针

const char*p="ABCDEF";//不是把字符串abcdef放在p中,二十把第一个字符的地址放在p中
//1.可以把字符串想像成一个字符数组,但是这个字符数组是不能修改的
//2.当字符串出现在表达式中时,他的值是第一个字符的地址
 rturn 0;
}

本质是把字符串ABCDEF\0的⾸字符的地址放到了p中。

可以把字符产想想成一个字符数组,但是这个字符数组是不能修改的

当常量字符串(eg:"abcdef")出现在表达式中的时候,他的值是第一个字符的地址

《剑指offer》中收录了⼀道和字符串相关的笔试题,我们⼀起来学习⼀下:

#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;
}
1

这⾥str3和str4指向的是⼀个同⼀个常量字符串。C/C++会把常量字符串存储到单独的⼀个内存区域, 当⼏个指针指向同⼀个字符串的时候,他们实际会指向同⼀块内存。

但是⽤相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。

所以str1和str2不同,str3和str4相同。

2. 数组指针变量

2.1 数组指针变量是什么?

之前我们学习了指针数组,指针数组是⼀种数组,数组中存放的是地址(指针)。我们已经熟悉: • 整形指针变量: int * pint; 存放的是整形变量的地址,能够指向整形数据的指针。

• 浮点型指针变量: float * pf; 存放浮点型变量的地址,能够指向浮点型数据的指针。

那数组指针变量应该是:存放的应该是数组的地址,能够指向数组的指针变量。

下⾯代码哪个是数组指针变量?

int *p1[10]; int (*p2)[10]; 1 2 思考⼀下:p1, p2分别是什么?

数组指针变量

int *p1[10];
int (*p2)[10];
  1. p1是数组,数组10个元素,每个元素的类型是int*,所以p1是指针数组
  2. p2是指针,指针10个元素,每个元素的类型是int,所以p2是数组指针

这⾥要注意:[]的优先级要⾼于*号的,所以必须加上()来保证p先和*结合。

2.2 数组指针变量怎么初始化

数组指针变量是⽤来存放数组地址的,那怎么获得数组的地址呢

int arr[10] = {0};
&arr;//得到的就是数组的地址

如果要存放整个数组的地址,就得存放在数组指针变量中,如下:

1 int(*p)[10] = &arr;

我们调试也能看到 &arr 和 p 的类型是完全⼀致的。

我们调试也能看到 &arr 和 p 的类型是完全⼀致的。

数组指针类型解析:

int            (*p)             [10] = &arr;

|                 |                   |

|                 |                   |

|                 |                  p指向数组的元素个数

|                 p是数组指针变量名

p指向的数组的元素类型

 7. assert断⾔

assert.h 头⽂件定义了宏 assert() ,⽤于在运⾏时确保程序符合指定条件,如果不符合,就报错终⽌运⾏。这个宏常常被称为“断⾔”。

1 assert(p != NULL);

上⾯代码在程序运⾏到这⼀⾏语句时,验证变量 p 是否等于 NULL :

如果确实不等于 NULL ,程序继续运⾏,否则就会终⽌运⾏,并且给出报错信息提⽰。

assert() 宏接受⼀个表达式作为参数。

如果该表达式为真(返回值⾮零), assert() 不会产⽣任何作⽤,程序继续运⾏。

如果该表达式为假(返回值为零), assert() 就会报错,在标准错误流stderr 中写⼊⼀条错误信息,显⽰没有通过的表达式,以及包含这个表达式的⽂件名和⾏号

assert() 的使⽤对程序员是⾮常友好的,使⽤ assert() 有⼏个好处:

1.能⾃动标识⽂件和出问题的⾏号。

2.⽆需更改代码就能开启或关闭 assert() 的机制。

如果已经确认程序没有问题,不需要再做断⾔,就在 #include 语句的前⾯,定义⼀个宏 NDEBUG 

#define NDEBUG

#include <assert.h>

然后,重新编译程序,编译器就会禁⽤⽂件中所有的 assert() 语句。

如果程序⼜出现问题,可以移 除这条 #define NDBUG 指令(或者把它注释掉),再次编译,这样就重新启⽤了 assert() 语句。

assert() 的缺点是:

1.增加了程序的运⾏时间。

⼀般我们可以在 Debug 中使⽤,在 Release 版本中选择禁⽤ assert 就⾏,在 VS 这样的集成开 发环境中,在 Release 版本中,直接就是优化掉了。

  1. 在debug版本写有利于程序员排查问题,
  2. 在 Release 版本不影响⽤⼾使⽤时程序的效率

8. 指针的使⽤和传址调⽤

8.1 strlen的模拟实现

库函数strlen的功能是求字符串⻓度,统计的是字符串中 \0 之前的字符的个数。

函数原型如下:

1 size_t strlen ( const char * str );

#include<stdio.h>
#include<string.h>
size_t my_strlen(char* s) {
	int count = 0;
	while (*s != 0) {
		count++;
		s++;

	}return count;
}

int main() {
	char arr[] = "abcdef";
	size_t len=	my_strlen(arr);
	printf("%zd", len);
	return 0;
}

注意事项:

1.定义字符长度为Int,可能会丢失数据,应该size_t.

2.size_t len = my_strlen(arr) 传递是数组元素的首地址,即传址调用

3.

1 size_t strlen ( const char * str );

优化代码,使*s,初始代码位置无法改变

4.size_t == unsighed int

参数s tr接收⼀个字符串的起始地址,然后开始统计字符串中 \0 之前的字符个数,最终返回⻓度

。 如果要模拟实现只要从起始地址开始向后逐个字符的遍历,只要不是 \0 字符,计数器就+1,这样直 到 \0 就停⽌。

参考代码如下:

8.2  传值调⽤和传址调⽤

        需要指针    不需要指针

学习指针的⽬的是使⽤指针解决问题,那什么问题,⾮指针不可呢?

例如:写⼀个函数,交换两个整型变量的值 ⼀番思考后,我们可能写出这样的代码:

8.2.1传值调用

#include<stdio.h>
void Swap1(int x, int y) {
	int tmp = x;
	x = y;
	y = tmp;
}

int main() {
	int a = 0;
	int b = 0;
	scanf_s("%d %d", &a, &b);
	printf("交换前:a=%d b=%d\n", a, b);
	Swap1(a, b);
	//传值调用
	printf("交换后:a=%d b=%d\n", a, b);
	return 0;
}

当我们运⾏代码,结果如下:

我们发现其实没产⽣交换的效果,这是为什么呢?

1.我们发现在main函数内部,创建了a和b,在调⽤ Swap1函数时,将a和b传递给了Swap1函数。

2.在Swap1函数内部创建了形参x和y接收a和b的值

3.x和y确实接收到了a和b的值,不过x的地址和a的地址不 ⼀样,y的地址和b的地址不⼀样,相当于x和y是独⽴的空间,那么在Swap1函数内部交换x和y的值, ⾃然不会影响a和b;当Swap1函数调⽤结束后回到main函数,a和b的没法交换。

4.Swap1函数在使⽤的时候,是把变量本⾝直接传递给了函数,这种调⽤函数的⽅式我们之前在函数的时候就知道了,这 种叫传值调⽤。

结论:实参传递给形参的时候,形参会单独创建⼀份临时空间来接收实参,对形参的修改不影响实 参。 所以Swap是失败的了。


那怎么办呢?

我们现在要 解决的就是当调⽤Swap函数的时候,Swap函数内部操作的就是main函数中的a和b,直接 将a和b的值交换了。

那么就可以使⽤指针了,在main函数中将a和b的地址传递给Swap函数,Swap 函数⾥边通过地址间接的操作main函数中的a和b,并达到交换的效果就好了。

8.2.2 传址调用

#include<stdio.h>
void Swap2(int* p1, int* p2) {
	int teme = *p1;
	*p1 = *p2;
	*p2 = teme;
}

int main() {
	int a = 0;
	int b = 0;
	scanf_s("%d %d", &a, &b);
	printf("交换前:a=%d b=%d\n", a, b);
	Swap2(&a, &b);
	//传值调用
	printf("交换后:a=%d b=%d\n", a, b);
	return 0;
}

我们可以看到实现成Swap2的⽅式,顺利完成了任务,这⾥调⽤Swap2函数的时候是将变量的地址传递给了函数,这种函数调⽤⽅式叫:传址调⽤。

传址调⽤,可以让函数和主调函数之间建⽴真正的联系,在函数内部可以修改主调函数中的变量;所以未来函数中只是需要主调函数中的变量值来实现计算,就可以采⽤传值调⽤。如果函数内部要修改 主调函数中的变量的值,就需要传址调⽤


如果您觉得有失偏颇请您在评论区指正,如果您觉得不错的话留个好评再走吧!!

您的鼓励就是对我最大的支持!  ! !

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值