读书笔记:《嗨翻C语言》 - [美]David Griffiths [美]Dawn Griffiths 著 / 程亦超 译

浅显易懂很适合入门,如果代码不强行翻译成中文就更好了。

出版时间:2013 年 9 月

章节:完整的C程序长啥样?

计算机会从main()函数开始运行程序。它的名字很重要:如果没有一个叫main()的函数,程序就无法启动。

当计算机在运行程序时,它需要一些方法来判断程序是否运行成功,计算机正是通过检查main()函数的返回值来做到这一点。如果让main()函数返回0,就表明程序运行成功;如果让它返回其他值,就表示程序在运行时出了问题。

当调用printf()时,可以包含任意数量的参数,但确保每个参数都要有一个对应的%格式符。

章节:程序工作了!

C语言比其他大多数语言的抽象层次更低,因此它不提供字符串,而是用了相似的东西来代替:以字符为元素的数组。

章节:别在字符串的尽头掉下去

在如今的很多语言中,计算机会时刻记录数组的大小,但C语言比大多数语言更低层,它无法确切地知道数组有多长,如果C语言想在屏幕上显示字符串,它就需要知道什么时候会到达字符数组的尾部,为此C语言加入了哨兵字符。

为什么字符要从0开始编号?为什么不是1?字符的索引值是一个偏移量:它表示当前要引用的这个字符到数组中第一个字符之间有多少字符。

单引号通常用来表示单个字符,而双引号通常用来表示字符串。

字符串字面值是常量。

C语言采取不同的方式在存储器中保存字符串字面值。总线错误意味着程序无法更新那一块存储器空间。

在C语言中,“等号”(=)用来赋值(assignment),而“双等号”用来检查两个值是否相等。

章节:做事情

C语言中大部分命令都是语句。简单的语句是一些动作,它们做事情,或告诉我们事情。你已经见过定义变量的语句、从键盘读取输入的语句以及向屏幕显示数据的语句。

章节:用break语句退出循环……

break非常有用,因为它有时是结束循环最简单有效的方法,但应该避免滥用break,因为它们会降低代码的可读性。

章节:……用 continue 继续循环

通常情况下,函数都需要包含一条return语句,但只要把函数的返回类型声明为void,没有return语句也无妨。

章节:C代码包含指针

指针就是存储器中某条数据的地址。

在函数调用时,可以只传递一个指针,而不用传递整份数据。

章节:深入挖掘存储器

每当声明一个变量,计算机都会在存储器中某个地方为它创建空间。如果在函数(例如main()函数)中声明变量,计算机会把它保存在一个叫栈(Stack)的存储器区段中;如果你在函数以外的地方声明变量,计算机则会把它保存在存储器的全局量段(Globals)。

章节:运行代码时,计算机在想什么

sizeof(指针)在32位操作系统中返回4,在64位操作系统中返回8。

编译器会把运算符编译为一串指令;而当程序调用函数时,会跳到一段独立的代码中执行。

编译器可以在编译时确定存储空间的大小。

章节:数组变量与指针又不完全相同

指针变量是一个用来保存存储器地址的变量,那数组变量呢?如果对数组变量使用&运算符,结果是数组变量本身。

章节:为什么数组从0开始

数组变量可以用作指针,这个指针指向数组的第一个元素,也就是说除了方括号表示法,还可以用*运算符读取数组的第一个元素。

表达式drinks[i]和*(drinks + i)是等价的,这解释了为什么数组要从索引0开始,所谓索引,其实就是为了找到元素的地址单元,指针需要加上的那个数字。

章节:致命处方案件

在编译器生成可执行文件时,编译器会根据变量的类型,用变量的大小乘以指针的增量或减量。

章节:用scanf()输入数字

怎么把数据输进数值字段呢?传递一个指向数值变量的指针就行了。

章节:scanf()会导致缓冲区溢出

缓冲区溢出很有可能会导致程序出错,这种情况通常被称为段错误或abort trap,不管出现什么错误消息,程序都会崩溃。

章节:fgets()配合sizeof一起使用

如果需要输入由多个字段构成的结构化数据,可以使用scanf();而如果想要输入一个非结构化的字符串,fgets()将是你的不二之选。

章节:字符串字面值不能更新

为了防止修改,字符串字面值通常保存在只读存储器中。

章节:如果想修改字符串,就复制它

如果你想把指针设成字符串字面值,必须确保使用了const关键字。

章节:神奇子弹案件

如果在变量声明中看到*,说明变量是指针。

可以将char指针声明成为const char *,以防代码用它修改字符串。

章节:把存储器保存在大脑里

栈:这是存储器用来保存局部变量的部分。每当调用函数,函数的所有局部变量都在栈上创建。它之所以叫栈是因为它看起来就像堆积而成的栈板:当进入函数时,变量会放到栈顶;离开函数时,把变量从栈顶拿走。奇怪的是,栈做起事来颠三倒四,它从存储器的顶部开始,向下增长。

堆用于动态存储。

章节:使用string.h

stdio.h提供了标准输入/输出函数,如printf和scanf。

章节:使用strstr()函数

strstr()函数会在第一个字符串中查找第二个字符串,如果找到,它会返回第二个字符串在存储器中的位置

章节:该审查代码了

程序总是首先运行main()函数。

可以在一个C程序中创建多个函数,但计算机总是先运行main()。

章节:可以用 < 重定向标准输入……

可以使用 < 操作符从文件中读取数据。

用 > 重定向标准输出。

章节:做一件事并把它做好

只做一件简单的事。

章节:bermuda工具

不同操作系统实现管道的方法不同,可能用存储器,也可能用临时文件。我们只要知道它从一端接收数据,在另一端发送数据就行了。

< 会把文件内容发送到流水线中第一个进程的标准输入, > 会捕获流水线中最后一个进程的标准输出。

章节:创建自己的数据流

fopen()函数接收两个参数:文件名和模式。共有三种模式,分别是w(写文件)、r(读文件)与a(在文件末尾追加数据)。

最早FILE是用宏定义的,而宏的名字通常都要大写。

章节:由库代劳

为了避免歧义,可以用--隔开参数和选项,比如set_temperature -c -- -4。getopt()看到--就会停止读取选项。

章节:声明与定义分离

声明只是一个函数签名:一条包含函数名、形参类型与返回类型的记录。

章节:创建第一个头文件

严格意义上讲,编译器只完成编译的步骤,即把C源代码转化为汇编语言。但宽泛地讲,编译是将C源代码转化为可执行文件的整个过程,这个过程由很多阶段组成,而gcc允许你控制这些阶段。gcc会预处理和编译代码。

章节:编译的幕后花絮

一旦有了全部的目标代码,就需要像拼“七巧板”那样把它们拼在一起,构成可执行程序。当某个目标代码的代码调用了另一个目标代码的函数时,编译器会把它们连接在一起。同时,链接还会确保程序能够调用库代码。最后,程序会写到一个可执行程序文件中,文件格式视操作系统而定,操作系统会根据文件格式把程序加载到存储器中运行。

章节:在程序中包含encrypt.h

为了防止两个源文件中的同名变量相互干扰,变量的作用域仅限于某个文件内。如果你想共享变量,就应该在头文件中声明,并在变量名前加上extern关键字。

章节:make需要知道什么?

make编译的文件叫目标(target)。严格意义上讲,make不仅仅可以用来编译文件。

章节:用makefile向make描述代码

生成方法都必须以tab开头。

章节:用结构创建结构化数据类型

struct是structured data type(结构化数据类型)的缩写。有了结构,就可以像下面这样把不同类型的数据写在一起,封装成一个新的大数据类型。

struct fish {
  const char *name;
  const char *species;
  int teeth;
  int age;
};

章节:使用“.”运算符读取结构字段

当把一个结构变量赋给另一个结构变量,计算机会复制结构的内容。如果结构中含有指针,那么复制的仅仅是指针的值。

章节:结构中的结构

计算机按字从存储器中读取数据,如果某个字段跨越了多个字,CPU就必须读取多个存储器单元,并以某种方式把读到的值合并起来。

章节:(t).age和t.age

t->age表示“由t指向的结构中的age字段”,也就是说happy_birthday()函数还能这么写:

void happy_birthday(turtle *a)
{
  a->age = a->age + 1;
  printf("Happy Birthday %s! You are now %i years old!\n", a->name, a->age);
}

计算机先对“点”运算符求值,然后对*运算符求值。

章节:枚举变量保存符号

有时你不想保存数字或文本,而是想保存一组符号。如果你想记录一周中的某一天,只想保存MON-DAY、TUESDAY、WEDNESDAY……这些符号。你不需要保存文本,因为一共只有七种不同的取值。

章节:有时你想控制某一位

每一个十六进制数对应一个长度为4的二进制数。只要知道数字0到15的二进制形式,就能很快地在心中完成转换。

章节:位字段的位数可调

以用位字段(bitfield)指定一个字段有多少位。如果你有一连串的位字段,计算机会放在一起,以节省空间,也就是说如果有8个1位的位字段,计算机就会把它们保存在一个字节中。

假设想在某个结构中保存月份(0到11的值),就可以用一个4位的位字段来保存,为什么?因为4位可以保存0到15,而3位只能保存0到7。

二进制字面值占了很大空间,而且十六进制通常写起来更快。

4位可以保存0到二进制数1111(也就是15)的值,但3位最大只能保存二进制数111(也就是7)。

章节:用C语言创建岛屿……

在C语言中,NULL的值实际上为0,NULL专门用来把某个指针设为0。

章节:用堆进行动态存储

栈是存储器用来保存局部变量的区域。数据保存在局部变量中,一旦离开函数,变量就会消失。

章节:首先,用malloc()获取空间

想象程序在运行时突然发现有大量数据要保存,程序想申请一个大容量储物柜来保存数据,在C语言中,可以用一个叫malloc()的函数来申请。你告诉malloc()需要多少存储器,它就会要求操作系统在堆中分配这点空间,然后malloc()会返回一个指针,指向堆上新分配的空间。

章节:有用有还

使用栈的时候,你无需操心归还存储器,因为这个过程是自动进行的。每当你离开函数,局部变量就会从栈中清除。

章节:调用free()释放存储器

malloc()函数分配空间并给出一个指向这块空间的指针。你需要用这个指针访问数据,用完以后,需要用free()函数释放存储器,就像把储物柜的钥匙还给服务员,好让别人能接着用。

章节:用malloc()申请存储器……

申请存储器的函数叫malloc(),是memory allocation(存储器分配)的意思。

章节:用strdup()修复代码

字符串字面值位于存储器的只读区域,该区域专门用来分配常量。

章节:函数名是指向函数的指针……

在C语言中,函数名也是指针变量。当你创建了一个叫go_to_warp_speed(int speed)函数的同时也会创建了一个叫go_to_warp_speed的指针变量,变量中保存了函数的地址。

章节:如何用数组解决刚才的问题?

用“返回类型(*变量名)(参数类型)”来声明新的函数指针。

章节:你的函数如何做到这点?

预处理器在编译阶段之前运行,它会做很多事情,包括把头文件包含进代码。

章节:一种平台一个叫法

绝大部分操作系统都支持动态库,它们的工作方式也大抵相同,但称呼却大相径庭。在Windows中,动态库通常叫动态链接库,后缀名是.dll;在Linux和Unix上,它们叫共享目标文件,后缀名.so;而在Mac上,它们就叫动态库,后缀名.dylib。

章节:岂止是安全问题

什么是内核?在大部分计算机上,系统调用就是操作系统内核中的函数。什么是内核?虽然你从来没在屏幕上看到过它,但内核其实一直都在那里控制计算机。内核是计算机中最重要的程序,它主管三样东西:

  1. 进程:只有当内核把程序加载到存储器时程序才能运行。内核创建进程,并确保它们得到了所需资源。内核同时也会留意那些变得贪得无厌或者已经崩溃的进程。
  2. 存储器:计算机所能提供的存储器资源是有限的,因此内核必须小心翼翼地分配每个进程所能使用的存储器大小。内核还能把部分存储器交换到磁盘从而增加虚拟存储器空间。
  3. 硬件:内核利用设备驱动与连接到计算机上的设备交互。你的程序在不了解键盘、屏幕和图形处理器的情况下就能使用它们,因为内核会代表你与它们交涉。

系统调用是程序用来与内核对话的函数。

章节:用fork()克隆进程

fork()会克隆当前进程。新建副本将从同一行开始运行相同程序,变量和变量中的值完全一样,只有进程标识符(PID)和原进程不同。

章节:waitpid()函数

重定向输入、输出,然后让进程相互等待,进程间通信就这么简单。

新文件总是按序加入描述符表,如果第一个空的描述符是4号,你的文件就会用它。

章节:…定时器发出SIGALRM信号

每次调用alarm()函数都会重置定时器,也就是说如果把闹钟调到10秒,但过一会儿又把它设为了10分钟,那么闹钟信号10分钟以后才会触发,第一个10秒计时就失效了。

章节:BLAB:服务器连接网络四部曲

在使用套接字与客户端程序通信前,服务器需要历经四个阶段:绑定(Bind)、监听(Listen)、接受(Accept)和开始(Begin),首字母缩写为BLAB。

章节:套接字不是传统意义上的数据流

端口号从0开始一直到65535,首先你需要决定用小号码(1024以下)还是大号码。很多计算机中,只有超级用户或管理员才有资格使用1024号以下的端口,因为小号的端口留给了一些知名服务,如网页服务器和邮件服务器。操作系统只允许管理员使用这些端口,防止普通用户启动一些多余的服务。

章节:……进程不是唯一答案

难道每当想要同时做几件事时都得创建进程吗?不见得,有以下几个原因。

  1. 创建进程要花时间。
  2. 有的机器新建进程只要花一丁点时间。虽然时间很短,但还是需要时间。如果你想要执行的任务才用几十毫秒,每次都创建进程就很低效。
  3. 共享数据不方便。
  4. 当创建子进程时,子进程会自动包含父进程所有数据的副本。但这些只是副本,如果子进程想把数据发回父进程,就需要借助管道之类的东西。

章节:用互斥锁来管理交通

假设你有两个线程,它们都想得到互斥锁A和B。倘若第一个线程得到了A,第二个线程得到了B,这两个线程就会陷入死锁。因为第一个线程无法得到B,第二个线程无法得到A,它俩都停滞不前。

章节:Allegro能做什么?

通过定时器,你就能创建一个按固定帧率(FPS)刷新屏幕的程序,比如每秒60帧。

章节:用static定义私有变量或函数

static关键字用来控制变量或函数的作用域,防止其他代码以意想不到的方式访问你的数据或函数。

发表评论

您的电子邮箱地址不会被公开。