3 挑战指针(harib01c)

前面说过“C语言中没有直接写入指定内存地址的语句”,实际上这不是C语言的缺陷,因为有替代这种命令的语句。一般大多数程序员主要使用那种替代语句,像这次这样,做一个函数write_mem8的,也就只有笔者了。如果有替代方案的话,大家肯定想用一下,笔者也想试试看。

write_mem3(i, i & 0x0f);

替代以上语句的是:

*i = i & 0x0f;

两个语句有点像,但又不尽相同。不管那么多了,先换成后面一种写法看看吧。好了,改完了,用“make run”命令运行一下。唉?奇怪,怎么会出错呢?

invalid type argument of `unary *'

类型错误?

■■■■■

没错,就是类型错误。这种写法,从本质上讲没问题,但这样就是无法顺利运行。我们从编译器的角度稍微想想就能明白为什么会出错了。回想一下,如果写以下汇编语句,会发生什么情况呢?

MOV [ 0x1234], 0x56

是的,会出错。这是因为指定内存时,不知道到底是BYTE,还是WORD,还是DWORD。只有在另一方也是寄存器的时候才能省略,其他情况都不能省略。

其实C编译器也面临着同样的问题。这次,我们费劲写了一条C语句,它的编译结果相当于下面的汇编语句所生成的机器语言,

MOV [i], (i & 0x0f)

但却不知道[i]到底是BYTE,还是WORD,还是DWORD。刚才就是出现了这种错误。

那怎么才能告诉计算机这是BYTE呢?

char *p; /*,变量p是用于内存地址的专用变量*/

声明一个上面这样变量p, p里放入与i相同的值,然后执行以下语句。

*p = i & 0x0f;

这样,C编译器就会认为“p是地址专用变量,而且是用于存放字符(char)的,所以就是BYTE.”。顺便解释一下类似语句:

char *p; /*用于BYTE类地址*/
short *p; /*用于WORD类地址*/
int *p; /*用于DWORD类地址*/

这次我们是一个字节一个字节地写入,所以使用了char。

既然说到这里,那我们再介绍点相关知识,“char i; ”是类似AL的1字节变量,“short i; ”是类似AX的2字节变量,“int i; ”是类似EAX的4字节变量。

而不管是“char *p”,还是“short *p”,还是“int *p”,变量p都是4字节。这是因为p是用于记录地址的变量。在汇编语言中,地址也像ECX一样,用4字节的寄存器来指定,所以也是4字节。

■■■■■

这样准备工作就OK了。再用“make run”运行一遍以下内容。

void HariMain(void)
{
    int i; /*变量声明。变量i是32位整数*/
    char *p; /*变量p,用于BYTE型地址*/

    for (i = 0xa0000; i <= 0xaffff; i++) {

        p = i; /*代入地址*/
        *p = i & 0x0f;

        /*这可以替代write_mem8(i, i & 0x0f); */
    }

    for (; ; ) {
        io_hlt();
    }
}

哇,居然不使用write_mem8就能显示出条纹图案,真是太好了。

嗯?且慢!仔细看看画面,发现有一行警告。

warning: assignment makes pointer from integer without a cast

这个警告的意思是说,“赋值语句没有经过类型转换,由整数生成了指针”。其中有两个单词的意思不太明白。类型转换是什么?指针又是什么?

类型转换是改变数值类型的命令。一般不必每次都注意类型转换,但像这次的语句中,如果不明确进行类型转换,C编译器就会每次都发出警告:“喂,是不是写错了?”顺便说一下,cast在英文中的原意是压入模具,让材料成为某种特定的形状。

指针是表示内存地址的数值。C语言中不用“内存地址”这个词,而是用“指针”。在C语言中,普通数值和表示内存地址的数值被认为是两种不同的东西,虽然笔者也觉得它们没什么不同,但也只能接受这种设计思想了。基于这种设计思想,如果将普通整数值赋给内存地址变量,就会有警告。为了避免这种情况的发生,可以这样写:

p = (char *)i;

这就对i进行了类型转换,使之成为表示内存地址的整数。(其实这样转换以后,数值一点都没变,但对于C编译器来说,类型的不同有着很大的差别。)以后再进行这样的赋值时,就不会出现这种讨厌的警告了。于是我们这样修改一下。

再运行一次“make run”吧。好了,不再出现那种烦人的警告了。write_mem8已经没用了,所以可以将它从naskfunc.nas中删除。

这样的写法虽然有点绕圈子了,但我们实现了只用C语言写入内存的功能。

COLUMN-2 只要使用类型转换,就可以不用指针之类的方法吗?

好不容易介绍完了类型转换,我们来看一个应用实例吧。如果定义:

p = (char *) i;

那么将上式代入下面语句中。

*p = i & 0x0f;

这样就能得到下式:

*((char *) i) = i & 0x0f;

这个语句执行起来毫无问题。虽然读起来不是很容易理解,但这样可以不特意声明p变量,所以笔者偶尔还是会使用的。

有没有觉得这种写法与“BYTE[i] = i & 0x0f; ”有些相像吗?在特别喜欢汇编语言的笔者看来,会有这种感觉呢。(笑)

COLUMN-3 还是不能理解指针

能有这种想法,说明你很诚实。那好,我们再尽量详细地讲解一下。

如果你曾经使用过C语言,并且听说过“指针”这个词,那么刚才的说明肯定让你觉得混乱,摸不着头脑。倒是那些从未接触过C语言的人更能理解一些。

这里,特别重要的一点是,必须想点办法让C语言完成以下功能:

MOV BYTE [i], (i & 0x0f)

也就是,向内存的第i号地址写入i & 0x0f的计算结果。而程序只是偶然地写成了:

int i;
char *p;

p = (char *) i;
*p = i & 0x0f;

必须要先理解以上程序。这可能与你所知道的指针的使用方法完全不同,不过暂时先不要想这个。总之上面4行,是MOV语句的替代物,这一点是最重要的。

从没听说过C语言指针的人,仅仅会想“哦,原来C语言中是这么写的,没那么复杂么。”的确如此,没什么不懂的嘛。

■■■■■

下面再稍微深入说明一下。我们常见的两个语句是:

p = (char *) i;
*p = i & 0x0f;

这两个语句有什么区别呢?这是不懂汇编的人常有的疑问。将以上语句按汇编的习惯写一下吧。假设p相当于ECX,那么写出来就是:

MOV ECX, i
MOV BYTE [ECX], (i & 0x0f)

它们的区别很清楚,即一个是给ECX寄存器赋值,一个给ECX号内存地址赋值。这完全是两回事。存储它们的半导体也不一样,一个在CPU里,一个在内存芯片里。在C语言中,虽然p与*p只有一字之差,但意思上的差别却如此之大。

如果执行顺序调过来会怎么样呢?也就是像这样:

*p = i & 0x0f;
p = (char *) i;

不是很熟悉指针的人可能认为这样也行。但是,这相当于:

MOV BYTE [ECX], (i & 0x0f)
MOV ECX, i

如果这么做,第一个MOV的时候,ECX的值不确定,是个随机数,这会导致i & 0x0f的结果写入内存的某个不可知的地址中。这样的后果很严重。

■■■■■

另一个比较常见的疑问,是关于声明的。在C语言中,如果不声明变量就不能使用。所谓声明,就是类似“int i; ”这种语句。有了这句话,变量i就可以使用了(与此不同的是汇编语言中,EAX, DL等,不声明也可以自由使用)。在C语言中,声明了10个变量,就可以用10个变量,这是理所当然的事。

既然如此,那为什么只声明了“char *p; ”却不仅能使用p,还可以使用*p呢?这让人搞不懂……确实,这个程序中,给p和*p赋值了。看上去,能够使用的变量数比实际声明的变量数要多。

遇到这种情况时,我们先回到汇编语言中看看。

MOV ECX, i
MOV BYTE [ECX], (i & 0x0f)

看着这个程序,就不会再有人认为其中有2个变量了。其中只有一个ECX。而且,同样是“MOV AL, [ECX]”, ECX是123的时候,和ECX是124的时候,放入AL的值也是不同的(只要这两处地址存放的不是同样的值)。这是因为地址不同,代表的内存区域不同。就好比不同的住址,住的人也不一样。

所以,同样是*p,因为p值的不同,记录的值也不同。

*p = 3;
p = p + 3;
i = *p;

也就是说如果执行以上片段,i不一定是3,因为地址已经变了。

费了半天劲,其实笔者想说的就是,*p并不是什么变量。确实,我们可以给*p赋值,也可以引用*p的值,这看起来就像变量一样。但即便如此,*p也不是一个变量,变量只有p。所谓*p,就相当于汇编中BYTE [p]这种语句的代替。

如果你还执拗地说*p是一个变量,那照这种逻辑,变量可远不止2个,还有很多很多。因为只要给p赋上不同的值,*p就代表完全不同区域的内存内容。

■■■■■

下一个问题也是关于声明的:“char *p; ”声明的是*p,还是p呢?

这也是一个常见的问题。先给出结论吧,声明的是p。“既然如此,那为什么不写成char*p;呢?”有这种想法,说明你这方面的直觉很好。笔者也认为这样写对于初学者来说更简单易懂。事实上,在C语言中写成“char* p; ”也可以,既不出错,也不出警告,运行也没问题。

但这种写法有点小问题。如果写成“char* p, q; ”,我们看上去会觉得p和q都表示地址的变量,但C编译器却不那样认为,q会被看作是一般的1字节的变量。也就是被解释成“char*p, q”。为了避免这样的误解,一般的程序员不写成“char* p; ”,所以笔者也按照这个习惯编写程序。另外,如果想要声明两个地址变量,就写成“char *q, *p; ”。

■■■■■

今天的专栏写得好长呀,我们来整理总结一下吧。首先,本书中出现的“char *p; ”不必看作指针,这是最重要的决窍。p不是指针,而是地址变量。不要使用“p是指针”这种模棱两可的说法,“p是地址变量”这种说法比较好。

将地址值赋给地址变量是理所当然的。并且,既然地址代表的是内存的地址,可以让该地址存放自己想放的任何值。虽然也可以将地址变量说成是指针,但笔者听到指针这个说法也很茫然,所以除了跟别人讨论时以外,笔者也不说指针什么的。

C语言中地址变量的声明,以及给内存地址赋值的,写法不是很习惯,但终究这只是写法的不同,思考问题的方法与汇编语言差不多。在C语言开发人员看来,“C语言的*p比汇编语言BYTE [p],更短小精悍”,确实,简洁是一个长处,但就是因为简洁,才让初学者不好理解。

C语言的很多初学者都在学习指针时受挫,以至于会想“如果没有指针就好了”。而事实上,没有指针的语言也确实是存在的。但这种语言很不好用,因为没有指针就无法往指定内存的地址存入数据,那怎么往VRAM上绘制图像呢?这种语言只能让写操作系统变得更加困难。

笔者也认为,C语言指针的语法很难理解,所以希望能改善。但它像汇编语言一样,能直接访问地址,这一点非常好。所以希望大家能这样想:“不是要废除指针,而是把指针改善得更直观易懂。”