c语言指针基础(中)

指针

assert断言

要想使用assert需要包含头文件<assert.h>,作用是程序在运行时要确定符合某种条件,如果符合程序正常运行,如果不符合,就会报错,停止运行。
例子:

int *p;
assert(p!=NULL)

程序运行到assert这条语句时,会判断p是不是空指针,如果不是空指针程序正常运行,时空指针程序报错停止运行。
assert接收的是一个表达式作为参数。如果该表达式为真(返回值!=0),assert不会起任何作用,程序还是正常运行。如果表达式为假(返回值=0),assert报错在标准错误 流 stderr 中写⼊⼀条错误信息,显⽰没有通过的表达式,以及包含这个表达式的⽂件名和⾏号。

#include<assert.h>
int main()
{
	int a = 0;
	int* p = &a;
	assert(p != NULL);
	{
		*p = 20;
		printf("%d ", *p);
	}
	
	return 0;
}

上面代码p明确指向了变量a,所以p不可能时空指针,所以程序正常运行,会将a的值改成20;
当p指向的是NULL时

#include<assert.h>
int main()
{
	int a = 0;
	int* p = NULL;
	assert(p != NULL);
	{
		*p = 20;
		printf("%d ", *p);
	}
	
	return 0;
}

在这里插入图片描述
他会告诉你在哪个文件的哪一行出现了错误。
assert()不仅仅可以用于指针,只要代码不符合你的预期时他都会产生作用。
比如:输入一个数,如果不是0就正常运行并且打印,如果是0,assert断言生效。

int main()
{
	int n = 0;
	scanf("%d", &n);
	assert(n != 0);//判断输入的值是不是0,不是0程序正常执行,是0系统报错程序停止运行。
	{
		printf("%d ", n);
	}
	return 0;
}

assert() 的使⽤对程序员是⾮常友好的,使⽤ assert() 有⼏个好处:它不仅能⾃动标识⽂件和 出问题的⾏号,还有⼀种⽆需更改代码就能开启或关闭 assert() 的机制。如果已经确认程序没有问 题,不需要再做断⾔,就在 #include <assert.h> 语句的前⾯,定义⼀个宏 NDEBUG 。

#define NDEBUG
#include<assert.h>

#define NDEBUG相当于assert的一个开关,当不确定程序有没有问题时就注释掉宏DEBUG,当确定程序没有问题时就在#include<assert.h>前加宏DEBUG
比如:

#define NDEBUG
#include<assert.h>
int main()
{
	int a = 0;
	int* p = &a;
	assert(p != NULL);
	{
		*p = 20;
		printf("%d ", *p);
	}
	
	return 0;
}

以上代码已经确定了程序没有问题这时就不需要assert来判断程序有没有问题了,这时就可以在#include<assert.h>前加宏DEBUG来使assert失效。

assert() 的缺点是,因为引⼊了额外的检查,增加了程序的运⾏时间。 ⼀般我们可以在 Debug 中使⽤,在 Release 版本中选择禁⽤ assert 就⾏,在 VS 这样的集成开 发环境中,在 Release 版本中,直接就是优化掉了。这样在debug版本写有利于程序员排查问题, 在 Release 版本不影响⽤⼾使⽤时程序的效率.

指针的使用和传址调用

模拟strlen

strlen是库函数,作用是统计字符串长度,在统计到’\0’时停止统计
自定义一个函数接收字符串的起始地址,然后统计’\0’之前字符的个数,只要不是\0计数器就++,然后将这个计数器存放的值返回。

size_t str_len(const char* p)//接收数组首元素地址,存放到指针变量p中,因为是要计算字符串个数不需要改变数组某个元素的值,所以可以在*前加count
{
	size_t count = 0;//计数器初始化为0
	assert(*p != NULL);//使用assert来判断p是不是空指针
	{
		while (*p != '\0')//当p指向的值不是\0时p++,计数器++
		{
			p++;
			count++;
		}
	}
	return count;
}
int main()
{
	char arr[] = "hello world";
	size_t len = str_len(arr);//传输数组首元素地址
	printf("%zd ", len);
}

传值调用和传址调用

传值调用

例如:写⼀个函数,交换两个整型变量的值
在没有学指针之前,写出来的代码可能会是:


void swap(int x, int y)
{
	int tmp = 0;
	tmp = x;
	x = y;
	y = tmp;
}

int main()
{
	int a = 10;
	int b = 20;
	printf("交换前:a=%d,b=%d\n", a, b);
	swap(a, b);
	printf("交换后:a=%d,b=%d\n", a, b);
	return 0;
}

在这里插入图片描述
这段代码虽然看起来没有太大的问题,也在swap函数中实现了两个值的交换,但最后打印上屏幕的却是没有交换时候的情况,原因如下。
因为在变量a,b创建时,会向内存申请空间假如a的地址是0x0011ff40,b的地址是0x0011ff44,程序进入到swap函数后由于x,y也是变量当作形参也会向内存申请空间,假如x的地址是:0x0011ff50,y的地址是0x0011ff54,所以会发现虽然将a,b的值传到了x,y中但a,x的地址不一样,b,y的地址不一样,函数swap虽然交换了,x,y的值但因为x,y是形参,形参的改变对实参没有作用,所以虽然x,y的值交换了,但a,b的地址没有进行交换,所以自然不会改变a,b本身的值。

总结成一句话:实参传递给形参时,形参会单独的创建一块临时空间来对形参进行接收,形参是实参的一份临时拷贝,对形参的改变不影响实参。

传址调用

要解决上面的问题就要想办法改变形参的同时也要相对应的改变实参,这时我们可以将a,b的地址传送给函数,然后交换两数的地址就可以形成两数的交换。

{
	int tmp = 0;
	tmp = *x;
	*x = *y;
	*y = tmp;
}

int main()
{
	int a = 10;
	int b = 20;
	printf("交换前:a=%d,b=%d\n", a, b);
	swap(&a, &b);
	printf("交换后:a=%d,b=%d\n", a, b);
	return 0;
}

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

数组名的理解

数组名就是数组首元素的地址。以前写一个数组,要将数组首元素 的地址存起来要将数组首元素拿出来在将它存放到指针变量中。但是现在可以直接将数组名存放到指针变量。
证明:数组名就是数组首元素地址。


int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	printf("%x\n", &arr[0]);//打印数组首元素地址
	printf("%x\n", arr);//打印数组名地址
	return 0;
}

所以可以证明数组名就是数组首元素地址。
但是有两个例外,一个是sizeof(arr)和&arr,除此之外所有的数组名都是数组首元素。

sizeof(数组名)

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	printf("%d ", sizeof(arr));
	return 0;
}

如果说数组名就是数组首元素的地址的话那么上面代码计算的是数组第一个元素的大小应该是4/8字节,但结果不是这样的
在这里插入图片描述
可以看出最后打印的是40,这个40其实是整个数组的大小,所以在sizeof(数组名)的情况下计算的是整个数组的大小,这时数组名就不是数组首元素了。

&数组名

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

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	printf("&arr     =%x\n", &arr);
	printf("&arr[0]  =%x\n", &arr[0]);
	printf("&arr+1   =%x\n", &arr+1);
	printf("&arr[0]+1=%x\n", &arr[0]+1);
	return 0;
}

在这里插入图片描述
可以发现当&arr和&arr[0]都+1时所得到的数值不一样,&arr+1跳过了40个字节,跳过的是整个数组&arr[0]+1跳过了4个字节,跳过的是一个元素。

指针访问数组

当知道数组名是数组首元素地址时,这个时候就可以通过第一个元素的地址来访问后面的元素,这时可以打印出整个数组的内容

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = arr;//将数组首元素地址存放到指针变量p中
	int sz = sizeof(arr) / sizeof(arr[0]);//计算数组元素个数
	for (int i = 0;i < sz;i++)
	{
		scanf("%d", &arr[i]);
	}
	for (int i = 0;i < sz;i++)
	{
		printf("%d ", *(p + i));//知道第一个元素的地址后,可以遍历后面的元素
	}
	return 0;
}

由于是将arr存放到了p中所以arr是=p的,这时上面的arr和p都是可以互换的

int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = arr;//将数组首元素地址存放到指针变量p中
int sz = sizeof(arr) / sizeof(arr[0]);//计算数组元素个数
for (int i = 0;i < sz;i++)
{
	scanf("%d", &arr[i]);
}
for (int i = 0;i < sz;i++)
{
	printf("%d ", *(arr + i));//知道第一个元素的地址后,可以遍历后面的元素
}
return 0;

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = arr;//将数组首元素地址存放到指针变量p中
	int sz = sizeof(arr) / sizeof(arr[0]);//计算数组元素个数
	for (int i = 0;i < sz;i++)
	{
		scanf("%d", p+i);
	}
	for (int i = 0;i < sz;i++)
	{
		printf("%d ", p[i]);//知道第一个元素的地址后,可以遍历后面的元素
	}
	return 0;
}

所以arr[i]p[i](p+i)==(arr+i)

一维数组传参

前面都是在main函数中用sizeof计算数组元素个数,那将一个数组传给函数后,函数内部求的元素个数和在main函数中求得的元素个数一样吗?

void text  (int arr[])
	{
	int sz = sizeof(arr) / sizeof(arr[0]);
	printf("text函数中的sz=%d", sz);
	}
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	printf("main函数中的sz=%d\n", sz);
	text(arr);
	return 0;
}

发现在函数外部和函数内部计算的sz是不一样的
原因:在main函数中,sizeof(arr)计算的是整个数组的大小是40,sizeof(arr[0])是首元素的大小是4,所以最后是40/4=10
在text函数中虽然形参使用数组的形式接收实参的arr,但实参的arr即不是sizeof(arr),也不是&arr,所以他的本质是数组首元素的地址,所以形参的部分可以看成int * arr,arr就是数组首元素的地址,他的类型是int ,int是4字节,arr[0]也是4字节,所以就是4/4=1
⼀维数组传参,形参的部分可以写成数组的形式,也可以写成指针的形式。

冒泡排序

这时有一个数组

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

这个数组是倒序的,现在希望它以升序的方式进行排列;这时可以用到冒泡排序。
冒泡排序的核⼼思想就是:两两相邻的元素进⾏⽐较,如果不满足顺序就就交换,满足顺序就找下一对。

void maopao(int* p, int sz)
{
	for (int i = 0;i < sz - 1;i++)//数组要执行几趟排序
	{
		for (int j = 0;j < sz - i - 1;j++)//一趟排序要交换几次
		{
			if (p[j] > p[j + 1])//当前面的数比后面的数大的时候两个数就交换
			{
				int tmp = p[j];//实现两数的交换
				p[j] = p[j + 1];
				p[j + 1] = tmp;
			}
		}
	}
}
void print(int arr[], int sz)
{
	for (int i = 0;i < sz;i++)
	{
		printf("%d ", arr[i]);
	}
}
int main()
{
	int arr[10] = { 10,9,8,7,6,5,4,3,2,1 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	maopao(arr, sz);
	print(arr, sz);
	return 0;
}

上面的代码虽然可以对数组交换但是当数组有几个元素本身就是有序的时候,它还会进行计算这样增加了程序运行的速度,
比如:

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

解决方法.

void maopao(int* p, int sz)
{
	for (int i = 0;i < sz - 1;i++)//数组要执行几趟排序
	{
		int falg = 1;//假设数组本身是有序的
		for (int j = 0;j < sz - i - 1;j++)//一趟排序要交换几次
		{
			if (p[j] > p[j + 1])//当前面的数比后面的数大的时候两个数就交换
			{
				falg = 0;//一旦交换了就证明数组是无序的,这时将falg置为0
				int tmp = p[j];//实现两数的交换
				p[j] = p[j + 1];
				p[j + 1] = tmp;
			}
		}
		if (falg == 1)//falg==1证明数组有序,跳出本次循环,不进行任何操作
		{
			break;
		}
	}
}
void print(int arr[], int sz)
{
	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 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	maopao(arr, sz);
	print(arr, sz);
	return 0;
}

二级指针

二级指针的概念

指针变量存放的是变量的地址,那么指针变量的地址应该存放在哪里?

int main()
{
	int a = 10;
	int* p = &a;//p存放的是变量a的地址,p就是一个一级指针
	int** pa = &p;//pa存放的是指针p的地址,那么pa是一个二级指针
	return 0;
}

用画图的方式理解二级指针
在这里插入图片描述
二级指针*号的应用

	int a = 10;
	int* p = &a;
	int** pa = &p;
	return 0;

pa前面的那个号代表pa是一个指针变量,int后面的号代表pa指向的类型是int *

二级指针的解引用操作

想通过二级指针改变变量的值可以对二级指针变量进行两次解引用操作,进行一次解引用操作找到的是一级指针的地址,进行第二次解引用操作就找到了变量的地址。

int main()
{
	int a = 10;
	int* p = &a;
	int** pa = &p;
	**pa = 20;
	printf("%d ", a);
	return 0;
}

在这里插入图片描述

指针数组

我们知道整形数组是存放整形的数组,字符数组是存放字符的数组,那么指针数组也应该就是存放指针变量的数组。
在这里插入图片描述
所以指针数组的每个元素都是指针,指向了同一片区域。

指针数组模拟二维数组

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

在这里插入图片描述

arr[0]指向的是arr1,arr[1]指向的是arr2

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值