C++11/14/17/20 新特性精讲:Lambda, `auto`, Rvalue references, Concurrency

C++ 现代化漫游指南:告别石器时代,拥抱新世界

各位看官,今天咱们不聊八股文,也不整那些晦涩难懂的术语,就聊聊C++这门老牌编程语言,怎么在C++11、14、17、20这些版本里,变得越来越年轻,越来越好用。咱们的目标是:告别石器时代,拥抱现代C++的新世界!

想象一下,你还在用着古老的C++98,写着冗长无比的代码,羡慕着其他语言的简洁高效。别担心,现代C++就像一个魔法棒,挥一挥,你的代码就能焕然一新。

Lambda表达式:让代码会“说话”

Lambda表达式,绝对是现代C++中最亮眼的新特性之一。它就像一个匿名函数,你可以随时随地定义并使用,无需像以前那样费劲地定义一个全局函数或者函数对象。

以前的写法:

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

struct IsEven {
    bool operator()(int x) const {
        return x % 2 == 0;
    }
};

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5, 6};
    std::vector<int> evenNumbers;

    std::copy_if(numbers.begin(), numbers.end(), std::back_inserter(evenNumbers), IsEven());

    for (int num : evenNumbers) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    return 0;
}

这段代码,为了找到偶数,我们需要定义一个专门的结构体 IsEven,然后才能在 std::copy_if 中使用。是不是感觉很麻烦?

现在的写法(Lambda):

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

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5, 6};
    std::vector<int> evenNumbers;

    std::copy_if(numbers.begin(), numbers.end(), std::back_inserter(evenNumbers), [](int x){ return x % 2 == 0; });

    for (int num : evenNumbers) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    return 0;
}

看到了吗?Lambda表达式 [](int x){ return x % 2 == 0; } 就像一个简洁的“小精灵”,直接告诉 std::copy_if "我要偶数!"。

Lambda表达式的语法很简单:

  • []: 捕获列表,可以捕获外部变量。
  • (): 参数列表,和普通函数一样。
  • {}: 函数体,编写你的逻辑代码。

捕获列表的奥秘:

捕获列表决定了Lambda表达式如何访问外部变量。

  • []: 不捕获任何变量。
  • [x]: 按值捕获变量 x。Lambda表达式内部会拷贝一份 x 的值。
  • [&x]: 按引用捕获变量 x。Lambda表达式内部直接使用 x,修改 x 也会影响外部变量。
  • [=]: 按值捕获所有外部变量。
  • [&]: 按引用捕获所有外部变量。

Lambda的用武之地:

Lambda表达式在很多地方都能大显身手:

  • 作为函数参数传递,比如 std::sort 的自定义排序规则。
  • 简化回调函数,让代码更简洁易懂。
  • 创建函数对象,方便地封装一些小功能。

总之,Lambda表达式就像一块乐高积木,可以灵活地组合到你的代码中,让代码更加简洁、优雅。

auto关键字:偷懒的艺术

auto关键字,绝对是程序员的福音。它可以让编译器自动推断变量的类型,省去了我们手动声明类型的麻烦。

以前的写法:

#include <iostream>
#include <vector>

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5, 6};
    std::vector<int>::iterator it = numbers.begin(); // 冗长!

    for (; it != numbers.end(); ++it) {
        std::cout << *it << " ";
    }
    std::cout << std::endl;

    return 0;
}

这段代码,为了遍历 std::vector,我们需要手动声明迭代器的类型 std::vector<int>::iterator,是不是感觉很累?

现在的写法(auto):

#include <iostream>
#include <vector>

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5, 6};
    auto it = numbers.begin(); // 简洁!

    for (; it != numbers.end(); ++it) {
        std::cout << *it << " ";
    }
    std::cout << std::endl;

    return 0;
}

看到了吗?只需要用 auto 声明迭代器,编译器就能自动推断出它的类型。

auto的注意事项:

  • auto 必须初始化,否则编译器无法推断类型。
  • auto 不能用于函数参数类型,因为函数参数类型必须在编译时确定。
  • auto 推断出的类型可能会和你预期的不一样,需要注意。

auto的用武之地:

  • 简化代码,减少冗余的类型声明。
  • 处理复杂的类型,比如模板类型。
  • 提高代码的可读性,让代码更专注于逻辑。

auto关键字就像一个智能助手,帮你处理类型声明的琐事,让你更专注于代码的逻辑。

Rvalue References:让移动语义飞起来

Rvalue references (右值引用) 是 C++11 中引入的一个重要特性,它和移动语义 (move semantics) 密切相关。要理解 Rvalue References,首先要理解左值 (lvalue) 和右值 (rvalue) 的概念。

左值 (lvalue):

左值是指可以出现在赋值语句左边的表达式。简单来说,左值通常是一个可以取地址的变量或者对象。

右值 (rvalue):

右值是指只能出现在赋值语句右边的表达式。右值通常是一个临时对象或者字面量。

Rvalue References 的作用:

Rvalue References 允许我们区分左值和右值,并对右值进行特殊处理,从而实现移动语义。移动语义可以避免不必要的拷贝操作,提高程序的性能。

以前的写法(拷贝构造):

#include <iostream>
#include <vector>

class MyString {
public:
    MyString(const char* str) {
        std::cout << "Constructor called!" << std::endl;
        size_ = strlen(str) + 1;
        data_ = new char[size_];
        strcpy(data_, str);
    }

    // 拷贝构造函数
    MyString(const MyString& other) {
        std::cout << "Copy constructor called!" << std::endl;
        size_ = other.size_;
        data_ = new char[size_];
        strcpy(data_, other.data_);
    }

    ~MyString() {
        std::cout << "Destructor called!" << std::endl;
        delete[] data_;
    }

private:
    char* data_;
    size_t size_;
};

int main() {
    MyString str1 = "hello";
    MyString str2 = str1; // 调用拷贝构造函数
    return 0;
}

这段代码中,str2 = str1 会调用拷贝构造函数,它会分配新的内存,并将 str1 的内容拷贝到 str2 中。如果 str1 很大,拷贝操作会非常耗时。

现在的写法(移动构造):

#include <iostream>
#include <vector>

class MyString {
public:
    MyString(const char* str) {
        std::cout << "Constructor called!" << std::endl;
        size_ = strlen(str) + 1;
        data_ = new char[size_];
        strcpy(data_, str);
    }

    // 拷贝构造函数
    MyString(const MyString& other) {
        std::cout << "Copy constructor called!" << std::endl;
        size_ = other.size_;
        data_ = new char[size_];
        strcpy(data_, other.data_);
    }

    // 移动构造函数
    MyString(MyString&& other) noexcept {
        std::cout << "Move constructor called!" << std::endl;
        data_ = other.data_;
        size_ = other.size_;
        other.data_ = nullptr;
        other.size_ = 0;
    }

    ~MyString() {
        std::cout << "Destructor called!" << std::endl;
        delete[] data_;
    }

private:
    char* data_;
    size_t size_;
};

int main() {
    MyString str1 = "hello";
    MyString str2 = std::move(str1); // 调用移动构造函数
    return 0;
}

这段代码中,str2 = std::move(str1) 会调用移动构造函数。移动构造函数不会分配新的内存,而是直接将 str1 的指针指向 str2 的内存,并将 str1 的指针置为 nullptr。这样就避免了拷贝操作,提高了程序的性能。

std::move 的作用:

std::move 可以将一个左值转换为右值。注意,std::move 只是一个类型转换,它并不会真正地移动对象。移动操作是由移动构造函数或者移动赋值运算符完成的。

Rvalue References 的用武之地:

  • 实现移动语义,避免不必要的拷贝操作。
  • 提高程序的性能,尤其是在处理大型对象时。
  • 编写更加高效的容器和算法。

Rvalue References 就像一个“能量转移器”,可以将一个对象的资源转移到另一个对象,避免了资源的浪费。

Concurrency:让程序飞起来

C++11 引入了标准化的线程库,让我们可以方便地编写并发程序。并发程序可以同时执行多个任务,提高程序的效率。

以前的写法(平台相关的线程库):

在 C++11 之前,我们需要使用平台相关的线程库,比如 Windows 的 CreateThread 或者 Linux 的 pthread_create。这些线程库的使用方法各不相同,使得代码的可移植性很差。

现在的写法(std::thread):

#include <iostream>
#include <thread>

void task(int id) {
    std::cout << "Task " << id << " is running!" << std::endl;
}

int main() {
    std::thread t1(task, 1);
    std::thread t2(task, 2);

    t1.join();
    t2.join();

    std::cout << "All tasks finished!" << std::endl;

    return 0;
}

这段代码创建了两个线程 t1t2,它们分别执行 task 函数。t1.join()t2.join() 会阻塞主线程,直到 t1t2 执行完毕。

std::thread 的注意事项:

  • 创建线程后,必须调用 join() 或者 detach()
  • join() 会阻塞当前线程,直到子线程执行完毕。
  • detach() 会将子线程和当前线程分离,子线程会在后台运行。
  • 避免多个线程同时访问共享资源,可以使用互斥锁 (mutex) 来保护共享资源。

Concurrency 的用武之地:

  • 提高程序的效率,尤其是在处理 CPU 密集型任务时。
  • 编写响应更快的 GUI 程序。
  • 处理网络请求,提高服务器的并发能力。

C++11 的线程库就像一个“多引擎”,可以让你的程序同时运行多个任务,提高程序的效率。

总结

C++11、14、17、20 引入了许多新的特性,让 C++ 变得更加现代化、高效、易用。Lambda 表达式让代码更简洁,auto 关键字让类型推断更方便,Rvalue References 让移动语义成为可能,Concurrency 让程序可以并发执行。

当然,现代 C++ 的新特性远不止这些,还有智能指针、constexpr、范围 for 循环等等。希望这篇文章能激发你学习现代 C++ 的兴趣,让你在编程的道路上越走越远。记住,拥抱变化,才能不被时代抛弃!

最后,祝各位编程愉快,代码无 bug!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注