论如何写出超凡脱俗的C程序代码

前言:

一:

在这我先给大家讲述一下我的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”,这是什么原因呢?原因出在源地址区间和目的地址区间有重叠的地方这就是内存的重叠拷贝,这个留给读者自己思考,我会在我的下篇博客中谈论

如果以上有错误,希望广大读者能够指明。

 

 

 

 

 

 

 

猜你喜欢

转载自blog.csdn.net/genzld/article/details/80326978