深入了解指针(一)

前言

指针可是数据结构的基础,其内容及重要又繁多,之前没有时间整理出来,现在放假了怎么说也要写一个,既是对前面的总结和梳理,也可以用来以后的查找知识,ok,废话不多,直接启动。

指针是什么

指针是C语言中的一种特殊数据类型,它存储了一个变量的内存地址。通过指针,我们可以直接访问和修改该内存地址上存储的值。指针变量本身也有自己的存储地址,而指向指针变量的存储地址就被称为指针的指针,即二级指针。多级指针的定义就是定义时使用多个"*"号。

1内存和地址

1.1内存

我们知道你的代码中的变量常量都是放在内存中的,我们电脑大多是4g或8g内存,这些内存空间被平均分了许多份,每份大小是1个字节

下面是常见的内存大小单位以及换算

我们可以把整个内存当成一个酒店,每个空间当作一个房间,那么每个房间的门牌号就是地址,我把地址告诉你了,你自然就能找到那个房间了。而在C语言中地址就是指针。

1.2地址的写法

我们知道你电脑cpu和内存要互相协同⼯作的。所谓的协同,⾄少相互之间要能够进⾏数据传递。那么如何通信呢?答案很简单,⽤"线"连起来,我们今天关⼼⼀组线,叫做地址总线。

我们可以简单理解,32位机器有32根地址总线,每根线只有两态,表⽰0,1【电脉冲有⽆】,那么
⼀根线,就能表⽰2种含义,2根线就能表⽰4种含义,依次类推。32根地址线,就能表⽰2^32种含
义,每⼀种含义都代表⼀个地址。如此一来每个字节大小的空间就被编好地址了。
地址信息被下达给内存,在内存上,就可以找到该地址对应的数据,将数据在通过数据总线传⼊
CPU内。
当然了,为了书方便我们写地址是以16进制写的,对32位机器如果某个空间地址是
10000000 10000000 10000000 10000000
换算成16进制就是0XF0 F0 F0 F0       前面的0X表示这个数是16进制,且字母大写
取地址操作符(&)
理解了内存和地址的关系,我们再回到C语⾔,在C语⾔中创建变量其实就是向内存申请空间,⽐如:
上述的代码就是创建了整型变量a,内存中申请4个字节,⽤于存放整数10,其中每个字节都
很明显他们里面存放的值分别是0a    00  00  00,用小端换算成10进制就是10
那我们如何能得到a的地址呢? 用到操作符&
当然了这里的地址是4个地址中最小的那个,你可以理解为从这个地址开始向后4个字节都是a的空间,于是乎我们顺藤摸瓜就能找到另外三个了
那我们通过取地址操作符(&)拿到的地址是⼀个数值,⽐如:0x006FFD70,这个数值有时候也是需要

2指针变量和地址

2.1取地址操作符&和指针变量

将指针存储起来,可以⽅便后期再使⽤的,那我们把这样的地址值存放在哪⾥呢?答案是:指针变量中,用什么方法呢?答案是取地址操作符
#include<stdio.h>
int main()
{
    int a = 10;
    int* p = &a;
    return 0;
}
指针变量也是⼀种变量, 这种变量就是⽤来存放地址的,存放在指针变量中的值都会理解为地址。
指针变量的类型:对应数据类型加上一个*
int对应int*
char对应char*
double对应double*
以此类推
可以理解为*是在说明他是个指针,而前面的int则说明它指向的地址里的数据是int类型,这个地址的值就被存到这个指针变量里了

2.2解引用操作符*

我们将地址保存起来,未来是要使⽤的,那怎么使⽤呢?
这里就要用到解引用操作符*了
和明显我们通过*p找到了a并且打印了出来,
这里我们通过指针p修改了a的值
可能有同学会觉得麻烦,感觉不如直接打印a或者修改a, 但以后函数的传值与传址就会明白了

2.3 指针变量的⼤⼩

前⾯的内容我们了解到,32位机器假设有32根地址总线,每根地址线出来的电信号转换成数字信号后
是1或者0,那我们把32根地址线产⽣的2进制序列当做⼀个地址,那么⼀个地址就是32个bit位,需要4
个字节才能存储。
如果指针变量是⽤来存放地址的,那么指针变的⼤⼩就得是4个字节的空间才可以。
同理64位机器,假设有64根地址线,⼀个地址就是64个⼆进制位组成的⼆进制序列,存储起来就需要
8个字节的空间,指针变的⼤⼩就是8个字节
x64环境下的
x86环境下
结论:
32位平台下地址是32个bit位,指针变量⼤⼩是4个字节
64位平台下地址是64个bit位,指针变量⼤⼩是8个字节
注意指针变量的⼤⼩和类型是⽆关的,只要指针类型的变量,在相同的平台下,⼤⼩都是相同的。

3. 指针变量类型的意义

3.1 指针的解引⽤

x会将a的4个字节全部改为0,y只是将b的第⼀个字节改为0。

结论:指针的类型决定了,对指针解引⽤的时候有多⼤的权限(⼀次能操作⼏个字节)。
⽐如: char* 的指针解引⽤就只能访问⼀个字节,⽽ int* 的指针的解引⽤就能访问四个字节。

3.2void* 指针

在指针类型中有⼀种特殊的类型是 void* 类型的,可以理解为⽆具体类型的指针(或者叫泛型指
针),这种类型的指针可以⽤来接受任意类型地址。但是也有局限性, void* 类型的指针不能直接进
⾏指针的+-整数和解引⽤的运算。
int main()
{
    int a = 10;
    void* p = &a;
    *p = 0;
    p++;
    return 0;
}
⼀般 void* 类型的指针是使⽤在函数参数的部分,⽤来接收不同类型数据的地址,这样的设计可以
实现泛型编程的效果。使得⼀个函数来处理多种类型的数据,

4. const修饰指针

4.1 const修饰指针变量

int main()
{
    int a = 10;
    const int* p = &a;
    *p = 5;
    return 0;
}
这时无法通过*p修改a的值
这时不能改变p的值
但是你可以通过二级指针的方法绕过去,可事实上不该这样做,因为他既然被const修饰了就说明写他的人不希望它被改变,
int main()
{
    int a = 10;
    int b = 5;
     int* const p = &a;
     int** b = &p;
     int*x = &b;
     b = &x;
    return 0;
}
结论:const修饰指针变量的时候
const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。
但是指针变量本⾝的内容可变。
const如果放在*的右边,修饰的是指针变量本⾝,保证了指针变量的内容不能修改,但是指针指
向的内容,可以通过指针改变

5. 指针运算

指针的基本运算有三种,分别是:
指针+- 整数
指针-指针
指针的关系运算

5.1 指针+- 整数

因为数组在内存中是连续存放的,只要知道第⼀个元素的地址,顺藤摸⽠就能找到后⾯的所有元素。

5.2 指针-指针

我们通过这个实现了strlen函数
//指针的关系运算
#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;
}

5.3 指针的关系运算

//指针的关系运算
#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;
}

6. 野指针

概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)

6.1 野指针成因

1. 指针未初始化

# include <stdio.h>
int main ()
{
int *p; // 局部变量指针未初始化,默认为随机值
*p = 20 ;
return 0 ;
}

2. 指针越界访问

 

# include <stdio.h>
int main ()
{
int arr[ 10 ] = { 0 };
int *p = &arr[ 0 ];
int i = 0 ;
for (i= 0 ; i<= 11 ; i++)
{
// 当指针指向的范围超出数组 arr 的范围时, p 就是野指针
*(p++) = i;
}
return 0 ;
}

3. 指针指向的空间释放

#include <stdio.h>
int* test()
{
 int n = 100;
 return &n;
}
int main()
{
 int*p = test();
 printf("%d\n", *p);
 return 0;
}

 6.2 如何规避野指针

6.2.1 指针初始化

如果明确知道指针指向哪⾥就直接赋值地址,如果不知道指针应该指向哪⾥,可以给指针赋值NULL.
NULL 是C语⾔中定义的⼀个标识符常量,值是0,0也是地址,这个地址是⽆法使⽤的,读写该地址
会报错。

6.2.2 ⼩⼼指针越界

6.2.3 指针变量不再使⽤时,及时置NULL,指针使⽤之前检查有效性

当指针变量指向⼀块区域的时候,我们可以通过指针访问该区域,后期不再使⽤这个指针访问空间的
时候,我们可以把该指针置为NULL。因为约定俗成的⼀个规则就是:只要是NULL指针就不去访问,
同时使⽤指针之前可以判断指针是否为NULL

6.2.4 避免返回局部变量的地址

如造成野指针的第3个例⼦,不要返回局部变量的地址。

7. assert断⾔

assert.h 头⽂件定义了宏 assert() ,⽤于在运⾏时确保程序符合指定条件,如果不符合,就报
错终⽌运⾏。这个宏常常被称为“断⾔”。
assert(p != NULL );
上⾯代码在程序运⾏到这⼀⾏语句时,验证变量 p 是否等于 NULL 。如果确实不等于 NULL ,程序
继续运⾏,否则就会终⽌运⾏,并且给出报错信息提⽰。
这个可以很好的处理野指针的问题
最后留个思考问题,函数传值与传址的区别,这就是我们为什么要学指针的一个原因。

总结

ok,今天的内容包括以下这些

1 .内存和地址
2. 指针变量和地址
3. 指针变量类型的意义
4. const修饰指针
5. 指针运算
6. 野指针
7. assert断⾔
也是相当多了,但关于指针我可能要写4篇博客吧,因为它真的很重要而且有难度,今天的算是把基础讲的差不多了,那么如果你觉得对你有帮助的话就点个免费的赞支持一下吧,谢谢!

 

  • 33
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值