文章目录
一、 extern “C”
有时候在C++工程中可能需要将某些函数按照C的风格来编译,在函数前加extern “C”,意思是告诉编译器,将该函数按照C语言规则来编译。比如:tcmalloc是google用C++实现的一个项目,他提供tcmallc()和tcfree两个接口来使用,但如果是C项目就没办法使用,那么他就使用extern “C”来解决。
1.1 C++调用C
假设现在我们在C++程序中实现一个函数需要调用一个栈。看下面代码:
#include <iostream>
using namespace std;
//给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。
//不需要看懂这个函数实现的功能。只用知道这个函数实现需要用到栈
bool isValid(char* s)
{
ST st;
StackInit(&st);
while (*s)
{
//判断是否是左括号
if (*s == '{'
|| *s == '['
|| *s == '('
)
{
stackPush(&st, *s);
s++;
}
//为右括号出栈匹配
else
{
//如果没有左括号,一上来就是右括号。
if (StackEmpty(&st))
{
StackDestroy(&st);
return false;
}
STDataType top = StackTop(&st);
stackPop(&st);
//匹配失败
if ((*s == ')' && top == '(')
|| (*s == '}' && top == '{')
|| (*s == ']' && top == '[')
)
{
StackDestroy(&st);
return false;
}
s++;
}
}
bool top = StackEmpty(&st);
//都匹配成功了
return true;
}
int main()
{
char str[10] = {'{','(', '[', ']', ')', '}'};
int ret = isValid(str);
cout << ret << endl;
return 0;
}
很显然,我们编译会出错因为我们在这个cpp工程当中找不到实现栈的库,那么我们就引入一个库,里面包含栈就可以了
此时我们新建一个工程,里面就是用c实现的栈,那么我们让其文件去调用这个库呢?我们以VS2019举例:
我们打开文件路径,此时就生成了一个静态库:
然后就在cpp工程中去调用它:
然后在进行配置:
我们去编译:看会不会编译成功呢?
这是为什么呢?这是一串看不懂的符号又是什么呢?此时我们发现这个错误是链接错误(LNK),但是我们明明有栈的实现啊,为什么还会发生链接错误呢?相信大家很快就能够发现问题,这是因为我们调用的是c实现的栈:
而此时我们在cpp中包含了这个栈的头文件,.但是我们在去调用c实现的栈的函数的时候,链接是采用的cpp方式链接的,但是在链接的时候却找不到这些符号,所以就会报错。如果大家学过程序的编译、链接就知道在代码在编译成可执行程序的过程中,是需要预处理、编译、汇编和链接,我们这里就不多讲,只需要知道在编译的时候会进行符号汇总,此时这些符号就会被汇总起来,在汇编的时候就会形成符号表,在链接的时候就会拿到这些符号去找函数的地址,但是此时cpp里面的函数像StackInit(?),去找函数地址的时候就会找不到。
那么我们将这个栈改为cpp实现的呢?可不可以呢?
我们在编译运行:
就可以了,这理解就比较简单了吧,我们在链接的时候,去找这些符号的时候,用cpp的方式就找到了。但是此时是c实现的栈我们有没有方法解决这个问题呢?
我们就可以采用extern “C”
再编译运行:
就可以了,那么extern “C”的作用就是告诉C++编译器,我里面的函数是c编译器编译的,链接的时候用c的函数名规则去找,就可以链接上。
1.2 C调用C++
此时我们将这个Test.cpp改为Test.c
我们同样需要一个实现栈的库:
这个栈的库是用c实现的。我们在编译运行Test.c:
没有错,这个理解起来就很容易了。但是我们将栈的实现改为cpp的呢?
我们再编译Test.c:
就报错了,同样也是报的链接(LNK)错误,但是发现此时这些符号和我们之前用c++掉c时候的报错是不一样的:
这也就进一步说明了在链接通过这些符号去找函数地址的时候,c和c++都会按照自己的方式去找。
此时我们也用extern“C”来该代码:
我们编译Test.c:
为什么这些函数还是没有定义,并且还有一个语法错误,这是为什么呢?此时我们注意,我们是在Test.c中编译的时候包含了头文件<Stack.h>,而预处理的时候就会把里面的代码展开,extern "C"也会被展开,但是在Test.c中并不认识extern “C”,所以就会报错。
怎么解决呢?我们可以用条件编译:
这里的条件编译的意思就是如果在cpp当中,就会将extern "C"替换为EXTERN ,如果没有在cpp当中就会被替换成空格。我们不想让extern "C"在Test.c中出现,那么就会将EXTERN替换为空格
此时我们编译运行Test.c:
此时这个问题就解决了。我们还有另外一种解决方法:
这个又是什么意思呢?同样是如果是在cpp当中,就会有__cplusplus,第一次条件判断,将extern “C” { 包含进来,第二次条件判断,就将 } 包含进来。如果不是在cpp当中就什么都不做。
此时我们编译Test.c:
同样也可以运行成功。
二、内联函数
2.1 概念
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数压栈的开销,内联函数提升程序运行的效率。
这是什么意思呢?
#include <iostream>
using namespace std;
int Add(int x, int y)
{
return x + y;
}
int main()
{
//调用Add的次数较多
Add(1, 2);
Add(1, 2);
Add(1, 2);
Add(1, 2);
Add(1, 2);
Add(1, 2);
return 0;
}
多次调用Add函数是会不断的开辟函数栈帧的,开辟函数栈帧消耗就比较大。
那么能不能优化一下呢?向这种小函数,我们就可以用宏实现来优化:
Add函数就会被直接替换。
但是一般使用宏的时候,直接替换,很容易出错。那么还有没有方法呢?答案是有的
如果在上述函数前增加inline关键字将其改成内联函数,在编译期间编译器会用函数体替换函数的调用。
查看方式:
- 在release模式下,查看编译器生成的汇编代码中是否存在call Add
2.在debug模式下,需要对编译器进行设置,否则不会展开(因为debug模式下,编译器默认不会对代码进行优化,以下给出vs2019的设置方式
此时我们看到就没有调用函数了。
2.2 特性
- inline是一种以空间换时间的做法,省去调用函数额开销。所以代码很长(大概10以上)或者有循环/递归的函数不适宜使用作为内联函数。
- inline对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数体内有循环/递归等等,编译器优化时会忽略掉内联。
- inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。
这是什么意思呢?比如:现在有Add声明
Add定义;
此时我们去调用它就会出错。
【面试题】
宏的优缺点?
优点:
1.增强代码的复用性 。
2.提高性能。
缺点:
1.不方便调试宏。(因为预编译阶段进行了替换)
2.导致代码可读性差,可维护性差,容易误用。
3.没有类型安全的检查 。
C++有哪些技术替代宏?
4. 常量定义 换用const
5. 函数定义 换用内联函数
三、auto关键字(C++11)
3.1 auto简介
在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,但遗憾的是一直没有人去使用它。
C++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。
举例:
实际当中一般不是这样使用。
注意:
使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型。
3.2 auto的使用细则
1.auto与指针和引用结合起来使用
用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&
2. 在同一行定义多个变量
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量
3.3 auto不能推导的场景
- auto不能作为函数的参数
- auto不能直接用来声明数组
- 为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法
- auto在实际中最常见的优势用法就是跟以后会讲到的C++11提供的新式for循环,还有lambda表达式等进行配合使用。
四、基于范围的for循环(C++11)
4.1 范围for的语法
在C++98中如果要遍历一个数组,可以按照以下方式进行:
对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此C++11中引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。
4.2 范围for的使用条件
- for循环迭代的范围必须是确定的
对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。
注意:以下代码就有问题,因为for的范围不确定
- 迭代的对象要实现++和==的操作。(暂时不了解)
五、指针空值nullptr(C++11)
5.1 C++98中的指针空值
在良好的C/C++编程习惯中,声明一个变量时最好给该变量一个合适的初始值,否则可能会出现不可预料的错误,比如未初始化的指针。如果一个指针没有合法的指向,我们基本都是按照如下方式对其进行初始化:
NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:
可以看到,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,比如:
程序本意是想通过f(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成0,因此与程序的初衷相悖。
在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void *)0。
- 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的
- 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
- 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。