C和指针---查漏补缺笔记

来源:互联网 发布:js解绑点击事件 编辑:程序博客网 时间:2024/04/30 10:20


注释 有时候用于把一段代码"注释掉",也就是使这段代码在程序中不起作用,但并不是将其真正从源文件中删除。要从逻辑上删除一段C代码,更好的办法是使用#if指令:

#if 0

statements

#endif

#if#endif之间的程序段就可以有效地从程序中去除,即使这段代码之间原先存在注释也无妨。


#include<stdio.h>

#defineMAX_COLS 20

#include#define属于预处理指令,因为它们是由预处理器解释的。预处理器读入源代码,根据预处理指令对其进行修改,然后把修改过的源代码递交给编译器。预处理器用名叫stdio.h的库函数头文件的内容替换第1#include指令语句,其结果就仿佛是stdio.h的内容被逐字写到源文件的那个位置。第二条#define把名字MAX_COLS定义为20,当这个名字以后出现在源文件的任何地方时,它就会被替换为定义的值。由于它被定义为字面值常量,所以这名字不能出现于有些普通变量可以出现的场合(比如赋值符的左边)。这名字一般都大写,用于提醒它们并非普通的变量。


在任何ANSIC的任何一种实现中,存在两种不同的环境。第一种是翻译环境,在这个环境里,源代码被转换为可执行的机器指令。第二种是执行环境,它用于实际执行代码。标准明确说明,这两种环境不必位于同一台机器上。

翻译由几个步骤组成,组成一个程序的每个(有可能有多个)源文件通过编译过程分别转换为目标代码。然后各个目标文件由链接器捆绑在一起,形成一个单一而完整的可执行程序。链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它也可以搜索程序员个人的程序库,将其中需要使用的函数也链接到程序中。

编译过程本身也由几个阶段组成,首先是预处理器处理。在这个阶段,预处理器在源代码上执行一些文本操作。例如,用实际值代替由#define指令定义的符号以及读入由#include指令包含的文件的内容。

然后,源代码经过解析,判断它的语句的意思。这个阶段是产生绝大多数错误和警告信息的地方。随后,便产生目标代码。目标代码是机器指令的初步形式,用于实现程序的语句。如果我们在编译程序的命令行中加入了要求优化的选型,优化器就会对目标代码进一步进行处理,使它效率更高。优化过程需要额外的时间,所以在程序调试完毕并准备生成正式产品之前一般不进行这个过程。


编译和链接

1.编译并链接一个完全包含于一个源文件的C程序:

ccprogram.c

这条命令产生一个称为a.out的可执行程序。中间会产生一个名为program.o的目标文件,但它在链接过程完成后会被删除。

2.编译并链接几个C源文件:

ccmain.c sort.c lookup.c

当编译的源文件超过一个时,目标文件便不会被删除。这就允许你对程序进行修改后,只对那些进行过改动的源文件进行重新编译,如下一条命令所示。

3.编译一个C源文件,并把它和现存的目标文件链接在一起:

ccmain.o lookup.o sort.c

4.编译单个C源文件,并产生一个目标文件(本例中为program.o),以后再进行链接:

cc-c program.c

5.编译几个C源文件,并为每个文件产生一个目标文件:

cc-c main.c sort.c lookup.c

6.链接几个目标文件:

ccmain.o sort.o lookup.o

上面那些可以产生可执行程序的命令均可以加上"-oname"这个选项,它可以使链接器把可执行程序保存在"name"文件中,而不是"a.out"。在缺省情况下,链接器在标准C函数库中查找。如果在编译时加上"-lname"标志,链接器就会同时在"name"的函数库中进行查找。这个选项应该出现在命令的最后。

执行

程序的执行过程也需要经历几个阶段。首先程序必须载入到内存中。在宿主环境中(也就是具有操作系统的环境),这个任务由操作系统完成。那些不是存储在堆栈中的尚未初始化的变量将在这个时候得到初始值。在独立环境(不存在操作系统的环境)中程序的载入必须由手工安排,也可能是通过把可执行代码置入只读内存(ROM)来完成。

然后,程序的执行便开始。在宿主环境中,通常一个小型的启动程序与程序链接在一起。它负责处理一系列日常事物,如收集命令行参数以便使程序能够访问它们。接着,便调用main函数。

现在,便开始执行程序代码。在绝大多数机器里,程序将使用一个运行时堆栈(stack),它用于存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程中将一直保留它们的值。

在程序执行的最后一个阶段就是程序的终止,它可以由多种不同的原因引起。在宿主环境中,启动程序将再次取得控制权,并可能执行各种不同的日常任务,如关闭那些程序可能使用过但并未显示关闭的任何文件。

枚举类型

枚举(enumerated)类型就是指它的值为符号常量而不是字面值的类型,它们以下面这种形式声明:

enumJar_Type{CUP,PINT,QUART,HALF_GALLON,GALLON};

这条语句声明了一个类型,称为Jar_Type这种类型的变量按下列方式声明:

enumJar_Type milk_jug,gas_can,medicine_bottle;

如果某种特别的枚举类型的变量只使用一个()声明,你可以把上面的两条语句组合成下面的样子:

enum{CUP,PINT,QUART,HALF_GALLON,GALLON} milk_jug,gas_can,medicine_bottle;

这种类型的变量实际上以整型的方式存储,这些符号名的实际值都是整型值。符号名被当作整型常量处理,声明为枚举类型的变量实际上是整数类型。


在字符串常量的存储形式中,所有的字符和NUL终止符都存储于内存的某个位置。在程序中使用字符串常量会生成一个“指向字符的常量指针”。当一个字符串常量出现于一个表达式中时,表达式所使用的值就是这些字符所存储的地址,而不是这些字符本身。因此,可以把字符串常量赋值给一个”指向字符的指针”,后者指向这些字符所存储的地址。但是,不能把字符串常量赋值给一个字符数组,因为字符串常量的直接值是一个指针,而不是这些字符本身。例如:

char*message=”Helloworld!”;这条语句把message声明为一个指向字符的指针,并用字符串常量中第1个字符的地址对该指针进行初始化。


typedef

你应该使用typedef而不是#define来创建新的类型名,因为后者无法正确地处理指针类型。例如:

#defined_ptr_to_char char *

d_ptr_to_chara,b;正确第声明了a,但是b却被声明为一个字符。在定义更为复杂的类型名字时,如函数指针或指向数组的指针,使用typedef更为合适。


常量

指针常量和常量指针:

constint *pci或者intconst *pci :一个指向整形常量的指针你可以修改指针值,但不能修改它所指向的值。相比之下:int*const cpi;则声明pci为一个指向整型的常量指针。此时指针是常量,它的值无法修改,但你可以修改它所指向的整型的值。


作用域

编译器可以确认4种不同类型的作用域——文件作用域,函数作用域,代码块作用域和原型作用域。标志符声明的位置决定它的作用域。位于一对花括号之间的所有语句称为一个代码块。任何在代码块的开始位置声明的标志符都具有代码块作用域,表示它们可以被这个代码块中的所有语句访问。任何在所有代码块之外声明的标识符都具有文件作用域(filescope),它表示这些标识符从它们的声明之外直到它所在的源文件结尾处都是可以访问的。在文件中定义的函数名也具有文件作用域,因为函数名本身并不属于任何代码块。在头文件中编写并通过#include指令包含到其他文件的声明就好像它们是直接写在那些文件中一样。它们的作用域并不局限于头文件的文件尾。


链接属性

当组成一个程序的各个源文件分别被编译之后,所有的目标文件以及那些从一个或多个函数库中引用的函数链接在一起,形成可执行程序。然而,如果相同的标识符出现在几个不同的源文件中时,它们是像Pascal表示同一个实体?还是表示不同的实体?标识符的链接属性决定如何处理在不同文件中出现的标识符。标识符的作用域与它的链接属性有关,但这两个属性并不相同。链接属性一共有3种——external(外部),internal(内部)none()。没有链接属性的标识符(none)总是被当作单独的个体,也就是说该标志符的多个声明被当作独立不同的实体。属于internal链接属性的标识符在同一个源文件内的所有声明中都指同一个实体,但位于不同源文件的多个声明则分属不同的实体。最后,属于external链接属性的标识符不论声明多少次,位于几个源文件都表示同一个实体,注意属于文件作用域的声明在缺省情况下为external链接属性

关键字externstatic用于在声明中修改标识符的链接属性。如果某个声明在正常情况下具有external链接属性,在它前面加上static关键字可以使它的链接属性变为internal。类似,也可以把函数声明为static。这可以防止它被其他源文件调用。static只对缺省链接属性为external的声明才有改变链接属性的效果。

存储类型

变量的缺省存储类型取决于它的声明位置。凡是在任何代码块之外声明的变量总是存储于静态内存中,也就是不属于堆栈的内存,这类变量称为静态变量。对于这类变量,你无法为它们指定其他存储类型。静态变量在程序运行之前创建,在程序的整个执行期间始终存在。它始终保持原先的值,除非给它赋一个不同的值或者程序结束。在代码块内部声明的变量的缺省类型是自动的,也就是说它存储于堆栈中。对于在代码块内部声明的变量,如果给它加上关键字static,可以使它的存储类型从自动变为静态。具有静态存储类型的变量在整个程序执行过程中一直存在。注意,修改变量的存储类型并不表示修改该变量的作用域,它仍然只能在该代码块内部按名字访问。函数的形式参数不能声明为静态,因为实参总是在堆栈中传递给函数,用于支持递归。关键字register可以用于自动变量的声明,提示它们应该存储于机器的硬件寄存器而不是内存中,这类变量称为寄存器变量。通常寄存器变量比存储于内存的变量访问起来效率更高。但是,编译器并不一定要理睬register关键字,如果有太多的变量被声明为register,它只选取前几个实际存储于寄存器中,其余的就按普通自动变量处理。


static关键字

1.当它用于函数定义时,或用于代码块之外的变量声明时,static关键字用于修改标识符的链接属性,从external改为internal,但标识符的存储类型和作用域不受影响。用这种方式声明的函数或变量只能在声明它们的源文件中访问。

2.当它用于代码块内部的变量声明时,static关键字用于修改变量的存储类型,从自动变量修改为静态变量,但变量的链接属性和作用域不受影响。用这种方式声明的变量在程序执行之前创建,并在程序的整个执行期间一直存在,而不是每次在代码块开始执行时创建,在代码块执行完毕后销毁。


switch语句 switch语句中的expression的结果必须是整型值。每个switch语句中的default子句可以出现在语句列表的任何位置,而且语句流会像贯穿一个case标签一样贯穿default子句。


位操作符 ^:XOR(异或)&AND()|:OR();

单目操作符 ~操作符对整型类型的操作数进行求补操作,操作数中所有原先为1的位变为0,所有原先为0的位变为1-操作符产生操作数的负值;(类型)操作符被称为强制类型转换(cast),具有很高的优先级,所以把强制类型转换放在一个表达式前面只会改变表达式的第1个项目的类型。如果要对整个表达式的结果进行强制类型转换,你必须把整个表达式用括号括起来。

逗号操作符    逗号操作符将两个或多个表达式分隔开来。这些表达式自左向右逐个进行求值,整个逗号表达式的值就是最后那个表达式的值。


函数定义    函数的定义就是函数体的实现。函数体就是一个代码块,它在函数被调用时执行。与函数定义相反,函数声明出现在函数被调用的地方。函数声明向编译器提供该函数的相关信息,用于确保函数被正确地调用。使用函数原型最方便(且最安全)的方法是把原型置于一个单独的文件,当其他源文件需要这个函数的原型时,就使用#include指令包含该文件。好处:1.现在函数原型具有文件作用域,所以原型的一份拷贝可以作用于整个源文件,较之在该函数每次调用前单独书写一份函数原型要容易得多。2。现在函数原型只书写一次,这样就不会出现多份原型的拷贝之间的不匹配现象。3.如果函数的定义进行了修改,我们只需要修改原型,并重新编译所有包含了该原型的源文件即可。4.如果函数的原型同时也被#include指令包含到定义函数的文件中,编译器就可以确认函数原型与函数定义的匹配。

可变参数列表

stdarg可变参数列表是通过宏来实现的,这些宏定义于stdarg.h头文件,它是标准库的一部分。这个头文件声明了一个类型va_list和三个宏——va_start,va_argva_end。我们可以声明一个类型为va_list的变量,与这几个宏配合使用,访问参数的值。

#include<stdarg.h>#include<stdio.h>//利用可变参数列表stdarg宏//计算指定数量的值的平均时间floataverage(int n_values, ...){va_listvar_arg;intcount;floatsum=0;//准备访问可变参数va_start(var_arg,n_values);//添加取自可变参数列表的值for(count=0;count<n_values;count+=1){sum+=va_arg(var_arg,int);}//完成处理可变参数。va_end(var_arg);returnsum/n_values;}intmain(int argc, char const *argv[]){floatav=average(5,1,2,3,4,5);printf("%f\n",av);return0;}

   上面的程序使用了这三个宏来计算指定个数的整数平均值,注意参数列表中的省略号:它提示此处可能传递数量和类型未确定的参数。在编写这个函数的原型时,也要使用相同的记法。

函数声明了一个名叫var_arg的变量,它用于访问参数列表的未确定部分。这个变量通过调用va_start来初始化。它的第1个参数是va_list变量的名字,第2个参数是省略号前最后一个有名字的参数。初始化过程把var_arg变量设置为指向可变参数部分的第1个参数。

为了访问参数,需要使用va_arg,这个宏接受两个参数:va_list变量和参数列表中下一个参数的类型。在这个例子中,所有的可变参数都是整型。在有些函数中,你可能要通过前面获得的数据来判断下一个参数的类型(比如printf)va_arg返回这个参数的值,并使var_arg指向下一个可变参数。

最后,当访问完毕最后一个可变参数之后,我们需要调用va_end

注意,可变参数必须从头到尾按照顺序逐个访问。如果你在访问了几个可变参数后想半途终止,可以。但是,如果想一开始就访问参数列表中间的参数,那是不行的。另外,由于参数列表中的可变参数部分没有原型,所以,所有作为可变参数传递给函数的值都将执行缺省参数类型提升。你可能同时注意到参数列表中至少要有一个命名参数。如果连一个命名参数也没有,你就无法使用va_start。这个参数提供了一种方法,用于查找参数列表的可变部分。对于这些宏,存在两个基本的限制。一个值的类型无法简单地通过检查它的位模式来判断,这两个限制就是这个事实的直接结果:1.这些宏无法判断实际存在的参数的数量。2.这些宏无法判断每个参数的类型。要回答这两个问题,就必须使用命名参数。

字符串,字符和字节

不受限制的字符串函数,   程序员必须确保结果字符串不会溢出原有内存。

复制字符串 char*strcpy(char *dst,char const *src);这个函数把参数src字符串复制到dst参数。如果参数srcdst在内存中出现重叠,其结果是未定义的。由于dst参数将进行修改,所以它必须是个字符数组或者是一个指向动态分配内存的数组的指针,不能使用字符串常量。strcpy函数返回第1个参数的一份拷贝,就是一个指向目标字符数组的指针。

连接字符串 char*strcat(char *dst,char const*src); strcat函数要求dst参数原先已经包含了一个字符串(可以是空字符串)。它找到这个字符串的末尾,并把src字符串的一份拷贝添加到这个位置。如果srcdst的位置发生重叠,其结果是未定义的。strcat函数返回第1个参数的一份拷贝,就是一个指向目标字符数组的指针。

字符串比较函数 intstrcmp(char const *s1,char const*s2);如果s1小于s2,strcmp函数返回一个小于0的值。如果s1大于s2,函数返回一个大于0的值。如果两个字符串相等,函数返回0

长度受限的字符串函数

char*strncpy(char *dst,char const *src,size_t len);

char*strncat(char *dst,char const *src,size_t len);

intstrncmp(char const *s1,char const *s2,size_t len);

strcpy一样,strncpy把源字符串的字符复制到目标数组。然而,它总是正好向dst写入len个字符。如果strlen(src)的值小于lendst数组就用额外的NUL字节填充到len长度。如strlen(src)的值大于或等于len,那么只有len个字符被复制到dst中。注意!它的结果将不会以NUL字节结尾

尽管strncat也是个长度受限的函数,但它和strncpy存在不同之处。它从src中最多复制len个字符到目标数组的后面。但是,strncat总是在结果字符串后面添加一个NUL字节,而且它不会像strncpy那样对目标数组用NUL字节进行填充。注意目标数组中原先的字符串并没有算在strncat的长度中。strncat最多向目标数组复制len个字符(再加上一个结尾的NUL字节),它才不管目标参数除去原先存在的字符串之后留下的空间够不够。

strncmp也用于比较两个字符串,但它最多比较len个字节。

字符串查找

char*strchar(char const *str,int ch);

char*strrchr(char const *str,int ch);

注意它们的第2个参数是一个整型值。但是,它包含了一个字符值。strchr在字符串str中查找字符ch1此出现的位置,找到后函数返回一个指向该位置的指针。如果该字符并不存在于字符串中,函数就返回一个NULL指针。strchr的功能和strchr基本一致,只是它所返回的是一个指向字符串中该字符最后一次出现的位置(最右边那个)

查找任何几个字符

char* strpbrk(char const *str,char const *group);这个函数查找任何一组字符第一次在字符串中出现的位置。返回一个指向str中第1个匹配group中任何一个字符的字符位置。如果未找到匹配,函数返回一个NULL指针。

查找一个字串

char*strstr(char const *s1,char const*s2);这个函数在s1中查找整个s21次出现的起始位置,并返回一个指向该位置的指针。如果s2并没有完整地出现在s1的任何地方,函数将返回一个NULL指针。如果第2个参数是一个空字符串,函数就返回s1

查找一个字符串前缀

strspanstrcspn函数用于在字符串的起始位置对字符计数。

size_tstrspn(char const *str,char const *group);

size_tstrcspn(char const *str,char const *group);

其中group字符串指定一个或多个字符。strspn返回str起始部分匹配group中任意字符的字符数。例如,如果group包含了空格,制表符等空白字符,那么这个函数将返回str起始部分空白字符的数目。str的下一个字符就是它的第1个非空白字符。strcspn函数和strspn函数正好相反,它对str字符串起始部分中不与group中任何字符匹配的字符进行计数。

查找标记

char*strtok(char *str,char const *sep);sep参数是个字符串,定义了用作分隔符的字符集合。第1个参数指定一个字符串,它包含零个或多个由sep字符串中一个或多个分隔符分隔的标记。strtok找到str的下一个标记,并将其用NUL结尾,然后返回一个指向这个标记的指针。(strtok函数执行任务时,它将会修改它所处理的字符串。如果源字符串不能被修改,那就复制一份,将这份拷贝传递给strtok函数。)如果strtok函数的第1个参数不是NULL,函数将找到字符串的第1个标记。strtok同时将保存它在字符串中的位置。如果strtok函数的第1个参数是NULL,函数就在同一个字符串中从这个被保存的位置开始像前面一样查找下一个标记。如果字符串内不存在更多的标记,strtok函数就返回一个NULL指针。在典型情况下,在第一次调用strtok时,向它传递一个指向字符串的指针。然后,这个函数被重复调用(1个参数为NULL)直到它返回NULL为止。

动态分配内存

mallocfree,分别用于执行动态内存分配和释放。这些函数维护一个内存池。malloc从内存池中提取一块合适的内存,并向该程序返回一个指向这块内存的指针。这块内存此时并没有以任何方式进行初始化。malloc实际分配的内存有可能比你请求的稍微多一点。但是,这个行为是由编译器定义的,所以你不能指望它肯定会分配比你的请求更多的内存。如果内存池是空的,或者它的可用内存无法满足你的请求,malloc函数向操作系统请求,要求得到更多的内存,并在这块新内存上执行分配任务。如果操作系统无法向malloc提供更多的内存,malloc就返回一个NULL指针。对于要求边界对齐的机器,malloc所返回的内存的起始位置将始终能够满足对边界对齐要求最严格的类型的要求。

free的参数必须是NULL,要么是一个先前从malloccallocrealloc返回的值。向free传递一个NULL参数不会产生任何效果。传递给free的指针必须是一个从malloccallocrealloc函数返回的指针。传给free函数一个指针,让它释放一块并非动态分配的内存可能导致程序立即终止或在晚些时候终止。试图释放一块动态分配内存的一部分也有可能引起类似的问题,释放一块内存的一部分是不允许的。动态分配的内存必须整块一起释放。但是realloc函数可以缩小一小块动态分配的内存,有效地释放它尾部的部分内存。不要访问已经被free函数释放了的内存。

0 0
原创粉丝点击