C++11(下) 入门三部曲终章(基础篇):夯实语法,解锁基础编程能力

文章目录


前言

上一篇博客重点讲解了可变参数模板,主要在于可变 二字。以往我们写函数一般都会把参数写死(也就是说传过去的参数的个数被固定了),这样反倒不灵活。有了可变参数模板,这就使得我们可以传任意个参数过去,增加了我们代码的灵活性。接下来我们看看可变参数模板是怎么在我们STL接口中运用起来的。


提示:以下是本篇文章正文内容,下面案例可供参考

1️⃣一、emplace接口(重点)

可以这么说,可变参数是emplace的语法基石 ,而emplace就是可变参数模板的经典落地实战

1.1push/insert为何低效?

  • 在emplace接口诞生之前,我们向容器插入数据通常都是调用push_back/push_front等接口进行操作,这样的操作固然没有问题,但却无形之中增加了不必要的临时对象的创建和销毁

  • 即使push/insert重载了右值引用的版本push_back(T&& x)(右值引用可以抢夺资源,调用移动构造,减少拷贝),看似解决了性能问题,但也逃不了内存的开销

我们先分C++11前与后关于插入接口的效率对比

①C++11前,只有左值引用的版本push(T& x)

cpp 复制代码
std::vector<std::string> vec;
// 传入字符串字面量,编译器先构造临时std::string对象
vec.push_back("hello emplace");
  • 先构造一个临时的string对象"hello emplace",第一步开销
  • 再调用拷贝构造,分配内存,把这个临时对象拷贝进容器中,第二步开销
  • 再销毁原先创建的临时对象

这个过程是显而易见的构造-拷贝-销毁的过程,几乎是冗余操作,尤其是到了自定义类型的对象,如string等,开销更大

②C++11后,重载右值版本进行移动构造push(T&& x)

cpp 复制代码
std::vector<std::string> vec;
// C++11后,匹配push_back(std::string&&),调用移动构造
vec.push_back("hello emplace");
  • 以"hello emplace"为参数,构造一个临时对象(分配内存,拷贝内容)
  • 调用std::string的移动构造函数,将临时对象的资源所有权转移到容器的内存空间中(仅修改指针指向,无堆内存分配、无内容拷贝,开销极小,但并非为 0)
  • 函数调用结束后,销毁临时的 std::string 对象(此时临时对象已无资源,销毁仅做简单清理 ------ 开销极小,但并非为 0)

核心痛点:传统接口的设计缺陷 ------ 只能接收 "已构造的对象"

  • 无论是 C++11 前的拷贝构造,还是 C++11 后的移动构造,传统 push/insert 接口的底层设计缺陷从未改变 :它们的参数是已构造完成的 T 类型对象(左值或右值),而非构造 T 对象所需的参数
  • 这就决定了:只要通过传统接口插入 "未提前构造的对象",就必须先在外部构造一个临时对象(左值 / 右值),再将这个对象传递给接口------ 临时对象的创建是无法通过移动语义规避的,这是传统接口的先天限制。

1.2emplace的核心原理

基于上面的分析,那有没有能够跳出临时对象的构建与销毁,直接在目标容器里构造对象的方法呢?答案就是emplace接口。

emplace 系列接口的设计思路很简单:不接受已构造的对象,而是接受构造对象所需的所有参数,然后在容器的目标内存位置,直接调用对象的构造函数完成构造。

而实现这一个思路,必须得解决两个问题

  • ①我们传的参数要对应匹配该容器的构造函数,所以就注定了我们传的参数个数、类型是随机的(因为要方便我们调用合适的构造函数)------可变参数模板 就解决了这个问题,通过Args&&... agrs参数包,可以让emplace接受任何类型、个数的参数
  • ②要保证传递给构造函数的参数类型不被破坏(左值还是左值,右值还是右值),避免不必要的拷贝 ------完美转发(std::forward<Args>(args)...)解决了这个问题,实现参数的无损传递。(上文提过,右值引用的变量具有左值属性)
cpp 复制代码
// 1. 极简待构造的类
class Product {
public:
    string name;
    int price;
    // 带参构造(仅保留核心)
    Product(string n,int p = 1)
        :name(n)
        ,price(p)
    { }
};

// 2. 极致简洁的容器类(仅含emplace核心)
class SimpleContainer {
public:
    // 核心emplace接口:可变参数模板接收构造参数
    template <typename... Args>
    void emplace(Args&&... args) {
        // 直接用参数构造对象(emplace本质:传构造参数,而非现成对象)
        obj = Product(forward<Args>(args)...);
    }

    // 方便验证结果(非核心,仅辅助)
    Product get_obj() { return obj; }

private:
    Product obj; // 存储的对象(简化为直接成员,无内存分配)
};

// 测试:仅调用emplace
int main() {
    SimpleContainer container;
    // emplace直接传构造参数(不用先造Product对象)
    container.emplace("耳机", 199);

    // 验证
    Product p = container.get_obj();
    cout << "emplace构造的对象:" << p.name << " | 价格:" << p.price << endl;
    return 0;
}
  • emplace 函数体里的 "传值" 不是 "额外开销",而是构造容器内对象的「必要步骤」;
  • push_back 的核心问题是 "多一次外部临时对象的构造 / 销毁",这是「冗余开销」;
  • 对普通开发者来说,不用纠结 "单次传值的微小开销",只要记住:传构造参数用 emplace,传现成对象用 push_back,这就够了。

2️⃣二、lambda

2.1lambda表达式语法

  • lambda表达式本质是一个匿名函数对象,跟普通函数不同得是他可以定义在函数内部。lambda表达式语法使用层而言没有类型,所以我们一般是用auto去接受lambda对象
  • lambda表达式的格式:[capture-list](parameters)->return type{function body}
  • [capture-list]:捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的带啊吗是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用,捕捉列表可以传值和传引用捕捉。捕捉列表的[]不能省略
  • (parameters):参数列表,与普通函数的参数列表功能类似,如果不需要参数传递,则可以联通()一起省略
  • ->return type:返回值类型,用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可以省略。一般情况下返回值类型明确情况下,也可省略,由编译器对返回类型进行推到
  • {function body}:函数体,不可省略
cpp 复制代码
int main()
{
	//lambda表达式
	auto add = [](int x, int y)->int {return x + y; };
	std::cout << add(1, 2) << std::endl;
	auto func = [] {std::cout << "hello czh" << std::endl; };
	func();

	int a = 0, b = 1;
	auto swap = [](int& x, int& y)
		{
			int tmp = x;
			x = y;
			y = tmp;
		};
	swap(a, b);
	std::cout << a << ":" << b << std::endl;
	return 0;
}

2.2捕捉列表

  • lambda表达式中默认只能用lambda函数体和参数中的变量,如果想用外层作用域的变量就要进行捕捉
  • 第一种捕捉方式是在捕捉列表中显示的传值捕捉和传引用捕捉,捕捉的多个变量用逗号分隔。[x,y,&z]表示x和y通过值捕捉,z通过传引用捕捉
  • 第二种就是隐式捕捉,[=],[&],这样我们在函数体里面使用什么变量,就会捕捉什么变量,所有的变量都是对应的值(=)捕捉和引用(&)捕捉
  • 第三种捕捉方式就是混合捕捉。[=,&x,&y]就代表我使用的变量除了x和y是传引用捕捉之外,其余都是值捕捉。当使用混合捕捉时,第一个元素必须是=或&
  • lambda的值捕捉的元素默认是被const修饰的
cpp 复制代码
int x = 0;
auto func1 = []()
	{
		x++;
	};
int main()
{
	//lambda表达式
	auto add = [](int x, int y)->int {return x + y; };
	std::cout << add(1, 2) << std::endl;
	auto func = [] {std::cout << "hello czh" << std::endl; };
	func();

	int a = 0, b = 1;
	auto swap = [](int& x, int& y)
		{
			int tmp = x;
			x = y;
			y = tmp;
		};
	swap(a, b);
	std::cout << a << ":" << b << std::endl;



	int a = 0, b = 1, c = 2, d = 3;
	auto func1 = [a, &b]//a值捕捉,b引用捕捉
		{
			b++;
			int ret = a + b;
			return ret;
		};
	std::cout << func1() << std::endl;

	auto func2 = [=]//使用到的变量均是值捕捉
		{
			int ret = a + b + c;
			return ret;
		};
	std::cout << func2() << std::endl;

	auto func3 = [&]//使用到的变量均是引用捕捉
		{
			a++;
			b++;
		};
	func3();
	cout << a << b;
	return 0;
}

3️⃣三、包装器

3.1function

cpp 复制代码
template<class T>
class function;

template<class Ref,class... Args>
class function<(Ref(Args...))>;
  • std::function是一个类模板,也是一个包装器。std::function的实力对象可以包装存储其他的可以调用对象,包括函数指针,仿函数,lambda,bind表达式等,存储的可调用对象被称为std::function的目标。若std::function不含目标,则称它为空。调用空会抛出异常
  • 函数指针、仿函数、lambda等可调用对象的类型各不相同,std::function的优势就是统一类型,对他们都可以进行包装,这样在很多地方就方便声明可调用对象的类型
cpp 复制代码
int f(int a, int b)
{
	return a + b;
}

struct func
{
public:
	int operator()(int a, int b)
	{
		return a + b;
	}
};

class Plus
{
public:
	Plus(int n = 10)
		:_n(n)
	{ }
	static int plusi(int a, int b)//static修饰成员变量属于类本身,只能访问静态成员
	{
		return a + b;
	}
	double plusd(double a, double b)
	{
		return (a + b) * _n;
	}
private:
	int _n;
};

int main()
{
	function<int(int, int)> f1 = f;
	func fc;
	function<int(int, int)> f2 = fc;
	function<int(int, int)> f3 = [](int a, int b) {return a + b; };

	cout << f1(1, 2) << endl;
	cout << f2(3, 4) << endl;
	cout << f3(5, 6) << endl;

	function<int(int, int)> f4 = &Plus::plusi;

	cout << f4(1, 1) << endl;
	function<double(Plus*, double, double)> f5 = &Plus::plusd;//要通过&取到函数的指针
	function<double(Plus, double, double)> f6 = &Plus::plusd;//可以传对象,也可以传对象的地址,也可以传右值
	function<double(Plus&&, double, double)> f7 = &Plus::plusd;

	Plus pd;
	cout << f5(&pd, 1.1, 1.1) << endl;
	cout << f6(pd, 1.1, 1.1) << endl;
	cout << f7(move(pd), 1.1, 1.1) << endl;
	cout<<f7(Plus(), 1.1, 1.1) << endl;


	return 0;
}

3.2bind

  • bind是一个函数模板,它也是一个可调用对象的包装器,可以把他看作一个函数适配器,对接受的fn可调用对象进行处理后返回一个可调用对象。bind可以用来调整参数个数和参数顺序。bind也在functional这个头文件
  • 调用bind的一般形式:auto newcallable = bind(callbable,arg_list);其中newcallable本身也是一个可调用对象,arg_list是一个逗号分割的参数列表,对应给定的callable的参数。当我们调用newcallable,newcallable会调用callable,并传给它arg_list中的参数
  • arg_list中的参数可能包含形如_n的名字,其中n是⼀个整数,这些参数是占位符,表⽰
    newCallable的参数,它们占据了传递给newCallable的参数的位置。数值n表⽰⽣成的可调⽤对象中参数的位置:_1为newCallable的第⼀个参数,_2为第⼆个参数,以此类推。_1/_2/_3...这些占位符放到placeholders的⼀个命名空间中
cpp 复制代码
#include <iostream>
#include <functional>
using namespace std;
using placeholders::_1;
using placeholders::_2;
using placeholders::_3;

int Sub(int a, int b)
{
    return (a - b) * 10;
}

int SubX(int a, int b, int c)
{
    return (a - b - c) * 10;
}

class Plus
{
public:
    static int plusi(int a, int b)
    {
        return a + b;
    }

    double plusd(double a, double b)
    {
        return a + b;
    }
};

int main()
{
    auto sub1 = bind(Sub, _1, _2);
    cout << sub1(10, 5) << endl;
    // bind 本质返回的一个仿函数对象

    // 调整参数顺序(不常用)
    // _1 代表第一个实参
    // _2 代表第二个实参
    // ...
    auto sub2 = bind(Sub, _2, _1);
    cout << sub2(10, 5) << endl;

    // 调整参数个数(常用)
    auto sub3 = bind(Sub, 100, _1);
    cout << sub3(5) << endl;
    auto sub4 = bind(Sub, _1, 100);
    cout << sub4(5) << endl;

    // 分别绑死第1、2、3个参数
    auto sub5 = bind(SubX, 100, _1, _2);
    cout << sub5(5, 1) << endl;
    auto sub6 = bind(SubX, _1, 100, _2);
    cout << sub6(5, 1) << endl;
    auto sub7 = bind(SubX, _1, _2, 100);
    cout << sub7(5, 1) << endl;

    // 成员函数对象进行绑死,就不需要每次都传递了
    function<double(Plus&&, double, double)> f6 = &Plus::plusd;
    Plus pd;
    cout << f6(move(pd), 1.1, 1.1) << endl;
    cout << f6(Plus(), 1.1, 1.1) << endl;

    // bind 一般用于,绑死一些固定参数
    function<double(double, double)> f7 = bind(&Plus::plusd, Plus(), _1, _2);
    cout << f7(1.1, 1.1) << endl;

    // 计算复利的 lambda
    auto func1 = [](double rate, double money, int year)->double {
        double ret = money;
        for (int i = 0; i < year; i++)
        {
            ret += ret * rate;
        }
        return ret - money;
    };

    // 绑死一些参数,实现出支持不同年华利率,不同金额和不同年份计算出复利的结算利息
    function<double(double)> func3_1_5 = bind(func1, 0.015, _1, 3);
    function<double(double)> func5_1_5 = bind(func1, 0.015, _1, 5);
    function<double(double)> func10_2_5 = bind(func1, 0.025, _1, 10);
    function<double(double)> func20_3_5 = bind(func1, 0.035, _1, 30);

    cout << func3_1_5(1000000) << endl;
    cout << func5_1_5(1000000) << endl;
    cout << func10_2_5(1000000) << endl;
    cout << func20_3_5(1000000) << endl;

    return 0;
}

总结

C++11到这里就先告一段落了,希望大家能够从这篇文章真真实实的学到东西,不懂可以私信博主交流噢

相关推荐
m0_748229996 小时前
PHP+Vue打造实时聊天室
开发语言·vue.js·php
亓才孓6 小时前
[JDBC]事务
java·开发语言·数据库
燃于AC之乐6 小时前
深入解剖STL List:从源码剖析到相关接口实现
c++·stl·list·源码剖析·底层实现
枫叶丹46 小时前
【Qt开发】Qt界面优化(一)-> Qt样式表(QSS) 背景介绍
开发语言·前端·qt·系统架构
汉克老师6 小时前
GESP2025年6月认证C++二级( 第一部分选择题(9-15))
c++·循环结构·求余·gesp二级·gesp2级·整除、
不想睡觉_6 小时前
优先队列priority_queue
c++·算法
灰子学技术14 小时前
go response.Body.close()导致连接异常处理
开发语言·后端·golang
二十雨辰14 小时前
[python]-AI大模型
开发语言·人工智能·python
Yvonne爱编码14 小时前
JAVA数据结构 DAY6-栈和队列
java·开发语言·数据结构·python