深入理解指针(1)

1. 内存和地址

       我们知道计算机在处理数据的时候,需要的数据是在内存中读取的,处理后的数据也会放回内存中, 这其实是把内存划分为⼀个个的内存单元,每个内存单元的⼤⼩取1个字节。以学生宿舍为例,⼀ 个字节空间⾥⾯能放8个⽐特位,就好⽐同学们住 的⼋⼈间,每个⼈是⼀个⽐特位。而有宿舍就有门牌号,也就是地址,有了地址也就能快速找到对应的内存单元了。而在C语言中这类地址被称为了指针。可以理解为内存单元的编号==地址==指针。CPU就是通过地址才能在内存中找到对应的信息的 。

2. 指针变量和地址

        2.1取地址操作符(&)

        而在C语⾔中创建变量其实就是向内存申请空间 

        如图所示,我创建了整型a,它便在内存里占四个字节,而这四个字节又有各自的地址分别为0x006FFD70   0x006FFD71   0x006FFD72   0x006FFD73。

        那么地址又该怎么拿出来呢?只需要一个&操作符便可以。

1 #include <stdio.h>
2 int main()
3 {
4    int a = 10;
5    &a;//取出a的地址 
6    printf("%p\n", &a);
7    return 0;8 
8 }

        以这段代码为例,它就会打印出a所占4个字节中地址较⼩的字节的地址,006FFD70 ,

        注意:打印址用%p

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

        就像 我们会设置变量来存储一些值方便后面使用一样,取出地址后为了方便后续使用,我们也需要将地址存放在一个地方,那便是指针变量。

1 #include <stdio.h>
2 int main()
3 {
4    int a = 10;
5    int * pa = &a;
6    //取出a的地址并存储到指针变量pa中 
7    return 0;
8 }

        上图便是指针变量的创建,pa即为a的指针变量。 这⾥pa左边写的是int*,*是在说明pa是指针变量,⽽前⾯的int是在说明pa指向的a是整型(int),int*才是pa的类型。

同理,要是创建char b='a'的指针变量则为,char* pb=&b 

        即然已经可以保存地址,那么该怎么使用指针变量呢?这⾥必须用到⼀个叫解引⽤(*)的操作符。

 int main()
 {
    int a = 100;
    int* pa = &a;
    printf("%d\n",*pa);
    *pa = 0;
    printf("%d\n",*pa);
    return 0;
 }

 以这段代码为例,*pa的意思就是通过pa中存放的地址,找到指向的空间,此时其实*pa==a

因此第一次打印的结果会是100,这是a变量原先的值。而第二次打印就会变成0,这是因为*pa修改了a变量的值。

        2.3指针变量的⼤⼩

        我们知道32位机器有32根地址总线,每根地址线出来的电信号转换成数字信号后是1或者0,那我们把32根地址线产⽣的2进制序列当做⼀个地址,那么⼀个地址就是32个bit位,需要4个字节才能存储。如果指针变量是⽤来存放地址的,那么指针变的⼤⼩就得是4个字节的空间才可以。

        同理64位机器,假设有64根地址线,⼀个地址就是64个⼆进制位组成的⼆进制序列,存储起来就需要8个字节的空间,指针变量的⼤⼩就是8个字节。 

        而指针变量的⼤⼩取决于地址的⼤⼩。因此在不同环境下他们的大小也不相同

int main() 
{
   printf("%zd\n", sizeof(char *));
   printf("%zd\n", sizeof(short *));
   printf("%zd\n", sizeof(int *));
   printf("%zd\n", sizeof(double *));
   return 0; 
}

x86 

以上分别为x86环境和x64环境下输出的结果。 

32位平台下地址是32个bit位,指针变量⼤⼩是4个字节

•64位平台下地址是64个bit位,指针变量⼤⼩是8个字节

•注意指针变量的⼤⼩和类型是⽆关的,只要指针类型的变量,在相同的平台下,⼤⼩都是相同的。

3. 指针变量类型的意义

        3.1指针的解引用

        即然指针的大小和类型无关,那为什么指针又要有类型区分呢。

 

                                             

        这分别是两次调试的结果 

        调试我们可以看到,代码1会将n的4个字节全部改为0,而代码2只是将n的第⼀个字节改为0。

        结论:指针的类型决定了,对指针解引⽤的时候有多⼤的权限(⼀次能操作⼏个字节)。

⽐如:char*的指针解引⽤就只能访问⼀个字节,⽽int*的指针的解引⽤就能访问四个字节。

        3.2指针+-整数

        我们再来看一段代码

1 #include <stdio.h>
2 int main()
3 {
4    int n = 10;
5    char *pc = (char*)&n;
6    int *pi = &n;
7    
8    printf("%p\n", &n);
9    printf("%p\n", pc);
10   printf("%p\n", pc+1);
11   printf("%p\n", pi);
12   printf("%p\n", pi+1);
13   return  0;
14 }

 

        我们可以发现,char*类型的指针变量+1跳过1个字节,int*类型的指针变量+1跳过了4个字节。这就是指针变量的类型差异带来的变化。指针+1,其实跳过1个指针指向的元素。指针可以+1,那也可以-1。

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

         3.3void*指针

        在指针类型中有⼀种特殊的类型是void *类型的,可以理解为⽆具体类型的指针(或者叫泛型指针),这种类型的指针可以⽤来接受任意类型地址。但是也有局限性,void*类型的指针不能直接进⾏指针的+-整数和解引⽤的运算。那么可能就有人要问了,即然无法进行解引用和+-操作,那它有什么用呢?事实上,void*指针可以作为函数参数,⽤来接收不同类型数据的地址,实现泛型编程的效果,后续如果想要继续解引用和+-操作也只需进行类型强制转换就行。

4. const修饰指针

        4.1const修饰变量

        变量是可以修改的,如果把变量的地址交给⼀个指针变量,通过指针变量的也可以修改这个变量。但是如果我们希望⼀个变量加上⼀些限制,不能被修改,怎么做呢?这就是const的作⽤。

        以这段代码为例,m就是无法修改的,而n则可以修改,

        但是如果我们换个思维,通过地址修改n的值呢?

        这样就可以修改n的值了,但这样显然是无意义的,即然要修改,那么为什么一开始要在n前加const呢?有没有什么方法,可以令n值真正的无法修改呢?

        答案当然是有的。

        4.2const修饰指针变量

        

        ⼀般来讲const修饰指针变量,可以放在*的左边,也可以放在*的右边,意义是不⼀样的。

        我们逐个分析。

        先看const在*号右边,const所固定的是其后面的变量,也就是pi,pi是一个指针,我们无法修改这个指针,也就是无法对其进行+-操作修改地址

        再看const在*左边,const固定的还是后面的变量,也就是int* pi,int* pi这个其实就是n,也就是说我们无法通过解引用操作来修改n的值

        最后看在两边,这就很好理解了,既无法解引用修改n的值,也无法进行+-操作修改地址

5. 指针运算

        指针的基本运算有三种,分别是:

        1:指针+-整数

        2:指针-指针

        3:指针的关系运算

        5.1指针+-整数

        

        以数组为例,我们只需要知道首元素地址和数组类型就可以知道后面元素的值。

如arr[3]=*(arr+3),值的注意的是,除了sizeof(数组名)和&数组名这两个的数组表示的是整个数组的大小和地址以外,所有的数组名都表示首元素地址,arr即为arr[0]的地址。

        5.2指针-指针

 

以这段计算字符长度的代码为例,指针减指针计算的是结果是两个指针之间相隔的元素数量

需要注意的是,两个指针类型需要相同,在内存的空间也应连续

        5.3指针的关系运算

可见指针也是有大小之分,也可以进行大小比较。

6. 野指针

        6.1野指针成因

         1:未进行初始化

        2:指针越界访问

        3:指针释放后未置空

        6.2如何规避野指针

        基本上上面三个错误不犯就行:)

7. assert断⾔

        assert.h头⽂件定义了宏assert(),⽤于在运⾏时确保程序符合指定条件,如果不符合,就报错终⽌运⾏。这个宏常常被称为“断⾔”

上⾯代码在程序运⾏到这⼀⾏语句时,验证变量p是否等于NULL。如果确实不等于NULL,程序继续运⾏,否则就会终⽌运⾏,并且给出报错信息提⽰,显⽰没有通过的表达式,以及包含这个表达式的⽂件名和⾏号,方便我们修改错误。甚至还有⼀种⽆需更改代码就能开启或关闭assert()的机制。如果已经确认程序没有问题,不需要再做断⾔,就在#include 语句的前⾯,定义⼀个宏NDEBUG。

这样编译器就会禁⽤⽂件中所有的assert()语句,减少运行时间,有需要再启动就好了。

8. 指针的使⽤和传址调⽤

        8.1strlen的模拟实现

        这里我就利用指针的相关知识实现strlen函数的模拟实现 

        这为strlen的函数参数,我们模仿它,如果要模拟实现只要从起始地址开始向后逐个字符的遍历,只要不是\0字符,计数器就+1,这样直到\0就停⽌。

如图

        8.2传值调⽤和传址调⽤

         讲了这么多,可能会有人觉得指针没啥用,不如直接修改原变量,拿外卖接下来就看看什么情况下非指针不可。

以这段代码为例,大家觉得a,b两值有进行交换吗?

        答案是没有,这是为什么?我们将a和b传入x和y、x和y进行了交换,为什么a和b没有进行交换?

        x和y确实接收到了a和b的值,不过x和y作为形参,x的地址和a的地址不⼀样,y的地址和b的地址不⼀样,相当于x和y是独⽴的空间,⾃然不会影响a和b,当Swap1函数调⽤结束后回到main函数,a和b没法交换。这种将a和b的值传过去的就叫传值调用

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

        那我们该如何实现交换的功能呢?只需传地址就好了。

        

        这就成功实现了功能,这⾥调⽤Swap2函数的时候是将变量的地址传递给了函数,这种函数调⽤⽅式叫:传址调⽤。

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值