目录
C++标准更新迭代非常快,我们熟悉的标准有C++98,C++11,C++14,其实最新的标准已经到C++20了但是支持该标准的IDE还很少。其实对于很多人来说,能掌握到C++14已经非常不错了。但是事实上是很多人对C++的掌握还停留在C++98上,连11的很多新特性都没有用到。C++新标准引入的新特性能提高编程的效率和代码的鲁棒性,学习一下对程序员的编程实战大有裨益,这里我结合一下自己的学习笔记和理解,总结了一下,希望对大家有所帮助。
这里提一句,之前在极客时间上学习了罗剑锋老师的C++实战笔记,感觉非常不错,收获也挺大的,建议感兴趣的可以去学习一下。
闲话少说,开始正文。
1.C++属性
C++的属性,主要用来控制程序的编译指令的, 针对C++属性标准并没有引入新的关键字,而是用两对中括号的形式来进行表示[[*****]],方括号中间可以添加对应的属性标签。
C++11中只定义了noreturn和carries_dependency两个属性标签,用处并不大。
C++14中定义了deprecated用来标记不推荐使用的函数或者类,使用该标签代表着这个函数或者类被废弃了。
使用方法如下:
[[deprecated("deadline:2021-10-16")]]
int test_fun();
如何在调用该函数的地方在编译的时候都会弹出下面的警告信息
warning: 'int test_fun' is deprecated: deadline:2021-10-16
在发布对外的库的时候,如果要提示用户接口或者类即将废弃,可以使用该属性标签,同时在我们编码的过程中如果要升级一个或者废弃一个接口也可以使用该属性标签。C++17和C++20又增加了五六个属性,比如fallthrough、likely等等。
现在的VS2015对C++14的支持还挺全面的,如果你的IDE是VS2015该特性可以放心使用
2.静态断言(static_assert)
静态断言和普通断言的区别就是,静态断言在编译的过程中生效,普通断言在运行过程中生效。静态断言可以用来在编译构造过程中进行断言检查。
//通过long类型的大小来判断是否运行在x64位系统上
//如果不满足条件,会提示对应的信息
static_assert(sizeof(long) >= 8,"must run on x64")
//判断模板的实例化类型,假设T是个模板参数即template<typename T>
//搭配标准库里面的type_traits使用
//可以通过静态编译来限制模板实例化的类型
static_assert(is_integral<T>::value,"int");
static_assert(is_pointer<T>::value, "ptr");
3.final、default、delete关键字的使用
在继承关系中通过C++11的关键字final阻断继承关系,显式的禁用继承,防止其他人有意或者无意的产生派生类。
class ClassInterface
{...}
class Implement final: public MyClassInterface
{...}
C++类里面,原先有四大函数,分别为:构造函数、析构函数、拷贝构造函数、拷贝赋值函数。C++11因为引入了右值和转移,又多出了两大函数:转移构造函数和转移赋值函数。
对于比较重要的构造函数和析构函数,如果不想自己写, 应该使用“=default”,明确指明,应该实现这个函数,但采用默认的形式。
class TestClass final
{
public:
TestClass() = default; //采用默认构造
~TestClass() = default; //采用默认析构
}
C++11还引入了“=delete”的形式,明确指明禁用某个函数形式,不仅限于构造/析构,还可以用于任何函数(成员函数\自由函数)。
class TestClass final
{
public:
TestClass(const TestClass&) = delete; //禁止拷贝构造
TestClass& operator=(const TestClass&) = delete; //禁止拷贝赋值
}
4.成员变量初始化
在C++11中可以在成员变量声明的时候对其进行初始化。
class DemoTest final
{
private:
int x = 20;
string m_test = "hello";
public:
DemoTest() = default;
~DemoTest() = default;
}
5.类型别名
C++11扩展了关键字using的用法,增加了typedef的能力,可以定义类型的别名,它的格式与typedef正好相反,别名在左,原名称在右边。
using uint_t = unsigned int; //using别名
typedef unsigned int uint_t; //等价的typedef
class TestClass final
{
public:
using this_type = DemoClass;
public:
using string_type = std::string;
using uint32_type = uint32_t;
private:
string_type m_name = "tom";
uint32_type m_age = 32;
}
6.委托构造函数
在C++11中,可以使用委托构造函数新特性,一个构造函数直接调用另外一个构造函数。这样我们可以对构造函数进行封装,对外暴露出统一的构造函数。
class DemoClass final
{
private:
int a;
public:
DemoDelegating(int x): a(x){}
DemoDelegating():DemoDelegating(0){}
DemoDelegating(const string& s):DemoDelegating(stoi(s)){}
}
7.const、volatile、mutable关键字
1.const
const是一个类型修饰符,可以给任何对象附加上“只读”属性,保证安全。它可以用来修饰引用和指针,const& 可以引用任何类型,是函数入口参数的最佳类型。它还可以修饰成员函数,表示函数“只读”的,const 对象只能调用const成员函数
2.volatile
它表示变量可能被“不被察觉”的修改,禁止编译器优化,影响性能,应当少用
3.mutable
它用来修饰成员变量,允许const成员函数修改,mutable变量的变化不影响对象的常量性,但要小心不要误用损坏对象。
C++11里面mutable多了一种用法,可以修复师lambda表达式。
C++11引入了新关键字constexpr,能够表示真正的编译阶段常量,甚至能够编写在编译阶段运行的数值函数。
8.防止头文件重复引入
VS编译器支持指令#pragma once ,也可以实现“#Include Guard”,但它不是标准的,不能跨平台,不推荐使用。在引入头文件的时候,为了防止头文件重复引用,通过宏定义来实现。
#ifndef _XXX_H_HEADER_
#define _XXX_H_HEADER_
...
#endif
虽然这种手法比较原始,但目前来说是唯一有效的方法,而且向下兼容C语言,建议强制使用。
9.auto/decltype自动类型推导
auto避免了对类型的“硬编码”,也就是说类型不是写死的,而是自动推导出来的。但是auto有固定的使用场景要注意。
1.auto的自动推导能力,只能用于“初始化”的场合。包括赋值初始化和花括号初始化,变量右边必须有一个表达式。如果只是变量声明是无法使用auto的。在类成员变量初始化的时候,目前C++不支持推导类型。
2.auto总是推导出的“值类型”,绝对不是“引用类型”
3.auto可以附加上const、volatile、*、&这样的类型修饰符,得到新的类型。
auto j = 1000L; //auto推导类型为long,j是long
auto& j1 = j; //auto是long j1是long&
auto* j2 =&j; //auto是long,j2是long*
const auto & j3 = j;//auto是long ,j3是const long&
auto j4 = &j3; //auto的推导类型是const long* j4是const long*
auto自动类型推导,要求必须要从表达式推导。而decltype的形式很像函数,后面的圆括号里添加用于计算类型的表达式,其它的和auto一样,也能加上const、*、&等修饰符。因为decltype自带表达式可以直接用来进行变量声明。
int x = 0;
decltype(x) x1; //推导为int,x1是int
decltype(x)& x2; //推到为int,x2是int&
decltype(x)* x3; //推导为int,x3是int*
decltype(&x) x4; //推导为int*,x4是int*
decltype(&x)* x5; //推导为int*,x5是int**
decltype(x2) x6 = x2;//推导为int&,x6是int&,引用必须赋值
auto和decltype的区别:decltype不仅能够推导出值类型,而且还能推导出引用类型,也就是表达式的原始类型。decltype可以直接从一个引用类型的变量推导出引用类型,而auto就会把引用去掉,推导出值类型。
可以把decltype看成是一个真正的类型名称,用在变量声明、函数返回值/参数、模板参数等任何类型出现的地方。
using int_ptr = decltype(&x); //int*
using int_ptr = decltype(x)&; //int&
虽然decltype类型推导更加精准,但是表达式要书写两次,左边类型推导,右边初始化。为了解决这个问题,C++14新增了一个decltype(auto)的形式,既可以精确推导出类型又能像auto一样,使用方便。
int x = 0;
decltype(auto) x1 = (x); //推导为int& (expr)是引用类型
decltype(auto) x2 = &x; //推导为int*
//在变量声明的时候尽量多用auto,在容器遍历的时候也可以使用auto
vector<int> input_vector = {1,2,3,4};
for(const auto& index : input_vector){
cout << index << "," //常量访问
}
在C++14里面auto新增了一个使用场景,能够推导返回值的类型。这样在写复杂函数的时候会很省事。
auot get_result_vector{
std::vector<int> result = {1,2,3};
return result;
}
C++14新增了字面量后缀“s”,表示标准字符串,可以通过auto str = "xxx"s;直接推导出std::string的类型。
decltype是auto的高级形式,更侧重于编译阶段的类型推导,所以常用在泛型编程里面,获取各种类型,配合typedef 或者using 更加方便。
//UNIX信号原型
void (*signal(int signo, void (*func)(int)))(int)
//使用decltyp声明类型
using sign_func_ptr_t = decltyp(&signal);
在定义类的时候由于auto被禁止使用,可以使用decltype。
class TestClass final
{
public:
using vector_type = std::vector<int>;
private:
vector_type m_vecotr;
//推导出迭代器的类型
using iter_type = decltype(m_vector.begin());
iter_type m_pos;
}
10.智能指针
由于auto_ptr在资源转移过程中调用流程容易引出错误,C++11使用unique_ptr代替了auto_ptr。常用的两种智能指针,分别是unique_ptr和shared_ptr。
unqiue_ptr名字虽然听起来像指针,但实际上并不是指针,而是一个对象。离开作用域的时候自动释放。另外也没有定义加减运算,不能随意移动指针定制,避免指针越界等操作。也不能不初始化,声明候直接使用。
unique_ptr<int> ptr1(new int(66));
assert(*ptr1 = 66);
assert(ptr1 != nullptr);
ptr1++; //导致编译错误
ptr2 += 2; //导致编译错误
unique_ptr<int> ptr3 ; //未初始化的智能指针
*ptr3 = 42 ; //错误操作了空指针
使用工厂函数make_unique()强制创建的时候必须初始化。make_unqiue()要求C++14。
auto ptr3 = make_uique<int>(48);
assert(ptr3 && *ptr3 == 48);
unique_ptr表示的所有权是唯一的,不允许共享,unique_ptr应用了C++的转移语意,同时禁止拷贝赋值,在指向另外一个unique_ptr赋值的时候,必须用std::move()显示的转移所有权。转移之后原先的unique_ptr变成了空指针。
auto ptr1 = make_unique<int>(42);
assert(ptr1 && *ptr1 == 42);
auto ptr2 = std::move(ptr1);
assert(!ptr1 && ptr2);
尽量不要对unique_ptr进行赋值操作,让其完全自动化管理指针。
shared_ptr和unique_ptr最大的不同点是:它的所有权可以被安全的共享,也就是说支持拷贝赋值。
shared_ptr<int> ptr1(new int(10));
auto ptr2 = make_shared<int>(42);
auto ptr3 = make_shared<string>("mystring");
shared_ptr通过使用“引用计数”,实现了安全共享。shared_ptr具有完整的“值语义”,所以它可以在任何场景替代原始指针,而不用再担心资源回收的问题,比如用于容器存储指针、用于函数安全返回动态创建的对象。
shared_ptr的引用计数的存储和管理都是有成本的,过度的使用shared_ptr会降低运行效率。
shared_ptr的引用计数销毁在运行阶段会变得很复杂,很难确定真正释放资源的时机。如果析构函数有非常复杂,严重阻塞的操作,一旦shared_ptr在某个不确定的时间点析构释放资源,就会阻塞整个进程或者线程。
11.Lambda表达式
lambda表达式就地定义函数,限制它的作用域和生命周期,实现函数的局部化。
lambda表达式用起来也就更灵活自由,能对它做各种运算,生成新的函数。这就像是数学里的复合函数那样,把多个简单功能的小lambda表达式组合,变成一个复杂的大lambda表达式。
lambda表达式除了可以像普通函数一样使用外,还可以捕获外部变量
nt n = 100;
auto func = [=](int x){cout << x + n << endl};//按值捕获
auto func = [&](int x){cout << x*n << endl}; //按引用捕获
auto func = [=,&n](int x){cout << x/n << endl};//n按引用传递,其它的按值传递
//定义嵌套的lambda表达式
//C++里面每个lambda表达式都是特殊类型只有编译器知道,所以用auto来声明
auto outerlambda = []()
{
auto innerlambda = [](int x)
{
return x*x;
}
}
因为lambda表达式不是普通的变量,C++也鼓励程序员尽量匿名使用lambda表达式。也就是不必显示的命名,直接就地使用。我们可以使用STL+lambda来替代for循环。
vector<int> input = {1,2,3,4,5};
find_if(std::begin(input),std::end(input),[](int x){return x>=5});
建议你在使用捕获功能的时候要小心,对于“就地”使用的小lambda表达式,可以用“[&]”来减少代码量,保持整洁;而对于非本地调用、生命周期较长的lambda表达式应慎用“[&]”捕获引用,而且,最好是在“[]”里显式写出变量列表,避免捕获不必要的变量。
虽然目前在C++里,纯函数式编程还比较少见,但“轻度”使用lambda表达式也能够改善代码,比如用“map+lambda”的方式来替换难以维护的if/else/switch,可读性要比大量的分支语句好得多。
map<int, function<void()>> funcs;
funs[1] =[](){...};
funs[4] =[](){...};
funs[6] =[](){...};
return funcs[x]();
这样就把switch/case 转换成了function+lambda
lambda表达式的返回值类型可以自动推导(相当于用了auto),但有的时候必须明确指定返回值类型,在入口参数的圆括号后用“->type”的形式。
在按值捕获外部变量的时候,可以给lambda表达式加上mutable修饰,允许修改变量。注意,这与按引用捕获不同,修改的只是变量的拷贝,不影响外部变量的原值。
因为每个Iambda表达式的类型都是唯一的,所以即使函数签名相同,lambda变量也不能互相赋值。解决办法是使用标准库里的stdl:function类,它是“函数的容器”“智能函数指针”,可以存储任意符合签名的"可调用物"(callableobject)搭配使用能够让lambda表达式用起来更灵活。