我们首先从思维导图切入,大致理解指针这一章的思维与框架:
基本概念的理解
内存和地址
我们都知道电脑在运行时,需要将内存加载到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。