文章目录
提示:以下是本篇文章正文内容,下面案例可供参考
一、void * 指针
在c与语言中,指针的类型有很多:
指针类型 | 指向 | 解引用 |
---|---|---|
char* | 指向字符的指针 | 解引用访问1个字节 |
short* | 指向短整型的指针 | 解引用访问2个字节 |
int* | 指向整型的指针 | 解引用访问4个字节 |
float* | 指向单精度浮点型的指针 | 解引用访问4个字节 |
特例
但是除去上面常见的一些类型,c语言中还有一个特别的指针类型叫做void * 指针类型
。
void*特点1
void*指针也称无具体类型的指针,代表这个指针是没有具体类型的。也就是它可以接收任意类型的指针。在下面图中,我们可以看到,用void*指针可以接收任意类型的指针。
在这里有的读者可以会很疑惑,我知道这个数据是什么类型的,直接用这个类型不就可以了,为啥还要单独弄出来一个void*的指针类型。在这里这里每个类型的数据都是直接给出来的,但是以后我们想要写一个函数,函数接收的数据类型是不可知的,我们总不能把所有数据类型都写在函数的参数中,于是我们用void*指针进行接收所有类型的指针就可以,这样既不会出错误,也不会出现多参数的情况。
void*特点2
void*指针也存在一些小问题,就是void*指针不能进行指针的加减操作和解引用操作
。这是因为void*指针是无具体类型的指针,你要对它进行解引用或者指针的加减,它访问不知道是几个字节。所以不能对void*类型的指针进行这些运算。
二、const关键字
在c语言中还有一个关键字叫const,const英文意思叫常数,所以在c语言中,const的作用就是给予变量常属性,使得被const修饰的变量无法修改。
1.const修饰变量
在这里可以看到const修饰变量,变量具有常属性,此时变量不可以被修改,而没有被const修饰的变量a此时可以被修改。b被const修饰,b此时具有常属性,b不可以被修改,但是b可不是常量,下面我们来验证一下。
定义数组我们只能用常量来定义数组的元素个数,上图显而易见,我们用const修饰变量b,b并不是变成常量了,它只是具有了常属性,不能被修改,所以被const修饰的变量我们也称常变量。 但是在c++中,const修饰的变量就是常量。
2.const修饰指针变量
在了解const修饰指针变量,我们先了解一下为什么要修饰指针变量,我直接修饰变量不就不能改变值了么?听我细细道来:
在这里我们虽然用const修饰变量b,b不能被修改,但是我们可以通过地址去修改它,这就好比法律,总是存在一些漏洞,这就需要进行修改和矫正,于是就需要const修饰指针变量。const修饰指针变量可以放在两个地方,一个是放在*的左边,一个是放在*的右边。
2. 1 const放在*的右边
int main()
{
int b = 10;
int a = 20;
int* const p = &b;
*p = 20;
//p = &a; //err
printf("%d",b);
return 0;
}
在这里运行代码后我们可以发现const放在*右边,修饰的指针变量p,const修饰指针变量p,p存放的是b的地址,此时存放在指变量p中的值(b的地址)不可改变,但是指针变量p所指向的那块空间(*p = b)可以改变。就和上面const修饰普通变量一样,const 修饰整型变量b,b中存放10,const修饰之后,b的值不能改;const修饰指针变量也一样,const修饰指针变量p,p中存放b的地址,此时存在p中的值也是不可以被修改的。总结来说,const放在*的右边,假设指针变量p指向a,此时指针变量指向不能变,也就是指针变量p不能指向其它变量。
2. 2 const放在*的左边
int main()
{
int b = 10;
int a = 20;
//int const* p = &b;
const int* p = &b;//这两种都是放在左边
*p = 20; //err
p = &a;
printf("%d",b);
return 0;
}
在这里运行代码可以看出来,此时const放在*的左边,const修饰的指针变量的指向,此时通过指针变量p指向去修改b中的值是不可以的,但是可以修改指针变量p中存放的地址。
2. 3 总结
const修饰指针变量的时候
-
const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来修改,但是指针变量本身的内容是可以修改的。
-
const如果放在* 的右边,修饰的是指针变量本身,保证了指针变量的内容不能修改,但是指针所指向的内容,是可以通过指针进行修改的。
三、指针的运算
指针的作用就是访问内存的,但是我们要如何去访问内存呢?这时候就必须依赖于我们各种指针的运算。
了解完指针概念之后,我们接下来要熟悉指针在使用过程中是如何运算的,指针的运算主要有三种:指针的加减整数,指针减去指针,指针的关系运算。
3. 1指针的加减整数
要了解指针加减整数的运算,我们得先了解指针+1的效果
- int a = 10;
- int * p = &a;
- p+1; //整型指针+1;—跳过4个字节
在这里为什么跳过四个字节呢,本质上就是跳过1✖sizeof(int),也就是跳过一个整型的大小。在这个基础上我们在引申一下,假设我们是type * 类型的指针(type是类型的意思),本质上就是跳过1✖sizeof(type)字节。如果type是字符型的指针,本质上就是跳过1✖sizoef(char)字节,也就是一个字节;假如是double类型的指针,本质上就是跳过1✖sizeof(double)字节 。
上面我们了解指针+1的效果,引申一下+n的效果
上面我们说了,指针+1就是跳过1✖sizeof(type),那么指针+n呢?
- int a = 10;
- int * p = &a;
- p+n; //整型指针+n;—跳过 n ✖ sizeof(int)个字节
基于上面对于指针加减整数的运算的理解,我们学习数组中指针的应用,指针的加减运算主要用于数组方面,在数组中,内存是连续存放的,知道首元素的地址可以顺藤摸瓜的找到后面的元素。
3.1.1利用指针打印数组所有内容
定义一个整型数组
int arr[5] = { 1,2,3,4,5 };
数组在内存中存放如下
在代码中的运用
int main()
{
int arr[5] = { 1,2,3,4,5 };
//打印数组内的元素
int* p = &arr[0];
int i = 0;
for (i = 0; i < 5; i++)
{
printf("%d ", *(p+i));
}
return 0;
}
分析
在这里我们首先定义了一个数组arr里面存放5个元素,然后我们取出首元素的地址放入p中,此时循环打印,我们通过解引用p可以访问首元素,p+1此时访问的是下一个元素的地址。对应i,当i=0的时候,访问数组中的第一个元素,以此类推,当i=4的时候,访问的是数组的最后一个元素。
注意
在这里*(p+i)不能写成 *p+i,因为 * 操作符的优先级比+的优先级高,所以不能这样写,此时不加括号的话,就成为了先解引的值加1了,而不是指向下一个元素了。
3. 2 指针 - 指针
指针减去指针的绝对值得到的是两个指针之间元素的个数,这里不存在指针+指针的操作,这是无意义的。
指针减去指针计算的前提条件一定是:两个指针指向了同一块空间
通过指针减去指针的方式实现strlen函数
strlen是一个统计字符个数的函数,它的结束条件是碰到\0停止,得到的是\0之前的元素的个数
char str[] = "abcdef"
数组在内存中存放如下
思路
在这里我们想要实现strlen的自我实现,并且运用指针减去指针的原理 ,因为数组传参传进来的就是首元素的地址,我们得先定义一个起始指针变量start用来存放首元素的地址,然后我们让地址进行一步一步加1,地址每次加一访问的都是下一个元素的地址,我们只需要判断该元素是不是\0如果是的话就停止指针加减操作,用结束的指针地址,减去起始指针的地址,就可以得到字符串的字符的个数了。
代码实现
int my_strlen(char* str)
{
char* start = str;
while (*str != '\0')
{
str++;
}
return str - start;
}
int main()
{
char str[] = "abcdef";
int ret = my_strlen(str);
printf("%d", ret);
return 0;
}
分析
在这里定义数组,指针指向首元素的地址放入start中,然后指针+1继续寻找后面的元素,直到找到字符’\0’停止寻找此时,指针指向的下标如下,此时用指针减去指针的方法,得到中间的元素个数。
3. 3 指针的关系运算
指针的关系运算就是指针和指针比较大小,也就是地址与地址比较大小,地址可以看成一个数据,因此是可以比较大小的。
我们学习了有关于内存的知识,在计算机中,计算机将内存划分为一个一个的内存单元,一个内存单元占一个字节,每个内存单元都有它相应的地址。在内存中也存在高地址和低地址。在数组中,数组元素在内存中的存储是由低地址到高地址方向连续存储的。
定义一个数组
定义一个数组,打开调试窗口中的内存窗口,找到数组arr在内存中的存储,这里我们可以发现每个整型数组元素占四个字节,每个字节都有自己的内存地址。
利用指针的关系运算打印出整型数组中的全部元素
#include <stdio.h>
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int *p = &arr[0];
int i = 0;
int sz = sizeof(arr)/sizeof(arr[0]);
while(p<arr+sz) //指针的⼤⼩⽐较
{
printf("%d ", *p);
p++;
}
return 0;
}
分析
在上面代码中,我们首先定义了一个数组arr存放1到10这10个整数,然后我们取出数组首元素的地址,因为数组在内存中是从低地址向高地址进行存放的,如上面的代码调试内存窗口可以看出,1的存放地址是最低的,10的存放地址是最高的。取出完地址放入指针变量p中后,我们让p与数组首元素地址加9进行相比(等价于第十个元素的地址),首元素地址小于最后一个元素的地址进入循环,首先通过指针变量p所指向的那份空间打印首元素,然后指针变量+1,再与最后一个元素的地址进行相比,依次直到指针变量p的地址大于等于最后一个元素的地址时,打印结束。
四、野指针
4. 1 什么叫野指针?
野指针就是指针指向的位置是不可知的(随机的,不正确的,没有明确限制的)。指针是指向一块有效的内存空间的,但是有一些指针他没有明确的指向,指向的位置是不可知的,不正确的,那它就是野指针。
接下来我们用一张图来解释一下什么叫野指针
如果这是一条宠物狗,那它有主人的,那么他就指向了它的主人,他的任何事情都由主人进行处理;假如他没有主人,那么他就是一条野狗,野狗是没有指向的,他会四处乱串,所以他的位置也是不可知的,同时他也是非常危险的,他没有接收训话,人一旦靠近它很可能就会被咬伤。在这里野狗就等同于野指针,野指针指向位置是不可知的,同时也是非常危险的,在指针中出现野指针会导致代码严重的后果。
4. 2 野指针的成因
正常情况下,我们应该知道指针指向哪里,
int a = 10;
int * p = &a;
在这里p是指向a的,它是有明确的指向
*p=20 。//此时解引用是有效的,因为它明确指向了a
4.2.1 指针未初始化
#include <stdio.h>
int main()
{
int *p;//局部变量指针未初始化,默认为随机值
*p = 20;
return 0;
}
分析
在这里指针定义并没有给指针进行初始化,在定义变量中,变量未进行初始化默认的是随机值,指针变量同样如此,指针变量未进行初始化,指针变量存的就是随机地址,此时通过指针变量去访问该地址中的值进行修改值是没有权限的。这个就好比你在某地订了个酒店,你朋友想去找你,但是他不知道哪个是你地址,他就随机找一个地址就想直接闯进去,这肯定不行的。在这里也是同样的道理,指针未初始化,给定的是随机地址,随机地址就是没有没有规定指向的,此时指针变量p就是野指针。
4.2.2 指针越界访问
#include <stdio.h>
int main()
{
int arr[10] = {0};
int *p = &arr[0]; //p此时不是野指针
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
for(i=0; i<=sz; i++)
{
//当指针指向的范围超出数组arr的范围时,p就是野指针
printf("%d ",*p);
p++;
}
return 0;
}
分析
在这里,我们定义了10个数组,通过指针访问却越界访问到了第十一个地址空间,此时当指针指向的范围超出数组的范围的时候,指针变量p就是野指针。野指针的指向是随机的,通过它访问的值是随机的,他可能是任何一个值。
4.2.3 指针所指向的那块空间释放
#include <stdio.h>
int* test()
{
int n = 100;
return &n;
}
int main()
{
int*p = test();
printf("%d\n", *p);
return 0;
}
分析
上述代码我们调用test函数,此时进入test函数内创建n变量,并给他一份空间,test函数返回n的地址用指针变量p进行接收,test函数调用完后,n变量所开辟的空间就还给系统空间了,但是我们已经把n那块地址放入p中,p还是指向那块空间,此时我们在主函数通过指针变量p去访问那块空间是没有访问权限的,所以此时代码是存在错误的。而指针变量p就是野指针。
4. 3 如何避免野指针
4.3.1指针初始化
如果明确知道指针指向哪里就直接赋值地址
int main()
{
int a = 10;
int* p = &a;
*p = 20;
return 0;
}
如果不知道指针应该指向哪里,可以给指针赋值NULL.
int main()
{
int* p = NULL ;
return 0;
}
NULL是c语言中定义的一个标识符常量,值是0,0也是地址,这个地址是无法使用的,读写该地址会报错 ,这是因为在巨大的内存系统中,有一部分内容是存放在系统内核中的,系统内核中的只能系统使用,用户是无法使用的,而NULL则是存放在系统内核中的。
这里给他初始化NULL,又不能使用为啥要用NULL初始化呢?
在这里虽然这种写法也不能直接去访问p,但是一旦p被初始化为空指针,就被标示起来了,表示p是一个空指针,你要注意了。就拿前面野狗的例子进行举例,在这里这个狗明明是野狗,你还放任不管是不是更加危险,假如我给它套上一个绳子栓到树上,虽然他还是一条野狗,那么下次你走过这里绕过树就不会被咬到,这样就相对安全。这里p我害怕他是一个野指针,所以我用NULL把他拴起来了,这样就安全一些,这样你看到这个野指针就知道它是一个野指针。这里你看到他是一个野指针,你还要去碰它(*p),那他不就和那个野狗一样去咬你,这是非常危险的。
4.3.2 小心指针越界
⼀个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是越界访问。
正确写法
#include <stdio.h>
int main()
{
int arr[10] = {0};
int *p = &arr[0];
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
for(i=0; i < sz; i++)
{
printf("%d ",*p);
p++;
}
return 0;
}
4.3.2 指针变量不再使用时,及时置NULL,指针使用之前检查有效性
当指针变量指向⼀块区域的时候,我们可以通过指针访问该区域,后期不再使⽤这个指针访问空间的时候,我们可以把该指针置为NULL。因为约定俗成的⼀个规则就是:
只要是NULL指针就不去访问,同时使⽤指针之前可以判断指针是否为NULL。
我们可以把野指针想象成野狗,野狗放任不管是⾮常危险的,所以我们可以找⼀棵树把野狗拴起来,就相对安全了,给指针变量及时赋值为NULL,其实就类似把野狗栓起来,就是把野指针暂时管理起来。
不过野狗即使拴起来我们也要绕着⾛,不能去挑逗野狗,有点危险;对于指针也是,在使⽤之前,我们也要判断是否为NULL,看看是不是被拴起来起来的野狗,如果是不能直接使⽤,如果不是我们再去使⽤。
int main()
{
int* p = NULL;
if (p != NULL)
{
}
return 0;
}