文章目录
- {}列表初始化
-
-
- [1. 初始化内置类型变量](#1. 初始化内置类型变量)
- [2. 初始化数组](#2. 初始化数组)
- [3. 初始化标准容器](#3. 初始化标准容器)
- [4. 初始化自定义类型](#4. 初始化自定义类型)
- [5. 构造函数初始化列表](#5. 构造函数初始化列表)
- [6. 初始化列表(initializer_list)](#6. 初始化列表(initializer_list))
- [7. 返回值初始化](#7. 返回值初始化)
- [8. 静态成员变量和全局变量的就地初始化](#8. 静态成员变量和全局变量的就地初始化)
- [9. 防止类型收窄](#9. 防止类型收窄)
- 总结
-
- decltype
- 右值引用
- 完美转发--由右值引用引发的问题
- [delete default](#delete default)
- 逗号表达式
- 可变参数模板
- emplace_back
- 仿函数
- function包装器/适配器/类模板
- bind函数模板/包装器/适配器
{}列表初始化
1. 初始化内置类型变量
- 直接初始化 :
int a{10};
。明确表达初始化过程,避免与赋值混淆。 - 空列表初始化 :如果
{}
内为空,则内置类型变量会被初始化为该类型的零值,如int b{};
会将b
初始化为0
。
2. 初始化数组
- C++98风格 :在C++98中,数组初始化就需要使用
{}
,如int arr1[] = {1, 2, 3};
。 - C++11风格 :C++11引入了更简洁的数组初始化方式,允许省略
=
,如int arr2[]{1, 2, 3};
。
3. 初始化标准容器
-
STL容器 :如
vector
、map
等,可以使用{}
进行初始化,cppvector<int> v{1, 2, 3}; map<int, float> m{ {1, 1.0f}, {2, 2.0f} };
4. 初始化自定义类型
- 无模板类 :对于自定义的无模板类,可以在构造函数中使用
{}
初始化成员变量,也可以在对象创建时直接使用{}
进行初始化,如struct A { A(int a = 0, int b = 0) : _a(a), _b(b) {} }; A a{1, 2};
。 - 模板类 :模板类同样支持使用
{}
进行初始化,如template<class T> struct B { B(T c = 0, T d = 0) : _c(c), _d(d) {} }; B<int> b{3, 4};
。
5. 构造函数初始化列表
- 在类的构造函数中,可以使用
{}
来初始化成员变量,这有助于确保成员变量在构造函数体执行之前就被正确初始化。
6. 初始化列表(initializer_list)
- C++11引入了
initializer_list
,允许构造函数接收一个初始化列表作为参数,从而可以更方便地初始化容器或自定义类型。 - 示例:
class C { C(initializer_list<int> lt) : _arr(lt) {} private: vector<int> _arr; }; C c = {1, 2, 3, 4};
7. 返回值初始化
- 在函数返回时,也可以使用
{}
来初始化返回值,这有助于确保返回值的正确性和类型安全。
8. 静态成员变量和全局变量的就地初始化
- 在C++11中,除了构造函数初始化列表外,还允许使用
=
或{}
对静态成员变量和全局变量进行就地初始化。
9. 防止类型收窄
- 列表初始化可以防止类型收窄,即当尝试将一种类型的值初始化为另一种类型(且这种转换可能会导致精度损失或范围超出)时,编译器会报错。
总结
cpp
int a{10}; //明确表达初始化过程,避免与赋值混淆
int b{}; //空列表初始化 内置类型变量会被初始化为该类型的零值
int arr1[] = {1, 2, 3};
int arr2[]{1, 2, 3};
int* pa = new int[4]{ 1,2,3,4 };
vector<int> v{1, 2, 3};
map<int, float> m{ {1, 1.0f}, {2, 2.0f} };
struct A
{
A(int a = 0, int b = 0) : _a(a), _b(b)
{
}
};
A a{1, 2};
template<class T>
struct B
{
B(T c = 0, T d = 0) : _c(c), _d(d)
{
}
};
B<int> b{3, 4};
class C
{
C(initializer_list<int> lt) : _arr(lt)
{
}
private:
vector<int> _arr;
};
C c = {1, 2, 3, 4};
std::string createString()
{
return {"Hello, World!"}; // 使用 {} 初始化返回值
}
static int staticVar = 42; // 使用 = 初始化
static std::string staticStringVar{"Static String"}; // 使用 {} 初始化
int globalVar = 100; // 使用 = 初始化
std::string globalStringVar{"Global String"}; // 使用 {} 初始化
char c1 = 256; // 可能不会报错,但值会被截断
// char c2{256}; // 这会编译错误,因为256超出了char的表示范围
// 尝试将浮点数转换为整数,同样使用 {} 可以防止类型收窄
float f = 3.14;
int i1 = f; // 隐式转换,f的值被截断为整数
// int i2{f}; // 这会编译错误,因为浮点数到整数的转换可能不安全
int i3 = static_cast<int>(f); // 显式转换,明确意图 // 安全的转换
int main()
{
auto i = { 10,20,30 };
cout << typeid(i).name() << endl; // class std::initializer list<int>
return 0;
}
int main()
{
vector<int> v1 = { 1,2,3,4,5 }; // initializer_list构造函数
list<int> l1 = { 10, 20, 30 }; // initializer_list构造函数
Date d1 = { 2024, 1, 9 }; // 参数匹配Date构造==》构造+拷贝=直接构造;
// 参数不匹配,{ 2024, 1, 9, 0}被识别成 initializer_list类型,参数不匹配报错
//vector& operator= (initializer_list<value_type> il);
v1 = {10, 20, 30}; // 这个时候调的是赋值重载,而不是 initializer_list 的构造
return 0;
}
decltype
将变量的类型声明为表达式指定的类型。decltype 可以推导对象的类型,这个类型是可以用来模板实参,或者再定义对象。
cpp
template<class T1, class T2>
void F(T1 t1, T2 t2)
{
decltype(t1 * t2) ret;
cout << typeid(ret).name() << endl;
}
int main()
{
const int x = 1;
double y = 2.2;
decltype(x * y) ret; // ret的类型是double
decltype(&x) p; // p的类型是int*
// 类型以字符串形式获取到
cout << typeid(ret).name() << endl;
cout << typeid(p).name() << endl;
vector<decltype(ret)> v; // 使用 ret 的类型去实例化 vector
F(1, 'a');
return 0;
}
右值引用
右值引用的出现
左值引用解决了:传引用传参 传引用返回;
左值引用无法解决:局部变量传引用返回。那么传值返回就要经历【构造+拷贝构造】
移动构造
移动构造本质是将参数右值的资源窃取过来,占为已有,就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己。
cpp
// 移动构造: 传过来的s是右值 比如 返回值或 ("hello" + "world")
//ape::string ret2 = (s1 + '!'); 右值拷贝 ret2.string( (s1 + '!') );
string(string&& s) noexcept
:_str(nullptr)
{
cout << " string(string&& s) -- 右值移动拷贝" << endl;
swap(s); //直接换过来
s._str = nullptr;
}
ape::string int_to_string(int value)
{
//return str; 构造一个右值tmp + 调用移动构造 使得s获得返回值
return move(str);// 这里只是为了调用我们的移动构造输出调试信息
// 实际应用中 str是一个出了函数就销毁的变量 这是错误代码 仅为演示!
// ape::string s = int_to_string(1234);
}
c++库里移动构造的行为
总结
左值引用通过引用直接操作 省去中间额外的拷贝
右值引用通过识别是左值还是右值 如果是右值 直接移动拷贝
为什么要有右值引用
为了提高效率 即 右值直接移动拷贝而非深拷贝 因为拷贝时拷贝对象获取到右值的资源后 右值就不再具有价值
因为想提升效率所以搞了右值 但是移动拷贝转移资源时需要"左值"属性 于是这样被设计:s作为"右值引用" 但是s的属性是左值!
完美转发--由右值引用引发的问题
Fun(forward(t));将t的属性置为原有属性 当原有属性为左值 你还是左值 是右值 你还是右值 当需要把原有属性是右值但是在该函数需要当成左值来用时 不加forward<T>(t)
即可
cpp
void Fun(int& x)
{
cout << "左值引用" << endl;
}
void Fun(const int& x)
{
cout << "const 左值引用" << endl;
}
void Fun(int&& x)
{
cout << "右值引用" << endl;
}
void Fun(const int&& x)
{
cout << "const 右值引用" << endl;
}
//万能引用/引用折叠:既可以引用左值 又可以引用右值
template<typename T>
//这里是一个右值引用 当这个函数被调用 t是参数的右值引用 但是此时 t的属性变为了左值
void PerfectForward(T&& t)//模板中的 && 不代表右值引用,而是万能引用,其既能接收左值又能接收右值
{
//Fun(forward<T>(t));
Fun(t);
}
int main()
{
PerfectForward(10); // 右值
int a;
PerfectForward(a); // 左值
PerfectForward(move(a)); // 右值
const int b = 8;
PerfectForward(b); // const 左值
PerfectForward(move(b)); // const 右值
return 0;
}
应用场景:如果不使用完美转发就只能手动把forward改成move()
右值引用的移动语义出来以后,对深拷贝的类的影响比较大,自定义类的深拷贝传值返回影响也较大,因为移动构造和移动赋值出来以后减少了它们的深拷贝;一些容器的插入接口也新增了右值版本,也减少了深拷贝。但是右值引用对于浅拷贝的类是没有意义的,因为它们没有资源可以转移
默认移动构造/赋值
delete default
cpp
Person(Person&& p) = delete; // 不让生成实现
Person(const Person& p) = default; // 强制编译器生成
C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用 default 关键字显示指定移动构造生成。
逗号表达式
逗号表达式的求值顺序是从左到右。整个逗号表达式的值是最后一个表达式的值。
特性
- 求值顺序:逗号表达式中的表达式按照从左到右的顺序被求值。
- 结果:逗号表达式的结果是最后一个表达式的值,而不是一个列表或者集合。
- 副作用:逗号表达式常用于利用其副作用,比如对变量进行多次赋值或调用具有副作用的函数。
示例
cpp
#include <iostream>
int main()
{
int a = 0, b = 0;
// 逗号表达式
(a = 5, b = a + 1, a + b);
// a 被赋值为 5,b 被赋值为 6,表达式的结果是 a + b = 11,但赋值给变量时仅使用最后一个表达式的值
// 注意:逗号表达式通常用于语句中,而不是直接赋值给变量
// 下面的赋值仅会接收逗号表达式的最后一个表达式的值
int result = (a = 2, b = a + 1, a + b); // result 被赋值为 3(即 a + b 的结果)
std::cout << "a = " << a << ", b = " << b << ", result = " << result << std::endl;
return 0;
}
注意事项
逗号操作符与逗号分隔符(在函数参数列表、初始化列表等中使用的)在语法上是不同的。逗号操作符用于创建逗号表达式,而逗号分隔符用于分隔列表中的元素。
可变参数模板
cpp
template <class ...Args>
void ShowList(Args... args)
{
cout << sizeof...(args) << endl;
}
int main()
{
ShowList(1, 2, 3, 4, 5);
ShowList(1, "abcde", 3.33);
return 0;
}
展开参数包
编译时的递归推演
cpp
// 递归终止函数
void _ShowList()
{
cout << endl;
}
// 展开函数
template <class T, class ...Args>
void _ShowList(const T& val, Args... args)
{
cout << val << " ";
_ShowList(args...);
}
template <class ...Args>
void ShowList(Args... args)
{
_ShowList(args...);
}
int main()
{
ShowList(1, 2, 3, 4, 5);
ShowList(1, "abcde", 3.33);
return 0;
}
逗号表达式
cpp
template <class T>
void PrintArg(T t)
{
cout << t << " ";
}
template <class ...Args>
void ShowList(Args... args)
{
// int arr[] = { PrintArg(args)... };
(PrintArg(args), ...);
cout << endl;
}
int main()
{
ShowList(1, 2, 3, 4, 5);
ShowList(1, "abcde", 3.33);
return 0;
}
emplace_back
cpp
int main()
{
list<string> lt;
lt.push_back("abc"); // const char* 构造string + string移动构造node
lt.emplace_back("abc"); // const char*直接移动构造node(完美转发)
// 同样的 多参数也是如此
list<pair<string,int>> lt;
lt.push_back(make_pair("abc",1));
lt.emplace_back("abc",1);
return 0;
}
修改list模拟实现类
cpp
template <class... Args>
list_node(Args&&... args)
: _data(args...)
, _next(nullptr)
, _prev(nullptr)
{
}
template <class... Args>
void emplace_back(Args&&... args)
{
Node* newnode = new Node(args...);
// 链接节点
}
仿函数
function的构造函数接收Functor()的临时对象将其封装为一个可调用对象
cpp
class TD
{
public:
void operator()()
{
cout << "Thread3" << endl;
}
};
int main()
{
function<void()> f = TD();
f();
TD td;
f = td;
f();
// 线程函数为仿函数
thread t1((TD())); // ( TD() ) 必须这样写 原因不明 不重要
thread t2(td);
t1.join();
t2.join();
cout << "Main thread!" << endl;
return 0;
}
function包装器/适配器/类模板
cpp
// 类模板原型如下
template <class Ret, class... Args>
class function<Ret(Args...)>;
模板参数说明:
Ret: 被调用函数的返回类型
Args...:被调用函数的形参
通过给这些可调用对象加上一层包装器,使得函数模板只实例化一份
function的构造函数接收Functor()的临时对象将其封装为一个可调用对象
包装器的应用
cpp
class Solution
{
typedef long long Long;
public:
int evalRPN(vector<string>& tokens)
{
stack<Long> st;
map< string, function<Long(Long, Long)> > m =
{
{"+", [](Long a, Long b) { return a + b; }},
{"-", [](Long a, Long b) { return a - b; }},
{"*", [](Long a, Long b) { return a * b; }},
{"/", [](Long a, Long b) { return a / b; }}
};
for (auto& e : tokens)
{
if (m.count(e))
{
Long right = st.top();
st.pop();
Long left = st.top();
st.pop();
st.push(m[e](left, right));
}
else
st.push(stoll(e));
}
return st.top();
}
};
bind函数模板/包装器/适配器
- 接收一个可调用对象 (callable object),生成一个新的可调用对象 来"适应"原对象的参数列表
调整参数顺序
cpp
int Sub(int a, int b)
{
return a - b;
}
int main()
{
function<int(int, int)> f1 = Sub;
cout << f1(10, 5) << endl;
// 调整参数顺序
function<int(int, int)> f2 = bind(Sub, placeholders::_2, placeholders::_1);
cout << f2(10, 5) << endl;// 10传给b 5传给a
return 0;
}
调整参数个数
cpp
int main()
{
function<int(int, int)> f1 = Sub;
cout << f1(10, 5) << endl;
// 调整参数个数,有些参数可以使用 bind 时固定
function<int(int)> f3 = bind(Sub, 20, placeholders::_1);
cout << f3(5) << endl;//20给a 5给b
return 0;
}
调整参数个数的应用
cpp
class Plus
{
public:
static int plusi(int a, int b)
{
return a + b;
}
double plusd(double a, double b)
{
return a + b;
}
};
int main()
{
// 静态成员函数名会自动退化为指向该函数的指针
// 写成&Plus::plusi也对 没必要
function<int(int, int)> f1 = Plus::plusi;
cout << f1(1, 2) << endl;
// 普通成员函数 必须加&
function<double(Plus*, double, double)> f2 = &Plus::plusd;
Plus ps;
cout << f2(&ps, 1.1, 2.2) << endl;
// 这里传 Plus 编译器会进行特殊处理,实际上是 Plus*
function<double(Plus, double, double)> f3 = &Plus::plusd;
cout << f3(Plus(), 1.11, 2.22) << endl;
// 通过提前bind简化调用的编写并显得统一
function<double(double, double)> f4 = bind(&Plus::plusd, Plus(), placeholders::_1, placeholders::_2);
cout << f4(1.11, 2.22) << endl;
return 0;
}
应用