31.程序员应如何理解标准库
记得当年在学了C/C++语言后一直有这样的疑惑,常用的printf函数以及C++中的cout函数到底是在哪里实现的?
相信不止我有这个疑问,这篇文章就来回答这个问题。
C/C++语言是怎样实现的
详细有的同学一定觉得编程语言是十分神秘的,实际上不是这样的。
一门编程语言的本质是什么?
本质上一门语言不过就是一堆规则(rules)而已,就像汉语中的主谓宾一样,就像
- if之后必须是一个括号(),这个括号中必须是一个bool表达式
- while之后必须是一个括号(),这个括号中必须是一个bool表达式
- continue语言必须出现在while语句中
- 等等
有的同学可能会问,为什么一定要有这堆规则呢,原来,只有有了规则之后编译器才能知道该怎么处理我们写的程序。
编译器在遇到if后就知道,接下来紧跟的一定是一个左括号,之后一定是一个bool表达式,再之后一定是一个右括号。
如果我们写的程序不满足这样的规则,结果就是编译器开始抱怨编译错误(compile error)。
让我们回到主题,实际上C/C++以及任何一门编程语言都是这样的一堆规则,对于C/C++来说,每年都有一群来自被称为International Organization for Standardization (ISO)组织的人来制定C/C++语言的规则,因此这群人坐下来讨论的这堆规则实际上就是一个标准,每一次讨论都会重新修改制定新的标准并对外发布,这就是为什么C/C++有各种版本: C99, C11, C++03, C++11, C++14等等,其中的数字其实就是来自指定标准的年份。
对外发布的标准中包含两部分内容:
- C/C++支持哪些特性
- C/C++API,程序员可以在他们的C/C++程序中直接调用这些API,这些API就被称为标准库 (Standard Library)
注意发布的标准中只定义了API,但是并不包括实现,肯定有同学会问,那么是谁来实现标准中定义的API呢?
C/C++标准库的实现
至此,我们终于可以开始讨论标准库的实现问题了,实际上专门有一群人负责来根据发布的API来实现标准库,程序员在实现除了一些比如数学计算之外,像文件读写、内存分配、线程创建等等相关的API的实现,这些程序员必须借助相应操作系统提供的功能,那么这些程序是怎样使用操作系统提供的功能的呢?答案就是借助系统调用(System Call),注意很多同学可能意识这一点,但是这一点相当重要,那就是我们所写的代码有很多是需要依赖操作系统的,操作系统其实提供了很多功能,程序员使用这些功能的方式其实就是借助系统调用(关于系统调用,博主在《操作系统:以程序员的角度》中有详细的讲解)。
因此我们知道,其实每一个平台(操作系统)上都有自己特定的标准库实现,因为不同的操作系统提供的功能是不同的,提供的系统调用也是不同的。
现在我们就可以回答最开始提出的问题了,原来printf和cout等等的代码是实现在标准库中,那么这些标准库在哪里呢,我们的程序又是怎么用到标准库的呢? 标准库在哪里?怎样使用?让我们用C语言写一个简单的Hello World程序:
#include <stdio.h>
int main() {
printf("hello world\n");
return 0;
}
然后编译、执行:
$ gcc helloworld.c -o hw
$ ./hw
hello world
我们可以看到程序正确运行了,但是问题来了,既然我们已经知道了printf其实是实现在了标准库中,那么这个过程中哪里涉及到标准库了?
要回答这个问题,我们需要知道编译可执行程序中的一个过程:链接,关于链接博主在《彻底理解链接器》系列文章中有详解的讲解。简单来说,链接的作用就是把程序依赖的各个库打包起来。要想看到可执行程序依赖哪些库,我们借助一个叫ldd的工具:
$ ldd hw
linux-vdso.so.1 => (0x00007ffe075d3000)
libc.so.6 => /usr/lib64/libc.so.6 (0x00007fcd58b75000)
/lib64/ld-linux-x86-64.so.2 (0x000055c5dbea4000)
我们注意到可执行程序hw依赖一个叫做libc.so.6的库,位于/usr/lib64/libc.so.6,这个libc.so.6就是我们苦苦寻找的标准库。
Linux中以.so结尾的文件被称为动态链接库,难怪我们看不到标准库的实现,原来都被实现好打包到了动态链接库中了,关于动态链接库详见《彻底理解链接器》中第三篇。
现在我们知道了标准库是什么,在哪里,有的同学可能会问,那么我们是怎么用标准库的呢?
原来,编译器gcc在编译程序是默认情况下就自动链接了标准库,因为大家写程序免不了使用标准库提供的API,因此gcc等编译器自动把标准库打包到了可执行程序了。
现在你应该明白了吧。
接下来我们就看看各个平台下标准库的实现。
Linux标准库实现
Linux下标准库的实现被称为GNU C Library,也被称为glibc,这个名字肯定有同学听过。
glibc是Linux平台中使用最为广泛的,然而有一段时间Linux发行版中的标准库多使用Libc,在经过了数年的开发后glibc又开始优于了Libc,Linux发行版又开始转回了glibc,现在在Linux发行版上你会看到磁盘上有一个libc.so.6的文件,这个文件其实就是现代版的glibc,只不过名字遵从了Linux发行版的习惯。
关于C++的标准库实现在了 libstdc++,你在Linux平台中使用ldd工具就能看到这个标准库。
Windows标准库实现
Windows标准库实现是和微软的官方编译器Visual Studio绑定在一起的,该标准库曾被称为C/C++ Run-time Library (CRT)
从Windows95开始,微软以MSVCRT+版本号.DLL的命名实行来发布,到了1997年,将其简化为了MSVCRT.DLL。
从Visual Studio 2015之后,Windows中C/C++标准库被称为了Universal C Runtime Library (Universal CRT,简称UCRT),即UCRTBASE.DLL,此后Windows标准库开始同Win10一起发布。
总结
一个看似简单的问题实际上往往并不那么简单,在这篇文章中,我们从一个简单的问题开始不断挖掘背后涉及到的方方面面,希望这篇文章能帮你彻底理解标准库。