C++学习(3)11新特性

1 类型推导

1.1 auto

auto可以让编译器在编译期推导出变量的类型

  • auto的使⽤必须马上初始化,否则⽆法推导出类型;
  • auto在⼀⾏定义多个变量时,各变量的推导不能产⽣⼆义性,否则编译失败;
  • auto不能⽤作函数参数;
  • 在类中auto不能⽤作⾮静态成员变量;
  • auto不能定义数组,可以定义指针;
  • auto⽆法推导出模板参数;
  • 在不声明为引⽤或指针时,auto会஺略等号右边的引⽤类型和cv限定;
  • 在声明为引⽤或者指针时,auto会保留等号右边的引⽤和cv属性;

1.1 decltype

decltype则⽤于推导表达式类型,这里只⽤于编译器分ຉ表达式的类型,表达式实际不会进⾏运算;不会像auto⼀样忽略引⽤和cv属性,decltype会保留表达式的引⽤和cv属性;

cv属性
CV限定符即cv-qualifier,C++语言中指constvolatile限定符。通常来说,C++语言中有两种情况不能使用CV限定符进行限定:
A、非成员函数不能使用CV限定
B、静态成员函数不能使用CV限定

对于decltype(exp)有:

  1. exp是表达式,decltype(exp)和exp类型相同
  2. exp是函数调⽤,decltype(exp)和函数返回值类型相同
  3. 其它情况,若exp是左值,decltype(exp)是exp类型的左值引⽤

auto和decltype的配合使⽤:

template<typename T, typename U>
auto add(T t, U u) -> decltype(t + u) {
    return t + u;
}

2 右值引⽤

左值右值:
左值: 可以放在等号左边,可以取地址并由名字;
右值: 不可以放在 i 等号左边,不能取地址,没有名字;
字符串字⾯值"abcd"也是左值,不是右值;
++i、--i是左值,i++、i--是右值;

2.1 将亡值

将亡值是指C++11新增的和右值引⽤相关的表达式
将亡值可以理解为即将要销毁的值,通过“盗取”其它变量内存空间⽅式获取的值,在确保其它变量不再被使⽤或者即将被销毁时,可以避免内存空间的释放和分配,延⻓变量值的⽣命周期,常⽤来完成移动构造或者移动赋值的特殊任务;

2.2 左值引⽤

左值引⽤就是对左值进⾏引⽤的类型,是对象的⼀个别名并不拥有所绑定对象的堆存,所以必须⽴即初始化。 对于左值引⽤,等号右边的值必须可以取地址,如果不能取地址,则会编译失败,或者可以使⽤const引⽤形式;

2.3 右值引⽤

表达式等号右边的值需要是右值,可以使⽤std::move函数强制把左值转换为右值;

2.4 移动语义

可以理解为转移所有权,对于移动语义,类似于转让或者资源窃取的意思,对于那块资源,转为⾃⼰所拥有,别⼈不再拥有也不会再使⽤。通过移动构造函数使⽤移动语义,也就是std::move;移动语义仅针对于那些实现了移动构造函数的类的对象,对于那种基本类型int、float等没有任何优化作⽤,还是会拷⻉,因为它们实现没有对应的移动构造函数;

浅拷⻉:

  • a和b的指针指向了同⼀块内存,就是浅拷⻉,只是数据的ᓌ单赋值;

深拷⻉:

  • 深拷⻉就是再拷⻉对象时,如果被拷⻉对象内部还有指针引⽤指向其它资源,⾃⼰需要重新开⼀块新内存存储资源;

2.5 完美转发

写⼀个接受任意实参的函数模板,并转发到其它函数,⽬标函数会收到与转发函数完全相同的实参,通过std::forward()实现;
想详细了解可以参见:
https://blog.csdn.net/zhizhengguan/article/details/115833949

3 nullptr

nullptr是⽤来代替NULL,⼀般C++会把NULL0视为同⼀种东西,这取决去编译器如何定义NULL,有的定义为((void*)0),有的定义为0;C++不允许直接将void* 隐式转换到其他类型,在进⾏C++重载时会发⽣混乱;
如:

void foo(char *);
void foo(int );

如果NULL被定义为 ((void*)0),那么当编译char *ch = NULL时,NULL被定义为 0,当foo(NULL)时,此时NULL为0,会去调⽤foo(int ),从⽽发⽣混乱;

为解决这个问题,从⽽需要使⽤NULL时,⽤nullptr代替;

C++11引⼊nullptr关键字来区分空指针和0nullptr 的类型为 nullptr_t,能够转换为任何指针或成员指针的类型,也可以进⾏相等或不等的⽐较。

4 范围for循环

基于范围的迭代写法,for(变量:对象)表达式

对string对象的每个字符做⼀些操作:

std::string str("some thing");
for(char c : str) cout << c << endl;//对字符串的每个字符进行cout操作

对vector进行遍历:

std::vector<int> arr(5, 100);
for(auto it : arr) cout << *it << endl;//对字符串的每个字符进行cout操作

5 初始化列表

C++定义了⼏种初始化⽅式,例如对⼀个int变量 x初始化为0:

int x = 0; // method1
int x = {0}; // method2
int x{0}; // method3
int x(0); // method4

采⽤花括号来进⾏初始化称为列表初始化,⽆论是初始化对象还是为对象赋新值。
⽤于对内置类型变量时,如果使⽤列表初始化,且初始值存在丢失信息⻛险时,编译器会报错。

long double d = 3.1415926536;
int a = {d}; //存在丢失信息⻛险,转换未执⾏。
int a = d; //确实丢失信息,转换执⾏。

6 lambda表达式

lambda表达式表示⼀个可调⽤的代码单元,没有命名的内联函数,不需要函数名因为我们直接(⼀次性的)⽤它,不需要其他地⽅调⽤它。

6.1 lambda 表达式的语法

[capture list] (parameter list) -> return type {function body }
// [捕获列表] (参数列表) -> 返回类型 {函数体 }
// 只有 [capture list] 捕获列表和 {function body } 函数体是必选的
auto lam =[]() -> int { cout << "Hello, World!"; return 88; };
auto ret = lam();
cout<<ret<<endl; // 输出88

-> int :代表此匿名函数返回int,⼤多数情况下lambda表达式的返回值可由编译器猜测得出,因此不需要我们指定返回值类型。

6.2 lambda 表达式的特点

变量捕获:

1. [] 不捕获任何变量,这种情况下lambda表达式内部不能访问外部的变量;
2. [&] 以引⽤⽅式捕获所有变量(保证lambda执⾏时变量存在);
3. [=] ⽤值的⽅式捕获所有变量(创建时拷⻉,修改对lambda内对象⽆୽响);
4. [=, &foo] 以引⽤捕获变量foo, 但其余变量都ᶌ值捕获;
5. [&, foo] 以值捕获foo, 但其余变量都ᶌ引⽤捕获;
6. [bar] 以值⽅式捕获bar; 不捕获其它变量;
7. [this] 捕获所在类的this指针;
int a = 1, b = 2, c = 3;
auto lam2 = [&,a](){//b,c以引用捕获,a以值捕获。
    b = 5; c = 6;//a=1;a不能赋值
    cout << a << b << c << endl;//输出 1 5 6
};
1am2();

void fcn(//值捕获
    size_t v1 = 42;
    autof = [v1] {returnv1;};
    v1 = 0;
    auto j = f();    //j=42;创建时拷贝,修改对1ambda内对象无影响

void fcn(){//可变1ambda
    size_t v1 = 42;
    auto f = [v1] () mutable{return++v1;};//修改值捕获可以加mutable
    v1 = 0;
    auto j = f(); //j = 43
}
void fcn(){//引用捕获
    size_t v1 = 42;//非const
    autof = [&v1](){return ++v1;};
    v1 = 0;
    autoj=f();//注意此时j=1;
}

lambda最⼤的⼀个优势是在使⽤STL中的算法(algorithms)库

int arr[] = {6, 4, 3, 2, 1, 5};
bool compare(int& a, int& b){//谓词函数
    return a>b;
}
std::sort(arr, arr+6, compare);
//1ambda形式:
std::sort(arr, arr+6, [](const int& a, const int& b){return a>b;}};
//降序排序
std::for_each(begin(arr), end(arr),[](const int& e){cout << "After:" << e << endl;});
//6,5,4,3,2,1

7 并发

7.1 std::thread

default (1) thread() noexcept;
initialization (2) template <class Fn, class... Args> explicit thread (Fn& fn, Args&... arg33);
copy [deleted] (3) thread(const thread&)=delete;
move (4) thread(thread&&x)noexcept;
  1. 默认构造函数,创建⼀个空的 thread 执⾏对象。
  2. 初始化构造函数,创建⼀个 thread对象,该 thread对象可被 joinable,新产⽣的线程会调⽤ fn 函数,该函数的参数由 args 给出。
  3. 拷⻉构造函数(被禁⽤),意ޱ着 thread 不可被拷⻉构造
  4. move 构造函数,move 构造函数,调⽤成功之后 x 不代表任何 thread 执⾏对象。

注意:
可被 joinablethread 对象必须在他们销毁之前被主线程 join 或者将其设置为 detached
std::thread在使⽤上容易出错,即std::thread对象在线程函数运⾏期间必须是有效的。

#include <iostream>
#include <thread>
void threadproc() {
    while(true) {
    std::cout << "I am New Thread!" << std::endl;
    }
}
void func() {
    std::thread t(threadproc);
}
int main() {
    func();
    while(true) {} //让主线程不要退出
    return 0;
}

以上代码再main函数中调⽤了func函数,在func函数中创建了⼀个线程,看起来好像没有什么问题,但在实际运⾏是会崩溃。
因为在func函数调⽤结束后,func中局部变量t(线程对象)销毁,而线程函数仍然在运⾏。所以在使⽤std::thread类时,必须保证线程函数运⾏期间其线程对象有效。

std::thread对象提供了⼀个detach⽅法,通过这个⽅法可以让线程对象与线程函数脱离关系,这样即使线程对象被销毁,也不影响线程函数的运⾏。只需要在func函数中调⽤detach⽅法即可,代码如下:

// 其他代码保持不变
void func() {
    std::thread t(threadproc);
    t.detach();
}

7.2 lock_guard

lock_guard是⼀个互斥量包装程序,它提供了⼀种⽅便的RAII(Resource acquisition is initialization )⻛格的机制来在作⽤域块的持续时间内拥有⼀个互斥量。

创建lockguard对象时,它将尝试获取提供给它的互斥锁的所有权。当控制流离开lockguard对象的作⽤域时,lock_guard析构并释放互斥量。

ਙ的特点如下:

  • 创建即加锁,作⽤域结束⾃动析构并解锁,⽆需⼿⼯解锁;
  • 不能中途解锁,必须等作⽤域结束才解锁;
  • 不能复制

7.3 unique_lock

unique_lock是⼀个通⽤的互斥量锁定包装器,它允许延迟锁定,限时Ⴎ度锁定,递归锁定,锁定所有权的转移以及与条件变量⼀起使⽤。

简单来说,uniquelocklockguard 的升级加强版,它具有 lock_guard 的所有功能,同时⼜具有其他很多⽅法,使⽤起来更强灵活⽅便,能够应对更复๥的锁定需要。

特点如下:

  • 创建时可以不锁定(通过指定第⼆个参数为std::defer_lock),⽽在需要时再锁定;
  • 可以随时加锁解锁;
  • 作⽤域规则同 lock_grard,析构时⾃动释放锁;
  • 不可复制,可移动;
  • 条件变量需要该类型的锁作为参数(此时必须使⽤unique_lock;

8、C++11之后的新特性概述

c++11后续又有11,14,17,20等众多新版本,C++14在11的基础上查缺补漏,并未加入许多新特性,C++17为C++11后的第一个大版本,在此暂不做过多介绍,有兴趣可以参考一下博客:
https://blog.csdn.net/yyz_1987/article/details/124877714。

8.1 14新特性概述

c++14并没有太大的改动,就连官方说明中也指出,c++14相对于c++11来说是一个比较小的改动,但是在很大程度上完善了c++11,所以可以说c++14就是在c++11标准上的查漏补缺。

8.1.1语言新特性

  1. 变量模板
    c++14可以定义变量模板
template<class T>
constexpr T pi = T(3.1415926535897932385L); // variable template
int main()
{
    std::cout << pi<double> << std::endl;//3.14159
    std::cout << pi<float> << std::endl;//3.14159
    std::cout << pi<int> << std::endl;//3
    return 0;
}

如果想在类内部定义变量模板,那么需要定义静态变量,同时也可以对模板变量进行特化。

  1. lambda表达式的改动
    支持lambda泛型参数;
    支持初始化捕获;
// x通过初始化捕获
// y通过类型推导获取类型
auto func = [x = 3](auto y) {return x + y; };
std::cout << func(4.5) << std::endl;
std::cout << func(7) << std::endl;

std::unique_ptr<Item> item(new Item());
auto func = [m = std::move(item)] { /* do something */ };

泛型参数这个比较好理解,初始化捕获有什么好处呢。在c++11中,lambda表示捕获变量只能通过值捕获或者引用捕获,支持了初始化表达式之后,我们就可以更灵活的捕获了,比如移动捕获。

  1. constexpr限制放宽
    constexpr是在c++11标准中被引入的,旨在让编译器可以在编译器就做好一些工作,而不必等到运行期,这个在很多时候可以提高程序在运行时的效率。 在c++11中,constexpr要求非常严格,这就导致了constexpr的并不是那么易用。在c++14中就可以使用if、局部变量和循环了(c++14可以,c++11报错)
constexpr int FuncNew(int n)
{
    if (n <= 0){
        return 0;
    }
    int sum = 0;
    for (int i = 0; i < n; ++i){
        sum += i;
    }
    return sum;
}
  1. 二进制字面量
    可以使用0b或者0B开头直接定义二进制变量。
{
    int bit1 = 0b1001;
    int bit2 = 0B1011;
    std::cout << bit1 << " " << bit2 << std::endl;// 输出结果:9 11
}
  1. 数字分隔符
    我们定义一个比较大的值的时候,有时候会很难一眼看出来他是多少,但是c++14之后,就可以对数字添加分隔符了,这使得大数字的可读性变得更高了。比如下面两个定义1亿的方式,第二个明显会比第一个可读性高很多。
int main(){
    // 一亿
    int val1 = 100000000;
    int val2 = 100'000'000;
    std::cout << val1 << " " << val2 << std::endl;
    return 0;
}

注意:数字分隔符并不会对数字本身有任何的影响,只是对数字可读性的增强。

  1. 函数返回值推导
    c++14新增了函数返回值的推导,当返回值声明为auto时,编译器会根据你的return语句推导出你的返回值类型。
template<typename T>
auto Func(T x, T y){
    return x + y;
}
int main(){
    std::cout << Func(3, 4) << std::endl; // 返回值推导为int
    std::cout << Func(3.1, 4.2) << std::endl; // 返回值推导为double
    return 0;
}

返回值的推导有几个限制条件:

  • 如果有多个推导语句,那么多个推导的结果必须一致
// 编译报错,第一个return推导为int,第二个return推导为double,两次推导结果不一致
auto Func(int flag){
    if (flag < 0){
        return 1;
    }
    else{
        return 3.14;
    }
}
  • 如果没有return或者return为void类型,那么auto会被推导为void。
auto f() {}              // returns void
auto g() { return f(); } // returns void
auto* x() {}             // error: cannot deduce auto* from void
  • 一旦在函数中看到return语句,从该语句推导出的返回类型就可以在函数的其余部分中使用,包括在其他return语句中。
auto Sum(int i){
    if (i <= 1){
        return i; // 返回值被推导为int
    }
    else{
        return Sum(i - 1) + i; // sum的返回值已经被推导出来了,所以这里是没有问题的
    }
}

但是如果还没被推导出来,那就不能使用。

auto Sum(int i){
    if (i > 1){
        return Sum(i - 1) + i;
    }
    else{
        return i;
    }
}
// 编译报错,因为Sum的返回值还没有被推导出来,所以还不能使用
//error: use of ‘auto Sum(int)’ before deduction of ‘auto’
  • 不能推导初始化列表。
auto func () { return {1, 2, 3}; }

// 编译报错
//error: returning initializer list
  • 虚函数不能使用返回值推导
struct Item{
    virtual auto Func();
};
// 编译报错
//error: virtual function cannot have deduced return type
  1. [[deprecated]]标记
    而c++14之后,c++引入了deprecated标记,可以在编译期间发出警告。
[[deprecated]]
void Func(){
    std::cout << "hello world!" << std::endl;
}

int main(){
    Func();
    return 0;
}
// 编译的时候会有警告信息
 warning: ‘void Func()’ is deprecated [-Wdeprecated-declarations]

8.1.2 库的新特性

  1. 新增std::make_unique
    std::unique_ptr<Item> pt = std::make_unique<Item>();
  2. 新增读写锁std::shared_timed_mutex与std::shared_lock
    c++11引入了多线程线程的一些库,但是是没有读写锁的,因此在c++14引入了读写锁的相关实现(头文件shared_mutex),其实c++14读写锁也还不够完善,知道c++17读写锁这块才算是完备起来,后面写c++17的时候计划把这块再完整的梳理一下。

std::shared_timed_mutex是带超时的读写锁对象
std::shared_lock是加锁的RAII实现,即构造时加锁,析构时解锁。

std::shared_timed_mutex mtx;
void Func(){
    // 加读锁,离开作用域时自动解锁
    std::shared_lock<std::shared_timed_mutex> lck(mtx);
    // do something
}
  1. std::exchange
    c++14新增了一个接口std::exchange(头文件utility),其实这个也并不算是新增的,因为这个接口其实在c++11的时候就有了,只不过在c++11中作为一个内部函数,不暴露给用户使用,在c++14中才把它暴露出来给用户使用。使用方法也很简单。
int main(){
    std::string s1 = "hello";
    std::string s2 = "world";
    std::exchange(s1, s2);
    std::cout << s1 << " " << s2 << std::endl;// 输出结果:world world
    return 0;
}

可以看到,exchange会把第二个值赋值给第一个值,但是不会改变第二个值。除此之外,我们这里说明一个关键的点。exchange的第二个值是万能引用,所以说他是既可以接收左值,也可以接收右值的,所以我们可以这样来使用。

  1. std::quoted
    建议看下官方介绍,我也没看懂。。。
    https://en.cppreference.com/w/cpp/14
posted @   星星星星yu  阅读(231)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 终于写完轮子一部分:tcp代理 了,记录一下
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
点击右上角即可分享
微信分享提示