三、基础语法1(30小时精通C++和外挂实战)
- 1,开发环境的搭建
- 2,cin和cout
- 3、4,函数重载
- 5,使用IDA分析exe
- 6.1,默认参数
- 6.2,默认参数的本质汇编
- 7,externC1作用
- 8,externC-C与C++混合开发
- 10,externC,定义宏防止头文件被重复包含
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"