自动类型推导
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 Initialization
)RAII。
认识 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 块放在后面,与函数体 同级并列。
其好处:不仅能够捕获函数执行过程中所有可能产生的异常,而且少了一级缩 进层次,处理逻辑更清晰。
谨慎使用异常
几个应当使用异常的判断准则:
- 不允许被忽略的错误;
- 极少数情况下才会发生的错误;
- 严重影响正常流程,很难恢复到正常状态的错误;
- 无法本地处理,必须“穿透”调用栈,传递到上层才能被处理的错误。
比如 构造函数 护士和失败,杜文杰 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 后查看