学习完了指针初阶后,相信你对指针一定有了一个初步、清晰的认识。下面我们将进入【进阶】部分的学习,难度会逐渐上升↑ Are you ready?
一、字符指针
1、指针存放单字符
在初阶部分,我们有学习到了不同的指针类型,其中就包含一种叫做【字符指针】,我这里再重点拎出来说说
- 所谓字符,也就是这个指针它指向一个字符
char ch = 'w'; char* pc = &ch;
- 那既然这指针指向了这个字符,即存放了这个字符的地址。我就可以通过
*
解引用去访问到这个地址中的内容,然后去进行一个修改
*pc = 'x';
运行之后可以看到字符ch的内容确实发生了变化
- 但若是我在初始化指针变量
pc
的时候在前面加上一个【const】作为修饰,此时还可以像上面这样去修改吗
const char* pc = &ch;
通过运行结果可以看出是不可以的,加上【const】作为修饰后pc
就为常量指针,其所指向的内容是不可以修改的,具体可以看看常量指针和指针常量的感性理解
2、指针存放字符串
对于字符来说,不仅接收单个字符,还可以一个字符串的首元素地址,我们来看看
char* ps = "abcdef";
- 可以看到通过
*
解引用访问到的是该字符串的首字符,因为指针里面指存放了它的地址,这就和一个整型指针里面存放了一个数组的首元素地址是同样的道理
- 不过有很多通过就会将此理解为把整个字符串
abcdef
存放到字符指针ps中,这其实是不对的,我们通过画图的方式来理解一下
不过对于上面这种写法其实还有一种缺陷,因为字符串是一个常量,那对于常量而言是不可修改的,但是我们却将其地址给到了一个字符型指针,那此时就可以通过循环的方式解引用修改整个字符串,这就不合乎逻辑了,所以在初始化字符指针
ps
的时候应该在前面带上一个【const】
- 其实就和上面存放单个字符的字符指针是一个意思
const char* ps = "abcdef";
3、一道剑指offer的面试题
- 下面是一道剑指offer中有关【字符指针】的面试题,放在这里作为讲解。接下去我想问:在两组字符串进行比较后输出的结果为多少
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; }
结果如下:
那有的同学就很疑惑,
str1
和str2
明明就是一样的,为何输出打印的结果是【are not same】呢?
- 我们在操作符章节有讲到
==
这个运算符,若只是两个普通变量之间的比较,用它就可以了,但是对于两个字符串之间的比较,可不能使用这个,而要用库函数中的strcmp,具体的规则可以查看官方文档,后期会出专门的文章做讲解 - 使用
==
运算符进行比较的时候并不是比较的两个字符串的内容,而是地址。那它们在定义的时候编译器分别为它们分配了各自的空间,所以它们的空间是独立的,内存地址也是不一样的。
💬那有同学问:那str3
和str4
又怎么解释呢?
- 还记得上面讲到过的【字符串是一个常量】这个概念吗,对于常量而言,是存放在内存中的只读数据区,也就是代码段,常量一般都存放在这个区域中,里面还存放有代码编译出来的一些指令,对于指令是不可以修改的,可以看看 C/C++内存分布
- 那对于这些常量字符串来说在内存中只会保存一份,也就是说str3和str4都指向内存中的同一块空间,那它们的地址就是相同的,所以输出的结果就是【are same】
二、指针常量与常量指针
【引入】
- 首先来看看下面这段代码,首先我定义了一个变量num为10,然后又对其进行了一个赋值修改,打印出来之后就是修改之后的值【相信这是最基本的认识】
int main(void) { int num = 10; num = 20; printf("num = %d\n", num); return 0; }
- 但若是我可以修改num值的话,别人也可以修改了,这就没有了安全性。所以我想给它加上一把锁🔒使得它无法被修改,这里介绍一种C语言中的关键字【const】,这个我在初识C语言也有说到过,若是我们在定义变量的时候在前面加上一个
const
做修饰,此时这个变量就会变成【常量】 - 这个就和Java中的 final关键字 是一个道理,若是加上了这个关键字做修饰之后,就要在定义的时候对其进行一个初始化,并且后面不能去修改它的值
const int num = 10;
- 可以看到,在加上
const
常进行修饰之后,这个变量就无法被修改了,若是有人想要去修改的话编译器就会报出警告⚠
以上均为引言,接下去我们来说说有关【常量指针】和【指针常量】之间的区别
【常量指针】
1、介绍与分析
- 上面看到,因为在定义num的时候前面加上了
const
常的修饰,就使得它变成了一个常量,无法被修改,在指针初阶章节,我有介绍过可以将一个指针进行解引用去修改这个指针所指向那块地址的值
int* p = # *p = 20;
- 可以看到,确实可以对其进行一个修改
- 那此时这个num的安全性就又降低了,所以我想再做制裁🗡,使得指针也无法对其解引用进行一个修改
- 那么又需要使用上面所说的
const
修饰符,也是和修饰num一个道理,只需要在前面加上一个【const】作为修饰即可
const int* p = #
- 可以看到,此时我们通过指针解引用的方式也无法对其进行修改❌
- 虽然是不可以通过指针解引用去修改这个指针所指向的值,但是可以去修改这个指针的指向,令其重新指向一个变量的地址,这是合法的
const int num = 10; //num = 20; int num2 = 20; const int* p = # //*p = 20; //err p = &num2;
- 不过原理还是一样的,我们无法通过这个指针进行解引用去修改它所指向的值
2、小结与记忆口诀
以上所描述的就是【常量指针】,一起来总结一下:book:
- 总结:对于常量指针而言,是将【const】放在
*
的左边,表示的是指针所指向的内容不能通过指针来修改,但指针变量本身可修改 - 口诀:常量指针所指向的是一个常量,不能修改;但是指针本身不是常量,可以修改
【指针常量】
知道了什么是【常量指针】,接下去让我们来看看什么是【指针常量】
1、介绍与分析
- 刚才我们将
const
放在*
的左边,现在我们换个地方,将它放在*
的右边试试
int* const p = #
- 此时若再去做这两步操作的时候你就会发现和【常量指针】完全不同,可以通过指针解引同去修改指向的值,但是无法再次修改指针的指向
*p = 20; p = &num2; //err
2、小结与记忆口诀
以上所描述的就是【指针常量】,一起来总结一下:book:
- 总结:对于指针常量而言,是将【const】放在
*
的右边,表示的是指针变量本身的指向不能修改,但是指针指向的内容可以通过指针来修改 - 口诀:指针常量这个指针本身就是一个常量,不能修改;但是指针所指向的内容不是常量,可以修改
👉一份凉皮所引发的故事👈
可能还是有同学对它们之间的关系不太理解。没关系,我们通过一个生活中的场景来介绍一下
- 现在这里有三行代码,有一个常量num指针p里面保存了它的地址,还有一个常量num2
- 我们假设这个
指针p
为一个女孩,num
为一个男孩,他是这个女孩的男朋友。有一天男孩陪女孩去逛街,女孩看到路边有人在卖凉皮,所以就想要男孩给他买一份凉皮吃,可是呢男孩身上只有【10块钱】,若是给女朋友买了凉皮自己就没钱用了,于是说:“不行,不给你买,凉皮有什么好吃的😕”
- 于是这个时候女孩就==生气了==,就对男孩说:“一份凉皮都不舍得给我买,还算是我男朋友吗?分手!”,于是看另一个男孩还不错,就想去找另一个男孩【他身上有100块钱】
- 于是这个时候男孩就不乐意了,好不容易追到的女朋友(不是靠钱),怎么能说分手就分手呢,不能分。此时它就做了一个动作:在这个操作符
[*]
的前面加上了const
作为修饰符,我们来回顾一下前面的知识
- 这里的
*p = 0
就相当于是指针通过解引同让num = 0
,那指的就是让男孩变得身无分文;这里的p = &num2
指的就是重新修改指针p的指向,使其指向另一个值的地址。👉这就是【常量指针】
- 此时男孩意识到事情的严重性,那个男的身上这么有钱,万一被它拐走了。想了想还是去给她买吧,一份凉皮罢了,就和女孩说:“行行行,给你买,但是你不可以换男朋友”。此时他就又做了一个动作:在这个操作符
[*]
的后面加上了const
作为修饰符,去掉了前面的const
- 同理,这里的
*p = 0
就相当于是指针通过解引同让num = 0
,那指的就是让男孩变得身无分文;这里的p = &num2
指的就是重新修改指针p的指向,也就是换一个男朋友。👉这就是【指针常量】
建议广大女性读者选择第二种男朋友,若是想下面这样的,就直接分手吧
- 在
[*]
的前后都加上了const修饰符,那么既无法通过指针去修改所指向的值,也无法修改指针的指向,虽然这使代码变得非常安全,但在还是没有这个必要╮(╯▽╰)╭ - 要想一个男朋友连吃的都不给你买,而且还不准你换男朋友,强行霸占你🔨这种情况还是赶紧分手吧!
【总结一下】:
- 对于【常量指针】而言,是将const放在
[*]
左边的,指针所指向的内容不能通过指针来修改,但指针变量本身可修改 - 对于【指针常量】而言,是将const放在
[*]
右边的,指针变量本身的指向不能修改,但是指针指向的内容可以通过指针来修改
三、指针数组与数组指针
本模块我们来介绍指针数组与数组指针之间的区别
【指针数组】
首先我想问你一个问题:指针数组是一个指针还是一个数组呢?
1、概念明细
- 好,解答一下上面的问题,对于【指针数组】来说,它是一个
数组
,而不是指针
int arr1[5]; //整型数组 - 存放整数的数组 char arr2[5]; //字符数组 - 存放字符的数组 int* arr3[5]; //指针数组 - 存放指针的数组
- 来看一下上面这三个数组的定义
- 对于
arr1
,他是一个整型数组,它里面存放的都是整数
; - 对于
arr2
,他是一个字符数组,它里面存放的都是字符
; - 对于
arr3
,他是一个指针数组,它里面存放的都是指针
;
- 通过这么的对比相信你对【指针数组】有了一初步的概念,它也是一个数组,里面放的都是指针
下面两个模块我将带你来回顾一下数组中的相关知识
2、数组地址偏移量与指针偏移量
- 首先对于一个数组而言,我们如果可以得到它的首元素地址,然后通过这个地址就可以顺藤摸瓜🍈就可以获取到后面的所有元素
- 但是光这么直接用
arr[0]
来访问太累了,不妨我们将数组的首元素地址给到一个指针变量,让它保存下这个地址,然后让它逐步地向后移动。如果对指针还不是很了解的看看这篇文章——> 底层之美,莫过于C【1024,从0开始】先去了解一下什么是指针
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; int* p = &arr[0];
- 可以看到,指针变量里面存放着的是数组arr的首元素地址,那我们现在要通过这个指针变量去访问到后面的所有元素该怎么做呢?
- 首先我们考虑先访问到第二个元素,要访问到一个元素首先考虑找到这个元素所在的地址,
p
指针第一个元素所在的地址,那么p + 1
便是指向2所在元素的地址,那要访问到这个地址上所在的内容,那就要使用到*
这个符号,对这块地址进行解引用*(p + 1)
,此时就可以访问到2这个元素了。那找3,找4也是一样的,只需要让这个指针向后偏移即可,所以我们可以通过循环去找,访问第i个元素便是*(p + i)
- 可能有些同学还是不太理解,没关系,我们通过代码来验证一下
for (int i = 0; i < 10; ++i) { printf("%p == %p\n", p + i, &arr[i]); } printf("\n");
- 可以看到,无论是对于
p + i
还是&arr[i]
,它们每次所访问的地址都是一样的,这其实也就意味着指针变量p在偏移的过程中相当于在代替数组首元素地址向后偏移
有了这些知识作为铺垫,我们就可以去尝试访问数组中的所有内容了
因为一维数组是一块连续的存储空间,所以我们只要得到这个数组的首元素地址。就可以通过p + i这样的方式找到它之后所有元素的地址,并且把他们地址进行解引用便能访问到数组中的所有元素
int main(void) { int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; int* p = &arr[0]; for (int i = 0; i < 10; ++i) { printf("%d ", *(p + i)); } printf("\n"); return 0; }
- 可以看到,通过将数组的首元素地址给到指针变量p,然后再使这个指针变量一位一位地向后偏移,每次偏移一个元素即4个字节,第i个元素的地址即为
p + i
,而当我们要去访问这个地址的内容时,直接对其进行解引用即可*(p + i)
,然后便可以看到数组中的十个元素都被打印出来了
3、指针变量与数组名的置换【✔】
- 因为【数组名 = 首元素地址】,那不妨
int* p = &arr[0]
便可以写成int* p = arr
,Ctrl + F5让代码走起来可以看到结果也是一样的
- 那我这么做就相当于是把arr赋给了p,那此时
arr
和p
也就是一回事,那也可以说【arr <==> p】,所以我们在使用到arr的地方可以换成p,使用到p的地方可以换成arr
- 那这个时候突然就想到一点我们上面在打印数组元素的时候都是使用
arr[i]
,那此时是不是可以将arr[i]
和*(arr + i)
做一个联系呢?当然是可以的:smile:
- 因为arr为数组名,数组名表示这个数组的首元素地址。首元素地址向后偏移i个位置之后到达下标为i的那个元素所在的位置,再对其进行解引用就找到下标为i这个地址所对应的元素——这也就是对于【*(arr + i)】的一个解释
- 那对于【arr[i]】又要怎么去解释呢?还记得我一开始讲一维数组的使用时说到
[]
是一个数组访问的操作符,那既然是操作符的话就会有操作数,操作数是谁呢?就是【arr】和【i】,那此时当我将arr[i]转换成*(arr + i)的时候,()
里面的也就是这两个操作数,根据==加法的交换律==就可以将【arr】和【i】进行一个交换,那也就变成了*(i + arr)
。 - 此时就可以去进行一个类推,因为
*(arr +i)
可以写成arr[i]
<—— ⭐ - 那么
*(i + arr)
是否可以写成i[arr]
呢 <——⭐
此时我们通过代码来尝试一下,将推测转化为实际
- 可以看到,依旧是可以的w(゚Д゚)==不过这种写法了解一下即可,不是很好理解,也不会用到==
- 刚才有说到
arr
和p
其实是一回事,那可以写【arr[i]】,是不是也可以写成【p[i]】呢?答案是:当然可以!
看完上面的这些,相信你已经晕了(((φ(◎ロ◎;)φ))),不过没有关系,将知识点做个总结就可以很清晰了
arr[i] == *(arr + i) == *(p + i) == p[i]
4、实例讲解
回顾了数组的相关知识后,再来看【指针数组】相关内容,就变得易如反掌:hand:
① 指针数组存放地址
- 好,首先来看到第一个案例,我定义了五个变量分别对它们进行了一个初始化,然后定义了一个指针数组,首先你要想到的就是
[指针接受地址]
这个概念 - 所以我将这五个变量的地址都存放到了这个【指针数组】中,然后去遍历这个数组便可以访问到这五个变量的地址了
int main(void) { int a = 1; int b = 2; int c = 3; int d = 4; int e = 5; int* arr[5] = { &a, &b, &c, &d, &e }; for (int i = 0; i < 5; ++i) { printf("%d ", *(arr[i])); } printf("\n"); return 0; }
- 接下去你要想到的就是
[解引用]
这个知识点,我说到指针其实就是地址,那对地址进行一个解引用其实可以将[*]
和[&]
进行一个抵消,这也就取到了五个变量的地址,通过下标i控制就遍历到了这五个变量
② 指针数组存放数组
- 好,再来看下面这段代码,我定义了三个整型数组,数组的个数都是5,然后又定义了一个指针数组,将三个整型数组的数组名都存放进去,我们知道
数组名即为首元素地址
,所以这是合法的 - 接下去我就要通过这个指针数组访问到这三个整型数组中的所有元素
int arr1[5] = { 1, 1, 1, 1, 1 }; int arr2[5] = { 2, 2, 2, 2, 2 }; int arr3[5] = { 3, 3, 3, 3, 3 }; int* parr[3] = { arr1, arr2, arr3 }; for (int i = 0; i < 3; ++i) { for (int j = 0; j < 5; ++j) { printf("%d ", *(parr[i] + j)); } printf("\n"); }
- 通过算法图示来看看,外层的遍历,可以访问到这个三个数组的首元素地址,此时我们若还要去访问到每个数组中的元素的话,就要再通过一个内部的循环去遍历每一个数组,这个操作的话相信你看过我的数组文章一定是没问题的
- 这里的
parr[i] + j
也就是位于每个数组的首地址向后偏移j个位置,所以访问到的就是下标为j这个位置的地址,但是我们要访问值的话就要加上一个解引用的操作。当然,通过【*】和【()】的规则我们也可以将*(parr[i] + j)
转换为*(*(parr + i) + j)
或者是parr[i][j]
- 来看一下运行结果
在学习了【指针数组】后,来辨析一下三个数组吧
int* arr1[10]; char* arr2[4]; char** arr3[5];
- 首先第一个arr1,数组大小为10,数组里面存放的都是
int*
的整型指针 - 然后第二个arr2,数组大小为4,数组里面存放的都是
char*
的字符指针 - 最后第三个arr3,数组大小为5,数组里面存放的都是
cahr**
的二级字符指针