C语言的重要特点: 强调表达式而不是语句

常见的表达式:

  • 变量和常量
  • a + (b * c)

4.1 算数运算符

特性说明代码
运算隐式类型转换intfloat运算: 结果为float
/截断性质1/2 == 0
/或者%的右操作数为0未定义行为
/或者%的操作数为负数结果不确定:
1. 向上/向下取整不确定 (C99确定向0取整)
2. -9%7可能是-2, 可能是5 (C99: 确定余数符号与被除数相同)
一元运算符二元运算符
+: 一元正号运算符
-: 一元负号运算符
+: 加法运算符
-: 减法运算符
*: 乘法运算符
/: 除法运算符
%: 求余运算符 (要求操作数必须是整数)

实例: 读取产品UPC

#include <stdio.h>

int main(void) {
    int d, i1, i2, i3, i4, i5, j1, j2, j3, j4, j5;
    int first_sum, second_sum, total;

    printf("Enter the first (single) digit: ");
    scanf("%1d", &d);
    printf("Enter first group of five digits: ");
    scanf("%1d%1d%1d%1d%1d", &i1, &i2, &i3, &i4, &i5);
    printf("Enter second group of five digits: ");
    scanf("%1d%1d%1d%1d%1d", &j1, &j2, &j3, &j4, &j5);

    first_sum = d + i2 + i4 + j1 + j3 + j5;
    second_sum = i1 + i3 + i5 + j2 + j4;
    total = 3 * first_sum + second_sum;

    printf("Check digit: %d\n", 9 - (total - 1) % 10);

    return 0;
}

运行结果

Enter the first (single) digit: 0
Enter first group of five digits: 13800
Enter second group of five digits: 15173
Check digit: 5

4.2 赋值运算符

4.2.1 简单赋值

特性说明代码
简单赋值通过=运算符将一个值赋值给变量

v = e (e可以是变量, 常量, 复杂表达式)
复合赋值使用复合赋值运算符的赋值操作 (右结合性)
比如: +=, -=, *=, /=, %=
赋值定义赋值在C语言中并不是语句, 而是运算符.
赋值操作产生结果:
1. 1 + 2产生的结果为3
2. v = e产生的结果为v的值
副作用运算符的其他功能
比如: =的副作用是改变左操作数的值

4.2.2 左值

特性说明代码
左值是一个指向某个内存位置的实体, 它可以出现在赋值语句的左侧

4.2.3 复合赋值

使用复合赋值运算符的赋值操作 (右结合性) 比如: +=, -=, *=, /=, %=

4.3 自增运算符和自减运算符

#C-point

特性说明代码
前缀自增/减运算符结果是: i + 1
副作用: 自增i
++i或者--i
后缀自增/减运算符结果是: i
副作用: 自增i
i++或者i--
后缀优先级高于前缀
后缀左结合
前缀右结合

实例 (重点)

i = 1;
j = 2;
k = ++i + j++;

自增运算符的优先级高于相加, 所以第三个语句相当于: k = (++i) + (j++)

再次拆分

i = i + 1; // 前缀自增
k = i + j;
j = j + 1; // 后缀自增

如果执行下述语句

i = 1;
j = 2;
k = i++ + j++;

第三个语句, 拆分

k = i + j;
i = i + 1;
j = j + 1;

4.4 表达式求值

没有括号的情况如何求表达式的值:

  • 优先给高优先级的运算符加上括号
  • 同优先级运算符根据结合性加上括号
a = b += c++ - d + --e / -f;
a = b += (c++) - d + (--e) / (-f); // 优先级
a = b += (c++) - d + ((--e) / (-f)); // 优先级
a = b += ((c++) - d) + ((--e) / (-f)); // 左结合性
(a = (b += ((c++) - d) + ((--e) / (-f)))); // 右结合性

4.5 表达式语句

C语言中任何表达式都可以用作语句

i++;

i * j - 1; // 没有语法错误, 但是也没有意义

(考点) 运算符的优先级和结合性

优先级与结合性定义: 术语表

优先级名称符号结合性
1数组取下标[]左结合性
1函数调用()左结合性
1取结构和联合的成员. ->左结合性
1自增(后缀)++左结合性
1自减(后缀)--左结合性
2自增(前缀)++结合性
2自减(前缀)--结合性
2取地址&结合性
2间接寻址*结合性
2一元正号+结合性
2一元负号-结合性
2按位求反~结合性
2逻辑非!结合性
2计算所需空间sizeof结合性
3强制类型转换()右结合性
4乘法类运算符* / %左结合性
5加法类运算符+ -左结合性
6移位运算符<< >>左结合性
7关系运算符< > <= >=左结合性
8判等运算符== !=左结合性
9按位与&左结合性
10按位异或^左结合性
11按位或|左结合性
12逻辑与&&左结合性
13逻辑或||左结合性
14条件运算符?:结合性
15赋值运算符=, *=, /=, %=
+=,-=, <<=, >>=,
&=, ^=, |=
结合性
16逗号,左结合性

练习题

答案链接:

  • http://knking.com/books/c2/answers/c4.html
  • https://github.com/williamgherman/c-solutions/tree/master/04

记号说明:

  • 加粗(错误): 表示错误的答案或者遗漏的知识点
  • 在代码块中(错误): 表示错误的答案

practice1-output

题目: 给出下列程序片段的输出结果。假设i 、j 和k 都是int 型变量

i = 5; j = 3;
printf("%d %d", i / j, i % j);

i = 2; j = 3;
printf("%d", (i + 10) % j);

i = 7; j = 8; k = 9;
printf("%d", (i + 10) % k / j);

i = 1; j = 2; k = 3;
printf("%d", (i + 5) % (j + 2) / k);

输出结果:

1 2
0
1
0

practice2-div

题目: 如果i 和j 都是正整数,(-i) / j 的值和-(i / j) 的值是否总一样? 验证你的答案

基本知识点:

  • C89: 不确定行为, 向零取整或者向下取整
  • C99及以后: 向零舍入

回答:

  • 假设i = 3, j = 2
  • C89向下截断:
    • (-i) / j = -3 / 2 = -2
    • -(i / j) = -(3 / 2) = -1
  • C99向零截断:
    • (-i) / j = -3 / 2 = -1
    • -(i / j) = -(3 / 2) = -1
  • 结论: 不一定相同

practice3-C89div

下列表达式在C89中的值是多少

  1. 8 / 5
  2. -8 / 5
  3. 8 / -5
  4. -8 / -5

回答:

1
-2或者-1
-2或者-1
1 (错误: 1或者2, 因为-8/-5仍然属于含有负数的求余操作, 所以仍然存在两种可能)

practice4-C99div

  1. 8 / 5
  2. -8 / 5
  3. 8 / -5
  4. -8 / -5

回答:

1
-1
-1
1

practice5- C89rem

求余符号处理区别:

  • C89: 无明确定义
  • C99: 余数符号与除数符号相同

题目:

  1. 8 % 5
  2. -8 % 5
  3. 8 % -5
  4. -8 % -5

回答:

3
-3或者5
-3或者5 (错误: 3或者-5, 理解为 8 - 5 = 3)
3或者-5 (错误: -3或者5)

practice6-C99rem

  1. 8 % 5
  2. -8 % 5
  3. 8 % -5
  4. -8 % -5

回答:

3
-3
-3 (错误: 3)
-5 (错误: -3)

practice7-UPC2 *

本章计算UPC校验位的方法最后几步是:把总的结果减去1,相减后的结果除以10取余数,用9减去余数。

换成下面的步骤也可以:总的结果除以10取余数,用10减去余数。这样做为什么不可行

int check = 9 - ((total - 1) % 10);
int check_2 = 10 - (total % 10);

回答:

  1. 特殊值法: 假设total % 10 == 0, 则check_2为10 (错误)

practice8-UPC3

如果把表达式9 - ((total - 1) % 10) 改成(10 - (total % 10)) % 10 ,upc.c 程序是否仍然正确?

int check = 9 - ((total - 1) % 10);
int check_2 = (10 - (total % 10)) % 10;

回答: 正确, 多出的% 10可以消除当total为10的倍数时的影响. (错误)

practice9-output2

给出下列程序片段的输出结果。假设i 、j 和k 都是int 型变量

i = 7; j = 8;
i *= j + 1;
printf("%d %d", i, j);

i = j = k = 1;
i += j += k;
printf("%d %d %d", i, j, k);

i = 1; j = 2; k = 3;
i -= j -= k;
printf("%d %d %d", i, j, k);

i = 2; j = 1; k = 0;
i *= j *= k;
printf("%d %d %d", i, j, k);

回答:

63 8
3 2 1
2 -1 3
0 0 0

practice10-output3

给出下列程序片段的输出结果。假设i 和j 都是int 型变量

i = 6;
j = i += i;
printf("%d %d", i, j);

i = 5;
j = (i -= 2) + 1;
printf("%d %d", i, j);

i = 7;
j = 6 + (i = 2.5);
printf("%d %d", i, j);

i = 2; j = 8;
j = (i = 6) + (j = 3);
printf("%d %d", i, j);

回答:

12 12
3 4
2 8 
6 9

practice11-output4

给出下列程序片段的输出结果。假设i 、j 和k 都是int 型变量

i = 1;
printf("%d ", i++ - 1);
printf("%d", i);

i = 10; j = 5;
printf("%d ", i++ - ++j);
printf("%d %d", i, j);

i = 7; j = 8;
printf("%d ", i++ - --j);
printf("%d %d", i, j);

i = 3; j = 4; k = 5;
printf("%d ", i++ - j++ + --k); // (i++ - j++) + --k
printf("%d %d %d", i, j, k);

回答:

0 2
4 11 6
0 8 7
3 4 5 4

practice12-output5

给出下列程序片段的输出结果。假设i 和j 都是int 型变量

i = 5;
j = ++i * 3 - 2; // ((++i) * 3) - 2
printf("%d %d", i, j);

i = 5;
j = 3 - 2 * i++;
printf("%d %d", i, j);

i = 7;
j = 3 * i-- + 2;
printf("%d %d", i, j);

i = 7;
j = 3 + --i * 2;
printf("%d %d", i, j);

回答:

6 16
6 -7
6 23
6 15

practice13-exp1

表达式++ii++中只有一个是与表达式(i += 1)完全相同的,是哪一个呢?验证你的答案。

回答: ++i(i += 1)完全相同, 主要从表达式值判断, (i += 1)表达式返回的是i自增以后的结果, 所以与前缀自增相同

practice14-exp2

添加圆括号,说明C语言编译器如何解释下列表达式

  1. a * b - c * d + e
  2. a / b % c / d
    • a - b + c - + d
  3. a * - b / c - d
/*
* 目标是表达式, 所以不需要跟;
*/

// 1
// 优先级处理
(a * b) - (c * d) + e
// 结合性处理, 加减法是左结合性
(((a * b) - (c * d)) + e)

// 2
// 优先级处理: 乘除余为同优先级
// 结合性处理: 均为左结合性
(((a / b) % c) / d)

// 3
// 优先级处理
(-a) - b + c - (+d)
// 结合性处理: 均为左结合性
((((-a) - b) + c) - (+d))

// 4
// 优先级处理
a * (-b) / c - d
// 结合性 + 优先级处理
(((a * (-b)) / c ) - d)

practice15-exp3

给出下列每条表达式语句执行以后i 和j 的值。(假设i 的初始值为1,j 的初始值为2。)

  1. i += j;
  2. i--;
  3. i * j / i;
  4. i % ++j;

回答:

  1. i = 3, j = 2
  2. i = 0, j = 2
  3. i = 1, j = 2
  4. i = 1, j = 3

编程题

code1-reverse1

编写一个程序,要求用户输入一个两位数,然后按数位的逆序打印出这个数。程序会话应类似下面这样:

用%d读入两位数,然后分解成两个数字。

Enter a two-digit number: 28

The reversal is: 82

代码:

#include <stdio.h>

int main(void) {
    int num;

    printf("Enter a two-digit number: ");
    scanf("%d", &num);

    printf("\nThe reverse is: %d%d\n", num % 10, num / 10);

    return 0;
}

编译并运行:

Enter a two-digit number: 28

The reverse is: 82

code2-reverse2

扩展上题中的程序使其可以处理3位数。

代码:

#include <stdio.h>

int main(void) {
    int num;

    printf("Enter a Three-digit number: ");
    scanf("%d", &num);

    printf("\nThe reverse is: %d%d%d\n", num % 10, (num / 10) % 10, num / 100);

    return 0;
}

编译并运行:

Enter a Three-digit number: 257

The reverse is: 752

code3-reverse3

重新编写编程题2中的程序,使新程序不需要利用算术分割就可以显示出3位数的逆序。

提示 :参考4.1节的upc.c 程序。

代码:

#include <stdio.h>

int main(void) {
    int d1, d2, d3;

    printf("Enter a Three-digit number: ");
    scanf("%1d%1d%1d", &d1, &d2, &d3);

    printf("\nThe reverse is: %d%d%d\n", d3, d2, d1);

    return 0;
}

编译并运行:

Enter a Three-digit number: 123

The reverse is: 321

code4-octal

编写一个程序,读入用户输入的整数并按八进制(基数为8)显示出来:

输出应为5位数,即便不需要这么多数位也要如此。提示 :要把一个数转换成八进制,首先将其除以8,所得的余数是八进制数的最后一位(本例中为1);然后把原始的数除以8,对除法结果重复上述过程,得到倒数第二位。(如第7章所示,printf可以显示八进制的数,所以这个程序实际上有更简单的写法。)

Enter a number between 0 and 32767: 1953

In octal, your number is: 03641

代码:

#include <stdio.h>

int main(void) {
    int num;

    printf("Enter a number between 0 and 32767: ");
    scanf("%d", &num);

    printf("\nIn octal, your number is: %d%d%d%d%d\n", 
            (num / (64 * 64)), 
            (num / (64 * 8)) % 8, 
            (num / 64) % 8, 
            (num / 8) % 8, 
            num % 8);

    return 0;
}

编译并运行:

Enter a number between 0 and 32767: 1953

In octal, your number is: 03641

code5-UPC2

重写4.1节的upc.c 程序,使用户可以一次输入11位数字,而不用先录入1位,再录入5位,最后再录入5位。

Enter the first 11 digits of a UPC: 01380015173

Check digit: 5

代码:

#include <stdio.h>

int main(void) {
    int i[5] ,j[5], d;
    int first_sum, second_sum, total;


    printf("Enter the first 11 digits of a UPC: ");
    scanf("%1d%1d%1d%1d%1d%1d%1d%1d%1d%1d%1d", &d, &j[0], &i[0], &j[1], &i[1], &j[2], &i[2], &j[3], &i[3], &j[4], &i[4]);

    first_sum = d + i[0] + i[1] + i[2] + i[3] + i[4];
    second_sum = j[0] + j[1] + j[2] + j[3] + j[4];
    total = 3 * first_sum + second_sum;

    printf("\nCheck digit: %d\n", 9 - (total - 1) % 10);

    return 0;
}

编译并运行:

Enter the first 11 digits of a UPC: 01380015173

Check digit: 5

code6-EAN

欧洲国家不使用北美的12位通用产品代码(UPC),而使用13位的欧洲商品编码(European Article Number, EAN)。跟UPC一样,每个EAN码的最后也有一个校验位。计算校验位的方法也类似:

  1. 首先把第2位、第4位、第6位、第8位、第10位和第12位数字相加;
  2. 然后把第1位、第3位、第5位、第7位、第9位和第11位数字相加;
  3. 接着把第一次加法的结果乘以3,再和第二次加法的结果相加;
  4. 随后,再把上述结果减去1;
  5. 相减后的结果除以10取余数;
  6. 最后用9减去上一步骤中得到的余数。

以Güllüoglu Turkish Delight Pistachio & Coconut为例,其EAN码为8691484260008。

  1. 第一个和为6+1+8+2+0+0=17,
  2. 第二个和为8+9+4+4+6+0=31。
  3. 第一个和乘以3再加上第二个和得到82,
  4. 减1得到81。
  5. 这个结果除以10的余数是1,
  6. 再用9减去余数得到8,

与原始编码的最后一位一致。请修改4.1节的upc.c 程序以计算EAN的校验位。用户把EAN的前12位当作一个数输入:

Enter the first 12 digits of an EAN: 869148426000

Check digit: 8

代码:

#include <stdio.h>

int main(void) {
    int i[6], j[6];
    int first, second, total;

    printf("Enter the first 12 digits of an EAN: ");
    scanf("%1d%1d%1d%1d%1d%1d%1d%1d%1d%1d%1d%1d", 
    i, j, i + 1, j + 1, 
    i + 2, j + 2, i + 3, j + 3, 
    i + 4, j + 4, i + 5, j + 5);

    first = j[0] + j[1] + j[2] + j[3] + j[4] + j[5];
    second = i[0] + i[1] + i[2] + i[3] + i[4] + i[5];
    total = (first * 3 + second) - 1;

    printf("\nCheck digit: %d\n", 9 - (total % 10));

    return 0;
}

编译并运行:

Enter the first 12 digits of an EAN: 869148426000

Check digit: 8