C语言_运算符_语句

优先级表格

左结合的意思:先计算运算符左侧的内容;右结合就是先计算运算符右侧的内容。

Priority & Binding Property in C

举几个例子

1
2
3
4
5
6
- 5 + 3 % 2    -> -4
3 * 7 % 3 -> 0
- i++ (i=5) -> 表达式值是-5, i值为6
-i+++i++
正确: - i ++ + i ++ -> 表达式值是0 , i值为7
错误: - i + ++i++ -> 错误,因为无论哪个++先操作,返回的都是一个不具名的临时变量,后一个++无法操作于临时无名变量。
  1. 后置++是等整个表达式计算完之后,再进行++;前置++是先++再参与到表达式中。

  2. 自增自减符(++--)只能用于更改具名的变量,不能更改没有固定内存地址的操作数。

  3. 而加减乘除(+ - * /)不能修改操作数,只能把每个操作数串联起来,组成表达式,最后计算值。

  4. 能改变变量本身的运算符只有++--和赋值运算符(= += &= >>=等等)

赋值运算符

1
2
3
4
x = 3 * 4    ->  x为12, 表达式也为12
x = y = 3 -> 连续赋值,右结合,先计算y=3子表达式,表达式值为3,则 -> x=3
x += p -> x = x + p
x *= y - 3 -> - 减号优先级大于 *= -> x = x * (y - 3)

关系运算符

1
2
3
4
5
//a = 100, b = 99, c = 101, ch = 98
a > b == c -> 1 == 101 -> 0
ch > 'a' + 1 -> 98 > 98 -> 0
d = a + b > c -> d = 199 > 101 -> d = 1 -> 1
b - 1 == a != c -> ==和!=同优先级,左结合 -> 98 == 99 != 101 -> 0 != 101 -> 1

比较大小的关系运算符会返回boolean(真、假),其中0代表假,非零代表真。系统返回的真值默认为1。

逻辑运算符&& ||

&&||优先级高。

1
2
3
4
5
6
//a = 100, b = 99, c = 101, x = 4, y = 6
a || b && c -> 先计算 b&&c -> a || 1 -> 1
!a && b -> ! 比&&优先级高, 0 && b -> 0
x >= 3 && x <= 5 -> >=比&&优先级高, 1 && 1 -> 1
!x == 2 -> ! 比==优先级高, 0 == 2 -> 0
a || 3 + 10 && 2 -> + 优先级最高, a || 13 && 2 -> a || 1 -> 1

逗号(坑)

  1. 逗号用在3个地方。一个是定义和初始化,一个是逗号表达式,还有传参的分隔。
  2. 逗号运算符是优先级别最低的(一定要记住,比=还低)。
  3. 逗号运算符遵循左结合,即从左到右依次计算,最后逗号表达式的值为最右边的值。
1
2
3
4
5
int main(void)
{
int x = 5, y = 7;
x + y, 6 - y, 9; //表达式值为9
}

定义和初始化

1
2
int x = 5, y = 7;
int z = x + y, 6 - y, 9; //error,前面如果有声明类型,不能在后面写逗号表达式
1
2
int x = 5, y = 7, z = 0; 
z = x + y, 6 - y, 9; //z被赋为12,但是因为逗号的优先级比=低,所以最后这个表达式值为9

函数传参中的逗号

这里会有坑出现。( 类似于量子测不准现象 doge

1
2
printf("%i\n",  x + y, 6 - y, 9 );   //打印出来为 x+y -> 12
printf("%i\n", (x + y, 6 - y, 9)); //加了括号之后, 打印9

这是因为,printf函数中有可变参,如果不加括号,则会把逗号视为参数的分隔符。

1
printf(const char * const format, ...)

后置++和逗号的爱恨情仇

1
2
int x = 5, y = 7, z = 0;
z = (x + y++, 6 - y++, 9, y); //z最后的值为9

在本次测试中,逗号表达式的值为9,即y在逗号表达式结束前就自增了,也就是说后置++这个平时动作最慢的老家伙也穿越不了逗号。看来逗号可以被封为无敌懒神。

然而,这只是本次测试的情况,后置++有时候比较讨厌,行为有时不一致,这和不同编译器不同的实现有关。

1
2
int x = 5, y = 7, z = 0;
z = x + y++, 6 - y++, 9, y; //z最后的值为12

括号是++和逗号的他俩的定心丸(其实也称得上搅屎棍),如果括号去掉,则z最后就等于12,即在y自增前把x+7赋给z。

按位运算符

位运算符直接操作变量的机器码(二进制)。

应用

  1. 网络程序中有时要专门处理某些位
  2. 工业控制中控制标识位
  3. 异或用于加密
1
2
3
4
#define A 0b0001
#define B 0b0010
#define C 0b0100
#define D 0b1000

要求,在不干扰其他灯的情况下,让B灯亮,按位或:

1
2
3
4
5
6
1000  ->  1010
P |= B:
1000
| 0010
-------
1010

检查B灯的亮灭,按位与:

1
2
3
4
5
6
1000  ->  返回一个0值或非0
P & B:
1000
& 0010
-------
0000

灭掉B,先按位非后按位与:

1
2
3
4
5
6
7
8
9
10
11
1010  ->  0000
~B:
~ 0010
-------
1101
P &= ~B:
1010
& 1101
-------
1000

异或用于加密:

1
2
3
4
5
6
7
8
9
text     ->   1101
password -> 0011
^
-------
locked 1110
password 0011
^
-------
text 1101

移位运算符

1
2
3
4
5
int main(void)
{
unsigned char a = 14u; //0000 1110
unsigned char b = a >> 1; //0000 0111 -> 7
}
1
2
3
4
5
int main(void)
{
unsigned char a = 128u; //1000 0000
unsigned char b = a >> 1; //0100 0000 -> 64
}
1
2
3
4
5
int main(void)
{
char a = 128u; //1000 0000 -> 无符号转为有符号 a 为-128
unsigned char b = a >> 1; //1100 0000 -> 有符号转为无符号 b 为 192
}

可得,对于有符号数,移位运算符保留了符号属性。如果不想保留符号特性,可以让操作数强制转为unsigned,如下:

1
2
3
4
5
int main(void)
{
char a = 128u; //1000 0000 -> 无符号转为有符号 a 为-128
unsigned char b = (unsigned char)a >> 1; //0100 0000 -> b为64
}

总结

(笃定地讲)所有表达式最后肯定会产生一个临时值,如果没有值产生,就不是表达式,而是一个语句。

语句

  1. 顺序语句

  2. 分支语句

    1. if
  3. 循环

    1. while
    2. do-while
    3. for

分支语句

1
2
3
4
5
6
7
8
int main(void)
{
if (0)
printf("true\n");
else
printf("false\n");
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
int main(void)
{
int x = 2;
if (x == 1)
printf("true\n");
else if (x == 2)
printf("true2\n");
else
printf("false\n");
return 0;
}

switch-case

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
int main()
{
if(a == 1)
printf("1\n");
else if(a == 2)
printf("2\n");
else if(a == 3)
printf("3\n");
else if(a == 4)
printf("4\n");
else
printf("else\n");

switch(a)
{
case 1:
printf("1\n");
break;
case 2:
printf("2\n");
break;
case 3:
printf("3\n");
break;
case 4:
printf("4\n");
break;
default:
printf("else\n");
break;
}
}

上面这个程序的所达到的效果,在逻辑上,if-else和switch-case是一样的。

switch后面括号内的变量只支持整型或枚举类型,枚举类型也是整型的一种。

如果分支很多且判别表达式值为整型,则使用switch-case更合适。

如果抛去这两个限制(分支的多少和判别表达式值的类型),则性能上switch-case更有优势:

  1. 编译器直接把case中的编号当做偏移量,对应到相应的内存地址上了。
  2. 于是直接略过了判断前面的过程,直接去找相应的分支。

循环

while

1
2
3
4
5
int main(void)
{
while (1)
printf("Hello");
}
1
2
3
4
5
6
7
8
int main(void)
{
while (1)
{
printf("Hello");
break;
}
}

continue

1
2
3
4
5
6
7
8
9
int main(void)
{
while (1)
{
printf("Hello");
continue;
printf("end"); //不走
}
}

求1到100加和:自然终止

1
2
3
4
5
6
7
8
int main(void)
{
//sum: 1 ~ 100
int i = 1, sum = 0;
while (i <= 100)
sum += i++;
printf("sum: %i\n", sum); // 5050
}

求1到100加和:条件break终止

1
2
3
4
5
6
7
8
9
10
11
12
int main(void)
{
//sum: 1 ~ 100
int i = 1, sum = 0;
while (1)
{
sum += i++;
if (i > 100)
break;
}
printf("sum: %i\n", sum); // 5050
}

do-while

不管while括号内条件是否符合,至少执行一次循环体。

1
2
3
4
5
6
7
8
int main()
{
int i = 10;
do
{
printf("Hello\n");
}while(i < 10); // 因为最后不是大括号结尾的,所以要加上;
}

for

for后的括号内是三条语句。第一条只运行一次,第二条是每次进入循环时都要判断,第三条是每次循环体运行完后执行。

1
2
3
4
5
6
7
8
9
10
int main(void)
{
//sum: 1 ~ 100
int sum = 0;
for (int i = 1; i <= 100; ++i)
{
sum += i;
}
printf("sum: %i\n", sum); // 5050
}

九九乘法表

1
2
3
4
5
6
7
8
9
10
int main(void)
{
for(int i = 1; i <= 9; ++i, printf("\n"))
{
for(int j = 1; j <= i; ++j)
{
printf("%i*%i=%-2i ", j, i, i * j);
}
}
}
  1. printf中的%-2i代表有符号整数,附加的效果是占两位,左对齐(负号的作用)。
  2. for表达式中的第三个表达式是最后做的处理,往往不需要返回值。在本例中,第一层for循环代表行的处理,第二层for循环代表列的处理,每当第二层for循环完毕后,需要换行,即每当第一层for循环体内容结束后,需要做两个事情:1、i加1;2、换行,可以合并为逗号表达式放在for表达式第三个空中。因为for括号中的第三个语句没有人来接受它的返回值,所以可以直接写printf这种无返回值的函数。

image-20240120173829903

总结

  1. while循环往往适用于不清楚运行次数的,不能精准控制步长的情况。
  2. for循环适用于知道运行次数的情况:需要明白i的范围、步长。

C语言_变量_常变量_作用域_static

作用域

局部变量

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<stdio.h>
void bar();
int main()
{
bar();
bar();
return 0;
}
void bar()
{
int val = 10;
printf("%i\n", val++);
}

以上程序两次都打印10。因为val的作用域只生存在函数当时所分配的栈帧中,此变量为自动/局部变量(auto/local variable),函数调用结束后变量自动销毁。

全局变量

全局变量(global variable)。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<stdio.h>
void var();
int val = 10;
int main()
{
bar();
bar();
return 0;
}
void bar()
{
printf("%i\n", val++);
}

第一次打印10,第二次打印11。

多文件下使用全局变量 - extern

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//文件A
#include<stdio.h>
void var();
//int val = 10;
extern int val;
int main()
{
bar();
bar();
return 0;
}
void bar()
{
printf("%i\n", val++);
}
//文件B
int val = 10;
  1. 只需要声明,不用赋值。
  2. extern声明之后不会再次分配额外的空间
  3. extern在编译时不检查。
  4. extern在链接时会检查其他文件中实际有没有这个变量。

更优雅的全局变量 - 静态变量

static修饰变量和函数的作用

  • static修饰变量: 在函数内部使用static修饰变量时,该变量称为静态局部变量。它的生命周期延长到整个程序运行期间,但作用域仅限于定义它的函数内部。静态局部变量在每次函数调用时不会被重新初始化,而是保留上一次调用结束时的值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>

void exampleFunction()
{
// 静态局部变量
static int counter = 0;
counter++;
printf("Counter: %d\n", counter);
}

int main()
{
exampleFunction(); // 输出:Counter: 1
exampleFunction(); // 输出:Counter: 2
exampleFunction(); // 输出:Counter: 3

return 0;
}

在这个例子中,静态局部变量counter在每次函数调用之间保留其值,而不会被重新初始化。

  • static修饰函数: 在函数声明或定义时使用static修饰,将函数的链接属性变为内部链接,限制其作用域在当前源文件内。这样,其他源文件无法调用这个静态函数。
1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

// 静态函数声明,限制其作用域在当前源文件内
static void staticFunction()
{
printf("This is a static function.\n");
}
int main()
{
staticFunction(); // 可以调用静态函数
return 0;
}

在这个例子中,使用static修饰的函数staticFunction只能在当前源文件内被调用。

static声明语句什么时候执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<stdio.h>
void var();

int main()
{
bar();
bar();
return 0;
}
void bar()
{
static int val = 10;
printf("%i\n", val++);
}

之前我一直以为static在函数第一次调用时会被执行,其他时候函数调用则跳过。经过调试,发现,static语句在第一次调用函数时也会直接跳过这条语句。这说明static变量在main函数执行前就被初始化了。

static的本质

  1. 本质上还是一个全局变量
  2. 在应用程序建立的时候,静态变量就创建好了。
  3. 在程序中写的意义只是说明变量名字的可见范围。如果写在大括号(不仅是函数体大括号,所有大括号都算)里,则出了大括号后就不认识该标识符;如果写在main函数外,则可见范围仅限于此文件,所有函数都可见。
    1. 如果函数里、函数外定义了同名static变量则:就近原则,优先选择本函数里的!
1
2
3
4
5
6
7
8
9
#include<stdio.h>
void fun()
{
static int val = 1;
}
int main()
{
printf("val: %i\n", val); // error, can't find val
}
1
2
3
4
5
6
7
8
9
10
11
#include<stdio.h>
static int val = 34;
void fun()
{
static int val = 1;
}
int main()
{
fun();
printf("val: %i\n", val); // 函数外的val,不认识fun函数里的val
}
1
2
3
4
5
6
7
8
9
10
11
12
13
#include<stdio.h>
static int val = 34;
void fun()
{
static int val = 1;
val = 2; // 优先选择本函数里的val
}
int main()
{
fun();
printf("val: %i\n", val); // 打印34。读取的是函数外的val,不认识fun函数里的val
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<stdio.h>
static int val = 34;
void fun()
{
{
static int val = 1;
}
val = 2; // 只认识函数外的val,函数里的上面这个大括号里的val不认识
}
int main()
{
fun();
printf("val: %i\n", val); // 函数外的val,不认识fun函数里的val
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<stdio.h>
static int val = 34;
void fun()
{
static int val = 1;
val = 2; // 函数外的val
}
int main()
{
static int val = 33;
fun();
printf("val: %i\n", val); // 打印33,优先选择本函数里的val
}

1
2
3
4
5
6
7
#include<stdio.h>
#define PI 3.14
int main()
{
int i = PI;
return 0;
}

常变量

如果PI赋予了普通的int型变量i,那么i可能会被篡改。这时可以用常变量修饰i

1
2
3
4
5
6
7
8
9
#include<stdio.h>
#define PI 3.14
int main()
{
//int i = PI;
//i = 9.78;
const int i = PI; // or: int const i = PI;
return 0;
}

但是常变量不建议这么使用。而是如下使用:

1
2
3
4
5
6
7
#include<stdio.h>
const float PI = 3.14f;
int main()
{
float i = PI;
return 0;
}

好处:

  1. 宏能做的,常变量都可以做到。
  2. 宏不能做到的:变量类型检查,常变量可以做。
    1. 虽然3.14f可以正确地表达float字面常量
    2. 但是90却不能正确地表达short字面常量,char、short诸如此类都是整型兼容的,形式与整型无区别,所以无法在宏定义时把类型信息附加上,因此丢失了类型信息。

其他总结

  1. #define宏定义和const的区别:
    • #define宏定义: 使用#define创建的宏是一种简单的文本替换机制。在预处理阶段,所有的宏名称都会被对应的文本替换,没有类型信息。例如:#define PI 3.14
    • const关键字: const用于创建常量,具有类型信息,并且在编译时进行类型检查。例如:const double PI = 3.14;。const创建的常量在内存中有分配空间,而宏定义只是简单的文本替换。
  2. 定义和声明的关系:
    • 定义: 定义是创建一个变量、函数、或其他实体,并分配相应的存储空间。例如:int x = 5;
    • 声明: 声明是告诉编译器某个实体的存在而不进行实际的创建。例如:extern int x;
    • 关系: 定义包含声明,但声明不一定包含定义。如果在某个文件中声明了一个变量,而在另一个文件中定义了该变量,那么编译时需要链接这两个文件。