【走进C++ (4)】内联函数 auto 范围for 空指针nullptr
文章目录一、内联函数1、概念2、特性3、浅谈宏的优缺点和替代方法二、auto 关键字1、概念2、使用场景3、不能推导的场景三、基于范围的 for 循环1、概念2、用法3、使用条件四、指针空值 nullptr1、概念2、注意事项3、总结一、内联函数1、概念调用函数需要进行参数传递、建立和销毁函数栈帧并保存、恢复部分寄存器因此会带来一定的函数调用开销。。为了减少函数调用带来的开销C引入了内联函数。以inline修饰的函数叫做内联函数编译时C编译器可能会在调用内联函数的地方展开没有了函数调用建立栈帧的开销内联函数就提升程序运行的效率。内联函数是在编译阶段完成展开而不是程序运行时进行替换。具体的代码如下#include iostream using namespace std; inline int Add(int a, int b) { return a b; } int main() { int x 1, y 0; int z Add(x, y); return 0; }我们在int Add(int a, int b)函数前面增加了inline表示建议将Add函数设置为内联函数。那为什么是建议呢这就需要进一步了解内联函数的特性。2、特性1、空间换时间。inline是一种以空间换时间的做法如果编译器将函数当成内联函数处理在编译阶段会用函数体替换函数调用。它减少了函数调用开销提高程序运行效率但也可能会使目标文件变大。比如有一个10行的函数代码此时有1万次调用该函数1如果用内联的方式展开函数那么编译后就会产生10*1万条代码即目标代码会明显增多。2如果不展开编译后代码就会有101万条代码。2、inline对于编译器而言只是一个建议。不同编译器关于inline实现机制可能不同一般情况下建议将函数规模较小即函数不是很长具体没有准确的说法取决于编译器内部实现、不是递归、且频繁调用的函数采用inline修饰否则编译器会忽略inline特性。下图为《Cprime》第五版关于inline的建议3、inline不建议声明和定义分离。inline分离会导致链接错误因为inline函数需要在调用处能够看到完整定义编译器才能决定是否展开。如果将声明和定义分离其他源文件无法看到函数定义就可能导致链接错误。具体验证如下Add.h// 在同一个文件中不重复包含 #pragma once #include iostream using namespace std; inline int Func(int a, int b);Add.cpp#include Add.h int Func(int a, int b) { return a b; }test.cpp#include Add.h int main() { Func(1, 2); return 0; }运行结果如下在编译后会产生Add.o和test.o又因为Add.h使用内联所以在test.o中展开了函数由于test.cpp中只有函数声明没有函数定义编译器无法展开函数而Add.cpp中的函数又被定义为inline最终可能导致链接失败。因此无法找到Func()函数所以编译失败。为了防止链接冲突或者失效有以下方式解决1声明和定义分离。2static静态修饰函数时只会在当前文件可见即改变链接属性。3inline在头文件中定义该函数就不会发生上述的问题所以建议小函数使用内联inline大函数使用静态static。3、浅谈宏的优缺点和替代方法1优点a增强代码的复用性。b提高性能。2缺点a不方便调试宏。因为预编译阶段进行了替换b导致代码可读性差可维护性差容易误用。c没有类型安全的检查 。3C中替代宏的技术a常量定义 换用const、enumb短小函数定义 换用 内联函数二、auto关键字1、概念在早期C/C中auto的含义是使用auto修饰的变量是具有自动存储器的局部变量会在函数结束以后自动销毁但由于局部变量默认就是自动存储期因此并没有存在的意义。而随着程序越来越复杂变量类型也越来越复杂经常会出现类型难以书写、可读性较差甚至容易写错等问题。在C11中auto就孕育而生了。标准委员会赋予了auto全新的含义即auto不再是一个存储类型指示符而是作为一个新的类型指示符来指示编译器auto声明的变量必须由编译器在编译阶段推导而得即根据初始化表达式自动推导变量的实际类型。#include iostream using namespace std; int main() { int i 0, j 0; auto k i; // 必须初始化根据右侧值进行推导 auto p i; auto* p1 i; // 带 * 表示该类型一定是指针 auto r i; // 带 表示该类型一定是引用 const int a 10; auto b a; // b的类型是int auto c a; // c的类型是const int // 使用typeid就可以查看类型 cout typeid(p1).name() endl; cout typeid(r).name() endl; return 0; }运行结果如下使用auto定义变量时必须对其进行初始化在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。auto后带有*或时推导得到的类型一定分别是指针类型和引用类型。需要注意的是auto推导时会忽略顶层const但保留底层const。2、使用场景auto可以自动推导类型遇到很长很复杂的类型时就可以使用auto使代码和变量定义更加简洁、易读。在实际开发中auto最常与C11提供的范围for循环、lambda表达式以及STL容器迭代器配合使用。#include iostream #include string #include map using namespace std; void func(int x, int y) { cout x y endl; } int main() { // 函数指针的定义 void(*pf1)(int, int) func; auto pf2 func; // 查看类型 cout typeid(pf2).name() endl; // 容器和迭代器的搭配 std::mapstd::string, std::string dict; std::mapstd::string, std::string::iterator it dict.begin(); auto it1 dict.begin(); cout typeid(it1).name() endl; return 0; }运行结果如下typeid不同编译器通过typeid输出的类型名称可能不同对于复杂类型也不够直观因此使用auto时仍需了解变量的实际类型。虽然auto能简化代码但也不应滥用特别是在多层函数返回类型推导时容易降低代码的可读性。具体的代码如下auto f1() { int a 1; return a; } auto f2() { // 进行其他更多的操作... return f1(); } auto f3() { // 进行其他更多的操作... return f2(); } int main() { // 此时如果去使用x找其类型就会变得很复杂 // 需要先查f3再到f2再到f1才能知道是什么类型 auto x f3(); return 0; }3、不能推导的场景1auto不能作为函数的参数。在C11中auto不能作为普通函数的形参类型但在泛型lambda和 C20 的缩写函数模板中允许作为函数参数。// 此处代码编译失败auto不能作为形参类型因为编译器无法对a的实际类型进行推导 void TestAuto(auto a) {}2auto不能直接声明数组类型但可以推导std::initializer_list。int a[] {1,2,3}; auto b[] {4,5,6}; // 错误 auto x {1,2,3}; // 这样才可以3为了避免与C98中的auto发生混淆C11只保留了auto作为类型指示符的用法。三、基于范围的for循环1、概念传统for循环需要程序员自己控制循环变量、循环终止条件以及下标当遍历数组或容器时代码比较繁琐并且容易因为循环边界写错而产生越界等问题。int main() { int arr[] { 1, 2,3,4,5 }; int sz sizeof(arr) / sizeof(arr[0]); for(int i 0; i sz; i) { cout arr[i] ; } cout endl; return 0; }对于这种只需要依次访问每个元素的场景程序完全可以自动完成遍历。因此C11引入了基于范围的for循环它可以依次取出范围中的每个元素赋值给迭代变量并自动完成迭代和结束判断。语法如下for ( auto 变量名 : 数组/容器)for循环后的括号由冒号分为两部分第一部分是范围内用于迭代的变量第二部分则表示被迭代的范围。2、用法范围for具体使用方法如下int main() { int arr[] { 1, 2,3,4,5 }; // 使用引用可以避免拷贝带来的额外开销还能直接修改数组元素 for (auto e : arr) { cout e ; e * 2; cout e endl; } return 0; }运行结果如下当遍历对象较大时使用引用还能避免元素拷贝提高程序运行效率和普通循环类似可以用continue来结束本次循环也可以用break来跳出整个循环。3、使用条件1范围for需要能够确定待遍历对象的起始位置和结束位置。对于数组而言编译器能够自动确定数组的起始位置和结束位置对于类类型应提供begin()和end()这样范围for才能完成遍历。错误的方式下面的代码for的范围不确定。void TestFor(int array[]) { // array[]会退化成指针编译器无法获取数组长度 for(auto e : array) cout e endl; }2begin()返回的迭代器需要支持递增、解引用*以及比较通常是!等操作这样范围for才能够完成遍历。四、指针空值nullptr1、概念在良好的C/C编程习惯中声明一个变量时最好给该变量一个合适的初始值否则可能会出现不可预料的错误比如未初始化的指针。如果一个指针暂时没有合法的指向在早期C/C中通常都会将其初始化成NULL或者 0。而NULL实际上是一个宏。在不同语言和实现中它的定义可能有所不同在 C 中可能被定义为((void*)0)而在 C 中通常被定义为整数常量0或0L。由于NULL本质上并不是一种独立的指针类型因此在函数重载等场景下容易产生歧义比如#include iostream using namespace std; void f(int i) { cout f(int) endl; } void f(int* p) { cout f(int*) endl; } int main() { f(0); f(NULL); // C中通常等价于f(0)调用函数时会调用f(int i) f((int*)NULL); // 所以引入nullptr它具有独立的类型可以准确匹配指针类型 f(nullptr); int* p nullptr; return 0; }运行结果如下程序本意是想通过f(NULL)调用指针版本的f(int*)函数但是由于NULL被定义成0与程序的初衷相悖因此就出现了nullptr。nullptr的类型为std::nullptr_t可以隐式转换为任意指针类型但不能隐式转换为整数类型bool除外。2、注意事项1在使用nullptr表示指针空值时不需要包含头文件因为nullptr是C11作为新关键字引入的。2在主流编译器中sizeof(nullptr)与sizeof((void*)0)所占的字节数相同。3为了提高代码的健壮性在后续表示指针空值时建议使用nullptr。3、总结NULLnullptr本质是宏关键字通常定义为0类型为std::nullptr_t可能匹配整数重载优先匹配指针重载C 和 C 都存在C11 新增