《C++之那些年踩过的坑(二)》
C++之那些年踩过的坑(二)
作者:刘俊延(Alinshans)
本系列文章针对我在写C++代码的过程中,尤其是做自己的项目时,踩过的各种坑。以此作为给自己的警惕。
转载请注明一下原文来自 : http://www.cnblogs.com/GodA/p/6554591.html
博客园的编译好渣,我用 Markdown 重写了一遍,内容也作了修正和调整,请移步:https://alinshans.github.io/2017/05/23/p1705231/
第一次修改:2017/3/26
发表于 : 2017/3/15
今天讲一个小点,虽然小,但如果没有真正理解它,没有真正熟悉它的里里外外,是很容易出错的 —— inline。
关于一些简单的介绍和使用,可以先看我 这篇笔记 。接下来进入正题。
一、如何使用 inline?
你知道,inline 函数可以减小函数调用的开销,你可能会想,嗯,我这个函数那么短,我把它声明为 inline,可以提高程序运行的效率!考虑这样一个例子:
// A.h
#include <cstdio>
class A
{
public:
void foo(int i);
void bar(int i)
{
std::printf("%d\n", i + );
}
};
// A.cc
#include "A.h" void A::foo(int i)
{
std::printf("%d\n", i);
}
// main.cc
#include "A.h"
int main()
{
A a;
a.foo();
a.bar();
}
首先,你知道,①inline 需要看到函数实体,所以要跟定义放在一起。于是你想在 A.cc 中在为 foo 的定义加上一个 inline :
inline void A::foo(int i)
然后开心的编译运行,WTF!!!编译器居然报错了?!!不就加了个 inline 吗!仔细观察编译器给的出错信息,如果你用的是VS,那么你大概会看到这样的信息: error LNK2019: 无法解析的外部符号……如果你用的是GCC,你会发现当你使用
g++ -c main.cc
时(即编译),是不会产生任何错误的,然后当你使用
g++ main.o -o a.out
时(即链接),就报错了。说明,这是链接的时候出错了。在这里要说明一下,大多数的建置环境都是在编译过程进行 inlining(为了替换函数调用,编译器需要知道函数的实体长什么样,这解释了①),某些可以在连接期完成,少数的可以在运行期完成。我们只考虑绝大部分情况:Inlining 在大多数C++程序中是编译期行为。
大部分函数默认的就是外部链接,也就是外部可以访问,而 inline 函数默认具有内部链接,也就是对本文件可见,对其它文件不可见。那么自然我们在 main.cc 中调用它,没法看到它的定义,于是就出现了连接错误。OK,你学到了 ②一般 inline 需要放在头文件中。
首先你要先了解一下内部链接与外部链接,可以看这里。它提到:
names of classes, their member functions, static data members (const or not), nested classes and enumerations, and functions first introduced with friend declarations inside class bodies
ok,类的成员函数是具有外部链接的,然后我们看这里,它提到:
An inline function or variable (since C++17) with external linkage (e.g. not declared static) has the following additional properties:
1) It must be declared inline in every translation unit.2) It has the same address in every translation unit.
嗯,意思很明白了,就是如果一个函数,是外部链接的,你给它搞成 inline 了,那么,请你在每一个编译单元都做一个 inline 定义。也就是说,如果你想让上面的代码运行,没问题,那请把 main.cpp 改成这样:
// main.cpp
#include <iostream>
#include "A.h" class A;
inline void A::foo(int i)
{
std::printf("%d\n", i);
} int main()
{
A a;
a.foo();
}
我想,如果有十个编译单元要引用它呢?一百个呢?你可能不愿意这样写。而在这里开头还有提到:
A function defined entirely inside a class/struct/union definition, whether it's a member function or a non-member friend function, is implicitly an inline function.
在类内定义的成员函数,是自动 inline 的,不需要你去加,LLVM CodingStandards 也是这样提出的。
那你可能会想马上想到还有一种情况:如果一个类成员函数,既不定义在类内,也不定义在编译单元,而是定义在头文件,并且在类外,这种情况,又会发生什么呢?也就是这样:
// A.h
#include <cstdio>
class A
{
public:
void foo(int i);
void bar(int i);
}; inline void A::bar(int i)
{
std::printf("%d", i + );
}
嗯,可以,这样写通过编译,并且可以运行了。不过,它如你所想提高效率了吗?我们可以探究一下。在vs下可以用调试看反汇编,现在用GCC分别运行以下命令:
g++ -E main.cc -o main.i
g++ -S main.i -o main.s
g++ -O2 -S main.i -o main2.s
我们来看一下 main.s 中的主要部分:
call ___main
leal -(%ebp), %eax
movl $, (%esp)
movl %eax, %ecx
call __ZN1A3fooEi
subl $, %esp
leal -(%ebp), %eax
movl $, (%esp)
movl %eax, %ecx
call __ZN1A3barEi
subl $, %esp
movl $, %eax
movl -(%ebp), %ecx
我们再看一下 main2.s 中的这个部分:
call ___main
leal -(%ebp), %ecx
movl $, (%esp)
call __ZN1A3fooEi
subl $, %esp
movl $, (%esp)
movl $LC0, (%esp)
call _printf
movl -(%ebp), %ecx
在不开优化的情况下,程序诚实的执行,开O2优化的情况下,我们已经看不到 bar 函数的调用了。不过这真的是拜你加的 inline 所赐的吗?为了验证,我们去掉 inline,打算再次重复上面的过程,然后你就会发现,WTF!!!编译器又报错了??发生了什么??
二、什么时候应该使用 inline?
嗯,终于我们来到了第二个问题,我们发现,当我们给函数去掉 inline 时,居然无法通过编译了!它给出来的错误信息是:重定义的符号。让我们冷静下来,想一想,然后你就会恍然大悟:一个函数可以有多次声明,但只能有一次定义,而我们定义在 A.h 的 bar 函数的定义,被 A.cc 和 main.cc 都包含了一遍!所以就出现了重定义的错误!是的是的,我也想不到有什么理由让一个类成员函数的定义即不出现在类内部,也不出现在编译单元,除非是模板类成员函数/类模板成员函数。不过你现在应该对 inline 与类成员函数的种种事情,有了非常清晰的认识了。即 ②不要把 inline 用在类的成员函数上。当然,也别写出上面那种情况的代码。
然后我们来看看 inline 跟普通函数结合的情况。这种情况,更容易被我们忽视,例如,我们想在 A.h 中加一个函数:
// A.h
int max(int a, int b)
{
return a < b ? b : a;
}
它很短,要不要使用 inline 呢?经过刚刚的问题,你应该会谨慎的想到,这里,要使用 inline ,如果不使用,就会出错。原因跟上面提到的是一样的。当然,它不是非得使用 inline 不可,你可以把它的函数定义放在源文件,就不会有重复定义的问题。甚至你也可以在头文件定义并且使用 static 修饰它,也可以解决问题,这个就不展开了。
但当你用上模板时,情况发生了改变。若你把这个 max 函数改成一个模板函数:
template <typename T>
T max(const T& a, const T& b)
{
return a < b ? b : a;
}
这个时候,无论你有没有使用 inline,它都是可以运行的。这是因为,模板是具有“内联”语义的。所以,类模板,函数模板,类函数模板,都不需要加 inline 。回到正题,什么时候可以使用 inline 呢?③使用 inline 当且仅当函数的定义在头文件中并且被多个源文件包含时。
三、inline 可以提升程序运行效率?
我们刚刚还没有完成我们的实验,我还没有打消你使用 inline 去“优化”程序的念头。所以,让我们再次做一次实验,这次为了方便,我在 main.cc 定义一个函数:
// main.cc
#include <cstdio> int test(int i)
{
i = i + ;
return i;
} int main()
{
std::printf("%d\n", test());
}
这样的短代码,你想优化了是吧?先别急,我们就这样,编译汇编看看,运行同样的命令:
g++ -E main.cc -o main.i
g++ -S main.i -o main.s
g++ -O2 -S main.i -o main2.s
然后看 main.s (未开优化)的主要部分:
call ___main
movl $, (%esp)
call __Z4testi
movl %eax, (%esp)
movl $LC0, (%esp)
call _printf
然后再看看 main2.s(开O2优化)的这个部分:
call ___main
movl $, (%esp)
movl $LC1, (%esp)
call _printf
嗯是的没错,你没有声明 inline,但是编译器的优化,帮你把这个函数内联展开了,所以在 main2.s 中看不到 test 的调用了。你加上 inline 重复这个过程,还是会得到一样的结果。还没有死心吗?你是不是想说,这个函数太简单了,我是编译器我都看得出来可以优化啊!听说复杂一点编译器就不会优化了!比如函数里面有循环,递归什么的!
好,于是你改成这样:
// main.cc
#include <cstdio> int test(int i)
{
int x = ;
for (int j = ; j < i; ++j)
{
x += j;
}
return x;
} int main()
{
std::printf("%d\n", test());
}
再次编译汇编,你猜猜你会看到什么?好吧,我只把 main2.s 中的那个部分给你看看:
call ___main
movl $, (%esp)
movl $LC1, (%esp)
call _printf
你还想说什么吗?如果还没死心,请继续尝试其他情况。我不会帮你试,不过我可以帮你试试这个情况:
// main.cc
#include <cstdio>
#include <cmath> inline int test(int i)
{
int prime[];
int k = ;
for (int n = ; n <= i; ++n)
{
bool is_prime = true;
for (int j = ; j <= static_cast<int>(std::sqrt(n)); ++j)
{
if (n % j == )
{
is_prime = false;
break;
}
}
if (is_prime)
{
prime[k] = n;
++k;
}
}
int sum = ;
for (int n = ; n < k; ++n)
{
sum += prime[n];
}
return sum;
} int main()
{
std::printf("%d\n", test());
}
嗯。。长是长了点,但是你声明了一个 inline 呀!好吧,我们再看看生成的两份汇编代码:
main.s:
call ___main
movl $, (%esp)
call __Z4testi
movl %eax, (%esp)
movl $LC1, (%esp)
call _printf
movl $, %eax
main2.s:
call ___main
movl $, (%esp)
call __Z4testi
movl $LC2, (%esp)
movl %eax, (%esp)
call _printf
xorl %eax, %eax
这一次,无论是否开优化,都调用了 test。然后你很无奈的发现,编译器是否选择内联,跟你声不声明没有半毛钱关系啊!!
四、 inline 的真正意义?
现在你该好好的思考,什么是 inline,是内联吗?inline 的意义是什么,是发起一个内联请求吗?
你认为加 inline 是为了提高程序的运行效率,但是事实上,并不会跟 inline 有什么关系啊。但有的时候,你不加 inline,却会出错。这跟“内联”两个字,好像已经没什么关系了?
好好的思考一下吧。
这么快就往下看了,花点时间在思考一下?
好吧。
inline,跟 static , extern 一样,都是链接指令,它在很久很久以前,是作为给编译器优化的提示符。而 inline 的含义是非绑定的,编译器可以自由的选择、决定是否 inline 一个函数。如今,编译器根本不需要这样的提示,如果它认为一个函数值得 inline,它会自动 inline,否则,即使你 inline 了,它也会拒绝。如果你仔细阅读 http://en.cppreference.com/w/cpp/language/inline 的话,尤其是其中的 Desription:
Description
An inline function or inline variable (since C++17) is a function or variable (since C++17) with the following properties:
1) There may be more than one definition of an inline function or variable (since C++17) in the program as long as each definition appears in a different translation unit and (for non-static inline functions) all definitions are identical. For example, an inline function or an inline variable (since C++17) may be defined in a header file that is #include'd in multiple source files.2) The definition of an inline function or variable (since C++17) must be present in the translation unit where it is accessed (not necessarily before the point of access).3) An inline function or variable (since C++17) with external linkage (e.g. not declared static) has the following additional properties:1) It must be declared inline in every translation unit.2) It has the same address in every translation unit.
你会发现,全文几乎没有提到 “优化代码”、“减小开销” 等等字眼。而你在网上所搜素到的关于 inline 的信息,几乎都告诉你,inline 可以怎么怎么优化。要么用了假的搜索引擎,要么看了假网页 ,要么…… 在这篇 SO 中,有一段话:
It is said that
inlinehints to the compiler that you think the function should be inlined. That may have been true in 1998, but a decade later the compiler needs no such hints. Not to mention humans are usually wrong when it comes to optimizing code, so most compilers flat out ignore the 'hint'.
static- the variable/function name cannot be used in other compilation units. Linker needs to make sure it doesn't accidentally use a statically defined variable/function from another compilation unit.
extern- use this variable/function name in this compilation unit but don't complain if it isn't defined. The linker will sort it out and make sure all the code that tried to use some extern symbol has its address.
inline- this function will be defined in multiple compilation units, don't worry about it. The linker needs to make sure all compilation units use a single instance of the variable/function.
看完你应该差不多能理解了。现在的编译器,并不需要你用 inline 提醒,所以,当且仅当你认为使用 inline 会加快程序运行效率时,不要使用 inline 。inline 这个关键字,在C++里就是一个骗局。它真正的意义并不是去内联一个函数,而是表示 别怕!无论你看到了多少个定义,但实体就我一个! 在 Reference 中有有这样一句话:
Because the meaning of the keyword inline for functions came to mean "multiple definitions are permitted" rather than "inlining is preferred", that meaning was extended to variables.
翻译过来就是 ④ inline 的含义更多的是 “允许多重定义” 而不是 “优先选择内联函数” 。全文基于 C++17 及以前的讨论。
五、总结
1、inline 需要看到函数实体,所以要跟定义放在一起
2、不要把 inline 用在类的成员函数上
3、使用 inline 当且仅当函数的定义在头文件中并且被多个源文件包含时
4、inline 的含义更多的是 “允许多重定义” 而不是 “优先选择内联函数”
5、模板不需要声明 inline,也具有 inline 的语义
※注:以上总结适用于不熟悉、不了解 inline 的同学。若对以上内容都了解,使用 inline 的时候,很明白很清楚在做什么,会发生什么,那就随便怎么用啦!
《C++之那些年踩过的坑(二)》的更多相关文章
- 简单物联网:外网访问内网路由器下树莓派Flask服务器
最近做一个小东西,大概过程就是想在教室,宿舍控制实验室的一些设备. 已经在树莓上搭了一个轻量的flask服务器,在实验室的路由器下,任何设备都是可以访问的:但是有一些限制条件,比如我想在宿舍控制我种花 ...
- 利用ssh反向代理以及autossh实现从外网连接内网服务器
前言 最近遇到这样一个问题,我在实验室架设了一台服务器,给师弟或者小伙伴练习Linux用,然后平时在实验室这边直接连接是没有问题的,都是内网嘛.但是回到宿舍问题出来了,使用校园网的童鞋还是能连接上,使 ...
- 外网访问内网Docker容器
外网访问内网Docker容器 本地安装了Docker容器,只能在局域网内访问,怎样从外网也能访问本地Docker容器? 本文将介绍具体的实现步骤. 1. 准备工作 1.1 安装并启动Docker容器 ...
- 外网访问内网SpringBoot
外网访问内网SpringBoot 本地安装了SpringBoot,只能在局域网内访问,怎样从外网也能访问本地SpringBoot? 本文将介绍具体的实现步骤. 1. 准备工作 1.1 安装Java 1 ...
- 外网访问内网Elasticsearch WEB
外网访问内网Elasticsearch WEB 本地安装了Elasticsearch,只能在局域网内访问其WEB,怎样从外网也能访问本地Elasticsearch? 本文将介绍具体的实现步骤. 1. ...
- 怎样从外网访问内网Rails
外网访问内网Rails 本地安装了Rails,只能在局域网内访问,怎样从外网也能访问本地Rails? 本文将介绍具体的实现步骤. 1. 准备工作 1.1 安装并启动Rails 默认安装的Rails端口 ...
- 怎样从外网访问内网Memcached数据库
外网访问内网Memcached数据库 本地安装了Memcached数据库,只能在局域网内访问,怎样从外网也能访问本地Memcached数据库? 本文将介绍具体的实现步骤. 1. 准备工作 1.1 安装 ...
- 怎样从外网访问内网CouchDB数据库
外网访问内网CouchDB数据库 本地安装了CouchDB数据库,只能在局域网内访问,怎样从外网也能访问本地CouchDB数据库? 本文将介绍具体的实现步骤. 1. 准备工作 1.1 安装并启动Cou ...
- 怎样从外网访问内网DB2数据库
外网访问内网DB2数据库 本地安装了DB2数据库,只能在局域网内访问,怎样从外网也能访问本地DB2数据库? 本文将介绍具体的实现步骤. 1. 准备工作 1.1 安装并启动DB2数据库 默认安装的DB2 ...
- 怎样从外网访问内网OpenLDAP数据库
外网访问内网OpenLDAP数据库 本地安装了OpenLDAP数据库,只能在局域网内访问,怎样从外网也能访问本地OpenLDAP数据库? 本文将介绍具体的实现步骤. 1. 准备工作 1.1 安装并启动 ...
随机推荐
- PHP那些最好的轮子
PHP那些最好的轮子 Databse 数据库ORM Doctrine 2 License : MIT Source Code Allo点评:Doctrine是功能最全最完善的PHP ORM,社区一直很 ...
- SpringMVC+RestFul详细示例实战教程
一.SpringMVC基础入门,创建一个HelloWorld程序 1.首先,导入SpringMVC需要的jar包. 2.添加Web.xml配置文件中关于SpringMVC的配置 <!--conf ...
- 用Spark学习矩阵分解推荐算法
在矩阵分解在协同过滤推荐算法中的应用中,我们对矩阵分解在推荐算法中的应用原理做了总结,这里我们就从实践的角度来用Spark学习矩阵分解推荐算法. 1. Spark推荐算法概述 在Spark MLlib ...
- 【java设计模式】之 工厂(Factory)模式
1.工厂模式的定义 工厂模式使用的频率非常高,我们在开发中总能见到它们的身影.其定义为:Define an interface for creating an object, but let subc ...
- (原创)Java多线程作业题报java.lang.IllegalMonitorStateException解决
作业: 有一个水池,水池容量500L,一边为进水口,一边为出水口,要求进水放水不能同时进行,水池一旦满了不能继续注水,一旦空了,不能继续放水,进水速度5L/s,放水速度2L/s. 这是我学多线程时做的 ...
- 吉特仓储管系统(开源WMS)--Web在线报表以及打印模板分享
很早之前就想写这篇文章与大家分享一下自己在吉特仓储管理系统中开发打印和报表的功能,在GitHub(https://github.com/hechenqingyuan/gitwms)上公开下载的代码中很 ...
- IIS7.0发布后关于"不能在此路径中使用此配置节”的解决办法
在系统为window sever2008,iis7.0上安装后发布出现 IIS Web Core 通知 BeginRequest 处理程序 尚未确定 错误代码 0x80070021 配置错误 不能在此 ...
- angular 输入框实现自定义验证
此插件使用angular.js.JQuery实现.(jQuery的引入需在angular 之前) 用户可以 在输入框输入数据后验证 必填项.整数型.浮点型验证. 如果在form 里面的输入框验证,可以 ...
- php常见面试问题
1. 如果没有开启cookies,session如何工作? PHP中的sessions通常会使用cookies的方法.但是如果没有cookies(浏览器禁用cookies),PHP sessions也 ...
- webpack入门与解析(一)
每次学新东西总感觉自己是不是变笨了,看了几个博客,试着试着就跑不下去,无奈只有去看官方文档. webpack是基于node的.先安装最新的node. 1.初始化 安装node后,新建一个目录,比如ht ...