指针初级篇

目录

指针是什么

 为什么要有指针

 指针的内存布局

 指针变量和解引用操作符(*)

指针+-整数

指针-指针

野指针

如何规避野指针

指针的使用和传址调用


指针是什么

在回答这个问题之前,我想先问几个问题?

1.如何看待下面代码中的a变量?

//demo1
#include<stdio.h>
int main()
{
   int a = 0;
   a = 10;      //1)
   int b = a;   //2)
   return 0;
}
结论:同样一个a变量,在不同的应用场景中,a本身的含义是不同的。
重新理解变量。
//定义一个变量,本质是在内存中根据类型来进行开辟空间。
//有了空间,就必须具有地址来标识空间,来方便CPU进行寻=址。有了空间,就可以把数据保存起来。
//所以,目前我们先讨论变量的空间和内容这两个概念

什么是指针?

指针就是地址!那么地址本质是什么呢?地址是数据,那么数据可不可以被保存在变量空间里面呢?当然可以

有没有指针变量这个概念? 

保存指针(地址)数据的变量就叫做指针变量

指针 和 指针变量又有何不同?我们口语中的"定义一个指针"究竟是什么意思?我们该如何理解这种说法? 

严格意义上,指针和指针变量是不同的,指针就是地址值,而指针变量是C中的变量,要在特定区域开辟空间,要用来保存 地址数据,还可以被取地址。(先分开)

但是,我们经常在口语化表达的时候,又经常将这两个概念混合,具体原因无从考证,不过个人认为与最早的C资料(书, 文档之类)的翻译有关。然后,书与书之间互相借鉴,形成了这样的说法。同时,简化说法,也更符合人的表达习惯,估计老外也是这么想的。(在关联)

那么我们以后怎么认为呢?我们分开理解,但是依旧关联使用。自己使用的时候,混合使用可以。和别人讨论,最好明确概念。

//demo2
#include<stdio.h>
int main()
{
    int a = 10;
    int *p = &a;
    p = 10;      //什么意思?
    int *q = p;  //什么意思?
    *p = 10;     //什么意思?
    int b = *p;  //什么意思?
    return 0;
}

 结论:指针就是地址,指针变量是一个变量,变量内部保存指针(地址)数据。

 为什么要有指针

 先举一个例子

假设一个学校里只有一栋宿舍,这栋宿舍里每个房间都没有门牌号,住在这栋楼里面的张三和李四两个从来不认识的人在食堂吃饭,这时,张三看到对面的李四在玩一个刚好他也在玩的游戏,便主动说对李四说,同学,等下咱俩一起去宿舍玩呗,李四倒也爽快,便痛快答应了,由于张三吃得快,很快就吃完了,便对李四说,要不你先吃着,我先回宿舍,等下你来找我,李四想也没想便答应了,等李四吃完想去找张三的时候,发现不知道张三住哪,便开始从第一层楼一个一个的敲门找张三,因为不能在宿舍里大声喧哗,所有李四也不敢大声的叫唤,只能硬着头皮一层一层的找,这次,终于在六楼的最边边找到了张三的宿舍,这时两人终于一起愉快的玩耍了起来。

例子讲完了,如果现实生活中真的如这个例子所说的一样,那朋友来你家玩的时候就会特别麻烦,要一层一层的去找你,这样效率很低,但是如果我们根据楼层和楼层的房间的情况,能给每个房子编上编号,比如:

一楼:101,102,103 .....

二楼:201,202,203 .....

三楼:.......

有了房间号,如果你的朋友得到房间号,就可以快速的找房间,找到你。 

生活中,每个房间有了房间号,就能提高效率,能快速的找到房间。 

这样具有指向性的数字,我们称之为指针!

类比到计算机中 

我们知道计算上CPU(中央处理器)在处理数据的时候,需要的数据是在内存中读取的,处理后的数 据也会放回内存中,那我们买电脑的时候,电脑上内存是8GB/16GB/32GB等,那这些内存空间如何⾼ 效的管理呢? 其实也是把内存划分为⼀个个的内存单元,每个内存单元的⼤⼩取1个字节。

CPU在内存中寻址的基本单位是多大?

在32位机器下,最多能够识别多大的物理内存?

既然CPU寻址按照字节寻址,但是内存又很大,所以,内存可以看做众多字节的集合

其中,每个内存字节空间,相当于一个学生宿舍,字节空间里面能放8个比特位,就好比同学们住的八人间,每个人是一个比特位。

每间宿舍都有门牌号就等价于每个字节空间对应的地址,即该空间对应的指针。

那么,为何要存在指针呢?

为了CPU寻址的效率。如果没有,该怎么找在字节空间中的数据呢?

究竟该如何理解编址 

 首先,必须理解,计算机内是有很多的硬件单元,而硬件单元是要互相协同工作的。所谓的协同,至少相互之间要能够进行 数据传递。

但是硬件与硬件之间是互相独立的,那么如何通信呢?答案很简单,用"线"连起来。

而CPU和内存之间也是有大量的数据交互的,所以,两者必须也用线连起来。

不过,我们今天关心一组线,叫做地址总线。

CPU访问内存中的某个字节空间,必须知道这个字节空间在内存的什么位置,而因为内存中字节很多,所以需要给内存进行编 址(就如同宿舍很多,需要给宿舍编号一样)

计算机中的编址,并不是把每个字节的地址记录下来,而是通过硬件设计完成的。

钢琴 吉他 上面没有写上“都瑞咪发嗦啦”这样的信息,但演奏者照样能够准确找到每一个琴弦的每一个位置,这是为何?因为制造商已经在乐器硬件层面上设计好了,并且所有的演奏者都知道。本质是一种约定出来的共识!

硬件编址也是如此

我们可以简单理解,32位机器有32根地址总线,每根线只有两态,表示0,1【电脉冲有无】,那么一根线,就能表示2中含 义,2根线就能表示4中含义,依次类推。32根地址线,就能表示2^32中含义,每一种含义都代表一个地址。

地址信息被下达给内存,在内存内部,就可以找到改地址对应的数据,将数据在通过数据总线传入CPU内寄存器

 

 指针的内存布局

//明确一点,该如何画出正确的指针指向图呢?
#include<stdio.h>
int main()
{
   int a = 10;
   int *p = &a;
   return 0;
}
//1. 这里定义了几个变量?在哪里定义的?
//2. 一个整形,有4个字节,那么应该有4个地址!
//   那么&a取了哪一个地址?那么如何全部访问这4个字节呢?
//3. 如何正确的画出指针指向图?

问题1.首先,这里定义了两个变量,一个a变量,一个p变量,既然是变量就要向内存申请空间,向这样的局部变量都是在栈上开辟的,后面我会专门讲在堆上开辟的空间,也就是所谓的动态内存分配,

问题2.

由这两个图可以看出,a的地址取出来的是a所占4个字节中地址较⼩的字节的地址。

虽然整型变量占⽤4个字节,我们只要知道了第⼀个字节地址,顺藤摸⽠访问到4个字节的数据也是可⾏的。

问题3.

至此,我们就解决了上述代码中所提到的三个问题 。

 指针变量和解引用操作符(*)

 我们通过取地址操作符(&)拿到的地址是⼀个数值,⽐如:0x006FFD70,这个数值有时候也是需要 存储起来,⽅便后期再使⽤的,那我们把这样的地址值存放在哪⾥呢?答案是:指针变量中。

#include <stdio.h>
int main()
{
   int a = 10;
   int* pa = &a;//取出a的地址并存储到指针变量pa中

   return 0;
}

 指针变量也是⼀种变量,这种变量就是⽤来存放地址的,存放在指针变量中的值都会理解为地址。

#include<stdio.h>
int main()
{
    int a = 10;
    int *p = &a;
    int b = *p;
    *p = 20;
    return 0;
}
//*p完整理解是,取出p中的地址,访问该地址指向的内存单元(空间或者内容)
           (其实通过指针变量访问,本质是一种间接寻址的方式)
//口诀:对指针解引用,就是指针指向的目标。所以*p,就是a

如何将数值存储到指定的内存地址

知道了指针的本质就是地址,地址就是数据,那么我们可以直接通过地址数据对变量进行访问吗?

//demo
#include<stdio.h>
int main()
{
    int a = 10;   //假设a变量的地址是0x12345678,那么访问a变量,还可以直接通过指针方式进行访问
    printf("%d\n", *(int*)0x12345678); //本质是一种直接寻址的方式
    *(int*)0x12345678 = 100;  //本质是一种直接寻址的方式
    //所以,C语言通过 int*p = &a;   这种指针变量的方式,访问目标数据有什么好处呢?
/*原因:大部分技术书,一定是落后于行业的。这本书也是,目前主流的编译器和操作系统,为了安全,已经有了很多内存
保护的机制。我们目前的win和Linux都有栈随机化这样的机制来方式黑客对用户数据地址进行预测。当然,还有其他的栈保
护机制,比如“金丝雀”技术之类的。这部分大家后面可以再了解一下,如果有机会我在给大家说。
经过试验,目前vs2013和Centos7上,使用C语言定义的局部变量,在每次运行的时候,地址都是不同的。
经过试验发现,定义全局变量,每次更改代码,地址也会发生变化。
所以这个实验没法正确做出来,但是程序崩溃,也能说明问题。*/
//所以int*p = &a这种方式,不关心a变量的地址值是多少,只需要指向a就行了,这也就让所谓的”金丝雀“技术存在的可能
    return 0;
}

指针+-整数

先看⼀段代码,调试观察地址的变化。

#include <stdio.h>
int main()
{
   int n = 10;
   char *pc = (char*)&n;
   int *pi = &n;

   printf("%p\n", &n);
   printf("%p\n", pc);
   printf("%p\n", pc+1);
   printf("%p\n", pi);
   printf("%p\n", pi+1);
   return 0;
}

我们可以看出, char* 类型的指针变量+1跳过1个字节, int* 类型的指针变量+1跳过了4个字节。 这就是指针变量的类型差异带来的变化.

结论:指针的类型决定了指针向前或者向后⾛⼀步有多⼤(距离)。

void* 指针 

在指针类型中有⼀种特殊的类型是 void* 类型的,可以理解为⽆具体类型的指针(或者叫泛型指 针),这种类型的指针可以⽤来接受任意类型地址。但是也有局限性, void* 类型的指针不能直接进 ⾏指针的+-整数和解引⽤的运算。

#include <stdio.h>
int main()
{
   int a = 10;
   int* pa = &a;
   char* pc = &a;
   return 0;
}

在上⾯的代码中,将⼀个int类型的变量的地址赋值给⼀个char*类型的指针变量。编译器给出了⼀个警 告(如下图),是因为类型不兼容。⽽使⽤void*类型就不会有这样的问题。

使⽤void*类型的指针接收地址: 

#include <stdio.h>
int main()
{
   int a = 10;
   void* pa = &a;
   void* pc = &a;

   *pa = 10;
   *pc = 0;
   return 0;
}

这⾥我们可以看到, void* 类型的指针可以接收不同类型的地址,但是⽆法直接进⾏指针运算。 

那么 void* 类型的指针到底有什么⽤呢? 

⼀般 void* 类型的指针是使⽤在函数参数的部分,⽤来接收不同类型数据的地址,这样的设计可以 实现泛型编程的效果。使得⼀个函数来处理多种类型的数据.

指针-指针

#include <stdio.h>
int my_strlen(char *s)
{
   char *p = s;
   while(*p != '\0' )
   p++;
   return p-s;
}
int main()
{
   printf("%d\n", my_strlen("abc"));
   return 0;
}

这段代码通过p指针和s指针相减得到了整个字符串的长度,所以可以得出,两指针相减,最终可得到,这两个指针中间所含元素个数。

野指针

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

野指针成因

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;
}

如何规避野指针

1.指针初始化

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

初始化如下:

#include <stdio.h>
int main()
{
    int num = 10;
    int*p1 = &num;
    int*p2 = NULL;

    return 0;
}

2. ⼩⼼指针越界

⼀个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是 越界访问。

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

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

我们可以把野指针想象成野狗,野狗放任不管是⾮常危险的,所以我们可以找⼀棵树把野狗拴起来, 就相对安全了,给指针变量及时赋值为NULL,其实就类似把野狗栓前来,就是把野指针暂时管理起来。

不过野狗即使拴起来我们也要绕着⾛,不能去挑逗野狗,有点危险;对于指针也是,在使⽤之前,我 们也要判断是否为NULL,看看是不是被拴起来起来的野狗,如果是不能直接使⽤,如果不是我们再去使⽤。

int main()
{
    int arr[10] = {1,2,3,4,5,67,7,8,9,10};
    int *p = &arr[0];
    for(i=0; i<10; i++)
    {
        *(p++) = i;
    }  
    //此时p已经越界了,可以把p置为NULL
    p = NULL;
    //下次使⽤的时候,判断p不为NULL的时候再使⽤
    //...
    p = &arr[0];//重新让p获得地址
    if(p != NULL) //判断
    {
        //...
    }
    return 0;
}

4.避免返回局部变量的地址

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

指针的使用和传址调用

学习指针的⽬的是使⽤指针解决问题,那什么问题,⾮指针不可呢?

例如:写⼀个函数,交换两个整型变量的值

⼀番思考后,我们可能写出这样的代码:

#include <stdio.h>
void Swap1(int x, int y)
{
   int 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);
   Swap1(a, b);
   printf("交换后:a=%d b=%d\n", a, b);
   return 0;
}

我们发现其实没产⽣交换的效果,这是为什么呢?

调试⼀下,试试呢?

 我们发现在main函数内部,创建了a和b,a的地址是0x00cffdd0,b的地址是0x00cffdc4,在调⽤ Swap1函数时,将a和b传递给了Swap1函数,在Swap1函数内部创建了形参x和y接收a和b的值,但是x的地址是0x00cffcec,y的地址是0x00cffcf0,x和y确实接收到了a和b的值,不过x的地址和a的地址不 ⼀样,y的地址和b的地址不⼀样,相当于x和y是独⽴的空间,那么在Swap1函数内部交换x和y的值, 自然不会影响a和b,当Swap1函数调⽤结束后回到main函数,a和b的没法交换。Swap1函数在使⽤的时候,是把变量本⾝直接传递给了函数,这种调⽤函数的⽅式我们之前在函数的时候就知道了,这 种叫传值调⽤。

结论:实参传递给形参的时候,形参会单独创建⼀份临时空间来接收实参,对形参的修改不影响实 参。

所以Swap是失败的了。

那怎么办呢?

我们现在要解决的就是当调⽤Swap函数的时候,Swap函数内部操作的就是main函数中的a和b,直接 将a和b的值交换了。那么就可以使⽤指针了,在main函数中将a和b的地址传递给Swap函数,Swap 函数⾥边通过地址间接的操作main函数中的a和b,并达到交换的效果就好了。

#include <stdio.h>

void Swap2(int*px, int*py)
{
   int tmp = 0;
   tmp = *px;
   *px = *py;
   *py = tmp;
}
int main()
{
   int a = 0;
   int b = 0;
   scanf("%d %d", &a, &b);
   printf("交换前:a=%d b=%d\n", a, b);
   Swap1(&a, &b);
   printf("交换后:a=%d b=%d\n", a, b);
   return 0;
}

 

我们可以看到实现成Swap2的⽅式,顺利完成了任务,这⾥调⽤Swap2函数的时候是将变量的地址传 递给了函数,这种函数调⽤⽅式叫:传址调用。 

传址调⽤,可以让函数和主调函数之间建⽴真正的联系,在函数内部可以修改主调函数中的变量;所 以未来函数中只是需要主调函数中的变量值来实现计算,就可以采⽤传值调⽤。如果函数内部要修改 主调函数中的变量的值,就需要传址调用。

数组及指针进阶篇-CSDN博客

  • 23
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值