当前位置: 首页 > news >正文

Modern C++(三)表达式

3、表达式

表达式:运算符和操作数的序列,指定一项计算。它可以是一个简单的常量、变量,也可以是复杂的函数调用、算术运算等。

表达式可以被分为以下几种:

  • 初等表达式:初等表达式是表达式的最基本形式,是组成更复杂表达式的基础单元。常见的初等表达式有:this、字面量(2或"hello"),标识表达式(变量名n)、lambda表达式、任何带括号的表达式、函数调用
  • 完整表达式:一个独立的表达式,它不是其他表达式的一部分,完整表达式的求值结果通常会被使用或产生副作用。完整表达式的求值完成后,程序会继续执行下一条语句。
    • 赋值语句:x = 10;
    • 函数调用:printf(“Hello”);
    • 条件语句:if (x > 0) { … }
  • 潜在求值表达式:潜在求值表达式是指可能会被求值的表达式,具体取决于程序的执行路径。潜在求值表达式的求值取决于上下文,可能不会实际发生。
    • 条件表达式:x > 0 ? a : b 中,a 或 b 会根据 x > 0 的结果被求值。
    • 逻辑运算符:x && y 中,如果 x 为假,y 不会被求值。
  • 弃值表达式:弃值表达式通常用于执行某些操作,而不关心其结果。

要注意 int x = 10; 算声明表达式,是一种特殊的表达式,不属于以上几种。

对表达式的求值可以产生一个结果,也可能产生一些副作用(表达式在求值过程中,除了返回一个结果之外,还会对程序的状态产生其他可观察到的改变,这些改变可以包括修改全局变量、改变文件内容、在屏幕上输出信息等)

每个C++表达式均被描述为具有两种独立的性质:类型(int、double、自定义类类型等)和值类别。值类别决定了表达式的值的一些特性,比如该值是否可以被移动、是否可以被赋值等。

3.1、值类别

C++ 中的表达式根据其求值结果的特性,被划分为三种基本值类别:纯右值 (prvalue)、亡值 (xvalue)、左值 (lvalue)。

3.1.1、左值

左值是具有持久对象身份的表达式,可以通过明确的身份被引用,通常是一个具名的变量或对象,可以被赋值、取地址

int x = 10; // x 是左值
int* p = &x; // 可以取地址

3.1.2、右值

一般来说,右值是那些不具有持久存储位置、通常是临时的对象或值。右值不能出现在赋值运算符的左边,因为它们代表的对象或值没有稳定的内存地址供我们去修改其内容。右值可以进一步细分为纯右值(prvalue,Pure rvalue)和亡值(xvalue)。

3.1.2.1、纯右值

纯右值通常是字面量、不代表对象的表达式结果。它们不具有标识符,生命周期通常很短暂,在表达式结束后就会被销毁。纯右值不能取地址,也不能被赋值,通常用于初始化或赋值。纯右值可以理解为是完全没有容器的临时值,有以下几种:

  • 字面量
int y = 42; // 42 是纯右值
  • 表达式结果(无需创建临时对象)
int z = x + y; // x + y 的结果是纯右值,计算完结果临时存在,且无需创建对象
  • 函数返回非引用类型,且返回类型是内置类型(如 int、double)
int f() { return 1; } //函数返回非引用类型,int是内置类型
3.1.2.2、亡值

亡值是右值的一种,专门用来优化临时对象的资源管理。亡值可以理解为有“临时容器”的右值,但这个容器即将被销毁,可以“偷”走它的资源。

问题是亡值的“临时容器”到底从哪来?它长什么样?

所谓临时容器就是临时对象,它是C++中没有名字、自动创建又自动销毁的对象。它有两个特点:

  • 实实在在的对象(有内存地址、有成员变量),没有名字(匿名)
  • 生命周期很短:通常在表达式结束后就会被销毁

亡值就是临时对象的引用。临时对象产生场景:

  • 直接创建临时对象:
std::string("hello"); // 直接用构造函数创建临时对象

代码会调用std::string的构造函数,在内存中创建一个匿名的std::string对象,里面存储字符串 “hello”。这个匿名对象没有名字,用完后马上会被销毁,它的存在就是一个 “临时容器”,所以它本身(以及对它的引用)就是亡值。

  • 函数返回非引用类型的对象
std::vector<int> f() {return {1, 2, 3}; // 返回一个临时 vector 对象
}std::vector<int> vec = f(); // 用 vec 接收 f() 的返回值

函数f()执行时,会在栈上创建一个临时的std::vector对象(存储 {1,2,3}),作为返回值。这个临时对象在f()调用结束后会被销毁,所以f()的返回值是亡值,允许通过移动语义“偷走” 它的内存资源(比如vec直接接管它的数组指针,避免拷贝)。

  • 使用 std::move转换左值
std::string str = "abc"; // str 是左值(有名字的容器)
std::string other = std::move(str); // 将 str 转为亡值

std::move(str) 并不会创建新对象,而是将已有对象str标记为 “即将被销毁”,此时 str不再被视为左值,而是一个即将被释放的临时容器。str在std::move之后,变成了一个 “被遗弃的容器”,other可以直接拿走它的资源,而不用拷贝数据。

int a = 1;
int b = std::move(a); // std::move(a) 是亡值

std::move(a) 的值类别是亡值,但对于 int 这种内置类型,亡值没有特殊意义。内置类型(如 int、double)没有资源可以 “移动”,只有值的拷贝没有资源转移。

亡值主要是用于触发移动语义,但是实际上并不一定会调用移动构造函数。移动构造函数只有在以下情况下会被调用:

  • 用亡值初始化对象时,且对象的类型定义了移动构造函数。
  • 显式使用std::move并将结果用于初始化或赋值
  • 如果编译器做了返回值优化,编译器可能会直接省略拷贝或移动,直接在目标位置构造对象,这时候不会调用移动构造函数。

接下来对比几段代码:

class MyClass {
public:// 默认构造函数MyClass() : data(new int[100]) {std::cout << "Default constructor" << std::endl;}// 拷贝构造函数MyClass(const MyClass& other) : data(new int[100]) {for (int i = 0; i < 100; ++i) {data[i] = other.data[i];}std::cout << "Copy constructor" << std::endl;}// 移动构造函数MyClass(MyClass&& other) noexcept : data(other.data) {other.data = nullptr;std::cout << "Move constructor" << std::endl;}// 析构函数~MyClass() {delete[] data;std::cout << "Destructor" << std::endl;}
private:int* data;
};// 第一段
MyClass getMyClassXValue() {// 栈上创建 objMyClass obj;// std::move(obj) 将 obj 转换为亡值,触发移动构造函数// 函数返回时,obj 被销毁(但 data 已为空,无害)。return std::move(obj);
}
int main() {// 右值引用绑定临时对象MyClass &&myObj = getMyClassXValue(); cout << "------" << endl;return 0;
}
// Default constructor
// Move constructor
// Destructor
// ------
// Destructor

这个例子中函数getMyClassXValue返回类型是MyClas(值类型),函数必须返回一个完整的对象(而非引用),return std::move(obj) 触发移动构造函数,创建一个临时对象。

// 第二段
MyClass&& getMyClassXValue() {MyClass obj;// 使用 std::move 将 obj 转换为亡值// 返回右值引用return std::move(obj);
} // 函数退出 obj 被销毁int main() {//myObj 绑定到已销毁的对象MyClass &&myObj = getMyClassXValue();cout << "------" << endl;return 0;
}
// Default constructor
// Destructor
// ------

getMyClassXValue返回局部变量obj的右值引用,并没有创建临时变量!函数调用完成obj会被销毁,myObj绑定的是被销毁的对象,所以出现悬空。

// 第三段:crash
MyClass&& getMyClassXValue() {MyClass obj;// 使用 std::move 将 obj 转换为亡值// 返回右值引用return std::move(obj);
} // 函数退出 obj 被销毁int main() {// 尝试拷贝/移动已销毁的对象MyClass myObj = getMyClassXValue(); return 0;
}
// Default constructor
// Destructor
// Move constructor
// crash

getMyClassXValue返回一个右值引用,不过离开getMyClassXValue作用域,obj被销毁
。myObj的创建调用了移动构造,obj被销毁,调用移动构造函数会拷贝这个无效指针(nullptr或随机值)。退出程序时,data会被释放,释放无效指针造成crash。

3.2、求值顺序

C++中无从左到右或从右到左求值的概念。表达式 a() + b() + c() 由于 operator+ 的从左到右结合性被分析成 (a() + b()) + c(),但在运行时可以首先、最后或者在 a() 和 b() 之间对 c() 求值:

#include <cstdio>int a() { return std::puts("a"); }
int b() { return std::puts("b"); }
int c() { return std::puts("c"); }void z(int, int, int) {}int main()
{z(a(), b(), c());       // 允许全部 6 种输出排列return a() + b() + c(); // 允许全部 6 种输出排列
}
/*
c
b
a
a
b
c
*/

3.3、常量表达式

能在编译期求值的表达式是常量表达式,这种表达式能用作非类型模板实参、数组大小,并用于其他要求常量表达式的语境,例如:

const int a = 10;
int arr[a] = {};

3.4、赋值运算符

赋值运算符用于修改对象的值,赋值运算符有(+、+=、-=、*=等等),这些运算符都可以重载并用于类型计算。运算符重载有两种形式:

  • 类内定义:T& T::operator =(const T2& b);
  • 类外定义:T& operator +=(T& a, const T2& b);

所有的内建赋值运算符都返回 *this,我们自己定义的重载一般也是返回 *this。

赋值运算可以细分为以下几类:

  • 复制赋值:以b内容的副本替换对象a的内容(不修改b),对于类类型,这会在特殊成员函数中进行
  • 移动赋值:以b的内容替换对象a的内容,并尽可能避免复制(可以修改b),对于类类型,这会在特殊成员函数中进行
  • 直接赋值:对于非类类型,对复制与移动赋值不加以区分,它们都被称作直接赋值
  • 复合赋值:以a的值和b的值间的二元运算结果替换对象a的内容

3.5、算术运算符

算术运算符计算特定算术运算的结果,并返回它的结果。不修改实参。

T T::operator+(const T2 &b) const;
T operator+(const T &a, const T2 &b);

3.6、成员访问运算符

该运算符用于访问操作数的一个成员,包括如下几个:

  • 下标:可重载
class MyArray {
public:int& operator[](int index) {return data[index];}
private:int data[5];
};MyArray arr;
arr[3] = 10;
  • 间接寻址:可重载

自定义类型中的间接寻址运算符重载一般用于智能指针或迭代器

template<typename T>
class MyPtr {
public:MyPtr(T* val) {ptr = val;}T& operator*() {return *ptr;}
private:T* ptr;
};int a = 10;
MyPtr<int> myptr = &a;
cout << *myptr << endl;
*myptr = 20;
cout << *myptr << endl;
  • 取地址:可重载
class Object {
public:uint64_t* operator&() { return &virtual_address; } // 自定义地址返回
private:uint64_t virtual_address;
};
Object obj;
uint64_t* addr = &obj; // 调用重载的 & 运算符
  • 对象的成员:a.b 不可重载

  • 指针的成员:可重载

template<typename T>
class MyPtr {
public:MyPtr(T* val) {ptr = val;}T& operator*() {return *ptr;}T* operator->() {return ptr;}
private:T* ptr;
};

重载之后经常看到这样的用法:

class MyClass {
public:void sayHello() { cout << "hello" << endl; }
};MyClass obj;
MyPtr<MyClass> pobj(&obj);
pobj->sayHello();

这里是先调用重载的运算符->获取指针,再用内置->访问成员

  • 指向对象的成员的指针:a.*b 无法重载

  • 指向指针的成员的指针:a->*b 可重载

3.7、new 表达式

创建并初始化拥有动态存储期的对象,有四种创建方式:

  • ::(可选) new (类型) new初始化器 (可选)
class MyClass {
public:void sayHello() { cout << "hello" << endl; }
};MyClass *p = ::new (MyClass)();
  • ::(可选) new 类型 new初始化器 (可选)
MyClass *p = new MyClass {};
  • ::(可选) new (布置实参) (类型) new初始化器 (可选)
MyClass *p = ::new MyClass{};
cout << p << endl;
p->~MyClass();MyClass *p2 = new (p) MyClass();
cout << p2 << endl;
  • ::(可选) new (布置实参) 类型 new初始化器 (可选)

new 表达式通过调用适当的分配函数分配存储。如果类型不是数组类型,那么函数名是operator new。如果类型是数组类型,那么函数名是operator new[]。

我们常见的operator new签名有如下几种:

// 标准分配
void* operator new(size_t);
// Placement new
void* operator new(size_t, void*);
// 自定义扩展
void* operator new(size_t, args...)

operator new的第一个参数size_t size不需要手动填写,它是由编译器自动计算并传入的。当使用 new T或new T[N]时,编译器会自动计算sizeof(T)(或sizeof(T) * N 对于数组)。如果是类类型且有虚函数、继承等,可能会包含额外的内存开销(如虚表指针)。如果用 new 分配 char、unsigned char 或 std::byte (C++17 起)的数组,那么它可能从分配函数请求额外内存,以此保证所有不大于请求数组大小的类型的对象在放入所分配的数组中时能够正确对齐。

总而言之,size的值由编译器决定!operator new和operator new[] 最明显的区别就是size计算方式的不同。

如果new表达式以::运算符开始,如::new T 或::new T[n],那么忽略类特有替换函数(在全局作用域中查找函数)。否则,如果T是类类型,那么就会从T的类作用域中开始查找。

在C++里,operator new 和 operator delete 可以作为类的成员函数(默认是静态的)或者全局函数来重载,不能放在全局命名空间之外的命名空间中声明,否则编译失败!一旦在全局命名空间重载了operator new、operator delete,就没办法使用::new调用默认的实现了。

void* operator new(std::size_t size) {std::cout << "my new called with value: " << size << std::endl;void *ptr = malloc(size);return ptr;
}void operator delete(void* p) {std::cout << "my delete called" << std::endl;std::free(p);
}int main()
{// 这里使用的operator new就是我们重载的版本,无法使用默认版本了。int *p = new int;delete p;return 0;
}

以下示例先从全局查找替换函数new,然后从类中查找new[]。

class MyClass {
public:MyClass(int val) : value(val) {std::cout << "MyClass constructor called with value: " << value << std::endl;}void* operator new[](std::size_t size) {std::cout << "MyClass new[] called with value: " << size << std::endl;void *ptr = malloc(size);return ptr;}void operator delete[](void* p) {std::cout << "MyClass delete[] called" << std::endl;std::free(p);}int value;
};int main()
{MyClass *p = new MyClass(10);delete p;MyClass* p2 = new MyClass[3]{ 1, 2, 3 };delete[] p2;return 0;
}/*
my new called with value: 4
MyClass constructor called with value: 10
my delete called
MyClass new[] called with value: 12
MyClass constructor called with value: 1
MyClass constructor called with value: 2
MyClass constructor called with value: 3
MyClass delete[] called
*/

placement new的作用是在已经分配好的内存块上构造对象,它绕过了普通new运算符自动分配内存的步骤,直接在指定的内存位置进行对象构造,这个已分配的内存块可以来自栈、堆或者其他地方。普通的new运算符会自动分配内存,并且在使用delete时,会自动调用对象的析构函数来释放对象占用的资源,然后再释放内存。但placement new只负责对象的构造,并不管理内存的分配与释放,所以不会自动调用析构函数。因此,无论这块内存是在堆上还是栈上,使用placement new 构造的对象都需要手动调用析构函数来销毁对象。

以下是一个简单自定义operator new示例:

// 自定义的 operator new 重载函数
void* operator new(std::size_t size, int num, double value) {std::cout << "Custom operator new called with size: " << size<< ", num: " << num << ", value: " << value << std::endl;return malloc(size);
}// 示例类
class T {
public:T() {std::cout << "T constructor called." << std::endl;}~T() {std::cout << "T destructor called." << std::endl;}
};int main() {double f = 3.14;// 第一次分配:自定义 operator new// 2, f 就是后面的两个参数,size由编译器计算T* obj = new(2, f) T;// 显式调用析构函数obj->~T();// 第二次分配:placement new(复用 obj 的内存)T* obj2 = new (obj) T;// 必须手动调用析构函数!obj2->~T();// 释放内存::operator delete(obj2);return 0;
}

关于::operator new 与 ::new 的本质区别:

  • ::operator new:它是一个函数,:: 表示调用全局命名空间中的 operator new 函数。operator new 函数的作用单纯是分配内存,它返回一个指向已分配内存块的指针,不会调用对象的构造函数。operator new 有多种重载形式,常见的形式为 void* operator new(std::size_t size),用于分配指定大小(size)的内存。

  • ::new:new 是 C++ 中的一个运算符,也就是我们通常说的 “new 表达式”。当使用 new 表达式时,它会完成两个主要步骤:首先调用 operator new 函数来分配内存,然后在分配好的内存上调用对象的构造函数来初始化对象。例如 T* ptr = new T();,这里的 new 表达式会先调用 operator new(sizeof(T)) 分配内存,接着调用 T 的构造函数。

3.8、delete表达式

delete表达式用于销毁先前由new表达式分配的对象,并释放获得的内存区域。

  • ::(可选) delete 表达式:销毁 new 表达式创建的单个非数组对象
  • ::(可选) delete[] 表达式:销毁 new[] 表达式创建的数组

全局版本的delete函数默认签名

void operator delete(void* ptr) noexcept;
void operator delete[](void* ptr) noexcept;
// placement 版,placement delete 的调用是由 C++ 运行时在异常发生时自动触发的,不能像普通 delete 那样直接调用
void operator delete(void* ptr, void* place) noexcept;
void operator delete[](void* ptr, void* place) noexcept;

所有 operator delete 必须声明为 noexcept(C++11 起),否则导致编译错误。每个 operator new 必须对应一个 operator delete(参数列表相同),用于处理构造失败的情况。如果自定义重载了 operator new(包括普通版本或数组版本),通常建议同时重载对应的 operator delete。

表达式求值得到的指针为ptr,ptr必须是以下之一:

  • 空指针,不做任何事;
  • 指向new表达式所创建的数组、非数组对象的指针:如果尝试delete数组中间部分的指针,这是未定义行为;
  • 指向new表达式所创建的对象:如果ptr是指向new所分配的对象的基类子对象的指针,那么基类的析构函数必须是虚函数。
  • 不能多次释放同一块内存!

3.9、typeid 运算符

  • typeid ( 类型 )
  • typeid ( 表达式 )

typeid运算符被用于查询类型的信息,可以知晓多态对象的动态类型以及静态类型的鉴别。typeid表达式是左值表达式:

  • 它的返回值是一个具有静态存储期的,多态类型std::type_info对象:typeid返回的对象在程序的整个生命周期内都存在,它不会随着函数的调用和返回而创建或销毁
  • 或者该类型的const限定版本类型的对象:如果类型或表达式是const,那么返回的std::type_info对象也是const

如果类型和表达式的类型具有cv限定,那么typeid的结果会指代对应的无cv限定类型(即typeid(T) == typeid(const T))

当应用于多态类型的表达式时,typeid 表达式的求值可能涉及运行时开销(虚表查找),其他情况下 typeid 表达式都在编译时解决。

#include <typeinfo>class Base {
public:virtual ~Base() {} // 基类析构函数声明为虚函数以支持多态
};class Derived : public Base {};int main() {Base* basePtr = new Derived();const std::type_info& typeInfo = typeid(*basePtr);// 不能修改 typeInfo 对象,因为它是 const 限定的// typeInfo = ...; // 错误,不能对 const 对象赋值std::cout << "Type name: " << typeInfo.name() << std::endl;delete basePtr;return 0;
}

不保证同一类型上的 typeid 表达式的所有求值都指代同一个 std::type_info 对象,不过这些 type_info 对象的 std::type_info::hash_code 相同,它们的 std::type_index 也相同。

const std::type_info& ti1 = typeid(A);
const std::type_info& ti2 = typeid(A);assert(&ti1 == &ti2); // 不保证
assert(ti1 == ti2); // 保证
assert(ti1.hash_code() == ti2.hash_code()); // 保证
assert(std::type_index(ti1) == std::type_index(ti2)); // 保证

3.10、转换

隐式转换

在语境中使用了某种类型 T1 的表达式,但语境不接受该类型而接受另一类型 T2 的时候,会进行隐式转换

  • static_cast:用于相对安全的、编译时可检查的类型转换,包括基本数据类型转换、类的继承关系转换等。
  • const_cast:专门用于去除或添加 const 或 volatile 限定符。
  • dynamic_cast:用于在具有继承关系的类之间进行安全的向下转换,会进行运行时类型检查。
  • reinterpret_cast:用于进行最底层的、危险的类型转换,只是简单地重新解释值的二进制表示。
http://www.xdnf.cn/news/886483.html

相关文章:

  • Kafka深度解析与原理剖析
  • MySQL数据库基础(一)———数据库管理
  • 华为OD最新机试真题-小明减肥-OD统一考试(B卷)
  • python编写赛博朋克风格天气查询程序
  • PyTorch中matmul函数使用详解和示例代码
  • vscode 离线安装第三方库跳转库
  • python3.9带 C++绑定的基础镜像
  • 【深尚想】OPA855QDSGRQ1运算放大器IC德州仪器TI汽车级高速8GHz增益带宽的全面解析
  • 基于ResNet残差网络优化梯度下降算法实现图像分类
  • 编程技能:格式化打印05,格式控制符
  • 人工智能AI在数字化转型有哪些应用?
  • Android设置顶部状态栏透明,以及状态栏字体颜色
  • TDengine 开发指南—— UDF函数
  • 【JeecgBoot AIGC】AI知识库实战应用与搭建
  • 01 Deep learning神经网络的编程基础 二分类--吴恩达
  • Windows应用-GUID工具
  • LFWG2024.08
  • BeeWorks 协同办公能力:局域网内企业级协作的全场景重构
  • 电脑提示dll文件缺失怎么办 dll修复方法
  • 【Elasticsearch】 查询优化方式
  • openvino如何在c++中调用pytorch训练的模型
  • 【Oracle】分区表
  • Maxscript快速入门(四)
  • C#、VB.net——如何设置窗体应用程序的外边框不可拉伸
  • Mermaid画UML类图
  • 深度学习N2周:构建词典
  • 【笔记】解决MSYS2安装后cargo-install-update.exe-System Error
  • Mybatis动态SQL语句
  • aitrader兼容talib,布林带的简单策略,创业板十年年年化15.5%,附代码
  • 成都芯谷金融中心·文化科技产业园:构建产业新城的实践与探索