内联函数
定义
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。
我们知道,在创建函数调用时,会产生一个函数栈帧,占用内存,如果我们函数中里面还有很多个不同或者是相同的函数(比如一个自定义函数当中有很多个交换两个数据的自定义函数),那么会栈溢出:
假设函数1,2,3,4都是一个函数,那么我们在C语言当中可以把这个函数定义成宏,在预编译的时候,头文件会被展开,宏会进行文本替换,所以,就不会创建这么多的函数栈帧。
不过用宏代替函数,写的时候非常麻烦,稍微不注意就会运算错误,并且还不能进行调试。
在C++中,用inline关键字修饰函数,会改掉宏替代函数的缺点,在调用的地方直接展开。
代码1:
#include <iostream> using namespace std; int add(int x, int y) { return x + y; } int main() { int x = 10; int y = 20; int z = add(x, y); cout << z << endl; }
没有inline修饰的函数在汇编时会有call add(调用add函数)
代码2
#include <iostream> using namespace std; inline int add(int x, int y) { return x + y; } int main() { int x = 10; int y = 20; int z = add(x, y); cout << z << endl; }
再查看这段代码时要做一些设置(因为debug下面需要调试,所以默认不会展开):
这里就没有去调用add函数,说明展开了。
特性
当然,也不是什么函数都能修饰成内联函数的。
不然,去把递归的函数修饰一下,岂不是很精彩。
- inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运行效率。
- inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性。
#include <iostream> using namespace std; inline int add(int x, int y) { int sum = x + y; sum = x + y; sum = x + y; sum = x + y; sum = x + y; sum = x + y; sum = x + y; sum = x + y; sum = x + y; sum = x + y; sum = x + y; sum = x + y; sum = x + y; sum = x + y; sum = x + y; sum = x + y; sum = x + y; sum = x + y; sum = x + y; sum = x + y; sum = x + y; sum = x + y; sum = x + y; sum = x + y; sum = x + y; sum = x + y; sum = x + y; sum = x + y; sum = x + y; sum = x + y; sum = x + y; return sum; } int main() { int x = 10; int y = 20; int z = add(x, y); cout << z << endl; }
所以,内联函数最好还是被调用频繁,而且行数不多的函数。
3. inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。
我们知道,在一个项目中,往往函数的定义和声明不在同一个文件内,所以编译器链接的时候,会有一个符号表,里面会存入函数的地址,然后再调用函数的时候会在符号表中找这个函数的地址从而进行调用。
然而用inline修饰过的函数却不会出现在函数表内,这时编译也不会通过:
这是编译报错,意思是链接错误。
因为每个文件都是自己工作自己的,不会干扰到其他文件,当编译器看到函数被inline修饰的时候就已经不会把地址放在符号表里面了,至于编译器忽略iniline的事情是再调用的时候,所以不用担心。
这个程序当中,头文件虽然在源文件展开,但是并没有函数的定义在源.cpp展开,所以没办法被使用。
当我们把定义和声明放在一起就可以运行了。
auto关键字
类型别名的思考
在C++当中会有一些很长的类型名,例:
#include <string> #include <map> int main() { std::map<std::string, std::string> m{ { "apple", "苹果" }, { "orange", "橙子" }, {"pear","梨"} }; std::map<std::string, std::string>::iterator it = m.begin(); while (it != m.end()) { //.... } return 0; }
std::map<std::string, std::string>::iterator 是一个类型,但是该类型太长了,特别容
易写错。
至于用typedef给类型取别名,那么我们就要记住很多别名,非常麻烦。
auto定义
auto作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。
typeid(b).name()的意思是打印b的类型
#include <iostream> using namespace std; int main() { int a = 10; auto b = a;//这里自动推导a赋值给b的类型是什么 auto c = "a"; auto& d = a; auto* e = &a; cout << typeid(b).name() << endl; cout << typeid(c).name() << endl; cout << typeid(d).name() << endl; cout << typeid(e).name() << endl; return 0; }
注意
使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型。
使用细则
用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&。
#include <iostream> using namespace std; int main() { int a = 10; auto b = &a; auto& d = a; auto* e = &a; cout << typeid(b).name() << endl; cout << typeid(d).name() << endl; cout << typeid(e).name() << endl; return 0; }
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
不能推导的场景
auto不能作为函数的参数
因为函数在创建栈帧的时候编译器无法推导a类型,也就无法得知栈帧创建的具体大小。
auto不能直接用来声明数组
基于范围的for循环
定义
for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。
这里等于把arr数组遍历一遍然后赋值给e。
for里面的arr数组是会遍历整个数组的。
当然如果你想改里面的值就要用引用了,指针是不行的,因为数组里面的都是int类型的。
注意:与普通循环类似,可以用continue来结束本次循环,也可以用break来跳出整个循环。
范围for的使用条件
for循环迭代的范围必须是确定的
对于数组而言,就是数组中第一个元素和最后一个元素的范围。
对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。
int test(int arr[])//因为传过来的不是数组,而是指针,所以无法确定数组的范围 { for (auto& e : arr) cout << e << endl; }
迭代的对象要实现++和==的操作。(这个以后说)
指针空值nullptr
我们平时初始化一个指针如果不知道他指向谁,那么就要让他指向一个空指针(NULL),这是一个良好的习惯。
其实NULL是一个宏,C头文件(stddef.h)中,可以看到如下代码:
#ifndef NULL #ifdef __cplusplus #define NULL 0//也就是说NULL等于0 #else #define NULL ((void *)0) #endif #endif
C++也是一样的,那么使用重载函数的时候就会遇到以下问题:
如果想让NULL走第二个函数就要强制转换类型,因为库不能随意改动,只能打补丁,所以就出现了一个关键字nullptr,效果是和NULL一样的,只不过没有这个BUG。
注意:
- 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
- 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
- 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。