• Home
  • About
    • Road to Coding photo

      Road to Coding

      只要那一抹笑容尚存, 我便心无旁骛

    • Learn More
    • Email
    • Github
  • Posts
    • All Posts
    • All Tags

<咸鱼书> C的本源

17 May 2018

C++打击有点大, 再翻回C看两眼, 当然只是闲着看, C++还是正事

经常有人说UNIX/Linux的发展历史, C也只是这个时期的一个附属产物, 但是我们今天就来看看C的起源

1. C的起源

首先,在这里致敬C,UNIX伟大的发明者Dennis Ritchie 及 Kenneth Thompson

两位计算机先驱提出了KISS的原则(Keep it simple stupid)

我们在这里也用简洁的语言来组织C的发展历史:

1> C的发展与UNIX息息相关,同期发明,相互依赖

2> 一切的开始是因为Mutics操作系统

3> Mutics失败后, Kenneth想要写一个个人的操作系统, 为了玩游戏,在PDP-7上

4> Ritchie入伙后, 他们为PDP-7编写了可以使用的操作系统UNIX,基于PDP-7 汇编

5> 因为PDP-7 只有8KB内存, Kenneth初始版本是基于解释器的,效率一般

6> 之后Kenneth又写了PDP-11版的UNIX

7> 创造UNIX时, 基于KISS的原则, 所有的软件都由小而简单的组件组成, 吸取了Mutics的教训

8> Ritchie因为解释器效率低, 实现了从BCPL而来的B得进化版,”New B” 基于编译系统, 提高效率

9> 很快, “New B”变成为了, 早期版本的C

C的基础历史就这样,但是他在发展过程中经历了许许多多的问题与困难

同时C引入了类型的概念, 不过是为了支持PDP-11的特性: 支持不同类型字长的数据

2. 早期C体验

C诡异离奇, 缺陷重重, 确取得了巨大成功 –Dennis Ritchie

早期C的体验并不是很好, 它更多的是作为面向编译器设计者的语言, 操作系统设计的语言.

即系统编程语言, 即使到了今天, C在系统编程方面的地位也不是能够轻易被撼动的 !

为了支持系统编程, C支持了一下特性:

  1. 数组下标从0开始, 为了支持偏移量的概念

  2. C语言的基本数据类型与底层硬件对应, 不像Pascal, C11开始才支持复数类型, 浮点数类型也是硬

    件支持后,才加入支持的

  3. auto关键字是摆设, 与静态区, 动态堆区想对应, 栈区的内存是对象缺省内存分配模式,

  4. 表达式数组名可以看作指针, 方便了系统编程者, 然而这个坑点, 这么多年死了多少人 !

  5. float自动扩展为double, 这个没啥意思, 当年扩展, 现在不扩展, 不过从C++的经验来看,

    一般建议直接写double, 两个开销差不了少, 而且double的精度也比较大

  6. register关键字, 摆设, 编译器就不鸟你

< C专家编程 >是本好书, 不过要有自己的观点和看法,上面是基于我自己的看法.

3. 标准I/O库与函数库

C语言本身不提供文件I/O, 都是基于标准函数库的

此处略过

但是, 切记 千万不要使用C预处理器,来修改程序已有结构

4. K&R C

< the C Programming Language > 在计算机历史上留下了浓墨重彩的一笔,

不过K&R C 指的是布莱恩.柯尼汉 + 丹尼斯.里奇 (而非是肯.汤普逊)

K&R C的代码风格以及思想很重要, 可以拜读以下大作,

但是, K&R C中有许多已经被修改掉的特性, 这些特性是时代的眼泪, 被删除也无可厚非

所以, 无论怎么说, 我们如果要研究学习,便是ANSI C (C89)

5. ANSI C

任何一门语言, 在发展过程中都会衍生出许多的变体, 如果不加控制, 就会使得这门语言松散

e.g.: Lisp, Basic就是这样凉凉的

所以, ANSI C 作为标准C就出现了

ANSI C标准对于K&R C做了一定程度上的修改, 在进行语言修改, 细节修订的同时, 保持语言特性及思想

不发生重大变化,维护了K&R C的核心思想,ANSI C 也即C89

而所谓的C90是ISO C

垃圾!

不要添乱—–立即解散ISO工作小组 – 匿名人士

6. K&R C与ANSI C的区别

既然K&R C和ANSI C是的两个重要版本, 那么就说说这两个版本的区别吧, 主要体现在下面四个方面上:

  1. 本质上改变, 就只有原型一个特性

  2. 新的关键字 , 区别不大, 只是添加了一定类型的支持

  3. “Silent Change” 保持原有语言特性不变, 只改变其中一些小细节的实现,

  4. 其他区别, 这些基本上是一般编程者触碰不到的

那么, 我们就来说说上面三个重大区别

6.1 函数原型

函数原型, 在ANSI C之前是没有的, 之前的都叫作, 函数声明, 这个特性是从C++学习过来的

char *strcpy();           // 函数声明

char *strcpy(char *dest, const char *src);       // 函数原型

这是一个重大变化, 主要是用于前置声明

K&R C时,对于函数参数的检查,一直推迟到链接时

而ANSI C使用函数原型, 将参数检查提前到编译时期

同时,进行函数定义时,是这样的:

char *strcpy()
char *dest, const char *src;
{
	...
}

char *strcpy(char *dext, const char *src)
{
	...
}

另外,我们说函数原型的引入主要是针对前置声明

但是, C11之后,C语言风格也发生转变, 与时俱进, 逐渐向C++/Java等面向对象靠拢

事实上, 不刻意强调, 写前置声明的机会已经很少了, Linux4.x版本后的实现可见一斑

6.2 关键字

关键字其实就那么一回事, const, volatile, signed, void其中常用到的也不多

C11后还加入了 _Generic, _Bool(真正意义上实现内置类型Boolen)

但是,说实话,意义不大(摊手), 一般写个C99就歇菜了

6.3 Silent Change

这一处的改变说大不大, 说小不小.不过还是很有意思的

6.3.1 类型转换

举个例子:

关于类型转换的问题, 众所周知, C是强类型,可以进行自动/强制类型转换

然而, 进行数值运算时, 会进行类型转换, 这里ANSI C与K&R C的区别就体现出来了

对于<=int类型的, K&R C转换为无符号类型, ANSI转换为有符号类型

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
	unsigned char i = 4;
	if (-1 < i)
    	printf("ANSI C\n");
	else
		printf("K&R C\n");

	return 0;
}

当然,出现了复杂类型的时候, 比如,int和unsigned int 肯定是符合下述规则的:

执行算术运算时, 如果类型不同, 就会发生转换, 数据类型一般朝着浮点精度更高, 长度更长的方向转换

整型数如果转换为signed不会丢失信息, 就转换为signed, 不然就转换为unsigned

对于, 无符号数和有符号数混用

需要十分注意: 切记不能混用,要不直接不定义无符号,要不全部使用强制类型转换, 否则翻车!

比如下面常见的程序:

int i = -1;
unsigned int j = 34;
if (i < (int)j)               // 否则,等着翻车吧!
	printf("<\n");

6.3.2 相容性

const char *str;
char c = 'r';

str = &c;
*str = 'a';    // error
c = 'a';       // correct


void test(const char **str)
int main(int argc, char **argv)
{
	test(argv);        // error
}

上面的测试函数, 就是典型的相容性问题

对于,str形式参数以及argv实际参数, 他们之间的关系还是类似于赋值的

str与argv是可以传参的, 他们都是char ** ,相容

他们指向的, *str, *argv也是相容的,

但是 **str, **argv(是const的对象) 不相容,

也即是说,*str与*argv本身相容, 但是指向对象不相容,

因此传参操作出现错误.

相容性不能传递

注: 此处的对象,指的是操作的数据,具名内存,并非OOP中的对象

7. #pragma

最后,说个好玩的:

GNU C Complier (GCC)在1.34版本实现中,因为设计师讨厌pragma

所以在有pragma的代码时,做了如操作:

do_pragma()
{
	close(0);
	if (open("/dev/tty",O_RDONLY, 0666) != 0)
		goto nope;
	close(1);
	if (open("/dev/tty", O_WRONLY, 0666) != 1)
		goto nope;
	
	exel("/usr/games/hack", "#pragma", 0);
	exel("/usr/games/rogue", "#pragam", 0);
	exel("/usr/new/emacs", "-f", "hanoi", "9", "-kill", 0);
	exel("/usr/local/emacs", "-f", "hanoi", "9", "-kill", 0);
nope:
	fatal("you are in a maze of twisty complier features, all different");
}

总之, 这位设计师, 就是不想你使用pragma与编译指令, 实际上pragma还是很好用的,它可以给编译器

完成指定操作,比如: 取消结构体对齐, 要求函数内联等等操作, 多使用, 挺好的.

闲了, C看着还是很有意思, 当然主线不能忘了, 不说了, 又要去板砖了

May 17, 2018 12:25 PM



Expert_C_Programming Share Tweet +1