C++11【下】

在这里插入图片描述

欢迎来到Cefler的博客
博客主页:那个传说中的man的主页
个人专栏:题目解析
推荐文章:题目大解析(3)


目录

  • 新的类功能
    • 类成员变量初始化
    • 强制生成默认函数的关键字default
    • 禁止生成默认函数的关键字delete
    • 继承和多态中的final与override关键字
  • 可变参数模板
    • empalce相关接口函数
  • Lambda表达式
    • 注意要点
    • 函数对象与lambda表达式
  • 包装器
    • function包装器
    • function包装器与map的配合使用
  • bind函数
    • 绑定函数指针和参数
    • 绑定函数对象和参数
    • 绑定成员函数和对象指针
    • 绑定函数对象和引用参数

新的类功能

原来C++类中,有6个默认成员函数:

  1. 构造函数
  2. 析构函数
  3. 拷贝构造函数
  4. 拷贝赋值重载
  5. 取地址重载
  6. const 取地址重载
    默认成员函数就是我们不写编译器会生成一个默认的。

C++11 新增了两个:移动构造函数移动赋值运算符重载

针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下

  • 如果你没有自己实现移动构造函数,且没有实现析构函数拷贝构造拷贝赋值重载中的任
    意一个,即四没。那么编译器会自动生成一个默认移动构造默认生成的移动构造函数,对于内置类
    型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,
    如果实现了就调用移动构造,没有实现就调用拷贝构造
  • 如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中
    的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内
    置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋
    值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造
    完全类似)
  • 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值,如果需要,要自己写了老弟。

类成员变量初始化

C++11允许在类定义时给成员变量初始缺省值,默认生成构造函数会使用这些缺省值初始化

强制生成默认函数的关键字default

C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原
因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以
使用default关键字显示指定移动构造生成。

class Person
{
public:
 Person(const char* name = "", int age = 0)
 :_name(name)
 , _age(age)
 {}
 Person(const Person& p)
 :_name(p._name)
 ,_age(p._age)
 {}
 Person(Person&& p) = default;
private:
 bit::string _name;
 int _age;
};
int main()
{
 Person s1;
 Person s2 = s1;
 Person s3 = std::move(s1);
 return 0;
}

禁止生成默认函数的关键字delete

如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明补丁
已,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即
可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。

class Person
{
public:
 Person(const char* name = "", int age = 0)
 :_name(name)
 , _age(age)
 {}
 Person(const Person& p) = delete;
private:
 bit::string _name;
 int _age;
};
int main()
{
 Person s1;
 Person s2 = s1;
 Person s3 = std::move(s1);
 return 0;
}

继承和多态中的final与override关键字

这个在【C++】多态中,不再赘述

可变参数模板

C++11引入了可变参数模板,它允许函数或类模板接受可变数量的参数。使用可变参数模板可以编写更加灵活和通用的代码。

下面就是一个基本可变参数的函数模板

// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}

上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数
包”,它里面包含了0到N(N>=0)个模版参数。我们无法直接获取参数包args中的每个参数的,
只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特
点,也是最大的难点,即如何展开可变模版参数。由于语法不支持使用args[i]这样方式获取可变
参数,所以我们的用一些奇招来一一获取参数包的值

下面是一个示例,展示了用递归函数方式展开参数包进行使用可变参数模板

#include 

// 基本情况,递归终止条件
void print() {
    std::cout << std::endl;
}

// 可变参数模板,递归调用打印每个参数
template<typename T, typename... Args>
void print(const T& first, const Args&... args) {
    std::cout << first << " ";
    print(args...);  // 递归调用,传递剩余参数
}

int main() {
    print(1, 2, 3, "hello", 4.5);
    return 0;
}

在上面的示例中,我们定义了一个可变参数模板函数print,它可以接受任意数量的参数。基本情况是一个空函数print(),当没有参数时输出一个换行符。递归情况使用模板参数包(typename... Args)来接受可变数量的参数,并使用递归调用打印每个参数。在main函数中,我们调用print函数,传递了整数、字符串和浮点数作为参数,它们都被打印出来。

使用逗号表达式展开参数包

template <class T>
void PrintArg(T t)
{
 cout << t << " ";
}
//展开函数
template <class ...Args>
void ShowList(Args... args)
{
 int arr[] = { (PrintArg(args), 0)... };
 //int arr[] = { PrintArg(args)...};
 cout << endl;
}
int main()
{
 ShowList(1);
 ShowList(1, 'A');
 ShowList(1, 'A', std::string("sort"));
 return 0;
}

empalce相关接口函数

template <class... Args>
void emplace_back (Args&&... args);

首先我们看到的emplace系列的接口,支持模板的可变参数,并且万能引用。那么相对insert和
emplace系列接口的优势到底在哪里呢?

int main()
{
 std::list< std::pair<int, char> > mylist;
 // emplace_back支持可变参数,拿到构建pair对象的参数后自己去创建对象
 // 那么在这里我们可以看到除了用法上,和push_back没什么太大的区别
 mylist.emplace_back(10, 'a');
 mylist.emplace_back(20, 'b');
 mylist.emplace_back(make_pair(30, 'c'));
 mylist.push_back(make_pair(40, 'd'));
 mylist.push_back({ 50, 'e' });
 for (auto e : mylist)
 cout << e.first << ":" << e.second << endl;
 return 0;
}
int main()
{
// 下面我们试一下带有拷贝构造和移动构造的bit::string,再试试呢
// 我们会发现其实差别也不到,emplace_back是直接构造了,push_back
// 是先构造,再移动构造,其实也还好。
 std::list< std::pair<int, bit::string> > mylist;
 mylist.emplace_back(10, "sort");
 mylist.emplace_back(make_pair(20, "sort"));
 mylist.push_back(make_pair(30, "sort"));
 mylist.push_back({ 40, "sort"});
 return 0;
}

emplace_backpush_back都是向容器中添加元素的成员函数,但它们有一些重要的区别。

push_back函数会创建一个临时对象,并将其复制或移动到容器中。如果容器存储的是对象而不是指针,那么这将涉及到对象的复制或移动构造函数和析构函数的调用。这个额外的开销可能会导致效率降低,特别是在频繁插入元素的情况下。

相比之下,emplace_back函数直接在容器的末尾就地构造一个新的元素(直接构造),而不是先创建一个临时对象。这意味着我们可以避免创建临时对象、复制或移动和析构操作,从而提高性能。实际上,emplace_back通常比push_back更快。

在使用emplace_back时,我们需要注意以下几点:

  1. emplace_back参数应该与容器中元素的构造函数相匹配。

  2. 如果容器存储的是指针,则必须传递指向堆中分配的对象的指针。否则,在容器释放时可能会出现内存泄漏。

  3. 对于std::vectorstd::string等容器,emplace_backpush_back的语义是相同的。

总之,emplace_back是一个更加高效的添加元素的方法,可以避免创建临时对象并提高性能。

Lambda表达式

C++11引入了lambda表达式,它是一种方便的方式来创建匿名函数。lambda表达式可以在需要函数对象的地方使用,并且可以捕获周围作用域中的变量。

一个基本的lambda表达式的语法如下:

[capture list](parameters)mutable -> return_type { 
    // 函数体
}

其中:

  • capture list:捕获列表,用于指定lambda表达式访问的外部变量。
  • parameters:参数列表,可选地指定输入参数。
  • return_type:返回类型,可选地指定返回值类型。
  • {}:函数体,包含实际的操作和代码逻辑。
  • mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性mutable关键字用于在lambda表达式中标记捕获的变量可以被修改。使用该修饰符时,参数列表不可省略(即使参数为空)。

下面是一个简单的示例,展示了lambda表达式的用法:

#include 

int main() {
    int x = 5;
    int y = 10;

    auto sum = [](int a, int b) -> int {
        return a + b;
    };

    auto result = sum(x, y);
    std::cout << "Sum: " << result << std::endl;

    return 0;
}

在上述示例中,我们定义了一个lambda表达式 sum,它接受两个整数参数并返回它们的和。然后,我们使用该lambda表达式计算变量 xy 的和,并将结果存储在变量 result 中。

lambda表达式还支持捕获外部变量。捕获列表可以指定要在lambda表达式内部使用的外部变量。可以通过值捕获([x])、引用捕获([&x])或混合捕获([x, &y])来捕获变量。

下面是一个使用捕获列表的示例:

#include 

int main() {
    int x = 5;
    int y = 10;

    auto sum = [x](int a, int b) -> int {
        return a + b + x;
    };

    auto result = sum(y, 15);
    std::cout << "Sum: " << result << std::endl;

    return 0;
}

在上述示例中,我们使用值捕获 [x] 来捕获变量 x,并在lambda表达式内部使用它计算结果。注意,在捕获列表中指定的变量将会被复制到lambda表达式的闭包中。

lambda表达式还可以省略参数列表和返回类型,让编译器自动推断。如果lambda表达式没有参数,则可以使用空括号 () 表示。如果返回类型可以被推断出来,则可以省略返回类型。

除了上述基本用法,lambda表达式还可以与标准库算法(例如std::for_eachstd::transform等)一起使用,以提供更强大的功能。

捕获列表说明

  • [var]:表示值传递方式捕捉变量var
  • [=]:表示值传递方式捕获所有父作用域中的变量(包括this)
  • [&var]:表示引用传递捕捉变量var,传引用不用multable,可以修改
  • [&]:表示引用传递捕捉所有父作用域中的变量(包括this)
  • [this]:表示值传递方式捕捉当前的this指针

注意要点

  • 父作用域指包含lambda函数的语句块

  • 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。
    比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量
    [&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量

  • 捕捉列表不允许变量重复传递,否则就会导致编译错误。
    比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复

  • 在块作用域以外的lambda函数捕捉列表必须为空。

  • 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者
    非局部变量都
    会导致编译报错。

  • lambda表达式之间不能相互赋值,即使看起来类型相同

函数对象与lambda表达式

函数对象,又称为仿函数,即可以想函数一样使用的对象,就是在类中重载了operator()运算符的
类对象。

class Rate
{
public:
 Rate(double rate): _rate(rate)
 {}
 double operator()(double money, int year)
 { return money * _rate * year;}
private:
 double _rate;
};
int main()
{
// 函数对象
 double rate = 0.49;
 Rate r1(rate);
 r1(10000, 2);
// lamber
 auto r2 = [=](double monty, int year)->double{return monty*rate*year; 
};
 r2(10000, 2);
 return 0;
 }

从使用方式上来看,函数对象与lambda表达式完全一样。
函数对象将rate作为其成员变量,在定义对象时给出初始值即可,lambda表达式通过捕获列表可
以直接将该变量捕获到。
C++11【下】_第1张图片
实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如
果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()。

包装器

C++11引入了一系列包装器(Wrappers),用于在编程中提供更多的灵活性和功能。这些包装器是标准库中的模板类,用于封装和操作其他类型的对象。

以下是C++11中的几个常见的包装器:

  1. std::pairstd::pair是一个模板类,用于存储两个不同类型的值。它提供了方便的方式来将两个值组合在一起,并且可以通过firstsecond成员访问其中的值。

  2. std::tuplestd::tuple是一个模板类,用于存储多个不同类型的值。与std::pair类似,std::tuple允许将多个值组合在一起,并且可以使用std::get函数按索引或类型访问其中的值。

  3. std::functionstd::function是一个通用的函数包装器,可以存储可调用对象(如函数、函数指针、lambda表达式等)。它提供了一种机制来传递和存储不同类型的可调用对象,并且可以像普通函数一样进行调用。

  4. std::reference_wrapperstd::reference_wrapper是一个模板类,用于包装对另一个对象的引用。它提供了一种非拥有(non-owning)引用的方式,允许将引用作为值进行传递和存储,而不是像指针一样对对象进行拥有。

这些包装器提供了更高层次的抽象,使得在编程中可以更灵活地处理不同类型的数据和对象。它们有助于提高代码的可读性、可维护性和重用性,并且在许多情况下可以简化编程任务。

function包装器

std::function是一个通用的函数包装器,它可以存储可调用对象(如函数名(函数指针)函数对象lambda表达式类的成员函数等)。使用std::function可以将任何可调用对象视为具有相同签名的函数,并使用相同的方法进行调用。

以下有案例使用

#include 
int f(int a, int b)
{
 return a + b;
}
struct Functor
{
public:
 int operator() (int a, int b)
 {
 return a + b;
 }
};
class Plus
{
public:
 static int plusi(int a, int b)
 {
 return a + b;
 }
 double plusd(double a, double b)
 {
 return a + b;
 }
};
int main()
{
 // 函数名(函数指针)
 std::function<int(int, int)> func1 = f;
 cout << func1(1, 2) << endl;
 // 函数对象
 std::function<int(int, int)> func2 = Functor();
 cout << func2(1, 2) << endl;
 // lamber表达式
 std::function<int(int, int)> func3 = [](const int a, const int b) 
{return a + b; };
 cout << func3(1, 2) << endl;
 
 // 类的成员函数
 std::function<int(int, int)> func4 = &Plus::plusi;
 cout << func4(1, 2) << endl;
 std::function<double(Plus, double, double)> func5 = &Plus::plusd;
 cout << func5(Plus(), 1.1, 2.2) << endl;
 return 0;
}

类的成员函数有点特殊,要&取地址和给出类域。

function包装器与map的配合使用

std::functionstd::map可以很好地配合使用,以实现基于字符串的事件处理程序或回调函数的映射。具体来说,可以将不同的字符串映射到不同的std::function对象上,然后根据字符串查找相应的函数并调用它。

例如,假设需要实现一个简单的命令行工具,用户可以输入不同的命令,然后程序会执行相应的操作。可以使用std::map将不同的命令字符串映射到相应的std::function对象上,然后在用户输入命令时查找相应的函数并调用它。

以下是一个示例代码:

#include 
#include 
#include 

void print_help() {
    std::cout << "Usage: command [args...]\n"
              << "Available commands:\n"
              << "  help - Print this help message\n"
              << "  echo - Print the input arguments\n";
}

void echo(const std::vector<std::string>& args) {
    for (const auto& arg : args) {
        std::cout << arg << " ";
    }
    std::cout << std::endl;
}

int main() {
    std::map<std::string, std::function<void(const std::vector<std::string>&)>> commands{
        {"help", [](const std::vector<std::string>& args){ print_help(); }},
        {"echo", echo}
    };

    std::string command;
    while (true) {
        std::cout << "> ";
        std::getline(std::cin, command);

        std::istringstream iss(command);
        std::string name;
        iss >> name;

        auto it = commands.find(name);
        if (it != commands.end()) {
            std::vector<std::string> args(std::istream_iterator<std::string>{iss},
                                          std::istream_iterator<std::string>{});
            it->second(args);
        }
        else {
            std::cout << "Unknown command: " << name << std::endl;
        }
    }

    return 0;
}

在上面的代码中,我们定义了两个命令函数print_helpecho,分别用于打印帮助信息和输出输入的参数。然后,我们将这两个函数分别存储在std::function对象中,并使用std::map将命令字符串映射到相应的函数上。

main函数中,我们使用一个无限循环来等待用户输入命令。每次输入命令后,我们根据空格拆分输入字符串,并将第一个单词作为命令名称。然后,我们在commands中查找与命令名称匹配的函数,并将剩余的参数传递给它进行处理。

需要注意的是,我们在std::map中使用了std::function对象作为值类型,并指定了函数签名为void(const std::vector&),以便与命令处理函数进行匹配。此外,我们还使用了C++11中的初始化列表语法来方便地初始化commands对象。

bind函数

std::bind是一个函数,它可以把一个可调用对象和一些参数绑定在一起,返回一个新的可调用对象。这个新的可调用对象可以像原来的可调用对象一样调用,但是其参数已经固定了。

std::bind的语法比较复杂,需要注意以下几点:

  1. 第一个参数是要绑定的可调用对象,可以是函数指针、函数对象、成员函数指针等。
  2. 之后的参数是要绑定的参数,可以是任意类型的值或引用(包括占位符_1_2等)。
  3. 返回值是一个新的可调用对象,其参数个数和类型与原来的可调用对象不同,但可以通过占位符进行传递。

下面是一些常见的用法

绑定函数指针和参数

#include 
#include 

void foo(int a, int b) {
    std::cout << a << " + " << b << " = " << (a + b) << std::endl;
}

int main() {
    auto f = std::bind(foo, 2, 3);
    f(); // 输出 2 + 3 = 5

    return 0;
}

在上面的代码中,我们使用std::bind将函数foo和参数2、3绑定在一起,生成一个新的可调用对象f。然后,我们调用f()就会执行foo(2, 3)

绑定函数对象和参数

#include 
#include 

struct Bar {
    void operator()(int a, int b) const {
        std::cout << a << " - " << b << " = " << (a - b) << std::endl;
    }
};

int main() {
    Bar bar;
    auto f = std::bind(bar, 5, std::placeholders::_1);
    f(3); // 输出 5 - 3 = 2

    return 0;
}

在上面的代码中,我们定义了一个函数对象Bar,并重载了其括号运算符。然后,我们使用std::bind将函数对象bar和参数5、占位符_1绑定在一起,生成一个新的可调用对象f。在调用f(3)时,实参3会替换掉占位符_1,最终会执行bar(5, 3)

绑定成员函数和对象指针

#include 
#include 

struct Baz {
    void hello(int n) const {
        std::cout << "hello " << n << std::endl;
    }
};

int main() {
    Baz baz;
    auto f = std::bind(&Baz::hello, &baz, std::placeholders::_1);
    f(42); // 输出 hello 42

    return 0;
}

在上面的代码中,我们定义了一个类Baz,其中包含一个成员函数hello。然后,我们创建一个Baz对象baz,并使用std::bind将其成员函数hello和对象指针&baz、占位符_1绑定在一起,生成一个新的可调用对象f。在调用f(42)时,实参42会替换掉占位符_1,最终会执行baz.hello(42)

需要注意的是,在绑定成员函数时,需要使用成员函数指针,同时还需要传入一个对象指针作为第一个参数(可以使用&取地址符)。

绑定函数对象和引用参数

#include 
#include 

struct Foo {
    void operator()(int& n) const {
        n += 10;
    }
};

int main() {
    int x = 5;
    Foo foo;
    auto f = std::bind(foo, std::ref(x));
    f(); // 修改 x 的值为 15
    std::cout << x << std::endl;

    return 0;
}

在上面的代码中,我们定义了一个函数对象Foo,其中包含一个括号运算符,接受一个整数引用,并将其加上10。然后,我们创建一个整数变量x,并使用std::bind将函数对象foo和整数引用x绑定在一起,生成一个新的可调用对象f。在调用f()时,x的值会被修改为15。

需要注意的是,在绑定引用参数时,需要使用std::ref函数将其转换为引用。如果直接绑定一个值,那么修改的只是副本,而不是原来的变量。


如上便是本期的所有内容了,如果喜欢并觉得有帮助的话,希望可以博个点赞+收藏+关注❤️ ,学海无涯苦作舟,愿与君一起共勉成长

C++11【下】_第2张图片
在这里插入图片描述

你可能感兴趣的:(C++,c++,c++11)