c_变量声明和定义(内存)/复杂声明参考/对象与左值/外部变量/自动变量/静态static变量/初始化/预处理/条件包含/类型定义typedef/onst*&*const&const int *pci

reference

  • The C Program Language(2nd Edtion)

C程序和外部对象(变量/函数)

  • C 语言程序可以看成由一系列的外部对象(广义对象)构成,这些外部对象可能是变量或函数

  • 形容词external 与internal 相对的,internal 用于描述定义在函数内部的函数参数及变量。

  • 外部变量定义在函数之外,因此可以在许多函数中使用。

    • 由于C 语言不允许在一个函数中定义其它函数,因此函数本身是“外部的”。
  • 默认情况下,外部变量与函数具有下列性质:通过同一个名字对外部变量的所有引用(即使这种引用来自于单独编译的不同函数)实际上都是引用同一个对象(标准中把这一性质称为外部链接)

  • 在C 语言中,所有变量(变量有多种分类)都必须先声明(但是未必要定义,可以是在外部定义)后使用。

    • 声明通常放在函数起始处,(在任何可执行语句之前)。
    • 声明用于说明变量的属性,它由一个类型名和一个变量表组成,例如:
  • 如果要在外部变量的定义之前使用该变量,或者外部变量的定义变量的使用不在同一个源文件中,则必须在相应的变量声明中强制性地使用关键字extern

  • 外部变量(非局部变量)的声明定义严格区分开来很重要。

定义外部变量

  • 变量声明用于说明变量的属性(主要是变量的类型),而变量定义除此以外还将引起存储器的分配。

    • 如果将下列语句放在所有函数的外部: (这种形式同时完成了变量的声明和定义(分配内存单元))
    • int sp;
    • double val[MAXVAL];
  • 那么这两条语句将定义外部变量sp与val,并为之分配存储单元,同时这两条语句还可以作为该源文件中其余部分的声明。

声明外部变量

  • 下面的两行语句:

    • extern int sp;
    • extern double val[];
  • 为源文件的其余部分声明了一个int 类型的外部变量sp 以及一个double 数组类型的外部变量val(该数组的长度在其它地方确定),但这两个声明并没有建立变量或为它们分配存储单元

组织外部变量

  • 在一个源程序的所有源文件中,一个外部变量只能在某个文件中定义一次,而其它文件可以通过 extern 声明来访问它
    • (定义外部变量的源文件中也可以包含对该外部变量的extern 声明)。
  • 外部变量的定义中必须指定数组的长度,但extern 声明则不一定要指定数组的长度。
  • 外部变量的初始化只能出现在其定义中(执行初始化)

自动变量

  • 函数可以返回基本类型、结构、联合或指针类型的值。
  • 任何函数都可以递归调用。
  • 局部变量通常是“自动的”,即在每次函数调用时重新创建
  • 函数定义可以不是嵌套的,但可以用块结构的方式声明变量。
  • (构成)一个完整C 语言程序的不同函数可以出现(分散)在多个单独编译的不同源文件中。
  • 变量可以只在函数内部有效,也可以在函数外部但仅在一个源文件中有效,还可以在整个程序中都有效。

静态变量(限定变量的作用域)

static 外部变量

  • 某些变量,比如文件stack.c中定义的变量sp与val以及文件getch.c中定义的变量buf 与bufp,它们仅供其所在的源文件中的函数使用,其它函数不能访问。
  • 用static声明限定外部变量与函数,可以将其后声明的对象的作用域限定为被编译源文件剩余部分
  • 通过static 限定外部对象,可以达到隐藏外部对象的目的,
    • 比如,getch-ungetch 复合结构需要共享buf与bufp两个变量,这样buf与bufp必须是外部变量,
    • 但这两个对象不应该被getch与ungetch函数的调用者所访问。

static 内部变量

  • static也可用于声明内部变量。static类型的内部变量同自动变量一样,是某个特定函数的局部变量,只能在该函数中使用,
  • 但它与自动变量不同的是,不管其所在函数是否被调用,它一直存在,而不像自动变量那样,随着所在函数的被调用和退出而存在和消失。
  • 换句话说,static类型的内部变量是一种只能在某个特定函数中使用但一直占据存储空间的变量。

初始化

  • 在不进行显式初始化的情况下,外部变量和静态变量都将被初始化为0,

  • 自动变量和寄存器变量的初值则没有定义(即初值为无用的信息)。

  • 对于外部变量与静态变量来说,初始化表达式必须是常量表达式,且只初始化一次

    • (从概念上讲是在程序开始执行前进行初始化)。
  • 对于自动变量与寄存器变量,则在每次进入函数或程序块时都将被初始化。

    • 对于自动变量与寄存器变量来说,初始化表达式可以不是常量表达式:
      • 表达式中可以包含任意在此表达式之前已经定义的值,包括函数调用,

初始化vs赋值

  • 实际上,自动变量的初始化等效于简写的赋值语句。
    • 究竟采用哪一种形式,还得看个人的习惯。考虑到变量声明中的初始化表达式容易被人忽略,且距使用的位置较远,我们一般使用显式的赋值语句。
数组的初始化
  • 可以在声明的后面紧跟一个初始化表达式列表,初始化表达式列表用花括号括起来,各初始化表达式之间通过逗号分隔。
    • 例如,如果要用一年中各月的天数初始化数组days,其变量的定义如下: int days[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
    • 当省略数组的长度时,编译器将把花括号中初始化表达式的个数作为数组的长度,在本例中数组的长度为12。
    • 如果初始化表达式的个数比数组元索数少,则对外部变量、静态变量和自动变量来说,没有初始化表达式的元素将被初始化为0,
    • 如果初始化表达式的个数比数组元素数多,则是错误的。
    • 不能一次将一个初始化表达式指定给多个数组元素,也不能跳过前面的数组元素而直接初始化后面的数组元素。

初始化字符数组

  • 字符数组的初始化比较特殊:可以用一个字符串来代替用花括号括起来并用逗号分隔的初始化表达式序列。
    • char pattern[] = "ould";
    • 同下面的声明是等价的:
      • char pattern[] = { 'o', 'u', 'l', 'd'};
      • 这种情况下,数组的长度是5(4 个字符加上一个字符串结束符’\0’)。

C 预处理器

  • C 语言通过预处理器提供了一些语言功能。
  • 从概念上讲,预处理器是编译过程中单独执行的第一个步骤。
  • 两个最常用的预处理器指令是:
    • #include 指令(用于在编译期间把指定文件的内容包含进当前文件中)和
    • #define指令(用任意字符序列替代一个标记)。
  • 本节还将介绍预处理器的其它一些特性,如条件编译与带参数的宏。
#include
  • 文件包含指令(即#include指令)使得处理大量的#define指令以及声明更加方便。
  • 在源文件中,任何形如:
    • #include “文件名” 或 #include <文件名>
    • 的行都将被替换为由文件名指定的文件的内容
    • 如果文件名用**引号(“”)**引起来,则在源文件所在位置查找该文件
      • 如果在该位置没有找到文件,
      • 或者如果文件名是用**尖括号<与>**括起来的,
      • 则将根据相应的规则查找该文件,这个规则同具体的实现有关
      • 被包含的文件本身也可包含#include指令。
  • 在大的程序中,#include 指令是将所有声明捆绑在一起的较好的方法。
    • 它保证所有的源文件都具有相同的定义与变量声明,这样可以避免出现一些不必要的错误。
    • 很自然,如果某个包含文件的内容发生了变化,那么所有依赖于该包含文件的源文件都必须重新编译
宏替换

宏定义的形式如下:
#define 名字 替换文本
这是一种最简单的宏替换——后续所有出现名字记号的地方都将被替换为替换文本。

  • #define指令中的名字与变量名的命名方式相同,替换文本可以是任意字符串。

  • 通常情况下,#define指令占一行,替换文本是#define指令行尾部的所有剩余部分内容,但也可以把一

  • 个较长的宏定义分成若干行,这时需要在待续的行末尾加上一个反斜杠符\

  • #define 指令定义的名字的作用域从其定义点开始,到被编译的源文件的末尾处结束。

  • 宏定义中也可以使用前面出现的宏定义。

  • 替换只对记号进行,对括在引号中的字符串不起作用。

    • 例如,如果YES是一个通过#define指令定义过的名字,则在printf("YES")或YESMAN中将不执行替换。
  • 替换文本可以是任意的,例如:

    • #define forever for (;;) /* infinite loop */
    • 该语句为无限循环定义了一个新名字forever。
  • 宏定义也可以带参数,这样可以对不同的宏调用使用不同的替换文本。

  • 例如,下列宏定义定义了一个宏max:

    • #define max(A, B) ((A) > (B) ? (A) : (B))
    • 使用宏max 看起来很像是函数词用,但宏调用直接将替换文本插入到代码中。
    • 形式参数(在此为A或B)的每次出现都将被替换成对应的实际参数。
    • 因此,语句:
      • x = max(p+q, r+s);
      • 将被替换为下列形式:
      • x = ((p+q) > (r+s) ? (p+q) : (r+s));
  • 如果对各种类型的参数的处理是一致的,则可以将同一个宏定义应用于任何数据类型,而无需针对不同的数据类型需要定义不同的max函数。

  • 仔细考虑一下max 的展开式,就会发现它存在一些缺陷。

    • 其中,作为参数的表达式要重复计算两次,如果表达式存在副作用(比如含有自增运算符或输入/输出),则会出现不正确的情况。例如:
      • max(i++, j++) /* WRONG */
      • 它将对每个参数执行两次自增操作。
  • 同时还必须注意,要适当使用圆括号以保证计算次序的正确性。考虑下列宏定义:

    • #define square(x) x * x /* WRONG */
    • 当用squrare(z+1)调用该宏定义时会出现什么情况呢?

宏还是很有价值的。

  • <stdio.h>头文件中有一个很实用的例子:getchar 与putchar 函数在实际中常常被定义为宏,这样可以避免处理字符时调用函数所需的运行时开销。
  • <ctype.h>头文件中定义的函数也常常是通过宏实现的。
#undef
  • 可以通过#undef指令取消名字的宏定义,这样做可以保证后续的调用是函数调用,而不是宏调用:
    • #undef getchar
    • int getchar(void) { ... }
  • 形式参数不能用带引号的字符串替换。
    • 但是,如果在替换文本中,参数名以#作为前缀则结果将被扩展为由实际参数替换该参数的带引号的字符串。例如,可以将它与字符串连接运算结合起来编写一个调试打印宏:
      • #define dprint(expr) printf(#expr " = %g\n", expr)
        • 使用语句 dprint(x/y)
          • 调用该宏时,该宏将被扩展为: printf("x/y" " = &g\n", x/y);
            • 其中的字符串被连接起来了,这样,该宏调用的效果等价于 printf("x/y = &g\n", x/y);
      • 在实际参数中,每个双引号"将被替换为",反斜杠\将被替换为\,因此替换后的字符串是合法的字符串常量。
预处理器运算符##
  • 预处理器运算符##为宏扩展提供了一种连接实际参数的手段。
  • 如果替换文本中的参数与#相邻,则该参数将被实际参数替换,##与前后的空白符将被删除,并对替换后的结果重新扫描。
    • 例如,下面定义的宏paste用于连接两个参数
      • #define paste(front, back) front ## back
      • 因此,宏调用paste(name, 1)的结果将建立记号name1

条件包含

  • 还可以使用条件语句对预处理本身进行控制,这种条件语句的值是在预处理执行的过程中进行计算。
    • 这种方式为在编译过程中根据计算所得的条件值选择性地包含不同代码提供了一种手段。
    • #if语句对其中的常量整型表达式(其中不能包含sizeof、类型转换运算符或enum常量)进行求值,
    • 若该表达式的值不等于0,则包含其后的各行,直到遇到#endif、#elif或#else 语句为止(预处理器语句#elif 类似于else if)。
    • #if 语句中可以使用表达式defined(名字),该表达式的值遵循下列规则:
      • 当名字已经定义时,其值为1;
      • 否则,其值为0。 例如,为了保证hdr.h文件的内容只被包含一次,可以将该文件的内容包含在下列形式的条件语句中:
#if !defined(HDR) 	
#define HDR 
/* hdr.h文件的内容放在这里 */ 
#endif 
  • 第一次包含头文件hdr.h时,将定义名字HDR;
    • 此后再次包含该头文件时,会发现该名字已经定义,这样将直接跳转到#endif处。
    • 类似的方式也可以用来避免多次重复包含同一文件。
    • 如果多个头文件能够一致地使用这种方式,那么,每个头文件都可以将它所依赖的任何头文件包含进来,用户不必考虑和处理头文件之间的各种依赖关系。
    • 下面的这段预处理代码首先测试系统变量SYSTEM,然后根据该变量的值确定包含哪个版本的头文件:
   #if SYSTEM == SYSV 
       #define HDR "sysv.h" 
   #elif SYSTEM == BSD 
       #define HDR "bsd.h" 
   #elif SYSTEM == MSDOS 
       #define HDR "msdos.h" 
   #else 
       #define HDR "default.h" 
   #endif 
   #include HDR *



#ifdef与#ifndef
  • C 语言专门定义了两个预处理语句#ifdef与#ifndef,它们用来测试某个名字是否已经定义。上面有关#if的第一个例子可以改写为下列形式:

    • #ifndef HDR 
      #define EDR 
      /* hdr.h文件的内容放在这里 */ 
      #endif 
      

C_pointer/object in the C language /express in C/

表达式_运算符+数据对象

  • C语言中的对象(数据对象)不同于面向对象语言中的对象概念

数据对象和变量

对象和左值
  • 对象是一个命名的存储区域
  • 左值(lvalue)是引用某个对象的表达式
  • 具有合适类型与存储类的标识符便是左值表达式的一个明显的例子。
    • 某些运算符可以产生左值
      • 例如,如果E是一个指针类型的表达式,*E则是一个左值表达式,它引用由E指向的对象
      • 名字“左值”来源于赋值表达式E1=E2,其中,左操作数E1必须是一个左值表达式。
  • 对每个运算符的讨论需要说明此运算符是否需要一个左值操作数以及它是否产生一个左值。
数据对象

变量和常量是程序处理的两种基本数据对象

  • 对象的类型决定该对象可取值的集合以及可以对该对象执行的操作

  • 声明语句说明变量的名字及类型,也可以指定变量的初值。

  • 运算符指定将要进行的操作。

  • 所有整型都包括signed(带符号)和unsigned(无符号)两种形式,

  • 且可以表示无符号常量与十六进制字符常量。

  • 浮点运算可以以单精度进行,还可以使用更高精度的long double 类型运算。

  • 字符串常量可以在编译时连接。

对象有时也称为变量,它是一个存储位置。

  • 对它的解释依赖于两个主要属性:存储类和类型。
  • 存储类决定了与该标识对象相关联的存储区域的生存期,类型决定了标识对象中值的含义。
  • 名字还具有一个作用域和一个连接。
    • 作用域即程序中可以访问此名字的区域,
    • 连接决定另一作用域中的同一个名字是否指向同一个对象或函数
变量的声明/定义
  • 初始化声明符表中的声明符包含被声明的标识符;
  • 声明说明符由一系列的类型和存储类说明符组成。
  • 声明说明符: (pass)
    • image-20220419194130559

Declarations

  • Declarations specify the interpretation given to each identifier;
  • they do not necessarily reserve storage(预留存储空间) associated with the identifier.
  • Declarations that reserve storage are called definitions.
类型限定符的效果 (basic)
  • 对象的类型可以通过附加的限定符进行限定
  • 声明为const的对象表明此对象的值不可以修改
  • 声明为volatile 的对象表明它具有与优化相关的特殊属性。限定符既不影响对象取值的范围,也不影响其算术属性。
算数类型
整形类型

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

浮点类型

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

派生类型

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

• 给定类型对象的数组
• 返回给定类型对象的函数
• 指向给定类型对象的指针
• 包含一系列不同类型对象的结构
• 可以包含多个不同类型对象中任意一个对象的联合
一般情况下,这些构造对象的方法可以递归使用。

指针生成
通过数组生成
  • 对于某类型T,如果某表达式或子表达式的类型为“T 类型的数组”,则此表达式的值是指向数组中第一个对象指针,并且此表达式的类型将被转换为“指向T 类型的指针”。
其他方式
  • 如果此表达式是一元运算符&或sizeof,则不会进行转换。
  • 类似地,除非表达式被用作&运算符的操作数, 否则,类型为“返回T 类型值的函数”的表达式被转换为“指向返回T 类型值的函数的指针”类型

存储类

存储类分为两类:自动存储类(automatic)和静态存储类(static)。声明对象时使用的一些关键字和声明的上下文共同决定了对象的存储类。

自动存储类
  • 自动存储类对象对于一个程序块来说是局部的,在退出程序块时该对象将消失。
  • 如果没有使用存储类说明符(specifier),或者如果使用了auto限定符,则程序块中的声明生成的都是自动存储类对象。声明为register的对象也是自动存储类对象,并且将被存储在机器的快速寄存器中(如果可能的话)。
存储类声明示例:寄存器变量使用

register声明的形式(自动变量)如下所示:

register int x; 
register char c; 

register声明只适用于自动变量以及函数的形式参数

下面是后一种情况的例子:

   f(register unsigned m, register long n) 
静态存储类
  • 静态对象可以是某个程序块的局部对象,也可以是所有程序块的外部对象
  • 无论是哪一种情况,在退出和再进入函数或程序块时其值将保持不变。
  • 在一个程序块(包括提供函数代码的程序块)内,静态对象用关键字static 声明
  • 在所有程序块外部声明且与函数定义在同一级的对象总是静态的。
  • 可以通过static 关键字将对象声明为某个特定翻译单元的局部对象(不同于自动变量),这种类型的对象将具有内部连接。
  • 省略显式的存储类通过关键字extern 进行声明时,对象对整个程序来说是全局可访问的,并且具有外部连接

类型定义(typedef)

  • C 语言提供了一个称为typedef的功能,它用来建立新的数据类型名,例如,声明
typedef int Length; 

Length定义为与int具有同等意义的名字。类型Length可用于类型声明、类型转换等,它和类型int完全相同,例如:

Length len, maxlen; 
Length *lengths[]; 

类似地,声明

typedef char* String; 

String 定义为与char *或字符指针同义,此后,便可以在类型声明和类型转换中使用
String,例如:

String p, lineptr[MAXLINES], alloc(int); 
int strcmp(String, String); 
p = (String) malloc(100); 

注意,typedef 中声明的类型在变量名的位置出现,而不是紧接在关键字typedef 之后。typedef 在语法上类似于存储类 externstatic 等。我们在这里以大写字母作为typedef定义的类型名的首字母,以示区别。

  • 这里举一个更复杂的例子:用typedef定义本章前面介绍的树节点。如下所示:
 typedef struct tnode *Treeptr; 
 
   typedef struct tnode { /* the tree node: */ 
       char *word;           /* points to the text */ 
       int count;            /* number of occurrences */ 
       struct tnode *left;   /* left child */ 
       struct tnode *right;  /* right child */ 
   } Treenode; 

上述类型定义创建了两个新类型关键字:Treenode(一个结构)和Treeptr(一个指向该结构的指针)。
这样,函数talloc可相应地修改为:

Treeptr talloc(void) 
   { 
       return (Treeptr) malloc(sizeof(Treenode)); 
   }
  • 这里必须强调的是,从任何意义上讲,typedef 声明并没有创建一个新类型,它只是为某个已存在的类型增加了一个新的名称而已。typedef 声明也没有增加任何新的语义;
  • 通过这种方式声明的变量与通过普通声明方式声明的变量具有完全相同的属性。

实际上,typedef 类似于#define 语句,但由于typedef 是由编译器解释的,因此它的文本替换功能要超过预处理器的能力。例如:

typedef int (*PFI)(char *, char *); 

该语句定义了类型PFI 是“一个指向函数的指针,该函数具有两个char *类型的参数,返回值类型为int”,它可用于某些上下文中,例如,可以用在第5 章的排序程序中,如下所示:

PFI strcmp, numcmp; 
  • 除了表达方式更简洁之外,使用typedef还有另外两个重要原因。首先,它可以使程序参数化,以提高程序的可移植性。
  • 如果typedef声明的数据类型同机器有关,那么,当程序移植到其它机器上时,只需改变typedef类型定义就可以了。一个经常用到的情况是,对于各种不同大小的整型值来说,都使用通过typedef 定义的类型名,然后,分别为各个不同的宿主机选择一组合适的short、int 和 long 类型大小即可。
    • 标准库中有一些例子,例如size_tptrdiff_t等。
  • typedef 的第二个作用是为程序提供更好的说明性——Treeptr 类型显然比一个声明为指向复杂结构的指针更容易让人理解。

复杂声明参考

The C(k&R)中有一节专门(5.12)介绍复杂声明,涉及语法分析树
复杂声明用的不多
完整形态的声明包含了许多部分
C 语言的语法力图使声明和使用相一致。对于简单的情况,C 语言的做法是很有效的,
但是,如果情况比较复杂,则容易让人混淆,原因在于,C 语言的声明不能从左至右阅读,而且使用了太多的圆括号

递归下降语法分析程序(示例)

// int *f(); // f: function returning pointer to int
// int (*pf)(); // pf: pointer to function returning int
对以下声明的分析:
// (*pfa[])()
在这里插入图片描述

按照该语法分析,pfa将被识别为一个name,从而被认为是一个direct-dcl。于是,pfa[]也是一个direct-dcl。
接着,*pfa[]被识别为一个dcl,因此,判定(*pfa[])是一个direct-dcl。
再接着,(*pfa[])()被识别为一个direct-dcl,因此也是一个dcl。

声明的形式

declaration-specifiers init-declarator-list_opt;(文法:句型)

  • declaration-specifiers:声明说明符
  • init-declarator-list:初始化声明符表_opt;
  • 相关文法:(产生式Production)
    • 编译原理
declaration-specifiers: 
      storage-class-specifier declaration-specifiers_opt 
      type-specifier declaration-specifiers_opt 
      type-qualifier declaration-specifiers_opt  
init-declarator-list: (非终结符)
      init-declarator 
      init-declarator-list , init-declarator  
init-declarator: 
      declarator 
      declarator = initializer(终结符)  
[declaration-specifiers(声明说明符)]: 
      storage-class-specifier(static/auto/register/...) declaration-specifiers_opt 
      type-specifier(int/float/double/...) declaration-specifiers_opt 
      type-qualifier(const/volatile) declaration-specifiers_opt  
[init-declarator-list]: 
(文法:产生式)
      [init-declarator]
      init-declarator-list , [init-declarator]  
[init-declarator]: 
      declarator (len,num1,..)
      declarator = initializer  (a=123,str1="abc")
  • specifiers说明符(eg.static,registor)

  • identifier标识符(eg. word1,abc,…)

  • qualifier 限定符(eg. const)

  • declarator([dɪ’klærətə] )声明符(与一下几种不同,较为抽象)

  • 从文法分析的递归定义来理解声明说明符(specifier)(opt表示可选)
  • the declaration-specifiers consist of a sequence of type and storage class specifiers

  • image-20220419193112784

声明符(declarator):

  • The declarators in the init-declarator list contain the identifiers(标识符) being declared;
Storage Class Specifiers(@存储类型说明符storage)

The storage class specifiers are:

  • ​ auto
  • ​ register
  • ​ static
  • ​ extern
  • ​ typedef
  • 其中,static,typedef较为常用
The type-specifier(@类型说明符type)

The type-specifiers are :

  • void
  • char
  • short
  • int
  • long
  • float
  • double
  • signed
  • unsigned
  • struct-or-union-specifier
  • enum-specifier
  • typedef-name
  • long和short这两个类型说明符中 最多有一个可同时与int一起使用,并且,在这种情况下省略关键字 int 的含义也是一样的。
  • long 可与double 一起使用。
  • signed 和unsigned这两个类型说明符中最多有一个可同时与int、int的short或long形式、char一起使用。
  • signed 和unsigned 可以单独使用,这种情况下默认为int 类型。
  • signed 说明符对于强制char 对象带符号位非常有用的;其它整型也允许带signed 声明,但这是多余的。
  • 除了上面这些情况之外,在一个声明中最多只能使用一个类型说明符
  • 如果声明中没有类型说明符,则默认为int类型。 类型也可以用限定符限定,以指定被声明对象的特殊属性。
qualifier (@type 类型限定符)

类型限定符包括:

  • const
  • volatile

  • 类型限定符可与任何类型说明符一起使用
  • 可以对const对象进行初始化,但在初始化以后不能进行赋值。
  • volatile对象没有与实现无关的语义。
    说明:const和volatile属性是ANSI标准新增加的特性。const用于声明可以存放在只读存储器中的对象,并可能提高优化的可能性。
    volatile 用于强制某个实现屏蔽可能的优化。

对于具有内存映像输入/输出的机器,指向设备寄存器的指针可以声明为指向volatile 的指针,目的是防止编译器通过指针删除明显多余的引用。
除了诊断显式尝试修改const对象的情况外,编译器可能会忽略这些限定符。

Declarator(@声明符)解释

Meaning of Declarators

  • A list of declarators(声明符列表) appears after a sequence of type and storage class specifiers.
  • Each declarator declares a unique main identifier(标识符), the one that appears as the first alternative of the production for direct-declarator. (direct-declarator(文法)产生式的候选式)
  • The storage class specifiers apply directly to this identifier, but its type depends on the form of its declarator.
  • A declarator is read as an assertion that when its dentifier appears in an expression of the same form as the declarator, it yields an object of the specified type.
  • Considering only the type parts of the declaration specifiers (The C (K&R) index Par. A.8.2) and a particular declarator, a declaration has the form T D, where T is a type and D is a declarator.
    • The type attributed to the identifier in the various forms of declarator is described inductively using this notation.
      • 在各种形式的声明符中归属于标识符的类型是使用这种符号归纳描述的
    • In a declaration T D where D is an unadored identifier, the type of the identifier is T.
    • In a declaration T D where D has the form
  ( D1 ) 
  • then the type of the identifier in D1 is the same as that of D.

  • The parentheses do not alter the type, but may change the binding of complex declarators.

  • 声明符表出现在类型说明符存储类说明符序列之后。
  • 每个声明符声明一个唯一的主标识符,该标识符是直接声明符产生式的第一个候选式
    • 存储类说明符可直接作用于该标识符,但其类型由声明符的形式决定。
    • 当声明符的标识符出现在与该声明符形式相同的表达式中时,该声明符将被作为一个断言,其结果将产生一个指定类型的对象。

声明符的例子(复合声明)

一个粗糙的方式是:从声明的标识符(identifier)为起点开始外分析(结合附近的较高优先级)判定这个声明符的含义:(是指针?数组?函数?基本变量?)

  • 如果(某个步骤中)分析出该)标识符)是个函数,那么下一步分析该函数的参数原型和返回值是什么
  • 如果(某个步骤中)分析出来的(标识符)是个数组,那么下一步分析数组的中的元素是什么类型的
  • 如果(某个步骤)分析出来的是个指针,那么下一步分析指针所指向的类型
char (*(*x())[])() 
    x: function returning pointer to array[] of 
    pointer to function returning char 
  • 从标识符x作为分析的起点,其附近的优先级最高的符号是[ ],那么x就是个函数
  • 该函数x()附是个无参数函数,而且
  • 在分析该函数的返回值,是个*(指针)
  • 该指针附近最亲密(优先级最高的是[ ]),即,该指针指向数组,
  • 该数组的(元素)类型是个*(指针类型)
  • 该指针指向函数(),此函数显然也是个无参数函数,并且返回值是个char型值
char (*(*x[3])())[5] 
   
  • 类似的分析,留作练习( x: array[3] of pointer to function returning pointer to array[5] of char )
指针声明符

在声明T D中,如果D具有下列形式:
* 类型限定符表 D1
且声明T D1中的标识符的类型为“类型修饰符T”,则D中标识符的类型为“类型修饰符 类型限定符表指向T 的指针”。星号*后的限定符作用于指针本身,而不是作用于指针指向的对象。

*const
  • 例子:
  • 下列声明:
  • int i, *pi, *const cpi = &i;//声明了多个变量
    • 声明了一个整型i和一个指向整型的指针pi。
    • 不能修改常量指针cpi的值,该指针总是指向同一位置,但它所指之处的值可以改变。
const *
  • const int ci = 3, *pci; //借助声明语法,一次性声明了三个变量.
    • 整型ci是常量,也不能修改(可以进行初始化,如本例中所示)。
    • pci 的类型是“指向const int 的指针”,pci 本身可以被修改以指向另一个地方,但它所指之处的值不能通过pci赋值来改变
数组声明符

在声明T D中,如果D具有下列形式:
D1[常量表达式opt]
且声明T D1 中标识符的类型是“类型修饰符T”,则D 的标识符类型为“类型修饰符T类型的数组”。

如果存在常量表达式,则该常量表达式必须为整型且值大于0。
若缺少指定数组上界的常量表达式,则该数组类型是不完整类型。

数组可以由算术类型、指针类型、结构类型或联合类型构造而成,也可以由另一个数组构造而成(生成多维数组)。
构造数组的类型必须是完整类型,绝对不能是不完整类型的数组或结构。
也就是说,对于多维数组来说,只有第一维可以缺省。

对于不完整数组类型的对象来说,其类型可以通过对该对象进行另一个完整声明或初始化来使其完整。
例如:
float fa[17], *afp[17];
声明了一个浮点数数组和一个指向浮点数的指针数组,而
static int x3d[3][5][7];
则声明了一个静态的三维整型数组,其大小为3×5×7。
具体来说,x3d是一个由3 个项组成的数组,每个项都是由5 个数组组成的一个数组5 个数组中的每个数组又都是由7 个整型数组成的数组
x3d、x3d[i]、x3d[i][j]3d[i][j][k]都可以合法地出现在某个表达式中。

前三者是数组类型,最后一个是int类型。
更准确地说,x3d[i][j]是一个有7 个整型元素的数组;
x3d[i]则是有5 个元素的数组,而其中的每个元素又是一个具有7 个整型元素的数组。

根据数组下标运算的定义,E1[E2]等价于*(E1+E2)

因此,尽管表达式的形式看上去不对称,但下标运算是可交换的运算。根据适用于运算符+和数组的转换规则,若E1是数组且E2是整数,则E1[E2]代表E1的第E2个成员。

在本例中,x3d[i][j][k]等价于*(x3d[i][j]+k)
第一个子表达式x3d[i][j]将按规则(A7.7)转换为“指向整型数组的指针”类型,这里的加法运算需要乘以整型类型的长度
它遵循下列规则:数组按行存储(最后一维下标变动最快),且声明中的第一维下标决定数组所需的存储区大小,但第一维下标在下标计算时无其它作用。

函数声明符

在新式的函数声明T D中,如果D具有下列形式:

D1(形式参数类型表) 

并且,声明T D1中标识符的类型为“类型修饰符T”,则D的标识符类型是“返回T类型值且具有‘形式参数类型表’中的参数的‘类型修饰符’类型的函数”。

形式参数的语法定义为:
形式参数类型表:
形式参数表
形式参数表, …
形式参数表:
形式参数声明
形式参数表, 形式参数声明
形式参数声明:
声明说明符 声明符
声明说明符 抽象声明符opt

旧式声明
  • 在旧式的函数声明T D中,如果D具有下列形式:
D1(标识符表opt) 

并且声明D1 中的标识符的类型是“类型修饰符T”,则D 的标识符类型为“返回T 类型值且未指定参数的‘类型修饰符’类型的函数”。

形式参数(如果有的话)的形式如下:

标识符表: 
标识符 
    标识符表, 标识符 

声明不提供有关形式参数类型的信息
例如,下列声明:

int f(), *fpi(), (*pfi)(); 
  • 声明了一个返回整型值的函数f、一个返回指向整型的指针的函数fpi 以及一个指向返回整型的函数的指针pfi
  • 它们都没有说明形式参数类型,因此都属于旧式的声明。
新式声明

在这种新式的声明中形式参数表指定了形式参数的类型

这里有一个特例,按照新式方式声明的无形式参数函数声明符也有一个形式参数表,该表仅包含关键字void

  • 如果形式参数表以省略号“, …”结尾,则该函数可接受的实际参数个数比显式说明的形式参数个数要多。详细信息参见A.7.3 节。
  • 如果形式参数类型是数组或函数,按照参数转换规则(参见A.10.1 节),它们将被转换为指针。
  • 形式参数的声明中惟一允许的存储类说明符是register,并且,除非函数定义的开头包括函数声明符,否则该存储类说明符将被忽略。
  • 类似地,如果形式参数声明中的声明符包含标识符,且函数定义的开头没有函数声明符,则该标识符超出了作用域。
    在下列新式的声明中:
int strcpy(char *dest, const char *source), rand(void); 

strcpy是一个返回int类型的函数,它有两个实际参数,第一个实际参数是一个字符指针,第一个实际参数星一个指向常量字符的指针。其中的形式参数名字可以起到注释说明的作用。
第二个函数rand不带参数,且返回类型为int

说明:到目前为止,带形式参数原型的函数声明符是ANSI标准中引入的最重要的一个语言变化。
它们提供了函数调用时的错误检查和参数强制转换,但引入的同时也带来了很多混乱和麻烦,而且还必须兼客这两种形式。
为了保持兼容,就不得不在语法上进行一些处理,即采用void作为新式的无形式参数函数的显式标记。

采用省略号“, …”表示函数变长参数表的做法也是ANSI标准中新引入的,并且,结合标准头文件<stdarg.h>中的一些宏,共同将这个机制正式化了。

更多示例
 
char **argv 
    argv:  pointer to char 
int (*daytab)[13] 
    daytab:  pointer to array[13] of int 
int *daytab[13] 
    daytab:  array[13] of pointer to int 
void *comp() 
    comp: function returning pointer to void 
void (*comp)() 
    comp: pointer to function returning void 
char (*(*x())[])() 
    x: function returning pointer to array[] of 
    pointer to function returning char 
char (*(*x[3])())[5] 
    x: array[3] of pointer to function returning 
    pointer to array[5] of char 

表达式

  • 表达式则把变量与常量组合起来生成新的值。
初等表达式
  • 初等表达式包括
    • 标识符、
    • 常量、
    • 字符串
    • 带括号的表达式。
  • 常量是初等表达式,其类型同其形式有关。
  • 字符串字面值是初等表达式。
    • 它的初始类型为“char类型的数组”类型(对于宽字符字符串,则为“wchar_t类型的数组”类型)
    • 它通常被修改为“指向char类型(或wchar_t类型)的指针”类型,其结果是指向字符串中第一个字符的指针。
      • 某些初始化程序中不进行这样的转换
    • 用括号括起来的表达式是初等表达式,它的类型和值与无括号的表达式相同
    • 此表达式是否是左值不受括号的影响。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值