C+±——11
c++11是c++ 在2011年出的新标准,是对c++的一次较大的优化和扩展,往标准库里面加入了非常多
提升开发效率的方法和工具。也是现在市面上用的最多的一个版本。面试往往c++11新标准
会是重中之重,也比较能考验对c++的理解。
其实绝大多数的知识点,对大多数程序员来说都是没有任何作用,只有涉及到类的设计,框架的设计才能用得到,
这一点对于学习者来说真的是有一点痛苦,但是学会了才可能用得到,学不会,即使有机会也没法用。
只能硬着头皮去掌握那些对暂时工作没有任何影响的知识点,突破了这个瓶颈,后面会好走很多。
//----------------------------------------------------------------------------------------------------
类型推导 auto 和 decltype
auto 类型可以在声明变量的时候大大的节省开发效率,当然,个人认为,可读性可能会些许下降。
auto 可以根据变量类型,函数返回值类型自动推导出一个最匹配的类型。
例如:
auto a = 1; // int
auto b = “1234”; // char*
auto c = sizeof(a);// uint32_t
// 类似于上面的这些情况都能做出正确的推导。
decltype 也有类似都作用
例如:
decltype(a) d; // int
decltype(1) e; // int
decltype(“asd”) f; // 这里编译不过去,通常会被解析为const char* 需要初始化的
两者用法的区别就是,auto 声明的时候就做了定义,但是 decltype 可以只声明,不定义
//----------------------------------------------------------------------------------------------------
空指针 nullptr
之前空指针返回的是 NULL 但是 NULL 就是0的宏定义,并不能真正表示为空指针 但是 nullptr 则可以
例如
void pp(void* a)
{
std::cout << “void*” << std::endl;
}
void pp(int a)
{
std::cout << “int” << std::endl;
}
pp(NULL); // 报错,它无法匹配到任何一个函数
pp(nullptr); // 正确,可以匹配到void*
//----------------------------------------------------------------------------------------------------
using 的新用法
前面说的关键字 typedef 给类起别名,using 的新用法也可以
例如
typedef int INT;
using INT = int;
// 上面两条是等价的
既然有了 typedef ,又来一个 using 新用法,c++官方当然不是闲的
因为 typedef 也有它的局限性
例如
template
using V = std::vector; // 正确
typedef std::vector V; // 错误,typedef只能用在确定的类,不能用模板
V v = {1,2,3};
// 这个例子就很明显的证明了,c++11的每个点都不是无缘无故的,这群人确实有一点骚气
//---------------------------------------------------------------------------------------------------
final
//final 有两个作用,一个是禁止类成为父类
例如
class A final
{};
class B : public A{}; // A不能被继承
//再就是不允许虚函数被覆盖
例如
class A
{
public:
virtual void p() final {}
};
class B : public A
{
public:
virtual void p() {}; // 错误,加了final 关键字,不允许被覆盖
};
//---------------------------------------------------------------------------------------------------
override
// 这个关键字前面也提了很多次了,作用是判断当前函数是否覆盖了父类的虚函数
例如
class A
{
public:
virtual void p() {};
void p1(){};
};
class B : public A
{
public:
void p() override {} // 正确,p()是父类的虚函数
void p1() override {} // 错误 p1()不是父类的虚函数
};
default 和 delete
这两个关键字的用法之前也提到过
default 的作用是生成默认构造函数
例如
class Base
{
public:
//无参构造函数
Base() = default;
//拷贝构造函数
Base(const Base& b) = default;
//移动构造函数
Base(Base&& b) = default; // 这里有一个右值引用,后面会提
//复制赋值操作符重载函数
Base& operator=(const Base& b) = default;
//移动赋值操作符重载函数
Base& operator=(Base&& b) = default;
};
用 default 生成的默认构造函数和c++提供的默认构造函数是等价的,这样可以减少很多代码量,增加可读性。
// 还记得为什么需要生成默认构造函数吗,可以回头看看前面的章节
delete 的作用是显示的禁用某些函数
例如
class Base
{
public:
Base() = delete; // 禁用无参构造函数
};
Base a; // 错误,被禁用了
//----------------------------------------------------------------------------------------------------
数组的 for 遍历 支持 普通数组 vector list set deque quque 等
for (类型 变量 : 数组) {
// 变量
}
例如
int array[] = {1,2,3};
for (auto i : array) {
std::cout << i << std::endl;
}
// 1 2 3
// 这种新的用法在效率上和传统都for循环比效率略微低了一点,但是书写方便
// 关于效率上的问题,可以自己做实验验证一下
//----------------------------------------------------------------------------------------------------
容器的赋值
在c++11版本之前,容器的初始化都是只能通过对应的操作才能完成,但是现在c++11提供了新的版本
例如:
std::map<int, int> m = {{1,1},{2,2}};
std::vector v = {1,2,3}; // 编译的时候加上-std=c++11 编译正确,但是默认的c++98是编译不过的
//当然 不用 = 号也可以实现相同的功能,包括数组
std::map<int, int> m {{1,1},{2,2}};
std::vector v {1,2,3};
int array[] {1,2,3};
//---------------------------------------------------------------------------------------------------
结构体的初始化
// 例如
class A
{
public:
int a;
char b;
};
A a = {1,‘c’};
// 对于结构体来说,允许使用新的方式进行初始化,进而避免了没必要的代码。
//----------------------------------------------------------------------------------------------------
enum class 枚举类
枚举类是一个强类,调用的时候必须加上 类名::
枚举类的名称不会和普通枚举有命名冲突
例如
enum
{
A,
B
};
// 不会有冲突
enum class EC
{
A,
B
};
EC c = EC::A //相当于枚举和类的结合
//----------------------------------------------------------------------------------------------------
explicit 防止构造函数的隐式转换
例如
class Cxstring {
public:
string _str;
int _size;
explicit Cxstring(int size) {
_size = size;
}
Cxstring(string str) {
_str = str;
}
};
// 都会调用Cxstring(int size)
Cxstring s1 = 1; // 错,explicit取消了隐式转换
Cxstring s2(2); // 显示初始化
Cxstring s6 = ‘c’;// 错, explicit取消了隐式转换
// 都会调用Cxstring(string str)
string a = “aaaa”;
Cxstring s3(a);
Cxstring s4(“aae”);
Cxstring s5 = a;
// 在给定义的类赋值的时候,explicit 避免了一些隐式转换
这个关键字的作用是做一些必要的限制,防止初始化的时候过于随意。
//----------------------------------------------------------------------------------------------------
右值引用和std::move()语义
左值和右值是c++的一个很重要的概念,定义一个变量,变量名就是左值,变量的值就是右值。
例如
int a = 10; //其中a是左值 10是右值
int b = a; // 其中 a 和 b都是左值。
左值需要记住的就是变量名。
右值就是那些定义完就会自动释放掉的值。
常见的右值有 常量 函数返回值
因为其特性,通常来说,只有左值才有引用。但是引用作为一种重要的提高效率的工具,很多时候右值也是需要引用的。
例如
std::vector get()
{
return std::vector({1,2,3});
}
void p(std::vector& v)
{
for (auto i : v) {
std::cout << i << std::endl;
}
}
想打印返回结果
auto a = get();
p(a);
// 分析上面的结果,再怎么着都无法避免掉这次看上去无用的拷贝。
// 主要是get()的返回值是左值,无法使用引用
这个时候右值引用的效果就出来了
右值引用是使用了2个引用符号
类 && 变量 = 左值。
再把上面的函数改一下
void p(std::vector&& v)
{
for (auto i : v) {
std::cout << i << std::endl;
}
}
p(get()); //完美的减少了一次拷贝
右值引用只能使用右值,左值就没办法使用了
p(a); //报错 a是左值
c++11中新增了一种move()函数,这是一个可以将左值转化为右值的工具。
例如:
p(std::move(a)); //正确 a被转化成了右值。
// move()语义也是c++11的一个重要方法,主要作用是在后面说的独占指针std::unique_ptr 指针,这里不多加赘述。
// 当然,将左值转换成右值也不是一个很好的解决方法,为了左值摒弃右值或者为了右值摒弃左值都
不符合我们的编程思想,c++11追求的是用一些更漂亮,更通用的方式解决更多问题。
// 这里就要说另一个概念了
引用符号的叠加:引用符号是可以叠加的,每增加一个引用符号,都会将转换成另一种引用
例如:
std::vector a = get();
std::vector& b = a; // 左值引用
std::ector&& c = get(); //右值引用
std::vector&&& d = a; //左值引用
这个时候模板函数就可以派上用场了
template
void p(T&& v)
{
for (auto i : v) {
std::cout << i << std::endl;
}
}
// 这里的T&& v 当传入右值的时候,会被推导成 类 && v ,当传入左值的时候,会被推导成 类 & && v
p(a); //p(std::vector&&& v)
p(get()); // p(std::vector&& v)
// 不得不说,模板真的是一种解决通用问题的漂亮工具
右值引用是设计函数的时候一种新增的重要思想,想让使用者舒服的使用设计的方法,将左值和右值都考虑进去
再用模板的方式结局它,是一种很不错的方法,我自己现在在工作的时候,会大量的使用模板,当然每个
人的工作阶段不一样,前期可能很少会用到,但是自己在实现功能的时候可以将这部分考虑进去,会是一个
非常亮眼的加分点。
//-----------------------------------------------------------------------------------------------------
lambda表达式
c++11之前在实现功能的时候,再简单的方法都需要定义一个函数,有很多需要调用函数指针的方法,会增加没必要的
代码负担,非必要情况下,最好不要定义太多函数,可能会导致命名冲突。比如很多文件都调用了compare()函数
在一个大型的项目中,是很难对所有compare()函数做有效管理的。这个时候,lambada表达式就起到了很重要的作用了。
lambda表达式的结构是 捕获列表-> 返回值类型 {函数体}
例如
bool compare(int a, int b) {return a >= b;}
std::vector v = {1,3,2};
std::sort(v.begin(), v.end(), compare);
// 这里需要单独定义一个函数才可以使用排序函数sort
但是在引入lambda表达式之后就可以写成后面的形式
std::sort(v.begin(), v.end(),[](int a, int b)->bool {return a >= b;})
//可以少定义一个compare函数,代码也简单明了很多。
再单独讲一下lambda表达式的几个概念
#捕获值 就是是否需要用到外面的变量
例如
int a = 10;
auto f = ->int {return a++;}; // 报错的,没有定义a
auto f = a->int {return ++a;}; // 正确,返回a
std::cout << f() << std::endl; // 11 返回a+1后的值
std::cout << a << std::endl; // 10,即使是捕获了a,也不会修改a
还有一种别的写法
auto f = =->int {return ++a;}; // 正确 = 相当于隐式的捕获函数前的所有变量,不需要挨个写入
再例如
auto f = &a->int {return ++a;};
std::cout << f() << std::endl; // 11 返回a+1后的值
std::cout << a << std::endl; // 11, 加了引用符号,可以修改a的值。
这里有一个坑,就是lambda表达式有延迟性。
std::cout << f() << " " << a << std::endl;// 11 10 这个原因我不知道为啥,我写的时候发现的
std::cout << a << std::endl; // 11 延迟了一个操作后变成了11
同样,也有另一种写法
auto f = &->int {return ++a;}; // & 会捕获函数前的所有变量,并获得引用
// lambda表达式的用法比较灵活,也是面试经常面的一部分东西,平时写代码的时候,那些没这么通用的代码
尽量用lambda表达式来结局,大一些的公司对代码要求比较高,写出lambda表达式是一个较为明显的加分项
也是框架设计者减少函数名冗余的一个重要工具。
//-----------------------------------------------------------------------------------------------------
智能指针 shared_ptr weak_ptr unique_ptr
智能指针是针对c++最大困难,”指针释放和资源管理“提供的最重要的工具,其中,用的最多的是shared_ptr和weak_ptr,
在c++11之前,非常多的项目都会出现运行时间长后内存占用率直线上升的问题,很大的问题就是申请的堆空间因为
不敢释放,不确定要不要释放等原因,导致运行越久,碎片垃圾越多的问题,而智能指针因为其自动释放,计数管理
等优势被广泛使用。
#unique_ptr 先说一下独占指针
独占指针就是一个指针只能给一个指针变量使用,它的赋值方法只留了一个,而且只能使用普通指针赋值。
例如
std::unique p(new int(10)); // 正确 鼓励使用的一种方式
std::unique p = new int(10); // 错误 独占指针除了std::make_unique()外不能使用 =
再例如
int* p = new int(10);
std::unique a§; // 正确 但是不鼓励使用这种初始化方法,会让独占失去本身的意义
std::unique b§; // 可以编译过但是执行的时候会崩溃
如果需要用独占指针给另一个独占指针赋值,则需要使用std::move()方法,当然,这里仍然不支持使用
std::unique b(std::move(a));
*b = 20;// 正确
*a = 10; // 可以编译过,但是运行时会报错,使用权已经转移到了b上
只能指针具有自动释放的功能,但是独占指针通常来说是不建议使用的,因为会有很多潜在的问题,也会给指针的使用者
带来很多没必要的麻烦,当使用普通指针初始化的方法时,它会失去它的独占属性,也是一个比较大的问题,
根据具体问题具体分析,不是到逼不得已的情况,可以摒弃这种方法,面试过程中,面试官也会花更多时间在另外
两种指针上面,后面就多花一些篇幅讲一下共享指针和弱指针。
#智能指针里的好基友 std::shared_ptr和std::weak_ptr
这两个指针是c++11里面最浓墨重彩的一笔,算是整个c++11最精华的一部分吧。主要是共享指针std::shared_ptr.它的计数属性和自动释放
的能力,给复杂的资源管理提供了非常大的便捷性。
每个 shared_ptr 对象在内部指向两个内存位置:
指向对象的指针。
用于控制引用计数数据的指针。
共享所有权如何在参考计数的帮助下工作:
当新的 shared_ptr 对象与指针关联时,则在其构造函数中,将与此指针关联的引用计数增加1。
当任何 shared_ptr 对象超出作用域时,则在其析构函数中,它将关联指针的引用计数减1。
如果引用计数变为0,则表示没有其他 shared_ptr 对象与此内存关联,
在这种情况下,它使用delete函数删除该内存。
举个例子
class A
{
public:
std::shared_ptr m_p;
};
int main() {
std::shared_ptr p = std::make_shared_ptr(10); // 调用构造函数count++, count = 1;
A a = new A();
a->m_p = p; // 赋值 = ,count++; count = 2;
delete a; // m_p析构 count–; count = 1;
// main结束后,p会调用析构函数 count --; count = 0; 指针自动释放
return 0;
}
//另一种定义
int* p = new int(10);
std::shared_ptr ptr = p; // 错误,类型不同,不能这样赋值;
std::shared_ptr ptr§; // 正确,但是不建议这样赋值,这样做会失去共享指针意义
例如:
std::shared_ptr ptr§; // count = 1;
delete p; // count不会做-1操作
std::cout << ptr.use_count() << std::endl;// count = 1; 计数错误之后,最终会导致程序崩溃
//需要注意的是,两种初始化方式有非常大的区别
// 推荐的std::make_shared<>() 方式,申请的内存空间和计数器空间是一起申请的,内存大概率是连续的
// 而用普通指针初始化智能指针申请的两个空间是分别申请的,大概率是不连续的
// 与普通指针的对比
智能指针是一个类,并不是真正意义的指针,它维护着两个部分,一个是申请的堆地址,另一个是计数数据地址
而普通指针是一个指针类型的变量,是一个内存地址。根据这个前提,就有以下几个区别。
智能指针不支持 ++ 和 – 的操作
智能指针不支持 delete 和 new 的操作
智能指针不支持[]的操作
// 但是,同样智能指针重载了常用的几种操作符
例如
class A
{
public:
int a;
};
std::shared_ptr p = std::make_shared_ptr({1});
if (p == nullptr || p != NULL) { // == 和 != 操作符
}
if (!p) { // 重载 !操作符
}
p = {2}; // 重载 操作符
p->a = 3; // 重载-> 操作符
// 重要的成员函数
use_count() 查看指针计数的数量
例如:
std::shared_ptr p = std::make_shared_ptr(10);
p.use_count(); // 1
reset() 取消指针关联
p.reset(); // 计数指针做-1操作
reset(ptr) 重新绑定新的指针
p.reset(std::make_shared_ptr(20)); // 和原来的指针关联取消,重新绑定新的指针
= nullptr 重置指针
p = nullptr; //设置为空指针
// 共享指针根据其计数功能完成智能的内存管理,但是因为只要使用到了内存的空间就要做+1的处理
也会导致一些释放上的问题,可以看一下下面的例子
例如:
class A
{
public:
std::shared_ptr p;
};
class B
{
public:
std::shared_ptr p;
};
std::shared_ptr pA = std::make_shared(); // countA = 1;
std::shared_ptr pB = std::make_shared(); // countB = 1;
pA->p = pB; // countB = 2
pB->p= pA; // countA = 2
// 释放 countA–; countB–; 但是countA = 1 countB = 1;不能被释放掉
// 这个就是所谓的智能指针循环引用问题,这也是引入弱指针的原因
将B做一个修改
class B
{
public:
std::weak_ptr p;
};
std::shared_ptr pA = std::make_shared(); // countA = 1;
std::shared_ptr pB = std::make_shared(); // countB = 1;
pA->p = pB; // countB = 2
pB->p= pA; // countA = 1 因为弱指针不会对让计数器做+1处理
// 释放
// countA --; countB–;
// countA = 0,pA释放,countB–;
// countB = 0, pB释放
// 就这样做到了完美释放
所以这里可以讲一下弱指针
std::weak_ptr<>()
弱指针是针对共享指针循环引用问题的一种指针,它的初始化只能使用使用只能指针初始化
例如
class A
{
public:
int a;
};
std::shared_ptr ptr = std::make_shared({10});
std::weak_ptr p1(ptr);
std::weak_ptr p2 = ptr;
// 弱指针只有数据的观测权和修改权,通过lock()方法得到共享指针的内存
例如
if (p1.lock()) {
p1.lcok()->a = 20; // 正确
std::cout << p1.lock()->a << std::endl;
}
// 智能指针大概就是这些东西,虽然看上去非常简单,但是想做到灵活的应用
还是比较困难的,需要大量阅读开源代码或者看设计模式相关的书籍。
//======================================================================================================
// c++11里面平时用的多的和问的多的问题基本就在上面了,掌握上面这些去一家以c++11为主的公司写业务肯定够了,
毕竟没有这么多复杂的类需要我们去做。不过看明白和会用差距还是很大的,这个东西需要多去根据业务分析自己的
需求,根据需求灵活应用c++11.
// c++11的东西实在是太多了,一共扩展了140个点,但是一个篇章将这么多又不合适,只能再拆分一下,
把不同的点拆分到不同的模块里面了。
// c++11的stl 模板 线程与异步 锁这些都不是三两句话可以说清楚的,所以需要单独去详细解读一下这些工具,
可以掌握 上面这些东西,做c++11的业务就绰绰有余了,后面这些更偏设计和架构了
// 后面的章节,也是基于c++11进行解析,直接摒弃之前的版本,这样便于理解,当然,也会讲一下c++98的版本处理问题
问题的劣势在哪里