- 1.内联函数
- 1.1.基础概念
- 1.2.内联函数实例讲解
- 1.2.1.内联函数引入
- 1.2.2.使用内联函数进行改进
- 1.2.3.对内联函数定义位置的改进
- 1.2.4. 终极解决办法——在头文件中定义内联函数
- 2.constexpr函数
- 3.函数指针
- 3.1.函数类型与函数指针类型
- 3.2.函数指针与重载
- 3.3. 函数指针作为函数的返回值
- 函数展开:对于非常简单的函数,在调用这个函数的地方直接执行这个函数,而不用真正去调用这个函数,从而降低函数调用的堆栈开销。
- 一个函数如果在被调用的地方展开,那么就必须在调用它的那个翻译单元中看到这个函数的定义,而不能只有声明,因为函数展开是编译期间的行为。
- 注意函数展开是编译期间的行为,不是链接期间或运行期间。
为什么需要内联(in line)?考虑如下的程序结构:
#includevoid fun() { std::cout << "hello" << std::endl; } void fun(); int main() { fun(); }
这种程序结构存在的问题是在翻译单元2中调用的fun函数实现非常简单,但是由于它是在另一个翻译单元中被定义的,所以我们只能进行函数调用,这样就会设置堆栈开销,对程序性能不利。
1.2.2.使用内联函数进行改进为了提升性能,我们可以把fun函数的定义放到一个翻译单元中,于是程序变成如下形式。
#includevoid fun() { std::cout << "hello" << std::endl; } int main() { fun(); // 此处有可能被展开为std::cout << "hello" << std::endl; }
为什么这样就可以提升性能呢?因为此时编译器会判断fun函数的逻辑,发现其逻辑非常简单,所以有可能会把fun函数在它被调用的地方进行展开,从而省去调用函数的队堆栈开销,对函数内部逻辑就地执行。
注意:
- 函数是否被展开是由编译器决定的,可能会展开,可能不会展开。编译器判断的准则是看函数内部是否足够简单,如果内部非常复杂,那么调用函数的堆栈开销相比函数体执行开销很小,就不会展开。通常情况下,使用-O编译优化选项则编译器把函数展开的可能性更大。(疑问:不管咋样,全部展开不行吗?这样不是性能最好吗?)
- 函数被当做内联函数进行展开并不是简单的拷贝过来,因为这样可能会噪声函数内部局部变量和调用函数的位置所在的域内的变量产生命名冲突,即变量重复定义的错误。所以内联函数并不是简单的拷贝过来,编译的时候对其会进行一些内部的命名处理。但是可以认为在执行逻辑上,就是就地执行函数内部的操作,而没有函数调用的堆栈开销。
1.2.2中的方法把函数放到调用它的那个翻译单元中进行定义,虽然可以让函数被当做内联函数进行展开了,但是存在的问题是另一个翻译单元中就没有这个函数的定义了。
比如如下情况,翻译单元1中也有对fun的调用,按理说fun很简单我们也想把fun在source.cpp中展开,但此时显然由于在source.cpp中看不到fun()函数的定义,因此对它无法展开。
#includevoid fun(); void call() { fun(); } void fun() { std::cout << "hello" << std::endl; } void call(); int main() { fun(); // 此处有可能被展开为std::cout << "hello" << std::endl; call(); // 其内部一定不能被展开 }
一个显然的想法是把fun也在source.cpp中定义,这样满足函数展开的条件,如下代码。但是这样会在链接期间产生函数重复定义的错误:也就是在整个可执行程序(编译出的可执行程序,包含所有翻译单元)中,fun函数被重复定义了,在source.cpp和main.cpp这两个翻译单元中都有定义。
#includevoid fun() { std::cout << "hello" << std::endl; } void call() { fun(); } void fun() { std::cout << "hello" << std::endl; } void call(); int main() { fun(); call(); }
同理,如果我们把fun函数的定义放在一个fun.h的头文件中定义,然后在source.cpp和main.cpp这两个翻译单元中包含这个头文件,本质上也是进行了重复定义,因为包含头文件就是把头文件内容直接拷贝过来而已,本质上和上面两种没有什么区别。
#pragma once #include1.2.4. 终极解决办法——在头文件中定义内联函数void fun() { std::cout << "hello" << std::endl; } #include "fun.h" void call() { fun(); } #include "fun.h" void call(); int main() { fun(); call(); }
这种方式使用inline关键字声明这是一个内联函数,代码如下所示:
#pragma once #includeinline void fun() { std::cout << "hello" << std::endl; } #include "fun.h" void call() { fun(); } #include "fun.h" void call(); int main() { fun(); call(); }
使用inline关键字的作用如下:
- 在编译期间,翻译单元在调用这个函数的位置会尝试对其进行展开。疑问:是尝试进行展开还是说一定会强制展开?
- 在链接期间,整个可执行程序发现在不同的翻译单元中都有fun函数的定义,但是由于声明了是inline函数,所以最后会保留一个定义,从而不会产生重复定义的错误。
因此,这里也可以说inline关键字对函数在链接期间的一处定义原则进行了修改:从程序级别的一处定义原则变成了翻译单元的一处定义原则,相当于降低了链接器对函数重定义的判断门槛。
(1)无inline的程序级别一处定义原则:在整个程序所能看到的所有翻译单元中,只能有fun函数的一处定义。因此不同的翻译单元中,不能有同样的fun函数的定义。
(2)有inline的翻译单元级别的一处定义原则:在整个程序所能看到的所有翻译单元中,可以有多个fun函数的定义,由于使用了inline关键字,链接器会自动保留一个fun的定义,从而不会出现函数重定义错误。但是在每一个翻译单元中,函数还是只能有一处定义的,比如main.cpp中包含的fun.h头文件中已经对fun进行了定义,那么你就不能再在main.cpp中再对fun函数进行定义。
2.constexpr函数- constexpr函数可以在编译期进行调用,也可以在运行期进行调用。
- 函数类型:注意函数也是有类型的,对比数组类型来看函数类型:
int a[3]; // a的类型是int [3] int fun(int x); // fun的类型是int(int)
- 函数指针
int fun(int x); using K = int(int); // 使用别名,函数类型 K* ptr; // 函数指针,类型是int (*) (int), 注意不是int* (int)
注意:当把函数当做形参传递给另一个函数的形参的时候,这个函数会从函数类型退化成函数指针类型,这个和数组是类似 的。因为数组不能复制,这里函数同样也不能赋值。对比如下:
// 1.数组类型退化成数组指针
int a[3];
void fun(int a[]); //
int main()
{
fun(a); // a会从int [3]退化成int*
}
// 2.函数类型退化成函数指针
int fun(int x)
{}
using K = int (int);
void call(K fun)
{}
int main()
{
call(fun); // fun从int (int)的函数类型退化成 int (*) (int)的指针类型
3.2.函数指针与重载
有函数重载时不能使用auto自动推导函数指针。
// 1.没有函数重载,合法
void fun(int){}
int main()
{
auto x = fun; // x退化成函数指针
}
// 2.有函数重载,是一组函数,不合法
void fun(int){}
void fun(int, int){}
int main()
{
auto x = fun;
}
3.3. 函数指针作为函数的返回值
实际上跟上面的是很相似的,就是说函数不能拷贝,所以返回的一定是函数指针,而不是函数本身。
int inc(int x);
int dec(int x);
auto fun(bool input)
{
if (input)
return inc;
else
return dec;
}
int main()
{
std::cout << (*fun(false))(100) << std::endl; // 输出99
}



