本文为刚毕业参加工作时,初学C语言时做的一些笔记,适合初学者读阅,内容不保证百分百正确,若您对其中的描述有任何疑问可在留言板块进行留言。

以下为我使用的C语言的运行和编译环境:
System Version : Ubuntu22.04.1
Linux kernel Version : 5.19.0
GCC Version : 11.3.0
STDC Version : 201710L

摘要: 简述了表达式的组成,类别,以及相关语法,解释什么是左值和右值。

1. 表达式的定义

表达式是由一系列运算符 operators 和操作数 operands 组成的。运算符指明了要进行何种运算和操作,而操作数则是运算符操作的对象。在C语言中,常量、变量、函数调用以及按C语言语法规则用运算符把运算数连接起来的式子都是合法的表达式。

  • 任何表达式都具有值和类型的两种属性
  • 在表达式结束后加上分号的句子称为表达式语句

表达式的语法规则
[operands] [operators] [operands] …
表达式可以是单个的常量或变量,也可是多个常量、变量组合在一起的更复杂的表达式。

表达式的作用

  • 数值计算
  • 指明数据对象
  • 产生副作用
  • 以上目的的结合

1.1 运算符

运算符是整个表达式的核心,它代表着表达式需要进行怎样的逻辑运算,预示着表达式的行为和结果,C语言中可用的运算符如下。

查看运算符优先级列表
运算符优先级列表
优先级 运算符 含义 使用形式 结合方向
1 () 优先级运算符 (表达式) 块结合
函数调用运算符 func_addr() 自左至右
[] 下标运算符 ptr[index]
-> 指向成员运算符 struct_ptr->member
. 取成员运算符 struct_obj.member
2 (type) 类型强制转换运算符 (int)8.2 块结合
- 负号运算符 -var 自右至左
++ 自增运算符 var++/++var
-- 自减运算符 var--/--var
* 解引用运算符 *ptr
& 取地址运算符 &var_name
! 逻辑非运算符 !expr
~ 按位取反运算符 ~expr
sizeof 内存占用长度运算符 sizeof(var)
3 * 乘法运算符 expr * expr 自左至右
/ 除法运算符 expr / expr
% 取模运算符 expr % expr
4 + 加法运算符 expr + expr 自左至右
- 减法运算符 expr - expr
5 << 左移运算符 var << expr 自左至右
>> 右移运算符 var >> expr
6 > 大于判断运算符 expr > expr 自左至右
>= 大于等于判断运算符 expr >= expr
< 小于判断运算符 expr < expr
<= 小于等于判断运算符 expr <= expr
7 == 等于判断运算符 expr == expr 自左至右
!= 不等于判断运算符 expr != expr
8 & 按位与运算符 expr & expr 自左至右
9 ^ 按位异或运算符 expr ^ expr 自左至右
10 | 按位或运算符 expr | expr 自左至右
11 && 逻辑与运算符 expr && expr 自左至右
12 || 逻辑或运算符 expr || expr 自左至右
13 ?: 条件运算符 expr1 ? expr2 : expr3 自右至左
14 = 赋值运算符 var = expr 自右至左
*= 乘后赋值运算符 var *= expr
/= 除后赋值运算符 var /= expr
%= 取模后赋值运算符 var %= expr
+= 相加后赋值运算符 var += expr
-= 相减后赋值运算符 var -= expr
<<= 左移后赋值运算符 var <<= expr
>>= 右移后赋值运算符 var >>= expr
&= 按位与后赋值运算符 var &= expr
^= 按位异或后赋值运算符 var ^= expr
|= 按位或后赋值运算符 var |= expr
15 , 逗号运算符 expr , expr 自左至右
注:同一优先级的运算符,运算顺序由其结合方向决定

1.2. 操作数

操作数就是表达式的操作对象,是表达式进行表达的窗口。任何变量或常量都可以是操作数,但在不同的表达式类型中,有其相应的语法规则进行约束。

2.常量表达式

常量表达式的特点

  • 表达式的值不会改变
  • 在编译期间可以进行确定的值
e.g. 常量表达式示例
1
2
3
4
const int a = 1;          //常量表达式
const int b = a + 1; //由于变量a是常量, 所以该表达式也为常量表达式
int c = 3; //非常量表达式, 变量c非常量
const int d = function(); //非常量表达式, function()的值在编译期间无法得到

3.左值与右值

左值与右值的概念来自于赋值表达式,左右值是表达式的值属性的一种描述,在更为复杂的 C++ 语法中,左右值的概念更为重要,理解好左值与右值对理解程序以及语法的设计很有帮助。

3.1. C语言中左值与右值的定义

C99 标准中定义lvalue为具有对象型别或除void外的不完整型别的表达式,也可以理解为一个左值表达式一定表示了一块内存区域。可以使用取地址运算符 & 的表达式一定是左值表达式。rvalue不指向任何对象,仅表示一个值。

在最常见的赋值表达式中,即含有 = 赋值运算符的表达式,若需要该表达式合法,则需要满足

  • = 号左侧的表达式必须是左值
  • = 号右侧的表达式可以是左值也可以是右值

左值的特点

  • 可通过取地址运算符获取左值的地址。
  • 可出现在赋值或组合赋值符号的左边或右边。
  • 可修改的左值可用作赋值运算符的左操作数。

右值的特点

  • 无法对右值进行取地址操作。
  • 右值不能放在赋值或者组合赋值符号的左边。
  • 右值可以用来初始化const左值引用。

3.2. 左值与运算符

  • 变量名、数组名、函数名
    const 修饰符修饰的变量名,数组名和函数名都是不可被修改的左值。

  • 字符串常量
    C语言中的字符串常量是一个左值,可以对其进行取地址。
    例如:字符串 "hello world", 对该字符串取地址是合法的 &"hello world"

  • 解引用操作符表达式的结果
    解引用操作符会产生一个左值,但其操作数可以是左值或右值,即可以对解引用操作符的对象进行赋值操作。

    e.g. 解引用
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    int a = 3;
    int *ptr = &a;
    *ptr = 2; //*ptr 是左值

    /*
    *((int *)0x7ffead2b47a0)是一个左值
    但对0x7ffead2b47a0这个地址的值的含义尚不明确
    若该区域没有被引用,则会使程序发生崩溃
    */
    *((int *)0x7ffead2b47a0) = 1;

3.3. 右值与运算符

  • 各种基本数据类型的字面常量
    字面常量不一定会占据存储空间,编译器可能会将这些字面常量当做立即数进行处理,故字面常量是右值。

    e.g. 字面常量
    1
    2
    3
    int a = 1;         // 1为字面常量,是右值
    float b = 1.1f; // 1.1为字面常量,是右值
    char c = 'h'; // 'h'为字面常量,是右值
  • 枚举常量
    编译器常常将枚举常量当做是值确认的整形的字面常量进行处理,在编译阶段,枚举常量就会被替换为字面常量,故枚举常量也是右值。

    e.g. 枚举常量
    1
    2
    enum week{Mon, Tues, Wed, Thur, Fir, Sat, Sun};
    week tody = Sun; // Sun为枚举常量,是右值
  • 双目运算符的运算结果
    C语言中,双目运算符的操作数可以是左值或者右值,但其运算的结果必然是右值。

  • 函数调用表达式的返回值
    C语言中,函数调用表达式的值总是一个右值,无法对其进行取地址。

    e.g. 函数返回值
    1
    2
    3
    int func(void) { ... }

    int *a = &func(); //这是不合法的

4. 逗号表达式

逗号表达式是指使用逗号运算符的表达式,用于将表达式连接起来,是一种使编程更简洁的语法。其一般形式为:
[表达式1], [表达式2], [...]

e.g. 逗号表达式
1
2
3
4
5
6
7
8
9
10
11
12
int a = 10;
int b = 10;

a = 2*a, 4*a; //结果为 a = 20, 赋值运算符的优先级高于逗号运算符

b = (2*b, 4*b); //结果为 b = 40

//不是逗号表达式,这里的逗号并不是被用作逗号运算符,而是被用作传递实参的分隔符
printf("a = %d, b = %d\n", a, b);

// (a, b)为逗号表达式, 结果为 a = 40, b = 40;
printf("a = %d, b = %d\n", (a, b), b);

上述例子中 4 * a2 * b 均是无意义的表达式,其没有产生任何的副作用,若在编译时打开 -Wall 选项,便会看见 -Wunused-value 的警告。

4.1. 常见用法

e.g. 同类型变量声明
1
int a, b, c, d, e;
e.g. 循坏语句
1
2
3
for(i = 0, j = 1; i < 2 && j < 3; i++, j++)

while(i++, i < 10)
e.g. return 语句
1
return a = 1, ... , a;

5. 前缀 ++ / -- 与后缀 ++ / --

前缀形式的等效表达式如下:

1
2
3
4
5
int& int::operator++()
{
*this += 1;
return *this;
}

前缀形式返回的是自身加1后的值。

后缀形式的等效表达式如下:

1
2
3
4
5
6
const int int::operator++(int)
{
int oldValue = *this;
++(*this);
return oldValue;
}

后缀形式返回保存的先前自身的值,自身已进行加1操作。

这里举例 C++ 的实现是因为在 C++ 的语法中,前缀操作是一个左值,而后缀操作是一个右值,这一点与C有着区别,C中不管是前缀操作还是后缀操作,其都是右值。

e.g.
1
2
int a = 3;
++a = 4;

使用gcc编译,会出现 lvalue required as left operand of assignment 的报错,而使用g++编译则不会。