难点
1 指针
计算机中所有的数据都必须放在内存中,不同类型的数据占用的字节数不一样,例如 int 占用 4 个字节,char 占用 1 个字节。
为了正确地访问这些数据,必须为每个字节都编上号码,就像门牌号、身份证号一样,每个字节的编号是唯一的,根据编号可以准确地找到某个字节。
下图是 4G 内存中每个字节的编号(以十六进制表示):
我们将内存中字节的编号称为地址(Address)或指针(Pointer)。
int a = 100;
char str[100] ="nihao youyou";
printf("a'--> address(point)= %#X \n str'-->address(point)=%#x\n:",&a,str);
执行结果:
a’–> address(point)= 0X42A181C
str’–>address(point)=0x42a17b0
%#X
表示以十六进制形式输出,并附带前缀0X
。a 是一个变量,用来存放整数,需要在前面加&
来获得它的地址;str 本身就表示字符串的首地址,不需要加&
需要注意的是,虽然变量名、函数名、字符串名和数组名在本质上是一样的,它们都是地址的助记符,但在编写代码的过程中,我们认为变量名表示的是数据本身
,而函数名、字符串名和数组名表示的是代码块或数据块的首地址。
2 指针变量
参考链接
数据在内存中的地址也称为指针,如果一个变量存储了一份数据的指针,我们就称它为指针变量。
在C语言中,允许用一个变量来存放指针,这种变量称为指针变量。指针变量的值就是某份数据的地址,这样的一份数据可以是数组、字符串、函数,也可以是另外的一个普通变量或指针变量。
现在假设有一个 char 类型的变量 c,它存储了字符 ‘K’(ASCII码为十进制数 75),并占用了地址为 0X11A 的内存(地址通常用十六进制表示)。另外有一个指针变量 p,它的值为 0X11A,正好等于变量 c 的地址,这种情况我们就称 p 指向了 c,或者说 p 是指向变量 c 的指针。
1 #include<stdio.h> 2
3 int main(void){
4
5 char a="s"; //变量, 保存一个字符"s"
6 char * point = &a; //指针变量,&a 取变量a的指针,保存到指针变量 point里。
7
8 printf("a address(point) ---> %#X \n",&a); // 变量a的指针(地址)
9 printf("point value ---> %#X \n",point);
10
11
12 return 0;
13 }
~
定义指针变量
定义指针变量与定义普通变量非常类似,不过要在变量名前面加星号*,格式为:
datatype *name;
*
表示这是一个指针变量,datatype
表示该指针变量所指向的数据的类型。
*
是一个特殊符号,表明一个变量是指针变量,定义 p1、p2 时必须带*。
而给 p1、p2 赋值时,因为已经知道了它是一个指针变量,就没必要多此一举再带上*,后边可以像使用普通变量一样来使用指针变量。也就是说,定义指针变量时必须带*,给指针变量赋值时不能带*。
假设变量 a、b、c、d 的地址分别为 0X1000、0X1004、0X2000、0X2004,下面的示意图很好地反映了 p1、p2 指向的变化:
需要强调的是,p1、p2 的类型分别是float*和char*
,而不是float和char,它们是完全不同的数据类型,读者要引起注意。
通过指针变量取得数据
参考链接
指针变量存储了数据的地址,通过指针变量能够获得该地址上的数据,格式为:
*pointer;
这里的*称为指针运算符,用来取得某个地址上的数据,请看下面的例子:
1 #include<stdio.h> 2
3 int main(void){
4
5 int a=100; //变量, 保存一个整数 100
6 int * point = &a; //指针变量,&a 取变量a的指针,保存到指针变量 point里。
7
8 printf("a address(point) ---> %#X \n",&a); // 变量a的指针(地址)
9 printf("point value ---> %#X \n",point);
10
11 printf("*(point value) ---> %d \n",*point); // 这里*是指针运算符,等同于 *(&a) 是一样的
12 return 0;
13 }
输入结果:
a address(point) —> 0X724B6064
point value —> 0X724B6064
*(point value) —> 100
CPU 读写数据必须要知道数据在内存中的地址,普通变量和指针变量都是地址的助记符,虽然通过 *p 和 a 获取到的数据一样,但它们的运行过程稍有不同:a 只需要一次运算就能够取得数据,而 *p 要经过两次运算,多了一层“间接”。
假设变量 a、p 的地址分别为 0X1000、0XF0A0,它们的指向关系如下图所示:
3 所有名称都是助记符,所有名称本质都是指针(地址)
int main()
{
int a = 100;
return 0;
}
0000000000001125 <main>:
1125: 55 push rbp
1126: 48 89 e5 mov rbp,rsp
1129: c7 45 fc 64 00 00 00 mov DWORD PTR [rbp-0x4],0x64
1130: b8 00 00 00 00 mov eax,0x0
1135: 5d pop rbp
1136: c3 ret
1137: 66 0f 1f 84 00 00 00 nop WORD PTR [rax+rax*1+0x0]
113e: 00 00
编译器根本不在乎,你的变量名词,因为那只是对程序员来说一种助记符号而已,编译器只根据类型 int型,给变量在栈中分配四个字节的空间,然后赋值(mov)立即数 0x00 00 00 64 (100)填满这四个字节。然后,变量a的指针也就是分配的这四个字节的首地址位置(因为一个字节一个地址,而一个地址就是一个指针)
c7 45 fc 64 00 00 00 mov DWORD PTR [rbp-0x4],0x64
也就是不管你是 int a, int aa, int aaa,表现都只是为 [rbp-0x4],我给你分配整型(int型)字节位数(四个字节)的连续地址:
1129: c7 45 fc 64 00 00 00
1129: c7
112A: 45
112B: fc
(int a = 100)
112C: 64
112D: 00
112E: 00
112F : 00
因此,也就理解了,指针运算,指针变量加减运算的结果跟数据类型的长度有关,而不是简单地加 1 或减 1。
以 a 和 pa 为例,a 的类型为 int,占用 4 个字节,pa 是指向 a 的指针,如下图所示:
刚开始的时候,pa 指向 a 的开头,通过 *pa 读取数据时,从 pa 指向的位置向后移动 4 个字节,把这 4 个字节的内容作为要获取的数据,这 4 个字节也正好是变量 a 占用的内存。
如果pa++;使得地址加 1 的话,就会变成如下图所示的指向关系:
这个时候 pa 指向整数 a 的中间,*pa 使用的是红色虚线画出的 4 个字节,其中前 3 个是变量 a 的,后面 1 个是其它数据的,把它们“搅和”在一起显然没有实际的意义,取得的数据也会非常怪异。
如果pa++;使得地址加 4 的话,正好能够完全跳过整数 a,指向它后面的内存,如下图所示:
不过C语言并没有规定变量的存储方式,如果连续定义多个变量,它们有可能是挨着的,也有可能是分散的,这取决于变量的类型、编译器的实现以及具体的编译模式,所以对于指向普通变量的指针,我们往往不进行指针的加减运算(也不是绝对的,比如数组,连续分配空间,指针就可加减运算进行索引),虽然编译器并不会报错,但这样做没有意义,因为不知道它后面指向的是什么数据。
不能对指针变量进行乘法、除法、取余等其他运算,除了会发生语法错误,也没有实际的含义。
*用法
在我们目前所学到的语法中,星号*主要有三种用途:
-
表示乘法,例如int a = 3, b = 5, c; c = a * b;,这是最容易理解的。
-
表示定义一个指针变量,以和普通变量区分开,例如
int a = 100;
int *p = &a;。 -
表示获取指针指向的数据,是一种间接操作,例如
int a, b, *p = &a;
*p = 100;
b = *p;
数组指针
数组(Array)是一系列具有相同类型的数据的集合,每一份数据叫做一个数组元素(Element)。数组中的所有元素在内存中是连续排列的,整个数组占用的是一块内存。以int arr[] = { 99, 15, 100, 888, 252 };【这里可以理解为,数组是一种复合类型的结构体,结构体定义:
struct tap_struct {
int x,y;
};
结构体最后要有引号;的,表示这是一个{initializer};
结构体声明:
struct tap_struct z1;
struct tap_struct z2;
数组也是一样,可以理解为也是结构体,
datatype name[ ] ={initializer};
其中 [ ] 就是 tap标记的。
】为例,该数组在内存中的分布如下图所示:
定义数组时,要给出数组名和数组长度,数组名可以认为是一个指针,它指向数组的第 0 个元素。在C语言中,我们将第 0 个元素的地址称为数组的首地址。以上面的数组为例,下图是 arr 的指向:
数组名的本意是表示整个数组,也就是表示多份数据的集合,但在使用过程中经常会转换为指向数组第 0 个元素的指针。
#include <stdio.h>
int main(){
int arr[] = { 99, 15, 100, 888, 252 };
int len = sizeof(arr) / sizeof(int); //求数组长度
int i;
for(i=0; i<len; i++){
printf("%d ", *(arr+i) ); //*(arr+i)等价于arr[i]
}
printf("\n");
return 0;
}
这里有个关键理解,就是 arr[i]
,不要去数,我的理解是 跳过第i个元素
。
int arr[] = { 99, 15, 100, 888, 252 };
不要去数
比如:
- arr[4] --> 跳过4个元素的地址(4 * sizeof(int) ),后开始的地址位置,就是252;
- arr[2] --> 跳过2个元素,那就是100,地址位置: 2 * sizeof(int) =2 * 4 = 8 ,也就是从 rbp-0x08位置开始,就是该元素。
- arr[3] -->跳过3个元素,那就是888。
1 #include<stdio.h>
3
4 int main()
5 {
6
7 int array [ ] = {10, 20, 30, 40 };
8 int len = sizeof(array) / sizeof(int);
9 printf("len : %d \n ", len);
10 for (int i =0 ; i< len; i++ ){
11 int tmp = *(array + i);
12 printf("array data point: %#X \n", (array+i));
13 printf("array data : %d \n", tmp);
14 }
15
16 int * p = array;
17 printf("array point : %#X \n", p); // p == array
18
19 int * p2 = &array[1];
20 printf("array[1] point : %#X \n", p2); // p2 == &array[1]
21 return 0;
22 }
~
len : 4
array data point: 0XCDB8370
array data : 10
array data point: 0XCDB8374
array data : 20
array data point: 0XCDB8378
array data : 30
array data point: 0XCDB837C
array data : 40
array point : 0XCDB8370
array[1] point : 0XCDB8374
为了进一步搞懂数组(数组名)与指针在汇编中执行过程,精简下
1 #include<stdio.h> 2
3 int main()
4 {
5 int array[] = {10, 20, 30,40 };
6 int * p = array;
7 printf(array);
8 printf(p);
9 return 0;
10 }
反编译
int main()
{
1135: 55 push rbp
1136: 48 89 e5 mov rbp,rsp
1139: 48 83 ec 20 sub rsp,0x20
int array[] = {10, 20, 30,40 };
113d: c7 45 e0 0a 00 00 00 mov DWORD PTR [rbp-0x20],0xa
1144: c7 45 e4 14 00 00 00 mov DWORD PTR [rbp-0x1c],0x14
114b: c7 45 e8 1e 00 00 00 mov DWORD PTR [rbp-0x18],0x1e
1152: c7 45 ec 28 00 00 00 mov DWORD PTR [rbp-0x14],0x28
int * p = array;
1159: 48 8d 45 e0 lea rax,[rbp-0x20]
115d: 48 89 45 f8 mov QWORD PTR [rbp-0x8],rax
printf(array);
1161: 48 8d 45 e0 lea rax,[rbp-0x20]
1165: 48 89 c7 mov rdi,rax
1168: b8 00 00 00 00 mov eax,0x0
116d: e8 be fe ff ff call 1030 <printf@plt>
printf(p);
1172: 48 8b 45 f8 mov rax,QWORD PTR [rbp-0x8]
1176: 48 89 c7 mov rdi,rax
1179: b8 00 00 00 00 mov eax,0x0
117e: e8 ad fe ff ff call 1030 <printf@plt>
return 0;
1183: b8 00 00 00 00 mov eax,0x0
}
DWORD 双字 就是四个字节
PTR pointer缩写 即指针
[]里的数据是一个地址值,这个地址指向一个双字型数据
比如mov DWORD PTR [rbp-0x20],0xa 把双字型(32位)数据立即数(数据)0xa(10)赋给内存地址[rbp-0x20] (0xffffe430)中。
运行过程:
当前地址:
e430 | 0x0a | 10 |
e434 | 0x14 | 20 |
e438 | 0x1e | 30 |
e43C | 0x28 | 40 |
e440 | 0xffffe530 | |
e444 | 0x00007fff | |
e448 | 0x00000000 | p |
e44C | 0x00000000 | |
e450 | rbp |
再往下执行一步,原0x00000000
的 p指针地址变化了,被赋值数组首元素地址0xffffe430
。
函数指针
表达式:
datatype (* point)(参数类型1,参数类型2..【与要绑定的函数参数个数、类型一致】);
因为括号()
优先级最高,高于*
,所以指针必须在括号里,否则就成了 datatype * point(参数) == datatype *(point(参数)) ,这什么鬼?
所以,指针必须加括号。
1 #include<stdio.h>
2 int test(int x, int y);
3 int main()
4 {
5 test(10,40);
6 int (* point)(int, int); // 只是声明,编译器不会分配内存空间,什么都没有;
9 return 0; 10 }
11 int test(int x, int y)
12 {
13 int z;
14 z = x + y;
15 return z;
16 }
~
这里然函数指针 point,指向test函数:
1 #include<stdio.h>
2 int test(int x, int y);
3 int main()
4 {
5 test(10,40);
6 int (* point)(int, int); // 只是声明,编译器不会分配内存空间,什么都没有;
7 point = &test; // 函数、结构体,跟一般类型变量一样,都需要&(连字号)进行取地址,而数组除外,数组名就是数组地址。
8
9 return 0; 10 }
11 int test(int x, int y)
12 {
13 int z;
14 z = x + y;
15 return z;
16 }
~
Load effective address——取有效地址,也就是取偏移地址。是取源操作数的偏移地址,并将其传送到目的操作数单元。类似于C语言的取地址符&。
我们再看下 [rip+0xb] ,盲猜就是test函数的首地址:
程序自身的test函数偏移地址:
函数指针的使用,与正常的函数类似:
1 #include<stdio.h> 2 int test(int x, int y);
3 int main()
4 {
5 test(10,40);
6 int (* point)(int, int); // 只是声明,编译器不会分配内存空间,什么都没有;
7 point = &test; // 函数、结构体,跟一般类型变量一样,都需要&(连字号)进行取地址,而数组除外,数组名就是数组地址。
8 printf("test == %d\n", point(10,40)); // point(10,40) == test(10,40)
9 return 0;
10 }
11 int test(int x, int y)
12 {
13 int z;
14 z = x + y;
15 return z;
16 }
~
执行:
test == 50