学习C语言要掌握的高级技能

  这是在学习杨福林先生的<<高级C语言>>时,收录总结的一些在C语言开发中的注意事项,希望对朋友们有些帮助,同时也希望来到这里的朋友给予一些补充与修正,多谢!(只是收录总结,希望朋友们不要给我冠以抄袭的头衔)

 

1.巧用do...while(0):

(1)可以用它来避免goto语句;

(2)在宏定义时,比如:

             #define REMOVE_BUFFER(p) do{delete p;p=0;}while(0)

当然也可以定义为:

            #define REMOVE_BUFFER(p) {delete p;p=0;}

前者调用时可以在句末加";":  if(case) REMOVE_BUFFER(p);

而后者不能:if(case) REMOVE_BUFFER(p)

    为了让代码具有统一风格,无疑后者是一很好选择

2.位运算的妙用:
(1) 奇偶判定:(a&1==0)?偶:奇
(2) 取int型变量a的第k位 (k=0,1,2……sizeof(int)),即a>>k&1
(3) 将int型变量a的第k位清0,即a=a&~(1< (4) 将int型变量a的第k位置1, 即a=a|(1< (5) int型变量循环左移k次,即a=a<>16-k   (设sizeof(int)=16)
(6) int型变量a循环右移k次,即a=a>>k|a<<16-k   (设sizeof(int)=16)
(7)整数的平均值
对于两个整数x,y,如果用 (x+y)/2 求平均值,会产生溢出,因为 x+y 可能会大于INT_MAX,但是我们知道它们的平均值是肯定不会溢出的,我们用如下算法:
int average(int x, int y)   //返回X,Y 的平均值
{  
     return (x&y)+((x^y)>>1);
}
(8)判断一个整数是不是2的幂,对于一个数 x >= 0,判断他是不是2的幂
boolean power2(int x)
{
    return ((x&(x-1))==0)&&(x!=0);
}
(9)不用temp交换两个整数
void swap(int x , int y)
{
    x ^= y;
    y ^= x;
    x ^= y;
}
(10)计算绝对值
int abs( int x )
{
int y ;
y = x >> 31 ;
return (x^y)-y ;        //or: (x+y)^y
}
(11)取模运算转化成位运算 (在不产生溢出的情况下)
         a % (2^n) 等价于 a & (2^n - 1)
(12)乘法运算转化成位运算 (在不产生溢出的情况下)
         a * (2^n) 等价于 a<< n
(13)除法运算转化成位运算 (在不产生溢出的情况下)
         a / (2^n) 等价于 a>> n
        例: 12/8 == 12>>3
(14) a % 2 等价于 a & 1      
(15) if (x == a) x= b;
            else x= a;
        等价于 x= a ^ b ^ x;
(16) x 的 相反数 表示为 (~x+1)

3.不经意的死循环:
 int main()
{
   int i,j[8];
   for(i=0;i<=8;i++)
    j[i]=0;
   return 0;
}
  因为变量 i 和数组 j[8]是保存在栈中,默认是由高地址向低地址方向存储. 输出变量地址可以发现(具体值可能不一样): i 存储位置在0xbfdab07b, j[0]、j[1]...j[7]在内存的地址分别是0xbfdab05c、0xbfdab060,...0xbfdab078. 如下所示:
   高地址 <--------------------------------------->低地址
                           i,j[7],j[6],j[5],j[4],j[3],j[2],j[1],j[0]
 
    如果在int i,j[8]后面再定义变量int c,那么c就存放在j[0]的往低方向的下一个地址0xbfdab058 .
    当然,也有可能由低地址向高地址存储,若int j[8],i;同样会有死循环出现,总之,一定要注意访问越界的情况.
另一个例子:
#include
int main()
{
        int i;
        char c;
        for(i=0;i<5;i++)
        {
                scanf("%d",&c);
                printf("i=%d ",i);
        }
        printf("/n");
}
编译后运行
[foxman@local~]#./a.out
0    (输入0)
i=0  (输出 i 值)
1
i=0
2
i=0
3
i=0
4
i=0
...
这样一直循环下去。

  问题在于,c被声明为char类型,而不是int类型。当程序要求scanf读入一个整数时,应该传递给它一个指向整数的指针。而程序中scanf得到的却是一个指向字符的指针,scanf函数并不能分辨这种情况,只能将这个指向字符的指针作为指向整数的指针而接受,并且在指针指向的位置存储一个整数。因为整数所占的存储空间要大于字符所占的存储空间,所以c附近的内存会被覆盖.

  由上面分析,i 和 c 是由高地址到低地址存储在栈中,这样在c所在位置尝试存储一个4字节变量,会占用比c高的3个字节(覆盖掉 i 字节的低3位),即使 i 总是为零,一直循环下去.

如果每次输入Ctrl+D作为字符终止符不存储int到c处,那么就会输出正常i=0..4了.

4.嵌入汇编采用_asm("...")形式,在一般情况下,不要使用嵌入汇编,它大大降低了程序的可移植性;
  调用系统命令用system("");形式。

5.指针要做到永远可控,对不使用的和初始的指针将其置为0;
对用new和malloc申请得来的地址,一定要用free和delete释放,且将指向该空间的指针置0,另外,对malloc/calloc返回的值,一定要检查其返回值是否为“空指针”(亦即检查分配内存的操作是否成功),而对new返回的值,则不需检测返回的指针值,因为,若分配成功,自然无检查的必要,若分配失败,new 就会抛出异常跳过后面的代码,如果你想检查 new 是否成功,应该捕捉异常:
       try {
          int* p = new int[SIZE];
          // 其它代码
       } catch ( const bad_alloc& e ) {
          return -1;
       }。
当然,标准 C++ 亦提供了一个方法来抑制 new 抛出异常,而返回空指针:
       int* p = new (std::nothrow) int; // 这样如果 new 失败了,就不会抛出异常,而是返回空指针
       if ( p == 0 ) // 如此这般,这个判断就有意义了
          return -1;
        // 其它代码

6.在计算机中进行算术表达式的计算是通过栈来实现的。算术表达式有两种表示方法,即中缀表示法和后缀表示法,前者通过转换成后者,然后实现其求值。
  中缀算术表达式转换成对应的后缀算术表达式的规则是:把每个运算符都移到它的两个运算对象的后面,然后删除掉所有的括号即可。比如,
  (25+x)*(a*(a+b)+b) ---> 25!x!+!a!a!b!+!*!b!+!*
  后缀表达式求值的算法是:从表达式字符串中逐个读取(不妨假定每个操作数就是一个字符),若遇操作数,则将其压入一个stack中;若遇操作符(指'=','-','*','/'),则取stack的栈顶元素为操作符的后一个操作数,栈顶的下一个元素为操作符的前一个操作数;若遇到终止符,则结束运算,若stack只有一个元素,其为所求值,反之则出错。
  思考:对-1-2如何计算?

7.结构体对齐的具体含义:
  #pragmapack规定的对齐长度,实际使用的规则是:
结构,联合,或者类的数据成员,第一个放在偏移为0的地方,以后每个数据成员的对齐,按照#pragmapack指定的数值和这个数据成员自身长度中,比较小的那个进行。
  也就是说,当#pragmapack的值等于或超过所有数据成员长度的时候,这个值的大小将不产生任何效果。
  而结构整体的对齐,则按照结构体中最大的数据成员和mapack指定值 之间,较小的那个进行。
  具体解释
#pragmapack(4)
class TestB
{
 public:
  int aa; //第一个成员,放在[0,3]偏移的位置,
  char a; //第二个成员,自身长为1,#pragmapack(4),取小值,也就是1,所以这个成员按一字节对齐,放在偏移[4]的位置。
  short b; //第三个成员,自身长2,#pragmapack(4),取2,按2字节对齐,所以放在偏移[6,7]的位置。
  char c; //第四个,自身长为1,放在[8]的位置。
};
这个类实际占据的内存空间是9字节
类之间的对齐,是按照类内部最大的成员的长度,和#pragmapack规定的值之中较小的一个对齐的。
所以这个例子中,类之间对齐的长度是min(sizeof(int),4),也就是4。
9按照4字节圆整的结果是12,所以sizeof(TestB)是12。


如果
#pragmapack(2)
class TestB
{
 public:
   int aa; //第一个成员,放在[0,3]偏移的位置,
   char a; //第二个成员,自身长为1,#pragmapack(2),取小值,也就是1,所以这个成员按一字节对齐,放在偏移[4]的位置。
   short b; //第三个成员,自身长2,#pragmapack(2),取2,按2字节对齐,所以放在偏移[6,7]的位置。
   char c; //第四个,自身长为1,放在[8]的位置。
};
//可以看出,上面的位置完全没有变化,只是类之间改为按2字节对齐,9按2圆整的结果是10。
//所以 sizeof(TestB)是10。

现在去掉第一个成员变量为如下代码:
#pragmapack(4)
class TestC
{
 public:
   char a;//第一个成员,放在[0]偏移的位置,
   short b;//第二个成员,自身长2,#pragmapack(4),取2,按2字节对齐,所以放在偏移[2,3]的位置。
   char c;//第三个,自身长为1,放在[4]的位置。
};
//整个类的大小是5字节,按照min(sizeof(short),4)字节对齐,也就是2字节对齐,结果是6
//所以sizeof(TestC)是6。


另外,使用位域的主要目的是压缩存储,其大致规则为:
    1) 如果相邻位域字段的类型相同,且其位宽之和小于类型的sizeof大小,则后面的字段将紧邻前一个字段存储,直到不能容纳为止;
    2) 如果相邻位域字段的类型相同,但其位宽之和大于类型的sizeof大小,则后面的字段将从新的存储单元开始,其偏移量为其类型大小的整数倍;
    3) 如果相邻的位域字段的类型不同,则各编译器的具体实现有差异,VC6采取不压缩方式,Dev-C++采取压缩方式;
    4) 如果位域字段之间穿插着非位域字段,则不进行压缩;
    5) 整个结构体的总大小为最宽基本类型成员大小的整数倍。
 
8.常见误区:
1) void main() 应该是 int main();
2) 误用fflush(stdin)来清空缓冲区,它在很多环境下是没定义的;
3) 强制转换malloc() 的返回值,因为void*(泛型指针)的出现,对其进行强制转换是不必要的,甚至会带来不必要的麻烦,类似地,使用calloc ,realloc等返回值时亦不需对其进行类型转换;
4) char c = getchar();习惯用 char 型变量接收 getchar、getc,fgetc 等函数的返回值,其实这么做是不对的,并且隐含着足以致命的错误。getchar 等函数的返回值类型都是 int 型,当这些函数读取出错或者读完文件后,会返回 EOF。EOF 是一个宏,标准规定它的值必须是一个 int 型的负数常量。通常编译器都会把 EOF 定义为 -1。问题就出在这里,使用 char 型变量接收 getchar 等函数的返回值会导致对 EOF 的辨认出错,或者错把好的数据误认为是 EOF,或者把 EOF 误认为是好的数据。例如:
 char c;   //假设编译器默认 char 为 unsigned char
 FILE *fp;
 ...
 while ((c = fgetc(fp)) != EOF )
   {
    putchar(c);
   }
这将是一个死循环,因为c升级为int时,FF --> 00 00 00 FF。
若定义 signde char c;
 (c = fgetc(fp)) != EOF? /* 读到值为 FF 的字符,误认为 EOF */
此时,当文件未读完时,循环已中断;

9.关于while(1)和for(/*?可以有代码 */;?/*?必须为空 */;?/*?可以有代码 */)的效率问题,因为前者总会判断1的真假性,而后者无条件循环,故后者优于前者;

10.产生随机数的方法:
1)产生一定范围内的随机数:
直接方法:rand() % N;
由于许多随机数发生器的低位比特并不随机。一个较好的方法是:
    (int)((double)rand() / ((double)RAND_MAX + 1) * N);
如果你不希望使用 double,另一个方法是:
    rand() / (RAND_MAX / N + 1);
2) 为什么每次执行程序,rand() 都返回相同顺序的数字?
    你可以调用 srand() 来初始化伪随机数发生器的种子,传递给 srand() 的值应该是真正的随机数,例如当前时间:
    #include
    #include
    srand((unsigned int)time((time_t *)NULL));
请注意,在一个程序执行中多次调用 srand() 并不见得有帮助!不要为了取得“真随机数”而在每次调用? rand() 前都调用 srand()!

11.C语言中的指针和内存泄漏:
(1)未初始化的内存;
(2)内存覆盖;
(3)内存读取越界;
(4)内存泄漏:在对指针赋值前,请确保内存位置不会变为孤立的。另外,在释放内存时,每当释放结构化的元素,而该元素又包含指向动态分配的内存位置的指针时,应首先遍历子内存位置,并从那里开始释放,然后再遍历回父节点。
(5)返回值的不正确处理,比如:
char *func()
{  return (char*)malloc[20]; }
void callingFunc ( )
{  func(); }
(6)始终要跟踪所有内存分配,并在任何适当的时候释放它们;
(7)访问空指针:访问空指针是非常危险的,因为它可能使您的程序崩溃。始终要确保您不是在访问空指针。
总结:
  始终结合使用 memset 和 malloc,或始终使用 calloc。
  每当向指针写入值时,都要确保对可用字节数和所写入的字节数进行交叉核对。
  在对指针赋值前,要确保没有内存位置会变为孤立的。
  每当释放结构化的元素(而该元素又包含指向动态分配的内存位置的指针)时,都应首先遍历子内存位置并从那里开始释放,然后再遍历回父节点。
  始终正确处理返回动态分配的内存引用的函数返回值。
  每个 malloc 都要有一个对应的 free。
  确保您不是在访问空指针。

你可能感兴趣的:(学习C语言要掌握的高级技能)