词法规则

程序由存储在文件中的一个或多个翻译单元(translation unit)组成

程序的翻译分几个阶段完成,翻译的第一阶段完成低级的词法转换,执行以字符#开头的行中的指令,并进行宏定义和宏扩展

在预处理完成后,程序被归约为一个记号序列

<!-- more -->

记号

c中共有6类记号:标识符、关键字、常量、字符串字面值、运算符和其他分隔符

空格符、横向制表符、纵向制表符、换行符、换页符和注释(统称空白符)在程序中仅用来分隔记号,因此将被忽略

相邻标识符、关键字和常量之间需要用空白符来分隔(但标识符、常量、字符串字面值等和运算符直接相连是很常见的)

如果到某一字符为止的输入流被分隔成若干记号,那么下一个记号就是后续字符序列中可能构成记号的最长字符串

注释

‘/’+’’……’’+’/'表示c语言中的注释符号,可以多行,编译程序时完全忽略这部分内容

c99以上标准中的注释符号为单边的//,单行注释

注释出现的位置:文件头,函数,重点语句块前

注释不能够嵌套,也不能出现在字符串字面值、字符字面值中

如main,printf等统称为系统预定义标识符,用户自定义标识符包括变量,常量,函数名等

标识符

标识符也称为名字,可以指代多种实体:函数、结构标记、联合标记和枚举标记;结构成员或联合成员;枚举常量;类型定义名;标号及对象等

标识符还具有一个作用域和一个连接,作用域即程序中可以访问此名字的区域,连接决定另一作用域中的同一个名字是否指向同一个对象或函数

标识符(包括常量、变量的名字等)是由字母和数字组成的序列,但其第一个字符必须为字母

下划线’_'被看作字母,通常用于命名较长的标识符名,提高其可读性

由于库例程(程序的意思,某个系统对外提供的功能接口或服务的集合)名通常以下划线开头,因此自定义标识符不应以下划线开头

区分大写小写字母,传统c程序中变量名使用小写字母,符号常量名全部使用大写字母

标识符可以为任意长度,对内部(内部-局部,外部-全局)标识符而言,编译器至少能识别前31个字符,在某些实现中可能更多,有效的字符数可能更多

内部标识符包括预处理器的宏名和其他所有所有没有外部链接的名字,带有外部链接的标识符限制更严格一些

如对函数和外部变量名而言,编译器能识别的字符数小于31个,由于这两种变量会被汇编程序和加载程序等使用,名字可能在这些其他地方被用掉了

ANSI标准中对外部标识符仅保证能识别前6个字符,并且不区分大小写(以上为c89标准,在c99标准中内部名识别63个,外部名识别31个)

标识符应尽量从字面上表达变量的用途,以防止发生混淆

内部变量一般使用较短变量名(尤其是循环控制变量),而外部变量尽量使用较长名字 */

关键字

如int,return等标识符被保留为关键字(或称系统保留字),不能用于其他用途,并且自定义标识符不能与关键字重名

C语言关键字列表

auto,enum,restrict,unsigned,break,extern,

return,void,case,float,short,Volatile,

char,for,signed,while,const,goto,sizeof,_Bool,continue,if,

static,_Complex,default,inline,struct,_Imaginary,do,int,switch,

double,long,typedef,else,register,union

某些实现还将fortran和asm保留为关键字

关键字const、signed、volatile是在ANSI C中加入的,enum和void是在K&R的第1版书后加入的,这些关键字现在都已被广泛应用

entry曾经被保留为关键字但从未被使用过,现已被剔除

数据类型

c语言数据表示方式:常量(常数、符号常量、常量表达式等),变量(定义变量必须先声明数据类型(变量初始化))

定义不同数据类型的作用:合理分配地址空间,不同数据类型具有不同存储长度,取值范围,允许的操作

数据类型包括:

基本类型 整型,字符型,单/双精度浮点型,空值型

构造(派生)类型 数组,结构,联合,枚举,指针

可以被解释为数字的类型统称为算术类型

由于char类型、各种大小的int类型(无论是否带符号)以及枚举类型都统称为整型类型(integral type)

类型float、double、long double统称为浮点类型(floating type)

此外void类型说明一个值的空集合,它常被用来说明不返回任何值的函数的类型

除基本类型外,还可以通过以下几种方法构造派生类型,从概念上讲派生类型可以有无限多个:

给定类型对象的数组、返回给定类型对象的函数、指向给定类型对象的指针、包含一系列不同类型对象的结构、可以包含多个不同类型对象中任意一个对象的联合

一般情况下构造对象的方法可以递归使用

对象的类型可以通过附加的类型限定符进行限定,声明为const的对象表明此对象的值不可以修改;声明为volatile的对象表明它具有与优化相关的特殊属性

限定符既不影响对象取值的范围,也不影响其算术属性

数据存储

int类型通常代表宿主机器体系架构决定的整数的自然长度,在16位编译器下是2字节,32/64位一般是4字节

long,short为限定符,通常用于限定整型(long也可以修饰long,double等)以提供满足需要的不同长度整型数,除非特别说明int类型都表示带符号数

short类型通常为16位(二进制表示),int既可以16位又可以32位,long至少为32位,long long为64位

当整数太大时,int或short类型可以当作long型处理

(较长的整数至少要占有与较短整数一样的存储空间,但是具体是实现可以使得一般整型与短整型、长整型具有同样的大小)

枚举是一个具有整型值的特殊类型,与每个枚举相关联的是一个命名常量的集合,枚举类型类似于整型

如果每个特定枚举类型的对象赋值不是其常量中的一个或者赋值不是同一个类型的表达式,则编译器通常会产生警告信息

long double表示高精度浮点型,同整型一样,浮点型长度也取决于具体实现

float,double,long double类型可表示相同长度,也可表示不同长度

可以存储在带符号对象中的非负值的集合是可以存储在相应的无符号对象中的值的子集,并且这两个集合的重叠部分的表示是相同的

unsigned,signed可用于限定任意char类型或整型,整型默认是signed(有正负)

unsigned类型的值总是>0(如:unsigned int a=-1;printf("%d",a>0?1:0);输出为1)

unsigned类型遵守模2^n定律(相当于结果右移n位),n为该类型占用的bit位,保证对无符号数的运算永远不会溢出

数据类型取值范围排序:long double > double > float > long long > long > int > short> char

同类型中unsigned > signed

如:char类型占用1字节(8位),则unsigned char取值范围0~255,signed char取值范围-128~127(二进制补码)

对char类型而言取值范围和是否带符号取决于具体机器,但可打印的字符总是正值,声明为char的对象要大到足以存储执行字符集中的任何字符

如果字符集中的某个字符存储在一个char类型对象中则该对象的值等于字符的整型编码值并且是非负值

(以unsigned char声明的无符号字符与普通字符占用同样大小的空间,以signed char显式声明的带符号字符与普通字符也占用同样大小的空间)

声明时int可以省略只保留限定符名short,long,unsigned,signed等

单精度浮点数(float)、双精度浮点数(double)、多精度浮点数(long double)中任何类型都可能是同义的,但精度从前到后是递增的

(老式c语言编译器支持long float,但在c89中就已经被double替换了)

有关类型长度定义的符号常量及其他与编译器和机器有关的属性见标准头文件<limits.h>、<float.h>

void对象

void对象的(不存在的)值不能够以任何方式使用,也不能够被显式或隐式地转换为任一非空类型

因为空(void)表达式表示一个不存在的值,只可以用在不需要值的地方,例如作为一个表达式语句或作为逗号运算符的左操作数(注意左操作数和左值的区别)

如:void func(void)强调函数没有任何参数,(void)0,(void)1,(void*)0,(void*)0x100000(常见于函数参数)

可以通过强制类型转换将表达式转换为void类型,例如在表达式语句中一个空的强制类型转换将丢掉函数调用的返回值,如:(void)func();

指向任何对象的指针都可以转换为void*类型,且不会丢失信息,如果将结果再转换为初始指针类型,则可以恢复初始指针

指针可以被赋值为void类型的指针,也可以赋值给void类型的指针,并可以与void*类型指针进行比较

(ANSI标准特别允许void*类型指针与其他对象指针在赋值表达式和关系表达式中混用,关键是不需要进行强制类型转换)

常量

程序中一般不直接使用常数,可定义宏常量(c中一般所指的常数)或const常量

常量区别于变量的最大的特点是常量不可修改

宏常量定义格式(一般定义在文件头预处理部分) 例:#define PI 3.1415926,宏定义中如果是表达式则一般用括号括起来,否则极容易出错

const定义方法与变量相同,具有数据类型,某些环境下可直接对其进行调试

任何变量的声明都可以使用const限定符修饰,从而指定变量不可修改,

例:const char msg[]=“warning:”;int len(const char[]);

在c的环境下,const是c99新增,并且c99只认定const变量为只读数据不可修改的变量,不是真正的常量,因此不能用于定义数组长度等用途

常量类型:

常量有多种类型,每种类型的常量都有一个数据类型

数值型常量:整型常量,浮点型常量,枚举常量

字符型常量:字符常量,字符串常量,转义字符,符号常量

整型常量分八进制,十进制,十六进制三种数制

浮点型常量为实数在计算机中的表示形式,十进制表示,分小数形式或指数形式(如3.14e-3为3.14*10^-3)

整型常量由一串数字组成,并且可以带有不同的前缀和后缀代表不同的含义:

带有0前缀的常量表示八进制形式;前缀为0x或0X的常量表示十六进制形式,如:十进制31可写成八进制037或十六进制0x1F

带有l、L后缀的常量表示long或long long或double型常量;u,U表示无符号常量;ul,UL表示unsigned long 如:3567l;56u;58924ul

常量可以同时拥有前缀和后缀,如:0xFul

浮点型常量由整数部分、小数点、小数部分、一个e或E、一个可选的带符号整型类型的指数和一个可选的表示类型的后缀组成(即f,F,l或L之一)

整数和小数部分均由数组序列组成,可以没有整数部分或小数部分(但不能两者都没有),同理也可以没有小数点或e和指数部分

浮点常量的类型由后缀确定,F或f后缀表示它是float类型,l或L后缀表明它是long double类型,没有后缀则默认是double类型

字符常量和字符串常量也可以称为单字符常量和多字符常量,字符串常量也称为字符串字面值(string literal)

字符常量为单引号括起的一个字符,按其对应的默认机器字符集(一般为ASCII)中的数值存储,占一个字节,可以像整数一样参与运算

字符常量不包括双引号字符和换行符,可以使用转义字符序列来表示这些字符以及其他一些字符

字符串常量为双引号括起的多个字符,例:China 存储格式为67 104 105 112 97 \0

字符串中使用"表示双引号字符,如:“I am a “string””

编译时可以将多个字符串常量连接起来,如:“hello,”" world"等价于"hello, world",这种连接未将较长的字符串分散开来提供了支持

如果程序试图修改字符串字面值,则行为是未定义的

从技术角度看,字符串常量就是字符数组,存储时加串结束标识符NULL或’\0’,因此字符串物理存储单元数比括在双引号中的字符数多一个

字符串存储类为static,使用给定的字符进行初始化,对相同的字符串字面值是否进行区分取决于具体实现

c中对字符串的长度没有限制,但程序必须扫描整个字符串后才能确定字符串的长度

标准库函数strlen()可返回字符串的长度,但不包括’\0’,标准头文件string.h中声明了strlen()和其他字符串函数

字符常量不同于仅包含一个字符的字符串,如:‘x’和"x",前者是一个整数,后者是字符和’\0’组成的字符数组

枚举常量是一个常量整型值的列表,如:enum escapes{NO,YES};

在没有显示说明的情况下,enum类型中第一个枚举值为0,第二个为1,以此类推

如果只指定了部分枚举成员的值,则未指定成员的值依照最后一个指定值向后递增

如:enum months{JAN=1,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC},其中FEB=2,MAR=3,以此类推

枚举为建立常量值与名字之间的关联提供了一种便利的方式,相对于宏定义其优势在于常量值可以自动生成

可以生成enum类型的变量,编译器会进行类型检查,但不检查这种类型的变量中存储的值是否有效(不会检查值是否属于定义的枚举常量所代表的值)

例:enum color{red,yellow,blue}mycolor=5;(在c中可以通过编译,但是c++不可以)

常量表达式

常量定义为标识符即为符号常量(如宏常量)

常量表达式是仅仅包含常量的表达式,这种表达式在编译时求值而不在运行时求值

可以出现在常量可以出现的任何位置,例:

#define MAXLINE 1000

#define LEAP 1

char line[MAXLINE+1];

int days[31+28+LEAP+31+30+31+30+31+31+30+31+30+31]

转义字符

转义字符为字符的特殊形式,表示不可打印或特殊用途

ANSI C中全部的转义字符序列为:

\0 空字符(null),通常用’\0’形式代替0,以强调某些表达式的字符属性,但其值为空

\a 响铃符\ 反斜杠

\b 回退符? 问号

\f 换页符’ 单引号

\n 换行符" 双引号

\r 回车符 \000 转义八进制数(0开头,数字范围0-7),表示ASCII码等于该值十进制形式的字符

\t 横向制表符 \xhh 转义十六进制数(0x开头,数字0-9,a-f,A-F),表示ASCII码等于该值十进制形式的字符

\v 纵向制表符

例:

#define VTAB ‘\013’ 或 #define VTAB ‘\xb’ 表示将VTAB替换为ASCII纵向制表符(\v的ASCII码为11)

#define BELL ‘\007’ 或 #define BELL ‘\x7’ 表示将BELL替换为ASCII响铃符(\a的ASCII码为7)

转义数字的位数没有限制,但如果字符值超过最大字符值或者字符值不在以上指定的转移字符序列中,则该行为是未定义的

如果转义数字的具体实现中将类型char看作是带符号的,则会对字符值进行符号扩展,类似于强制转换成char类型

扩展字符集

在c的某些实现中,还有一个扩展的字符集,不能用char类型来表示

扩展集中的常量要以一个前导符L开头(例如L’x’),称为宽字符常量,这种常量的类型为wchar_t

w_char_t是一种整型类型,定义在头文件<stddef.h>中

与通常的字符常量一样,宽字符常量可以使用八进制或十六进制转移字符序列

但如果值超过wchar_t可以表示的范围则结果是未定义的

与宽字符常量一样,扩展字符集中的字符串字面值也以前导符L表示,如L"…",宽字符常量字面值的类型为"wchar_t类型的数组"

将普通字符串字面值和宽字符字符串字面值进行连接的行为是未定义的

通常情况下美国和西欧所用的字符集可以用char类型来编码,增加wchar_t的主要目的是为了表示亚洲的语言

变量

变量为程序执行时值可以变动的量,需先定义后使用,变量有时也称为对象,表示一个存储位置

存放变量的地址空间中首单元地址为变量地址,存放内容为变量值

对变量的解释依赖于两个主要属性:存储类和类型,存储类决定了与该标识对象相关联的存储区域生存期,类型决定了标识符对象中值的含义

变量声明

所有变量必须先声明后使用,尽管某些变量可以通过上下文进行隐式声明

注意,如果只定义指针,没有初始化时系统是不会给它分配内存的!!!

一个声明指定一种变量类型,后面跟的变量表可以包含一个或多个该类型的变量,例:

int lower,upper,step=MAX+1;

char c,line[1000];

也可以等价地写成多个变量拆开声明的形式:

int lower; 这种形式虽然需要占用更多的篇幅,但便于向声明语句添加注释和进行修改

int upper;

int step=MAX+1; 声明同时可以对变量进行初始化,右边的表达式充当初始化表达式

char c;

char line[1000];

如果变量不是自动变量(动态局部变量),则只能进行一次初始化操作

从概念上来说是在程序开始运行之前,并且初始化表达式必须为常量表达式

对显式初始化的自动变量,每次进入函数或程序块时都将被自动初始化一次,初始化表达式可以是任何表达式

默认情况下,外部变量与静态变量被初始化为0,未经显式初始化的自动变量值为无效值

变量初始化规则

不进行显式初始化的情况下,外部变量和静态变量都将被自动初始化为0,自动变量和寄存器变量的初值则是没有意义的随机值(没有初始化过程)。定义标量变量时(只能存放单个数值的变量),可以在变量名后紧跟一个等号和一个表达式来初始化变量,如:

int x=1;char squote=’\’;long day=1000L*60L*60L*24L;

定义数组时也分显式初始化和隐式初始化,显式初始化数组元素如:

const int array_size=3;int array[array_size]={1,2,3};

若无显式初始化,则函数体外定义的内置数组,元素均为0;函数体内定义的初始数组,无初始化(随机数填充),这是隐式初始化。

对于外部变量与静态变量来说,初始化表达式必须是常量表达式,且只初始化一次。从概念上讲是程序开始执行前进行初始化(实际还是与机器实现有关),对于自动变量与寄存器变量来说,每次进入函数或程序块时都将进行初始化,初始化表达式可以不是常量表达式,表达式中可以包含任意在此表达式之前已经定义的值,包括函数调用,如:

int binsearch(int x,int v[],int n){int low=0,int high=n-1,int mid=(low+high)/2;}

实际上自动变量的初始化等效于先声明后赋值,究竟采用哪种形式看个人的习惯。

存储类型

存储类分为两类:自动存储类(automatic)和静态存储类(static)

声明对象时使用的一些关键词和声明上下文共同决定了对象的存储类,自动存储类对象对于一个程序块来说是局部的,在退出程序块时该对象消失

如果没有使用存储类说明符或者如果使用了auto限定符,则程序块中的声明生成的都是自动存储类对象

声明为register的对象也是自动存储类的对象,并且将被存储在机器的快速寄存器中(如果可能的话)

静态对象可以是某个程序块的局部对象,也可以是所有程序块的外部对象,无论是哪一种情况在退出和再进入函数或程序块时其值保持不变

在一个程序块(包括提供函数代码的程序块)内,静态对象用关键字static声明,在所程序块外部声明且与函数定义在同一级的对象总是静态的

可以通过static关键字将对象声明为某个特定翻译单元的局部对象,这种类型的对象将具有内部连接

当省略显式的存储类或通过关键字extern进行声明时(在程序块外部),对象对整个程序来说是全局可访问的,并且具有外部连接

对象和左值

对象是一个命名的存储区域,左值(lvalue)是引用某个对象的表达式,具有合适类型与存储类的标识符便是左值表达式的一个明显的例子

某些运算符可以产生左值,例如:如果E是一个指针类型的表达式,*E是一个左值表达式,它引用由E指向的对象

名字“左值”来源于赋值表达式E1=E2,其中左操作数E1必须是一个左值表达式

对每个运算符的讨论需要说明此运算符是否需要一个左值操作数以及它是否产生一个左值

初等表达式

初等表达式是标识符、常量、字符串或带括号的表达式

一个标识符只要是按下面所讨论的方式适当说明的就是初等表达式,其类型由说明指定:

如果一个标识符指定一个对象且其类型是算术、结构、联合或指针类型,那么它是一个左值;

一个常量是一个初等表达式,其类型依赖于它的形式;

一个字符串字面值是一个初等表达式,它的初始类型是char数组类型(对于宽字符字符串,则为wchar_t数组类型),

通常被修改为指向char类型(wchar_t类型)的指针,从而结果是指向字符串中第一个字符的指针;

用括号括起来的表达式是一个初等表达式,它的类型和值与无括号的表达式一致,此表达式是否是左值不受括号的影响

后缀表达式

后缀表达式中的运算符遵循从左到右的结合规则。

后缀表达式的形式主要包括:

1.初等表达式

2.后缀表达式[表达式] 即数组引用形式

带下标的数组由一个后缀表达式后跟一个括在方括号中的表达式来表示。方括号前的后缀表达式的类型必须为“指向T类型的指针”,

其中T为某种类型,方括号中表达式的类型必须为整型,结果得到的下标表达式的类型为T

表达式E1[E2]在定义上等同于*((E1)+(E2))

3.后缀表达式(变元表达式表opt)

函数调用由一个后缀表达式(称为函数标志符)后跟由圆括号括起来的包含一个可能为空的、由逗号分隔的赋值表达式列表组成,这些表达式就是函数的变元

如果后缀表达式包含一个在当前作用域中不存在的标识符,则此标识符将被隐式地声明,等同于在执行此函数调用的最内层程序块中包含下列声明:

extern int 标识符(); 该后缀表达式(在可能的隐式声明和指针生成后)的类型必须为“指向返回T类型的函数的指针”

其中T为某种类型,且函数调用的值的类型为T

(旧语法中在通过指向函数的指针来调用此函数时必须有一个显式的*运算符,ANSI C标准允许现存的一些编泽程序用同样的语法来进行函数调用和

通过指向函数的指针来进行函数调用,旧的语法仍然可用)

变元表达式表形式:

赋值表达式 (注意是编译层次上的赋值表达式即可)

变元表达式表,赋值表达式

术语变元用来表示传递给函数调用的表达式,而术语参数则用来表示由函数定义或函数说明所接收的输入对象(或其标识符),

通常也可用术语“实际变元”和“形式参数”来区分它们

在准备调用函数时,要对它的每个变元进行复制,所有变元传递严格地按值进行

函数可能会改变其参数对象的值(即变元表达式值的拷贝),这个改变不会影响变元的值,但是可以将指针作为变元传递,以使函数可以改变指针所指向的对象的值

函数可以用两种方式说明;在新的方式中,形式参数的类型是作为函数类型的一部分显式指定的,这种说明称为函数原型,而在旧的方式中,参数类型没有说明

在函数调用的作用域中,如果函数是以旧方式声明的则按以下方式对每个实际变元进行缺省变元提升:

对每个整型变元进行整型提升;

将每个float类型的变元转换为double类型;

如果调用时变元的数目与函数定义中参数的数目不等,或者某个变元类型提升后与相应的参数类型不一致,那么函数调用的结果是未定义的

其中类型一致性依赖于函数定义是以新方式进行的还是以旧方式进行的:

如果定义是旧方式的,那么类型一致性检查将在提升过的函数调用的变元类型和提升过的参数类型之间进行;

如果定义是新方式的,那么提升过的实际变元类型必须与没有提升过的形式参数本身的类型一致;

如果在函数调用的作用域中函数说明是以新方式进行的,那么变元将被转换为函数原型中的相应参数类型,就像是赋值一样

变元数目必须与显式说明的参数数目相同,除非函数说明的参数表以省略号(,…)结束,在这种情况下变元的数目必须等于或超过参数的数目;

其后无显式指定类型的参数与之对应的变元要进行缺省变元提升

如果函数定义是以旧方式进行的,那么在调用中可见的原型中的每个变元类型必须与相应函数定义中的参数类型一致(函数定义中的参数类型已进行过变元提升)

(这些规则特别复杂,因为必须要考虑到新旧方式函数的混合使用,应尽可能避免新旧方式混合使用)

变元的求值次序没有指定,不同的编译器的实现方式各不相同,但是在进入函数前变元和函数命名符是完全求值的,包括所有的副作用

对任何函数都可以进行递归调用

4.后缀表达式.标识符

5.后缀表达式->标识符

6.后缀表达式++ / –

c语言运算符表

运算符按照优先级大小由上向下排列,在同一行的运算符具有相同优先级。第二行是所有的一元运算符。

优先级 运算符 名称或含义 使用形式 结合方向 说明
1 [] 数组下标 数组名[常量表达式] 左到右
() 圆括号 (表达式)/函数名(形参表)
. 成员选择(对象) 对象.成员名
-> 成员选择(指针) 对象指针->成员名
2 - 负号运算符 -表达式 右到左 单目运算符
(类型) 强制类型转换 (数据类型)表达式
++ 自增运算符 变量名/变量名 单目运算符
自减运算符 –变量名/变量名– 单目运算符
* 取值运算符 *指针变量 单目运算符
& 取地址运算符 &变量名 单目运算符
! 逻辑非运算符 !表达式 单目运算符
~ 按位取反运算符 ~表达式 单目运算符
sizeof 长度运算符 sizeof(表达式)
3 / 表达式/表达式 左到右 双目运算符
* 表达式*表达式 双目运算符
% 余数(取模) 整型表达式/整型表达式 双目运算符
4 + 表达式+表达式 左到右 双目运算符
- 表达式-表达式 双目运算符
5 << 左移 变量<<表达式 左到右 双目运算符
>> 右移 变量>>表达式 双目运算符
6 > 大于 表达式>表达式 左到右 双目运算符
>= 大于等于 表达式>=表达式 双目运算符
< 小于 表达式<表达式 双目运算符
<= 小于等于 表达式<=表达式 双目运算符
7 == 等于 表达式==表达式 左到右 双目运算符
!= 不等于 表达式!= 表达式 双目运算符
8 & 按位与 表达式&表达式 左到右 双目运算符
9 ^ 按位异或 表达式^表达式 左到右 双目运算符
10 按位或 表达式 表达式
11 && 逻辑与 表达式&&表达式 左到右 双目运算符
12 逻辑或 表达式
13 ?: 条件运算符 表达式1? 表达式2: 表达式3 右到左 三目运算符
14 = 赋值运算符 变量=表达式 右到左
/= 除后赋值 变量/=表达式
*= 乘后赋值 变量*=表达式
%= 取模后赋值 变量%=表达式
+= 加后赋值 变量+=表达式
-= 减后赋值 变量-=表达式
<<= 左移后赋值 变量<<=表达式
>>= 右移后赋值 变量>>=表达式
&= 按位与后赋值 变量&=表达式
^= 按位异或后赋值 变量^=表达式
= 按位或后赋值 变量 =表达式
15 , 逗号运算符 表达式,表达式,… 左到右 从左向右顺序运算

注意+、-、*、&作为一元运算符时优先级高于作为二元运算符的时候

c语言没有定义在表达式求值过程中的溢出、除法检查和其他异常的处理

大多数现有c语言的实现在进行有符号整数表达式的求值时以及在赋值时忽略溢出异常,但并不是所有实现都这样做

对除数为0和所有浮点异常的处理,不同的实现有不同的方式,有时候可以用非标准库函数进行调整

运算符优先级:

优先级指不同运算符之间运算次序,先高后低,共分15级

单目运算符优先级高于双目运算符高于三目运算符

算数运算符高于关系运算符高于逻辑运算符

移位运算符高于关系运算符高于按位逻辑运算符

逗号运算符优先级最低,赋值运算符,各种赋值运算符优先级次低

圆括号优先级最高

运算符结合性

运算符结合性:同优先级的运算符在同一个表达式中,且没有括号的时候,多个运算符与操作数之间的结合方式

如:x+=10+1中,操作数10和1与+结合后的返回结果与x和+=结合

相同优先级下按结合性决定运算顺序

右结合性 单目,三目运算符,赋值运算符

左结合性 其余运算符

运算符的优先级和结合性有明确的规定,但除少数例外情况外表达式的求值次序没有定义,甚至有些有副作用的子表达式也没有定义

因此除非运算符的定义保证了其操作数按某一特定顺序求值,否则具体的实现可以自由选择任意求值次序,甚至可以交换求值次序

每个运算符将其操作数生成的值结合起来的方式与表达式的语法分析方式是兼容的

运算符的优先级与结合性规定了表达式中相邻运算符间的相对运算次序,但是对于操作数而言其求值顺序依赖于具体实现

如:exp1+exp2exp3 可以确定的是运算会先于加法运算,但其中运算数exp2和exp3以及exp1和exp2*exp3的求值顺序

在不同编译器的实现下是不一样的

对于因操作数计算的次序不同产生不同,结果的表达式为带副作用的表达式,在计算时会影响其他操作数的值,引起副作用的运算符为带副作用的运算符

如:x=1,(x+2)*(++x); 第二个表达式在不同的操作数计算顺序下结果是不一样的

x=f()+g(); 其中f()可以在g()之前计算也可以在g()之后计算,因此如果f或g改变了另一个函数所用的变量则x的结果可能依赖于两个函数的计算顺序

(这种情况为保证特定的计算顺序最好将结果保存在中间变量当中)

在c++中规定,先计算逻辑与(&&)和逻辑或(||)的第一个操作数,再计算第二个操作数,以便进行短路求值

条件(?:)、逗号(,)运算符也规定了操作数的计算次序,除此以外,其他运算符没有规定操作数的计算次序,计算次序由具体的编译器决定。

因此在表达式中尽量避免在操作数中引入带副作用的运算符

函数调用、嵌套赋值语句、自增与自减运算符都可能产生“副作用”,对表达式求值的同时,修改了某些变量的值

如:printf("%d %d",++n,power(2,n));编译器会发出警告,结果取决于在不同编译器中n的自增是发生在power调用前还是调用之后

在有这些“副作用”影响的表达式中,执行结果与表达式中的变量被修改的顺序存在着微妙的依赖关系

c对大多数这类问题有意未进行具体规定,表达式会出现什么样的“副作用”由编译器决定,而最佳的求值顺序与机器结构有很大关系

(但是ANSI C明确规定所有对参数的副作用都必须在函数调用之前生效)

无论如何,在任何一种编程语言中,如果代码的执行结果与求值顺序相关,都是不好的程序设计风格

尤其是不知道这些问题在各种机器上是如何解决的,就最好不要尝试运用某种特殊的实现方式

隐式类型转换

c中很多情况下在表达式中会进行隐式的数据类型转换:

一般而言是将"比较窄的"的操作数转换为"比较宽的"操作数,进行不丢失信息的转换,

如int i;char c;i=c,c=i; 此时c的值不变,但反过来就可能发生信息丢失 float x;int i;i=x; 此时x的小数部分被截去后值赋给

整型提升:在一个表达式中凡是可以使用整型的地方都可以使用带符号或无符号的字符、短整型或整型位字段,还可以使用枚举类型的对象

如果原始类型的所有值都可以用int类型表示,则其值将被转换为int类型,如果遇到无法提升的情况(比如short类型)则转换为unsigned int类型

这一过程称为整型提升(integral promotion)

其中将任何整数转换为某种指定的无符号类型数的方法为:以该无符号类型能够表示的最大值+1为模,找出与此整数同余的最小非负数

在二进制补码表示中,如果该无符号类型的位模式较窄,这相当于左截取;如果该无符号类型的位模式较宽,这相当于对带符号值进行符号扩展和对无符号值进行0填充

将任何整数转换为带符号类型时,如果它可以在新类型中表示出来,则其值保持不变,否则它的值同具体实现有关

由于函数调用的参数是表达式,在传递参数时也可能发生类型转换,因此在声明参数时应尽量声明为范围大的类型

同时也需注意类型的使用条件,如不能将下标设置为浮点型等

针对可能导致丢失信息的表达式,编译器可能会发出警告信息,如将long,float赋给int等,但这种表达式并不非法

许多运算符会在运算过程中引起转换,并产生结果类型,其效果是将所有操作数转换为同一公共类型,并以此作为结果类型

这种方式的转换称为普通算术类型转换

如果二元运算符(具有两个操作数)的两个操作数类型不同,则需先把较低类型转换为较高类型,结果返回较高的类型

如果不考虑unsigned类型操作数,表达式中只需要下列非正式规则即可:(优先级按顺序下降)

1.如果其中一个操作数类型为long double,则将另一个操作数转换为long double类型

2.如果其中一个操作数类型为double,则将另一个操作数转换为double类型

3.如果其中一个操作数类型为float,则将另一个操作数转换为float类型

4.将char与short类型的操作数直接自动转为int类型(整型提升)

5.整型提升后,如果任何一个操作数为unsigned long int类型,则将另一个操作数转换为unsigned long int类型

6.如果一个操作数为long int类型且另一个操作数为unsigned int类型,则结果依赖于long int类型是否可以表示所有的unsigned int类型值

如果long int类型不能覆盖所有的unsigned int取值,则将两个操作数都转换为unsigned long int类型

7.如果一个操作数为long int类型,则另一个操作数转换为long int类型

8.如果一个操作数为unsigned int类型,则另一个操作数转换为unsigned int类型

9.最后,如果以上都没有,则将两个操作数转换为int类型

注意,表达式中float类型操作数不会自动转换为double类型

一般而言,数学函数(如<math.h>中定义的函数)使用双精度类型变量

使用float主要为了在使用较大数组时节省存储空间,以及节省执行时间(双精度算术运算很费时),但精度上有所欠缺

当表达式中包括unsigned类型时,转换规则要更复杂,主要原因在于进行的转换是与机器相关的,例:

假定int占16位,long占32位,则-1L<1U,因为此时unsigned int被隐式转换为signed long,

但是-1L>1UL,因为此时signed long被转为unsigned long,即-1在一些情况下可能被转为一个比1大的正整数

(这种情况下long可以覆盖unsigned的取值,因此两数都转换为long即可)

c中没有指定char类型是带符号(signed)还是无符号(unsigned),因此在将char转化为int时其结果可能为负整数

不同机器的结果可能不同,某些机器中,如果char类型最左一位是1,则转换为负整数(符号扩展后,高于1字节的位数全置为1,算术移位),

另一些机器中,在将char转换为int后,在char类型值左边加0(逻辑移位),使转换后的值总是正值

要是转换后的字符总是正值,可以先将char转为unsigned char

c的定义保证机器的标准打印字符集中的字符为非负值,因此在表达式中这些字符总是正的,但是其位模式在某些机器中可能是负的

为保证程序的可移植性,如果要在char类型中存储非字符数据,最好指定signed或unsigned限定符

如果需要将浮点类型的值转换为整型,小数部分将被丢弃,如果结果值不能用整型值表示则其行为是未定义的

特别是将负的浮点数转换为无符号整型的结果是没有定义的;

如果需要将整型值转换为浮点类型时,如果该值在该浮点类型可以表示的范围内但不能精确表示,则结果可能是下一个较高或较低的可表示值

如果该值超出可表示范围,则其行为是未定义的;

将一个精度较低的浮点值转换为相同或更高精度的浮点类型时,它的值保持不变

将一个较高精度的浮点类型值转换为较低精度的浮点类型时,如果它的值在可表示范围内,则结果可能是下一个较高或较低的可表示值

同样的,如果该值超出可表示范围,则其行为是未定义的

ASCII字符集中,大写字母与其对应的小写字母数值之间的间隔是固定的,并且每个字母表是连续的,如a~z,A~Z之间只有字母

但是在EBCDIC字符集中字母就不是连续的

标准头文件<ctype.h>中定义了一组与字符集无关的测试和转换函数,如isdigit©对应于测试语句c>=‘0’&&c<=‘9’

指针和整数

指针可以加上或减去一个整型表达式,在这种情况下整型表达式的转换按照加法运算符的方式进行

两个指向同一数组中同一类型的对象的指针可以进行减法运算,其结果将转换为整型,转换方式按照减法运算符的方式进行;

值为0的整型常量表达式或强制转换为void*类型的表达式,可通过强制转换、赋值或比较操作转换为任意类型的指针,其结果将产生一个空指针

(注意此空指针等于指向同一类型的另一空指针,但不等于任何指向函数或对象的指针)

作为一种良好的编程习惯,很多程序员都不愿意在程序中到处出现未加修饰的0或者其他空指针常量,为了让程序中的空指针使用更加明确从而保持统一的编程风格

专门定义了一个标准预处理宏NULL,其值为“空指针常量”,VC++中定义预处理宏NULL的代码如下:

#ifndef NULL

#ifdef __cplusplus

#define NULL 0

#else

#define NULL ((void*)0)

#endif

#endif

指针可以转换为整型,但此整型必须足够大,所要求的大小和映射函数(转换方式)依赖于具体的实现

整型对象可以显式地转换为指针,这种映射通常是将一个足够宽的从指针转换来的整数转换为同一个指针,其他情况依赖于具体的实现

指向某一类型的指针可以显式地转换为指向另一类型的指针,但是如果该指针指向的对象不满足一定的存储对齐要求,则结果指针可能会导致地址异常

指向某对象的指针可以转换为一个指向具有更小相同存储对齐限制的对象的指针,并可以保证原封不动地再转换回来

“对齐”概念依赖于具体实现,但char类型具有最小的对齐限制

对于某类型T,如果某表达式或子表达式的类型为“T的数组”。那么此表达式的值是指向数组中第一个对象的指针,并且此表达式的类型被转换为“指向T的指针”

如果此表达式是一元运算符&、++、–或sizeof的运算分量,或是赋值类运算符或圆点运算符.的左运算分量.那么转换不会发生

(int a[]={0,10,20};

printf("%p\n",a); 输出:0x7ff7b3a6a36c

printf("%p\n",&a[0]); 输出:0x7ff7b3a6a36c

printf("%p\n",&a); 输出:0x7ff7b3a6a36c

printf("%p\n",a+1); 输出:0x7ff7b3a6a370

printf("%p\n",&a[0]+1); 输出:0x7ff7b3a6a370

printf("%p\n",&a+1); 输出:0x7ff7b3a6a378

可以发现,数组名a、数组名首元素a[0]的地址、数组名a的地址三者的值是一样的,因此变量a存储的地址值即一个指针常量

但是从进行增1运算后的结果来看,a和a[0]都是增加4个字节的长度,但是&a增1后直接位移了12个字节,也就是整个数组的

总长度,说明&运算符和数组名结合后a并不能视作为指针类型,变量类型(符号表中的种属?)表示的是数组类型)

类似地,类型为“返回T的函数”的表达式被转换为类型“指向返回T的函数的指针”,除非此表达式被用作&运算符的运算分量

一个指针可以转换为同类型的另一个指针,但增加或删除了指针所处的对象类型的限定符(比如volatile等)的情况有些特殊:

如果增加了限定符则新指针与原指针等价,不同的是增加了限定符带来的限制

如果删除了限定符,则对底层对象的运算仍受实际声明中的限定符的限制

指向一个函数的指针可以转换为指向另一个函数的指针,调用转换后指针所指函数的结果依赖于具体的实现

但是如果转换后的指针被重新转换为原来的类型,则结果与原来的指针一致

强制类型转换

任何表达式中都可以使用一个称为强制类型转换的一元运算符强制进行显式类型转换,优先级同其他一元运算符,基本格式为:

(类型名)表达式

表达式将按转换规则被转换为类型名指定的类型,即表达式首先被赋值给类型名指定的类型的某个变量,然后再用该变量替换整条语句,

强制类型转换只生成一个指定类型的n的值,n本身的值并没有改变

例:伪随机发生器的函数rand,及初始化随即种子数的函数srand,其中使用了强制类型转换(标准库中包含一个可移植的实现,其中使用了类似方法)

unsigned long int next=1;

   int rand()

   {

    next=next*1103515245+12345;

    return (unsigned int)(next/65536)%32768;

   }

   void srand(unsigned int seed)

   {

    next=seed;

   }