文章目录
前言
我们来继续学习指针!
一、assert断言
assert.h头文件定义了宏assert(),用于在运行时确保程序符合指定条件,如果不符合,就报错终止运行,这个宏常常被称为“断言”
assert(p != NULL);
上面代码在程序运行到这一行语句时,验证变量p是否等于NULL,如果确实不等于NULL,程序继续运行,否则就会终止运行,并且给出报错信息提示
assert()宏接受一个表达式作为参数,如果该表达式为真,assert()不会产生任何作用,程序继续运行;如果该表达式为假,assert()就会报错,在标准错误流stderr中写入一条错误信息,显示没有通过的表达式,以及包含这个表达式的文件名和行号
assert()的使用对程序员是非常友好的,使用assert()有几个好处:它不仅能自动标识文件和出问题的行号,还有一种无需更改代码就能开启或关闭assert()的机制,如果已经确认程序没有问题,不需要再做断言,就在#include <assert.h>语句的前面,定义一个宏NDBUG
#define NDBUG
#include <assert.h>
然后,重新编译程序,编译器就会禁用文件中所有的assert()语句,如果程序又出现问题,可以移除这条指令,再次编译,就又启用了assert()语句
assert()引入了额外的检查,增加了程序的运行时间,一般我们可以在Debug中使用,在Release版本中选择禁用assert就行,在VS这样的集成开发环境中,在Release版本中,直接就是优化掉了,这样在debug版本写有利于程序员排查问题,在Release版本不影响用户使用时程序的效率
二、指针的使用和传址调用
strlen的模拟实现
库函数strlen的功能是求字符串的长度,统计的是字符串中\0之前的字符的个数
函数原型如下:
size_t strlen(const char* str);
参数str接收一个字符串的起始地址,然后开始统计字符串中\0之前的字符个数,最终返回长度
如果要模拟实现只要从起始地址开始向后逐个字符的遍历,只要不是\0字符,计数器就+1,这样知道\0就停止
在这里,my_strlen(arr)就是传址调用
传值调用和传址调用
学习指针的目的就是使用指针解决问题,可是有什么问题,是非指针不可的呢?
写一个函数,作用是交换两个变量的值
下面是第一种方案:
可以发现,这种方法失败了,这是为什么呢?
原因在这:
a,b变量有自己的地址,x,y变量也有自己的地址,也就是说传值调用函数时函数的实参传递给形参时,形参是实参的一份临时拷贝,有自己独立的空间,对形参的修改不会影响实参
也就是说,如果真要修改实参的话,应该使用传址调用
Swap(&a, &b);
传址调用,可以让函数和主调函数之间建立真正的联系,在函数内部可以修改主调函数中的变量,所以未来函数中只是需要主调函数中的变量值来实现计算,就可以采用传值调用,如果函数内部要修改主调函数中的变量的值,就需要传址调用
三、数组名的理解
在之前我们就有使用指针访问数组的内容
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int *p = &arr[0];
这里我们使用&arr[0]的方式拿到了数组第一个元素的地址,但是其实数组名本来就是地址,而且是数组首元素的地址,我们来做个测试吧!
输出结果如下:
显然,我说的没错,数组名就是数组首元素的地址,请再次牢记!
另外,我还想要大家记住两个例外结论:
1.sizeof内部单独放一个数组名的时候,数组名表示的是整个数组,计算的是整个数组的大小,单位是字节
2.&数组名,数组名表示的是整个数组,取出的是整个数组的地址
除此之外,遇到的所有数组名都是数组首元素的地址
首元素的地址和整个数组的地址的区别:
虽然两者从值来说是一样的,但是两者差了不少,后面会慢慢展示
&arr 和 &arr + 1差了0x28个字节,(2 * 16 + 8 = 40),也就是是10个int的大小
四、使用指针访问数组
有了前面的知识,又结合数组的特点,我们就可以很方便的使用指针来访问数组了
插个有意思的,上图中你可以看出来其实arr[i] 跟 *(arr + i)其实是等价的
这说明了这个数组下标引用符[]没那么神奇,实际上,程序跑起来的时候,底层上前者也会被编译器转换为后者
又因为 *(arr + i) 与 *(i + arr)从结果来看是没差的
所以有一个大胆的想法,arr[i]与i[arr]也是等价的,实践后发现果然如此:
勤动脑,是不会错的
五、一维数组传参的本质
数组我们学过了,之前也讲了,数组是可以传递给函数的,于是我们来讨论一下数组传参的本质
首先从一个问题开始,我们之前都是在函数外部计算数组的元素个数,那我们可以把数组传给一个函数后,函数内部求数组的元素个数吗?
这个程序的输出结果很有意思,sz1输出10,sz2输出1
其实我们想,如果想单个形参变量一样拷贝实参,那么数组形参就要拷贝一个数组,这太占用内存了
所以,其实我们传递的是数组首元素的地址,而形参部分int arr[],其实本质上是int* arr
六、冒泡排序
排序的方法有很多种,有时候你不得不感概计算机前辈的智慧,我后期会出一篇文章来讲解八大排序
冒泡排序的核心思想就是:两两相邻的元素进行比较
冒泡排序的核心逻辑是,假如一个数组arr有n个元素,那么第一趟遍历 1 ~ n 个元素,两两相比,小的往左移动,这样最大的元素就变成了第 n 个元素;第二趟遍历 1 ~ n - 1 个元素,同样的,第二大的元素就变成了第 n - 1 个元素,直至排完
其实思路还是蛮简单的,这时候我们就要开始设计,首先每次排好一个元素,一共有 n 个元素,一共需要排 n - 1 次,因为最后一次,剩下最后一个元素就是停在正确的位置上,所以我们需要一个 for 循环 ;而对于第 i 趟,我们要从第2个元素移动到第 n - i + 1 个元素 (arr[i] 与 arr[i - 1])
思路明确,代码自然而然地就写出来了,要对自己有自信:
这时候,我们再考虑C语言的模块化和结构化的特点,封装两个函数BubbleSort和Print,这时候,也就是相当于传址调用了
甚至还可以来点小优化,比如说某一趟走完发现都不需要移动,说明此时已经排完序了,直接跳出来即可
这要怎么实现?就交给大家了!
这是C的哲学,也是C的美感
七、二级指针
指针变量也是变量,是变量就有地址,那指针变量的地址存放在哪里?
这就是二级指针
对于二级指针的运算有:
- *ppa 通过对 ppa 中的地址进行解引用,这样找到的是pa, *ppa 其实访问的是pa
- **ppa 先通过*ppa找到pa,然后对pa进行解引用操作:*pa,那找到的是a
其实类比一下,也有三级指针,只是二级指针都用的较少,更别提三级指针了
八、指针数组
何为指针数组?
指针数组是指针还是数组?
还是以类比作为理解方式,我们想整型数组是什么?就是存放整型的数组,字符数组是存放字符的数组
这样想的话,那么指针数组就是存放指针的数组,每个元素都是同类型的指针
用指针数组模拟二维数组
注意,这是模拟出二维数组的效果,但是不是二维数组!
你会发现,之所以说是模拟,其实是因为parr所指向的每个int*所相当的一维数组是不连续的
总结
感觉如何,从本章开始,大家是不是感觉难度上来了呢,加油,后面会更精彩的!