0%

C语言的原码、反码和补码

经典著作《C和指针》说过,对于C语言而言,只有四种类型:

  1. 整形
  2. 浮点型
  3. 指针型
  4. 聚合型(比如数组、结构体等)

有人就说了,明显的有char类型吗,不过你可以看看char的范围,就能理解这个char是一个微型的正数,范围在-128~127或者0-255之间,为什么会是两个范围呢,仔细想想signedunsigned的区别。

建议阅读这个经典著作,对指针的理解会上升一个层次

其中整数在计算机内部基本使用补码来表示,这里就说下原码、反码和补码。

先抛出他们的定义:

  • 原码:原码就是符号位加上真值的绝对值, 即用第一位表示符号, 其余位表示值

  • 反码:正数的反码是其本身,负数的反码是在其原码的基础上, 符号位不变,其余各个位取反.

  • 补码:正数的补码就是其本身,负数的补码是在其原码的基础上, 符号位不变, 其余各位取反, 最后+1(也可以认为是在反码的基础上+1)

举个例子说明一下:

类别 正数 负数 备注(十进制)
原码 0b00011011 0b10011011 27/-27
反码 0b00011011 0b11100100 27/-100
补码 0b00011011 0b11100101 27/-101
对于计算机而言,能理解的只有二进制,也就是电路里面的高低或者叫开关。

C语言的符号常量

​ 前面已经看完了变量,正常情况下就要来聊聊常量了,从字面意义上看,变量与常量的概念比较容易理解:

  • 变量:就是能改变的量
  • 常量:就是不能改变的量

对于变量而言用的最多的也是见得做多的估计就是int i;这个语句的,而常量在什么时候用呢?

比如银行的利率,蔬菜的价格,电路的增益,线缆的损耗等等,可能会更改的值,更改不是原因,主要是在使用的时候一定要是多处使用到了,这样才能发挥它的作用。

先说一下符号变量的一般用法,如下所示:

1
#define 变量名 变量值

来个不用代码片段的,比如白菜的价格是2.5元一斤,张三、李四、王五,每个人来个三四五斤,需要多少钱,代码如下:

1
2
3
float zhangsan_price = 2.5 * 3;
float lisi_prince = 2.5 * 4;
float wangwu_prince = 2.5 * 5;

不错,代码看着简介明了,忽然章丘遇到天灾了,白菜统一涨价到5.5元一斤,OK,代码修改如下所示:

1
2
3
float zhangsan_price = 5.5 * 3;
float lisi_prince = 5.5 * 4;
float wangwu_prince = 5.5 * 5;

真是中国好邻居呀,干嘛都一起,这样也好操作,但是如果要统计一个村300多户都买几斤大白菜,那么需要修改的就是300多处,那样岂不是超级崩溃,OK,这时就需要符号常量了,我们略微修改,增加符号变量:

1
2
3
4
5
#define PRICE 2.5

float zhangsan_price = PRICE * 3;
float lisi_prince = PRICE * 4;
float wangwu_prince = PRICE * 5;

涨价了怎么办呢,只需要需改第一句,把2.5更改为5.5即可,简单快捷粗暴:

1
2
3
4
5
#define PRICE 5.5

float zhangsan_price = PRICE * 3;
float lisi_prince = PRICE * 4;
float wangwu_prince = PRICE * 5;

300多户,丝毫不惧,来个百八十万照旧一次修改,全部收益。

恩,符号变量的效果就是如此了,你是否已经了然于胸。

来个小例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*beginner/define/define1.c*/
#include <stdio.h>

#define BONUS 0.1

int main()
{
int salary;

salary = 30;

printf("The salary this year is %.2fk\n", salary * (1 + BONUS));

return 0;
}

每年浮动10%应该是常态,希望各位老板仁慈,对待优秀员工,把整个符号常量设大一点,哈哈,不用太多,0.5就可以了,😁。

相应地Makefile如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
#beginner/define/Makefile

ALL : define1

define1 : define1.c
gcc -o define1 define1.c


.PHONY : clean

clean:
rm -f define1

输入make,然后执行各个程序输出如下:

1
2
$  ./define1
The salary this year is 33.00k

C语言键盘输入采集scanf函数

前面说过C语言的额printf函数,那么这一次说说scanf函数。

这两个函数像两兄弟,比如在C++里面经常遇到setget,就差不到就是scanfprintf的意思,printf用来通过终端输出一些内容,scanf用来通过终端输入一些内容。

先看看这个函数的原型:

1
2
3
# include <stdio.h>

int scanf(const char *format, ...);

哇哦,这么多没有见过的,貌似不懂什么意思,没问题,一步一步来理解。

先来个好理解的,这个函数的意思就是:

1
scanf("输入控制符", 输入参数);

就是将键盘输入的参数转换为输入控制符规定格式的数据,然后保存到输入参数地址所表示的变量里面。

举个简单的例子:

1
2
int i;
scanf("%d", &i);

这个小段的意思就是输入一个数字,把这个数字作为传入参数赋值给变量i。

这个有个问题需要特别注意,就是&i的使用,很多人刚开始都会只输入i,实际需要传入的是一个地址,特别留意。

来个简单的小例子:

1
2
3
4
5
6
7
8
9
10
11
/*beginner/scanf/scanf1.c*/

#include <stdio.h>

int main(void)
{
int i;
i = 1314;
printf("i = %d\n", i);
return 0;
}

这个我们最开始编写的程序,如果需要打印一个我们需要的,就要每次修改源码,然后再编译运行,比如刚开始我们要一生一世1314,后来一想不对呀,要生生世世3344方显诚心吗,改过来,如此这般。

但是如果碰到一个变化不断的数字怎么办,比如让你说如N个学生的成绩,用来判断是否达到老师的期望,这个时候再改源码就不合适了,怎么办呢,就需要使用scanf函数了,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
/*beginner/scanf/scanf2.c*/

#include <stdio.h>

int main(void)
{
int i;
scanf("%d", &i);
printf("i = %d\n", i);
return 0;
}

不错,已经每次可以通过终端输入打印出来不同的值了。

但是最后有个提示对不对,好的,大部分人会这么写,但是就会出现各种各样的问题:

1
2
3
4
5
6
7
8
9
10
11
12
/*beginner/scanf/scanf3.c*/

#include <stdio.h>

int main(void)
{
int i;
scanf("please input i %d", &i);
printf("i = %d\n", i);
return 0;
}

正确的写法是怎么样的呢,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
/*beginner/scanf/scanf4.c*/

#include <stdio.h>

int main(void)
{
int i;
printf("please input i :");
scanf("%d", &i);
printf("i = %d\n", i);
return 0;
}

看到区别了吗,区别就是把要显示的东西还是使用printf来,只有最简单的输入用scanf来,其实用scanf也是可以的,如果按照scanf3的代码来,你需要在终端中输入的语句是:please input i 1314,这样才能满足你的需求,但显然这不符合你的初衷,对吧。

所以这也说明了另一个问题,那就是scanf的格式与输入需要保持严格一致。

编译运行

直接输入make就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#beginner/scanf/Makefile
ALL : scanf1 scanf2

scanf1: scanf1.c
gcc -o scanf1 scanf1.c

scanf2: scanf2.c
gcc -o scanf2 scanf2.c

scanf3: scanf3.c
gcc -o scanf3 scanf3.c

scanf4: scanf4.c
gcc -o scanf4 scanf4.c

.PHONY : clean

clean:
rm -f scanf1 scanf2 scanf3 scanf4

运行自行琢磨。

C语言的switch分支选择语句

switch本身作为分支选择语句主要是为了多个分支选择,其实它的语句用前面提到的if..else if..else if...else也可以解决,不过总归会比较冗长,相比较而言,switch语句看起来短小精悍,容易理解。

​ 最容易理解用来使用switch语句的为计算学生分数,比如输入A就证明分数在85~100分,输入B就证明分数在70-85,输入C勉强通过为60-70分,输入D,不好意思,还不到60分了。

swtich分支选择语句

一般形式如下:

1
2
3
4
5
6
7
8
9
switch(表达式)
{
case 表达式1:语句1
case 表达式2:语句2
...
case 表达式n-1:语句n-1
case 表达式n:语句n;
default:语句n+1
}

这个语句的具体含义为:

  1. 首先判断表达式;
  2. 如果表达式为表达式1,就执行语句1;
  3. 如果表达式为表达式2,就执行语句2;
  4. 以此类推
  5. default语句为默认不满足任何语句的时候执行的语句

这里有一个特别需要注意的,就是每一个语句最后都要加上break,切记,不然执行的结果并不一定是你认为的那样,因为在每个表达式,它没有跳出来。

简单举几个例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*beginner/switch/switch1.c*/
#include <stdio.h>

int main()
{
int score;
score = 'A';

switch (score)
{
case 'A':
printf("Wow, Awesome!!\n");
case 'B':
printf("Yeah, Great!!\n");
case 'C':
printf("Ok, passed!!\n");
case 'D':
printf("Sorry, you lose it!!\n");
}

return 0;
}

这个例子有一个最大的不足在于如果不满足上面说的四种情况,需要一个default语句来处理异常情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*beginner/switch/switch2.c*/
#include <stdio.h>

int main()
{
int score;
score = 'A';

switch (score)
{
case 'A':
printf("Wow, Awesome!!\n");
case 'B':
printf("Yeah, Great!!\n");
case 'C':
printf("Ok, passed!!\n");
case 'D':
printf("Sorry, you lose it!!\n");
default:
printf("BAD input\n");
}

return 0;
}

这个例子的问题就在于每一个语句最后没有跟break语句,导致每个执行都会有输出。

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
/*beginner/switch/switch3.c*/
#include <stdio.h>

int main()
{
int score;
score = 'A';

switch (score)
{
case 'A':
printf("Wow, Awesome!!\n");
break;
case 'B':
printf("Yeah, Great!!\n");
break;
case 'C':
printf("Ok, passed!!\n");
break;
case 'D':
printf("Sorry, you lose it!!\n");
break;
default:
printf("BAD input\n");
break;
}

return 0;
}

完美的switch就应该是这个样子了,😁。

相应地Makefile如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#beginner/switch/Makefile

ALL : switch1 switch2 switch3

switch1 : switch1.c
gcc -o switch1 switch1.c

switch2 : switch2.c
gcc -o switch2 switch2.c

switch3 : switch3.c
gcc -o switch3 switch3.c

.PHONY : clean

clean:
rm -f switch1 switch2 switch3

输入make,然后执行各个程序输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ ./switch1
Wow, Awesome!!
Yeah, Great!!
Ok, passed!!
Sorry, you lose it!!

$ ./switch2
Wow, Awesome!!
Yeah, Great!!
Ok, passed!!
Sorry, you lose it!!
BAD input

$ ./switch3
Wow, Awesome!!

如何选择C语言的循环语句

对于C语言的三种语句而言,其实彼此基本可以实现功能,也就是说用for能解决的,用while也能解决,反之也是。

再看一下,两种循环结构:

  1. for循环
  2. while、do-while循环

那么是如何来进行选择呢,什么时候用哪种循环呢,这就要看具体情况,叫做it depends,^_^。

一般而言,_选用的原则_如下所示:

  1. 如果循环次数在执行循环体之前就已确定,一般用for语句。如果循环次数是由循环体的执行情况确定的,一般用 while语句或者do- while语句;
  2. 当循环体至少执行一次时,用do-while语句,反之,如果循环体可能一次也不执行,则选用while语句
    C++/C循环语句中,for语句使用频率最高,while语句其次,do语句很少用。

C语言的while循环语句

​ C语言本身有3种程序结构,分别是:

  1. 顺序结构:语句一句一句执行,从头执行到结尾;
  2. 选择结构:可以理解为是分支结构,根据判断结构来选择向哪个方向进行,类似前面说的if、else语句;
  3. 循环结构:循环结构是这3个里面最复杂的,因为它自身有一个循环体,结构本身也是根据判断的结构,来决定循环体到底执行多少次,只要条件满足,循环体可以执行到天荒地老,此恨绵绵。主要的有上一次的for循环,还有今天说的while循环

while循环有两个,分别为while循环和do-while循环

while循环

一般形式如下:

1
2
3
4
while(表达式)
{
语句;
}

其中执行过程如下:

  1. 计算while后面括号里表达式的值,若其结果非0,则转入2,否则转3
  2. 执行循环体,转到1
  3. 退出循环,执行循环体下面的语句

由于是先执行判断后执行循环体,所以循环体可能一次都不执行。

循环体可以为空语句;,不过这个基本没有什么卵用。

do-while循环

while循环类似,还有一个do-while循环,一般形式如下:

1
2
3
4
5
do
{
语句;
}
while(表达式);

执行过程如下所示:

  1. 执行循环体,转2
  2. 计算while后面括号里表达式的值,若其结果非0,则转入1,否则转3
  3. 退出循环,执行循环体下面的语句。

注意:do……while语句最后的分号(;)不可少,否则提示出错。

这里可以看到,两种循环的区别是do-while至少执行一次。

简单举几个例子如下:

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
/*beginner/while/while.c*/
#include <stdio.h>

int main()
{
int i = 0;
int sum = 0;

while (i < 101)
{
sum += i;
i++;
}

printf("1 + ... + 100 = %d\n", sum);

i = 0;
sum = 0;
do
{
sum += i;
i++;
} while (i < 101);

printf("1 + ... + 100 = %d\n", sum);

return 0;
}

相应地Makefile如下所示:

1
2
3
4
5
6
7
8
9
#beginner/for/Makefile

ALL : while

while : while.c
gcc -o while while.c

clean:
rm -f while

输入make,然后./while输出如下:

1
2
3
$./while
1 + ... + 100 = 5050
1 + ... + 100 = 5050

描述
r{c} 使用字符{c}来替换单个字符(当前光标所在的位置上)
R 替换字符直到你按下注意:当移动到一行的最后时,该动作如同A一样,而不是回绕替换下一行的字符。

示例一:整个文件替换

:%s/old-text/new-text/g 这条命令将把old-text替换为new-text。

示例二:单行替换并提示

:s/old-text/new-text/gi 这条命令将把当前行的old-text替换为new-text并在替换前提示确认;

示例三:指定行内替换

:N,Ms/old-text/new-text/g 这条指令将把第N行到第M行的old-text替换为new-text。

示例四:指定可视块内替换

:’<,’>s/old-text/new-text/g 这条指令将选定可视区域内的old-text替换为new-text。

示例五:替换从当前行开始的N行文本

:s/old-text/new-text/g N 这条指令将替换从当前行开始的N行之内的old-text为new-text。

示例六:替换全匹配单词

对于前面的技巧,可能会在his替换为her的时候,This也替换为Ther,这就不是我们的本意了,此时我们可以使用:

:s/<old-text>/new-text/ 来替换整个单词,而不会截取其中的一部分。

示例七:使用正则表达式同时替换两个单词

:s/(old-text1|old-text2)/new-text/g 这条指令将会同时替换old-text1和old-text2为new-text。

示例八:交互式的查找替换

:s/old-text/new-text/gc 这条指令将会在替换每一个单词的时候提示时候替换,这对于只是希望替换其中一部分的还是蛮有用处的。

C语言的for循环语句

​ C语言的for循环语句用的那是相当的多,等等,这话我好像说了好几次了,哈哈。

​ 在小时候爸爸妈妈为我,从1加到100,最后值是多少呢,然后从早到晚就算起来了;再后来知道的神通高斯;再后来知道了C语言的for语句,😁。

​ for循环的结构比较简单,如下所示:

1
2
3
4
for (表达式1;表达式2;表达式3)
{
//do something
}

它的执行过程如下所示:

  1. 先求解表达式1;
  2. 求解表达式2,若其值为真(非0),则执行循环体,否则结束循环;
  3. 执行完循环体,再求解表达式3。
  4. 重复执行步骤 2) 和 3),直到循环结束。

按照上面的例子,简单举几个例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*beginner/for/for1.c*/
#include <stdio.h>

int main()
{
int i;
int sum = 0;

for (i = 0; i < 101; i++)
sum += i;
printf("1 + ... + 100 = %d\n", sum);

return 0;
}

相应地Makefile如下所示:

1
2
3
4
5
6
7
8
9
10
#beginner/for/Makefile

ALL : for1

for1 : for1.c
gcc -o for1 for1.c

clean:
rm -f for1

输入make,然后./operator4输出为:

1
2
$ ./for1
1 + ... + 100 = 5050

C语言的if语句

​ C语言里面有一个问题,就是虽然对于逻辑运算符而言,存在真假,不过C语言里面是没有这个概念的,不想Python里面有True和False,对于C而言,非假即为真,也就是如果值不是0,那么就认为是真的。

此处为何非假即为真,而不是非真即为假呢,因为只有0为假,其他均为真

那么什么时候会用到if语句呢,答案是很多时候,但凡开始结构化语句判断就会涉及到,比如:

  1. 判断是正数 如示例if1.c

  2. 判断成绩:不及格或及格 如示例if2.c

  3. 判断年龄段:婴儿、儿童、青少年、成人等, 如示例if3.c

这三种情况从简单到复杂覆盖了if语句的情况。

  1. 最简单的就是判断是否真假:
1
2
3
4
if (true)
{
//do something
}

这里假定定义了true为真,比如#define true 1

  1. 第二种情况为判断真假
1
2
3
4
5
6
7
8
if (true)
{
// do something
}
else
{
// do something else
}
  1. 第三种情况为判断各种情况的真假
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if (a)
{
// a do something
}
else if (b)
{
// b do something
}
else if (c)
{
// c do something
}
else
{
// do something else
}

按照上面的例子,简单举几个例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
/*beginner/if/if1.c*/
#include <stdio.h>

int main()
{
int value = 5;

if (value > 0)
printf("Yes, positive number\n");

return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*beginner/if/if2.c*/
#include <stdio.h>

int main()
{
int score = 75;

if (score < 60)
printf("Not passed\n");
else
printf("Passed\n");

return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*beginner/if/if3.c*/
#include <stdio.h>

int main()
{
int age = 16;

if (age < 3)
printf("Baby\n");
else if (age < 5)
printf("preschooler\n");
else if (age < 10)
printf("schoolchild\n");
else if (age < 12)
printf("preteen\n");
else if (age < 18)
printf("teenager\n");
else
printf("adult\n");

return 0;
}

相应地Makefile如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#beginner/if/Makefile

ALL : if1 if2 if3

if1 : if1.c
gcc -o if1 if1.c

if2 : if2.c
gcc -o if2 if2.c

if3 : if3.c
gcc -o if3 if3.c

.PHONY : clean

clean:
rm -f if1 if2 if3

输入make,然后./operator4输出为:

1
2
3
4
5
6
$ ./if1
Yes, positive number
$ ./if2
Passed
$ ./if3
teenager

Vim书签命令的快速总结

  • ma – 创建一个书签a
  • `a –跳转到书签a的精确位置(行和列)
  • ‘a – 调整到书签a所在行的起始位置
  • :marks — 显示所有的书签;
  • :marks a – 显示名称为a书签的详细信息;
  • `. – 跳转到最后一次执行改变的精确位置(行和列)。
  • ‘. – 跳转到最后一次执行改变的行起始位置。

如果书签名称为大写字符,那么它就是一个全局书签。

如何显示所有书签

如果你创建几个书签,忘记了它们的名称,你可以很容易的获得书签列表,输入:marks,显示如下:

:marks

除了局部书签、全局书签以外,在Vim内部任何时候输入:marks,你可以获得下面几行。这些标识‘单引号,”双引号,[,],^和.点号由Vim创建和管理。你不需要直接控制它们。

:marks

你可以使用上面显示的缺省标识,如下:

缺省标识 描述
`” 到退出之前最后一次编辑的位置
`[ 到先前改变或者复制文本的第一个字符
`] 到先前改变或复制文本的最后一个字符
‘< 到先前选择可视化区域的第一行
‘> 到先前选择可视化区域的最后一行
‘. 到最后一次该标的位置
‘^ 到最后一次插入模式停止的光标所在位置