C语言编程疑点难点逐个剖析一.docx
《C语言编程疑点难点逐个剖析一.docx》由会员分享,可在线阅读,更多相关《C语言编程疑点难点逐个剖析一.docx(29页珍藏版)》请在冰豆网上搜索。
![C语言编程疑点难点逐个剖析一.docx](https://file1.bdocx.com/fileroot1/2023-4/16/22019bd4-c27d-4a82-b716-473c314b6125/22019bd4-c27d-4a82-b716-473c314b61251.gif)
C语言编程疑点难点逐个剖析一
1.C语言源文件的作用是什么?
源文件用于保存程序的实现 (implementation)。
2.C语言头文件的作用是什么?
头文件作为一种包含功能函数、数据接口声明的载体文件,用于保存程序的声明(declaration)。
头文件的主要作用在于调用库功能,对各个被调用函数给出一个描述,其本身不包含程序的逻辑实现代码,它只起描述性作用,告诉应用程序通过相应途径寻找相应功能函数的真正逻辑实现代码。
3.typedef的作用是什么?
用法?
在C/C++语言中,typedef常用来定义一个标识符及关键字的别名,它是语言编译过程的一部分,但它并不实际分配内存空间,实例像:
typedefintINT;
typedefintARRAY[10];
typedef(int*)pINT;
typedef可以增强程序的可读性,以及标识符的灵活性,但它也有“非直观性”等缺点。
typedef用来声明一个别名,typedef后面的语法,是一个声明。
用途一:
定义一种类型的别名,而不只是简单的宏替换。
可以用作同时声明指针型的多个对象。
比如:
char*pa,pb;//这多数不符合我们的意图,它只声明了一个指向字符变量的指针,
//和一个字符变量;
以下则可行:
typedefchar*PCHAR;//一般用大写
PCHARpa,pb;//可行,同时声明了两个指向字符变量的指针
用途二:
用在旧的C代码中(具体多旧没有查),帮助struct。
以前的代码中,声明struct新对象时,必须要带上struct,即形式为:
struct结构名对象名,如:
structtagPOINT1
{
intx;
inty;
};
structtagPOINT1p1;
而在C++中,则可以直接写:
结构名对象名,即:
tagPOINT1p1;
估计某人觉得经常多写一个struct太麻烦了,于是就发明了:
typedefstructtagPOINT
{
intx;
inty;
}POINT;
POINTp1;//这样就比原来的方式少写了一个struct,比较省事,尤其在大量使用的时候
或许,在C++中,typedef的这种用途二不是很大,但是理解了它,对掌握以前的旧代码还是有帮助的,毕竟我们在项目中有可能会遇到较早些年代遗留下来的代码。
用途三:
用typedef来定义与平台无关的类型。
比如定义一个叫REAL的浮点类型,在目标平台一上,让它表示最高精度的类型为:
typedeflongdoubleREAL;
在不支持longdouble的平台二上,改为:
typedefdoubleREAL;
在连double都不支持的平台三上,改为:
typedeffloatREAL;
也就是说,当跨平台时,只要改下typedef本身就行,不用对其他源码做任何修改。
标准库就广泛使用了这个技巧,比如size_t。
另外,因为typedef是定义了一种类型的新别名,不是简单的字符串替换,所以它比宏来得稳健(虽然用宏有时也可以完成以上的用途)。
用途四:
为复杂的声明定义一个新的简单的别名。
方法是:
在原来的声明里逐步用别名替换一部分复杂声明,如此循环,把带变量名的部分留到最后替换,得到的就是原声明的最简化版。
举例:
1.原声明:
int*(*a[5])(int,char*);
变量名为a,直接用一个新别名pFun替换a就可以了:
typedefint*(*pFun)(int,char*);
原声明的最简化版:
pFuna[5];
2.原声明:
void(*b[10])(void(*)());
变量名为b,先替换右边部分括号里的,pFunParam为别名一:
typedefvoid(*pFunParam)();
再替换左边的变量b,pFunx为别名二:
typedefvoid(*pFunx)(pFunParam);
原声明的最简化版:
pFunxb[10];
3.原声明:
doube(*)()(*e)[9];
变量名为e,先替换左边部分,pFuny为别名一:
typedefdouble(*pFuny)();
再替换右边的变量e,pFunParamy为别名二
typedefpFuny(*pFunParamy)[9];
原声明的最简化版:
pFunParamye;
理解复杂声明可用的“右左法则”:
从变量名看起,先往右,再往左,碰到一个圆括号就调转阅读的方向;括号内分析完就跳出括号,还是按先右后左的顺序,如此循环,直到整个声明分析完。
举例:
int(*func)(int*p);
首先找到变量名func,外面有一对圆括号,而且左边是一个*号,这说明func是一个指针;然后跳出这个圆括号,先看右边,又遇到圆括号,这说明(*func)是一个函数,所以func是一个指向这类函数的指针,即函数指针,这类函数具有int*类型的形参,返回值类型是int。
int(*func[5])(int*);
func右边是一个[]运算符,说明func是具有5个元素的数组;func的左边有一个*,说明func的元素是指针(注意这里的*不是修饰func,而是修饰func[5]的,原因是[]运算符优先级比*高,func先跟[]结合)。
跳出这个括号,看右边,又遇到圆括号,说明func数组的元素是函数类型的指针,它指向的函数具有int*类型的形参,返回值类型为int。
也可以记住2个模式:
type(*)(….)函数指针
type(*)[]数组指针
———————————
陷阱一:
记住,typedef是定义了一种类型的新别名,不同于宏,它不是简单的字符串替换。
比如:
先定义:
typedefchar*PSTR;
然后:
intmystrcmp(constPSTR,constPSTR);
constPSTR实际上相当于constchar*吗?
不是的,它实际上相当于char*const。
原因在于const给予了整个指针本身以常量性,也就是形成了常量指针char*const。
简单来说,记住当const和typedef一起出现时,typedef不会是简单的字符串替换就行。
陷阱二:
typedef在语法上是一个存储类的关键字(如auto、extern、mutable、static、register等一样),虽然它并不真正影响对象的存储特性,如:
typedefstaticintINT2;//不可行
编译将失败,会提示“指定了一个以上的存储类”。
4.#difine的作用是什么?
#define的用法:
不加分号。
#define为一宏定义语句,通常用它来定义常量(包括无参量与带参量),以及用来实现那些“表面似和善、背后一长串”的宏,它本身并不在编译过程中进行,而是在这之前(预处理过程)就已经完成了,但也因此难以发现潜在的错误及其它代码维护问题,它的实例像:
#defineINTint
#defineTRUE1
#defineAdd(a,b)((a)+(b));
#defineLoop_10for(inti=0;i<10;i++)
在ScottMeyer的EffectiveC++一书的条款1中有关于#define语句弊端的分析,以及好的替代方法,大家可参看。
5.条件编译的作用是什么?
如何使用?
三种预处理:
宏定义、文件包含、条件编译
条件编译属于三种预处理中的一种,条件编译的最主要目的是防止头文件的重复包含和编译,例如:
一个c文件包含同一个h文件多次,如果不加#ifndef宏定义,会出现变量重复定义的错误。
条件编译常用的有四个预处理命令:
#if、#else、#elif、#endif。
#if指令的形式为:
1
2
3
#if 常量表达式
代码块
#endif
#if后面的常量表达式为值,则编译它与#endif之间的代码,否则跳过这些代码。
指令#endif标识一个#if块的结束。
#else被使用来标志#if的末尾和#else块的开始。
这是必须的,因为任何#if仅有一个#endif与之关联。
#elif意指"elseif",它形成一个ifelseif嵌套语句用于多种编译选择。
#elif后面跟一个常量表达式,如果表达式是真,则编译其后的代码块,不对其他#elif表达式进行检测,否则顺序测试下一块。
常见的形式如下:
形式1:
1
2
3
4
5
#ifdef 标识符
/*程序段 1*/
#else
/*程序段 2*/
#endif
它的作用是当标识符已经由#define定义过了,则编译程序段1,否则编译程序段2,也可以使用简单形式
1
2
3
#ifdef 标识符
/*程序段1*/
#endif
形式2:
1
2
3
4
5
6
#ifndef 标识符
#define 标识符
/*程序段 1*/
#else
/*程序段 2*/
#endif
它的作用是当标识符没有由#define定义过,则编译程序段1,否则编译程序段2,也可以使用简单形式
1
2
3
4
#ifndef 标识符
#define 标识符
/*程序段 1*/
# endif
形式3:
1
2
3
4
5
#if 表达式
/*程序段 1*/
#else
*程序段 2*/
# endif
它的作用是当“表达式”值为真时编译程序段1。
否则则编译程序段2,也可以使用简单形式
1
2
3
# if 表达式
/*程序段 1*/
# endif
形式4:
1
2
3
4
5
6
7
8
#if 表达式1
/*程序段 1*/
#elif 表达式2
/*程序段 2*/
............
#elif 表达式n
/*程序段n */
#endif
它的作用是当“表达式1”值为1时编译程序段1,表达式2的值为真是编译程序段2,否则依次顺序判断到表达式n。
最后,条件编译的条件是一个常量表达式,支持逻辑与&&和或||运算。
以上四种形式的条件编译预处理结构都可以嵌套使用,
标识符:
在理论上来说可以是自由命名的,但每个头文件的这个标识符都应该是唯一的。
标识的命名规则一般是头文件名全大写,前后加下划线,并把文件名中的“.”也变成下划线,如:
stdio.h。
1
2
3
4
#ifndef _STDIO_H_
#define _STDIO_H_
/*程序段 */
#endif
6.typedef与#define的区别?
从以上的概念便也能基本清楚,typedef只是为了增加可读性而为标识符另起的新名称(仅仅只是个别名),而#define原本在C中是为了定义常量,到了C++,const、enum、inline的出现使它也渐渐成为了起别名的工具。
有时很容易搞不清楚与typedef两者到底该用哪个好,如#defineINTint这样的语句,用typedef一样可以完成,用哪个好呢?
我主张用typedef,因为在早期的许多C编译器中这条语句是非法的,只是现今的编译器又做了扩充。
为了尽可能地兼容,一般都遵循#define定义“可读”的常量以及一些宏语句的任务,而typedef则常用来定义关键字、冗长的类型的别名。
宏定义只是简单的字符串代换(原地扩展),而typedef则不是原地扩展,它的新名字具有一定的封装性,以致于新命名的标识符具有更易定义变量的功能。
请看上面第一大点代码的第三行:
typedef(int*)pINT;
以及下面这行:
#definepINT2int*
效果相同?
实则不同!
实践中见差别:
pINTa,b;的效果同int*a;int*b;表示定义了两个整型指针变量。
而pINT2a,b;的效果同int*a,b;表示定义了一个整型指针变量a和整型变量b。
7.函数的形参含有指针的情况,要判断该指针是否为NULL?
1、若函数的形参接收到的是一个空指针,则函数 对该空指针的间接访问是非法的。
2、判断指针是否为NULL指针的方法有两种:
①if(s==NULL)returnfalse; ②if(!
s) returnfalse;
例如:
intmain()
{
char*s=NULL;
if(s==NULL){
printf("nocontent\n");
}else{
printf("%s\n",s);
}
if(!
s){
printf("nocontent\n");
}else{
printf("%s\n",s);
}
return0;
}
显示的是:
nocontent
nocontent
8.异常和断言的区别?
作者:
晨池
链接:
来源:
知乎
著作权归作者所有。
商业转载请联系作者获得授权,非商业转载请注明出处。
关于异常和断言,个人以为,阐述最清楚的当属“契约式编程”。
简而言之,检查前条件使用ASSERT,检查后条件使用异常。
以一个函数为例,它要求在开始执行的时候满足一系列条件,这些条件被称为“前条件”或者“先验条件”,比如,参数不为空,某全局变量应该为1,等等。
不满足前条件,是不能调用此函数的,如果出现了前条件不满足仍然调用了此函数,可以认为这是一个设计错误。
检查前条件,可以使用ASSERT。
这个函数执行以后,也会满足一系列条件,这些条件被称为“后条件”或者“后验条件”,比如返回值满足什么关系,某全局变量设置称为什么什么,等等。
这应该是函数执行的结果,在前条件满足的情况下,后条件如果没有满足是一种不正常的情况,那么使用异常来处理。
比如函数strcpy,它的前条件是1、第一个参数是一个NULL结尾的字符串;2、第二个参数所指向的内存空间足够用来复制。
那么我们可以用ASSERT来检查是否满足这两个条件。
它的后条件应该是1、第二个参数所指向的内存空间中的字符串和第一个参数里面的是一样的。
在函数执行完毕以后,我们可以检查一下是否一样,如果不一样,可以抛异常(比如当时这个空间正好被另外一个线程写入了一些东西导致异常,或者由于系统原因这块内存同时也分给了别人等等)。
当然实际上strcpy处于效率考虑没有做这些检查。
契约式编程中前条件不满足是程序错误,需要修改,这也与一般理解中出现ASSERT是程序错误符合;后条件不满足往往也是一些意外原因,也和一般理解中出现异常是意外情况符合。
但是契约式编程还有一个问题是“不变式”,不变式指在函数执行之前和之后都不发生变化,我理解,在函数执行以前可以用ASSERT对不变式进行检查,在执行以后如果发现不变式发生了变化,那么应该抛出异常。
断言表示程序写错了,只要发生断言(更正:
此处应为断言失败),意味着至少有一个人得修改代码。
它的性质如同编译错误。
例如一个函数规定某输入参数非空,来个断言。
如果调用者送了空参数触发断言失败,要么调用方改代码不传空参数,要么被调用方改代码允许空参数处理。
如果代码书写完全正确,但因外界环境或者用户操作仍然可能发生的事件,都不适合用断言,可以使用异常,或者条件判断处理。
至于异常,对不同语言来说含义不同。
不可一概而论。
作者:
pansz
链接:
来源:
知乎
著作权归作者所有。
商业转载请联系作者获得授权,非商业转载请注明出处。
9.assert断言的作用
程序一般分为Debug版本和Release版本,Debug版本用于内部调试,Release版本发行给用户使用。
断言assert是仅在Debug版本起作用的宏,它用于检查“不应该”发生的情况。
如果是Release版本,编译器忽略所有的断言(看成空语句)。
示例6-5是一个内存复制函数。
在运行过程中,如果assert的参数为假,那么程序就会中止(一般地还会出现提示对话,说明在什么地方引发了assert)。
“我断言这是真!
如果不是真,那么就终止程序并发出错误报告!
”
这个宏检查传给它的表达式是否非零,如果不是非零值(即是0),就会发出一条出错信息并调用abort(终止程序)。
只在调试期间有效。
assert一般只用于内部函数对参数有效性进行检查,如果该函数作为一个外部接口来使用时,一般需要利用if,else语句进行防错设计。
assert(0&&"strcpyerror"); 的意义是什么?
难道就是为了提示出错信息?
void*memcpy(void*pvTo,constvoid*pvFrom,size_tsize)
{
assert((pvTo!
=NULL)&&(pvFrom!
=NULL));//使用断言
byte*pbTo=(byte*)pvTo;//防止改变pvTo的地址
byte*pbFrom=(byte*)pvFrom;//防止改变pvFrom的地址
while(size-->0)
*pbTo++=*pbFrom++;
returnpvTo;
}
示例6-5复制不重叠的内存块
assert不是一个仓促拼凑起来的宏。
为了不在程序的Debug版本和Release版本引起差别,assert不应该产生任何副作用。
所以assert不是函数,而是宏。
程序员可以把assert看成一个在任何系统状态下都可以安全使用的无害测试手段。
如果程序在assert处终止了,并不是说含有该assert的函数有错误,而是调用者出了差错,assert可以帮助我们找到发生错误的原因。
很少有比跟踪到程序的断言,却不知道该断言的作用更让人沮丧的事了。
你化了很多时间,不是为了排除错误,而只是为了弄清楚这个错误到底是什么。
有的时候,程序员偶尔还会设计出有错误的断言。
所以如果搞不清楚断言检查的是什么,就很难判断错误是出现在程序中,还是出现在断言中。
幸运的是这个问题很好解决,只要加上清晰的注释即可。
这本是显而易见的事情,可是很少有程序员这样做。
这好比一个人在森林里,看到树上钉着一块“危险”的大牌子。
但危险到底是什么?
树要倒?
有废井?
有野兽?
除非告诉人们“危险”是什么,否则这个警告牌难以起到积极有效的作用。
难以理解的断言常常被程序员忽略,甚至被删除。
10.预编译有很多有用的功能,你会用预编译么?
11.栈stack和堆heap分别指什么?
简单的来讲,stack上
分配的内存系统自动释放,heap上分配的内存,系统不释放,哪怕程序退出,那
一块内存还是在那里。
stack一般是静态分配内存,heap上一般是动态分配内存。
由malloc系统函数分配的内存就是从堆上分配内存。
从堆上分配的内存一定要自
己释放。
用free释放,不然就是术语--"内存泄露"(或是"内存漏洞")--Memory
Leak。
于是,系统的可分配内存会随malloc越来越少,直到系统崩溃。
还是来看
看"栈内存"和"堆内存"的差别吧。
栈内存分配
-----
char*
AllocStrFromStack()
{
charpstr[100];
returnpstr;
}
堆内存分配
-----
char*
AllocStrFromHeap(intlen)
{
char*pstr;
if(len<=0)returnNULL;
return(char*)malloc(len);
}
对于第一个函数,那块pstr的内存在函数返回时就被系统释放了。
于是所返回的
char*什么也没有。
而对于第二个函数,是从堆上分配内存,所以哪怕是程序退出
时,也不释放,所以第二个函数的返回的内存没有问题,可以被使用。
但一定要调
用free释放,不然就是MemoryLeak!
在堆上分配内存很容易造成内存泄漏,这是C/C++的最大的"克星",如果你的程序
要稳定,那么就不要出现MemoryLeak。
所以,我还是要在这里千叮咛万嘱付,在
使用malloc系统函数(包括calloc,realloc)时千万要小心。
12.malloc和free的操作规则。
对于malloc和free的操作有以下规则:
1)配对使用,有一个malloc,就应该有一个free。
(C++中对应为new和delete)
2)尽量在同一层上使用,不要像上面那种,malloc在函数中,而free在函数外。
最好在同一调用层上使用这两个函数。
3)malloc分配的内存一定要初始化。
free后的指针一定要设置为NULL。
注:
虽然现在的操作系统(如:
UNIX和Win2k/NT)都有进程内存跟踪机制,也
就是如果你有没有释放的内存,操作系统会帮你释放。
但操作系统依然不会释放你
程序中所有产生了MemoryLeak的内存,所以,最好还是你自己来做这个工作。
(有的时候不知不觉就出现MemoryLeak了,而且在几百万行的代码中找无异于
海底捞针,Rational有一个工具叫Purify,可能很好的帮你检查程序中的Memory
Leak)
13.h和c文件的使用
H文件和C文件怎么用呢?
一般来说,H文件中是declare(声明),C文件中是
define(定义)。
因为C文件要编译成库文件(Windows下是.obj/.lib,UNIX下
是.o/.a),如果别人要使用你的函数,那么就要引用你的H文件,所以,H文件中
一般是变量、宏定义、枚举、结构和函数接口的声明,就像一个接口说明文件一
样。
而C文件则是实现细节。
H文件和C文件最大的用处就是声明和实现分开。
这个特性应该是公认的了,但
我仍然看到有些人喜欢把函数写在H文件中,这种习惯很不好。
(如果是C++
话,对于其模板函数,在VC中只有把实现和声明都写在一个文件中,因为VC不
支持export关键字)。
而且,如果在H文件中写上函数的实现,你还得在makefile
中把头文件的依赖关系也加上去,这个就会让你的makefile很不规范。
最后,有一个最需要注意的地方就是:
带初始化的全局变量不要放在H文件中!
14.函数参数中的const
对于一些函数中的指针参数,如果在函数中只读,请将其用const修饰,这样,别
人一读到