一、前言🥳
距离上一次更新已经好几天了,这里我向读者深表歉意😭,这一次的内容确实比较多,我也花了不少时间整理,但是我向你们保证绝对不会断更的😎,请大家继续支持😝。在前一篇文章中,我们学习了指针的部分内容,感兴趣的小伙伴可以移步上篇文章进行阅读和学习。现在我们接着学习指针的有关内容。
二、正文😁
1.const修饰指针😋
const这个关键字有两个作用:
- const修饰普通变量
- const修饰指针变量
1.1 const修饰普通变量
首先,我们对const修饰普通变量进行讲解。
对于一个普通变量,它的值是可以进行修改的,如图:
对于一个被const修饰的变量,它的值是不可以通过上图的方式进行修改😣。const是常属性的意思,被const修饰后,变量具有了常属性(不能被修改)。const可以放在int前,也可以放在int后,只是我们一般放在int前。
这里做一个小小的知识延申:🥺
在C语言中,图示中的a是常变量,它的本质还是变量,只是在const修饰的情况下,编译器在语法层面上不允许修改这个变量。
在C++语言中,图示的a是常量。
在C语言中,C99之前,在一个数组的创建中,数组的大小,我们只能给一个常量值,结合我们上述知识,我们将a用const修饰之后,使a具有常属性,现在我们可以用a作为数组的大小吗?试试看!
很显然,这样是行不通的,因为在此时a的本质还是变量。
前文中,我们说到在C++语言中,经过const修饰,我们的a变为了常量,我们不妨试试在C++语言下的情况😤。
我们的编译通过了!所以在C++中,const修饰的变量会变为常量。🤫
但是呢,一开始我提到不可以用以上的方式进行修改,意味着通过其他方式可以进行修改,我们可以首先拿出a的地址交给p,通过p找到a进行修改,如图:
不过,我们使用const对变量进行修饰,就是为了是它具有常属性,不让别人对它进行改变,我们这样做有些刻意违反规则,我们不如对指针也进行const修饰。
1.2 const修饰指针变量
const修饰指针有3种情况:
- const放在*的左边
- const放在*的右边
- const放在*的两边
我们先对没有加const的进行理解,我在这里引入一个例子:
int num =100;
int* p =#
num在内存申请了一块空间存放值100,这块空间的地址我们假设为0x0012cc60,我们在上述中,用p来存放num的地址。这里我们对num的值的修改可以有两种方法👻:
- 对p里面存的地址进行修改,如:
int n =10;
p = &n;
- 利用地址找到num进行修改,如:*p =200
情况1:const放在*的左边
例:
int const *p
const int *p
此时const限制的是* p,表示指针指向的内容不能通过指针来改变了。
但是指针变量本身的值是可以进行修改的,如图:😲
情况2:const放在 * 的右边
int * const p
此时const限制的是p本身,表示指针变量p本身不可以修改了,但是指针指向的内容可以通过指针变量来修改。
情况3: const放在*的两边
const int* const p;
此时,指针变量p不能被修改,指针变量的内容也不能被修改。
2.指针的运算😃
指针的基本运算有三种:
- 指针 ± 整数
- 指针 - 指针
- 指针的关系运算
2.1 指针 ± 整数
对于一个数组,里面的元素在内存中的存放是连续的,我们只需知道第一个元素的地址,我们就能找到后续所有元素的地址。
这里我们通过指针,分别拿到了数组中各个元素的地址,我们再进行解引用,就可以拿到各个元素,如下图:
通过上面的例子,不难想到只要我们拿到某个内存单元的地址,我们就可以对它周边的内存单元进行访问,因为内存单元是连续的。🥰
2.2 指针 - 指针
(指针 - 指针)相当于(地址 - 地址),得到的是指针之间的元素个数(会有情况带负号)。
但是呢,我们来看下一组例子:
此处我们可以看出,这里是有大小地址之分的,大地址减小地址是正数,小地址减大地址是负数。
注意:这种运算的前提是,两个指针指向同一块内存空间。
这段代码的错误有两点:😫
- arr1和arr2的两块空间在内存中是否连续
- 计算arr1和arr2之间的元素个数时,是按int还是char类型计算
讲了这么多,大家可能感到有些无聊。现在我举个例子来介绍它的作用🤩。
我们此时要计算一个字符串的长度。
很自然的我们就写出了这段代码,strlen统计的是字符串中 \0 之前的字符个数,我们可以利用指针的知识写一个我们自己的strlen函数来完成这个功能🥰。
思路:
- 数组名是数组首元素的地址,arr == &arr[0]
- 我们需要一个字符指针来接收数组首元素的地址
- 我们需要一个计数器在没有指向 \0 来进行不断加1来计算数组的元素个数
代码实现:
#include<stdio.h>
size_t My_strlen(char* p)
{
size_t count = 0;
while (*p != '\0')
{
count++;
p++;
}
return count;
}
int main()
{
char arr[] = "abcdef";
size_t sz = My_strlen(arr);
printf("%zd\n", sz);
return 0;
}
用指针减指针也行,只用起始地址和\0的地址相减就行。
代码实现:
#include<stdio.h>
size_t My_strlen(char* p)
{
char* start = p;
while (*p != '\0')
{
p++;
}
return p - start;
}
int main()
{
char arr[] = "abcdef";
size_t sz = My_strlen(arr);
printf("%zd\n", sz);
return 0;
}
2.3 指针的关系运算
其实就是两个指针比较大小。🤓
此时我们利用指针的关系运算来打印数组内容。
思路:
我们用int * p来存放首元素的地址,在没有到达最后一个数之前不断移动,结合数组的知识,数组随着下标的增长,地址是由低到高变化的。
或者反过来也行。
#include<stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int sz = sizeof(arr) / sizeof(arr[0]);
int* p = &arr[sz-1];
while (p >= &arr[0])
{
printf("%d ", *p);
p--;
}
return 0;
}
3.野指针🤔
野指针指向的位置是不可知的,指向的空间是不属于我自己的。
3.1野指针的成因
- 指针未初始化
- 指针越界访问
- 指针指向的空间释放
3.1.1 指针未初始化
这里的p我们没有初始化,里面存放的地址是随机的,则 * p就是非法访问,p就是野指针。
3.1.2 指针越界访问
这里arr只能存放10个元素,但是循环体要循环12次,指针出现了越界访问,p会变为野指针。
3.1.3 指针指向的空间释放
#include<stdio.h>
int* test()
{
int n = 100;
return &n;
}
int main()
{
int* p = test();
printf("%d\n", *p);
return 0;
}
我们来理解这段代码,首先调用了test这个函数,用int * p来接收返回值。在这个test函数中,创建了变量n,并存放了值100,并返回n的地址。最后打印p解引用以后的内容。
问题分析:😆
局部变量n在进入test这个函数时创建,出函数时被销毁。出函数时,创建的空间不再属于n,还给了操作系统。此时我们把n的地址返回给了p,p得到这个地址时,p就成为了野指针。
3.2 如何避免野指针
- 指针初始化
- 小心指针越界
- 指针变量不再使用时,及时置为NULL,指针在使用之前检查有效性
- 避免返回局部变量的地址
3.2.1 指针初始化
如果知道指针指向哪里,就直接赋值地址。如果不知道指针指向哪里,可以赋值NULL(空指针)。
int main()
{
int a = 4;
int* p1 = &a;//直接赋值
int* p2 = NULL;
return 0;
}
对于NULL的理解:👻
NULL是C语言中定义的一个标识符常量,值是0,同时0也是地址,该地址是无法使用的,读写该地址会报错。
当然我们可以统合一下其他内容进行理解:
3.2.2 小心指针越界
这个我们在前面已经讲过了,这里我们不再重复了😘。
3.2.3指针变量不再使用时,及时置为NULL,指针在使用之前检查有效性
指针变量在指向一个区域的时候,我们可以通过指针对该区域进行访问,如果后期我们不再使用该指针访问空间时,我们可以将指针暂时设置为NULL。
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = &arr[0];
int i = 0;
for (i = 0; i < 10; i++)
{
*(p++) = i;
}
//此时指针越界,把p设置为NULL
p = NULL;
//下次使用之前进行判断,如果p不是NULL时,再使用
//........
p = &arr[0];//p重获地址
if (p != NULL)//判断
{
//......
}
return 0;
}
3.2.4 避免返回局部变量的地址
这个我们在前面也举了例子,这里不再一一赘述😤。
4.assert断言😉
assert.h头文件定义了宏assert(),用于在运行时确保程序符合指定条件,如果不符合,就报错终止运行。这个宏常常被定义为“断言”。
assert(表达式);
如果这个表达式为真,则什么都不发生,如果为假,则报错。
assert在报错的时候会显示文件名和行号,便于检查。
所以assert()的使用对程序员是非常友好的:
它不仅能自动标识文件和出问题的行号,还有一种无需更改代码就能开启或关闭assert()的机制。
如果已经确认程序没有问题,不需要再做断言,就在#include<assert.h>语句的前面,定义一个宏NDEBUG。
#define NDEBUG
#include<assert.h>
int main()
{
int a = 10;
int* p = NULL;
assert(p != NULL);
return 0;
}
assert()的缺点是:引入了额外的检查,增加了程序的运行时间。
注意:
assert我们一般在Debug中使用,在Release版本中选择禁用assert。
assert不是只能断言指针,我们这里只是以指针为例,我们想断言什么都行。
5.指针的使用和传值调用😗
指针的使用我们其实在前文中有展现,就是我们自己的计算字符串的长度的函数My_strlen,学到这里我们可以对刚才写的代码进行优化,添入assert()。
#include<assert.h>
#include<stdio.h>
size_t My_strlen(char* p)
{
size_t count = 0;
assert(p != NULL);
while (*p != '\0')
{
count++;
p++;
}
return count;
}
int main()
{
char arr[] = "abcdef";
size_t sz = My_strlen(arr);
printf("%zd\n", sz);
return 0;
}
我们创建My_strlen是求字符串长度的,我们不希望p指向的字符串被修改!我们现在接着优化🫡。
#include<assert.h>
#include<stdio.h>
size_t My_strlen(const char* p)
{
size_t count = 0;
assert(p != NULL);
while (*p != '\0')
{
count++;
p++;
}
return count;
}
int main()
{
char arr[] = "abcdef";
size_t sz = My_strlen(arr);
printf("%zd\n", sz);
return 0;
}
其实到这里代码已经没有问题了,我们进行简化就行😋。
#include<assert.h>
#include<stdio.h>
size_t My_strlen(const char* p)
{
size_t count = 0;
assert(p != NULL);
while (*p )
{
count++;
p++;
}
return count;
}
int main()
{
char arr[] = "abcdef";
size_t sz = My_strlen(arr);
printf("%zd\n", sz);
return 0;
}
5.2 传值调用和传址调用😶🌫️
这是函数调用的两种方式。
我们现在通过例子来理解:要求写一个函数交换两个整型变量的值。
刚开始时我们的代码可能写成这样:
#include<stdio.h>
void Swap(int x, int y)
{
int tmp = 0;
tmp = x;
x = y;
y = tmp;
}
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\n", a, b);
return 0;
}
但是呢,我们会发现运行后结果并不是我们想要的🫠。
我们进行调试观察:
显然a和x、b和y的地址并不相同,当实参的值传递给形参的时候,形参是有独立的空间的,对形参的修改不会影响实参。此时我们使用的方法就是传值调用。
那有没有什么方法使我们改变的是a、b变量本身呢?🤨
我们可以通过指针远程遥控我们的a和b实现交换。
#include<stdio.h>
void Swap(int* pa, int* pb)
{
int tmp = 0;
tmp = *pa;
*pa = *pb;
*pb = tmp;
}
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\n", a, b);
return 0;
}
此时我们传递的是a和b的地址,并借助tmp完成了交换,这个就是传址调用。
总结:🤓
未来函数中,如果只需要的是主调函数中的变量值来实现计算,就采用传值调用。如果函数内部要修改主调函数中变量的值,就需要传址调用。
此时我们的指针内容还没有完😆,下期blog我会接着讨论有关指针的内容!如果大家感兴趣,请一键三连。如果有问题请各位大佬在评论区斧正,十分感谢🥰!我一定会继续努力的!