C语言难点总结

发布者:EtherealGaze最新更新时间:2015-12-22 来源: eefocus关键字: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 ", i);
   i = 0;
   getnumber2(w);
   i = atoi(w);
   printf("getnumber2 = [%s] ", w);
   printf("getnumber2 = %d ", 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 = '';
    printf("getnumber1 = [%s] ", 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 = '';
     return;
}

这个小代码没有考虑各种边界情况和错误处理。
用bc++5.5的编译器编译后,运行。
输入和输出如下:
D:cscan>getnum
12
getnumber1 = [12]
getnumber1 = 1
12
getnumber2 = [12]
getnumber2 = 12

为什么getnumber1读入的char*为12,但是atoi的结果却是1?
同时,getnumber2读入的也是12,atoi的结果是对的。

这个问题就和变量生存期有关吧.
每个函数都有它的栈区,函数一返回再访问这个函数的栈区的变量就是不确定的了.
char * getnumber1(void)
{
    char w[20];
    .......
    return w;
}
从这个函数中返回后w[20]的内容就不确定了.
而getnumber2之所以正确是因为w[20]定义在main函数中.

再看个经常讨论的例子(选自<<高质量c/c++编程指南>>)

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变量.如果只从书上来看,或许觉得很简单的.但纸上得来终觉浅,实际写程序就涉及到代码的组织了,什么样的变量应该声明为全局,是不是因为全局变量不好就因噎废食而不用它呢?

 

                  内存管理

看下面一段对内存扩大的代码,你能看出它的毛病吗?

  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()试图直接从堆上现存的数据后面的那些字节中获得附加的字节,如果能够满足,自然天下太平;可如果数据后面的字节不够的话,问题就出来了,那么就使用堆上第一个有足够大小的自由块,现存的数据然后就被拷贝至新的位置,而老块则放回到堆上。[page]

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改变先前已分配的内存块的大小,该内存块的原有内容从该块的开始位置到新块和老块长度的最小长度之间得到保留。

      如果该内存块的新长度小于老长度,realloc释放该块尾部不再想要的内存空间,返回的pv不变。

      如果该内存块的新长度大于老长度,扩大后的内存块有可能被分配到新的地址处,该块的原有内容被拷贝到新的位置。返回的指针指向扩大后的内存块,并且该块扩大部分的内容未经初始化。

      如果满足不了扩大内存块的请求,realloc返回NULL,当缩小内存块时,realloc总会成功。

      如果pv为NULL,那么realloc的作用相当于调用malloc(size),并返回指向新分配内存块的指针,或者在该请求无法满足时返回NULL。

      如果pv不是NULL,但新的块长为零,那么realloc的作用相当于调用free(pv)并且总是返回NULL。

      如果pv为NULL且当前的内存块长为零,结果无定义


之所以造成这个函数这么多的问题,主要是这个函数的界面设计问题.我们应该了解它的各个雷区,在实际应用中避免犯错.

看看下面这个释放链表的函数,找找它的问题.

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的原因吧).实际上,我用一句粗话总结:"上完了厕所一定要记得冲".这说大了,是一个人是否道德的表现.

                  

                       文件操作

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 ");
 }

  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 ");
        return 0;
    }
    if((temp = fopen("temp.txt", "w"))==NULL)
    {
        printf("error2 ");
        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 ");
 
    }
    fclose(fp);
    fclose(temp);
    remove(filename);
    rename("temp.txt",filename);
    return 0;
}

 


                    标准库函数

拿string.h中的strcat函数为例.看看下面这个程序,你能看出它的问题吗?

#include
#include
int main()
{
 char a[100];
 char b[] = "abcde";
 char c[] = "fgh";
 
 strcat(a,b);
 strcat(a,c);
 printf("%s ",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++)
  ;/*这个循环在寻找''*/
 --s1;
 while (*s1++ = *s2++)
  ;
 return(os1);
}
问题的关键在于传递给strcat(s1,s2)的两个参数都要是以''结尾的。而程序中传递给strcat()的第一个参数a[]没有初始化,导致了连接错误,从而引起缓冲区溢出。

改正 char a[100] = {''};进行初始化.

对函数的理解我们到底是怎样呢?望文生义的现象往往是错误的根源.所以unix v6,v7,glibc的源码是我们应该经常查阅的.

   

                边界条件,溢出等问题

历史上的很多重大事故都是由于溢出引起的.很多黑客瞄准的都是溢出这快"蛋糕".

美国阿里亚娜火箭试飞失败就是由于溢出引起的.火箭的速度太快了,造成了数据超过了变量能承受的位数.

美国导弹巡洋舰约克敦号的推进系统问题是由于没有考虑边界问题.造成了除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=’’;

    ReverseStr( strStart );

}

在上面的代码中,i也要取负,这与前面的例子相同,为什么它就可以正常工作呢?这是因为:如果i是最小负数-32768,二进制补码形式表示为0x8000,然后通过将所有位倒装(即 0变 1)再加 1来取负,从而得到-i为 0x8000,若为有符号数,则表示-32768,若为无符号数,则表示32768。按定义,由二进制补码表示的任意数,通过将其每一位倒装再加l,可以得到该数的负值。因此0x8000表示的是最小负数-32768的负值,即32768,因此应解释为无符号数。


像这种考虑溢出的问题我们往往都不容易考虑到,也是我们犯错的根源.


对于边界条件,像文件操作,循环语句终止条件的编写,链表头尾节点的操作等等都是值得注意的,以后再一一举例.

关键字:C语言 引用地址:C语言难点总结

上一篇:C语言试题大全一
下一篇:MSP430F149单片机对SDRAM控制程序设计

推荐阅读最新更新时间:2024-03-16 14:41

单片机C语言实现NOP 循环移位
INTRINS.H:内部函数 函数名: _crol_,_irol_,_lrol_ 原 型: unsigned char _crol_(unsigned char val,unsigned char n); unsigned int _irol_(unsigned int val,unsigned char n); unsigned int _lrol_(unsigned int val,unsigned char n); 功 能:_crol_,_irol_,_lrol_以位形式将val 左移n 位,该函数与8051 RLA 指令 相关,上面几个函数不同于参数类型。 例: #include intrins.h main() { un
[单片机]
C语言面向对象编程的最佳实践
一、前言 以STM32为例,打开网络上下载的例程或者是购买开发板自带的例程,都会发现应用层中会有stm32f10x.h或者stm32f10x_gpio.h,这些文件严格来时属于硬件层的,如果软件层出现这些文件会显得很乱。 使用过Linux的童鞋们肯定知道linux系统无法直接操作硬件层,打开linux或者rt_thread代码会发现代码中都会有device的源文件,没错,这就是驱动层。 二、实现原理 原理就是将硬件操作的接口全都放到驱动链表上,在驱动层实现device的open、read、write等操作。当然这样做也有弊端,就是驱动find的时候需要遍历一遍驱动链表,这样会增加代码运行时间。 三、代码实现 国际惯例,
[单片机]
单片机C语言的优越性
·不懂得单片机的指令集,也能够编写完美的单片机程序; ·无须懂得单片机的具体硬件,也能够编出符合硬件实际的专业水平的程序; ·不同函数的数据实行覆盖,有效利用片上有限的RAM空间; ·程序具有坚固性:数据被破坏是导致程序运行异常的重要因素。C语言对数据进行了许多专业性的处理,避免了运行中间非异步的破坏; ·C语言提供复杂的数据类型(数组、结构、联合、枚举、指针等),极大地增强了程序处理能力和灵活性; ·提供auto、static、const等存储类型和专门针对8051单片机的data、idata、pdata、xdata、code等存储类型,自动为变量合理地分配地址; ·提供small、compact、large等编译模
[单片机]
单片机C语言知识点全攻略
根据有网友提出美中不足的是所用单片机编程语言为汇编,基于此,电子发烧友网再接再厉再次为读者诚挚奉上非常详尽的《单片机C语言知识点全攻略》系列单片机C语言学习教程,本教程共分为四部分,主要知识点如下所示。 第一部分知识点:    第一课 建立你的第一个KeilC51项目    第二课 C51HEX文件的生成和单片机    第三课 C51数据类型    第四课 C51常量 第二部分知识点:    第五课 C51变量    第六课 C51运算符和表达式    第七课 运算符和表达式(关系运算符)    第八课 运算符和表达式(位运算符)    第九课 C51运算符和表达式(指针和地址运算符) 第三部分知识点:
[单片机]
单片机<font color='red'>C语言</font>知识点全攻略
C语言及ARM中堆栈指针SP设置的理解与总结
什么是栈: 百度这么说:栈是一种特殊的线性表,是一种只允许在表的一端进行插入或删除操作的线性表。表中允许进行插入、删除操作的一端称为栈顶。表的另一端称为栈底。栈顶的当前位置是动态的,对栈顶当前位置的标记称为栈顶指针。当栈中没有数据元素时,称之为空栈。栈的插入操作通常称为进栈或入栈,栈的删除操作通常称为退栈或出栈。 简易理解: 客栈,即临时寄存的地方,计算机中的堆栈主要用来保存临时数据,局部变量和中断/调用子程序程序的返回地址。程序中栈主要是用来存储函数中的局部变量以及保存寄存器参数的,如果你用了操作系统,栈中还可能存储当前进线程的上下文。设置栈大小的一个原则是,保证栈不会下溢出到数据空间或程序空间。CPU在运行程序时,
[单片机]
C语言基础知识科普
C语言是单片机开发中的必备基础知识,本文列举了部分STM32学习中比较常见的一些C语言基础知识。 1 位操作 下面我们先讲解几种位操作符,然后讲解位操作使用技巧。C语言支持以下六种位操作:79c55d0c-080b-11ed-ba43-dac502259ad0.png 下面,重点讲解一下位操作在单片机开发中的一些实用技巧。1.1 在不改变其他位的值的状况下,对某几个位进行设值 这个场景在单片机开发中经常使用,方法就是我们先对需要设置的位用&操作符进行清零操作,然后用 | 操作符设值。 比如,我要改变GPIOA的状态,可以先对寄存器的值进行&清零操作:79dc505c-080b-11ed-ba43-dac5
[单片机]
51单片机C语言学习笔记6:51单片机C语言头文件及其使用
很多初学单片机者往往对C51的头文件感到很神秘,而为什么要那样写,甚至有的初学者喜欢问,P1口的P为什么要大写,不大写行不行呢?其实这个是在头文件中用sfr定义的,现在定义好了的是这样的 sfr P1 = 0x90; 也就是说,到底大写,还是小写,就是在这里面决定的。这就说明,如果你要用小写,就得在头文件中改为小写。其实它都是为了编程序方便才这样写的,在程序编译时,就会变成相应的地址(如P1就变成了0x90)。 下面是一个标准的C51头文件REG52.H: (此文件一般在C:KEILC51INC下 ,INC文件夹根目录里有不少头文件,并且里面还有很多以公司分类的文件夹,里面也都是相关产品的头文件。如果我们要使用自己写
[单片机]
详解C语言字节对齐
  一、什么是对齐,以及为什么要对齐:   1. 现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定变量的时候经常在特定的内存地址访问,这就需要各类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐。   2. 对齐的作用和原因:各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。其他平台可能没有这种情况,但是最常见的是如果不按照适合其平台的要求对数据存放进行对齐,会在存取效率上带来损失。比如有些平台每次读都是从偶地址开始,如果一个int型(假设为 32位)如果存放在偶地址开始的地方,
[单片机]
小广播
添点儿料...
无论热点新闻、行业分析、技术干货……
设计资源 培训 开发板 精华推荐

最新单片机文章
何立民专栏 单片机及嵌入式宝典

北京航空航天大学教授,20余年来致力于单片机与嵌入式系统推广工作。

换一换 更多 相关热搜器件
电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 电信业务审批[2006]字第258号函 京公网安备 11010802033920号 Copyright © 2005-2024 EEWORLD.com.cn, Inc. All rights reserved