• 【C++实战】 语言特性


    自动类型推导

    auto

    因为C++ 是一种静态强类型的语言,任何变量都要有一个确定的类型,否则就不能用。在声明变量的 时候,必须要明确地给出类

    • 就是关键字 auto,在代码里的作用像是个“占位符”(placeholder)。 写上它,你就可以让编译器去自动“填上”正确的类型。
    • 因为 C++ 太复杂,“自动类型推导”有时候可能失效,给不出你想要 的结果, 比如把下图的 std::string 推导成了 const char[6].
    • 在这里插入图片描述

    在这里插入图片描述

    • 除了简化代码,auto 还避免了对类型的“硬编码”,也就是说变量类型不是“写死”的, 而是能够“自动”适应表达式的类型。
      注意
    • auto 的“自动推导”能力只能用在“初始化”的场合(就是赋值初始化或者花括号初始化(初始化列表、Initializer list)), 这样你才能在左边放上 auto,编译器才能找 到表达式,帮你自动计算类型。
    • 只是“纯”变量声明,那就无法使用 auto。因为这个时候没有表 达式可以让 auto 去推导
    auto x = 0L; // 自动推导为long
    auto y = &x;  // 自动推导为long* 
    auto z {&x};// 自动推导为long*
    auto err;// 错误,没有赋值表达式,不知道是什么类型
    
    • 1
    • 2
    • 3
    • 4
    • 类成员变量初始化的时候,目前的 C++ 标准不允 许使用 auto 推导类型
      在这里插入图片描述
    class X final {
    auto a = 10; // 错误,类里不能使用auto推导类型 };
    
    • 1
    • 2

    auto 的推导规则,保证它能够按照你的意思去工作。

    • auto 总是推导出“值类型”,绝不会是“引用”;
    • auto 可以附加上 const、volatile、*、& 这样的类型修饰符,得到新的类型。
      在这里插入图片描述

    decltype

    • decltype 的形式很像函数,后面的圆括号里就是可用于计算类型的表达式(和 sizeof 有点 类似),其他方面就和 auto 一样了,也能加上 const、*、& 来修饰。
    • 已经自带表达式,所以不需要变量后面再有表达式,也就是说可以直接声明变量
      在这里插入图片描述
    • decltype 还可以直接从一个引用类型的变量推导出引用类型,而 auto 就会把引用去掉,推导出值类型。
    • 特别在用于初始化的时候,表达式要重复两次 (左边的类型计算,右边的初始化),把简化代码的优势完全给抵消了。所以,C++14 就又增加了一个“decltype(auto)”的形式,既可以精确推导类型,又能像 auto 一样方便使用。

    在这里插入图片描述

    auto 和 decltype

    • auto 还有一个“最佳实践”,就是“range-based for”,不需要关心容器元素类型、迭代器返回值和首末位置,就能非常轻松地完成遍历操作。不过,为了保证效率,最好使 用“const auto&”或者“``auto&`”。

    在这里插入图片描述

    • C++14 里,auto 还新增了一个应用场合,就是能够推导函数返回值,这样在写复杂函 数的时候,比如返回一个 pair、容器或者迭代器,就会很省事。
      在这里插入图片描述
    • auto 的高级形式,更侧重于编译阶段的类型计算,所以常用在泛型编程里,获取各种类型,配合 typedef 或者 using 会更加方便。当你感觉“这里我需要一个特殊类型”的时 候,选它就对了。
    • auto 的高级形式,更侧重于编译阶段的类型计算,所以常用在泛型编程里,获取各种 类型,配合 typedef 或者 using 会更加方便。当你感觉“这里我需要一个特殊类型”的时 候,选它就对了。
    • 定义函数指针 就可以用到 decltype
      在这里插入图片描述+ 在定义类的时候,因为 auto 被禁用了,所以这也是 decltype 可以“显身手”的地方。它 可以搭配别名任意定义类型,再应用到成员变量、成员函数上,变通地实现 `auto 的功能

    在这里插入图片描述

    const/volatile/mutable 常量与变量

    在这里插入图片描述

    const/volatile

    • const : 表示“常量”。最简单的用法就是,定义程序用到的数字、字符串常量,代替宏定义。
    • const 定义的常量在预处理阶段并不存在,而是直到运行阶段才会出现。
    • const 只读变量 无法修改。虽然可以用获取变量地址 进行强制写入,但破坏了变量性 ,不推荐。
    // 需要加上volatile修饰,运行时才能看到效果 
    const volatile int MAX_LEN = 1024; // 如果 volatile  就不会修改其内容
    auto ptr = (int*)(&MAX_LEN); 
    *ptr = 2048;
    cout << MAX_LEN << endl; // 输出2048
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • volatile 会禁止编译器做优化,所以除非必要,应当少用 volatile,把 const 理解成 read only(虽然是“只读”,但在运行阶段没有什么是不可以改变的,也可以强制写入),把变量标记成 const 可以让编译器做更好的优化。##+

    基本的 const 用法

    在编译阶段防止有意或者无意的修改。

    int x = 100;
    const int& rx = x; // 常量引用  用它作为入口参数,一来保证效率,二来保 证安全
    const int* px = &x; //常量指针
    
    
    //常见的用法是,const 放在声明的最左边,表示指 向常量的指针。
    string name = "uncharted"; 
    const string* ps1 = &name; // 指向常量 
    *ps1 = "spiderman"; // 错误,不允许修改
    
    // const 在“*”的右边,表示指针不能被修改,而指向的 变量可以被修改:
    //不建议使用
    string* const ps2 = &name; // 指向变量,但指针本身不能被修改 
    *ps2 = "spiderman";  // 正确,允许修改
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    与类相关的 const 用法

    class DemoClass final { 
    private:
    	const long MAX_SIZE = 256;	 // const 成员变量
    	int m_value; 				//成员变量
    public: 
    	int get_value() const { // const 成员函数
    		return m_value;
    //函数的执行过程是 const 的,不会修改对象的状态(即成员变量)成员函数 是一个“只读操作”。
    	}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    在这里插入图片描述

    关键字 mutable

    • mutable 却只能修饰类里面的成员变量,表示变量即使是在 const 对象里,也是可以修改的。
    • 标记为 mutable 的成员不会改变对象的状态,也就是不影响对象的常量 性,所以允许 const 成员函数改写 mutable 成员变量。
    • 对于这些有特殊作用的成员变量,你可以给它加上 mutable 修饰,解除 const 的限制,让任何成员函数都可以操作它
    • 和volatile 一样 慎用!!
    class DemoClass final { 
    private:
    	mutable mutex_type m_mutex;
    public: 
    	void save_data() const {
    		// do someting with m_mutex 
    		}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    在这里插入图片描述

    智能指针

    • 指针是源自 C 语言的概念,本质上是一个内存地址索引,代表了一小片内存区域(也可能 会很大),能够直接读写内存。
    • 因为它完全映射了计算机硬件,所以操作效率高,是 C/C++ 高效的根源。当然,这也是引 起无数麻烦的根源。访问无效数据、指针越界,或者内存分配后没有及时释放,就会导致运 行错误、内存泄漏、资源丢失等一系列严重的问题。
    • 其他的编程语言,比如 Java、Go 就没有这方面的顾虑,因为它们内置了一个“垃圾回 收”机制,会检测不再使用的内存,自动释放资源,让程序员不必为此费心。
    • 其实,C++ 里也是有垃圾回收的,不过不是 Java、Go 那种严格意义上的垃圾回收,而是广义上的垃圾回收,这就是构造 / 析构函数和 RAII 惯用法(Resource Acquisition Is InitializationRAII

    认识 unique_ptr

    • 但它实际上并不是指针, 而是一个对象。所以,不要企图对它调用 delete,它会自动管理初始化时的指针,在离开作用域时析构释放内存。
      在这里插入图片描述
     //它也没有定义加减运算,不能随意移动指针地址,这就完全避免了指针越界等危险操 作,可以让代码更安全:
    ptr1++; // 导致编译错误
    ptr2 += 2;// 导致编译错误
    
    
    //除了调用 delete、加减运算,初学智能指针还有一个容易犯的错误是把它当成普通对象来 用,不初始化,而是声明后直接使用:
    unique_ptr<int> ptr3; // 未初始化智能指针
    *ptr3 = 42 ; // 错误!操作了空指针
    //未初始化的 unique_ptr 表示空指针,这样就相当于直接操作了空指针,运行时就会产生致命的错误(比如 core dump)
    
    
    //你可以调用工厂函数 make_unique() ,强制创建智能指针的时候 必须初始化。 C++14
    auto ptr3 = make_unique<int>(42); assert(ptr3 && *ptr3 == 42);// 工厂函数创建智能指针
    auto ptr4 = make_unique<string>("god of war"); 
    // 工厂函数创建智能指针 assert(!ptr4->empty());
    
    
    template<class T, class... Args> // 可变参数模板
    std::unique_ptr<T>  // 返回智能指针 
    my_make_unique(Args&&... args) { // 可变参数模板的入口参数
    	return std::unique_ptr<T>(// 构造智能指针
    	new T(std::forward<Args>(args)...));
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    unique_ptr 的所有权

    • unique_ptr表示指针的所有权是“唯一”的,不允许共享,任何时候只能有一 个“人”持有它。
    • unique_ptr 应用了C++的“转移”(move)语义,同时禁止了拷贝赋值,所以,在向另一个 unique_ptr 赋值的时候,要特别留意,必须用 std::move() 函数显式地声明所有权转移。
    • 尽量不要对 unique_ptr 执行赋值操作,让它“自生自灭”,完全 自动化管理。
      在这里插入图片描述

    认识 shared_ptr

    • shared_ptr:它的所有权是可以被安 全共享的,也就是说支持拷贝赋值,允许被多个“人”同时持有,就像原始指针一样。
      在这里插入图片描述
    • shared_ptr 支持安全共享的秘密在于内部使用了“引用计数”。
    • 引用计数最开始的时候是1,表示只有一个持有者。如果发生拷贝赋值——也就是共享的时候,引用计数就增加,而发生析构销毁的时候,引用计数就减少。只有当引用计数减少到 0,也就是说,没有任何人使用这个指针的时候,它才会真正调用 delete 释放内存。
      在这里插入图片描述
    • 因为 shared_ptr 具有完整的“值语义”(即可以拷贝赋值),所以,它可以在任何场合替代原始指针,而不用再担心资源回收的问题,比如用于容器存储指针、用于函数安全返回动态创建的对象。

    shared_ptr 的注意事项

    • 引用计数的存储和管理都是成本,过度使用 shared_ptr 就会降低运行效率。你也不需要太担 心,shared_ptr 内部有很好的优化,在非极端情况下,它的开销都很小。
    • ** shared_ptr 的销毁动作:**你要特别小心对象的析构函数,不要有非常复杂、严重阻塞的操作。一旦 shared_ptr 在某 个不确定时间点析构释放资源,就会阻塞整个进程或者线程。
      在这里插入图片描述
    • shared_ptr 的引用计数也导致了一个新的问题,就是“循环引用”,这在把 shared_ptr 作为类成员的时候最容易出现,典型的例子就是链表节点
      在这里插入图片描述
    • 两个节点指针刚创建时,引用计数是 1,但指针互指(即拷贝赋值)之后,引用计数都变成了 2
    • shared_ptr 意识不到这是一个循环引用,多算了一次计数,后果就是引用计数无法减到 0,无法调用析构函数执行 delete,最终导致内存泄漏。
    • weak_ptr : 它专门为打破循环引用而设计,只观察指针,不会增 加引用计数(弱引用),但在需要的时候,可以调用成员函数 lock(),获取 shared_ptr(强引用)。

    在这里插入图片描述

    • 1、智能指针是代理模式的具体应用,它使用 RAII 技术代理了裸指针,能够自动释放内存, 无需程序员干预,所以被称为“智能指针”。
    • 2、如果指针是“独占”使用,就应该选择 unique_ptr,它为裸指针添加了很多限制,更加 安全。
    • 3、如果指针是“共享”使用,就应该选择 shared_ptr,它的功能非常完善,用法几乎与原 始指针一样。
    • 4、应当使用工厂函数 make_unique()、make_shared() 来创建智能指针,强制初始化,而 且还能使用 auto 来简化声明。
    • 5、 shared_ptr 有少量的管理成本,也会引发一些难以排查的错误,所以不要过度使用。

    既然你已经理解了智能指针,就尽量不要再使用裸指针、new 和 delete 来操作内存了。

    在这里插入图片描述

    在这里插入图片描述

    exception 用好异常

    • 处理异常的基本手段是“错误码” 如下图 , 看起来很乱 , 可读性差, 可以被忽略,存在安全隐患。
      在这里插入图片描述
    • 异常就是针 对错误码的缺陷而设计的,它有三个特点。
      • 异常的处理流程是完全独立的,throw 抛出异常后就可以不用管了,错误处理代码都集 中在专门的 catch 块里。这样就彻底分离了业务逻辑与错误逻辑,看起来更清楚。
      • 异常是绝对不能被忽略的,必须被处理。如果你有意或者无意不写 catch 捕获异常,那 么它会一直向上传播出去,直至找到一个能够处理的 catch 块。如果实在没有,那就会 导致程序立即停止运行,明白地提示你发生了错误,而不会“坚持带病工作”。
      • 异常可以用在错误码无法使用的场合

    异常的用法和使用方式

    基本的 try-catch 写法:
    在这里插入图片描述

    • try 把可能发生异常的代码“包”起来,然后编写 catch 块捕获异常并处理。

    • 因为 C++ 已经为处理异常设计了一个配套的异常类型体 系,定义在标准库的 头文件里。标准异常的继承体系如下:
      在这里插入图片描述
      在这里插入图片描述
      在这里插入图片描述

    • 最好不要直接用 throw 关键字,而是要封装成一个函数通过引入一个“中间层”来获得更多 的可读性、安全性和灵活性。

    • 属性

    • 使用 catch 捕获异常的时候也要注意,C++ 允许编写多个 catch 块,捕获不同的异常,再 分别处理。但是,异常只能按照 catch 块在代码里的顺序依次匹配,而不会去找最佳匹配 所以,最好只用一个 catch 块, 绕过这个“坑”。

    • catch 块就像是写一个标准函数,所以入口参数也应当使用“const &”的形式,避免对 象拷贝的代价:
      在这里插入图片描述

    • function-try形式 就是把整个函数体视为一个大 try 块,而 catch 块放在后面,与函数体 同级并列。

    • 其好处:不仅能够捕获函数执行过程中所有可能产生的异常,而且少了一级缩 进层次,处理逻辑更清晰。
      在这里插入图片描述

    谨慎使用异常

    几个应当使用异常的判断准则

    1. 不允许被忽略的错误;
    2. 极少数情况下才会发生的错误;
    3. 严重影响正常流程,很难恢复到正常状态的错误;
    4. 无法本地处理,必须“穿透”调用栈,传递到上层才能被处理的错误。
      比如 构造函数 护士和失败,杜文杰 socket通信失败。。。

    保证不抛出异常

    • noexcept 专门用来修饰函数,告诉编译器:这个函数不会抛出异常。编译器看到 noexcept,就得到了一个“保证”,就可以对函数做优化,不去加那些栈展开的额外代 码,消除异常处理的成本。
    • 和 const 一样,noexcept 要放在函数后面:
      在这里插入图片描述
    • noexcept 的真正意思是:“我对外承诺不抛出异常,我也不想处理异常,如果真的有异常发生,直接崩溃(crash、core dump

    lambda

    在这里插入图片描述

    • C++, 所有的函数都是全局的,没有生存周期的 概念(static、名字空间的作用很弱,只是简单限制了应用范围,避免名字冲突)。而且函 数也都是平级的,不能在函数里再定义函数,也就是不允许定义嵌套函数、函数套函数。

    认识lambda

    在这里插入图片描述

    • lambda 表达式除了可以像普通函数那样被调用,还有一个普通函数所不具备的 特殊本领,就是可以“捕获”外部变量,在内部的代码里直接操作。
    • lambda 具有闭包的 性质 简单理解为一个“活的代码块”“活的函数”可以跳离定义点,把这段代码“打包”传递到其他地方去执行,而仅凭函数 的入口参数是无法做到这一点的。能够像函数一样被调用,像变量一样被传递;
      这就导致函数式编程与命令式编程(即面向过程)在结构上有很大不同,程序流程不再是按步骤执行的“死程序”,而是一个个的“活函数”,像做数学题那样逐步计算、推导出结 果.
      在这里插入图片描述

    使用 lambda 的注意事项

    lambda 形式

    • lambda 的形式:[ ],术语叫“lambda 引出符”(lambda introducer)。
    • auto f1 = [](){}; // 相当于空函数,什么也不做
    • 要有良好的缩进格式, 也鼓励程序员尽量**“匿名**”使 用 lambda 表达式。而且因为“匿名”,lambda 表达式调用完后也就不存在了 (也有被拷贝保存的可能),这就最小化了它的影响范围,让代码更加安全。
      在这里插入图片描述

    lambda的变量捕获

    • “[=]”表示按值捕获所有外部变量,表达式内部是值的拷贝,并且不能修改;
    • “[&]”是按引用捕获所有外部变量,内部以引用的方式使用,可以修改;
      在这里插入图片描述
    • “捕获”也是使用 lambda 表达式的一个难点,关键是要理解“外部变量”的含义。“upvalue”,也就是在 lambda 表达式定义之前所有出现的变量,不管它是局部的还是全局的。
    • 变量生命周期的问题 : 对于“就地”(auto 赋值的)使用的小 lambda 表达式, 可以用“[&]”来减少代码量,保持整洁;而对于非本地调用、生命周期较长的 lambda 表 达式应慎用“[&]”捕获引用,而且,最好是在“[]”里显式写出变量列表,避免捕获不必要的变量。
      在这里插入图片描述

    泛型的 lambda

    • C++14 lambda 可以实现“泛型化” 利用了 auto , 摆脱了冗长的模板参数和函数参数列表
    • https://github.com/chronolaw/cpp_study/blob/master/section2/lambda.cpp在这里插入图片描述
      在这里插入图片描述
      在这里插入图片描述

    思考题

    auto 和 decltype 虽然很方便,但用多了也确实会“隐藏”真正的类型,增加阅读时的 理解难度,你觉得这算是缺点吗?是否有办法克服或者缓解?

    用auto后最好用注释说明它是个什么,后续该怎么用,否 则会导致后面的代码比较难懂。

    说一下你对 auto 和 decltype 的认识。你认为,两者有哪些区别呢?(推导规则、应用 场合等)

    用auto后最好用注释说明它是个什么,后续该怎么用,否 则会导致后面的代码比较难懂。

  • 相关阅读:
    (Spring笔记)SpringMVC中前端提交数据到后端的五种注入参数方式
    C++引用
    探索电子元器件商城:从原型到批量生产的选择
    vue 组件基础
    【线代】矩阵的秩
    VS 2019报错无法打开源文件
    B+树索引(3)之索引推导优化
    浅析 Vue3 响应式原理
    HTML5教程之移动端Web页面布局
    视觉语言模型详解
  • 原文地址:https://blog.csdn.net/weixin_49486457/article/details/126554668