深入理解指针

简介: 深入理解指针

我们首先从思维导图切入,大致理解指针这一章的思维与框架:

基本概念的理解

内存和地址

我们都知道电脑在运行时,需要将内存加载到cpu中,等待运行完毕后,又返回到cpu当中,那么,cpu是如何快速找到地址的呢?这就涉及到了地址的概念,我们把内存分为一个一个内存单元,每个内存单元的大小取一个字节,再把一个字节分为8个byte。(每个内存单元都有一个编号)这样就可以通过地址寻找到内存。c语言中,我们给地址起了新的名字,叫作指针,但是,指针是如何进行编址的呢?

计算机中的编址,并不是把每个字节的地址记录下来,而是由硬件设备完成的。计算机有很多硬件单元,而硬件单元是要相互协同工作的(数据之间要能够进行数据传递),那么数据之间如何通信呢?答案是用“线”连接起来,CPU和内存也有大量数据交互,也需要用线连起来。不过,我们今天关注一组线,叫作地址总线,32位机器有32位地址线,每根地址线能代表两种含义0或者1,总共就能表示2^32种含义,每种含义都代表一个地址,地址信息被下达给内存,就可以找到数据,再通过地址总线传入CPU内寄存器。

指针变量和解引用操作符

我们了解到一个事实:那就是,当创建变量时,其实就是在向内存申请空间。此时我们用取地址操作符取出的就是地址较小字节的地址。

指针变量在x32环境下为4个字节,再x64环境下为8个字节。那么,指针变量有什么意义呢?

指针的类型决定了对指针解引用时有多大权限。

我们将地址存在指针变量当中。解引用操作符就相当于把值从地址里拿出来。我们把对a的修改,转移成了对a的修改。

指针的运算

指针的类型决定了指针向前一步或者向后一步有多大。

指针加减整数

指针加减整数就跳到对应的数值上。

指针减去指针

结果为两个整数之间元素的个数

指针的关系运算

体现两个指针的大小关系

void*

void*类型的指针可以接收不同类型的地址,但无法进行指针运算。

const修饰指针

当const修饰变量时,原有的值不能被修改。但是,当我们绕过变量本身,修改地址,就可以改变原有的值。

#include<stdio.h>
int main()
{
  const int n = 0;
  int* p = &n;
  *p = 20;
  return 0;
}

如图的情况,就可以通过改变地址来改变const修饰的变量的值。

当const放在*左边,不能通过指针改变所指向的内容,但是能够改变指针变量本身。

当const放在*右边,能通过指针改变所指向的内容,但是不能够改变指针变量本身。

野指针

何为野指针:野指针即为指针指向位置不可知,不正确的,没有限制的。

造成野指针的原因:

第一个原因是指针越界访问(当指针范围超过数组arr的范围),第二原因是指针指向的空间已经被释放,第三个原因是指针未初始化(默认为随机值)。

如何规避野指针:

1初始化指针。若明确知道指针指向哪里就赋值地址,如果不知道,则置为NULL。

2避免越界访问。一个程序向内存中申请了哪些空间,就只能指向哪些空间。

3当指针不再使用时,及时置空NULL。只要是NULL指针就不会访问,同时使用指针之前可以判断指针是否为NULL。

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

assert断言

assert用于确保程序执行时的判定条件,如果不符合,就报错终止程序运行,这个宏被称为断言。assert()报错后,在标准错误流stderr中写入一条错误信息,显示没有通过的表达式,以及包含这个表达式的文件名以及行号。

assert能自动标识文件和出问题的行号,还有一种无需更改代码就能开启和关闭assert的机制。如果文件没有问题,不需要断言,就在#include<stdio.h>的前面加上NDEBUG宏定义。

assert的缺点就是加入了检查,增加了程序的运行时间。

数组指针

arr在大多情况下表示的是首元素的地址,只有在两种情况下表示的是整个数组的地址,第一种情况是:sizeof(arr),第二种情况是&arr。

使用指针访问数组

数组名arr是首元素的地址,可以赋值给p,数组名arr和p是等价的,我们可以用arr[i]或者p[i]访问数组。

一维数组传参的本质

在数组传参的时候,传递的是数组名,也就是说本质上数组传参本质上传递的是数组⾸元素的地址。所以在传参的时候sizeof(arr)传递的是首元素的地址的大小,而不是数组的大小。所以在函数内部,无法求出数组元素的个数。

一维数组传参,可以写成数组的形式,也可以写成指针的形式。

# define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
void test(int arr[])//写成数组形式,本质上还是指针
{
}
void test(int* arr)//参数写成指针形式
{
  printf("%d\n", sizeof(arr));
}
int main()
{
  int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
  test(arr);
  return 0;
}

指针数组模拟二维数组

指针变量

字符指针变量

int main()
{
char ch = 'w';
char *pc = &ch;
*pc = 'w';
return 0;
}

代码 const char* pstr = "hello bit."; 特别容易让同学以为是把字符串 hello bit 放

到字符指针 pstr ⾥了,但是本质是把字符串 hello bit. ⾸字符的地址放到了pstr中。

数组指针变量

整形指针变量: int * pint; 存放的是整形变量的地址,能够指向整形数据的指针。

浮点型指针变量: float * pf; 存放浮点型变量的地址,能够指向浮点型数据的指针。

那么,数组指针变量应该是:存放的应该是数组的地址,能够指向数组的指针变量。

int (*p)[10];[]的优先级要⾼于*号的,所以必须加上()来保证p先和*结合。指向的是大小为10个整型的数组。p是一个指针,指向一个数组,叫作指针数组。

转移表

函数指针的实现:int(*p[5])(int x, int y) = { 0, add, sub, mul, div };

ret = (*p[input])(x, y);//定义了一个函数指针数组。

qsort函数

回调函数就是⼀个通过函数指针调⽤的函数。

qsort函数的实现

void calc(int(*pf)(int, int))
{
int ret = 0;
int x, y;
printf("输⼊操作数:");
scanf("%d %d", &x, &y);
ret = pf(x, y);
printf("ret = %d\n", ret);
}

sizeof和strlen

这一部分的内容 我们就通过一些题目来学习吧~

一维数组:

2.16 --sizeof(数组名)的场景

3.这里的a并没有单独放在sizeof内部,所以这里的a代表首元素地址,a+0还是首元素地址,大小是4或者8个字节。

4.a是首元素的地址,*a就是首元素,大小就是首元素,大小为4

*a=a[0]=*(a+0)

5.a是首元素地址,a+1是第二个元素地址,类型为int*。大小为4或8个字节。

6.a[1]就是第二个元素,大小为4个字节。

7.&a是数组的地址,数组的地址也是地址,大小是4/8个字节

8.有两种理解方式①*和&抵消,

②将整个数组的地址取出来再解引用,得到的就是整个数组。int(*)[4]

9.&a+1是跳过整个数组得到的地址,大小是4/8个字节。

10.首元素地址大小,4或8字节。类型int*

11.数组第二个元素地址。4/8

字符数组:

2.6个元素,6个字节

3.首元素地址,就是4或8个字节。

4.首元素,大小为1个字节。

5.第二个元素,1字节

6.4/8 char(*)[4]

7.4/8

8.第二个元素

1.求到\0。数组中没有\0,就会导致越界访问。结果随机。

2.数组首元素地址。随机

3.参数 const char*。*arr是首元素,就是'a',97。97地址不允许访问。strlen得到的就是野指针。err

4.error。

5.是数组的地址。起始位置是数组第一个元素,随机值x

6.随机值x–6

7.随机x-1

2.7

3.arr+0就是首元素地址。

4.arr是首元素地址,*arr就是首元素,大小是1字节。

5.第二个元素,大小一个字节。

6.数组的地址。4/8

7.跳过整个数组。4/8

8.第二个元素地址。4/8

1.6整个数组地址

2.首元素地址,\0之前有6个字符

3.首元素,97,出错

4.b 98出错

5.数组的地址 6

6.指针指向\0后面

7.第二个元素后,5

1.算出的是p指针的大小,4/8个字节

2.char*跳过一个字符。是b的地址。4/8

3.p的类型是char*,*p的类型是char类型,一个字节

4.①p[0]等价于*(p+0)。p加0还是a的地址,解引用得到a,一个字节

②当作数组理解,把常量字符串想象成数组,p可以理解成数组名。p[0]就是首元素。

5.&p是4/8个字节,类型为char**

6.q+1,跳过char*,跳过p指针大小指向p的尾部。是地址就是4/8个字节。

7.4/8

2.6

3.5,指向第二个元素

4.就是a字符,97,err

5.等价*(p+0)

6.&p是指针变量p的地址,和字符串abcdef没有关系,答案是随机值,p指针存放的是什么,不知道

7.随机值,和上面的随机值没有关系

8.5

二维数组:

1.3*4*4

2.4

3.16

4.a[0]并没有单独放在sizeof内部。a[0]就是数组首元素的地址==arr[0][0],加1后是arr[0][1]的地址。大小是4/8

5.解引用,表示第一行第二个元素,大小为4

6.第二行的地址,数组指针的地址是4/8个字节。

7.方法一:第二行16 方法二:a[1]是第二行的数组名,相当于把a[1]单独放在sizeof内部

8.a[0]是第一行的数组名,&a[0]取出的就是数组的地址,就是第一行的地址。 +1就是第二行的地址。4/8个字节

9.访问第二行,大小是16个字节。

10.a作为数组名既没有单独放在sizeof内部,a表示数组首元素的地址,也就是第一行的地址,*a就是第一行,计算的就是第一行的大小,16个字节

*a==*(a+0)==a[0]

11.没有访问元素,a[3]无需真实存在,只需要通过类型判断就能算出长度。

a[3]是第四行的数组名,单独放在sizeof内部,计算的是第四行的大小,16个字节

指针运算笔试题目

&a Int(*)[5]

所以答案为5  2

指针加减整数。结构体指针加一跳过一个结构体。0x100000+1-->0x1000020十六进制:0x1000014。

强制转化为unsigned long就不是指针了。整型值+1,就是加上真实的1。

强制转化为unsigned int*,本质上是跳过一个整型+4。

a[0]==&a[0][0]。 a[0]是数组名,数组名又表示首元素地址。其实就是a[0][0]的地址。

%p:

%d(打印有符号的整数):1000 0000 0000 0000 0000 0000 0000 0100

                                             1111 1111  1111  1111  1111  1111 1111  1011

                                             1111  1111  1111 1111   1111 1111  11111 1100补码

四个二进制位换算一个十六进制位 fffffffc(x86环境)

如上图所示,打印的值分别为10 5。

#include <stdio.h>
int main()
{
char *a[] = {"work","at","alibaba"};
char**pa = a;
pa++;
printf("%s\n", *pa);
return 0;
}

char*p="abcdef";是把首元素的地址赋值给p  。

我们来逐个解读各个语句的含义:**++cpp,原先cpp指向c+3,加1后其指向c+2,解引用后打印POINT。

**--**++cpp,cpp指向c+2,++后指向c+1,--后将c+1改为c,指向c,打印TER。

*cpp[-2]+3,翻译过来就是*(*(cpp-2))+3,-2的时候拿的是c+3,再+3,得到的是ST。

cpp[-1][-1]+1翻译过来就是*(*(cpp-1)-1)+1,cpp-1后指向c+2,只有++cpp或者--cpp的时候指针才会动。-1后得到了c+1。打印EW。

相关文章
|
7月前
|
编译器
深入理解指针(2)
深入理解指针(2)
41 2
|
7月前
|
存储 数据处理 C++
C++中的指针:深入理解与应用
C++中的指针:深入理解与应用
|
7月前
|
存储 C语言
c 指针
c 指针
41 0
|
7月前
|
存储
什么是指针
什么是指针。
43 5
|
存储 C语言
【C】指针详解(一篇文章带你玩转指针)
很多人学习C语言都在为指针头疼,今天一篇文章带你玩转指针。
|
7月前
|
存储 程序员
C指针详解
C指针详解。
32 0
|
存储 安全 C语言
C语言知识点之 指针1
C语言知识点之 指针1
62 0
|
存储 C++
c/c++指针
c/c++指针
50 0
|
编译器
指针详解+剖析
我们可以通过&取地址操作符取出变量的内存地址,然后把这个地址呢就存放在一个变量中,这个变量就是指针变量。
93 0
指针(一)
系统给内存的每一个字节,分配一个编号,这个编号就是内存地址
指针(一)