# 1.C++中的变量名是如何存储及引用
int a = 0;
如上,在C++
中声明一个变量时,可以知道a
是一个int
类型的变量占用4
个字节,其值为0
,这里就有个疑问,如果a
表示的变量占用了4
个字节是其值的内存空间,那么a
本身存储在哪里了呢?
在知乎上有这个问题,下面的回答内容也很好。简单来说就是,对于C/C++
这种需要预处理/编译/汇编/链接的翻译成机器代码的语言,变量名不需要储存,只是为了方便程序员编程,在编译器编译时会确定每个变量的地址,所有的局部变量读写都会变成(栈地址 + 偏移)的形式。
int main() {
int a = 0;
return 0;
}
得到对应汇编代码如下:
(gdb) disassemble /m main
Dump of assembler code for function main:
0x0000555555555129 <+0>: endbr64
0x000055555555512d <+4>: push %rbp
0x000055555555512e <+5>: mov %rsp,%rbp
0x0000555555555131 <+8>: movl $0x1,-0x4(%rbp)
0x0000555555555138 <+15>: mov $0x0,%eax
0x000055555555513d <+20>: pop %rbp
0x000055555555513e <+21>: retq
End of assembler dump.
可见只有指令,并没有变量的声明。当使用引用时,
int main() {
int a = 0;
int &b = a;
return 0;
}
得到对应汇编代码如下:
(gdb) disassemble /m main
Dump of assembler code for function main:
0x0000000000001149 <+0>: endbr64
0x000000000000114d <+4>: push %rbp
0x000000000000114e <+5>: mov %rsp,%rbp
0x0000000000001151 <+8>: sub $0x20,%rsp
0x0000000000001155 <+12>: mov %fs:0x28,%rax
0x000000000000115e <+21>: mov %rax,-0x8(%rbp)
0x0000000000001162 <+25>: xor %eax,%eax
0x0000000000001164 <+27>: movl $0x1,-0x14(%rbp)
0x000000000000116b <+34>: lea -0x14(%rbp),%rax
0x000000000000116f <+38>: mov %rax,-0x10(%rbp)
0x0000000000001173 <+42>: mov $0x0,%eax
0x0000000000001178 <+47>: ov -0x8(%rbp),%rdx
0x000000000000117c <+51>: xor %fs:0x28,%rdx
0x0000000000001185 <+60>: je 0x118c <main+67>
0x0000000000001187 <+62>: callq 0x1050 <__stack_chk_fail@plt>
0x000000000000118c <+67>: leaveq
0x000000000000118d <+68>: retq
End of assembler dump.
当使用指针时,
int main() {
int a = 0;
int *b = &a;
return 0;
}
得到对应汇编代码如下:
(gdb) disassemble /m main
Dump of assembler code for function main:
0x0000000000001149 <+0>: endbr64
0x000000000000114d <+4>: push %rbp
0x000000000000114e <+5>: mov %rsp,%rbp
0x0000000000001151 <+8>: sub $0x20,%rsp
0x0000000000001155 <+12>: mov %fs:0x28,%rax
0x000000000000115e <+21>: mov %rax,-0x8(%rbp)
0x0000000000001162 <+25>: xor %eax,%eax
0x0000000000001164 <+27>: movl $0x1,-0x14(%rbp)
0x000000000000116b <+34>: lea -0x14(%rbp),%rax
0x000000000000116f <+38>: mov %rax,-0x10(%rbp)
0x0000000000001173 <+42>: mov $0x0,%eax
0x0000000000001178 <+47>: mov -0x8(%rbp),%rdx
0x000000000000117c <+51>: xor %fs:0x28,%rdx
0x0000000000001185 <+60>: je 0x118c <main+67>
0x0000000000001187 <+62>: callq 0x1050 <__stack_chk_fail@plt>
0x000000000000118c <+67>: leaveq
0x000000000000118d <+68>: retq
End of assembler dump.
是的,没有看错,使用指针和引用得到了相同的汇编代码,
同样可以比较,函数里面用引用传参和指针传参,生成的汇编代码也是一样的。
// by pointer
void func1(int &a){}
int main() {
int b = 0;
int &a = b;
func1(a);
return 0;
}
// by reference
void func1(int *a){}
int main() {
int b = 0;
int *a = &b;
func1(a);
return 0;
}
因此,从汇编的角度来看,引用跟指针实际上就是同一个东西。引用和指针,更多的是编程语言语法方面的设计,也就是由编译器搞出来的概念,实际上它们最后生成的汇编代码是一样的。而只所以引入引用,据说是为了加运算符重载 (opens new window),没有引用的话,前自增<return type> operator ++(class T)
的语义就难以说明清楚。必须在声明引用的同时就要对它初始化,并且,引用一经声明,就不可以再和其它对象绑定在一起了,这和指针常量int * const p
有极大的相似之处。引用的一个优点是它一定不为空,因此相对于指针,它不用检查它所指对象是否为空,这增加了效率。
const int &a; // 常引用,不能通过引用改变原来变量的值,可以传入右值
int func(const int &a)
{
a += 10; //error
return 0;
}
int b = 100;
func(b); // OK
func(100); // OK
# 2.C++中的左值与右值
左值和右值是C++
中的基本概念,简单来说,左值就是一个表达式等号左边的部分,右值就是等号右边的部分。如:
int a = 10; // a 是左值,10是右值
int b = a; // b 是左值,a自动转为右值
如上,其实左值表示的是对象的引用,而右值正是被左值指向的对象。像变量名/数组下标/返回引用类型函数的返回值等都是左值,左值有对应确定的存储区域,因此可以取其地址。
而像字符串/数字/运算符或函数的运算结果都是右值,右值不需要在内存中存储,只是程序执行中的中间结果,无法寻址。
只有左值才能用在赋值表达式的左边,而右值必须和一个表达式的逻辑对应,因此只能存在于赋值表达式右边。像取地址符&
/自增运算符++
/自减运算符--
都需要左值作为其参数。
返回引用的函数的调用产生的是左值,返回值的函数调用产生的是右值
#include <cstdio> #include <cstring> void printStr(std::string &s) { printf("%s\n", s.c_str()); } std::string getValueString() { std::string s = "getValueString"; return s; } std::string s; std::string & getRefString() { s = "getRefString"; return s; } int main() { getValueString() += " <==> ";// error, getValueString() is lvalue printfStr(getValueString()); // error, getValueString() is lvalue getRefString() += " <==> ";// correct, getRefString() is rvalue printfStr(getValueString()); // correct, getValueString() is rvalue // getRefString <==> getRefString }
左值可以自动隐式转为右值,但右值无法隐式转为左值
int a = 10; int b = a; // a is converted implicitly to an rvalue
class Person
{
public:
Person(std::string_view name) : name_(name) {}
std::string_view getName() & { return name_;}
private:
std::string_view name_;
};
Person getBob()
{
return Person("Bob");
}
void main(void)
{
Person p("John");
std::cout << p.getName() << std::endl;
// function "Person::getName" (declared at line 31) cannot be called on an rvalue
std::cout << getBob().getName() << std::endl;
}
上面的函数中&
就是一个引用限定符,表明函数只能被左值变量调用,如上代码,getBob()返回的是一个右值调用了getName
函数,IDE
会立刻提示语法错误如上。
# 3.右值引用
以上介绍的引用即通常所说的引用,是指左值引用lvalue reference
,而2011年08月
份发布的C++11
中引入右值引用rvalue reference
。在定义左值引用时都是定义变量的引用,而不能定义一个指向临时数据的左值引用,这可以使用右值引用来实现。
int a = 1;
int &b = a; // lvalue reference, correct
int &b = 1; // error, lvalue reference can not refer to a temporary value;
int *a = &10; // error: lvalue required as unary ‘&’ operand
此时,可以通过&&
的方式定义指向临时值的右值引用:
int x = 1;
int &&b = 1;
int &&c = x; // 错误,x是一个左值
int &&d = b; // 错误,b是一个左值
将以下代码转成汇编对比的结果为:
// 一级指针
int main() {
int a = 10;
int *b = &a;
return 0;
}
// 右值引用
int main() {
int a = 10;
int &&b = 10;
return 0;
}
对比以上的代码可以看到,使用右值引用时,汇编代码几乎和一级指针相同,除了多出两行:
0x000000000000116b <+34>: mov $0xa,%eax
0x0000000000001170 <+39>: mov %eax,-0x18(%rbp)
右值引用相当于创建了临时指针-0x18
用来存放10
,在一级指针中使用的时a
的地址-0x1
给指针赋值,在右值引用中使用系统自动生成的变量地址-0x18
给p
赋值,因此右值引用实际上就是一级指针,只是在语言层面的语义区分,其底层实现仍然是借用指针。
了解了右值引用后,再看#2中printStr
函数,要想打印非返回类型为值的函数调用的结果,可以借用右值引用:
#include<cstdio>
#include<string>
std::string getStr() {
std::string s = "rvalue";
return s;
}
void printStr(std::string &&str) {
printf("%s", str.c_str());
}
int main() {
printStr(getStr()); // correct
return 0;
}
# 4.移动语义move函数
C++11
标准库在头文件utility
中,引入了std::move
函数,其目的是提高程序运行的效率,把以前一些需要“先拷贝,再删除源对象”的操作,转化为直接把源对象移动到目标位置,如STL
容器中的push_back
操作等。
template< class T >
constexpr std::remove_reference_t<T>&& move( T&& t ) noexcept;
从函数声明可以看出,move
返回的是std::remove_reference_t
类型的右值引用,std::move
通常用来表示一个对象有可能是从对象t
移动过去的,实现了对象资源的高效传递,避免了对象的销毁重建,其作用等同于将对象static_cast
类型转换为一个右值引用rvalue reference
。
如下,在std::string
类中,其第9
个构造函数是带noexcept
修饰的move constructor
,可以实现将资源从一个string
直接移动到另一个string
,减少资源的复制删除。
#include<cstdio>
#include<string>
#include<vector>
void printStr(std::string &str) {
printf("%s\n", str.c_str());
}
int main() {
std::string str = "this";
// move (9) string (string&& str) noexcept;
std::string s(std::move(str));
printStr(str);
printStr(s); // content str is moved to s, str will be empty
// std::string &&s = std::move(str);
// 通过move实现资源的移动,减少复制
std::vector<std::string> v;
v.push_back(std::move(s));
printStr(v[0]);
printStr(s); // s empty
}
通过实验对比,使用std::move
往vector
中push_back string
10000次,性能相差近4倍。
#include <cstdio>
#include <cstring>
#include <vector>
#include<opencv2/opencv.hpp>
int main()
{
std::vector<std::string> v;
// take 9.26ms
long st = cv::getTickCount();
for(int i = 0; i < 10000; i++)
{
std::string s = "this";
v.push_back(s);
}
long et = cv::getTickCount();
printf("%.5f\n", (float)(et - st) / cv::getTickFrequency());
v.clear();
// take 2.35ms
st = cv::getTickCount();
for(int i = 0; i < 10000; i++)
{
std::string s = "this";
v.push_back(std::move(s));
}
et = cv::getTickCount();
printf("%.5f\n", (float)(et - st) / cv::getTickFrequency());
}
# 参考文献
- 1.https://www.zhihu.com/question/34266997#:~:text=%E5%8F%98%E9%87%8F%E5%90%8D%E6%98%AF%E7%BB%99%E7%BC%96%E8%AF%91,%E4%B8%AA%E7%AC%A6%E5%8F%B7%E5%AF%B9%E5%BA%94%E4%B8%80%E4%B8%AA%E5%9C%B0%E5%9D%80%E3%80%82 (opens new window)
- 2.https://www.xianwaizhiyin.net/?p=2763 (opens new window)
- 4.https://learning.oreilly.com/library/view/c-in-a/059600298X/ (opens new window)
- 4.https://www.zhihu.com/question/26203703 (opens new window)
← C++中的内存管理 C++中的string →