首页 > 代码库 > C语言中容易被忽略的细节(第三篇)

C语言中容易被忽略的细节(第三篇)

前言:本文的目的是记录C语言中那些容易被忽略的细节。我打算每天抽出一点时间看书整理,坚持下去,今天是第一篇,也许下个月的今天是第二篇,明年的今天又是第几篇呢?……我坚信,好记性不如烂笔头。第三篇了,fight~...


第一篇链接:C语言中容易被忽略的细节(第一篇)

第二篇链接:C语言中容易被忽略的细节(第二篇)


1、__attribute__((noreturn))

__attribute__可设置函数属性、变量属性和类型属性。__attribute__((noreturn))设置了函数属性,noreturn通知编译器函数从不返回值,当遇到函数需要返回值却还没运行到返回值处就已退出来的情况,该属性可以避免出现错误信息。例如:

void Test();
int main(void)
{
    int x;
    scanf("%d", &x);

    if (x > 0)
        Test();
    else
        return 0;
}
上面代码在编译阶段提示警告:warning: control reaches end of non-voidfunction。产生警告的原因是:Test()函数没有返回值,所以main()函数有可能没有返回值,程序不知如何处理。

void Test() __attribute__((noreturn));
若将Test()的声明修改为以上,则相当于通知编译器Test()函数从不返回值,也就不会产生警告了。

注意:(1)attribute前后的下划线输入的方法是:Shift+减号(输入两次);(2)__attribute__机制有很多其他用处,是GNU C的特色,建议读到相关内容时候再总结,做到有的放矢。

2、如果编译器按照内存地址递减的方式来给变量分配内存,以下代码有什么问题?

int i, a[10];
    for (i = 0; i <= 10; i++)
        a[i] = 0;

解析:数组越界,程序陷入死循环。内存中数组a之后的四个字节实际上分配给了整形变量i,对a[10]赋值为0实际上是将计数器i的值设置为0。


3、在if/else结构中,要尽量把为TRUE的概率较高的条件判断置于前面,这样可以提高该段程序的性能。switch的效率比if/else结构高,即使程序真的不需要default处理,也应保留语句default:break;


4、与零值比较的正确方法
(1)布尔变量与零值比较

if语句判断其条件表达式的真假并不是通过把它的计算结果转换为布尔类型的临时变量进行的,而是将其结果直接和0进行比较,如果不等于0则表示真,否则为假。不要将布尔变量直接与true、1、-1、0等进行比较。

bool flag;  
if (flag)  //表示flag为真  
if (!flag) //表示flag为假  
(2)整型变量与零值比较
int value;  
if (value =http://www.mamicode.com/= 0)  >

(3)浮点变量与零值比较

计算机表示浮点数(float或double)都有一个精度限制。对于超过了精度限制的浮点数,计算机会把它们精度之外的小数部分截断。因此,本来不相等的两个浮点数在计算机中可能就变成相等的了。例如:float a = 10.222222225, b = 10.222222229;在数学上a和b是不等的,但在32位计算机中它们就是相等的。(注:float可保证6位有效数字,double和long double可保证10位有效数字)在针对实际应用环境编程时,总是有个精度要求,而直接比较一个浮点数和另外一个值(浮点数或整数)是否相等(==)或不等(!=)可能得不到符合实际需要的结果,因为==和!=比较操作采用的精度往往比实际应用中要求的精度高。

可将“>”和“<”直接用于浮点数之间比较及浮点数和整数的比较。!(a > b) && !(a < b)与a == b的语义是等价的,所以也不建议用于判断浮点数相等与否。

#define EPSILON 1e-6  
if (abs(x - y) <= EPSILON) //x等于y  
if (abs(x - y) > EPSILON)  //x不等于y  
  
if (abs(x) <= EPSILON)      //x等于0  
if (abs(x) > EPSILON)       //x不等于0  

(4)指针变量与零值比较

指针变量的零值是“空值”(记为NULL),即不指向任何对象。尽管NULL的值与0相同,但两者意义不同(类型不同)。

if (p == NULL)  
if (p != NULL) 

备注:使用if (NULL == p)、if (100 == i)这种写法比较好,因为如果误将==写为=,因为编译器不允许对常量赋值,就可以检查到错误。


5、va_list、va_start()、va_arg()和va_end()

C标准函数库的stdarg.h头文件定义了可变参数函数使用的宏。可变参数函数内部必须定义一个va_list变量,然后使用宏va_start、va_arg和va_end来读取。相关定义如下:

typedef char* va_list;
void va_start ( va_list ap, prev_param ); /* ANSI version */
type va_arg ( va_list ap, type ); 
void va_end ( va_list ap ); 

举个简单例子如下:

#include <stdarg.h>
#include <stdio.h>

double average(int count, ...)
{
    va_list ap;
    int j;
    double tot = 0;
    va_start(ap, count);                //使va_list指向起始的参数
    for(j=0; j<count; j++)
        tot+=va_arg(ap, double);        //检索参数,必须按需要指定类型
    va_end(ap);                         //释放va_list
    return tot/count;
}

int main(void)
{
    double a[5] = {1, 0, 0, 0, 0};
    printf("%lf\n", average(5, a[0], a[1], a[2], a[3], a[4]));
    return 0;
}

C语言中可变参数函数在没有长度检查和类型检查,在传入过少的参数或不符的类型时可能会出现溢位的情况。


6、求值顺序

C语言中只有4个运算符(&&,||,? :和, )存在规定的求值顺序。&&和||首先对左侧操作数求值,只在需要时才对右侧操作数求值;对于a ? b : c,操作数a首先被求值,根据a的值再求操作数b或c的值;对于逗号运算符,先对左侧操作数求值,然后该值被“丢弃”,再对右侧操作数求值。例如:

i = 0;
while (i < n)
    y[i] = x[i++];

以上从数组x中复制前n个元素到数组y中的代码是不正确的。因为赋值运算符并不保证任何求值顺序。y[i]的地址将在i的自增操作执行之前被求值还是之后求值是不确定的。

注意:运算符的优先级与求值顺序是完全不同的概念。


7、对于数组结尾之后的下一个元素,取它的地址是合法的,读取这个元素的值的结果是未定义的,而且绝少有C编译器能够检测出这个错误。

8、字符数组不一定是字符串

字符数组是元素为字符变量的数组,字符串是以’\0’(ASCII码值为0x00)为结束字符的字符数组。

(1)对于字符数组来说,它并不在乎中间或末尾有没有’\0’结束字符,因为数组知道它自己有多少个元素,况且’\0’对它来说是一个合法的元素。

(2)如果字符数组中没有’\0’结束标志,却被当做字符串来用时可能会导致“内存访问冲突”或篡改其他内存单元,strlen函数的结果异常等。举个简单的例子:

#include <stdio.h>

int main(void)
{
    char arr1[] = {'a', 'b', '\0', 'c', 'd'};
    char arr2[] = "Hello";
    char *p = "Hello";

    printf("%d %d\n", sizeof(arr1), strlen(arr1));      //结果5 2
    printf("%d %d\n", sizeof(arr2), strlen(arr2));      //结果6 5
    printf("%d %d\n", sizeof(p), strlen(p));            //结果4 5
    return 0;
}

9、不要用字面常量来初始化引用

const int &a = 0;

以上语义并非是把引用初始化为NULL,而是创建一个临时的int对象并用0来初始化它,然后再用它来初始化引用a,而该临时对象将一直保留到a销毁的时候才会销毁。


10、引用的创建和销毁并不会调用类的构造函数和析构函数。在二进制层面,引用一般是通过指针来实现的,只不过编译器帮我们完成了转换。

C语言中容易被忽略的细节(第三篇)