C 语言数据类型

int 类型

  1. int 的几种类型:short intintlong intlong long,还有对应的无符号,intlong int 的字节数相同

  2. sizeof 运算符可以用来测量任何类型或变量在内存中所占用的字节数,这个运算符的结果是一个 size_t 类型的值,表示以字节为单位的内存大小,且结果总是非负的。sizeof 运算符的结果不会是 0,因为 C 语言不允许定义空数组或空结构体。使用 sizeof 运算符来获取类型的大小是一个好习惯,因为它比硬编码固定的整数更灵活,也更容易维护。

  3. int 类型在大多数现代计算机上是 4 个字节,但在某些小型机或嵌入式系统中可能是 2 个字节。依赖 int 的大小可能会影响程序的可移植性。

  4. int 类型的数字范围是 +0 和 -0 的补码,由于 -0 的补码被当做最小值,所以负值比正值多了一个数,即 -2^{31} \sim 2^{31}-1 (2^{31}-1 = 2147483647)

  5. 不同 int 类型对应的输出符号,比如 %d 对应 int%llu 对应 long long unsigned int,无符号的都是 %u%x 对应 16 进制(以 0x0X 开头),%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 类型是一个多功能的数据类型,它既可以用于存储字符,也可以作为小整数的存储方式。

  1. 字符表示:当用作字符类型时,char 类型通常用来存储字符的 ASCII 码值。ASCII 码是一种字符编码的标准,它将字符映射到特定的数字代码上。
  2. 整数表示:在整数的上下文中,char 类型被视为一个 1 字节大小的整数类型。这使得它适用于存储小范围内的整数值。
  3. 长度规定:C 语言标准明确规定了 char 类型的长度为 1 个字节,这为程序员提供了一个可预测的存储大小。
  4. 符号性:尽管 C 语言标准没有规定 char 类型是有符号还是无符号,但大多数编译器,包括 MSVC,将 char 类型实现为有符号。这意味着 char 类型的值可以是正数、零或负数,具体取决于实现和编译器。
  5. 平台差异:需要注意的是,不同编译器和平台可能对 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> 引入。

布尔类型有两种表示形式:_Boolbool

布尔类型的核心特性是它的值只区分两种状态:真(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;
}

变量的三要素

  1. 变量名:这是变量在代码中的唯一标识符。它允许程序员在程序中引用变量存储的数据。变量名应该是描述性的,以便于理解其用途。
  2. 数据类型:数据类型决定了变量可以存储的数据类型以及所需的内存空间大小。例如,int 用于存储整数,float 用于存储浮点数,char 用于存储单个字符。数据类型还影响变量的存储方式,即数据在内存中的表示形式。
  3. 取值:这是指变量可以存储的具体数据值。变量的取值范围由其数据类型决定。例如,一个 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 语言中是一种特殊的变量,它们具有以下特点:

  1. 定义位置:全局变量定义在文件内部,但在函数体 {} 之外。因此,它们也被称为外部变量。
  2. 存储位置:全局变量存储在数据段中,这是程序的只读数据区域。
  3. 作用域:全局变量的作用域从声明它们的位置开始,一直延伸到整个文件。通过特定的语法,如 extern 关键字,可以将全局变量的作用域扩展到整个程序中。这意味着全局变量在整个程序的范围内都是可见的。
  4. 初始值:全局变量具有默认的初始零值。这包括全局变量数组和结构体,它们的元素也都具有默认的零值。
  5. 生命周期:全局变量在整个程序的运行期间都有效。程序启动时,全局变量被创建;程序结束时,全局变量被销毁。在 C 语言中,这种生命周期被称为“静态存储期限”。所有存储在数据段中的数据变量或常量都具有静态存储期限。

重点

  • 全局变量的生命周期与程序的生命周期相同,它们在程序启动时初始化,并在程序结束时销毁。
  • 存储在数据段中的数据具有静态存储期限,这意味着它们在程序的整个运行期间都存在,并且可以在程序的任何地方访问(只要作用域允许)。

局部变量

局部变量的定义和特点

定义:

局部变量是在函数体内部定义的变量。它们只在定义它们的函数中可见,并且在函数调用结束后自动销毁。

特点

  1. 定义位置:局部变量必须在函数体的花括号 {} 内部定义。这意味着它们只能在定义它们的函数中使用。
  2. 存储位置:局部变量通常存储在虚拟内存空间的栈中。当函数被调用时,局部变量的内存空间被分配,当函数返回时,这些内存空间被释放。
  3. 作用域:局部变量的作用域仅限于它们被定义的函数体内部。从变量被声明的那一刻起,到函数体的结束(即 } 处),局部变量是可见的。
  4. 初始值:与其他类型的变量不同,局部变量在 C 语言中没有默认的初始化值。它们不会自动初始化为零或任何其他值。因此,在使用局部变量之前,必须手动赋予它们一个初始值。如果未初始化就使用局部变量,将会导致未定义行为,这可能引发程序错误或不可预测的结果。

未初始化的局部变量

  • 随机值:未初始化的局部变量可能会包含任何值,这些值是之前使用该内存空间的函数留下的残留数据。因此,这些值是随机的,不可预测的。
  • 未定义行为: 在标准 C 语言中,使用未初始化的局部变量会导致未定义行为。这意味着编译器可以自由地决定如何处理这种情况,可能是编译错误、运行时崩溃,或者返回一个看似合理的值。

C 语言的设计哲学

  • C 语言的这种设计提供了极大的灵活性,允许程序员以非常接近硬件的方式编写代码。然而,这也意味着程序员需要更加小心地管理内存和变量的初始化,以避免未定义行为。

重要提示

  • 使用局部变量时,必须确保它们在使用前已经被正确初始化。未初始化的局部变量可能包含随机的垃圾值,这可能导致程序运行不稳定或产生错误的输出。
  • 在编写函数时,考虑将变量定义在尽可能小的作用域内,这有助于减少命名冲突,并提高代码的可读性和可维护性。

局部变量声明风格:

  1. C90 及以前: 在早期的 C 语言标准(如 C90)中,要求所有局部变量必须在函数的开头声明。这意味着在函数体内部的任何位置都不能声明新的局部变量。
  2. C99 以后: 从 C99 标准开始,放宽了这一限制,允许在函数体的任何位置声明局部变量。这为编程提供了更大的灵活性。
  3. 现代编程建议: 尽管 C99 及以后的版本允许在函数体内部任何位置声明局部变量,但建议仍然遵循“哪里使用,就在哪里定义”的原则。这样做可以提高代码的可读性和可维护性。

局部变量的类型

C 语言提供了多种类型的变量,它们根据定义位置、存储方式、生命周期、作用域和初始化值等因素有所不同。以下是 C 语言中常见的几种变量类型:

  1. 局部变量:这些变量在函数内部定义,它们的作用域仅限于函数体内部。局部变量的生命周期与函数调用相关,当函数调用结束时,局部变量的内存会被自动释放。
  2. 全局变量:这些变量在函数外部定义,它们的作用域是整个程序。全局变量的生命周期贯穿整个程序的运行过程,直到程序结束。
  3. 静态全局变量:与全局变量类似,但它们具有静态存储期,这意味着它们在程序开始时初始化,并且在程序运行期间一直存在。
  4. 静态局部变量:这些变量在函数内部定义,但使用 static 关键字修饰。它们具有静态存储期,即使函数调用结束,它们的值也会保留,直到程序结束。

推荐原则

  1. 优先使用局部变量:局部变量是推荐的首选,因为它们的作用域有限,有助于避免命名冲突,并减少了内存的使用。只有在局部变量不适用时,才考虑使用其他类型的变量。
  2. 优先使用栈空间:在申请内存空间时,应优先考虑使用栈空间,因为它提供了快速的内存分配和释放。只有在栈空间不满足需求时,才考虑使用堆空间或其他内存分配方法。

静态局部变量

静态局部变量的特点

  1. 定义位置:静态局部变量定义在函数体 {} 内部,与普通局部变量相同,但通过 static 关键字进行修饰。
  2. 作用域:静态局部变量的作用域限于定义它们的函数内部,与普通局部变量相同。
  3. 存储位置:与全局变量类似,静态局部变量存储在数据段中,这是一个只读数据区域。
  4. 生命周期:静态局部变量具有静态存储期限,这意味着它们在整个程序的运行期间都有效。程序启动时创建,程序结束时销毁。
  5. 初始值:静态局部变量具有默认的初始零值,包括数组和结构体中的元素。

静态局部变量的独特之处

  • 静态局部变量不能跨函数访问,它们的作用域仅限于定义它们的函数内部。
  • 与普通局部变量不同,静态局部变量不存储在函数的栈帧中,而是存储在数据段中。这意味着它们在函数调用之间保持其值,可以跨多次函数调用共享数据。

总结

  1. 局部变量:应优先使用局部变量,因为它们具有最小的作用域,仅在定义它们的函数或代码块内部有效。局部变量存储在栈上,具有自动存储期,这意味着它们在函数调用时创建,在函数返回时销毁。这有助于避免命名冲突,并提高程序的模块化。
  2. 静态全局变量:当需要在不同函数之间共享数据,但又不希望这些数据跨文件可见时,可以使用 static 修饰的全局变量。static 关键字限制了全局变量的作用域,使其只能在定义它们的文件内部访问。这有助于实现数据封装和隐藏。
  3. 全局变量:如果需要在多个文件之间共享数据,可以使用全局变量。全局变量在整个程序范围内可见,可以通过在头文件中声明它们来实现跨文件访问。然而,全局变量的使用应谨慎,因为它们可能导致代码难以维护和调试。
  4. 静态局部变量:在某些情况下,可能需要在多次函数调用之间保留局部变量的值。这时,可以使用 static 修饰的局部变量。静态局部变量存储在数据段中,具有静态存储期,这意味着它们在程序的整个运行期间都存在。这使得它们可以在函数调用之间保持其值,实现跨调用的数据保存。
特性 局部变量 全局变量 静态局部变量 static 修饰的全局变量
定义位置 函数内部 函数外部 函数内部 函数外部
存储位置 数据段 数据段 数据段
存储期限(生命周期) 从函数调用开始到函数退出为止(自动存储期限) 程序启动到程序结束(静态存储期限) 程序启动到程序结束(静态存储期限) 程序启动到程序结束(静态存储期限)
作用域 仅限于所在函数 整个程序的所有文件, 但在其它文件使用需要用 extern 声明 仅限于所在函数 仅限于定义它的文件
初始化时机 每次函数调用时都初始化, 不同函数调用初始化不同 程序启动时初始化,只初始化一次。 函数第一次调用时进行初始化,只初始化一次 程序启动时初始化,只初始化一次。
默认初始化 不手动初始化,可能得到一个随机值 即便不手动初始化,也会默认初始化为 0 值 即便不手动初始化,也会默认初始化为 0 值 即便不手动初始化,也会默认初始化为 0 值
是否可以被其他文件访问 在其它文件中使用 extern 关键字链接访问

常量

const 关键字的用途

  • 修饰变量const 关键字用来修饰变量声明,使得该变量成为一个常量。这意味着一旦初始化后,其值不能被修改。
  • 修饰数组:当 const 修饰数组时,它表示数组的元素不能通过数组名和下标来修改。这样的数组被称为常量数组。

const 的限制

  1. 可修改性
    • 尽管 const 修饰的变量在编译时不能通过变量名修改,但这并不意味着它不能被修改。可以通过指针或其他手段在运行时修改这些变量的值。
  2. 编译时确定性
    • const 常量并不总是要求在编译时确定其值。它也可以在运行时确定,这与字面值和宏常量不同,后者的值必须在编译时确定。
  3. 数组长度
    • const 常量不能用作数组声明中的数组长度,因为它的值可能不是编译时常量表达式。

理解 const

  • const 关键字在编译阶段提供了一种语法检查,确保不能通过变量名修改变量的值。然而,这种保护是有限的,因为它不能防止运行时通过其他手段修改变量的值。
  • 因此,const 常量有时被称为“只读变量”,但这个术语可能会引起误解,因为它暗示了一种编译时的保护,而实际上 const 提供的保护是编译时的,而不是运行时的。

表达式的主要作用和副作用

表达式的主要作用

  • 计算结果:表达式的主要作用是计算并产生一个值。这个值可以是整数、浮点数、字符等,具体取决于表达式的内容和运算符。
  • 小表达式在大表达式中的作用:当一个简单的表达式(小表达式)是更复杂表达式(大表达式)的一部分时,小表达式的结果会被用来计算大表达式的最终结果。在这种情况下,小表达式的主要作用是为大表达式提供必要的中间值。

表达式的副作用

  • 修改变量值:最常见的副作用是修改变量的值。任何包含赋值运算符 = 的表达式都会改变变量的值。例如: int a = 10; 这个表达式的主要作用是将值 10 赋给变量 a,其副作用是创建了变量 a 并赋予了初始值。
  • 执行 IO 操作:另一个常见的副作用是执行输入/输出操作。函数调用本身是一个表达式,其主要作用是返回函数的返回值。例如: printf("Hello, World!"); 这个表达式的主要作用是打印字符串到标准输出,而副作用是函数 printf 返回的整数值(通常是打印的字符数)。

副作用与主要作用的关系

  • 相对独立:副作用和主要作用是相对独立的,它们不会互相干扰。即使一个表达式有副作用,它仍然会执行其主要作用并产生一个结果。
  • 编程实践中的考虑:在编程时,开发者需要同时考虑表达式的主要作用和可能的副作用。正确地管理这些副作用是编写可靠和可维护代码的关键。

举例:

  1. 加法表达式
    • 表达式:a + b
    • 主要作用:计算并返回变量 ab 的和。
    • 副作用:没有副作用。
  2. 位左移表达式
    • 表达式:a << 1
    • 主要作用:计算并返回变量 a 左移 1 位的结果。
    • 副作用:没有副作用。
  3. 位右移赋值表达式
    • 表达式:a >>= 1
    • 主要作用:计算并返回变量 a 右移 1 位的结果。
    • 副作用:修改变量 a 的值,使其等于右移 1 位后的结果。
  4. 赋值表达式
    • 表达式:int c = a + b
    • 主要作用:计算并返回 a + b 的值。
    • 副作用:修改变量 c 的值,现在 c 的值是 a + b 的和。
  5. 条件表达式(三元运算符)
    • 表达式:while((ch = getchar()) != '\n')
    • 主要作用:返回 getchar() 函数调用的返回值是否不等于换行符的布尔值,从而用来控制 while 循环。
    • 副作用:执行 getchar() 函数调用,从标准输入读取一个字符,并将其存储在变量 ch 中。
  6. 赋值操作
    • 表达式: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);
}

运算符的优先级

运算符优先级:

运算符优先级决定了在没有括号的情况下,表达式中各个运算符的计算顺序。以下是一些关键点:

  1. 一元运算符:这些运算符(如 ++--!~*(解引用)、&(取地址)等)的优先级最高,它们首先被计算。
  2. 算术运算符:算术运算符(如 +-*/% 等)的优先级次之,它们在一元运算符之后计算。
  3. 移位运算符:移位运算符(如 <<>>)的优先级低于算术运算符,但高于关系运算符。
  4. 关系运算符:关系运算符(如 <><=>= 等)的优先级低于移位运算符。
  5. 相等运算符:相等运算符(如 ==!=)的优先级低于关系运算符。
  6. 位运算符:位运算符(如 &(按位与)、|(按位或)、^(按位异或)等)的优先级低于相等运算符。
  7. 逻辑运算符:逻辑运算符(如 &&||)的优先级低于位运算符。
  8. 赋值运算符:赋值运算符(如 =+=-= 等)的优先级最低,它们在所有其他运算符之后计算。

运算符结合性:

当表达式中包含多个相同优先级的运算符时,运算符的结合性决定了计算的顺序。有两种结合性:

  • 左结合性:表达式从左到右计算。例如,a + b + c 将首先计算 a + b,然后将结果与 c 相加。
  • 右结合性:表达式从右到左计算。例如,赋值运算符通常具有右结合性,这意味着 a = b = c 将首先计算 b = c,然后将结果赋给 a

最佳实践:

  • 使用括号:为了确保表达式的计算顺序符合预期,建议使用括号来明确运算顺序。这不仅可以避免优先级引起的错误,还可以使代码更易于理解。
  • 代码清晰性:即使知道运算符的优先级,也应优先考虑代码的可读性和清晰性。使用括号可以提高代码的可读性,减少歧义。

举例:

  1. 求数组区间中间索引的惯用法:

    在编程中,求数组区间的中间索引是一个常见的操作。但是,直接使用 (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; // 正确的
    
  1. 结合性的例子:结构体类型
    在 C 语言中,结构体是一种聚合数据类型,可以包含多个成员。考虑以下结构体类型和表达式:
  • 结构体类型
typedef struct Student { 
    int age;
};
  • 结构体实例Student sl;

  • 表达式sl.age++;

    在这个表达式中,.++ 的优先级相同,但结合性是左结合性,所以先计算 sl.age,然后对结果进行递增。

  1. 结合性的例子:指针

    指针是 C 语言中的一个重要概念,用于访问内存中的数据。考虑以下指针和表达式:

  • 指针类型Student *p;

  • 表达式p->age++; 在这个表达式中,->++ 的优先级相同,但结合性是左结合性,所以先计算 p->age,然后对结果进行递增。

  1. 结合性的例子:赋值

    赋值运算符是右结合性的,这意味着赋值表达式从右向左计算。考虑以下赋值表达式:

  • 赋值表达式int a = b = c = 10; 这个表达式首先计算 c = 10,然后将结果赋给 b,最后将 b 的值赋给 a。最终,abc 的值都是 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)运算符因其独特的性质而在编程中非常有用。以下是按位异或运算符的几个关键性质:

  1. 与 0 异或:任何整数与 0 进行异或运算的结果是它本身。即 a ^ 0 = a
  2. 与自身异或:任何整数与自身进行异或运算的结果是 0。即 a ^ a = 0
  3. 交换律:异或运算满足交换律,即改变操作数的顺序不会影响结果。即 a ^ b = b ^ a
  4. 结合律:异或运算也满足结合律,这意味着括号内的运算顺序不影响最终结果。即 (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
}
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
下一篇