红联Linux门户
Linux帮助

gcc下的内联函数

发布时间:2015-11-13 10:00:10来源:linux网站作者:cloudblaze

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


普通函数

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

#include <stdio.h>
#include <stdlib.h>

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
calladd
movl%eax, %esi
movl$.LC0, %edi
movl$0, %eax
callprintf
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与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标准):

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

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


Linux下gcc与g++的差别:http://www.linuxdiyf.com/linux/13654.html

fedora22无法联网的情况下rpm安装gcc5.1:http://www.linuxdiyf.com/linux/12804.html

Linux编译安装GCC 5.1.0:http://www.linuxdiyf.com/linux/11923.html

在Mac OS X 10.10.3下使用源码包编译安装GCC5.1:http://www.linuxdiyf.com/linux/11906.html

Linux源码安装GCC编译器:http://www.linuxdiyf.com/linux/7348.html