C中一些小问题

转自http://github.tiankonguse.com/blog/2014/12/05/c-base.html

1遍历数组

问题:
有时候我们要遍历一个不知道大小的数组,但是我们有数组的名字,于是我们可以通过 sizeof 获得数组的大小了。
有了大小我们就可以遍历这个数组了。
一般情况下大家都是从下标 0 开始计数,于是从来不会遇到下面的问题。
如果你遇到下面的问题你能想出是什么原因吗?

代码:

#include<stdio.h>
#define TOTAL_ELEMENTS (sizeof(array) / sizeof(array[0]))
int array[] = {23,34,12,17,204,99,16};
int main() {
    for(int d=-1; d <= (TOTAL_ELEMENTS-2); d++) {
        printf("%d\n",array[d+1]);
    }
    return 0;
}

输出是

--------------------------------
Process exited after 0.1068 seconds with return value 0
请按任意键继续. . .

分析
sizeof 返回的类型是 unsigned int .
unsigned int 与 int 进行运算还是 unsigned int。
然后 -1 和 unsigned int 比较,会先把 -1 转化为 unsigned int。
这样 -1 的 unsigned int 就很大了,所以没有输出了。

2. do while

问题;
大家在 do while 中使用过 continue 吗?
没有的话来看看这个问题吧。

代码:

#include<stdio.h>
int main() {
    int i=1;
    do {
        printf("%d\n",i);
        i++;
        if(i < 15) {
            continue;
        }
    } while(false);
    return 0;
}

输出:

1
--------------------------------
Process exited after 0.08437 seconds with return value 0
请按任意键继续. . .

分析:
for 循环遇到 continue 会执行for 小括号内的第三个语句。
while 和 do…while 则会跳到循环判断的地方。

3. 宏展开

问题:
大多数情况下,我们的宏定义常常是嵌套的。
这就涉及到展开宏定义的顺序了。
下面来看看其中一个问题。

代码:

#include <stdio.h>
#define f(a,b) a##b       //##是一个连接符号,用于把参数连在一起
#define g(a)   #a       //#是‘字符串化’的意思。把跟在后面的参数转成一个字符串
#define h(a) g(a)
int main() {
    printf("%s\n",h(f(1,2)));
    printf("%s\n",g(f(1,2)));
    return 0;
}

输出:

12
f(1,2)
--------------------------------
Process exited after 0.1038 seconds with return value 0
请按任意键继续. . .

分析:
宏会扫描一遍,把可以展开的展开,展开一次后会再扫描一次看又没有可以展开的宏。
两个过程,第一个

  ↓ 
> h(f(1,2))
    ↓ 
> g(f(1,2))
    ↓
> g(12)
  ↓
> g(12)
  ↓
> "12"

第二个:

  ↓
> g(f(1,2))
  ↓
> "f(1,2)"

4 print返回值

问题:
你知道 printf 的返回值是什么吗?
猜猜下面的代码输出是什么吧。

代码:

#include <stdio.h>
int main() {
    int i=43;
    printf("%d\n",printf("%d",printf("%d",i)));
    return 0;
}

输出:

4321

--------------------------------
Process exited after 0.112 seconds with return value 0
请按任意键继续. . .

分析:
printf 的返回值是输出的字符的长度。
所以第一个输出 43 返回2.
第二个输出 2 返回 1. 第三个输出1. 于是输出的就是 4321 了。

5数组参数

问题:
对于函数传参为数组时,你知道到底传的是什么吗?

代码:

#include<stdio.h>
#define SIZE 10
void size(int arr[SIZE][SIZE]) {
    printf("%d %d\n",sizeof(arr),sizeof(arr[0]));
}
int main() {
    int arr[SIZE][SIZE];
    size(arr);
    return 0;
}

输出:

4 40
Process returned 0 (0x0)   execution time : 0.039 s
Press any key to continue.

分析
对于第二个输出,应该是 40 这个大家都没有什么疑问的。
但是第一个是几呢?
你是不是想着是 400 呢?
答案是 4.
这是因为对于数组参数。第一级永远是指针形式。
也就是说数组参数永远是指针数组。
所以第一级永远是指针,而剩下的级数由于需要使用 [] 运算符, 所以不能是指针。

6. size of参数

问题:
当我们有时候想让代码简洁点的时候,会把运算压缩到一起。
但是在 sizeof 中就要小心了。

代码:

#include <stdio.h>
int main() {
    int i;
    i = 10;
    printf("i : %d\n",i);
    printf("sizeof(i++) is: %d\n",sizeof(i++));
    printf("i : %d\n",i);
    return 0;
}

输出:

i : 10
sizeof(i++) is: 4
i : 10     //此处不是11
Process returned 0 (0x0)   execution time : 0.039 s
Press any key to continue.

分析:
sizeof 是一个关键字,但是在编译器运算。
所以编译器是不会进行我们的那些算术等运算的。
而是直接根据返回值推导类型,然后根据类型推导出大小的。

7. 位运算符左移

问题:
这个问题没什么说的,你运行一下就会先感到诧异,然后会感觉确实应该是这个样字,甚至会骂这代码写的太不规范了

代码:

#include <stdio.h>
#define PrintInt(expr) printf("%s : %d\n",#expr,(expr))
int FiveTimes(int a) {
    return a<<2 + a;   //此处先计算2+a
}
int main() {
    PrintInt(FiveTimes(1));
    return 0;
}

输出:

FiveTimes(1) : 8
Process returned 0 (0x0)   execution time : 0.624 s
Press any key to continue.

分析:
需要我提示吗?
三个字:优先级

8. 浮点数

问题:
大家经常使用 浮点数,知道背后的原理吗?

代码:

#include <stdio.h>
int main() {
    float a = 12.5;
    printf("%d\n", a);
    printf("%d\n", *(int *)&a);
    return 0;
}

输出:

12.500000
1095237632
Process returned 0 (0x0)   execution time : 0.651 s
Press any key to continue.

分析:
为了方便大家测试,我提供了一个32位浮点型向二进制的转换器
首先 int 和 float 在 32位机器上都是 四字节的。
对于整数储存,大家都没什么疑问。
比如 10 的二进制,十六进制如下

00000000 00000000 00000000 00001010
0X0000000A在这里插入代码片

由于最高位代表符号,所以整数可以表示的范围就是

0X80000000 -2^31 
0XFFFFFFFF -1 负整数最小值
0X00000000 0
0X00000001 1 正整数最小值
0X7FFFFFFF 2^31-1 正整数最大值

上面的二进制也就决定了 4字节的整数范围是 -2 ^ 31 到 2 ^ 31 - 1 .

对于一个浮点数,可以表示为 (-1)^S * X * 2^Y .
其中 S 是符号,使用一位表示。
X 是一个 二进制在 [1, 2) 内的小数,一般称为尾数,用23位表示。
Y 是一个整数,代表幂,一般称为阶码,用8位表示。

其中 Y 又涉及符号问题了。
8位的Y可以表示0到255,取指数偏移值 127 (2^(8-1) - 1)作为分界线,小于127的数是负数,大于的是正数。
这里我不明白为什么不使用以前数字的表示方法。

比如 12.5 的二进制是 1100.1 。
转化为上面的公式就是 (-1)^0 * 1.1001 * 2^3

下面我们来推导一下这个数字的二进制是什么吧。

符号为正,所以第一位就是0了

3 + 127 就是 130 了。于是使用 10000010 可以表示。

1.1001 一般不表示小数前的1,于是只需要表示 1001 即可,于是使用 10010000000000000000000 就可以表示了。

于是 12.5 的 float 的二进制表示就推算出来了

0 10000010 10010000000000000000000
01000001 01001000 00000000 00000000

然后这个二进制对应着整数 1095237632 。
这样一切都解释清楚了。

当然还要注意一个问题,这里有这么一个特殊规定:阶码Y如果是0, 尾数X就不再加1了。

9 宏的定义

问题:大家都定义过宏吧,你的宏定义规范吗?
代码:

#include <stdio.h>
#define MUL(x,y) (x)*(y)
#define MUL1(x,y) x*y
#define LOG(msg) printf("line:%d\n",__LINE__);printf("msg:%s\n",msg)
int main() {
    int a = 2, b = 3, c;
    c = MUL(a+1, b+1);
    printf("%d * %d :mul = %d\n", a, b, c);
    c = MUL1(a+1, b+1);
    printf("%d * %d :mul1 = %d\n", a, b, c);
    c = MUL(a+1, b+1);
    if(c != 12)
        LOG("mul error");
    return 0;
}

输出:

2 * 3 :mul = 12
2 * 3 :mul1 = 6
msg:mul error
Process returned 0 (0x0)   execution time : 0.039 s
Press any key to continue.

分析:
编程语言中所有的坑大部分都是代码编写不规范导致的。
比如使用队列时,下面的语句你加大括号吗?

while(!que.empty()){
    que.pop();
}

下面我们来看看上面的代码为什么错了,以及怎么解决。
首先大家可能都清楚我们的宏变量都需要括号括起来。
比如 MUL, 如果不加括号, 就是 MUL1 了, 然后输出变成 6 了。为什么是 6 呢?我们模拟一下展开过程

//a = 2, b = 3
MUL1(a+1, b+1)
a+1 * b+1 
a + b + 1
6

很好,不加括号确实应该是6.
那第二个为什么会输出 msg:mul error 呢?展开后再格式化后我们可以看到是下面的样子

if(c != 12)
    LOG("mul error");
=>
if(c != 12)
    printf("line:%d\n",__LINE__);printf("msg:%s\n",msg);
=>
if(c != 12)
    printf("line:%d\n",__LINE__);
    
printf("msg:%s\n",msg);

大家应该会想到需要在宏里面加大括号,但是我们该如何加呢?
加之前大家可以先看看我们的宏定义

#define MUL(x,y) (x)*(y)
#define MUL1(x,y) x*y
#define LOG(msg) printf("line:%d\n",__LINE__);printf("msg:%s\n",msg)

我们的宏定义后面都缺少一个分号,为什么这样做呢?
为了满足视觉上的合理性,即调用时我们往往会在后面加一个分号

c = MUL(a+1, b+1);
c = MUL1(a+1, b+1);
LOG("mul error");

好的,这个背景介绍完了,我们来看看加上大括号后的样子吧。加大括号的时候肯定是先把分号补上了。

#define MUL(x,y) {(x)*(y);}
#define MUL1(x,y) {x*y;}
#define LOG(msg) {printf("line:%d\n",__LINE__);printf("msg:%s\n",msg);}

然后我们惊奇的发现竟然编译不过去。
当然如果你编译过去了,只是简单的收到几个警告,那说明你用的是最新版本的支持C++11的编译器。
这里假设你编译不过去了。为什么编译不过去呢?我们把第一个宏展开看看是什么吧。

c = {(a+1)*(b+1);};

天呢,这是什么东西,显然是有问题的。

好吧,这个问题我们没办法解决了。
在我们不知道怎么办的时候,我们意外的发现 LOG(“mul error”); 竟然编译过去了。
为什么呢? 再次展开一下

if(c != 12)
    LOG("mul error");
=>
if(c != 12)
    {printf("line:%d\n",__LINE__);printf("msg:%s\n",("mul error"));};
    
=>
if(c != 12){
    printf("line:%d\n",__LINE__);
    printf("msg:%s\n",("mul error"));
}
;

好吧,除了最后多了一个分号空语句,其他的地方都是完美的。
但是这个可恶的分号会影响 else 语句的。

if(c != 12)
    LOG("mul error");
else
    printf("ok\n");
=>
if(c != 12){
    printf("line:%d\n",__LINE__);
    printf("msg:%s\n",("mul error"));
}
;
else
    printf("ok\n");   

这个时候又会编译不过去的。
但是这个错误是由于我们的代码编写不规范导致的,我们通过加大括号可以解决。

if(c != 12){
    LOG("mul error");
}else{
    printf("ok\n");
}
=>
if(c != 12){
    {
        printf("line:%d\n",__LINE__);
        printf("msg:%s\n",("mul error"));
    }
    ;
}else{
    printf("ok\n"); 
}

虽然加大括号后展开的代码看着比较奇怪,但是最起码我们暂时解决了一个问题。
你也知道,大部分程序员都是有洁癖的。
因此怎么能容忍这么丑陋的代码存在呢?
于是我们需要寻找看起来比较优美的展开代码。
程序员的智慧是无限的,还真找到两个来。

#define FOO(X)   do { something;}   while (0)
#define FOO(X)   if (1) { something; }   else

上面两个代码就是比较优美的代码,展开后是这个样子

#define LOG(msg) do {printf("line:%d\n",__LINE__);printf("msg:%s\n",msg);} while (0)
if(c != 12){
    LOG("mul error");
}else{
    printf("ok\n");
}
    
=>
if(c != 12){
    do {
        printf("line:%d\n",__LINE__);
        printf("msg:%s\n",msg);
    }  while (0);
}else{
    printf("ok\n");
}

#define LOG(msg) if (1) { printf("line:%d\n",__LINE__);printf("msg:%s\n",msg); } else
if(c != 12){
    LOG("mul error");
}else{
    printf("ok\n");
}
    
=>
if(c != 12){
    if (1) { 
        printf("line:%d\n",__LINE__);
        printf("msg:%s\n",msg);
     } else ;
}else{
    printf("ok\n");
}

上面两个代码看起来优美多了,而且 do{}while(0) 用的更多一些,毕竟 else ; 看着还是有那么一点不舒服。
但是优美归优美,我们还是需要把所有问题解决了才算真正的优美。
那对于 这个丑陋的代码 {(a+1)*(b+1);}; 到底该如何解决呢?
这么丑陋的代码是由这个语言本身的语法导致的,所以只好从语言本身上解决了。
简单的说使用目前的方法没办法完美解决,即定义一个规则不能满足所有情况,于是官网提供了一个新的语法
c++11 定义了新的语法:

#define MUL(x,y) ({(x)*(y);})
c = ({(a+1)*(b+1);});

看到了什么?

简单的说就是对宏语句加个大括号,然后大括号外加个小括号。最后一个值作为返回值。
这和很多解释性语言的函数类似,最后一条语句的返回值作为函数的返回值。
这个问题颇为复杂,不过前端时间我写了这么一篇文章 宏 do{}while(0)., 感兴趣的可以看一看。http://github.tiankonguse.com/blog/2014/09/30/do-while.html
看到这,有些人可能会感觉有点不对劲,但是那里不对劲有说不上来。
于是再看一遍,找到原因了:作者在误导人啊。为什么呢?
还记得上面加大括号的时候我们是往宏上加的,有的人可能会说我们先把代码写规范了,在看看会有那些问题呗。

#include <stdio.h>
#define MUL(x,y) ((x) * (y))
#define LOG(msg) printf("line:%d\n",__LINE__);printf("msg:%s\n",msg)
int main() {
    int a = 2, b = 3, c;
    c = MUL(a+1, b+1);
    printf("%d * %d :mul = %d\n", a, b, c);
    if(c != 12) {
        LOG("mul error");
    }
    return 0;
}

理想情况下,代码先写规范了, 发现什么问题都没有。
但是现实和理想还是有差距的。细心的人可能发现我的 MUL 这个宏和上面的宏还是有点区别的, 最外面又加了一个小括号,这也是一坑。那究竟什么样的代码才是规范的呢?这个不好说,因为一个语言的所有细节都要整理出来的话,那将会是一个很大的文档。
所以目前这些只好靠经验,阅读书籍,看别人的文档,一个坑一个坑的踩之后才能慢慢的了解这些细节。
如果谁知道有这么一个文档的话,可以留言告诉我,我只知道有一个简单的文档 Google C++ Style Guide

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值