《C程序设计语言》笔记(二)

时间:2022-05-23 23:33:14

四:函数与程序结构

        1:函数之间的通信可以通过参数、函数返回值以及外部变量进行。

        2:如果函数定义中省略了返回值类型,则默认为int类型。如果没有函数原型,则函数将在第一次出现的表达式中被隐式声明,比如sum += atof(line);那么atof这个函数的返回值将被假定为int类型,但上下文不对其参数做任何假设。

         如果函数声明中不包含参数,比如double atof();   那么编译程序也不会对函数atof的参数做任何假设,并会关闭所有的参数检查。这是为了兼容比较老的C语言程序而做的特殊处理。

 

         3:外部变量与内部变量相比,具有更大的作用域和更长的生存期。自动变量只能在函数内部使用,从其所在的函数被调用时变量开始存在,在函数退出时变量也将消失。而外部变量是永久存在的,它们的值在一次函数调用到下一次函数调用之间保持不变。

 

        4:名字的作用域指的是程序中可以使用该名字的部分。对于在函数开头声明的自动变量来说,其作用域是声明该变量名的函数。函数的参数也是这样的,实际上可以将它看做是局部变量。

        外部变量或函数的作用域从声明它的地方开始,直到所在的文件的末尾结束。

        如果要在外部变量的定义之前使用该变量,或者外部变量的定义与变量的使用不在同一个源文件中,则必须在相应的变量声明中强制性的使用关键字extern

 

         将外部变量的声明与定义严格区分开来很重要。变量声明用于说明变量的属性,而变量定义除此外还将引起存储器的分配。如果将下列语句放在所有函数的外部:

int sp;

double val[100];

         那么这两条语句将定义外部变量sp与val,并为之分配存储单元,同时这两条语句还可以作为该源文件中其余部分的声明。

         而下面的两行语句,为源文件的其余部分声明了一个int类型的外部变量sp以及一个double数组类型的外部变量val,但这两个声明并没有建立变量并为它们分配存储单元:

extern int sp;

extern doubleval[];

 

         5:用static声明限定外部变量与函数,可以将其后声明的对象的作用域限定为被编译源文件的剩余部分。通过static限定外部对象,可以达到隐藏外部对象的目的。如果把函数声明为static类型,则该函数名除了对该函数声明所在的文件可见外,其他文件都无法访问。

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

 

         6:register声明告诉编译器,它所声明的变量在程序中使用频率较高。register变量放在机器的寄存器中,这样可以是程序更小、执行速度更快。但编译器可以忽略此选项。

         register只适用于自动变量以及形参。无论寄存器变量实际上是不是存放在寄存器中,它的地址都是不能访问的。

 

         7:每次进入程序块时,在程序块内声明以及初始化的自动变量都将被初始化。自动变量,包括形参,可以隐藏同名的外部变量和函数但是在一个好的程序设计风格中,应该避免出现变量名隐藏外部作用域中相同名字的情况。

 

         8:对于外部变量和静态变量来说,初始化表达式必须是常量表达式,且只初始化一次,这在程序开始执行前进行初始化。而对于自动变量与寄存器变量来说,初始化表达式可以不是常量表达式,可以是函数调用。

 

         9:初始化数组时,如果省略数组的长度,则编译器将把大括号中的初始化表达式个数作为数组的长度。

如果初始化表达式的个数比数组元素少,则对外部变量、静态变量和自动变量来说,没有初始化表达式的额元素将被初始化为0.如果初始化表达式的个数比数组元素多,则会出错。

         字符数组可以用字符串进行初始化,比如char pattern[] =“ould”;       该语句等价于:

char pattern[] = { 'o', 'u', 'l', 'd', ‘\0’};

 

         10:预处理

         预处理器将进行宏替换、条件编译和包含指定的文件。以“#”开头的命令行就是预处理器处理的对象。这些命令行可以出现在任何地方,其作用可延续到所在翻译单元的末尾。每一行都会单独进行分析预处理过程,在逻辑上可划分为下面几个连续的阶段:

         a:进行三字符序列替换

         三字符组(trigraph)与双字符组(Digraph)是3个或者2个字符的序列,在编译器预扫描源程序时被替换为单个字符。以解决某些键盘不能输入某些编程必须的字符问题。

         C语言的源程序的字符集是基于7位ASCII字符集,是ISO 646-1983 不变代码集的一个超集。因此某些国家的键盘就难以输入C语言的一些运算符

         为解决上述的C语言源代码输入问题,C语言标准规定预处理器在扫描处理C语言源文件时,替换下述的3字符出现为1个字符:

三字符组

替换为

??=

#

??/

\

??'

^

??(

[

??)

]

??!

|

??<

{

??>

}

??-

~

         比如代码:printf("??=\n");将会输出”#”。GCC需要-trigraphs选项,才支持三字符组。但会给出编译警告。

1994年公布了一项C语言标准的修正案,引入了更具有可读性的5个双字符组。这也包括进了C99标准。

双字符组

替换为

<:

[

:>

]

<%

{

%>

}

%:

#

         不同于三字符组在源文件的任何出现都会被预处理器替换,双字符如果出现在字符串字面值、字符常量、程序注释中将不被替换。双字符组的替换发生在编译器对源程序的tokenization阶段(即识别出关键字、标识符等,类似于自然语言的“断词”),仅当双字符组作为一个token或者token的组成部分时(如%:%:被替换为预处理运算符##),双字符组才被替换为单字符。(以上内容出自*)

 

         b:将以反斜杠”\”结尾的指令行中,末尾的”\”,和其后的换行符删除掉,从而可以把若干指令行合并为一行。

 

         c:将程序分成用空白符分隔的记号。注释将被替换为一个空白符。接着执行预处理指令,进行宏扩展。

 

         d:将字符常量和字符串字面值中的转义字符序列,替换为等价字符,然后,把相邻的字符串字面值连接起来。

 

         e:收集必要的程序和数据,并将外部函数和对象的引用与其定义相连接,翻译经过以上处理得到的结果,并进行链接过程。

 

(1):文件包含

         #include“filename”        or    #include<filename>

         如果文件名用引号引起来,则在源文件所在的位置查找该文件;如果在该位置没有找到文件,或者如果文件名使用尖括号<>括起来的,则将根据相应的规则查找该文件。

 

(2):宏替换

         #define 名字 替换文本

         其中,名字与变量名的命名方式相同,替换文本可以是任意字符串。通常#define指令占一行,替换文本是#define指令行尾部的所有剩余内容,但是也可以通过反斜杠\将一个较长的宏定义分成若干行。

         用#define指令定义同一名字是错误的,除非第二次定义的替换文本与第一相同。

         #define指令定义的名字,它的作用域从其定义点开始,到被编译的源文件的末尾处结束。宏定义也可以使用前面出现的宏定义。宏替换对于字符串中的记号不起作用,比如如果YES是通过#define定义过的名字,则在printf(“YES”)中,不执行宏替换。

         可以通过#undef指令,取消名字的宏定义。将#undef用于未知标示符(也就是未用#define指令定义的标示符),并不会导致错误。

 

         a:#将参数字符串化。在替换文本中,如果参数名以”#”作为前缀,则结果将被扩展为:由实际参数替换该参数的带引号的字符串。比如:

         #defineSTR(s)                 #s

        printf(STR(pele)“\n”);            //输出pele

 

         如果实参中有双引号或反斜杠\,则将会替换为\”或\\。所以,替换后的字符串是合法的字符串常量。

 

         b:##是连接符,如果替换文本中的参数与##相邻,则该参数被实际参数替换时,##与前后的空白符都将删除,比如:

         #define paste(front,back)     front##back

         paste(name, 1)将替换为name1

 

         c:注意:凡宏定义里有用'#''##'的地方,宏参数是不会再展开的。

         #defineA                                  2

         #defineSTR(s)                         #s

        #defineCONS(a,b)                   (int)(a##e##b)

 

         printf("stris: %s\n", STR(A));

这行会被展开为:

         printf("stris: %s\n",  “A”);

 

        printf("%s\n",CONS(A, A));  

这一行被展开为:

        printf("%s\n",(int)(AeA));      //编译错误

 

         A不会再被展开,解决这个问题的方法很简单,多加一层中间转换宏。加这层宏的用意是把所有宏的参数在这层里全部展开,那么在转换宏里的那一个宏(_STR)就能得到正确的宏参数。

#defineA                                           2

#define_STR(s)                        #s

#defineSTR(s)                          _STR(s)

#define_CONS(a,b)                         (int)(a##e##b)

#defineCONS(a,b)                  _CONS(a,b) 

 

printf("stris : %s\n",STR(A));         //输出 str is 2

printf("%d\n",CONS(A,A));            //输出:200

 

(3):条件编译

         可以使用条件语句对预处理过程进行控制,条件语句的值是在预处理执行的过程中进行计算。整型常量表达式指的是表达式中的操作数都是整数类型的。

         每个条件编译指令(#if, #elif, #else, #endif)在程序中均独占一行。

 

         #if语句,对其中的常量整形表达式(其中,不能包含sizeof,类型转换运算符或enum常量)进行求值。若该表达式的值不等于0,则包含其后的各行,直到遇到#endif、#elif或#else语句为止。

         在#if中,也可以使用表达式”defined(名字)”或者”defined 名字,如果名字已经定义,则其值为1,否则为0比如为了防止头文件重复包含,可以用下面的形式:

#if!defined(HDR)

#define HDR

...

#endif

         C中,专门定义了两个预处理语句#ifdef#ifndef,因此,上面的例子也可以用这种形式:

#ifndef(HDR)

#define HDR

...

#endif

 

(4):其他

         #line 常量 “文件名”    或           #line 常量

         这样的命令,将使编译器认为:下一行源代码的行号是常量,并且,当前的输入文件名是文件名比如下面的代码,将输出:”the file is hh, line is 100” :

#line 100 "hh"

printf("thefile is %s, line is %d\n", __FILE__,__LINE__);

 

         #error  [用户自定义的错误消息]

         当预处理器预处理到#error命令时,将停止编译并输出用户自定义的错误消息。比如下面的代码:

#ifndef A

#error no defineA

#endif

         在编译时,会输出:”error:#error no define A”

 

        __LINE__                 源文件行数

        __FILE__         源文件名字

        __DATE__        编译日期,形式为”Mmm dd yyyy”,比如Oct 272014

        __TIME__        编译时间,形式为”hh:mm:ss”,比如21:46:19

        __STDC__        整型常量1,只有在遵循标准的实现中,该标示符才被定义为1.

 

五:指针与数组

         1:ANSI C 使用类型void* 代替char *作为通用指针的类型。

         2:地址运算符&,只能应用于内存中的对象,即变量与数组元素。它不能作用于表达式、常量或register类型的变量。

         3:int *ip;      这样声明是为了便于记忆,该声明语句表明表达式*ip的结果是int类型。

double *dp, atof(char*);       该表达式中,*dp和atof都是double类型。

         4:每个指针都必须指向某种特定的数据类型,例外情况是void类型的指针,它可以存放指向任何类型的指针,但是不能对其进行引用操作,也就是说void *vp = &b,*vp = 3;这样的是错误的。

         5:(*ip)++;      该表达式中,括号是必须的,因为*和++运算符具有等同的优先级,但是具有从右向左的结合顺序。如果不加括号,b = *ip++,等价于:b = *ip;  ip++;

 

         6:在计算数组元素a[i]的值时,C语言实际上现将其转换为*(a+1)的形式,然后再求值。因此,这两种形式是等价的。相应的,如果pa是一个指针,那么pa[i] *(pa+i)也是等价的。也就是说:一个通过数组和下标实现的表达式可等价地通过指针和偏移量实现。

         但是,指针和数组名之间有一个不同之处:指针是一个变量,数组名不是变量。

 

         7:在函数定义中,形式参数char s[]char *s是等价的。

         8:如果确信相应的元素存在,则p[-1]、p[-2]这样的表达式在语法上也是合法的。

 

         9:如果p是一个指向数组中某个元素的指针,则p+=i,将对p进行加i的增量运算,使其指向指针p当前所指向元素之后的第i个元素。

 

         10:指针与整数之间不能相互转换,但0是唯一的例外:常数0可以赋值给指针,指针也可以和0进行比较。实际中,常用NULL代替常量0,符号常量NULL定义在<stddef.h>中。

 

         11:如果指针pq指向同一个数组的成员,那么它们之间就可以进行类似于==、!=、 <、 >=的关系比较运算

         但是,指向不同数组的元素的指针之间的算术和比较运算没有定义。

         有效的指针运算包括:相同类型指针之间的赋值运算;指针同整数之间的加法或减法运算;指向相同数组中元素的两个指针的减法或比较运算;将指针赋值为0或指针与0之间的比较运算。其他形式的指针运算都是非法的。

 

         12:字符串常量是一个字符数组。像printf(“hello, world\n”);这样的一个字符串出现在函数中,实际上是通过字符指针访问该字符串的。

        char *pmessage = “hello, world”;pmessage是一个指针,其初值指向一个字符串常量,之后它可以被修改,以指向其他地址,但是不能修改字符串的内容。

 

         13:注意一下strcpy函数实现的进化:

void strcpy(char *s, char *t)

{

         inti;

         i= 0;

        while((s[i]= t[i]) != ‘\0’) i++;

}

 

void strcpy(char *s, char *t)

{

        while((*s= *t) != ‘\0’)

         {

                 s++;

                 t++;

         }

}

 

void strcpy(char *s, char *t)

{

        while((*s++= *t++) != ‘\0’);

}

 

void strcpy(char *s, char *t)

{

         while(*s++= *t++);

}

 

         14:下面的两个表达式是进栈和出栈的标准用法:        *p++ =val;                       val= *--p;

 

         15:判断是否闰年的条件: leap = yeal % 4 ==0 && yeal % 100 !=0 || year % 400 == 0;

 

         16:C语言中,二维数组实际上是一种特殊的一维数组,它的每个元素也是一个一维数组。数组元素按行存储。

         如果将二维数组作为参数传递给函数,那么在函数的参数声明中必须指明数组的列数。比如函数:f(int  daytab[2][13]);  可以写成 f(intdaytab[][13]);  也可以写成f(int(*daytab)[13]); 它表明参数是一个指针,它指向具有13个整型元素的一维数组。

 

         17:下面两个定义:

int a[10][20];

int *b[10];

         从语法角度讲,a[3][4]和b[3][4]都是对一个int对象的合法引用。但a是一个真正的二维数组,他分配了200个int类型长度的存储空间,但是对于b来说,该定义仅仅分配了10个指针。指针数组的一个重要优点在于,数组的每一行长度可以不同,也就是,b的每个元素不必都指向一个具有20个元素的向量,某些元素可以指向具有2个元素的向量,某些元素可以指向具有50个元素的向量。

         a匹配的指针是数组指针:int (*c)[20];

 

         在结构中放置数组,比如: struct s_tag{inta[100];};   这样,可以用赋值语句拷贝整个数组。

 

         18:C声明

         C语言的声明所存在的最大问题是你无法以一种人们所习惯的自然方式从左向右阅读一个声明,在ANSIC引入volatile和const关键字后,情况就更糟糕了。

         

         涉及指针和const的声明可能会出现几种不同的顺序:

const int *grape;

int const *grape;

int * const grape_jelly;

         在最后一种情况下,指针是只读的,而在另外两种情况下,指针所指向的对象是只读的。当然对象和指针有可能都是只读的,下面两种声明方法都能做到这一点:

const int *const grape_jam;

int const *const grape_jam;

 

         声明器(declarator)----它是所有声明的核心。简单地说,声明器就是标识符以及与它组合在一起的任何指针、函数括号、数组下标等,如下图所示。为方便起见,我们把初始化内容(initializosr)也放到里面,并分类表示。

         一个声明由下表所示的各个部分组成(并非所有的组合形式都是合法的)。声明确定了变量的基本类型以及初始值。

 

         C语言声明优先级规则:

         A:声明从它的名字开始读取,然后按照优先级顺序依次读取。

         B:优先级从高到低依次是:

                 B.1:声明中被括号括起来的那部分

                 B.2:后缀操作符:括号 ( ) 表示这是一个函数,而方括号 [ ] 表示这是一个数组。

                 B.3:前缀操作符:星号*表示“指向…的指针”。

         如果const volatlle关健字的后面紧跟类型说明符(intlong),那么它作用于类型说明符。在其他清况下,constvolatile关键字作用于它左边紧邻的指针星号。

 

         用优先级:规则分析C语言声明一例

char * const*(*next)()

适用规则

解释

A

首先,看变量名“next”,并注意到它直接被括号所括住

B.1

所以先把括号里的东西作为一个整体,得出“next是一个指向…的指针”

B

然后考虑括号外面的东西,在星号前缀和括号后缀之间做出选择

B.2

B.2规则告诉我们优先级较高的是右边的函数括号,所以得出“next是一个函数指针,指向一个返回…的函数”

B.3

然后,处理前缀“*”,得出指针所指的内容

C

最后,吧“char *const”解释为指向字符的常量指针

         拼在一起,读作:

“next是一个指向函数的指针,该函数返回另一个指针,该指针指向一个只读的指向char的指针”,大功告成。

 

以上摘自《C专家编程》