【12 深入理解指针 (1)】

目录

  1. 内存和地址
  2. 指针变量和地址
  3. 指针变量类型的意义
  4. const修饰指针
  5. 指针运算
  6. 野指针
  7. assert断言
  8. 指针的使用和传址调用

1. 内存和地址

先讲一个案例:
一栋宿舍楼有100个房间,要找一个房间,但是没有编号,只能挨个去找,这样效率很低,可以给每个房间上个编号:

一楼: 101,102,103…
二楼: 201,202,203…

有了房间号,就可以快速找到
在这里插入图片描述
在这里插入图片描述

把上面的例子放到计算机中,又是什么样子?
CPU在处理数据的时候,都是放在内存中处理的,处理完放到内存去,那么内存空间怎么高效管理的?

计算机中常见的内存单位 (补充) : 一个比特位可以存储一个2进制的0或1

bit -比特位
byte -字节  1byte=8bit
KB  1KB=1024 byte
MB  1MB=1024 KB
GB  1GB=1024 MB
TB  1TB=1024 GB
PB  1PB=1024 TB

每个内存单元相当于一个宿舍,一个字节空间里能放8个比特位,相当于一个房间住8个人,每个人就是一个比特位
每个内存空间也有一个编号,有了编号cpu就可以快速找到一个内存空间
在这里插入图片描述

c语言中内存内存单元的编号称为地址,地址也叫指针
所以也可以理解为: 内存单元的编号地址指针

1.1 如何理解编址

在这里插入图片描述

首先,计算机内有很多的内存单元,而硬件单元是互相协同工作的。所谓的协同,至少相互之间能够进行数据传递
硬件和硬件之间是互相独立的,如何通信呢?用“线”连起来
cpu和内存之间也有大量的数据交互的,所以两者必须用线连起来,有控制总线、数据总线和地址总线
需要读写数据时,cpu利用控制总线发出命令,从内存中通过地址总线获取地址,32为机器有32根地址总线,每根有两种状态,0和1表示脉冲有无,32根线就能表示32个数据,构成了一个地址,总共可以表示232种含义,每一种含义都是一个地址。然后在数据总线就可以取到数据,通过数据总线传入cpu计算,计算完成存到内存

2. 指针变量和地址

2.1 取地址操作符 (&)

在c语言中,创建变量其实就是向内存申请空间

nt main()
{
 int a = 10;
 return 0;
}

在这里插入图片描述

上述代码创建了整型变量n,内存中申请了4个字节,用于存放整数10,每个字节都有地址,四个地址分别是:

0x00E4F9C0
0x00E4F9C1
0x00E4F9C2
0x00E4F9C3

如何得到a的地址呢?

int a=10;
printf("%d",&a);

利用&符号取到变量的地址,会得到4个字节中地址较小的字节的地址: 0x00E4F9C0
虽然整形变量占4个字节,我们只取到了第一个字节地址,顺腾摸瓜可以访问到4个字节的数据

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

2.2.1 指针变量

我们取到的地址也需要存储起来,方便后期再使用,那存在哪里呢,答案是: 指针变量中

int a=10;
int* p=&a;   //取到a的地址并存放到指针变量p中

指针变量也是一种变量,是用来存放地址的

2.2.2 如何理解指针类型

p的类型是int*,如何理解呢?
*号说明是指针变量,而int说明指向的对象是整形的
在这里插入图片描述
如果换成char类型,怎么存储呢?

char ch='w';
char* p=&ch;

2.2.3 解引用操作符

只要拿到了变量的地址,就可以通过地址去找到对象,这里需要一个操作符叫解引用操作符 (*)

int a=10;
int* pa=&a;
*pa=0;

上面就运用了解引用的操作,通过*pa=0,访问地址存的对象,把变量修改为0
想修改变量,为什么不直接a=0,而要用指针呢?用指针操作多了一种途径,对变量的修改会更加灵活,后面会有更多运用

2.3 指针变量的大小

32为机器假设有32根地址总线,32个数字信号组合成一个序列,就是地址,有32个bit位,需要4个字节才能存储
如果指针变量是用来存放地址的,那么指针的大小就得4个字节才能存储
同理64位机器,假设有64根地址线,一个地址就是64个二进制位组成的二进制序列,存储起来需要8个字节的空间,指针变量的大小就是8个字节

对以下代码分别用32位和64位编译

 printf("%zd\n", sizeof(char *));
 printf("%zd\n", sizeof(short *));
 printf("%zd\n", sizeof(int *));
 printf("%zd\n", sizeof(double *));

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

结论

  • 32位平台下地址是32个bit位,指针变量大小是4个字节
  • 64位平台下地址是64个bit位,指针变量大小是8个字节
  • 注意指针变量的大小和类型是无关的,只要指针类型的变量,在相同的平台下,大小都是相同的

3. 指针变量类型的意义

为什么要有指针变量的类型,因为都是有特殊意义的,接下来继续学习

3.1 指针的解引用

//代码1
#include <stdio.h>
int main()
{
 int n = 0x11223344;
 int *pi = &n; 
 *pi = 0; 
 return 0;
//代码2
include <stdio.h>
int main()
{
 int n = 0x11223344;
 char *pc = (char *)&n;
 *pc = 0;
 return 0;
}
}

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

代码1将变量n地址处4个字节全部变为0,而代码2只将第一个字节改为0
结论: 指针的类型决定了,对指针解引用的时候有多大的权限 (一次能操作几个字节)
比如char类型只能访问一个字节,int可以访问四个

3.2 指针±整数

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

代码运行的结果:
在这里插入图片描述

可以看出,char类型指针每次+1跳过1个字节,int跳过四个字节
总结: 指针的类型决定了指针向前或向后走一步有多大

3.3 void* 指针

有一种特殊的指针类型是void*,可以理解为无具体类型的指针 (或者叫泛型指针),这种类型的指针可以用来接受任意类型地址。但也有局限性,不能直接进行指针的±和解引用的运算,一般用来接收指针参数,实现泛型编程

int a=10;
int* pa=&a;
char* pc=&a;

在这里插入图片描述
编译时会给一个警告,因为int类型的变量给一个char的指针,类型不兼容,会警告,但是用void就不会有问题

 int a = 10;
 void* pa = &a;
 void* pc = &a;
 
 *pa = 10;
 *pc = 0;

在这里插入图片描述
这里可以看到,void*类型的指针可以接收不同类型的地址,但无法直接进行指针运算

4. const修饰指针

4.1 const修饰变量

变量是可以修改的,如果把变量的地址交给一个指针变量,通过指针变量也可以修改值,如果不允许修改,怎么做?这时可以用const

int m=20;
m=20;  //m是可以修改的
const int n=0;
n=20;   //err,n不可以修改

上述n虽然是变量,但被const修饰,就加了限制,修改值不符合规则就报错,没办法直接修改n

const int n=0;
int* p=&n;
*p=20;
printf("n=%d",n);

在这里插入图片描述

可以看到通过指针间接修改了const修饰的n值,但这是不合理的,那么怎么让p拿到地址但不能修改呢?

4.2 const修饰指针变量

//const放在*号左边的情况
void test1()
{
 //代码2
 int n = 10;
 int m = 20;
 const int* p = &n;
 *p = 20;//ok?
 p = &m; //ok?
}
//const放在*号右边的情况
void test2{
 int n = 10;
 int m = 20;
 int *const p = &n;
 *p = 20; //ok?
p = &m; //ok?

//两边都有const的情况
void test4()
{
 int n = 10;
 int m = 20;
 int const * const p = &n;
 *p = 20; //ok?
 p = &m; //ok?
}
}

结论: const修饰指针变量时

const如果放在号左边,修饰的是指针指向的内容,指针指向的内容不能改变,但指针本身可变
const如果放在
号右边,修饰的是指针本身,保证指针本身不能改变,不可以指向别的内容,但指针指向的内容可以改变
如果两边都有,那指针本身和指向的内容都不可以改变

5. 指针运算

指针的基本运算有三种:

  • 指针±整数
  • 指针-指针
  • 指针的关系运算

5.1 指针±整数

数组是连续存放的,只需要知道第一个元素的地址,就能找到后面的元素
在这里插入图片描述

 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]);
 for(i=0; i<sz; i++)
 {
 printf("%d ", *(p+i));//p+i 这⾥就是指针+整数
 }

通过指针+整数,跳到下一个数组元素的地址访问,完成数组的遍历

5.2 指针-指针

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

strlen函数可以计算字符串的长度,怎么计算呢?它的参数传入的是第一个字符的地址,通过字符串结束符’\0’的判断和指针的变化来确定字符串长度。这里我们也实现一个求字符串长度的函数,传入字符串的首地址,先记录字符串首地址,然后不是‘\0’时不断+指针,最后通过结束时的指针-首地址指针,就会返回其中元素的个数。
指针-指针的结果是中间元素的个数,是一个整数

5.3 指针的关系运算


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

指针可以比较大小,通过判断指针大小完成数组遍历

6. 野指针

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

6.1 野指针的原因

  1. 指针未初始化
int* p;  //指针未初始化,默认为随机值
*p=20;
  1. 指针越界访问
 int arr[10] = {0};
 int *p = &arr[0];
 int i = 0;
 for(i=0; i<=11; i++)
 {
 //当指针指向的范围超出数组arr的范围时,p就是野指针
 *(p++) = i;
 }

指针加了十一次,超过了数组的范围
3. 指针指向的空间释放

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

n是个局部变量,会被销毁,用指针p接收了n的地址又进行操作,成了野指针非法访问会导致错误

越界访问,一个程序向内存申请了哪些空间,通过指针也只能访问这些空间,超出就会报错

6.2 如何避免野指针

明确知道指针指向哪里就赋值,不知道就设初始值为NULL,NULL是一个标识符常量,值是0,0也是个地址,但这个地址是无法访问的,读写会报错

//NULL的定义
 #ifdef __cplusplus
 #define NULL 0
 #else
 #define NULL ((void *)0)
 #endif

int n=10;
int* p1=&n;  //直接明确初始化
int* p1=NULL;  //设空值 

避免返回局部变量的地址,防止第3个代码的问题

局部变量未初始化是随机值
全局变量和静态局部变量未初始化值默认0,因为是存储在静态区的

6.3 指针不再使用时,及时设置NULL,使用之前检查有效性

只要是NULL指针就不去访问,同时使用之前要判断指针是否为NULL
可以吧野指针想象成野狗,放任不管是危险的,设为NULL,就是把狗找棵树拴起来,暂时管理野指针
但是即使拴着直接去挑逗也是危险的,所以使用之前判断是否为NULL,看是不是被拴起来的,不是NULL再使用

 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) //判断
 {
 //...
 }

7. assert断言

assert.h定义了宏assert(),用于在运行时确保程序符合指定条件,如果不符合就报错终止运行,这个宏被称为"断言"

assert (p!=NULL)

这样会验证p的值是否为NULL,如果符合条件不等于NULL,会继续运行,否则终止运行,并给出报错信息提示
assert()宏接受一个表达式作为参数,如果表达式为真(返回值非零),assert()不会产生任何作用,程序继续运行。如果为假(返回值非零),assert()就报错,在标准错误stderr中写入一条错误信息,显示没有通过的表达式,以及包含这个表达式的文件名和行号

使用assert有几个好处,不仅能标识文件和出问题的行号,还有一种无需更改代码就能开启或关闭assert的机制。如果已经确认程序没有问题,不需要再断言,就在#include <assert.h>语句前面,定义一个宏NDEBUG

define NDEBUG
#include <assert.h>

加上NDEBUG定义,重新编译,编译器就会禁用文件中所有的assert()语句。如果程序又出问题,可以移除这条宏,再次编译,重新启用assert()语句
一般可以在Debug中使用,在换成Release版本时禁用就可以,在vs环境中,Release版本会自动优化掉,这样不会影响效率

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

8.1 strlen的模拟实现

库函数strlen功能是求字符串长度,统计字符串’\0’之前的字符个数
函数原型如下:

size_t strlen (const char* str);

参数str接收一个字符串的起始地址,然后开始统计字符串\0前的字符个数,最终返回长度
模拟实现只需要遍历字符串,不是\0,计数器+1

int my_strlen(const char * str)
{
 int count = 0;
 assert(str);
 while(*str)
 {
 count++;
 str++;
 }
 return count;
}

传入字符串其实是传的字符串的首字符地址

8.2 传值调用和传址调用

什么问题,非用指针不可呢?
例如: 写一个函数,交换两个整数的值

#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的地址是0x00cffdd,b的地址是0x00cffdc4,在调用时,用形参x和y接收a,b的值,但x的地址是0x00cffcec,y的地址是0x00cffcf0,x和y确实接收了值,不过地址不一样,相当于x和y是独立的内存空间,那么函数内交换值不会影响到a和b本来的值。swap函数是吧变量本身传给了函数,这种叫传值调用

结论: 实参传递给形参的时候,形参会单独创建一份临时空间来接收实参,对形参的修改不影响实参,所以swap是失败的

那该怎样才能改变实参的值,可以使用指针,将a和b的地址传递给swap函数,通过访问地址修改a和b,最终达到交换的效果

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

输出结果:
在这里插入图片描述

可以看到,顺利交换了,这种调用函数将变量的地址传给函数的方式叫: 传址调用
如果函数内部要修改主调函数中变量的值,就需要传址调用

  • 8
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值