当前位置: C语言 -- 基础 -- 表达式

表达式(十七)

十三、赋值运算符

C语言中赋值运算符(assignment operators)包括:=*=/=%=+=-=<<=>>=&=^=|=,其中=是简单赋值运算符(simple assignment operator),其余赋值运算符统称为复合赋值运算符(compound assignment operators)。

赋值运算符将值存储到左操作数指定的对象中;赋值运算符的左操作数应为可修改左值(可修改左值不能是数组类型、不能是不完整类型、并且没有类型限定符const限定的左值;如果可修改左值是结构或者联合,应不存在类型限定符const限定的成员。)。

赋值运算符具有右结合性,即从右向左运算。左操作数存储值的更新在左操作数和右操作数的值计算之后。左操作数和右操作数的值计算是未排序的。赋值表达式的值是左操作数赋值后的值,但不是左值;赋值表达式的类型是左操作数左值转换(lvalue conversion)后的类型。

int arr[5] = {0};
int i = 1;
int j = 1;

/*
** 数组元素arr[2]的值为2。
** 赋值表达式arr[++i] = ++j的值为2,类型为int类型。
*/
arr[++i] = ++j;

1、简单赋值

简单赋值表达式的语法格式如下所示:

unary-expression = assignment-expression

其中unary-expression为一元表达式,assignment-expression为赋值表达式。


简单赋值表达式应满足以下条件之一:

-- 左操作数具有原子的、限定的或者非限定的算术类型,右操作数具有算术类型。

int i;
const int ci = 5;

i = ci;

-- 左操作数具有与右操作数类型兼容的结构或者联合类型的原子、限定或者非限定版本。

struct s{
  int i;
  float f;
};

const struct s cs = {6, 3.14f};
struct s ss;

ss = cs;

-- 左操作数具有原子的、限定的或者非限定的指针类型,并且左操作数和右操作数都是指向兼容类型的限定或者非限定的指针,左操作数指向类型具有右操作数指向类型的所有类型限定符。

int i;
int *ptr = &i;
volatile int *vp = NULL;

vp = ptr;

-- 左操作数具有原子的、限定的或者非限定的指针类型,并且一个操作数是指向对象类型的指针,另一个操作数是指向void类型的限定或者非限定的指针,左操作数指向类型具有右操作数指向类型的所有类型限定符。

int *ptr = NULL;

ptr = malloc(5*sizeof(int));

-- 左操作数具有原子的、限定的或者非限定的指针类型,右操作数是空指针常量(null pointer constant)。

int *ptr;

ptr = NULL;

-- 左操作数具有原子的、限定的或者非限定的nullptr_t类型,右操作数是空指针常量(null pointer constant)或者具有nullptr_t类型。

nullptr_t p;
p = nullptr;

-- 左操作数具有原子的、限定的或者非限定的指针类型,右操作数是空指针常量(null pointer constant)或者具有nullptr_t类型。

int *ptr;
ptr = nullptr;

-- 左操作数具有原子的、限定的或者非限定的bool类型,右操作数是指针或者具有nullptr_t类型。

bool b;

b = &b;

简单赋值运算中,右操作数的值会转换成赋值表达式的类型,并替换左操作数指定对象中的存储值。

如果一个对象的存储值从另一个对象读取,并且这两个对象以某种方式存储重叠,这两个对象必须占据相同的内存区域,并且两个对象的类型应是兼容类型的限定版本或者非限定版本;否则其行为是未定义的。

struct s1 {int i;};
struct s2 {int i;};

union u {
  struct s1 s11;
  struct s2 s21;
  struct s1 s12;
};
...
union u data = {5};
data.s12 = data.s11;    //合法。
data.s21 = data.s12;    //未定义行为。

如果右操作数的值超出左操作数的类型所能表示的值域范围,右操作数的值存储到左操作数指定的对象时会被截断。

unsigned char uc;
unsigned u;
unsigned long long int ull = ULLONG_MAX;  //宏ULLONG_MAX值为18446744073709551615。

uc = (u = ull); //uc值为255,u值为4294967295。

赋值时使用强制类型转换可能产生一些不安全的操作。

const char ch = 'A';
const char *cp = &ch;

char *p =(char *)cp;
*p = 'B';	//修改了变量ch的值,现在变量ch的值为'B'。

2、复合赋值

复合赋值表达式的语法格式如下所示:

unary-expression op= assignment-expression

其中unary-expression是一元表达式,op是对应的二元运算符,assignment-expression是赋值表达式。


复合赋值表达式E1 op= E2等价于简单赋值表达式E1 = E1 op (E2),唯一的区别在于复合赋值表达式中左值E1只被评估一次。

对于-=+=运算符,左操作数是指向完整对象类型的原子的、限定的或者非限定的指针,右操作数是整数类型;或者左操作数是原子的、限定的或者非限定的算术类型,右操作数是算术类型。

int i = 3;
int arr[5];
int *ptr = arr;

i -= 6;
ptr += 1;

对于*=/=运算符,左操作数是原子的、限定的或者非限定的算术类型,右操作数是算术类型。

int i = 3;
float f = 3.6f;

i *= 1.4;
f /= 3;

对于%=运算符,左操作数是原子的、限定的或者非限定的整数类型,右操作数是整数类型。

int i = 3;
float f = 3.6f;

i %= 3; //合法。
f %= 3; //非法。

对于<<=>>=&=^=|=运算符,左操作数是原子的、限定的或者非限定的整数类型,右操作数是整数类型。

int i = 3;
float f = 3.6f;

i <<= 3; //合法。
f <<= 3; //非法。

i >>= 3; //合法。
f >>= 3; //非法。

i &= 3; //合法。
f &= 3; //非法。

i ^= 3; //合法。
f ^= 3; //非法。

i |= 3; //合法。
f |= 3; //非法。

对于复合赋值运算符,如果一个操作数是十进制浮点类型(decimal floating type),另一个操作数不能是标准浮点类型(standard floating type)、复数类型或者虚数类型。

// 以下复合赋值表达式非法;
// dd是_Decimal64类型,属于十进制浮点类型;
// d是double类型,属于标准浮点类型。
_Decimal64 dd = 1.23DD;
double d = 1.23;
d += dd;  //非法。

对于复合赋值表达式E1 op= E2,如果E1具有原子类型,复合赋值操作是具有memory_order_seq_cst内存顺序语义的读-修改-写操作。

假设E1为原子整数类型,E2为整数类型,T1E1的类型,T2E2的类型,复合赋值(op=)等价于:

T1 *address = &E1;
T2 value = E2;
T1 before = *address;
T1 after;

do
{
  after = before op value;
}while(!atomic_compare_exchange_strong(address, &before, after));

其中after是复合赋值的结果值。


十四、逗号运算符

逗号表达式的语法格式如下所示:

expression , expression

其中expression为表达式。


逗号运算符具有左结合性,即从左向右评估。逗号运算符的左操作数会被评估为void类型表达式;在左操作数的评估和右操作数的评估之间存在一个序列点(sequence point),即先评估完左操作数,再评估右操作数;右操作数的值和类型是逗号表达式的值和类型。逗号运算符不会生成左值。

int i = 0;
float f = 0.0f;

i = 5, f = i + 3.14f; //i的值为5,逗号表达式的类型是float类型,值是8.14。

逗号运算符不能出现在逗号用于分隔列表项的上下文中,例如:函数的参数列表,初始化列表等。这种情况下逗号运算符应在括号表达式中使用。

func((x=1, x+6), 5); //func函数有两个实参,其中第一个实参的值为7。



主要参考资料:

1、ISO/IEC 9899:2024

2、ISO/IEC 9899:2018

3、ISO/IEC 60559:2020

4、wiki.sei.cmu.edu : Do not depend on the order of evaluation for side effects

5、en.cppreference.com : Order of evaluation

6、programming-books.io : Modifying any object more than once between two sequence points

7、open-std.org : Fixing the rules for type-based aliasing

8、accu.org : WHAT IS THE STRICT ALIASING RULE AND WHY DO WE CARE?

9、ibm.com : Type-based aliasing

10、open-std.org : Rationale for Internationa Standard - Programming Languages - C

11、en.cppreference.com : Expressions

12、learn.microsoft.com : Primary Expressions

13、wiki.sei.cmu.edu : Call functions with the correct number and type of arguments

14、en.cppreference.com : Non-static data members

15、math.oxford.emory.edu : Postfix Expressions