前言:
一:
在这我先给大家讲述一下我的C学习的经历以及经验。
第一阶段:
刚接触C语言,这种编程语言时,毋庸置疑当然是一头雾水,拿起一本C语言书就想把里面的所有的东西全部都啃完,非常的盲目!后来受到了我大学导师的教诲:每读一本书,最重要的是先把目录全部了解,然后进行模块划分(需要记录,总结,不是有一句话好脑子不如烂笔头吗),然后在编程时遇到问题,找到对应的章节进行查阅并且深深细究整理,这样不仅解决了问题,还提高了你的时间利用率和知识点的掌握!可能是我专业的问题,以及受导师的影响:后来我做任何事,事先都会进行总体把握,在进行模块划分,一部分一部分攻克。
第二阶段:当我们能够熟练的掌握C中的知识点时,那我们就一定能编写出成功的代码吗?这就像我们学驾驶证一样,我们把理论知识和驾车知识全部都理解了,那么你就能开车了吗?当然不是,你还需要能够正确启动车辆然后加上上车进行大量的正确的练习,这里强调的是正确的练习,也就是必须要有良好的习惯,最基本的前提是你必须能够启动车辆,也就是你能够编写出正确可运行的代码。
这方面我的心得是:就像你小时候刚学习走路的时间,你不多跌到几次!你怎么会知道你在哪种路况或者你迈出哪只脚时你容易跌倒,然后你才会注意!你只有在多次失败的基础上才能建立成功,也就是当你突破某个瓶颈时,你的代码正确率会大大提高,这就需要大量的训练。
第三阶段:当你能写出正确的代码时还是不够的,因为公司里面要求的不仅是正确性,还有最重要的效率问题,这里包括时间效率和空间效率,即:你编写出的代码精简,效率高,这是你需要大量的解析前辈们的代码,阅读查阅,并且每次将你的不理解之处细究出来,同时和大量训练结合。当你坚持以后,回过头来你会发现,你当初的代码实在是不堪入目。
第四阶段:这是最高境界,现在的我还处在二到三阶段的过渡期,所以这里就不给初学者分享太多该阶段(毕竟自己没有达到)
二:
每次成功都是在一次次的提升中实现的,下面我给大家介绍这种提升的含义。
想必大家在上C语言课的时间,老师都会让大家写一个函数,那就是字符串的拷贝函数,
这足够简单吧?相信很多同学很快就给出了答案,且大部分同学的代码会是这样的(绝大部分在校大学生)
程序1:
void MyMemCopy(char *dst ,char *str ,int count)
{
while(count--)
{
*dst++=*str++;
}
}
分析:在大学期间,仅仅靠上课讲的,,相比于哪些连函数声明都写不出来的,能够写出这些代码已经很不错了,那么上面的 函数有什么缺陷呢?如果我给出的不是字符串,而是整型一维数组,或者是结构体类型之下的,那么上面的函数就不能编译了
error C2664: 'MyMemMove' : cannot convert parameter 1 from 'TheStruct *' to 'char *'
这是什么问题呢,很容易地可知这是由于类型不匹配问题,也就是该函数只能接受特定的类型参数,那么我们需要的是他的通用性,也就是他必须能接受所有的类型!这里涉及到无类型指针。我们知道有一种特别的指针,任何类型的指针都可以对它赋值,那就是void *,
程序2:
void MyMemCopy(void *dst,void *str ,int count)
{
while(count--)
{
*(char*)dst=*(char*)str;
(char*)dst++;
(char*)str++;
}
}
解析:这里不仅涉及了无类型指针,(查阅无类型指针有哪些特性)在函数内部我们还对他进行了强制转换。这样我们就实现了一劳永逸,可以实现任何类型的copy。
这里还有几个细节需要强调,为了实现链式表达式,我们应该将返回值也改为void *。此外,如果我们不小心将“*(char *)dst = *(char *)str;”写反了,写成“*(char *)str = *(char *)dst;”编译照样通过,而为了找出这个错误又得花费不少时间。所以我们必须做一些处理,也就是使被拷贝内容不可改变,所有对str所指的内容赋值都应该被禁止。这里用到了const!在void *const str(这里涉及到了const的用法,本质以及修饰用法)
如果当你写反了,系统会提示你是不允许你对str进行赋值运算的
error C3892: 'str' : you cannot assign to a variable that is const
程序 3:
void *MyMemCopy(void *dst ,void *const str,const int count)
{
void *ret=dst;//我们定义一个无类型指针来记录dst所指向内存的首地址
while(count--)
{
*(char*)dst=*(char*)str;
++(char*)dst;
++(char*)str;
}
return ret;//返回该内存的首地址,并非是str
}
分析:有的同学看到这里可能会感觉现在的代码已经很完美了,可以解决所有拷贝问题了,那么现在再来考虑这样一种情况,有使用者这样调用库:MyMemCopy(NULL,str, count),这是完全可能的,因为一般来说这些地址都是程序计算出来的,那就难免会算错,出现零地址或者其它的非法地址也不足为奇。然后你的程序会马上被unpass掉,更糟糕的是你不知道你的代码问题出现在哪里,你还需要在大量的代码中解决这个bug。其实这是一个参数合法性的问题,我们只需要对其进行合理性检查。
程序4:
void *MyMemCopy(void *dst ,void *const str,const int count)
{
if(NULL==dst||NULL==str)
return NULL;
void *ret=dst;
while(count--)
{
*(char*)dst=*(char*)str;
++(char*)dst;
++(char*)str;
}
return ret;
}
分析:这里为什么要将NULL放在前呢?这与大家的习惯是不同,我们能够想象到,一些粗心的程序员,或许会出现这样的问题if(NULL=dst||NULL=str),这样将==误写成了=,如果不是NULL在前,那样的话编译仍然会通过,但是执行时就出现崩溃了,但是将NULL写在前,那么在编译时就不能通过,这里涉及到NULL的用法,自行查阅不过多解释。所以我们要养成良好的程序设计习惯:常量与变量作条件判断时应该把常量写在前面。不能给常量赋值!
程序4代码首先对参数进行合法性检查,如果不合法就直接返回,这样虽然程序unpass掉的可能性降低了,但是性能却大打折扣了,因为每次调用都会进行一次判断,特别是频繁的调用和性能要求比较高的场合,它在性能上的损失就不可小觑。
如果通过长期的严格测试,能够保证使用者不会使用零地址作为参数调用MyMemCopy函数,则希望有简单的方法关掉参数合法性检查。我们知道宏就有这种开关的作用,
程序5:
void * MyMemCopy(void *dst,const void *src,int count)
{
void *ret=dst;
#ifdef DEBUG
if (NULL==dst||NULL ==str)
{
return NULL;
}
#endif
while (count--)
{
*(char *)dst = *(char *)str;
++(char *)dst;
++(char *)src;
}
return ret;
}
分析:这里有涉及到了#define DEBUG的用法, 如果在调试时我们加入“#define DEBUG”语句,增强程序的健壮性,那么在调试通过后我们再改为“#undef DEBUG”语句,提高程序的性能。
但是事实上在标准库里已经存在类似功能的宏:assert,而且更加好用,它还可以在定义DEBUG时指出代码在那一行检查失败,而在没有定义DEBUG时完全可以把它当作不存在。assert(_expression_r)的使用非常简单,当_expression_r为0时,调试器就可以出现一个调试错误,有了这个好东西代码就容易多了。(查询assert的用法)
程序6:
void * MyMemCopy(void *dst,const void *str,const int count)
{
assert(dst);
assert(src);
void *ret=dst;
while (count--)
{
*(char *)dst = *(char *)str;
++(char *)dst;
++(char *)src;
}
return ret;
}
到目前为止,在语言层面上,我们的程序基本上没有什么问题了,那么是否真的就没有问题了呢?这就要求程序员从逻辑上考虑了,这也是优秀程序员必须具备的素质,那就是思维的严谨性,否则程序就会有非常隐藏的bug,就这个例子来说,如果用户用下面的代码来调用你的程序。
程序7:
void Test()
{
char p [256]= "hello,world!";
MyMemCopy(p+1,p,strlen(p)+1);
printf("%s\n",p);
}
如果你身边有电脑,你可以试一下,你会发现输出并不是我们期待的“hhello,world!”(在“hello world!”前加个h),而是“hhhhhhhhhhhhhh”,这是什么原因呢?原因出在源地址区间和目的地址区间有重叠的地方。这就是内存的重叠拷贝,这个留给读者自己思考,我会在我的下篇博客中谈论
如果以上有错误,希望广大读者能够指明。