Chinaunix首页 | 论坛 | 博客
  • 博客访问: 105024
  • 博文数量: 16
  • 博客积分: 1401
  • 博客等级: 上尉
  • 技术积分: 170
  • 用 户 组: 普通用户
  • 注册时间: 2008-03-04 20:51
文章分类

全部博文(16)

文章存档

2011年(1)

2008年(15)

我的朋友

分类: C/C++

2008-03-05 09:13:11

勿在浮沙筑高台
用候捷老师书上的这句话鞭策自己
想想自己在学习c语言的路上,遇上过很多挫折,走过很多弯路。造成c语言这么诡秘难学的原因,我认为就是从谭浩强老师的<<语言程序设计>>kernighan大师的<程序设计语言>>之间没有一道很好的桥梁,c语言的很多细节问题没有总结。国内有<>,<>,可是却没有<>。c语言太简单了吗?肯定不是。应该是很多人没有想过把自己的感受写出来与大家分享,或是由于文人相轻的坏毛病。豪杰公司的创始人梁肇新的书,林锐博士的书在很多"牛人"看来都是垃圾..........,在网上好像不批判一番就显不出自己高明似的。大家都是从菜鸟过来的,都是一个学习的过程,你认为很简单的问题可能对于别人就很难。
我在这里将自己以前经常迷惑的问题一一总结,或许问题对很多大虾认为太简单了,但这样既有助于自己的提高,又能给当初像我一样无助的人一些帮助。其中参考了不少书籍和网上的资料,非常感谢,不对的地方还请大虾指正。
 
个人感觉之所以很多人对c语言觉得难学,有几个因素,也是c语言的几个难点
 
 
1. 对编译器,链接器,加载器,调试器的理解不够,对于gcc、gdb等没有深刻认识;对变量的生存期、栈和堆的区别、变量的初始化问题、传指针和传值的实质搞得不清楚。
 
传指针的实质是传值。传值的时候,我们是做了一个复制品,在函数中只是对复制品在操作进入函数和返回函数都是这个道理经历了一个复制的过程。这个问题在钱能的那本c++教材上讲的很清楚。
举个例子(csdn上的一个例子),这个程序的作用是从控制台读取两个数,存到数组中,然后输出。但这两种方式中有一种是错误的。
 
#include
#include
#include
 
char * getnumber1(void);
void  getnumber2(char *);
 
int main()
{
    int i = 0;
    char w[20];

    i = atoi(getnumber1());         
    printf("getnumber1 = %d\n", i);

    i = 0;
    getnumber2(w);

    i = atoi(w);
    printf("getnumber2 = [%s]\n", w);
    printf("getnumber2 = %d\n", i);

    return 0;
}
 
char *getnumber1(void)
{
    char w[20];
    char *p;
    int c;
 
    p = w;
    while (isspace(c = getchar()))
        ;

    if (c != EOF)
        *p++ = c;

    for (;;) {
        if (isspace(c = getchar())) {
            ungetc(c, stdin);
            break;
        }

        *p++ = c;
    }

    *p = '\0';
    printf("getnumber1 = [%s]\n", w);
    return w;
}
 
void  getnumber2(char *w)
{
    char *p;
    int c;
 
    p = w;
    while (isspace(c = getchar()))
        ;

    if (c != EOF)
        *p++ = c;

    for (;;) {
        if (isspace(c = getchar())) {
            ungetc(c, stdin);
            break;
        }

        *p++ = c;
    }

    *p = '\0';
    return;
}
这个小代码没有考虑各种边界情况和错误处理。
bc++5.5的编译器编译后,运行。
输入和输出如下:
D:\c\scan>getnum
12
getnumber1 = [12]
getnumber1 = 1
12
getnumber2 = [12]
getnumber2 = 12
为什么getnumber1读入的char*12,但是atoi的结果却是1
同时,getnumber2读入的也是12atoi的结果是对的。
这个问题就和变量生存期有关吧.
每个函数都有它的栈区,函数一返回再访问这个函数的栈区的变量就是不确定的了.
char * getnumber1(void)
{
char w[20];
.......
return w;
}
从这个函数中返回后w[20]的内容就不确定了.
getnumber2之所以正确是因为w[20]定义在main函数中.
再看个经常讨论的例子(选自<<高质量c/c++编程指南>>,这本书有些东西说的还是很好的,有利于进一步阅读更加高深的书籍.)我偷点懒就copy一下了: )
void GetMemory(char *p, int num)
{
       p = (char *)malloc(sizeof(char) * num);
}
void Test(void)
{
       char *str = NULL;
       GetMemory(str, 100);    // str 仍然为 NULL     
       strcpy(str, "hello"); // 运行错误
}
Test函数的语句GetMemory(str, 200)并没有使str获得期望的内存,str依旧是NULL,为什么?
毛病出在函数GetMemory中。编译器总是要为函数的每个参数制作临时副本,指针参数p的副本是 _p,编译器使 _p = p。如果函数体内的程序修改了_p的内容,就导致参数p的内容作相应的修改。这就是指针可以用作输出参数的原因。在本例中,_p申请了新的内存,只是把_p所指的内存地址改变了,但是p丝毫未变。所以函数GetMemory并不能输出任何东西。事实上,每执行一次GetMemory就会泄露一块内存,因为没有用free释放内存。
如果非得要用指针参数去申请内存,那么应该改用“指向指针的指针”.
void GetMemory2(char **p, int num)
{
       *p = (char *)malloc(sizeof(char) * num);
}
void Test2(void)
{
       char *str = NULL;
       GetMemory2(&str, 100);      // 注意参数是 &str,而不是str
       strcpy(str, "hello");
       cout<< str << endl;
       free(str);
}
由于“指向指针的指针”这个概念不容易理解,我们可以用函数返回值来传递动态内存。这种方法更加简单,如下所示.
 
char *GetMemory3(int num)
{
       char *p = (char *)malloc(sizeof(char) * num);
       return p;
}
void Test3(void)
{
       char *str = NULL;
       str = GetMemory3(100);      
       strcpy(str, "hello");
       cout<< str << endl;
       free(str);
}
用函数返回值来传递动态内存这种方法虽然好用,但是常常有人把return语句用错了。这里强调不要用return语句返回指向“栈内存”的指针,因为该内存在函数结束时自动消亡,如下所示。
 
char *GetString(void)
{
       char p[] = "hello world";
       return p;  // 编译器将提出警告
}
void Test4(void)
{
char *str = NULL;
str = GetString();   // str 的内容是垃圾
cout<< str << endl;
}
 
说完了这个,来说说变量初始化问题.
函数中的局部变量如果你只定义了而不初始化那么它的值是个随机值.我们经常可以看到下面这样的错误代码.
#include
int main(void)
{
int sum;
int a[4] = {1,2,3,4};
for(int i = 0; i < 4; i++)
sum += a[i];
printf("The total is %d", sum);
return 0;
}
 这段代码啥问题?就是sum未初始化,它的初始值不是0.
数组初始化又是一个问题.还有全局变量和静态变量的问题.
像全局变量,static变量.如果只从书上来看,或许觉得很简单的.但纸上得来终觉浅,实际写程序就涉及到代码的组织了,什么样的变量应该声明为全局,是不是因为全局变量不好就因噎废食而不用它呢?
 
 
2.对内存管理地不够好.也就是对malloc,free,realloc3个函数掌握得不够透彻只用一样东西,而不明白他的实质,实在不高明。
 
看下面一段对内存扩大的代码,你能看出它的毛病吗?
  char *a;
  a = (char *)malloc(100);
  if( a == NULL);
    exit(-1);
........
 a = (char *)realloc( a, 1000);
对于这个问题,你看出这段程序的错误了吗?如果没看出,也没什么关系.这个错误虽然很严重,但却很微妙,如果不给出一点暗示很少人会发现它。所以我们给出一个提示:如果a是指向将要改变其大小的内存块的唯一指针,那么当realloc的调用失败时会怎样?回答是当realloc返回时会把NULL填入a,冲掉这个指向原有内存块的唯一指针。简而言之,上面的代码会产生内存块丢失的现象。
我们有多少次在要改变一个内存块的大小时,想到要把指向新内存块的指针存储到另一个不同的变量中?
realloc函数就那么简单吗?答案是否定的.呵呵.由于最初这个函数设计界面的问题,造成了这个函数的问题.下面这个问题来自gawk中的代码.问题非常玄妙.
下面这个函数保存了一个静态指针,该指针指向某些动态分配的数据,有时候函数不得不增加空间.函数中有一些自动指针指向这块数据.
void manage_table(void)
{
static struct table *table;
struct table *cur, *p;
int i;
size_t count;
table = (struct table*)malloc(count * sizeof(struct table));
/*填表*/
cur = &table[i];/*指向第i个条目*/
cur->i = j;
.....
if(some condition)/*需要增大该表*/
{
count += count / 2;
p = (struct table *)realloc(table, count * sizeof(struct table));
table = p;
}
cur->i = j;/*给它重新赋值*/
.....
}
这个问题可能比上一个还细微.如果不了解realloc的实现,是不容易发现这个问题的.realloc函数是从堆上分配内存的,当扩大一块内存空间时, realloc()试图直接从堆上现存的数据后面的那些字节中获得附加的字节,如果能够满足,自然天下太平;可如果数据后面的字节不够的话,问题就出来了,那么就使用堆上第一个有足够大小的自由块,现存的数据然后就被拷贝至新的位置,而老块则放回到堆上。
cur->i = j;/*给它重新赋值*/
上面这条语句就是问题的根源.
应该这样.
cur = &table[i];
cur->i = j;/*给它重新赋值*/
这种问题如何更好的避免呢?kernighan(大师就是不一样)建议我们用索引代替指针,这个问题就优雅地解决了.像下面这样:
table[i].i = j;
看看unix v6/v7的源代码,能让我们对很多函数的功能有更清醒的认识.让我们看看realloc到底都能怎么用吧.
void* realloc( void* pv, size_t size );
realloc改变先前已分配的内存块的大小,该内存块的原有内容从该块的开始位置到新块和老块长度的最小长度之间得到保留。
 l      如果该内存块的新长度小于老长度,realloc释放该块尾部不再想要的内存空间,返回的pv不变。
l         如果该内存块的新长度大于老长度,扩大后的内存块有可能被分配到新的地址处,该块的原有内容被拷贝到新的位置。返回的指针指向扩大后的内存块,并且该块扩大部分的内容未经初始化。
l         如果满足不了扩大内存块的请求,realloc返回NULL,当缩小内存块时,realloc总会成功。
l         如果pvNULL,那么realloc的作用相当于调用malloc(size),并返回指向新分配内存块的指针,或者在该请求无法满足时返回NULL
l         如果pv不是NULL,但新的块长为零,那么realloc的作用相当于调用free(pv)并且总是返回NULL
l         如果pvNULL且当前的内存块长为零,结果无定义
之所以造成这个函数这么多的问题,主要是这个函数的界面设计问题.我们应该了解它的各个雷区,在实际应用中避免犯错.
看看下面这个释放链表的函数,找找它的问题. 
void freelist(Nameval *listp)
{
   for(;listp != NULL;listp = listp->next)
        free(listp);
}
对于第二个问题.
相信很多人都犯过这个错误.还往往找不到原因.想想listp都被释放掉了,你还能用
listp->next这个东西吗?
正确做法:
  Nameval *next;
  for(;listp != NULL;listp = next)
  { next = listp->next;
    free(listp);
  }
一定要注意free的顺序.是不是挺简单的问题.但如果不注意的话你是不是会犯呢?
如果是在c++,内存管理更是一个让人头疼的问题(这或许是很多人转投java的原因吧).实际上,我用一句粗话总结:"上完了厕所一定要记得冲".这说大了,是一个人是否道德的表现.
内存管理还有很多问题,以后慢慢总结.
 
 
3. 对文件操作不太熟悉。fgetc/fputc、fgets/fputs、fscanf/fprintf、fread/fwrite这些函数有什么区别,该在什么时候用?如何判断结束?相信很多人都不是很知道.这几个函数是有很多区别的,有很多细节值得注意.<>(APUE)一书有一章对这个进行了详细的探讨.不过还有很多值得注意的地方.像行缓冲,全缓冲等概念很多人可能知道地比较少.
 
说文件这个问题,今天csdn上就有人问了个问题,大家看看.
问题:
读取某一文本文件,自动删除每行开始的空格和数字.
测试文件c.txt内容如下:
12aada
  3dada
a2dada
134a
qa
输出结果应该为:
aada
dada
adada
a
qa
用fgets/fputs这两个函数实现这个程序还是有很多值得注意的(用fgetc/fputc问题要少一些).
#include
#include
#include
#include
#define MAXSIZE 80
int convert_file (char *filename );
int main(int argc, char **argv)
{
 if(argc!=2)
 {
  printf("error\n");
 }
  convert_file(argv[1]);
 return 0;
}

int convert_file (char *filename )
{
 FILE *fp;
 FILE *temp;
 int i,k;
 //long fpos;
 
 if((fp=fopen(filename, "r"))==NULL)
 {
  printf("error1\n");
  return 0;
 }
    if((temp = fopen("temp.txt", "w"))==NULL)
 {
  printf("error2\n");
  return 0;
 }
 char array[MAXSIZE];
 
 
 
 memset(array, ' ', MAXSIZE);
 int length;
 while(fgets(array, MAXSIZE, fp)!=NULL)
 {
    i=0;
  length = strlen(array);
  k = 0;
  while(i <= length)
  {
   
   if((array[i] != ' ') &&(!isdigit(array[i])))
   {
    array[k++] = array[i];
   }
   i++;
  }
  fputs(array,stdout);
  
  
  if(fputs(array, temp) == EOF)
   printf("error3\n");
  
 }
 fclose(fp);
    fclose(temp);
 remove(filename);
 rename("temp.txt",filename);
 return 0;
}
 
 
4.对很多标准库函数(像string.h、stdlib.h中的函数)的实现细节不够了解,有时候出错了都不知道怎么错的.
 
拿string.h中的strcat函数为例.看看下面这个程序,你能看出它的问题吗?
#include
#include
int main()
{
 char a[100];
 char b[] = "abcde";
 char c[] = "fgh";
 
 strcat(a,b);
 strcat(a,c);
 printf("%s\n",a);
 return 0;
}
要想找出毛病,来看unix v7中strcat函数的源代码.
/*
 * Concatenate s2 on the end of s1.  S1's space must be large enough.
 * Return s1.
 */
char *
strcat(s1, s2)
register char *s1, *s2;
{
 register char *os1;
 os1 = s1;
 while (*s1++)
  ;/*这个循环在寻找'\0'*/
 --s1;
 while (*s1++ = *s2++)
  ;
 return(os1);
}
问题的关键在于传递给strcat(s1,s2)的两个参数都要是以'\0'结尾的。而程序中传递给strcat()的第一个参数a[]没有初始化,导致了连接错误,从而引起缓冲区溢出。
改正 char a[100] = {'\0'};进行初始化.
对函数的理解我们到底是怎样呢?望文生义的现象往往是错误的根源.所以unix v6,v7,glibc的源码是我们应该经常查阅的.
 
   
5.对指针和数组的区别没有透彻理解.
 
<>对指针和数组介绍得最为清晰.
 
 
6.不会选择合适的数据结构来解决问题.数组,可增长数组,链表,二叉树,哈希表这几种结构到底该怎么选择,不少人都不知所措.想起当初自己学习的时候,总是担心数组大小不够怎么办.像这种问题相信困扰着很多人.
 
这个问题,<<程序设计实践>>有着比较好的阐述.以后把我的感受说一下.
 
 
7.对边界条件,溢出等问题考虑得不够.
 
历史上的很多重大事故都是由于溢出引起的.很多黑客瞄准的都是溢出这快"蛋糕".
美国阿里亚娜火箭试飞失败就是由于溢出引起的.火箭的速度太快了,造成了数据超过了变量能承受的位数.美国导弹巡洋舰约克敦号的推进系统问题是由于没有考虑边界问题.造成了除0错误.
举个溢出的例子(摘自<<微软c编程精粹>>)
函数原型为void myitoa( int i, char *str ),功能是将整数转换为字符串.
 
void myitoa( int i, char *str )
{
    char *strDigits;
if( i < 0 )
{
        *str++ = -’;
               i = -i;             /* i变成正值 */
        }
/* 反序导出每一位数值 */
strDigits = str;
do
*str++ = i%10 + 0’;
while( (i/=10) > 0 );
*str=’/0’;
ReverseStr( strDigits );   /* 将数字的次序转为正序 */
}
 
若该代码在二进制补码机器上运行,当i等于最小的负数(例如,16位机器的-32768)时就会出现问题。原因在于表达式i= -i中的-i上;即上溢超出了int类型的范围。然而,真正的错误在于程序员实现代码的方式上:程序员没有完全按照他自己的设计来实现代码,而只是近似实现了他的设计。
在设计中要求:“如果i是负的,加入一个负号,然后将i的无符号数值
部份转换成ASCII。”而上面的代码并没有这么做。它实际执行了:“如果i是负的,加入一个负号,然后将i的正值也就是带符号的数值部分转换为ASCII。”就是这个有符号的数字引起了所有的麻烦。如果完全根据算法并使用无符号数,代码会执行得很好。
可以将上述代码分为两个函数,这样做十分有用。
void IntToStr( int i, char *str )
{
if( i < 0 )
{
        *str++ = -‘;
        i = -i;
}
UnsToStr(( unsigned )i, str );
}
 
void UnsToStr( unsigned u, char *str )
{
char * strStart = str;
do
        *str++ = (u%10) + 0’;
while(( u/=10 )>0 );
*str=’\0’;
ReverseStr( strStart );
}
在上面的代码中,i也要取负,这与前面的例子相同,为什么它就可以正常工作呢?这是因为:如果i是最小负数-32768,二进制补码形式表示为0x8000,然后通过将所有位倒装(即 0 1)再加 1来取负,从而得到-i 0x8000,若为有符号数,则表示-32768,若为无符号数,则表示32768。按定义,由二进制补码表示的任意数,通过将其每一位倒装再加l,可以得到该数的负值。因此0x8000表示的是最小负数-32768的负值,即32768,因此应解释为无符号数。
像这种考虑溢出的问题我们往往都不容易考虑到,也是我们犯错的根源.
对于边界条件,像文件操作,循环语句终止条件的编写,链表头尾节点的操作等等都是值得注意的,以后再一一举例.
 
 
8.没有良好的程序风格,经常随自己的意思写程序.造成程序很难读懂,其实程序的正确性,可读性是很关键的.
 
9.违反了一个重要规则------"做你明白的事,明白你做的事".
 
10.浮躁.踏不下心来学习.而且人云亦云,就认为c/c++很难学,甚至说它们没用.拿我自己来说,每天用在学习上的时间太少了,荒废的时间太多.得道有先后,树业有专攻.只有投入的时间多,收获才会大!
 
 
阅读(2630) | 评论(1) | 转发(0) |
给主人留下些什么吧!~~

chinaunix网友2008-12-23 18:14:39

总结是这种写