3.C++中将一个函数按照C的风格来编译
使用extern "C"可以在C++文件中引用c文件的函数
f.h文件
1. #pragma once 2. #define _CRT_SECURE_NO_WARNINGS 1 3. #include<stdio.h> 4. 5. int add(int a, int b);
f.c文件
1. #include "f.h" 2. 3. int add(int a, int b) 4. { 5. return a + b; 6. }
main.cpp
1. #include "f.h" 2. 3. int main() 4. { 5. int ret = add(2, 3); 6. printf("%d",ret); 7. 8. return 0; 9. }
程序报错,这是因为当c++调用add函数时,add函数被编译器编译后,函数签名变成了"?add@@YAHHH@Z",链接时,main函数却找不到这个符号。
为了能够让c++调用c文件中实现的函数,需要给该函数加上extern "C",修改f.h文件如下:
1. #pragma once 2. #define _CRT_SECURE_NO_WARNINGS 1 3. #include<stdio.h> 4. 5. //写法一 6. extern "C" int add(int a, int b); 7. 8. //写法二,能针对多行声明:可将要按照c风格进行编译的所有函数声明全部放在{ }中 9. //#ifdef __cplusplus 10. //extern "C"{ 11. //#endif 12. //int add(int a, int b); 13. //#ifdef __cplusplus 14. //}; 15. //#endif
编译成功
为什么加上extern "C"后,就能执行成功呢?
这是由于windows下的编译器会在c语言符号(变量和函数)前加上“_”,即add函数被编译后的符号为_add。因为c语言如果只有函数声明而缺少函数定义,会报缺少"_add"符号:
f.h
1. #pragma once 2. #define _CRT_SECURE_NO_WARNINGS 1 3. #include<stdio.h> 4. 5. //extern "C" int add(int a, int b); 6. #ifdef __cplusplus 7. extern "C" 8. #endif 9. int add(int a, int b);
f.c
1. #include "f.h" 2. 3. //int add(int a, int b) 4. //{ 5. // return a + b; 6. //}
main.c
1. #include "f.h" 2. 3. int main() 4. { 5. printf("%d", add(2, 3)); 6. return 0; 7. }
在VS下,c++文件调用c实现的函数时,编译时,c函数add编译后的符号为"_add"。而在c++的main中,add编译后的符号为"?add@@YAHHH@Z"。两个符号不一致,导致链接失败。而当add在extern "C"中声明后,c++的main中就会将add编译成符号"_add",符号一致,链接成功。
五、内联函数
inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数压栈的开销,因此,内联函数能够提升程序运行的效率。
1. #include<iostream> 2. 3. int Add(int a, int b) 4. { 5. int c = a + b; 6. return c; 7. } 8. 9. int main() 10. { 11. int ret = 0; 12. ret = Add(1, 2); 13. 14. return 0; 15. }
VS环境下,F10-调试-窗口-反汇编,发现会call Add
加上inline关键字后,在release模式和debug模式下查看是否存在call Add语句
(1)release模式下,直接查看编译器生成的汇编代码中是否存在call Add
(2)debug模式下,编译器默认不会对代码进行优化,需要对编译器进行设置,否则不会展开:调试-调试属性-配置属性-C/C++-常规-调试信息格式-程序数据库(/Zi)
调试-调试属性-配置属性-C/C++-常规-内联函数扩展-只适用于_inline(/Ob1)
查看反汇编结果,没有call Add,直接使用了add指令:
(1)inline是一种以空间换时间的做法,省去调用函数额开销。所以代码很长或者有循环/递归的函数不适宜使用作为内联函数。
(2)inline对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数体内有循环/递归等等,编译器优化时会忽略掉内联。
(3)inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到
f.c
1. #pragma once 2. #include <iostream> 3. using namespace std; 4. inline void f(int i);
f.cpp
1. #include "f.h" 2. 3. void f(int i) 4. { 5. cout << i << endl; 6. }
main.cpp
1. #include "f.h" 2. 3. int main() 4. { 5. f(10); 6. return 0; 7. }
编译报错,链接错误,如下所示。将inline修饰的函数声明和定义放在.h或源文件就可以了。
假如有一个函数,进行编译汇编后有100条指令,如果有10个地方调用
(1)该函数不加inline,建立栈帧,总计100 + 10 = 110条指令
(2)该函数加inline,不建立栈帧,每个地方都展开,总计100 * 10 = 1000条指令
从110条指令变成1000条指令,虽然建立栈帧不一定比不建立栈帧慢,但是编译出来的可执行程序变大,安装软件的人体验变差,执行程序内存消耗变多。
C语言为了避免小函数建帧的消耗,提供宏函数支持,在预处理阶段展开。既然C语言已经解决了,为什么C++还要提供inline函数?
因为宏有以下缺点:
(1)不支持调试(编译阶段进行了替换)
(2)宏函数语法复杂,容易出错
(3)没有类型安全检查
c++使用三种法师代替宏:枚举、const常量定义、inline内联函数。
六、指针空值nullptr
NULL实际是一个宏,NULL的定义:
NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。但对于如下代码:
1. void f(int) 2. { 3. cout<<"f(int)"<<endl; 4. } 5. 6. void f(int*) 7. { 8. cout<<"f(int*)"<<endl; 9. } 10. 11. int main() 12. { 13. f(NULL); 14. f((int*)NULL); 15. 16. return 0; 17. }
在c++中,NULL被定义为0,想用f(NULL)用空指针NULL作为参数,但是根据打印结果,发现 f(NULL)调用的是参数为int的f函数:
这是因为C++中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void *)0。 为此,C++11新增了关键字nullptr,用于表示空指针。为向后兼容,C++11仍然可以使用0来表示空指针,因此表达式nullptr=0为true,但使用nullptr提供了更高的类型安全。
注意:
1. 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
2.nullptr是指针类型,不能转化为整形类型,可以隐式转换为任意类型的指针,也可以隐式转换为bool类型代表false。
3. 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
4. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。