C++11的一些特性

发布于:2025-06-22 ⋅ 阅读:(15) ⋅ 点赞:(0)

一、右值引用与移动构造

1.1右值引用

        左值与右值最大的区别就是,左值可以取地址,而右值不能取地址。例如:

1;//常量
10 + 2;//表达式
A();//匿名对象

还有将亡值,也就是超出作用域将要被销毁掉的值

        以上这些都是右值,不能取地址。那么什么是右值引用呢?

        与左值引用类似,右值引用用来引用右值。

int&& a = 1;//常量
int&& b = 10 + 2;//表达式
A&& c = A();//匿名对象

        区别于左值引用只使用一个“&”,右值引用需要使用“&&”与左值引用进行区分。右值引用不能绑定左值,但是如果使用move可以将左值转化为右值被右值引用,但是这个时候右值引用绑定的是左值的拷贝,并不是左值本身。

        另外,右值引用后的变量实际上是一个左值,也就是说例如:int&& a = 1; 这里的a实际上是左值,这是为了给移动构造做铺垫。

1.2移动构造

        当函数返回值是函数内创建的临时变量时,接受返回值的过程实际上需要经历两次拷贝构造,如果构造是深拷贝就会浪费资源,而移动构造就是为解决这个问题产生的:

class MyString {
private:
    char* data_;
    size_t size_;

public:
    // 移动构造函数
    MyString(MyString&& other)
        : data_(other.data_), // 直接"偷"指针
          size_(other.size_)  // 复制size
    {
        // 关键步骤:置空源对象,防止其析构时释放我们刚偷的内存
        other.data_ = nullptr;
        other.size_ = 0;
    }

    // 析构函数
    ~MyString() {
        delete[] data_; // 安全:如果data_是nullptr,delete[]是安全的
    }

    // ... 拷贝构造函数、拷贝赋值、移动赋值、其他成员函数 ...
};

        上面是移动构造的示例,通过右值引用接收将亡值,再将将亡值的数据直接转移走,如果正常拷贝构造需要new空间,那么使用移动构造就可以省去这个过程,节省大量资源。而移动赋值也是一样的,直接将数据转移,而不需要new新空间然后再赋值,从而将两次拷贝构造消耗的资源节省出来。

// 移动赋值运算符
    MyString& operator=(MyString&& other) {
        // 1. 防止自赋值 (虽然移动自赋值少见,但安全第一)
        if (this != &other) {
            // 2. 释放当前对象持有的资源
            delete[] data_;
            // 3. "偷"资源
            data_ = other.data_;
            size_ = other.size_;
            // 4. 置空源对象
            other.data_ = nullptr;
            other.size_ = 0;
        }
        return *this; // 5. 返回 *this
    }

        而右值引用的变量是左值,也是因为移动构造需要修改这个变量,如果变量是右值不能修改,那么移动构造就失去了意义。

二、lambda表达式与std::function

2.1 lambda表达式

        lambda表达式的格式为 [capture-list] (parameters) mutable -> return-type {function-body}

2.1.1 capture-list

        capture-list就是捕捉列表,用来捕捉当前作用域中的变量:

  • []:不捕获任何变量

  • [x]:按值捕获变量 x

  • [&x]:按引用捕获变量 x

  • [=]:按值捕获所有外部变量

  • [&]:按引用捕获所有外部变量

  • [this]:捕获当前类的 this 指针

  • [x, &y]:混合捕获(x 按值,y 按引用)

  • [=, &x]:默认按值捕获,但 x 按引用

  • [&, x]:默认按引用捕获,但 x 按值

         捕捉列表不能省略,用来确定是lambda表达式。

2.1.2 parameters

        类似于函数的参数,用来接收输入的参数,这个部分可以省略。

2.1.3 mutable

        如果有这个关键字,那么就能修改捕获到的值,但是不会影响被捕获的值本身,因为如果不是引用捕捉,那么捕捉实际上是对外部变量进行了拷贝。这个关键字也可以省略。

2.1.4  -> return-type

        用来标明返回的类型,通常也可以省略,因为会自动识别返回类型。

2.1.5 {function-body}

        这个部分{}内部的内容可以不写,但是这个花括号必须写,是函数的主体。

        lambda表达式使用时,类似于伪函数,通常使用auto f1 = [capture-list] (parameters) mutable -> return-type {function-body};接收lambda表达式,然后f1(parameters)即可。

int main(void) {
	auto f1 = [](int a, int b)->bool { return a < b;};
	cout << f1(1, 2);
	return 0;
}

        例如这样,就是一个简单的lambda表达式。

2.2 std::function

        function是一个包装器,可以将函数指针,伪函数,lambda表达式包装成类,例如:

int main(void) {
	function<bool(int, int)> f1 = [](int a, int b)->bool { return a < b;};
	cout << f1(1, 2);
	return 0;
}

         此时,f1的类型就是class std::function<bool __cdecl(int,int)>,如果不进行包装,f1的类型就比较复杂:class `int __cdecl main(void)'::`2'::<lambda_1>,且不同编译器的命名方式也不同。因为lambda表达式的类型是隐藏的。

        那么function有什么用呢?可以作为函数指针,伪函数,lambda表达式的类型去初始化类模板:

int main(void) {
	map<string, function<int(int, int)>> mop;
	mop.insert(make_pair("+", [](int a, int b) {return a + b;}));
	mop.insert(make_pair("-", [](int a, int b) {return a - b;}));
	mop.insert(make_pair("*", [](int a, int b) {return a * b;}));
	mop.insert(make_pair("/", [](int a, int b) {return a / b;}));
	cout << mop["+"](1, 2);
	return 0;
}

        这样就可以很简单的完成识别字符串中的运算类型并进行运算,而不需要使用switch写一大串,增加了可读性,且修改方便。

2.3 std::bind

int Add(int a, int b) {
	return a + b;
}

int main(void) {
	function<int(int)> f1 = bind(Add, placeholders::_1, 10);
	cout << f1(2);
	return 0;
}

         当函数参数固定时,重复输入就会比较麻烦,这个时候,使用bind就可以固定住想要固定的参数,就如上面代码所示,将b固定为10。

        placeholders::_1表示接收到的第一个参数_2则为第二个,例如:

int test(int a, int b, int c) {
	return a - b + c;
}

int main(void) {
	function<int(int, int)> f1 = bind(test, 10, placeholders::_2, placeholders::_1);
	cout << f1(2, 3);
	return 0;
}

        这里就是将a固定为10,第一个参数给到c,第二个参数给到b,因此结果等于10-3+2=9。

        bind通常与function一起使用。

三、统一初始化与初始化列表

        统一初始化其实就是可以使用{}进行初始化,但是这样的初始化有不同的含义,例如:

int a{1};
double b{};//初始化为0.0

class A{
public:
    A(int a)
        :_a(a)
    {}
private:
    int _a;
};
A c{10};

        在上面的例子中,初始化与直接使用()的方式类似,但是不能使用浮点数类型来初始化int类型,会报错因为涉及精度转换,类似的 float f1{1e40}; 这样也会报错。

        在这种情况下的初始化,本质上类似于类型转换,因此如果精度不正确就会报错。

list<int> l1  = {1, 2, 3, 4, 5};
map<string, string> s1 = {{"banana", "香蕉"},
                          {"apple", "苹果"},
                          {"orange", "橘子"}};

        而像上面这种情况下的初始化,则涉及到initializer_list这个在C++11中才有的容器。

        在list的例子中,{1, 2, 3, 4, 5}被初始化成initializer_list<int>类型,然后传入list的初始化函数:

        因此,如果要自定义的类型支持这种类似于C语言数组的初始化方式,就需要实现支持initializer_list的初始化方式。因为initializer_list支持迭代器,因此,只需要用范围for,然后调用insert或者push_back等即可。 

四、其他

  1. auto类型推导,当类型过长或复杂时,使用auto直接推导类型。
  2. nullptr 用来区别于NULL,代表空指针。
  3. decltype 可以进行类型推导,尽管typeid().name也可以推导类型,但是decltype可以用来初始化类模板。
  4. override 用于指示派生类中的成员函数应该重写基类中的虚函数,没有重写就会报错,类似于一种强制重写的机制。
  5. finish 用于限制类的继承或成员函数的重写。用在类则不能被继承,用在成员函数则不能被重写。

 对于智能指针和并发控制的介绍等后面再更新了。

 


网站公告

今日签到

点亮在社区的每一天
去签到