☰
详解C++语言中的加法运算符与赋值运算符的用法
2026/10/26 4:09:45 网站建设 项目流程

前言


+和=是 C++ 里最常用的两个运算符,也是最容易被「想当然」的两个。加法涉及整数提升、算术转换、有符号溢出和浮点精度;赋值涉及右结合、返回值、自赋值、类型转换,以及拷贝赋值与移动赋值的区别。很多看似简单的表达式,实际行为由标准里几条不起眼的规则决定。


常见的误解包括:认为short + short的结果是short(实际会提升为int);认为有符号整数溢出会「回绕成负数」(实际是未定义行为,undefined behavior, UB);认为a += b与a = a + b完全等价(前者只求值一次左操作数,且转换规则略有不同);认为赋值表达式没有值(它返回左操作数的引用,所以可以链式赋值)。


本文分「加法」「赋值」「复合赋值」「重载」四部分讲清这些规则,重点标注哪些是标准保证、哪些是 UB、哪些依赖具体实现。


一、加法运算符


1.1 操作数转换


对内置类型,+执行「通常算术转换」(usual arithmetic conversions):



  1. 先做整数提升:bool、char、short等转换为int(若int能表示其全部值)。

  2. 若两个操作数类型不同,按等级和符号性转换成公共类型。常见规则是:有符号与无符号同等级时,转换成无符号;低等级转换成高等级。


因此short a = 1, b = 2; auto c = a + b;中c的类型是int,不是short。char + char也是int。这解释了为什么用auto接收两个short的和会得到int。


有符号与无符号混合时尤其危险:


// 适用于 C++17
#include <iostream>

int main() {
const int i = -1;
const unsigned u = 1;
if (i < u) {
std::cout << "less\n";
} else {
std::cout << "not less\n";
}
return 0;
}

按标准语义,i会转换成unsigned,-1变成该类型能表示的最大值,因此条件为假,输出 "not less"。编译器通常会对这种比较给出警告,不要忽略。


1.2 溢出


无符号整数的加法按模 2^N 回绕,这是标准定义的行为,不是 UB。有符号整数的加法溢出则是 UB,编译器可以假设它不会发生,并据此做优化。因此判断溢出不能等溢出后再检查,必须在运算前检查:


// 适用于 C++17
#include <limits>

bool addWouldOverflow(int a, int b) {
if (b > 0 && a > std::numeric_limits<int>::max() - b) {
return true;
}
if (b < 0 && a < std::numeric_limits<int>::min() - b) {
return true;
}
return false;
}

对浮点数,溢出得到无穷大(IEEE 754 语义),不是 UB,但业务上通常也要检查std::isfinite。浮点加法还不满足结合律,(a + b) + c与a + (b + c)可能得到不同结果,这是精度问题,不是错误。


1.3 字符串与指针


std::string重载了+,但要求至少一个操作数是std::string。"a" + "b"是两个const char*相加,是指针算术,结果无意义,甚至可能越界。正确写法是std::string("a") + "b"或std::string("a") + std::string("b")。


指针加法只允许「指针 + 整数」,且结果必须落在同一数组(含尾后位置)内;两个指针相加没有定义。指针相减只在同一数组内才有定义,结果是std::ptrdiff_t。


二、赋值运算符


2.1 右结合与返回值


赋值是右结合运算符,a = b = c解析为a = (b = c)。b = c的结果是b的引用,因此a得到c的值。内置类型的赋值返回左值引用,可以继续被赋值、取地址。


赋值还涉及「先求值哪边」的问题。C++17 起,简单赋值的右操作数先于左操作数求值,赋值副作用在两侧值计算之后。但像i = i++这样的表达式仍然不要写:自增的副作用与赋值的副作用之间的顺序并不足以让你安全依赖,实际项目中应直接避免。


2.2 拷贝赋值与移动赋值


自定义类型如果管理资源,需要定义拷贝赋值和移动赋值。拷贝赋值要处理自赋值、释放旧资源、复制新资源;移动赋值要偷取资源并把源置于可析构状态。更简单的做法是拷贝并交换:


// 适用于 C++17
#include <algorithm>
#include <cstddef>
#include <utility>

class Buffer {
public:
Buffer() = default;

Buffer(const Buffer& other)
: size_(other.size_),
data_(other.size_ == 0 ? nullptr : new char[other.size_]) {
if (data_ != nullptr) {
std::copy(other.data_, other.data_ + other.size_, data_);
}
}

Buffer(Buffer&& other) noexcept
: size_(other.size_), data_(other.data_) {
other.size_ = 0;
other.data_ = nullptr;
}

Buffer& operator=(Buffer other) noexcept {
swap(other);
return *this;
}

~Buffer() {
delete[] data_;
}

void swap(Buffer& other) noexcept {
std::swap(data_, other.data_);
std::swap(size_, other.size_);
}

private:
char* data_ = nullptr;
std::size_t size_ = 0;
};

按值接收参数,左值走拷贝、右值走移动;交换后临时对象析构,负责释放旧资源。自赋值时先复制一份再交换,也安全。


三、复合赋值


a += b大致等价于a = a + b,但有三点不同:



  1. 左操作数只求值一次。若左操作数是*p++这类有副作用的表达式,两者行为不同。

  2. 运算结果会隐式转换回左操作数的类型,可能发生窄化。

  3. 对自定义类型,+=是独立的重载,不一定由+和=组合而成。


// 适用于 C++17
int i = 10;
i += 2.7; // 先按 double 算出 12.7,再转换回 int,i 变为 12

严格说,i += 2.7先按double做加法得到 12.7,再转换回int得到 12。若写成i = i + 2.7,同样先得到double再转换,结果一致;但若左操作数类型是自定义类,两条路径可能调用不同的重载。


复合赋值通常返回*this的引用,因此(a += b) += c合法。内置类型也是如此。


四、重载加法与赋值


重载+时,推荐按值接收左操作数并复用+=:


// 适用于 C++17
class Money {
public:
explicit Money(long long cents) : cents_(cents) {}

Money& operator+=(const Money& rhs) {
cents_ += rhs.cents_;
return *this;
}

friend Money operator+(Money lhs, const Money& rhs) {
lhs += rhs;
return lhs;
}

long long cents() const { return cents_; }

private:
long long cents_;
};

operator=作为成员函数必须返回*this的引用,才能支持链式赋值;参数用const Money&或按值。不要把operator=声明为friend之外的自由函数——赋值运算符必须是成员函数(C++ 规定)。


重载+不改变优先级和结合性,也不改变内置类型的语义。不要重载&&、||去模拟短路,那会失去短路求值;也不要重载,,除非有非常明确的理由。


常见坑点



  1. ❌ 认为short + short得到short。


// 适用于 C++17
short a = 30000, b = 30000;
short c = a + b; // 先按 int 相加,再窄化回 short,结果实现定义

✅ 用int或更宽类型接收。


// 适用于 C++17
int c = a + b;


  1. ❌ 依赖有符号溢出回绕。


// 适用于 C++17
int x = INT_MAX;
int y = x + 1; // UB,不能假设得到 INT_MIN

✅ 运算前检查范围,或改用无符号类型并明确模运算语义。


// 适用于 C++17
if (x > INT_MAX - 1) { /* 处理溢出 */ }


  1. ❌ 混合有符号与无符号比较。


// 适用于 C++17
int i = -1;
unsigned u = 1;
if (i < u) { /* 实际为假 */ }

✅ 统一类型,或显式转换并确认值域。


// 适用于 C++17
if (i < static_cast<int>(u)) { /* ... */ }


  1. ❌ 两个字符串字面量相加。


// 适用于 C++17
auto s = "hello" + "world"; // 两个 const char* 相加,不是拼接

✅ 至少一个操作数是std::string。


// 适用于 C++17
std::string s = std::string("hello") + "world";


  1. ❌operator+返回引用。


// 适用于 C++17
Money& operator+(const Money& a, const Money& b); // 返回局部对象,悬垂

✅ 按值返回。


// 适用于 C++17
Money operator+(Money lhs, const Money& rhs);


  1. ❌operator=不返回引用。


// 适用于 C++17
void operator=(const Money& rhs); // 无法链式赋值,也不符合惯例

✅ 返回*this。


// 适用于 C++17
Money& operator=(const Money& rhs) { /* ... */ return *this; }


  1. ❌ 自赋值时先释放再复制。


// 适用于 C++17
Buffer& operator=(const Buffer& rhs) {
delete[] data_;
data_ = new char[rhs.size_]; // rhs 就是自己时,数据已被释放
// ...
}

✅ 先复制再释放,或用拷贝并交换。


// 适用于 C++17
Buffer& operator=(Buffer other) { swap(other); return *this; }


  1. ❌ 认为a += b与a = a + b对所有类型完全等价。


// 适用于 C++17
// 自定义类型可能只重载了 +=,没有 +;或两者语义不同

✅ 以类型提供的运算符为准,不假设两者可以互相替换。


总结




主题关键规则



整数提升char/short参与运算先转int

混合符号有符号可能转成无符号,比较结果可能反直觉

有符号溢出UB,必须运算前检查

无符号溢出模 2^N 回绕,标准定义

赋值右结合,返回左值引用

复合赋值左操作数只求值一次,结果转回左类型

重载+按值返回,复用+=

重载=必须是成员,返回*this,处理自赋值



加法与赋值的语法都很短,但背后的类型转换和求值顺序规则很长。写表达式时尽量让操作数类型一致、让副作用单独成句,大多数坑就不会出现。




需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询