前情回顾:C语言指针超详细攻略:掌握地址的艺术(上)_c语言地址指针-CSDN博客
一、数组名的理解
在上一讲的内容中我们在使用指针访问数组的内容时,有这样的代码:
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
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;
}
下面给出输出结果:
我们发现数组名和数组首元素的地址打印出的结果一模一样,数组名就是数组首元素的地址。
我们再试一下下面的一串代码:
#include<stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("%d ", sizeof(arr));
return 0;
}
输出的结果是:40,如果arr是数组首元素的地址,那输出应该的应该是4/8才对。(分别对应X86与X64的环境)
其实数组名就是数组首元素(第⼀个元素)的地址是对的,但是有两个例外:
• sizeof(数组名),sizeof中单独放数组名,这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节
• &数组名,这里的数组名表示整个数组,取出的是整个数组的地址(整个数组的地址和数组首元素的地址是有区别的)
除此之外,任何地方使用数组名,数组名都表示首元素的地址。
接下来我们看另一串代码:
#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);
printf("&arr = %p\n", &arr);
return 0;
}
这时候我们运行代码发现三个打印的结果一模一样,从上面的学习我们知道arr其实就是等价于&arr[0],那arr和&arr又有啥区别呢? 我们给上述代码各个都加1再来看看结果:
#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[0] + 1 = %p\n", &arr[0] + 1);
printf("arr = %p\n", arr);
printf("arr + 1 = %p\n", arr + 1);
printf("&arr = %p\n", &arr);
printf("&arr + 1 = %p\n", &arr + 1);
return 0;
}
输出结果:
我们不难看出差值是不一样的,前面俩个的差值都是4,因为这是十六进制的数字,即为4*16^0=4,即相差四个字节,第三个相差的是28,是十六进制的28即为0x28,化为十进制即为2*16^1+8*16^0=40,即相差四十个字节。
我们发现&arr[0]和&arr[0]+1相差4个字节,arr和arr+1相差4个字节,是因为&arr[0]和arr都是首元素的地址,+1就是跳过一个元素。
但是&arr和&arr+1相差40个字节,这就是因为&arr是数组的地址,+1操作是跳过整个数组的。
我们只需要记住除了上面的俩个特例外,数组名就是数组首元素的地址。
二、使用指针访问数组
通过前面的学习,再结合数组的特点我们便可以很方便的使用指针访问数组了:
#include<stdio.h>
int main()
{
int arr[10] = { 0 };
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
int* p = arr;//这里其实是&arr[0]
for (i = 0; i < sz; i++)
{
scanf_s("%d ", p);//p就是&arr[0]
p++;
}
p = arr;
for (i = 0; i < sz; i++)
{
printf("%d ", *(p + i));
}
return 0;
}
*(p+i)换成p[i]也是能够正常打印的,所以本质上p[i]是等价于*(p+i)的,我们可以得出:
arr[i]==*(arr+i)==*(i+arr)==i[arr]
三、一维数组传参的本质
数组我们学过了,之前也讲了,数组是可以传递给函数的,这个小节我们讨论一下数组传参的本质。首先从一个问题开始,我们之前都是在函数外部计算数组的元素个数,那我们可以把数组传给一个函数后,函数内部求数组的元素个数吗?
我们写出下面的代码:
#include<stdio.h>
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,10 };
int sz1 = sizeof(arr) / sizeof(arr[0]);
printf("sz1 = %d\n", sz1);
test(arr);
return 0;
}
输出结果:
我们发现在函数内部是没有正确获得数组的元素个数
这就要学习数组传参的本质了,上个小节我们学习了:数组名是数组首元素的地址;那么在数组传参的时候,传递的是数组名,也就是说本质上数组传参传递的是数组首元素的地址。
所以函数形参的部分理论上应该使用指针变量来接收首元素的地址。那么在函数内部我们写 sizeof(arr)计算的是⼀个地址的大小(单位字节)而不是数组的大小(单位字节)。正是因为函数的参数部分是本质是指针,所以在函数内部是没办法求的数组元素个数的。
#include<stdio.h>
void test(int arr[ ])//参数写成数组形式,本质上还是指针
{
printf("%d\n", sizeof(arr));
}
void test(int* arr)//参数写成指针形式
{
printf("%d\n", sizeof(arr));//计算一个指针变量的大小
}
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
test(arr);
return 0;
}
总结:一维数组传参,形参的部分可以写成数组的形式,也可以写成指针的形式
四、冒泡排序
冒泡排序的核心是:俩俩相邻的元素进行比较。
例题:我们给出一个无序的数组,现在我们想将其变为升序数组。
算法步骤
- 比较相邻元素:对数组中的相邻元素进行两两比较。
- 交换元素:若顺序错误(如升序排列时前一个元素大于后一个元素),就将它们互换位置。
- 重复操作:对每一对相邻元素重复上述比较和交换步骤,直至数组末尾。
- 缩减范围:每完成一轮比较,下一轮比较的元素数量就减少一个。
- 终止条件:持续重复上述过程,直到整个数组都被排序。
接下来给出代码:
#include<stdio.h>
void bubble_sort(int arr[], int sz)//参数接受数组元素个数
{
for (int i = 0; i < sz - 1; i++)//n个元素就有n-1趟
{
int j = 0;
for (j = 0; j < sz - 1 - i; j++)
{
if (arr[j] > arr[j + 1])
{
int tap = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tap;
}
}
}
}
void print(int arr[], int sz)
{
for (int i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
}
int main()
{
int arr[] = { 3,1,7,5,8,9,0,2,4,6 };
int sz = sizeof(arr) / sizeof(arr[0]);
bubble_sort(arr, sz);
print(arr, sz);
return 0;
}
我们发现这段代码是可以解决我们的问题但是效率很低,因为假如我只有一组乱序本来应该只要执行一次程序,但是上述代码会跑完整个过程,有没有什么办法可以优化这段代码呢?
其实我们只要假设某一趟执行完就已经有序,然后不断判断就可以了,接下来我将给出优化后的代码:
#include<stdio.h>
void bubble_sort(int arr[], int sz)//参数接受数组元素个数
{
for (int i = 0; i < sz - 1; i++)//n个元素就有n-1趟
{
int flag = 1;//假设这一趟已经有序了
int j = 0;
for (j = 0; j < sz - 1 - i; j++)
{
if (arr[j] > arr[j + 1])
{
flag = 0;//发生交换就说明,无序
int tap = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tap;
}
}
if (flag == 1)//这一趟没交换就说明已经有序了,后续无无序排序了
break;
}
}
void print(int arr[], int sz)
{
for (int i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
}
int main()
{
int arr[] = { 3,1,7,5,8,9,0,2,4,6 };
int sz = sizeof(arr) / sizeof(arr[0]);
bubble_sort(arr, sz);
print(arr, sz);
return 0;
}
这就是冒泡排序的一个简单应用了,后面在数据结构的内容中我还会为大家介绍更多的排序算法。
五、二级指针
指针变量也是变量,是变量就有地址,那指针变量的地址存放在哪里?答案就是二级指针。
下面我们通过一幅图来了解一下二级指针
我们回想我们学习一级指针的时候pa是int*,int代表pa指向的对象是int类型的,*说明pa是指针变量;二级指针也与其类似ppa是int**,int*代表ppa指向的对象是int*类型的,*说明ppa是指针变量。
对于二级指针的运算有:
• *ppa通过对ppa中的地址进⾏解引⽤,这样找到的是pa,*ppa其实访问的就是pa.
int b = 20;
*ppa = &b;//等价于 pa = &b;
• **ppa先通过*ppa找到pa, 然后对pa进⾏解引⽤操作:*pa,那找到的是a.
**ppa = 30;
//等价于*pa = 30;
//等价于a = 30;
六、指针数组
1、指针数组的介绍
我们根据名字来理解一下,指针就是地址,数组的作用就是存放相同类型的元素,我们可以通俗的解释为:存放指针的数组
我们想一下之前学的整型数组和字符数组:
指针数组的每个元素都是用来存放地址(指针)的:
指针数组的每个元素是地址,又可以指向一块区域。
2、指针数组模拟二维数组
我们直接给出代码来看:
下面给出示意图:
parr[i]是访问parr数组的元素,parr[i]找到的数组元素指向了整型一维数组,parr[i][j]就是整型一维数组中的元素。上述的代码模拟出二维数组的效果,实际上并非完全是二维数组,因为每一行部并非是连续的。
七、二维数组传参的本质
我们通过前面的学习已经知道了:二维数组其实就是一维数组的数组,二维数组的每个元素都是一维数组。
我们用之前的方式来打印一个二维数组代码如下:
这里实参是二维数组,形参也写成二维数组的形式,我们思考一下还有什么其他的写法吗?
⾸先我们再次理解一下二维数组,二维数组其实可以看做是每个元素是一维数组的数组,也就是二维数组的每个元素是一个一维数组。那么二维数组的首元素就是第一行,是个一维数组。
如下图:
所以,根据数组名是数组首元素的地址这个规则,二维数组的数组名表示的就是第一行的地址,是一维数组的地址。根据上面的例子,第一行的一维数组的类型就是 int [5] ,所以第一行的地址的类型就是数组指针类型 int(* )[5] 。那就意味着二维数组传参本质上也是传递了地址,传递的是第⼀一行这个一维数组的地址,那么形参也是可以写成指针形式的。代码如下:
#include<stdio.h>
void test(int(*p)[5], int r, int c)
{
int i = 0;
int j = 0;
for (i = 0; i < 3; i++)
{
for (j = 0; j < 5; 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} };
test(arr, 3, 5);
return 0;
}
大家想必对这段代码形参部分很陌生,大家要想理解就需要自行在下面的内容中去寻找答案,我们现在首先要记住俩个关键点:
1、二维数组传参,形参的部分可以写成数组,也可以写成指针形式。
2、二维数组首元素的地址就是一个一维数组的地址
八、字符指针变量
1、介绍
在指针的类型中我们知道有⼀种指针类型为字符指针char*;一般使用如下:
下面给出另一种使用方式:
首先我们要知道这里的 “hello bit.”是一个常量字符串(不能被修改)
代码 const char* pstr = "hello bit."; 特别容易让同学以为是把字符串 hello bit 放到字符指针pstr里了,但是本质是把字符串 hello bit. 首字符的地址放到了pstr中。
上面代码的意思是把一个常量字符串的首字符 h 的地址存放到指针变量 pstr 中。
如果我们想要打印h就可以写成printf("%s",*pstr); 打印整个字符串要写成printf("%s",pstr);传入首元素的地址即可,切记不可以*pstr。
2、一道和字符串相关的笔试题
下面我们来看一道和字符串相关的笔试题:
#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相同。
上述题目比较的是俩个数组的首元素的地址而不是数组的内容,我们要是想比较俩个字符串的内容,就要使用strcmp函数来实现,后续会为大家介绍。
九、数组指针变量
1、介绍
我们联想一下前面学的指针数组,指针数组是一种数组,数组中存放的是地址(指针)。我们类比一下就可以知道这个数组指针变量实际上就是一个指针变量。
• 整形指针变量: int * pint; 存放的是整形变量的地址,能够指向整形数据的指针
• 浮点型指针变量: float * pf; 存放浮点型变量的地址,能够指向浮点型数据的指针
那数组指针变量应该是:存放的应该是数组的地址,能够指向数组的指针变量。
下面来猜一下哪个是数组指针变量
int* p1[10];
int (*p2)[10];
思考一下p1与p2分别是什么:
解释:p1先于[10]结合,变成了数组名(就类似于arr[10]这种),每个类型都是int*类型的;p2是数组指针变量,存放的是数组的地址,p2指向的就是一个数组的地址。
我们将他与我们之前所学的整型指针变量做一个全面的对比如下:
解释:p先和*结合,说明p是一个指针变量,然后指针指向的是一个大小为10个整型的数组。所以p是一个指针,指向一个数组,叫数组指针。这里要注意:[ ]的优先级要高于*号的,所以必须加上()来保证p先和*结合。
2、初始化
数组指针变量是用来存放数组地址的,那怎么获得数组的地址呢?就是我们之前学习的&数组名。
int arr[10] = {0};
&arr;//得到的就是数组的地址
如果要存放个数组的地址,就得存放在数组指针变量中,如下:
int(*p)[10] = &arr;
我们通过调试可以看到&arr与p的类型是完全一致的。
数组指针类型解析:
十、函数指针变量
我们通过前面的学习知道数组有自己的地址,那肯定会引发思考:函数有没有自己的地址呢?答案当然是有的,那么有数组指针变量有没有函数指针变量呢?答案依旧是有的而且和数组指针变量有着异曲同工之妙。
我们先来小结一下前面的所学内容:
整型指针:存放的是整型的指针,指向的是整型变量
字符指针:存放的是字符的指针,指向的是字符变量
数组指针:存放的是数组的地址,指向的是数组
函数指针:存放的是函数的地址,指向的是函数
1、介绍
我们首先通过一段代码来进行测试:
#include <stdio.h>
void test()
{
printf("hehe\n");
}
int main()
{
printf("test: %p\n", test);
printf("&test: %p\n", &test);
return 0;
}
我们发现输出的结果是一模一样的。确实打印出来了地址,所以函数是有地址的,函数名就是函数的地址,当然也可以通过&函数名的方式获得函数的地址。
如果我们要将函数的地址存放起来,就得创建函数指针变量咯,函数指针变量的写法其实和数组指针非常类似。如下:
void test()
{
printf("hehe\n");
}
void (*pf1)() = &test;
void (*pf2)()= test;
int Add(int x, int y)
{
return x+y;
}
int(*pf3)(int, int) = Add;
int(*pf3)(int x, int y) = &Add;//x和y写上或者省略都是可以的
函数指针类型解析:
我们也可以通过函数指针调用指针指向的函数:
#include <stdio.h>
int Add(int x, int y)
{
return x+y;
}
int main()
{
int(*pf3)(int, int) = Add;
printf("%d\n", (*pf3)(2, 3));
printf("%d\n", pf3(3, 5));
return 0;
}
这里的*依旧是可以省略,大家可以自行去运行看看结果。
2、俩段有趣的代码
代码一:(*(void (* )( ) )0)( );
大家可以自行去思考一下,下面给出答案:这段代码是在调用0地址处的一个函数。
解释:
1、void(* )( )是一个函数指针类型,这个指针指向的函数没有参数,返回类型是void
2、(void(* )( ))0这段代码是在将0强制类型转换为这种函数指针类型,这就意味这0地址处有这么一个函数
3、(*(void(* )( ))0)( )对0地址进行解引用,去调用0地址处的这个函数,根据函数指针的类型我们可以知道这个函数没有参数也没有返回值。
代码二:void (*signal(int , void(* )(int)))(int);
大家可以自行思考一下,下面给出答案:这段代码是一次函数的声明,声明的这个函数叫signal。
解释:
1、signal函数有2个参数,第一个参数是int类型的,第二个参数是一个函数指针类型(void(* )(int)),这个函数指针指向的函数参数是int,返回类型是void
2、signal函数的返回类型也是一个函数指针类型(void(* )(int)),该函数指针指向的函数参数是int,返回类型是void
3、实际上我们可以这么看(void(* )(int))signal(int,void(* )(int));写成这样可以便于我们理解但是实际敲代码的时候不可以敲成这样
3、typedef关键字
typedef是用来类型重命名的,可以将发杂的类型简单化。
比如,你觉得 unsigned int 写起来不方便,如果能写成 uint 就方便多了,那么我们可以使用:
typedef unsigned int uint;
//将unsigned int 重命名为uint
如果是指针类型,能否重命名呢?其实也是可以的,比如,将 int* 重命名为 ptr_t ,这样写:
typedef int* ptr_t;
但是对于数组指针和函数指针稍微有点区别:比如我们有数组指针类型 int(* )[5] ,需要重命名为 parr_t ,那可以这样写:
typedef int(*parr_t)[5]; //新的类型名必须在*的右边
函数指针类型的重命名也是⼀样的,比如,将 void(* )(int) 类型重命名为 pf_t ,就可以这样写:
typedef void(*pfun_t)(int);//新的类型名必须在*的右边
那么要简化上面的代码二,可以这样写:
typedef void(*pfun_t)(int);
pfun_t signal(int, pfun_t);
十一、函数指针数组
我们通过名字应该就可以猜到了这又是一个数组,数组里的元素应该是函数指针。
1、介绍
函数指针数组是指针数组的一种,存放的是函数指针。那函数指针的数组如何定义呢?
int (*parr1[3])();
int *parr2[3]();
int (*)() parr3[3];
大家可以来猜一下哪个是函数指针数组,答案应该是parr1。
解释:parr1先和[ ]结合,说明parr1是数组,数组的内容是什么呢?是 int (* )( ) 类型的函数指针。
下面我们简单使用一下:
int Add(int x, int y)
{
return x + y;
}
int Sub(int x, int y)
{
return x - y;
}
int Mul(int x, int y)
{
return x * y;
}
int Div(int x, int y)
{
return x / y;
}
#include<stdio.h>
int main()
{
int (*parr[4])(int, int) = { Add,Sub,Mul,Div };
int i = 0;
for (i = 0; i < 4; i++)
{
int r = parr[i](9, 3);
//这里可以不用写*,如果要解引用应该写成(*parr)[i]
printf("%d\n", r);
}
return 0;
}
2、应用——转移表
函数指针数组的用途:转移表
举例:计算器的一般实现:
#include<stdio.h>
void menu()
{
printf("*******************\n");
printf("***1.Add 2.Sub***\n");
printf("***3.Mul 4.Div***\n");
printf("***0.exit ***\n");
printf("*******************\n");
}
int Add(int x, int y)
{
return x + y;
}
int Sub(int x, int y)
{
return x - y;
}
int Mul(int x, int y)
{
return x * y;
}
int Div(int x, int y)
{
return x / y;
}
int main()
{
int x;
int y = 0;
int input = 1;
int ret = 0;
do
{
menu();
printf("请选择:>");
scanf_s("%d", &input);
switch (input)
{
case 1:
printf("请输入操作数:>");
scanf_s("%d %d", &x, &y);
ret = Add(x, y);
printf("ret = %d\n", ret);
break;
case 2:
printf("请输入操作数:>");
scanf_s("%d %d", &x, &y);
ret = Sub(x, y);
printf("ret = %d\n", ret);
break;
case 3:
printf("请输入操作数:>");
scanf_s("%d %d", &x, &y);
ret = Mul(x, y);
printf("ret = %d\n", ret);
break;
case 4:
printf("请输入操作数:>");
scanf_s("%d %d", &x, &y);
ret = Div(x, y);
printf("ret = %d\n", ret);
break;
case 0:
printf("退出计算器\n");
break;
default :
printf("选择错误,请重新选择:>\n");
break;
}
} while (input);
return 0;
}
通过这段代码我们发现我们可以很简单的实现我们想要的程序,但是我们要思考一下这段代码有大量的重复部分,有点冗余了,我们可不可以简化这段代码,再结合我们之前学的函数指针数组,我们便可以很简单实现:
#include<stdio.h>
void menu()
{
printf("*******************\n");
printf("***1.Add 2.Sub***\n");
printf("***3.Mul 4.Div***\n");
printf("***0.exit ***\n");
printf("*******************\n");
}
int Add(int x, int y)
{
return x + y;
}
int Sub(int x, int y)
{
return x - y;
}
int Mul(int x, int y)
{
return x * y;
}
int Div(int x, int y)
{
return x / y;
}
int main()
{
int x, y;
int input = 1;
int ret = 0;
int(*p[5])(int x, int y) = { NULL,Add,Sub,Mul,Div };
do
{
menu();
printf("请选择:>");
scanf_s("%d", &input);
if ((input <= 4 && input >= 1))
{
printf("请输入操作数:>");
scanf_s("%d %d", &x, &y);
ret = (*p[input])(x, y);
printf("ret = %d\n", ret);
}
else if (input == 0)
{
printf("退出计算器\n");
}
else
{
printf("输入有误\n");
}
} while (input);
return 0;
}
这样代码明显变得简单了许多,大家也可以继续寻找看看代码还有没有更好的改进办法。
十二、下期预告
预计十天左右给大家更新<下>(作者目前不在家中),在更新完指针后我会花几天时间更一篇指针特别篇里面全是指针的题目与关键内容,帮助大家更好的解决指针的疑惑同时加深巩固已学内容,共勉之!