




版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
C语言常见错误及问题分析C语言编程常见错误分析C语言是目前世界上最通用的编程语言之一,也是目前研发使用最多的编程语言。同各种各样的bug作斗争,是每一个C程序员每天所面临的课题。本文将从微观角度出发,对一些常见出错类型的案例进行分析,希望大家今后能避免类似的错误。数字和表达式错误变量的错误数组和指针的错误逻辑和流程的错误数字和表达式的错误运算符和优先级的错误字节序的错误魔鬼数字宏定义的错误sizeof的错误最常见的运算符错误就是“=”和“==”的误用intmain(){ intret; ret=GetVars(); if(ret=VOS_OK) { ...... } return0; }错误后果:1.变量被错误赋值。2.逻辑判断不正确。建议和结论:尽管是初级bug但是还是常有发生,建议写成“VOS_OK==ret”的形式,这样在编译的时候即可发现这种错误。“++”和“--”在表达式中的应用#definemypower(a)((a)*(a))intmain(){ inti=1,j=2; j=mypower(++i); printf("\r\n%i=dj=%d",i,j); return0; }错误后果:结果与期望的不一致建议与结论:1.对于“++”和“--”这种基本的知识还是应该掌握的2.自增和自减变量在本表达式中不要再引用,否则可能依赖编译器实现3.没有把握的用法千万不要用,否则可能导致意想不到的错误优先级问题也是编码初期容易出现的问题C语言有众多的运算符号,它们之间的优先级关系非常复杂,即使是一个熟练的C程序员,要清楚地记住这些优先级关系也绝非易事。if(high<<8|low)==>if((high<<8)|low)if(a|b&&a&c)==>if((a|b)&&(a&c))if(a|b==1)==>if((a|b)==1)建议和结论:不要使用默认优先级,使用括号来保证自己的运算优先级,不要考验字节的记忆力。字节序错误网络设备和网络协议的开发涉及到许多字节序问题。网络序:所有设备、系统都一样,表示字节在网络中的传输顺序,也就是设备接收、发送数据的顺序。数据总是按照从“高字节”===>“低字节”的顺序发。主机序:依赖于CPU,表示的是字节在内存中的存放顺序。对于Intel系列CPU:高字节存放高位,低字节存放低位;和网络序相反;一般称为“小尾”或者“小端”(littleendian)对于PPC系列CPU:高字节存放低位,低字节存放高位;和网络序相同;一般称为“大尾”或者“打端”(bigendian)对于32位int型数0x12345678,intel和ppc系列CPU的存放格式分别如下:假定有一种协议报文,报文类型为2个字节,某一特定类型为0xABCD两个因为字节序错误导致的真实案例案例一:某一种协议,其中一种报文类型是3,但是在代码中填写报文类型时没有进行字节序转换,导致对端总是识别报文类型为0x0300,于是对端认为是非法报文,直接作丢弃处理后果:当时使用的是Intel的CPU,这种错误类型的报文从来没有被处理过,当时也没有做单元测试,错误很久以后才被发现。因为这个报文是用来进行协议性能优化的,修改过后协议性能得到极大提升。案例二:某一种协议,报文中有一个字段用DWORD表示报文长度,但是在填写报文时遗漏了字节序转换,因为使用的是Intel的CPU,导致对端从收到的报文中提取出的报文长度为类似0x4800000000的巨大数字。我们字节的设备在接收时也遗漏了字节序转换,所以从收到的报文中反而能正确提取出报文长度,该设备也一直没有与cisco的设备做互通测试,使该问题一直没有暴露出来。后果:该设备第一次开局时,与cisco设备互通,结果周边所有cisco的设备异常重启(cisco当年设备的鲁棒性也比较差劲!)。字节序错误不只在报文中存在一.联合域定义:union{ ULONGulIP; UCHARszIP[4]; }stIPAddr;如果赋值:stIPAddr.szIP[0]=192;stIPAddr.szIP[1]=168;stIPAddr.szIP[2]=0;stIPAddr.szIP[3]=1;则大端和小端上stIPAddr.ulIP的值不同:大端:stIPAddr.ulIP=0xC0A80001;小端:stIPAddr.ulIP=0x0100A8C0;二.指针强转:定义如下变量:ULONGulTool=0x12345678;USHORT*pusTool=(USHORT*)&ulTool;UCHAR*pucTool=(UCHAR*)&ulTool;则在大端和小端上,*pusTool和*pucTool的值是不同的。大端系统:*pusTool=0x1234*pucTool=0x12小端系统:*pusTool=0x5678*pucTool=0x78建议和结论:1.填写报文和解析报文时一定要注意字节序问题,使用相应的宏操作(htonl、ntohl等)2.当取变量的一部分值时,如联合体、强制指针转换等,需要考虑字节序问题3.互通测试很重要魔鬼数字问题魔鬼数字是指直接使用数字,而不是使用预先定义好的宏、常量、枚举等,这是一种不好的编程习惯。如下:l=round/3.14159;DeadInt=HelloInt*4;pstPack=malloc(36);pTcp=pIp+20;constdoublePI=3.14159;l=round/PI;#defineDEADTIME4DeadInt=HelloInt*DEADTIME;#definePACKSIZE36pstPack=malloc(PACKSIZE);#defineIPHEADLEN20pTcp=pIp+IPHEADLEN;建议和结论:1.魔鬼数字是一种不好的编程习惯,一方面代码可读性差,另一方面在修改多出数字时容易造成遗漏,从而各处使用导致不一致。2.不是所有的数字都是魔鬼数字3.有明确意义的数字应该定义长宏、枚举或者常量,如申请的内存大小、函数的返回值、各种标记位等。宏定义错误宏定义最常见的问题就是没有使用足够的括号去保证展开的正确性。示例1:#definemul1(a,b)(a*b)#definemul2(a,b)((a)*(b))intmain(){ intx=0; x=mul1(1+2,5);/*x=11*/ x=mul2(1+2,5);/*x=15*/}示例2:#defineadd1(a,b)(a)+(b)#defineadd2(a,b)((a)+(b))intmain(){ intx=0; x=add1(1+2)*5;/*x=11*/ x=add2(1+2)*5;/*x=15*/}建议和结论:1.宏定义会忠实地进行展开,这个展开过程忽略运算符、优先级和函数。2.宏定义里面的算术表达式里面的各个参数需要加括号,整个表达式本身也需要加括号。Sizeof问题Sizeof是一个编译时处理的操作符,sizeof最常见的问题就是混淆了结构的体积和结构指针的体积。structtheNode{ inta; charb[20];}Node;...intx=0;structNode*pstNode;x=sizeof(Node);//x=24x=sizeof(pstNode);//x=4建议与结论:1.sizeof是编译器在编译时处理的,而不是在程序运行时处理的2.结构指针的体积与结构体的体积是两回事,在32位机上,指针一般都是32位长的(即4字节),而结构体的长度则依赖于结构体定义structtheNode{ intb[5]; shortc;}Node;sizeof(Node)=?另一个常见的错误是某些结构体定义没有正确使用#pragmapack,导致结构体体积的计算与理想有偏差。#pragmapack(1)structtheNode{ intb[5]; shortc;}Node;sizeof(Node)=?建议和结论:1.对齐有利于提高存储效率,常见的系统一般默认4字节或8字节对齐,编译时编译器将选取系统对齐和本结构中最常基础结构二者中的较小值作为该结构的实际对齐值。2.这个错误经常发生在定义报文结构是时,报文结构一般都应该按pack(1)来定义。变量的错误变量的类型和存储全局变量局部变量全局变量:定义在任何函数的外部,生命周期是在整个程序的周期内。-定义时没有初始化,或者初始化为0的全局变量存放在bss段(对于bss段,操作系统在加载时会自动全部清0)-定义时初始化为非0的全局变量存放在data段局部变量:定义在函数内,只能在所在函数内访问-静态局部变量存放在全局堆中,生命周期是在整个程序-普通局部变量存放在栈中,生命周期是在函数内注意:不管什么变量都要注意初始化问题,变量不初始化而直接作为右值使用是一个常犯的错误。全局变量在定义是初始化,会使app文件增大。因此,对于全局的大数组,应该尽量避免在定义是初始化,可以在程序执行的初始化阶段进行初始化。intarray[1000][1000]={1};intmain(intargc,char*argv[]){ return0;}用VC编出来的app大小大约为180Kintarray[1000][1000];intmain(intargc,char*argv[]){ return0;}用VC编出来的app大小大约为4.75M建议和结论:1.尽量避免对大的全局变量在定义时进行初始化,这样可以减小app大小,节省存储空间2.无论初始化与否,全局变量总是要占用运行时的内存空间,因此要避免定义不必要的大型全局变量普遍局部变量是存放在当前任务或系统栈中,因此避免定义过大的局部变量,从而使堆栈溢出。intmain(intargc,char*argv[]){ intarray[1000][1000]={0}; return0;}有什么问题?示例:intfunc1(){ inta[4000]; func2(0); ...
}intfunc1(){ intb[4000]; func3(0); ...}错误后果:1.在嵌入式设备上,每个任务的栈大小是有限的,一般在任务创建是指定(一般是4-40k),一旦发生栈空间溢出,容易造成栈被写坏,系统死机2.变量超大等错误无法通过编译等手段发现3.一旦发生栈被写坏,则函数调用栈也已被破坏,使得问题难以定位。建议和结论:1.编写代码时不要定义大的局部变量,如果必须要使用大的内存,则可以通过malloc从堆中申请。2.使用局部数组时,要谨防写越界。3.如果发生调用栈损坏,可以从局部变量超大和局部数组写越界这个思路开始追查,看看问题出现时,可能发生的调用栈。示例:char*func1(){ chararr[200]; strcpy(arr,"abcd"); returnarr;}错误后果:1.字符串的内存赋值到栈中,这个空间在func1返回后就不再有意义2.如果func1返回后继续向arr这个地址写入内容,则会造成栈写坏,可能导致系统崩溃建议与结论:1.局部变量一定不要超越其作用域的范围2.编码时,在函数返回指针时要特别注意,千万不要返回栈空间地址。思考:下面程序有什么问题?structQueueglobal_q;/*定义一个队列*/...voidospf_routing_calc(){ structQueueNoden; clean_queue(global_q);/*清空队列*/ ... EnQueue(global_q,n);/*入队列*/ ... if(err)/*出错返回*/ { return } clean_queue(global_q);/*清空队列*/ return;/*计算成功返回*/}建议与结论:1.局部变量一定不要超出其作用域的范围2.要特别关注函数中异常分支的处理,看看异常分支中有没有进行必要的资源回收和回退处理。数组和指针的错误访问越界指针释放错误指针移位错误数组和指针的混用访问越界数组和内存的访问越界是一类最常发生的错误,这类错误一旦发生,经常会引起内存链损坏、调用栈写坏等严重后果常见问题和建议:1.字符缓冲区:在进行字符串操作时(strcpy、strcat等),要确保目的缓冲区的大小足够大.(包括后面的‘\0’)2.报文缓冲区:需要考虑各种报文长度,如正常报文、畸形报文、非法报文等3.数组定义:数组大小已变化,而引用的地方没有作相应修改,引起访问越界。(一般是魔鬼数字导致修改不全,建议用宏来标识数组大小)4.数组下标:检查数组下标的合法性,防止下标过大导致数组访问越界。5.字符串的’\0’结尾:定义字符串数组时忘记后面的’\0’,是一种常见错误。如charstr[3]="abc";指针释放错误指针释放内存错误是非常大的一类错误,一代代的C程序员绞尽脑汁地同这些错误作斗争,在消灭错误的同时,他们也在不断创造新的错误!最简单的一类错误就是遗漏指针释放,导致内存泄漏。主要原因:1.异常处理分支、多个处理分支中遗漏内存释放和相关的资源回收处理。建议:a:关注各个分支的资源释放处理,尤其是新增加一个分支时。b:将资源释放集中处理,整合成一个流程。2.责任主体不清,接口设计没有明确释放主体a:设计定义接口时,要明确定义资源的申请者和释放者。引用已释放的指针也是最常见的一类错误。如:free(pIntf);printf("freeinterface%s",pIntf->name);建议与结论:1.即使是刚释放的内存,也不能再访问,因为里面的内容已经没有意义了。2.养成释放内存后,立即将指针设成NULL的良好习惯。指针赋值除了容易造成内存释放后再访问的问题外,还容易导致内存重复释放。pRoute->pIntf=pIntf;...free(pIntf);...if(NULL!=pRoute->pIntf){ ... theID=pRoute->pIntf->theID;/*错误访问*/ free(pRoute->pIntf)/*重复释放*/ ...}建议与结论:1.对于等价指针的情况,必须要严格明确申请者和释放者。2.在释放的时候要将所有的等价指针设成NULL指针移位错误指针可以通过加减运算进行推移。需要注意的是,指针的加、减运算都是基于它所指向的对象尺寸大小进行考虑的,常见的错误就是额外的计算了数据类型的体积。示例:intarr[100];int*p=arr;p=p+sizeof(int);/*错误*/p=p+1;/*正确*/建议和结论:1.指针偏移时,编译器已经考虑了对象的体积,编程者不需要再画蛇添足。2.void型指针由于编译器不知道其对象体积,所以不能进行加、减偏移运算指针和数组的混用指针和数组有相同之处,但是在绝大多数情况下二者含义是不同的,不可混淆。char*b="abc";。chara[4]="abc";1.给指针赋值为数组首地址,可以通过推移指针来访问数组各元素chara[4];char*p=a;通过a[1]和*(p+1)都可以正确访问数组2.函数使用数组作为参数时,数组地址只能以指针的方式传入,此时通过数组或指针的方式定义都是可以的。chara[4];intfunc(char*p){ ...}...ret=func(a);char*p=a;流程和逻辑的错误统计和计数的错误任务切换统计和计数错误统计包括很多,如报文数目统计、错误统计、各种表项统计等。统计计数的常见错误一般有以下几种情况:1.统计计数变量没有初始化。2.多个分支时,某些分支中遗漏统计。3.统计计数变量溢出统计计数错误引起的问题大多不太严重,但有时也可能导致严重的问题。一个真实的案例:某一子系统采用一个LONG型计数器记录系统启动到当前的毫秒数,并以此进行定时器调度,不幸的是,这个计数器没有进行溢出保护,也就是说0x7FFFFFFF/1000/3600/24=24.8天后,这个计数器将发生溢出。后果:这个设备在实验室从来没有这么长时间运行过,问题一直没有被发现;结果,一次开局,路由器在网上运行了二十多天,计数器发生溢出,定时器系统崩溃,系统重启。由于没有调用栈,重启时没有任何操作,问题很难定位;直到又过了二十多天,问题再次出现,研发人员发现间隔的时间惊人的一致,才发现了这个问题。建议与结论:1.统计计数虽然简单,但也不可以掉以轻心,分支流程容易遗漏,需要特别关注。2.对于计数器一定要考虑什么时候会溢出,以及溢出时的保护处理。任务切换1.在多任务编程时,需要考虑任务切换时对全局资源的保护处理。2.如果多个任务间有严格的时序要求,则需要保证各个任务间的同步,防止乱序而导致意想不到的结果。C语言程序设计常见问题1.下面代码有什么问题?当我试图访问p2是得到了错误,为什么?char*p1,p2;p2=(char*)0x80000000;作者的原因是定义两个char型的指针,但是上述代码实际上等价于:char*p1,p2;或者char*p1;charp2;因此作者在后面的指针赋值语句时会产生错误正确的定义法是:char*p1,*p2;2.C语言的关键字extern在函数的声明中起到什么作用?如:externintfunc(int);如果函数的声明中带有关键字extern,除了暗示这个函数可能在其他源文件里定义外,无其他作用。下面两个函数的声明没有明显区别:externintfunc(int);intfunc(int);3.下面两种对于定义string_t数据类型的方法,哪一种更好?typedefchar*string_t#definestring_tchar*通常讲,typedef要比#define好,尤其在对指针的处理上。示例:typedefchar*string_t1#definestring_t2char*string_t1s1,s2;string_t2s3,s4上述变量s1、s2、s3都被定义成了char*,而s4被定义成了char,而不是预期的char*。根本原因就是#define只是进行字符串的简单替换。除此之外,宏定义有#ifdef和#ifndef等用来进行逻辑判断,这是它的长处。4.typedef中的嵌套定义问题。下面定义有问题吗?typedefstructmystruct{ inta; MY_STRUCTURE*pnext;}MY_STRUCTURE;规范的做法是:typedefstructmystruct{ inta; structmystruct*pnext;};typedefstructmystructMY_STRUCTURE5.下面的方法定义数组有问题吗?constinta=5;intarray[a];这个问题讨论的是常量与只读变量的区别。常量,如5、“abc”等肯定是只读的,因为程序中根本没有地方存放它们的值,别说修改它了。而只读变量,则是在内存中开辟一个地方来存储它的值,只不过这个值不允许修改。C语言的const就是用来限定一个变量不允许修改的修饰符。上述代码的a被修饰为只读变量,但它本质上还是变量,而不是常量。C语言规定,数组的定义必须是常量,而不能是只读变量。6.下面的代码中编译器会报一个错误,为什么?typedefchar*charptr;charstr[4]="abc";constchar*p1=str;constcharptrp2=str;p1++;p2++;p2++有问题,会报错。因为constcharptrp2与#define不同,不是进行简单的字符串替换,它实际上定义的是一个常指针,即指针初始化后不允许改变。类似于constlongxyz;只不过charptr是我们自己定义的类型。7.为什么结构体变量不能用“==”和“!=”进行比较?C语言是一种低级语言,没有一种简单有效的办法实现结构体变量的比较。具体原因有两个:1)结构体对齐问题,导致的一些填充域,这些填充域可能是随机值,因此按字节比较;2)如果按域比较,结构体中若含有指针域,则指针域所指内容的比较则无法实现。因此结构体的比较往往需要各应用模块根据自己的需要专门写一个比较函数8.如何初始化一个联合体的任意成员?typedefunionmyunion{ intx; shorty;}UN;UNux={4,1};//很遗憾,标准C不支持初始化union的任意成员,而只能初始化它的第一个成员。9.下面代码能告诉我们b在a和c之间吗?if(a<b<c){ ...}上述代码实际等价于if((a<b)<c){ ...}意思是,取(a<b)判断的逻辑结果,然后再和c比大小。10.C语言的指针很重要,也很灵活,不过它到底有哪些好处?请列举1.方便使用动态分配的数组2.对相同类型或相似类型的多个变量进行通用访问3.变相改变函数的只传递特性,如将变量地址作为参数传入函数,这样就可以修改该变量的值4.动态扩展数据结构,如链表、hash表5.遍历数组6.节省函数调用代价,将参数尤其是大个的参数,按指针传递,以减少开销。7.…………11.下面的代码打印出来的结果是多少?inti,array[5],*ip;ip=array;for(i=0;i<5;i++){ array[i]=i;}printf("\r\n%d",*(ip+3*sizeof(int)));呵呵。。,具体打印什么我也不清楚。问题就出在ip+3*sizeof(int)上了。指针的相加实际上已经考虑了其类型的长度。示例:如果ip所指向的地址为0x80000000,则ip+1所指的地址0x80000004。因此本题中不需要再乘以sizeof(int)了。12.*p++到底是给谁加1?是给指针p还是p所指的内容加1?单目操作符和操作的结合顺序是:从右到左一个表达式中单目运算符的执行顺序是:从左到右因此本题中实际上是对指针p加1,而不是p的内容加1,如果要对p的内容加1,则用下面的表达式:(*p)++13.我有一个char*类型的指针,恰好指向一个int型,我想让指针跳过这个int,跳到下一个char,试问下面代码能否实现?(int*)p++;这种实现标准C不支持,但是主流编译器默认都会支持,如VC和gcc。虽然这么用一般不会有什么问题,不过还是建议大家不要这么用,上面问题可以直接用下面代码替代:p+=sizeof(int);14.为什么我不能对void*型的指针进行算术运算?如:void*p=array;p+=5;前面说过,对指针进行算术运算(指针加、减运算)时实际上已经考虑了该指针所指类型的大小了。同理,如果无法知道该指针类型的大小,则无法进行指针的算术运算。因为编译器根本不知道你这个指针所指的变量占几个字节,也就无法进行指针偏移。15.有些头文件中将NULL定义为0,为什么?NULL用于表示指针为空指针;一般用于指针变量的初始化。NULL的具体的机器表示也随机器而定,不过一般都是0.16.在源文件里定义了一个数组:intarray[5];我能否在另一个文件里声明一个指针,从而引用这个数组?externint*array;不可以,程序运行时会告诉你非法访问!指向类型T的指针并不等价于类型T的数组。正确的用法是:externintarray[];17.有人说数组名无法赋值,但是下面程序确实可以工作,难道我记错了?intfun(charsz[100]){ ... if('\0'==sz[0]) { sz=NONE; } ...}在C语言中,数组无法真正传递给函数,因而在编译器内部这个函数就被解释为:intfun(char*sz)因此,C语言函数参数若是数组,则实际传递的是一个指针,也就是该数组的首地址。因此上述函数当然没有问题。18.假定有一个整型数组a,则a和&a有什么区别?inta[2];a和&a有什么区别?a是指向数组第一个元素的指针,而&a则是指向整个数组的指针。intx,a[2];int*p=NULL;p=a;x=*p;p=&a;19.什么时候需要定义指向数组的指针而不是数组元素的指针?如何定义?intarray[2][3]={{1,2,3},{4,5,6}};如果需要对行进行遍历的话,就需要一个指向行的指针,定义如下:
intarray[2][3]={{1,2,3},{4,5,6}}; int(*p)[3]=0; //int*p2=0; p=&array[1]; //p2=array[1];20.有人写了一个将整数转换成字符串的函数,char*itoa(intn){ charbuf[20]; sprintf(buf,"%d",n); returnbuf;}如果按下面调用,有什么问题? char*p3; p3=itoa(5); { intc; c=1; } printf("%s",p3);char*itoa(intn)函数返回了栈空间的地址,要知道,栈空间的内容在该函数返回后就不再受到保护,也就是说此时函数的栈空间的已经被系统回收。如果函数返回了栈空间的内存,则很容易出现错误的结果,因为该空间可能已经分配给了其他函数或任务。21.为什么有些代码中malloc申请内存时的返回值总是强制类型转换一下,不转行不行?char*p=(char*)malloc(100);在C语言引入void*指针类型之前,这种强制转换主要是为了消除编译告警。在C语言引入了void*指针类型之后,这种转换已经没有必要,但是这是一种良好的编程习惯,建议大家继续保持这种强制转换,增加代码可读性。22.下面的程序有什么问题?#definePI3.14intfit_size(doublerount){ if((rount/PI==10)||(rount/PI==20)) { return1; } return0}此题同样是浮点数比较问题。浮点数不能直接与某一个整数进行比较,而是应该和一个范围比较。上面程序应该改成:#definePI3.14intfit_size(doublerount){ if((fabs(rount/PI-10)<0.000001)||(fabs(rount/PI-20)<0.000001)) { return1; } return0}23.下面这样的写法正确吗?intfun(inta){ intk=0; switch(a) { default: k=1; break; case1: k=2; break; case2: k=3; break; } return0}这种写法正确,因为switch……case
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 2025年电子合同法律适用与实践探讨
- 2025桥梁建设施工合同
- 2025建筑施工机械租赁合同模板
- 2025写字间租赁合同样本
- 2025个体健身房器材特许经营合同
- 2025商业大厦与装修公司合作的合同
- 2025临时建筑买卖合同模板
- 《2025机械设备租赁合同》
- 实习劳动合同方协议
- 风险代理合同范本
- 园林景观规划设计计费指导意见
- 35kV及以下电力电缆使用维护手册
- 2022年青海大学医学院附属藏医院医护人员招聘笔试模拟试题及答案解析
- 英语四级仔细阅读讲解及技巧
- 城市地理学-第八章城市空间分布体系
- 3,5-二甲基吡唑生产工艺规程
- 拆除工程安全的应急预案工程应急预案
- A4横线稿纸模板(可直接打印)
- 四线制方向电路
- 食堂干货类食材临时采购需求书
- 注射模具设计说明书
评论
0/150
提交评论