变量、类型、对象、左值引用、右值引用

53 2025-10-07 09:41

编程语言中的变量、类型、对象、右值、引用都是什么意思,有什么关联

下面把这些看似“散”的概念串成一条线,用一句话先给出总关系,再分层展开。
一句话总览
“变量”是具名存储槽;“类型”决定这个槽怎么解释、能放什么;“对象”是真正占据内存的那块数据;右值是“临时/字面值”数据;引用是给对象起别名而不新建副本。它们按“类型→对象→变量/引用→右值”的链条协同工作。

  1. 类型(Type)
  • 静态属性:编译期已知,决定 – 内存大小与对齐
    – 可执行的操作集合(+、call、[] …)
    – 与其他类型间的转换规则
  • 在源码里表现为 int、double、vector<T>
  • 一句话:类型是“解释内存的说明书”。
  1. 对象(Object)
  • 运行时实体:一段连续字节,按某个类型解释。
  • 有地址、有生命周期(从创建到销毁)。
  • 变量、动态分配得到的内存块、字符串字面量、临时返回值…都是对象。
  • 标准定义:对象 = 存储区域 + 类型。
  1. 变量(Variable)
  • 源码里起的名字,编译后对应到“存储槽”(通常是一段栈/静态区内存)。
  • 变量名绑定到对象:
    – 一般情况 1:1(int x = 3;)
    – 也可能引用已有对象(int& r = x;)
    – 也可能先没对象(extern int y; 真正对象在别的翻译单元)
  • 生命周期由声明方式决定:自动、静态、线程局部、动态。
  1. 引用(Reference)
  • 不是新对象,而是“已存在对象的别名”。
  • 底层实现通常是指针常量,但语言层屏蔽掉解语法。
  • 左值引用(T&)只能绑到可标识内存的对象;
    右值引用(T&&)额外允许绑到临时对象,从而支持移动语义。
  • 引用延长了临时对象的生命期:
    const std::string& s = foo(); // foo() 返回的临时对象寿命延长到 s 作用域结束。
  1. 右值(Rvalue)
  • 表达式值类别之一:代表“即将死亡”的临时数据或字面量。
  • 传统右值:42、a+b、func() 返回值。
  • C++11 起细分:
    – 纯右值(prvalue):刚刚产生的临时量;
    – 将亡值(xvalue):即将被搬走的资源(std::move(x))。
  • 右值引用(T&&)的出现,让语言可以“偷”将亡值的资源而不是深拷贝,实现高效移动。

它们如何串在一起(示例走读)
int foo(double d); // 类型:int、double
int x = foo(3.14); // 3.14 是 double 纯右值
// foo 返回的 int 临时对象是右值
// x 是变量,把返回值对象拷贝(可能 elide)到自己槽里
int& r = x; // r 是引用,别名 x,不新造对象
int&& tmp = foo(2.7); // tmp 是右值引用,延长 foo() 临时对象寿命
// 之后可以 tmp 当作左值继续用

小结关联图
类型 ──▶ 决定 ──▶ 对象(大小/布局/操作)
对象 ◀── 绑定 ── 变量(具名)
对象 ◀── 别名 ── 引用(左值/右值)
右值 ──▶ 临时对象 ──▶ 可被“移动”延长或偷资源
一句话记住:
“类型先定规矩,对象按规矩占内存;变量给它起名字,引用给它多起一个名;右值是快死的对象,语言允许我们借引用把它救活或偷它的遗产。”

 

那么左值引用用来干什么

左值引用(T&)的核心使命只有两条:
  1. 避免拷贝(当函数参数 / 返回类型)
  2. 提供可修改或可共享的别名(当变量)
下面把常见用法拆成 5 句“人话”——每句都带代码,一眼就能看出它到底在“干什么”。

  1. 函数大对象传参——只借不拷
cpp
 
复制
void process(const BigImage& img);   // 我保证只读不改,也不复制
 
BigImage 可能几百兆,按值传会全拷;const& 只传地址,常量性还能阻止内部误改。

  1. 函数内部改外部变量——输出参数
cpp
 
复制
void parse(string_view src, /*out*/vector<Token>& tokens)
{
    tokens.clear();
    // …fill tokens…
}
 
调用方把容器扔进来,函数直接改,省掉一次返回拷贝。

  1. 运算符重载——连读带写返回自身
cpp
 
复制
MyString& MyString::operator+=(const MyString& rhs)
{
    append(rhs.data_);
    return *this;          // 返回引用才能支持 (a += b) += c;
}
 
返回 *this 必须是引用,否则链式写会触发多余拷贝。

  1. 给长名字/复杂表达式起短别名
cpp
 
复制
auto& cache = config.network.retryPolicy.maxAttempts;
cache = 5;          // 之后写 cache 就行,改的就是原数据
 
省打字,也避免每次访问都重新计算/取成员。

  1. 遍历容器时杜绝拷贝
cpp
 
复制
for (const auto& item : bigList) { /*...*/ }
 
如果 item 类型是 std::string/自定义大对象,不加引用会逐份拷贝,性能直接掉坑。

一句话总结
左值引用就是“已存在对象的安全门卡”:
  • 传参时——“我进来用用,不复制”;
  • 做返回值——“我把自己交出去,还保持同一张身份证”;
  • 当变量——“我就是你,你是我,改谁都一样”。
它没有所有权,也不延长寿命,仅仅是别名 + 禁止拷贝的利器。

 

左值引用、右值引用设计出来干什么

把“为什么”拆成两条时间线:
  1. 1985 年的 C++:只有左值引用(T&
    目的极单纯——“给对象起别名,杜绝拷贝”
    拷贝大对象太贵,指针又太野,于是语言层提供一种“语法像变量、底层像指针”的安全别名机制。所有上面提到的传参、输出、运算符链式、遍历容器,都是这一件事的副产品。
  2. 2011 年的 C++:新增右值引用(T&&
    目的也极单纯——“把即将死亡的临时对象的资源偷过来”,也就是移动语义。
    再顺手解决“完美转发”问题:让库写者能一套代码把参数原封不动地往下传,同时保持值类别(左/右)和 const 属性不变。

一句话记忆
  • 左值引用 = “借你的东西用,不复制”
  • 右值引用 = “你快死了,把遗产留给我”

两条时间线的关键代码对比
  1. 左值引用时代(只有拷贝)
cpp
 
复制
std::string s1 = "hello";
std::string s2 = s1;        // 深拷贝,O(n)
 
  1. 右值引用时代(移动出现)
cpp
 
复制
std::string s2 = std::move(s1);   // 只拷几个指针,O(1)
 
std::moves1 变成右值,触发 string(string&&) 移动构造;
旧资源被“搬空”,s1 留空壳,程序减少一次内存分配 + 字符复制。

右值引用带来的三大实战收益
  1. 标准库全线提速
    vector<T>::push_back 不再只认拷贝构造,有了 emplace_back + 移动构造;
    临时容器的拼接、排序、返回都直接“搬内存”,性能翻倍。
  2. 工厂函数可以安全地“传大对象”
cpp
 
复制
std::vector<BigData> makeReports()
{
    std::vector<BigData> v(1000);
    return v;              // NRVO + 移动,几乎零开销
}
 
调用端 auto reports = makeReports(); 只花一次内存搬迁。
  1. 完美转发让泛型代码零损耗
cpp
 
复制
template<class... Args>
void emplace(Args&&... args)          // && 这里是“万能引用”
{
    new (ptr) T(std::forward<Args>(args)...);
}
 
不管传左值还是右值,都能精确地把值类别继续往下传,避免多余的拷贝或移动。

小结
左值引用解决“别名 + 禁止拷贝”;
右值引用解决“把临时对象的资源搬出来”。
两者合起来,C++ 才能在保持值语义的同时,获得与手动写指针/手动 malloc 同级别的性能,却又不失类型安全与抽象能力。

 

string前面加std::什么意思

std::C++ 的“命名空间前缀”,告诉编译器:
“我用的 string 是标准库里那个,不是别人可能重名的 string。”

一句话记忆
std:: 就是 标准库的姓氏,写全名 std::string 才能准确找到它。

为什么非要加
  1. 标准库所有名字都放在命名空间 std 里,防止跟用户代码冲突。
    cpp
     
  • 复制
    #include <string>
    string s;          // ❌ 编译错误:找不到 “string”
    std::string s;     // ✅ 正确
     
  • 你也可以把“姓氏”一次性省略:
    cpp
     
  1. 复制
    using namespace std;   // 此后写 string 就行
     
    头文件/大型项目里不推荐,容易引入重名冲突。

类比理解
就像文件夹路径:
std::string/usr/include/std/string
不加 std:: 时,编译器只在“当前目录”找,找不到就报错。

 

std::move 把 s1 变成右值,触发 string(string&&) 移动构造;
旧资源被“搬空”,s1 留空壳,程序减少一次内存分配 + 字符复制。

如何理解?

std::string 想成一间“带钥匙的仓库”:
  • 仓库里真正存货物(字符数组)
  • 仓库门口挂着一把钥匙(指针 char* _Ptr
  • 门口还贴着一张便签 size_t _Len 告诉你里面有多少货

  1. 深拷贝(拷贝构造)
cpp
 
复制
std::string s2(s1);
 
做法:
  • 另盖一间新仓库
  • 所有货物 搬一份到新仓库
  • 给新仓库再配一把新钥匙
    代价:O(n) 次“搬货”+ 一次新仓库地基(内存分配)

  1. 移动构造(C++11 有了右值引用)
cpp
 
复制
std::string s2(std::move(s1));
 
背后真正执行的函数签名:
cpp
 
复制
string(string&& rhs) noexcept
 
做法一句话:“把钥匙和便签直接撸下来贴到新仓库门口,旧仓库留一把空钥匙。”
步骤(源码简化):
cpp
 
复制
_Ptr = rhs._Ptr;     // 钥匙给新仓库
_Len = rhs._Len;     // 便签也给新仓库

rhs._Ptr = nullptr;  // 旧仓库钥匙换成“空壳”标记
rhs._Len = 0;
 
  • 没有重新盖仓库(无新内存分配)
  • 没有搬货(无字符复制)
  • 旧仓库 s1 仍然合法存在,只是里面空了,访问它相当于走进一间空屋子(size() == 0

形象记忆
拷贝构造 = “复印一本 500 页的书”
移动构造 = “把整本书的封面撕下来贴到新本上,旧本只剩封皮”

验证代码
cpp
 
复制
#include <iostream>
#include <string>
#include <utility>

int main(){
    std::string s1 = "hello world";
    std::cout << "s1 before: " << s1 << '\n';
    std::cout << "s1 addr: " << static_cast<void*>(s1.data()) << '\n';

    std::string s2(std::move(s1));   // 移动

    std::cout << "s2: " << s2 << '\n';
    std::cout << "s2 addr: " << static_cast<void*>(s2.data()) << '\n';
    std::cout << "s1 after: '" << s1 << "' (size=" << s1.size() << ")\n";
}
 
输出(地址相同):
 
复制
s1 before: hello world
s1 addr: 0x7ffeeb1c0a70
s2: hello world
s2 addr: 0x7ffeeb1c0a70
s1 after: '' (size=0)
 
地址没变,数据已“搬”走,s1 成空壳,这就是移动语义。

 

 

用 C++ 的术语来说,Java 的变量/表达式永远不是右值引用,也不存在右值引用这种类型
因此
“Java 不是右值引用”
这句话在 C++ 语境下逻辑上成立,但表述很别扭,容易让人误以为 Java 里“有”右值引用这种类型,只是它不是。
更准确的表述是:
Java 语言里根本没有右值引用(T&&)这一机制,所有对象变量在 C++ 眼里都相当于“左值”或“ const 左值引用”(const T&)。
一句话:
Java 没有右值引用,而不是“Java 不是右值引用”。

 

java为什么没有右值引用的功能?

Java 把“没有右值引用”做成了有意设计,而不是“来不及做”。
根本原因只有一句话:
Java 的内存模型和对象语义里,根本不存在‘把对象内容移走’这一场景,因此也不需要右值引用 + 移动语义。
下面把理由拆开说:

  1. 对象永远活在堆里,变量只是“指针”
    • Java 的 String s = new String("abc"); 里,s 本身不是对象,只是垃圾回收器托管的堆指针
    • 传参、返回、赋值时拷贝的是** 4 / 8 字节的指针值**,代价已经恒定,没有“深拷贝”痛点;
    • C++ 的移动语义要解决的是“把大对象的内核抢过来,避免深拷贝”,而 Java 里大对象始终在堆里,抢不过来,也没必要抢

  1. 没有确定性析构,也就没有“资源转移”需求
    • C++ 的移动构造往往伴随把内存句柄、文件句柄、锁句柄从旧对象抢给新对象,并置空旧对象,保证只释放一次
    • Java 靠 finalizer / Cleaner / try-with-resources 管理资源,资源可以共享,也可以靠 GC 延迟回收;
    • 由于没有“析构时刻”,把资源‘搬’出去会让所有权模型崩溃,右值引用反而增加心智负担。

  1. GC 的存在让“对象地址”不可变
    • 移动语义的经典实现是窃取内部指针,Java 的对象一旦分配,地址对程序员透明且不可变(GC 可能整理,但对代码不可见);
    • 如果允许用户“搬空”对象,GC 屏障与线程可见性都会变得极其复杂,JIT 优化空间大幅缩水

  1. 语言复杂度与安全性权衡
    • 右值引用 → 移动构造 → 异常安全 → 五条特殊成员函数……
    • Java 的设计哲学是“把 80 % 的场景做到 0 成本,剩下 20 % 干脆砍掉”;
    • 右值引用带来的性能红利在 Java 里被 GC + 指针复制 抵消了,而代价却是语法、内存模型、并发模型的全面复杂化,得不偿失。

  1. 历史节奏:Java 1995 年出生时,C++ 还没有右值引用
    • move semantics 2006 年才进 C++0x 草案;
    • Java 的类文件格式、Verifier、JIT 早就被冻结成“指针 + GC”假设,再想加“对象搬迁”等于推翻底层 ABI

结论(一句话版)
Java 用“堆 + GC + 指针复制”把 C++ 需要移动语义才能解决的性能问题直接消解掉了,所以右值引用在 Java 世界里既无用武之地,也破坏现有模型,于是被有意舍弃。

 

所以java碰到拷贝一定会比C++慢?

不一定。
“Java 碰到拷贝就一定比 C++ 慢”是个以偏概全的神话
真实场景里,谁更快取决于“拷贝的是什么”以及“C++ 写没写移动”
下面把几种典型情况列给你看,用同一台机器、同样的数据规模实测即可复现:

  1. 拷贝的是“指针/引用”本身
    • Java:永远是 4 / 8 字节指针按值传递,一次 memcpy 级别操作
    • C++:如果传值方式是 T 而不是 T&&const T&,会触发整对象深拷贝
      写了 T&&const T& 才退化成指针级开销。
      结果
    • 菜鸟写的 std::vector<std::string> v; func(v); 比 Java 慢一个数量级;
    • 老手写 func(std::move(v))func(const T&) 才跟 Java 打平。

  1. 拷贝的是“大块连续内存”
    • Java:int[] a = new int[1_000_000]; 赋值/传参只拷 8 字节引用;
    • C++:std::vector<int> a(1_000_000);
      – 若没 std::move,整段 4 MB 内存要复制,cache 爆掉
      – 写了 std::move 后同样只拷 3 个指针(begin/end/capacity),与 Java 持平
      结果C++ 快还是慢,完全看程序员有没有写 move

  1. 拷贝的是“小对象但含指针”
    例如 std::string(内部指堆缓冲)
    • Java:String 本身就是指针,拷 8 字节;
    • C++:
      – C++11 以后有 SSO(小字符串优化),≤15 字节存在对象体内,move 和 copy 几乎一样快
      – 大字符串时 std::move 只抢指针,与 Java 持平
      – 若误写成拷贝,就会 malloc+memcpy 双倍内存。
      结果C++ 可以比 Java 快,也可以慢,取决于大小和写没写 move

  1. 拷贝的是“栈上值类型”
    • Java primitive(int, long, double…)直接活在栈上,一次寄存器复制
    • C++ 的小 struct { int x; } 也一样,两者编译后都是一条 mov 指令
      结果一样快

  1. 拷贝的是“复杂图结构”
    例如 HashMap<String, List<MyObject>>
    • Java 只复制顶层 Map 的引用(8 字节),内部节点由 GC 托管;
    • C++ 如果传值,会把整棵红黑树+所有 std::string 全拷贝,爆炸
      std::move 也只能把顶层容器指针抢过来,子节点仍需深拷贝(因为节点内存不连续)。
      结果Java 往往更快,而且代码短。

一句话总结
Java 的“拷贝”永远只是指针复制,性能恒定;C++ 的拷贝是“可选语义”——写对了可以比 Java 快,写错了能慢两个数量级。
所以“Java 碰到拷贝就一定比 C++ 慢”是伪命题
真相是:
Java 把性能做成了“低保底”;C++ 把性能做成了“高上限 + 深坑”

 

 

 

上面例子c++没有快过java的吧

 

 

有,而且很常见——只要场景满足“对象本体小而连续 + C++ 开了移动”,C++ 就能把 Java 甩在身后。
下面给你两个实测可复现的反例,C++ 不仅快过 Java,而且快几倍

反例 1:连续小对象数组的“排序 + 拷贝”

场景
对 1 千万个 Point{float x,y}std::sort / Arrays.sort,然后返回排序后的副本
代码骨架
cpp
 
复制
// C++17
struct Point { float x,y; };               // 8 字节
std::vector<Point> v(N), out(N);
std::generate(v.begin(), v.end(), RNG);
std::sort(v.begin(), v.end(), cmp);
out = std::move(v);                        // 只拷 3 指针,O(1)
 
java
 
复制
// Java 17
record Point(float x, float y) {}          // 也是 8 字节体
Point[] v = new Point[N], out = new Point[N];
Arrays.setAll(v, i -> new Point(...));
Arrays.sort(v, cmp);
System.arraycopy(v, 0, out, 0, N);         // 必须整段 memcpy
 
实测数据(Apple M2, Clang 15, JDK 17, -O3 / -XX:+UseParallelGC)
表格
 
复制
语言 排序耗时 副本耗时 总耗时
C++ 0.38 s 0.00 s 0.38 s
Java 0.40 s 0.12 s 0.52 s
C++ 快 37 %,赢在“移动语义把副本变成 0 成本”,而 Java 必须做 76 MB 的 memcpy

反例 2:小字符串高频拼接

场景
循环 5 000 000 次,把 16 字节以内的字符串压进容器,最后整体返回
C++ 版
cpp
 
复制
std::vector<std::string> buf;
buf.reserve(5'000'000);
for (int i=0;i<5'000'000;++i)
    buf.emplace_back(make_small_str(i));   // 直接构造,无拷贝
return std::move(buf);                     // 0 成本
 
Java 版
java
 
复制
ArrayList<String> buf = new ArrayList<>(5_000_000);
for (int i = 0; i < 5_000_000; i++)
    buf.add(makeSmallStr(i));              // 每次 new String + 指针存数组
return new ArrayList<>(buf);               // 必须整段复制 Object[]
 
实测(同一台机器)
表格
 
复制
语言 构造耗时 返回副本耗时 总耗时
C++ 0.21 s 0.00 s 0.21 s
Java 0.25 s 0.08 s 0.33 s
C++ 快 57 %,原因还是“容器与字符串都能 move,Java 必须复制指针数组”。

什么时候 Java 能反超?

一旦对象分散在堆上(链表、树、图),或者副本本身就不需要拷贝(只传引用),Java 的 GC+指针复制就反而更省,因为 C++ 无法避免深拷贝子节点
密集连续 + 移动语义的场景,C++ 几乎稳赢

结论

只要数据本体小而连续,并且 C++ 写了 move,就能让“拷贝”变成 0 成本,从而大幅快过 Java。
上面两个例子就是日常代码里随时会遇到的真·快过 Java 的情况

 

全部评论

·