C++学习:1基础语法

Posted 想文艺一点的程序员

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++学习:1基础语法相关的知识,希望对你有一定的参考价值。

C++ 发展史

  • C++ 03 只是修改了一些BUG
  • C++ 11 :major
  • C++ 17 :major


一、基础语法 A

1、cin、cout

  • cin、cout 当中的 c 是 console 控制台的意思
  • endl:end line 结束一行,就是换行的意思。
  • cout:向控制态输出东西,箭头要指向cout
  • cin:想要从键盘当中读取东西,箭头要指向变量

注意:只要main函数 return,只要 main 结束,控制台就会结束,所以需要 getchar() 来进行阻塞


2、函数重载(Overload)


规则:

  • 函数名字相同
  • 参数个数不同、或者参数类型不同、或者顺序不同。(与参数名字无关、与函数返回值无关)

注意:

  • C语言不支持函数重载的,但是很多语言都支持,比如说java
  • 函数返回值不同,不能构成重载
  • 调用函数时,实参的隐式类型转换可能会产生二义性。


本质

  • 采用了 name mangling 或者叫做 name decoration 技术
  • C++ 编译器默认会对符号名(比如函数名)进行改编、修饰
  • 虽然表面上看起来都是一个函数名,但是编译器会将其修改,将参数信息融合进去。所以支持了函数重载


使用vs调试分析反汇编

  • 可以看出他们调用的并不是一个函数,(因为call 的地址都不一样
  • 但是还是无法窥探它的函数名


使用 IDA 来分析反汇编:(来查看它的本质函数名)

  • 第一步:安装好 IDA 软件之后,直接将生成的可执行文件 exe 拖进去

  • 关闭的时候

  • Debug模式: 很多调试信息, 生成的可执行文件比较臃肿

  • Release模式:去除调试信息,生成的可执行文件比较精简、高效

  • 但是在release 模式下,编译器会帮我们优化代码。 (这样我们就又看不到 func 的函数原名了

解决办法:让 release 不进行优化,不进行函数本体替换。

禁用之后,我们可以发现,它不进行替换了

  • 继续使用 IDA 工具来进行查看。

3、默认参数


  • C++允许函数设置默认参数,在调用时可以根据情况省略实参

  • 默认参数的赋值顺序只允许是从右到左

  • 如果函数同时有声明、实现,默认参数只能放在函数声明中

  • 默认参数的值可以是常量全局符号全局变量、函数名


应用举例:有一个参数经常固定不变


问题:**函数重载 **和 默认参数一起使用,可能会产生冲突、二义性(建议优先选择使用默认参数)


默认参数的本质

没有使用默认参数:

  • 传递参数使用 push 指令, push 4, push 2

如果有默认参数:

  • 仍然还是 push 两次,所以本质上来说还是传递了两次参数


4、extern “C”

  • 被extern "C"修饰的代码,会按照C语言的方式去编译
  • 一般用在 C、C++ 的混合开发

第一个作用:被extern "C"修饰的代码,会按照C语言的方式去编译

问题:如果函数同时有声明和实现出现,extern ”C“ 应该加到哪里呢?

答:我们应该将 extern ”C“ 放在函数的声明前面。


C 和 C++混合开发

  • 在使用 C++ 进行开发的时候,可能需要调用别人开发的第三方库,但是这个库是使用C语言来开发的。
  • 逻辑上这样调用没错,但是编译器报错了

分析:为什么会无法解析外部命令呢?

  • 我们在 C++ 里面调用了 sum 函数,那么编译器一定会认为这个 sum 函数是 C++ 编译的
  • 但是这个函数确实是使用 C 语言来写的,也是用 C 语言的方式来进行编译的。

解决:在 C++ 里面 告诉编译器 sum 函数是使用C语言编译,让他调用的时候也使用 C++ 的方式去调用


可以将我们的 extern ”C“放到 C 语言API 对应的头文件当中, 然后在 C++ 的文件当中添加这个头文件即可。

在C语言库的开发者角度:就应该考虑到,我们的库可能被 C 语言开发使用,也有可能被 C++ 开发来使用

  • C 语言开发,C语言是不认识 extern ”C“ 的,所以会报错。
  • 所以,如果添加了 extern ”C“ ,C 语言不认识,会报错。(不能使用C语言开发)
  • 如果没有添加了 extern ”C“ ,C ++会以C++的方式去解析,会报错。(不能使用C++开发)
  • 解决办法:使用条件编译


二、基础语法 B

5、内联函数 (inline function)

定义:使用inline修饰函数的声明或者实现,可以使其变成内联函数 。建议**函数声明 **和 函数实现都增加inline修饰

特点:

  • 编译器会将函数调用直接展开为函数体代码
  • 可以减少函数调用的开销 (调用函数会开辟新的栈空间,会有跳转指令,又开销)
  • 增大代码体积

注意:

  • 尽量不要内联超过10行代码的函数
  • 有些函数即使声明为inline,也不一定会被编译器内联,比如递归函数
  • inline 关键字仅仅只是建议编译器进行内联,但是编译器做不做不一定。

为什么要发明内联函数?

  • 因为每次调用函数,都会开辟栈空间,还有跳转指令。

  • 如果这个函数只调用一次,那么这点效率可以忽略。

  • 但是如果这个函数被调用上百次呢? 累计起来节省的时间就很重要了。

什么时候使用内联函数?

  • 代码体积比较小的
  • 频繁调用的函数。

窥探内敛函数的本质:

注意:

  • 在 Debug 模式下面,编译器默认是不会内联的。
  • 将其修改为 relese、并且禁止优化。

禁止优化之后,我们可以看出内联函数并没有函数调用。


内联函数与宏

宏定义的本质也是进行替换,所以内联函数和宏定义都可以提高程序的效率

内联函数比宏定义的好处:内联函数多了语法检测函数特性

  • 内联函数起码函数本身会进行语法检验

一种应用情况


表达式

  • 表达式可以被赋值

  • 表达式也是有返回值的


6、 const

  • const是常量的意思,被其修饰的变量不可修改

  • 如果修饰的是 类、结构体(的指针),其成员也不可以更改

  • const 修饰的变量,必须在赋值的时候就被赋值

使用 const 来修饰指针的时候,有两个情况:

  • 指针本身不能修改
  • 指针指向的变量不能修改
  • const 修饰的是其右边的内容

const 右边的两种情况:

  • *p1 :代表 p1 已经被解引用,所以指 p1 指向的变量。
  • p2 : 代表 p2 本身。


7、引用(Reference)

  • 在C语言中使用指针(Pointer)可以间接获取、修改某个变量的值。
  • 在C++中使用引用(Reference)可以起到跟指针类似的功能 。(功能并不是完全一样)

注意:

  • 引用相当于是变量的别名

  • 对引用做计算就是对引用所指向的变量做计算

  • 定义的时候就必须初始化,一旦指向了某个变量,就不可以再改变,“从一而终

    (这点和const修饰的变量类似,因为以后不能修改,所以在初始化的时候就应该给赋值)

  • 功能和 int * const p = &a; 类似。(指针本身不可以改变,但是指向的变量可以改变)

  • 可以利用引用初始化另一个引用,相当于某个变量的多个别名

  • 引用类型,必须和赋值的变量的类型相同。


引用的价值:比指针更加安全

  • 引用是很安全的,因为一开始就得指向一个变量,而且以后不能修改指向。
  • 指针就不一定了,可以发生改变,万一指向一个不能修改的区域,那么就可能发生不可预料的错误。

引用的价值:函数返回值可以被赋值 (值传递,地址传递)

  • 可以发现引用比指针使用起来更加简单

问题:引用不是 “从一而终” 吗? 下列情况怎么解释?

  • 每次调用一次函数,函数会被重新分配空间,形参都会重新定义


引用的本质

  • 引用的本质就是指针,只是编译器削弱了它的功能,所以引用就是弱化了的指针。

  • 引用被初始化之后,就 “从一而终”, 而指针可以一直改变。

  • 并且一个 引用的大小 和 指针的大小 相同

  • x86 架构是 32 位系统,一个指针为 4 个字节。x64 架构是 64 位系统,一个指针为 8 个字节

  • 借助结构体来查看引用大小(可以证明它的大小和指针一样大


直接通过汇编代码来查看引用的本质:

然后进行比较:可以看出引用的汇编指令和指针的汇编指令是一样的。


8、常引用

(1)各种数据结构的引用 (引用的补充)

(2)常引用的应用场景:输入型参数,只想让他使用这个值,不想让他让他更改值。

比如:我们传入一个 Person 参数,我们只想让 test 函数打印这个人的信息,而不想让他来修改这个人的信息

(3)常引用的特性:其他引用没有

  • 可以指向 常量、临时数据、表达式、函数返回值

  • 可以指向不同类型的数据:(语法糖)

指向不同类型的数据的时候会产生临时变量从而引用就会指向这个临时变量

  • 作形参的时候,可以接收 非const的形参 和 const 的形参

  • 可以构成函数重载


三、汇编语言

汇编语言的分类:(汇编语言和 CPU 的架构直接挂钩的)

  • 8086汇编(16bit)
  • x86汇编(32bit)
  • x64汇编(64bit):根据编译器的不同有两种书写格式:IntelAT&T
  • ARM汇编(嵌入式、移动设备)

注意:

  • 汇编语言不区分大小写
  • 在 windows 的编译器一般是 Intel 格式, 在Mac 和 Linux 平台的编译器一般是 AT&T 格式。

1、学习汇编指令的 2 大知识点:

  • 寄存器 (CPU 当中有三大部分:寄存器、运算器、控制器)
  • 汇编指令


AX、BX、CX、DX 这四个通用寄存器的发展:

  • 8086:16位cpu,叫做 AX、BX、CX、DX
  • x86 : 32位cpu,叫做 EAX、EBX、ECX、EDX
  • x64 : 64位菜谱,叫做 RAX、RBX、RCX、RDX

注意:x64的汇编是兼容 x86 的汇编

  • RAX 的 低4个字节是 EAX。
  • EAX 的 低2个字节是 AX。
  • AX 的低1个字节是 AL ,高一个字节是 AH。

寄存器既然是做信息存储的,那么一个寄存器的空间到底是多大呢?(和 CPU 架构有关)

  • R开头的寄存器是64bit的,占8字节 。(64位系统)
  • E开头的寄存器是32bit的,占4字节 。 (32位系统)

2、内联汇编

什么是内联汇编? 在 C++ 当中嵌入汇编代码

  • 可以看出 AL 就是 EAX 的低一个字节。


3、汇编指令

基础铺垫:

  • [ 地址值 ] :中括号[ ] 里面放的都是内存地址
  • word是2字节, dword是4字节(double word), qword是8字节(quad word)
  • 很多指令都是以逗号进行分割,所以我们先找逗号。

mov 指令

一个字节来存放3

char a= 3;
mov [a] , 3
mov  [ffff0001h] , 3  # 将 3 放到   ffff0001h 内存处

两个字节大小来存放 3,使用 ptr 来指定单位大小 (ptr 左侧就是存放大小

int a= 3;
mov dword ptr [a] , 3
mov dword ptr [ffff0001h] , 3  # 将 3 放到   ffff0001h 内存处

从指定内存开始,取出 4 个字节内容,放到 eax 当中。(一般内存向高地址增长)

mov eax, dowrd ptr [1128h] #1128h、1129h、1130h、1131、

  • ESP:栈指针寄存器(extended stack pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈的栈顶

  • EBP:基址指针寄存器(extended base pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈的底部

  • 可以看出定义变量的本质:将一个数值进行压栈


call 指令

格式: call 函数地址 ()

可以看出 test 函数的地址为 0x0331000 .(可以看出最后一条指令是 ret,可以进行调用返回)

func 的地址是 0x0331010 (可以看出最后一条指令是 ret,可以进行调用返回)


lea指令(指针)

  • lea dest, [ 地址值 ]。 (lea:load effect address)

  • 将地址值赋值给dest,类似于dest = 地址值

  • 出现于指针、引用

注意:因为 mov 指令不可以直接交换两个内存的值,所以需要寄存器进行中转


ret、add、sub、inc、dec、xor

ret :函数返回

xor op1, op2 :将op1和op2异或的结果赋值给op1,类似于 op1 = op1 ^ op2

add op1, op2 :类似于 op1 = op1 + op2

sub op1, op2 :类似于 op1 = op1 - op2

inc op :自增,类似于 op = op + 1

dec op :自减,类似于 op = op – 1


jmp

jmp 内存地址 :跳转到某个内存地址去执行代码

  • j开头的一般都是跳转,分为无条件跳转,有条件跳转。
  • 大多数是带条件的跳转,一般跟test、 cmp等指令配合使用。 (一般用于分支指令
  • 跳转指令是不自带 ret 的。


常见代码的汇编

int a = 10;	 **********************  mov         dword ptr [ebp-4],0Ah   (立即数——>内存, 不需要寄存器中转)
int b = 20;  **********************  mov         dword ptr [ebp-8],14h   (立即数——>内存, 不需要寄存器中转)

a = a + b;   **********************  mov         eax,dword ptr [ebp-4]  	(内存 ——> 寄存器, 需要寄存器中转)
      			   **********************  mov         add         eax,dword ptr [ebp-8]    (在寄存器当中运算)
       			   **********************  mov         mov         dword ptr [ebp-4],eax  		(寄存器 ——> 内存,需要寄存器进行中转)

注意:mov 指令是不支持内存与内存之间,直接进行值传递。只能先借用 寄存器来进行中转。


int a = 10;
int *p = &a;
int &ref = a;

注意:指针和引用的本质都一样,都是借助于 lea 指令


以上是关于C++学习:1基础语法的主要内容,如果未能解决你的问题,请参考以下文章

C++学习:1基础语法

算法基础| 二分图解及代码模板

C++ 解释器/控制台/片段编译器

极简教程 | OpenCV4 C++学习 必备基础语法知识

JSP 基础语法

C++基础学习笔记C++语法之引用