分类目录归档:C/C++/C#

奇怪的C语言数组形式

想起以前读『C陷阱与缺陷』时颠覆过我对数组认知的一个栗子: "0123456789"[n] 居然是一个合法的数组形式。

1
printf("%c", "0123456789"[0]);

以上代码运行后打印字符‘0’,依次类推,printf("%c", "0123456789"[1])打印字符‘1’,printf("%c", "0123456789"[2])打印字符‘2’……这种用法用来解决某些机器的字符集里数字不是顺序排列的问题。例如ASCII码表里字符‘0’-‘9’分别对应着编码0x30-0x39,但有些机器里不是这么按顺序排的……犹记得『K&R』也有提到某些架构的机器上不宜用c+‘0'这样方法来求c的数字表示,原因也是如此,不过具体哪些类型机器会采取这种策略就不得而知了。
  
C语言里,一个字符串常量可以用来表示一个字符数组,所以在数组名出现的地方都可以用字符串常量来替换。

--EOF--

数组和指针并不总是相同

『C专家编程』(Expert C Programming)足足花了三个章节的篇幅来讲解C语言中数组和指针的异同点,足以见得数组和指针对人们的误导能力有多强,从作者总结的冷幽默中也能察觉到一二:“在你阅读并理解前面的章节之前不要阅读这一节内容,因为它可能会使你的脑力永久退化。” = =!

数组和指针对于初学者来讲当然是不同的,很显然的是两个概念。等渐渐入门之后,会越来越发现,数组和指针就是一个东西,比如,无论将数组还是指针当作参数传给一个函数,函数内部总是将它们当作指针来处理,也就是说,编译器会将数组类型的参数退化为指针,再进行处理。此外,还能举出很多两者可以混用的例子来,甚至到最后,想举出一个例子证明数组和指针不能混用反而变得不容易起来。数组和指针最为本质的区别需从它们在内存中如何寻址说起,要对数组中元素进行存取操作,程序只需简单的将数组首地址和数组下标(对于不是1个字节长度的数据类型,编译器会对数组下标进行步长处理)相加便可得到所需的结果,数组首地址就是定义该数组时的变量名,示意图如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
char a[5] = "12345";
...
char c = a[2];
                                Addr(a)-->+-----------+
                               0x12340000 | '1'(0x31) |
                                          +-----------+
                               0x12340001 | '2'(0x32) |
                                          +-----------+
                               0x12340002 | '3'(0x33) |
                                          +-----------+
                               0x12340003 | '4'(0x34) |
                                          +-----------+
                               0x12340004 | '5'(0x35) |
                                Addr(c)-->+-----------+
                               0x12340005 | '3'(0x33) |
                                          +-----------+

示例一 数组下标寻址方式
(Download ASCII Picture)

要为c赋值,首先要获得a[2]的值,a[2]的计算方法为:
1、取得a的地址,0x12340000。
2、通过数组下标计算步长,所得结果与a的地址相加,得地址0x12340002。
3、取地址0x12340002中的内容,赋值给c。

以上是数组下标引用方式的计算步骤,而指针方式的赋值则不是那么回事了,指针方式的示例图如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
char a[5] = "12345";
...
char* p = a;
char c= *p;
                              Addr(a)-->+-----------+
                             0x12340000 | '1'(0x31) |<--+
                                        +-----------+   |
                             0x12340001 | '2'(0x32) |   |
                                        +-----------+   |
                             0x12340002 | '3'(0x33) |   |
                                        +-----------+   |
                             0x12340003 | '4'(0x34) |   |
                                        +-----------+   |
                             0x12340004 | '5'(0x35) |   |
                              Addr(p)-->+-----------+   |
                             0x12340005 |   0x00    |---+
                                        +-----------+
                             0x12340006 |   0x00    |
                                        +-----------+
                             0x12340007 |   0x34    |
                                        +-----------+
                             0x12340008 |   0x12    |
                              Addr(c)-->+-----------+
                             0x12340009 | '1'(0x31) |
                                        +-----------+

示例二 指针寻址方式
(Download ASCII Picture)

要为c赋值,首先要解引用p,p的解引用过程为:
1、取得p的地址,0x12340005。
2、取地址0x12340005上的内容0x12340000(由4个字节组合而成)。
3、取地址0x12340000上的内容。
如果c的赋值表达式涉及指针运算,步骤3取内容前需对地址0x12340000进行运算。

某些场合下,当程序中将数组和指针混用时,就会出现bug,以下是一种可能情况:
在文件A中定义a为数组:char a[5] = "12345";
在文件B中用extern方式声明a为指针:extern char *a;
当文件B中代码试图对a进行解引用时,根据编译器对a的理解,它是一个指针,所以用上述示例二的间接方式进行寻址,所以char c = *a语句实际上被分解为以下步骤:
1、取得a的地址,0x12340000。
2、取地址0x12340000上的内容0x34333231(由4个字节组合而成)。
3、取地址0x34333231上的内容。
错误就是这样发生了。。。根据原来的意图,我们在步骤1后面应该直接进行步骤3。同理,当在文件A中定义了指针p,而在文件B中用extern方式声明p为数组,也会发生类似的错误。这就是一个数组和指针不同的典型例子。

此外,从上述示例一和示例二所描述的寻址过程来看,不考虑编译器优化的情况下,数组方式比指针方式效率更高,因为数组方式比指针方式少一个访问内存的步骤。

--EOF--

C/C++函数默认返回值

之前有个误区,认为凡是声明了返回值类型(非void)的函数,要是在函数体中没有显式return相应类型的返回值时,编译器会报错的。今天读别人写的程序发现一个声明了返回类型是int的函数,它最后没有显式调用return返回值,但是程序流程走下来发现也没有bug,不解。遂查了下资料,解开疑惑。

C/C++不像Java中的方法,声明了非void类型的方法必须要返回一个确定类型返回值。在我使用的MSVC编译器中,一个有返回类型的函数如果函数体中没有显式调用return语句,编译器会给出一个C4716的警告(warning): warning C4716: 'func' : must return a value。那么这种情况下函数会返回什么值呢?首先的想法就是编译器帮你插入一条默认返回值。然而实际上不是这样的,C/C++函数没有默认的返回值,C/C++标准中没这种说法,因此本文题目其实起得就不够严谨。可是不写return语句确实又能返回,写一个简单的测试程序其实就能测出,没有return语句的函数会返回一个随机数,类似一个未初始化局部变量那样的数,这个数哪里来的呢?实际上,把一段程序经编译后生成汇编代码,就能看见真相。

这是一个C/C++的函数定义,它什么都不干,就返回一个整数1234。

1
2
3
4
int func()
{
    return 1234;
}

经编译后,这段函数定义会被编译成如下汇编代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
...
3:    int func()
4:    {
00401020   push        ebp
00401021   mov         ebp,esp
00401023   sub         esp,40h
00401026   push        ebx
00401027   push        esi
00401028   push        edi
00401029   lea         edi,[ebp-40h]
0040102C   mov         ecx,10h
00401031   mov         eax,0CCCCCCCCh
00401036   rep stos    dword ptr [edi]
5:        return 1234;
00401038   mov         eax,4D2h
6:    }
0040103D   pop         edi
0040103E   pop         esi
0040103F   pop         ebx
00401040   mov         esp,ebp
00401042   pop         ebp
00401043   ret
...

其他不用考虑,直接看第14行、15行。可以看到return 1234被编译成了mov eax,4D2h,由此可见,其实编译器在处理函数返回值的时候,其实是把待返回的值放在eax寄存器中,然后调用函数再从该寄存器中将值(被调函数的返回值)取出。

这点可再次通过程序验证一下,直接在被调函数体中内嵌汇编代码,为eax寄存器赋值,查看程序执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
 
int func() 
{
    _asm{
        mov eax, 1234;
    }
}
 
int main() 
{
    printf("%d\n", func()); //打印func()函数返回值1234。
    return 0;
}

回到上面问题,如果没有显示调用return返回语句,那么在调用程序中还是会去eax寄存器中取值,只是这时的eax寄存器中值是不确定的,因此会打印一串无意义的数字,于是程序就有了潜在的bug。 (衔山)

--EOF--

C#的委托和事件

为新项目做准备才学了C#,发现里面的委托(Delegate)和事件(Event)特别重要,看了一个下午感觉也不是很懂,现依葫芦画瓢整理出一个控制台版的demo程序,委托和事件的基本用法就像这样,具体的应用等以后有机会深入了再总结。

委托其实就是C/C++里的函数指针,它可以指向一些指定类型的函数,设置委托之后,程序中哪里要调用原函数的地方都可以用委托代替。它的一个好处就是一个委托可以代理多个函数,当然这些函数必须有相同的参数类型、参数个数、参数顺序和返回类型,简而言之,函数签名一定要一致。委托其实是一个类,因此,程序中任何可以声明类的地方都可以声明一个委托,如下形式:

1
public delegate void TestDelegate(int i);

以上声明了一个AlarmEventHandler的委托类,并且规定,能够委托给它的方法类型只能是1个int型参数、返回值为void的。委托声明之后,可以把一些合法类型的方法通过"+="或"-="操作符委托给这个委托的实例(有点绕~~)。像这样:

1
2
3
4
5
6
TestDelegate aDelegate = new TestDelegate(); //实例化一个委托
aDelegate += Class1.func1; //把Class1.func1委托给aDelegate 
aDelegate += Class1.func2; 
aDelegate += Class1.func3;
 
aDelegate(param); //委托调用

假设Class1的func1、func2、func3方法类型都是接受1个int参数,返回值void。当第6行aDelegate(param)执行后,就相当于func1、func2、func3都执行了一遍。这就是委托。

事件总是与委托配合使用,事件除了要用关键字event修饰之外,它还有需声明一个委托类型。一个事件就是一个通知,它会通知声明时指定的委托类型所委托的那些方法(又有点绕~~),接上例,假如我声明一个TestDelegate类型的事件,那么当我在代码中发出这样一个事件通知之后,上述的func1、func2、func3都会被调用。见例子:

1
2
public event TestDelegate testEvent; //声明事件
testEvent(param); //事件发生,发出通知。

声明一个TestDelegate委托类型的事件之后,当程序中调用testEvent()发出事件通知时,在TestDelegate委托中注册过的方法都会执行到。

下面这个关于委托和事件的demo程序的功能为:从0到10000计数,每当遇见一个1000的倍数时,就把这个数打印出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace DelegateAndEvent
{
    public class Counter
    {
        private int counter;
 
        public delegate void AlarmEventHandler(int i); //声明委托
        public event AlarmEventHandler alarm; //声明事件
 
        public Counter() {
            counter = 0; //构造函数初始化变量counter。
        }
 
        public void DoCounter() { 
            while (counter <= 10000){
                if (counter % 1000 == 0)
                {
                    if (alarm != null)
                    {
                        alarm(counter); //符合条件时触发一个alarm事件
                    }
                }
                counter++;
            }
        }
        /**
         *将要被委托的方法. 
         */
        public void Alarm(int i) {
            Console.WriteLine("counter = " + i);
        }
 
        public static void Main() {
            Counter c = new Counter(); //实例化一个Counter类。
 
            //将c.Alarm通过AlarmEventHandler委托与事件c.alarm建立联系。
            //此后,c.alarm发出事件通知都会告知(调用)c.Alarm() 
            c.alarm = new AlarmEventHandler(c.Alarm);
 
            //c.DoCounter()中会触发多次c.alarm()事件,
            //意味着c.Alarm()函数会调用多次。
            c.DoCounter();
        }
    }
}

执行结果如下图:

看了上面这个程序之后,肯定会觉得实现这样的功能用什么委托和事件啊~~确实是这样的,这个例子举得有些挫,完全体现不出委托和事件的优势来,呵呵~~

--EOF--

C语言异常处理机制的实现

C语言没有高级语言(C++, Java, C#, etc.)那样完善的异常处理机制,但是它却提供了一组setjmp()和longjmp()函数来实现自己的异常处理,这一点经常被人所忽略。

setjmp()和longjmp()是通过操作过程的活动记录来实现它强大的转移能力的,先介绍它们的使用方法:

  • setjmp(jmp_buf jb):它表示使用jmp_buf型的变量jb来记录当前的位置(活动记录),并且返回0。它必须在longjmp()之前调用,这里的"之前"是指程序执行逻辑上的之前,而不是代码所在位置。
  • longjmp(jmp_buf jb, int retval):它表示让程序回到jb所记录的位置上去,即恢复到执行setjmp()函数时的活动记录现场,但是它的返回值是retval,该值由自己指定。通过返回值,后面的程序就能知道前面的程序是通过setjmp()返回的还是通过longjmp()返回的。
  •  
    有一点要注意,使用setjmp()/longjmp()必须要包含头文件setjmp.h(活动记录结构体jmp_buf就是在它里面定义的)。它们的执行流程可以用下面的程序进行展示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    
    #include <stdio.h>
    #include <setjmp.h>
     
    jmp_buf buf;
    void func()
    {
        printf("before longjmp() in func()\n");
        longjmp(buf, 1);
        printf("after longjmp() in func()\n");
    }
     
    int main()
    {
        int ret = setjmp(buf);
        if (ret == 0)
        {
            printf("first time to execute!\n");
            func();
        }else
        {
            printf("jmped from func()\n");
        }
     
        return 0;
    }

    执行结果为:

    1
    2
    3
    4
    
    me@ubuntu:~/workspace/cexception$ ./a.out 
    first time to execute!
    before longjmp() in func()
    jmped from func()

    稍微解释一下:程序从main()函数开始,第一次执行setjmp()的结果返回0,程序进入if分支,打印“first time to execute!”。接着调用func()函数,打印"before longjmp() in func()",当执行到longjmp(buf, 1)时,程序转回第14行的setjmp()函数,但是此时返回值为longjmp()函数中指定的1,于是接下去执行else分支,打印"jmped from func()"。这个过程中if…else…语句执行了2次。

    当程序中调用多次longjmp()时,程序就会可能多次回到setjmp()调用的地方,根据这点特性,setjmp()/longjmp()函数就能被用来进行错误恢复,当程序执行过程中出现一个不可恢复的错误时,就能将控制权转回某个定点,重新执行。C语言的异常处理机制就是通过setjmp()/longjmp()来实现的。当setjmp()/longjmp()、if…else…、switch…case…语句联合起来使用时,就能实现C++中的try…throw…catch…语句的简化版。示例程序如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    
    #include <stdio.h>
    #include <setjmp.h>
     
    jmp_buf buf;
    void func1()
    {
        printf("func1() throws exception.\n");
        longjmp(buf, 1);
    }
     
    void func2()
    {
        printf("func2() throws exception.\n");
        longjmp(buf, 2);
    }
     
    void func3()
    {
        printf("func3() throws exception.\n");
        longjmp(buf, 3);
    }
     
    int main()
    {
        int ret = setjmp(buf);
        if (ret == 0)
        {
            func1();
            func2();
            func3();
        }else
        {
            switch(ret)
            {
                case 1:
                    printf("catch exception from func1().\n");
                    break;
                case 2:
                    printf("catch exception from func2().\n");
                    break;
                case 3:
                    printf("catch exception from func3().\n");
                    break;
                default:
                    break;
            }
        }
     
        return 0;
    }

    程序执行结果如下:

    1
    2
    3
    
    me@ubuntu:~/workspace/cexception$ ./cexception.o 
    func1() throws exception.
    catch exception from func1().

    主程序的if分支相当于一个try语句块,func1()、func2()和func3()函数体中的longjmp()就相当于一个throw语句。当一个"异常"被抛出后,外层程序在else分支(对应于catch语句)中将它捕捉,然后可以进行相应的"异常"处理(switch…case…语句)。

    setjmp()/longjmp()可以做到跨函数甚至跨文件地转移,这与goto语句只能在当前函数中转移不同。但是与此同时,与goto一样,过多地使用setjmp()/longjmp()会与面向过程的程序设计思想相悖,导致程序结构混乱,难以理解和调试。

    Reference:
    [1]: Peter Van Der Linden 著, 徐波 译. C专家编程[M]. 人民邮电出版社, 2009.12.

    --EOF--