由来
这是我在作为学生组织的面试官时,对熟练掌握 C/C++ 的面试者提出几个的问题,以及对于这几个问题我自己的答案。
语法规则和特定技巧很大程度上只是记忆的问题,而在几个事实上知晓与否并不能断定面试者的高下。C 语言是一门十分接近底层实现的语言,许多的设计决定与实现直接相关,如果想要合适地运用,也要求程序员对于 C 的实现有透彻的理解。因此,这几个问题主要考察的是面试者在 C 原理与设计层面上的理解。
问题
为什么以下的语句不会导致非法内存访问?
1
2
3
4
5struct {
int i;
float f;
} *s = NULL;
printf("%p", &(*s).f);C++ 中的类实现了继承。请简单阐述如何在 C 中实现
struct
的继承。在 C 中
void *
类型可以自动转换成其他指针类型,但在 C++ 中需要显式转换。请谈一谈 C 和 C++ 为什么采取了这两种设计。(请简述 C 语言中变量自动初始化的规则。)
在 C 语言中,全局变量和静态局部变量可以自动初始化为全零,但局部变量不会进行自动初始化。请谈一谈 C 为什么采取了这样的设计。
我的答案
C 语言中类型只存在于编译前,
struct
只是对偏移量计算等操作的简化而已,因此,计算&(*s).f
,不过是计算s + sizeof(s.i)
而已,并不会像其他语言那样导致空指针异常。因为 C 中的
struct
不过是对偏移量计算的简化,可以通过在子类的头部直接放置一个父类成员,之后进行类型转换,就能实现数据成员的继承,示例如下:1
2
3
4
5
6
7
8
9
10
11typedef struct {
int i;
} base_t;
typedef struct {
base_t base;
float f;
} derived_t
derived_t *derived = malloc(sizeof(derived_t));
base_t *base = (base_t *) derived;为了实现函数的继承和多态,则需要函数指针,
this
指针参数,构造器等实现,此处不再赘述。在 C 中,
void *
指向的是未知类型的内存区域,无论按照什么类型理解,都是合法的;并且 malloc 这样的常用调用返回类型只能是void *
,允许自动转换可以大大减少麻烦。在 C++ 中,类型可能是类,而为了实现面向对象编程的特性,在类中保存了一些元信息,因此将一块未知的内存区域作为一个类来理解,这个操作不一定合法;为了保持类和非类指针在语法上的统一,选择了禁止
void *
的自动转换。由于很多全局变量的初始值都需要是零,而在可执行文件中存储这么多零并没有意义,因此 C 语言规定全局变量初值为零,由此可以在生成的可执行文件中只记录全局变量所需空间的大小,而省略为零的值。至于静态局部变量,它的生命周期要求与全局变量相同,只是在编译时赋予了不同作用域的限制而已。为了实现这个特性,在大多数操作系统中,全局变量和静态局部变量被存放在一个全局的数据存储位置(BSS 段),在程序开始执行前由系统进行分配和清零。
局部变量与函数调用相关,在系统调用栈上动态分配,自动初始化将会在运行时不断带来额外负担,因此 C 语言将初始化交给程序员处理,不进行自动初始化。
结语
理解了 C 的实现与设计之后,使用 C 就得心应手了--你了解语言这样设计的原因和目的,也知道自己的一行代码会被翻译成怎样的指令,由此,豁然开朗。
当然,C 有适合的任务,也有不适合的任务,任何语言都是如此,所以复杂性上的欠缺并不影响我对 C 语言的评价。
大道至简,C 的许多设计与 Unix 哲学若合一契。也是因此我对 C/C++ 这种说法不太喜欢。