User:渚 花/C语言教程
--渚_花(讨论) 2023年10月5日 (四) 22:08 (CST)
#include <stdio.h> #include <stdlib.h> int main(){ printf("Hello world!\n"); getchar(); return 0; }
上面是C语言的hello world代码。如果你是初学者的话,作为学习第一门编程语言的仪式,先尝试一下执行以下上面的hello world代码吧。具体网上有大量教程。
初学者学习一门任何一门新语言的第一个程序,都是在命令行上打印hello world,这是一个传统。因此也有下面这个编程笑话
“ | 某程序员对书法十分感兴趣,退休后决定在这方面有所建树。于是花重金购买了上等的文房四宝。一日,饭后突生雅兴,一番磨墨拟纸,并点上了上好的檀香,颇有王羲之风范,又具颜真卿气势,定神片刻,泼墨挥毫,郑重地写下一行字:hello world | ” |
——https://zhuanlan.zhihu.com/p/603433946 |
推荐初学者使用VSCode或者Codeblocks电脑的这两个软件。另外,如果是使用VSCode的话。推荐使用Code Runner插件,这个插件可以省去大量的配置工作
- 为什么这个hello world代码和其他教程的不一样?这个下面第零章内容会详细讲
如果报错了怎么办?因为上面的代码太基础了,所以会有很多初学者遇到和你一样的错误。解决方法是,把错误信息复制下来,百度
- 学习计算机电脑的科学一定要学会使用百度,对于一些提问类的内容可以使用知乎。不要迷信一些“stackoverflow比百度好”等。简单的问题百度就可以解决
晕字怎么办 | ||
---|---|---|
|
绪言
本教程将尝试以用一些与其他教程不同的方式,比如将常量运算和变量运算分离等,解决大量新人难以理解的问题,比如很多新人到了指针之后就完全学不懂的问题
本教程的示例代码较少,几乎没有习题和作业
第零章 入门
为什么学习编程语言都要以hello world入门呢?这是因为每门编程语言的hello world程序,都“麻雀虽小五脏俱全”,里面携带着这门编程语言大量的语法信息
这一章开始会出现大量的专有名词。其中很多第一次出现的专有名词后面会详细解释,但有一些名词并不会给出解释,需要读者自己通过语境进行理解。给出大量专有名词一是为了给读者一种“C语言是一个好大的开放世界”的探索的感觉,二是为了方便读者遇到问题时进行表述,期望解决“我遇到了某个问题不知道怎么描述”的现象。这里很多内容只需要熟悉,而不需要完全弄懂
为什么是第零章?这是因为程序员计数都是从0开始计数的。比如C语言的数组,C++的vector
,Python的列表中最开始的元素的下标是0而不是1,尽管lua是个例外
- 学习计算机科学的时候很多时候是这样,只需先大概了解一些内容和原理,大概了解一些专有名词,待到需要或感兴趣时再细了解即可
- 计算机科学的知识太多,是学不过来的,且新知识产出的速度很可能会大于你学习的速度。但只需要学习的科学和技术可以用其帮助其他人,解决其他人的需求,就可以为社会做贡献了
- 至于怎么知道人们需求什么,可以通过观察下自己需求什么,从了解自己和自己的需求开始
- 但是需求也分能满足的需求和不能满足的需求两种,所以需要区分
- 如果想实际应用计算机科学的知识到实践,你会发现一些知识只需要了解大概原理,甚至无需了解,只需调用前人程序员设计好的接口即可
- 某种层面来说,程序员是一个自掘坟墓的职业。这个东西就像发现新大陆一样,如果地图上所有的内容都被发现完了,那么后来的探险者探险就没有拓展的意义了
- 如果你想发展计算机理论,那么如果你希望你的研究成果能落地被实际应用,那么你至少需要调查程序员需要发展什么样的理论,或者根据自己的需求来发展理论(因为你遇到的问题,别人很可能也遇到过)
- 但是需要注意的是,计算机科学的一个共识是不要重复造轮子。当然可以做小程序做着玩或者达到练习目的,但如果自己做出来的轮子没有比前人的轮子达到一定的优势,那么不要想着要取代前人的轮子
- 因为很多好用的轮子已经得到了大量使用,如果替换一个新的轮子取代旧轮子需要大量替换工作
- 但是需要注意的是,计算机科学的一个共识是不要重复造轮子。当然可以做小程序做着玩或者达到练习目的,但如果自己做出来的轮子没有比前人的轮子达到一定的优势,那么不要想着要取代前人的轮子
- 计算机科学的知识太多,是学不过来的,且新知识产出的速度很可能会大于你学习的速度。但只需要学习的科学和技术可以用其帮助其他人,解决其他人的需求,就可以为社会做贡献了
- 计算机科学相对于其他很多科学,历史并不长。但是计算机科学是人类智慧的结晶
- 前人的艰辛,你至少都要重来一遍。也许科学的发展就是这样,虽然高峰高不可攀,但每一代人都在用自己的生命为它的发展贡献或多或少的部分,然后后人踩着前人的尸体作为台阶,再后来的人把我们作为台阶
“ |
きみ2に |
” |
——Patricia,花开物语印象曲 |
你好世界
#include <stdio.h> // 每一行//后的内容是注释,注释不会对代码执行结果产生影响 #include <stdlib.h> // 上面一行引用了一个名字叫做`stdio.h'的头文件,这一行引用了一个名字叫做`stdlib.h'的头文件 int main(){ // 每一行两个斜线后面的内容表示【注释】,注释并不会被识别为代码内容 printf("Hello world!\n"); getchar(); // 在一些设备上如果不加上这一行代码,可能会出现类似于闪退的情况 return 0; }
我们一行一行开始讲。前两行是功能为引用头文件的编译预处理指令,其中#include
表示引用,<>
内是被引用的头文件名字。前两行分别引用了名字叫做stdio.h
和stdlib.h
的头文件
- 引用是什么意思:在预处理时,编译器会简单粗暴地将头文件内容复制粘贴,替换掉
#include
语句所在行- 什么是预处理:编译器在将C语言代码编译为可执行文件时,分为多个步骤。预处理是其中的一个步骤
- 虽然被计算机执行的是可执行文件,但我们还是经常说执行代码
- 至于这两个头文件具体在电脑的哪个位置,不同设备和环境中头文件的位置可能不同,感兴趣可以百度搜索
- 你可以在大多数代码编辑器中长按ctrl,然后用鼠标点击代码中的头文件名字,就可以查看该头文件的内容
- 引用标准库的头文件的时候,需要用
<>
把头文件名字框起来。如果引用程序员自己写的头文件,则使用""
- 很多初学者会把
stdio
当做是studio
。实际上,std
是standard
的缩写,i
表示input
,o
表示output
。该头文件的功能是给出标准输入输出的函数声明(这里涉及到链接)。stdlib
中的lib
是library
,这里并不会是图书馆,而是库的意思
然后是3-7行。这里实现了一个名字叫main()
的函数。int
的意思是整形(一种数据类型,简称类型),写在函数名字前面表示这个函数的返回值的类型是int
类型
int
是integer
的缩写,这个数据类型通常用来表示和存储整数- 通常不用整形这个词来称呼
int
类型,更常用的称呼是int
类型
main()
函数是一个特殊的函数,被称为入口函数。之所以main()
函数被叫做入口函数,是因为编译后的可执行文件会从调用main()
函数开始
- 一些教程会将第三行写成
int main(void)
,这个具体在函数声明时会讲到,不推荐这样写 - 有些教程会写成
int main(int argc, char **argv)
(第二个形式参数(或简称形参)char **argv
也可以写成char argv[]
,这两种写法都是正确的,这里涉及到命令行参数的问题,后面的内容会详细讲到 - 还有一种写法是
main()
,这种写法省略了前面的int
。早期的C语言是可以省略int
的,但现在不推荐这样写 - 一些教程会将左大括号
{
写到main()
函数的下面,这种写法是正确的。这种写法涉及到代码风格问题,下面会详细讲到
main()
函数后面由左大括号和右大括号框柱的内容叫代码块,代码块中有多条语句,每一条语句后面都要加一个分号。如果调用了main()
函数,代码块中的语句会从上到下从左到右逐条执行
可以看到,main( )
代码块内部有缩进,这个会在后面代码风格部分讲到
- 通常缩进是一个tab,或两个或4个空格
#include <stdio.h> #include <stdlib.h> int main(){ printf("Hello world!\n"); getchar(); return 0; }
现在简单介绍函数调用。第四行调用了一个叫做printf()
的函数,该函数可用于打印字符串
- 计算机科学的函数与数学的函数有很大差别,请注意
- 这里的打印并不是在打印机上打印,而是在光标处打印。每次打印一个字符时,光标位置右移,同时原来光标位置替换为被打印的字符,详情可以看下面的视频
- 一些教程,如C语言中文网,会用
puts()
实现打印字符串功能。它与printf()
函数有区别,但在此代码中可以互相替换 printf()
中的f
是format的缩写,意为按照指定格式打印
函数(function)调用的语法是function_name(argument1, argument2, ...)
,函数名字后面需要跟着一个括号,括号中按顺序填充函数参数(argument)。如果函数没有参数,可以将括号内部置空,但不允许省略括号
printf("Hello world!\n"); getchar();
上面两行分别调用了两个函数,第一个是printf()
。这里它只有一个参数,为字符串常量"Hello world\n"
。第二个函数getchar()
没有参数
- 一些教程会把
getchar()
换成system("pause")
。这是为了让程序阻塞在这里,以防止程序窗口一执行完printf()
语句就自动关闭,就像闪退一样,这样就会看不到打印效果。后面的代码示例会省略getchar()
getchar()
函数的本来用途是,从'标准输入流'stdin
中读取一个字符并返回。它会等待用户通过键盘键入字符,待用户输入回车后,再从缓冲区中读取字符system()
函数的本来用途是,调用系统命令或shell命令,其唯一的一个字符串常量参数是将要被调用的命令pause
系统命令会等待用户在键盘键入任意键- 关于任意键(any key):曾有一个公司推出的软件中其中一个部分有这样的提示语,
Press Any Key to Continue
这时软件用户只需要在键盘上键入任意键即可继续。然后很多用户打电话询问公司Any Key
在键盘的哪个位置。原来很多用户认为这里需要按下一个键盘上一个名字叫做Any Key
的键,而用户在键盘上找不到这个键。所以后来很多提示语都变成了Press Enter to Continue
,而实际上用户键入任何键都可以
- 关于任意键(any key):曾有一个公司推出的软件中其中一个部分有这样的提示语,
- shell这个词据我所知并没有合适的中文翻译,一些人会戏称地翻译为贝壳。终端是terminal的翻译,而不是shell的翻译。常见的shell有bash、powershell等
代码的倒数第二行return 0;
的意思是,main()
函数的返回值是0
,或者说,main()
函数返回0
main()
函数的返回值具有特殊的意义,它表示程序的返回值。通常如果一个程序的返回值为0
,表示程序正常运行,用其他值(比如-1
)表示程序异常终止- bash中,可以通过
echo (?$)
来查看最后一个运行的程序的返回值
- bash中,可以通过
关于注释:C语言的注释有两种,分别是//这种注释可以注释掉//后面的内容
和/* 这种注释可以跨行 */
两种。注释内容不会被识别为代码内容,也不会被执行,可用于便于未来的程序员包括未来的自己阅读代码
- 注释掉是什么意思:有时为了代码调试(debug),故意在代码某一行前面加上
//
,以将该行代码内容变为注释,这样被注释掉的代码不会被执行,以对比代码执行结果较未被注释掉前的区别,以达到调试的效果
#include <stdio.h> #include <stdlib.h> int main(){ // 我是一条注释 printf("1\n"); /* printf()函数 printf("2\n"); * 可以通过这种方式 printf("3\n"); * 打印数字 printf("4\n"); */ printf("5\n"); printf("6\n"); return 0; }
下面是上面代码的执行结果,请注意数字2 3 4不会被打印。至于为什么不会被打印,留作习题
- 提示:在代码高亮渲染下,注释的颜色是和代码的颜色不一样的
1 5 6
代码规范与风格
不同的代码规范与代码风格略有区别,但是有一点是确定的,如果要参与别人的代码项目,必须按照对方的代码风格贡献代码,除非对方的代码风格本就特别混乱。很多大厂会指定程序员的代码风格,以
这一节的所有示例代码的目的都是为了讲解代码风格,所以不要把注意点放在这些代码的实际功能和执行效果上。这里的部分示例代码是C++代码。下面部分内容可能涉及到没学到的内容,暂时不要求理解
- 说到代码强迫症,这一点是很多程序员都会有的,就是看到特别愚蠢的代码会忍不住自己上手改一下的感觉,
包括( )自己的代码。新人可能会觉得一些规范没必要,但是如果编程经验时间长了,就会发现一些写起来很愚蠢的代码很难阅读和维护,看起来很别扭。我认为也许艺术就是这样,因为世界上丑陋的东西见多了,便更加认识到美好的东西的美好之处,更加追求美好的东西,这也是为什么很多人实际上欣赏不来很高深的艺术的原因 - 代码也可以看做是一种艺术创作。画家用画笔创作,程序员用键盘创作
事实上,编译器在编译的时候,会忽略掉换行空格与tab等空白字符。所以你写成这样也可以,尽管这样会很不易读
#include <stdio.h> #include <stdlib.h> int main(){printf("Hello world!\n"); getchar(); return 0;}
教程最开始的hello world示例代码是右派写法,即把左大括号{
放到每一行右面。对应的,下面是左派写法,即把左大括号放到第二行的开头。一些代码规范会要求在一些情况使用左派,一些情况使用右派
- 我个人最开始学编程的时候习惯左派写法,但是时间长了发现右派写法可以在屏幕中用更少的空间装下更多的内容
#include <stdio.h> #include <stdlib.h> int main() { // 左派写法 printf("Hello world!\n"); getchar(); return 0; }
需要注意的是,右大括号,和左派写法的左大括号,需要单独占一行。不推荐下面的写法
#include <stdio.h> #include <stdlib.h> int main(int argc, char **argv) { if (argc == 1) // 左大括号要单独占一行 { // 推荐这样的左大括号用法,单独占一行 printf("There is no argument\n"); // 这一行并没有和上一行的左大括号写在同一行 return 0; } printf("The number of arguments is %d\n", argc-1); // 不推荐上一行这样写,推荐将printf()函数另起一行 return 0; }
一行最好不要写太长,如果写太长的话,推荐分行写。如果if ()
语句或for ()
语句一行需要写很长的话,我个人推荐下面的写法
// 提前声明,我不是不会auto for ( // 在写 `if ()' 语句和 `for()' 语句时,最好在`if'和`for' 之后敲一个空格,以与函数调用区分开 vector<int>::iterator iter = v.begin(); // 括号内的内容推荐多一个缩进 iter != v.end(); iter++){ // 请注意这里和下面,右小括号和左大括号放在一起时的写法。这里的写法比较不容易区分iter++是否是大括号内的内容 count++; int val = *iter; if ( val >= lower_value && // 表达式太长需要换行时,运算符通常写在当前行末尾,而不是第二行开头 val < upper_value && // 为了好看,这里的`upper_value'前面敲了两个空格 is_prime(val) ){ // 请注意这里和上面右小括号和左大括号放在一起时的写法 sum += val; } else sum -= val; }
关于空格的使用:通常在每个二元运算符左右各加一个空格,而一元运算符后面不加空格。通常在每个逗号和分号后面加一个空格
- 这些是二元运算符,通常左右分别加一个空格(但其实有时为了好看反而不加空格)
- 赋值类:(=) (+=) (-=) (*=) (/=) (%=)等
- 运算类:(+) (-) (*) (/) (%) (
- 位运算类:(<<) (>>) (&) (|) (^)
- 比较运算类:(<) (>) (<=) (>=)
- 逻辑运算类:(&&) (||)
- 这些是一元运算符,通常不在右边加空格
- 负号:(-)
- 自加/自减:(++) (--)
- 逻辑非: (!)
- 按位非:(~)
- 取地址:(&)
- 指针解引用:(*)
- 这些运算符不加空格
- 结构体成员访问:(.) (->)
- 函数调用 (f())
有时为了排版好看,故意用多个空格或tab,比如下面代码
void Bit_vector::write_buffer_to_file(FILE* output){ fwrite(&capacity, sizeof(int), 1, output); fwrite(&end_bit_index_of_byte, sizeof(int), 1, output); fwrite(&end_byte_index, sizeof(int), 1, output); fwrite(buffer, sizeof(char),end_byte_index+1, output); }
第一章 类型与编码
上一章写起来特别抽象,可能对于没有编程基础的人很难理解。问题不大,直接进入第一章。本教程有意将常量运算与变量运算分开,以便于讲解更深入的内容
printf()函数入门
先介绍一个简单的printf()
打印功能,这里用于打印int
类型的数据。printf()
函数的详情功能可以查看这个页面,下面也会详细介绍
"%d\n"
中的%d
是占位符printf()
函数会扫描第一个参数的字符串扫描并打印,如果扫描到占位符,则用printf()
后面的参数代替占位符- 尤其注意下面第二个
printf()
函数第一个参数有两个占位符。打印时,第一个占位符被替换成了第二个参数9
,第二个占位符被替换成了第三个参数30
- 打印不同类型时需要用到不同的占位符,比如
%d
用于打印整形,%c
用于打印char
类型等,详情查看这个页面
"%d\n"
中的'\n'
表示换行,即把光标移动到下一行的开始位置- C语言中,单个字符字面量用单引号括起来,而字符串字面量用双引号(关于字面量,将在后面详细介绍)
- 如果不加上换行的话,输出结果会黏在一起,就像这样
2023930192359
#include <stdio.h> #include <stdlib.h> int main(){ printf("%d\n", 2023); printf("%d\n%d\n", 9, 30); printf("%d\n", 9+10); printf("%d\n", 3+10*2); // (*)的优先级大于(+) printf("%d\n", 10-3-2); // (-)是左结合的 printf("%d\n", 10-(3-2)); // (())可以改变运算的优先级 return 0; }
上面代码的执行结果是下面
2023 9 30 19 23 5 9
运算顺序、优先级与结合性
下面先介绍运算符的运算顺序、优先级与结合性。关于运算符具体的功能,将在后面的内容分节介绍
上面代码中,printf("%d\n", 3+10*2);
的结果是打印23
。注意这里的运算顺序,并不是先运算(3+10)*2
,而是先运算3+(10*2)
。我们可能认为这理所当然,而在C语言中,这涉及到一个优先级的问题。在这里,*
的优先级是高于+
的
优先级和结合性可以查看, 这个网站。括号可以改变运算的优先级
然后说什么是结合性。在运算10-3-2
的时候,我们理所当然认为应该先运算(10-3)-2
,而不是10-(3-2)
。因为,-
是左结合的,或者叫从左到右运算。一些运算符是右结合的,比如(=)。虽然还没到学变量的时候,但还是给出下面的代码
#include <stdio.h> #include <stdlib.h> int main(){ int a; a = 3+2;// (=)是右结合的,所以会先计算(=)右边的内容,等到结果5计算完毕后,再进行(=)运算 printf("%d\n", a); return 0; }
优先级相同的运算符的结合性一定相同。运算时,先运算优先级高的,然后运算优先级低的。同等优先级的,按照结合性进行从左到右或者从右到左运算。括号可以改变优先级
更多的进位制与char类型
上面我们涉及到的都是int
类型的常量,现在介绍一个新的类型char
和它的编码表示。一个char
类型的大小是一个字节(byte),一个字节是8位二进制数字。下面一段介绍什么是二进制数字
我们经常使用的是十进制数字,十进制数字是由0123456789
这些数字之中的一个或多个组成的串(但不可以有前导零)。二进制数字是一串用0
或1
组成的串(可以有前导零)
- 为了区分各个进制的数字,通常在数字前面加上前缀。C语言中,十进制数字无需前缀,二进制数字前缀为
0b
,八进制数字前缀为0
,十六进制数字常用前缀为0x
- 二进制数字可以有前导零的意思就是,类似于
0b00001010
这样的8位二进制数字是合法的。十进制数字通常不可以有前导零,所以1023
是合法的十进制数字,而01023
不是合法的十进制数字
二进制数字和十进制数字可以互相换算,详情可以查看此文。下面是二进制数字0b01001010
换算成十进制数字的例子,其中,每一个二进制位的位置对应着一个权重。运算时,将每一位的值乘以这一位对应的权重,然后求和,就可以得到十进制数字
128 64 32 16 8 4 2 1 每一位的权重 * * * * * * * * 乘以 +--+--+--+--+--+--+--+--+ ob| 0| 1| 0| 0| 1| 0| 1| 0|每一位的值 +--+--+--+--+--+--+--+--+ 0+64+ 0+ 0+ 8+ 0+ 2+ 0 得到的数字 = 74 加在一起就是对应的十进制数字
计算机科学常见数字表示还有十六进制,它由0123456789ABCDEF
16个字符组成,其中字母可以小写。下面给出了每一个字符对应的十进制数字对应表,以及一个十六进制数字换算示例
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | F| E| D| C| B| A| 9| 8| 7| 6| 5| 4| 3| 2| 1| 0| +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ |15|14|13|12|11|10| 9| 8| 7| 6| 5| 4| 3| 2| 1| 0| +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
65536 4096 256 16 1 * * * * * +-----+-----+-----+-----+-----+ | 0xa| 6| 0| f| 3| | 10| 6| 0| 15| 3| +-----+-----+-----+-----+-----+ 655360+24576+ 0+ 240+ 3=680179
上文提到,char
类型是一个字节大小,而一个字节是8个二进制位。因为每个二进制位都有两种可能(分别是0
或1
),所以char
类型的数字一共有256种,可以用来表示-128~127
或0~255
之间的数字,也可以用来表示字符,毕竟char
是character
的缩写
char
类型的值可以用来表示ASCII码字符,ASCII码字符与char
类型的值的对应关系可以查看这个网页。单个ASCII字符的字面量,例如单个'a'
,的类型也是char
类型
- 什么是字面量:在代码中,字面量是与变量区分开的概念。字面量直接表示它的的值(value),也就是说,程序员可以直接从字面量中看出一个字面量的值。例如我们可以从字面量
0xf
中看出它的值是int
类型的十进制数字15
。而对于变量,无法从它的名字看出它的值int
类型的字面量没有后缀,所以没有后缀的,不带小数点的数字字面量都是int
类型- 字面量都是常量
TODO:可以使用两个十六进制数字来表示一个字节
基本数据类型与数据编码表示
TODO:大端与小端
TODO:sizeof()关键字
位运算
位运算一共有6种,分别是按位非~
、按位与&
、按位或|
、按位异或、左移、右移
我们从最简单的运算:逻辑非运算讲起。我们知道,计算机通过0
或1
的编码来存储数据。非运算很简单,即在运算后,把操作数每一个二进制位中的0
变成1
,把1
变成0
。逻辑非的真值表以及一个运算示例如下
- [来源请求],所以(&) (|)的运算优先级是历史遗留问题
自动类型转换
类型转换分为两种,一种是自动类型转换,一种是手动类型转换
手动类型转换
C语言中手动类型转换的语法是第一行。C++中支持更复杂的类型转换,可以使用第二条语法来增强可读性
(type) value type(value)
手动类型转换常用于解决除法运算结果是整数的问题
#include <stdio.h> #include <stdlib.h> int main(){ printf("%lf\n", (double)10 / 3); // 先将10转换为double类型,然后10/3就变成了(double)除以(int)。根据上述自动类型转换内容,除号的两个操作数均变为(double),所以运算结果是3.33333 printf("%lf\n", 10/(double)3); // printf("%lf\n", (double)(10 / 3)); return 0; }
上面代码的执行结果是下面。为什么第三行输出是3.000000
,因为它是先进行的(int 10)/(int 10)
,得到(int 3)
,然后由(int 3)
转为double
类型
3.333333 3.333333 3.000000
函数入门
#include <stdio.h> #include <stdlib.h> // 习惯将不同作用的代码用空行分开。比如上面的代码是引用头文件,下面的代码是函数声明 int f(); // 一些教程会在函数被实现之前,在开头给出其声明 // 这里的空行也是如此,用于分开函数声明和函数实现 int f(void){ // 此函数先给出的声明,然后给出的实现 return 10; } // 习惯为了好看,在函数实现后加一个空行 char g(){ // 此函数声明时同时实现 return '+; //43 } int main(){ printf("%d\n", f()); printf("%c\n", g()); printf("%d\n", f()+g()); printf("%c\n", getchar()); return 0; }
上面代码的执行结果是下面
10 63 53
然后程序会阻塞到这里。此时输入一个字符,假设是a,然后回车,然后程序就会将这个字符打印出来,然后退出
上面代码声明了三个函数,这三个函数的返回类型第一个和第三个是int
类型,第二个是char
类型。这两个函数都没有参数,没有参数的函数可以在其参数列表置空,也可以写入void
- 函数返回值应该要么与函数声明的返回类型一致,要么可以自动转换为函数声明的返回类型
章末知识窗:CPU层面运算是怎样进行的
- 章末知识窗内容是选学内容
编译器将C语言代码编译成可执行文件,可执行文件内部是可以直接由CPU执行的机器码。机器码可读性很差,所以通常将一些机器码简记为汇编。汇编有两种风格,分别是Intel和AT&T,这两种风格有很大差别。此处使用AT&T汇编风格。linux系统中可以使用objdump
命令查看一个可执行文件的反汇编代码
- 很多IDE将代码的编译等步骤集成了起来,所以你可能只需要点一下按钮,IDE就会自动帮你完成编译步骤,然后直接执行已编译后的程序
48 83 ec 08 sub $0x8, %rsp # 汇编代码的注释使用的是井号,而不是两个斜线
上面的示例中,第一行机器码(用十六进制数字表示)对应的汇编代码为第二行
运算时,CPU会将数据存储到寄存器中,一些CPU内有16个寄存器。CPU的运算单元可以根据寄存器的值以及机器代码来取出其中两个寄存器的值,进行运算,并将运算结果存入其中一个寄存器中。在调用函数时,寄存器用于传递函数参数
#include <stdio.h> int main(){ printf("%d\n", 3+10*2): return 0; }
上面的C语言代码可以编译后的可执行文件的反汇编是下面的汇编代码,可以通过看注释来体验一下最底层CPU层面代码是怎样运行的。需要注意的是,几乎所有编译器都会进行优化,所以你进行反编译的时候,极有可能不会得到和下面相同的反汇编代码(所以下面的反汇编是我根据现有代码改编的) TODO:添加代码高亮,将注释对其
<main>: mov $0xa, %edx # 将寄存器%edx赋值为立即数$3.汇编中,立即数前面需要加$符号,0x表示该立即数十六进制数字表示。$0xa对应十进制数字是`10' mov $0x2, %eax # 将寄存器%eax赋值为立即数2。此时,%edx寄存器的值为10,%eax寄存器的值为2 imul %eax, %edx # 让寄存器%eax的值乘以%edx的值,结果保存在%edx寄存器中 mov $0x3, %eax # 将寄存器%eax的值赋值为立即数3。此时,%edx寄存器的值为20,%eax寄存器的值为3 add %edx, %eax # 让寄存器%edx的值乘以%eax的值,结果保存在%eax寄存器中 mov %eax %esi # 将寄存器%eax的值赋值给%esi,这样%esi的值就是刚刚的运算结果。%esi用于传递printf()函数的第二个参数 lea 0xe83(%rip), %rax # % 这一行和下两行可以忽略 mov %rax, %rdi # 这一行和上一行用于将字符串常量"%d\n"作为第一个参数传入 mov $0x0 %eax call <printf@plt> # 调用printf()函数。下面几行可以忽略 mov $0x0, %eax # 对应return 0。%eax用于传递函数的返回值 leave ret # 函数调用结束,返回
章末知识窗:C语言的版本
第三章 变量与条件
前两章都是关于常量运算的,这里开始变量运算
变量的声明与关键字
TODO:变量的名字不可以是关键字 TODO:变量的关键字标记
- 什么是常量:程序不可以修改的量
- 什么是变量:变量这个词具有多义性,其概念将会在第三章详细介绍。
- C语言中,变量可以是常量,这种变量被称为常变量。
const
关键字可以标记一个变量为常变量,后面会详细介绍
变量的地址与函数栈
TODO:静态变量。虽然这里还没有讲到变量的自加运算和指针,但还是给出一个静态变量的使用示例 TODO:CSAPP中将program stack译作程序栈
变量的运算
TODO:需要注意自加与自减运算
逻辑运算与三目运算
TODO:的返回值是0或1
if-else语句
TODO:可以没有else
TODO:if-else if-else 是一种特殊的 if-else 使用方式
函数的声明与实现
TODO:是否是计数
函数的调用与函数库
TODO:math函数库
switch语句
章末知识窗:汇编层面是怎样实现跳转的
TODO:调换下顺序
goto语句
TODO:如果goto来goto去可能会太乱,不推荐使用
第章 循环与递归
while循环
for循环
do-while循环
递归函数
TODO:用计算10000!%10001为例引入群论,顺便介绍王义和的离散数学引论 TODO:stack overflow
章末知识窗:其他编程语言是怎样运行的
我们都知道,C语言是编译型编程语言,也就是说,C语言代码被编译器编译为可执行文件,然后才可以执行。本质上,被执行的并不是C语言代码,而是被可执行文件
第四章 指针与数组
一级指针及其运算
高级指针
内存的申请与释放
TODO:介绍malloc free函数,内存泄漏和堆
数组与指针
多维数组
C语言字符串
函数指针
章末知识窗:关于最早的C语言编译器
TODO:一个先有鸡还是先有蛋的问题
第四章 结构体与面向对象
typedef关键字
结构体
TODO:C语言和C++的struct不同 TODO:介绍sizeof()操作,因为结构体内部分配内存情况未知
结构体的访问与指针访问
面向对象与句柄
编程范式大概可分为两种:面向过程和面向对象(object-oriented programming,简称OOP或OO看上去很唬人——我在用面向对象编程哎——其实不是什么新鲜东西)
面向结果编程(认真你就输了) |
---|
还有一种编程范式是面向结果编程,通常用于糊弄编程作业。比如下面的Python代码用于糊弄打印前10个质数的作业
print(2) # Python 用井号开始注释,语句后不需要加分号 print(3) # Python 的 print()函数可以自动换行 print(5) '''Python 也可以用这种方式作为注释''' print(7) '''这种注释的本质是一个没有被赋值给变量的字符串''' print(9) '''这种注释可以跨行''' print(11) # 你发没发现,你现在可以看懂一些Python代码了 print(13) print(17) print(19) print(23) print(29) |
TODO:需要在同一个文件夹或目录下 TODO:其中file是一个FILE*类型的文件句柄。这其中,文件句柄并不是文件本身,但是程序员可通过文件句柄来读取文件
#include <stdio.h> #include <stdlib.h> int main(){ const char* file_name = "file.txt" FILE* file = fopen("file.txt", "r+"); if (file == NULL){ printf("Unable to open %s\n"); return -1;// 在程序出现错误时,通常不return 0 } for(char c; c = fgetc(); c!=EOF){ putchar(c); } printf("\n"); return 0; }
第五章 预处理与链接
这一章就要实践起来了,打开Code::Blocks或者VSCode,以及安装一个linux虚拟机(推荐用VMVare和Ubuntu入门),因为很多指令在虚拟机环境下更容易操作
“ | 纸上得来终觉浅,绝知此事要躬行 | ” |
——陆游《冬夜读书示子聿》 |
熟悉虚拟机的操作
编译实际上是怎样进行的
编写自己的头文件
编写Make文件与手动链接
宏
TODO:macro名字的由来
编译预处理指令
第六章 实现一个链表
很多C语言教程会在这部分使用实现一个2048游戏、十步万度游戏、扫雷游戏或者迷宫游戏等作为最后一章的实践内容
简单的数据结构:链表
TODO: 二叉树的定义和操作,这里选择不带有头结点的二叉树,每次执行返回头结点的指针
实现链表的插入
TODO:printf()函数debug
第七章 学完这些学什么
你已经攻略了C语言娘了,现在,尝试一下下面的内容吧
C语言是学习其他编程语言的跳板。如果能学明白C语言,那么其他编程语言自然不会有太多吃力。因为C语言已经有50多年的历史,在它之后的语言都或多或少参考了一些C语言
C语言被大量应用在嵌入式。可以尝试买一个51单片机玩玩,大概80软妹币以内,然后学习模电和数电,走上嵌入式工程师的道路。找时间我也可以出一个单片机教程
除了嵌入式以外,C语言的应用场景较少。可以尝试学一下C++。在高效性上C++的执行效率仅次于C语言。以后有时间我会考虑做一个C++教程
可以直接学CSAPP(Computer System: A Programmers' Perspective,深入理解计算机系统),这个是计算机科学最经典的教材之一[来源请求]
- 其实我认为这里的System应该翻译为体系。这本书的内容并不都是关于计算机系统的
- 如果没有C语言基础,这本书是学不明白的
可以攻略考取大学的计算机专业
可以尝试参加一些代码项目,哪怕只能贡献翻译也挺好的。可以帮我把这个烂摊子收拾一下U:渚 花/Lua参考手册(翻译)
可以看一些开源代码。如果看不懂的话推荐先学习数据结构与常见算法,和常见设计模式
- 我认为设计模式可大体按照应用场景分为软件设计模式和游戏设计模式
可以尝试学一下软件工程的内容。学完之后就可以尝试为一些开源软件贡献代码了
可以尝试攻略一下洛谷娘
可以尝试攻略一些常见的编程语言娘,比如Java娘、Python娘等。Python的设计和其他很多主流编程语言不同,被称为Pythonic
Python哲学 |
---|
在Python命令行输入 >>> import this |
可以尝试攻略一下世界上最好的编程语言娘PHP娘
- 实际上PHP是世界上最好的语言是一个梗
外部注释与链接
此教程主要参考菜鸟教程、苏小红《C语言程序设计 第四版》(也推荐这本)
TODO
待添加的内容
- 因为早期的设备内存空间小,所以早期的C语言代码的变量名和函数名都会趋于简单,[来源请求],所以
atoi()
这种可读性很差的库函数名是历史遗留问题。包括函数声明和实现分离,也是历史遗留问题,虽然很多教程还是会这样教,但我认为现在已经完全无必要
一个static关键字的例子
#include <stdio.h> #include <stdlib.h> // 如果pcount不是NULL,则在其指向的内存地址中写入该函数执行次数 // 否则仅返回加法返回值 int add(int a, int b, int* pcount){ static int count = 0; // 静态变量只会在函数第一次被调用时被赋值 count++; if (pcount){ // 等价于 if (pcount != NULL) *pcount = count; } return a+b; } int main(){ int count = 0; add(1, 2, NULL); printf("%d\n", add(1, 2, &count)); printf("%d\n", count); printf("%d\n", add(3, 4, &count)); printf("%d\n", count); return 0; }
下面是上面代码的执行结果
3 2 7 3