目录
一.内联函数
1.回顾c语言中的“宏函数”
2.内联函数
3.内联函数的特性
二.C++ auto 关键字
1.auto的基本概念
2.auto使用的注意事项
3.auto不能使用的地方
三. C++11中的 nullptr
一.内联函数
1.回顾c语言中的“宏函数”
先给出一段简单的代码:
int Add(int left, int right) { return left + right; } int main() { int ret = 0; ret = Add(1, 2); return 0; }
转到汇编代码:
可见为了实现一个简单的加法,调用Add函数要执行的汇编指令很多,而且为了调用函数还要执行指令跳转(并且要在栈区上为函数开辟栈帧空间),如果Add函数被重复大量地使用,则会消耗很大一部分系统性能。因此C语言中为了提高程序的运行效率,对于类似的简单函数(注意仅限于非递归且简短的函数),我们常使用宏来替代:
#define Add(X,Y) ((X)+(Y)) int main() { int ret = 0; ret = Add(1, 2); return 0; }
宏的作用相当于代码语句的替换,上面代码段中的宏,是把Add(X,Y)形式的语句替换成((X)+(Y)),这种替换的过程是在预处理的阶段完成的。
使用宏替换后,运行上面的代码段,转到反汇编:
可见使用宏代替那些简短的非递归(且会被大量使用)的函数可以一定程度上提高程序的性能
但是由于宏的本质是代码替换,所以有时候会让代码变得混乱难以维护,而且宏本身的使用容易出错,C++就提供了一种类似的语法机制--内联函数来代替宏。
2.内联函数
inline关键字修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的汇编指令处(call指令)将被调函数展开成一系列汇编指令并在主函数的栈帧空间中实现被调函数的功能(类似于宏替换,但不是在预处理的阶段完成的),系统无需为被调函数建立函数栈帧,从而提升了程序运行的效率。
用之前的例子举例说明,使用inline修饰Add函数前:
使用inline修饰Add函数后:
可见系统并没有为inline Add函数建立函数栈帧,也没有执行任何指令跳转,程序性能有所提升。(但是注意,Add函数的函数体(包含其指令段)依然被原模原样地存放在只读常量区,只是编译器在编译时将函数体中必要的指令“搬”到了主函数的指令段中取代了call指令)。
3.内联函数的特性
(1)内联函数是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用能够完成函数功能的指令段替换调用函数的call指令。
缺陷:可能会使目标文件变大(汇编指令是要占内存的,编译器用一系列指令段替换call指令会使文件的总指令条数增加。)
优势:少了调用函数的系统开销,提高程序运行效率。
(2)inline只能修饰一些功能简单,代码段简短的非递归函数,如果inline用于修饰一个复杂的函数,则编译器在编译时会自动忽略inline关键字,所以inline对于编译器而言只是一个建议性的关键字而不是要强制执行的命令。
(3)内联函数(被inline修饰的函数)不建议将其声明和定义分离(只用定义即可,定义本身也是一种声名),分离会导致链接错误。因为使用inline,调用函数时call指令被替换了,没有call指令,链接器就链接不到函数体的指令段了。(因此内联函数一般和主函数定义在同一个源文件中)
二.C++ auto 关键字
1.auto的基本概念
C++11中,auto用于定义变量,auto定义的变量的类型由变量定义和初始化语句等号的右边的值的类型决定,auto作为一个类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。
比如:int TestAuto() { return 10; } int main() { int a = 10; auto b = a; //变量b c d的类型由编译器根据等号右边的值自动识别 auto c = 'a'; //typeid(b).name() 是一个可以返回类型名字符串的方法 auto d = TestAuto(); cout << typeid(b).name() << endl; cout << typeid(c).name() << endl; cout << typeid(d).name() << endl; return 0; }
在面向对象的复杂编程中, 有时我们很难确定一个表达式的返回值会是什么类型的,这种时候就可以用auto声明的变量来接收表达式的值。还有些时候,C++编程中,表达式的值是一些很复杂的自定义类型值,这时也可以用auto声明的变量来接收表达式的值。
比如如下场景:
#include <iostream> #include <time.h> #include <string> #include <map> using std::cout; using std::endl; int main() { std::map<std::string, std::string> m{ { "apple", "苹果" }, { "orange","橙子" }, {"pear","梨"} }; auto it = m.begin(); 用auto声名的变量it来接收表达式的返回值 while (it != m.end()) { //.... } return 0; }
上面代码段中的std::map<std::string, std::string>就是一个复杂的类型,m.begin()返回值就可以用auto声名的变量it来接收,十分方便。
typedef(类型重定义)也可以简化上述代码,但是typedef用于重定义指针类型并用于声明变量时,无法用const来保护指针所指向内存空间。
比如:
typedef char* pstring; int main() { const pstring p1=NULL; return 0; }
因此typedef的使用也是由局限性,相比之下auto的使用更方便灵活。
2.auto使用的注意事项
(1) 使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型
(2)auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型
(3)用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须
加&
比如:int main() { int x = 10; auto a = &x; auto* b = &x; a,b的类型最终是一样的 auto& c = x; 想定义引用必须在auto后面加上& cout << typeid(a).name() << endl; cout << typeid(b).name() << endl; cout << typeid(c).name() << endl; *a = 20; *b = 30; c = 40; return 0; }
(4) 当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量
比如:
void TestAuto() { auto c = 3, d = 4.0; 该行代码会编译失败,因为c和d的初始化表达式类型不同 }
3.auto不能使用的地方
1. auto不能作为函数的参数
2. auto不能直接用来声明数组
比如:
void TestAuto() { int a[] = {1,2,3}; auto b[] = {4,5,6}; 无法通过编译 }
三. C++11中的 nullptr
在良好的C/C++编程习惯中,声明一个变量时最好给该变量一个合适的初始值,否则可能会出现不可预料的错误,比如未初始化的指针。在C语言中,如果一个指针没有合法的指向,我们基本都是按照如下方式对其进行初始化:
void TestPtr() { int* p1 = NULL; // …… }
NULL实际是一个宏,在编译时NULL会被替换为整形0,因此使用NULL在类型上并不严谨,在一些极端情形下可能会导致错误。
于是C++11 将nullptr作为新关键字引入,用于表示指针空值。
为了提高代码的健壮性,在表示指针空值时建议最好使用nullptr
void TestPtr() { int* p1 = nullptr; // …… }