初识 C++ 的时候, 觉得会个 STL 就差不多了, 后来发现了 C++11 这个东西, 以及 C++14,C++17QAQ, 看了一下, 好高深不学, emmmm 真香 = =
这里就只讲一下对 ACM 写代码有很高帮助的部分特性, 因为大部分 OJ 和比赛只支持 11, 所以 14 和 17 就不讲了, 然后还有 C++11 增加的元组 tuple 和几个容器另谈.
一, nullptr
在之前版本的 c++ 中, NULL 的值其实就是 0, 因为其实就是 #define NULL 0, 有些时候是((void *) 0)emmm, 不说那些废话了. 所以这些就会遇到一个问题,
例如下面这两个函数
- int fun(char* ch){}
- int fun(int num){}
当 char ch = NULL, 这样一个值去代入函数时, 编译会调用下面那个函数, 而不是第一个. 而 nullptr 类型是 nullptr_t, 就是专门为了区别空指针和 0, 所以以后写代码 nullptr 代替 NULL 就能行了.
二, constexpr
constexpr 变量必须是一个常量, 必须用常量表达式来初始化, 例如下面代码
- const int a = 10;
- int b = 10;
- constexpr int d = 10; // 正确
- constexpr int c = a + 10; // 正确
- constexpr int e = b; // 错误
值得一提的是如果成员函数标记为 constexpr, 则默认其是内联函数, 如果变量声明为 constexpr, 则默认其是 const, 这个会在构造函数中用到.
三, 类型推导
auto
其实以前的 c++ 中就有 auto, 但是 emmm, 废话不说, 直接说内容吧, 有了 auto 就很方便了. 比如下面这个代码
- map<int,int>::iterator it = mp.begin();
- auto it = mp.begin();
谁方便一眼便知道了吧, auto 可以把自动推导成变量或者函数.
比如
- int fun(int i){
- cout <<i << endl;
- }
- function<int(int)> f;
- auto a = fun;
这里 auto 其实就和自动推导成了 function<int(int)>类型. 值得注意的时, 定义 auto 类型变量必须赋初值, 不然则会编译错误. 还有就是函数的返回值不能直接用 auto 代替, 例如
auto fun(int i){ }
这是会报错的, 但是不是函数的返回值就不能是 auto 类型了呢, 其实是可以的, 但只是不能直接这样定义, 下面我们会讲到如何把函数返回值定义为 auto.
decltype
顾名思义, 就是推导一个变量的类型是啥, 和 sizeof 用法类似, 它的出现就是为了弥补 auto 的缺陷.
- auto a = 1,b = 2;
- decltype(a+b) z;
拖尾返回类型
上面不是说怎么把函数值弄为 auto 吗, 其实在以前得 c++ 中也可以用 typename, 比如
- template<typename R, typename T, typename U>
- R add(T x, U y) {
- return x+y
- }
当有多个这种函数的时候, 就会显得代码很冗长, 很不人性化 qwq, 所以就出现了拖尾返回类型, 例如下面这个代码
- auto fun(int i) -> bool{
- return i&1;
- }
这个代码编译是没有问题的, 后面的 ->bool 就是说明函数的返回类型是布尔类型, 所以就可以使用 auto 作为函数类型, 当然也可以和 decltype 连用写出下面的代码.
- template<typename T>
- auto fun(T i) -> decltype(i*i){
- return i*i;
- }
简洁易懂, 而且看着特别舒服有木有.
四, 区间迭代
在其他语言里面经常可以看到 for(a : b)这样的循环用法, 而在 c++ 里面, 你要是想对一个容器进行区间迭代, 你得这样写
1 for(vector<int>::iterator iter = vec.begin(); iter != vec.end(); iter++)
虽然看上去很整洁, 但也太不人性化了吧, 所以 11 就引用了: 进行区间迭代, 现在你可以直接用 auto 和: 写出下面的代码
1 for(auto &iter : vec)
这真的很 coool
五, 尖括号'>'
当多个容器叠加的时候, 我们都是用空格将>>区分开, 因为在以前版本的 c++ 中>>是会看成右移操作符号的, 但是 11 更新后, 你完全不用担心这个问题, 例如下面代码, 在 11 中编译是没有任何问题的
1 vector<vector<int>> q;
六, 初始值
在以前的 c++ 版本中, 当我们需要对一个结构体或者类赋初值, 我们得写一个函数, 例如
- struct node{
- int a,b;
- node(int _a,int _b){this->a = _a,this->b = _b;}
- node(int _a,int _b):a(_a),b(_b){}
- };
但现在, c++11 有五种不同的方式对各种变量初始化, 你可以直接用大括号赋值, 下面提供了几种不同的结构体赋值方法.
- struct node{
- int a,b;
- };
- node asd{1,2};
- node as = {1,2};
对于其他变量和容器也适用.
七, 类型别名以及默认别名
类型别名
typedef 大家肯定使用过, 一般将一个类或者结构体做别名, 但对于容器, 比如下面用法
- template<typename T>
- typedef vector<T> qwq;
是不合法的, 所以 c++11 引入了 using, 和 typedef 功能相同.
- template<typename T>
- using asd = vector<T>;
当需要使用 vector 时候, 就直接调用即可.
1 asd<long long> vec;
默认别名
对于某个函数
- template<typename T1,typename T2>
- auto fun(T1 a,T2 b)->bool{
- return a <b;
- }
但是如果你想制定默认模板参数类型怎么办, c++11 提供了一种便利, 可以直接指定默认参数
- template<typename T1 = int,typename T2 = int>
- auto fun(T1 a,T2 b)->bool{
- return a <b;
- };
八, lambda 表达式
Lambda 表达式是用来创建匿名函数的. 什么叫做匿名函数, 就是没有名字 qwq, 那么为什么要用到匿名函数呢, 举个例子, 比如 sort 的第三个参数, 谓词函数, 一般我们会写一个比较函数, 但这样的后果就是, 如果有多个 sort 你就得, 每个函数去找他的比较函数, 实属麻烦. 各位看下面这个例子
1 sort(a.begin(),a.end(),[](int a,int b)->bool{return a <b;});
就算你不懂什么是 lambda 表达式, 但你也能猜到这个 sort 就是按照 a<b 比较吧. 那么 lambda 表达式是什么样子的呢. 大致就是
1 [capture](parameters)opt->return-type{body}
emmm 后面那一截和普通函数没什么区别, 参数, 返回值类型, 函数体. 和函数最大的不同就是匿名函数是临时函数, 没有函数名, 及拿及用, 当然你也可以用 function 保存一个匿名函数.
至于 opt 是个什么东西 = = 下面说, 这个一般不会用上.
其中:
1. 返回值类型 ->return-type 可以省略, 由语言自动推导, 但只限于 lambda 表达式语句简单.
2. 引入 lambda 表达式的前导符是一对方括号, 叫做 lambda 引入符. lambda 表达式可以使用与其相同范围内的变量, 一共有下面这么几种方式捕获变量
- []// 不捕获任何外部变量
- [=]// 以值形式捕获所有外部变量
- [&]// 以引用方式捕获所有外部变量
- [x,&y]//x 以值形式捕获, y 以引用形式捕获
- [=,&x]//x 以引用形式捕获, 其余变量以值捕获
- [&,x]//x 以值形式捕获, 其余变量以引用形式捕获
[this]捕获当前类的指针. 捕获 this 的目的是可以在 lambda 中使用当前类的成员函数和成员变量.
对于 [=] 和[&],lambda 可以直接使用 this 指针, 但是对于 [] 的形式, 如果要使用 this 指针, 必须显式传入
3.opt 函数选项
可以选填 mutable,exception,attribute.
mutable 说明 lambda 表达式体内的代码可以修改被捕获的变量, 并且可以访问被捕获的对象的 non-const 方法.
exception 说明 lambda 表达式是否抛出异常以及何种异常
attribute 用来声明属性
4.lambda 表达式不能直接被赋值, 闭包类型禁用了赋值操作符号, 但是可以用 lambda 表达式去初始化另一个 lambda 表达式, 例如
- auto a = []{cout <<1 <<endl;};
- auto b = a;
也可以吧 lambda 表达式赋值给相对应的函数指针, 例如
1 function<int(int)> fun = [](int a){return a;};
那么就可以很方便的利用 lambda 表达式填充各种谓词函数了, 例如下面便是一个斐波那契数列
- array<int,10> a;
- auto f0 = 0, f1 = 1;
- generate(a.begin(),a.end(),[&f0,&f1]()->int{int v = f1;f1 += f0, f0 = v;return v;});
- for_each(a.begin(),a.end(),[](int v){cout <<v << " ";});
- cout << endl;;
代码一眼看上去就能知道意思, 无需定义额外函数. 大部分的 STL 算法, 都可以搭配 lambda 表达式来实现想要的效果.
九, 右值引用和 move
什么是右值引用, 先看一个例子.
- string a(x);
- string b(x+y);
- string c(fun());
- // 如果使用以下拷贝构造函数
- string(const string& str){
- size_t size = strlen(str.data)+1;
- data = new char[size];
- memcpy(data,str.data,size);
- }
则只有第一行的 x 深度拷贝有必要, 因为其他地方还可能会用到 x,x 就是一个左值. 但第二行和第三行的参数则是右值, 因为表达式产生的匿名 string 对象, 之后没法再用.
c++11 引入了一种机制 "右值引用", 用 && 来表示右值引用, 以便我们通过重载直接使用右值参数, 例如下面这个构造函数:
- string(string&& that){
- data = that.data;
- that.data = 0;
- }
我们没有深度拷贝堆内存中的数据, 而是仅仅复制了指针, 并把源对象的指针置空. 事实上, 我们 "偷取" 了属于源对象的内存数据. 由于源对象是一个右值, 不会再被使用, 因此客户并不会觉察到源对象被改变了. 在这里, 我们并没有真正的复制, 所以我们把这个构造函数叫做 "转移构造函数", 他的工作就是把资源从一个对象转移到另一个对象, 而不是复制他们.
那么赋值操作符就可以写成
- string& operator=(string that){
- std::swap(data, that.data);
- return *this;
- }
注意到我们是直接对参数 that 传值, 所以 that 会像其他任何对象一样被初始化, 那么确切的说, that 是怎样被初始化的呢? 对于 C++ 98, 答案是复制构造函数, 但是对于 C++ 11, 编译器会依据参数是左值还是右值在复制构造函数和转移构造函数间进行选择.
如果是 a=b, 这样就会调用复制构造函数来初始化 that(因为 b 是左值), 赋值操作符会与新创建的对象交换数据, 深度拷贝. 这就是 copy and swap 惯用法的定义: 构造一个副本, 与副本交换数据, 并让副本在作用域内自动销毁. 这里也一样.
如果是 a = x + y, 这样就会调用转移构造函数来初始化 that(因为 x+y 是右值), 所以这里没有深度拷贝, 只有高效的数据转移. 相对于参数, that 依然是一个独立的对象, 但是他的构造函数是无用的(trivial), 因此堆中的数据没有必要复制, 而仅仅是转移. 没有必要复制他, 因为 x+y 是右值, 再次, 从右值指向的对象中转移是没有问题的.
转移左值是十分危险的, 但是转移右值却是很安全的. 如果 C++ 能从语言级别支持区分左值和右值参数, 我就可以完全杜绝对左值转移, 或者把转移左值在调用的时候暴露出来, 以使我们不会不经意的转移左值.
复制构造函数执行的是深度拷贝, 因为源对象本身必须不能被改变. 而转移构造函数却可以复制指针, 把源对象的指针置空, 这种形式下, 这是安全的, 因为用户不可能再使用这个对象了.
有时候, 我们可能想转移左值, 也就是说, 有时候我们想让编译器把左值当作右值对待, 以便能使用转移构造函数, 即便这有点不安全. 出于这个目的, C++ 11 在标准库的头文件< utility > 中提供了一个模板函数 std::move. 实际上, std::move 仅仅是简单地将左值转换为右值, 它本身并没有转移任何东西. 它仅仅是让对象可以转移.
- unique_ptr<Shape> a(new Triangle);
- unique_ptr<Shape> b(a); //false
- unique_ptr<Shape> c(move(a)); //true
请注意, 第三行之后, a 不再拥有 Triangle 对象. 不过这没有关系, 因为通过明确的写出 move(a), 我们很清楚我们的意图: 亲爱的转移构造函数, 你可以对 a 做任何想要做的事情来初始化 c; 我不再需要 a 了, 对于 a, 您请自便.
当然, 如果你在使用了 mova(a)之后, 还继续使用 a, 那无疑是搬起石头砸自己的脚, 还是会导致严重的运行错误.
总之, move(val)将左值转换为右值(可以理解为一种类型转换), 使接下来的转移成为可能.
十, 正则表达式
正则表达式描述了一种字符串匹配的模式. 一般使用正则表达式主要是实现下面三个需求:
1) 检查一个串是否包含某种形式的子串;
2) 将匹配的子串替换;
3) 从某个串中取出符合条件的子串.
C++11 提供的正则表达式库操作 string 对象, 对模式 std::regex (本质是 basic_regex)进行初始化, 通过 std::regex_match 进行匹配, 从而产生 smatch (本质是 match_results 对象).
我们通过一个简单的例子来简单介绍这个库的使用. 考虑下面的正则表达式:
[a-z]+.txt: 在这个正则表达式中, [a-z] 表示匹配一个小写字母, + 可以使前面的表达式匹配多次, 因此 [a-z]+ 能够匹配一个及以上小写字母组成的字符串. 在正则表达式中一个 . 表示匹配任意字符, 而 . 转义后则表示匹配字符 . , 最后的 txt 表示严格匹配 txt 这三个字母. 因此这个正则表达式的所要匹配的内容就是文件名为纯小写字母的文本文件.
regex_match 用于匹配字符串和正则表达式, 有很多不同的重载形式. 最简单的一个形式就是传入 string 以及一个 regex 进行匹配, 当匹配成功时, 会返回 true, 否则返回 false. 例如:
- string fnames[] = {"foo.txt", "bar.txt", "test", "a0.txt", "AAA.txt"};//`\` 会被作为字符串内的转义符, 需要对 `\` 进行二次转义, 从而有 `\\.`
- regex txt_regex("[a-z]+\\.txt");
- for (const auto &fname: fnames)
- cout <<fname << ":" << regex_match(fname, txt_regex) << endl;
另一种常用的形式就是依次传入 string/smatch/regex 三个参数, 其中 smatch 的本质其实是 match_results, 在标准库中, smatch 被定义为了 match_results, 也就是一个子串迭代器类型的 match_results. 使用 smatch 可以方便的对匹配的结果进行获取, 例如:
- regex base_regex("([a-z]+)\\.txt");
- smatch base_match;
- for(const auto &fname: fnames) {
- if (regex_match(fname, base_match, base_regex)) {
- // sub_match 的第一个元素匹配整个字符串
- // sub_match 的第二个元素匹配了第一个括号表达式
- if (base_match.size() == 2) {
- string base = base_match[1].str();
- cout << "sub-match[0]:" << base_match[0].str() << endl;
- cout << fname << "sub-match[1]:" << base << endl;
- }
- }
- }
代码运行结果为
- foo.txt: 1
- bar.txt: 1
- test: 0
- a0.txt: 0
- AAA.txt: 0
- sub-match[0]: foo.txt
- foo.txt sub-match[1]: foo
- sub-match[0]: bar.txt
- bar.txt sub-match[1]: bar
十一, 构造函数 & 继承 & 修饰符
C++11 引入了委托构造的概念, 这使得构造函数可以在同一个类中一个构造函数调用另一个构造函数, 从而达到简化代码的目的:
委托构造
- class Base {
- public:
- int value1;
- int value2;
- Base() {
- value1 = 1;
- }
- Base(int value) : Base() { // 委托 Base() 构造函数
- value2 = 2;
- }
- };
继承构造
在继承体系中, 如果派生类想要使用基类的构造函数, 需要在构造函数中显式声明.
假若基类拥有为数众多的不同版本的构造函数, 这样, 在派生类中得写很多对应的 "透传" 构造函数. 如下:
- struct A
- {
- A(int i) {}
- A(double d,int i){}
- A(float f,int i,const char* c){}
- //... 等等系列的构造函数版本
- };
- struct B:A
- {
- B(int i):A(i){}
- B(double d,int i):A(d,i){}
- B(folat f,int i,const char* c):A(f,i,e){}
- //...... 等等好多个和基类构造函数对应的构造函数
- };
constexpr 构造
如果想要使得函数拥有编译时计算的能力, 则使用关键字 constexpr
- class Square {
- public:
- constexpr Square(int e) : edge(e){};
- constexpr int getArea() {return edge * edge;}
- private:
- int edge;
- };
- int main() {
- Square s(10);
- cout << s.getArea() << endl;
- return 0;
- }
十二, 删除的函数以及新增的函数
字符串和数值类型转换
itoa 等等字符串和数值类型的转换成为历史.
提供了 to_string 和 stox 方法, 将字符串和数值自由转换;
- // 数值转字符串
- std::string to_string(int value);
- std::string to_string(long int value);
- std::string to_string(long long int value);
- std::string to_string(unsigned int value);
- std::string to_string(unsigned long long int value);
- std::string to_string(float value);
- std::string to_string(double value);
- std::wstring to_wstring(int value);
- std::wstring to_wstring(long int value);
- std::wstring to_wstring(long long int value);
- std::wstring to_wstring(unsigned int value);
- std::wstring to_wstring(unsigned long long int value);
- std::wstring to_wstring(float value);
- std::wstring to_wstring(double value);
- // 字符串转数值
- std::string str = "1000";
- int val = std::stoi(str);
- long val = std::stol(str);
- float val = std::stof(str);
c++11 还提供了字符串 (char*) 转换为整数和浮点类型的方法:
atoi: 将字符串转换为 int
atol: 将字符串转换为 long
atoll: 将字符串转换为 long long
atof: 将字符串转换为浮点数
随机数函数
生成随机数, 免去了以前需要自行调用 srand 初始化种子的步骤, 因为有时候忘记初始化导致结果错误.
- std::random_device rd;
- int randint = rd();
- std::chrono
获取时间函数, 比以前方便许多.
- std::chrono::duration<double> duration // 时间间隔
- std::this_thread::sleep_for(duration); //sleep
- LOG(INFO) <<"duration is" << duration.count() << std::endl;
- std::chrono::microseconds // 微秒
- std::chrono::seconds // 秒
- end = std::chrono::system_clock::now(); // 获取当前时间
- all_of(),any_of(),none_of(),copy_n(),iota()
- #include<algorithm>
- #include<numeric>
- all_of(first,first+n,ispositive());//false
- any_of(first,first+n,ispositive());//true
- none_of(first,first+n,ispositive());//false
- int source[5]={0,12,34,50,80};
- int target[5];
- // 从 source 拷贝 5 个元素到 target
- copy_n(source,5,target);
- //iota()算法可以用来创建递增序列, 它先把初值赋值给 *first, 然后用前置 ++ 操作符增长初值并赋值到给下一个迭代器指向的元素, 如下:
- inta[5]={0};
- charc[3]={0};
- iota(a,a+5,10);//{10,11,12,13,14}
- iota(c,c+3,'a');//{'a','b','c'}
原子变量和正则表达式
std::atomic<XXX>
用于多线程资源互斥操作, 属 c++11 重大提升, 多线程原子操作简单了许多.
C 正则 (regex.h) 和 boost 成为历史
hash 进入 STL
新增基于 hash 的无序容器.
对于容器的 emplace
作用于容器, 区别于 push,insert 等, 如 push_back 是在容器尾部追加一个容器类型对象, emplace_back 是构造 1 个新对象并追加在容器尾部
对于标准类型没有变化, 如 std:;vector<int>,push_back 和 emplace_back 效果一样
如自定义类型 class A,A 的构造函数接收一个 int 型参数,
那么对于 push_back 需要是:
- std::vector<A> vec;
- A a(10);
- vec.push_back(a);
对于 emplace_back 则是:
- std::vector<A> vec;
- vec.emplace_back(10);
避免无用临时变量. 比如上面例子中的那个 a 变量.
对于容器的 shrink_to_fit
这个改进还是有点意义的, 日常程序应该能减少不少无意义的内存空间占用
push,insert 这类操作会触发容器的 capacity, 即预留内存的扩大, 实际开发时往往这些扩大的区域并没有用途
- std::vector<int> v{1, 2, 3, 4, 5};
- v.push_back(1);
- std::cout <<"before shrink_to_fit:" << v.capacity() << std::endl;
- v.shrink_to_fit();
- std::cout << "after shrink_to_fit:" << v.capacity() << std::endl;
可以试一试, 减少了很多.
十三, 动态指针 & 智能指针
C++98 标准库中提供了一种唯一拥有性的智能指针 std::auto_ptr, 该类型在 C++11 中已被废弃, 因为其 "复制" 行为是危险的.
auto_ptr 的危险之处在于看上去应该是复制, 但实际上确是转移. 调用被转移过的 auto_ptr 的成员函数将会导致不可预知的后果. 所以你必须非常谨慎的使用 auto_ptr , 如果他被转移过.
C++ 11 中, std::auto_ptr< T > 已经被 std::unique_ptr<T > 所取代, 后者就是利用的右值引用.
十四, 会使用的库
- <utility>
- <unordered_map>
- <unordered_set>
- <random>
- <tuple>
- <array>
- <numeric>
需要深入了解的话就自己去找资料了吧 qwq, 我觉得用的最多的可能还是 lambda 表达式和区间迭代了吧, 越来越像 python 了, qwq 希望有所帮助.
来源: https://www.cnblogs.com/xenny/p/9671099.html