[编译环境][gcc]内联函数

C标准在C99中才提供对内联函数的支持,但gcc却早已做到了这一点。内联函数的作用是尽可能地让函数允许地快一些,就如同宏一样。本文将用实例来探讨gcc下的内联函数,消除一些人们对它的疑惑。需要说明的是,本文所列出的所有代码均是在64位 ubuntu linux 15.04下用gcc4.9.2编译。


普通函数

我们先写一个普通的C程序(源文件为main.c),代码如下:

#include 
#include 

int add(int a, int b);

int main(int argc, char** argv)
{
    printf("a + b = %d\n", add(1, 2));

    return (EXIT_SUCCESS);
}

int add(int a, int b)
{
    return a + b;
}

我们使用下列编译命令的到该程序的汇编代码:

gcc main.c -S -o main.s
    .file   "main.c"
    .section    .rodata
.LC0:
    .string "a + b = %d\n"
    .text
    .globl  main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    subq    $16, %rsp
    movl    %edi, -4(%rbp)
    movq    %rsi, -16(%rbp)
    movl    $2, %esi
    movl    $1, %edi
    call    add
    movl    %eax, %esi
    movl    $.LC0, %edi
    movl    $0, %eax
    call    printf
    movl    $0, %eax
    leave
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
    .globl  add
    .type   add, @function
add:
.LFB1:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movl    %edi, -4(%rbp)
    movl    %esi, -8(%rbp)
    movl    -4(%rbp), %edx
    movl    -8(%rbp), %eax
    addl    %edx, %eax
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE1:
    .size   add, .-add
    .ident  "GCC: (Ubuntu 4.9.2-10ubuntu13) 4.9.2"
    .section    .note.GNU-stack,"",@progbits

是的,输出的汇编代码确实地有些长,但为了能够看到代码的全貌,我没有做删减。同时也为了简单一些,我没有使用其他编译选项来影响汇编代码的输出。从汇编代码中第21行我们看到在main函数中调用了add函数。


内联函数

内联函数的声明与定义

虽然看过很多有关内联函数的声明定义的文章,甚至查阅了gnu的技术文档,但仍然没有搞明白应该如何声明定义一个内联函数,难道可以有多种方法吗?

To declare a function inline, use the inline keyword in its declaration, like this:
static inline int
inc (int *a)
{
return (*a)++;
}

上面这部分引用自gnu的技术文档。从这段话来看,只要在一个函数的声明部分添加上关键字inline,这样就可以让一个函数变成内联函数。但经过我多次尝试发现,其实如果在函数定义的时候加上关键字inline也是可以的。同时我发现,把关键字加到不同的位置,产生的效果可能会有所不同。并且关键字加在不同的位置,可能会产生混淆,在某个地方以为是以普通方式调用函数实际上却内联函数了,或是以为采用的是内联函数的方式却以普通方式调用函数了。因此,我在这里做出一个约定:本文所提到的内联函数,其声明与定义所采用的修饰符是一致的。也就是说,内联函数的声明部分是把其函数头后面加上分号得到的。如下:

inline int add(int a, int b) __attribute__((always_inline));

inline int add(int a, int b)
{
    return a + b;
}

需要注意的一点是,关键字inline仅仅是让函数尽可能快地执行函数。但具体是用内联方式还是普通方式执行函数,这具体要看编译阶段,编译器是如何决定的。因此,为了看到内联方式的效果,我们在内联函数声明部分添加函数属性 attribute((always_inline)) (因网页编辑器的原因,应该在 attribute 左右两边均加上两个下划线 ‘_’)来强制编译器总是采用内联方式,如上面的代码。如果不添加该属性,则仍然会以普通函数的方式调用该函数。
另外,还需要注意的一点是,某些情况下就算我们并没有使用关键字inline来说明某个函数是内联函数,但编译器仍然会以内联方式编译这个函数。这种情况通常发生在函数比较短小,逻辑比较简单,并且采用优化方式编译。如果这种情况破坏了我们的逻辑设计,我们就需要使用函数属性attribute((noinline))来告诉编译器,这个函数不允许内联。

inline与static inline以及extern inline

我们可以在某个函数的声明定义部分添加关键字inline使其变成内联函数。但我们还可以另外再添加关键字static或extern产生不同效果的内联函数。下面是三种内联函数的声明与定义:

inline int add(int a, int b) __attribute__((always_inline));
inline int add(int a, int b)
{
    return a + b;
}

static inline int add(int a, int b) __attribute__((always_inline));
static inline int add(int a, int b)
{
    return a + b;
}

extern inline int add(int a, int b) __attribute__((always_inline));
extern inline int add(int a, int b)
{
    return a + b;
}

下表是在遵循不同的标准下产生的效果:

标准 inline static inline extern inline
c11 无定义、局部函数 无定义、局部函数 有定义、全局函数
c99 无定义、局部函数 无定义、局部函数 有定义、全局函数
c89 语法错误 语法错误 语法错误
gnu11 无定义、局部函数 无定义、局部函数 有定义、全局函数
gnu99 无定义、局部函数 无定义、局部函数 有定义、全局函数
gnu89 有定义、全局函数 无定义、局部函数 无定义、局部函数

无定义:编译器没有为内联函数单独生成指令,即不能以函数调用的方式该函数,在汇编代码中也不能以call指令调用该函数。
有定义:编译器为内联函数单独生成指令,可以被其他函数调用。
局部函数:该函数没有产生外部符号,只能被内部函数所调用,而不能被外部函数所调用。
全局函数:该函数产生了外部符号,既可以被内部函数所调用,也可以被外部函数所调用。
从上表中,我们发现c89标准是不支持内联函数的,而gnu89支持的内联函数却与其他标准支持的内联函数产生的效果却不同。

调用内联函数与访问内联函数地址

我们操作某个函数通常是调用函数,但我们有时候会通过函数指针来调用函数。函数指针中保存的其实是函数的地址。如果编译器没有给内联函数单独生成指令,那么就无从谈起函数的地址了,也就不能通过函数指针来调用函数。
如果我们在调用着函数中访问了内联函数的地址,那么这个时候又会对这个内联函数产生了另外的影响。用inline和extern inline来修饰函数产生的内联函数与上表中列出的效果一致。但用static inline来修饰函数产生的内联函数却是有定义并且为局部函数。

内联函数的应用

某个函数要么被同一个编译单元(同一个编译单元可以理解为同一个C源文件)调用,要么被其他编译单元(其他编译单元可以理解为不同的C源文件)调用。下面是应用内联函数的一点建议(不考虑gnu89标准):

  1. 如果我们仅希望某个内联函数只内嵌到本编译单元的某个函数,那么我们可以用inline来修饰函数。这个内联函数即可以定义在本编译单元的源文件中,也可以定义在头文件中然后被包含在本编译单元的源文件中。但需要注意的是,调用者函数不能访问内联函数的地址。这样没有单独为内联函数生成的指令,所有调用内联函数的地方均直接嵌入了其指令。
  2. 如果我们希望不论是本编译单元还是其他编译单元,调用内联函数的地方均直接将其指令嵌入到调用者的代码中,那么我们可以在头文件中定义内联函数并且用inline来修饰它。
  3. 如果我们希望在本编译单元中采用内联方式,而在其他编译单元中采用普通调用的方式,那么我们可以用extern inline来修饰函数并且在本编译单元的源文件中定义该内联函数。
  4. 如果我们希望在本编译单元中采用内联的方式,并且本编译单元中某个地方需要访问内联函数地址,那么就需要用static inline来修饰函数。
  5. 如果我们希望在本编译单元中采用内联的方式,并且本编译单元或其他编译单元中某个地方需要访问内联函数地址,那么就需要用extern inline来修饰函数。

前三点不考虑访问内联函数地址的情况,后两点是考虑了访问内联函数地址的情况。具体应该如何定义内联函数,需要考虑具体的需求情况。

你可能感兴趣的:(编译环境)