文章目录
前言
对于C语言当中的符号,相信大家并不陌生,我们在进行运算,输入和输出的时候不免就会涉及到各种各样的符号,本章我们将要对这些符号进行更深一步探讨,比如说 printf("%d\n", sizeof(‘1’)); 输出的答案会是什么呢? 而5/(-2)的输出结果又是什么呢?这些我们在学习C语言的过程中是基本上没有涉及到过的,因为这需要更多的知识储备,下面我们就来更深一步的了解这些符号。
一、注释符号
1.1基本注释使用
1.2基本注释注意事项
我们在linux平台上演示,看下面代码:
我们编译会有问题吗?gcc编译一下:
答案是第9行有问题,为什么前面没有问题,而最后一个就有问题呢?其实很好理解,因为我们的注释符号被替换成为了空格,而前面被替换为空格之后也不会影响编译,而最后一个被替换为了空格后就会产生编译错误。那么真的是这样的吗?
此时我们预编译生成test.i
打开test.i
所以注释被替换,本质是替换成空格。
再看下面代码:
根据上面说的,我们看到#和define中间会有空格,那么编译会出错吗?我们gcc编译一下test1.c
这是因为平台不同吗?我们在vs2019上试一下:
此时我们发现编译并没有出错, 所以我们不能认为#和define就一定是连接到一起的,这种说法是错误的,但是我们在书写时应该将#和define连到一起写,这也是一种编码的良好规范。
那么注释支持嵌套吗?
结果很显然是不支持的,直接就报错了。再看这段代码
会输出y吗?相信大件也能看出来这里的*直接于/结合在一起了,所以我们在计算的时候要注意一下这里。
1.3注释的基本要求
这里大家可以多看一下:写注释对于我们程序员写代码是很重要的,其实大多数程序员在写代码的时候是很讨厌写注释的,但是我们写的代码不仅仅是我们自己能够看懂,也要别人能够读懂,所以写注释是每个程序员必备的要求。
二、接续符和转义符
2.1续行功能()
先看一个例子:
这个代码大家很容易理解,但是当我们if里面的判断条件很长的时候,我们就需要续行符了:
那么我们改变\\的位置试一下
\之前加空格是不会报错的
此时我们就会发现 \ 之后加空格就会出错。所以我们使用续行符的时候就要注意了。有些人可能在想我在写C语言代码的时候重来没有用过 \ 啊,但是我也可以往下面续行,我想说的是,虽然可以,但是这样写代码会造成二义性,我们写的代码是一行还是多行呢?不利于别人理解,所以这也是一种良好的编码规范。
2.2转义
我们先来了解一下什么转义字符,什么是字面字符:
字面字符简单理解就是就是我们看到的是什么字符就是什么字符,而转义字符就是将一些字符赋予某些特殊意义的字符。
这里的转义字符我们就不多讲了,相信大家也很熟悉了。接下来我们来看下我们没有了解过的:
\r 和 \n 分别是什么大家知道吗?大家可能会想,这上面不是有吗? \r 是回车, \n 是回车换行。
对的,其实这没有错。那我们首先就先来了解一下什么是回车?什么又是换行?
回车:光标回到当前行的最开始。
换行:光标移动到下一行。
我们用张图片表示:
大家可能会想,刚刚不是才说 \n 是回车换行吗?为什么你吧它认为是换行呢?其实 \n 就是换行的意思,只是我们使用的大多数语言当中把 \n 当为回车换行了。所以我们现在理解的 \n 其实就是回车换行。大家不妨可以看下自己的键盘上的enter键,就是回车加换行。
我们看一个好玩的程序
#include<stdio.h>
#include<windows.h>
int main()
{
int index = 0;
//"\\"的意思是将转义字符(\)转义成为字面字符
const char* lable = "|/-\\";
while (1)
{
index %= 4;
printf("[%c]\r", lable[index]);//回到当前行最开始
index++;
Sleep(300);//睡眠0.3秒
}
printf("\n");
return 0;
}
这个程序是死循环,效果就是光标不断旋转,大家可以试一试,这里还有一个倒计时的小程序:大家可以去玩一玩。
#include<stdio.h>
#include<windows.h>
int main()
{
int i = 10;
while (i >= 0)
{
Sleep(1000);
printf("%2d\r", i);
i--;
}
printf("倒计时结束\n");
return 0;
}
从10到1每隔一秒输出。
三、单引号和双引号
3.1基本概念
单引号是字符,双引号是字符串
那么大家看看这个代码输出的结果是什么?
#include<stdio.h>
#include<windows.h>
int main()
{
printf("%d\n", sizeof(1));
printf("%d\n", sizeof("1"));
//C99标准的规定,'a'叫做整型字符常量(integer character constant),被看成是int型
printf("%d\n", sizeof('1'));
char c = '1';
printf("%d\n", sizeof(c));
return 0;
}
前面两个和最后一个很好理解,第一个是整形字符,输出的自然就是4,第二个是字符串还有\0,所以输出的就是2,最后一个是char类型,就是1,那么为什么 ‘1’ 却是4呢?我们换上linux平台
同样的代码,在linux上会是一样的吗?
此时我们发现在linux平台上也是4,那么既然是4个字节,是不是说 ‘1’ 就是整形呢?对的,其实我们在理解字符的时候一直把’1‘看为一个字节,所以就会产生一种误区,那么既然是4个字节,那么char c = ‘1’;其实就是发生了截断,是将4个字节的int类型截断为了一个字节的char类型。(注意:这仅是C语言当中,c++中的答案是不一样的)
3.2特殊情况
所以单引号引起的必须要字符,而双引号引起的字符串可以不带字符,因为本身就存在’\0’
3.3为何计算机需要字符
计算机本质只认识二进制,那么计算机为何需要字符呢?直接全部二进制不香吗?
因为计算机是为了解决人的问题,可是,人怎么知道计算机解决了人的问题??你输出二进制结果,人能直接看懂吗?
所以,为何计算机需要需要字符,本质是为了让人能看懂。 用一张图片来表示:
那为什么又是英文的呢?中文不香吗?
最早的计算机是美国人发明的,他们的语言是英语,大家想想,英语,不就是26个英文字母+一大堆标点符号组成的吗? 另外,计算机刚刚开始发明,人美国人只要能解决他们的问题就行,所以就有了现在的简单字符
计算机只认识二进制,而人只认识字符。所以,一定要有一套规则,用来进行二进制和字符的转化,这个就叫做ASCII码表
其实我们在用C语言提供的scanf(格式化输入)和printf(格式化输出)的时候就是将输入字符格式化为对应的变量类型和将对应的变量类型格式化为字符输出,比如说今天定义一个inta=0; scanf("%d", &a);我们输入的都是一个一个字符而scanf能将我们输入的一个一个字符转化为int 类型的数据。同理:printf("%d\n", a);也是将一个int类型的数据转化为一个一个的字符输出到屏幕上
怎么证明呢?首先我们看printf函数的返回值
再看下面的代码:
相信大家都明白了吧。
四、逻辑运算符
4.1&&
级联两个(多个)逻辑表达式,必须同时为真,结果才为真
例如:
这个相信大家都没问题。此时我们将++j改为 j++会有一样吗?
显然没有输出you can see me,这是因为j++是j是先判断,后++。所以不会输出。这也问题不大
4.2||
级联两个(多个)逻辑表达式,必须至少一个为真,结果才为真
例如:
这里的输出为什么是1和0呢?是因为++i已经大于0了,||只要满足其中一个为真就是真,所以后面的运算就不用进行了。这也很好理解。
短路
上面的一个条件不满足,已经不需要在看后续的条件的情况,就叫做短路
输入条件为1就继续,当条件为0的时候就直接停止,这其实就是一个if判断
五、位运算符
5.1基本概念
以代码的方式,分别理解下面位操作的基本含义
&& vs & || vs |
&& 和 ||: 级联的是多个逻辑表达式,需要的是真假结果
& 和 | : 级联的是多个数据,逐比特位进行位运算
5.2重点符号^
首先看这个代码:
int main()
{
printf("%d\n", 5 ^ 5);
return 0;
}
这个很容易理解
#include<stdio.h>
#include<windows.h>
int main()
{
printf("%d\n", 0 ^ 1);
printf("%d\n", 0 ^ 2);
printf("%d\n", 0 ^ 3);
printf("%d\n", 0 ^ 4);
return 0;
}
大家看这段代码输出的是什么?
<此时我们发现0和1、2、3、4异或都还是1、2、3、4,任何数和0异或都是它本身吗?没错。这是因为异或的定义:两个数同二进制位相异则为1,相同则为0,所以0和任何数异或都不会影响。那么我们再看下面这个例子:
#include<stdio.h>
#include<windows.h>
int main()
{
printf("%d\n", 4 ^ 5 ^ 4);
printf("%d\n", 5 ^ 4 ^ 4);
printf("%d\n", 4 ^ 4 ^ 5);
return 0;
}
会输出什么呢?
我们发现交换几个数异或的位置结果不会发生改变。这其实根交换律一样的。不会影响结果。而两个数异或为就为0,任何数和0异或还是它本身。所以结果都是4。
那么我们现在来做一道题:交换两个整数
大家可能首先会想到的就是定义一个临时变量:
那么我们在来限制一下条件,就是不能使用临时变量。那么大家可能又会想到下面这种方法:
但是这种方法就会有一种问题,如果两个数很大,就会发生整形溢出。那么还有方法没有呢?
就是异或:
相信大家直接看输出的结果还是会有疑问,我们画图来表示:
此时相信大家对异或又更熟悉了吧。
<< (左移)和 >>(右移)
推荐:位操作需要用宏定义好后在使用
比如现在我们要将某个数的一个二进制位设置为1,那么就可以使用宏定义。
#include<stdio.h>
#include<windows.h>
// 0 | 0 0 | 1 任何数与上0都是是它本身
// 1 | 1 0 | 1 任何数与上1都是1
#define SETBIT(x, n) (x |= (1 << (n - 1)))
void ShowBits(int x)
{
int num = sizeof(x) * 8 - 1;//最大移位
while (num >= 0)
{
if (x & (1 << num)) //比特位为1就打印
{
printf("1 ");
}
else//比特位位0就打印
{
printf("0 ");
}
num--;
}
printf("\n");
}
int main()
{
int x = 0;
//设置指定比特位为1
//x = 0 | (1 << 4)
// 0000 0000 0000 0000 0000 0000 0000 0000 0
// 0000 0000 0000 0000 0000 0000 0001 0000 (1<<4)
// 0000 0000 0000 0000 0000 0000 0001 0000 x
SETBIT(x, 5);
ShowBits(x);//显示int的所有比特位
return 0;
}
就是我们想要打印的结果。
是一个整形提升问题
5.3整形提升问题
先看代码:
#include<stdio.h>
#include<windows.h>
int main()
{
char c = 0;
printf("%d\n", sizeof(c));
printf("%d\n", sizeof(~c));
printf("%d\n", sizeof(c << 1));
printf("%d\n", sizeof(c >> 1));
return 0;
}
大家认为会输出的是什么?
这是为什么呢?是因为平台不同吗?此时我们在linux平台上演示:
gcc编译test.c:
此时我们发现在linux上输出的结果也是一样的。这是为什么呢?一个char类型不是1个字节吗?怎么在进行位运算后就变为了4个字节呢?下面做出解释:
无论任何位运算符,目标都是要计算机进行计算的,而计算机中只有CPU具有运算能力(先这样简单理解),但计算的数据, 都在内存中。故,计算之前(无论任何运算),都必须将数据从内存拿到CPU中,拿到CPU哪里呢?毫无疑问,在CPU 寄存器 中。
而寄存器本身,随着计算机位数的不同,寄存器的位数也不同。一般,在32位下,寄存器的位数是32位。
可是,你的char类型数据,只有8比特位。读到寄存器中,只能填补低8位,那么高24位呢?
就需要进行“整形提升”。
我们转到在vs2019上转到反汇编
此时发现反汇编代码上直接就已经显示出来了结果就是1,4, 4,4,而不是我们程序运行起来过后sizeof算出的结果。这就是为什么我们看到的是4了。
5.4左移和右移
(<<)左移: 最高位丢弃,最低位补零
( >>)右移:
- 无符号数:最低位丢弃,最高位补零[逻辑右移]
- 有符号数:最低位丢弃,最高位补符号位[算术右移]
<<(左移)
(>>)右移
逻辑右移:
算术右移:
如何理解"丢弃"
基本理解链: << 或者 >> 都是计算,都要在CPU中进行,可是参与移动的变量,是在内存中的。
所以需要先把数据移动到CPU内寄存器中,在进行移动。
那么,在实际移动的过程中,是在寄存器中进行的,即大小固定的单位内。那么,左移右移一定会有位置跑到"外边"的情况。
我们用一张图片形象的表示:
六、 ++和 --操作
6.1基本操作(以++为例)
6.2深刻理解 a++
#include<stdio.h>
#include<windows.h>
int main()
{
int a = 8;
int b = a++;
printf("%d %d\n", a, b);
return 0;
}
这个代码相信大家都知道:此时我们就在汇编角度去深刻理解它:
同样我们来看这段代码:
#include<stdio.h>
#include<windows.h>
int main()
{
int a = 8;
int b = ++a;
printf("%d %d\n", a, b);
return 0;
}
答案也很容易就出来了:我们也从汇编角度去看一看根上面的a++有什么不同:
可能大家理解比较难,不过大家大概对前置++和后置++应该有了更深的吧!我们在写代码使用前置++或则后置++的时候,如果没有接受方(int a = 10; a++;),其实没有影响,所以我们不要纠结使用前置++还是后置++,但是一旦有接受方(int a = 10; int b = a++;),此时使用前置++和后置++就会有影响了。
强烈不推荐代码书写:
我们先在VS2019展示
大家看这一段代码:
#include<stdio.h>
#include<windows.h>
int main()
{
int i = 1;
int j = (++i) + (++i) + (++i);
printf("%d\n", j);
return 0;
}
相信大家看完也是一脸懵吧!我们看答案
然后我们换上linux平台:
此时我们gcc编译test.c,运行a.out:
发现此时运行出来的结果10,同样一段代码,在不同的平台上运行出来的结果也不同,所以写的这段代码是极其不友好的,大家千万不要书写出这样的代码。
补充:
这是什么意思呢?举个例子:
这个大家了解一下即可!
七、取余(/)和取模(%)运算
7.1取余
我们在C语言中使用取余(/)的时候,结果都是将小数去掉后所得到的的,那么为什么要将小数去掉呢?我们接下来就谈一谈取整。
7.2取整
相信大家都能看出来,这其实是向0取整的,我们在数轴上表示:
此时我们在看上面(5/2)h和(-5/2)的结果为什么是2和-2呢?其实我们C语言默认使用的就是0向取整。C语言又这样的函数能够实现向0取整吗?答案是有的:
我们也可以使用它验证:
我们发现确实是向0取整,那么我们能不能不向0取整呢?答案是也是可以的:
是什么意思呢?用通俗的话说就是向小的方向取整:我们用数轴表示:
我们用代码来演示:
那既然有想负无穷取整,那必然也有正无穷取整:
数轴表示:
代码演示:
还有一种我们最熟悉的取整方式,就是四舍五入:
代码演示:
汇总例子
既然有这么多的取整方式,那么我们在进行取整的时候,具体使用那种方式取整呢?这里没有定论,具体使用那种方式取整应该具体场景,不同的时候有不同的取整方式。
7.3取模
取模概念: 如果a和d是两个自然数,d非零,可以证明存在两个唯一的整数 q 和 r,满足 a = q*d + r 且0 ≤ r < d。其中,q 被称为商,r 被称为余数。
这是什么意思呢?我们看一个例子;
这个相信大家就很好理解了,但是如果是下面的代码呢?
这个其实也不难理解,因为此时是向0取整,所以-10/3 = -3,余数自然就是-1了 ,那么linux平台中也是-1吗?换上linux平台
此时我们gcc编译test.c,运行a.out
我们发现也是一样的,我们在pythoy上看一下:
这里的结果为什么是2呢?
结论:很显然,上面关于取模的定义,并不能满足语言上的取模运算
因为在C中,现在-10%3出现了负数,根据定义:满足 a = qd + r 且0 ≤ r < d,C语言中的余数,是不满足定义的, 因为,r<0了。
故,大家对取模有了一个修订版的定义:
如果a和d是两个自然数,d非零,可以证明存在两个唯一的整数 q 和 r,满足 a = qd + r , q 为整数,且0 ≤ |r| < |d|。其中,q 被称为商,r 被称为余数。
有了这个新的定义,那么C中或者Python中的“取模”,就都能解释了。
是什么决定了这种现象
由上面的例子可以看出,具体余数r的大小,本质是取决于商q的。
而商,又取决谁呢?取决于除法计算的时候,取整规则。
上面在c语言中,是向0取整,而Python中是向负无穷取整,所以会存在这两种现象
取余和取模一样吗?
细心的同学,应该看到了,我上面的取模都是带着""的。说明这两个并不能严格等价(虽然大部分情况差不多)
取余或者取模,都应该要算出商,然后才能得出余数。
本质 1 取整: 取余:尽可能让商,进行向0取整。 取模:尽可能让商,向-∞方向取整
故:C中%,本质其实是取余。 Python中%,本质其实是取模。(后面不考虑python,减少难度)
理解链:
- 对任何一个大于0的数,对其进行0向取整和-∞取整,取整方向是一致的。故取模等价于取余
- 对任何一个小于0的数,对其进行0向取整和-∞取整,取整方向是相反的。故取模不等价于取余
- 同符号数据相除,得到的商,一定是正数(正数vs正整数),即大于0! 故,在对其商进行取整的时候,取模等价于取余。
本质 2 符号: 参与取余的两个数据,如果同符号,取模等价于取余
计算数据同符号
vs2019中
Python 3.7中
如果参与运算的数据,不同符号呢?
VS2019
Python 3.7中
我们不怎么严谨的数学推导,理解一下即可:
如果a和d是两个自然数,d非零,可以证明存在两个唯一的整数 q 和 r,满足 a = q*d + r , q 为整数,且0 ≤ |r| < |d|。其中,q 被称为商,r 被称为余数。
a = qd + r 变换成 r = a - qd 变换成 r = a + (-q*d) 对于:x = y + z,这样的表达式,x的符号 与 |y|、|z|中大的数据一致
而r = a + (-qd)中,|a| 和 |-qd|的绝对值谁大,取决于商q的取整方式。 c是向0取整的,也就是q本身的绝对值是减小的
如:-10/3=-3.333.33 向0取整 -3. a=-10 |10|, -qd=-(-3)3=9 |9|
10/-3=-3.333.33 向0取整 -3. a=10 |10|, -qd=-(-3)(-3)=-9 |9|
绝对值都变小了
python是向-∞取整的,也就是q本身的绝对值是增大的。
-10/3=-3.333.33 '//'向-∞取整 -4. a=-10 |10|, -qd=-(-4)3=12 |12|
10/-3=–3.333.33 '//'向-∞取整 -4. a=10 |10|, -qd=-(-4)(-3)=-12 |12|
绝对值都变大了
结论:如果参与取余的两个数据符号不同,在C语言中(或者其他采用向0取整的语言如:C++,Java),余数符号,与被除数 相同。
总结
- 浮点数(或者整数相除),是有很多的取整方式的。
- 如果a和d是两个自然数,d非零,可以证明存在两个唯一的整数 q 和 r,满足 a = q*d + r , q 为整数,且0 ≤ |r|< |d|。其中,q 被称为商,r 被称为余数。
- 在不同语言,同一个计算表达式,“取模”结果是不同的。我们可以称之为分别叫做正余数 和 负余数
- 具体余数r的大小,本质是取决于商q的。而商,又取决于除法计算的时候,取整规则。
- 取余vs取模: 取余尽可能让商,进行向0取整。取模尽可能让商,向-∞方向取整。
- 参与取余的两个数据,如果同符号,取模等价于取余
- 如果参与取余的两个数据符号不同,在C语言中(或者其他采用向0取整的语言如:C++,Java),余数符号,与被除数相同。(因为采用的向0取整)
我们最后在看一个代码:
#include <stdio.h>
#include <windows.h>
int main()
{
printf("%d\n", 2 / (-2)); //-1
printf("%d\n", 2 % (-2)); //2=(-1)*(-2)+r
system("pause");
return 0;
}
相信大家对这个代码的结果就一目了然了吧!
总结
本篇就是关于符号的全部内容,相信大家看完这篇文章有对C语言的符号又有了更深的认识。以后我们在面对C语言符号的时候都能够用不同的角度去看待。