C语言:大战指针“哥斯拉”(2)

目录

一、前言

二、assert断言

三、传值调用与传址调用

(一)传值调用 

(二)传址调用

四、指针和数组之间的联系与应用

(一)数组名的理解

(二)指针访问数组

(三)一维数组传参的本质

(四)冒泡排序

(五)二级指针

(六)指针数组

(七)指针数组模拟二维数组

五、字符指针变量

六、数组指针变量

(一)数组指针变量介绍

(二)数组指针变量应用

 七、函数指针变量

(一)函数指针变量的理解

 (二)函数指针变量的使用

八、总结


一、前言

大家经过对指针的初步学习,已经掌握了打败指针“哥斯拉”的第一招,今天蜡笔小欣将和大家一起来学习第二招,让大家对指针有更加深入的了解。

二、assert断言

assert是一个宏,经常被我们称为“断言”,我们在使用它时要写一个头文件<assert.h>,它能够让我们在运行程序时确保程序符合指定的条件,当条件不符合时,则会终止条件运行并且给出报错信息提示。
下面我给大家举个例子:

assert(p != NULL);

上面这个代码能够帮助我们验证变量p是否等于NULL,如果不等于NULL,则程序继续运行;若变量p等于NULL,程序会终止运行,并给出报错信息提示。

三、传值调用与传址调用

(一)传值调用 

下面我们举个栗子:交换两个变量的值

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
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 <stdio.h>
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 <stdio.h>
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);//+4

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

printf("&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 <stdio.h>
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 <stdio.h>
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 <stdio.h>
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 <stdio.h>
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 < sz - 1 - i; j++)//size-1-i是因为每一趟就会少一个数比较
        {
            //相邻两个元素进行比较
            if (arr[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 <stdio.h>
int main()
{
    int* arr1[10];//存放10个整型指针变量
    char*  arr2[10];//存放10个字符指针变量
    double* arr3[10];//存放10个双精度浮点型指针变量
    return 0;
}

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

(七)指针数组模拟二维数组

指针数组可以模拟出二维数组的效果,但不是二维数组。让我们一起看看应该示例:

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

你们猜猜运行结果会是什么呢?

这个结果是你想的那个答案吗?蜡笔小欣来给大家揭秘一下:因为str3与str4指向同一个常量字符串。在C/C++中,会把常量字符串存储到单独的一个内存区域,当几个指针指向同一个字符串时,它们实际上会指向同一块内存,但用相同的常量字符串去初始化不同数组时,就会开辟不同的内存块。因此str1和str2不同,str3和str4相同。

六、数组指针变量

(一)数组指针变量介绍

上面讲到指针数组是存放指针的数组,举一反三,而数组指针是存放数组的地址,能够指向数组的指针变量

(二)数组指针变量应用

二维数组传参,形参可以写成数组的形式,也可以写成指针的形式。下面我们来看数组指针变量的应用:遍历整个二维数组。

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
//二维数字传参,形参写的是二维数组
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 <stdio.h>
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 <stdio.h>
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)是等价的

八、总结

相信大家对指针的进一步学习后,学会了打败指针“哥斯拉”的第二招,这第二招的招式内容较多、也比较复杂,需要大家反复学习,才能熟练地运用。下期再和蜡笔小欣一起学习打败指针“哥斯拉”的终极绝招,感谢大家的鼓励与支持,我们下期再见!!!

  • 50
    点赞
  • 47
    收藏
    觉得还不错? 一键收藏
  • 23
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 23
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值