掌握C语言指针,轻松解锁代码高效性与灵活性(中)
✨✨ 欢迎大家来到贝蒂大讲堂✨✨
🎈🎈养成好习惯,先赞后看哦~🎈🎈
所属专栏:C语言学习
贝蒂的主页:Betty‘s blog
1. 引言
前面给大家介绍了一些指针的基本概念,今天就让我们继续深入指针的世界,和贝蒂一起打败指针大魔王吧
2. 二级指针
指针变量也是变量,是变量就有地址,那我们就把存放指针变量地址的指针称为二级指针。
可能理解起来有点绕,我们可以通过下面示意图演示一下
代码如下:
int a = 10;
int* pa = &a;//一级指针,存放a的地址
int** ppa = &a;//二级指针,存放指针变量p的地址
- 不能直接把&&a赋值给ppa哦,因为&&在C语言中是且的意思”
(1)对ppa解引用,找到pa,也就是说*ppa==pa
(2)对pa解引用,找到a,也就是说**ppa==a
int* b = *ppa;//找到a的地址
int c = **ppa;//找到a
- 依次内推我们可以衍生出三级指针,四级指针。
3. 数组与指针的关系
3.1 数组名的理解
我们在前面学习数组时就明白,数组名是首元素地址,但是讲解的不够深入,今天就让我们深入了解一下吧~
首先让我们观察一下如下代码
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
//&arr[0],arr,&arr的区别
printf("&arr[0] = %p\n", &arr[0]);//首元素地址
printf("arr = %p\n", arr);//一维数组数组名
printf("&arr = %p\n", &arr);//对整个数组取地址
return 0;
}
从结果来说&arr[0],arr,&arr到底有什么区别呢?
让我们再看看下面这段代码
#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;
}
输出结果:
-
&arr[0]与arr+1都是跳过4个字节,相当于跳过1个整型元素。
-
&arr+1跳过40个字节,相当于10个整型,也就是整个数组。
总结:arr与&arr[0]都是首元素地址,指向数组第一个元素。&arr以首元素地址表示,但是指向的是整个数组。
3.2 sizeof与数组名
我们知道sizeof实际上是获取了数据在内存中所占用的存储空间,单位是字节。
让我们看看下面这段代码吧
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("%d\n", sizeof(arr));//计算大小
return 0;
}
输出结果:40
不知道大家有没有疑惑?如果数组名是首元素地址的话,我们知道在32位机器上大小为4,在64位机器上大小为8。那为什么是40呢?
其实数组名就是数组⾸元素(第⼀个元素)的地址,但是有两个例外:
• sizeof(数组名),sizeof中单独放数组名,这⾥的数组名表⽰整个数组,计算的是整个数组的⼤⼩,单位是字节
• &数组名,这⾥的数组名表⽰整个数组,取出的是整个数组的地址(整个数组的地址和数组⾸元素的地址是有区别的)
3.3 数组与指针等价关系
假设有一个一维数组和一个二维数组
int arr[5]={1,2,3,4,5};
int arr[3][3]={{1,2,3},{4,5,6},{7,8,9}}
我们要访问它的每个元素,有哪些方法呢~
- 数组访问
int arr1[5] = { 1,2,3,4,5 };
for (int i = 0; i < 5; i++)
{
printf("%d ", arr1[i]);
}
int arr2[3][3] = { {1,2,3},{4,5,6},{7,8,9} };
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 3; j++)
{
printf("%d ", arr2[i][j]);
}
printf("\n");
}
- 指针访问
for (int i = 0; i < 5; i++)
{
printf("%d ", *(arr1+i));
}
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 3; j++)
{
printf("%d ", *(*(arr2 + i) + j));
}
}
通过对上面代码的观察,我们可以总结如下规律
-
arr[i]与*(arr+i)等价。
-
arr[i][j]与((arr+i)+j)等价。
3.4 指针数组
(1) 指针数组的概念
指针数组顾名思义就是存放指针的数组 ,数组中每个元素都是指针,存放的都是地址。
int*parr1[5];//存放五个整型指针变量
char*parr2[5];//存放五个字符指针变量
float*parr3[5];//存放五个浮点数指针变量
代码示例
int arr1[] = { 1,2,3 };
int arr2[] = { 4,5,6 };
int arr3[] = { 7,8,9 };
//将每个数组的首元素地址都存进去
int* parr[3] = { arr1,arr2,arr3 };
示意图:
(2) 指针数组的理解
int main()
{
int arr1[] = { 1,2,3 };
int arr2[] = { 4,5,6 };
int arr3[] = { 7,8,9 };
int* parr[3] = { arr1,arr2,arr3 };
printf("%p\n", parr);//打印指针数组首元素地址,也就是打印存放arr1空间的地址
printf("%p\n", parr[0]);//arr1数组首元素地址
printf("%p\n", *parr);//arr1首元素地址
printf("%d\n", **parr);//相当于对arr1首元素地址解引用,指的的是1
printf("%d\n", *parr[0]);//也相当于对arr1首元素地址解引用,为1
printf("%d\n", *parr[1]);//相当于对arr2首元素地址解引用,为4
return 0;
}
输出结果:
012FFE30
012FFE6C
012FFE6C
1
1
4
(3) 模拟二维数组
通过上述我们对指针数组的理解,我们可以间接来模拟出二维数组。
代码如下:
int main()
{
int arr1[] = { 1,2,3 };
int arr2[] = { 4,5,6 };
int arr3[] = { 7,8,9 };
//将每个数组的首元素地址都存进去
int* parr[3] = { arr1,arr2,arr3 };
int i = 0;
int j = 0;
for (i = 0; i < 3; i++)
{
for (j = 0; j < 3; j++)
{
printf("%d ", parr[i][j]);
}
printf("\n");//换行)
}
return 0;
}
- 模拟的二维数组并不是真正的二维数组,因为二维数组在内存中是连续存储的,而模拟出来的数组内存存储并不连续
3.5 数组指针
(1) 数组指针的概念
同理,指针数组的本质是一个数组;那么数组指针的本质就是个指针,指向一个数组的指针。
int(*parr1)[5];//指向一个有五个元素的整型数组
char(*parr2)[5];//指向一个有五个元素的字符数组
float(*parr3)[5];//指向一个有五个元素的浮点数数组
(2) 数组指针的理解
int main()
{
int arr[5] = { 1,2,3,4,5 };
int(*parr)[5] = &arr;
//对数组名取地址代表整个数组的地址
printf("%p\n", parr);//整个数组的地址一般用数组首元素地址表示
printf("%p\n", parr[0]);//相当于*(parr+0)==arr,首元素地址
printf("%p\n", *parr);//首元素地址
printf("%d\n", **parr);//相当于对首元素地址解引用,指的的是1
printf("%d\n", *parr[0]);//也相当于对首元素地址解引用,为1
printf("%d\n", *parr[1]);//等价于*(*(parr+1)),parr+1跳过一个数组大小的地址,越界访问
return 0;
}
输出结果:
012FF6F0
012FF6F0
012FF6F0
1
1
-858993460(越界访问,随机数)
示意图:
3.6 指针数组与数组指针的区别
可能有许多小伙伴区别不清楚指针数组与数组指针,但是如果写成指针的数组,数组的指针,可能更好理解。接下来让我们具体分析一下吧?
首先我们要清楚一个优先级顺序:()>[]>*
-
在int*parr[]中,parr先与[]结合(数组),而parr前面声明的变量类型是int*。所以这是一个数组,数组中每个元素的类型是int*的指针,这一类我们统称为指针数组。
-
在int(*parr)[]中,parr先与结合(指针),而后除开(parr)是一个int []的数组类型。所以这是一个指针,这个指针指向的是一个数组,这一类我们称为数组指针。
3.7 字符串
我们先看一下下面这段代码
char arr1[]="im betty";
char arr2[]={'a','b','c','\0'};
这是一种常见的字符串的表示形式,以'\0'作为其结尾标志。
但是还有另外一种表示形式,代码如下
//const可以省略
const char* p1 = "im betty";
const char* p2 = "abc";
我们知道const修饰在*前,不能改变指针变量所指向的值,所以这个字符串是不能改变的,这种字符串我们称为常量字符串
- 本质上是将字符串中的首元素地址存放进指针变量。
知道这些之后,让我们来看一道题吧
输出什么?
#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相同。
3.8 数组传参
(1) 一维数组传参
我们在之前学习函数时候就讲过一维数组传参,让我们来复习一下吧。
代码如下
void print(int arr[])//写成数组的形式
{
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", arr[i]);
}
}
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
print(arr);//将数组传递给print函数
return 0;
}
我们传参是传的数组名,我们知道数组名是首元素的地址,既然是地址,自然就能用指针来接受,所以就有了另外一种写法。
void print(int*p)//用指针来接收
{
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ",*(p+i));
}
}
(2) 二维数组的传参
先让我们看看一般二维数组是如何传参的吧
void print(int arr[][3])//行可以省略,列不可以
{
int i = 0;
for (i = 0; i < 3; i++)
{
int j = 0;
for (j = 0; j < 3; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
int main()
{
int arr[3][3] = {{1, 2, 3}, { 4,5,6 }, { 7,8,9 }};
print(arr);//将数组传递给print函数
return 0;
}
那么指针接收如何写呢,还是int*p吗,我们知道二维数组可以看成把每一行当做一个元素的一维数组,数组名首元素地址自然是第一行元素的地址,所以要用数组指针来接收哦~
代码如下:
void print(int(*p)[3])//明确元素个数
{
int i = 0;
for (i = 0; i < 3; i++)
{
int j = 0;
for (j = 0; j < 3; j++)
{
printf("%d ",*(*(p+i)+j));
}
printf("\n");
}
}
本文由博客一文多发平台 OpenWrite 发布!