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.注释规范
- 边写代码边修改注释
- 注释是提示而不是文档(应当说Why而不是How,触发业务逻辑非常复杂)
- 一目了然的代码不加注释
- 全局变量、常量定义必须加注释
- 注释能采用英文就用英文(避免乱码),避免使用缩写。特别是不常用的注释
- 注释可以写在同一行或者右上方
- 代码比较长,有多重嵌套的时候,最好在一些段落的结束处加上注释
- 注释的缩进和代码的缩进一致
- 说明数值的单位是何
- 对变量的范围给出注释,尤其是参数
- 对一系列的数字编号做注释,尤其是在编写底层驱动程序的时候(比如引脚编号)
- 对于函数的入口何出口数据、条件语句、分支语句给出注释(结合情况就行)
- 除非必要,应避免在一行代码或者表达式的中间插入注释
- 在快语句过长的代码结尾加上注释,说明代码块结束的是哪一部分的块
- 在分发软件时会出现不同版本(免费/收费)但是一份代码的情况,为了避免维护成本提高,可以使用宏指令(比如#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 <stdio.h>
#include <windows.h>
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 <stdio.h>
int main()
{
char ch = '1';//这里实际上一直在发生一个现象,就是截断4个字节数据写到1个字节的数据,因为在C99标准规定,'a'叫做整型字符常量,被看作是int类型
printf("%zd", sizeof('1'));//因此这里输出的是4(字节),而不是1(字节)
return 0;
}
但是如果是在C++中运行就会看到这里输出的是1而非4了
- 双引号:sizeof(“”)现象
#include <stdio.h>
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 <stdio.h>
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 <stdio.h>
//置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 <stdio.h>
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 <stdio.h>
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 <stdio.h>
int main()
{
int i = 2.9;
int j = -2.9;
printf("%d %d", i, j);
return 0;
}//可以看到输出了“2 -2”
- 库函数trunc做的就是“向零取整”的工作(头文件是math.h)
#include <stdio.h>
#include <math.h>
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 <stdio.h>
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<d),其中q叫做商,r叫做余数”。其中q叫做商,r为余数。为什么C不满足呢?因为这个r小于0了。
- 因此我们应该重新修订这个定义为“对于整数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”,具体先计算谁由编译器决定,我们唯一可以确定地是:两个乘法运算永远比中间的加法运算优先。文章来源:https://www.toymoban.com/news/detail-475287.html
11.总结
今天我带您了解了:注释、续行符/转义符、单双引号等有关符号的使用细节,也许这些扣得比较细,您只需了解即可,或者至少不会因为这些细小琐碎得点而犯错。本节中提到的关于取余/取模运算,这里也仅作复习,更加详细的取整细节,您可以去我的上一篇博文C语言32个关键字看看,那里会更加详细……
最后,望与君共勉。文章来源地址https://www.toymoban.com/news/detail-475287.html
到了这里,关于017+limou+C语言符号的深化理解的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!