一、c语言的汇编表示
1、C/C++/VC6/VS之间的区别
C、C++是编程语言
VC6、VS是集成开发环境
我选择用VC6来学习
因为VC6小巧,功能较少,利于学习
本笔记是基于汇编语言学习C
2、第一个C程序
1 | void int() |
最简单的程序 F7构建 F5运行
构建就是生成exe的过程
__asm就是代表是汇编代码
3、如何查看汇编
首先按F9打断点再运行

这个就是查看汇编

看完后按shift+f5退出
4、什么是函数
在汇编中也提到了函数就是一系列指令的集合,为了完成重复使用的功能
C语言中函数格式:
返回类型 函数名(参数列表)
{
}
返回类型、函数名不能省略
参数列表可以省略
函数名、参数名命名规则:
1.只能以字母、数字、下划线组成,且第一个字母必须是字母或下划线
2.区分大小写
3.不能使用关键字
例如:
1 | int plus(int x,int y) |
汇编:
1 | int plus(int x,int y){ |
5.函数的调用
1 | plus(1,2); |
二、参数传递与返回值
1.函数定义
返回类型 函数名(参数列表)
{
return;
}
例子:
1 | int plus(int x,int y) |
int说明他是四个字节
short 2个字节
char 1个字节
2.函数如何执行
要擅于画堆栈图
注意:和DTdebug类似 单步步过为F10 步入为F11

c语言中使用堆栈传参
c语言中返回值存储在eax中
三、变量
1.声明变量
变量类型 变量名;
如 int a;
2.全局变量
当变量不在函数里边声明时,此变量为全局变量
如:
在汇编中
变量其实就是一个内存地址的编号
赋值就是
注意:如果不重写编译,全局变量的内存地址永远不变。游戏外挂中的找”基址”就是找全局变量
全局变量中的值任何程序都可以更改

谁最后执行x就时什么值
全局变量不初始化的话默认为0
3.局部变量

局部变量在函数里边声明
如果函数没有被执行,那么局部变量没有内存空间
局部变量的内存时在堆栈中分配的,函数执行时才分配。我们无法预知函数何时执行,也就意味着,我们无法确定局部变量的内存地址
因为局部变量的地址内存时不确定的,所以局部变量只能在函数内部使用,其他函数不能使用

这样就能保证函数执行时间能够让我们用来实验

所以这样是不能编译的 因为x只是局部变量
4.变量与参数的内存布局

这个程序运行的堆栈图


eax就是返回值也就是局部变量z,会储存在缓冲区里边
缓冲区用来存储局部变量
ebp+8是参数区
ebp-4是缓冲区,局部变量区

include 是头文件 %d 是以十进制显示变量r
四、函数嵌套调用的内存布局

堆栈图

其实就是重复执行两次
五、整数类型
1.c语言中的变量类型

2. 整数类型的宽度

3.数据溢出

数据溢出舍弃的是高位
如果舍弃低位的话 应该是 1000 0000 对应的就是 80 但结果是 00
4.有符号数与无符号数
4.1什么时候使用有符号、无符号
unsigned char x = 1; 这个定义是无符号数
signed char x = 1; 这个定义的是有符号数
4.2有符号数与无符号数区别
有符号数最高位是符号位 也就是说 0000 0000 char 八位 对于正数来说只能0-127 对于负数来说是-128- -1
无符号数就是0-255
当有符号数低字节类型赋值给高字节类型时,低位直接补齐,高位则补符号位
如:char x = -1 是0xFF int y = x 后 y是0xFFFFFFFF
若 char x =127 是0x7F int y =x 后 y就是 0x0000007F
而无符号数拓展的时候直接低位补齐 高位补0
比较的时候 如果两个数都为无符号数
由于-1对应的是 0xFFFFFFFF 所以大于 1对应的 0x00000001
六、浮点类型
1、浮点数概念
浮点数也称小数或实数。
C语言中采用float和double关键字来定义小数,float称为单精度浮点型,double称为双精度浮点型,long double更长的双精度浮点型。
在任何区间内(如1.0 到 2.0 之间)都存在无穷多个实数,计算机的浮点数不能表示区间内所有的值。
float只能表达6-7位的有效数字,不能用“==”判断两个数字是否相等。
double能表达15-16位有效的数字,可以用“==”判断两个数字是否相等。
long double占用的内存是double的两倍,但表达数据的精度和double相同。
2、浮点数的输出
float采用%f占位符。double采用%lf占位符。测试结果证明,double不可以用%f输入,但可以用%f输出,但是不建议采用%f,因为不同的编译器可能会有差别。
long double采用%Lf占位符,注意,L是大写。
浮点数输出缺省显示小数点后六位。
浮点数采用%lf输出,完整的输出格式是%m.nlf,指定输出数据整数部分和小数部分共占m位,其中有n位是小数。如果数值长度小于m,则左端补空格,若数值长度大于m,则按实际位数输出。
3、整数与浮点数的转换
在浮点数的取值范围内,整数转换为浮点数不会有精度的损失,浮点数转换为整数后,会丢弃小数位。
4、科学计数法
用科学记数法表示数时,不改变数的符号,只是改变数的书写形式而已,可以方便的表示日常生活中遇到的一些极大或极小的数 。如:光的速度大约是300,000,000米/秒;全世界人口数大约是:6,100,000,000,这样的数书写和显示都很不方便,为了免去写这么多重复的0,将其表现为这样的形式:6,100,000,000=6.1×109,即6.1E9或6.1e9。
0.00001=1×10-5,即绝对值小于1的数也可以用科学记数法表示为a乘10-n的形式。即1E-5或1e-5。
科学计数法采用%e或%E输出,完整的输出格式是%m.ne或%m.nE,指定输出数据整数部分和小数部分共占m位,其中有n位是小数。如果数值长度小于m,则左端补空格,若数值长度大于m,则按实际位数输出。
七、字符与字符串
1.字符的使用
1 | int x = 'A'; |
但我们查看汇编代码的时候,发现内存中A不是A而是41h B是42h
这就要看ASCII码了
二进制 | 十进制 | 十六进制 | 字符/缩写 | 解释 |
---|---|---|---|---|
00000000 | 0 | 00 | NUL (NULL) | 空字符 |
00000001 | 1 | 01 | SOH (Start Of Headling) | 标题开始 |
00000010 | 2 | 02 | STX (Start Of Text) | 正文开始 |
00000011 | 3 | 03 | ETX (End Of Text) | 正文结束 |
00000100 | 4 | 04 | EOT (End Of Transmission) | 传输结束 |
00000101 | 5 | 05 | ENQ (Enquiry) | 请求 |
00000110 | 6 | 06 | ACK (Acknowledge) | 回应/响应/收到通知 |
00000111 | 7 | 07 | BEL (Bell) | 响铃 |
00001000 | 8 | 08 | BS (Backspace) | 退格 |
00001001 | 9 | 09 | HT (Horizontal Tab) | 水平制表符 |
00001010 | 10 | 0A | LF/NL(Line Feed/New Line) | 换行键 |
00001011 | 11 | 0B | VT (Vertical Tab) | 垂直制表符 |
00001100 | 12 | 0C | FF/NP (Form Feed/New Page) | 换页键 |
00001101 | 13 | 0D | CR (Carriage Return) | 回车键 |
00001110 | 14 | 0E | SO (Shift Out) | 不用切换 |
00001111 | 15 | 0F | SI (Shift In) | 启用切换 |
00010000 | 16 | 10 | DLE (Data Link Escape) | 数据链路转义 |
00010001 | 17 | 11 | DC1/XON (Device Control 1/Transmission On) | 设备控制1/传输开始 |
00010010 | 18 | 12 | DC2 (Device Control 2) | 设备控制2 |
00010011 | 19 | 13 | DC3/XOFF (Device Control 3/Transmission Off) | 设备控制3/传输中断 |
00010100 | 20 | 14 | DC4 (Device Control 4) | 设备控制4 |
00010101 | 21 | 15 | NAK (Negative Acknowledge) | 无响应/非正常响应/拒绝接收 |
00010110 | 22 | 16 | SYN (Synchronous Idle) | 同步空闲 |
00010111 | 23 | 17 | ETB (End of Transmission Block) | 传输块结束/块传输终止 |
00011000 | 24 | 18 | CAN (Cancel) | 取消 |
00011001 | 25 | 19 | EM (End of Medium) | 已到介质末端/介质存储已满/介质中断 |
00011010 | 26 | 1A | SUB (Substitute) | 替补/替换 |
00011011 | 27 | 1B | ESC (Escape) | 逃离/取消 |
00011100 | 28 | 1C | FS (File Separator) | 文件分割符 |
00011101 | 29 | 1D | GS (Group Separator) | 组分隔符/分组符 |
00011110 | 30 | 1E | RS (Record Separator) | 记录分离符 |
00011111 | 31 | 1F | US (Unit Separator) | 单元分隔符 |
00100000 | 32 | 20 | (Space) | 空格 |
00100001 | 33 | 21 | ! | |
00100010 | 34 | 22 | “ | |
00100011 | 35 | 23 | # | |
00100100 | 36 | 24 | $ | |
00100101 | 37 | 25 | % | |
00100110 | 38 | 26 | & | |
00100111 | 39 | 27 | ‘ | |
00101000 | 40 | 28 | ( | |
00101001 | 41 | 29 | ) | |
00101010 | 42 | 2A | * | |
00101011 | 43 | 2B | + | |
00101100 | 44 | 2C | , | |
00101101 | 45 | 2D | - | |
00101110 | 46 | 2E | . | |
00101111 | 47 | 2F | / | |
00110000 | 48 | 30 | 0 | |
00110001 | 49 | 31 | 1 | |
00110010 | 50 | 32 | 2 | |
00110011 | 51 | 33 | 3 | |
00110100 | 52 | 34 | 4 | |
00110101 | 53 | 35 | 5 | |
00110110 | 54 | 36 | 6 | |
00110111 | 55 | 37 | 7 | |
00111000 | 56 | 38 | 8 | |
00111001 | 57 | 39 | 9 | |
00111010 | 58 | 3A | : | |
00111011 | 59 | 3B | ; | |
00111100 | 60 | 3C | < | |
00111101 | 61 | 3D | = | |
00111110 | 62 | 3E | > | |
00111111 | 63 | 3F | ? | |
01000000 | 64 | 40 | @ | |
01000001 | 65 | 41 | A | |
01000010 | 66 | 42 | B | |
01000011 | 67 | 43 | C | |
01000100 | 68 | 44 | D | |
01000101 | 69 | 45 | E | |
01000110 | 70 | 46 | F | |
01000111 | 71 | 47 | G | |
01001000 | 72 | 48 | H | |
01001001 | 73 | 49 | I | |
01001010 | 74 | 4A | J | |
01001011 | 75 | 4B | K | |
01001100 | 76 | 4C | L | |
01001101 | 77 | 4D | M | |
01001110 | 78 | 4E | N | |
01001111 | 79 | 4F | O | |
01010000 | 80 | 50 | P | |
01010001 | 81 | 51 | Q | |
01010010 | 82 | 52 | R | |
01010011 | 83 | 53 | S | |
01010100 | 84 | 54 | T | |
01010101 | 85 | 55 | U | |
01010110 | 86 | 56 | V | |
01010111 | 87 | 57 | W | |
01011000 | 88 | 58 | X | |
01011001 | 89 | 59 | Y | |
01011010 | 90 | 5A | Z | |
01011011 | 91 | 5B | [ | |
01011100 | 92 | 5C | \ | |
01011101 | 93 | 5D | ] | |
01011110 | 94 | 5E | ^ | |
01011111 | 95 | 5F | _ | |
01100000 | 96 | 60 | ` | |
01100001 | 97 | 61 | a | |
01100010 | 98 | 62 | b | |
01100011 | 99 | 63 | c | |
01100100 | 100 | 64 | d | |
01100101 | 101 | 65 | e | |
01100110 | 102 | 66 | f | |
01100111 | 103 | 67 | g | |
01101000 | 104 | 68 | h | |
01101001 | 105 | 69 | i | |
01101010 | 106 | 6A | j | |
01101011 | 107 | 6B | k | |
01101100 | 108 | 6C | l | |
01101101 | 109 | 6D | m | |
01101110 | 110 | 6E | n | |
01101111 | 111 | 6F | o | |
01110000 | 112 | 70 | p | |
01110001 | 113 | 71 | q | |
01110010 | 114 | 72 | r | |
01110011 | 115 | 73 | s | |
01110100 | 116 | 74 | t | |
01110101 | 117 | 75 | u | |
01110110 | 118 | 76 | v | |
01110111 | 119 | 77 | w | |
01111000 | 120 | 78 | x | |
01111001 | 121 | 79 | y | |
01111010 | 122 | 7A | z | |
01111011 | 123 | 7B | { | |
01111100 | 124 | 7C | | | |
01111101 | 125 | 7D | } | |
01111110 | 126 | 7E | ~ | |
01111111 | 127 | 7F | DEL (Delete) | 删除 |
这样也就不难看出来为什么A对应41h了
有人说char是字符型变量类型是错的
其实他就是整形,储存的不过是ASCII码罢了。
2.字符串的使用
printf(“Hello World!”)其实也就是向某个内存地址存入ASCII码
例如
字符串就是一串字符组成
那如何储存字符串呢,得用到数组类型
1 | char buffer[20] = "Hello World!" |
八、中文字符
1.ASCII码无法满足中文
ascii码中无法存放这么多中文
2.如何才能在计算机中储存中文呢

如:
3.这种方式也有弊端

九、运算符与表达式
1.什么是运算符、表达式
1 | + - >都是运算符 |
但无论表达式多么复杂 都只是一个数
当char和short类型参与运算的时候,根据汇编代码,会把char和short数据存入eax 也就是会拓展成为int类型

这张图代表如果哟double数据会全部转化为double
运算结果就是double
2.运算符
2.1算数运算符

自加和自减都有两种情况
x++ 或++x
x++是先运算在自加
++x是先自加再运算
十、分支语句
1.if语句
1 | void main() // 入口程序 |
其中表达式()中都有个结果 0为false 其他为true
注意:if语句在;就已经结束了
1 | void main() // 入口程序 |
如果是这种情况 ,那么第一句不会执行,第二句不受if语句影响
运行结果:

所以可以用大括号如:
1 | if(x>y){ |
那么这零个语句都会受if语句影响
2.if else语句
还有一种情况
1 | if(x>y){ |
若if结果为true则执行第一条语句为false则执行第二条else里边的语句
3.if else if 语句
1 | int x = 10; |
虽然x满足两个条件
但只会执行遇到的第一个条件
输出结果:

那如果再加else呢 如:
1 | int x = 10; |
这个代表除了else之外所有语句都没有执行else才会执行
4.从汇编角度
其实也很简单

就是先比较两个数大小,再决定要不要执行printf函数
5.switch语句
5.1switch语句的格式

break是跳出switch
default是case中没有与表达式匹配的就执行default语句
5.2条件合并的写法

n=1和2都执行同一个语句
5.3swiitch和if..else区别

5.4switch为什么高效

switch语句中通过计算和最后一个jmp能准确跳到case语句
switch语句在case小于4(不一定,取决于编译器)的时候不会生成大表
效率和if..else是差不多的
但生成大表后,这个大表会储存每一个case语句的内存地址,能够直接jmp跳转
switch算法特别强悍,无论是大于case大于100还是啥都能使用
但大表所储存的地址都是连续的 那case 表达式 1,2直接跳到5怎么处理呢
他会在1,2,5之前填充default的地址。
十一、循环语句
1.goto语句
1 | void MyPrint(int x) |
这就是简单的一个循环
他是通过判断i是否小与x
在汇编层次上其实就是jmp
2.while语句
1 | void MyPrint(int x) |
其实功能是一样的
不过while语句是判断表达式里边是否为True,如果为True则执行语句直到为false为止
2.1break 语句
break用来跳出循环或者swtich语句
如果break在if里边,他会跳出最近的循环语句而不是if语句
2.2continue语句
continue用来跳出本次循环,他不会执行下边的语句,会跳到循环语句
3.do while语句

他会先执行一次代码,再循环
3.1 do while的汇编代码

3.2while的汇编代码

4.for语句

for语句会先执行表达式1然后判断表达式2是否进入循环,进入一次循环退出后执行表达式3
1 | for(int i=0,i<10,i++) |
反汇编


十二、数组
1.数组的定义
数据类型 变量名[常量]
常量代表是数组的长度
[]必须为常量
如果为变量的话,程序不知道要分配多大的内存
2.数组的初始化
int age[10]={1,2,3,4,5,6,7,8,9,10}

汇编代码
还可以 int age[] = {1,2,3,4,5,6,7,8,9,10}
是一样的
3.数组的内存分配
那我们用char age[10]的话如何分配呢
根据汇编代码我们发现他分配了12个字节,按道理应该是10个,为什么是12个呢
因为有本机宽度这个概念,例如32位的电脑处理4字节起来更适应,所以他直接分配了3个四字节
4.数组的读写
用索引
如 age[0] = 1;
就是给数组第一个位置赋值0
声明数组的时候[]中不能为变量
但使用的时候可以使用变量
5.数组越界访问
如:
int arr[10];
arr[10] = 100;
对于普通程序员来说,要避免越界
但懂汇编的话,其实越界也是一种手段
如:
1 |
|
这个运行结果是是什么呢

这是为啥呢
来看汇编代码

就是因为越界导致ebp+4被修改为Fun函数的地址
所以导致ret跳转不是正常跳转到原本的地址而是跳转到了Fun函数的地址执行了Fun函数
十三、多维数组
1.多维数组的定义

2.二维数组的初始化

3.二维数组的内存分布
无论是多少维数组 底层内存分布都是连续的 和一维数组一样
所以在储存方面数组没区别
4.二维数组的读写

5.多维数组的存储与读写

1 | 3*4*3+3*3+2 |
十四、结构体
1.结构体的定义
什么是结构体,其实就是一个容器,我们自己定义的容器

这个容器中可以包含不同类型的数据
结构体不是变量,是类型
2.结构体的定义
3.变量的声明

4.结构体类型变量的读写

5.定义类型的同时,声明变量

十五、字节对齐
1.字节对齐的定义

2.sizeof的使用
sizeof可以获取宽度

3.pragma pack()的使用


这个函数取消强制对齐功能

十六、结构体数组
1.结构体数组初始化

2.结构体成员的使用

3.字符串成员的处理


这样太笨了
可以直接使用这个
4.结构体数组的内存结构

十七、指针类型
1.定义带”*”类型的变量

2.指针变量赋值

3.指针变量宽度
指针类型的变量宽度永远是4字节、无论类型是什么,无论有几个*

4.指针类型自加和自减
不带*类型的变量,++或–都是加一或者减一
带*类型的变量,++或–新增或减少的数量是去掉一个*后变量的宽度
如:char* a 自加的话就是加char的宽度就是1
char* *a自加的话就是加char*的宽度就是4
5.指针类型的加法运算
指针类型的变量可以加减一个整数,但不能乘或除
指针类型变量与其他整数相加或相减时:
指针类型变量+N = 指针类型变量 + N*(去掉一个*后类型的宽度)
例如:
1 | char* a; |
结果就是a加上char*去掉一个*后也就是char的宽度乘5答案也就是105
1 | short* b; |
这个就是加上short的宽度乘5答案就是100+10=110
6.指针类型变量的比较
指针类型变量的比较是无符号类型的比较
十八、&的使用
这个是取地址符
1.作用
&用来获取变量的地址,不能用于常数
2.&变量的类型
&变量的类型就是变量的类型加一个*
如:
char a;
那么&a就是char*
char* a;
那么&a就是char**
3.指针变量赋值

十九、取值运算符
1.”*”的几种用法
1.1乘法运算符
int x = 1;
int y = 2;
int z = x*y;
1.2定义新的类型
char x;
char* y;
1.3取值运算符
*+指针类型的运算符
作用就是把地址对应的数据取出来
2.*指针类型的类型
和&取地址相反
*指针类型的类型是指针类型减去一个*后的类型
如int* a;
那么*(a)就是int类型
int** a;
*a就是int*类型;
二十、数组的参数传递
1.基本类型参数传递

x的值是不变的

因为执行plus的时候传入的仅仅只是x的值而不是x的地址,所以函数改变不了x的值
2.数组作为参数


从汇编看得出来,数组传递的是地址
3.用指针操作数组

在汇编代码中可以看出,这两种形式是一样的
所以arr[i]===*(p+i)
二十一、字符串
1.字符串的表现形式
1.1第一种
1 | char str[6] = {'A','B','C','D','E','F'}; |

这是输出结果
因为printf会打印到知道读到0为止,所以会多几个
1.2第二种
1 | char str[] = "ABCDE"; |

正常打印,这样写会在后面加0

这种编译器会先将ABCDascii码存在ebp-8,然后再将E存在ebp-4里边
1.3第三种
1 | char* str = "ABCDE"; |
这种形式是将ABCDE写入常量内存空间中,不支持一般的方法修改

2.常用的字符串函数
2.1 int strlen(char* s)
返回值是字符串s的长度,不包括约束符/0
这个函数有个缺点 ,中英文混合的时候会将中文判定为两个字节
2.2 char* strcpy(char* dest,char* src)
复制字符串src到dest中。返回指针为dest的值
2.3 char* strcat(char* dest,char* src)
将字符串src添加到dest尾部,返回指针为dest的值
2.4 int strcmp(char* s1,char* s2);
一样返回0 不一样则非0
二十二、指针取值的两种方式
1.一级指针和多级指针

2.p[0]和*(p+0)
这两个在汇编是完全一样的

二十三、结构体指针
1.特征


2.如何用指针读写结构体
读取结构体:
1 | struct Point { |
二十四、指针数组与数组指针
1.指针数组的定义
1 | char arr[10]; |
2.指针数组的赋值
1 | char* a = "Hello"; |
3.结构体指针数组

4.分析代码
1 | int arr[] = {1,2,3,4,5,6,7,8,9,0}; |
有什么不一样吗
其实前两个是一样的,但后面的&arr不一样,他是数组指针 int(*)[10]
5.数组指针的定义

px就是指针名
6.数组指针的宽度与赋值

指针宽度都为4
赋值

7.数组指针的运算

8.数组指针的取值

*px[0]
9.数组指针的使用

输出结果是1和3
10.二维数组指针访问一维数组

在内存中其实二维数组和一维数组分布是一样的
因为数组长度为2
所以这个指针只能访问1到4,要访问5的话就应该px++;

二十五、调用约定
1.函数调用约定

2.常见的几种调用约定



汇编结果没有平衡堆栈

变成了内平栈
二十六、函数指针
1.函数指针类型变量的定义

2.函数指针变量赋值

3.使用函数指针变量

函数指针就是指向函数地址
4.通过函数指针绕过断点

其中0x77D5055C是MessageBox函数地址
这样的话调试者想要在MessageBox函数下断点的话就没用,因为我们虽然也是用了MEssageBox函数,但是是通过指针调用的
5.函数指针运算
函数指针无法运算,因为+ - 的话是变化去掉*后的宽度,也就是函数的宽度,函数的宽度无法判定
二十七、预处理
1.预处理定义

2.宏定义

2.1简单宏
例如

执行前就会把TRUE换成1
宏定义可以为任何东西 函数也行
2.2带参数的宏

编译结果:

宏定义也能够多行

2.3注意事项:

2.4函数和宏的区别
宏没有堆栈,它只会在你使用的地方粘贴一份
而函数使用的话会Push参数然后调用
所以宏的话会显得更加臃肿,会用很多重复代码
3.条件编译
1 |
|
这种,if后面是0所以就不会被编译
3.1作用
开发的时候
1 |
|
可以用来调试,当调试完成后,调试代码太多可以直接将DEBUG改为0就可以避免一个个去删除
4.常见的预处理指令

例子:

编译结果

例子:

5.包含文件
#include 后边跟头文件,就是将头文件的内容复制过来
头文件后缀是.h
例如我要调用A.CPP里的函数
我在A.H文件中申明了A函数
那么我在要调用A函数的CPP里边使用
#include “a.h”就行了
5.1包含文件的两种方式

5.2重复包含
当A.H和B.H都包含C.H
在Main.cpp中同时包含A.H和B.H的话
Main.CPP预编译就会有两个C.H
就会产生重复包含的错误
解决方法:

加条件编译,意思就是如果有人定义了,就不再定义了
当包含A.H的时候因为ZZZ没有定义就定义ZZZ然后申明Point结构
包含B.H的时候因为ZZZ被定义了就跳过
还有一种解决方法就是
不在头文件包含其他头文件,而是在cpp文件里边包含C.H
一般都是用第二种方法
那还有一个问题
就是我要其他文件能调用A.CPP的函数,就要在A.H中加
可是Myprint1又要Point也就是C.H中的结构怎么办呢,CPP文件中包含了C.H可以识别,但A.H又不让包含其他头文件该怎么办
接下来就要用到前置申明

直接在头文件中声明Point
但要切记,因为A.H中的Point不是C.H中的Point,我们要用C.H中的Point覆盖A.H中的Point,所以include应该先包含A.H再包含C.H