一个专门做酒店招聘的网站,如何建立门户网站,全国城市感染率排名,国内搜索引擎排名第一的是这里写目录标题 左值右值左值引用和右值引用右值引用和移动构造函数std::move 移动语义返回值优化移动操作要保证安全 万能引用std::forward 完美转发传入左值传入右值 左值
左值是指可以使用 符号获取到内存地址的表达式#xff0c;一般出现在赋值语句的左边#xff… 这里写目录标题 左值右值左值引用和右值引用右值引用和移动构造函数std::move 移动语义返回值优化移动操作要保证安全 万能引用std::forward 完美转发传入左值传入右值 左值
左值是指可以使用 符号获取到内存地址的表达式一般出现在赋值语句的左边比如变量、数组元素和指针等。
下面是左值的举例
int i 42; // i 是一个 left value
int *p i // i 是一个左值可以通过 获取内存地址
int ldemoFoo() {return i;
}ldemoFoo() 42;// 这里使用函数返回引用的形式, ldemoFoo 是一个左值
int *p1 ldemoFoo() 左值lvalue代表的是对象的身份和它在内存中的位置它一般出现在赋值语句的左侧左值通常是可修改的可以修改的左值。 Notice : 左值代表一个具体的对象位置在内存中有明确位置通常是可以修改的
左值的特点包括
可寻址性左值可以取得地址即你可以使用 运算符来取得一个左值的地址。可赋值性左值可以出现在赋值语句的左侧你可以将一个值赋给它。
下面类型的值都是左值
变量名如 int x;x是一个左值。数组元素如 arr[0]arr[0] 是一个数组的左值元素。结构体或类的成员如 obj.member obj.member 是一个对象的左值成员。解引用的指针如 *ptr*ptr 是通过指针访问的对象的左值。
并非所有的左值都是可修改的。例如const 限定的左值就不应该被修改。
右值
在C中右值rvalue是指哪些不代表内存的中具体位置, 不能被赋值和取地址的值 。 一般出现在赋值操作符的右边表达式结束就不存在的临时变量。
右值的典型例子包括 字面量、临时对象以及某些表达式的结果。 右值主要用来表示数据值本身而不是数据所占据的内存位置。
右值的关键特性包括
不可寻址性右值不能取得地址尝试对右值使用 运算符会导致编译错误。可移动性 由于右值不代表持久的内存位置因此可以安全地 “移动” 它们的资源到另一个对象而无需进行复制。这就是为什么右值经常与移动语义一起使用。临时性许多右值是临时对象它们在表达式结束后就会被销毁。
Notice:右值必须要有一个包含内存地址变量去接收这个指否则就会丢弃
C 中右值的例子有
字面量比如整数10、字符’A’、浮点数3.14。函数返回的临时值如 getRandomNumber() 返回的随机数注意函数也有可能返回左值。由运算符产生的值比如表达式 a b 的结果假设 a 和 b 是数值类型的变量。空指针常量nullptr。字符串字面量比如hello world。类的右值构造函数或移动构造函数生成的临时对象如 MyClass() 创建的临时对象。通过std::move()转换得到的右值std::move(myObject)其中 myObject 是一个左值。数组下标的表达式如果数组是右值那么数组下标也是右值例如 arr[0]其中arr是一个临时数组。类成员的右值访问如果类有一个返回右值的成员函数那么该函数返回的结果是右值。
// 10 A 都是右值字面量
int a 10;
char b A;// generateResult 返回值是一个临时对象也就是右值
int a generateResult(20, 10);
// a b 产生的结果也是右值
int m a b;
// hello world 是右值
const char *pName hello world;
// nullptr 是右值
int32_t *p nullptr;DemoClass p DemoClass();注意函数返回值不一定只能是右值也有可能是左值比如返回引用的情形
int testlvaluefuncyion() {int i;return i;
}int testrvaluefuncyion() {int i 5;return i;
}{testlvaluefuncyion();// 正确函数返回值可作为左值testlvaluefuncyion() 10;int *p1 testlvaluefuncyion();std::cout function return value as leftvalue std::endl;
}// 函数返回值是int类型此时只能作为右值
{testrvaluefuncyion();//testrvaluefuncyion() 10;std::cout function return value as rightvalue std::endl;
}左值引用和右值引用
C中的引用是一种别名代表的就是变量的地址本身可以通过一个变量别名访问一个变量的值。 int a b 表示可以通过引用 a 访问变量 b , 注意引用实际就是指向变量 b,等于是变量 b 的别名 左值引用是指对左值进行引用的引用类型通常使用 符号定义 右值引用是指对右值进行引用的引用类型通常使用 符号定义
C11引入了右值引用允许我们将右值绑定到引用上。这在 移动语义和 完美转发 等高级功能中非常有用。
class DemoClass {...};
// 接收一个左值引用
void foo(X x);
// 接收一个右值引用
void foo(X x);X x;
foo(x); // 传入参数为左值调用foo(X);X bar();
foo(bar()); // 传入参数为右值调用foo(X);通过重载左值引用和右值引用两种函数版本满足在传入左值和右值时触发不同的函数分支。 注意 void foo(const X x); 同时接受左值和右值传参。
void foo(const X x);
X x;
foo(x); // ok, foo(const X x)能够接收左值传参X bar();
foo(bar()); // ok, foo(const X x)能够接收右值传参// 新增右值引用版本
void foo(X x);
foo(bar()); // ok 精准匹配调用foo(X x)定义右值引用的方法
int a 10;
// 定义左值引用
int lvalue_ref a; // 定义右值引用
int rvalue_ref 10 20; 右值引用和移动构造函数
假设定义一个类 DemoContainerClass包含一个指针成员变量 p该指针指向了另一个成员变量 DemoBasicClass假设 DemoBasicClass 占用了很大的内存创建和复制 DemoBasicClass 都需要很大的开销。
class DemoBasicClass {
public:DemoBasicClass() {std::cout __FUNCTION__ construct call std::endl;}~DemoBasicClass() default;DemoBasicClass(const DemoBasicClass ref) {std::cout __FUNCTION__ copy construct call std::endl;}
};class DemoContainerClass{
private:DemoBasicClass *p nullptr;
public:DemoContainerClass() {p new DemoBasicClass();}
~DemoContainerClass() {if( p ! nullptr) {delete p;}
}
DemoContainerClass(const DemoContainerClass ref) {std::cout __FUNCTION__ copy construct call std::endl;p ref.p;
}DemoContainerClass operator(const DemoContainerClass ref) {std::cout __FUNCTION__ operator construct call std::endl;DemoBasicClass* tmp new DemoBasicClass(*ref.p);delete this-p;this-p tmp;return *this;
}上面定义了 DemoContianerClass 的赋值构造函数 现在假设有下面的场景
{DemoContainerClass p;DemoContainerClass q;p q;
}输出如下
rValurRefDemo::DemoBasicClass::DemoBasicClassconstruct call
rValurRefDemo::DemoBasicClass::DemoBasicClassconstruct call
rValurRefDemo::DemoContainerClass::operator operator construct call
rValurRefDemo::DemoBasicClass::DemoBasicClasscopy construct callDemoContainerClass p 和 DemoContainerClass q 初始化时都会执行 new DemoBasicClass所以会调用两次 DemoBasicClassconstruct 执行 p q 时会调用一次 DemoBasicClass 的拷贝构造函数根据 ref 复制出一个新结果。 由于 q 在后面的场景还是可能使用的为了避免影响 q在赋值的时候调用DemoBasicClass 的构造函数复制出一个新的 DemobasicClass 给 p 是没有问题的。
但在下面的场景下这样是没有必要的
static rValurRefDemo::DemoContainerClass demofunc() {return rValurRefDemo::DemoContainerClass();
}{DemoContainerClass p;p demofunc();
}这种场景下demofunc 创建的那个临时对象在后续的代码中是不会用到的所以我们不需要担心赋值函数中会不会影响到那个 DemobasicClass 临时对象也就没有必要创建一个新的 DemoBasicClass 类型给 p 更高效的做法是直接使用 swap 交换对象的 p 指针这样做有两个好处
不需要调用 DemobasiClass 的构造函数提高效率交换之后demofunc 返回的临时对象拥有 p 对象的 p 指针在析构时可以自动回收避免内存泄漏
这种避免高昂的复制成本从而直接将资源从一个对象移动到另一个对象的行为就是C的 移动语义。 哪些场景适合移动操作呢无法获取内存地址的右值就很合适我们不需要担心后续的代码会用到这个值。 添加移动赋值构造函数如下
DemoContainerClass operator(DemoContainerClass rhs) noexcept {std::cout __FUNCTION__ move construct call std::endl;std::swap(this-p, rhs.p);return *this;
};输出结果如下
###############################################################
rValurRefDemo::DemoBasicClass::DemoBasicClassconstruct call
rValurRefDemo::DemoBasicClass::DemoBasicClassconstruct call
rValurRefDemo::DemoContainerClass::operator move construct callstd::move 移动语义
C提供了std::move函数这个函数做的工作很简单通过隐藏掉入参的名字返回对应的右值。
std::cout ############################################# std::endl;
{DemoContainerClass p;DemoContainerClass q;// OK 返回右值,调用移动赋值构造函数 q,但是 q 以后都不能正确使用了p std::move(q);}std::cout ####################################### std::endl;
{DemoContainerClass p;// OK 返回右值,调用移动赋值构造函数 效果和 demofunc 一样p std::move(demofunc());
}输出结果如下
###############################################################
rValurRefDemo::DemoBasicClass::DemoBasicClassconstruct call
rValurRefDemo::DemoBasicClass::DemoBasicClassconstruct call
rValurRefDemo::DemoContainerClass::operator move construct call
###############################################################
rValurRefDemo::DemoBasicClass::DemoBasicClassconstruct call
rValurRefDemo::DemoBasicClass::DemoBasicClassconstruct call
rValurRefDemo::DemoContainerClass::operator move construct call一个容易犯错的例子
class Base {
public:// 拷贝构造函数Base(const Base rhs);// 移动构造函数Base(Base rhs) noexcept;
};class Derived : Base {
public:Derived(Derived rhs)// wrong. rhs是左值会调用到 Base(const Base rhs).// 需要修改为Base(std::move(rhs)): Base(rhs) noexcept {...}
}返回值优化
考虑下面的情形
DemobasicClass foo() {DemobasicClass x;return x;
};DemobasicClass bar() {DemobasicClass x;return std::move(x);
}大家可能会觉得 foo 需要一次复制行为从 x 复制到返回值bar 由于使用了 std::move满足移动条件所以触发的是移动构造函数从x移动到返回值。复制成本大于移动成本所以 bar 性能更好。
实际效果与上面的推论相反bar中使用std::move反倒多余了。现代C编译器会有返回值优化。换句话说编译器将直接在foo返回值的位置构造x对象而不是在本地构造x然后将其复制出去。很明显这比在本地构造后移动效率更快。
移动操作要保证安全
比较经典的场景是std::vector 扩缩容。当vector由于push_back、insert、reserve、resize 等函数导致内存重分配时如果元素提供了一个 noexcept 的移动构造函数vector 会调用该移动构造函数将元素移动到新的内存区域否则 则会调用拷贝构造函数将元素复制过去。
万能引用
完美转发是C11引入的另一个与右值引用相关的高级功能。它允许我们在函数模板中将参数按照原始类型左值或右值传递给另一个函数从而避免不必要的拷贝和临时对象的创建。
为了实现完美转发我们需要使用 std::forward 函数和通用引用也称为转发引用。通用引用是一种特殊的引用类型它可以同时绑定到左值和右值。通用引用的语法如下
通用引用的形式如下
templatetypename T
void foo(T param); 万能引用的ParamType是T既不能是const T也不能是std::vector 通用引用的规则有下面几条
如果 expr 是左值, T 和 param 都会被推导为左值引用如果 expr 是右值, T会被推导成对应的原始类型, param会被推导成右值引用注意虽然被推导成右值引用但由于param有名字所以本身还是个左值。在推导过程中expr的const属性会被保留下来。
参考下面示例
templatetypename T
void foo(T param);// x是一个左值
int x 2 7;
// cx 是带有const的左值
const int cx x;
// rx 是一个左值引用
const int rx cx;// x是左值所以T是intparam类型也是int
foo(x);// cx是左值所以T是const intparam类型也是const int
foo(cx);// rx是左值所以T是const intparam类型也是const int
foo(rx);// 27是右值所以 T 是intparam类型就是 int
foo(27);std::forward 完美转发
templatetypename T, typename Arg
std::shared_ptrT factory_v4(Arg arg)
{ return std::shared_ptrT(new T(std::forwardArg(arg)));
}// std::forward的定义如下
templateclass S
S forward(typename remove_referenceS::type a) noexcept
{return static_castS(a);
}传入左值
int p; auto a factory_v4§;
根据万能引用的推导规则factory_v4中的 Arg 会被推导成 int。这个时候factory_v4 和 std::forwrd等价于
shared_ptrA factory_v4(int arg)
{ return shared_ptrA(new A(std::forwardint(arg)));
}int std::forward(int a)
{return static_castint(a);
}这时传递给 A 的参数是 int, 调用的是拷贝构造函数 A(int ref), 符合预期
传入右值
auto a factory_v4(3);
shared_ptrA factory_v4(int arg)
{ return shared_ptrA(new A(std::forwardint(arg)));
}int std::forward(int a)
{return static_castint(a);
}此时std::forward作用与std::move一样隐藏掉了arg的名字返回对应的右值引用。 这个时候传给A的参数类型是X即调用的是移动构造函数A(X)符合预期。 ####重要参考 深浅拷贝和临时对象