ghostbody / 15-cpp-learning

15 c++ learning repository of SDCS SYSU
6 stars 3 forks source link

C++11 features #16

Open MoRunChang2015 opened 8 years ago

MoRunChang2015 commented 8 years ago

Lambda function

Lambda 函数/表达式也称匿名函数是C++11新加入的一个特性,可以以匿名的方式声明一个函数,这样的匿名函数大多数情况下会作为回调函数使用,也可以通过函数指针的方法给匿名函数一个名字用于调用,如以下例子:

#include <iostream>
#include <vector>

using namespace std;
int main() {
    auto p = []{ cout << "HaHa" << endl;};
    p();
    return 0;
}

编译运行会在命令行中输出“HaHa”字符串。 Lambda函数的基本格式是 [ 函数对象参数 ](函数参数)mutable/exception声明->返回类型{函数体} 接下来逐项来解释

  1. 函数对象参数 这标识一个lambda的开始,所以这部分必须存在不能省略。函数对象参数是用于传递给编译器自动生成的函数对象类的构造函数。函数对象只能使用那些到定义lambda为止是lambda所在作用域可见的局部变量(保护this指针),函数对象参数有以下形式

    1.空,表示没有使用任何函数对象参数,函数体内不能使用外部的局部变量 2.=,函数体内可以使用外部的局部变量,并且是按值传递的形式(默认以const形式传入) 3.&, 函数体内可以使用外部的局部变量,并且是引用传递的形式 4.this,函数体内可以使用llambda所在类中成员变量 5.=/&, &a, b, 函数体内可以使用外部的局部变量,并且a是引用传递方式,b是值传递方式其他变量按照值/引用传递的方式。

2.函数参数,即传入函数的的参数,若没用参数时可以省略 3.mutable和exception声明,这部分也可以省略,加上mutable时,可以对按值传入的拷贝修改(修改拷贝的值)如下程序

#include <iostream>
#include <vector>

using namespace std;
int main() {
    int a = 1;
    auto p = [=]{ a = 2;cout << "HaHa" << endl;};
    p();
    return 0;
}

编译会出错因为a在lambda函数体里是const int类型不能修改它的只,只有加上mutable后

#include <iostream>
#include <vector>

using namespace std;
int main() {
    int a = 1;
    auto p = [=]()mutable{ a = 2;cout << "HaHa" << endl;};
    p();
    cout << a << endl;
    return 0;
}

编译可以通过,但是最后输出的a的值还是1,exception声明用于表示函数可以抛出的异常类,如需要抛出整形异常,只要加上throw(int),如下

#include <iostream>
#include <vector>

using namespace std;
int main() {
    int a = 1;
    auto p = [=]()throw(int){ throw(a);cout << "HaHa" << endl;};
    try{
        p();
    } catch(int e) {
        cout << a << endl;
    }
    return 0;
}

4.->返回值声明,用于标识返回值的类型,若为空则可以省略或者若编译器可以推断出返回类型也可省略。 5.函数体,函数执行语句,不可省略 lambda函数经常用于for_each的回调函数如下程序

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;
int main() {
    vector<int> v;
    for (int i = 1;  i <= 5; i++)
        v.push_back(i);
    int total = 0;
    for_each(v.begin(), v.end(), [&](int v) {total += v;});
    cout << total << endl;
    return 0;
}

通过lambda作为回调函数计算1~5的和,结果输出为15

MoRunChang2015 commented 8 years ago

Auto

用auto关键字关键字声明变量让编译推断出正确的类型,这样可以让程序更加得简介也减少了程序员的负担,但是有时候也会减低程序的可读性。 下面例子展示auto关键字的用法

#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;

auto getValue(vector<int>::iterator it)->int {
    return *it;
}

int main() {
    vector<int> v;
    v.push_back(1);
    auto ans = getValue(v.begin());
    cout << ans  << endl;
    return 0;
}

从上面的例子可以看出,auto类型可以作为函数的返回值类型的,但是要在函数后面指明(好像是为了好看),还有使用auto关键字要注意以下几点 1、使用auto关键字的变量必须有初始值,编译器才能推导出变量类型。 2、在传递const变量的时候,使用auto必须自己加const

const int a =10;
auto b = a;                // b的类型是int
const auto c = a;      // c 的类型是const int
MoRunChang2015 commented 8 years ago

右值引用和移动复制

在理解右值引用之前首先要理解什么是左值什么是右值。 左值是一个指向某内存空间的表达式,并且我们可以用&操作符获得该内存空间的地址。右值就是非左值的表达式。 左值与右值的差别简单地讲就是能否对其取地址。 在了解右值引用之前要先理解左值引用

左值引用

左值引用分为产量左值引用和非常量左值引用

非常量左值引用

    int a = 5;
    const int b = 5;

    int& v1 = a; // 绑定到非常量左值
    int& v2 = 5; // 常量右值, compile error
    int& v3 = b; // 常量左值, compile error
    int& v4 = a + b; // 非常量右值, compile error, a + b为临时对象

常量左值引用

    int a = 5;
    const int b = 5;

    const int& v1 = a; // 绑定到非常量左值   
    const int& v2 = 5; // 常量右值
    const int& v3 = b; // 常量左值
    const int& v4 = a + b; // 非常量右值, a + b为临时对象

C++11 增加一个新的非常数引用(reference)类型,称作右值引用(R-value reference),标记为T &&。右值引用所引用的临时对象可以在该临时对象被初始化之后做修改,这是为了允许 move 语义。

右值引用

右值引用也像左值引用一样分为常量右值应用,和非常量右值引用

非常量右值引用

int a = 5;  
const int b = 5;  

int&& v1 = 5;  
int&& v2 = a;   //compile error  
int&& v3 = b;   //compile error  
int&& v4 = a + b;  

常量右值引用

int a = 5;  
const int b = 5;  

const int&& v1 = 5;  
const int&& v2 = a; //compile error  
const int&& v3 = b; //compile error  
const int&& v4 = a + b;  

c++11引入右值引用主要是为了实现move语义

Move语义

根据上面的定义可知,右值经常是一些计算结果的中间值,它没有名字,后面到程序也无法调用这个右值,但是这样的右值也是需要构造和销毁的,构造和销毁产生对内存的操作将会影响整个程序的性能。如以下程序

#include <iostream>
#include <algorithm>

using namespace std;

class Person {
    char* name;

   public:
    Person(const char* p) {
        size_t n = strlen(p) + 1;
        name = new char[n];
        memcpy(name, p, n);
    }
    Person(const Person& p) {
        size_t n = strlen(p.name) + 1;
        name = new char[n];
        memcpy(name, p.name, n);
    }
    ~Person() { delete[] name; }
};

Person getAlice() {
    Person p("alice");       // 对象创建。调用构造函数,一次 new 操作
    return p;                      // 返回值创建。调用拷贝构造函数,一次 new 操作
                                          // p 析构。一次 delete 操作
}
int main() {
    Person a = getAlice();  // 对象创建。调用拷贝构造函数,一次 new 操作
                                            // 右值析构。一次 delete 操作
    return 0;
}                                            // a 析构。一次 delete 操作

就是这样一个简单的程序调用了3次构造函数和3次析构函数(原理上都是3次,但实际上编译器会对程序做一个返回值的优化减少了构造和析构的次数) 实际运行结果如下

#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;

class Person {
    char* name;
public:
    Person(const char* p) {
        size_t n = strlen(p) + 1;
        name = new char[n];
        memcpy(name, p, n);
        cout << "constructor" << endl;
    }
    Person(const Person& p) {
        size_t n = strlen(p.name) + 1;
        name = new char[n];
        memcpy(name, p.name, n);
        cout << "copy constructor" << endl;
    }
    const Person& operator=(const Person& rhs) {
        cout << "operator = " << endl;
        delete[] name;
        size_t n = strlen(rhs.name) + 1;
        name = new char[n];
        memcpy(name, rhs.name, n);
        return *this;
    }
    ~Person() {
        delete[] name;
        cout << "destructor" << endl;
    }
};

Person getAlice() {
    Person p("alice");
    return p;
}

int main() {
    cout << "______________________" << endl;
    Person a = getAlice();
    cout << "______________________" << endl;
    a = getAlice();
    cout << "______________________" << endl;
}

constructor


constructor operator = destructor


destructor

在执行a=getAlice()语句时调用了重载过了的=运算符,销毁了以前的内存也重新分配了内存。 引入了右值引用后,rhs不一定要声明为const。它是可以变的,这样我们就可以把右值的数据name直接拿过来而不需要重新申请内存。右值引用的语法是&& 新的=运算符重载和复制构造函数如下

    Person(Person&& p) {
        cout << "move copy constructor" << endl;
        name = p.name;
        p.name = nullptr;
    }
    const Person& operator=(Person&& rhs) {
        cout << "move operator=" << endl;
        delete[] name;
        name = rhs.name;
        rhs.name = nullptr;
       return *this;
    }

程序运行结果如下


constructor


constructor move operator= destructor


destructor

这样就避免了一次内存释放和一次内存申请,使程序的性能提升。

MoRunChang2015 commented 8 years ago

Smart Pointer

c++11中的标准库中包含的智能指针主要用于确保程序不存在内存和资源泄露且是异常安全的。下面引用msdn上的例子说明

void UseSmartPointer()
{
    // Declare a smart pointer on stack and pass it the raw pointer.
    unique_ptr<Song> song2(new Song(L"Nothing on You", L"Bruno Mars"));

    // Use song2...
    wstring s = song2->duration_;
    //...

} // song2 is deleted automatically here.

我们可以看出,智能指针其实是在一个类模板,在声明的时候要通过类的构造函数获取资源实例,在智能指针生命周期结束时自动调用类的析构函数释放内存。

class LargeObject
{
public:
    void DoSomething(){}
};

void ProcessLargeObject(const LargeObject& lo){}
void SmartPointerDemo()
{    
    // Create the object and pass it to a smart pointer
    std::unique_ptr<LargeObject> pLarge(new LargeObject());

    //Call a method on the object
    pLarge->DoSomething();

    // Pass a reference to a method.
    ProcessLargeObject(*pLarge);

} //pLarge is deleted automatically when function block goes out of scope.

智能指针也重载了->运算符和*运算符,使得智能指针可以像一般的指针一样使用,而且智能指针占用的内存跟一般指针是一样的。

c++标准库智能指针的类型

  1. unique_ptr 只允许基础指针的一个所有者。可以移到新所有者,但不会复制或共享。unique_ptr 小巧高效;大小等同于一个指针且支持 右值引用,从而可实现快速插入和对 STL 集合的检索。
  2. shared_ptr 采用引用计数的智能指针。如果你想要将一个原始指针分配给多个所有者(例如,从容器返回了指针副本又想保留原始指针时),请使用该指针。直至所有 shared_ptr 所有者超出了范围或放弃所有权,才会删除原始指针。大小为两个指针;一个用于对象,另一个用于包含引用计数的共享控制块。
  3. weak_ptr 结合 shared_ptr 使用的特例智能指针。 weak_ptr 提供对一个或多个 shared_ptr 实例拥有的对象的访问,但不参与引用计数。如果你想要观察某个对象但不需要其保持活动状态,请使用该实例。