《探索C++11:现代语法的性能优化策略(中篇)》
C++11 语法的重要性
C++11 是 C++ 标准的一次重大更新,引入了许多现代编程特性,显著提升了代码的简洁性、安全性和性能。这些新特性不仅优化了开发效率,还使得 C++ 在系统编程、嵌入式开发和高性能计算等领域更具竞争力
C++11 的核心改进
C++11 的改进涵盖多个方面,包括自动类型推导、智能指针、Lambda 表达式、范围 for 循环、右值引用和移动语义等。这些特性使得代码更易于编写和维护,同时减少了常见错误的可能性
现代 C++ 编程风格
借助 C++11,开发者可以编写更简洁、更高效的代码。例如,使用
auto
关键字可以减少冗长的类型声明,而智能指针(如std::unique_ptr
和std::shared_ptr
)则能有效管理内存,避免资源泄漏。此外,Lambda 表达式提供了更灵活的匿名函数机制,使得算法和回调的实现更加直观
目录
【一】可变模板参数
(1)引入
(2)语法
(3)使用参数包
(4)获取参数包大小
【二】列表初始化
(1)使用
(2)特点
【三】包装器
(1)包装函数
(2)包装仿函数
(3)包装成员函数
(4)混合包装
(5)知识点解释(注意)
(1)预留参数位置
(2)&类名::成员函数
(3)&对象名
(4)固定参数
【一】可变模板参数
注意:此语法我们知道即可,不用仔细研究!
(1)引入
之前我们学习了泛型编程:模板。例如:这些模板的参数T、V都是固定个数的,今天我们要学习的是不固定的模板参数,属于动态性的!
template<class T, class V>
void Func()
{;
}
在C语言中我们其实已经接触了可变的模板参数,比如:printf,它的参数是不固定的
printf("%s", "小明");printf("%s 喜欢 %s", "小明", "小美");
可变模板参数的好处:允许同一个模板接收0、1、2.....多个模板参数
(2)语法
可变模板参数的核心是 参数包 ,用 ... 表示,以函数模板为例:
模板参数包:表示 “一组类型参数”,语法是
typename... Args
(Args
是参数包的名字,可自定义)template<class...Args> //或者 template<typename...Args>
函数参数包:表示 “一组对应类型的实际参数”,语法是
Args... args
(args
是参数包的名字,可自定义)void Func(Args...args) {; }
此时我们如果想调用这个函数,可以直接传参,参数包会自己形成对应参数:
Func();
Func(1);
Func(1, 2.5);
Func(1, 2.5, "小明");
那么如何去使用参数包呢?比如打印、赋值这些功能?
(3)使用参数包
参数包(Args...
或args...
)是一个 “打包” 的集合,编译器不知道怎么直接处理它(比如不能直接cout << args
)。我们需要把它 “拆开”,逐个处理 —— 这个过程叫解包
在 解包 中递归是最简单的一种方法,我们先来看一下递归解法!
递归思路:
(1)递归我们需要保证至少有一个参数,因此需要手动增加一个模板参数,然后递归调用
template<typename T, typename... Args> void Func(T first, Args... args) {cout <<" " << "参数:" << first;Func(args...); }
(2)当没有参数时,调用结束终止函数
// 递归终止条件 void Func() {cout << endl;cout << "递归结束" << endl; }
(3)提前写一个声明:让函数模板知道有结束函数,放心调用
// 提前声明:让模板函数知道“无参数版本存在” void Func();
效果展示如下:
(4)获取参数包大小
这个语法有点奇怪,我们记住就行:sizeof...
【二】列表初始化
我们先看看之前的初始化方式(我感觉还挺好用的,但是新版本出来了我们得无条件接受!):
现在C++11版本出来之后,引入了 {} ,几乎所有的场景都可以用它初始化,下面我们看如何使用
(1)使用
比如内置类型:省略了赋值符号,相较于旧版本如果初始错误会报错,旧版本只是警告
int a{ 10 };//等价于 int a = 10;double b{ 2.4 };//等价于 double b = 2.4;bool c{ true };//等价 bool c = true;
比如数组类型:同理可省略赋值符号
int arr[]{ 1,2,3,4 };//相当于 int arr[]={1,2,3,4}pair<int, int> brr[]{ {1,2},{2,4},{3,3} };
其余自定义类型一样可以省略赋值符号直接 {} ,这里就不列举了!
(2)特点
空 {} 表示默认初始化,例如:
对于自定义类型,我们看个例子:
当你写
{ }
时,编译器会自动把它转换成initializer_list
类型。而标准库容器(vector
、map
等)的构造函数都支持接收initializer_list
参数,所以才能直接用{}
初始化
【三】包装器
包装器:比如不同的工具(比如函数、lambda、仿函数等)如果想把它们存到同一个容器里,或作为统一的参数传递,直接操作会很麻烦。包装器就是为解决这个问题而生的(包装不同功能的工具可以统一调用)
我们今天要学习的是其中一种包装器:function
作用:把各种可调用对象包装成一个统一类型的对象
头文件:#include <functional>
只要某个可调用对象的参数类型和返回值类型符合预期就可以用来包装,例如:
- 普通函数
int add(int, int)
- lambda 表达式
[](int a, int b) { return a - b; }
- 仿函数(重载
operator()
的类)struct Multiply { int operator()(int, int); }
声明格式:
std::function<函数签名>
,其中 "函数签名" 是返回值类型(参数类型列表)
比如:
std::function<int(int, int)>
表示:能接收两个int
参数,返回int
的可调用对象
(1)包装函数
例如:现在有一个普通函数和一个lambda表达式
int Func1(int a, int b)
{return a + b;
}
现在我对它进行包装:
// 定义一个能包装 "int(int, int)" 签名的function
function<int(int, int)> func;// 绑定普通函数
func = Func1;// 调用(和调用普通函数一样)
std::cout << func(2, 3) << std::endl; // 输出 5
(2)包装仿函数
例如现在有一个仿函数:
struct Func
{int operator()(int a, int b){return a * b;}
};
我们对它进行包装:
//定义包装器
function<int(int, int)> V;//绑定仿函数
V = Func();//调用
cout << V(2, 3) << endl;
(3)包装成员函数
例如现在有一个成员函数:
class Func1
{
public:int divide(int a, int b) const {return a * b;}
};
现在进行包装器包装:
//实例化对象
Func1 V;
//包装器
function<int(int, int)> func1;
//绑定成员函数func1 = bind(& Func1::divide, V, placeholders::_1, placeholders::_2);//或者func1 = bind(& Func1::divide, Func1(), placeholders::_1, placeholders::_2);
注意:placeholders::_1, placeholders::_2 和 & 符号后面会在知识点解释里面讲解!
// 调用:(8, 2)cout << func1(8, 2) << endl; // 16
(4)混合包装
一个包装器似乎只能绑定一个目标,那么如何绑定多个呢?没错用vector!
例如现在有三种工具:函数、仿函数和成员函数
void Func1()
{cout << "Func1" << endl;
}struct Func2
{void operator()(){cout << "Func2" << endl;}
};class Func3
{
public:void divide() {cout << "Func3" << endl;}
};
现在我对它们进行包装调用:
//实例化类
Func3 func;//包装器
vector<function<void()>> V;//包装
V.push_back(Func1);
V.push_back(Func2());
V.push_back(bind(&Func3::divide, func));//调用
for (auto e : V)
{//这里的e相当于拿到了每一个vector内容,也就是包装器参数调用e();
}
(5)知识点解释(注意)
(1)预留参数位置
placeholders::_1
、placeholders::_2为预留参数位置,它在命名空间placeholders里面,有多少参数就传多少即可!这个命名空间里面的参数个数范围在_1 ~ _20
例如:没有参数就不用传(如果传了_1和_2编译就会报错,因为成员函数没有接收位置)
bind(&Func3::divide, func)
例如:有两个参数就传_1 , _2,以此类推
bind(&Func3::divide, func, placeholders::_1、placeholders::_2)
注意:如果我们想交换参数的位置,就直接交换p
laceholders::_1
、placeholders::_2位置
函数调用的第一个参数永远传给
placeholders::_1,然后_1再传给函数的第一个参数
函数调用的第二个参数永远传给
placeholders::_2,然后_2再传给函数的第二个参数
总结:函数调用传参的位置有固定传送对象的,而传送对象有固定传送位置的
例如:
如果我们交换了_1 , _2之后,也就交换了函数参数的位置,例如:
(2)&类名::成员函数
这个叫获取成员函数指针
语法规则:要获取类的非静态成员函数的指针,必须使用
&类名::成员函数名
的形式
普通函数(如
void Func1()
)的指针可以直接用Func1
或&Func1
(两种写法等价)bind(Func3::divide, func)
bind(&Func3::divide, func)
成员函数必须显式用
&
,不能直接写Func3::divide
(否则编译器无法识别为 “成员函数指针”)bind(&Func3::divide, func)
(3)&对象名
总结精髓:避免拷贝
语法规则:
&func
是对Func3
类型的对象func
取地址,得到Func3*
类型的指针(即对象的地址)
作用:将这个对象指针传递给
bind
,是为了告诉bind
:当绑定后的可调用对象被执行时,要在func
这个对象上调用divide
成员函数
替代写法:除了传递 “对象指针”,也可以传递 “对象引用”(用
std::ref(func)
),效果类似,但传递指针(&func
)更直接,且能避免不必要的对象拷贝
(4)固定参数
固定参数的位置会传固定值,例如:
bind(&Func3::divide, func, placeholders::_1、placeholders::_2、10)
那么下面这个成员函数就会接收:placeholders::_1、placeholders::_2、10
class Func1
{
public:int divide(int a, int b,int c) const {return a * b;}
};