017+limou+C语言符号的深化理解

0.前言

您好,这里是limou3434的一篇博客,感兴趣您可以看看我的其他博文系列。本次我主要给您带来了C语言符号相关的知识。

1.注释

1.1.去除注释的本质

实际上在编译期间,去注释的本质是:将注释替换成空格

1.2.C++风格注释

C++风格注释是使用“//”将一整行代码行全部注释,而“//注释”还可以使用续行符“\”接替注释另起一行。

/*合法*/#/*合法*/define/*合法*/STR/*合法*/abcd/*合法*/efgh 

//这个宏的定义是没有问题的,注意宏的#和define之前可以留有空格

1.3.C语言风格注释

“/**/”不可以嵌套使用,这是因为C语言风格注释遵循就近原则,如果允许嵌套在这里就会出现问题,注意带有指针解引用的除法和C风格注释不能混用。

  • 以下代码存在问题
int x = 10; 
int* p = &x; 
int y = 20; 
y = x/*p;
  • 应该改成以下的代码
int x = 10; 
int* p = &x; 
int y = 20; 
y = x/(*p);//或者写成y = x / *p

1.4.注释规范

  1. 边写代码边修改注释
  2. 注释是提示而不是文档(应当说Why而不是How,触发业务逻辑非常复杂)
  3. 一目了然的代码不加注释
  4. 全局变量、常量定义必须加注释
  5. 注释能采用英文就用英文(避免乱码),避免使用缩写。特别是不常用的注释
  6. 注释可以写在同一行或者右上方
  7. 代码比较长,有多重嵌套的时候,最好在一些段落的结束处加上注释
  8. 注释的缩进和代码的缩进一致
  9. 说明数值的单位是何
  10. 对变量的范围给出注释,尤其是参数
  11. 对一系列的数字编号做注释,尤其是在编写底层驱动程序的时候(比如引脚编号)
  12. 对于函数的入口何出口数据、条件语句、分支语句给出注释(结合情况就行)
  13. 除非必要,应避免在一行代码或者表达式的中间插入注释
  14. 在快语句过长的代码结尾加上注释,说明代码块结束的是哪一部分的块
  15. 在分发软件时会出现不同版本(免费/收费)但是一份代码的情况,为了避免维护成本提高,可以使用宏指令(比如#ifdef)来变相注释部分不想删除的注释,甚至不嫌弃的话还可以使用if语句(有点矬,特别不推荐)

2.续行符和转义符

“\”在C语言中有两个最基本的用途,一是换行符,二是转义符

2.1.续行符

续行符是为了提高代码的可读性存在的,在续行符后面不要包含空格(加上“\”其实会加强对“换行”的自描述)

2.2.转义符

使得一些特殊字符不以字面值的形式出现,或者反过来特殊转字面

//1.字面转特殊
printf("n /n");//其中n是字面值,'\n'是转移后的特殊字符

//2.特殊转字面
printf(""");//出错
printf("\"");//打印出双引号符号

2.3.回车和换行

  • 严格来说,回车(\r)和换行(\n)是不同的概念,大部分语言当中回车和换行是合并为一个’\n’中的,即:大部分语言中的’\n’是身兼顾两职的
  • C语言的旋转光标代码
#include 
#include 
int main()
{
	const char* lable = "|/-\\";
	int index = 0;
	while (1)
	 {
		index %= 4;
		printf("[%c]\r", lable[index]);
		index++;
		Sleep(20);
	}
	return 0;
}

3.单引号和双引号

  • 单引号:对于C语言的C99中’1’等整型字符常量的定义
#include 
int main()
{
	char ch = '1';//这里实际上一直在发生一个现象,就是截断4个字节数据写到1个字节的数据,因为在C99标准规定,'a'叫做整型字符常量,被看作是int类型
	printf("%zd", sizeof('1'));//因此这里输出的是4(字节),而不是1(字节)
	return 0;
}

但是如果是在C++中运行就会看到这里输出的是1而非4了

  • 双引号:sizeof(“”)现象
#include 
int main()
{
	printf("%zd\n", sizeof("abcd"));
	printf("%zd\n", sizeof("ab"));
	printf("%zd\n", sizeof(""));//因为有'\0'的存在,并且每个字符都是按char存储
	return 0;
}
//输出5、3、1
  • 说到字符我们还应该;了解一下“为什么计算机需要字符?”

计算机是为人类所服务的,计算机只识别二进制,为了和人类产生交互,就存在了ASCII码值表等字符集合

4.逻辑运算符&&和||

  • 与按位与(&)和按位或(|)区分开来
    • &&(||)级联的是多个逻辑表达式,得真假结果
    • 而&(|)级联的是多个数据,按照逐比特位进计算,得出数据结果
  • 注意两者都有短路效应,这个效应某些时候可以替代if语句
int flag = 0;
scanf("%d", &flag);
flag && function();//其中function()是在别处定义的代码,其是否执行取决于flag的输入值,同样也可以使用“||”来形成判断语句

5.位操作

  • 注意位操作需要加上符号位,例如:在C语言中“~(-1)”就会变成“0”
#include 
int main()
{
	int i = -1;
	//1000 0000|0000 0000|0000 0000|0000 0001(原)
	//1111 1111|1111 1111|1111 1111|1111 1110(反)
	//1111 1111|1111 1111|1111 1111|1111 1111(补)
	int j = -3;
	//1000 0000|0000 0000|0000 0000|0000 0011(原)
	//1111 1111|1111 1111|1111 1111|1111 1100(反)
	//1111 1111|1111 1111|1111 1111|1111 1101(补)
	int z = i ^ j;
	//1111 1111|1111 1111|1111 1111|1111 1111(补)
	//1111 1111|1111 1111|1111 1111|1111 1101(补)
	//											^
	//-------------------------------------------
	//0000 0000|0000 0000|0000 0000|0000 0010(a^b后,补,检测到是正数)
	//0000 0000|0000 0000|0000 0000|0000 0010(反)
	//0000 0000|0000 0000|0000 0000|0000 0010(原)
	printf("%d\n", z);
	printf("%d\n", ~(i));

	int a = -2;
	//1000 0000|0000 0000|0000 0000|0000 0010(原)
	//1111 1111|1111 1111|1111 1111|1111 1101(反)
	//1111 1111|1111 1111|1111 1111|1111 1110(补)
	int b = 3;
	//0000 0000|0000 0000|0000 0000|0000 0011(原)
	//0000 0000|0000 0000|0000 0000|0000 0011(反)
	//0000 0000|0000 0000|0000 0000|0000 0011(补)
	int c = a | b;
	//1111 1111|1111 1111|1111 1111|1111 1110(补)
	//0000 0000|0000 0000|0000 0000|0000 0011(补)
	//											|
	//-------------------------------------------
	//1111 1111|1111 1111|1111 1111|1111 1111(a|b后,补,检测到是负数)
	//1111 1111|1111 1111|1111 1111|1111 1110(反)
	//1000 0000|0000 0000|0000 0000|0000 0001(原)== -1
	printf("%d\n", c);
	return 0;
}
  • 有些教材会用真假来解释位操作符的运算过程,但这是不太严谨的一种说法,应该用0/1比特位来解释才更加严谨
  • 异或的常用结论
    • 任何数和0异或都不变
    • 异或支持交换律、结合律
  • 位操作最好使用宏定义好后再使用,避免出错,但是
  • 一个关于位运算的代码例子
#include 
//置1
#define SETBIT(x, n) (x |= (1<<(n - 1)))
//置0
#define CLRBIT(x, n) (x &= ~(1<<(n - 1)))
void ShowBits(int x)
{
	int num = sizeof(x) * 8 - 1;
	while (num >= 0)
	{
		if (x & 1 << num)
		{
			printf("1 ");
		}
		else
		{
			printf("0 ");
		}
		num--;
	}
}
int main()
{
	int x = 0;
	//1.设置指定比特位为1
	SETBIT(x, 3);
	SETBIT(x, 5);
	CLRBIT(x, 5);
	//2.显示int的所有bit位
	ShowBits(x);
	return 0;
}
  • 另外一个位运算例子
int main()
{
    char ch = 0;
    printf("%zd\n", sizeof(ch));
    printf("%zd\n", sizeof(~ch));
    printf("%zd\n", sizeof(ch >> 1));
    printf("%zd\n", sizeof(ch << 1));
  	printf("%zd\n", sizeof(!ch));
    return 0;
}
//可以看到输出了1,4,4,4,1(最后一个数字在linux中是4)

这样输出的原因是无论是何种运算符,在进行计算的时候,目标都是要计算机进行计算。而计算机中只有CPU具有运算能力,计算的数据都要从内存中拿取到CPU的寄存器中。寄存器的位数和计算机的位数是一样的,因此char类型的数据读到寄存器中,也只能填补8个位,高24位只能进行整型提升。并且sizeof不是函数是关键字,sizeof在编译期间就硬编码进程序,而正是在整型提升这个阶段进行计算的,而后续ch变量又被截断(这样仅仅是简单理解,忽略了很多细节)。

另外通过最后一个数也可以看到,不同编译器对“整型提升”可能还存在不同的解释(Linux的可能会比较准确)

  • 深刻理解“>>“和“<<”

最重要的是右移操作符,分为“逻辑右移”和“算术右移”两种,前者补0,后者补符号位(这会影响到是否“*”或“/”2的问题)。
而在VS中右移是算术右移动,而如何右移则取决于自身的类型,和保存什么数据是没有关系的。
在使用位移操作的时候也需要注意优先级的问题。

unsigned i = -1;
i = i >> 1;//补0而不是补1,因为-1的数据类型是unsigned
0x01 << 2 + 3;//加法运算符的优先级大于右移操作符,注意不能移动负数或者超出计算机位数的整数

6.花括号

花括号的作用是“打包”

7.++和–

  • 分为前置加加和后置加加、前置减减和后置减减
  • 深刻理解后置(汇编理解)
#define _CRT_SECURE_NO_WARNINGS 1
#include 
int main()
{
00007FF7D73D1750  push        rbp  
00007FF7D73D1752  push        rdi  
00007FF7D73D1753  sub         rsp,128h  
00007FF7D73D175A  lea         rbp,[rsp+20h]  
00007FF7D73D175F  lea         rcx,[__79481D6E_main@c (07FF7D73E1008h)]  
00007FF7D73D1766  call        __CheckForDebuggerJustMyCode (07FF7D73D1343h)  
	int a = 10;
00007FF7D73D176B  mov         dword ptr [a],0Ah//在内存中开辟空间a,放入值0Ah
	int b = a++;
00007FF7D73D1772  mov         eax,dword ptr [a]//把内存的数据a移动到CPU里的eax寄存器
00007FF7D73D1775  mov         dword ptr [b],eax//把eax的内容放到b里面
00007FF7D73D1778  mov         eax,dword ptr [a]//把内存的数据a移动到CPU里的eax寄存器
00007FF7D73D177B  inc         eax//将eax自增
00007FF7D73D177D  mov         dword ptr [a],eax//将计算结果从寄存器移动到内存
	return 0;
00007FF7D73D1780  xor         eax,eax  
}
00007FF7D73D1782  lea         rsp,[rbp+108h]  
00007FF7D73D1789  pop         rdi  
00007FF7D73D178A  pop         rbp  
00007FF7D73D178B  ret 
#define _CRT_SECURE_NO_WARNINGS 1
#include 
int main()
{
00007FF6A29A1B70  push        rbp  
00007FF6A29A1B72  push        rdi  
00007FF6A29A1B73  sub         rsp,108h  
00007FF6A29A1B7A  lea         rbp,[rsp+20h]  
00007FF6A29A1B7F  lea         rcx,[__79481D6E_main@c (07FF6A29B1008h)]  
00007FF6A29A1B86  call        __CheckForDebuggerJustMyCode (07FF6A29A1343h)  
	int a = 10;
00007FF6A29A1B8B  mov         dword ptr [a],0Ah  
	a++;
00007FF6A29A1B92  mov         eax,dword ptr [a]  
00007FF6A29A1B95  inc         eax  
00007FF6A29A1B97  mov         dword ptr [a],eax  
	return 0;
00007FF6A29A1B9A  xor         eax,eax  
}
00007FF6A29A1B9C  lea         rsp,[rbp+0E8h]  
00007FF6A29A1BA3  pop         rdi  
00007FF6A29A1BA4  pop         rbp  
00007FF6A29A1BA5  ret 
  • inc是汇编指令中的一种,用于将指定操作数的值加1。它是add指令的一种特殊形式,其目的地操作数(即所要增加的值)为1时,可以使用inc指令代替add指令来节省代码空间
  • 上述代码证明,如果没有到使用a,那么a++仅仅会进行++,不进行“使用操作”

8.取整与“取余/取模”运算

8.1.向零取整

C语言、C++、Java默认都是向零取整

  • C程序的默认情况(除法等情况)
#include 
int main()
{
	int i = 2.9;
	int j = -2.9;
	printf("%d %d", i, j);
	return 0;
}//可以看到输出了“2 -2”
  • 库函数trunc做的就是“向零取整”的工作(头文件是math.h)
#include 
#include 
int main()
{
	printf("%d %d", (int)trunc(2.9), (int)trunc(-2.9));
	return 0;
}//也是输出零向取整的结果“2 -2”

8.2.向-∞取整(地板取整/高斯取整)

floor(-2.9);//-3.0
floor(-2.1);//-3.0
floor(2.9);//2.0
floor(2.1);//2.0

8.3.向+无穷取整

ceil(-2.9);//2.0
ceil(-2.1);//2.0
ceil(2.9);//3.0
ceil(2.1);//3.0

8.4.四舍五入取整

round(2.1);//2.0
round(2.9);//3.0
round(-2.1);//-2.0
round(-2.9);//-3.0

8.5.“取余/取模”的细节

  • 除法和“取余/取模”运算在不同的语言当中有可能不太一样

以下是python中代码结果

C:\Users\DELL>python
Python 3.8.5 (tags/v3.8.5:580fbb0, Jul 20 2020, 15:57:54) [MSC v.1924 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> -10//3
-4
>>> -10%3
2

以下是C语言中代码结果

#include 
int main()
{
	int a = -10;
	int b = 3;
	printf("%d\n", a / b);//-3
	printf("%d\n", a % b);//-1
	return 0;
}
  • 根据数学中对“取模/取余”的定义,可以发现上述部分代码(例如C语言的)是不严格满足这个定义的
    • 在数学中“对于整数a、q、d、r,其中d为非0,有a=q*d+r(0<=r
    • 因此我们应该重新修订这个定义为“对于整数a、q、d、r,其中d为非0,有a=q*d+r(0<=|r|<|d|),其中q叫做商,r叫做余数”,这样C语言的运算结果就也是正确的了
    • 这样余数就会分为正余数和负余数,而“余数的大小”取决于“商q”,“商q”取决于除法除法的“除法规则”,“除法规则”就是“取整规则”。因此会发现,结果的不一样实际上是语言的取整规则在作怪
      • 而C语言的取整规则默认是向0取整,“-10/3=-3(向0取整)”,再根据公式“-10=-3*3+r”,得到“r=-1”
      • python的取整规则默认是向-∞取整,“-10/3=-4(向-∞取整)”,再根据公式“-10=-4*3+r”,得到“r=2”
  • 取余和取模其实是不一样的
    • 取余:尽可能让商进行向0取整,因此C语言的是取余,因此严格来说这个操作符%就叫“取余”
    • 取模:尽可能让商进行向-∞取整,因此python的是取模,因此严格来说这个操作符%就叫“取模”
  • 尽管由于取整规则不同导致余数不相等,但是也有结果相等的时候
    • 不难发现,只要是操作数都是正数,则进行“取余/取模”运算,无论是哪一种,得到的结果都是一样的。因为对于正数来说,“0向取整”和“-∞取整”的方向恰好是一样的
  • 在C语言中,被除数的符号和余数的符号是一样的,但是注意仅限于C语言(例如python就不满足),当然最好还是统一使用“取整模式+求余公式”来计算,这样不仅得到余数值,还得到余数符号

9.符号贪心匹配法

在考虑运算符和优先级之前还会做一个事情,即“符号匹配”。C的符号在进行匹配的时候,使用了贪心算法:每一个符号从左到右应该包含尽可能多的字符。

例如“==”中间不能加入空白、“a+++10”应该理解为“a++ + 10”、“i+++++j”应该写成“i++ + ++j”,因此在平时写代码的时候,还是要要注意适当空格(这有可能会影响到语法使用!而不仅仅是为了美观)。

甚至在有的编译器没有对贪心实现完全,哪怕是正确的语法没有给与空格,还会进行报错。

10.计算路径的不唯一的本质

其本质是“数据在寄存器中的存储顺序不一样”,例如:对于“a * b + c * d”,光从语法层次是无法判断先进行"a * b"还是“c * d”,具体先计算谁由编译器决定,我们唯一可以确定地是:两个乘法运算永远比中间的加法运算优先。

11.总结

今天我带您了解了:注释、续行符/转义符、单双引号等有关符号的使用细节,也许这些扣得比较细,您只需了解即可,或者至少不会因为这些细小琐碎得点而犯错。本节中提到的关于取余/取模运算,这里也仅作复习,更加详细的取整细节,您可以去我的上一篇博文C语言32个关键字看看,那里会更加详细……
最后,望与君共勉。

你可能感兴趣的:(C语言学习笔记,c语言,开发语言)