3.1 基础复习:表达式、语句和代码块

表达式

表达式和语句是 C 语言中的基础概念。什么是表达式呢?表达式就是由一系列操作符和操作数构成的式子。操作符可以是 C 语言标准规定的各种算术运算符、逻辑运算符、赋值运算符、比较运算符等。操作数可以是一个常量,也可以是一个变量。表达式也可以没有操作符,单独的一个常量甚至是一个字符串,也是一个表达式。下面的字符序列都是表达式:

 + 

i =  +
i = i++ +
"wit"

表达式一般用来数据计算或实现某种功能的算法。表达式有2个基本属性:值和类型。如上面的表达式2+3,它的值为5。根据操作符的不同,表达式可以分为多种类型,如:

  • 关系表达式
  • 逻辑表达式
  • 条件表达式
  • 赋值表达式
  • 算术表达式
  • ……

语句

语句是构成程序的基本单元,一般形式如下:

表达式 ;
i = + ;

表达式的后面加一个; 就构成了一条基本的语句。编译器在编译程序、解析程序时,不是根据物理行,而是根据分号 ; 来判断一条语句的结束标记的。如 i = 2 + 3; 这条语句,你写成下面的形式也是可以编译通过的:

i =
+ ;

代码块

不同的语句,使用大括号{}括起来,就构成了一个代码块。C 语言允许在代码块里定义一个变量,这个变量的作用域也仅限于这个代码块内,因为编译器就是根据{}来做入栈出栈操作来管理变量的作用域的。如下面的程序:

int main(void)
{
int i = ;
printf("i=%d\n",i);
{
int i = ;
printf("i=%d\n",i);
}
printf("i=%d\n",i);
return ;
}

运行结果为:
i=3
i=4
i=3

3.2 语句表达式

语句表达式的定义

GNU C 对 C 标准作了扩展,允许在一个表达式里内嵌语句,允许在表达式内部使用局部变量、for 循环和 goto 跳转语句。这样的表达式,我们称之为语句表达式。语句表达式的格式如下:

({ 表达式1; 表达式2; 表达式3; })

语句表达式最外面使用小括号()括起来,里面一对大括号{}包起来的是代码块,代码块里允许内嵌各种语句。语句的格式可以是 “表达式;”这种一般格式的语句,也可以是循环、跳转等语句。

跟一般表达式一样,语句表达式也有自己的值。语句表达式的值为内嵌语句中最后一个表达式的值。我们举个例子,使用语句表达式求值。

int main(void)
{
int sum = ;
sum =
({
int s = ;
for( int i = ; i < ; i++)
s = s + i;
s;
});
printf("sum = %d\n",sum);
return ;
}

在上面的程序中,通过语句表达式实现了从1到10的累加求和,因为语句表达式的值等于最后一个表达式的值,所以在 for 循环的后面,我们要添加一个 s; 语句表示整个语句表达式的值。如果不加这一句,你会发现 sum=0。或者你将这一行语句改为100; 你会发现最后 sum 的值就变成了100,这是因为语句表达式的值总等于最后一个表达式的值。

语句表达式内使用 goto 跳转

在上面的程序中,我们在语句表达式内定义了局部变量,使用了 for 循环语句。在语句表达式内,我们同样也可以使用 goto 进行跳转。

int main(void)
{
int sum = ;
sum =
({
int s = ;
for( int i = ; i < ; i++)
s = s + i;
goto here;
s;
});
printf("sum = %d\n",sum);
here:
printf("here:\n");
printf("sum = %d\n",sum);
return ;
}

3.3 在宏定义中使用语句表达式

语句表达式的亮点在于定义复杂功能的宏。使用语句表达式来定义宏,不仅可以实现复杂的功能,而且还能避免宏定义带来的歧义和漏洞。下面就以一个宏定义例子,让我们来见识见识语句表达式在宏定义中的强悍杀伤力!

假如你此刻正在面试,面试职位是:Linux C 语言开发工程师。面试官给你出了一道题:

请定义一个宏,求两个数的最大值。

别看这么简单的一个考题,面试官就能根据你写出的宏,来判断你的 C 语言功底,来决定给不给你 Offer。

合格

对于学过 C 语言的同学,写出这个宏基本上不是什么难事,使用条件运算符就能完成:

#define MAX(x,y) x > y ? x : y

这是最基本的 C 语言语法,如果连这个也写不出来,估计场面会比较尴尬。面试官为了缓解尴尬,一般会对你说:小伙子,你很棒,回去等消息吧,有消息,我们会通知你!这时候,你应该明白:不用再等了,赶紧把这篇文章看完,接着面下家。这个宏能写出来,也不要觉得你很牛 X,因为这只能说明你有了 C 语言的基础,但还有很大的进步空间。比如,我们写一个程序,验证一下我们定义的宏是否正确:

#define MAX(x,y) x > y ? x : y
int main(void)
{
printf("max=%d",MAX(,));
printf("max=%d",MAX(,));
printf("max=%d",MAX(,));
printf("max=%d",MAX(!=,!=));
return ;
}

测试程序吗,我们肯定要把各种可能出现的情况都测一遍。这不,测试第4行语句,当宏的参数是一个表达式,发现实际运行结果为 max=0,跟我们预期结果 max=1 不一样。这是因为,宏展开后,就变成了这个样子:

printf("max=%d",!=>!=?!=:!=);

因为比较运算符 > 的优先级为6,大于 !=(优先级为7),所以展开的表达式,运算顺序发生了改变,结果就跟我们的预期不一样了。为了避免这种展开错误,我们可以给宏的参数加一个小括号()来防止展开后,表达式的运算顺序发生变化。这样的宏才能算一个合格的宏:

#define MAX(x,y) (x) > (y) ? (x) : (y)

中等

上面的宏,只能算合格,但还是存在漏洞。比如,我们使用下面的代码测试:

#define MAX(x,y) (x) > (y) ? (x) : (y)
int main(void)
{
printf("max=%d", + MAX(,));
return ;
}

在程序中,我们打印表达式 3 + MAX(1, 2) 的值,预期结果应该是5,但实际运行结果却是1。我们展开后,发现同样有问题:

 + () > () ? () : ();

因为运算符 + 的优先级大于比较运算符 >,所以这个表达式就变为4>2?1:2,最后结果为1也就见怪不怪了。此时我们应该继续修改这个宏:

#define MAX(x,y) ((x) > (y) ? (x) : (y))

使用小括号将宏定义包起来,这样就避免了当一个表达式同时含有宏定义和其它高优先级运算符时,破坏整个表达式的运算顺序。如果你能写到这一步,说明你比前面那个面试合格的同学强,前面那个同学已经回去等消息了,我们接着面试下一轮。

良好

上面的宏,虽然解决了运算符优先级带来的问题,但是仍存在一定的漏洞。比如,我们使用下面的测试程序来测试我们定义的宏:

#define MAX(x,y) ((x) > (y) ? (x) : (y))
int main(void)
{
int i = ;
int j = ;
printf("max=%d",MAX(i++,j++));
return ;
}

在程序中,我们定义两个变量 i 和 j,然后比较两个变量的大小,并作自增运算。实际运行结果发现 max = 7,而不是预期结果 max = 6。这是因为变量 i 和 j 在宏展开后,做了两次自增运算,导致打印出 i 的值为7。

遇到这种情况,那该怎么办呢? 这时候,语句表达式就该上场了。我们可以使用语句表达式来定义这个宏,在语句表达式中定义两个临时变量,分别来暂储 i 和 j 的值,然后进行比较,这样就避免了两次自增、自减问题。

#define MAX(x,y)({     \
int _x = x; \
int _y = y; \
_x > _y ? _x : _y; \
})
int main(void)
{
int i = ;
int j = ;
printf("max=%d",MAX(i++,j++));
return ;
}

在语句表达式中,我们定义了2个局部变量 _x、_y 来存储宏参数 x 和 y 的值,然后使用 _x 和 _y 来比较大小,这样就避免了 i 和 j 带来的2次自增运算问题。

你能坚持到了这一关,并写出这样自带 BGM 的宏,面试官心里可能已经有了给你 Offer 的意愿了。但此时此刻,千万不要骄傲!为了彻底打消面试官的心理顾虑,我们需要对这个宏继续优化。

优秀

在上面这个宏中,我们定义的两个临时变量数据类型是 int 型,只能比较两个整型的数据。那对于其它类型的数据,就需要重新再定义一个宏了,这样太麻烦了!我们可以基于上面的宏继续修改,让它可以支持任意类型的数据比较大小:

#define MAX(type,x,y)({     \
type _x = x; \
type _y = y; \
_x > _y ? _x : _y; \
})
int main(void)
{
int i = ;
int j = ;
printf("max=%d\n",MAX(int,i++,j++));
printf("max=%f\n",MAX(float,3.14,3.15));
return ;
}

在这个宏中,我们添加一个参数:type,用来指定临时变量 _x 和 _y 的类型。这样,我们在比较两个数的大小时,只要将2个数据的类型作为参数传给宏,就可以比较任意类型的数据了。如果你能在面试中,写出这样的宏,面试官肯定会非常高兴,他一般会跟你说:稍等,待会 HR 会跟你谈待遇问题。

还能不能更牛逼?

如果你想薪水拿得高一点,待遇好一点,此时不应该骄傲,你应该大手一挥:且慢,我还可以更牛逼!

上面的宏定义中,我们增加了一个type类型参数,来兼容不同的数据类型,此时此刻,为了薪水,我们应该把这个也省去。如何做到?使用typeof就可以了,typeof是GNU C新增的一个关键字,用来获取数据类型,我们不用传参进去,让typeof直接获取!

#define max(x, y) ({   \
typeof(x) _x = (x); \
typeof(y) _y = (y); \
(void) (&_x == &_y);\
_x > _y ? _x : _y; })

在这个宏定义中,使用了 typeof 关键字用来获取宏的两个参数类型。干货在(void) (&x == &y);这句话,简直是天才般的设计!一是用来给用户提示一个警告,对于不同类型的指针比较,编译器会给一个警告,提示两种数据类型不同;二是,当两个值比较,比较的结果没有用到,有些编译器可能会给出一个warning,加个(void)后,就可以消除这个警告!

此刻,面试官看到你的这个宏,估计会倒吸一口气:乖乖,果然是后生可畏,这家伙比我还牛逼!你等着,HR待会过来跟你谈薪水!

恭喜你,拿到offer了!

嵌入式C语言自我修养 03:宏构造利器:语句表达式的更多相关文章

  1. 嵌入式C语言自我修养 04:Linux 内核第一宏:container_of

    4.1 typeof 关键字 ANSI C 定义了 sizeof 关键字,用来获取一个变量或数据类型在内存中所占的存储字节数.GNU C 扩展了一个关键字 typeof,用来获取一个变量或表达式的类型 ...

  2. 嵌入式C语言自我修养 13:C语言习题测试

    13.1 总结 前面12节的课程,主要针对 Linux 内核中 GNU C 扩展的一些常用 C 语言语法进行了分析.GNU C 的这些扩展语法,主要用来完善 C 语言标准和编译优化.而通过 C 标准的 ...

  3. 嵌入式C语言自我修养 10:内联函数探究

    10.1 属性声明:noinline & always_inline 这一节,接着讲 __atttribute__ 属性声明,__atttribute__ 可以说是 GNU C 最大的特色.我 ...

  4. 嵌入式C语言自我修养 01:Linux 内核中的GNU C语言语法扩展

    1.1 Linux 内核驱动中的奇怪语法 大家在看一些 GNU 开源软件,或者阅读 Linux 内核.驱动源码时会发现,在 Linux 内核源码中,有大量的 C 程序看起来“怪怪的”.说它是C语言吧, ...

  5. 嵌入式C语言自我修养 06:U-boot镜像自拷贝分析:section属性

    6.1 GNU C 的扩展关键字:attribute GNU C 增加一个 __atttribute__ 关键字用来声明一个函数.变量或类型的特殊属性.声明这个特殊属性有什么用呢?主要用途就是指导编译 ...

  6. 嵌入式C语言自我修养 12:有一种宏,叫可变参数宏

    12.1 什么是可变参数宏 在上面的教程中,我们学会了变参函数的定义和使用,基本套路就是使用 va_list.va_start.va_end 等宏,去解析那些可变参数列表我们找到这些参数的存储地址后, ...

  7. 嵌入式C语言自我修养 11:有一种函数,叫内建函数

    11.1 什么是内建函数 内建函数,顾名思义,就是编译器内部实现的函数.这些函数跟关键字一样,可以直接使用,无须像标准库函数那样,要 #include 对应的头文件才能使用. 内建函数的函数命名,通常 ...

  8. 嵌入式C语言自我修养 02:Linux 内核驱动中的指定初始化

    2.1 什么是指定初始化 在标准 C 中,当我们定义并初始化一个数组时,常用方法如下: ] = {,,,,,,,,}; 按照这种固定的顺序,我们可以依次给 a[0] 和 a[8] 赋值.因为没有对 a ...

  9. 嵌入式C语言自我修养 05:零长度数组

    5.1 什么是零长度数组 顾名思义,零长度数组就是长度为0的数组. ANSI C 标准规定:定义一个数组时,数组的长度必须是一个常数,即数组的长度在编译的时候是确定的.在ANSI C 中定义一个数组的 ...

随机推荐

  1. Java反射机制动态代理

    1.什么事反射机制动态代理 在一段代码的前后动态执行其他操作,比如有一个方法是往数据库添加一个记录,我们可以通过动态代理,在操作数据库方法的前和后添加代码执行打开数据库连接和关闭数据库连接. 2.演示 ...

  2. 180328_gVim设置tab为4个空格

    找到gVim安装目录 C:\Program Files (x86)\Vim\ 更改目录下的 _vimrc 文件,这是一个只读文件,需要管理员权限进行修改和保存. 在文件末尾添加下列内容: set ts ...

  3. Mouse For Winpad

    Mouse For Winpad 用WIN10的平板模式玩游戏总会到一些兼容性问题,之前在推Re:LieF的时候就遇到了触屏不能像鼠标指针一样进行游戏,于是猜想一定是程序没进行标准化MFC的编程.于是 ...

  4. 二、vue学习--父元素如何获取子元素的值,子元素如何获取父元素的值

      下图是父元素: 下图是子元素,获取父元素的值,使用props定义属性,这样就可以获取到父元素上传过来的set .place.type,拿到值就可以做一些自己的逻辑处理 二.子元素给父元素传值? 下 ...

  5. 安装nvm之后node不可用,“node”不是内部或外部命令,也不是可运行的程序或批处理文件(ng)

    安装nvm: 1.下载nvm压缩包地址:https://github.com/coreybutler/nvm-windows/releases 2.下载后解压在目标文件夹中,我这里是H:\applic ...

  6. Android组件系列----Activity的生命周期

    [声明] 欢迎转载,但请保留文章原始出处→_→ 生命壹号:http://www.cnblogs.com/smyhvae/ 文章来源:http://www.cnblogs.com/smyhvae/p/3 ...

  7. Python DDT(data driven tests)模块心得

    关于ddt模块的一些心得,主要是看官网的例子,加上一点自己的理解,官网地址:http://ddt.readthedocs.io/en/latest/example.html ddt(data driv ...

  8. Linux查看磁盘读写

    ---------- 查看磁盘读写---------iostat -k 1 SQL> ho iostatLinux 2.6.32-279.el6.x86_64 (server-92)  08/1 ...

  9. [UI] 精美UI界面欣赏[3]

    精美UI界面欣赏[3]

  10. Linux 系统级别优化_【all】

    Linux 系统优化 1.Linux系统关闭SELinux 2.Linux系统开机到登录之前启动流程 3.Linux系统设置运行级别 4.Linux系统重要的开机自启动的服务 5.Linux查看系统当 ...