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等等,其中的数字其实就是来自指定标准的年份。

对外发布的标准中包含两部分内容:

  1. C/C++支持哪些特性
  2. C/C++API,程序员可以在他们的C/C++程序中直接调用这些API,这些API就被称为标准库 (Standard Library)

注意发布的标准中只定义了API,但是并不包括实现,肯定有同学会问,那么是谁来实现标准中定义的API呢?

C/C++标准库的实现

至此,我们终于可以开始讨论标准库的实现问题了,实际上专门有一群人负责来根据发布的API来实现标准库,程序员在实现除了一些比如数学计算之外,像文件读写、内存分配、线程创建等等相关的API的实现,这些程序员必须借助相应操作系统提供的功能,那么这些程序是怎样使用操作系统提供的功能的呢?答案就是借助系统调用(System Call),注意很多同学可能意识这一点,但是这一点相当重要,那就是我们所写的代码有很多是需要依赖操作系统的,操作系统其实提供了很多功能,程序员使用这些功能的方式其实就是借助系统调用(关于系统调用,博主在《操作系统:以程序员的角度》中有详细的讲解)。

因此我们知道,其实每一个平台(操作系统)上都有自己特定的标准库实现,因为不同的操作系统提供的功能是不同的,提供的系统调用也是不同的。

现在我们就可以回答最开始提出的问题了,原来printf和cout等等的代码是实现在标准库中,那么这些标准库在哪里呢,我们的程序又是怎么用到标准库的呢? 标准库在哪里?怎样使用?让我们用C语言写一个简单的Hello World程序:

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

然后编译、执行:

  1. $ gcc helloworld.c -o hw
  2. $ ./hw
  3. hello world

我们可以看到程序正确运行了,但是问题来了,既然我们已经知道了printf其实是实现在了标准库中,那么这个过程中哪里涉及到标准库了?

要回答这个问题,我们需要知道编译可执行程序中的一个过程:链接,关于链接博主在《彻底理解链接器》系列文章中有详解的讲解。简单来说,链接的作用就是把程序依赖的各个库打包起来。要想看到可执行程序依赖哪些库,我们借助一个叫ldd的工具:

  1. $ ldd hw
  2. linux-vdso.so.1 => (0x00007ffe075d3000)
  3. libc.so.6 => /usr/lib64/libc.so.6 (0x00007fcd58b75000)
  4. /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一起发布。

总结

一个看似简单的问题实际上往往并不那么简单,在这篇文章中,我们从一个简单的问题开始不断挖掘背后涉及到的方方面面,希望这篇文章能帮你彻底理解标准库。