C++ 从&到&&
人类发展史,就是不断挖坑、填坑的过程。
语言发展史也是如此!
任何一门设计合理的语言,给你的限制或提供的什么特性,都不是没有代价的。
C的指针
指针:pointer
指针的思想起源于汇编。指针思想是编程思想历史上的重大飞跃。
每一个编程语言都使用指针。C语言将指针完全暴露给了用户。潘多拉之盒。
使用指针的必要性:资源管理,即地址管理。
思想层:将地址包了一层。
语法层:T *p; *p;
编译器:包含一个intptr_t类型成员的结构体。
汇编层:寄存器间接寻址MOV。

C语言中只有一种参数传递方式:值传递。
void f(int p)
void f(int *p)
利用指针交换两个数字
#include <stdio.h>
void Swap(int *p1,int *p2){
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
int main(){
int a = 10;
int b = 20;
Swap(&a,&b);
printf("%d %d\n",a,b);
return 0;
}
指针的级数Int *p; int **p; int ***p;
理论上无限级,无限套娃。实际上受编译器限制。
指针是一扇门,推开门,后面是整个世界。
C++的引用
引用:reference
已存在变量的别名。
使用引用的必要性:资源使用
思想层:受限制的指针。
语法层:T a; T &p=a;
编译器:给你做了保证,一定是经过初始化的指针
汇编层:和指针一样。
在汇编层,指针和引用是完全一样的。
引用是一个语法糖,T a; T &p=a;等价于 T *const p = &a
int x=0;
00676664 mov dword ptr [x],0
int &a = x;
0067666B lea eax,[x]
0067666E mov dword ptr [a],eax
a = 1;
00676671 mov eax,dword ptr [a]
00676674 mov dword ptr [eax],1
int *p = &x;
0067667A lea eax,[x]
0067667D mov dword ptr [p],eax
*p = 2;
00676680 mov eax,dword ptr [p]
00676683 mov dword ptr [eax],2
int *const p2 = &x;
00676689 lea eax,[x]
0067668C mov dword ptr [p2],eax
*p2 = 3;
0067668F mov eax,dword ptr [p2]
00676692 mov dword ptr [eax],3
引用的情况:
int a = 1;
const int b = 1;
int &ref1 = a;
int &ref2 = 1;//ERROR
const int &ref3 = b;
const int &ref4 = 1;
Q:唯独int &ref2 = 1;//ERROR?
A:C++的早期这种语法是被允许的,但是在函数调用传参数时,会给程序员带来误解,于是后面就禁止了这种语法。
引用规则的特例:const引用
void f(int &i){}
void f(const int &i){}
int main(){
int i = 1;
f(i);//call f(int &i)
f(2);//call f(const int &i)
return 0;
}
void f(int &i){}
//void f(const int &i){}
int main(){
int i = 1;
f(i);//call f(int &i)
f(2);//ERROR
return 0;
}
//void f(int &i){}
void f(const int &i){}
int main(){
int i = 1;
f(i);//call f(const int &i)
f(2);//call f(const int &i)
return 0;
}
C++语言中就有了新的参数传递方式:引用传递 void f(T &p) 。实质也是传值。
自定义类型最好用引用传递,可以避免不必要的构造函数和析构函数的调用。
内置类型建议用值传递,自定义类型建议用引用传递,内置类型,值传递会比按引用传递更高效。
解释见:这里
利用引用交换两个数字
#include <iostream>
#include <stdlib.h>
using namespace std;
void swap(int &a, int &b){
int tmp = a;
a = b;
b = tmp;
}
int main(){
int a = 3;
int b = 4;
swap(a, b);
cout << "a=" << a<<" " << "b=" << b << endl;
return 0;
}
引用的级数
只能一级,引用的对象必须是一个已存在的地址。引用变量本身的地址,外界不能访问。
References are not objects; they do not necessarily occupy storage,
Because references are not objects, there are no arrays of references, no pointers to references, and no references to references。int& a[3]; // ERRORint&* p; // ERRORint& &r; // ERROR
引用和指针叠加int a; int *p = &a; int *&r = p; //OK
使用引用的场景:
- 给函数传递可变参数
- 给函数传递大型对象
- 引用函数返回值;
Q:引用能实现的基本上指针都可以实现,那为什么C++还需要引入引用呢?
A:最初主要是为了支持运算符重载。
c = a + b是可以接受的写法,而c = &a + &b 就不是很方便而且有歧义了。
写法上的方便是要第一考虑的。
Q:C++引入了引用,那为什么C++不和Java一样让指针对使用者不可见呢?
A:历史原因。为了兼容C语言。程序员是自由的。
Q:C++为什么选择&作为引用的标识符?
A:需要用一个符号告诉编译器,传的是引用。&在C语言中是取地址的作用,于是就选择了它。
Q:this为什么是指针类型,而不是引用类型?
A:历史原因。this诞生早于引用。某种意义上来讲,this应该被设计为引用类型。
Q:Why is "this" not a reference?
A:Because "this" was introduced into C++ (really into C with Classes) before references were added. Also, I chose "this" to follow Simula usage, rather than the (later) Smalltalk use of "self".
Q:拷贝构造函数参数一定是引用,不然编译通不过,为什么?
A:因为在入参的过程中,如果不是引用,会首先进行一次值拷贝;而要实现的就是拷贝构造,就会造成不断的递归最后爆炸。
Q:引用是受限制的指针,哪里受限制了?
A:引用变量本身的地址外界不可获得,当然编译器是可以的。
Q:引用变量是否占有内存空间?
A:引用可以不占用,也可以占有。语法规定对引用变量的操作都是对被引用对象的操作。
struct S {
int a;
int &b;
};
int x = 123;
S s(123,x);
sizeof(S)=?//32位环境等于8
non-const引用的汇编视角
const引用的汇编视角
说明:const引用变量绑定没有地址的对象时,会生成一个临时变量/匿名对象来中转。
全局定义
const int& a = 123; 123的匿名对象在堆上
局部定义
void f{
const int& a = 456; 456的匿名对象在栈上
}
往下走用*,往上走用&。
C++的第一个坑:兼容了C语言的指针。
C++的构造
3种构造语义:
- 构造函数constructor
- 拷贝构造copy constructor
- 拷贝赋值运算符copy assignment operator
构造函数S()
出厂设置
拷贝构造S(const S &other)
把A的数据复制给B。B(A);
拷贝赋值运算符S& operator=(const S &other)
先把B的资源释放,再把A的数据复制给B。B=A;
变量按内存分为两种:
- 不包含指针。trivial type。篮球。
- 包含指针。handle type。风筝和风筝线。
拷贝的分类
- 浅拷贝
引用语意(reference semantics)
缺陷:若写法不对,可能会发生double free。
Q:为什么编译器所有的默认的行为都是浅拷贝?
A:深拷贝不一定能实现。指向的对象可能是多态的,也可能是数组,也可能有循环引用。所以只能留待成员变量的类来决定怎样实现复制。
有时候为了防止默认拷贝发生,可以声明一个私有的拷贝构造函数。这种做法比较常见,但不可取。 - 深拷贝
值语意(value semantics)
缺陷:出现了额外的构造和析构,性能损失。
深拷贝和浅拷贝的本质区别就是两个对象的行为属性是否是独立变化的。
C++的第二个坑:拷贝构造函数
思考:
T为handle type,T A(...),T B(A)。A赋值给B。如果A不再使用了,能不能让B直接接管A的所有资源呢?(移动语义move semantics)
在不破坏现有语法规则情况下,你会如何设计?
- C++03现有语法不支持移动语义。需要新增移动语义。
- 如何标识对象的资源是可以被移动的呢?
- 这种机制必须以一种最低开销的方式实现,并且对所有的类都有效。
- 设计在编译层,与运行层面无关。
C++的设计者们注意到,大多数情况下,右值所包含的对象都是可以安全的被移动的。
左值与右值
左值和右值的概念
CPL语言引入了表达式值的类型value categories这种概念:左值和右值,left or right of assignment。
C语言沿用了类似的分类:左值和其他,locator value and other
C++98 沿用了C语言的分类,但是略微调整。引入了新定义:右值rvalue = non-lvalue。
C++11 新增了xvalue(an “eXpiring” value),并调整了之前左值和右值的定义。
(i)has identity: it's possible to determine whether the expression refers to the same entity as another expression, such as by comparing addresses of the objects or the functions they identify (obtained directly or indirectly);
(m)can be moved from: move constructor, move assignment operator, or another function overload that implements move semantics can bind to the expression.
准则 1:能不能分辨两个表达式指的是同一个物体。比如我们可以通过比较地址。
准则 2:能不能使用移动语义。比如看看能不能用调用移动构造函数。
- i&~m:lvalue 左值
- i&m:xvalue 将亡值
- ~i&m:prvalue 纯右值
- i:glvalue泛左值
- m:rvalue右值
C++17
分类和 C++11 是一样的,但是语义上更加明确了。
- glvalues:有自己地址的长寿对象
- prvalues:为了初始化而用的短命对象
- xvalue:资源已经不需要了,而且可以再利用的长寿对象
为了优化这样一个情况:T(T(T(x)))==>T(x),将prvalues的定义略微调整了下。
具体可以参考Copy elision (复制消除)
左值引用和右值引用
T &Lref; // 左值引用,就是传统的c++引用T &&Rref; // 右值引用
Q:为什么使用&&做为右值引用的标识符?
A:惯性设计。标准委员玩标点符号是真的可以。
规则:
- non-const左值引用只能绑定non-const左值
- non-const右值引用只能绑定non-const右值
- const左值引用,可以绑定任意。
- const右值引用,可以绑定non-const右值和const右值。注:这个使用的场景很少很少。
如何判定
namespace test {
template <typename T> struct is_lvalue_reference {
const static bool value = false;
};
template <typename T> struct is_lvalue_reference<T &> {
const static bool value = true;
};
template <typename T> struct is_rvalue_reference {
const static bool value = false;
};
template <typename T> struct is_rvalue_reference<T &&> {
const static bool value = true;
};
template <typename T> struct is_lvalue {
const static bool value = is_lvalue_reference<T>::value && (!is_rvalue_reference<T>::value);
};
template <typename T> struct is_xvalue {
const static bool value = (!is_lvalue_reference<T>::value) && is_rvalue_reference<T>::value;
};
template <typename T> struct is_prvalue {
const static bool value = (!is_lvalue_reference<T>::value && !is_rvalue_reference<T>::value);
};
template <typename T> struct is_rvalue {
const static bool value = (is_xvalue<T>::value || is_prvalue<T>::value);
};
template <typename T> struct is_glvalue {
const static bool value = (is_xvalue<T>::value || is_lvalue<T>::value);
};
}
struct Foo {};
Foo funRetFoo();
Foo &funRetFooLRef();
Foo &&funRetFooRRef();
TEST(TypeTraits, isRvalue) {
//base type
EXPECT_FALSE(::test::is_lvalue_reference<int>::value);
EXPECT_FALSE(::test::is_rvalue_reference<int>::value);
EXPECT_FALSE(::test::is_lvalue<int>::value);
EXPECT_FALSE(::test::is_xvalue<int>::value);
EXPECT_TRUE(::test::is_prvalue<int>::value);
EXPECT_FALSE(::test::is_glvalue<int>::value);
EXPECT_TRUE(::test::is_rvalue<int>::value);
// return obj
EXPECT_FALSE(::test::is_lvalue_reference<decltype(funRetFoo())>::value);
EXPECT_FALSE(::test::is_rvalue_reference<decltype(funRetFoo())>::value);
EXPECT_FALSE(::test::is_lvalue<decltype(funRetFoo())>::value);
EXPECT_FALSE(::test::is_xvalue<decltype(funRetFoo())>::value);
EXPECT_TRUE(::test::is_prvalue<decltype(funRetFoo())>::value);
EXPECT_FALSE(::test::is_glvalue<decltype(funRetFoo())>::value);
EXPECT_TRUE(::test::is_rvalue<decltype(funRetFoo())>::value);
// return ref obj
EXPECT_TRUE(::test::is_lvalue_reference<decltype(funRetFooLRef())>::value);
EXPECT_FALSE(::test::is_rvalue_reference<decltype(funRetFooLRef())>::value);
EXPECT_TRUE(::test::is_lvalue<decltype(funRetFooLRef())>::value);
EXPECT_FALSE(::test::is_xvalue<decltype(funRetFooLRef())>::value);
EXPECT_FALSE(::test::is_prvalue<decltype(funRetFooLRef())>::value);
EXPECT_TRUE(::test::is_glvalue<decltype(funRetFooLRef())>::value);
EXPECT_FALSE(::test::is_rvalue<decltype(funRetFooLRef())>::value);
// return rref obj
EXPECT_FALSE(::test::is_lvalue_reference<decltype(funRetFooRRef())>::value);
EXPECT_TRUE(::test::is_rvalue_reference<decltype(funRetFooRRef())>::value);
EXPECT_FALSE(::test::is_lvalue<decltype(funRetFooRRef())>::value);
EXPECT_TRUE(::test::is_xvalue<decltype(funRetFooRRef())>::value);
EXPECT_FALSE(::test::is_prvalue<decltype(funRetFooRRef())>::value);
EXPECT_TRUE(::test::is_glvalue<decltype(funRetFooRRef())>::value);
EXPECT_TRUE(::test::is_rvalue<decltype(funRetFooRRef())>::value);
int lvalue;
// 模拟=号左边
EXPECT_TRUE(::test::is_lvalue_reference<decltype(*&lvalue)>::value);
EXPECT_FALSE(::test::is_rvalue_reference<decltype(*&lvalue)>::value);
EXPECT_TRUE(::test::is_lvalue<decltype(*&lvalue)>::value);
EXPECT_FALSE(::test::is_xvalue<decltype(*&lvalue)>::value);
EXPECT_FALSE(::test::is_prvalue<decltype(*&lvalue)>::value);
EXPECT_TRUE(::test::is_glvalue<decltype(*&lvalue)>::value);
EXPECT_FALSE(::test::is_rvalue<decltype(*&lvalue)>::value);
//operator++()
EXPECT_FALSE(::test::is_lvalue_reference<decltype(lvalue++)>::value);
EXPECT_FALSE(::test::is_rvalue_reference<decltype(lvalue++)>::value);
EXPECT_FALSE(::test::is_lvalue<decltype(lvalue++)>::value);
EXPECT_FALSE(::test::is_xvalue<decltype(lvalue++)>::value);
EXPECT_TRUE(: