现在的位置: 首页 > 综合 > 正文

是谁在愚弄程序员?

2013年08月15日 ⁄ 综合 ⁄ 共 4636字 ⁄ 字号 评论关闭

        今天参加了一个小小的笔试,试卷上有一些模棱两可的指针问题。回来做了测试,感觉结果很古怪,就去查了源码。感觉大失所望。为什么库函数的设计如此不合理呢?原来我们每天遵循着不合理的规则,出现了错误却时常埋怨自己。到底是谁的错呢?是谁在愚弄程序员呢?下面说说我遇到的问题,及库函数的一些问题,希望你能从中得到答案.。

今天的笔试题可以说全部是指针问题。总体感觉下来,体量太小,题目也比较简单,考不出真正的实力.不过经过仔细的分析,小题中却蕴含着大问题.下面我们看看其中两道我认为不太合理的题目:
void test()
{
    char a[10],b[10];
    int i;
    for(i = 0; i < 10; ++i)
    {
       b[i] = 'a';
    }
    strcpy(a,b);
}
指出程序中的错误:
刚开始一看,我就懵了.正让我从何说起呢。我们仔细看这个程序,它的不合理的地方是我们应该在数组b的最后一个位置放上一个'/0',这样才凸显合理性.然而程序中并没有放,我们也不能说他是错误吧.现在我们继续看程序:
strcpy(a,b);
把数组b里面的内容复制到a中.很明显这里有问题.因为我们在数组b中没有放置'/0',我们根本不知道数组b到底有多长.也就是说我们根本就无法知道数组a到底能不能放下数组b中的内容,并且很大很能是放不下的.也就是说数组a的空间太小了。那数组a该申请多大的空间呢?不知道.那么说这道题的错误该说是什么呢?经过这样的分析好像我们说数组中应该放一个'/0'.不过仔细想想缺少一个'/0'也该不算是一个错误把.呵呵…所以我认为这道题不是太好.
void test(const char* str)
{
    char a[10];
    int i;
    if (strlen(str) <= 10)
    {
       strcpy(a,str);   
    }
}
指出上面程序中的错误:
经过仔细的分析后,你可以看看上面的程序,你确定你找出所有的错误了吗?
我给出的答案是:将 <= 换成 < .你的答案是不是也是如此呢?如真是这样,那我们真是同病相怜啊.好的,废话不多说了,让我们一起看看这个程序的问题吧:
1: 你信任参数指针str吗?
    其实你不该信任它。我们看看他会是什么:
    ①:数组 ②: 正常有值的指针 ③: NULL指针 ④: 野指针.
哇,没想到吧.它可以是这么多东西.恐怖吗?别怕,下面我们分别看看这几种情况:
①: 数组
如果是数组,并且该数组正常有值的话,那么他有两种情况,第一,有’/0’.那整个程序就没问题了,我们不必修改任何地方.第二,没有’/0’.那么我可以说你惨了,无论我们怎么修改程序,都无法根本的解决问题了。为什么呢?如果一个字符数组中存有值,并且没有’/0’,那么我们根本不知道它有多大。即我们使用strlen()函数测出来的值一般不会是一个正确的值.那么我们怎么去修改程序也无法解决问题了。我们可以说这不是这一个函数该解决的问题,是程序员的问题了,我们传参时应该传递正确的参数.
②: 正常有值的指针.
这个问题很好解决,一般编译器会自动为我们的指针末尾加上一个’/0’。不过我们若是使用strlen()测试的话,’/0’是不会被包括在内的.所以此时我们应该为’/0’预留出一个空间.因此程序只需将<= 改为 < 就OK了.
③: NULL指针
若是遇到这个问题,它可以说是一个程序员的问题,也可以说是函数设计的问题.首先我们不应该将一个NULL指针传递给一个函数.这是一种不正确的行为.在说该函数的设计,它应该考虑到传递NULL指针这种情况,并且该种情况也很容易处理.只需:
if (str == NULL)
{
    return;
}
④: 野指针
遇到这种情况,你就投降吧.你无论如何设计函数,都无法解决问题的.这绝对是一个程序员的问题.这就是为什么很多经典的书籍都反复强调:
在我们定义一个指针时,如果不立即使用.最好给它初始化为NULL,如:
char *p = NULL;
在你使用完一个指针后,如果它是堆指针,一定要给他释放,目的是为了防止内存泄漏.但在我们释放完内存后千万别忘了给它赋值为NULL。以防止它变成野指针被误用.如:
char *p = (char*)malloc(16);
free(p);
p = NULL;
好了,至此四种情况我们都讨论完了,你说这个程序我们该如何修改呢?我想比较合适一点的答案是:
程序开始增加:
if (str == NULL)
{
    return;
}
然后将<= 改为 < .
分析其他无法处理的情况,应该是程序员的问题了.不属于该函数管理的范畴.
好了,说完我面试的两道小题,下面看看我们的库函数吧.我的库函数是在VC2008的安装文件夹里的一个src文件夹里找到的.里面有很多我们常用的库函数的源码.听到这里我想你一定垂涎雨滴,很想去看看大师们设计的库函数吧.是否是很想好好的瞻仰与模仿一番啊?
先别,等我说完,我相信你的这个欲望就不会那么强烈了.
1 strlen函数.
我们有一个小问题引出这个函数吧:
#include <stdio.h>
#include <string.h>
void main()
{
    char a[10];
    printf("%d/n",strlen(a));
}
大家猜猜这个程序的结果该是多少.在我的机器上每次输出的结果都是3.奇怪吗?其实如果你看了库函数就不觉得奇怪了:
size_t __cdecl strlen (
        const char * str
        )
{
        const char *eos = str;
 
        while( *eos++ ) ;
 
        return( eos - str - 1 );
}
while( *eos++ ) ;我们可以看出该函数是查到’/0’才结束的.而我们刚申请的数组a[10],由于我们并没有初始化,所以它里面还存有原来的值.里面可能有’/0’也可能没有.所以用strlen输出a的长度是随即的.不信我们可以做一个实验,改变a[10]中原有内存值,使strlen(a)输出一致结果.
#include <stdio.h>
#include <string.h>
void SetMemory()
{
    char a[100];
    int i;
    for (i = 0; i < 100; ++i)
    {
       a[i] = 0;
    }
}
void test()
{
    char a[10];
    printf("%d/n",strlen(a));
}
int main(int argc, char *argv[])
{
    SetMemory();
    test();
    return 0;
}
请大家在C编译器上运行此问题,答案为0.VC++6.0上我实验了不行.
从strlen函数源码中我们也可以看出它返回的长度不包括’/0’。
真不知这种寻找’/0’定长度的设计方法好不好.反正很多时候它会带给我们很多烦恼,不过除了这种设计没有其他更好的方法了.有的同学可能会说(其实我原来也这么想):那数组有多长他就给返回多长不就行了.
好的如果按照这正想法设计我们看看需要做些什么:
首先在设计函数时需要分两部分,一个是数组,一个是指针.
指针的时候又可以分为堆指针和栈指针。堆指针我们需要记录它申请的长度,然后返回。栈指针也就是说我们仅仅定义了一个指针还没有给他分配空间,那么我们该范围它的长度为多少呢?按0算吧.好的,那么现在我们去看数组,你说一个数组我们该如何知道它的长度呢,只有记录,单凭内存是看不出来的.我们需要记录数组申请时的长度.然后在strlen中查找返回就是了.好的,我们看看这样的设计好吗?我想是不错的,不过就是多占用了一些空间记录数组的长度,而指针的申请空间长度已经在记录了.
其实库函数的设计还有一个不合理的地方,那就是他不能处理NULL指针.并且如何你不小心传进去了一个NULL指针.很可能会出现内存错误.郁闷吧?如果我们不能看到库函数的实现,那么我们也就不知道它能处理那些东西。那么我们就不会认为这样很正常的使用是错误的,那么如果让我们在一个大程序里寻找这样一个内存错误将是一件很困难的事情.
对此微软推出了另一个库函数:
size_t __cdecl strnlen(const char *str, size_t maxsize)
{
    size_t n;
 
    /* Note that we do not check if s == NULL, because we do not
     * return errno_t...
     */
 
    for (n = 0; n < maxsize && *str; n++, str++)
        ;
 
    return n;
}
对于这个比上一个合理了很多,安全了很多,至少在你正确的传进maxsize的情况下他不会越界访问,但感觉还是有点怪怪的.
2 strncpy
char * __cdecl strncpy (
        char * dest,
        const char * source,
        size_t count
        )
{
        char *start = dest;
 
        while (count && (*dest++ = *source++))    /* copy string */
                count--;
 
        if (count)                              /* pad out with zeroes */
                while (--count)
                        *dest++ = '/0';
 
        return(start);
}
由于无法找到strcpy的源码,只能去看这个了.仔细看这个库函数,你会发现一个情况:
       if (count)                              /* pad out with zeroes */
                while (--count)
                        *dest++ = '/0';
这句基本上执行不到.其中的count参数我们一般传进去的都是 <= strlen(source)+1;(包括了‘/0’)也就是说count总是在遇到’/0’之前或是同时也减为0了.那么上面的代码也就无法执行了,除非谁傻到传一个大于strlen(source)+1的值进去,那么上面代码就可以发挥作用了。上面代码没有执行也就意味着我们得到的字符串中没有’/0’,后果我们可想而知.据说是为了安全才在原来strcpy的基础上加了一个参数count,但从这里并没有看到count起到了什么作用,并且库函数应该为dest末尾加上’/0’.这应该属于它的职责,如果把这个也交给程序员,那就太令人恶心了…
好了就说这么多把。昨天看了这两个库函数有些失望,其他的还没看.说句实在话,其实并不是指针难学,而是我们不了解很多的实现与内存的原理,如果了解了,那么指针和一些语法的东西并没有什么差别,所以我建议大家在学习指针的时候多多去看看库函数,去了解一下内存的原理.这样会对你帮助很多.看了这两个库函数感觉有些失望,知不知道很多的内存错误和不明的结果是应该埋怨程序员还是该埋怨库函数的设计不合理.不管怎么样,作为一个客户我们只能去使用了,毕竟没掏钱,呵呵…还是安心的去了解人家的库,遵守人家的规则吧.

抱歉!评论已关闭.