第一章 C++简介、重载、extern “C” 、默认参数
内容概要
- C++
- 语法
- 通过反汇编窥探底层
- Windows编程
- Windows API
- 就是Windows系统中提供的一些API(一些函数)
- 外挂程序要访问植物大战僵尸游戏的内存数据
- 因此就需要用到跨进程
- 外挂程序跟植物大战僵尸游戏是不同的2个进程
- 跨进程访问的话就需要用到Windows相关的API
- MFC
- MFC就是用来画界面的
- Windows API
- 项目实战
- X86汇编
- 软件破解
- 植物大战僵尸外挂
C++简介
- C++ 是由 Bjarne Stroustrup 于 1979 年在新泽西州美利山贝尔实验室开始设计开发的。C++ 进一步扩充和完善了 C 语言,最初命名为带类的C,后来在 1983 年更名为 C++。
- C++ 是一种静态类型的、编译式的、通用的、大小写敏感的、不规则的编程语言,支持过程化编程、面向对象编程和泛型编程。
- C++ 被认为是一种中级语言,它综合了高级语言和低级语言的特点。
- C++ 可运行于多种平台上,如 Windows、MAC 操作系统以及 UNIX 的各种版本
- C++ 是 C 的一个超集,事实上,任何合法的 C 程序都是合法的 C++ 程序。
- 注意:使用静态类型的编程语言是在编译时执行类型检查,而不是在运行时执行类型检查。
C++的应用领域
- 游戏(Cocos2d-X)、图像、多媒体、网络、嵌入式
- 数据库(Oracle、MySQL)、浏览器(Chrome)、搜索引擎(Google)
- 操作系统、驱动程序、编译器(GCC、LLVM)、编程语言(Swift)
- HPC(High Performance Computing,高性能计算)
- iOS开发(Runtime、AsyncDisplayKit)
- Android开发(NDK、fresco【匿名共享内存,Ashmem,Anonymous Shared Memory】)
- Java开发(JNI)
- 总结
- C++之所以应用范围如此广泛,得益于它的高效性、稳定性、跨平台性
- 虽然C++在很多大型应用中,无法施展拳脚;但在某些领域,如同巨人一般而且是不可或缺的顶梁柱
- 基本只要是用到C++的地方,都是高大上的地方
C++的组成
- 标准的 C++ 由三个重要部分组成:
- 核心语言: 提供了所有构件块,包括变量、数据类型和常量,等等。
- C++ 标准库: 提供了大量的函数,用于操作文件、字符串等。
- 标准模板库(STL): 提供了大量的方法,用于操作数据结构等。
有没有必要学习C++?
- C++可以说是当今很多流行语言(Java、Python等)的老祖宗,学习C++,相当于理解了流行语言的前世今生
- 多尝试几种不同的编程语言,能提供不同的编程思维视角,站在更高的维度去思考代码
- C++是一门在面向过程和面向对象方面都比较完善的语言,能让我们更接近真相(本质)
- C++程序员转什么领域都可以很快上手
- 如果你想做个普通的程序员,学好所熟悉的语言基本够用,如果你的理想还要更大一点,C++是进阶必备
- 修炼内功,掌握本质,提升逼格
- 程序员鄙视链:汇编 -> C -> C++ -> Java、C# -> PHP
- 既然C++执行效率这么高,为什么还要出现新的编程语言?所有编程的地方都用C++不就好了么?
- 每一门语言都自己的优缺点,都有自己擅长的领域
- 新的编程语言趋向于更加简洁、高效(开发效率)
- 为了适应不同的应用场景,新的编程语言就诞生了
- 统计分析:R
- 网站开发:HTML、CSS、JavaScript
- 访问数据库:SQL
- 创造一门新的编程语言的成本,有时候会比改进一门编程语言的成本更低
开发环境简介
- C++语法阶段
- Mac:Xcode
- Windows:Visual Studio Community 2017
- 项目实战阶段
- Windows:Visual Studio Community 2017
- VS下载地址
- https://visualstudio.microsoft.com
常识
- 语法须知
- C++的源文件扩展名是:cpp(c plus plus的简称)
- C++程序的入口是main函数(函数即方法,一个意思)
- 注意:在Java中,先有类,才能有方法,方法在类中。即main函数必须放在一个类中。Java可以有多个main函数入口,但是运行时要选择哪个作为入口。
- C++完全兼容C语言的语法,很久以前,C++叫做C with classes
- C++的发展史
- C98 开始使用
- C03、C11、C14、C17
C++快速入门
-
main函数
int main(){ return 0; }
- 每个C++程序都包含一个或多个函数,而且必须有一个命名为main
- 操作系统通过调用main函数来执行程序,main函数则执行组成自己的语句并返回一个值给操作系统
- 操作系统通过main函数的返回值来确定程序是否成功执行完毕。返回0值表示程序成功执行完毕。
- 每个C++程序必须含有main函数,且main函数是唯一被操作系统显示调用的函数。
- 定义main函数和定义其他函数一样。定义函数必须指定4个元素:返回类型、函数名、圆括号内的形参表(形参可能为空)和函数体。main函数的形参个数是有限的。
- main函数的返回值必须是int类型。int类型是内置类型,即该类型是由C++语言定义的。
- 在大多数操作系统中,main函数的返回值是一个状态指示器,返回值0往往表示main函数成功执行完毕。任何其他非零的返回值都有操作系统定义的含义。通常非零返回值表明有错误出现,每一种操作系统都有自己的方式告诉用户main函数返回什么内容。
- 编译与执行程序
- 程序编写完需要进行编译,如何编译,与操作系统和编译器有关。
- 许多基于PC的编译器都在集成开发环境(IDE)中运行。IDE将编译器与相关的构建和分析工具绑定在一起。
- 大多数编译器,包括那些来自IDE的,都提供了命令行界面。
- 程序源文件的命名规范
- 不管我们使用命令行工具还是IDE,大多数编译器希望待编译的程序保存在文件中。
- 程序文件称作源文件。
- 大多数系统中,源文件的名字由文件名和文件后缀两部分组成。
- 依据惯例,文件后缀表明该文件是程序。文件后缀通常也表明程序是用什么序言编写的,以及选择哪一种编译器运行。
- C++程序文件的后缀与所运行的具体编译器有关,常见的有:
- prog1.cc(Windows命令行)
- prog1.cxx
- prog1.cpp(VS)
- prog1.cp
- prog1.c
- 用命令行运行编译器
- 在控制台窗口输入 :
cc prog1.cc
- 在windows系统生成
a.exe
可执行文件。在NUIX生成为a.out
可执行文件 - 运行可执行文件
- windows :
a.exe
- unix:
./a.out
- windows :
- 在控制台窗口输入 :
- 程序的输入输出
-
带输入输出的简单程序如下:
#include<iostream> int main(){ /* cout:是标准输出对象 std:名称空间,std就相当于文件夹,cout就相当于文件; ::是作用域操作符 endl: 换行,是一个特殊值,叫操纵符,把这个值输入到cout中就会换行。还有一个作用就是刷新控制台。 可以用 "\n" 代替以上代码里的 endl。 */ //输出 std::cout << "input a number:" << std::endl; int iv,iv2; //输入 std::cin >> iv >> iv2; //输出 std::cout << iv <<","<< iv2 << std::endl; //system("pause"); return 0; }
- C++并没有直接定义进行输入输出(IO)的任何语句,这种功能是由标准库提供的。
- IO库提供了大量的设施。最长用的就是处理格式化输入和输出的iostream库。
- iostream库的基础是两种命名为istream和ostream的类型,分别表示输入流和输出流。
- 流是指要从某种IO设备上读入或者写出的字符序列。
- 也就是说一定是输入、输出设备,比如电脑的:键盘、鼠标、显示器,移动手机的:摄像头、显示屏等
- 一定捕获、输出数据
- 随着时间的流逝,捕获、输出不同的数据。
- 术语流试图说明字符是随着时间顺序生成或消耗的。
- 标准输入输出对象
- 标准库定义了4个IO对象。
- 处理输入时使用命名为cin(读作see-in)的istream类型对象。这个对象也称为标准输入
- 处理输出时使用命名为cout(读作see-out)的ostream类型对象,这个对象也称为标准输出
- 标准库还定义了另外2个ostream对象,分别命名为cerr和clog(分别读作“see-err”和“see-log”)。
- cerr对象又叫做标准错误,通常用来输出警告和错误信息给程序的使用者。
- clog对象用于产生程序执行的一般信息。
- 一般情况下,系统将这些对象(输入、输出)与执行程序的窗口连接起来。
- 注意理解这句话,就是所这些输入、输出对象会自动调用程序窗口
- 从cin读入时,数据从执行程序的窗口读入
- 写到cout、cerr、clog时,输出写至同一窗口。
-
写入到流
std::cout << "input a number:" << std::endl;
- 这是一个表达式,C++中一个表达式由一个或几个操作数和通常是一个操作符组成
- 该语句的表达式使用输出操作符(«),在标准输出上输出提示语。
- 这个语句用了2次输出操作符。每个输出操作符实例都接受2个操作数:左操作数必须是ostream对象; 右操作数是要输出的值。
- 操作符将其右操作数写到作为其左操作数的ostream对象。
- 意思就是操作符(«)将它右边的值(“要输出的字符串”),写入它左边的对象(std::cout)中。
- C++中,每个表达式都会产生一个结果,通常是将操作符作用到其操作数所产生的值。
- 当操作符是输出操作符时,结果是左操作数的值。也就是说输出操作返回的值是输出流本身。即输出操作符返回的是其左操作数
-
因此,我们可以将输出请求连接在一起,上面的输出语句等价于下面:
(std::cout << "input a number:") << std::endl;
std::cout << "input a number:"
这句返回值是std::cout
-
因此等价于:
(std::cout << "input a number:") ; std::cout << std::endl
- endl 是一个特殊值,称为操纵符。将他写入输出流时,具有输出换行的效果,并且刷新与设备相关联的缓冲区。通过刷新缓冲区,用户可以立即看到写入到流中的输出。
- 使用标准库中的名字
- 注意我们发现程序中使用的是
std::cout
/std::endl
,而不是cout
/endl
.- cout:是标准输出对象
- std:名称空间,std就相当于文件夹,cout就相当于文件;
- :: :是作用域操作符
- 前缀
std::
表明cout和endl是定义在命名空间std中的。 - 使用命名空间,程序员可以避免由于无意中使用了与库中所定义名字相同而引发冲突。
- 因为标准库定义的名字是定义在命名空间中,所以我们可以使用相同的名字。
- 标准库使用命名空间的副作用是,当我们使用标准库中的名字时,必须显式的表达出使用的是命名空间std下的名字。
std :: cout
的写法是使用了作用域操作符 ::,表示使用的是定义在命名空间std中的cout。
- 注意我们发现程序中使用的是
-
输入流
std::cin >> iv >> iv2;
- 输入操作符(»)行为与输出操作符相似。
- 接受一个istream对象作为其左操作数,接受一个对象作为其右操作数,他从istream操作数读取数据并保存到右操作数中。
-
与输出操作符一样,输入操作符返回值为其左操作数
std::cin >> iv ; std::cin >> iv2 ;
-
函数重载(Overload)
- 规则
- 函数名相同
- 参数个数不同、参数类型不同、参数顺序不同
- 注意
- 返回值类型与函数重载无关
- 调用函数时,实参的隐式类型转换可能会产生二义性
- C语言不支持函数重载
- 本质
- 采用了name mangling或者叫name decoration技术
- C++编译器默认会对符号名(变量名、函数名等)进行改编、修饰,有些地方翻译为“命名倾轧”
- 重载时会生成多个不同的函数名,不同编译器(MSVC、g++)有不同的生成规则
- 通过IDA打开【VS_Release_禁止优化】可以看到
- 百度搜索ida然后下载安装即可
- 将当前的项目设置为release模式(因为debug模式会产生很多调试代码)
- 然后右击项目->属性->选项->c/c++->优化->将优化选择为”已禁用”即可,然后编译运行
- 找到编译后的.exe文件,拖入到ida中即可
-
在ida的中的函数名的列表中既可以看见(下面4个函数名都生成了不同的名字,不同的编译器生成名字的规则不一样)
#include<iostream> using namespace std; // display_v void display() { cout << "display() " << endl; } // display_i void display(int a) { cout << "display(int a) " << a << endl; } // display_l void display(long a) { cout << "display(long a) " << a << endl; } // display_d void display(double a) { cout << "display(double a) " << a << endl; } int main() { display(); display(10); display(10L); display(10.0); getchar(); return 0; }
- 采用了name mangling或者叫name decoration技术
-
代码举例:
-
函数重载
int sum(int a, int b) { return a + b; } int sum(int a, int b,int c) { return a + b + c; } int sum(double a, int b) { return a + b; } int sum(int a, double b) { return a + b; } //调用 cout << sum(3, 5) << sum(2,3,4)<< sum(10.1,3)<<sum(3,12.9) << endl;
-
返回值类型与函数重载无关
//这两个函数就不行 int test(int a, int b) { return a + b; } double test(int a, int b) { return a + b; } //因为这样调用函数时就会产生二义性,编译器不知道调用哪一个
-
调用函数时,实参的隐式类型转换可能会产生二义性
//调用函数时,实参的隐式类型转换可能会产生二义性 void display(long a) { cout << a << endl; } void display(double a) { cout << a << endl; } //这么调用就有问题了,12既可以隐试转换为long,也可以转换为double ,就不知道调用那个一个函数了,产生了二义性。 //display(12);
-
extern “C”
- 被extern “C”修饰的代码会按照C语言的方式去编译
- c语言与C++的编译方式是不太一样的
- 默认情况下在.cpp中编写的代码会按照C++的方式编译。
- 但是有时会希望写在C++中的一些代码按照C的方式编译
//这两个函数 前面都加上extern "C",编译就会报错,因为这两个函数都会按照C语言的方式去编译,C语言不允许函数重载。 //写法1: extern "C" void func() { cout << "func()" << endl; } extern "C" void func(int a) { cout << "func(int a)" << a << endl; } //写法2: extern "C" { void func() { cout << "func()" << endl; } void func(int a) { cout << "func(int a)" << a << endl; } }
-
如果函数同时有声明和实现,要让函数声明被extern “C”修饰,函数实现可以不修饰
/* //方法1: extern "C" void func(); extern "C" void func(int a); */ //方法2: extern "C" { void func(); void func(int a); } int main() { func(); func(10); cout << "hello world !" << endl; getchar(); return 0; } //实现 void func() { cout << "func()" << endl; } void func(int a) { cout << "func(int a)" << a << endl; }
- 由于C、C++编译规则的不同,在C、C++混合开发时,可能会经常出现以下操作
- 混合场景:
- 有一个sum.h 和sum.c,然后在main.cpp中导入sum.h,然后调用sum函数,则报错
- 报错原因如下:
- C++的编译方式跟C的编译方式不一样
- 函数实现在.c中实现的,编译器编译时就是按照C语言的方式去编译,C文件中所有函数的实现编译成相应的函数名。比如(_sum)
- 然而在cpp中导入sum.h头文件,编译器会默认将sum.h头文件中所有声明的函数按照C++的方式编译,因此编译后的函数名(比如:sum_int),跟.c文件中函数实现按照C方式编译的函数名不一样,然后在.cpp中调用函数时,因此找不到。
- 解决办法
- 方法1:C++在调用C语言API时,需要使用extern “C”修饰C语言的函数声明
- 即:sum.h放在extern “C”中导入
- 这么写,导入的c的头文件都会按照c的方式去编译,因此调用函数的时候,也会按照c编译的函数名去找,就可以找到
//入口 #include<iostream> using namespace std; //导入c的API extern "C" { #include"sum.h" } int main() { cout << sum(10, 20) << endl; getchar(); return 0; } //sum.h 文件 #ifndef __SUM_H #define __SUM_H int sum(int a, int b); #endif // !__SUM_H //sum.c 文件 #include"sum.h" // _sum,c语言的方式编译后的函数名。 int sum(int a, int b) { return a + b; }
- 方法2:有时也会在编写C语言代码中直接使用extern “C” ,这样就可以直接被C++调用
- 在sum.h文件中做处理,即可以被C语言调用,也可以被C++调用
//sum.h 文件 #ifndef __SUM_H #define __SUM_H //__cplusplus:宏,C++编译器自己定义的,当编译环境是C++环境时,编译器就会定义这个宏 //如果为c++编译环境 #ifdef __cplusplus extern "C" { #endif // __cplusplus int sum(int a, int b); //如果为c++编译环境 #ifdef __cplusplus } #endif // __cplusplus #endif // !__SUM_H //入口 #include<iostream> using namespace std; //导入c的API #include"sum.h" int main() { cout << sum(10, 20) << endl; getchar(); return 0; }
- 方法1:C++在调用C语言API时,需要使用extern “C”修饰C语言的函数声明
- 混合场景:
默认参数
- C++允许函数设置默认参数,在调用时可以根据情况省略实参。规则如下:
-
默认参数只能按照右到左的顺序
//设置参数的默认值:a 默认为10 void func(int a=10) { cout << "func(int a)" << a << endl; } //调用方式 func();//或者func(40); //默认参数必须从右到左边 //必须b先有默认值,a才能有 //因为函数调用是从左边开始传参 //这个错误 //void func(int a = 20, int b) { // //} //func(20); 此时20传递给的是a,那么b就没有值,所以默认值必须从右至左 //正确 void func(int a, int b=10) { }
-
如果函数同时有声明、实现,默认参数只能放在函数声明中
//默认值只能设置在声明中 void func(int a = 20, int b = 10, int c = 30); void func(int a, int b , int c ) { cout << "a==" << a << endl; cout << "b==" << b << endl; cout << "c==" << c << endl; }
-
默认参数的值可以是常量、全局符号(全局变量、函数名)
//默认参数为全局变量 int age = 90; void func(int a = 20, int b = 10, int c = age) { cout << "a==" << a << endl; cout << "b==" << b << endl; cout << "c==" << c << endl; } //默认参数为函数名,函数名就是函数的入口地址 //函数指针可以指向一个函数 void test() { cout << "test()" << endl; } void display(int a, void (*func)() = test) { cout << "a is " << a << endl; cout << "func is " << func << endl; func(); } //调用 func(); display();
-
-
函数重载、默认参数可能会产生冲突、二义性(建议优先选择使用默认参数)
void display() { cout << "display" << endl; } void display(int a = 10, int b = 20) { cout << "a is " << a << endl; cout << "b is " << b << endl; } //调用 display();//这么掉一共就会有二义性了