Skip to content

Latest commit

 

History

History
987 lines (783 loc) · 24.4 KB

04.第四章:表达式.md

File metadata and controls

987 lines (783 loc) · 24.4 KB

第四章:表达式

表达式基础

#include <iostream>

void fun(int p1, int p2)
{
    std::cout << p1 << ' ' << p2 << '\n';
}
int main()
{
    int x = 0;
    fun(x = x + 1 ,x = x + 1); 	// 不同编译器输出结果不一样
    // clang打印结果: 1  2
    // gcc打印结果: 2  2
    
    /*
    //clang
    x = x + 1  ->  arg1
    p1 = arg1
    x = x + 1  ->  arg2
    p2 = arg2
    
    g++
    x = x + 1  ->  arg1
    x = x + 1  ->  arg2
    p1 = arg1
    p2 = arg2
    
 ////////////////////////////
    int a = 1;
    int b = 2;
    
    b = 4;// 编译尝试调整先后顺序,提高程序执行速度
    a = 3;
    */
}

// 正解
int main()
{
    // 生成依赖关系,防止编译器进行乱序调整
    int x = 0;
    x = x + 1;
    x = x + 1;
    fun(x,x);
}

左值与右值

#include <iostream>

// 一个对象不是左值or右值
// 仅在其作为表达式存在时,且同时对这个对象所表示的表达式进行求值之后,
// 得到的结果 才是左值和右值
int main()
{ 
    int x;
    x = 3;
    // 3 = x; 无意义
}


// 纯右值

struct Str
{
    
};
int main()
{
    int{};// 构造临时对象,这类对象可用于操作符和操作数,或初始化中使用 // 纯右值
    Str{};
}


// 亡值
#include <iostream>
#include <vector>

void fun(std::vector<int>&& par)		// && 代表右值引用
{
    
}

int main()
{
    std::vector<int> x;
    fun(std::move(x));		// std::move(x) 使用x构造xvalue,转换成将亡值
    // 定义x为将亡值,表明后续不会再对x中包含的资源进行任何的处理,因为x即将消亡
}

struct Str{};

int main()
{
    const int x = 3;	// 左值 + lvalue
    x = 3; 	//左值不一定能放在左边
    
    int x = int();	// int() 纯右值
    Str x = Str();
    Str() = Str(); 	// 纯右值可以放在等号左边
}
struct Str
{
    int x;
}
void fun(const int& par)
{
    
}
    
// 左值与右值的转换
int main()
{
    int x = 3;
    int y = x;	// 左值转换为右值
    x + y = 3;
    
    // 临时具体化
    Str().x;	// prvalue -> xvalue 
   // 从Str()内存中取出相应的x
    fun(3);
}

decltype 说明符

  1. 如果实参是其他类型为 T 的任何表达式,且

    a) 如果 表达式值类别亡值,将会 decltype 产生 T&&

    b) 如果 表达式 的值类别是左值,将会 decltype 产生 T&

    c) 如果 表达式 的值类别是纯右值,将会 decltype 产生 T

    如果 表达式 是返回类类型纯右值的函数调用,或是右操作数为这种函数调用的逗号表达式,那么不会对该纯右值引入临时量。 (C++17 前)
    如果 表达式 是除了(可带括号的)立即调用以外的 (C++20 起)纯右值,那么不会从该纯右值实质化临时对象:即这种纯右值没有结果对象。 (C++17 起)

    该类型不需要是完整类型或拥有可用的析构函数,而且类型可以是抽象的。此规则不适用于其子表达式:decltype(f(g())) 中,g() 必须有完整类型,但 f() 不必。

#include <cstdio>

// prvalue → type
int main()
{
    decltype(3) x;	// 3是纯右值,且3是int型;decltype 产生 Type
    // 等价于 -> int x;
}

// lvalue → type&
int main()
{
    int x;
    decltype(x) y;	//   -> int y; prvalue → type
    decltype( (x) )	y = x;	//  -> int &y = x
}
#include <cstdio>
#include <utility>

// xvalue → type&&
int main()
{
    int x;
    decltype( std::move(x) ) y;	// std::move定义在<utility>,y是一个引用需要初始化
    decltype( std::move(x) ) y = x; // 错误,y是一个右值引用,不能绑定在左值上
    decltype( std::move(x) ) y = std::move(x); // 把x定义成将亡值
}
// C++ insights
int main()
{
  int x;
  int && y = std::move(x);
  return 0;
}

类型转换

#include <iostream>

int main()
{
    3 + 0.5;	// 隐式类型转换
    "abcd" + 0.5;	// 无法找到一个公共的类型,无法转换
    double x = "abcd";	// 错误,"abcd"无法转换成double
}

转换顺序

隐式转换序列由下列内容依照这个顺序所构成:

  1. 零或一个标准转换序列

  2. 零或一个用户定义转换

  3. 零或一个标准转换序列(仅当使用用户定义的转换时)。

当考虑构造函数或用户定义转换函数的实参时,只允许一个标准转换序列(否则将实际上可以将用户定义转换串连起来)。从一个非类类型转换到另一非类类型时,只允许一个标准转换序列。

标准转换序列由下列内容依照这个顺序所构成:

  1. 零或一个来自下列集合者:左值到右值转换数组到指针转换函数到指针转换;

  2. 零或一个数值提升数值转换

  3. 零或一个函数指针转换;(C++17 起)

  4. 零或一个限定转换

数值提升

整型提升

​ 小整型类型(如 char)的纯右值可转换成较大整型类型(如 int)的纯右值。具体而言,算术运算符不接受小于 int 的类型作为它的实参,而在左值到右值转换后,如果适用就会自动实施整型提升。此转换始终保持原值。

  • signed charsigned short 可转换到 int;
  • 如果 int 能保有它的整个值范围,那么 unsigned charchar8_t (C++20 起) 或 unsigned short 可转换到 int,否则可转换到 unsigned int;
  • char可转换到 int 或 unsigned int,取决于它的底层类型是 signed char 还是 unsigned char(见上文);
  • wchar_tchar16_tchar32_t (C++11 起) 可转换到以下列表中能保有它的整个值范围的首个类型:int、unsigned int、long、unsigned long、long long、unsigned long long (C++11 起);
  • 底层类型不固定的无作用域枚举类型可转换到以下列表中能保有它的整个值范围的首个类型:int、unsigned int、long、unsigned long、long long、unsigned long long、扩展整数类型(以大小顺序,有符号优先于无符号) (C++11 起)。如果值范围更大,那么不应用整型提升;

浮点提升

​ float 类型纯右值可转换成 double 类型的纯右值。值不更改。

数值转换

不同于提升,数值转换可以更改值,而且有潜在的精度损失。

  1. 整型转换

  2. 浮点转换

  3. 浮点整型转换

  4. 指针转换

  5. 成员指针转换

  6. 布尔转换

#include <iostream>

int main()
{
    3 + 0.5;	// 隐式类型转换
    "abcd" + 0.5;	// 无法找到一个公共的类型,无法转换
    double x = "abcd";	// 错误,"abcd"无法转换成double
    
    static_cast<double>(3) + 0.5;	// 显示类型转换
    static_cast<double>("abcd") + 0.5; // 错误
    
    std::cout << (3 / 4) << std::endl;	// 取整 -> 0
    int x = 3;
    int y = 4;
    std::cout << (x / static_cast<double>(y) ) << std::endl;	// 输出0.75
    std::cout << static_cast<double>(x / y)  << std::endl;	//先对括号内的求解,得到了0,再转换成double型
}


#include <iostream>

int main()
{
    int* ptr;
    void* v = ptr;
    
    //int* ptr2 = v;	// 报错,不支持由 void* 到 int* 的隐式转换
    
    int* ptr2 = static_cast<int*>(v);	// 可以,显示转换的应用
    
}

// C语言的处理方法,C++会用函数重载的方法
void fun(void* par, int t)
{
    if (t == 1)
    {
        int* ptr = static_cast<int*>(par);
        // ...
    }
    else if (t == 2)
    {
        double* ptr = static_cast<double*>(par);
        // ...
    }
}

int main()
{
    int* ptr;
    double* ptr2;
    fun(ptr,1);
    fun(ptr2,2);
}

const_cast

#include <iostream>

int main()
{
    const int* ptr;
    static_cast<int*>(ptr);	// 错误,不能去除常量性
    const_cast<int*>(ptr);	// 可以帮助去除或增加常量性
}

// const_cast 使用起来还是比较危险,行为不确定
int main()
{
    int x = 3;
    const int& ref = x;
    int& ref2 = const_cast<int&>(ref);	// 把原来的const去掉了
    ref2 = 4;	// 通过ref2改变 x 的值
    std::cout << x << std::endl;
    
    // 危险在于  若 const int x = 3;最后编译输出是3或4,但编译不会报错
    // 且不同编译器可能输出结果不同(3是直接打印出来,4是被修改了)
}

// reinterpret_cast<double>(x)	把一段内存空间强行看成另外的含义;重新解释
int main()
{
    int x = 3;
    double y = reinterpret_cast<double>(x);		// 错误,有转换限制
}
int main()
{
    int x = 3;
    int* ptr = &x;
    double* ptr2 = reinterpret_cast<double*>(ptr);
    std::cout << *ptr2 << std::endl;	// 数据表示形式不同,
    // 强行用double方式解释表示int的内存
    // int占四个字节,double解释会取这四个字节以及后四个字节,因此解析出的值不一样
    float* ptr2 = reinterpret_cast<float*>(ptr);
    // 在相应的编译环境下,float也占四个字节,因此每次解析出的值一样
}

C 形式的类型转换 // C++中不推荐

#include <iostream>

int main()
{
    double(3);
}

表达式详述

#include <iostream>

int main()
{
    int x = 3;
    int y = 5;
    + x;		// 一元操作符
    x + y;		// 二元操作符,x、y转换为纯右值,并得到一个纯右值
    7 * + 3;	// 一元操作符优先级最高,此处+3优先级高
}

int main()
{
    int a[3] = {1, 2, 3};
    int* ptr = a;
    ptr = ptr + 1;	// 含义:指针移动相应的位数
    ptr = ptr - 1;
    
    std::cend(a) - std::cbegin(a);	// 两个指针之间包含的元素个数
    std::cend(a) + std::cbegin(a);	// 两个指针不能相加,没有具体的含义
}

int main()
{
    int a[3] = {1, 2, 3};
    auto x = a;	// -> int* x = a;
    auto& x = a;	// -> int (&x)[3] = a;
    
    const auto& x = +a;		// -> int *const & x = +a;  用 + 强制实现了类型转换
    // + 不能应用于数组,但能应用于指针
    
    // 如果去掉 +
    const auto& x = a;	// -> int const (&x)[3] = a;  它就是数组的引用,不再是指针的引用
}


// 一元 + 操作符会产生 integral promotion
int main()
{
    short x = 3;
    auto y = x;		// -> short y = x
    auto y = +x;	// 类型提升-> int y = +static_cast<int>(x)
    auto y = -x;	// 类型提升-> int y = -static_cast<int>(x),且 - 会改变取值 -3
}


// 求余只能接收整数类型操作数,结果符号与第一个操作数相同
// 满足 (m / n) * n + m % n == m
int main()
{
    4 / 3;
    4 % 3;
    std::cout << (4 / 3) * 3 + (4 % 3) << std::endl;
}

逻辑与关系操作符

#include <iostream>

int main()
{
    3 < 5;
    int a[3];
    auto ptr1 = std::begin(a);
    auto ptr2 = std::end(a);
    ptr1 != ptr2;
    // ptr1 != 4;	不能把指针和算数类型操作数混用
}

int main()
{
    true && true;
    int x = 3;
    3 && x;		//只要能转换成bool值,逻辑操作符就是合法的
    // 左值会被转换为相应的右值,转换完之后进行相应的求 与 操作
    
    
    a && b && c;
    !...			// 除逻辑非外,其它操作符都是左结合的
}

//逻辑与、逻辑或具有短路特性
int main()
{
    a && b 		// 先对 a 求bool值,如果 a 为假直接返回 false;均为真才返回真
    a + b		// 对比 a+b; 系统先对a和b求值在进行相加,对a和b哪个先求值都可以;但上述一定是对a先求值
}

int main()
{
    int* ptr = nullptr;
    // 短路逻辑,保证程序操作可控
    if (ptr && (*ptr == 3))		//编译可通过,因为ptr是flase直接返回;*ptr本身错误
    {
        
    }
}

int main()
{
    a || b	 	// 只要一个为真就真,同&&; 先对a判断,a为真则直接返回真
    a && b && c	// 因为短路特性,也因此设计成了左结合
}


//逻辑与的优先级高于逻辑或
int main()
{
    a && b || c			// 先算逻辑与的部分,再求或
    a || b && c			// 建议加括号
    (a || (b && c))
}

//通常来说,不能将多个关系操作符串连
int main()
{
    int a = 3;
    int b = 4;
    int c = 5;
    
    std::cout << (c > b > a) << std::endl;		// -->false
    // c > b > a  --> true >a --> 1 > a --> false
    std::cout << (c > b) && (b > a) << std::endl;
    
    std::cout << (a == b == c) << std::endl;	// -->false
    std::cout << (a == b) && (b == c) << std::endl;
}

//不要写出 val == true 这样的代码
int main()
{
    int a = 3;
    if (a)	//可以		
    {
        
    }
    // if (a == true) 不好,包含了关系操作符	--> a == 1
    // 系统会把它们转换至公共类型,将bool值true隐式类型转换成 1 ,
    // 再对表达式进行判断,最后得出错误的结果
}


//Spaceship operator: <=>
int main()
{
    -1,0,1	// C语言中用函数
    a <=> b // 返回a和b的关系
    auto res = (a <=> b)
    if (a > b)		// 如果数据结构比较复杂,比较起来比较耗时
    {
        
    }
    else if (a < b)	// 可能需要判断多次
    {
        
    }
    else
    {
        
    }
}


int main()
{
    int a = 3;
    int b = 5;
    auto res = (a <=> b);	// 只需要判断一次
    // 较新,有些版本编译器支持的不好
    if (res > 0)
    // if (res == std::strong_ordering::greater)
    {
        std::cout << "a > b\n";
    }
    // if (res == std::strong_ordering::less)
    else if (res < 0)
    {
        std::cout << "a < b\n";
    }
    else if (res == 0)
    // else if (res == std::strong_ordering::equal)
    {
        std::cout << "a == b\n";
    }
}

// 注意Spaceship operator: <=> 的返回
//strong_ordering
//weak_ordering
//partial_ordering
int main()
{
  int a = 5;
  int b = 5;
  std::strong_ordering res = (a <=> b);	// strong_ordering 刻画两个东西的关系
  return 0;
}

// C++ insights
#include <cstdio>
#include <compare>

int main()
{
    auto res = (3 <=> 5);
    // --> std::strong_ordering res = (3 <=> 5);
    auto res = (3.0 <=> 5.0);
    // --> std::partial_ordering res = (3.0 <=> 5.0);
}

#include <iostream>
#include <cmath>

int main()
{
    std::cout << sqrt(-1) << std::endl;
    // --> 在C++中会输出 -nan    Not a Number
}

#include <iostream>
#include <cmath>
#include <compare>

int main()
{
    double f = 3.0;
    auto res = (sqrt(-1) <=> 5.0);
    
    std::cout << (res > 0) << std::endl;
    std::cout << (res < 0) << std::endl;
    std::cout << (res == 0) << std::endl;
    std::cout << (res == std::partial_ordering::unordered) << std::endl;
    
}

位操作符

#include <iostream>


int main()
{
    signed char x = 3;				// 00000011
    std::cout << ~x << std::endl;	 // 11111100  -> -4
    signed char y = 5;				// 00000101
    std::cout << (x & y) << std::endl;	// 00000001	-> 1
    std::cout << (x | y) << std::endl;	// 00000111	-> 7
    std::cout << (x ^ y) << std::endl;	// 00000110	-> 6  取值不同取1,取值相同取0
    
    // 注意这里没有短路逻辑
    // 按位与 与 逻辑与 的区别
	x && y 
}

// C++ insights
#include <cstdio>
// 注意计算过程中可能会涉及到 integral promotion
int main()
{
    signed char x = 3;
    signed char y = 5;
    auto z = x & y;
    // --> 
    int z = static_cast<int>(x) & static_cast<int>(y);
}

// 移位操作符
#inlude <iostream>

int main()
{
    signed char x = 3;					// 00000011
    std::cout << (x << 1) << std::endl;	  // 00000110  左移一位
    std::cout << (x >> 1) << std::endl;	  // 00000001  右移一位
    
    signed char y = -4;					// 11111100
    std::cout << (y << 1) << std::endl;	  // 11111000  左移一位	--> -8
    std::cout << (y >> 1) << std::endl;	  // 11111110  右移一位 --> -2
    // 补的是符号位
    // 需要加 ()  << 移位操作符  << 操作符重载
}

//移位操作在一定情况下等价于乘(除) 2 的幂,但速度更快
//左移乘2,右移除2
int main(){
    int x = 3;
    constexpr int y = 2;	//编译期常量
    // 若能使用常量表达式尽量使用常量表达式,给编译器更多优化空间
    std::cout << x * y << std::endl;
    // -->
    std::cout << (x << 1) << std::endl;
}

// 注意整数的符号与位操作符的相关影响
int main()
{
    unsigned char x = 0xff;		// 11111111
    // 0000..00011111111
    // 1111..11100000000
    auto y = ~x;	// 整型提升    -->  -256
    std::cout << y << std::endl;
    
    
    signed char x = 0xff;	// 补的是符号位(二进制补码)
    // 1111..11111111111
    // 0000..00000000000
    auto y = ~x;	// 整型提升    -->  0
    std::cout << y << std::endl;
    
    char x = 0xff;  // 不确定,根据编译器而定
}

// C++ insight
int main()
{
  unsigned char x = 255;
  int y = ~static_cast<int>(x);		// 先对 x 进行提升,在对其进行按位取反
  std::cout.operator<<(y).operator<<(std::endl);
  return 0;
}


// 右移保持符号,但左移不能保证
// 左移可能把符号位移出去
int main()
{
    int x = 0x80000000;		// 10....0
    std::cout << x << std::endl;			// -2147483648
    std::cout << ( x >> 1 ) << std::endl;	 // -1073741824	   //110...0
    std::cout << ( x << 1 ) << std::endl;	 // 0	溢出		//000...0
    
    unsigned int x = 0x80000000;	// 10....0
    std::cout << x << std::endl;
    std::cout << ( x >> 1 ) << std::endl;	// 010....0		unsigned int 第一位不作符号位解释
    std::cout << ( x << 1 ) << std::endl;	// 000....0
    
    int x = -1;							// 11...11	
    std::cout << x << std::endl;
    std::cout << ( x >> 1 ) << std::endl;	// 11...11		--> -1
    std::cout << ( x << 1 ) << std::endl;	// 11...10		--> -2 
}

赋值操作符

#include <iostream>

struct Str {};
// 左操作数为可修改左值;
// 右操作数为右值,可以转换为左操作数的类型
int main()
{
    int x = 3;
    x = 5;
    
    int y;
    y = true;	// 可以转换为左操作数的类型
    
    x = Str(); 	// 错误,不能把Str()转换成int类型
}

// 赋值操作符是右结合的
// 求值结果为左操作数
int main()
{
    int x;
    int y;
    
    x = y = 3;		// 从右往左算。 y = 3 -> x = y;
    x = 5 = 2       // 错误,先做了5 = 2,不合法
    (x = 5) = 2;	// 可以,改变操作顺序,(x = 5)得到左操作数,再 x = 2
}

// 可以引入大括号(初始化列表)以防止收缩转换( narrowing conversion ) 
int main()
{
    short x;	// 16位类型,占两个字节  // 不同机器可能占的字节不同
    x = 0x80000000; // 占四个字节,int可以转换成short类型;这里把前四位扔掉,只取了后四位,因此输出为0
    std::cout << x << std::endl;
    
    x ={ 0x80000000 };		// 若发现收缩转换,系统直接报错   narrowing conversion;防止潜在错误

}

int main()
{
    int y = 3;
    short x;
    x = { y };		// 直接报错,因为 y 是变量,无法保证是否会发生收缩转换
    std::cout << x <<std::endl;
    
    constexpr int y = 3; // 告诉编译器 y 是编译期常量
    x = { y };		// y 一定为 3 ,因此不会产生收缩转换,因此编译器可通过
    
    const int y = 3;		// 运行期常量
    short x;
    x = { y };				// 虽然是const int但还是可能产生收缩转换(作业:改代码使它产生收缩转换)
    std::cout << x <<std::endl;
}

int main()
{
    const volatile int y = 3;		//运行期常量
    short x;
    int *p = const_cast<int*>(&y);
    *p = 256927476;
    x = { y };
   
    // 复合赋值运算符
    x = x + 3;
    x += 3;
    x = x * 3;
    x *= 3;
}

int main()
{
    int x = 2;
    int y = 3;
    x^=y^=x^=y;  // x 与 y 交换,节省了一块内存,没有速度上的优势
    std::cout << x << '\n';		// -> 3   x 与 y 交换
    std::cout << y << '\n';		// -> 2
    
    // 解析
    x = 2; y = 3;
    x^=y;	// x = 2^3, y = 3
    y^=x;	// x = 2^3; y = 3^2^3 =	3^3^2 =	0^2 = 2;// 亦或操作有交换律
    // 任何值与0亦或都是它本身
    x^=y;	// x = 2^3^2 = 3; y = 2;
}

自增与自减运算符

#include <iostream>

int main()
{
    int x = 3;
    int y;
    // ++; --  分前缀与后缀两种情况
    y = x++; // -> x = 4; y = 3
    y = ++x; // -> x = 4; y = 4
    std::cout << x << '\n';
    std::cout << y << '\n';
    
    // 操作数为左值;前缀时返回左值;后缀时返回右值
    x++;  // 此时 x 已经更新    返回右值   
    ++x;  // 前缀时返回左值     对操作数对应的变量进行返回
    // 建议使用前缀形式
    ++(++x);  // 合法  ++++x; 从右到左
    (x++)++;  // 非法  括号出来的是右值
    
    std::cout << (5 + ++++x) << '\n';  // 合法 但(5+++++x)不合法,因为编译器解析是从左到右
}
// 补充:为什么用前缀    如果需要获得x的原始值
// 后缀需要用一个临时变量保存,需要额外付出临时变量构造、拷贝和内存成本;前缀不需要
// 但不必因噎废食

其它操作符

#include <iostream>


// 成员访问操作符: . 与 ->

struct Str
{
    int x;
};

int main()
{
    // . 的左操作数是左值(或右值),返回左值(或右值 xvalue )
    Str a;	// a 是左值
    a.x;	// a.x返回左值
    decltype(a.x) y;	// decltype ( 实体 )
    /////////////////////////////
    // C++ insight
    Str a = Str();
    int y;
    //////////再加个括号表示表达式//////////////////
    decltype((a.x)) y = a.x;	//decltype ( 表达式 )
    // --> int & y = a.x;  即 a.x 返回左值
    
    // decltype((Str().x)) y = a.x;  	// 报错
    //  如果 表达式 的值类别是亡值,将会 decltype 产生 T&&;
    decltype((Str().x)) y =std::move(a.x);
    // --> int && y = std::move(a.x);
    ////////////////////////////
    Str* ptr = &a;
    (*ptr).x;		// 若去掉括号,错误; 因为 . 操作符优先级高于*		//指针不支持成员访问
    // 简化上述写法
    ptr -> x;
    decltype((ptr->x)) y = a.x;
    // int & y = a.x; // 如果 表达式 的值类别是左值,将会 decltype 产生 T&;
    
}

// 条件操作符    true ? 3:5    由 ?  :  组成
// 唯一的三元操作符
// 有求值顺序,先对第一个操作数求值;
// 根据操作数求值是真是假选择第二个或第三个操作数进行求值
int main()
{
    std::cout << (true ? 3:5) << std::endl;
    
    int x = 1;
    int y =2;
    false ? (++x) : (++y);
    std::cout << x << '\n';
    std::cout << y << '\n';
}
// 第二个操作数与第三个操作数类型需相同
int main()
{
    bool value;
    // ....
    value ? 1 : "hello"; // 最后返回的类型不确定,故非法 
}

// 如果表达式均是左值,那么就返回左值,否则返回右值
int main()
{
    int x = 2;
    int y = 3;
    true ? 1 : x; 	// 一个右值一个左值;返回右值
    false ? y : x;  // x 左值 y 左值 返回左值
}

// 右结合
int main()
{
    int score = 100;
    // 右结合
    int res = (score > 0) ? 1 : (score == 0) ? 0 : -1;	// 合法,右结合
    // 不建议这种写法
    std::cout << res << std::endl;
}

逗号操作符

#include <iostream>

void fun(int x1, int x2)
{
    
    
}
// 确保操作数会被从左向右求值
// 求值结果为右操作数
int main()
{
    2,3
    std::cout << (2,3,4,5) << std::endl;
    
    fun(2,3);	//此处不是逗号操作符;函数调用表达式;
    // 把 2 赋予 x1 ; 把 3 赋予 x2
    
    int x;
    int y;
    (++x),(++y); // 先算第一个再算第二个
}

sizeof 操作符

#include <iostream>

int main()
{
    int x;
    sizeof(int);   // 是个类型需要加括号
    sizeof x;	// 表达式  ->  sizeof(x)
    // 建议统一使用sizeof(),形式统一
}

// 并不会实际求值,而是返回相应的尺寸
int main()
{
    int* ptr = nullptr;
    sizeof(*ptr);	// 假装对它求值,其实没有;所以合法
    // -> sizeof(int)
}

其它操作符

#include <iostream>

namespace ABC
{
    int x;
}

// 域操作符
int main()
{
    int x;
    int y = ABC::x;
}

// 函数调用操作符 ()
// 索引操作符 []
// 抛出异常操作符 throw