朱兆祺教你如何攻破C语言学习、笔试与机试的难点

第一节  C语言编程中的几个基本概念
1.1      include< >与#include" " 
1.   #include< >和#include" "有什么区别?

这个题目考查大家的基础能力,#include< >用来包含开发环境提供的库,
#include" "用来包含.c/.cpp文件所在目录下的头文件。注意:有些开发环境可以在当前目录下面自动收索(包含子目录),有些开发环境需要指定明确的文件路径名。

1.2      switch()

1.   switch(c) 语句中 c 可以是 int, long, char, float, unsigned int 类型?
其实这个题目很基础,c应该是整型或者可以隐式转换为整型的数据,很明显不能是实型(float、double)。所以这个命题是错误的。

1.3      const

1.   const有什么用途?
虽然const很常用,但是我相信有很多人仍然答不上来。
(1) 欲阻止一个变量被改变,可以使用const 关键字。在定义该 const 变量时,通常需要对它进行初 始化,因为以后就没有机会再去改变它了;
(2) 对指针来说,可以指定指针本身为 const,也可以指定指针所指的数据为 const,或二者同时指定为 const;
(3) 在一个函数声明中,const 可以修饰形参,表明它是一个输入参数,在函数内部不能改变其值;
(4) 对于类的成员函数,若指定其为 const 类型,则表明其是一个常函数,不能修改类的成员变量;
(5) 对于类的成员函数,有时候必须指定其返回值为 const 类型,以使得其返回值不为“左值”。

1.4      #ifndef/#define/#endif

1.   头文件中的 #ifndef/#define/#endif 干什么用?
其实#ifndef、#define、#endif这些在u-boot、linux内核文件中经常见到,在这么大型的程序中大量使用,可见它的作用不可小觑。
这些条件预编译多用于对代码的编译控制,增加代码的可裁剪性,通过宏定义可以轻松的对代码进行裁剪。
#ifndef/#define/#endif最主要的作用是防止头文件被重复定义。

1.5      全局变量和局部变量

1.         全局变量和局部变量在内存中是否有区别?如果有,是什么区别?
全局变量储存在静态数据库,局部变量在堆栈。 其实,由于计算机没有通用数据寄存器,则函数的参数、局部变量和返回值只能保存在堆栈中。提示:局部变量太大可能导致栈溢出,所以建议把较大数组放在main函数外,防止产生栈溢出。
思考:如程序清单1. 1所示。会出现怎样的情况?
程序清单1. 1  大数组放在main函数中导致堆栈溢出
int main(int argc, char *argv[])
{
    int iArray[1024 * 1024];
    return 0;
}
 
 
第二节  数据存储与变量

2.1      变量的声明与定义
1.         如程序清单2. 1所示会不会报错?为什么?如果不会报错,又是输出什么结果?
程序清单2. 1  变量的声明与定义
#include<stdio.h>
 
static int  a   ;
static int  b[] ;
 
int main( int argc , char *argv[] )
{
    printf( "%d  %d \n" , a , b[0] ) ;
    return 0 ;
}
 
static int   a = 8 ;
static int   b[4]  ;
这个程序是不会报错的,并且连警告都不会出现。输出的结果是:8  0
static int a ,这句程序是声明全局变量a;static int b[],这句程序是声明全局数组变量b,并且是不完全声明,也就是可以省略数组下标。static int a = 8,这里才是定义全局变量a,static int b[4],这里是定义全局变量b。
2.2      局部变量与全局变量的较量
1.         请问如程序清单2. 2所示输出什么?
程序清单2. 2  局部变量与全局变量
#include<stdio.h>
 
static int a = 8 ;
int main( int argc , char *argv[] )
{
    int a = 4 ;
 
    printf( "%d \n" , a ) ;
 
    return 0 ;
}
C语言规定,局部变量在自己的可见范围内会“挡住”同名的全局变量,让同名的全局变量临时不可见。即在局部变量的可见范围内不能访问同名的全局变量。因此本程序输出为:4。
2.3      charintfloatdouble的数据存储
1.         请问如程序清单2. 3所示,ij输出什么?
程序清单2. 3  数据存储
float i = 3 ;
int   j = *(int*)(&i) ;
 
printf( "i = %f \n" , i ) ;
printf( "j = %#x \n" , j ) ;
i是毋庸置疑是:3.000000。但是j呢?3.000000?答案是否定的,j是输出:0x4040 0000。有人会问了,难道j是随机输出?瞎说,j输出0x4040 0000是有依据,是一个定值!
由于i是float数据类型,而j是int数据类型。理论上说,j是取了i的地址然后再去地址,应该得到的就是i的值:3。但是问题的关键就是float数据类型的存储方式和int数据类型不一样,float是占用4个字节(32位),但是float存储是使用科学计数法存储,最高位是存储数符(负数的数符是0,正数的数符是1);接下来8位是存储阶码;剩下的23位是存储尾数。上面i=3.000000,那么3.000000(10进制) = 11(2进制) = <v:shape id=_x0000_i1027 style="WIDTH: 40.5pt; HEIGHT: 21.75pt" equationxml=' 121.1 脳<21' type="#_x0000_t75"> (二进制)。数据在电脑中存储都是二进制,这个应该都没有疑问。那么这里的数符为:0 ,阶码为:E – 127 = 1 ,那么阶码为:E = 128 即为:1000 0000 (2进制) 尾数为:100 0000 0000 0000 0000 0000 。那么存储形式就是:0100 0000  0100 0000 0000 0000 0000 0000。这个数据转换成16进制就是0x4040 0000。
2. 1  数据存储方式

char、int、float、double的存储方式如2. 1所示。
提问:如果i = -3.5 的话,请问j输出多少?
i = -3.500000
j = 0xc0600000
这个希望读者自行分析。
再问:如果如程序清单2. 4所示。
程序清单2. 4  数据存储
double i = 3 ;
int     j = *(int*)(&i) ;
 
printf( "i = %lf \n" , i ) ;
printf( "j = %#x \n" , j ) ;
这样的话,j又输出多少呢?
提示:double(  8个字节(64位)  )的存储方式是:最高位存储数符,接下来11位存储阶码,剩下52位存储尾数。
是不是得不到你想要的结果?double是8个字节,int是4个字节。一定别忘记了这个。用这个方法也同时可以验证大小端模式!
2.4      容易忽略char的范围
1.         如程序清单2. 5所示,假设&b=0x12ff54,请问三个输出分别为多少?
程序清单2. 5  char的范围
unsigned int b = 0x12ff60 ;
printf("( (int)(&b)+1 )         = %#x \n" , ( (int)(&b)+1 )           ) ;
printf("*( (int*)( (int)(&b)+1 ) )  = %#x \n" , *( (int*)( (int)(&b)+1 ) )   ) ;
printf("*( (char*)( (int)(&b)+1 ) ) = %#x \n" , *( (char*)( (int)(&b)+1 ) )  ) ;
很显然,&b是取无符号整型b变量的地址,那么(int)(&b)是强制转换为整型变量,那么加1即为0x12ff54+1=0x12ff55。所以( (int)(&b)+1 )是0x12ff55。
2. 3  指针加1取字符型数据
由于( (int)(&b)+1 )是整型数据类型,通过(int *)( (int)(&b)+1 )转化为了整型指针类型,说明要占4个字节,即为:0x12ff55、0x12ff56、0x12ff57、0x12ff58,再去地址*( (int *)( (int) (&b)+1 ) )得到存储在这4个字节中的数据。但是很遗憾,0x12ff58我们并不知道存储的是什么,所以我们只能写出0x**0012ff。**表示存储在0x12ff58中的数据。如图2. 2所示。
2. 2  指针加1取整型数据
以此类推,*( (char *)( (int) (&b)+1 ) ) = 0xff。如图2. 3所示。
但是,*( (char *)( (int) (&b)+1 ) )输出的却是:0xff ff ff ff !
问题出现了,为什么*( (char *)( (int) (&b)+1 ) )不是0xff,而是0xff ff ff ff?char型数据应该占用1个字节,为什么会输出0xff ff ff ff?
使用%d输出,
printf("*( (char*)( (int)(&b)+1 ) ) = %d \n" , *( (char*)( (int)(&b)+1 ) ) ) ;
结果为-1???
问题出在signed char 的范围是:-128~127,这样肯定无法储存0xff,出现溢出。所以将
printf("*( (char*)( (int)(&b)+1 ) ) = %#x \n" , *( (char*)( (int)(&b)+1 ) ) ) ;
改成
printf("*( (unsigned char*)( (int)(&b)+1 ) ) = %#x \n" ,
*( (unsigned char*)( (int)(&b)+1 ) ) ) ;
就可以输出0xff,因为unsigned char 的范围是:0~255(0xff)。

 

图2.1 数据存储方式

图2.1  数据存储方式

 

图2.2 指针加1取整型数据

图2.2  指针加1取整型数据

 

图2.3 指针加1取字符型数据

图2.3  指针加1取字符型数据
 
 
 
 
 
第三节  数学算法解决C语言问题

3.1      N!结果中0的个数
1.        99!结果中后面有多少个0
谁跟你说过高数没用?数学是C语言的支撑,没有数学建模的支撑就没有那么经典的C语言算法!
如果你能一个一个乘出来我算你狠!我也佩服你!
0也就意味着乘积是10的倍数,有10的地方就有5.有5就不担心没2.10以内能被5整除的只有5,但是能被2整除多的去了。所以就简化成了这个数只要有一个因子5就一定对应一个0.
所以可以得出下面结论:
    当0 < n < 5时,f(n!) = 0;
    当n >= 5时,f(n!) = k + f(k!), 其中 k = n / 5(取整)。
如程序清单3. 1所示。
程序清单3. 1  N!中0的个数
#include<stdio.h>   
 
int fun(int iValue)   
{   
    int iSum = 0;   
    while(iValue / 5 != 0)   
    {   
        iSum     += (iValue / 5 );   
        iValue  /= 5;   
    }   
    return iSum;   
}   
  
int main( int argc , char *argv[] )   
{   
int iNumber, iZoreNumber;  
 
    scanf( "%d", &iNumber);   
    iZoreNumber = fun(iNumber);   
printf( "%d\n", iZoreNumber);
   
return 0;   
}
所以99!后面有多少个0的答案是:99 / 5 = 19 , 19 / 5 = 3 ; 3 / 5 = 0 .也就是:19 + 3 + 0 = 22.
这里延伸为N!呢,一样的,万变不离其宗!
3.2      N!结果中的位数
1.   请问N!结果中有几位数?
数学!还是数学,数学真的博大精深,如果大学没有好好学数学,会成为你一辈子的遗憾。
我们先假设:N! = 10 ^A,我们知道 10^1~10^2(不含10^2)之间是2位数,10^2~10^3(不含10^3)之间是3位数,以此类推,10^A~10^(A+1)(不含10^(A+1))则是(A+1)位数,那就是说,我们只要求出A,即可知道N!有几位数。A=log10(N!),
N!= 1*2*3……*N,那么A= log10(1)+log10(2)+……+log10(N).
这样好计算了吧!程序如程序清单6. 2所示。
程序清单6. 2  N!结果中的位数
#include <stdio.h>
#include <math.h>
int main(int argc, char *argv[])
{
    int iNumber , i = 0 ;
    double sum = 0 ;
    printf("请输入iNumber :");
    scanf( "%d" , &iNumber );
    for( i = 1 ; i < ( iNumber + 1 ) ; i++) {
       sum += log10(i) ;
    }
    printf(" N!%d \n" , (int)sum  +  1  ) ;
    return 0;
}
我们看下调试结果:
请输入iNumber :10
N!有7位
请按任意键继续. . .
    网友可以自行验证。
 
 
第四节  关键字、运算符与语句
1.1      static
1.         如程序清单4. 1所示,请问输出i、j的结果?
程序清单4. 1  static
#include <stdio.h>
 
static int  j ;
 
void fun1(void)
{
    static int i = 0 ;
    i++ ;
    printf("i = %d   " , i );
}
 
void fun2(void)
{
    j = 0 ;
    j++ ;
    printf("j = %d \n" , j );
}
 
int main(int argc, char *argv[])
{
   
    int k = 0 ;
    for( k = 0 ; k < 10 ; k++ ){
       fun1() ;
       fun2() ;
       printf("\n");
    }
   
    return 0;
}
答案:
i = 1   j = 1
 
i = 2   j = 1
 
i = 3   j = 1
 
i = 4   j = 1
 
i = 5   j = 1
 
i = 6   j = 1
 
i = 7   j = 1
 
i = 8   j = 1
 
i = 9   j = 1
 
i = 10   j = 1
 
请按任意键继续. . .
 
很多人傻了,为什么呢?是啊,为什么呢?!
由于被static修饰的变量总存在内存静态区,所以运行这个函数结束,这个静态变量的值也不会被销毁,函数下次使用的时候仍然能使用这个值。
有人就问啊,为什么j一直是1啊。因为每次调用fun2()这个函数,j都被强行置0了。
static的作用:
(1) 函数体内 static 变量的作用范围为该函数体,不同于 auto 变量,该变量的内存只被分配一次, 因此其值在下次调用时仍维持上次的值;
(2) 在模块内的 static 全局变量可以被模块内所用函数访问,但不能被模块外其它函数访问;
(3) 在模块内的 static 函数只可被这一模块内的其它函数调用,这个函数的使用范围被限制在声明 它的模块内;
(4) 在类中的 static 成员变量属于整个类所拥有,对类的所有对象只有一份拷贝;
(5) 在类中的 static 成员函数属于整个类所拥有,这个函数不接收 this 指针,因而只能访问类的static 成员变量。
 
1.2      for循环的深究
1.         如程序清单4. 2所示,输出结果是什么?
程序清单4. 2  for循环
#include <stdio.h>
 
int main(int argc, char *argv[])
{
    int  i = 0 ;
   
    for( i = 0 ,printf("First = %d  " , i ) ;
         printf("Second = %d  " , i ) , i < 10 ;
        i++ , printf("Third = %d  " , i ))
{
           printf("Fourth = %d  \n" , i) ;
}
   
    return 0;
}
这个题目主要考察对for循环的理解。我们先来看看运行程序会输出什么?
First = 0  Second = 0  Fourth = 0
Third = 1  Second = 1  Fourth = 1
Third = 2  Second = 2  Fourth = 2
Third = 3  Second = 3  Fourth = 3
Third = 4  Second = 4  Fourth = 4
Third = 5  Second = 5  Fourth = 5
Third = 6  Second = 6  Fourth = 6
Third = 7  Second = 7  Fourth = 7
Third = 8  Second = 8  Fourth = 8
Third = 9  Second = 9  Fourth = 9
Third = 10  Second = 10  请按任意键继续. . .
从输出我们就可以知道程序到底是什么运行:
首先i = 0 , 所以输出:First = 0 ; 接着输出:Second = 0  ;  i < 10 成立,则输出:Fourth = 0 。就此完成第一个循环。接着 i ++ , 此时i = 1 ,输出:Third = 1 ;接着输出:Second = 1 ;i < 10 成立,则输出:Fourth = 1 ······以此类推。
 
1.3      尺子——sizeof
1.   如程序清单4. 3所示,sizeof(a),sizeof(b)分别是多少?
程序清单4. 3  sizeof
#include <stdio.h>
int main(int argc, char *argv[])
{
    char  a[2][3] ;
    short b[2][3] ;
    printf( "sizeof(a) = %d \n" , sizeof( a ) ) ;
    printf( "sizeof(b) = %d \n" , sizeof( b ) ) ;
    return 0;
}
这个题目比较简单,由于char 是1个字节、short是2个字节,所以本题答案是:
sizeof(a) = 6
sizeof(b) = 12
请按任意键继续. . .
 
好的,再来看看如程序清单4. 4所示,sizeof(a),sizeof(b)分别是多少?
程序清单4. 4  sizeof
#include <stdio.h>
int main(int argc, char *argv[])
{
    char  *a[2][3] ;
    short *b[2][3] ;
    printf( "sizeof(a) = %d \n" , sizeof( a ) ) ;
    printf( "sizeof(b) = %d \n" , sizeof( b ) ) ;
    return 0;
}
是数组指针呢,还是指针数组呢?这里涉及*和[]和优先级的问题。我告诉大家的是这两个数组存放的都是指针变量,至于为什么,在后续章节会详细解释,然而指针变量所占的字节数为4字节,所以答案:
sizeof(a) = 24
sizeof(b) = 24
请按任意键继续. . .
 
1.4      ++ii++
1.   或许大家都知道,++i是先执行i自加再赋值,但是i++是先赋值再自加,但是还有隐藏在后面的东西呢?
int i = 0 ;
int iNumber = 0 ;
iNumber = (++i) + (++i) + (++i) ;
C-Free编译输出是:7,有的编译器输出是:9。这两个答案都是对的,编译器不同所不同。7 = 2+2+39=3+3+3。区别在于答案是7的先执行(++i)+(++i)再执行+(++i),但是答案是9的是一起执行。
 
这只是前奏,先看几个让你目瞪口呆的例子。编译环境是VS2010
int i = 0 ;
int iNumber = 0 ;
iNumber = (i++) + (++i) + (++i) ;
printf( "iNumber = %d \n" , iNumber ) ;
这里输出是:4!!!4 = 1+1+2。
 
int i = 0 ;
int iNumber = 0 ;
iNumber = (++i) + (i++) + (++i) ;
printf( "iNumber = %d \n" , iNumber ) ;
这里输出是:4!!!4=1+1+2。
 
int i = 0 ;
int iNumber = 0 ;
iNumber = (++i)  + (++i) + (i++) ;
printf( "iNumber = %d \n" , iNumber ) ;
这里输出是:6!!!6=2+2+2。
 
这里至少能说明两个问题,其一,先执行前面两个,再执行第三个;其二,(i++)这个i的自加是最后执行!
int i = 0 ;
int iNumber = 0 ;
iNumber = (++i)  + (i++) + (++i) + (++i) + (i++) ;
printf( "iNumber = %d \n" , iNumber ) ;
这个会是多少?!答案是:10!!!10=1+1+2+3+3!
不同的编译器或许会存在不同的答案,希望读者自行进行验证。
 
1.5      scanf()函数的输入
1.   如程序清单4. 5所示,运行程序,当显示Enter Dividend: , 输入的是a,按下Enter之后程序会怎么运行?
程序清单4. 5  scanf()函数的输入
#include<stdio.h>
 
int main(void)
{
        float fDividend,fDivisor,fResult;
 
        printf("Enter Dividend:");
        scanf("%f",&fDividend);
 
        printf("Enter Divisor:");
        scanf("%f",&fDivisor);
 
        fResult=fDividend/fDivisor;
        printf("Result is: %f\n",fResult);
 
        return 0;
}
这个问题有人会说,肯定是显示Enter Divisor:要我输入除数咯。是这样吗?
答案是:如果你在Enter Dividend:之后输入非数字,按下Enter之后显示的不是Enter Divisor: 要你输入除数,而是程序到此就运行结束,显示一个不确定答案,这个答案每一次都会变。如果你在Enter Divisor:之后输入非数字,按下Enter之后显示的不是Reslut的结果, 而是程序到此就运行结束,显示一个不确定答案,这个答案每一次都会变。
由于scanf()使用了%f,当输入数字的时候,scanf()将缓冲区中的数字读入fDividend,并清空缓冲区。由于我们输入的并非数字,因此scanf()在读入失败的同时并不会清空缓冲区。最后的的结果是,我们不需要再输入其他字符,scanf()每次都会去读取缓冲区,每次都失败,每次都不会清空缓冲区。当执行下一条scanf()函数读取除数时,由于缓冲区中有数据,因此它不等待用户输入,而是直接从缓冲区中取走数据。
那么防止输入非数字的程序应该怎样呢?
#include<stdio.h>
 
int main( int argc , char *argv[] )
{
    float   fDividend , fDivisor , fResult ;
    int     iRet ;
    char    cTmp1[ 256 ] ;
 
    printf( "Enter Dividend \n" ) ;
    iRet  =  scanf( "%f" , &fDividend ) ;
 
if ( 1 == iRet )
{
        printf( "Enter Divisor \n" ) ;
        iRet = scanf( "%f" , &fDivisor ) ;
        if ( 1== iRet )
{
            fResult = fDividend / fDivisor ;
            printf( "Result is %f \n" , fResult ) ;
        }
        else
{
            printf( "Input error ,not a number! \n" ) ;
            gets(cTmp1) ;
            return 1 ;
        }
    }
else
{
        printf( "Input error , not a number! \n" ) ;
        gets(cTmp1) ;
        return 1 ;
    }
 
    return 0 ;
}
 
1.6      scanf()函数的返回值
1.         如程序清单4. 6所示,请问输出会是什么?
程序清单4. 6  scanf()函数的返回值
int a , b ;
printf( "%d \n" , scanf("%d%d" , &a , &b) ) ;
输出输入这个函数的返回值?!答案是:2。只要你合法输入,不管你输入什么,输出都是2。那么我们就要深入解析scanf这个函数。scanf()的返回值是成功赋值的变量数量。
 
1.7      const作用下的变量
1.         阅读如程序清单4. 7所示,想想会输出什么?为什么?
程序清单4. 7  const作用下的变量
const int   iNumber = 10 ;
printf(" iNumber = %d  \n" ,  iNumber) ;
int *ptr = (int *)(&iNumber) ;
*ptr  = 100 ;
printf(" iNumber = %d  \n" ,  iNumber) ;
const的作用在第四章已经详细讲了,这里就不再累赘,答案是:10,10。这里补充一个知识点:
const int *p                   指针变量p可变,而p指向的数据元素不能变
int* const p                   指针变量p不可变,而p指向的数据元素可变
const int* const p            指针变量p不可变,而p指向的数据元素亦不能变
 
1.8      *ptr++*(ptr++)*++ptr*(++ptr)++(*ptr)(*ptr)++的纠缠不清
1.         如程序清单4. 8所示程序,输出什么?
程序清单4. 8  *ptr++
int  iArray[3] = { 1 , 11 , 22} ;
int  *ptr     = iArray ;
 
printf( "*ptr++ = %d \n" , *ptr++ ) ;
printf( "*ptr   = %d \n" , *ptr   ) ;
纠结啊,是先算*ptr还是ptr++;还是纠结啊,ptr是地址加1还是偏移一个数组元素!
这里考查了两个知识点,其一:*与++的优先级问题;其二,数组i++和++i的问题。*和++都是优先级为2,且都是单目运算符,自右向左结合。所以这里的*ptr++和*(ptr++)是等效的。
首先ptr是数组首元素的地址,所以ptr++是偏移一个数组元素的地址。那么ptr++运算完成之后,此时的ptr是指向iArray[1],所以第二个输出*ptr = 11 。如图4. 1所示。那么倒回来看第一个输出,ptr++是在执行完成*ptr++之后再执行的,所以,*ptr++ = 1 。
4. 1  ptr++
 
 
 
 
 
 
如程序清单4. 9所示程序,输出会是什么?
程序清单4. 9  *++ptr
int  iArray[3] = { 1 , 11 , 22} ;
int  *ptr       = iArray ;
 
printf( "*++ptr = %d \n" , *++ptr ) ;
printf( "*ptr   = %d \n" , *ptr   ) ;
这个解释和上面解释差不多,就是++ptr和ptr++的差别,所以这里的两个输出都是:11。同样的道理,*++ptr和*(++ptr)是等效。
 
再如程序清单4. 10所示,输出又会是什么?
程序清单4. 10  *ptr++
int  iArray[3] = { 1 , 11 , 22} ;
int  *ptr       = iArray ;
 
printf( "(*ptr)++ = %d \n" , (*ptr)++ ) ;
printf( "*ptr   = %d \n" , *ptr   ) ;
这个的输出是:1,2。原因请读者分析。
<ignore_js_op>

4.jpg (94.96 KB, 下载次数: 6)

 

图4.1 ptr++

图4.1  ptr++
posted @ 2013-08-06 21:27  电子木木  阅读(579)  评论(0编辑  收藏  举报