2022-9-20-C++11新特性(二)

简介: 2022-9-20-C++11新特性

转移构造函数

class MyString
{
public:
    ...
    //移动构造函数
    //参数是非const的右值引用
    MyString(MyString && t)
    {
        str = t.str; //拷贝地址,没有重新申请内存
        len = t.len;
        //原来指针置空,必须修改
        t.str = NULL;
        cout << "移动构造函数" << endl;
    }
    ...
private:
    char *str = NULL;
    int len = 0;
};

和拷贝构造函数类似,有几点需要注意:

参数(右值)的符号必须是右值引用符号,即&&。

参数(右值)不可以是常量,因为我们需要修改右值。

参数(右值)的资源链接和标记必须修改,否则,右值的析构函数就会释放资源,转移到新对象的资源也就无效了。

有了右值引用和转移语义,我们在设计和实现类时,对于需要动态申请大量资源的类,应该设计转移构造函数和转移赋值函数,以提高应用程序的效率。转移赋值函数

class MyString
{
public:
    ...
    //移动赋值函数
    //参数为非const的右值引用
    MyString &operator=(MyString &&tmp)
    {
        if(&tmp == this)
        {
            return *this;
        }
        //先释放原来的内存
        len = 0;
        delete [] str;
        //无需重新申请堆区空间
        len = tmp.len;
        str = tmp.str; //地址赋值
        tmp.str = NULL;
        cout << "移动赋值函数\n";
        return *this;
    }
   ...
private:
    char *str = NULL;
    int len = 0;
};

标准库函数 std::move()

如果已知一个命名对象不再被使用而想对它调用转移构造函数和转移赋值函数,也就是把一个左值引用当做右值引用来使用。标准库提供了函数 std::move(),这个函数以非常简单的方式将左值引用转换为右值引用。

完美转发 std::forward()

完美转发适用于这样的场景:需要将一组参数原封不动的传递给另一个函数。

“原封不动”不仅仅是参数的值不变,在 C++ 中,除了参数值之外,还有一下两组属性:左值/右值和 const/non-const。完美转发就是在参数传递过程中,所有这些属性和参数值都不能改变,同时,而不产生额外的开销,就好像转发者不存在一样。在泛型函数中,这样的需求非常普遍。

C++11是通过引入一条所谓“引用折叠”(reference collapsing)的新语言规则,并结合新的模板推导规则来完成完美转发。

一旦定义中出现了左值引用,引用折叠总是优先将其折叠为左值引用。

智能指针

C++11中有unique_ptr、shared_ptr与weak_ptr等智能指针(smart pointer),定义在中。可以对动态资源进行管理,保证任何情况下,已构造的对象最终会销毁,即它的析构函数最终会被调用。

unique_ptr

unique_ptr持有对对象的独有权,同一时刻只能有一个unique_ptr指向给定对象(通过禁止拷贝语义、只有移动语义来实现)。

unique_ptr指针本身的生命周期:从unique_ptr指针创建时开始,直到离开作用域。

离开作用域时,若其指向对象,则将其所指对象销毁(默认使用delete操作符,用户可指定其他操作)。

#include <iostream>
#include <memory>
using namespace std;
int main()
{
    unique_ptr<int> up1(new int(11));   // 无法复制的unique_ptr
    //unique_ptr<int> up2 = up1;        // err, 不能通过编译
    unique_ptr<int> up3 = move(up1);    // 现在p3是数据的唯一的unique_ptr
    up3.reset();            // 显式释放内存
    up1.reset();            // 不会导致运行时错误
    up4.reset(new int(44)); //"绑定"动态对象
    up4 = nullptr;//显式销毁所指对象,同时智能指针变为空指针。与up4.reset()等价
    unique_ptr<int> up5(new int(55));
    int *p = up5.release(); //只是释放控制权,不会释放内存
    delete p; //释放堆区资源
    return 0;
}

shared_ptr

shared_ptr允许多个该智能指针共享第“拥有”同一堆分配对象的内存,这通过引用计数(reference counting)实现,会记录有多少个shared_ptr共同指向一个对象,一旦最后一个这样的指针被销毁,也就是一旦某个对象的引用计数变为0,这个对象会被自动删除。

int main()
{
    shared_ptr<int> sp1(new int(22));
    shared_ptr<int> sp2 = sp1;
    cout << "count: " << sp2.use_count() << endl; //打印引用计数2
    sp1.reset();    //显式让引用计数减1
    cout << "count: " << sp2.use_count() << endl; //打印引用计数1
    return 0;
}

通常情况下shared_ptr可以正常运转,但是在循环引用的场景下,shared_ptr无法正确释放内存。循环引用,顾名思义,A指向B,B指向A,在表示双向关系时,是很可能出现这种情况的,例如:

#include <iostream>
#include <memory>
using namespace std;
class Son;
class Father {
public:
    shared_ptr<Son> son_;
    Father() {
        cout << __FUNCTION__ << endl;
    }
    ~Father() {
        cout << __FUNCTION__ << endl;
    }
};
class Son {
public:
    shared_ptr<Father> father_;
    Son() {
        cout << __FUNCTION__ << endl;
    }
    ~Son() {
        cout << __FUNCTION__ << endl;
    }
};
int main()
{
    auto son = make_shared<Son>();
    auto father = make_shared<Father>();
    son->father_ = father;
    father->son_ = son;
    cout << "son: " << son.use_count() << endl;
    cout << "father: " << father.use_count() << endl;
    return 0;
}

可以看到,程序分别执行了Son和Father的构造函数,但是没有执行析构函数,出现了内存泄漏

weak_ptr

weak_ptr是为配合shared_ptr而引入的一种智能指针来协助shared_ptr工作,它可以从一个shared_ptr或另一个weak_ptr对象构造,它的构造和析构不会引起引用计数的增加或减少。没有重载 * 和-> 但可以使用lock获得一个可用的shared_ptr对象

weak_ptr的使用更为复杂一点,它可以指向shared_ptr指针指向的对象内存,却并不拥有该内存,而使用weak_ptr成员lock,则可返回其指向内存的一个share_ptr对象,且在所指对象内存已经无效时,返回指针空值nullptr。

#include <iostream>
#include <memory>
using namespace std;
int main()
{
    shared_ptr<int> sp1(new int(22));
    shared_ptr<int> sp2 = sp1;
    weak_ptr<int> wp = sp1; // 指向shared_ptr<int>所指对象
    cout << "count: " << wp.use_count() << endl; //打印计数器 2
    sp1.reset();
    cout << "count: " << wp.use_count() << endl; // 1
    sp2.reset();
    cout << "count: " << wp.use_count() << endl; // 0
    return 0;
}

解决循环引用的问题:

class Son;
class Father {
public:
    SharedPtr<Son> son_;
    Father() {
        cout << __PRETTY_FUNCTION__ << endl;
    }
    ~Father() {
        cout << __PRETTY_FUNCTION__ << endl;
    }
};
class Son {
public:
    WeakPtr<Father> father_;  // 将SharedPtr改为WeakPtr
    Son() {
        cout << __PRETTY_FUNCTION__ << endl;
    }
    ~Son() {
        cout << __PRETTY_FUNCTION__ << endl;
    }
};
int main()
{
    auto son_ = new Son();  // 创建一个Son对象,返回指向Son对象的指针son_
    auto father_ = new Father();  // 创建一个Father对象,返回指向Father对象的指针father_
    SharedPtr<Son> son(son_);  // 调用SharedPtr构造函数:son.counter=1, son.weakref=0
    SharedPtr<Father> father(father_);  // 调用SharedPtr构造函数:father.counter=1, father.weakref=0
    son.resource->father_ = father;  // 调用WeakPtr赋值函数:father.counter=1, father.weakref=1
    father.resource->son_ = son;  // 调用SharedPtr赋值函数:son.counter=2, son.weakref=0
    cout << "son: " << son.use_count() << endl;
    cout << "father: " << father.use_count() << endl;
    return 0;
}

闭包实现

闭包有很多种定义,一种说法是,闭包是带有上下文的函数。即有状态的函数。

那什么叫 “带上状态” 呢? 意思是这个闭包有属于自己的变量,这些个变量的值是创建闭包的时候设置的,并在调用闭包的时候,可以访问这些变量。

函数是代码,状态是一组变量,将代码和一组变量捆绑 (bind) ,就形成了闭包。

闭包的状态捆绑,必须发生在运行时。

仿函数:重载 operator()

仿函数实现闭包:

class MyFunctor
{
public:
    MyFunctor(int tmp) : round(tmp) {}
    int operator()(int tmp) { return tmp + round; }
private:
    int round;
};
int main()
{
    int round = 2;
    MyFunctor f(round);//调用构造函数
    cout << "result = " << f(1) << endl; //operator()(int tmp)
    return 0;
}

std::bind绑定器

std::function

在C++中,可调用实体主要包括:函数、函数指针、函数引用、可以隐式转换为函数指定的对象,或者实现了opetator()的对象。

C++11中,新增加了一个std::function类模板,它是对C++中现有的可调用实体的一种类型安全的包裹。通过指定它的模板参数,它可以用统一的方式处理函数、函数对象、函数指针,并允许保存和延迟执行它们。

#include <iostream>
#include <functional>   //std::cout
using namespace std;
void func(void)
{//普通全局函数
    cout << __func__ << endl;
}
class Foo
{
public:
    static int foo_func(int a)
    {//类中静态函数
        cout << __func__ << "(" << a << ") ->: ";
        return a;
    }
};
class Bar
{
public:
    int operator()(int a)
    {//仿函数
        cout << __func__ << "(" << a << ") ->: ";
        return a;
    }
};
int main()
{
    //绑定一个普通函数
    function< void(void) > f1 = func;
    f1();
    //绑定类中的静态函数
    function< int(int) > f2 = Foo::foo_func;
    cout << f2(111) << endl;
    //绑定一个仿函数
    Bar obj;
    f2 = obj;
    cout << f2(222) << endl;
    /*
     运行结果:
        func
        foo_func(111) ->: 111
        operator()(222) ->: 222
    */
    return 0;
}

std::function对象最大的用处就是在实现函数回调,使用者需要注意,它不能被用来检查相等或者不相等,但是可以与NULL或者nullptr进行比较。

std::bind

std::bind是这样一种机制,它可以预先把指定可调用实体的某些参数绑定到已有的变量,产生一个新的可调用实体,这种机制在回调函数的使用过程中也颇为有用。

C++98中,有两个函数bind1st和bind2nd,它们分别可以用来绑定functor的第一个和第二个参数,它们都是只可以绑定一个参数,各种限制,使得bind1st和bind2nd的可用性大大降低。

在C++11中,提供了std::bind,它绑定的参数的个数不受限制,绑定的具体哪些参数也不受限制,由用户指定。

lambda表达式

C++11中的lambda表达式用于定义并创建匿名的函数对象,以简化编程工作。

lambda表达式的基本构成:

  1. 函数对象参数

[],标识一个lambda的开始,这部分必须存在,不能省略。函数对象参数是传递给编译器自动生成的函数对象类的构造函数的。函数对象参数只能使用那些到定义lambda为止时lambda所在作用范围内可见的局部变量(包括lambda所在类的this)。函数对象参数有以下形式:

  • 空。没有使用任何函数对象参数。
  • =。函数体内可以使用lambda所在作用范围内所有可见的局部变量(包括lambda所在类的this),并且是值传递方式(相当于编译器自动为我们按值传递了所有局部变量)。
  • &。函数体内可以使用lambda所在作用范围内所有可见的局部变量(包括lambda所在类的this),并且是引用传递方式(相当于编译器自动为我们按引用传递了所有局部变量)。
  • this。函数体内可以使用lambda所在类中的成员变量。
  • a。将a按值进行传递。按值进行传递时,函数体内不能修改传递进来的a的拷贝,因为默认情况下函数是const的。要修改传递进来的a的拷贝,可以添加mutable修饰符。
  • &a。将a按引用进行传递。
  • a, &b。将a按值进行传递,b按引用进行传递。
  • =,&a, &b。除a和b按引用进行传递外,其他参数都按值进行传递。
  • &, a, b。除a和b按值进行传递外,其他参数都按引用进行传递。
  1. 操作符重载函数参数
    标识重载的()操作符的参数,没有参数时,这部分可以省略。参数可以通过按值(如:(a,b))和按引用(如:(&a,&b))两种方式进行传递。
  2. 可修改标示符
    mutable声明,这部分可以省略。按值传递函数对象参数时,加上mutable修饰符后,可以修改按值传递进来的拷贝(注意是能修改拷贝,而不是值本身)。
  3. 函数返回值
    ->返回值类型,标识函数返回值的类型,当返回值为void,或者函数体中只有一处return的地方(此时编译器可以自动推断出返回值类型)时,这部分可以省略。
  4. 函数体
    {},标识函数的实现,这部分不能省略,但函数体可以为空。
    除去在语法层面上的不同,lambda和仿函数有着相同的内涵——都可以捕获一些变量作为初始化状态,并接受参数进行运行。

而事实上,仿函数是编译器实现lambda的一种方式,通过编译器都是把lambda表达式转化为一个仿函数对象。因此,在C++11中,lambda可以视为仿函数的一种等价形式。

lambda表达式的类型在C++11中被称为“闭包类型”,每一个lambda表达式则会产生一个临时对象(右值)。因此,严格地将,lambda函数并非函数指针。

不过C++11标准却允许lambda表达式向函数指针的转换,但提前是lambda函数没有捕获任何变量,且函数指针所示的函数原型,必须跟lambda函数函数有着相同的调用方式。

lambda表达式的价值在于,就地封装短小的功能闭包,可以及其方便地表达出我们希望执行的具体操作,并让上下文结合更加紧密。

线程

在C++11之前,C/C++一直是一种顺序的编程语言。顺序是指所有指令都是串行执行的,即在相同的时刻,有且仅有单个CPU的程序计数器执行代码的代码段,并运行代码段中的指令。而C/C++代码也总是对应地拥有一份操作系统赋予进程的包括堆、栈、可执行的(代码)及不可执行的(数据)在内的各种内存区域。

而在C++11中,一个相当大的变化就是引入了多线程的支持。这使得C/C++语言在进行线程编程时,不比依赖第三方库。

目录
相关文章
|
1月前
|
编译器 程序员 定位技术
C++ 20新特性之Concepts
在C++ 20之前,我们在编写泛型代码时,模板参数的约束往往通过复杂的SFINAE(Substitution Failure Is Not An Error)策略或繁琐的Traits类来实现。这不仅难以阅读,也非常容易出错,导致很多程序员在提及泛型编程时,总是心有余悸、脊背发凉。 在没有引入Concepts之前,我们只能依靠经验和技巧来解读编译器给出的错误信息,很容易陷入“类型迷路”。这就好比在没有GPS导航的年代,我们依靠复杂的地图和模糊的方向指示去一个陌生的地点,很容易迷路。而Concepts的引入,就像是给C++的模板系统安装了一个GPS导航仪
104 59
|
1月前
|
存储 编译器 C++
【C++】面向对象编程的三大特性:深入解析多态机制(三)
【C++】面向对象编程的三大特性:深入解析多态机制
|
1月前
|
存储 编译器 C++
【C++】面向对象编程的三大特性:深入解析多态机制(二)
【C++】面向对象编程的三大特性:深入解析多态机制
|
1月前
|
编译器 C++
【C++】面向对象编程的三大特性:深入解析多态机制(一)
【C++】面向对象编程的三大特性:深入解析多态机制
|
1月前
|
存储 安全 编译器
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值(一)
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值
|
28天前
|
C++
C++ 20新特性之结构化绑定
在C++ 20出现之前,当我们需要访问一个结构体或类的多个成员时,通常使用.或->操作符。对于复杂的数据结构,这种访问方式往往会显得冗长,也难以理解。C++ 20中引入的结构化绑定允许我们直接从一个聚合类型(比如:tuple、struct、class等)中提取出多个成员,并为它们分别命名。这一特性大大简化了对复杂数据结构的访问方式,使代码更加清晰、易读。
32 0
|
2月前
|
编译器 C++ 计算机视觉
C++ 11新特性之完美转发
C++ 11新特性之完美转发
50 4
|
2月前
|
Java C# C++
C++ 11新特性之语法甜点1
C++ 11新特性之语法甜点1
33 4
|
2月前
|
安全 程序员 编译器
C++ 11新特性之auto和decltype
C++ 11新特性之auto和decltype
39 3
|
2月前
|
设计模式 缓存 安全
C++ 11新特性之week_ptr
C++ 11新特性之week_ptr
35 2