目录
一、前言
二、assert断言
三、传值调用与传址调用
(一)传值调用
(二)传址调用
四、指针和数组之间的联系与应用
(一)数组名的理解
(二)指针访问数组
(三)一维数组传参的本质
(四)冒泡排序
(五)二级指针
(六)指针数组
(七)指针数组模拟二维数组
五、字符指针变量
六、数组指针变量
(一)数组指针变量介绍
(二)数组指针变量应用
七、函数指针变量
(一)函数指针变量的理解
(二)函数指针变量的使用
八、总结
一、前言
大家经过对指针的初步学习,已经掌握了打败指针“哥斯拉”的第一招,今天蜡笔小欣将和大家一起来学习第二招,让大家对指针有更加深入的了解。
二、assert断言
assert是一个宏,经常被我们称为“断言”,我们在使用它时要写一个头文件,它能够让我们在运行程序时确保程序符合指定的条件,当条件不符合时,则会终止条件运行并且给出报错信息提示。
下面我给大家举个例子:
assert(p != NULL);
上面这个代码能够帮助我们验证变量p是否等于NULL,如果不等于NULL,则程序继续运行;若变量p等于NULL,程序会终止运行,并给出报错信息提示。
三、传值调用与传址调用
(一)传值调用
下面我们举个栗子:交换两个变量的值
#define _CRT_SECURE_NO_WARNINGS#include void Swap(int x, int y){int z = 0;z = x;x = y;y = z;}int main(){int a = 0;int b = 0;scanf("%d %d", &a, &b);printf("交换前:a=%d,b=%d\n", a, b);Swap(a, b);printf("交换后:a=%d,b=%d", a, b);return 0;}
运行结果如下:
看到这个结果,你可能会想为什么上面a和b的值没有交换呢?原因是我们在传值调用函数时,函数的实参传给形参时,形参会单独创建一份临时空间来接收实参,形参是实参的一份临时拷贝!形参有它自己的独立空间,我们修改形参并不会影响实参。
因此我们上面使用Swap来进行传值调用,只是在函数内部进行交换变量,在Swap函数调用结束后返回在main函数,变量a和b的值并没有交换。
(二)传址调用
传值调用函数不能实现交换两个变量的值,我们可以通过进行传址调用来实现。
下面是使用传址调用的代码:
#define _CRT_SECURE_NO_WARNINGS#include void Swap(int* pa, int* pb){int z = 0;z = *pa;//z=a*pa = *pb;//a=b*pb = z;//b=z}int main(){int a = 0;int b = 0;scanf("%d %d", &a, &b);printf("交换前:a=%d,b=%d\n", a, b);Swap(&a, &b);printf("交换后:a=%d,b=%d", a, b);return 0;}
运行结果:
我们通过初步学习指针后,使用指针访问变量的地址,从而访问变量的值。所有我们可以在主函数中将变量a和b的地址传给Swap函数,让Swap函数在内部修改主函数变量a和b的值,从而达到交换变量的效果。
四、指针和数组之间的联系与应用
(一)数组名的理解
让我们通过以下的程序让我们更好地理解数组名。
#define _CRT_SECURE_NO_WARNINGS#include int main(){int arr[10] = { 0 };printf("%d\n", sizeof(arr));//1.sizeof内部单独放一个数组名的时候,数组名表示的是整个数组,计算的是整个数组的大小//单位是字节//2.&数组名,数组名表示的是整个数组,取出的是整个数组的地址//除此之外,遇到的所有数组名都是数组首元素的地址printf("&arr[0] = %p\n", &arr[0]);printf("arr = %p\n", arr);printf("&arr = %p\n", &arr);return 0;}
输出结果:
40
&arr[0] = 000000FA71AFFA78
arr = 000000FA71AFFA78
&arr = 000000FA71AFFA78
通过上面可以发现&arr[0]、arr、&arr的输出结果一样,那我们该怎么区别它们呢?通过下面这段代码相信能让大家更好地理解。
printf("&arr[0] = %p\n", &arr[0]);//int*printf("&arr[0] = %p\n", &arr[0]+1);//+4printf("arr = %p\n", arr);//int*printf("arr = %p\n", arr+1);//+4printf("&arr = %p\n", &arr);printf("&arr = %p\n", &arr+1);
运行结果如下:
通过上面的运行结果我们可以得到结论:
&arr[0]与arr+1都是int型,跳过4个字节,而&arr表示的是整个数组,取出是整个数组的地址,所以&arr+1跳过整个数组,也就是40个字节。
Tips:
1.数组在内存中是连续存放的;
2.数组名就是首元素的地址(方便找到起始位置),所以我们可以使用指针来访问数组。
(二)指针访问数组
我们现在已经学习了指针的部分知识,如果要访问数组的元素,也可以通过指针来完成。举个栗子:在数组中输入10个整数并打印出来。
#define _CRT_SECURE_NO_WARNINGS#include int main(){int arr[10] = { 0 };int sz = sizeof(arr) / sizeof(arr[0]);int* p = arr;int i = 0;for (i = 0; i < sz; i++){//scanf("%d",&arr[i]);//scanf("%d", arr + i);scanf("%d", p + i);}for (i = 0; i < sz; i++){//printf("%d ", arr[i]);//printf("%d ", *(arr + i));printf("%d ", *(p + i));}return 0;}
运行结果:
因为p指针存放的是数组首元素的地址, 所以p+i实际上计算的是数组 arr下标为 i 的地址,arr[i]与 *(arr+i)、*(p+i)等价,使用上面的这几种形式,我们都能够通过指针来访问数组。
(三)一维数组传参的本质
数组传参时传递的是数组名,本质上数组传参传递的是数组首元素的地址,对于函数的形参,有时候我们会使用指针变量来接收首元素的地址。我们通过下面这个例子来让大家加深一下理解。
#define _CRT_SECURE_NO_WARNINGS#include void test(int arr[])//int* arr{int sz = sizeof(arr) / sizeof(arr[0]);printf("%d", sz);}int main(){//数组传参的时候,传递的并非是数组,//传递的是数组首元素的地址int arr[10] = { 0 };test(arr);//数组首元素的地址return 0;}
输出结果:1
这时候我们就会好奇为什么不应该输出10而是输出1呢?这是因为在函数内部使用sizeof(arr)计算的是首元素地址的大小而不是整个数组的大小。
我们在看看下面的例子
#define _CRT_SECURE_NO_WARNINGS#include void test(int *arr,int sz){int i = 0;for (i = 0; i < sz; i++){printf("%d ", *(arr + i));}}int main(){int arr[] = { 1,2,3,4,5,6,7,8,9 };int sz = sizeof(arr) / sizeof(arr[0]);test(arr, sz);return 0;}
输出结果:1 2 3 4 5 6 7 8 9
运行结果也验证了一维数组的传参本质就是指针。因为把整个数组传到函数里面代价是非常大的,我们前面讲过函数的实参传给形参时,形参会单独创建一份临时空间来接收实参,如果将整个数组传进来,就会白白浪费很大的空间。因此一维数组传参,形参的部分可以写成数组的形式,也可以写成指针的形式。
(四)冒泡排序
接下来我们学习冒泡排序,把数组、函数、指针的内容相结合起来。
冒泡排序的思想:相邻两个元素进行比较,若不满足顺序则进行交换,每次比较一轮,就会找到序列中最大的一个或最小的一个。这个数就会从序列的最右边冒出来。
动画演示如下:
我们通过一个例子来更好地理解与使用,如:编写一个冒泡排序函数,对一个整型数组的元素进行升序排序。
#define _CRT_SECURE_NO_WARNINGS#include void BubbleSort(int arr[], int sz){int i = 0;//排序趟数for (i = 0; i < sz-1; i++)//size-1是因为不用与自己比较,所以比的数就少一{int flag = 1;//假设已经有序//一趟冒泡排序的过程int j = 0;for (j = 0; j arr[j + 1]){//交换int temp = 0;temp = arr[j];arr[j] = arr[j + 1];arr[j + 1] = temp;flag = 0;//若进行交换,则flag变为0}}if (flag == 1)//如果某一趟没有交换位置,则说明已经排好序,直接退出循环{break;}}}int main(){int arr[] = { 9,7,8,1,2,4,3,5,6 };int sz = sizeof(arr) / sizeof(arr[0]);BubbleSort(arr, sz);int i = 0;for (i = 0; i < sz; i++){printf("%d ",arr[i]);}return 0;}
运行结果:
我们有时在编写一些程序时就会使用到冒泡排序,通过冒泡排序可以让我们更好地把数组、函数、指针的内容相结合起来。
(五)二级指针
指针变量也是变量,是变量就会有地址,那么指针变量的地址储存在哪里呢?让我们通过下面的例子一起来了解二级指针。
*pp对pp中的地址进行解引用,找到p,*pp访问的就是p,
*p对p中的地址进行解引用,找到a,所以**pp访问的就是a。
(六)指针数组
指针数组究竟是指针呢还是数组呢?答案当然是存放指针的数组啦!
#define _CRT_SECURE_NO_WARNINGS#include int main(){int* arr1[10];//存放10个整型指针变量char*arr2[10];//存放10个字符指针变量double* arr3[10];//存放10个双精度浮点型指针变量return 0;}
指针数组的每个元素都是地址,可以指向另一块区域。
(七)指针数组模拟二维数组
指针数组可以模拟出二维数组的效果,但不是二维数组。让我们一起看看应该示例:
#define _CRT_SECURE_NO_WARNINGS#include int main(){int arr1[] = { 1,2,3,4,5 };int arr2[] = { 2,3,4,5,6 };int arr3[] = { 3,4,5,6,7 };//数组名是数组首元素地址,类型都是int*,可以存放在arr数组里int* arr[3] = { arr1,arr2,arr3 };int i = 0;for (i = 0; i < 3; i++){int j = 0;for (j = 0; j < 5; j++){printf("%d ", arr[i][j]);}printf("\n");}return 0;}
运行结果:
是不是很神奇,指针数组竟然可以模拟出二维数组的效果,arr[i]访问arr数组的元素,arr[i]找到数组元素执指向整型一维数组,arr[i][j]就是整型一维数组中的元素,实际上并非完全是二维数组,每一行不是连续的。
arr1数组、arr2数组和arr3数组都是有独立的空间,并不是连续。
五、字符指针变量
通过前面的学习,相信大家已经对字符指针有了一定的了解,今天我们再来看看字符指针变量的另一种使用形式。
const char* p = "abcdef";//不是把常量字符串abcdef\0存放在p中,//而是把第一个字符的地址存放在p中//1.把字符串看成一个字符数组,但这个数组不能修改//当常量字符串出现在表达式中,它的值就是第一个字符串的地址
常量字符串的第一个字符地址被存放在p中。
如下面代码所示:
#define _CRT_SECURE_NO_WARNINGS#include 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");elseprintf("str1 and str2 are not same\n");if (str3 == str4)printf("str3 and str4 are same\n");elseprintf("str3 and str4 are not same\n");return 0;}
你们猜猜运行结果会是什么呢?
这个结果是你想的那个答案吗?蜡笔小欣来给大家揭秘一下:因为str3与str4指向同一个常量字符串。在C/C++中,会把常量字符串存储到单独的一个内存区域,当几个指针指向同一个字符串时,它们实际上会指向同一块内存,但用相同的常量字符串去初始化不同数组时,就会开辟不同的内存块。因此str1和str2不同,str3和str4相同。
六、数组指针变量
(一)数组指针变量介绍
上面讲到指针数组是存放指针的数组,举一反三,而数组指针是存放数组的地址,能够指向数组的指针变量。
(二)数组指针变量应用
二维数组传参,形参可以写成数组的形式,也可以写成指针的形式。下面我们来看数组指针变量的应用:遍历整个二维数组。
#define _CRT_SECURE_NO_WARNINGS#include //二维数字传参,形参写的是二维数组void print(int(*p)[5], int x, int y){int i = 0;for (i = 0; i < x; i++){int j = 0;for (j = 0; j < y; j++){//printf("%d ", p[i][j]);相似的写法printf("%d ", *(*(p + i) + j));}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, 3, 5);//打印数组的内容return 0;}
运行结果:
二维数组实际上是元素是一维数组的数组。对于二维数组,首元素就是第一行,首元素的地址就是第一行的地址。所以二维数组传参本质上是传递了第一行这个一维数组的地址。
七、函数指针变量
(一)函数指针变量的理解
函数指针变量是用来存放函数地址的,也能通过地址来调用函数。指向函数的指针,我们称为函数指针变量。举个栗子:
#define _CRT_SECURE_NO_WARNINGS#include int Add(int x, int y){return x + y;}int main(){int a = 10;int b = 20;int ret = Add(a, b);printf("%p \n", Add);//打印函数名printf("%p \n", &Add);//打印函数名地址return 0;}
打印结果:
你会发现在函数中,Add与&Add两种形式的地址都一样。
Tips:
数组名——数组首元素地址
&数组名——整个数组的地址
函数名——函数的地址
&函数名——函数的地址
(二)函数指针变量的使用
对函数来说,Add和&Add的意义和值都一样。这点一定要区别于数组。
#define _CRT_SECURE_NO_WARNINGS#include int Add(int x, int y){return x + y;}int main(){int(*pf1)(int a, int b) = &Add;//pf就是函数指针变量int ret = Add(10, 20);printf("%d\n", ret);int (*pf2)(int, int) = Add;//(int a,int b)里面a和b可以省略int ret1 = Add(4, 6);printf("%d\n", ret1);int(pf3)(int a, int b);//(*pf3)里面的*可以省略不写int ret2 = Add(5, 5);printf("%d\n", ret2);return 0;}
输出结果:
30
10
10
在调用函数时,定义一个pf的函数指针指向函数。那么int ret = (*pf)(int a,int b)和int ret = (pf)(int a,int b)是等价的。
八、总结
相信大家对指针的进一步学习后,学会了打败指针“哥斯拉”的第二招,这第二招的招式内容较多、也比较复杂,需要大家反复学习,才能熟练地运用。下期再和蜡笔小欣一起学习打败指针“哥斯拉”的终极绝招,感谢大家的鼓励与支持,我们下期再见!!!