int 类型
int
的几种类型:short int
,int
,long int
,long long
,还有对应的无符号,int
和long int
的字节数相同-
sizeof
运算符可以用来测量任何类型或变量在内存中所占用的字节数,这个运算符的结果是一个size_t
类型的值,表示以字节为单位的内存大小,且结果总是非负的。sizeof
运算符的结果不会是 0,因为 C 语言不允许定义空数组或空结构体。使用sizeof
运算符来获取类型的大小是一个好习惯,因为它比硬编码固定的整数更灵活,也更容易维护。 -
int
类型在大多数现代计算机上是 4 个字节,但在某些小型机或嵌入式系统中可能是 2 个字节。依赖int
的大小可能会影响程序的可移植性。 -
int
类型的数字范围是 +0 和 -0 的补码,由于 -0 的补码被当做最小值,所以负值比正值多了一个数,即 -2^{31} \sim 2^{31}-1 (2^{31}-1 = 2147483647) -
不同
int
类型对应的输出符号,比如%d
对应int
,%llu
对应long long unsigned int
,无符号的都是%u
,%x
对应 16 进制(以0x
或0X
开头),%o
对应八进制(以数字0
开头),C 语言标准不支持直接以二进制形式写出整型字面值,但可以使用十六进制或八进制来间接表示二进制数
#include <stdio.h>
#include <limits.h>
int main() {
short short_int = 0;
int i = 100;
long long_int = 0;
long long longlong_int = 0;
unsigned int unsigned_int = 123;
unsigned long unsigned_long = 111;
printf("short int %d\n", short_int); // short int 0
printf("int in oct: %o\n", i); // int in oct: 144
// d = decimal
// x = hex
// o = oct
// hd%: short decimal
// %d: decimal
// %ld: long decimal
// %lld: long long decimal
// %hu: unsigned short decimal
// \n: new line
// size_t
size_t size_of_int = sizeof(int);
printf("short int: %d\n", sizeof(short int)); // short int: 2
printf("int: %d\n", sizeof(int)); // int: 4
printf("long int: %d\n", sizeof(long int)); // long int: 4
printf("long long: %d\n", sizeof(long long int)); // long long: 8
printf("max int %d, min %d\n", INT_MAX, INT_MIN); // max int 2147483647, min -2147483648
printf("max int %ld, min %ld\n", LONG_MAX, LONG_MIN); // max int 2147483647, min -2147483648
printf("unsigned max int %u, unsigned min %d\n", UINT_MAX, 0); // unsigned max int 4294967295, unsigned min 0
return 0;
}
char 类型
char
类型是一个多功能的数据类型,它既可以用于存储字符,也可以作为小整数的存储方式。
- 字符表示:当用作字符类型时,
char
类型通常用来存储字符的 ASCII 码值。ASCII 码是一种字符编码的标准,它将字符映射到特定的数字代码上。 - 整数表示:在整数的上下文中,
char
类型被视为一个 1 字节大小的整数类型。这使得它适用于存储小范围内的整数值。 - 长度规定:C 语言标准明确规定了
char
类型的长度为 1 个字节,这为程序员提供了一个可预测的存储大小。 - 符号性:尽管 C 语言标准没有规定
char
类型是有符号还是无符号,但大多数编译器,包括 MSVC,将char
类型实现为有符号。这意味着char
类型的值可以是正数、零或负数,具体取决于实现和编译器。 - 平台差异:需要注意的是,不同编译器和平台可能对
char
类型的实现有所不同。在某些平台上,char
可能被实现为无符号类型。
#include <stdio.h>
#include <limits.h>
int main() {
char a = 'a';
char char_1 = '1';
char i = 0;
// 按字符集 ASCII 127,打印出来
printf("char a: %d\n", a); // char a: 97
printf("char 1: %d\n", char_1); // char 1: 49
printf("char 'i': %d\n", i); // char 'i': 0
// 字面量 literal
// \n : newline
// \b : backspace
// \r : return
// \t : table
// \' : ' 字符字面量
// \" : " 字符串字面量
char char_1_escape_oct = '\61';
char char_1_escape_hex = '\x31';
printf("char 1: %c\n", char_1); // char 1: 1
printf("char 1: %c\n", char_1_escape_oct); // char 1: 1
printf("char 1: %c\n", char_1_escape_hex); // char 1: 1
// Unicode CJK(中文,日语,韩文) Code point.
// C95 开始,增加了宽字符
wchar_t zhong = L'中'; // 把汉字转化为 unicode 编码,实质上是 unsigned short,存入变量 zhong,所以是两个字节
wchar_t zhong_hex = L'\u4E2D'; // '中'字符 Unicode 编码对应的 16 进制
printf("中:%d\n", zhong); // 中:20013
printf("中:%d\n", zhong_hex); // 中:20013
return 0;
}
float 和 double 类型
#include <stdio.h>
int main() {
float a_float = 3.14f; // 需要保持 6 位有效数字, 数值范围在 (+-)10^-37 ~ 10^37
printf("size of float: %d\n", sizeof(float)); // 具体占的字节大小由编译器决定,此处占 4 个字节
double a_double = 3.14; // 15~16 位有效数字
printf("size of double: %d\n", sizeof(double)); // 8 个字节
float lat = 39.90815f;
printf("%f", 39.908156f - lat); // 0.000008,前 7 位精准的
float money = 3.14f; // 千万不要用 float 或 double 来描述货币
return 0;
}
数值 12369 可以表示为科学记数法形式 1.2369e4,其中“1”是隐含的符号位,它默认为 1,因而在存储时不占用额外的位。指数部分占用 8 位,其值的范围从 -127 到 128,用于调整尾数的权重。尾数部分则由 23 位组成,它存储了数值的实际小数部分,从而确保了数值的精确度。
符号 | 尾数 | 指数 |
---|---|---|
1 | 23 | 8 |
+- | 小数部分,决定精度 | -127~128,决定范围 |
布尔类型
在 C 语言中,布尔类型(bool)是一种特殊的数据类型,它通过包含头文件 #include <stdbool.h>
引入。
布尔类型有两种表示形式:_Bool
和 bool
。
布尔类型的核心特性是它的值只区分两种状态:真(true)和假(false)。在布尔逻辑中,任何非零值都被视为真,而零值则代表假。
对于一个初始为真的布尔变量,只有当其被赋值为零时,其状态才会转变为假;其他任何赋值操作都会保持其为真状态。
对于一个初始为假的布尔变量,任何非零的赋值都会将其状态转变为真。
# include <stdio.h>
# include <stdbool.h>
void TestBool() {
// C 语言中的 bool 类型
_Bool is_enabled = true;
printf("is_enabled: %d\n", is_enabled); // is_enabled: 1
is_enabled = -10; // 结果为 true 的值不会被改变
printf("revised is_enabled: %d\n", is_enabled); // revised is_enabled: 1
is_enabled = 0; // 结果为 true 只有赋值为 0 时,其值才会变化
printf("revised is_enabled: %d\n", is_enabled); // revised is_enabled: 0
bool is_visible = false;
printf("is_visible: %d\n", is_visible); // is_visible: 0
is_visible = 1; // 结果 false 的变量,只要改为非 0 值,都会变成 1,无论改为正数还是负数
printf("revised is_visible: %d\n", is_visible); // revised is_visible: 1
is_visible = 20;
printf("revised is_visible: %d\n", is_visible); // revised is_visible: 1
}
变量
#include <stdio.h>
#define COLOR_RED 0xFF0000
#define COLOR_GREEN 0x00FF00
#define COLOR_BLUE 0x0000FF
int main() {
// const <type> readonly variable
const int kRed = 0xFF0000;
const int kGreen = 0x00FF00;
const int kBlue = 0x0000FF;
printf("kRed: %d\n", kRed); // kRed: 16711680
// 利用指针进行 const 只读常量的修改
int *p_k_red = &kRed;
*p_k_red = 0;
printf("kRed: %d\n", kRed); // kRed: 0
// macro
printf("COLOR_RED: %d\n", COLOR_RED); // COLOR_RED: 16711680
#undef COLOR_RED // 取消定义的宏,后续不可使用 COLOR_RED
// 字面量 literal,宏定义就相当于这个
3;
3u;
3l;
3.f;
3.9;
'c';
"cs";
L'中';
L"中国";
// 硬编码 hard code
int background_color_hard = "0x00FF00";
int background_color = COLOR_GREEN;
return 0;
}
变量的三要素
- 变量名:这是变量在代码中的唯一标识符。它允许程序员在程序中引用变量存储的数据。变量名应该是描述性的,以便于理解其用途。
- 数据类型:数据类型决定了变量可以存储的数据类型以及所需的内存空间大小。例如,
int
用于存储整数,float
用于存储浮点数,char
用于存储单个字符。数据类型还影响变量的存储方式,即数据在内存中的表示形式。 - 取值:这是指变量可以存储的具体数据值。变量的取值范围由其数据类型决定。例如,一个
int
类型的变量可以存储整数值,而一个float
类型的变量可以存储浮点数值。
变量的声明与定义
- 变量的声明:这是告知编译器变量的类型和名称的过程。
声明的语法格式为:
数据类型 变量名;
声明是一个编译时活动,它定义了变量的属性,但并不立即为变量分配内存空间。
-
变量的定义:定义不仅声明了变量,还为变量分配了内存空间。定义的语法与声明相同,但它在程序运行时发生。重要的是要认识到,并非所有声明都是定义;有些声明仅用于告知编译器变量的存在,而不分配内存。
变量的初始化与赋值
-
变量的初始化:这是指为变量赋予初始值的过程。通常,这是通过使用赋值操作符
=
来完成的,例如:int a = 10; //初始化变量
初始化是变量生命周期的开始,确保变量在使用前有一个确定的值。
-
变量的赋值:在初始化之后,可以使用赋值操作符
=
为变量赋予新的值。这是变量使用过程中的一个常见操作,允许变量的值在程序执行期间发生变化。
#include <stdio.h>
#include <limits.h>
int main() {
// <type> <name>;
int value;
// <type> <name> = <initialized value>
int value_init = 3;
value = 4;
printf("value: %d\n", value); // value: 4
printf("size of value: %d\n", sizeof(value)); // size of value: 4
printf("address of value: %#x\n", &value); // address of value: 0x61ff10
// key words 标识符 identifier,变量命名规范
// 1. a-zA-Z0-9_
// 2. 数字不能在第一位
// 3. Google code style, a-z_a-z, person_name
float a_float3 = 3.14f;
float a_float = 3.14f;
return 0;
}
全局变量
全局变量在 C 语言中是一种特殊的变量,它们具有以下特点:
- 定义位置:全局变量定义在文件内部,但在函数体
{}
之外。因此,它们也被称为外部变量。 - 存储位置:全局变量存储在数据段中,这是程序的只读数据区域。
- 作用域:全局变量的作用域从声明它们的位置开始,一直延伸到整个文件。通过特定的语法,如
extern
关键字,可以将全局变量的作用域扩展到整个程序中。这意味着全局变量在整个程序的范围内都是可见的。 - 初始值:全局变量具有默认的初始零值。这包括全局变量数组和结构体,它们的元素也都具有默认的零值。
- 生命周期:全局变量在整个程序的运行期间都有效。程序启动时,全局变量被创建;程序结束时,全局变量被销毁。在 C 语言中,这种生命周期被称为“静态存储期限”。所有存储在数据段中的数据变量或常量都具有静态存储期限。
重点:
- 全局变量的生命周期与程序的生命周期相同,它们在程序启动时初始化,并在程序结束时销毁。
- 存储在数据段中的数据具有静态存储期限,这意味着它们在程序的整个运行期间都存在,并且可以在程序的任何地方访问(只要作用域允许)。
局部变量
局部变量的定义和特点
定义:
局部变量是在函数体内部定义的变量。它们只在定义它们的函数中可见,并且在函数调用结束后自动销毁。
特点:
- 定义位置:局部变量必须在函数体的花括号
{}
内部定义。这意味着它们只能在定义它们的函数中使用。 - 存储位置:局部变量通常存储在虚拟内存空间的栈中。当函数被调用时,局部变量的内存空间被分配,当函数返回时,这些内存空间被释放。
- 作用域:局部变量的作用域仅限于它们被定义的函数体内部。从变量被声明的那一刻起,到函数体的结束(即
}
处),局部变量是可见的。 - 初始值:与其他类型的变量不同,局部变量在 C 语言中没有默认的初始化值。它们不会自动初始化为零或任何其他值。因此,在使用局部变量之前,必须手动赋予它们一个初始值。如果未初始化就使用局部变量,将会导致未定义行为,这可能引发程序错误或不可预测的结果。
未初始化的局部变量:
- 随机值:未初始化的局部变量可能会包含任何值,这些值是之前使用该内存空间的函数留下的残留数据。因此,这些值是随机的,不可预测的。
- 未定义行为: 在标准 C 语言中,使用未初始化的局部变量会导致未定义行为。这意味着编译器可以自由地决定如何处理这种情况,可能是编译错误、运行时崩溃,或者返回一个看似合理的值。
C 语言的设计哲学:
- C 语言的这种设计提供了极大的灵活性,允许程序员以非常接近硬件的方式编写代码。然而,这也意味着程序员需要更加小心地管理内存和变量的初始化,以避免未定义行为。
重要提示:
- 使用局部变量时,必须确保它们在使用前已经被正确初始化。未初始化的局部变量可能包含随机的垃圾值,这可能导致程序运行不稳定或产生错误的输出。
- 在编写函数时,考虑将变量定义在尽可能小的作用域内,这有助于减少命名冲突,并提高代码的可读性和可维护性。
局部变量声明风格:
- C90 及以前: 在早期的 C 语言标准(如 C90)中,要求所有局部变量必须在函数的开头声明。这意味着在函数体内部的任何位置都不能声明新的局部变量。
- C99 以后: 从 C99 标准开始,放宽了这一限制,允许在函数体的任何位置声明局部变量。这为编程提供了更大的灵活性。
- 现代编程建议: 尽管 C99 及以后的版本允许在函数体内部任何位置声明局部变量,但建议仍然遵循“哪里使用,就在哪里定义”的原则。这样做可以提高代码的可读性和可维护性。
局部变量的类型
C 语言提供了多种类型的变量,它们根据定义位置、存储方式、生命周期、作用域和初始化值等因素有所不同。以下是 C 语言中常见的几种变量类型:
- 局部变量:这些变量在函数内部定义,它们的作用域仅限于函数体内部。局部变量的生命周期与函数调用相关,当函数调用结束时,局部变量的内存会被自动释放。
- 全局变量:这些变量在函数外部定义,它们的作用域是整个程序。全局变量的生命周期贯穿整个程序的运行过程,直到程序结束。
- 静态全局变量:与全局变量类似,但它们具有静态存储期,这意味着它们在程序开始时初始化,并且在程序运行期间一直存在。
- 静态局部变量:这些变量在函数内部定义,但使用
static
关键字修饰。它们具有静态存储期,即使函数调用结束,它们的值也会保留,直到程序结束。
推荐原则:
- 优先使用局部变量:局部变量是推荐的首选,因为它们的作用域有限,有助于避免命名冲突,并减少了内存的使用。只有在局部变量不适用时,才考虑使用其他类型的变量。
- 优先使用栈空间:在申请内存空间时,应优先考虑使用栈空间,因为它提供了快速的内存分配和释放。只有在栈空间不满足需求时,才考虑使用堆空间或其他内存分配方法。
静态局部变量
静态局部变量的特点:
- 定义位置:静态局部变量定义在函数体
{}
内部,与普通局部变量相同,但通过static
关键字进行修饰。 - 作用域:静态局部变量的作用域限于定义它们的函数内部,与普通局部变量相同。
- 存储位置:与全局变量类似,静态局部变量存储在数据段中,这是一个只读数据区域。
- 生命周期:静态局部变量具有静态存储期限,这意味着它们在整个程序的运行期间都有效。程序启动时创建,程序结束时销毁。
- 初始值:静态局部变量具有默认的初始零值,包括数组和结构体中的元素。
静态局部变量的独特之处:
- 静态局部变量不能跨函数访问,它们的作用域仅限于定义它们的函数内部。
- 与普通局部变量不同,静态局部变量不存储在函数的栈帧中,而是存储在数据段中。这意味着它们在函数调用之间保持其值,可以跨多次函数调用共享数据。
总结
- 局部变量:应优先使用局部变量,因为它们具有最小的作用域,仅在定义它们的函数或代码块内部有效。局部变量存储在栈上,具有自动存储期,这意味着它们在函数调用时创建,在函数返回时销毁。这有助于避免命名冲突,并提高程序的模块化。
- 静态全局变量:当需要在不同函数之间共享数据,但又不希望这些数据跨文件可见时,可以使用
static
修饰的全局变量。static
关键字限制了全局变量的作用域,使其只能在定义它们的文件内部访问。这有助于实现数据封装和隐藏。 - 全局变量:如果需要在多个文件之间共享数据,可以使用全局变量。全局变量在整个程序范围内可见,可以通过在头文件中声明它们来实现跨文件访问。然而,全局变量的使用应谨慎,因为它们可能导致代码难以维护和调试。
- 静态局部变量:在某些情况下,可能需要在多次函数调用之间保留局部变量的值。这时,可以使用
static
修饰的局部变量。静态局部变量存储在数据段中,具有静态存储期,这意味着它们在程序的整个运行期间都存在。这使得它们可以在函数调用之间保持其值,实现跨调用的数据保存。
特性 | 局部变量 | 全局变量 | 静态局部变量 | static 修饰的全局变量 |
---|---|---|---|---|
定义位置 | 函数内部 | 函数外部 | 函数内部 | 函数外部 |
存储位置 | 栈 | 数据段 | 数据段 | 数据段 |
存储期限(生命周期) | 从函数调用开始到函数退出为止(自动存储期限) | 程序启动到程序结束(静态存储期限) | 程序启动到程序结束(静态存储期限) | 程序启动到程序结束(静态存储期限) |
作用域 | 仅限于所在函数 | 整个程序的所有文件, 但在其它文件使用需要用 extern 声明 | 仅限于所在函数 | 仅限于定义它的文件 |
初始化时机 | 每次函数调用时都初始化, 不同函数调用初始化不同 | 程序启动时初始化,只初始化一次。 | 函数第一次调用时进行初始化,只初始化一次 | 程序启动时初始化,只初始化一次。 |
默认初始化 | 不手动初始化,可能得到一个随机值 | 即便不手动初始化,也会默认初始化为 0 值 | 即便不手动初始化,也会默认初始化为 0 值 | 即便不手动初始化,也会默认初始化为 0 值 |
是否可以被其他文件访问 | 否 | 在其它文件中使用 extern 关键字链接访问 | 否 | 否 |
常量
const
关键字的用途
- 修饰变量:
const
关键字用来修饰变量声明,使得该变量成为一个常量。这意味着一旦初始化后,其值不能被修改。 - 修饰数组:当
const
修饰数组时,它表示数组的元素不能通过数组名和下标来修改。这样的数组被称为常量数组。
const
的限制
- 可修改性:
- 尽管
const
修饰的变量在编译时不能通过变量名修改,但这并不意味着它不能被修改。可以通过指针或其他手段在运行时修改这些变量的值。
- 尽管
- 编译时确定性:
const
常量并不总是要求在编译时确定其值。它也可以在运行时确定,这与字面值和宏常量不同,后者的值必须在编译时确定。
- 数组长度:
const
常量不能用作数组声明中的数组长度,因为它的值可能不是编译时常量表达式。
理解 const
:
const
关键字在编译阶段提供了一种语法检查,确保不能通过变量名修改变量的值。然而,这种保护是有限的,因为它不能防止运行时通过其他手段修改变量的值。- 因此,
const
常量有时被称为“只读变量”,但这个术语可能会引起误解,因为它暗示了一种编译时的保护,而实际上const
提供的保护是编译时的,而不是运行时的。
表达式的主要作用和副作用
表达式的主要作用:
- 计算结果:表达式的主要作用是计算并产生一个值。这个值可以是整数、浮点数、字符等,具体取决于表达式的内容和运算符。
- 小表达式在大表达式中的作用:当一个简单的表达式(小表达式)是更复杂表达式(大表达式)的一部分时,小表达式的结果会被用来计算大表达式的最终结果。在这种情况下,小表达式的主要作用是为大表达式提供必要的中间值。
表达式的副作用:
- 修改变量值:最常见的副作用是修改变量的值。任何包含赋值运算符
=
的表达式都会改变变量的值。例如:int a = 10;
这个表达式的主要作用是将值 10 赋给变量a
,其副作用是创建了变量a
并赋予了初始值。 - 执行 IO 操作:另一个常见的副作用是执行输入/输出操作。函数调用本身是一个表达式,其主要作用是返回函数的返回值。例如:
printf("Hello, World!");
这个表达式的主要作用是打印字符串到标准输出,而副作用是函数printf
返回的整数值(通常是打印的字符数)。
副作用与主要作用的关系:
- 相对独立:副作用和主要作用是相对独立的,它们不会互相干扰。即使一个表达式有副作用,它仍然会执行其主要作用并产生一个结果。
- 编程实践中的考虑:在编程时,开发者需要同时考虑表达式的主要作用和可能的副作用。正确地管理这些副作用是编写可靠和可维护代码的关键。
举例:
- 加法表达式:
- 表达式:
a + b
- 主要作用:计算并返回变量
a
和b
的和。 - 副作用:没有副作用。
- 表达式:
- 位左移表达式:
- 表达式:
a << 1
- 主要作用:计算并返回变量
a
左移 1 位的结果。 - 副作用:没有副作用。
- 表达式:
- 位右移赋值表达式:
- 表达式:
a >>= 1
- 主要作用:计算并返回变量
a
右移 1 位的结果。 - 副作用:修改变量
a
的值,使其等于右移 1 位后的结果。
- 表达式:
- 赋值表达式:
- 表达式:
int c = a + b
- 主要作用:计算并返回
a + b
的值。 - 副作用:修改变量
c
的值,现在c
的值是a + b
的和。
- 表达式:
- 条件表达式(三元运算符):
- 表达式:
while((ch = getchar()) != '\n')
- 主要作用:返回
getchar()
函数调用的返回值是否不等于换行符的布尔值,从而用来控制while
循环。 - 副作用:执行
getchar()
函数调用,从标准输入读取一个字符,并将其存储在变量ch
中。
- 表达式:
- 赋值操作:
- 表达式:
ch = getchar()
- 主要作用:返回
getchar()
函数调用的返回值。 - 副作用:执行
getchar()
函数调用,从标准输入读取一个字符,并改变变量ch
的值。
- 表达式:
运算符
#include <stdio.h>
int main() {
int first = 1;
int second;
int third;
// =
third = second = first;
printf("%d, %d\n", second, third); // 1, 1
int left, right;
left = 2;
right = 3;
int sum;
sum = left + right; // 5
int diff = left - right; // -1
int product = left * right; // 6
int quotient = left / right; // 0
float quotient_float = left / right; // 0.000000
float quotient_float_correct = left * 1.f / right; // 0.666666....
int remainder = left % right; // 2
int quotient_1 = 100 / 30; // 3
printf("sum: %d\n", sum);
printf("diff: %d\n", diff);
printf("product: %d\n", product);
printf("quotient: %d\n", quotient);
printf("quotient_1: %d\n", quotient_1);
printf("quotient_float: %f\n", quotient_float);
printf("quotient_float_correct: %f\n", quotient_float_correct);
printf("remainder: %d\n", remainder);
// > < >= <= == !=
// true: 1, false: 0
printf("3 > 2: %d\n", 3 > 2); // 1
printf("3 < 2: %d\n", 3 < 2); // 0
printf("3 <= 3: %d\n", 3 <= 3); // 1
printf("3 >= 3: %d\n", 3 >= 3); // 1
printf("3 == 3: %d\n", 3 == 3); // 1
printf("3 != 3: %d\n", 3 != 3); // 0
// && 与 || 或
printf("3 > 2 && 3 < 2: %d\n", 3 > 2 && 3 < 2); // 0
printf("3 > 2 || 3 < 2: %d\n", 3 > 2 || 3 < 2); // 1
// ++ --
int i = 1;
int j = i++; // j = 1, i = 2;
int k = ++i; // k = 3, i = 3;
printf("i: %d\n", i);
printf("j: %d\n", j);
printf("k: %d\n", k);
}
运算符的优先级
运算符优先级:
运算符优先级决定了在没有括号的情况下,表达式中各个运算符的计算顺序。以下是一些关键点:
- 一元运算符:这些运算符(如
++
、--
、!
、~
、*
(解引用)、&
(取地址)等)的优先级最高,它们首先被计算。 - 算术运算符:算术运算符(如
+
、-
、*
、/
、%
等)的优先级次之,它们在一元运算符之后计算。 - 移位运算符:移位运算符(如
<<
、>>
)的优先级低于算术运算符,但高于关系运算符。 - 关系运算符:关系运算符(如
<
、>
、<=
、>=
等)的优先级低于移位运算符。 - 相等运算符:相等运算符(如
==
、!=
)的优先级低于关系运算符。 - 位运算符:位运算符(如
&
(按位与)、|
(按位或)、^
(按位异或)等)的优先级低于相等运算符。 - 逻辑运算符:逻辑运算符(如
&&
、||
)的优先级低于位运算符。 - 赋值运算符:赋值运算符(如
=
、+=
、-=
等)的优先级最低,它们在所有其他运算符之后计算。
运算符结合性:
当表达式中包含多个相同优先级的运算符时,运算符的结合性决定了计算的顺序。有两种结合性:
- 左结合性:表达式从左到右计算。例如,
a + b + c
将首先计算a + b
,然后将结果与c
相加。 - 右结合性:表达式从右到左计算。例如,赋值运算符通常具有右结合性,这意味着
a = b = c
将首先计算b = c
,然后将结果赋给a
。
最佳实践:
- 使用括号:为了确保表达式的计算顺序符合预期,建议使用括号来明确运算顺序。这不仅可以避免优先级引起的错误,还可以使代码更易于理解。
- 代码清晰性:即使知道运算符的优先级,也应优先考虑代码的可读性和清晰性。使用括号可以提高代码的可读性,减少歧义。
举例:
- 求数组区间中间索引的惯用法:
在编程中,求数组区间的中间索引是一个常见的操作。但是,直接使用
(a + b) / 2
可能会导致溢出。为了避免这个问题,可以使用位运算符来优化计算:
- 优化表达式:
(b * a) / 2 + a
这个表达式首先计算b * a
,然后右移 1 位,最后加上a
。这样可以避免溢出,并且计算出正确的中间索引。 -
正确使用括号:
(b * a >> 1) + a
使用括号可以确保计算的顺序是正确的,避免歧义。((b - a) >> 1) + a; // 正确的 (b - a) >> 1 + a; // + 先计算,错误 b - a >> 1 + a; // + 先计算,错误 (b - a >> 1)+ a; // 正确的
- 结合性的例子:结构体类型
在 C 语言中,结构体是一种聚合数据类型,可以包含多个成员。考虑以下结构体类型和表达式:
- 结构体类型:
typedef struct Student {
int age;
};
- 结构体实例:
Student sl;
-
表达式:
sl.age++;
在这个表达式中,
.
和++
的优先级相同,但结合性是左结合性,所以先计算sl.age
,然后对结果进行递增。
-
结合性的例子:指针
指针是 C 语言中的一个重要概念,用于访问内存中的数据。考虑以下指针和表达式:
-
指针类型:
Student *p;
-
表达式:
p->age++;
在这个表达式中,->
和++
的优先级相同,但结合性是左结合性,所以先计算p->age
,然后对结果进行递增。
-
结合性的例子:赋值
赋值运算符是右结合性的,这意味着赋值表达式从右向左计算。考虑以下赋值表达式:
- 赋值表达式:
int a = b = c = 10;
这个表达式首先计算c = 10
,然后将结果赋给b
,最后将b
的值赋给a
。最终,a
、b
和c
的值都是 10。
优先级 | 运算符 | 描述 | 结合性 |
---|---|---|---|
0 | 小括号() |
最高优先级 | 从左到右 |
1 | ++ -- |
后缀自增与自减,即a++这种 | 从左到右 |
1 | 小括号() |
函数调用的小括号 | 从左到右 |
1 | [] |
数组下标 | 从左到右 |
1 | . |
结构体与联合体成员访问 | 从左到右 |
1 | -> |
结构体与联合体成员通过指针访问 | 从左到右 |
2 | ++ -- |
前缀自增与自减,即–a这种 | 从右到左 |
2 | + - |
一元加与减,表示操作数的正负 | 从右到左 |
2 | ! ~ |
逻辑非与按位非 | 从右到左 |
2 | (type_name) |
强制类型转换语法 | 从右到左 |
2 | * |
解引用运算符 | 从右到左 |
2 | & |
取地址运算符 | 从右到左 |
2 | sizeof |
取大小运算符 | 从右到左 |
3 | * / % |
乘法、除法及取余运算符 | 从左到右 |
4 | + - |
二元运算符的加法及减法 | 从右到左 |
5 | >> |
左移及右移位运算符 | 从右到左 |
6 | <= |
分别为 | 从右到左 |
6 | > >= |
分别为 > 与 ≥ 的关系运算符 | 从右到左 |
7 | == != |
分别为 = 与 ≠ 的关系运算符 | 从右到左 |
8 | & |
按位与 | 从右到左 |
9 | ^ |
按位异或 | 从右到左 |
10 | | |
按位或 | 从右到左 |
11 | && |
逻辑与 | 从右到左 |
12 | || |
逻辑或 | 从右到左 |
13 | ?: |
三目运算符 | 从右到左 |
14 | = |
简单赋值 | 从右到左 |
14 | += -= |
和及差复合赋值 | 从右到左 |
14 | *= /= %= |
积、商及余数复合赋值 | 从右到左 |
14 | >>=` |
左移及右移复合赋值 | 从右到左 |
14 | &= ^= |= |
按位与、异或及或复合赋值 | 从右到左 |
15 | , |
逗号 | 从左到右 |
自增和自减运算符
自增和自减运算符可以用来递增或递减变量的值。它们有两种形式:前缀(++a
或 --a
)和后缀(a++
或 a--
)。
- 前缀形式:
- 主要作用:返回变量自增或自减后的值。
- 副作用:改变变量的值,使其自增或自减 1。
- 后缀形式:
- 主要作用:返回变量自增或自减前的值。
- 副作用:改变变量的值,使其自增或自减 1。
指针操作中的应用:
在指针操作中,自增和自减运算符特别重要,因为它们可以用来移动指针的位置。以下是一些关键点:
- 解引用运算符(
*
):它用于获取指针指向位置的元素的值。 - 后缀
++
的优先级:比解引用运算符*
更高,所以后缀++
会先计算。 - 表达式
p++
:- 主要作用:返回指针
p
当前指向的元素的指针。 - 副作用:指针
p
指向数组的下一个元素。
- 主要作用:返回指针
- 表达式
*p++
:- 主要作用:返回指针
p
当前指向元素的取值。 - 副作用:指针
p
指向数组的下一个元素。
- 主要作用:返回指针
举例:
假设有一个指向 int
数组的指针 p
,数组元素如下: [0,31,28,31]
- 使用
p++
:*p++
将返回数组的第一个元素(0),然后p
将移动到指向数组的第二个元素(31)。
- 使用
(*p)++
:*p
将返回数组的第一个元素(0),然后p
将移动到指向数组的第二个元素(31),但是*p
的值不会改变。
- 遍历数组:
*p++
常用于遍历数组和字符串,因为它在每次迭代中都会返回当前元素的值,并将指针移动到下一个元素。
位运算
在按位运算符中,按位异或(XOR)运算符因其独特的性质而在编程中非常有用。以下是按位异或运算符的几个关键性质:
- 与 0 异或:任何整数与 0 进行异或运算的结果是它本身。即
a ^ 0 = a
- 与自身异或:任何整数与自身进行异或运算的结果是 0。即
a ^ a = 0
- 交换律:异或运算满足交换律,即改变操作数的顺序不会影响结果。即
a ^ b = b ^ a
- 结合律:异或运算也满足结合律,这意味着括号内的运算顺序不影响最终结果。即
(a ^ b) ^ c =a ^ (b ^ c)
#include <stdio.h>
int main() {
// bit operators & | ^ ~
#define FLAG_VISIBLE 0x1 // 2^0, 0001, 是否可见
#define FLAG_TRANSPARENT 0x2 // 2^1, 0010, 是否透明
#define FLAG_RESIZABLE 0x4 // 2^2, 0100, 是否可以改变大小
int window_flags = FLAG_RESIZABLE | FLAG_TRANSPARENT; // 0110, 定义一个窗口可以改变大小,也可以设置透明
int resizable = window_flags & FLAG_RESIZABLE; // 0100, 此处为前端去获取后端传来的 window_flags 值,做一个与操作,看是否可以设置窗口大小
int visible = window_flags & FLAG_VISIBLE; // 0000, 是否可以设置可见
// << >>, 一种新的定义方式,和上面效果相同
#define FLAG_VISIBLE 1 << 0 // 2^0, 0001, 1 向左移 0 位
#define FLAG_TRANSPARENT 1 << 1// 2^1, 0010, 1 向左移 1 位
#define FLAG_RESIZABLE 1 << 2 // 2^2, 0100, 1 向左移 2 位
#define FLAG 1 << 0
printf("FLAG: %d \n", FLAG); // 1, 直接定义当做整数使用便好,只是定义方式不同
int x = 1000;
x * 2;
x << 1;
x / 2;
x >> 1;
//
x *= 2; // x = x * 2;
x /= 2; // x = x / 2;
x += 2;
x -= 2;
x %= 2;
x >>= 1; // x = x >> 1
x <<= 1; // x = x << 1
x = 1;
printf("x: %d\n", x); // 1
// ,
int y = -1;
int z = (y = x = x * 2, x = x + 3); // 逗号运算符,此句最后的结果是逗号后面的表达式的值
printf("x: %d\n", x); // 5
printf("y: %d\n", y); // 2
printf("z: %d\n", z); // 5
}