1.{}初始化
我们在书写代码的时候可以通过{}进行初始化
int main(void)
{
struct P
{
P(int a, int b)
{
_a = a;
_b = b;
cout << "P" << endl;
}
int _a;
int _b;
};
int a1 = { 1 };
int a2 = 1;
int a3{ 1 };
int arr1[] = { 1,2,3 };
int arr2[]{ 1,2,3 };
//自定义类型会自动调用其构造函数
P s1{ 1,2 };
P s2 = { 1,2 };
P s11(1, 3);
//这两组编译会报错 因为本质上{}是多参数的隐式类型转换 会产生临时变量 临时变量具有常性
//用普通对象接受会发生权限放大不合法
/*P& s3 = { 3,4 };
P& s4 { 3,4 };*/
const P& s3 = { 3,4 };
const P& s4 = { 3,4 };
P* pa = new P[4]{ {1,2},{2,3},{3,4},{5,6} };
int* pb = new int[3] {0};
return 0;
}
我们发现无论是自定义类型还是内置类型
都可以通过{}初始化 但是要注意
本质上{}是多参数的隐式类型转换
会产生临时变量 临时变量具有常性 用普通对象接受会发生权限放大不合法
也会自动调用自定义类型的构造函数
我们也可以不用写= 但是最好还是写着 这样可读性高
2.initializer_list
int main(void)
{
struct P
{
P(int a, int b)
{
_a = a;
_b = b;
cout << "P" << endl;
}
int _a;
int _b;
};
//下面两个是不是同一个语法 ?
//不是 vector后面可以加很多值 但是自定义类型s1只能两个参数
P s1 = { 1,2 };
vector<int> s2 = { 1,2,3 };
auto il = { 1,2,3,4,5 };
cout << typeid(il).name() << endl;
//本质上还是调用initializer_list的构造函数
initializer_list<int>il1 = { 1,2,3,4,5 };
initializer_list<P> s11 = { {1,2},{3,4},{5,6} };
cout << sizeof(il1) <<" "<< sizeof(il) << endl;
//c++不支持这种写法是因为和 initializer_list有冲突
//int* ptr1 = { 1,2,3 };
return 0;
}
这个代码打印的结果是什么?
那么什么是initializer_list呢?
initializer_list是一个自定义类型
它有两个成员
实现原理 内部结构:initializer_list<T> 仅包含两个成员:
T* begin():指向第一个元素的指针。
T* end():指向最后一个元素之后位置的指针。
initializer_list 内部仅存储指针和大小,其管理的元素通常存储在临时内存中:
编译期确定的常量元素: 如果元素是编译期常量(如字面量 {1, 2, 3}),编译器可能将元素存储在只读内存(类似字符串常量)或静态区。
这些元素在程序启动时创建,生命周期为整个程序运行期。
运行期生成的元素: 如果元素是运行期计算的结果(如函数返回值),元素通常存储在栈上临时区域,生命周期与 initializer_list 对象绑定。
当 initializer_list 超出作用域时,元素可能被销毁(除非被其他对象持有)。
initializer_list<T> 对象的大小固定为两个指针的大小(例如在 64 位系统上为 16 字节),与 T 的类型和元素数量无关。
只读性:无法修改 initializer_list 中的元素(没有 push_back() 等方法)。
类型一致性:花括号内的元素必须可隐式转换为同一类型 T。
优先级:如果类同时有接受 initializer_list 和其他参数的构造函数,列表初始化优先匹配 initializer_list 版本。
vector能这样初始化的原因是因为vector有 initializer_list的构造函数
3. decltype
decltype 是 C++11 引入的编译时类型推导关键字,核心作用是根据表达式的形式推导其类型,且不执行表达式(仅分析类型)。
int main(void)
{
auto s1 = malloc;
auto s = 2.3;
cout << typeid(s1).name() << endl << typeid(s).name() << endl;
decltype(malloc) s2;
cout << typeid(s2).name() << endl;
int a = 2;
double b = 2.3;
//本质上是先计算a*b再decltype其类型
decltype(a * b) s2;
decltype(a * b) s3 = 23;
return 0;
}
class A
{
private :
decltype(malloc) pf1;
};
比如说这个地方的A类的pf1和s2里面就只能用decltype
因为用auto就一定能要初始化
但是decltype初不初始化都可以!!!
4.右值引用
左值(
左值是指在内存中拥有可标识的地址、可以获取其存储位置的表达式。通俗来讲,左值就是有 “名字”、能被取地址的对象。最典型的左值就是变量,
另外,返回非引用类型的函数调用结果也是右值
右值
右值与左值相对,是指没有固定存储位置、不能获取其地址的表达式。右值通常是一个临时对象或字面常量
左值与右值的核心区别:
- 可寻址性:左值有固定的内存地址,可以通过&运算符获取;右值没有固定地址,无法取地址。
- 生命周期:左值的生命周期通常由其作用域决定,在作用域内一直存在;右值大多是临时对象,在表达式结束后立即销毁。
- 赋值角色:左值可以作为赋值目标,出现在=左侧;右值只能作为数据源,出现在=右侧(除了 C++11 引入的移动语义场景) 。
匿名对象属于右值
左值引用
左值引用使用&符号声明,只能绑定到左值。
左值引用的主要用途包括:
- 避免对象拷贝:在函数参数传递和返回值时,使用左值引用可以减少不必要的对象拷贝,提高性能。
-
作为函数返回值:返回左值引用的函数可以让调用者直接操作函数内部的对象(通常是静态对象或成员变量)。
右值引用
右值引用是 C++11 引入的新特性,使用&&符号声明,专门用于绑定右值。
右值引用的核心作用是实现移动语义和完美转发:
- 移动语义:允许将右值的资源(如动态分配的内存)直接 “移动” 给其他对象,避免不必要的拷贝,提高性能。
-
完美转发:在模板函数中,右值引用可以保留实参的左值或右值属性,确保参数传递的正确性和高效性。
左值和右值之间的转换
左值引用修饰右值要加const
右值引用修饰左值要用move函数强制将左值转换成右值
int main(void)
{
double s1 = 2.3;
double s2 = 3.4;
//s1 s2都是左值 s1+s2表达式有一个返回值 这个返回值是一个临时变量 这个临时变量是右值
//左值引用可以引用右值 但是要加const
//错误示范 double& s3 = (s1 + s2);
const double& s3 = s1 + s2;
//右值引用给左值取别名 直接是不可以的
//错误示范 double&& s5 = s1;
double&& s6 =move(s2);//move函数可以强制将左值转换成右值
double&& s4 = s1 + s2;
return 0;
}
右值引用的作用
namespace ly {
class num {
public:
// 构造函数
num(int a = 0, float b = 0.0f, double c = 0.0, int* p = nullptr)
: _a(a), _b(b), _c(c), ptr(nullptr)
{
if (p) {
// 深拷贝:分配新内存并复制值
ptr = static_cast<int*>(malloc(sizeof(int)));
*ptr = *p;
}
std::cout << "构造函数:深拷贝 " << this << std::endl;
}
// 拷贝构造函数(深拷贝)
num(const num& other)
: _a(other._a), _b(other._b), _c(other._c), ptr(nullptr)
{
if (other.ptr) {
ptr = static_cast<int*>(malloc(sizeof(int)));
*ptr = *other.ptr;
}
std::cout << "拷贝构造:深拷贝: " << this << " from " << &other << std::endl;
}
// 移动构造函数
num(num&& other) noexcept
: _a(other._a), _b(other._b), _c(other._c), ptr(other.ptr)
{
other.ptr = nullptr; // 转移所有权后将源对象指针置空
std::cout << "移动构造: " << this << " from " << &other << std::endl;
}
// 拷贝赋值运算符(深拷贝)
num& operator=(const num& a) {
std::cout << "赋值重载:深拷贝 " << this << " from " << &a << std::endl;
if (this != &a) {
// 释放当前资源
free(ptr);
ptr = nullptr;
// 深拷贝
if (a.ptr) {
ptr = static_cast<int*>(malloc(sizeof(int)));
*ptr = *a.ptr;
}
_a = a._a;
_b = a._b;
_c = a._c;
}
return *this;
}
// 移动赋值运算符
num& operator=(num&& b) noexcept {
std::cout << "右值引用赋值: " << this << " from " << &b << std::endl;
if (this != &b) {
// 释放当前资源
free(ptr);
// 转移资源所有权
ptr = b.ptr;
b.ptr = nullptr; // 仅需这一行,不要重复设置ptr
_a = b._a;
_b = b._b;
_c = b._c;
}
return *this;
}
~num() {
std::cout << "析构函数: " << this << std::endl;
free(ptr); // 安全释放,ptr可能为nullptr
}
public:
int _a;
float _b;
double _c;
int* ptr;
};
}
ly::num func1()
{
int value = 42; // 创建一个局部变量
int* pt3 = &value; // 指向局部变量的指针
ly::num other(1, 2.0f, 3.0, pt3); // 传递指针
return other;
}
ly::num func2()
{
int value1 = 43; // 创建一个局部变量
int* pt4 = &value1; // 指向局部变量的指针
ly::num other(1, 2.0f, 3.0, pt4); // 传递指针
return other;
}
int main(void)
{
int value1 = 100; // 创建有效对象
int value2 = 200;
int* pt1 = &value1; // 指向有效对象
int* pt2 = &value2;
ly::num apply;
std::cout << std::endl << std::endl;
apply = func1(); // 注意:func()中的pt3指向局部变量,返回后可能悬空
std::cout << std::endl << std::endl;
ly::num apply1 = func2();
std::cout << std::endl << std::endl;
return 0;
}
内置类型的右值是纯右值
自定义类型的右值是将亡值
比如函数返回的自定义类型的右值
生命周期只在这一行 所以被称为将亡值
我们来看移动赋值运算符和移动构造函数
本质上都是通过右值引用实现资源转移的
移动构造函数(通过右值引用实现)相比传统的左值引用拷贝构造函数,
核心优势在于避免不必要的资源复制,显著提升性能。
以下是详细对比和解释
: 一、核心区别:资源管理方式
1. 拷贝构造函数(左值引用) 必须复制资源:无论资源(如动态内存、文件句柄)是否可转移,都需创建新资源并复制数据。
开销:深拷贝操作通常涉及内存分配、数据复制等耗时操作。
2. 移动构造函数(右值引用) 转移资源所有权:直接接管临时对象(右值)的资源,无需复制。 开销:仅需修改指针指向,时间复杂度为 O (1)。
注意:这个地方的移动构造实现的是资源的转移 不会导致两个指针指向同一块空间
被析构两次 要区别于浅拷贝!!!
编译器对于同一行的同一步骤连续的拷贝会进行优化 合二为一 移动拷贝同理
编译器会把函数的返回值识别成右值(不管会不会拷贝临时变量)
注意:但是返回值如果是引用不管是左值引用还是右值引用 返回值都是左值!!!
我们来看一下右值引用的一些使用场景
int main(void)
{
list<string> it;
string s1("123");
string s2("456");
//左值传给val(这个地方参数是引用 所以就不用拷贝) 然后再开空间拷贝构造s1
it.push_back(s1);
//右值传给val(这个地方参数是引用 所以就不用拷贝) 直接把s2的资源移动构造
it.push_back(move(s2));
//本质上是隐式类型转换 要拷贝构造给临时对象(匿名对象)
// 再把匿名对象传给 val 然后开空间拷贝构造匿名对象
//
//但是有了右值引用 我们就不用开空间拷贝匿名对象这一步 变成直接转移匿名对象的资源(移动构造)
it.push_back("789");
}
//func函数不能用string&或者string&&作为返回值
// 因为这两种返回的是左值和右值的别名 但是出了函数作用域
//返回值被销毁了别名就失效了 和野指针一样
string func()
{
string s1("123");
return s1;
}
int main(void)
{
//s1作为返回值 s1先深拷贝给临时变量 临时变量再深拷贝给s2
//
//但是如果有移动构造 临时变量就不用深拷贝给s2 就直接通过资源转移把临时变量转移给s2了
//少了一次深拷贝 大大提升了效率
string s2 = func();
return 0;
}
const+左值引用可以接收右值 为什么还需要右边值引用呢?
右值引用主要并不是用来专门接收右值的
而是来解决左值引用无法解决的一些情况
比如说减少一些没有必要的深拷贝
5.完美转发
我们先来看这段代码
首先我们知道163行和164行g的参数是右值 所以 匹配的是第156行的g函数
但是为什么其参数ss是右值 调用的f函数匹配的确是参数是左值的f还函数呢?
这很好理解 因为不管是s还是ss在g函数的函数体内 都是一个可以取地址的局部变量
所以自然就是左值了!!!
同时我们还要理解右值引用得到的到底是右值 还是左值 这个下文有介绍
void f(string& s) {
cout << "左值" << s<< endl<<endl;
}
void f(string&& ss) {
cout << "右值" << ss << endl<<endl;
}
void g(string& s) {
//f(static_cast<string&>(s));
f(forward<string&>(s));
}
void g(string&& ss){
//f(static_cast<string&&>(ss));
f(forward<string&&>(ss));
}
int main(void) {
string s1("123");
g(s1);
g("456");
g(move(s1));
return 0;
}
那如果我们希望s调用左值引用的f函数
ss调用右值引用的f函数该怎么办呢?
有两种办法 第一就是用static_cast函数
第二种就是std::forward函数
但是在遇到模板的情况下 变成了完美转发(万能引用)
函数形参时是万能引用 在函数体代码中不是
折叠规则
下图是foward函数的大致代码
然后我们来看完美转发
函数实参是左值 就会被推到为实参类型的引用
如果是右值 就会被推到为实参的非引用类型
void f(string& s) {
cout << s<< " 左值" << endl;
}
void f(string&& ss) {
cout << ss << " 右值" << endl;
}
template<class T>
void g(T&& s){
f(forward<T>(s));
}
int main(void) {
string s1("123");
g(s1);//传的参数是左值 类型是string 所以T就会被实例化成string&
//g函数就变成
/*
void g(string& && s){
f(forward<string&>(s));
}
forward返回的就是static_cast(string& &&)(t)
f(static_cast(string& &&)(t))
因为C++不允许引用的引用所以就出现了折叠规则
由于折叠规则变成
void g(string& s){
f(forward<string&>(s));
}
forward返回的就是static_cast(string&)(t)
f(static_cast(string &)(t))
*/
g("223");//传的参数是右值 类型是string T就会被实例化成string
//g函数就变成
/*
void g(string && s){
f(forward<string>(s));
}
forward返回的就是static_cast(string &&)(t)
f(static_cast(string &&)(t))
*/
return 0;
}
关于右值引用到底是右值还是左值我们要分两种情况讨论
首先如果右值引用是、右值 合适吗? 不合适 因为右值不能取地址 也不能修改 如果不能修改 我们怎么进行资源转移?
如果右值引用是右值 那么我们在资源转移的时候会出现大问题
class MyString { /* 包含堆内存指针data */ };
void process(MyString&& x) {
// 假设x是右值(违反实际规则)
MyString a = x; // 第一次使用x:触发移动构造,x的资源被转移到a,x.data变为nullptr
MyString b = x; // 第二次使用x:尝试移动一个已被掏空的x,访问nullptr,导致未定义行为
}
// 调用时,传递一个临时对象(右值)
process(MyString("hello"));
如果x是右值,两次赋值都会触发移动构造:
第一次转移资源后,x的内部资源已被掏空(指针为nullptr);
第二次移动时,会试图访问nullptr,导致崩溃。
如果右值引用是左值的化那么这个问题就可以解决了
void process(MyString&& x) { // x是左值(右值引用变量)
MyString a = x; // 编译报错!x是左值,默认尝试调用拷贝构造(如果禁用拷贝则报错)
MyString b = std::move(x); // 显式转为右值,触发移动构造:资源从x转移到b,可控
}
那么如果右值引用是左值同样也会有一些麻烦
比如说上面的f函数和g函数 我想让右值引用匹配右值 左值引用匹配左值
让左值引用和右值引用分开处理
但是左值引用和右值引用得到的都是右值 我无法做区分
于是这就出现了完美转发 完美转发的出现 让我们可以实现右值引用和左值引用的分开处理
在STL里面完美转发被使用的较多 比如下面这张图 args是可变模板参数 在 C++11(2)
里面有介绍
6.lambda表达式
lambda 表达式本质上是一个可调用对象,也被称为匿名函数对象。
它可以在代码中直接定义,不需要像普通函数那样有一个明确的函数名。
从语法上看,一个基本的 lambda 表达式由捕获列表、参数列表、可选的返回值类型以及函数体组成。
语法结构 其一般形式为:[capture list] (parameter list) -> return type { function body }
捕获列表:用于指定在 lambda 表达式中如何访问外部作用域中的变量,可以是值捕获、引用捕获或者隐式捕获。
比如[a]表示按值捕获变量a,[&a]表示按引用捕获变量a,[=]表示隐式值捕获所有外部可见变量,[&]表示隐式引用捕获所有外部可见变量 。
参数列表:和普通函数的参数列表类似,定义了 lambda 表达式接受的参数。例如(int x, int y)表示接受两个int类型的参数。 参数名称可以不写 例如(int,int)
返回值类型:可以显式指定,也可以让编译器自动推导。
当函数体只有一个返回语句时,编译器通常能准确推导出返回值类型;但如果函数体有多条语句,为了保证代码的清晰和正确性,最好显式指定返回值类型。
函数体:包含了 lambda 表达式要执行的具体代码逻辑,和普通函数的函数体一样,实现具体的功能。
比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复
lambda的底层是仿函数 仿函数的对象调用operator() 只不过这个仿函数的名称类型 我们不知道 对我们来说是匿名的而已
可以通过汇编得到验证!!!