转自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