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或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标准):
前三点不考虑访问内联函数地址的情况,后两点是考虑了访问内联函数地址的情况。具体应该如何定义内联函数,需要考虑具体的需求情况。