C语言总结

难点

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语言并没有规定变量的存储方式,如果连续定义多个变量,它们有可能是挨着的,也有可能是分散的,这取决于变量的类型、编译器的实现以及具体的编译模式,所以对于指向普通变量的指针,我们往往不进行指针的加减运算也不是绝对的,比如数组,连续分配空间,指针就可加减运算进行索引),虽然编译器并不会报错,但这样做没有意义,因为不知道它后面指向的是什么数据。
不能对指针变量进行乘法、除法、取余等其他运算,除了会发生语法错误,也没有实际的含义

*用法

在我们目前所学到的语法中,星号*主要有三种用途:

  1. 表示乘法,例如int a = 3, b = 5, c; c = a * b;,这是最容易理解的。

  2. 表示定义一个指针变量,以和普通变量区分开,例如
    int a = 100;
    int *p = &a;。

  3. 表示获取指针指向的数据,是一种间接操作,例如
    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)中。

运行过程:
在这里插入图片描述
当前地址:
在这里插入图片描述

e4300x0a10
e4340x1420
e4380x1e30
e43C0x2840
e4400xffffe530
e4440x00007fff
e4480x00000000p
e44C0x00000000
e450rbp

再往下执行一步,原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

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值