《嗨翻C语言》(下)
动态存储(189~)
堆上的数据不会自动清除,因此堆是保存数据结构的绝佳场所。
可以把在堆上保存数据想象成在储物柜中寄存物品。
当程序不断地申请存储器,又不释放那些不再需要的存储器,就会发生存储器泄漏。
创建数据不会发生泄漏,只有当程序失去了所有对数据的引用才会导致泄漏。
用
malloc()
获取空间,memory allocation;调用free()
释放存储器malloc()
接收一个参数:所需要的字节数。因此malloc()
经常与sizeof
运算符一起使用。malloc()
返回的是通用指针,即void*
类型的指针。
strdup()
函数可以把字符串复制到堆上。strdup()
函数总是在堆上创建空间,而不是在栈上。所以千万记得要用free()
函数释放空间。valgrind工具,它用于Linux操作系统中。
当程序想分配堆存储器时,valgrind将会拦截你对
malloc()
和free()
的调用,然后调用自己的malloc()
和free()
。垃圾收集(garbage collection),一些语言会跟踪你在堆上分配的数据,当你不再使用这些数据时,就会释放它们。C语言没有“垃圾收集”
虽然不必在程序结束前释放所有数据,操作系统会在程序结束时清除所有存储器。不过,你还是应该显式释放你创建的每样东西,这是一种好的习惯。
——很不懂作者为什么在章节快结束的时候,放上一句这么没营养的话。有机会去查一查原文,确认是不是译者的问题。个人认为以下表述更恰当:
虽然操作系统会在程序结束时清除所有存储器。不过,你还是应该显式释放你创建的每样东西,这是一种好的习惯。
高级函数(201~)
函数指针
此章节介绍“学习如何把函数作为参数传递,从而提高代码的智商”。
有些时候,我们希望“把代码打包传给函数”。“把代码打包”就是封装成函数,所以,问题就是:怎么把函数传给函数?
在C语言中,函数名也是指针变量。
当你创建了一个叫
go_to_warp_speed(int speed)
函数的同时也会创建了一个叫go_to_warp_speed
的指针变量,变量中保存了函数的地址。只要把函数指针类型的参数传给find()
,就能调用它指向的函数了。上述中
go_to_warp_speed
指针变量是一个常量。类似int* a=4
中的4
,char* str="abc"
中的"abc"
。扩展
1
2
3
4
5
6/* 此处声明时的*和 赋值操作时的*应不是同一个意思?!
那么为什么初始化不写成int* a=&4?? —就是声明了一个指针,指向了常量存储区的一块地址 */
int* a; //声明
int b=4;
*a=4; //改变指针指向存储器中的值,但单纯这么写是错的
a=&b; //给指针赋值,改变指针指向的存储器函数名是指向函数的指针……两者并不完全相同,函数名是L-value,而指针变量是R-value,因此函数名不能像指针变量那样自加或自减。
函数指针是C语言最强大的特性之一。
函数指针的语法
- 没有函数类型,即:不能用
function * pointer_func
声明函数指针。 - 因为需要把函数的返回类型和接收参数类型告诉C编译器,所以创建函数指针如下:
返回类型(* 指针变量)(参数类型),例如
1
2
3int (*warp_fn)(int); //创建函数指针
wrap_fn = go_to_warp_speed; //函数指针赋值
wrap_fn(4); //使用函数指针
Q1:如果函数指针是指针,为什么调用时不需要再它们前面加*
?A1:可加可不加。
wrap_fn(4)
和(*wrap_fn)(4)
都可以。如果fp
是函数指针,那么可以用fp(参数,……)
调用函数。 也可以用(*fp)(参数,……)
,两种情况都能工作。Q2:可以用
&
取得函数的地址吗?A2:可以。
find(sports_or_workout)
和find(&sports_or_workout)
都可以。如果你有函数shoot()
,那么shoot
和&shoot
都指向了shoot()
函数。Q3:那为什么不这么写?
A3:即使省略
*
和&
,C编译器也能识别他们,(而且)这样代码更好读。4
或者"abc"
既表示本身的值,又代表其在常量存储区的地址?此处亦类似?
C标准库的排序函数
C标准库的排序函数会接收一个比较器函数(comparator function)指针,用来判断两条数据的大小。
qsort()
函数看起来像这样:1
2
3
4qsort(void* array, //数组指针
size_t length, //数组长度
size_t item_size, //数组中每个元素的长度
int (*comparator)(const void*, const void*));qsort()
函数在原数组上进行改动。
创建函数指针数组
在数组中保存函数,就必须告诉编译器函数的具体特征:函数返回什么类型以及接收什么参数。
1
void (* replies[])(response) = {dump, second_chance, marriage};
函数指针数组让代码易于管理,它们让代码变得更短、更易于扩展,从而可以伸缩。
——至于说降低程序可读性,那是因为你笨。
可变参数函数(variadic function)(228~)
C标准库中有一组宏(macro)可以帮助你建立自己的可变参数函数。
——TODO:kindle中描述这一组宏的图片看不清,抽空在网上找一下。
静态库和动态库
(省略。在接触Linux下C++开发之后,这方面知识不算陌生。)
系统调用(249~)
在大部分计算机上,系统调用就是操作系统内核中的函数,是程序用来与内核对话的函数。
system()
简单易用
安全问题
无法应付复杂的应用场景
操作系统必须解释你传给
system()
的字符串,这可能引发错误,尤其当你动态创建命令字符串时。
exec()
exec()
函数通过运行其他程序来替换当前进程。exec()
函数的版本众多,但可以分为两组:列表函数和数组函数。- 如果
exec()
调用成功,当前程序就会停止运行。一旦程序运行了exec()
以后的代码,就说明出了问题。
每个进程都有一组环境变量。
失败黄金法则
尽可能收拾残局
把
errno
变量设为错误码errno
变量是定义在errno.h
中的全局变量,和它定义在一起的还有很多标准错误码- 可以使用
string.h
中strerror()
函数查询标准错误信息
返回
-1
- 系统调用出错时通常会返回
-1
,但不是绝对的。 fileno()
:根据文件指针获取文件描述符。失败时不返回-1
exit()
是唯一没有返回值而且不会失败的函数。- 记住:一定要检查系统调用的返回值
- 系统调用出错时通常会返回
fork()
fork()
会克隆当前进程- 新建副本将从同一行开始运行相同程序。
- 原进程叫父进程,而新建副本叫子进程。
进程需要以某种方式区分自己是父进程还是子进程
fork()
会返回一个整型值:为子进程返回0
,为父进程返回一个正数。父进程将接收到子进程的进程标识符。与Unix和Mac不同,Windows天生不支持
fork()
。
进程间通信(274~)
重定向
三大默认数据流:标准输入、标准输出和标准错误;文件连接和网络连接也属于数据流。
进程需要记录数据流的连向,比如标准输出连到了哪里。进程用文件描述符(就是个数字)表示数据流
描述符表:描述符表的前三项万年不变:0号标准输入,1号标准输出,2号标准错误
- 标准输入/输出/错误在描述符表中的位置是固定的,但它们指向的数据流可以改变。
- 每打开一个文件,操作系统都会在描述符表中新注册一项
dup2()
复制数据流exit()
是系统调用!exit()
系统调用是结束程序的最快方式waitpid()
函数会等子进程结束以后才返回- 为了得到子进程的退出状态,可以把
pid_status
的值传给WEXITSTATUS()
宏
- 为了得到子进程的退出状态,可以把
管道
在命令行用管道连接两条命令时,实际把它们当成了父子进程来连接。child | parent,详见shell中管道的原理
pipe()
函数建立管道pipe()
函数创建了管道,并返回了两个描述符:fd[1]
用来向管道写数据,fd[0]
用来从管道读数据,
信号
- 当信号到来时,进程必须停止手中一切工作去处理信号。进程会查看信号映射表,表中每个信号都对应一个信号处理器函数。
捕捉信号,然后运行自己的代码
sigaction
是一个函数包装器,是一个结构体。- 创建
sigaction
以后,需要用sigaction()
函数来让操作系统知道它的存在 - 有一个例外,代码捕捉不到
SIGKILL
信号,也没法忽略它 - 在自定义的信号处理函数中使用
raise()
,这样程序就能在接收到低级别的信号时引发更高级别的信号
定时器
不要同时使用
alarm()
和sleep()
。两者都使用了间隔计时器,因此同时使用这两个函数会发生冲突。一个进程只有一个定时器。因此每次调用
alarm()
函数都会重置定时器定时器由操作系统的内核管理,如果一个进程有很多定时器,内核就会变得很慢,因此操作系统需要限制进程能使用的定时器个数。
信号可以让程序从容结束,而间隔定时器可以帮助处理一些超时任务。
网络编程(323~)
C程序用数据流读写字节。如果想要写一个与网络通信的程序,就需要一种新数据流——套接字。
在使用套接字与客户端程序通信前,服务器需要经历四个阶段:
绑定(Bind)
- 为了绑定它(端口),你需要两样东西:套接字描述符(还记得“文件描述符”吗?)和套接字名。
监听(Listen)
接受(Accept)
开始(Begin)
之前见过的数据流都可以用
fprintf()
和fscanf()
与它们通信。但套接字是双向的:- 如果想向套接字输出数据,就要用
send()
函数; recv()
函数;
- 如果想向套接字输出数据,就要用
绑定端口有延时:当你在某个端口绑定了套接字,在接下来的30秒内,操作系统不允许任何程序再绑定它,包括上一次绑定这个端口的程序。
recv()
读取数据- 字符串不以
\0
结尾。 - 当用户在telnet输入文本时,字符串以
\r\n
结尾。 recv()
将返回字符个数,如果发生错误就返回-1
,如果客户端关闭了连接,就返回0
。recv()
调用不一定能一次接收到所有字符。revc()
用起来十分繁琐,最好把它封装在某个函数中
- 字符串不以
getaddrinfo()
获取域名的地址getaddrinfo()
会在堆上创建一种叫名字资源的新数据结构- 因为名字资源在堆上创建,所以要用一个叫
freeaddrinfo()
的函数清除它
多线程(342~)
你可以使用很多线程库,这里我们将使用最流行的一种:POSIX线程库,也叫pthread。
线程函数的返回类型必须是
void*
。可以用
pthread_create()
创建并运行线程。如果程序运行完这段代码就结束了,线程也会随之灭亡,因此必须等待线程结束:
pthread_join()
会接收线程函数的返回值,并把它保存在一个void
指针变量中
通常当两个线程读写相同变量时,代码就是非线程安全的。
PTHREAD_MUTEX_INITIALIZER
实际上是一个宏,当编译器看到它,就会插入创建互斥锁的代码。如果你希望把某个整型值传给线程,并让它返回某个整型值,一种方法是:
- 用
long
,因为它的大小和void
指针相同,可以把它保存在void
指针变量中
- 用
问:怎样设计高效的多线程程序? 答:减少线程需要访问的共享数据的数量。
写在最后
另一种理解思路:
C语言允许你创建只能在函数局部作用域访问的全局变量:
- 函数内:
static
关键字会把变量保存在存储器中的全局量区。
- 函数内:
static
关键字用来控制变量或函数的作用域- 函数外使用
static
关键字,表示“只有这个.c文件中的代码能使用这个变量(或函数)”
- 函数外使用
关于“自动化测试”,你又了解多少?
之前读书时做的笔记全部誊抄、整理完毕,上篇中的知识点基本都已熟悉掌握,下篇的内容因为学校时练习不多,实际做项目经验也很少,所以只处于了解、知道的层次,勤复习,多练习。笔记内容包含编程过程中的“为什么这样”,也包括一些具体的语法细节、函数使用(实际上后者是不应该杂糅到一起的,但是基础太差)。在以后的复习、练习过程中,争取将语法细节、函数使用这些具体化的内容一点点去掉。