【C++实战】 语言特性

发布于:2023-01-04 ⋅ 阅读:(314) ⋅ 点赞:(0)

自动类型推导

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;// 错误,没有赋值表达式,不知道是什么类型
  • 类成员变量初始化的时候,目前的 C++ 标准不允 许使用 auto 推导类型
    在这里插入图片描述
class X final {
auto a = 10; // 错误,类里不能使用auto推导类型 };

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
  • 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";  // 正确,允许修改

与类相关的 const 用法

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

在这里插入图片描述

关键字 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 
		}
};

在这里插入图片描述

智能指针

  • 指针是源自 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)...));
}

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后最好用注释说明它是个什么,后续该怎么用,否 则会导致后面的代码比较难懂。

本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到