2.3特性🐱
1、inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提升程序运行效率。
2、inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建议:将函数规模较小(即函数不是很长,具体没有准确的标准,取决于编译器内部实现)、不是递归、且频繁调用的小函数采用inline修饰,否则编译器会忽略inline特性。下图为《C++prime》第五版关于inline的建议:
3、inline不建议声明和定义分离,分离会导致链接错误。因为inline在预处理阶段被展开,没有有效函数地址了,链接会找不到。
4、如果函数调用过多,比如有10000个调用的地方,这时候就不建议使用内联函数了。因为比如说内联函数有30行代码,inline展开就有30W行代码,而这些代码是冗余的,无用重复代码,他会大大增加可执行程序的大小。这时就使用普通函数即可,它只需要消耗10000+30行代码。
对于第三点,我们可以在编译器上来查看一下:
// F.h #include <iostream> using namespace std; inline void f(int i); // F.cpp #include "F.h" void f(int i) { cout << i << endl; } // main.cpp #include "F.h" int main() { f(10); return 0; } // 链接错误:main.obj : error LNK2019: 无法解析的外部符号 "void __cdecl f(int)" (?f@@YAXH@Z),该符号在函数 _main 中被引用
这里为什么会报链接错误呢?我们在学习C语言预处理时知道,程序执行要经过预处理-->编译-->汇编-->链接这几个阶段,而链接要完成的任务是:
1、合并段表,相同的段进行合并;
2、符号表的合并和重定义,选取有效地址。
这里报错是因为符号表合并找不到有效地址而出错。为什么找不到呢?
因为在C++编译器中,当编译器遇到inline关键字后,除了会替换之外,内联函数就不会进入符号表,(这一点和宏是契合的,预处理之后不进入符号表)所以当替换的是内联函数的声明,而不是定义且在当前文件下找不到定义的时候,就会出现错误。
所以这里的建议是,编写内联函数在头文件的时候,不要声明,直接定义在头文件。
如果内联函数是解决宏函数缺陷的问题,那么对于宏定义的比如常量等其他问题我们是如何解决的呢?
C++通常用这些技术替代宏:
1、常量定义换用const enum
2、短小函数定义 换用内联函数。
三、auto关键字🐅
3.1类型别名思考🐱
为什么引入auto这个关键字呢?因为随着程序越来越复杂,程序中用到的类型也越来越复杂,经常体现在:
1、类型难于拼写
2、含义不明确导致容易出错
比如:
#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给类型取别名,比如说: #include <string> #include <map> typedef std::map<std::string, std::string> Map; int main() { Map m{ { "apple", "苹果" },{ "orange", "橙子" }, {"pear","梨"} }; Map::iterator it = m.begin(); while (it != m.end()) { //.... } return 0; }
但是还是不够方便。在编程时,常常需要把表达式的值赋给变量,这就要求在声明变量的时候清楚地知道表达式的类型。然而有时候要做到这点并不容易,因此C++11给auto赋予了新的含义。
3.2auto简介🐱
auto会根据表达式的类型自行调整为对应的类型,auto 声明的变量必须由编译器在编译时期推导而得 。
int TestAuto() { return 10; } int main() { int a = 10; auto b = a; auto c = 'a'; auto d = TestAuto(); cout << typeid(b).name() << endl; cout << typeid(c).name() << endl; cout << typeid(d).name() << endl; return 0; }
这里的typeid().name()的作用是拿到变量的类型。
注意:
使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种"类型"的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型。
3.3 auto的使用细则🐱
①.auto与指针和引用结合起来使用😀
用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时必须加&
int main() { int x = 10; auto a = &x; auto* b = &x; auto& c = x; cout << typeid(a).name() << endl; cout << typeid(b).name() << endl; cout << typeid(c).name() << endl; *a = 20; *b = 30; c = 40; return 0; }
②.在同一行定义多个变量😀
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译
器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量 。
3.4 auto不能推导的场景🐱
①auto不能作为函数的参数😀
void Func(auto x) { printf("Func(int x)\n"); } void Func(int x) { printf("Func()\n"); } int main() { Func(5.2); Func(5); return 0; } //错误 C2668 “Func”: 对重载函数的调用不明确
这里,很显然,如果使用auto,编译器是无法区分参数类型的,因为auto任何类型都可以兼容,这样和函数重载也会冲突。
②auto不能直接用来声明数组😀
void TestAuto() { int a[] = { 1,2,3 }; auto b[] = { 4,5,6 }; } //错误 C2119 "b": 无法从空的初始值设定项推导 "auto []" 的类型 //错误 C3318 “auto []”: 数组不能具有其中包含“auto”的元素类型
为了避免与 C++98 中的 auto 发生混淆, C++11 只保留了 auto 作为类型指示符的用法。
auto 在实际中最常见的优势用法就是跟以后会讲到的 C++11 提供的新式 for 循环,还有
lambda 表达式等进行配合使用。
四、基于范围的for循环🐅
4.1范围for的语法🐱
在C++98中如果要遍历一个数组,可以按照以下方式进行:
void TestFor() { int array[] = { 1, 2, 3, 4, 5 }; for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i) array[i] *= 2; for (int* p = array; p < array + sizeof(array)/ sizeof(array[0]); ++p) cout << *p << endl; }
对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时还会容易犯错,因此C++11中引入了基于范围的for循环。for循环后的括号由冒号":"分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。
比如我们想打印一个数组,这样就可以:
void TestFor() { int array[] = { 1, 2, 3, 4, 5 }; for (auto e : array) cout << e << " "; return; }
但是,如果我们想对数组里的元素操作,这样是否可以呢?
我们发现,数组里的数据并没有乘以2,说明我们遍历的数组,不是数组本身,而是一个临时拷贝,并不影响数组的值,所以我们如果想对数组里的数操作,需要使用引用:
注意:范围for与普通循环类似,是可以用continue来结束本次循环,也可以用break来跳出整个循环。
4.2范围for的使用条件🐱
①.for循环迭代的范围必须是确定的😀
对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。
注意:对于传参的数组,是不能用范围for循环的。
void TestFor(int a[]) { for (auto& e : a) cout << e << endl; } int main() { int a[] = { 1,2,4,5,6,7 }; TestFor(a); return 0; }
像这样,传过去的只是一个指针,也就是数组首个元素的地址。
五、指针空值nullptr(C++11)🐅
5.1 C++98中的指针空(NULL)🐱
我想先问一个问题,NULL是一个地址吗?如果你觉得NULL是一个地址,那么请看以下代码:
如果NULL是地址,那么为什么会调用第二个函数而打印出f(int)呢?
这其实和C++98的bug有关,C98中对于NULL的定义,我们可以在头文件(stddef.h)中看到
#ifndef NULL #ifdef __cplusplus #define NULL 0 #else #define NULL ((void *)0) #endif #endif
我们看到,NULL实际是个宏,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量,上面很显然程序将它作为0来处理而不是空值的指针。
5.2 空值指针nullptr🐱
为了应对这种错误,C++11引入了nullptr,将其定义为(void*)0,它是一个关键字,不需要引头文件。
注意:
1、在C++11中,sizeof(nullptr)
与sizeof((void*)0)
所占的字节数相同。
2、为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。