字符串就是一串以NUL字节结尾的字符。
NUL是作为字符串终止符,它本身并不被看作是字符串的一部分。
字符串常量(string literal)就是源程序中被双引号括起来的一串字符。
例如,字符串常量:"Hello"
printf函数的第一个参数是一个字符串,描述输出的格式,格式常常以字符串常量的形式出现。
格式字符串包含格式指定符(格式代码)以及一些普通字符。这些普通字符将按照原样逐字打印出来,但每个格式指定符将使后续参数的值按照它所指定的格式打印。
NUL是ASCII字符集中'\0'字符的名字,NULL在头文件stdio.h中定义,它是一个值为0的指针。它们都是整形值,其值也相同。
并不存在预定义的符号NUL,所以如果你想使用它而不是字符常量'\0',必须自行定义。
%g:打印一个浮点值;
%ld:长整型值,long;
%lf:双精度实型值,double;
char input[MAX_INPUT]; /*容纳输入行的数组*/
while( gets( input ) != NULL )
gets函数从标准输入读取一行文本并把它存储于作为参数传递给它的数组中。
一行输入又一串字符组成,以一个换行符(newline)结尾。gets函数丢弃换行符,并在该行的末尾存储一个NUL字节('\0')。然后,gets函数返回一个非NULL值,表示该行已被成功读取。
假如一个程序的源代码又几个源文件所组成,那么使用该函数的源文件都必须写明该函数的原型。把原型放在头文件中并使用#include指令包含它们,可以避免由于同一个声明的多份拷贝而导致的维护性问题。
stdlib.h定义了EXIT_SUCCESS和EXIT_FAILURE符号;
如果你有一些声明需要用于几个不同的源文件,方法:你在一个单独的文件中编写这些声明,然后用#include指令把这个文件包含到需要使用这些声明的源文件中。这样,你就只需要这些声明的一份拷贝,用不着在许多不同的地方进行复制,避免了在维护这些代码时出现错误的可能性。
于处理器读入源代码,根据预处理指令对其进行修改,然后把修改过的源代码递交给编译器。
scanf("%d",&columns[num])
格式码%d表示需要读取一个整形值。字符是从标准输入读取,前导空白将被跳过。然后这些数字被转换为一个整数,结果存储于指定的数组元素中。
scanf函数转换了一个整数之后,它就返回1这个值;
标准并未硬性规定C编译器对数组下标的有效性进行检查,而且绝大多数C编译器确实也不进行检查。因此,如果你需要进行数组下标的有效性检查,你必须自行编写代码。如果此处不进行num<max这个测试,而且程序所读取的文件包含超过20个列标号,那么多出来的值就会存储在紧随数组之后的内存位置,这样就会破坏原先存储在这个位置的数据,可能是其他变量,也可以是函数的返回地址。
puts函数把指定的字符串写到标准输出并在末尾添上一个换行符;
int ch;
ch=getchar();
为什么ch被声明为整形,而我们事实上需要它来读取字符?答案是EOF是一个整形值,它的位数比字符类型要多,把ch声明为整形可以防止从输入读取的字符意外地被解释为EOF。但同时,这也意味着接收字符的ch必须足够大,足以容纳EOF,这就是ch使用整形值的原因。
字符只是小整数而已,所以用一个整形变量容纳字符值并不会引起任何问题;
while( (ch = getchar()) != EOF && ch != '\n' )
;
这个while循环的循环体没有任何语句。仅仅完成while表达式的测试部分就足以达到我们的目的,所以循环体就无事可干。while语句之后的单独一个分号称为空语句,它就是应用于目前这个场合,也就是语法要求这个地方出现一条语句但又无需执行任何任务的时候。这个分号独占一行,这就是为了防止读者错误地以为接下来的语句也是循环体的一部分。
putchar函数,它接受一个整形参数,并在标准输出中打印该字符(字符在本质上也是整形)
在字符串内进行搜索的函数是strchr,它接受两个参数,第1个参数是字符串,第2个参数是一个字符。这个函数在字符串参数内搜索字符参数第1次出现的位置,如果搜索成功就返回指向这个位置的指针,如果搜索失败就返回一个NULL指针。
strstr函数的功能类似,但它的第2个参数也是一个字符串,它搜索第2个字符串在第1个字符串中第1次出现的位置。
在#include文件中放置函数原型;
在ANSI C的任何一种实现中,存在两种不同的环境。
翻译环境:源代码被转换为可执行的机器指令。
执行环境:用于实际执行代码。
标准明确说明,这两种环境不必位于同一台机器上。
例如,交叉编译器就是在一台机器上运行,但它所产生的可执行代码运行于不同类型的机器上。
翻译阶段由几个步骤组成,组成一个程序的每个(有可能有多个)源文件通过编译过程分别转换为目标代码(object code)。然后,各个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序。链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它也可以搜索程序员个人的程序库,将其中需要使用的函数也链接到程序中。
编译过程本身也由几个阶段组成,
首先是预处理器(preprocessor)处理。这个阶段,预处理器在源代码上执行一些文本操作。例如,用实际值代替由#define指令定义的符号以及读入由#include指令高喊的文件内容。
然后,源代码经过解析(parse),判断它的语句的意思。第2个阶段是产生绝大多数错误和警告信息的地方。
随后,便产生目标代码。目标代码是机器指令的初步形式,用于实现程序的语句。
在缺省情况下,链接器在标准C函数库中查找。如果在编译时加上"-lname"标志,链接器就会同时在"name"的函数库中进行查找。这个选项应该出现在命令行的最后。
程序的执行过程也需要经历几个阶段。首先,程序必须再入到内存中。在宿主环境中(也就是具有操作系统的环境),这个任务由操作系统完成。那些不是存储在堆栈中的尚未初始化的变量将在这个时候得到初始值。
然后,便开始执行程序代码。在绝大多数机器里,程序将使用一个运行时堆栈(stack),它用于存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程中将一直保留它们的值。
打印调试信息:
printf("%s-%d\n",__FILE__,__LINE__);
在整数字面值后面添加字符L或l(这是字母l),可以使这个整数被解释为long整形值,字符U或u则用于把数值指定为unsigned整形值。如果在一个字面值后面添加这两组字符中的各一个,那么它就被解释为unsigned long整形值。
枚举类型的声明:
enum Jar_Type { CUP, PINT, QUART, HALF_GALLON, GALLON };
这条语句声明了一个类型,称为Jar_Type。
这种类型的变量按下列方式声明:
enum Jar_Type milk_jug, gas_can, medicine_bottle;
如果某种特别的枚举类型的变量只使用一个声明,你可以把上面两条语句组合成:
enum { CUP, PINT, QUART, HALF_GALLON, GALLON }
milk_jug, gas_can, medicine_bottle;
编译器负责把变量赋值给计算机内存中的位置;
事实上,当一个函数每次被调用时,它的自动变量(局部变量)可能每次分配的内存位置都不相同。
许多人对C语言不存在字符串类型感到奇怪,不过C语言提供了字符串常量。事实上,C语言存在字符串的概念:一串以NUL字节结尾的零个或多个字符。字符串通常存储在字符数组中,这也是C语言没有显示的字符串类型的原因。之所以选择NUL作为字符串的终止符,是因为它不是一个可打印的字符。
字符串常量可以是空的,如:"",即使是空字符串,依然存在作为终止符的NUL字节。
ANSI C声明如果对一个字符串常量进行修改,其效果是未定义的。许多ANSI编译器不允许修改字符串常量。如果你需要修改字符串,请把它存储于数组中。
当一个字符串常量出现于一个表达式中时,表达式所使用的值就是这些字符所存储的地址,而不是这些字符本身。因此,你可以把字符串常量赋值给一个"指向字符的指针",后者指向这些字符所存储的地址。但是,你不能把字符串常量赋值给一个字符数组,因为字符串常量的直接值是一个指针,而不是这些字符本身。
C数组另一个值得关注的地方是,编译器并不检查程序对数组下标的引用是否在数组的合法范围之内;
typedef允许你为各种数据类型定义新名字。typedef声明的写法和普通的声明基本相同,只是把typedef这个关键字出现在声明的前面。例如,下面这个声明:
char *prt_to_char;
把变量prt_to_char声明为一个指向字符的指针。
但是在你添加关键字typedef后,声明变为:
typedef char *ptr_to_char;
这个声明把标识符ptr_to_char作为指向字符的指针类型的新名字。
你可以像使用任何预定义名字一样在下面的声明中使用这个新名字。例如:
prt_to_char a;
声明a是一个指向字符的指针。
提示:你应该使用typedef而不是#define来创建新的类型名,因为后者无法正确处理指针类型。例如:
#define d_ptr_to_char char *;
d_ptr_to_char a,b;
正确地声明了a,但是b却被声明为一个字符。
在定义更为复杂的类型名字时,如函数指针或指向数组的指针,使用typedef更为合适;
const关键字声明的常量如何在一开始就拥有一个值?
法1:可以在声明时对它进行初始化,如:int const a = 15;
法2:在函数中声明为const的形参在函数被调用时会得到实参的值;
#define指令是另一种创建名字常量的机制。例如,下面这两个声明都为50这个值创建了名字常量。
#define MAX_ELEMENTS 50
int const max_elements = 50;
在这种情况下,使用#define比使用const变量更好。因为只要允许使用字面值常量的地方都可以使用前者,比如声明数组的长度。const变量只能用于允许使用变量的地方。
名字常量非常有用,因为它们可以给数值起符号名,否则它们就只能写成字面值的形式。用名字常量定义数组的长度或限制循环的计数器能够提高程序的可维护性————如果一个值必须修改,只需要修改声明就可以了。修改一个声明比搜索整个程序修改字面值常量的所有实例要容易得多。
有三个地方可以用于存储变量:普通内存、运行时堆栈、硬件寄存器。
变量的缺省存储类型取决于它的声明位置。凡是在任何代码块之外声明的变量总是存储于静态内存中,也就是不属于堆栈的内存,这类变量称为静态(static)变量。
静态变量在程序运行之前创建,在程序的整个执行期间始终存在。它始终保持原先的值,除非给它赋一个不同的值或者程序结束。
在代码块内部声明的变量的缺省存储类型是自动的(automatic),也就是说它存储于堆栈中,称为自动(auto)变量。有一个关键字auto就是用于修饰这种存储类型的,但它极少使用,因为代码块中的变量在缺省情况下就是自动变量。
在程序执行到声明自动变量的代码块时,自动变量才被创建,当程序的执行流离开代码块时,这些自动变量便自行销毁。
在静态变量的初始化中,我们可以把可执行程序文件想要初始化的值放在当程序执行时变量将会使用的位置。当可执行文件载入到内存时,这个已经保存了正确初始值的位置将赋值给那个变量。如果不显示地指定其初始值,静态变量将初始化为0;
对于在代码块内部声明的变量,如果给它加上关键字static,可以使它的存储类型从自动变为静态。具有静态存储类型的变量在整个程序执行过程中一直存在,而不仅仅子啊声明它的代码块的执行时存在。注意,修改变量的存储类型并不表示修改该变量的作用域,它仍然只能在该代码块内部按名字访问。
函数的形式参数不能声明为静态,因为实参总是在堆栈中传递给函数,用于支持递归。
关键字register可以用于自动变量的声明,提示它们应该存储于机器的硬件寄存器而不是内存中,这类变量称为寄存器变量。
自动变量和静态变量的初始化存在一个重要的差别:
在静态变量的初始化中,我们可以把可执行程序文件想要初始化的值放在当程序执行时变量将会使用的位置。当可执行文件载入到内存时,这个已经保存了正确初始值的位置将赋值给那个变量。如果不显示地指定其初始值,静态变量将初始化为0。
自动变量的初始化需要更多的开销,因为当程序链接时还无法判断自动变量的存储位置。事实上,函数的局部变量在函数的每次调用中可能占据不同的位置。基于这个理由,自动变量没有缺省的初始值;
重点static关键字:
当它用于函数定义时,或用于代码块之外的变量声明时,static关键字用于修改标识符的链接属性,从external改为internal,但标识符的存储类型和作用域不受影响。用这种方式声明的函数或变量只能在声明它们的源文件中访问。
当它用于代码块内部的变量声明时,static关键字用于修改变量的存储类型,从自动变量修改为静态变量,但变量的链接属性和作用域不受影响。用这种方式声明的变量在程序执行之前创建,并在程序的整个执行期间一直存在,而不是每次在代码块开始执行时创建,在代码块执行完毕后销毁。
属于文件作用域的声明在缺省情况下为external链接属性;
int d(int e)
{
...
}
int f()
{
...
}
函数d不可以调用函数i,因为在d之前不存在i的原型。
具有external链接属性的实体在其它语言的术语里称为全局(global)实体,所有源文件中的所有函数均可以访问它。只要变量并非声明于代码块或函数定义内部,它在缺省情况下的链接属性即为external。如果一个变量声明于代码块内部,在它前面添加extern关键字将使它所引用的是全局变量而非局部变量。
具有external链接属性的实体总是具有静态存储类型。
不要把整形值和枚举值混在一起使用
在定义类型的新名字时,使用typedef而不是#define
用const声明其值不会修改的变量
空语句:本身只包含一个分号;
break和continue语句:
在while循环中可以使用break语句,用于永久终止循环。在执行完break语句之后,执行流下一条执行的语句就是循环正常结束后应该执行的那条语句。
在while循环中也可以使用continue语句,它用于永久终止当前的那次循环。在执行完continue语句之后,执行流接下来就是重新测试表达式的值,决定是否继续执行循环。
这两条语句的任何一条如果出现于嵌套的循环内部,它只对最内层的循环起作用,你无法使用break或continue语句影响外层循环的执行。
如果循环体内执行了continue语句,循环体内的剩余部分便不再执行,而是立即开始下一轮循环。
当循环体只有遇到某些值才会执行的情况下,continue语句相当有用:
while( (ch = getchar()) != EOF )
{
if( ch < '0' || ch > '9')
continue;
/*process only the digits*/
}
for语句如果省略条件部分,表示测试的值始终为真;
在for语句中也可以使用break语句和continue语句。break语句立即退出循环,而continue语句把控制流直接转移到调整部分。
把一个数组的所有元素初始化为0:
for( i = 0; i < MAX_SIZE; i += 1 )
array[i] = 0;
当你需要循环体至少执行一次时,选择do语句;
常量表达式(constant-expression)是指在编译期间进行求值的表达式;
switch( expression ),其中expression的结果必须是整形值;
goto 语句标签;
要使用goto语句,你必须在你希望跳转的语句前面加上语句标签。
语句标签就是标识符后面加个冒号。
在一种情况下,即使是结构良好的程序,使用goto语句也可能非常合适————就是跳出多层嵌套的循环。由于break语句只影响包围它的最内层循环,要想立即从深层嵌套的循环中退出只有使用一个办法,就是使用goto语句。
for语句是while循环的一种常用组合形式的速记写法。
当需要出现一条语句但并不需要执行任何任务时,可以使用空语句。
设置和清除变量值中单个位:
value |= 1 << bit_number;
value &= ~ ( 1 << bit_number );
sizeof操作符判断它的操作数的类型长度,以字节为单位。操作数既可以是个表达式(常常是单个变量),也可以是两边加上括号的类型名。如:
sizeof ( int ) sizeof x
sizeof的操作数是个数组名时,它返回该数组的长度,以字节为单位;
判断表达式的长度并不需要对表达式进行求值,所以,sizeof( a=b+1 )并没有向a赋任何值;
前缀和后缀形式的增值操作符都复制一份变量值的拷贝。用于周围表达式的值正是这份拷贝。这些操作符的结果不是被它们所修改的变量,而是变量值的拷贝,认识这一点非常重要。它之所以重要是因为它解释了你为什么不能像下面这样使用这些操作符:
++a = 10;
++a的结果是a值的拷贝,并不是变量本身,你无法向一个值进行赋值。
在C中,你必须使用双等于号==来比较相等性;
b = a > 5 ? 3 : -20;
.和->操作符用于访问一个结构的成员。
C并不具备显式的布尔类型,所以使用整数来代替。
避免混合使用整形值和布尔值。如果一个变量包含了一个任意的整形值,你应该显示地对它进行测试:
if( value != 0 )
不要使用简写法来测试变量是零还是非零,因为这类形式错误地暗示该变量在本质上是布尔型的。
寻常算数转换:
long double
double
float
unsigned long int
long int
unsigned int
int
记住,左值意味着一个位置,而右值意味着一个值。
~(按位取反)
^(异或操作)
有符号值的右移位操作是不可移植的。
使用条件操作符替代if语句以简化表达式。
使用复合赋值符可以使程序更易于维护。
有一点非常重要,你必须记住,名字与内存位置之间的关联并不是硬件所提供的,它是由编译器为我们实现的。所有这些变量给了我们一种更方便的方法记住地址————硬件仍然通过地址访问内存位置。
int *a;
如果变量是静态的,它会被初始化为0;但如果变量是自动的,它根本不会被初始化;
标准定义了NULL指针,表示不指向任何东西。要使一个指针变量为NULL,你可以给它赋一个零值。为了测试一个指针变量是否为NULL,你可以将它与零值进行比较。
对所有的指针变量进行显式的初始化是种好做法。如果你已经知道指针将被初始化为什么地址,就把它初始化为该地址,否则就把它初始化为NULL。
间接访问操作(*)只能作用于指针类型表达式
*100 = 25;(错,因为100为整形)
*(int *)100 = 25;(OK,强制类型转换把值100从"整形"转换为"指向整形的指针")
这个技巧唯一有用之处是你偶尔需要通过地址访问内存中某个特定的位置,它并不是用于访问某个变量,而是访问硬件本身。
指针变量和其他变量一样,占据内存中某个特定的位置,所以用&操作符得到它的地址是合法的!
当一个指针和一个整数量执行算数运算时,整数在执行加法运算前始终会根据合适的大小进行调整。这个"合适的大小"就是指针所指向类型的大小,"调整"就是把整数值和"合适的大小"相乘。
让指针指向数组最后一个元素后面的那个位置是合法的,但对这个指针执行间接访问可能会失败;
两个指针相减的结果的类型是ptrdiff_t,它是一种有符号整数类型。减法运算的值是两个指针在内存中的距离,因为减法运算的结果将除以数组元素类型的长度。
例如,float a[];数组元素的类型为float,每个元素占据4个字节的内存空间。如果数组的起始位置为1000,指针p1的值是1004,p2的值是1024,但表达式p2-p1的结果将是5,因为两个指针的差值(20)将除以每个元素的长度(4)。
标准允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针进行比较,但不允许与指向数组第一个元素之前的那个内存位置的指针进行比较。
指针就是它的值表示内存地址的变量;
在对指针执行间接访问前,指针必须进行初始化;
对未初始化的指针变量执行间接访问操作是非法的;
在极少见的情况下,我们偶尔需要使用指针常量,这时我们可以通过把一个整形值强制转化为指针类型来创建它。
指针运算只有作用于数组中其结果才是可以预测的。对任何并非指向数组元素的指针执行算数运算是非法的。如果一个指针减去一个整数后,运算结果产生的指针所指向位置在数组第一个元素之前,那么它也是非法的。加法运算稍有不同,如果结果指针指向数组最后一个元素后面的那个内存位置仍是合法的(但不能对这个指针执行间接访问操作),不过再往后就不合法了。
指针减法的结果经过调整(除以数组元素类型的长度),表示两个指针在数组中相隔多少个元素。
向编译器提供一些关于函数的特定信息显然更为安全,我们可以通过两种方法来实现。首先,如果同一源文件的前面已经出现了该函数的定义,编译器就会记住它的参数数量和类型,以及函数的返回值类型。接着,编译器便可以检查该函数的所有后续调用(在同一个源文件中),确保它们是正确的。
第二种向编译器提供函数信息的方法是使用函数原型(function prototype)。原型总结了函数定义的起始部分的声明,向编译器提供有关该函数应该如何调用的完整信息。使用原型最方便(且最安全)的方法是把原型置于一个单独的文件,当其他源文件需要这个函数的原型时,就是用#include指令包含该文件。这个技巧避免了错误键入函数原型的可能性,它同时简化了程序侧维护任务,因为这样只需要该原型的一份物理拷贝。如果原型需要修改,你只需要修改它的一处拷贝。
例,使用函数原型的好方法:
#include "func.h"
void a()
{
...
}
void b()
{
...
}
文件func.h包含了下面的函数原型
int *func( int *value, int len );
另外,把函数原型包含在定义函数的文件中可以使编译器确认它们之间的匹配;
一个没有参数的函数的原型应该写成下面这个样子:
int *func( void );
关键字void提示没有任何参数,而不是表示它有一个类型为void的参数。
所有的函数都应该具有原型,尤其是那些返回值不是整形的函数。
检查一个数是否满足偶校验,也就是它的二进制位模式中1的个数是否为偶数。
/*把一个数组的所有元素都设置为零*/
void clear_array( int array[], int n_elements )
{
/*从数组最后一个元素开始,逐个清除数组中的所有元素。注意,前缀自增避免了越出数组边界的可能性*/
while( n_elements > 0)
array[ --n_elements ] = 0;
}
C函数的所有参数均以"传值调用"方式进行传递,这意味着函数将获得参数值的一份拷贝。这样,函数就可以放心修改这个拷贝值,而不必担心会修改调用程序实际传递给它的参数。
但是,如果被传递的参数是一个数组名,并且在函数中使用下标引用该数组的参数,那么在函数中对数组元素进行修改实际上修改的是调用程序中的数组元素。函数将访问调用程序的数组元素,数组并不会被复制。这个行为被称为"传址调用"。
数组参数的这种行为似乎与传值调用规则相悖。但是,此处其实并无矛盾之处————数组名的值实际上是一个指针,传递给函数的就是这个指针的一份拷贝。下标引用实际上是间接访问的另一种形式,它可以对指针执行间接访问操作,访问指针指向的内存位置。参数(指针)实际上是一份拷贝,但在这份拷贝上执行间接访问操作所访问的是原先的数组。
让一个函数在不同的时候接受不同数目的参数是不是可以呢?答案是肯定的。
可变参数列表是通过宏来实现的,这些宏定义与stdarg.h头文件中,它是标准库的一部分。这个头文件声明了一个类型va_list和三个宏————va_start、va_arg和va_end;
如果函数体内没有任何语句,那么该函数就被称为存根,它在测试不完整的程序时非常有用,它可以为那些此时尚未实现的代码保留一个位置。编写这类存根,或者说为尚未编写的代码"站好位置",可以保持程序在结构上的完整性,以便于编译和测试程序的其它部分。例如:
void func(void)
{
}
有些函数的参数列表包含可变的参数数量和类型,它们可以使用stdarg.h头文件所定义的宏来实现。
int b[10];
数组名的值是一个指针常量,数组名的类型是"指向int的常量指针";
只有当数组名在表达式中使用时,编译器才会为它产生一个指针常量。
只有在两种场合下,数组名并不用指针常量来表示————就是当数组名作为sizeof操作符或单目操作符&的操作数时。
sizeof返回整个数组的长度,而不是指向数组的指针的长度。取一个数组名的地址所产生的是一个指向数组的指针,而不是一个指向某个指针常量值的指针。
int a[10];
int b[10];
int *c;
...
c = &a[0];等价于c = a;
b = a;
是非法的。你不能使用赋值符把一个数组的所有元素复制到另一个数组。你必须使用一个循环,每次复制一个元素。
a = c;
是非法的。记住,在这个表达式中,a的值是个常量,不能被修改;
*( b + 3)
b的值是一个指向整形的指针,所以3这个值根据整形值的长度进行调整;
除优先级之外,下表引用和间接访问完全相同,例如,
array[subscript]
*( array + ( subscript ) )
在使用下标引用的地方,你可以使用对等的指针表达式来代替。例如,
int array[10];
int *ap = array + 2;
记住,在进行指针加法运算时,会对2进行调整;
ap[0],对等的表达式是*(ap+(0)),即是array[2],记住,C的下标引用和间接访问表达式是一样的。
数组初始化:
int array[10], a;
for( a = 0; a < 10; a += 1)
array[a] = 0;
或者:
int array[10], *ap;
for( ap = array; ap < array + 10; ap++ )
*ap = 0;
不要为了效率上的细微差别而牺牲可读性,这点非常重要。
你必须记住,效率并不是唯一的因素,通常代码的简洁性更为重要;
表达式&array[SIZE]可以在编译时求值,因为SIZE是个数字常量;
那些必须在运行时求值的表达式较之诸如&array[SIZE]或array+SIZE这样的常量表达式往往代价更高。
声明为寄存器变量的指针通常比位于静态内存和堆栈中的指针效率更高;
int a[5];
int *b;
a++编译通不过,因为a的值是个常量;
当数组名作为函数的参数时,调用该函数时,数组名也是通过传值调用的,传递给被调函数的是数组名(即是指针)的一份拷贝,无论函数对参数(指针)如何进行修改,都不会修改调用程序的指针实参本身(但可能修改它所指向的内容);即,并不会修改调用程序的实参,因为只有传递给函数的那份拷贝进行了修改;
在许多机器上,register变量所产生的代码将比静态内存中的变量和堆栈中的变量所产生的代码执行速度更快;
静态和自动初始化
数组初始化的方法类似于标量变量的初始化方式————也就是取决于它们的存储类型。
存储于静态内存的数组只初始化一次,也就是在程序开始执行之前。如果数组未被初始化,数组元素的初始值将会自动设置为零。当这个文件载入到内存中准备执行时,初始化后的数组值和程序指令一样被载入到内存中。因此,当程序执行时,静态数组已经被初始化完毕。
对于自动变量而言,因为自动变量位于运行时堆栈中,执行流每次进入它们所在的代码块时,这类变量每次所在的内存位置可能并不相同。在程序开始之前,编译器没有办法对这些位置进行初始化。所以,自动变量在缺省情况下是未被初始化的。如果自动变量的声明中给出了初始值,每次当执行流进入自动变量声明所在的作用域时,变量就被一条隐式的赋值语句初始化。这条隐式的赋值语句和普通的赋值语句一样需要时间和空间来执行。数组的问题在于初始化列表中可能有很多值,这就可能产生许多条赋值语句。对于那些非常庞大的数组,它的初始化时间可能非常可观。
因此,这里就需要权衡利弊。当数组的初始化局部于一个函数(或代码块)时,你应该仔细考虑一下,在程序的执行流每次进入该函数(或代码块)时,每次对数组进行重新初始化是不是值得。如果答案是否定的,你就把数组声明为static,这样数组的初始化只需在程序开始前执行一次。
字符数组的初始化:
char message[] = { 'H', 'e', 'l', 'l', 'o', 0 };
这个方法当然可行。但除了非常短的字符串,这种方法确实很笨拙。因此,语言标准提供了一种快速方法用于初始化字符数组:
char message[] = "Hello";
尽管它看上去像是一个字符串常量,实际上并不是。
你如何分辨字符串常量和这种初始化列表快速记法呢?
当用于初始化一个字符数组时,它就是一个初始化列表。在其它任何地方,它都表示一个字符串常量。
例如,
char message1[] = "Hello";
char *message2 = "Hello";
这两个初始化看上去很像,但它们具有不同的含义。前者初始化一个字符数组的元素,而后者则是一个真正的字符串常量。这个指针变量初始化为指向这个字符串常量的存储位置。
运行时环境
许多(但不是所有)C编译器会在C代码所声明的外部名字前加一个下划线,以免与各个库函数所使用的名字冲突。
NUL是作为字符串终止符,它本身并不被看作是字符串的一部分。
字符串常量(string literal)就是源程序中被双引号括起来的一串字符。
例如,字符串常量:"Hello"
printf函数的第一个参数是一个字符串,描述输出的格式,格式常常以字符串常量的形式出现。
格式字符串包含格式指定符(格式代码)以及一些普通字符。这些普通字符将按照原样逐字打印出来,但每个格式指定符将使后续参数的值按照它所指定的格式打印。
NUL是ASCII字符集中'\0'字符的名字,NULL在头文件stdio.h中定义,它是一个值为0的指针。它们都是整形值,其值也相同。
并不存在预定义的符号NUL,所以如果你想使用它而不是字符常量'\0',必须自行定义。
%g:打印一个浮点值;
%ld:长整型值,long;
%lf:双精度实型值,double;
char input[MAX_INPUT]; /*容纳输入行的数组*/
while( gets( input ) != NULL )
gets函数从标准输入读取一行文本并把它存储于作为参数传递给它的数组中。
一行输入又一串字符组成,以一个换行符(newline)结尾。gets函数丢弃换行符,并在该行的末尾存储一个NUL字节('\0')。然后,gets函数返回一个非NULL值,表示该行已被成功读取。
假如一个程序的源代码又几个源文件所组成,那么使用该函数的源文件都必须写明该函数的原型。把原型放在头文件中并使用#include指令包含它们,可以避免由于同一个声明的多份拷贝而导致的维护性问题。
stdlib.h定义了EXIT_SUCCESS和EXIT_FAILURE符号;
如果你有一些声明需要用于几个不同的源文件,方法:你在一个单独的文件中编写这些声明,然后用#include指令把这个文件包含到需要使用这些声明的源文件中。这样,你就只需要这些声明的一份拷贝,用不着在许多不同的地方进行复制,避免了在维护这些代码时出现错误的可能性。
于处理器读入源代码,根据预处理指令对其进行修改,然后把修改过的源代码递交给编译器。
scanf("%d",&columns[num])
格式码%d表示需要读取一个整形值。字符是从标准输入读取,前导空白将被跳过。然后这些数字被转换为一个整数,结果存储于指定的数组元素中。
scanf函数转换了一个整数之后,它就返回1这个值;
标准并未硬性规定C编译器对数组下标的有效性进行检查,而且绝大多数C编译器确实也不进行检查。因此,如果你需要进行数组下标的有效性检查,你必须自行编写代码。如果此处不进行num<max这个测试,而且程序所读取的文件包含超过20个列标号,那么多出来的值就会存储在紧随数组之后的内存位置,这样就会破坏原先存储在这个位置的数据,可能是其他变量,也可以是函数的返回地址。
puts函数把指定的字符串写到标准输出并在末尾添上一个换行符;
int ch;
ch=getchar();
为什么ch被声明为整形,而我们事实上需要它来读取字符?答案是EOF是一个整形值,它的位数比字符类型要多,把ch声明为整形可以防止从输入读取的字符意外地被解释为EOF。但同时,这也意味着接收字符的ch必须足够大,足以容纳EOF,这就是ch使用整形值的原因。
字符只是小整数而已,所以用一个整形变量容纳字符值并不会引起任何问题;
while( (ch = getchar()) != EOF && ch != '\n' )
;
这个while循环的循环体没有任何语句。仅仅完成while表达式的测试部分就足以达到我们的目的,所以循环体就无事可干。while语句之后的单独一个分号称为空语句,它就是应用于目前这个场合,也就是语法要求这个地方出现一条语句但又无需执行任何任务的时候。这个分号独占一行,这就是为了防止读者错误地以为接下来的语句也是循环体的一部分。
putchar函数,它接受一个整形参数,并在标准输出中打印该字符(字符在本质上也是整形)
在字符串内进行搜索的函数是strchr,它接受两个参数,第1个参数是字符串,第2个参数是一个字符。这个函数在字符串参数内搜索字符参数第1次出现的位置,如果搜索成功就返回指向这个位置的指针,如果搜索失败就返回一个NULL指针。
strstr函数的功能类似,但它的第2个参数也是一个字符串,它搜索第2个字符串在第1个字符串中第1次出现的位置。
在#include文件中放置函数原型;
在ANSI C的任何一种实现中,存在两种不同的环境。
翻译环境:源代码被转换为可执行的机器指令。
执行环境:用于实际执行代码。
标准明确说明,这两种环境不必位于同一台机器上。
例如,交叉编译器就是在一台机器上运行,但它所产生的可执行代码运行于不同类型的机器上。
翻译阶段由几个步骤组成,组成一个程序的每个(有可能有多个)源文件通过编译过程分别转换为目标代码(object code)。然后,各个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序。链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它也可以搜索程序员个人的程序库,将其中需要使用的函数也链接到程序中。
编译过程本身也由几个阶段组成,
首先是预处理器(preprocessor)处理。这个阶段,预处理器在源代码上执行一些文本操作。例如,用实际值代替由#define指令定义的符号以及读入由#include指令高喊的文件内容。
然后,源代码经过解析(parse),判断它的语句的意思。第2个阶段是产生绝大多数错误和警告信息的地方。
随后,便产生目标代码。目标代码是机器指令的初步形式,用于实现程序的语句。
在缺省情况下,链接器在标准C函数库中查找。如果在编译时加上"-lname"标志,链接器就会同时在"name"的函数库中进行查找。这个选项应该出现在命令行的最后。
程序的执行过程也需要经历几个阶段。首先,程序必须再入到内存中。在宿主环境中(也就是具有操作系统的环境),这个任务由操作系统完成。那些不是存储在堆栈中的尚未初始化的变量将在这个时候得到初始值。
然后,便开始执行程序代码。在绝大多数机器里,程序将使用一个运行时堆栈(stack),它用于存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程中将一直保留它们的值。
打印调试信息:
printf("%s-%d\n",__FILE__,__LINE__);
在整数字面值后面添加字符L或l(这是字母l),可以使这个整数被解释为long整形值,字符U或u则用于把数值指定为unsigned整形值。如果在一个字面值后面添加这两组字符中的各一个,那么它就被解释为unsigned long整形值。
枚举类型的声明:
enum Jar_Type { CUP, PINT, QUART, HALF_GALLON, GALLON };
这条语句声明了一个类型,称为Jar_Type。
这种类型的变量按下列方式声明:
enum Jar_Type milk_jug, gas_can, medicine_bottle;
如果某种特别的枚举类型的变量只使用一个声明,你可以把上面两条语句组合成:
enum { CUP, PINT, QUART, HALF_GALLON, GALLON }
milk_jug, gas_can, medicine_bottle;
编译器负责把变量赋值给计算机内存中的位置;
事实上,当一个函数每次被调用时,它的自动变量(局部变量)可能每次分配的内存位置都不相同。
许多人对C语言不存在字符串类型感到奇怪,不过C语言提供了字符串常量。事实上,C语言存在字符串的概念:一串以NUL字节结尾的零个或多个字符。字符串通常存储在字符数组中,这也是C语言没有显示的字符串类型的原因。之所以选择NUL作为字符串的终止符,是因为它不是一个可打印的字符。
字符串常量可以是空的,如:"",即使是空字符串,依然存在作为终止符的NUL字节。
ANSI C声明如果对一个字符串常量进行修改,其效果是未定义的。许多ANSI编译器不允许修改字符串常量。如果你需要修改字符串,请把它存储于数组中。
当一个字符串常量出现于一个表达式中时,表达式所使用的值就是这些字符所存储的地址,而不是这些字符本身。因此,你可以把字符串常量赋值给一个"指向字符的指针",后者指向这些字符所存储的地址。但是,你不能把字符串常量赋值给一个字符数组,因为字符串常量的直接值是一个指针,而不是这些字符本身。
C数组另一个值得关注的地方是,编译器并不检查程序对数组下标的引用是否在数组的合法范围之内;
typedef允许你为各种数据类型定义新名字。typedef声明的写法和普通的声明基本相同,只是把typedef这个关键字出现在声明的前面。例如,下面这个声明:
char *prt_to_char;
把变量prt_to_char声明为一个指向字符的指针。
但是在你添加关键字typedef后,声明变为:
typedef char *ptr_to_char;
这个声明把标识符ptr_to_char作为指向字符的指针类型的新名字。
你可以像使用任何预定义名字一样在下面的声明中使用这个新名字。例如:
prt_to_char a;
声明a是一个指向字符的指针。
提示:你应该使用typedef而不是#define来创建新的类型名,因为后者无法正确处理指针类型。例如:
#define d_ptr_to_char char *;
d_ptr_to_char a,b;
正确地声明了a,但是b却被声明为一个字符。
在定义更为复杂的类型名字时,如函数指针或指向数组的指针,使用typedef更为合适;
const关键字声明的常量如何在一开始就拥有一个值?
法1:可以在声明时对它进行初始化,如:int const a = 15;
法2:在函数中声明为const的形参在函数被调用时会得到实参的值;
#define指令是另一种创建名字常量的机制。例如,下面这两个声明都为50这个值创建了名字常量。
#define MAX_ELEMENTS 50
int const max_elements = 50;
在这种情况下,使用#define比使用const变量更好。因为只要允许使用字面值常量的地方都可以使用前者,比如声明数组的长度。const变量只能用于允许使用变量的地方。
名字常量非常有用,因为它们可以给数值起符号名,否则它们就只能写成字面值的形式。用名字常量定义数组的长度或限制循环的计数器能够提高程序的可维护性————如果一个值必须修改,只需要修改声明就可以了。修改一个声明比搜索整个程序修改字面值常量的所有实例要容易得多。
有三个地方可以用于存储变量:普通内存、运行时堆栈、硬件寄存器。
变量的缺省存储类型取决于它的声明位置。凡是在任何代码块之外声明的变量总是存储于静态内存中,也就是不属于堆栈的内存,这类变量称为静态(static)变量。
静态变量在程序运行之前创建,在程序的整个执行期间始终存在。它始终保持原先的值,除非给它赋一个不同的值或者程序结束。
在代码块内部声明的变量的缺省存储类型是自动的(automatic),也就是说它存储于堆栈中,称为自动(auto)变量。有一个关键字auto就是用于修饰这种存储类型的,但它极少使用,因为代码块中的变量在缺省情况下就是自动变量。
在程序执行到声明自动变量的代码块时,自动变量才被创建,当程序的执行流离开代码块时,这些自动变量便自行销毁。
在静态变量的初始化中,我们可以把可执行程序文件想要初始化的值放在当程序执行时变量将会使用的位置。当可执行文件载入到内存时,这个已经保存了正确初始值的位置将赋值给那个变量。如果不显示地指定其初始值,静态变量将初始化为0;
对于在代码块内部声明的变量,如果给它加上关键字static,可以使它的存储类型从自动变为静态。具有静态存储类型的变量在整个程序执行过程中一直存在,而不仅仅子啊声明它的代码块的执行时存在。注意,修改变量的存储类型并不表示修改该变量的作用域,它仍然只能在该代码块内部按名字访问。
函数的形式参数不能声明为静态,因为实参总是在堆栈中传递给函数,用于支持递归。
关键字register可以用于自动变量的声明,提示它们应该存储于机器的硬件寄存器而不是内存中,这类变量称为寄存器变量。
自动变量和静态变量的初始化存在一个重要的差别:
在静态变量的初始化中,我们可以把可执行程序文件想要初始化的值放在当程序执行时变量将会使用的位置。当可执行文件载入到内存时,这个已经保存了正确初始值的位置将赋值给那个变量。如果不显示地指定其初始值,静态变量将初始化为0。
自动变量的初始化需要更多的开销,因为当程序链接时还无法判断自动变量的存储位置。事实上,函数的局部变量在函数的每次调用中可能占据不同的位置。基于这个理由,自动变量没有缺省的初始值;
重点static关键字:
当它用于函数定义时,或用于代码块之外的变量声明时,static关键字用于修改标识符的链接属性,从external改为internal,但标识符的存储类型和作用域不受影响。用这种方式声明的函数或变量只能在声明它们的源文件中访问。
当它用于代码块内部的变量声明时,static关键字用于修改变量的存储类型,从自动变量修改为静态变量,但变量的链接属性和作用域不受影响。用这种方式声明的变量在程序执行之前创建,并在程序的整个执行期间一直存在,而不是每次在代码块开始执行时创建,在代码块执行完毕后销毁。
属于文件作用域的声明在缺省情况下为external链接属性;
int d(int e)
{
...
}
int f()
{
...
}
函数d不可以调用函数i,因为在d之前不存在i的原型。
具有external链接属性的实体在其它语言的术语里称为全局(global)实体,所有源文件中的所有函数均可以访问它。只要变量并非声明于代码块或函数定义内部,它在缺省情况下的链接属性即为external。如果一个变量声明于代码块内部,在它前面添加extern关键字将使它所引用的是全局变量而非局部变量。
具有external链接属性的实体总是具有静态存储类型。
不要把整形值和枚举值混在一起使用
在定义类型的新名字时,使用typedef而不是#define
用const声明其值不会修改的变量
空语句:本身只包含一个分号;
break和continue语句:
在while循环中可以使用break语句,用于永久终止循环。在执行完break语句之后,执行流下一条执行的语句就是循环正常结束后应该执行的那条语句。
在while循环中也可以使用continue语句,它用于永久终止当前的那次循环。在执行完continue语句之后,执行流接下来就是重新测试表达式的值,决定是否继续执行循环。
这两条语句的任何一条如果出现于嵌套的循环内部,它只对最内层的循环起作用,你无法使用break或continue语句影响外层循环的执行。
如果循环体内执行了continue语句,循环体内的剩余部分便不再执行,而是立即开始下一轮循环。
当循环体只有遇到某些值才会执行的情况下,continue语句相当有用:
while( (ch = getchar()) != EOF )
{
if( ch < '0' || ch > '9')
continue;
/*process only the digits*/
}
for语句如果省略条件部分,表示测试的值始终为真;
在for语句中也可以使用break语句和continue语句。break语句立即退出循环,而continue语句把控制流直接转移到调整部分。
把一个数组的所有元素初始化为0:
for( i = 0; i < MAX_SIZE; i += 1 )
array[i] = 0;
当你需要循环体至少执行一次时,选择do语句;
常量表达式(constant-expression)是指在编译期间进行求值的表达式;
switch( expression ),其中expression的结果必须是整形值;
goto 语句标签;
要使用goto语句,你必须在你希望跳转的语句前面加上语句标签。
语句标签就是标识符后面加个冒号。
在一种情况下,即使是结构良好的程序,使用goto语句也可能非常合适————就是跳出多层嵌套的循环。由于break语句只影响包围它的最内层循环,要想立即从深层嵌套的循环中退出只有使用一个办法,就是使用goto语句。
for语句是while循环的一种常用组合形式的速记写法。
当需要出现一条语句但并不需要执行任何任务时,可以使用空语句。
设置和清除变量值中单个位:
value |= 1 << bit_number;
value &= ~ ( 1 << bit_number );
sizeof操作符判断它的操作数的类型长度,以字节为单位。操作数既可以是个表达式(常常是单个变量),也可以是两边加上括号的类型名。如:
sizeof ( int ) sizeof x
sizeof的操作数是个数组名时,它返回该数组的长度,以字节为单位;
判断表达式的长度并不需要对表达式进行求值,所以,sizeof( a=b+1 )并没有向a赋任何值;
前缀和后缀形式的增值操作符都复制一份变量值的拷贝。用于周围表达式的值正是这份拷贝。这些操作符的结果不是被它们所修改的变量,而是变量值的拷贝,认识这一点非常重要。它之所以重要是因为它解释了你为什么不能像下面这样使用这些操作符:
++a = 10;
++a的结果是a值的拷贝,并不是变量本身,你无法向一个值进行赋值。
在C中,你必须使用双等于号==来比较相等性;
b = a > 5 ? 3 : -20;
.和->操作符用于访问一个结构的成员。
C并不具备显式的布尔类型,所以使用整数来代替。
避免混合使用整形值和布尔值。如果一个变量包含了一个任意的整形值,你应该显示地对它进行测试:
if( value != 0 )
不要使用简写法来测试变量是零还是非零,因为这类形式错误地暗示该变量在本质上是布尔型的。
寻常算数转换:
long double
double
float
unsigned long int
long int
unsigned int
int
记住,左值意味着一个位置,而右值意味着一个值。
~(按位取反)
^(异或操作)
有符号值的右移位操作是不可移植的。
使用条件操作符替代if语句以简化表达式。
使用复合赋值符可以使程序更易于维护。
有一点非常重要,你必须记住,名字与内存位置之间的关联并不是硬件所提供的,它是由编译器为我们实现的。所有这些变量给了我们一种更方便的方法记住地址————硬件仍然通过地址访问内存位置。
int *a;
如果变量是静态的,它会被初始化为0;但如果变量是自动的,它根本不会被初始化;
标准定义了NULL指针,表示不指向任何东西。要使一个指针变量为NULL,你可以给它赋一个零值。为了测试一个指针变量是否为NULL,你可以将它与零值进行比较。
对所有的指针变量进行显式的初始化是种好做法。如果你已经知道指针将被初始化为什么地址,就把它初始化为该地址,否则就把它初始化为NULL。
间接访问操作(*)只能作用于指针类型表达式
*100 = 25;(错,因为100为整形)
*(int *)100 = 25;(OK,强制类型转换把值100从"整形"转换为"指向整形的指针")
这个技巧唯一有用之处是你偶尔需要通过地址访问内存中某个特定的位置,它并不是用于访问某个变量,而是访问硬件本身。
指针变量和其他变量一样,占据内存中某个特定的位置,所以用&操作符得到它的地址是合法的!
当一个指针和一个整数量执行算数运算时,整数在执行加法运算前始终会根据合适的大小进行调整。这个"合适的大小"就是指针所指向类型的大小,"调整"就是把整数值和"合适的大小"相乘。
让指针指向数组最后一个元素后面的那个位置是合法的,但对这个指针执行间接访问可能会失败;
两个指针相减的结果的类型是ptrdiff_t,它是一种有符号整数类型。减法运算的值是两个指针在内存中的距离,因为减法运算的结果将除以数组元素类型的长度。
例如,float a[];数组元素的类型为float,每个元素占据4个字节的内存空间。如果数组的起始位置为1000,指针p1的值是1004,p2的值是1024,但表达式p2-p1的结果将是5,因为两个指针的差值(20)将除以每个元素的长度(4)。
标准允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针进行比较,但不允许与指向数组第一个元素之前的那个内存位置的指针进行比较。
指针就是它的值表示内存地址的变量;
在对指针执行间接访问前,指针必须进行初始化;
对未初始化的指针变量执行间接访问操作是非法的;
在极少见的情况下,我们偶尔需要使用指针常量,这时我们可以通过把一个整形值强制转化为指针类型来创建它。
指针运算只有作用于数组中其结果才是可以预测的。对任何并非指向数组元素的指针执行算数运算是非法的。如果一个指针减去一个整数后,运算结果产生的指针所指向位置在数组第一个元素之前,那么它也是非法的。加法运算稍有不同,如果结果指针指向数组最后一个元素后面的那个内存位置仍是合法的(但不能对这个指针执行间接访问操作),不过再往后就不合法了。
指针减法的结果经过调整(除以数组元素类型的长度),表示两个指针在数组中相隔多少个元素。
向编译器提供一些关于函数的特定信息显然更为安全,我们可以通过两种方法来实现。首先,如果同一源文件的前面已经出现了该函数的定义,编译器就会记住它的参数数量和类型,以及函数的返回值类型。接着,编译器便可以检查该函数的所有后续调用(在同一个源文件中),确保它们是正确的。
第二种向编译器提供函数信息的方法是使用函数原型(function prototype)。原型总结了函数定义的起始部分的声明,向编译器提供有关该函数应该如何调用的完整信息。使用原型最方便(且最安全)的方法是把原型置于一个单独的文件,当其他源文件需要这个函数的原型时,就是用#include指令包含该文件。这个技巧避免了错误键入函数原型的可能性,它同时简化了程序侧维护任务,因为这样只需要该原型的一份物理拷贝。如果原型需要修改,你只需要修改它的一处拷贝。
例,使用函数原型的好方法:
#include "func.h"
void a()
{
...
}
void b()
{
...
}
文件func.h包含了下面的函数原型
int *func( int *value, int len );
另外,把函数原型包含在定义函数的文件中可以使编译器确认它们之间的匹配;
一个没有参数的函数的原型应该写成下面这个样子:
int *func( void );
关键字void提示没有任何参数,而不是表示它有一个类型为void的参数。
所有的函数都应该具有原型,尤其是那些返回值不是整形的函数。
检查一个数是否满足偶校验,也就是它的二进制位模式中1的个数是否为偶数。
/*把一个数组的所有元素都设置为零*/
void clear_array( int array[], int n_elements )
{
/*从数组最后一个元素开始,逐个清除数组中的所有元素。注意,前缀自增避免了越出数组边界的可能性*/
while( n_elements > 0)
array[ --n_elements ] = 0;
}
C函数的所有参数均以"传值调用"方式进行传递,这意味着函数将获得参数值的一份拷贝。这样,函数就可以放心修改这个拷贝值,而不必担心会修改调用程序实际传递给它的参数。
但是,如果被传递的参数是一个数组名,并且在函数中使用下标引用该数组的参数,那么在函数中对数组元素进行修改实际上修改的是调用程序中的数组元素。函数将访问调用程序的数组元素,数组并不会被复制。这个行为被称为"传址调用"。
数组参数的这种行为似乎与传值调用规则相悖。但是,此处其实并无矛盾之处————数组名的值实际上是一个指针,传递给函数的就是这个指针的一份拷贝。下标引用实际上是间接访问的另一种形式,它可以对指针执行间接访问操作,访问指针指向的内存位置。参数(指针)实际上是一份拷贝,但在这份拷贝上执行间接访问操作所访问的是原先的数组。
让一个函数在不同的时候接受不同数目的参数是不是可以呢?答案是肯定的。
可变参数列表是通过宏来实现的,这些宏定义与stdarg.h头文件中,它是标准库的一部分。这个头文件声明了一个类型va_list和三个宏————va_start、va_arg和va_end;
如果函数体内没有任何语句,那么该函数就被称为存根,它在测试不完整的程序时非常有用,它可以为那些此时尚未实现的代码保留一个位置。编写这类存根,或者说为尚未编写的代码"站好位置",可以保持程序在结构上的完整性,以便于编译和测试程序的其它部分。例如:
void func(void)
{
}
有些函数的参数列表包含可变的参数数量和类型,它们可以使用stdarg.h头文件所定义的宏来实现。
int b[10];
数组名的值是一个指针常量,数组名的类型是"指向int的常量指针";
只有当数组名在表达式中使用时,编译器才会为它产生一个指针常量。
只有在两种场合下,数组名并不用指针常量来表示————就是当数组名作为sizeof操作符或单目操作符&的操作数时。
sizeof返回整个数组的长度,而不是指向数组的指针的长度。取一个数组名的地址所产生的是一个指向数组的指针,而不是一个指向某个指针常量值的指针。
int a[10];
int b[10];
int *c;
...
c = &a[0];等价于c = a;
b = a;
是非法的。你不能使用赋值符把一个数组的所有元素复制到另一个数组。你必须使用一个循环,每次复制一个元素。
a = c;
是非法的。记住,在这个表达式中,a的值是个常量,不能被修改;
*( b + 3)
b的值是一个指向整形的指针,所以3这个值根据整形值的长度进行调整;
除优先级之外,下表引用和间接访问完全相同,例如,
array[subscript]
*( array + ( subscript ) )
在使用下标引用的地方,你可以使用对等的指针表达式来代替。例如,
int array[10];
int *ap = array + 2;
记住,在进行指针加法运算时,会对2进行调整;
ap[0],对等的表达式是*(ap+(0)),即是array[2],记住,C的下标引用和间接访问表达式是一样的。
数组初始化:
int array[10], a;
for( a = 0; a < 10; a += 1)
array[a] = 0;
或者:
int array[10], *ap;
for( ap = array; ap < array + 10; ap++ )
*ap = 0;
不要为了效率上的细微差别而牺牲可读性,这点非常重要。
你必须记住,效率并不是唯一的因素,通常代码的简洁性更为重要;
表达式&array[SIZE]可以在编译时求值,因为SIZE是个数字常量;
那些必须在运行时求值的表达式较之诸如&array[SIZE]或array+SIZE这样的常量表达式往往代价更高。
声明为寄存器变量的指针通常比位于静态内存和堆栈中的指针效率更高;
int a[5];
int *b;
a++编译通不过,因为a的值是个常量;
当数组名作为函数的参数时,调用该函数时,数组名也是通过传值调用的,传递给被调函数的是数组名(即是指针)的一份拷贝,无论函数对参数(指针)如何进行修改,都不会修改调用程序的指针实参本身(但可能修改它所指向的内容);即,并不会修改调用程序的实参,因为只有传递给函数的那份拷贝进行了修改;
在许多机器上,register变量所产生的代码将比静态内存中的变量和堆栈中的变量所产生的代码执行速度更快;
静态和自动初始化
数组初始化的方法类似于标量变量的初始化方式————也就是取决于它们的存储类型。
存储于静态内存的数组只初始化一次,也就是在程序开始执行之前。如果数组未被初始化,数组元素的初始值将会自动设置为零。当这个文件载入到内存中准备执行时,初始化后的数组值和程序指令一样被载入到内存中。因此,当程序执行时,静态数组已经被初始化完毕。
对于自动变量而言,因为自动变量位于运行时堆栈中,执行流每次进入它们所在的代码块时,这类变量每次所在的内存位置可能并不相同。在程序开始之前,编译器没有办法对这些位置进行初始化。所以,自动变量在缺省情况下是未被初始化的。如果自动变量的声明中给出了初始值,每次当执行流进入自动变量声明所在的作用域时,变量就被一条隐式的赋值语句初始化。这条隐式的赋值语句和普通的赋值语句一样需要时间和空间来执行。数组的问题在于初始化列表中可能有很多值,这就可能产生许多条赋值语句。对于那些非常庞大的数组,它的初始化时间可能非常可观。
因此,这里就需要权衡利弊。当数组的初始化局部于一个函数(或代码块)时,你应该仔细考虑一下,在程序的执行流每次进入该函数(或代码块)时,每次对数组进行重新初始化是不是值得。如果答案是否定的,你就把数组声明为static,这样数组的初始化只需在程序开始前执行一次。
字符数组的初始化:
char message[] = { 'H', 'e', 'l', 'l', 'o', 0 };
这个方法当然可行。但除了非常短的字符串,这种方法确实很笨拙。因此,语言标准提供了一种快速方法用于初始化字符数组:
char message[] = "Hello";
尽管它看上去像是一个字符串常量,实际上并不是。
你如何分辨字符串常量和这种初始化列表快速记法呢?
当用于初始化一个字符数组时,它就是一个初始化列表。在其它任何地方,它都表示一个字符串常量。
例如,
char message1[] = "Hello";
char *message2 = "Hello";
这两个初始化看上去很像,但它们具有不同的含义。前者初始化一个字符数组的元素,而后者则是一个真正的字符串常量。这个指针变量初始化为指向这个字符串常量的存储位置。
运行时环境
许多(但不是所有)C编译器会在C代码所声明的外部名字前加一个下划线,以免与各个库函数所使用的名字冲突。
本文深入讲解C语言的基础概念,包括字符串、数组、指针、函数等核心内容,并探讨了编译原理、内存管理及程序执行流程。此外,还介绍了如何使用预处理指令、函数原型、类型定义等高级特性。

被折叠的 条评论
为什么被折叠?



