详细了解指针2

本文详细解释了C语言中的assert断言功能,探讨了指针的使用,包括strlen函数的模拟、传值与传址调用的区别,以及数组名、二级指针和指针数组的原理。此外,还涉及了一维数组的传参本质和冒泡排序的优化实现。
摘要由CSDN通过智能技术生成

1.assert断言

首先,assert.h头文件定义了宏assert(),用于在运行是确保程序符合指定条件,如果不符合就报错终止运行。这个宏常常被称为“断言”

assert(p != NULL);

上面的代码在程序运行到这一句话的时候,验证p变量是否不等于NULL,如果确实不等于那么程序就继续运行,如果等于则终止程序,并且给出报错信息提示

assert()宏接收一个表达式作为参数。如果该表达式为真,assert()不会产生任何作用,程序运行。如果表达式为假,assert()就会报错,在标准错误流stderr中写入一条错误信息,显示没有通过的表达式,以及包含这个表达式的文件名和行号。

assert()宏的好处:

1.能自动标识文件和出问题的行号

2.无需更改代码就能开启和关闭assert()(如果不需要断言了,只需有在#include<assert.h>语句定义NDEBUG)

#define NDEBUG
#include <assert.h>

2.指针的使用和调用

2.1 strlen的模拟实现

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

函数原型如下

size_t strlen ( const char * str );

参数str接收⼀个字符串的起始地址,然后开始统计字符串中 \0 之前的字符个数,最终返回长度。 如果要模拟实现只要从起始地址开始向后逐个字符的遍历,只要不是 \0 字符,计数器就+1,这样直到 \0 就停⽌。

参考代码如下:

#include<assert.h>
int my_strlen(const char* str)//首字符的地址,const限制字符串被修改
{
	int count = 0;
	assert(str!=NULL);
	while (*str!='\0')
	{
		count++;
		str++;
	}
	return count;
}
int main()
{
	char arr[] = "abcdefg";
	int len = my_strlen(arr);
	printf("%d ", len);
	return 0;
}

2.2  传值调用和传址调用

学习指针的目的是为了用指针解决问题,下面这个问题大家一起看看把,为什么这个只能用指针解决。

写一个函数,交换两个整数的变量

首先我们用非指针的方法去写一下这个代码。(其实就是传值调用)

void Change(int a, int b)
{
	int c = 0;
	c = a;
	a = b;
	b = c;
	
}
int main()
{
	int x = 10;
	int y = 20;
	printf("交换前:x=%d y=%d", x, y);
	Change(x, y);
	printf("交换后:x=%d y=%d", x, y);
	return 0;
}

我们运行后可以看出来,X和Y的值并没有发生交换,这是什么问题造成的呢?

我们在程序中调试下可以看到,只是在交换函数中,形参的a,b值发生了变化,但是x和y并没有变化。所以我们也可以知道,形参只是实参的一份临时拷贝,对形参的修改不会影响实参

接下来我们就试试用指针的方式调用,看能不能去交换这两个值也就是传址调用

void Change(int* pa, int* pb)
{
	int c = 0;
	c = *pa;//c=x
	*pa = *pb;//x=y
	*pb = c;//y=c
	
}
int main()
{
	int x = 10;
	int y = 20;
	printf("交换前:x=%d y=%d\n", x, y);
	Change(&x, &y);
	printf("交换后:x=%d y=%d", x, y);
	return 0;
}

我们可以看到这次通过传址的方式,实现了两个变量的交换。所以对传址调用做个总结

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

3. 数组名的理解

前面当我们想用指针访问的数组的时候我们可以这样写代码

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

在这里我们使用&arr[0]的方式去访问数组中第一个元素的地址,但是其实数组名本身就是地址,而且是数组首元素的地址。

#include <stdio.h>
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;
}

输出结果:

很容易看出来,数组名就是数组首元素的地址 

这里有两个特列需要牢记

1.sizeof(数组名),sizeof中单独放数组名,这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节

2.&数组名,这里的数组名表示的是整个数组,取出的是整个数组的地址(和首元素地址一样)

除了这两个以外,其他任何情况使用数组名都是首元素地址的意思。

3.1&数组名和数组名的区别

&arr[0] = 0077F820
&arr[0]+1 = 0077F824
arr = 0077F820
arr+1 = 0077F824
&arr = 0077F820
&arr+1 = 0077F848

这里我们发现&arr[0]和&arr[0]+1相差4个字节,arr和arr+1 相差4个字节,是因为&arr[0] 和 arr 都是 首元素的地址,+1就是跳过⼀个元素。 但是&arr 和 &arr+1相差40个字节,这就是因为&arr是数组的地址,+1 操作是跳过整个数组的

4.使用指针访问地址 

例:使用指针的方式打印数组中的元素

int main()
{
	int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
	int* p = arr;
	int sz = sizeof(arr)/sizeof(arr[0]);
	for (int i = 0; i < sz; i++)
	{
		printf("%d ", *(p + i));
	}
	return 0;
}

5. 一维数组的传参本质

在讲解一维数组之前,我们先思考一个问题,我们之前都是在函数外部计算数组的元素个数,那我们可以把函数传递给一个函数后,函数内部求数组元素吗?接下来咱们写代码试试

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,0 };
	int sz1 = sizeof(arr) / sizeof(arr[0]);
	printf("sz1=%d\n", sz1);
	test(arr);
	return 0;
}

从结果上看,在函数内部计算元素个数明显是错误的。问题出在哪里呢?

我们前面讲过,arr其实就是数组首元素的地址,那么在数组传参的时候其实传的就是数组名arr,也就是说数组传参本质上是传送的是首元素的地址

 所以函数的形参部分理论上应该写成指针变量来接收首元素的地址。那么在函数内部我们写sizeof(arr)计算的是一个地址的大而不是数组的大小。正是因为函数的参数部分本质是指针,所以数组内部无法求得元素的个数

void test(int * arr)//参数写成指针形式
{
	int sz2 = sizeof(arr) / sizeof(arr[0]);
	printf("sz2=%d\n", sizeof(arr));
}

总结:一维数组传参的时候,形参既可以写成数组的形式也可以写成指针的形式。

6.冒泡排序

冒泡排序核心思想就是:两两相邻的元素进行比较。

void bublle(int* arr, int sz)
{
	int i = 0;
	int j = 0;
	for (i = 0; i < sz - 1; i++)//趟数
	{
		for (j = 0; j < sz - i - 1; j++)
		{
			if (*(arr+j) > *(arr + j + 1))//几对比较,对数
			{
				int temp = 0;
				temp = *(arr+j);
				*(arr+j) = *(arr + j + 1); 
				*(arr + j + 1) = temp;
			}
		}
	}
		
}
int main()
{
	int arr[10] = { 9,8,7,6,5,4,3,2,1,0 };
	//排序
	int sz = sizeof(arr) / sizeof(arr[0]);
	//升序
	bublle(arr, sz);
	//打印
	for (int i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	return 0;
}

其实上面代码还是可以优化的,因为如果已经是有序的序列的话,其实就不用再进行比较了。根据这个思路我们可以这样优化。

void bublle(int* arr, int sz)
{
	int i = 0;
	int j = 0;
	for (i = 0; i < sz - 1; i++)//趟数
	{
		int flag = 1;//假设这一趟是有序的
		for (j = 0; j < sz - i - 1; j++)
		{
			if (*(arr+j) > *(arr + j + 1))//几对比较,对数
			{
				flag = 0;//发生交换说明是无序的
				int temp = 0;
				temp = *(arr+j);
				*(arr+j) = *(arr + j + 1); 
				*(arr + j + 1) = temp;
			}
		}
		if (flag == 1)
		{
			break;
		}
	}
		
}
int main()
{
	int arr[10] = { 9,8,7,6,5,4,3,2,1,0 };
	//排序
	int sz = sizeof(arr) / sizeof(arr[0]);
	//升序
	bublle(arr, sz);
	//打印
	for (int i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	return 0;
}

7. 二级指针

指针变量也是变量,是变量就有地址,那么指针变量的地址存在哪里呢?

答案是,存在二级指针

7.1 对于二级指针的运算

1. *ppa通过对ppa中地址的解引用,这样就找到了pa,*ppa其实访问的就是pa。

int b= 10;
*ppa = &a; //等价于 pa = &b;

2. **ppa先通过*ppa找到pa的地址,然后对pa再进行解引用,*pa,那就找到了a

**ppa = 30;
//等价于*pa = 30;
//等价于a = 30;

8. 指针数组

在讨论指针数组之前,需要明确一点,指针数组究竟是指针还是数组呢?

我们做个类比,整形数组,我们理解是存放整形的数组叫整形数组

字符数组,存放字符的数组是字符数组

所以,指针数组其实就是存放指针的数组

指针数组的每个元素都是用来存放地址的(指针)

指针数组里面的每一个元素又可以指向一块区域

9. 指针数组模拟二维数组

int main()
{
	int arr1[] = { 1,2,3,4,5 };
	int arr2[] = { 2,3,4,5,6 };
	int arr3[] = { 3,4,5,6,7 };
	//数组名是数组首元素地址,类型是int*,就可以存放再parr数组中
	int* parr[3] = { arr1,arr2,arr3 };
	int i = 0;
	int j = 0;
	for (i = 0; i < 3; i++)
	{
		for (j = 0; j < 5; j++)
		{
			printf("%d ", parr[i][j]);
		}
		printf("\n");
	}
	return 0;
}

parr[i]是访问parr数组元素,parr[i]找到数组元素指向了整形一维数组,parr[i][j]就是整形一维数组中的元素。这里的parr[i][j]等价于*(*(parr+i)+j)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值