C 语言学习 1

这个是给 2018 级 C 语言辅导准备的文档,写来写去写了好多,索性在网站也发一份。有很多是我觉得老师不会讲,但一开始很难理解,不理解又听不懂的,有学 C 的可以看一看。

基础中的基础

计算机只能识别机器码,机器码对程序员不友好,肯·汤普森和丹尼斯·里奇为了能更容易编写 UNIX,设计和实现了 C 语言。

从代码到程序

C 语言本质上是纯文本(就是所谓的 txt 文本文件),使用扩展名 .c.h 主要是为了能够让人从名字上识别这是 C 源码或头文件,你可以使用任何能够编辑文本的程序编辑 C 语言。

编写 C 程序主要有下面几个步骤:

  1. 用任何你喜欢的文本 编辑器 编写代码(记事本也行,但很难用)。
  2. 调用 编译器 将程序员能看懂的代码翻译成机器码组成的程序。
  3. 运行程序,得到结果。

名词解释

  • 编辑器:用来编写代码,可以是任何能编写纯文本文件的程序(Word 这类程序生成的并不是纯文本)。
  • 编译器:用来翻译代码到机器码,属于比较复杂的程序,通用的有微软的 MSVC,GNU 的 GCC 或者 LLVM 配套的 clang。
  • 集成开发环境(IDE):一种集合了文本编辑和编译代码功能的程序,通常还会集合一些附加的功能比如调试等。其中编译代码的功能通常是调用上面提到的那几种编译器进行。

注意:IDE 并不是编写代码的必需品,只要能编写文本又能调用编译器就可以写程序,因此不要问诸如“要用 VS 写 C 语言,那 VS 是用什么写的?写 VS 的公司是神吗?”这种 弱智 问题。没有 C 或者 C++ 很可能就不会有 VS,但是没有 VS 依然可以写程序。

注意:不建议使用 VC++ 6.0 编写 C 程序,这个程序的年龄比在座各位都大,当时 C 语言还没有完整的标准,和我们现在的 C 相差很多。并且实际上这是个 C++ 的 IDE,而 C 和 C++ 并不完全一样,应该视为两种语言。

可以使用 VS 或者 Code::Blocks 编写 C 程序,苹果 macOS 用户可以使用 Xcode,这些程序对于最新的 C 标准有着非常好的支持。

注意:对于 VS 用户需要注意默认 VS 会要求你使用它私有的一些函数而不是标准的 C 函数,这个需要在代码开始添加一行 #define _CRT_SECURE_NO_WARNINGS 来关闭。对于 Code::Blocks 用户请下载集成了编译器的安装包(简而言之,体积最大的),否则你下载下来以后会发现自己并没有编译器,没法编译代码。

学习 C 语言的正确方法

多写,多练,光动脑不动手什么语言都学不会,可以从模仿别人的代码开始:

  1. 首先对照别人的代码,自己完整的输入一遍程序,这里不是要你复制粘贴,当你能正确的输入程序的时候,说明你已经记住了 C 的结构,不会漏掉分号或者括号等。
  2. 然后阅读代码,并推测这段程序会得到什么结果。然后运行代码
  3. 如果和你预测的结果不一样,研究为什么不一样。
  4. 修改代码中的某一部分,然后重新进行 2 3 4 步,直到你已经完全掌握了这个程序的功能。
  5. 接下来你可以尝试自己从头写一份代码解决问题了。

语法基础

对于计算机最基础的功能自然是做计算,所以 C 语言需要有 数字符号(必须是英文符号)两种组成成分。

数字

没什么好说的,你可以直接写十进制的数字,但是如果你想写十六进制的数字,只要以 0x0X 开头就行,比如 0x1F 就是 31,八进制则是以 0 开头,比如 076 就是 62。

运算符

最基础的运算符自然是加减乘除,即 +-*/。(除号这个方向叫斜杠,\ 这个叫反斜杠。)

求余数又叫做取模,符号是 %,比如 5 % 2 结果是 1。

还有位运算 按位与 &,按位或 |,按位异或 ^,左移 <<,右移 >>,它们处理的是内存中的二进制。

然后是赋值运算符 =,这个符号 将右边的值赋给左边,因此左边必须是变量,右边必须是有值的表达式(表达式的概念往后看),比如 2 = 1 + 1; 很显然不行,2 不是变量,这里也不是比较相等。

还有一些整合的运算符,比如 +=-=*=/=x += 1 就等价于 x = x + 1。取模和位运算也可以这样和赋值结合起来。

然后是一些用来比较关系的符号,比如 大于 >,小于 <,等于 ==(判断相等用两个等号),不等于 !=,大于等于 >=,小于等于 <=。还有逻辑运算符号 与 &&(同真则真),或 ||(同假则假),非 !(反转)。

在说明之前,需要先了解在 C 语言里什么是真,什么是假,简单来说,0 和所有等于 0 的值都是假,其他所有值都是真。对于关系运算,真返回 1,假返回 0。

需要注意比如数学上的 1 < x < 2,在 C 语言中是不成立的,因为按照 C 的处理顺序,会先处理 1 < x,这个式子只可能是 0 或者 1,最后就变成 1 < 2 或者 0 < 2,永远是真的了。正确写法是 x > 1 && x < 2,使用逻辑运算。

a != b 就等价于 !(a == b)

, 逗号运算符表示按顺序进行用逗号分隔的子表达式。

() 小括号就如同数学中的括号一般改变运算顺序。

[] 中括号用于取数组元素,比如取 数组 arr 的第五个元素(如果有)就是 arr[5]

& 用于获取一个变量在内存中的地址,而 * 用于通过地址获取该位置的值。

. 用于通过结构体变量名字访问结构体内容,比如对于 struct point 变量 pointpoint.position_x。而 * 用于通过结构体指针取结构体内容,比如有指向 struct point 变量 point 的指针 ptr,可以 ptr->position_x

++-- 分别是让变量自增 1 和自减 1。

sizeof 这个运算符会给出后面变量的类型或类型本身的字节数,比如 sizeof char 或者 sizeof(char) 值为 1。

?: 是唯一一个三元运算符,使用类似于 条件 ? 表达式1 : 表达式2,如果条件为真,运行表达式1,否则运行表达式2。

运算符有非常复杂的优先级顺序,在这里列出表格,建议经常查阅,如果你在写程序的时候遇到不确定的地方,给你想先算的式子加上小括号总没错

类别 运算符 结合顺序
后缀 () [] -> . ++ -- 从左到右
一元 + - ! ~ ++ -- (type) * & sizeof 从右到左
乘除 * / % 从左到右
加减 + - 从左到右
移位 << >> 从左到右
关系 < <= > >= 从左到右
相等 == != 从左到右
位与 & 从左到右
位异或 ^ 从左到右
位或 ` `
逻辑与 && 从左到右
逻辑或 `
条件 ?: 从右到左
赋值 `= += -= *= /= %=>>= <<= &= ^= =`
逗号 , 从左到右

注意:C 并没有用于计算类似于 xy 这种乘方的运算符,而是需要调用函数计算,这里没有 **^ 也不是用来算这个的。

标识符

除数字和符号之外,我们还需要更多的词语来给 C 中的东西起名字,这些词语被称为标识符,合法(说人话就是有效)的标识符需要满足以下两条要求:

  • 组成成分是 字母 A-Za-z(区分大小写),数字 0-9,下划线 _(下划线不是连字符 -,因为这个是加减乘除的减)。
  • 第一个字符必须是字母或者下划线,不能是数字(如果以数字开头,判断这个词到底是数字还是标识符就会变得很麻烦,所以不支持)。

比如 ifareYouOKAreyouokDota2_357 甚至是 _(单个下划线)都是合法且 不同 的标识符。

注意:尽量不要在程序里使用中文,如果你实在不知道某个词怎么翻译成英语,用拼音也可以勉强接受。千万不要在程序里使用中文标点符号,它们和英文的标点符号是不同的字符,编译器不认识它们。如果你经常分不清中文和英文标点符号,考虑更换你编辑器的字体。

在大部分情况下,代码中连续的空白符(制表符 tab,换行符 newline 和空格 space)会被当作一个空白处理,因此写几个空格都是无关紧要的。

关键字

在合法的标识符的基础上,C 语言规定如下的标识符是它自己 本身 用到的词,这些词 不能被用户或者其他的库用作名字。最开始的 C 语言只有如下 32 个关键字:

1
2
3
4
5
6
7
8
char        short       int        unsigned
long        float       double     struct
union       void        enum       signed
const       volatile    typedef    auto
register    static      extern     break
case        continue    default    do
else        for         goto       if
return      switch      while      sizeof

C99 标准(就是 1999 年出来的标准)添加了这几个:

1
_Bool        _Complex        _Imaginary        inline        restrict

C11 标准(就是 2011 年出来的标准)添加了这几个:

1
2
_Alignas        _Alignof        _Atomic        _Generic        _Noreturn
_Static_assert  _Thread_local

除去这些,其他都是用户可用的标识符,你可以给你的变量或者函数起任意满足标识符要求并且不是关键字的名字。

注意:考试的时候看清楚题干要求,如果问的是 标识符,那么关键字也是正确的,如果问的是 用户标识符、函数名或者变量名,那就不能是关键字。

关键字说明

这次只说明一些基础的关键字,对于存储类别关键字暂时不做说明。

数据类型关键字

除了 void 这个特别的词用来表示“没有类型”,其它的类型关键字都有具体的指代。这些关键字用来标记内存中存储的数据类型是什么,基础的类型有下面几个:

  • char:长度为 1 字节,通常用来存储字符。
  • short:长度为 2 字节,用来存储短整数。
  • int:长度不确定,考试时候通常认为是 4 字节,通常用来存储整数。
  • long:长度不确定,通常用来存储长整数(大部分现代机器上,intlong 一样长)。
  • long long:长度为 8 字节,是 C 里最长的整数类型。
  • float:长度为 4 字节,存储单精度浮点数,通常精确度较低。
  • double:长度为 8 字节,存储双精度浮点数,精确度较高,开销略大。

还有一些修饰这些类型用的关键字:

  • signed:这个数据可以表示带符号的数字,通常第一位用作符号位,总的数据范围一半正数一半负数(大部分数据类型默认就是带符号的)。
  • unsigned:这个数据表示的是不带符号位的数字,即只有 0 和正数,没有负数。例如 unsigned int

声明变量参照下面的 结构 部分。

有关数据范围,对于整数,先把长度转成 bit 数,然后有多少 bit 范围就是 2 的多少次方,有符号数正负分别一半一半,负数比正数多一个数,无符号数则就是 0 到这个数。

还有一个定义结构类型的关键字 struct,这个关键字用来定义结构体,结构体可以把一些数据打包在一起,构成一个独立的类型,比如

1
2
3
4
struct point {
        int position_x;
        int position_y;
};

定义了一个结构体,以后就可以使用 struct point 作为一个新的数据类型。访问结构体使用 .->

还有一个类型别名关键字 typedef,它用来给一个现有的数据定义一个别名,例如 typedef float scalar;,然后就可以用 scalar 作为一个类型,这个类型和 float 是一样的。

有关数据类型还要注意一点,相同的数据类型进行运算,得到的类型还是原来的类型,如果是不同的数据,会向表示范围更大的数据类型转变。比如整数和短整数进行计算得到整数,整数和浮点数运算得到浮点数,双精度浮点数和单精度浮点数运算得到双精度浮点数。

在将一个值赋给另一个值的时候类型会被转换成被赋值的数据类型,比如 int a = 3.0; 实际上 a 的值为 3。但上述的运算规则发生在赋值之前,因此就算你写 float b = 5 / 2;,b 的值也是 2.0f 而不是 2.5,因为首先 5 / 2 是整数除整数,得到的就是整数 2,2 转换成浮点数也是 2.0f,因此正确的写法是把 5 或者 2 中的一个改成小数比如 float b = 5.0 / 2;,此时是浮点数与整数运算,首先将整数 2 变成浮点数 2.0,然后得到 2.5。

流程控制关键字

条件语句

if else 的格式如下,如果条件为真,执行语句1,否则执行语句2,也可以不写 else 和语句2。

1
2
3
4
if (条件)
        语句1;
else
        语句2;

嵌套的 else 会和最近的 if 匹配,除非使用大括号显式限制范围。

1
2
3
4
5
if (条件)
        if (条件)
                语句1;
else
        语句2;

上面这个 else 会和第二个 if 匹配,而不是第一个。

1
2
3
4
5
if (条件) {
        if (条件)
                语句1;
} else
        语句2;

这个就和第一个匹配了。

switch case 也可以用作条件判断,switch 后面必须接一个条件变量而不是条件语句(这个变量的值必须可以直接用 == 判断相等,很抱歉字符串不可以哈哈哈)。形式如下。

1
2
3
4
5
6
7
8
9
10
11
switch (条件变量) {
case1:
        语句1;
case2:
case3:
        语句2;
        break;
default:
        语句3;
        break;
}

根据条件变量值的不同,会跳到不同的 case 向下执行,重点是向下,也就是说如果匹配了值1,运行语句1后会向下运行语句2,只有在遇到 break 的时候才会跳出 switch 语句,同样不管匹配的是值2还是值3,都会运行语句2,如果没有匹配,会运行 default 标签下面的内容,当然也可以不写 default

循环语句

有三种循环语句,首先是 while 语句,语法如下。

1
2
while (条件)
        语句;

该循环会在条件为真时执行语句,条件为假则退出循环,每次循环都会先判断一次条件再决定是否循环。

然后是 for 语句,语法如下。

1
2
for (循环开始前做一次; 条件; 每次循环结束都做)
        语句;

该语句在循环开始前执行第一个分号前的内容 一次,然后判断条件决定是否进入循环,在每次循环中的语句结束后,都会做第二个分号之后的内容。如果不想做某一个部分,直接空着就可以,但要保留分号。比如下面的 for 循环就和 while 循环等价。

1
2
for (; 条件;)
        语句;

最后是 do while 循环,用于需要先做一次循环内容再做判断的情况。

1
2
3
do
        语句;
while (条件);
1
2
3
4
5
do {
        语句1;
        语句2;
        语句3;
} while (条件);

跳转语句

有四种跳转。

goto 是无条件的基于标签的跳转。标签就是在某一句前写一个标识符然后加冒号。

1
2
标签: 语句;
goto 标签;

这段代码会造成一个死循环,用于在这两句之间跳来跳去。不建议使用 goto 语句,因为在程序里跳来跳去很容易让人逻辑混乱,从而无法调试

return 语句通常用于在函数中返回一个值,比如 return 0; 返回整数 0,当函数返回之后,该函数就结束了,即使后面还有语句也会被忽略,如果不想返回值,直接 return;

剩余两种都是主要用于循环中的语句。

break 用于打断一个循环,从循环中跳出来,比如下面的循环。

1
2
for (; 1; 语句)
        break;

如果没有 break 这个语句是一个死循环并且每次都运行语句,但是有 break,第一次进入循环之后就跳了出来,不会执行语句。

continue 用于跳过本次循环中的剩余部分,直接进行下次循环。

1
2
3
4
5
for (开始; 1; 语句1) {
        语句2;
        continue;
        语句3;
}

这个程序在执行完开始之后,每次循环都执行语句1,然后 continue 直接结束这次循环进入下一次,因此语句3就不会被执行,但对于 for 循环语句1会被执行。

通常情况下,breakcontinue 都和 if 搭配使用,它们可以解决大部分情况而不需要 goto

结构

C 程序主要包含以下部分:

  • 预处理器指令
  • 函数
  • 变量
  • 语句和表达式
  • 注释

注释

注释用于表示“这部份是给我自己看的,编译器并不需要这部份”,通常我们用它写一些提示自己的语句,编译器会忽略它们。写注释是个好习惯。

第一种注释使用 /**/,凡是在这两个符号之间的都是注释,这种注释可以跨越多行。

1
2
3
4
5
6
/*
我是注释
我是注释
我是注释
*/
/*我是注释*/

第二种注释使用 //,它的范围是从 // 到这一行的结尾(换行符)。

1
2
// 我是注释
我不是注释

调试程序的时候也可以临时注释掉一部分问题代码,相比删除,这样可以随时恢复它们。

语句和表达式

表达式通常做一个动作并得到一个值(注意没有值也算是一种特殊的值),比如 1 + 1 是一个表达式,这个表达式的值为 2,表达式可以互相组合,比如 (1 + 1) * 2,这个表达式的结果为 4。

需要注意一些特别的运算符表达式的值,比如我们令 int i = 1;。不管是 i++ 还是 ++i,执行结束后 i 都会变成 2,但是从表达式的值的角度来说,i++ 这个式子的值是 1,而 ++i 值是 2,-- 同理。

也就是说, i++ == 1真的++i == 1假的++ 在前就先加后值,++ 在后就先值后加。

不要自作聪明,对于 C 语言标准,并没有规定诸如 y=i+++++i(其实就是 y = i++ + ++i)的运算过程。编译器可以认为这两个式子里的 i 都是 1,也可以认为一个是 1 一个是 2,还可以认为都是 2,那 y 就可能是 2 3 4 甚至是奇奇怪怪的不可预测的值。如果考试出现这种问题,建议直接告诉老师题有问题。

C 语言规定语句以分号 ; 结束而不是以换行符结束,因此你可以在一行里写多条语句,也可以在多行里写一条语句,编译器会阅读其中的分号。一个单独的分号也是一个语句——它什么也不做,我们叫它空语句。

比如

1
1 + 1; 2 + 2;

是两条语句。但是

1
2
3
(
        1 + 1
) * 2;

是一条语句。

对于 C 关键字中的流程控制关键字,通常它们的作用 只能控制它们下面的一条语句,大括号 {} 可以在文法上将几个语句结合成一个语句(说人话就是如果你想在这些关键字后面接两条或以上的语句,就要加大括号),例如

1
2
3
4
if (true) {
        do_one();
        do_another();
}

当然,一个空的大括号 {} 也是空语句。

函数

函数是 C 语言中最主要的组成部分,C 语言程序主要就是由各个函数组成。函数类似于数学中的函数,它们接受一些参数,然后做一些操作,最后返回 一个 值(C 限定函数只能返回单个结果或者不返回结果)。

一个函数的结构通常是像下面这样

1
2
3
4
返回值的类型 函数名(参数类型 参数变量名, 参数类型 参数变量名)
{
        函数的内容
}

返回值的类型限制了函数返回的数据的类型(说人话就是你前面写啥类型后面就得返回啥),如果没有返回值,这里使用 void

函数名则是一个用户标识符,然后在小括号 () 里放接受的参数列表,按照 参数类型 参数变量名 的格式,中间用逗号连接,如果不接受参数,这里使用 void

上面这一部分通常叫做 函数头

函数的内容放在大括号里面,这一部分也通常叫 函数体,在这里你可以写 C 语句。

如果你指定了函数类型,函数内容里必须用 return 表达式返回一个对应类型的值,否则你会得到一个 Error。

比如有一个限制为处理整数的数学函数 f(x) = x * 2,写成 C 的函数就是下面这样。

1
2
3
4
int f(int x)
{
        return x * 2;
}

变量

变量对应着内存里的一块空间,里面存储着一些数据,必须 先声明一个变量才能使用,声明指定了变量的类型和名字。

声明变量的时候使用类似这样的格式。

1
变量类型 变量名 = 初始值, 变量名 = 初始值;

首先是变量的类型,然后跟着变量名组成的列表,列表之间用逗号分割,也可以同时提供初始值,只需要使用 = 赋值,可以省略 = 和初始值,这时这个变量的值是 不确定的

例如 int a, b, c = 2;

题外话:建议给变量起个容易理解它是什么的名字,而不是一堆乱七八糟的字母,比如 my_age 就比 x3 更容易理解。在变量名里写上变量类型倒不是个好主意,因为稍微高级点的编辑器都能推导类型,这样写纯粹是浪费空间。

变量是有作用域的,你不能在作用域外面调用作用域里面的变量,但反过来是可以的,也就是说作用域满足嵌套关系。

同一个作用域里变量不可以重复声明,否则会得到一个 Error。但如果内层作用域声明了一个和外层重名的变量,内层的会暂时覆盖外层的,也就是说你暂时没办法访问外层的那个。

通常来说,函数是一层作用域,然后则是对于每个判断或循环语句,它们的条件和它们控制的代码块(就是大括号里面)有一个单独的作用域。也就是说你在循环体里面声明的变量在外面是不能访问的。比如这段代码。

1
2
3
4
5
6
7
8
9
// 你应该能看懂这个函数没有返回值也没有参数。
void func(void)
{
        int x = 1; // 函数作用域。
        int y = 2;
        for (int x = 2; x < 10; ++x)
                printf("%d %d\n", x, y);        // 这里会输出 2 2 到 9 2 共八行数。
        printf("%d\n", x);        // 这里的 x 还是 1!
}

如你所见,第一次我们声明的变量 x 和 y 属于函数作用域,随后 for 语句中声明的 x 就是循环代码块作用域了,这个 x 暂时掩盖了外面的 x,当离开循环之后,我们又访问到了函数作用域的 x。

我们还可以一次批量分配多个元素,这种方式被称为数组,数组的声明方式通常是下面这样:

1
数据类型 变量名[数组长度] = {元素, 元素, 元素};

= 和后面的部分是为了进行初始化,当然你也可以不进行初始化。

数组中的元素在内存中是连续的,当你想访问其中的某个元素,只需要使用 数组名[元素序号]这里的序号是从 0 开始的!也就是说长度为 5 的数组,元素序号分别是 0、1、2、3、4。

你可以写超出数组长度的序号,C 并不会阻止你这样做,但通常这样会触发一个错误,因为那个位置的内存并不一定让你访问。换句话说,C 语言相信你会限制访问的长度,所以它不进行限制。

如果你在声明的时候没有进行初始化,那你以后就不能再像初始化元素一样给整个数组直接用 = 赋值了,具体的原因需要等讲过指针之后再进行说明。这个时候你可以给 数组名[元素序号] 进行单个的赋值。比如把 arr 的三个元素都设置成 0。

1
2
3
4
int arr[3];
arr[0] = 0;
arr[1] = 0;
arr[2] = 0;

相比于在代码里书写多个变量如 int a1, a2, a3; 使用类似 int arr[3]; 让我们有了在循环中处理它们的办法,你不能在循环中处理变量的名字(这可不是简单地拼字),但你却可以循环处理数组的下标。比如把 arr 的三个元素都设置成 0 还可以这样做。

1
2
3
int arr[3];
for (int i = 0; i < 3; ++i)
        arr[i] = 0;

预处理器指令

严格来说,预处理器指令并不是 C 语言的一部分,它们在编译器最开始处理代码的时候进行工作,然后当编译器开始将代码翻译成机器码的时候,预处理器已经工作完毕了。

预处理指令以 # 开头,每一行为一个语句,大部分预处理指令都是与文本替换有关。

这里介绍几个简单的例子,复杂的后面会单独说明。

#include <文件名> 表示将这个文件的内容插入到这一行的位置,一般用来引用头文件,< > 括起来的是系统的头文件名,如果引用的是自己项目里的头文件,则需要用 #include "文件名"

#define 标识符1 标识符2 表示在文件里所有出现的标识符2都会被替换成标识符1,比如下面的代码。

1
2
3
#define PI 3.14
int c = 2 * PI * 5;
int s = PI * 5 * 5;

在编译器翻译代码到机器码的时候,它看到的代码其实是

1
2
int c = 2 * 3.14 * 5;
int s = 3.14 * 5 * 5;

示范

Hello world!

好了,在介绍完这些复杂的东西之后,终于可以进行一个简单的程序示范了,现在书写一个 hello world 就不会遇到有哪里说不明白的情况。

1
2
3
4
5
6
#include <stdio.h>
int main(void)
{
        printf("Hello world!\n");
        return 0;
}

首先第一行的 #include <stdio.h> 表示我们把 stdio.h 这个头文件里的内容全部插入进来,stdio 表示 standard input output 即标准输入输出(这里的输入输出是从程序的角度)。C 语言自身标准规定了许多有用的内置函数给我们使用,但我们首先要在代码里声明它们,这个头文件就包含了一些函数的声明,后续会继续说明头文件的作用与内容。

然后我们声明一个名称为 main,返回类型为 int,不接受参数的函数,这个函数是 C 语言规定的程序入口,也就是说操作系统运行你的程序,实际上是从调用 main() 开始的,这里需要注意的是在 C 语言标准中,main() 的返回值必须为 int,你可能见到过 void main() 或者干脆连 void 这种返回类型都不写的,这些都是错误写法,某些不标准的编译器可能支持,但实际上标准里没有它们,你在写代码的时候不能这么写。

这个函数的内容只有两句,第一句 printf("Hello world!\n"); 会在你的终端界面输出一句 Hello world! 并换行,在这个过程中我们调用了这个叫做 printf() 的函数,它是 C 标准库规定的格式化输出函数(f 可以理解成 format),但我们这次只是用它输出一条字符串,\n 是一个转义字符,我们用它表示“回车”这个无法在代码里打出来的符号,如果你去掉它,然后再输出点什么,你会发现这两次的输出会出现在同一行里面——C 不会乱做任何你没让它做的事情。有关 printf() 和转义字符的更多内容,下次会进行介绍。

对于字符串,你可以简单的理解为小说或剧本里面人物说的话,如果不用双引号,剧本里的文字就是叙述性的语句,并不会被观众得知,程序也是一样,不加双引号的均是程序代码,当你想跟用户交流,使用字符串吧。有关字符串的存储方式,也会在后续讲解。

最后,main() 函数返回整数 0,这个返回值会被它上层的程序捕获(可以简单理解为操作系统),0 在这里约定是程序运行成功的意思,一个复杂的程序可能有多种多样的失败原因,这个时候我们可以用无数的非零值代表各种错误。

还记得之前关于 return 的说明吗?如果我们把 main() 改写成下面这样。

1
2
3
4
5
6
int main(void)
{
        printf("Hello world!\n");
        return 0;
        printf("老师永远也不会看见这句话。");
}

你有可能会得到编译器的警告,但它只是提醒你最后这句 printf() 不会被执行,如果你的老师不看源码,他/她永远也不知道这句话。(另外这句话输出之后并不会换行。)

有关缩进

缩进这个词有点陌生,程序员用它指代代码的排版格式,尽管 C 给了你把所有程序写在同一行里的能力,但我估计看过这种代码的人都想把作者打一遍出气。

1
2
#include <stdio.h>
int main(void){printf("Hello world!\n");printf("Hello world!\n");printf("Hello world!\n");printf("Hello world!\n");printf("Hello world!\n");printf("Hello world!\n");printf("Hello world!\n");printf("Hello world!\n");printf("Hello world!\n");printf("Hello world!\n");printf("Hello world!\n");printf("Hello world!\n");printf("Hello world!\n");printf("Hello world!\n");printf("Hello world!\n");printf("Hello world!\n");printf("Hello world!\n");printf("Hello world!\n");printf("Hello world!\n");return 0;}

当你写了一个复杂一点的程序的时候,有必要修理一下程序格式,让它变的更容易理解,我们在前面说过一个分层次的东西叫作用域,刚好可以利用它。我们可以在每一个作用域的内部语句的行首增加一定数量的空白,来突出层次感。比如在这段代码里指出一个错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void func(void)
{
        for (int i = 0; i < 10; ++i) {
                for (int j = 0; j < 10; ++j) {
                        for (int k = 0; k < 10; ++k) {
                                if (0 = k % 2) {
                                        printf("老师会看见很多次这句话。");
                                }
                        }
                        for (int k = 0; k < 10; ++k) {
                                for (int l = 0; l < 10; ++l) {
                                        if (l % 2 == 0) {
                                                printf("老师会看见很多次这句话。");
                                        }
                                }
                        }
                }
        }
}

就比在这段代码里

1
void func(void){for(int i=0; i<10; ++i){for(int j=0; j<10; ++j){for(int k=0; k<10; ++k){if(0=k%2){printf("老师会看见很多次这句话。");}}for(int k=0; k<10; ++k){for(int l=0; l<10; ++l){if(l%2==0){printf("老师会看见很多次这句话。");}}}}}}

要容易的不止一点半点。

通常这个空白是一个 Tab 按键,或者八个空格,或者四个空格,只要你的一份代码里选择其中一个就可以了,不要搞得参差不齐就行。

通常还建议你只在一行里书写一个语句,因为你所看见的代码可能有误导性,比如

1
2
if (条件) do_one();
        do_another();

究竟哪个函数受到条件控制呢?现在也许你分得清,当你写了几百行代码后,可能你就感觉它开始辣眼睛了,建议你写成下面这样:

1
2
3
if (条件)
        do_one();
do_another();

还有一个建议是,虽然人们在左大括号放在上一行行尾还是单起一行的行首争论不清,但右大括号单独放一行总是没错的。左大括号的两种风格都是好的,你只需要坚持某一个风格就好了。

1
2
3
if (条件) {
        do_one(); }
        do_another();

就不如

1
2
3
4
if (条件) {
        do_one();
}
do_another();

二元运算符两侧建议加空格,比如 int a = 1 + 1 * 3; 就比 int a=1+1*3; 清晰。

这些都只是建议,但当你写了一定数目的代码,它们能帮助你少犯乱七八糟的错误,别人读你的代码也会更容易。

提问的艺术

有时候你自己实在搞不清楚一个问题了,需要问别人。且慢!别人可能很忙,时间宝贵,在一大片代码里找出错的位置就像大海捞针一样愚蠢,特别是许多时候,编译器已经可以告诉你代码哪里错了的情况下。

在 Google 上搜索“提问的艺术”能找到好多有用的东西,这里只说一条最重要的:当你提问时,请贴上你完整的代码、编译器编译过程中输出的警告和错误(通常在 IDE 下面的小窗口里)以及你代码运行时的输出截图(如果程序通过了编译但运行出错了)

最愚蠢的是只说一句没头没脑的“我代码错了!怎么办!”,你就是说一千遍,它还是错的。第二愚蠢的是说一句“我代码错了!我先干了什么什么后干了什么什么又怎么怎么样!”,你是要自己重新描述一遍?还是打算自己从嘴里重新发明个 C 语言?万一你犯的错误是漏了个分号或者括号,我不信你能用嘴描述一遍你代码里的所有分号的位置。

贴上完整代码保证别人帮你除错的时候不需要回头问你然后等待回复,贴上编译器输出可以直接看到它提示的可能错误的位置(而不是一句一句去读去想),输出截图也是同样的道理。

别人帮你是自愿花费他的时间,做到以上这一点至少能让别人帮你找错误更容易。

既然看了喵写的文章,不打算投喂一下再走吗?哼!