系列文章目录
文章目录
1. 操作符分类
2. 算术操作符
在 C 语言中,算术操作符用于执行基本的数学运算,包括加法、减法、乘法、除法和取模运算。这些操作符是 C 语言中最常用的操作符之一,它们可以应用于整型和浮点型数据。下面详细介绍每个算术操作符及其用途:
2.1 加法操作符 +
加法操作符用于计算两个数的总和。当操作数为数值类型时,结果是它们的算术和。
int a = 5;
int b = 3;
int sum = a + b; // sum 的值为 8
2.2 减法操作符 -
减法操作符用于计算两个数的差。它从第一个操作数中减去第二个操作数。
int a = 5;
int b = 3;
int difference = a - b; // difference 的值为 2
2.3 乘法操作符 *
乘法操作符用于计算两个数的乘积。
int a = 5;
int b = 3;
int product = a * b; // product 的值为 15
2.4 除法操作符 /
除法操作符用于两个数的除法运算。需要注意的是,如果两个操作数都是整数,那么执行的将是整数除法,结果将被截断为整数。如果操作数中至少有一个是浮点数,那么执行的将是浮点除法,结果是浮点数。
int a = 10;
int b = 3;
int quotient = a / b; // quotient 的值为 3(整数除法)
double c = 10.0;
double result = c / b; // result 的值为 3.333333(浮点除法)
2.5 取模操作符 %
取模操作符用于计算两个整数相除的余数。注意,取模操作符只能用于整数。
int a = 10;
int b = 3;
int remainder = a % b; // remainder 的值为 1
3. 移位操作符
在 C 语言中,移位操作符用于将位模式(即二进制数)向左或向右移动指定的位数。这些操作符非常有用,特别是在底层编程、优化和处理位级操作时。C 语言提供了两种类型的移位操作符:左移位操作符 (<<
) 和右移位操作符 (>>
)。
3.1 左移位操作符 (<<
)
左移位操作符 (<<
) 将位模式向左移动指定的位数。在左移时,最右边的空位被填充为0。左移一位的效果等同于将数值乘以2,因此左移操作经常被用来高效地计算乘法。
result = operand << num_bits;
operand
是要移动的数。num_bits
是要移动的位数。result
是操作后的结果。
unsigned int a = 3; // 二进制表示为 0000 0011
unsigned int result = a << 2; // 结果为 0000 1100,相当于 3 * 2^2 = 12
3.2 右移位操作符 (>>
)
右移位操作符 (>>
) 将位模式向右移动指定的位数。右移的填充位取决于数字的类型(有符号还是无符号):
- 对于无符号数,空缺位将填充0。
- 对于有符号数,右移通常是算术右移,空缺位填充符号位(即最左边的位),以保持数的符号不变。
result = operand >> num_bits;
operand
是要移动的数。num_bits
是要移动的位数。result
是操作后的结果。
示例代码:
unsigned int a = 12; // 二进制表示为 0000 1100
unsigned int result = a >> 2; // 结果为 0000 0011,相当于 12 / 2^2 = 3
int b = -13; // 例如,二进制可能表示为 1111 0011(取决于具体的系统)
int result_neg = b >> 2; // 结果可能为 1111 1100,注意符号位的保持
- 快速乘除法:使用移位操作来快速实现乘以2的幂或除以2的幂。
- 位域操作:直接操作数据的位模式,例如在设定或清除特定的标志位时。
- 数据打包与解包:在网络通信中对数据进行打包和解包。
- 资源优化:在资源有限的设备上优化性能和内存使用。
注意:
- 移位操作数(
num_bits
)过大可能导致未定义的行为。确保移位的位数小于操作数的位宽。 - 对于负整数的右移操作,行为依赖于编译器和平台,因为 C 标准没有规定负数的右移行为。
- 滥用移位操作可能导致数据丢失,特别是当超出数据类型的位长度时。
4. 位操作符
位操作符在 C 语言中用于执行位级操作,这意味着这些操作是直接在操作数的二进制表示上进行的。C 语言提供了几个位操作符:位与(AND)、位或(OR)、位异或(XOR)和位非(NOT)。
4.1 位与操作符 &
位与操作符用于对两个操作数进行逐位与操作。只有在两个相应的位都为1时,结果的那位才为1,否则为0。
result = operand1 & operand2;
代码示例:
int a = 12; // 二进制表示为 1100
int b = 10; // 二进制表示为 1010
int result = a & b; // 结果为 1000 (8)
4.2 位或操作符 |
位或操作符用于对两个操作数进行逐位或操作。如果两个相应的位中至少有一个为1,那么结果的那位就为1,否则为0。
result = operand1 | operand2;
代码示例:
int a = 12; // 二进制表示为 1100
int b = 10; // 二进制表示为 1010
int result = a | b; // 结果为 1110 (14)
4.3 位异或操作符 ^
位异或操作符用于对两个操作数进行逐位异或操作。如果两个相应的位不同,那么结果的那位就为1;如果相同,那么结果的那位就为0。
result = operand1 ^ operand2;
代码示例:
int a = 12; // 二进制表示为 1100
int b = 10; // 二进制表示为 1010
int result = a ^ b; // 结果为 0110 (6)
4.4 位非操作符 ~
位非操作符(或称位取反)是一个单目操作符,用于对操作数的每一位进行取反操作。1变0,0变1。
result = ~operand;
代码示例:
int a = 12; // 二进制表示为 1100
int result = ~a; // 结果为 ...11110011 (依赖于数据类型和系统,假设是32位int,则为 -13)
4.5 练习
编写代码实现:求一个整数存储在内存中的二进制中1的个数。
方法 1: 基于除以2的循环
这种方法只适用于非负整数。对于负数,由于整数除法的特性(舍去小数部分),可能会导致无限循环。
#include <stdio.h>
int main()
{
int num = 10; // 初始化数字
int count = 0; // 用于计数num的二进制表示中1的个数
while (num) // 当num不为0时,执行循环
{
if (num % 2 == 1) // 如果当前最低位是1,计数器增加
count++;
num = num / 2; // num除以2,右移一位,丢弃最低位
}
printf("二进制中1的个数 = %d\n", count);
return 0;
}
方法 2: 位掩码检查
这种方法正确处理了正数和负数。循环固定32次,这对于32位整数是有效的,但对于不同的数据类型可能不适用或效率不高。
#include <stdio.h>
int main()
{
int num = -1; // 初始化数字,这里是-1
int count = 0; // 计数器
for (int i = 0; i < 32; i++) // 循环检查每一位是否为1
{
if (num & (1 << i)) // 将1左移i位,使用位与操作检查第i位是否为1
count++;
}
printf("二进制中1的个数 = %d\n", count);
return 0;
}
方法3:
这种方法被认为是最高效的方法之一,因为它的迭代次数直接依赖于1的个数。每次操作都清除最右边的1,直到num变为0。它同样适用于正数和负数。
#include <stdio.h>
int main()
{
int num = -1; // 初始化数字为-1
int count = 0; // 计数器
while (num) // 当num不为0时
{
count++; // 增加计数器
num = num & (num - 1); // 清除最低位的1
}
printf("二进制中1的个数 = %d\n", count);
return 0;
}
5. 赋值操作符
赋值操作符在 C 语言中用于设置变量的值。最基本的赋值操作符是单一的等号(=
),这些操作符结合了算术操作和赋值操作,以简化常见的操作。
5.1 基本赋值操作符
等号是最基本的赋值操作符,用于将右侧表达式的值赋给左侧的变量。
代码示例:
int a = 5; // 将5赋值给整数变量a
5.2 复合赋值操作符
复合赋值操作符结合了其他操作(如加法、减法、乘法等)和赋值,这样可以在执行操作的同时更新变量的值。
-
+=(加后赋值)
将右侧的值加到左侧的变量上,然后将结果赋值给左侧的变量。
int a = 5;
a += 3; // 等同于 a = a + 3; a现在是8
-
-=(减后赋值)
从左侧的变量中减去右侧的值,然后将结果赋值给左侧的变量。
int a = 5;
a -= 3; // 等同于 a = a - 3; a现在是2
-
*=(乘后赋值)
将左侧的变量与右侧的值相乘,然后将结果赋值给左侧的变量。
int a = 5;
a *= 3; // 等同于 a = a * 3; a现在是15
-
/=(除后赋值)
将左侧的变量除以右侧的值,然后将结果赋值给左侧的变量。
int a = 15;
a /= 3; // 等同于 a = a / 3; a现在是5
-
%=(模后赋值)
用左侧的变量除以右侧的值计算余数,然后将结果赋值给左侧的变量。
int a = 16;
a %= 3; // 等同于 a = a % 3; a现在是1
-
<<=(左移后赋值)
将左侧的变量左移指定的位数,然后将结果赋值给左侧的变量。
int a = 1;
a <<= 3; // 等同于 a = a << 3; a现在是8
-
>>=(右移后赋值)
将左侧的变量右移指定的位数,然后将结果赋值给左侧的变量。
int a = 16;
a >>= 3; // 等同于 a = a >> 3; a现在是2
-
&=(位与后赋值)
对左侧的变量和右侧的值进行位与操作,然后将结果赋值给左侧的变量。
int a = 12;
a ^= 5; // 等同于 a = a ^ 5; a现在是9
-
|=(位或后赋值)
对左侧的变量和右侧的值进行位或操作,然后将结果赋值给左侧的变量。
int a = 12;
a |= 5; // 等同于 a = a | 5; a现在是13
6. 单目操作符
6.1 单目操作符介绍
在 C 语言中,单目操作符是只需要一个操作数的操作符。它们直接作用于其后的变量或表达式,执行各种运算。单目操作符包括几种常见类型,如逻辑非、位非、正号、负号、增量和减量操作符。
-
逻辑非
!
逻辑非操作符 !
用于反转其操作数的布尔值。如果操作数为 0
(假),则结果为 1
(真);如果操作数为非零,则结果为 0
(假)。
int a = 0;
int result = !a; // result 的值为 1,因为 !0 为真
-
位非
~
位非操作符 ~
对整型操作数的每一位进行逻辑非操作,即将所有的 0
变为 1
,所有的 1
变为 0
。
int a = 5; // 二进制表示为 0000 0101
int result = ~a; // 结果为 1111 1010 (在一个字节的二进制表示中)
-
正号
+
正号操作符 +
是单目操作符中最直观的一个,它表明其操作数是正值。在大多数情况下,这个操作符对操作数没有实际影响。
int a = 5;
int result = +a; // result 的值仍然为 5
-
负号
-
负号操作符 -
用于改变其操作数的符号。如果操作数是正的,使用负号后变为负的;如果是负的,则变为正的。
int a = 5;
int result = -a; // result 的值为 -5
-
前置递增
++
前置递增操作符 ++
用于将其操作数增加 1
,并返回增加后的值。
int a = 5;
int result = ++a; // a 和 result 都变为 6
-
前置递减
--
前置递减操作符 --
用于将其操作数减少 1
,并返回减少后的值。
int a = 5;
int result = --a; // a 和 result 都变为 4
-
取地址操作符
&
取地址操作符返回其操作数的内存地址。
int a = 5;
int *ptr = &a; // ptr 现在指向 a 的地址
-
间接访问(解引用)操作符
*
解引用操作符用于访问指针指向的变量的值。
int a = 5;
int *ptr = &a;
int value = *ptr; // value 是 5
-
sizeof 运算符
sizeof
可以用来测量变量、数据类型、表达式或数组的大小。结果的类型是 size_t
,这是一个在 <stddef.h>
头文件中定义的无符号整型。
int arr[10];
size_t arrSize = sizeof(arr); // 返回整个数组的大小,如果int是4字节,则返回40字节
size_t numElements = sizeof(arr) / sizeof(arr[0]); // 计算数组中的元素数量,结果是10
sizeof 在数组参数中的表现
当数组作为参数传递给函数时,数组会退化为指向其首元素的指针,因此在函数内部使用 sizeof
并不会返回数组的实际大小,而是返回指针的大小。
void printArraySize(int arr[]) {
size_t size = sizeof(arr); // 这里返回的是指针的大小,而不是数组的大小
printf("Array size inside function: %zu bytes\n", size);
}
int main() {
int myArray[10];
printf("Array size in main: %zu bytes\n", sizeof(myArray)); // 正确显示数组大小
printArraySize(myArray); // 显示指针大小
return 0;
}
6.2 练习
实现一个功能,其中包含:
- 初始化一个整数数组。
- 使用指针遍历数组,计算数组元素的总和。
- 打印数组中每个元素的地址和值。
- 打印数组的总大小和单个元素的大小。
- 动态调整数组元素的值。
#include <stdio.h>
int main() {
int numbers[] = {10, 20, 30, 40, 50};
int *ptr = numbers; // ptr 指向数组的第一个元素
int total = 0; // 用于存储数组元素的总和
size_t size = sizeof(numbers) / sizeof(numbers[0]); // 数组元素的数量
// 使用指针遍历数组并计算总和
for (int i = 0; i < size; ++i) {
total += *ptr; // 累加指针指向的当前元素
ptr++; // 移动指针到下一个元素
}
// 重置指针到数组开头
ptr = numbers;
// 打印数组的每个元素的地址和值
printf("Array elements and their addresses:\n");
for (int i = 0; i < size; ++i) {
printf("Element %d: %d, Address: %p\n", i, *ptr, (void*)ptr);
*ptr++ += 5; // 增加当前元素的值并将指针移到下一个元素
}
// 打印数组总大小和单个元素的大小
printf("Total size of array: %zu bytes\n", sizeof(numbers));
printf("Size of one element: %zu bytes\n", sizeof(numbers[0]));
// 再次打印修改后的数组
printf("Modified array:\n");
for (int i = 0; i < size; ++i) {
printf("%d ", numbers[i]);
}
printf("\n");
// 打印总和
printf("Total sum of elements: %d\n", total);
return 0;
}
7. 关系操作符
关系操作符用于比较两个值的大小或相等性,根据比较的结果返回布尔值(0
表示假,非 0
(通常是 1
)表示真)。
等于 (==
):
- 比较两个操作数是否相等。
- 如果相等,则返回
1
;如果不相等,则返回0
。
不等于 (!=
):
- 比较两个操作数是否不相等。
- 如果不相等,则返回
1
;如果相等,则返回0
。
大于 (>
):
- 比较左操作数是否大于右操作数。
- 如果左边大于右边,则返回
1
;否则返回0
。
小于 (<
):
- 比较左操作数是否小于右操作数。
- 如果左边小于右边,则返回
1
;否则返回0
。
大于等于 (>=
):
- 比较左操作数是否大于或等于右操作数。
- 如果左边大于或等于右边,则返回
1
;否则返回0
。
小于等于 (<=
):
- 比较左操作数是否小于或等于右操作数。
- 如果左边小于或等于右边,则返回
1
;否则返回0
。
8. 逻辑操作符
逻辑操作符用于执行布尔逻辑运算,这些操作符通常用于组合多个条件表达式,以控制程序的决策流程。逻辑操作符的结果总布尔类型,即 0
(假)或 1
(真)。
逻辑与 (&&
):
- 类型:布尔逻辑操作符。
- 操作:比较两个条件或表达式,并返回一个布尔值。只有当两个操作数都为真(非零)时,结果才为真(1)。否则,结果为假(0)。
- 短路特性:如果第一个操作数为假(0),则不评估第二个操作数,因为无论第二个操作数是什么,整个表达式的结果都已确定为假。
if (a > 0 && b > 0) {
// 仅当a和b都大于0时,执行代码块
}
按位与 (&
)
- 类型:位操作符。
- 操作:对两个数的每一位进行与操作。只有对应的两位都是1时,结果位才是1,否则是0。
int a = 5; // 二进制:0101
int b = 3; // 二进制:0011
int result = a & b; // 结果为1,二进制:0001
逻辑或 (||
):
- 类型:布尔逻辑操作符。
- 操作:比较两个条件或表达式,并返回一个布尔值。如果至少一个操作数为真(非零),结果就为真(1)。只有当两个操作数都为假(0)时,结果才为假(0)。
- 短路特性:如果第一个操作数为真,那么不评估第二个操作数,因为整个表达式的结果已确定为真。
if (a > 0 || b > 0) {
// 只要a或b任一大于0,执行代码块
}
按位或 (|
)
- 类型:位操作符。
- 操作:对两个数的每一位进行或操作。如果对应的任何一位是1,结果位就是1,否则是0。
int a = 5; // 二进制:0101
int b = 3; // 二进制:0011
int result = a | b; // 结果为7,二进制:0111
练习:
#include <stdio.h>
int main()
{
int i = 0, a = 0, b = 2, c = 3, d = 4; // 初始化变量i, a, b, c, d
// 使用逻辑与操作符。逻辑与的短路特性意味着如果发现任何一个操作数结果为假,则停止计算
// a++ 的值是 0 (后置递增,使用旧值),因此整个表达式的结果为假,并且后面的表达式不会执行
i = a++ && ++b && d++; // a++ 为0, 所以 i = 0, 递增停止在 ++b
printf("a = %d\n b = %d\n c = %d\nd = %d\n", a, b, c, d);
return 0;
}
逻辑与操作符的短路特性
在这个表达式中,a++ 是后置递增,意味着它的值在表达式计算之前使用。因为 a 的初始值为 0,所以 a++ 返回 0,并且在表达式计算后 a 变成了 1。由于 && 操作符的短路特性,一旦 a++ 返回 0(假),后面的表达式 ++b 和 d++ 都不会被执行。
变量的最终值
- a = 1:因为 a++ 是后置递增。
- b = 2:++b 没有执行,因此 b 的值没有改变。
- c = 3:变量 c 在这段代码中没有被修改。
- d = 4:由于短路,d++ 没有执行。
9. 条件操作符
语法和结构
expression1 ? expression2 : expression3
- expression1:这是一个布尔表达式,其结果必须是真(非零)或假(零)。
- expression2:如果 expression1 为真(非零),则计算并返回此表达式的值。
- expression3:如果 expression1 为假(零),则计算并返回此表达式的值。
代码示例:
int age = 20;
const char* status = age >= 18 ? "adult" : "minor";
printf("You are an %s.\n", status);
在这个例子中,条件 age >= 18 被评估。因为 age 是 20,条件为真,所以 status 被赋值为 "adult"。如果 age 小于 18,status 将被赋值为 "minor"。
10. 逗号表达式
10.1 语法
(expression1, expression2, expression3, ..., expressionN)
其中 expression1 到 expressionN-1 都将被评估并执行,但它们的结果被丢弃,只有 expressionN 的结果被返回和使用。
10.2 代码示例
多变量初始化
#include <stdio.h>
int main() {
int a, b, c;
for (a = 0, b = 10, c = 20; a < 10; a++, b--, c += 2) {
printf("a: %d, b: %d, c: %d\n", a, b, c);
}
return 0;
}
在这个示例中,逗号操作符用于在循环的初始化和迭代部分执行多个表达式。每次循环中,变量 a 递增 1,变量 b 递减 1,变量 c 增加 2。循环从 a = 0 开始,并在 a 小于 10 时结束。
表达式分组
#include <stdio.h>
int main() {
int x = 1, y = 2, z = 3;
int result;
result = (x += 10, y += 10, z += 10);
printf("result: %d, x: %d, y: %d, z: %d\n", result, x, y, z);
return 0;
}
在这个示例中,每个变量都增加了 10,但 result 变量的值是最后一个表达式 z += 10 的结果,因此结果是 13。变量 x、y 和 z 分别从其初始值(1, 2, 3)增加 10。
11. 下标引用、函数调用和结构成员
11.1 下标引用 ([])
下标引用操作符用于访问数组中的元素。数组可以被视为一系列连续存储的元素,下标引用允许我们通过索引来访问这些元素。索引通常从0开始,表示数组的第一个元素。
int arr[10]; // 声明一个整数数组,包含10个元素
arr[0] = 5; // 将数组的第一个元素设置为5
printf("First element: %d\n", arr[0]); // 打印数组的第一个元素
11.2 函数调用 (())
函数调用操作符用于执行一个函数。在 C 语言中,函数定义包含返回类型、函数名称和参数列表。函数调用需要提供函数名称和括号中的参数,以执行函数并返回结果。
#include <stdio.h>
int add(int x, int y) {
return x + y;
}
int main() {
int result = add(5, 3); // 调用函数add,传递参数5和3
printf("Sum: %d\n", result);
return 0;
}
11.3 结构成员访问 (. 和 ->)
C 语言允许定义复合数据类型,称为结构体(struct),它可以包含多个不同类型的数据项。结构成员访问操作符用于访问结构体变量的成员。
- . 操作符用于直接访问结构体的成员。
- -> 操作符用于通过结构体指针访问其成员。
#include <stdio.h>
typedef struct {
int x;
int y;
} Point;
int main() {
Point p = {10, 20}; // 创建一个Point类型的结构体变量p
Point *ptr = &p; // 创建一个指向p的指针ptr
printf("p.x: %d\n", p.x); // 使用.操作符访问p的成员x
printf("ptr->y: %d\n", ptr->y); // 使用->操作符访问ptr指向的成员y
return 0;
}
p.x: 10
ptr->y: 20
12. 表达式求值
12.1 操作符的优先级
操作符的优先级决定了在表达式中哪些操作先被执行。在 C 语言中,不同的操作符具有不同的优先级。例如,乘法和除法操作符 (*
和 /
) 的优先级高于加法和减法操作符 (+
和 -
)。这意味着在表达式 a + b * c
中,先执行 b * c
,然后将结果与 a
相加。
操作符的结合性
当表达式中有多个相同优先级的操作符时,操作符的结合性决定了它们的求值顺序。大多数二元操作符在 C 语言中是左结合的,意味着这些操作符会从左向右求值。例如,在表达式 a - b - c
中,首先计算 a - b
的结果,然后从该结果中减去 c
。
12.2 隐式类型转换
在求值过程中,操作数可能需要进行类型转换以匹配另一个操作数的类型,或者适应操作符的期望。这称为隐式类型转换或强制类型转换。例如:
算术转换:当操作数类型不同时,较“小”的类型(如 int)可能会被提升为较“大”的类型(如 double)来避免精度损失。
整型提升:小整数类型(如 char 和 short)通常在参与表达式计算时提升为 int 或 unsigned int。
示例 1: 算术运算中的隐式转换
#include <stdio.h>
int main() {
int i = 10;
double d = 3.14;
double result = i + d; // int 被隐式转换为 double
printf("%f\n", result); // 输出 13.140000
return 0;
}
在这个例子中,整数 i
在与双精度浮点数 d
相加时自动被转换成 double
类型,以确保运算的精度。
示例 2: 逻辑表达式中的隐式转换
#include <stdio.h>
int main() {
int a = 0;
double pi = 3.14;
if (pi) {
printf("pi is true\n"); // 输出 "pi is true"
}
if (a) {
printf("a is true\n");
}
return 0;
}
在这个例子中,double 类型的 pi 被隐式转换为 bool 类型(true),因为在 C 中任何非零值都被认为是真。相反,a 为 0(假),因此其对应的代码块不执行。
12.3 显式类型转换
显式类型转换是程序员明确指示编译器进行特定类型转换的操作。这通常通过使用类型转换运算符来实现。
示例 1: 使用强制类型转换运算符
#include <stdio.h>
int main() {
double d = 3.14;
int i = (int)d; // 显式地将 double 转换为 int
printf("%d\n", i); // 输出 3,小数部分被丢弃
return 0;
}
这里使用了强制类型转换 (int)
显式地将 double
类型的 d
转换为 int
类型,丢弃了小数部分。
示例 2: 显式转换改变表达式结果
#include <stdio.h>
int main() {
int total_bytes = 1025;
int size = 512;
double n_blocks = (double)total_bytes / size;
printf("%f\n", n_blocks); // 输出 2.001953
return 0;
}
在这个例子中,如果没有 (double) 强制转换,total_bytes / size 将执行整数除法,结果为 2。通过显式转换 total_bytes 到 double,我们保证了进行的是浮点除法,这样可以得到更精确的结果。