当前位置: 首页 > 随笔 > 正文

pascal语言语法 C|语法的合理性理解和分析试想

作者:admin 发布时间:2023-04-04 07:31:46 分类:随笔 浏览:65


  C|语法的合理性理解和分析

  试想如果你作为C语言或C语言编译器的的设计者,肯定不会任意设置语法规则,除了考虑不能有歧义以外,还会考虑其合理性。

  1 效率是第一位的,安全处于次要位置

  了解C语言“效率第一、安全次之”的原则(因为天才的程序员可以回避掉所有所谓的安全问题),对一些语法设置和编译器行为的合理性理解就豁然开朗了。

  1.1 数组边界不做检查;

  1.2 整数溢出不做检查;

  1.3 对于局部变量不做初始化(Deubg版会初始化一个特定值,如0xcc);

  1.4 输入输出不是语言自身功能的一部分,而是放到了标准库中。

  C以前的很多语言,把输入输出作为语言自身功能的一部分。比如在Pascal 中与C的printf()的功能相当的, 是使用write()这样的标准规范。 它在 Pascal的语法规则中受到了特别对待。

  相对这种方式,C语言将printf()这样的输入输出功能从语言的主体部分分离出来, 让它单纯地成为库函数。 对于编译器来说,printf()函数和其他由普通程序员写的函数并没有什么不同。

  从程序员的角度来看,printf ()操作一下子就完成了。 其实为了完成这个操作, 需要在幕后做诸如向操作系统进行各种各样的请求等非常复杂的处理。C语言并没有把这种复杂的处理放在语言主体部分,而将它们全部规划在 函数库中。

  很多编译型的语言会将被称为 “run-time routine” (运行时例程)的机器码悄悄地” 嵌入到编译(链接)后的程序中, 输入输出这样的功能就是包含在 run-time routine 之中的。C语言基本上没有必须要 “悄悄地” 嵌入运行时的复杂功能*。由于稍微复杂一点的功能被全部规划到了库中, 程序员只需要 去显式地调用函数。

  2 对字符串的操作

  C是只使用标量(scalar)的语言。

  标量就是指char、int、double和枚举型等数值类型,以及指针。相对地,像数组、结构体和共用体这样的将多个标量进行组合的类型,我们称之为聚合类型(aggregate)。

  早期的C语言一度只能使用标量。

  例如:

  char str[]=“abc”;if (str==“abc”)

  这样的代码为什么不能执行预期的动作呢?确实已经将”abc”放到了str中, 条件表达式的值却不为真。这是为什么?

  对于这样的疑问,通常给出的答案是“这个表达式不是在比较字符串的内容, 它只是在比较指针”,其实还可以给出另外一个答案:

  字符串其实就是char类型的数组,也就是说它不是标量,当然在C里面不能用==进行比较了。(包括字符串的合并也不是使用“ ”,而是使用string.h中的库函数,如strcpy();)

  C就是这样的语言,一门”不用说对于输入输出,就连数组和结构体也放弃了通过语言自身进行整合利用” 的语言。

  但是,如今的C (ANSIC)通过以下几个追加的功能,已经能够让我们整合地使用聚合类型了。

  结构体的一次赋值;

  将结构体作为函数返回值传递;

  将结构体作为函数返回值返回;

  auto变量的初始化;

  当然,这些都是非常方便的功能,如今已经可以积极地使用了(不如说应该去使用)。可是在早期的C语言里,它们是不存在的。为了理解C语言的基本原则, 了解早期的C语言也不是什么坏事。

  特别要提出来的是, 即使是ANSIC, 也还不能做到对数组的整合利用。

  将数组赋值给另外一个数组,或者将数组作为参数传递给其他函数等手段,在C语言中是不存在的。

  3 数组下标

  P[i]是*(p i)的简单写法,实际上,至少对于编译器来说,[]这样的运算符完全可以不存在。可是,对于人类来说,*(p i)这种写法在解读上比较困难,写起来也麻烦(键入量大)。因此,C语言引入了[]运算符。就像这样,这些仅仅是为了让人类容易理解而引入的功能,的确可以让我们感受到编程语言的甜蜜味道(容易着手),有时我们称这些功能为语法糖(syntax sugar或者syntactic sugar)。

  4 全局变量与extern

  C编译器使用分开编译的机器,一个大型的应用可以多个程序员合作,大型应用在层层分解出众多的接口后,各自可以去实现一些接口,分开编译,在链接时再链接到一起,所以分散在某一文件中的全局变量、函数可以被其它文件引用。但需要注意的是,链接发生在编译之后,所以需要在使用其它文件中定义的全局变量时,要先用extern声明,以告诉编译器,该符号会在链接时寻找其定义。

  为了在链接器中将名称结合起来, 各目标代码大多都具备一个符号表(symbol table) (详细内容需要依赖实现细节)。

  5 函数调用返回后,栈内存清理了,返回值怎样保存?

  确实,函数调用返回后,栈内存会清理。对于寄存器大小的数据,会保存在寄存器。超过寄存器容量的返回数据,编译器会事先在栈内存中分配一块区域,用来保存返回值。

  6 可变长参数与参数压栈顺序

  大部分的C语言入门书籍往往在一开始就频繁地使用printf ()这个输出文字信息的函数, 利用这个函数, 可以将可变个数的参数填充到字符串中的指定位置。

  C语言的参数是从右往左被堆积在栈中的。

  另外, 在C语言中, 应该是由调用方将参数从栈中除去。顺便提一下,Pascal和Java是从左往右将参数堆积在栈中的。这种方式能够从前面开始对参数进行处理,所以对于程序员来说比较直观。此外, 将参数从栈中除去是被调用方应该承担的工作。大部分情况下, 这种方式的效率还是比较高的。

  为什么C故意采取和Pascal、Java相反的处理方式呢?其实就是为了实现可变长参数这个功能。

  比如, 对于像printf(“%d, %sn”, 100, str);

  这样的调用, 栈的状态:

  再看一个复杂的声明:

  void (*signal(int sig, void (*func)(int)))(int);

  signal is function(sig is int, func is pointer to function(int) returning void) returning pointer to function(int) returning void

  此时, 运用typedef 可以让声明变得格外得简洁。

  typedef void(*sig_t)(int);sig_t signal(int sig, sig_t func);

  sig_t代表”指向信号处理函数的指针” 这个类型。

  19 多维数组的类型

  我们知道,表达式的操作数,赋值运算符两边的左值、右值需要类型一致,除非编译器能够做隐式类型转换,否则会报错。

  如多维数组int arr[3][4][5]是什么类型呢?如果用arr做右值,左值的类型是什么呢?

  在C中,除标识符以外,有时候还必须定义”类型”。

  具体来说,遇到以下情况需定义”类型”

  I 在强制转型运算符中

  II 类型作为sizeof 运算符的操作数

  比如,将强制转型运算符写成下面这样:

  (int*)

  这里指定”int*” 为类型名。

  从标识符的声明中,将标识符取出后,剩下的部分自然就是类型名。

  int hoge; 类型是:int

  int *hoge; 类型是:int *

  double(*p)[3]; 类型是:double(*)[3]

  void(*func)(); 类型是:void(*)()

  同样的问题,对于typedef类型定义,新的类型标识就是标识符,抽出标识符剩下的就是类型定义:

  int func(int){return 0;};typedef int (*funcpT)(int);typedef struct{ int a; double b;}struT;int main(){ funcpT funcp=func; struT str; return 0;}

  回到上面的问题:

  如多维数组int arr[3][4][5]是什么类型呢?如果用arr做右值,左值的类型是什么呢?

  对于数组名,其逻辑含义是数组元素的首地址,逻辑上相当于&arr[0]。

  arr[]指向一个二维数组arr[4][5], 所以其类型是一个特定指针,一个指向一个二维数组arr[4][5]的指针,其类型是int (*)[4][5]。

  int (*arrp)[4][5]=arr;

  用作函数参数时:

  int func(int (*arrp)[4][5], int n);

  或者语法糖的写法:

  int func(int arr[][4][5], int n);

  -End-


标签:in类型语言函数数组


相关推荐

最新推荐

关灯