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

目录

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

1. 巧用do…while(0)

2. 位运算的妙用

3. 不经意的死循环

4. 嵌入汇编或者调用外部命令

5. 指针和内存泄漏

6. 算术表达式

7. 结构体对齐

8. while(1)和for(;;)的性能

9. 随机数的生成

10. 常见误区



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


1. 巧用do…while(0)

(1)可以用它来避免goto语句;
(2)在宏定义时,比如:

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

//调用时要在句末加“;”
if(case) REMOVE_BUFFER(p);

当然也可以定义为:

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

//调用
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 &= ~(1<<k)

(4) 将int型变量a的第k位置1

a |= 1<<k

(5) int型变量循环左移k次

a = (a<<k)| (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 - 1) //等价于  a % (2^n)

(12)乘法运算转化成位运算 (在不产生溢出的情况下)

a << n //等价于 a * (2^n) 

(13)除法运算转化成位运算 (在不产生溢出的情况下)

//例: 12/8 == 12>>3
a >> n //等价于a / (2^n) 

(14) %2 转化为位运算

a & 1 //等价于 a % 2

(15)如果x != a时,赋值为a, 否则赋值为b

/*等价于
 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 <stdio.h>
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 1 2 3 4了.


4. 嵌入汇编或者调用外部命令

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


5. 指针和内存泄漏

(1) 指针要做到永远可控,对不使用的和初始的指针将其置为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;
 // 其它代码

(2) 关于内存的一些常见错误及注意事项
a. 未初始化的内存;
b. 内存覆盖;
c. 内存读取越界;
d. 内存泄漏:在对指针赋值前,请确保内存位置不会变为孤立的。另外,在释放内存时,每当释放结构化的元素,而该元素又包含指向动态分配的内存位置的指针时,应首先遍历子内存位置,并从那里开始释放,然后再遍历回父节点。
e. 返回值的不正确处理,比如:

char *func(){
	return (char*)malloc[20]; 
}
void callingFunc ( ){  
	func(); 
}

f. 始终要跟踪所有内存分配,并在任何适当的时候释放它们;
g. 访问空指针:访问空指针是非常危险的,因为它可能使您的程序崩溃。始终要确保您不是在访问空指针。
总结:

  • 始终结合使用 memset 和 malloc,或始终使用 calloc。
  • 每当向指针写入值时,都要确保对可用字节数和所写入的字节数进行交叉核对。 在对指针赋值前,要确保没有内存位置会变为孤立的。
  • 每当释放结构化的元素(而该元素又包含指向动态分配的内存位置的指针)时,都应首先遍历子内存位置并从那里开始释放,然后再遍历回父节点。
  • 始终正确处理返回动态分配的内存引用的函数返回值。
  • 每个 malloc 都要有一个对应的 free。
  • 确保您不是在访问空指针。


6. 算术表达式

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


7. 结构体对齐

#pragmapack规定的对齐长度,实际使用的规则是:
结构,联合,或者类的数据成员,第一个放在偏移为0的地方,以后每个数据成员的对齐,按照#pragmapack指定的数值和这个数据成员自身长度中,比较小的那个进行。
也就是说,当#pragmapack的值等于或超过所有数据成员长度的时候,这个值的大小将不产生任何效果。
而结构整体的对齐,则按照结构体中最大的数据成员和mapack指定值 之间,较小的那个进行。

示例1

// 具体解释
#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

示例2
下面改成 #pragmapack(2)

#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。

示例3
现在去掉第一个成员变量为如下代码:

#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. while(1)和for(;;)的性能

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


9. 随机数的生成

1)产生一定范围内的随机数:
直接方法:

rand() % N;

由于许多随机数发生器的低位比特并不随机。一个较好的方法是:

(int)((double)rand() / ((double)RAND_MAX + 1) * N);

如果你不希望使用 double,另一个方法是:

rand() / (RAND_MAX / N + 1);
  1. 为什么每次执行程序,rand() 都返回相同顺序的数字?
    你可以调用 srand() 来初始化伪随机数发生器的种子,传递给 srand() 的值应该是真正的随机数,例如当前时间:
#include<stdlib.h>
#include<time.h>
srand((unsigned int)time((time_t *)NULL));

请注意,在一个程序执行中多次调用 srand() 并不见得有帮助!不要为了取得“真随机数”而在每次调用 rand() 前都调用 srand()!


10. 常见误区

  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。
若定义 signed char c;
(c = fgetc(fp)) != EOF? /* 读到值为 FF 的字符,误认为 EOF */
此时,当文件未读完时,循环已中断。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

JJustRight

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值