指针、野指针、指针常量、常量指针

指针

概念

指针是什么?

在计算机科学中,指针(Pointer)是编程语言中的一个对象,利用地址,它的值直接指向存在电脑存储器中另一个地方的值。由于通过地址能找到所需的变量单元,可以说,地址指向该变量单元。因此,将地址形象化的称为“指针”。意思是通过它能找到以它为地址的内存单元

内存地址

  • 字节(byte):字节是内容的容量单位,英文称为byte,一个字节有8位,即1byte = 8bits
  • 字(word):4byte = 1字 半字:2Byte
  • 地址:系统为了便于区分每一个字节而对它们逐一进行的编号,称为内存地址。
    • 地址编号在同一块内存是连续的

在这里插入图片描述

​ 32Bit系统:内存的地址编号宽度一般是32Bit

​ 64Bit系统:内存的地址编号宽度一般是64Bit

基地址

  • 单字节数据:对于单字节数据而言,其地址就是其字节编号。例如:char

  • 多字节数据:对于多字节数据而言,其地址是其所有字节中编号最小的那个,称为基地址。例如:short、int、long、double…

在这里插入图片描述

地址相关操作–取地址符

  • 每个变量都是一块内存,都可以通过取址符& 获取其地址

  • 例如:

    int a = 100;
    printf("整型变量 a 的地址是: %p\n", &a); 
    char c = 'x'; 
    printf("字符变量 c 的地址是: %p\n", &c); 
    double f = 3.14;
    printf("浮点变量 f 的地址是: %p\n", &f);					 	
    
  • 注意:

    • 虽然不同的变量占内存空间的大小是不同的,但是他们的地址的大小却是一样的
    • 不同的地址虽然形式上看起来是一样的,但由于他们代表的内存尺寸和类型都不同,因此它们在逻辑上是严格区分的。

指针基础

  • 指针的概念

    • 指针本身是一种数据类型 ,用指针类型(*)定义的变量称之为指针变量—简称指针

    • 地址。比如 &a 是一个地址,&a 指向变量 a

    • 专门用于存储地址的变量,又称指针变量。

  • 指针的定义

    int    *p1; // 用于存储 int 型数据的地址,p1 被称为 int 型指针,或称整型指针
    char   *p2; // 用于存储 char 型数据的地址,p2 被称为 char 型指针,或称字符指针
    double *p3; // 用于存储double型数据的地址,p3 被称为 double 型指针
    
  • 指针的赋值:赋给指针的地址,类型需跟指针的类型相匹配。

    int a = 100; 
    p1 = &a; // 将一个整型地址,赋值给整型指针p1
    
    char c = 'x'; 
    p2 = &c; // 将一个字符地址,赋值给字符指针p2
    
    double f = 3.14; 
    p3 = &f; // 将一个浮点地址,赋值给浮点指针p3
    

    那指针类型的意义是什么?

在这里插入图片描述

所以,指针的类型决定了指针向前或者向后走一步有多大(距离)

  • 指针的索引—解引用:通过指针,取得其指向的目标

    *p1 = 200; // 将 p1 指向的目标(即a)修改为200,等价于 a = 200;
    *p2 = 'y'; // 将 p2 指向的目标(即c)修改为'y',等价于 c = 'y';
    *p3 = 6.6; // 将 p3 指向的目标(即f)修改为6.6,等价于 f = 6.6;
    

    内存空间的变化

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

所以,指针的类型决定了对指针解引用的时候有多大的权限(能操作几个字节),比如:char的指针解引用就只能访问一个字节,所以*pc = 0就将空间中的44改为00,而int的指针解引用就能访问四个字节,所以*pi = 0就将内存中的 00332211改为00000000。(第3幅图应该圈住4个00)

  • 指针的尺寸–即指针变量占用空间大小

    • 指针尺寸指的是指针所占内存的字节数
    • 指针所占内存,取决于地址的长度,而地址的长度则取决于系统寻址范围,即字长
    • 结论:指针尺寸只跟系统的字长有关,跟具体的指针的类型无关

总结:指针就是变量,用来存放地址的变量。(存放在指针中的值都被当成地址处理)。

到这里的问题是:

  1. 一个小的单位到底是多大?(1个字节)

  2. 如何编址?

    从上面的内存地址部分我们了解到,一个字节给一个对应的地址是比较合理的。

    对于32位的机器,假设有32根地址线,那么假设每根地址线在寻址的是产生一个电信号正电/负电(1或0)

    那么32根地址线产生的地址就会是:

    00000000 00000000 00000000 00000000

    00000000 00000000 00000000 00000001

    11111111 11111111 11111111 11111111

    这里就有2的32次方个地址,每个地址标识一个字节,那我们就可以给(232Byte == 232/1024KB == 232/1024/1024MB== 4GB)4G的空闲进行编址。

    同样的方法,那么64位机器,如果给64根地址线,那能编址多大空间?(远大于32位系统)

    所以:

    1. 32位的机器上,地址是32个0或者1组成二进制序列,那地址就得用4个字节的空间来存储,所以一个指针变量的大小就应是4字节

    2. 那如果在64位机器上,如果有64位地址线,那一个指针变量的大小是8字节,才能存放一个地址。

    总结:

    1. 指针是用来存放地址的,地址是唯一标识一块地址空间的。

    2. 指针的大小在32位平台是4字节,在64位平台是8字节。

野指针

概念

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

野指针成因

  1. 指针未初始化
int *p;//局部变量指针未初始化,默认为随机值
*p = 20;//将20存放到一块未知的空间,是很危险的。
  1. 指针越界访问
int arr[10] = {0};
int *p = arr;
int i = 0;
for(i = 0;i<=11;i++)
{
   	//当指针指向的范围超出数组arr的范围时,p就是野指针
	*(p++) = i;
}
  1. 指针指向的空间释放
int* test()
{
    int a = 10;//a是局部变量,函数返回后该变量就会被释放
    return &a;
}
int main()
{
    int *p = test();//这时候获得的空间是一块未知空间,是没有权限操作该空间的(因为该空间可能被分配给其它的程序使用,强行操作是很危险的)
    *p = 20;
    return 0;
}

如何规避野指针?

  1. 指针初始化
int a = 10;
int *pa = &a;//初始化
int *p = NULL;//NULL为(void*)0,是所有程序都访问不到的区域
//当不知道怎么给指针初始化的时候,先给指针初始化为NULL,这样操作至少不会出现段错误。
int *pa = NULL;//NULL用来初始化指针的,给指针赋值
  1. 小心指针越界
  2. 指针指向空间释放后立即将其置为NULL
  3. 指针使用之前检查有效性

野指针的危害

  1. 使用野指针,相当于访问了非法的内存,常常导致段错误。(segmentation fault)
  2. 使用野指针,可能会破坏系统的关键数据,导致系统奔溃等严重后果。

指针运算

  • 指针加法意味着地址向上移动若干个目标
  • 指针减法意味着地址向下移动若干个目标
  • 示例:
int  a = 100; 	
int *p = &a; // 指针 p 指向整型变量 a 		
int *k1 = p + 2; // 向上移动 2 个目标(2个int型数据) 	
int *k2 = p - 3; // 向下移动 3 个目标(3个int型数据)

在这里插入图片描述

标准规定:

允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但不允许与指向第一个元素之前的那个内存位置的指针进行比较。

空指针

  1. 当无法确定一个地址所对应的内存的数据类型时,将该地址类型定义为void型,即通用型。

    void *p = malloc(8);//申请一块未知用途的空间
    
  2. void 型指针在解引用时,必须转化为某种具体的数据类型指针,否则无法解引用。

    double f = 3.14
    *p = f;//错误的
    *(double *)p = f; //正确,进行类型转换 
    

很多情况下,我们不可避免地会遇到野指针,比如刚定义的指针无法立即为其分配一块恰当的内存,又或者指针所指向的内存被释放了等等。一般的做法就是将这些危险的野指针指向一块确定的内存,比如零地址内存(NULL)。

Linux规定:进程的虚拟内存从0x0000 00000x0804 8000为不可访问的内存,这段内存的权限为0,也就是任何进程访问这段内存就一定会遇到权限不足无法运行的错误而被系统强制关闭,以免程序拿到一个野指针,造成更大的损失。

NULL实际上就是一个宏:#define NULL (void *)0

在这里插入图片描述

// 1,刚定义的指针,让其指向零地址以确保安全:
char *p1 = NULL;
int  *p2 = NULL;


// 2,被释放了内存的指针,让其指向零地址以确保安全:
char *p3 = malloc(100); //让 p3 指向一块大小为100个字节的内存
free(p3);               //释放这块内存,此时 p3 相当于指向了一块非法内存
p3 = NULL;              //让 p3 指向零地址

二级指针

指针变量也是变量,是变量就有地址,用来存放指针变量地址的变量就是二级指针变量。以此类推,也可以得到三级指针、四级指针…

int a = 10;
int *pa = &a;
int **ppa = &pa;
//a的地址存放在pa中,pa的地址存放在ppa中;
//pa是一级指针,ppa是二级指针。

二级指针的运算

  1. *ppa通过对ppa中的地址进行解引用,这样找到的是pa,*ppa其实访问的就是pa。

  2. **ppa先通过*ppa找到pa。然后对pa进行解引用操作:*pa,那找到的是a。

    **ppa = 30;
    等价于*pa = 30;
    等价于 a = 30

指针与数组的关系

  1. 数组名可以作为一个指针来使用,数组名是一个指针常量 ,不能改变只能用。

    char  a[10];
    char *p;
    a = p;//错误的,a只是一个指针常量
    能够参与运算
    p = a;//允许
    
  2. 指针和数组的访问方式可以互换

  • 数组名当成一个指针 用指针的运算访问成员 ; *a[n] == (a+n)
  • 指针用数组的下标访问方式 访问数组成员;*(p+0) == p[0] == a[0]
  • 数组指针—指向一个数组的指针;*char (p)[20];
  • 指针数组—存放若干个相同类型指针的数组;char *p[20];//定义了一个数组 有20个成员 20个char *类型成员

指针常量与常量指针

指针常量:int * const p;//关键字const修饰的是p,所以p是一个常量

int a = 10;
int b = 20;
int *const p = &a;//定义时就需要初始化,不能int *const p = NULL;   p = &a;这是错误的,因为这时候的p已经没办法改变了
p = &b;//这是错误的,p是常量,没办法改变的
*p = 20//这是正确的

常量指针:int const *p; 或者 const int *p;//这时候的const修饰的是*,p是一个变量,*p是一个常量.(这种表达我不知道对不对,但这样理解是没问题的)

int a = 10;
int b = 20;
int const *p = &a//可以int const *p = NULL; p = &a;
*p = 20;//这是错误的,*p是一个常量,不可改变
p = &b;//这是正确的

>>
结构体及其对齐规则
系统IO与标准IO
C语言实现24点游戏算法

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值