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 inline hints 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++之那些年踩过的坑(二)》的更多相关文章

  1. 简单物联网:外网访问内网路由器下树莓派Flask服务器

    最近做一个小东西,大概过程就是想在教室,宿舍控制实验室的一些设备. 已经在树莓上搭了一个轻量的flask服务器,在实验室的路由器下,任何设备都是可以访问的:但是有一些限制条件,比如我想在宿舍控制我种花 ...

  2. 利用ssh反向代理以及autossh实现从外网连接内网服务器

    前言 最近遇到这样一个问题,我在实验室架设了一台服务器,给师弟或者小伙伴练习Linux用,然后平时在实验室这边直接连接是没有问题的,都是内网嘛.但是回到宿舍问题出来了,使用校园网的童鞋还是能连接上,使 ...

  3. 外网访问内网Docker容器

    外网访问内网Docker容器 本地安装了Docker容器,只能在局域网内访问,怎样从外网也能访问本地Docker容器? 本文将介绍具体的实现步骤. 1. 准备工作 1.1 安装并启动Docker容器 ...

  4. 外网访问内网SpringBoot

    外网访问内网SpringBoot 本地安装了SpringBoot,只能在局域网内访问,怎样从外网也能访问本地SpringBoot? 本文将介绍具体的实现步骤. 1. 准备工作 1.1 安装Java 1 ...

  5. 外网访问内网Elasticsearch WEB

    外网访问内网Elasticsearch WEB 本地安装了Elasticsearch,只能在局域网内访问其WEB,怎样从外网也能访问本地Elasticsearch? 本文将介绍具体的实现步骤. 1. ...

  6. 怎样从外网访问内网Rails

    外网访问内网Rails 本地安装了Rails,只能在局域网内访问,怎样从外网也能访问本地Rails? 本文将介绍具体的实现步骤. 1. 准备工作 1.1 安装并启动Rails 默认安装的Rails端口 ...

  7. 怎样从外网访问内网Memcached数据库

    外网访问内网Memcached数据库 本地安装了Memcached数据库,只能在局域网内访问,怎样从外网也能访问本地Memcached数据库? 本文将介绍具体的实现步骤. 1. 准备工作 1.1 安装 ...

  8. 怎样从外网访问内网CouchDB数据库

    外网访问内网CouchDB数据库 本地安装了CouchDB数据库,只能在局域网内访问,怎样从外网也能访问本地CouchDB数据库? 本文将介绍具体的实现步骤. 1. 准备工作 1.1 安装并启动Cou ...

  9. 怎样从外网访问内网DB2数据库

    外网访问内网DB2数据库 本地安装了DB2数据库,只能在局域网内访问,怎样从外网也能访问本地DB2数据库? 本文将介绍具体的实现步骤. 1. 准备工作 1.1 安装并启动DB2数据库 默认安装的DB2 ...

  10. 怎样从外网访问内网OpenLDAP数据库

    外网访问内网OpenLDAP数据库 本地安装了OpenLDAP数据库,只能在局域网内访问,怎样从外网也能访问本地OpenLDAP数据库? 本文将介绍具体的实现步骤. 1. 准备工作 1.1 安装并启动 ...

随机推荐

  1. Storm InvalidTopologyException: null

    异常信息: backtype.storm.generated.InvalidTopologyException: null at backtype.storm.daemon.common$valida ...

  2. delphi 预览图片2 (MouseUP)

    这个是自己项目在使用的,所以带有些业务功能的代码. 逻辑上使用的大多是 mouseup ,MouseMove,Mousedown.使用recttangle容器实现滑动.网上有这个下载demo. 另外移 ...

  3. MySQL锁详解

    一.概述 数据库锁定机制简单来说就是数据库为了保证数据的一致性而使各种共享资源在被并发访问访问变得有序所设计的一种规则.对于任何一种数据库来说都需要有相应的锁定机制,所以MySQL自然也不能例外.My ...

  4. webSocket错误收集

    关于 使用WebSocket报如下错误, Uncaught InvalidStateError: Failed to execute 'send' on 'WebSocket': already in ...

  5. 通过CXF方式实现webservice服务

    一.CXF的介绍 Apache CXF 是一个开放源代码框架,提供了用于方便地构建和开发 Web 服务的可靠基础架构.它允许创建高性能和可扩展的服务,您可以将这样的服务部署在 Tomcat 和基于 S ...

  6. CSS3 3D变形效果

    CSS3 3D变形效果 CSS3 transform3D变形 transform的含义是:改变,使-变形:转换 三维变换使用基于二维变换的相同属性,如果您熟悉二维变换,你们发现3D变形的功能和2D变换 ...

  7. iOS 设置#ffff 这种颜色

    UI给图的时候给的是#f2f2f2 让我设置.没有你要的rgb. 所以只能自行解决封装了代码 HexColors.h #import "TargetConditionals.h" ...

  8. ASP.NET给前端动态添加修改 CSS样式JS 标题 关键字

    有很多网站读者能换自己喜欢的样式,还有一些网站想多站点共享后端代码而只动前段样式,可以采用动态替换CSS样式和JS. 如果是webform 开发,可以用下列方法: 流程是首先从数据中或者xml读取数据 ...

  9. iOS RunTime你知道了总得用一下

    说点题外话: 我刚来现在这家公司的时候,老板让我下载一个脉脉,上去找找自己的同行,多认识些同行.其实初衷的好的,但最近这两天我把它卸载了,不为别的,负能量太多!iOS这行自从2016就没景气过,在这行 ...

  10. ubuntu中的apache的基本技巧

    1. 显示apache的版本号 XXX@XXX-ThinkPad-Edge-E431:~$ apache2 -v Server version: Apache/ (Ubuntu) Server bui ...