C企业要求与实践book.doc_第1页
C企业要求与实践book.doc_第2页
C企业要求与实践book.doc_第3页
C企业要求与实践book.doc_第4页
C企业要求与实践book.doc_第5页
已阅读5页,还剩58页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

C企业要求与实践(讲义)第1章 C语言基础1.1 main函数的参数与返回值1.1.1 main的参数命令行界面的程序,通常都需要输入命令行参数帮助程序执行。假定有一个可执行程序名为test。那么运行该程序的的命令行如下:test带命令行参数是同一行中的附加项:test c TEST其中 c 和 TEST就是命令行参数。C程序可以将这些附加参数读出来,并为自己所用,比如作为程序运行的条件(经常看到调试参数 D 就是这么一个)。C程序通过使用main()的参数来读取这些附加参数,下面的repeat.c给出一个读出main参数的例子:例11:#include #include int main(int argc, char *argv) int count; printf(The command line has %d arguments:n, argc - 1); for(count = 1; count =”或“=-EPSINON) & (x=EPSINON)其中EPSINON是允许的误差(即精度)。l 指针变量与零值比较应当将指针变量用“=”或“!=”与NULL比较。指针变量的零值是“空”(记为NULL)。尽管NULL的值与0相同,但是两者意义不同。假设指针变量的名字为p,它与零值比较的标准if语句如下:if (p = NULL)/ p与NULL显式比较,强调p是指针变量if (p != NULL)不要写成if (p = 0) / 容易让人误解p是整型变量if (p != 0) 或者if (p)/ 容易让人误解p是布尔变量if (!p)l 对if语句的补充说明有时候我们可能会看到 if (NULL = p) 这样古怪的格式。不是程序写错了,是程序员为了防止将 if (p = NULL) 误写成 if (p = NULL),而有意把p和NULL颠倒。编译器认为 if (p = NULL) 是合法的,但是会指出 if (NULL = p)是错误的,因为NULL不能被赋值。程序中有时会遇到if/else/return的组合,应该将如下不良风格的程序if (condition)return x;return y;改写为if (condition)return x;elsereturn y;或者改写成更加简练的 return (condition ? x : y);在使用运算符&的表达式,要尽量把最有可能为false的子表达式放在&的左边;同样在使用运算符 | 的表达式,要尽量把最有可能为TRUE的子表达式放在 | 的左边。因为C语言对逻辑表达式的判断采取“突然死亡法”;如果&左边的子表达式计算结果为FALSE,则整个表达式就为FALSE,后面的字表达式没有必要再计算;如果 | 左边的子表达式计算结果为TRUE,则整个表达式就为TRUE,因此后面的子表达式没有必要再计算。者可以提高程序的执行效率。1.3.2 switch分支有了if语句为什么还要switch语句?switch是多分支选择语句,而if语句只有两个分支可供选择。虽然可以用嵌套的if语句来实现多分支选择,但那样的程序冗长难读。这是switch语句存在的理由。switch语句的基本格式是:switch (variable)case value1 : break;case value2 : break;default : break;l 每个case语句的结尾不要忘了加break,否则将导致多个分支重叠(除非有意使多个分支重叠)。l 不要忘记最后那个default分支。即使程序真的不需要default处理,也应该保留语句default : break; 这样做并非多此一举,而是为了防止别人误以为你忘了default处理。1.4 循环结构C循环语句中,for语句使用频率最高,while语句其次,do语句很少用。本节重点论述循环体的效率。提高循环体效率的基本办法是降低循环体的复杂性。在多重循环中,如果有可能,应当将最长的循环放在最内层,最短的循环放在最外层,以减少CPU跨切循环层的次数。例如示例1.4(b)的效率比示例1.4(a)的高。for (row=0; row100; row+)for ( col=0; col5; col+ )sum = sum + arowcol;for (col=0; col5; col+ )for (row=0; row100; row+) sum = sum + arowcol;示例1.4(a) 低效率:长循环在最外层 示例1.4(b) 高效率:长循环在最内层如果循环体内存在逻辑判断,并且循环次数很大,宜将逻辑判断移到循环体的外面。示例1.4(c)的程序比示例1.4(d)多执行了N-1次逻辑判断。并且由于前者老要进行逻辑判断,打断了循环“流水线”作业,使得编译器不能对循环进行优化处理,降低了效率。如果N非常大,最好采用示例1.4(d)的写法,可以提高效率。如果N非常小,两者效率差别并不明显,采用示例1.4(c)的写法比较好,因为程序更加简洁。for (i=0; iN; i+)if (condition) DoSomething();else DoOtherthing();if (condition)for (i=0; iN; i+) DoSomething();else for (i=0; i 0 )*pbTo + = *pbFrom + ;return pvTo;为了不在程序的Debug版本和Release版本引起差别,assert不应该产生任何副作用。所以assert不是函数,而是宏。程序员可以把assert看成一个在任何系统状态下都可以安全使用的无害测试手段。如果程序在assert处终止了,并不是说含有该assert的函数有错误,而是调用者出了差错,assert可以帮助我们找到发生错误的原因。如果搞不清楚断言检查的是什么,就很难判断错误是出现在程序中,还是出现在断言中。幸运的是这个问题很好解决,只要加上清晰的注释即可。这本是显而易见的事情,可是很少有程序员这样做。这好比一个人在森林里,看到树上钉着一块“危险”的大牌子。但危险到底是什么?树要倒?有废井?有野兽?除非告诉人们“危险”是什么,否则这个警告牌难以起到积极有效的作用。难以理解的断言常常被程序员忽略,甚至被删除。l 在函数的入口处,使用断言检查参数的有效性(合法性)。l 在编写函数时,要进行反复的考查,并且自问:“我打算做哪些假定?”一旦确定了的假定,就要使用断言对假定进行检查。第3章 指针、数组和字符串3.1 指针的本质程序是由指令和数据组成的,其中数据在程序运行时是存放在内存单元中的,指针的本质就是这个内存单元的地址。首先,指针是变量,它和常用的整型变量、字符变量等没有本质区别,不同的是整型变量的值是整型,字符变量的值是字符类型,而指针变量的值是一个内存单元的地址,即通常是一个32位的二进制数字(32位系统)。通过下面的程序,我们可以查看指针的真实面貌。例3.1:#include int main( int argc, char *argv )int i = 15;char c = 0;float f = i;int *pi = &i;int *ppi = πprintf(Address of i is : 0x%pn, &i);printf(Value of pi is : 0x%pn, pi);printf(Address of pi is : 0x%pn, &pi);printf(Value of pi is : 0x%pnn, ppi);printf(value of c is : %cn, c);printf(Address of c is : 0x%pnn, &c);printf(value of f is : %fn, f);printf(Address of f is : 0x%pnn, &f);getch();输出结果:Address of i is : 0x0012FF7CValue of pi is : 0x0012FF7CAddress of pi is : 0x0012FF70Value of ppi is : 0x0012FF70value of c is : 0Address of i is : 0x0012FF78value of f is :15.000000Address of i is : 0x0012FF74观察输出结果,变量pi的值就是i变量的地址的值,变量ppi的值就是pi变量的地址的值。通过这个例子可以了解到指针的本质,可以将指针变量看作一个32位的二进制整数,而这个整数正是一个内存单元的地址,通过访问这个内存地址可以访问到该指针指向的内存单元的值。3.2 指针的类型及其支持的运算指针的类型实际上指针指向的内存单元所存放的数据的类型。int *pInt;int *ppInt;char *pChar;void *pVoid;pInt的类型是int *,也就是整型指针。ppInt的类型是int*,即整型指针的指针。pChar的类型是char*,即字符类型的指针。通常我们可以通过使用typedef把不同的指针类型定义下来,然后直接使用定义后的类型。typedef int *IntPtr;typedef int* IntPtrPtr;typedef char*CharPtr;typedef void*VoidPtr;为什么要使用上面的定义呢,通过下面的例子我们可以看到它的作用。例3.2:#include typedef int*IntPtr;int main( int argc, char *argv )int i = 15;int* ipa, ipb;IntPtr pa, pb;ipa = &i;ipb = &i;pa = &i;pb = &i;getch();在该程序中设置断点单步跟踪ipa,ipb,pa,pb这四个变量,从跟踪结果中可以发现ipa是int*类型,而ipb仍然是int类型;而pa、pb都是int*类型。这说明在语句:int* ipa,ipb; 中,*符号结合到了ipa中,实际上是等效于:int *ipa; int ipb;而使用IntPtr的方式来定义的两个变量等效于int* pa; int* pb; 因此如果需要将两个变量都定义为指针类型就需要使用IntPtr,在使用int*时需要注意其中*号的结合性。全局指针变量的默认初始值是NULL。而对于局部指针变量p,你必须显示地指定其初值,否则p的初始值是不可预测的(不是NULL)。当你第一次使用它的时候就可能会用if( p != NULL)来检查p的有效性,然而此时if语句的确不起作用。如果忘记给p赋初值,那么你第一次使用p的时候就会倒是运行时错误。作为一个好的编程习惯,不管指针变量是全局还是局部的、静态的还是非静态的,都应该要么为指针变量赋予一个有效的初始值,要么将其初始化为NULL。从指针的本质来分析,它实际上是一个整数,因此它应该可以进行整数能够参与的所有算术运算,但是由于它的本质是内存地址,所以很多算术运算对指针都没有意义。使用较多的运算如下:l 指针自增(+)l 指针自减(- -)l 指针加n(n为正整数)l 指针减n(n为正整数)l 指针比较(常用的是=和!= )l 指针赋值l 取地址和反引用例3.3:int a50;int* pa;int b=0;int* pb;pa = a; /指针赋值,a相当与a数组的地址,也可以看作指针pa+; /这时pa指向a数组的第二个元素,实际上地址不是加一 /而是加上了sizeof(int)pa+=20;/这时pa指向a数组的第二十二个元素,指针加上了20*sizeof(int)if( pa !=NULL)/指针比较b = *pa;/反引用,*pa表示pa所指的内存单元的值pb = &b;/取地址,pb取b变量的内存单元的地址需要注意void*类型的指针不能参与算术运算,只能进行赋值、比较和sizeof操作,同样也不能对void*类型的指针使用*符号来反引用,因为其指针所指的值类型无法确定因此无法对其取值。3.3 指针传递如果函数的参数或返回值被声明为指针类型,那么函数接受的参数或返回值就是地址,而不是指针所指的内存单元的值。例3.4:#include void funcptr( int *p)(*p)+;/将指针所指向的内存单元的值加一p+;/将指针本身加一,即指向a数组的下一个元素int main( int argc, char* argv)int a5=1,2,3,4,5;int *pa;pa = a;printf(Value of pa is :0x%pn, pa);printf(Value of a0 is :%dn, a0);funcptr( pa );printf(Value of pa is :0x%pn, pa);printf(Value of a0 is :%dn, a0);getch();输出结果为:Value of pa is: 0x0012FF6CValue of a0 is: 1Value of pa is: 0x0012FF6CValue of a0 is: 2在执行完函数funcptr()后,a0的值增加了一,而指针pa本身的内容并没有发生改变,也就是pa所指向的位置仍然是a0。这是为什么呢?我们知道函数的参数传递分为传值方式和传地址方式,传值方式的参数本身在函数内的修改在函数结束后不会被保存,而传地址方式参数的在函数内的修改在函数结束后被保存下来了。这里的函数funcptr()实际上就是采用的传地址方式,因为pa所指的值在函数结束后的确被修改了,但为什么pa指针本身的值的修改却没有被保存呢。实际上从严格的意义上来说函数的参数传递全部都是值传递,没有所谓的地址传递,参数本身在函数中的修改都是不会被保存的,因为进入函数时,参数的值都会被复制一个备份来在函数中使用,而不是直接使用参数,这样可以保证参数的值在退出函数时不会发生变化。所谓的传地址方式,实际上是将指针作为参数传递,那么参数实际上一个指针,也就是一个内存单元的地址(一个32位的整数),而不是参数所指向的内存单元的值,因此在函数中指针本身是被保护的,而指针所指向的内存单元的值并不是参数,它没有被作为参数来保护,所以上例中指针所指向的内存单元的值被修改了,而指针本身并没有被修改。下面是一个关于内存申请的函数的例子:例3.5:#include void allocate( char *p, int size )/申请size大小的内存空间,并将首地址赋值给pp = ( char * )malloc(size*sizeof(char);int main( int argc, char* argv)int *pa;pa = NULL;allocate( pa, 50 );if( pa = NULL )printf( value of pa is NULL !n);elseprintf(value of pa is :0x%pn, pa);getch();可能大家认为这个程序是没有问题的,但是allocate函数执行后,pa的值确为NULL。如果大家深刻理解了上一个例子,那么这个例子就容易分析了,原因就是allocate函数中将char* p作为参数,主程序中将pa作为参数传入,那么函数需要保护参数pa并保证pa的值(是其本身的值,即地址本身,而不是指针指向的内存单元的值)不被修改,而函数中对参数p做了赋值,而且这个赋值是直接对指针本身的修改,因此这个修改不会被保存。很多时候我们都需要一个像allocate这样的函数来完成内存的申请,那怎么样才能在函数中完成这一功能并将分配的内存的地址传递出函数呢?有两种方法:第一种方法是将这个地址作为返回值来传递出函数。例3.6:char* allocate1( size )return( (char * )malloc( size * sizeof(char) ) );allocate1函数直接返回了申请的内存的首地址,如果在主程序中使用pa来接收返回值就可以得到申请的内存的首地址了。第二种方法是使用指针的指针作为参数,因为将指针的指针作为参数时,函数保护的是指针的指针,而不会保护指针本身了。例3.7:#include void allocate2( char *p, int size )/申请size大小的内存空间,并将首地址赋值给*p*p = ( char * )malloc(size*sizeof(char);int main( int argc, char* argv)int *pa;pa = NULL;/取pa的地址,即指针的指针allocate2( &pa, 50 ); if( pa = NULL )printf( value of pa is NULL !n);elseprintf(value of pa is :0x%pn, pa);getch();allocate2函数将申请内存的地址传递给了*p,也就是pa,这样实现了内存首地址的参数传递。最后需要注意的是:我们可以将指针或指针的指针作为参数或变量进行传递,但是在传递的过程当中要注意指针所指向的地址一定要是有效的,如果访问的指针无效可能会使整个程序崩溃。如果使用指针接收malloc申请的内存的首地址,一定要在程序结束以前将申请的内存释放(通过free函数),不要在指针的传递或运算过程中丢失了首地址的值,或是忘记了内存释放,这样都会造成内存泄露,内存泄露会使你的程序出现问题,特别是在嵌入式设备上运行的程序,甚至会导致程序终止,这绝不是危言耸听。3.4 数组的本质任何数组,其所有元素的内存中都是连续字节存放的,也就是说保存在一大块连续的内存区中。数组元素的下标编号从0开始,最后一个元素的下标等于元素个数减一。下标必须是整数或整数的表达式。通过下标引用一个数组元素,在本质上和引用一个同类型的变量没什么区别,编译器通过下标值来计算你所引用的元素在内存中的地址。因此,在语义上,下标操作符返回的是一个元素的引用。int a5 = 1, 2, 3, 4, 5 ;int *pa = a;a3 = 100;假设数组a的地址为0x0012FF6C,如果需要计算a3,那么编译器会计算a3的地址,即a+3*sizeof(int) = 0x0012FF6C + 3*4 = 0x0012FF78,然后将0x0012FF78所指向的内存单元的值修改为100。当然,这个计算是由编译器来完成的,我们只需要理解它的完成方法就可以了。pa和a的值实际上是相同的,pa指向的就是数组a的首地址,那么a3也可以表示为 *(p+3), 对于这里的(p+3),编译器会自动将其转变为p + 3*sizeof(int),所以可以认为pa也是数组a的别名,甚至可以这样来访问:a3 和 pa3是等效的,*(a + 3)和*(p+3)也是等效的。也就是说数组名就是指针,但是a作为数组a的首地址,它是不能被修改的,而p是可以被修改的。a=a+1;p = p+1;上面第一个语句是错误的,a不能被修改,但是第二句是正确的。在声明数组时,一般有如下三种方式:l 明确地指出它的元素个数,编译器会按照给定的元素个数来分配存储空间。例如:int a10;l 不指明元素个数而直接初始化,编译器会根据你提供的初始值的个数来确定数组的元素个数。例如: int a = 1, 2, 3, 4, 5 ;l 同时指定元素个数并且初始化。例如: int a5 = 1, 2, 3, 4, 5 ;但是不允许既不指定元素个数、又不初始化,因为编译器不知道到底该为数组分配多少存储空间。另外需要注意的是定义数组时,不能使用变量来表示数组的个数,只能使用常数或宏替换。编译器在给数组分配内存空间的时候总是以用户指定的元素个数为准。如果初始值的个数多于指定的个数,则报错;如果用户没有指定元素个数,则编译器计算初始值的个数作为数组的元素个数;如果初始值个数小于指定的数组元素个数,则后面的元素全部自动初始化为0。不存在元素个数为0的数组。分辨下列语句对数组的定义是否正确:int b100; int c = 1, 2, 3, 4, 5 ;int d5 = 1, 2, 3, 4, 5, 6, 7 ;int e10 = 5, 6, 7, 8, 9 ;int f10 = 5, , 12, , 2 ;在程序中对数组的下标通常使用一个变量来表示,编译器在编译时无法预知下标变量的具体值,因此编译器不会对下标的越界发出错误警告。比如数组a的定义为:int a5= 1, 2, 3, 4, 5 ;int i, j = 6; i = j;ai+;实际上上面的程序在编译时不会报错,但是在运行时由于i下标已经超出了数组的范围,会访问到无效的内存区域,会对程序造成无法预料的影响,通常会出现非法内存访问的错误。3.5 二维数组二维数组在C语言中都是以“行序优先”来存储元素的,例如数组a23总共有2*3=6个元素,这六个元素在内存中仍然是按照顺序分配存储的。存储的顺序依次为:a00,a01

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论