三、基础语法1(30小时精通C++和外挂实战)

三、基础语法1(30小时精通C++和外挂实战)

1,开发环境的搭建

前面三节课必须看

课程一共15天,讲完C++语法就会讲外挂,破解最后两天讲解

Exe为windows的可执行文件


这是空项目里面什么也没有,源文件放的是我们源代码文件.cpp,右击源文件添加c++文件

Main函数,C++一启动就从main函数这个入口进入

VS很多设置比较反人类

注释快捷键

https://blog.csdn.net/Niteip/article/details/14697677

看《Ogre3d beginner Guide》时其实好多东西我没写blog,现在又想干脆把工程发一下,当个保存

一看工程,哇,500多M,好吧研究哪些不需要。

ipch的文件夹,和一个与工程同名的.sdf文件,而且ipch下面的文件和.sdf文件都很大,这些文件是Visual Studio用来保存预编译的头文件和Intellisense用的,删除这些文件对于工程的开发完全没有影响。那如果我既想使用预编译的头文件和Intellisense,又不想看到这些无聊的文件该怎么办呢?

工具->选项->文本编辑器->C/C++ ->高级->回退位置 那里,两个都设成true

如果你设置了回退位置,那么IPCH等就会到那里去,否则会在系统TEMP里,有个VC++文件夹下。



上面的常用快捷键要牢记

当我们输入一个字母要另其自动补全,默认框内使用tab。

我们将其折叠

在这里方法和函数是一个概念

//方法=函数

我们怎么打开之前创建的程序,.sln为项目文件,可双击。

或者打开开发工具,打开等

后期开发外挂VS需要安装一些组件

打开工具-获取工具和功能,不要随便勾选,只勾选使用C++的桌面开发,将来开发的外挂就是跑在windows系统上的桌面程序,所以勾选此

下面一张图,只要保证右边的程序勾选即可

当打开旧版的程序需要重新生成解决方案

我们新建出来的C++文件希望以utf-8编码(中文编码)形式存储文件内容

但VS默认编码不是utf-8,如果用VS开发的代码,直接使用别的工具打开的话可能发现中文是乱码的

中文编码UTF-8,GB2312,GB18030

如何更改编码格式,工具扩展和更新,联机,forceutf8(with FOM),安装完后新建的C++文件默认就是utf8编码了

2,cin和cout

入口就是程序一启动会执行的函数,main函数入口,其他编程语言是必须先有类再有函数,如java语言必须先有类,再有方法(函数),而C++直接就能定义函数

C++在此的基础上有了面向对象的东西(C with classes)

#include <iostream>		//C++头文件不用写.h
using namespace std;		
//上面两句暂时认为固定写法,写完后,就能使用cout了

int main(){
	cout << "hello world!" << endl;
	
	cout << "hello";		//输出hello
	//cout << "\n"; 		//此处为换行
	cout << endl;			//endline结束一行,也可以认为是换行
	cout << "world";		//输出world

//面向对象、运算符(操作符)重载
	//上面三句等价于下面一句
	cout << "hello" << endl << "world";


	cout << "please type a number:" << endl;
	int age;
	cin >> age;//cin从键盘接收东西,并赋值给age这个变量(我们输入一个数字敲回车,就将数字赋值给age)

	cout << "age is" << age << endl;

getchar();//如果一个getchar,这个getchar也会识别前面输入数字的回车,此函数执行完成后执行下一步,页面会一闪而过此时需要两个getchar
	getchar();//此函数是等待键盘输入的意思(如果敲回车,就会读取键盘输入),键盘不输入任何东西就一直等待,就不会退出
	return 0;
}

下面这个程序执行,首先打印,打印后就return 0 结束了,所有窗口一闪而过程序退出了,

注意:只要main函数退出就意味着程序结束

那怎么证明刚刚打印存在呢?我们右击项目,在文件资源管理器中打开资文件,打开debug文件,里面有个exe文件,我们下面写的代码就转换成了exe可执行文件

int main(){
	cout << "hello world!" << endl;

	return 0;
}

3、4,函数重载

两个函数的函数名完全一样,但是后面的参数的个数,参数的类型或者参数的顺序不同称为参数重载

c语言不支持函数重载
C++支持函数重载(很多编程语言都支持如java)

当编译时,尽管源文件中有多个C++文件,一些C++文件中午main函数,当编译时,在这些文件中找那个文件中有main函数,然后执行,多个文件都编译,找他们中的main函数再执行

C++为什么存在重载,编译器会将三个函数名改编,最终形成新的函数名(新函数名包含参数的信息名),这些函数就能同时存在,可以调用这些函数;C语言没有这种技术namemangling,不支持重载,若相同编译后的名字也相同,会冲突

//display_int(编译后的不同名字)
void display(int a){
	cout << "display(int)" << a << endl;
}
//display_long 
void display(long a){
	cout << "display(long )" << a << endl;
}
//display_double 
void display(double a){
	cout << "display(double)" << a << endl;
}

不同的编译器有不同的编译规则(编译后的命名也不相同)

那么我们怎么证明编译后的函数名是不一样的呢?

反汇编

如何做,先按F9打断点,此时就不能通过ctr+F5(直接执行程序,不能调试,就算打了断点也不理会直接执行),按F5此为调试,一旦进入断点模式,就可以右击转到反汇编(只有在调试状态才能转到反汇编,快捷键ALT+G)

在调试模式下

 57: 	dispaly();		//此为C++代码
001A5ADE E8 D2 B9 FF FF       call        dispaly (01A14B5h)  //加粗为汇编代码
//此句代码在内存中只占5个字节

    58: 	display(10);			//此为C++代码,转完汇编后是下面三句加深的代码
001A5AE3 6A 0A                push        0Ah  
001A5AE5 E8 DA B9 FF FF       call        display (01A14C4h)  	//调用
001A5AEA 83 C4 04             add         esp,4  
    59: 	display(10L);//long类型
001A5AED 6A 0A                push        0Ah  
001A5AEF E8 D5 B9 FF FF       call        display (01A14C9h)  
001A5AF4 83 C4 04             add         esp,4  
    60: 	display(10.5);//double类型
001A5AF7 83 EC 08             sub         esp,8  
    60: 	display(10.5);//double类型
001A5AFA F2 0F 10 05 58 CD 1A 00 movsd       xmm0,mmword ptr ds:[1ACD58h]  
001A5B02 F2 0F 11 04 24       movsd       mmword ptr [esp],xmm0  
001A5B07 E8 B3 B9 FF FF       call        display (01A14BFh)  
001A5B0C 83 C4 08             add         esp,8  
    61: 
    62: 	getchar();
001A5B0F 8B F4                mov         esi,esp  
001A5B11 FF 15 D0 01 1B 00    call        dword ptr ds:[1B01D0h]  
001A5B17 3B F4                cmp         esi,esp  
001A5B19 E8 16 B8 FF FF       call        __RTC_CheckEsp (01A1334h)  
    63: 	return 0;
001A5B1E 33 C0                xor         eax,eax  

那么左侧第二列的是什么,为机器码(采用16进制形式显示0和1)

1010 1010 1111 0001 1010 1011  ---转成16进制为-- AA F1 AB	(此处占3个字节)
两个16进制位代表8个二进制位,两个16进制也就是1个字节
001A5ADE E8 D2 B9 FF FF       call        dispaly (01A14B5h)		//此句代码在内存中只占5个字节,当然想要读懂意思看右边汇编
    58: 	display(10);			//此句代码在内存中只占10个字节
001A5AE3 6A 0A                push        0Ah  
001A5AE5 E8 DA B9 FF FF       call        display (01A14C4h)  	//调用
001A5AEA 83 C4 04             add         esp,4 

机器码与汇编是1对1的关系,有了机器码就能得到汇编,反之亦然

我们平时看机器码比较困难,就看右边的汇编

最左侧的是001A5B1E是内存地址,程序一启动,代码就放在内存中,每一句代码就有字节内存地址,要执行某一句代码就要找到代码字节地址从这个字节地址开始执行。每一句机器码都有自己的内存地址

平时写的代码按顺序向下执行的,机器码也是如此,机器指令的内存地址是连续的,机器码是连续存储的一个字节都不差,挨的紧紧密密,不会留空隙,空内存

我们平时编写代码如java之类在内存中都是机器码E8 D2 B9 FF FF 的二进制,右侧的汇编我们能知道其做了什么

dispaly (01A14B5h)  
display (01A14C4h)
display (01A14C9h)  

好像上面调用的三个都是display,我们的结论没有错,真实是编译后的每个函数名不同,只是VS这个开发工具为了方便我们开发者看东西与源代码对的上,展示的是display,但还是有所不同(01A14C9h) 不同,此为函数内存地址不一样,函数内存地址不一样最终执行代码也是不一样的

我们可以点击调试中的逐语句F11,可以一条一条执行汇编语言

从函数地址我们可以看到他们确实是不同函数,但是我们还是无法看到它将我们函数名改掉了,怎么办,借助工具IDA PRO 可以进行一些逆向,对我们最终生成的可执行文件进行逆向操作

我们现在ctr+F5启动此程序会生成一个可执行文件exe,在debug中,我们刚刚编写的这些C++代码最终变成0和1都放在debug中的exe中了,但是当我们双击exe时,exe又会将0和1载进执行代码

刚刚我们按F5时能看到机器码,机器码是存放在exe文件内的,只是双击时,会将机器码载进内存

void dispaly(){
	cout << "display()" << endl;
}
//display_int
void display(int a){
	cout << "display(int)" << a << endl;
}
//display_long 
void display(long a){
	cout << "display(long )" << a << endl;
}
//display_double 
void display(double a){
	cout << "display(double)" << a << endl;
}

这些函数都在exe文件中,我们能否窥探一下这个exe,看看这些函数的真实名字,使用IDA

5,使用IDA分析exe

我们现在ctr+F5启动此程序会生成一个可执行文件exe,在debug中,我们刚刚编写的这些C++代码最终变成0和1都放在debug中的exe中了,但是当我们双击exe时,exe又会将0和1载进执行代码

刚刚我们按F5时能看到机器码,机器码是存放在exe文件内的,只是双击时,会将机器码载进内存

void dispaly(){
	cout << "display()" << endl;
}
//display_int
void display(int a){
	cout << "display(int)" << a << endl;
}
//display_long 
void display(long a){
	cout << "display(long )" << a << endl;
}
//display_double 
void display(double a){
	cout << "display(double)" << a << endl;
}

这些函数都在exe文件中,我们能否窥探一下这个exe,看看这些函数的真实名字,使用IDA

IDA的头像是个女的,据说是世界上第一个女程序员,我们点击new,点击取消,将刚刚的exe拖进去,它能识别出这是个PE文件(potable excutable),像我们windows的可执行文件都是PE格式的文件,此时我们点击OK

我们发现左侧有个function window,在此会展示出我们所有函数的名字,项目中所有函数名称都会展示出来,但是并没有找到想要的display函数开头的,有很多sub这些都是debug模式下生成的东西

我们的项目中有两个模式debug和release。

如果在debug模式下启动此程序会有很多调试信息,生成的可执行文件比较臃肿

而release模式运行此程序会去除调试信息,生成的可执行文件比较精简高效,速度比较快

刚刚我们是用debug模式生成的exe,有很多调试信息就看不到我们display,我们选择release模式下再运行一下,一般我们要发布我们的项目,我们肯定选择release模式,我们的项目想要上线不能太多,要高效,而且项目发布根本不需要调试信息和断点信息

我们在VS选择release模式运行一下,然后选择项目右击打开文件夹,打开release下的exe

在IDA会在debug下生成ID0\ID1等文件,所以点击IDAfile点击CLOSE时选择不保存(don't pack database)、回收垃圾(collect)、不保存数据库(don't save),最后OK,此时debug中由IDA生成的文件就清楚了

我们将release文件夹下的exe拖入,点击OK,此时有些不同了,最起码能看到main了,如果是release模式的话,我们的main函数进行了优化,在右面的框中,看到直接打印display(),display(long)等等,在此模式下优化保证代码高效如果调用display发现display无非就是打印,默认调用display函数,会给display开辟一个栈空间给函数,函数执行完里面这个代码后,再回收这个栈空间,这四个函数都有开辟四次栈空间回收四次栈空间,编译器会觉的重复,就直接main函数执行下面四句函数,不再调用

int main(){
//dispaly();
	//display(10);
	//display(10L);//long类型
	//display(10.5);//double类型
上面四句全部替换为下面四句,不再调用
cout << "display()" << endl;
cout << "display(int)" << a << endl;
cout << "display(long )" << a << endl;
cout << "display(double)" << a << endl;

	getchar();
	return 0;
}

最终执行效果是一样的,上面直接执行四句的效率最高,这就是优化,所以在左边看不到display,被优化掉了,在release模式下,打断点,调试,右击反汇编,可以看到是有优化的,没有调用,直接打印如下

  57: 	dispaly();
00C812A6 68 30 1A C8 00       push        0C81A30h  
00C812AB 51                   push        ecx  
00C812AC 8B 0D 5C 30 C8 00    mov         ecx,dword ptr ds:[0C8305Ch]  
00C812B2 BA BC 31 C8 00       mov         edx,0C831BCh  
00C812B7 E8 34 05 00 00       call        std::operator<<<std::char_traits<char> > (0C817F0h)  
00C812BC 83 C4 04             add         esp,4  
00C812BF 8B C8                mov         ecx,eax  
00C812C1 FF 15 40 30 C8 00    call        dword ptr ds:[0C83040h]  
    58: 	display(10);
00C812C7 8B 0D 5C 30 C8 00    mov         ecx,dword ptr ds:[0C8305Ch]  
00C812CD BA C8 31 C8 00       mov         edx,0C831C8h  
00C812D2 68 30 1A C8 00       push        0C81A30h  
00C812D7 6A 0A                push        0Ah  
00C812D9 E8 12 05 00 00       call        std::operator<<<std::char_traits<char> > (0C817F0h)  
00C812DE 8B C8                mov         ecx,eax  
00C812E0 FF 15 64 30 C8 00    call        dword ptr ds:[0C83064h]  
00C812E6 8B C8                mov         ecx,eax  
00C812E8 FF 15 40 30 C8 00    call        dword ptr ds:[0C83040h]  

如果是debug模式

 57: 	dispaly();		//此为C++代码
001A5ADE E8 D2 B9 FF FF       call        dispaly (01A14B5h)  //调用display

    58: 	display(10);			//此为C++代码,转完汇编后是下面三句加深的代码
001A5AE3 6A 0A                push        0Ah  					//传参10
001A5AE5 E8 DA B9 FF FF       call        display (01A14C4h)  	//调用
001A5AEA 83 C4 04             add         esp,4  

所以release模式是编译器帮忙优化代码,它觉得能优化就优化,能提速就提速,但还是没有证明刚刚四个函数最终函数名不一样,此时有个坑,但debug模式看不到,左边会生成一大堆debug模式下的函数名,找不到我们的函数,但是release确实左边少掉了很多东西,看到main函数,有些优化掉了,怎么办

我们可以选择release模式,禁止优化即可,右击项目选择属性,c++有个优化,禁用掉。此时在release模式下再F5开始执行不调试,此时右击反汇编(Alt+g),可以看到有call不是直接打印了

此时将生成的release的exe没有优化过的,也就是存在上面四个函数的,看看函数的名字,此时IDA打开的是这个文件,路径一样需要重新加载,不用关掉,点击file下的路径0,此时看main函数,点击左方main,右侧代码有calldisplay

不同的编译器有不同的行为,如果是G++编译器编译生成的函数名会是令一种名称

最终生成什么函数名不影响我们开发,函数名无论多么古怪,都不用我们管,最终都会调用,而且最终汇编是那个名字,还是要根据内存地址去找,什么名字不重要

我们通过这些看到了本质,在网上搜的只有结论,我们不知道对不对,也不知道怎么验证,但现在可以验证了,通过汇编看本质

6.1,默认参数


今天的最后一个知识点,默认参数

#include <iostream>
using namespace std;

//下面定义括号中的是有默认参数的
int sum(int v1=5, int v2=6){
	return v1 + v2;
}

int main(){

	cout << sum() << endl;//11		这个不传参也正确,里面的默认参数相加
	cout << sum(10) << endl;//16	这个传了第一个参数10将第一个默认参数5覆盖掉,与第二个默认参数6相加,结果16
	cout << sum(10, 20) << endl;//30

	getchar();
	return 0;
}

注意默认参数必须从右到左,下面的代码是错误的

int sum(int v1=5, int v2){
	return v1 + v2;
}

int main(){
	cout << sum() << endl;//此处无论上面还是int sum(int v1, int v2=6)都报错,必须传递一个参数覆盖第一个值	
cout << sum(10) << endl;//上面第二个默认值没有,此时传的参数10只覆盖了第一个,第二个必须传参才行此时报错,可将其改为int sum(int v1, int v2=6)
	getchar();
	return 0;
}

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

很多时候main函数写在最前面,而其他函数写main函数在后面,但有些编译器函数放在main函数后面会不认识main函数中的函数的,从上往下走,这时需要将其进行提前的声明

#include <iostream>
using namespace std;

int sum(int v1 = 5, int v2 = 6);		//此处为函数声明
int main(){

	cout << sum() << endl;//11
	cout << sum(10) << endl;//16
	cout << sum(10, 20) << endl;//30

	getchar();
	return 0;
}

int sum(int v1, int v2){		//此处为函数实现
	return v1 + v2;
}

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

全局变量,写在函数外面的为全局变量

int age = 20;					//全局变量age
int sum(int v1 = 5, int v2 = age);

int main(){

	int age = 20;				//局部变量

	cout << sum() << endl;//11
	cout << sum(10) << endl;//16
	cout << sum(10, 20) << endl;//30

	getchar();
	return 0;
}

int sum(int v1, int v2){
	return v1 + v2;
}

那什么是函数名呢,回顾一下知识点,指针

void test(){
	cout << "test()" << endl;
}
int main(){
	//test();//平时调用直接这样
	//我们也可以通过下面这种方式,带*的p是指针,指向了test这个函数,将函数名赋值给P说明P指向这个函数,接着我们可以直接p();调用这个test函数
	void(*p)() = test;//void代表P指向的函数无返回值,右边()代表指向的函数是没有参数的
	p();
	getchar();
	return 0;
}


void test(int a){
	cout << "test(int)" << endl;
}
int main(){
	
	void(*p)(int) = test;//void代表P指向的函数无返回值,右边(int)代表指向的函数需要传递int类型的参数的
	p(10);			//将10传递给了test
	getchar();
	return 0;
}

说明这个指针是可以存储函数名的

void func(int v1,void(*p)(int){
	p(v1);			//相当于test(v1)

}

void test(int a){
	cout << "test()" << endl;
}

int main(){
	func(20, test);		//调用func将20传给V1,再将test传给指针P,最终结果是test(v1)

	getchar();
	return 0;
}

下面的传递函数名也是可以的

void func(int v1,void(*p)(int)=test){
	p(v1);
}

void test(int a){
	cout << "test(int)" << a << endl;
}

int main(){
	func(20);		//这个函数也是对的
	func(20, test);

	getchar();
	return 0;
}

如果函数的实参经常是同一个值,可以考虑使用默认参数

默认参数有什么价值如下

Sum(1,20)
Sum(2,20)
Sum(3,20)
Sum(4,20)
Sum(5,20)

Int Sum(int V1, int V2=20){

}
Int main(){
Sum(1)
Sum(2)
Sum(3)
Sum(4,25)
Sum(5)

}

C语言是没有默认参数这一说法的

函数重载和默认参数产生冲突时,我们就保留默认参数的函数吧,这个功能要多一些

6.2,默认参数的本质汇编

先看一下没有默认参数的程序的汇编代码

#include <iostream>
using namespace std;

int sum(int v1, int v2){
	return v1 + v2;
}

int main(){
	sum(1, 2);		#此处打断点
	sum(3, 4);

	getchar();
	return 0;
}

打断点,调试,右击反汇编

 23: 	sum(1, 2);
0034530E 6A 02                push        2  		#push传参
00345310 6A 01                push        1  		#push传参
00345312 E8 C0 BD FF FF       call        sum (03410D7h)  	#调用sum函数
00345317 83 C4 08             add         esp,8  		#牵扯到堆栈操作,先不讲
    24: 	sum(3, 4);
0034531A 6A 04                push        4  	#push传参
0034531C 6A 03                push        3  	#push传参
0034531E E8 B4 BD FF FF       call        sum (03410D7h)  	#调用sum函数,地址相同
00345323 83 C4 08             add         esp,8  

上面两个sum调用的地址相同,但传的参数不同
    18: 	return v1 + v2;
00B8518E 8B 45 08             mov         eax,dword ptr [v1]  
00B85191 03 45 0C             add         eax,dword ptr [v2]  	#add  V1与V2相加

下面是有默认参数的

int sum(int v1, int v2=4){
	return v1 + v2;
}

int main(){
	sum(1);
	getchar();
	return 0;
}

    23: 	sum(1);
0016530E 6A 04                push        4  
00165310 6A 01                push        1  
00165312 E8 C0 BD FF FF       call        sum (01610D7h)  
00165317 83 C4 08             add         esp,8  

发现是两个push,就算是有默认参数的情况,本质还是传递两个参数,其中是默认值,不存在调用函数在函数中将4赋值给V2

下面sum(1)和sum(1,4)的汇编代码是完全一样的,生成的机器码也是一样的(此处存疑)

    23: 	sum(1);
00A7530E 6A 04                push        4  
00A75310 6A 01                push        1  
00A75312 E8 C0 BD FF FF       call        sum (0A710D7h)  
00A75317 83 C4 08             add         esp,8  
    24: 	sum(1, 4);
00A7531A 6A 04                push        4  
00A7531C 6A 01                push        1  
00A7531E E8 B4 BD FF FF       call        sum (0A710D7h)  
00A75323 83 C4 08             add         esp,8  

首先内存地址是不一样的,但机器码是一样的(此处存疑),右击去掉地址显示

    23: 	sum(1);
6A 04                push        4  
6A 01                push        1  
E8 C0 BD FF FF       call        sum (0A710D7h)  
83 C4 08             add         esp,8  
   24: 	sum(1, 4);
6A 04                push        4  
6A 01                push        1  
E8 B4 BD FF FF       call        sum (0A710D7h)  
83 C4 08             add         esp,8  

将上面的赋值到一个软件(beyond compare),进行比较,红色的是不一样的,打开beyond compare,点击文本比较

我们发现只有一点不同,这里是E8的计算方法不同

E8 C0 BD FF FF       call        sum (0A710D7h) 
E8 B4 BD FF FF       call        sum (0A710D7h)

函数调用基本上是E8,既然我们调用的sum函数,sum函数地址是一样的,那为什么机器码有一点点不一样,E8右边的机器码是算出来的,与sum函数地址和下方的内存地址是有关联的

后面会详细讲汇编会有这个白皮书

Intel 机器码的白皮书PDF(天书)

Sum(1)确实等价于sum(1,4),只是因为call sum 处的位置不一样,所以生成的机器码有一点点的不同

不要纠结这个E8计算,知道这是个调用就行了

Push 4 是到栈里面(函数栈空间)

我们这课程时挖掘编程本质,利用C++和汇编挖掘编程本质

Hopper是Mac平台的

我们破解的是release优化后的exe文件

7,externC1作用

被extern "C"修饰的代码会按照C语言的方式去编译

#include <iostream>
using namespace std;

void func(){

}

void func(int v){

}

int main(){

	getchar();
	return 0;
}

在C语言中是没有函数重载的

这两个重载函数是写在CPP文件内的,编译器以看到有在这个文件,就会按照C++的标准去编译

,我们将extern "C"加到函数前

#include <iostream>
using namespace std;

extern  "C" void func(){

}

extern "C" void func(int v){

}

int main(){

	getchar();
	return 0;
}

我们运行此程序结果报错,不允许重载函数

我们是按照C语言的方式去编译被extern "C" 修饰的函数的

如果函数同时有声明和实现,要让函数声明被extern "C" 修饰,函数实现可以不修饰

extern "C" void func();
extern "C" void func(int v);

int main(){

	getchar();
	return 0;
}

void func(){

}

void func(int v){

}

此处报错,不能重载,证明extern "C" 放在声明前十有用的

 void func();
extern "C" void func(int v);

int main(){
	func();
	func(10);
	getchar();
	return 0;
}

void func(){
	cout << "func()" << endl;

}

void func(int v){
	cout << "func(int v)" << endl;
}

当代码变为上方时就不报错了,为什么,因为 void func();由C++编译器编译,extern "C" void func(int v);由C语言编译器编译,这两个函数不同时存在于C语言的编译,即两者编译后的函数名是不同的,所以不报错

如果两个函数都有extern "C" 意味着都通过C语言编译,C语言是不支持重载的。

下面两句去掉参数,是二意性的问题,有歧义

 void func();
extern "C" void func();

我们还可以这样使用

 extern "C"{
	 void func();
	 void func(int v);
 } 

8,externC-C与C++混合开发

extern "C"一般用在什么地方,用在C,C++混合开发

由于C,C++编译规则的不同,在C,C++混合开发时,可能经常出现以下操作

我们在开发时可能会经常用到第三方框架或者第三方的库(可能是用C语言写的开源库)

我们新建一个C文件math.c文件,里面函数如下

int sum(int v1, int v2){
	return v1 + v2;
}

int delta(int v1, int v2){
	return v1 - v2;
}

而main.cpp文件中为

#include <iostream>
using namespace std;

int sum(int v1, int v2);
int delta(int v1, int v2);

int main(){
	/*func();
	func(10);*/
	cout << sum(10,20) << endl;
	cout << delta(10,20) << endl;
	getchar();
	return 0;
}

此时在C++文件有对C文件中的函数的声明,但是运行后出现调用错误,这是因为两个文件的编译器不同,在C++文件中声明的函数编译后的名字与C文件中实现函数编译后的名字不同。所以在math.c找不到声明的函数,就调用报错了。

我们应告诉C++文件中sum和delta函数是按照C语言方式编译的,此时我们在之前加入extern "C"即可,声明在main.cpp文件中,实现在math.c文件中,我们只需在声明前加入extern "C"即可,如下

extern  "C"int sum(int v1, int v2);
extern  "C"int delta(int v1, int v2);

或者

extern  "C" {
	int sum(int v1, int v2);
	int delta(int v1, int v2);
}

所以C++就知道了这两个函数是C语言编译的,此时,就执行成功了

现实并不像上面只有两个文件简单

第三方框架或者第三方库要比上面的只有两个文件要复杂

将来库中的函数可能会很多,如果每个函数都要在文件前方声明的话会很复杂,别人使用你写的库会很麻烦,还需要自己声明。

我们怎么办,我们将声明放到头文件中,在头文件夹右击新建一个头文件math.h

.c文件使用来方函数实现的,.h文件为头文件一般用来存放函数声明的

如下math.h

//header file
int sum(int v1, int v2);
int delta(int v1, int v2);
int divide(int v1, int v2);

而main.cpp文件下我们改为如下

#include <iostream>
using namespace std;

extern  "C" {
	#include "math.h"				//主要改此地方即可
}

int main(){
	/*func();
	func(10);*/
	cout << sum(10,20) << endl;
	cout << delta(10,20) << endl;
	cout << divide(30,3) << endl;

	getchar();
	return 0;
}

#include "math.h"			//此句代码很简单,就相当于将math.h文件中的所有内容拷贝到main.cpp文件中,也就是将其中的所有声明拷贝到此文件下

如果我们开发第三方库、第三方框架给别人用肯定有一个头文件。

我们在使用别人的框架时,它可能给我们一个头文件、一个.C文件,因为有些东西我们只能用C语言这种比较底层的语言实现,效率会高一些。我们使用别人的库也很简单只需要包含一下别人的头文件即可,但是别人用C语言编写的,我们就要在包含的头文件先加一个extern "C",这样就能识别出来C语言写的,我们调用就没问题了

我们在main.cpp文件下将头文件前加extern "C",每次调用都要加extern "C",挺麻烦的,我们直接在math.h这个头文件中将这些声明用extern "C"包起来,这样在main.cpp文件下,直接#include "math.h" 就行了如下

math.h文件下内容

//header file
extern  "C"{
	int sum(int v1, int v2);
	int delta(int v1, int v2);
	int divide(int v1, int v2);
}

Main.cpp文件下内容

#include <iostream>
using namespace std;

#include "math.h"				//此处之前无需extern  "C",头文件已经将声明按C语言编译								识别了
int main(){
	cout << sum(10,20) << endl;
	cout << delta(10,20) << endl;
	cout << divide(30,3) << endl;

	getchar();
	return 0;
}

上面程序是可以运行的。

那么如果我们直接用C文件other.c来调用这个头文件main.h行吗,不行,C语言的编译器不认识extern "C",所以这样就会报错

Other.c

#include "math.h"			//此处就相当于Math.h中的声明内容

void other(){
	sum(10, 20);
}

Math.h内容
extern  "C"{
	int sum(int v1, int v2);
	int delta(int v1, int v2);
	int divide(int v1, int v2);
}

我们去掉extern "C"就行了,但是C++又麻烦了,又回到之前的状态了,此时我们应该怎么做呢?

我们现在希望extern "C"{}在C++环境下出现,但在C环境下消失,那应如何操作,很简单,我们要知道一个知识点宏定义这个是C语言的知识,在编译器会在C++环境下定义一个宏,宏其已经定义好了为#define __cplusplus,只要是C++环境它就会自动给你定义一个宏证明你是C++环境,只要是C++文件就默认有个宏#define __cplusplus(这个我们是看不到的,文件自动形成的,不用我们写),而C语言没有这个宏,我们可以通过判断这个宏来判断这个文件是否为C++文件

在math.h头文件中内容如下(很完全)

//header file:存放函数的声明
#ifdef __cplusplus
extern  "C"{
#endif

	int sum(int v1, int v2);
	int delta(int v1, int v2);
	int divide(int v1, int v2);

#ifdef __cplusplus
}
#endif

在这里,如定义了C++这个宏(也就是__cplusplus),在#ifdef __cplusplus 与 #endif之间的代码extern "C"{才能参与编译,下面的 }也是在这个条件中

这样,我们在C++环境和在C语言环境都可以直接使用#include "math.h" ,而无需加extern "C"之类的了

在other.c文件下代码为

#include "math.h"

#include <stdio.h>		//因为在C语言环境下,调用此头文件,调用此库可以使用下面的							printf函数,否则C++的cout是不认识的
void other(){
	printf("other - %d\n", sum(10, 20));		//打印此数值,验证此函数成功调用,\n换行
}

Main.cpp的内容如下
#include <iostream>
using namespace std;

#include "math.h"
extern "C" void other();

int main(){
	other();

	cout << sum(10,20) << endl;
	cout << delta(10,20) << endl;
	cout << divide(30,3) << endl;

	getchar();
	return 0;
}

如果是C++文件,默认在文件在最前面定义宏,相当于#define __cplusplus ,而调用#include math.h头文件,在里面从上向下执行,#ifdef __cplusplus(如果定义了这个宏__cplusplus就向下执行),执行到#endif结束,也就是执行在这两者之间的代码

在C语言环境,在此文件是没有这个宏#define __cplusplus,没有定义那么在执行头文件内的内容时,没有定义宏也就不执行#define __cplusplus与#endif之间的代码,即便将其拷贝到C语言文件下也是不会执行的

当我们开发第三方库的时候我们要加入#ifdef __cplusplus 换行 extern "C"{ 换行 #endif,以及#ifdef __cplusplus 换行 } 换行 #endif,也就是下面类似的代码

#ifdef __cplusplus
extern  "C"{
#endif
	int sum(int v1, int v2);
	int delta(int v1, int v2);
	int divide(int v1, int v2);
#ifdef __cplusplus
}
#endif

这样我们的库,C语言文件能调用,C++文件也能调用,我们会看到有些第三方库的头文件,有#ifdef和#endif

正常来说我们在自己的库math.c文件下也要添加对自己库调用的头文件math.h,因为在math.c文件中的函数可能会进行相互调用。

声明(#include ".h")可以随便写,写多少分没关系,但实现只能有一份。

10,externC,定义宏防止头文件被重复包含

  • 我们经常使用#ifndef、#define、#endif来防止文件的内容被重复包含(任何版本都能用,而且可以针对某段代码)
  • #pragma once可以防止整个文件的内容被重复包含(版本不同可能此句不能用,针对所有文件)

我们编写第三方库时,有可能包含头文件#include "math.h"写了很多份,后面想用的时候不确定前面有没有包含,就有写了一份,这样可能会导致重复包含头文件的现象,到时编译器编译的时候会比较臃肿,虽然这样写多少没关系,但预处理就是执行#include "math.h"(将头文件中的函数声明拷贝到此文件中),拷贝了很多次,为了防止重复包含,会在文件使用#ifndef#endif将所有内容包含,如下

Math.h文件中的内容

//header file:存放函数的声明
#ifndef ABC						#如果没有定义ABC这个宏
#define ABC						#就定义ABC这个宏(可以用来防止重复定义的现象)

#ifdef __cplusplus
extern  "C"{
#endif
	int sum(int v1, int v2);
	int delta(int v1, int v2);
	int divide(int v1, int v2);
#ifdef __cplusplus
}
#endif

#endif							#与ifndef 相互对应

这样写还是存在一个问题,将来会包含多个头文件,如果多个头文件下的#ifndef ABC 重复了,就会导致某些#include 的文件包含无法执行,相当于不存在。如果过防止重复包含的宏ABC一样,就会导致某些头文件不能执行

这是因为宏的命名不规范导致的,我们要是的每个头文件中的#ifndef ABC宏的命名是唯一的,有规范,这个很简单,用文件名作为宏名就行了,文件名是唯一的我们可以

#ifndef _MATH_H
#define _MATH_H
#endif

新建一个other.h文件,其中内容
#ifndef __OTHER_H
#define __OTHER_H
void other();
#endif

而math.h中的内容,定义的宏的名字以文件名前两个下划线,后面大写来实现的
#ifndef __MATH_H
#define __MATH_H

#ifdef __cplusplus
extern  "C"{
#endif
	int sum(int v1, int v2);
	int delta(int v1, int v2);
	int divide(int v1, int v2);
#ifdef __cplusplus
}
#endif

#endif

我们只需在头文件之前添加#pragma once这样就能保证此文件内容在被引用时不会重复包含

如新建一个test.h中内容如下

#pragma once
void test();

即便在main.cpp文件中写了很多

#include "test.h"
#include "test.h"
#include "test.h"

也是相当于只有一个

#include "test.h"
相关推荐
Qter_Sean19 分钟前
自己动手写Qt Creator插件
开发语言·qt
何曾参静谧23 分钟前
「QT」文件类 之 QIODevice 输入输出设备类
开发语言·qt
爱吃生蚝的于勒1 小时前
C语言内存函数
c语言·开发语言·数据结构·c++·学习·算法
小白学大数据3 小时前
Python爬虫开发中的分析与方案制定
开发语言·c++·爬虫·python
冰芒猓4 小时前
SpringMVC数据校验、数据格式化处理、国际化设置
开发语言·maven
失落的香蕉4 小时前
C语言串讲-2之指针和结构体
java·c语言·开发语言
红中马喽4 小时前
JS学习日记(webAPI—DOM)
开发语言·前端·javascript·笔记·vscode·学习
杜杜的man5 小时前
【go从零单排】Closing Channels通道关闭、Range over Channels
开发语言·后端·golang
java小吕布5 小时前
Java中Properties的使用详解
java·开发语言·后端
versatile_zpc6 小时前
C++初阶:类和对象(上)
开发语言·c++