百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程字典 > 正文

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

toyiye 2024-06-21 12:38 8 浏览 0 评论

试想如果你作为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-

相关推荐

为何越来越多的编程语言使用JSON(为什么编程)

JSON是JavascriptObjectNotation的缩写,意思是Javascript对象表示法,是一种易于人类阅读和对编程友好的文本数据传递方法,是JavaScript语言规范定义的一个子...

何时在数据库中使用 JSON(数据库用json格式存储)

在本文中,您将了解何时应考虑将JSON数据类型添加到表中以及何时应避免使用它们。每天?分享?最新?软件?开发?,Devops,敏捷?,测试?以及?项目?管理?最新?,最热门?的?文章?,每天?花?...

MySQL 从零开始:05 数据类型(mysql数据类型有哪些,并举例)

前面的讲解中已经接触到了表的创建,表的创建是对字段的声明,比如:上述语句声明了字段的名称、类型、所占空间、默认值和是否可以为空等信息。其中的int、varchar、char和decimal都...

JSON对象花样进阶(json格式对象)

一、引言在现代Web开发中,JSON(JavaScriptObjectNotation)已经成为数据交换的标准格式。无论是从前端向后端发送数据,还是从后端接收数据,JSON都是不可或缺的一部分。...

深入理解 JSON 和 Form-data(json和formdata提交区别)

在讨论现代网络开发与API设计的语境下,理解客户端和服务器间如何有效且可靠地交换数据变得尤为关键。这里,特别值得关注的是两种主流数据格式:...

JSON 语法(json 语法 priority)

JSON语法是JavaScript语法的子集。JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔花括号保存对象方括号保存数组JS...

JSON语法详解(json的语法规则)

JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔大括号保存对象中括号保存数组注意:json的key是字符串,且必须是双引号,不能是单引号...

MySQL JSON数据类型操作(mysql的json)

概述mysql自5.7.8版本开始,就支持了json结构的数据存储和查询,这表明了mysql也在不断的学习和增加nosql数据库的有点。但mysql毕竟是关系型数据库,在处理json这种非结构化的数据...

JSON的数据模式(json数据格式示例)

像XML模式一样,JSON数据格式也有Schema,这是一个基于JSON格式的规范。JSON模式也以JSON格式编写。它用于验证JSON数据。JSON模式示例以下代码显示了基本的JSON模式。{"...

前端学习——JSON格式详解(后端json格式)

JSON(JavaScriptObjectNotation)是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。它基于JavaScriptProgrammingLa...

什么是 JSON:详解 JSON 及其优势(什么叫json)

现在程序员还有谁不知道JSON吗?无论对于前端还是后端,JSON都是一种常见的数据格式。那么JSON到底是什么呢?JSON的定义...

PostgreSQL JSON 类型:处理结构化数据

PostgreSQL提供JSON类型,以存储结构化数据。JSON是一种开放的数据格式,可用于存储各种类型的值。什么是JSON类型?JSON类型表示JSON(JavaScriptO...

JavaScript:JSON、三种包装类(javascript 包)

JOSN:我们希望可以将一个对象在不同的语言中进行传递,以达到通信的目的,最佳方式就是将一个对象转换为字符串的形式JSON(JavaScriptObjectNotation)-JS的对象表示法...

Python数据分析 只要1分钟 教你玩转JSON 全程干货

Json简介:Json,全名JavaScriptObjectNotation,JSON(JavaScriptObjectNotation(记号、标记))是一种轻量级的数据交换格式。它基于J...

比较一下JSON与XML两种数据格式?(json和xml哪个好)

JSON(JavaScriptObjectNotation)和XML(eXtensibleMarkupLanguage)是在日常开发中比较常用的两种数据格式,它们主要的作用就是用来进行数据的传...

取消回复欢迎 发表评论:

请填写验证码