带你深度学习指针(1)

前言:这篇文章将全方面带你了解指针的作用。

1.内存和地址

1.1内存

 计算机cpu在处理数据时,在内存读取数据,处理后的数据再放回内存。
把内存划分为一个个内存单元,每个内存单元大小为一个字节。
补充:一个字节有8个比特位,比特位是存储二进制的0或1。

每一个内存单元都有一个编号,有了这个编号cpu可以快速找到一个内存空间。
c语言中将编号称作指针,因此我们可以理解为: 内存单元编号==地址==指针

1.2如何理解编址

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

但是硬件间相互独立,是如何实现联系的呢?
答案很简单,用“线”连接起来。

而cpu和内存之间有大量数据交互,所以,两者也需要用线连接。

cpu在内存中访问空间,必须知道空间的位置,因此对字节空间进行编址。

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

我们可以简单理解,32位的机器中,cpu和内存间存在32根地址总线,每根线有0和1两种状态,因此32根就可以表示2^32种地址。

例如:地址信息通过地址总线,可以在内存中找到地址对应的数据,再通过地址总线将数据传入cpu内寄存器。

2.指针变量和地址

2.1取地址操作符(&)

理解地址和内存关系,我们再回到c语言中,c语言中创建变量就是在向内存申请空间,比如:

其中&为取地址操作符,我们发现变量a有四个字节,其中内存中44的地址和变量a的地址相同,那是因为变量的地址取自最小的字节编号。

因此我们只要知道第一个字节地址,就能访问4个字节的数据。

2.2解引用操作符(*)
#include<stdio.h>
int main()
{

int a=10;
int*pa=a;

return 0;
}

在这个代码中pa的类型是int*,其中*说明pa是指针变量,而int说明pa指向的对象是整形(int)。 

#include<stdio.h>
int main()
{

int a=10;
int*pa=a;

*pa=20;

printf("a=%d",a);
return 0;
}

在这个代码中*pa,pa指向的是a的地址,而*pa表示解引用a的地址,*pa等价于a。

a=20
2.3指针变量的大小

指针变量是专门存放地址的。而指针变量的大小取决于机器,在32位的平台上,指针大小为32比特位,也就是4字节,同理64位平台上指针大小是8字节。

注意指针变量大小与类型无关,只与平台有关。

3.指针变量类型的意义

指针变量大小和类型无关,那么我们为什么还要有各种各样的指针类型呢?

3.1指针的解引用

根据代码我们发现pa类型为int*,对*pa赋值为0,int a的每个字节都变成00; 

当pa类型为char*时,对*pa进行赋值,只改变了int a的第一个字节内容。

结论 :指针类型决定了,对指针解引用时有多大的访问权限。

比如:char*类型解引用时每次只能访问一个字节,而int*可以访问4个字节。

3.2指针+-整数
#include<stdio.h>
int main()
{
    int arr[4] = {0};
    int * pa =&arr[0];
    char* pb = &arr[0];
    printf("pa=%p\n", pa);
    printf("pb=%p\n", pb);
    for (int i = 0; i < 4; i++)
    {
        pa=pa+1;
        pb=pb+1;
    }
    printf("pa=%p\n", pa);
    printf("pb=%p\n", pb);
  	return 0;
}

由图中代码运算发现,int*pa和char*pb虽然类型不同,但是他们都是arr[0]的地址。对两个指针变量分别进行4此加1操作,pa数值变大了16,而pb只变大了4。

 

这是由于他们的指针指向的类型不同,指针指向int类型,每加1,指针所访问的内存地址加4;而指针指向char类型,每加1,指针所访问的内存地址加1。

结论:指针类型决定加一或减一,指针向后或向前走一步有多大(距离)。 

3.3void*指针

在指针中有一种特殊的类型是void*类型的,可以理解为无具体类型的指针,void*可以接受任意类型的地址。但是也有局限性,void*类型的指针不能直接进行指针的+-整数和解引用的运算。

#include<stdio.h>
int main()
{
   int a;
   char b;
   
   char*p=&a;//这种操作会使得编译器报警告。

   void*p1=&a;
   p1=&b;//void*类型可以接受任意类型的地址。

   p1++;
   *p1=10;//这两种操作都是错误的,void*类型指针无法进行解引用和加减整数操作。

}

4.const修饰指针

当const修饰int类型时,我们就无法修改该整形。

 在指针中同样可以用const修饰,但const有两种不同的用法,将const分别放到“*”前后,作用就不同。

这种情况,如果int* p=&a;虽然无法直接对a修改,都是可以通过*p对a的内存进行修改。

4.1当const放到“*”前

const放到“*”前,有两种写法,如:int a,b;const int* p=&a和int const *p=&a,其实这两种写法的作用是相同的,都是限制的是*p,这样就无法修改*p,但可以修改指针p所指向的对象,如p=&b。

#include <stdio.h>
int main()
{
   int a=5,b;
   
   const int *p1=&a;
   int const *p2=&b;

   *p1=1;
   *p2=1;
  
   
}

当对p1指向对象进行修改:

#include <stdio.h>
int main()
{
   int a=5,b=4;
   
   const int *p1=&a;
   printf("%d\n",*p1);
   
   p1=&b;
   printf("%d",*p1);
  
   
}

 结论:当const修饰指针时,const在“*”前面,则*p不能修改;但是p可以修改。

补充:1.p里面存放的是地址(a的地址)。2.p是变量,有自己的地址。3.*p是p指向的空间。

4.2当const放到“*”后

const放到“*”后,如int* const p,这样写限制的是指针p,使指针p指向的对象无法修改;但是*p的修改不受限制。

#include <stdio.h>
int main()
{
   int a=5,b=4;
   
   int* const p1=&a;
   printf("%d\n",*p1);
   
   p1=&b;
   printf("%d",*p1);
  
   
}

这样编译器会报错:

 但可以对*p进行修改:

#include <stdio.h>
int main()
{
   int a=5,b=4;
   
   int* const p=&a;
   printf("%d\n",*p);
   
   *p=b;
   printf("%d",*p);
  
   
}

总结:当const在“*”后,可以修改*p,不能修改指针p的指向。 

5.指针的运算

5.1指针+-整数

因为数组的地址是连续的,所以只需要知道首元素地址,就能访问其他的所有元素。

#include <stdio.h>
int main()
{
    int arr[5]={3,4,5,6,7};
    int* p=&arr[0];
    for(int i=0;i<5;i++)
       {
          printf("%d/n",*(p+i));//p+i为指针指向的地址,*(p+i)就是该地址的空间。

       }
  

  return 0;
}

5.2指针-指针

指针-指针=地址-地址。

#include <stdio.h>
int main()
{
    int arr[5]={3,4,5,6,7};
    
    printf("%d",&arr[4]-&arr[0]);    

  return 0;
}

对于上面的代码,我们很容易错误以为结果是16,实际上运算结果是4。 

在c语言中,指针-指针不等于地址见字节个数,而是两个指针间元素个数。 

注意:这种运算前提是,两个指针必须指向同一块空间。

#include <stdio.h>
int main()
{
    int arr[5]={3,4,5,6,7};
    char srr[5]={a,b,c,d,e};

    printf("%d",&char[4]-&arr[0]);    

  return 0;
}

 

因为两个指针指向不是同一块空间,无法确定两块空间之间有没有空隙,并且无法确定元素的类型是char还是int。 

5.3指针的关系运算

指针大小比较,即地址大小比较。

#include <stdio.h>
int main()
{
    int arr[5]={4,5,6,7,8};
    int *p=&arr[0];
    int sz=sizeof(arr)/sizeof(arr[0]);
    while(p<arr+sz)//arr是数组名,也是数组首元素地址。
       {  
           printf("%d",*p);
           p++;     
       }    
          return 0;

}

6.野指针

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

6.1野指针的成因

1.指针没有初始化

#include <stdio.h>
int main()
{
     int* p;

     return 0;

}

非法访问内存。 

注:局部变量不初始化,为随机值;全局变量不初始化,为0。

2. 指针越界

#include <stdio.h>
int main()
{
     int arr[5]={0};
     int* p=arr[0];

     for(int i=0;i<10;i++)
        {
             *p=1;
             p++;//当指针超出arr范围访问就是越界访问,p就成了野指针。

        }

     return 0;

}

3.指针指向的空间释放  

int* test()
{
   int a=10;//假如a地址为0x001233CD
   return &a;//将a的地址返回
}
int main()
{
   int*p=test();//此时p接收a地址,p指向0x001233CD
   printf("%d",*p);//但是a的地址在上一个函数结束后,销毁了,因此p此时是野指针。
   return 0;
}

6.2如何避免野指针
     6.2.1初始化野指针

     如果知道指针明确指向哪里,直接赋值地址,如果不知道指向哪里,可以赋值NULL。
     如:int* p=NULL;此时*p不能赋值,*p=10这种操作是错误的。

     6.2.2小心指针越界

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

     6.2.3指针变量不再使用时,及时置NULL
     一个指针我们使用完后,如果不再使用,即使对指针置NULL,如:int a=10;int *p=&a;             p=NULL。
     同时,如果用一个指针前,先去判断他是不是空指针,如果不是我们再去使用他。

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

    不要将局部变量的地址返回。

7.assert断言

assert.h头文件定义了宏assert(),用于确保运行时程序符合指定条件,如果不符合,就报错终止运行。这个宏通常被叫做“断言”。

#include <stdio.h>
#include <assert.h>
int main()
{
     
     int*p=NULL;

     assert(p!=NULL);

     return 0;

}

上述代码中,assert可以判断条件是否满足,如果条件是真的,即表达式返回值非0,则代码运行不会有影响;反之条件错误,表达式返回值为0,assert()就会报错,在标准错误流stderr中写入一条错误信息,显示没有通过表达式,以及包含表达式的文件名和行号。

 assert()会减少程序运行效率,因此我们一般在Debug中使用,在Relase模式中是禁用assert。 

若是已确定程序没有问题,为了减少程序运行负担,可以选择不再使用assert(),此时只需在头文件assert.h前加上#define NDEBUG,即可使assert()失效,如需再使用断言,将#define NDEBUG删去即可。 

#include <stdio.h>
#define NDEBUG//这个宏必须写在assert.h前面。
#include <assert.h>
int main()
{
     
     int*p=NULL;

     assert(p!=NULL);

     return 0;

}

8.指针的使用和传值调用

8.1strlen的模拟实现 

库函数strlen的功能是求字符串长度,统计字符串中\0之前的字符个数。

函数原型:

size_t strlen(const char* str);

模拟实现,参数str接收一个地址开始,只要不是\0,计数器就加1,最终返回长度数值。

size_t my_strlen(const char*str)
{
     assert(str);\\空指针ASCII码值为0,所以如果str指向NULL,程序报错。
  
     size_t count=0;
     while(*str)// \0的ASCII码值为0,如果是\0,则循环终止。
        {
             count++;
             str++;
        }

        return count;

}
8.2传值调用和传址调用
8.2.1传值调用

当我们需要利用函数完成两个值的交换:

#include <stdio.h>
void swp(int x, int y)
{
	int w = x;
	x = y;
	y = w;

}

int main()
{
	int a = 10, b = 20;
	printf("a=%d,b=%d\n", a, b);

	swp(a, b);
	printf("a=%d,b=%d", a, b);

}

通过运行程序我们发现,通过给函数传值,并没有对a和b的值造成修改: 

这是因为,传值调用函数时,函数的形参是实参的一份临时拷贝!形参有自己的独立空间,对形参的修改不会影响实参。 

通过监视我们发现,x和y虽然对a和b数值进行了拷贝,但是它们的地址却不是同一块空间。通过函数对x和y的值进行交换,并不能影响a和b内存上的数值发生改变。

 8.2.2传址调用

我们可以通过传址的方法,对a和b进行交换:

#include <stdio.h>
void swp(int *x, int *y)
{
	int w = *x;
	*x = *y;
	*y = w;

}

int main()
{
	int a = 10, b = 20;
	printf("a=%d,b=%d\n", a, b);

	swp(&a, &b);
	printf("a=%d,b=%d", a, b);

}

通过运行我们发现,a和b的值发生了改变。

通过监视我们发现指针x和y它们分别指向的是a和b地址,: 

因此我们对x和y解引用,也就是对a和b地址上的空间进行修改。 

结论:如果我们只是需要用到函数外变量的值,不需要对变量进行改变,那么我们只需要传值调用。
反之,如果我们需要修改函数外变量的值,那么我们就需要,传址调用。

  • 17
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小张正在路上

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值