if (strcmp(language, "C"))
if语句是C/C++中非常常见的语句,它的效果是根据条件选择其中一个分支执行。我们先来复习一下C语言中的if语句的完整语法:
if (条件表达式)真-分支语句if (条件表达式)真-分支语句else假-分支语句
在C语言中,if括号内的条件必须是一个标量类型的表达式。标量类型包括以下类型:
- 字符类型
charsigned charunsigned char - 整数类型
shortintlonglong long以及它们对应无符号版本 - 枚举类型
enum - 浮点类型
floatdoublelong double以及它们对应的复数和虚数版本 - 指针类型
简而言之,只要表达式的类型能够与0进行相等性比较即可:当表达式的结果不等于0,就执行真-分支语句;否则,若存在假-分支语句,就执行假-分支语句。
1 | if (expr) // 等价于 if (expr != 0) |
if的两个分支必须是单条语句,最常见的做法当然是复合语句——将多条语句用花括号括起来,它们只算一条语句。
1 | // 常见易错点: |
从C99开始,if语句会建立一个作用域,在条件表达式中引入的名字可以在两个分支中访问,但离开if作用域就不再能访问了。并且,它的两个分支语句即使不是复合语句,也会将它当作如同复合语句一样建立一个局部作用域。
当然,由于条件必须是表达式,而C语言很难在表达式中引入新的名字,所以这一点其实很难体现出来,也就很少有人注意到。而且大多数情况下,if的分支都是复合语句,它们本来就会建立作用域。
1 |
|
在嵌套的if语句中,else总是与离他最近的未配对的if匹配。为了避免歧义,即使if的分支仅需一条表达式语句,也建议用花括号括起来。另外,善用编辑器的自动格式化功能,if各分支的缩进也能提示你它们是如何配对的。
1 |
|
另外,if - else if也是常见的用法,它并不是什么特殊的语法,只是在外层if语句的else分支又嵌套了另一个if语句。
1 | int a; scanf("%d", &a); |
最后,只要程序进入了真分支,不论是条件为真,还是你用goto语句跳过if的条件判断直接进入真分支,假分支都不会执行,执行完真分支总是会跳过假分支。
1 | goto true_branch; |
思考题:请问在C89和C99中,下列代码分别输出什么?(答案见文末)
1 |
|
if (language == "C++"s)
C++兼容99%的C语言if语义,并在此基础上进行了扩展。
if (条件)真-分支语句if (条件)真-分支语句else假-分支语句
先来看看条件的部分。C++从一开始就有bool类型,因此在if中也不再是将条件表达式与0进行比较,而是将结果隐式转换为bool。如果转换的结果是true,则执行真-分支语句;如果转换的结果是false,则执行假-分支语句。
由于if的条件期待一个bool值,这里的隐式转换也称为按语境转换。这是一种特殊的隐式转换,它与普通的隐式转换最大的区别在于它可以调用用户定义的explicit转换函数。当然,对于内置类型来说,C和C++的if行为完全一致——非0值隐式转换为true,0隐式转换为false。
1 | struct Flag { |
重载operator bool也是相当常见的做法,例如std::istream。它转换到bool的结果表示上一次读取操作是否成功,成功则为true,失败则为false。当然,这只是一个笼统的结果,如果想知道具体造成失败的原因,还是要查看std::istream的各个标志位。
1 |
|
然后,语法中只说了它是个条件,但并未强调表达式。在C++中,条件可以是一条简单声明。这里的简单声明有如下限制:它只能声明单个非数组变量,并且必须初始化。这样的if语句将根据该变量的初始值执行相应的分支。
1 |
|
可以在条件中定义变量,if自身建立的作用域的效果也更加明显了。C++的if语句建立的作用域与C语言的稍有区别。C语言会建立一个if自身的作用域,在条件表达式中定义的名字会被分支语句中相同的名字隐藏;但C++不会引入这么一个额外的作用域,它的作用域是其两个分支作用域的和。条件中定义的新名字仍然可以在两个分支中访问,但分支中再定义相同的名字会导致编译错误。
1 |
|
C++17:史诗级更新
C++17开始,if的功能获得了极大的扩展。
if constexprₒₚₜ (初始化语句ₒₚₜ 条件)真-分支语句if constexprₒₚₜ (初始化语句ₒₚₜ 条件)真-分支语句else假-分支语句
如果你觉得条件中只能声明单个非数组变量限制太大,那么你现在可以在条件前面加一条初始化语句。它可以是一条表达式语句,简单声明,或者从C++23开始还可一是using声明。表达式语句可以让你在判断之前执行一些初始化操作,而简单声明则可以定义变量、结构化绑定、甚至typedef。
1 |
|
1 |
|
除了初始化语句,C++17的另一项重要更新是 constexpr if。如前文所示,在if和括号之间加上一个constexpr关键字的形式叫做 constexpr if 语句。
constexpr if 语句的条件必须是一个常量表达式。如果求值为true,则舍弃假分支语句;反之求值为false,则舍弃真分支语句。
如果被舍弃的分支包含return语句,那么它们不会参与函数返回类型的推导。
1 | template<typename T> |
被舍弃语句可以 ODR 使用未定义的变量,但仍然至少要有声明。
1 | extern int x; |
利用这一特性,你可以用 constexpr if 来让不符合要求的模板引发静态断言。
1 | template <class T> |
被舍弃的语句仍然会经历完整语法检查,除了上述情况,其他违反语法规定的情况都会导致编译错误。所以 constexpr if 不是#if预处理器的的替代品,虽然它们在特定的场景能做到相同的效果。
C++23:常量求值语境补完计划
C++23又引入了consteval if 语句,它的语法规则如下。
if !ₒₚₜ consteval复合语句1if !ₒₚₜ consteval复合语句1else复合语句2
它的效果是:对于不带逻辑非运算符的版本,在明显常量求值语境中执行复合语句1;否则,如果有else分支,则执行复合语句2。对于带逻辑非运算符的版本,在非明显常量求值语境中执行复合语句1;否则,如果有else分支,则执行复合语句2。并且,在常量求值语境执行的分支是立即函数语境,即if consteval的复合语句1或者if !consteval的复合语句2。
1 |
|
那么重点就是这个明显常量求值语境,什么是明显常量求值语境呢?简单来说就是语法上要求常量表达式的地方。例如数组的长度、case标签后面的表达式、模板的非类型模板实参、上面提到的 constexpr if 的条件等,还有一些明显常量求值语境大家可以自行查阅相关文档,这里就不赘述了。
这个特性主要是为了补完std::is_constant_evaluated()的一些缺陷。例如它并不能让普通if的分支变成立即函数语境:
1 | consteval int f(int i) { return i; } |
再例如,它和constexpr if一起使用会造成意想不到的错误。刚才提到,constexpr if的条件是明显常量求值语境,因此std::is_constant_evaluated()会永远返回true。
1 | constexpr bool f() { |
if的替代品
condition ? expr1 : expr2
条件运算符也经常被称为三元运算符或三目运算符,因为它有三个操作数。与if类似,它也是根据条件表达式的求值结果来选择一个表达式求值。如果条件求值为true,则求值第一个表达式expr1;否则,求值第二个表达式expr2。它和if最大的区别在于,if是语句,而条件表达式是表达式。
1 | bool condition; |
当然,用函数将if包裹起来也能做到将if语句转化为表达式:
1 | int& max(int& a, int& b) { |
而C++11引入的lambda表达式则更加方便。这当然只是个简单的例子,如果需要进行复制的判断,那么lambda的优势就显现出来了。
1 | bool condition; |
另一个替代if的奇技淫巧是利用逻辑运算符的短路性质。称之为奇技淫巧并不为过,因为它不太直观,滥用会导致代码难以理解。一个比较常用的地方是利用这一性质避免解引用空指针。
1 | struct node { |
有时候,通过一些数学运算可以完全消除掉if语句,例如下面的例子,计算一个有符号整数的绝对值。将一个32位的有符号整数右移31位,如果它是正数,那肯定得到0;如果它是负数,肯定能得到-1。于是将这个结果乘以2加上1,整数得到1,负数得到-1,乘以自身就能取得它的绝对值。这个技巧也不是很推荐你使用,首要原因当然是它不直观。其次很多人认为消除if可以带来性能提升,这实际上是一种误解。在现代CPU的分支预测的加持下,if的性能比下面这段复杂的运算更快是很正常的事情。当然,一切都以实际的性能测试为准,没有测试的提前优化是万恶之源。
1 | int32_t abs(int32_t a) { |
参考资料
附:
思考题答案是编译错误。C语言
if的分支必须是“语句”,而C语言中声明不属于语句。C++有声明语句,允许这种代码(虽然没啥意义),并且会输出0。