指针&指针变量
指针
计算机中所有的数据都必须放在内存中,不同类型的数据占用的字节数不一样,例如 int 占用 4 个字节,char 占用 1 个字节。为了正确地访问这些数据,必须为每个字节都编上号码,就像门牌号、身份证号一样,每个字节的编号是唯一的,根据编号可以准确地找到某个字节
我们将内存中字节的编号称为地址(Address) / **指针(Pointer)
地址从 0 开始依次增加,对于 32 位环境,程序能够使用的内存(可寻址空间**)为 232Bytes=4GB,最小的地址为 0,最大的地址为 0XFFFFFFFF
指针变量
定义 | 用于存放地址的变量 |
---|---|
声明 | int* intptr(空格位置无影响) 含义:intptr可以保存一个int变量的地址 |
大小 | 1word (32bits机器即为4bytes,64bits机器即为8bytes) |
操作 | - &:取地址(address-of)操作符,获取操作数地址 - :解引用(dereference)操作符,对指针解引用,*获取当前指针指向地址区域中的值 |
运算 | 对指针的+,-,++,—,+=等运算都是针对内存中地址的运算,ptr+n即表示地址+n*元素长度(取决于ptr指向类型)** |
程序分析1—取地址和解引用
#include <stdio.h>
void main()
{
int x = 0, y = 1;
int* ptr = &y;
*ptr = x;
printf_s("x = %d, y = %d\n", x, y);
// 结果x = 0, y = 0
}
反汇编中重要部分分析
int* ptr = &y;
lea eax,[y] //lea eax,[y],把&y写入eax中
mov dword ptr [ptr],eax //把eax值放在ptr指向空间,即写入&y
*ptr = x;
mov eax,dword ptr [ptr] //把ptr指向空间的数据写入eax,即写入&y
mov ecx,dword ptr [x] //把&x指向空间的数据写入ecx,即写入x
mov dword ptr [eax],ecx //把ecx中数据写入对eax取址指向的地址空间,即y=[%y]=ecx=x
程序分析2—指针类型转换
#include <stdio.h>
void main()
{
int x = 0x8041;
char* ptr = (char*)&x;
printf_s("%c", *ptr);
// 结果A(A的ASCII编码65即41h)
}
char的大小为1byte但char依旧为指针,大小4byte,用ptr保存&x
对ptr解引用,即改变&x指向空间的值,但经过强转后**不再是对int解引用而是对char*,所以结果只会看到&x指向空间的第一个字节,即8041中的41
程序分析3—指针运算**
#include <stdio.h>
void main()
{
int x = 0x8041;
char* ptr = (char*)&x;
int* intptr = &x;
}
调试
| |
- char类型ptr+1,ptr中地址增加了1byte(char元素长度)
- int类型intptr+1,intptr中地址增加4bytes(int元素长度)
变化sizeof(指向类型)k个bytes,
即**留出空间给指向类型的k个元素* |
| —- | —- |
函数指针(变量)
#include <stdio.h>
int add(int a, int b) {
return a + b;
}
void main()
{
int x = 0, y = 1;
int (*func)(int,int) = &add;
//或者int (*func)(int,int) = add;
printf_s("%d", func(x, y));
//或者printf_s("%d", (*func)(x, y));
}
声明 | 返回类型函数指针参数: int (*func)(int,int): |
---|---|
意义 | 指针func保存了add()的起始地址 |
要点 | - 函数指针”“需要和指针名称用()包围 - 调用时可直接func()也可显式说明(fun)() |
数组
C中数组名和指向数组首元素指针是同义的,当ptr指向数组后:
- 元素地址: ptr == a ptr + k == &a[k]
元素值: (ptr+k) ==(a+k) == a[k]
#include <stdio.h>
void main()
{
int a[5] = {0,1,2,3,4};
int* ptr = a;
printf_s("ptr = %p\n", ptr);
printf_s("a = %p\n", a);
printf_s("ptr + 2 = %p\n", ptr + 2);
printf_s("&a[2] = %p\n", &a[2]);
printf_s("*(ptr+2) = %d\n", *(ptr + 2));
printf_s("*(a+2) = %d\n", *(a + 2));
printf_s("*(ptr+2) = %d\n", *(ptr+2));
printf_s("*(a+2) = %d\n", *(a+2));
printf_s("a[2] = %d\n", a[2]);
}
二维数组
参考链接: C语言二维数组指针
示例代码 ```cinclude
void main() { //为了方便统一使用&p打印,使用16进制数 int a[2][3] = {0x00,0x01,0x02,
0x10,0x11,0x12};
int(ptr)[3] = a; printf_s(“a = %p\n”, a); printf_s(“a = %p\n”, a); printf_s(“*a = %p\n\n”, a);
printf_s(“a+1 = %p\n”, a + 1); printf_s(“(a+1) = %p\n”, (a + 1)); printf_s(“(a+1) = %p\n”, (a + 1)); printf_s(“(a+1) = %p\n\n”, (a + 1));
printf_s("a[1] = %p\n", a[1]);
printf_s("*a[1] = %p\n", *a[1]);
printf_s("a[1][1] = %p\n", a[1][1]);
printf_s("*(*(a+1)+1) = %p\n", *(*(a + 1) + 1));
printf_s("*(a[1] + 1) = %p\n", *(a[1] + 1));
}
结果:<br />![image.png](https://cdn.nlark.com/yuque/0/2019/png/670787/1576915132286-86f278ca-0ac3-4e5d-9fb4-c04e157d80da.png#align=left&display=inline&height=226&name=image.png&originHeight=341&originWidth=577&size=41371&status=done&style=none&width=383)
- 观察内存,得到结论:二维数组在概念上是二维的,但在**内存中所有元素都是连续排列**的
![image.png](https://cdn.nlark.com/yuque/0/2020/png/670787/1578225272834-9a7585cc-ea2f-4a94-8d1b-370a99409302.png#align=left&display=inline&height=62&name=image.png&originHeight=124&originWidth=819&size=9034&status=done&style=none&width=409.5)
- 数组名a是int[2][3]起始地址
- 一次解引用***a:依旧为地址**,概念上第一个int[3]起始地址
- 二次解引用****a:取到第一个int[3]起始元素值**,可监视验证
![image.png](https://cdn.nlark.com/yuque/0/2019/png/670787/1576915563128-0a495c50-7e5b-4139-8bbe-cb89fc958846.png#align=left&display=inline&height=75&name=image.png&originHeight=110&originWidth=575&size=8694&status=done&style=none&width=394)
- ⭐二维数组a[2][3]获取元素值的方法
- **①两次解引用****
- **②int[j][k]**
- **③*(a[j]+k)**
- **④*((int*)a+4*j+k):如果没有int*强转,直接运算,变化以int[3]大小为单位**
**<br />**个人理解**
- 数组b[3]={0,1,2},*b可得到0,但二维数组a[2][3]必须两次解引用,可以**看作二维数组对一维数组额外做了一层封装从而*a=b,**a=*b,**可通过监视验证,
![image.png](https://cdn.nlark.com/yuque/0/2019/png/670787/1576916153033-ad2be860-0736-4059-9941-d9b0fe5fc7f5.png#align=left&display=inline&height=77&name=image.png&originHeight=132&originWidth=650&size=11545&status=done&style=none&width=379)
- 结合👆指针运算,a+1使地址+sizeof(int[3])即12(0xA)字节,而*a+1使地址+sizeof(int)即4(0x04)字节
![image.png](https://cdn.nlark.com/yuque/0/2019/png/670787/1576918608003-2b36c0a8-258f-422b-9112-aaa86fb27579.png#align=left&display=inline&height=72&name=image.png&originHeight=112&originWidth=584&size=12035&status=done&style=none&width=374)
<a name="xAUzd"></a>
#### 数组指针&指针数组
参考链接: [C语言数组指针和指针数组](http://c.biancheng.net/view/335.html)
```c
#include <stdio.h>
void main()
{
int a[2][3] = { 0x00,0x01,0x02,0x10,0x11,0x12 };
int(*aptr)[3] = a;
int b[3] = { 0,1,2 };
int* bptr = b;
}
如上,可以创建指向数组的指针对数组访问,对于二维数组同样适用,但极易和指针数组搞混
指针数组 | 声明: int ptr[3] 含义: 运算优先级[]>,因此声明构成一个数组定义 - 数组名ptr - 大小为3 - 元素类型为int* |
---|---|
数组指针 | 声明: int(aptr)[3] 含义: 运算优先级()>[],因此声明构成一个指针定义 - 指针名称aptr - *指向一个包含3个int元素的数组 |
字符串
C中没有string这一基本类型,实际上是一个以“\0”结尾的char[],并且char[]每个元素声明后会初始化为’\0’(0x00)
结构体&联合体
Structure
参考链接: C 结构体
定义:C 数组允许定义可存储相同类型数据项的变量,结构是 C 编程中另一种用户自定义的可用的数据类型
声明:结构体声明有三种形式,其中使用typedef可以创建自定义的新类型,并通过该类型声明新的结构体变量,见👇
存储空间:结构的大小取决于其对齐的结果
Union
参考链接: C 共用体
定义:union是一种特殊的数据类型,允许在相同的内存位置存储不同的数据类型,但是任何时候只能有一个成员带有值
声明:见示例
存储空间:Union的大小只需满足最大成员即可
示例(vs默认8字节对齐,开头主动声明为4字节)
#include <stdio.h>
#pragma pack(4)
typedef struct
{
int a[7];
char b;
double c;
} Stru;
union Data
{
int i;
float f;
char str[20];
} data;
void main()
{
Stru s;
Data u;
printf_s("%d\n", sizeof(s));
printf_s("%d\n", sizeof(u));
}
结果为40,20,structure分析见👇,union的最大成员为char[20],故为20字节
结构成员对齐
参考链接: C语言内存对齐和结构补齐
为了优化CPU从内存中取数据的效率,compiler会对结构体对齐
对齐原则
- 第一个成员的**首地址为0**
- 每个成员的**首地址是size的整数倍**,当size大于基准时,按照基准对齐
最后以结构总体对齐:结构体**结束地址要是基准的整数倍**
#pragma pack(4) //以4字节对齐
typedef struct
{
int a[7];
char b;
double c;
} Stru;
int[]: 从0开始,占据7*4=28bytes
- b: sizeof(char)=1,任何位置都是1的倍数,则直接放入
- sizeof(double)=8>4,按4字节对齐,当前地址29不是4的整数倍,则29+3=32,补3字节
- 结束地址28+1+3+8=40,满足4的整数倍
- 最终结构的size为40bytes
32位机以双字(dword)进行传输,一次读8bytes(2*32bits),经过对齐,40bytes内容需要CPU从内存中读取5次
选择题知识点
⭐给定代码
#include <stdio.h>
void main()
{
char a[100] = { "Hello World!" };
a[99] = *((char*)(((int)&a[0]) + 4));
int b[100] = { 1,2,3,4,5,6,7 };
b[99] = *((int*)(((int)&b[0]) + 4));
}
①a[99]的值等于?a[4]
(int)&a[0]获取&a[0],转化为数字,直接+4后地址前进4字节,即为&a[4],对其解引用,获取a[4]
②b[99]等于?b[1]
(int)&b[0]获取&b[0],转化为数字,直接+4后地址前进4字节,即为&b[1],解引用,获取b[1]
**结论:地址化为int类型后+1表示地址前进1字节不是所有现代处理器都需要对齐
⭐计算机中地址A,A+1,A+2,A+3保存int 256,令int aptr=A在另一台设备上B,B+1,B+2,B+3保存int 256,令int bptr = B,不同设备int都是4字节长度,则 |
①A+1中数据和B+1相同🚫
②A+1中数据和B+2相同🚫
③aptr == bptr ✅ | 关于①②,设计到数据格式中大小端问题256=0x0100,同时为有符号数,所以实际表示为4字节:0x00000100,将其入栈保存
- 大端下栈帧向高地址生长,A:00,A+1:01,A+2:00,A+3:00
- (X86默认)小端下栈帧向低地址生长,A:00,A+1:00,A+2:01,A+3:00
此时①②错误,③:对int* ptr解引用,无论地址格式,都获取其中int值,即256 | | —- | —- |VS中的内存窗口:以多种方式显示内存情况,但是不出现变量名称(见👆结构对齐配图)
- 给定代码
则语句①(ary+3)=10②pary[4]=10③pary++④ary++中,错误的是ary++,数组名(起始地址)不可直接运算int a[10];
int *ptr = a;
6.给定代码,结果为10 5*,sizeof计算分配空间的长度,strlen只计算字符串有效长度(无\0) ```cinclude
include
void main() { char s[10] = “hello”; printf(“%d %d”, sizeof(s), strlen(s)); }
7. 给定代码
```c
#include <stdio.h>
#include <string.h>
void main()
{
char str1[] = "abc";
const char* str2 = "abc";
const char* str3 = "abc";
int i = (str2 == str3);
}
则调试结果如下
- 对比6,当没有指定char[]大小时,sizeof计算”字符+\0”空间
- 由于char在C中存放在专门的段中,对于相同字符串,不同指针实际指向一个地址
- C语言在数组越界访问时不会报错