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