龙空技术网

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

小智雅汇 554

前言:

此刻朋友们对“移位运算符的优先级”大约比较注意,看官们都想要学习一些“移位运算符的优先级”的相关知识。那么小编同时在网上汇集了一些有关“移位运算符的优先级””的相关知识,希望姐妹们能喜欢,各位老铁们快快来学习一下吧!

试想如果你作为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, %s\n", 100, str);

这样的调用, 栈的状态:

重要的是, 无论需要堆积多少个参数, 在返回的过程中总能第一时间找到第一个参数的地址。从图中我们可以看出, 从printf ()的局部变量来看, 第一个参数(指向"%d'%s\n"的指针) 一定存在于距离固定的场所。如果从前往后堆积参数,就肯定不能找到第一个参数。另外,还可以通过第二个参数(上面的100)提供约束其它参数的信息。

对于可变长参数的函数,是不能通过原型声明来校验参数的类型的(是通过几个宏定义的)。另外,函数的执行需要被调用方完全信任调用方传递的参数的正确性。因此,对于使用了可变长参数的函数,调试会经常变得比较麻烦。一般只有在这种情况下,才推荐使用可变长参数的函数:如果不使用可变长参数的函数,程序写起来就会变得困难。

7 不支持模板的C的函数库如何实现泛型?

C是强类型语言,但其函数库的函数总是希望以泛型存在的,但C又不支持函数模板,如何实现类泛型的语法呢?使用void*指针。这也是函数库中一些函数的参数与返回值是void*类型的原因,如qsort()、malloc()、memmove()等。

ANSIC以前的C,因为没有void*这样的类型,所以malloc()返回值的类型就被简单地定义成char*。char*是不能被赋给指向其他类型的指针变量的,因此在使用malloc()的时候,必须要像下面这样将返回值进行强制转型:

book_data_p=CBookData*)malloc(sizeof(BookData));

ANSIC中,malloc()的返回值类型为void*,void*类型的指针可以不强制转型地赋给所有的指针类型变量。因此,像上面的强制转型现在已经不需要了(在一些比较老的编译器中可能需要)。

8 *p++

一些解释认为操作符*和++两者优先级相同,是因为连接规则是从右向左。

根据BNF规则,后置运算符比前置运算符有较高的优先级。

(函数声明符()与数组声明符[]优先级相同)。

9 复合的赋值运算符的运算符为什么要写在“=”号前面

变量x 运算符op = 表达式;

一方面,表面是先做运算符op,然后再做赋值操作。

另一方面,如果写在后面的话,如

x=-y;

会产生歧义,究竟是x=x-y?还是-y赋值给x呢?

10 ++和--运算符的混合写法

x=n++与x=++n,从写的顺序便有隐含计算的顺序:

按就近原则去理解就行。

x=n++; // x=n;n++;

x=++n; // ++n; x=n

11 字符或字符串为什么要使用引号?

如果不使用引用,无法将其与标识符区别开来。

12 数组为什么不能直接整体赋值,而结构体变量却可以?

这当然与编译器对数组与结构体的定义与实现有关,但也有其合理性。

C语言的数组可以初始化,但不能直接赋值,对于字符数组,可以使用strcpy()来复制一个值。

因为数组名是数组首元素的首地址,有常量性质。只能逐个元素赋值。

数组名做为基地址,因为其类型相同,所以可以用递增的数字下标做为索引或偏移。

而结构体变量名却并没有常量性质。

结构体数据虽然也是占据一片连续的内存空间,但其成员的类型可以各不相同,结构体变量是基地址,其偏移通过成员名来标识。

将数组封装到结构体,便可以整体赋值了,到传到函数时,也可以使用sizeof求出其实际长度了。

13 结构体变量的声明

结构体类型的定义,如:

    // 单独定义结构体类型    struct Person{        char name[22];        int age;    };    // 其类型就是struct Person,如同定义一个整形变量一个变量    int i;    struct Person;    // 既然下述代码在整体上是一个类型,所以可以在定义类型的同时来声明和定义变量或指针变量    struct Person{        char name[22];        int age;    }person,*pperson; // 声明和定义变量自然要以";"结尾

也可以用typedef来声明类型标识符

    // 如同声明一个别名类型一样,结构体类型在整体上也可以声明一个表明类型    typedef unsigned int uint;    typedef struct Person{        char name[22];        int age;    }person_t; // 因为使用了typedef,这里的person_t就不再是变量,而是类型了    person_t person1;

也可以不写结构体名称,或使用与类型相同的名称:

    typedef struct{        char name[22];        int age;    }person;         typedef struct person{        char name[22];        int age;    }person; 
14 ASCII编码的规律性

编码肯定不是胡乱编,有其内在的规律性,这种规律性除了就数字、字母编在一起外,如果再从二进制编码去理解,你就可以理解为什么这样编了。

0 (00000000):\0

13(00001101): \n

32(00100000): " "

48(00110000):0

65(01000001): A

97(01100001): a

这样的编码考虑了以2的多少次幂开始,以方便非常运算或运算更有效率。

15 一些函数返回值

strcmp(char* a, char* b),返回负数表示a<b,因为本身比较的就是ASCII,所以比较是逐字符进行的,如a[i]-b[i]

fgets()返回0表示成功,因为0是唯一的表示,其它值表示其它情况。

16 位运算的移位运算符的优先级

位运算的运算对象是位、整数的二进制位。如果不考虑溢出,其左移n位相当于乘2^n。右移n位相当于除2^n(整数运算的结果还是整数,是不考虑其余数的),所以其优先级与算术运算符相当,在算术运算符之后。

(运算符&、^、|的优先级介于关系运算符和逻辑运算符&&、||之间)。

17 switch语句

其中的case提供入口,break提供出口。

18 复杂声明的理解

声明中*、()和[]并不是运算符。在语法规则中,运算符的优先顺序是在别的地方定义的。

18.1 首先着眼于标识符(变量名或者函数名)。

18.2 从距离标识符最近的地方开始,依照优先顺序解释派生类型(数组、函数、指针)。优先顺序说明如下,

18.2.1 用于整理声明内容的括弧()

18.2.2 用于表示数组的[],用于表示函数的()

18.2.3 用于表示指针的*

18.3 解释完成派生类型,使用"of"、"to"、"returning"将它们连接起来。

18.4 最后, 追加数据类型修饰符(在左边, int、double等)。

18.5 英语不好的人, 可以倒序用中文来解释。

其核心在于区分英文与中文的习惯,英文是先表达中心词,而中文是先来一堆修饰。

举例:int (*func_p)(double);

① 首先着眼于标识符。

int (*func_p)(double);

英语的表达为:

func_p is

② 因为存在括号, 这里着眼于*。

int (*func_p)(double);

英语的表达为:

func_p is pointer to

③ 解释用于函数的(), 参数是double。

int (*func_p)(double);

英语的表达为:

func_p is pointer to function(doubl) returning

④ 最后, 解释数据类型修饰符int。

int (*func_p)(double);

英语的表达为:

func_p is pointer to function(double) returning int

⑤ 翻译成中文:

func_p 是指向返回int 的函数的指针。

int hoge;

以下是一些常见实例:

hoge is int

int hoge[lO];

hoge is array(元素个数10) of int

int hoge[10][3];

hoge is array( 元素个数10) of array(元素个数3) of int

int *hoge[10];

hoge is array(元素个数10) of pointer to int

int (*hoge)[3];

hoge is pointer to array( 元素个数3) of int

int func(int a);

func is function(参数为int a)returning int

int (*func) (int a) ;

func is pointer to function( 参数为int a) returning int

int atexit(void (*func)(void));

atexi t is function (func is pointer to function (void) returning void) returning i nt

int* (*func_table[10])(int a);

func_table is a array of pointer to function(参数int a) returning int*

指向返回int* 的函数(参数为int a) 的指针的数组(元素个数10)

如果画成图,可以用这样的链结构来表示:

再看一个复杂的声明:

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-

标签: #移位运算符的优先级