C语言变量

C语言二进制、八进制、十六进制详解

什么是二制制?

在数学计算中,二进制计数系统的公分母是最小的,它以2为基数。你还记得在小学或中学时所学的不同的计数系统吗?笔者在上小学时,曾在一堂数学课中学过以6为基数的计数系统;你先数1,2,3,4,5,然后是10,11,12,13,14,15,然后是20,等等,实际上,应该先数0,1,2,3,4,5,然后是10,1l,12,13,14,15,等等。从O开始数,能比较清楚地看出每6个数字组成一组——因此6就是基数。注意,你应该从O开始一起数到比基数小1的数(因为基数是6,所以你应该从O数到5)。当你数到5后,接着应该开始数两位数。如果你思考一下,你就会发现这与以10为基数(十进制)的计数系统是类似的——在你数到比基数小1的数(9)后,就转到两位数,并继续往下数。

计算机中的计数系统以2为基数——即二进制。由于以2为基数,所以你先数O,1,然后是10,11,然后是100,101,110,111,然后是1000,1001,1010,1011,1100,1101,1110,1111,等等。与以6为基数时不同,在以2为基数时,在数到两位数之前,只需从O数到1。

那么,为什么在计算机中要以2为基数呢?其原因在于计算机中使用了晶体管。晶体管使现代计算机的出现成为可能。晶体管就象电灯开关,电灯开关有“开”和“关”两种状态,晶体管也是如此。你可以认为“关”表示0,“开”表示1,这样,你就可以用一个晶体管(如果你愿意,也可以用一个电灯开关)来进行从。到1的计数了。仅仅使用两个数字(O到1)还不能做任何复杂的计算,但是我们还可以继续下去。假设有一个电灯开关控制面板,上面有4个大电灯开关,尽管每个开关只有两种状态,但是这些开关组合起来就会有16或2。(4个开关,每个2种状态)种不同的状态。这样,你就可以用4个开关来进行从。到15的计数了,见表20.22。

  表20.22  进制计数
-------------------------------------------------------
      开关         十进制值            幂
-------------------------------------------------------
       O             O
       1             1                 20
      10             2                 21
      11             3
     100             4                 22
     101             5
     110             6
     111             7
    1000             8                 23
    1001             9
    1010            10
    1011            11
    1100            12
    1101            13
    1110            14
    1111            15
-------------------------------------------------------
上表说明了很重要的三点:
通过把开关并排放在一起,你就可以用它们来计数了——在本例中最多可以数到15(总共16次计数);
你可以把每个开关看作是一个二进制位,就象十进制系统中的十进制位一样;
如果每个开关都代表一个二进制位,那么它们刚好也都代表一个2的幂(20,21,22,23,等等)。

此外,请注意,在表中出现2的幂的地方,计数结果就要增加一个二进制位。这与十进制系统是相同的,每增加一个十进制位时,这个新的十进制位也正是一个10的幂(1=100,10=101,100=102,等等)。明白了这一点后,你就可以很容易地把二进制数转换为十进制数了,例如,二进制数10111就是(1×24)+(O×23)+(1×22)+(1×21)+(1×20),它等于十进制的(16+0+4+2+1)或23。10 1110 1011,一个大得多的二进制数,就是(1×29)+(O×28)+(1×27)+(1×26)+(1×25)+(0×24)+(1×23)+(O×22)+(1×21)+(1×20),它等于十进制的(512+0+128+64+32+0+8+0+2+1)或747。

那么所有这些和我们有什么关系呢?在计算机领域中,存在着位(bit),半字节(nibble)和字节(byte)。一个半字节是4位,一个字节是8位。什么是一个位呢?它就是一个晶体管。因此,一个字节就是8个相邻的晶体管,就象表20.1中的4个开关一样。记住,如果你有4个组合在一起的开关(或晶体管),你就可以数24或16,你可以把这看作是由开关组成的一个半字节。如果一个半字节是4个晶体管组合在一起,那么一个字节就是8个晶体管组合在一起。你可以用8个晶体管数到2。或256,从另一个角度看,这意味着一个字节(含8个晶体管)可以表示256个不同的数字(从0到 255)。再深入一点,Intel 386,486和Pentium处理器被叫做32位处理器,这意味着这些Intel芯片所进行的每一次运算都是32位宽或32个晶体管宽的。32个晶体管,或32位,等价于232或4,294,967,296,即它们能表示超过40亿个不同的数字。

当然,上述介绍还不能解释计算机是如何利用这些数字产生那种神奇的计算能力的,但它至少解释了计算机为什么要使用以及是如何使用二进制计数系统的。

什么是八进制?

八进制是以8为基数的一种计数系统。在八进制系统中,你是这样计数的:O,1,2,3,4,5,6,7,10,ll,12,13,等等。下面比较了八进制(第二行)和十进制(第一行)中的计数过程:
    O,l,2,3,4,5,6,7,8,9,10.11,12,13,14,15,16
    0,1,2.3,4,5,6,7,10,11,12,13,14,15,16,17,20
注意,在八进制中,在数到7后,就要增加一个八进制位,第二个八进制位显然就是8?(等于十进制的8)。如果你数到第三个八进制位(八进制的100),那将是8?或十进制的64,因此,八进制的100等于十进制的64。

现在,八进制已经不象以前那样常用了,这主要是因为现在的计算机使用的是8,16,32或64位处理器,最适合它们的计数系统是二进制或十六进制(见20.24中有关十六进制计数系统的介绍)

C语言支持八进制字符集,这种字符要用反斜杠字符来标识。例如,在C程序中,下面的语句并不少见:
    if(x=='\007')break;
这里的"\007"恰好就是ASCII值为7的字符;该语句用来检查终端鸣笛字符。另一个常见的八进制数是"\033",即Escape字符(在程序中它通常表示为"\033")。然而,八进制数现在已经很少见了——它们被十六进制数代替了。

什么是十六进制?

十六进制(hexadecimal,缩写为hex)是以16为基数的计数系统,它是计算机中最常用的计数系统。十六进制中的计数过程为:O,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F,10,11,12,13,14,15,16,17,18,19,1A,1B,1C,1D,1E,1F,等等。十六进制中的字母是几个单位数标识符,表示十进制的10到15。要记住在不同基数下的计数规则,即从O数到比基数小1的数字,在十六进制中这个数就是十进制的15。因为西式数字中没有表示大于9的单位数,所以就用A,B,c,D,E和F来表示十进制的10到15。在十六进制中,数到F之后,就要转到两位数上,也就是1OH或Ox1O。下面对十六进制(第二行)和十进制(第一行)的计数过程作一下比较:
    1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,……
    1,2,3,4,5,6,7,8,9,A, B, C, D, E, F, 10,……
 
注意,十进制的10等于十六进制的A。与前面讨论过的计数系统一样,每增加一个十六进制位,实际上就增加了一个16的幂,即160(1),161(16),162(256),163(4096),等等。因此,十六进制数3F可以展开为(3×161)+(F×160),等于十进制的(48+15)或63;十六进制数13F可以展开为(1×162)+(3×161)+(F×160),等于十进制的(256+48+15)或319。在c程序中,这两个数用0x3F或Oxl3F这样的形式来表示,其中的“0x”前缀用来告诉编译程序(和程序员)该数字应被当作十六进制数来处理。如果不加“0x”前缀,你就无法判断一个数究竟是十六进制数还是十进制数(或者是八进制数)。

对表20.22稍作改进,加入十六进制的计数过程,就得到了表20.24:
—————————————————————————————————
  二进制    十进制值    二进制幂    十六进制    十六进制幂
—————————————————————————————————
  0000        O                       O
  0001        1    20                 1            160
  0010        2    21                 2
  0011        3                       3
  0100        4    22                 4
  0101        5                       5
  0110        6                       6
  0111        7                       7
  1000        8    23                 8
  1001        9                       9
  1010        10                      A
  1011        11                      B
  1100        12                      C
  1101        13                      D
  1110        14                      E
  1111        15                      F
  10000       16   24                10             161
—————————————————————————————————
笔者在上表的最后又加了一行,使计数达到十进制的16。通过比较二进制、十进制和十六进制·你就会发现:“十”在二进制中是“1010”,在十进制中是“10”,在十六进制中是“A”;。。十六”在二进制中是“1 0000"或“10000”,在十进制中是“16”,在十六进制中是“1O”,,(见上表的最后一行)。这意味着什么呢?因为今天的16,32和64位处理器的位宽恰好都是16的倍数,所以在这些类型的计算机中用十六进制作为计数系统是非常合适的。

十六进制位和二进位之间有一种“倍数”关系。在上表的最后一行中,二进制值被分为两部分(1 0000)。4个二进制位(或者4位)可以计数到15(包括O在内共16个不同的数字),而4位(bit)正好等于一个半字节(nibble)。在上表中你还可以发现,一个十六进制位同样可以计数到15(包括。在内共l 6个不同的数字),因此,一个十六进制位可以代表4个二进制位。一个很好的例子就是用二进制表示十进制的15和16,在二进制中,十进制的15就是1111,正好是4个二进制位能表示的最大数字;在十六进制中,十进制的15就是F,也正好是一个十六进制位能表示的最大数字。十进制的16要用5个二进制位(1 0000)或两个十六进制位(10)来表示。下面把前文提到过的两个数字(0x3F和0x13F)转换为二进制:
    3F              111111
    l3F          100111111

如果把前面的空格换为O,并且把二进制位分成4位一组,那么看起来就会清楚一些:
    3F     0 0011 1111
    l3F    1 0011 1111

你并不一定要把二进制位分成4位一组,只不过当你明白了4个二进制位等价于一个十六进制位后,计数就更容易了。为了证明上述两组数字是相等的,可以把二进制值转换为十进制值(十六进制值到十进制值的转换已经在前文中介绍过了);二进制的111111就是(1×25)+(1×24)+(1×23)+(1×22)+(1×21)+(1×20),等于十进制的(32+16+8+4+2+1)或63,与0x3F的转换结果相同。二进制的1 0011 1111就是(1×28)+(O×27)+(0×26)+(1×25)+(1×24)+(1×23)+(1×22)++(1×21)+(1×20),等于十进制的(256+32+1 6+8+4+2+1)或319。因此,十六进制和二进制能象手掌和手套那样相互匹配。

C语言中,如何把十六进制或八进制的值赋给一个变量

怎样把一个十六进制的值赋给一个变量?

c语言支持二进制、八进制、十进制和十六进制的计数系统,在表示一个数字时,用某个特殊的字符来区别其所属的计数系统是必要的。在表示二进制数时,要在数字的末尾加上“b”(如101b);在表示八进制数时,要使用反斜杠(如\014);在表示十六制数时,要使用“0x”字符序列(如0x34);显然,在表示十进制数时,不需要任何标识符,因为十进制是缺省的计数系统。

要把一个十六进制的值赋给一个变量,你可以象下面这样做:
int x ;
x=0x20;                 /* put hex 20(32 in decimal) into x */
x='0x20' ;                / * put the ASCII character whose value is
                                    hex 20 into x * /
只有了解了十六进制计数系统,你才能知道要赋的值应该如何表示,详见20.24。

怎样把一个八进制的值赋给一个变量?

把一个八进制的值赋给一个变量与把一个十六进制的值赋给一个变量一样简单:
int x ;
x=\033;                 / * put octal 33 (decimal 27) into x * /
x='\033' ;               / * put the ASCII character whose value is
                                    octal 33 into x * /
同样,只有了解了八进制计数系统,你才能知道要赋的值应该如何表示,详见20.23。

编写C语言程序时为什么要使用静态变量

静态变量作为一个局部变量是很合适的,它在函数退出后不会失去其本身的值。例如,有一个要被调用很多次的函数,它的一部分功能就是计算自己被调用的次数。你不能用一个简单的局部变量来实现这部分功能,因为每次进入该函数时,这个变量都没有被初始化。如果把这个计数变量说明为静态的,那么它就会象一个全局变量那样保留自己的当前值。

那么为什么不直接使用一个全局变量呢?你可以使用一个全局变量,而且这样做没有错误。问题是使用了大量全局变量的程序维护起来很麻烦,尤其是有许多函数都各自访问一个全局变量的程序。再说一遍,这样做没有错误,这只是一个程序设计和可读性是否好的问题。如果你把这样的变量说明为静态的,你就可以提醒自己(或者其它可能读你的程序的人)它是局部变量,但要象全局变量那样被处理(保留自己的值)。如果你把它说明为全局的,那么读这个程序的人一定会认为有很多地方要引用它,尽管实际上并不是这样。

总而言之,当你需要一个能保持自己的值的局部变量时,使用静态变量是一种好的编程习惯。

怎样才能只得到一种特定类型的数据,例如字符型数据?

与几乎所有有关计算机科学的问题一样,这个问题的答案也依赖于你要做什么。例如,如果你要从键盘上读入字符,你可以使用scanf():   
  scanf("%C",&c);
此外,你也可以使用一些现成的C库函数:
    c=getchar();
这些方法所产生的结果基本上都一样,只不过使用scanf()能为程序员提供更多的安全性检查。

如果要接收其它类型的数据,有两种方法可供使用。你可以逐个字符地读入数据,并且每次都检查读入的数据是否正确。你也可以使用scanf(),并通过检查其返回值来确定读入的数据是否都正确。

你可以用第二种方法简单而高效地读入一串记录,并检查它们是否都正确。下例就实现了这一点:
    #include<stdio.h>
    main()
    {  
       int i,a,b:
       char c;
       void ProcessRecord(int,int,char);
       for(i=O;i<i100;++a)/*Read 100 records*/
       {
          if(scanf("%d%d%c",&a,&b,&c)!=3)
             printf("data line %d is in error.\n");
          else
             ProcessRecord(a,b,c);
       }  
       return(O);
    }

C语言的char,short,int和long类型分别有多长?

其长度分别为一字节,至少两字节,至少两字节和至少4字节。除此之外,不要再依赖任何约定。

char类型的长度被定义为一个8位字节,这很简单。

short类型的长度至少为两字节。
在有些计算机上,对于有些编译程序,short类型的长度可能为4字节,或者更长。

int类型是一个整数的“自然”大小,其长度至少为两字节,并且至少要和short类型一样长。在16位计算机上,int类型的长度可能为两字节;在32位计算机上,可能为4字节;当64位计算机流行起来后,int类型的长度可能会达到8字节。这里说的都是“可能”,例如,早期的Motorala 68000是一种16/32位的混合型计算机,依赖于不同的命令行选项,一个68000编译程序能产生两字节长或4字节长的int类型。

long类型至少和int类型一样长(因此,它也至少和short类型一样长)。long类型的长度至少为4字节。32位计算机上的编译程序可能会使short,int和long类型的长度都为4字节——也可能不会。

如果你需要一个4字节长的整型变量,你不要想当然地以为int或long类型能满足要求,而要用typedef把一种固有的类型(一种确实存在的类型)定义为你所需要的类型,并在它的前后加上相应的#ifdef指令:
    #ifdef FOUR_BYTE_LONG
    typedef long int4;
    #endif
如果你需要把一个整型变量以字节流的方式写到文件中或网络上,然后再从不同的计算机上读出来,你可能就会用到这样的类型。

如果你需要一个两字节长的整型变量,你可能会遇到一些麻烦!因为并不一定有这样的类型。但是,你总是可以把一个较小的值存放到一个由两个char类型组成的数组中。

怎样在程序中存取重要的DOS内存位置?

与DOS和BIOS函数一样,有很多内存位置也包含了计算机的一些有用和有趣的信息。你想不使用中断就知道当前显示模式吗?该信息存储在40:49H(段地址为40H,偏移量为49H)中。你想知道用户当前是否按下了Shift,Ctrl或Alt键吗?该信息存储在40:17H中。你想直接写屏吗?单色显示(Monochrome)模式的视频缓冲区起始地址为B800:O,彩色文本模式和16色图形模式(低于640×480 16色)的视频缓冲区起始地址为B8000:0,其余标准图形模式(等于或高于640×480 16色)的视频缓冲区起始地址为A000:O,详见14.8。下面的例子说明了如何把彩色文本模式的字符打印到屏幕上,注意它只是对前文中的例子做了一点小小的修改。
# include <stdlib. h>
# include <dos. h>
char GetAKey(void) ;
void OutputString(int, int, unsigned int, char * );
main (int argc, char * *  argv)
{
     char str[l28];
     union REGS regs;
     int ch, tmp;
     / * copy argument string; if none, use "Hello World" * /
     strcpy(str,  (argv[1] == NULL  ? "Hello World"  : argv[1]));
     / * print the string in red at top of screen * /
     for(tmp = 0;((ch =  GetAKeyO)  ! = 27); tmp+=strlen(str)) {
        outputString(0,  tmp, 0x400,str);
     }
 }
char
GetAKey()
 {
     union REGS regs;
    regs. h. ah =  0;          / *  get character * /
    int86(0xl6, &regs, &regs);
    return((char)regs. h. al);
}
void
OutputString(int row, int col, unsigned int video Attribute,  char * outStr)
{
    unsigned short far *  videoPtr;
    videoPtr= (unsigned short far * )  (0xB800L <<16);
videoPtr + = (row *  80) + col;  /*  Move videoPtr to cursor position * /
videlAttribute & = 0xFF00;        / *  Ensure integrity of attribute  * /
    / *  print string to RAM * /
    while ( * outStr ! = '\0'){
       / *  If newline was  sent, move pointer to next line, column 0  * /
       if( (* outStr ==  '\n')  || (*outStr ==  'V') ){
          videoPtr + = (80- (((int)FP-OFF(videoPtr)/2) % 80));
          outStr+ + ;
          continue;
       }
       / *  If  backspace was requested, go back one  * /
       if( *outStr = = 8){
            videoPtr -- ;
            outStr++ ;
            continue;
       }
       /*  If BELL was requested, don't beep,  just print a  blank
           and go on  * /
       if ( * outStr = =  7) {
            videoPtr+ + ;
            outStr++ ;
            continue ;
       }
       / *  If TAB was requested, give it eight spaces  * /
       if ( * outStr ==  9){
             * videoPtr++  =  video Attribute  |  '   ' ;
             * videoPtr++  =  video Attribute  |  '   ' ;
             * videoPtr++  =  video Attribute  |  '   ' ;
             * videoPtr++  =  video Attribute  |  '   ' ;
             * videoPtr++  =  video Attribute  |  '   ' ;
             * videoPtr++  =  video Attribute  |  '   ' ;
             * videoPtr++  =  video Attribute  |  '   ' ;
             * videoPtr++  =  video Attribute  |  '   ' ;
           outStr+ + ;
           continue;
      }
      / * If  it was a regular character, print it  * /
       * videoPtr = videoAttribute  |  (unsigned char) * outStr;
      videoPtr+ + ;
      outStr + + ;
    } 
    return;
}
显然,当你自己来完成把文本字符打印到屏幕上这项工作时,它是有些复杂的。笔者甚至已经对上例做了一些简化,即忽略了BELL字符和一些其它特殊字符的含义(但笔者还是实现了回车符和换行符)。不管怎样,这个程序所完成的任务与前文中的例子基本上是相同的,只不过现在打印时你要控制字符的颜色和位置。这个程序是从屏幕的顶端开始打印的。如果你想看更多的使用内存位置的例子,可以阅读20.12和20.17——其中的例子都使用了指向DOS内存的指针来查找关于计算机的一些有用信息。

mso-bidi-font-family:宋体; mso-font-kerning:0pt'>正在执行重要的代码时,把这一情况通知DOS。然而,该标志对程序员也是很有用的,因为他们能由此知道什么时候DOS处于忙状态。尽管从DOS 2.0版开始就有这个函数了,但因为Microsoft最近已经公开了这个函数,所以从技术角度上讲它已不再是一个未公开的函数。有几本很不错的书介绍了已公开和未公开的DOS函数,对这个问题有兴趣的读者可以去阅读这些书。

怎样检索环境变量(environment variables)的值?

ANSI C标准提供了一个名为getenv()的函数来完成这项任务。getenv()函数很简单一把指向要查找的环境串的指针传递给它,它就返回一个指向该变量值的指针。下面的程序说明了如何从C中获得环境变量PATH的值:
# include <stdlib. h>
main(int argc, char *  *  argv)
{
    char envValue[l29];             / *  buffer to store PATH * /
    char *  envPtr = envValue ;     / *  pointer to this buffer  * /
    envPtr = getenv("PATH");        /*  get the PATH */
    printf ("PATH= %s\n" , envPtr) ;   / * print the PATH * /
}
如果你编译并运行了这个程序,你就会看到与在DOS提示符下输入PATH命令完全相同的结果。事实上,你可以用getenv()检索AUTOEXEC.BAT文件中的或者系统引导后在DOS揭示符下输入的所有环境变量的值。

这里有一个小技巧。当运行Windows时,Windows设置了一个名为WINDIR的新的环境变量,它包含了Windows目录的路径全名。下面这段简单的程序用来检索这个串:
# include <stdlib. h>
main(int argc, char * *  argv)
{
    char envValue[l29];
    char *  envPtr = envValue ;
   envPtr = getenv("windir");
    / * print the Windows directory * /
    printf("The Windows Directory is  %s\n" ,  envPtr);
}
这个程序还可以用来判断当前是否正在运行Windows,以及DOS程序是否运行在一个DOS shell下,而不是运行在“真正的"DOS下。注意,程序中的windir字符串是小写——这一点很重要,因为它对大小写是敏感的。如果你使用WINDIR,getenv()就会返回一个NULL串(表示变量未找到错误)。

用一putenv()函数也可以设置环境变量。但要注意,该函数不是一个ANSI标准函数,在某些编译程序中它可能不以这个名字出现,或者根本就不存在。你可以用一putenv()函数做许多事情。实际上,在上面那个例子中,Windows正是用这个函数创建了windir环境变量。

16位和32位的数是怎样存储的?

一个16位的数占两个字节的存储空间,即高位字节和低位字节(见10.5中的介绍)。如果你是在纸上书写一个16位的数,你总是会把高位字节写在前面,而把低位字节写在后面。然而,当这个数被存储到内存中时,并没有固定的存储顺序。

如果我们用M和L分别表示高位字节和低位字节,那么可以有两种方式把这两个字节存储到内存中,即M在前L在后或者L在前M在后。把M存储在前的顺序被称为“正向(forward)”或“高位优先(big—endian)”顺序;把L存储在前的顺序被称为“逆向(reverse)”或“低位优先(little—endian)”顺序。

big—endian这个术语的含义是数的“高位(big end)”存储在前,同时这也是对《Gulliver'sTravels》这本书中的一个词的引用,在该书中big—endian一词是指那些从大头开始吃一个煮鸡蛋的人。

大多数计算机按正向顺序存储一个数,Intel CPU按逆向顺序存储一个数,因此,如果试图将基于Intel CPU的计算机连到其它类型的计算机上,就可能会引起混乱。

一个32位的数占4个字节的存储空间,如果我们按有效位从高到低的顺序,分别用Mm,Ml,Lm和Ll表示这4个字节,那么可以有4!(4的阶乘,即24)种方式来存储这些字节。在过去的这些年中,人们在设计计算机时,几乎用遍了这24种方式。然而,时至今天,只有两种方式是最流行的,一种是(Mm,MI,Lm,LD,也就是高位优先顺序,另一种是(Ll,Lm,Ml,Mm),也就是低位优先顺序。和存储16位的数一样,大多数计算机按高位优先顺序存储32位的数,但基于Intel CPU的计算机按低位优先顺序存储32位的数。

C语言中的高位字节和低位字节是什么意思?

通常我们从最高有效位(most significant digit)开始自左向右书写一个数字。在理解有效位这个概念时,可以想象一下你的支票数额的第一位增加1和最后一位增加1之间的巨大区别,前者肯定会让你喜出望外。

计算机内存中一个字节的位相当于二进制数的位,这意味着最低有效位表示1,倒数第二个有效位表示2×1或2,倒数第三个有效位表示2×2×1或4,依此类推。如果用内存中的两个字节表示一个16位的数,那么其中的一个字节将存放最低的8位有效位,而另一个字节将存放最高的8位有效位,见图10.5。存放最低的8位有效位的字节被称为最低有效位字节或低位字节,而存放最高的8位有效位的字节被称为最高有效位字节或高位字节。
            高位字节                       低位字节
  ↓--------------------------↓ ↓---------------------------↓            
 
1514131211109.8.7.6.5.4.3.2.1.0.
 
                  图 10.5 双字节整数中的位

C语言里的移位和乘以2这两种方式中哪一种更好?

不管你采用哪种方式,任何合格的优化编译程序都会产生相同的代码,因此你可以采用使程序的上下文更易读的那种方式。你可以用DOS/Windows上的CODEVIEW或UNIX机上的反汇编程序(通常被称为"dis”)这样的工具来查看下述程序的汇编代码:

例10.4乘以2和左移一位经常是相同的
    void main()
    {
      unsigned int test_nbr = 300;
      test_nbr * =2;
      test_nbr = 300;
      test_nbr << = 1;
    }

C语言的位域(bit fields)是可移植的吗?

位域是不可移植的。因为位域不能跨越机器字,而且不同计算机中的机器字长也不同,所以一个使用了位域的程序在另一种计算机上很可能无法编译。

假设你的程序能在另一种计算机上编译,将位分配给位域时所遵循的顺序仍然是没有定义的。因此,不同的编译程序,甚至同一编译程序的不同版本所产生的代码,很可能无法在由原来的编译程序所生成的数据上工作。通常应该避免使用位域,除非计算机能直接寻址内存中的位并且编译程序产生的代码能利用这种功能,并且由此而提高的速度对程序的性能是至关重要的。

C语言中的位屏蔽(bit masking)是怎么回事

位屏蔽的含义是从包含多个位集的一个或一组字节中选出指定的一(些)位。为了检查一个字节中的某些位,可以让这个字节和屏蔽字(bit mask)进行按位与操作(C的按位与运算符为&)——屏蔽字中与要检查的位对应的位全部为1,而其余的位(被屏蔽的位)全部为0。例如,为了检查变量flags的最低位,你可以让flags和最低位的屏蔽字进行按位与操作:
    flags&1;
为了置位所需的位,可以让数据和屏蔽字进行按位或操作(C的按位或运算符为|)。例如,你可以这样置位flags的最低位:
    flags = flags | 1;
或者这样:
    flags |= 1;
为了清除所需的位,可以让数据和对屏蔽字按位取反所得的值进行按位与操作。例如,你可以这样清除flags的最低位:
    flags = flags& ~1;
或者这样:
    flags&=~1 ;
有时,用宏来处理标志会更方便,例10.2中的程序就是通过一些宏简化了位操作。

例10.2 能使标志处理更方便的宏
/* Bit Masking * /
/ * Bit masking can be used to switch a character
  between lowercase and uppercase * /
#define BIT_POS(N)            ( 1U «(N) )
#define SET_FLAG(N,F)         ( (N) |  = (F) )
#define CLR_FLAG(N,F)         ( (N) &= -  (F) )
#define TST_FLAGCN,F)         ( (N) & (F)  )
#define BIT_RANGE(N,M)         ( BIT_POS((M) + 1- (N))-1<<(N))
#define BIT_SHIFTL(B,N)        ( (unsigned)(B)«(N) )
#define BIT_SHIFTR(B,N)        ( (unsigned)(B)»(N) )
#define SET_MFLAG(N,F,V)       ( CLR_FLAG(N,F), SET_FLAG(N,V) )
#define CLR_MFLAG(N,F)         ( (N) &= ~(F) )
#define GET_MFLAG(N,F)         ( (N) & (F) )

# include <stdio. h>
void main()
{
  unsigned char ascii_char = 'A';    /* char  = 8 bits only */
  int test_nbr = 10;
  printf("Starting character =  %c\n" , ascii_char);
  /"   The 5th bit position determines if the character is
       uppercase or lowercase.
       5th bit =  0 - Uppercase
       5th bit =  1- Lowercase    * /
  printf ("\nTurn 5th bit on = %c\n" , SET_FLAG(ascii_char, BIT_POS(5)));
  printf ("Turn 5th bit  off = %c\n\n",CLR_FLAG(ascii_char, BIT_POS(5)));
  printf ("Look at shifting bits\n");
  printf (" = = = = = = = = = = = = = = = =\n" );
  printf ("Current value = %d\n" , test_nbr)i
  printf ("Shifting one position left = %d\n" ,
        test_nbr = BIT_SHIFTL(test_nbr, 1) );
  printf ("Shifting two positions right =  %d\n" ,
        BIT_SHIFTR(test_nbr,  2) );
}

宏BIT_POS(N)能返回一个和N指定的位对应的屏蔽字(例如BIT_POS(O)和BIT_POS(1)分别返回最低位和倒数第二位的屏蔽字),因此你可以用
    #define A_FLAG BIT_POS(12)
    #define A_FLAG BIT_P0S(13)
代替
    #define A_FLAG 4096
    #define A_FLAG 8192
这样可以降低出错的可能性。

宏SET_FLAG(N,F)能置位变量N中由值F指定的位,而宏CLR_FLAG(N,F)则刚好相反,它能清除变量N中由值F指定的位。宏TST_FLAG(N,F)可用来测试变量N中由值F指定的位,例如:
    if (TST_FLAG (flags, A_FLAG))
          /* do something * /;
宏BIT_RANGE(N,M)能产生一个与由N和M指定的位之间的位对应的屏蔽字,因此,你可以用
    # define FIRST_OCTAL_DIGIT     BIT_RANGE (0,2)   /*111"/
    # define SECOND-OCTAL-DIGIT    BIT-RANGE(3,5)    /* 111000*/
代替
    #define FIRST_OCTAL_DIGIT 7      /*111*/
    #define SECOND_OCTAL_DIGIT 56    /* 111000 * /
这样可以更清楚地表示所需的位。

宏BIT_SHIFT(B,N)能将值B移位到适当的区域(从由N指定的位开始)。例如,如果你用标志C表示5种可能的颜色,你可以这样来定义这些颜色:
    #define C_FLAG        BIT-RANGE(8,10)     /* 11100000000 */
   /* here are all the values the C flag can take on * /  
   # define C_BLACK   BIT-SHIFTL(0,8)   /* ooooooooooo */
   # define C-RED     BIT_SHIFTL(1,8)   /* 00100000000 */
   # define C-GREEN   BIT_SHIFTL(2,8)   /* 01000000000 */
   # define C-BLUE    BIT-SHIFTL(3,8)   /* 01100000000 */
   # define C_WHITE   BIT-SHIFTL(4,8)   /* 10000000000 */
   # defineC-ZERO C-BLACK  
   # defineC-LARGEST C-WHITE  
   /* A truly paranoid programmer might do this */
   #if C_LARGEST > C_FLAG
          Cause an error message. The flag C_FLAG is not
          big enough to hold all its possible values.
   #endif /* C_LARGEST > C_FLAG */
宏SET_MFLAG(N,F,V)先清除变量N中由值F指定的位,然后置位变量N中由值V指定的位。宏CLR_MFLAG(N,F)的作用和CLR_FLAG(N,F)是相同的,只不过换了名称,从而使处理多位标志的宏名字风格保持一致。宏GET_MFLAG(N,F)能提取变量N中标志F的值,因此可用来测试该值,例如:
    if (GET_MFLAG(flags, C_FLAG) == C_BLUE)
         /*do something */;
注意:宏BIT_RANGE()和SET_MFLAG()对参数N都引用了两次,因此语句
    SET_MFLAG(*x++,C_FLAG,C_RED);
的行为是没有定义的,并且很可能会导致灾难性的后果。

C语言中存储标志(flag)效率最高的方法

标志的作用是对程序执行过程中的两种或更多种选择作出决定。例如,在执行MS-DOS的dir命令时,可以用“/w”标志使该命令在屏幕上显示若干列文件名而不是每行只显示一个文件名。在3.5中你可以看到另外一个例子,该例通过一个标志从两种可能类型中选择一种在一个联合中使用。因为一个标志一般只有少数几个(通常是两个)值,所以,为了节省内存空间,ǔ2换峤桓霰曛敬娣旁谝桓鍪粲谒约旱膇nt或char类型中。

存储标志值的效率是存储空间和存取速度之间的一种折衷。存储空间利用效率最高的存储方法是用数目足够的位来存储标志值的所有可能值,但大多数计算机不能直接寻址内存中单独的一位,因此标志值要从存放它的字节中提取。存取速度最快的存储方法是将每个标志值都存放到一个属于它自己的整型变量中,但是,当一个标志只需要一位存储空间而变量的长度为32位时,那么其余的31位就全部浪费掉了,因此这种方法的存储空间利用效率非常低。

如果标志的数目不多,那么使用哪种存储方法是没有关系的。如果标志的数目很多,那么最好将它们压缩存储在一个字符数组或整型数组中。这时,需要通过一种被称为位屏蔽(bit masking)的过程来提取这些标志值,即屏蔽掉不需要的位,只处理所需的位。

有时,为了节省存储空间,可能会将一个标志和另外一个值存放在一起。例如,如果一个整型的值小于整型所能表示的最大值,那么就可用它的高阶位来存放标志;如果某些数据总是2或4的倍数,那么就可用它的低阶位来存放标志。在3.5的例子中,就使用了一个指针的低阶位来存放一个标志,该标志的作用是从两种可能的类型中选择一种作为该指针所指向的对象类型。

细说C语言位(bit)和字节(byte)

位指的是二进制系统中的一位,它是最小的信息单位。位的用处可以从两方面去分析:第一,计算机对位的值可以有任意多种解释,例如表示"yes’’或"no”,或者表示磁盘是否已插入驱动器,或者表示某个鼠标键是否被按下;第二,将若干位的值连接起来后,就可以表示更复杂的数据,而且每增加一位,可以表示的可能的值的数目就会增加一倍。

换句话说,一位可以表示两种可能的值,即“O”和“1”;两位可以表示2×2或4种可能的值,即“00”,“01”,“10”和“11”;类似地,三位可以表示2×2×2或8种可能的值……。对计算机来说,位的这种特性既是最有力的支持——因为很复杂的数据(例如本书内容)可以被分解为位的表示后存储起来,又是最大的限制——因为在现实生活中许多事物的值是不精确的,这样的值无法用数目有限的若干位来表示。

程序员始终必须清楚每一项数据需要用多少位来表示。因为位作为单位太小,所以为了方便起见,大多数计算机所处理的信息单位是被称为字节的位块。字节是大多数计算机中最小的可寻址的信息单位,这意味着计算机给每一个字节的信息都赋予一个地址,并且一次只能存取一个字节的信息。一个字节中的位的数目可以是任意的,并且在不同的计算机中可以不同。最常见的情况是每个字节中有8位,即可以存放256个不同的值。8位这样的长度非常适合于存放表示ASCII(the American Standard Code for Information Interchange)字符的数据。

下述程序可以显示空格符以后的ASCII字符和PC机的图形字符集:
# include <stdio. h>
void main (void);
void main()
{
  /" Display ASCII char set " /
  unsigned char space  =  '' ;     /* Start with SPACE
                                         char =  8 bits only * /
  int ctr =  0;
  printf(" ASCII Characters\n" )»
  printf (" = = = = = = = = = = = = = = = =\n" ) ;
  for  (ctr = O; ctr + space <256; ctr+ + )
    printf("%c", ctr  + space);
  printf ("\n");
}
请注意,变量ctr必须是int类型,而不能是char类型,因为char类型只含8位,只能存放从0至255之间的值(signed char类型只能存放从-128至127之间的值)。如果ctr是char类型,它就永远不会存放256或比256更大的值,程序也就永远不会结束。此外,如果你在非PC机的计算机上运行上述程序,那么程序所打印的非ASCII字符可能会导致乱屏。

因为计算机是以字节块的方式工作的,所以大多数程序也以这种方式工作,有时,考虑到要存放的数据项的数目,或者移动每一位的信息所需的时间,节省内存空间就显得很有必要。这时,我们通常会用少于一个字节的空间来存放那些只有少数可能值的数据,这也就是本章要讨论的主要内容。

C语言中,用const说明常量有什么好处?

使用关键字const有两个好处;第一,如果编译程序知道一个变量的值不会改变,编译程.序就能对程序进行优化;第二,编译程序会试图保证该变量的值不会因为程序员的疏忽而被改变。

当然,用#define来定义常量也有同样的好处。用const而不用#define来定义常量的原因是const变量可以是任何类型(如结构,而用#define定义的常量不能表示结构)。此外,const变量是真正的变量,它有可供使用的地址,并且该地址是唯一的(有些编译程序在每次使用用#define定义的字符串时都会生成一份新的拷贝)。

可以在C语言头文件中说明static变量吗?

如果说明了一个static变量,就必须在同一个文件中定义该变量(因为存储类型修饰符static和extern是互斥的)。你可以在头文件中定义一个static变量,但这会使包含该头文件的源文件都得到该变量的一份私有拷贝,而这通常不是你想得到的结果。

C语言中,说明一个变量和定义一个变量有什么区别?

说明一个变量意味着向编译程序描述变量的类型,但并不为变量分配存储空间。定义一个变量意味着在说明变量的同时还要为变量分配存储空间。在定义一个变量的同时还可以对变量进行初始化。下例说明了一个变量和一个结构,定义了两个变量,其中一个定义带初始化:
extern int decll;  / * this is a declaration * /
struct decl2 {
        int member;
} ;  / * this just declares the type--no variable mentioned * /
int def1 = 8;       / * this is a definition * /
int def2;        / * this is a definition * /

换句话说,说明一个变量相当于告诉编译程序“在程序的某个位置将用到一个变量,这里给出了它的名称和类型”,定义一个变量则相当于告诉编译程序“具有这个名称和这种类型的变量就在这里”。

一个变量可以被说明许多次,但只能被定义一次。因此,不应该在头文件中定义变量,因为一个头文件可能会被一个程序的许多源文件所包含。

C语言可以在头文件中说明或定义变量吗?

被多个文件存取的全局变量可以并且应该在一个头文件中说明,并且必须在一个源文件中定义。变量不应该在头文件中定义,因为一个头文件可能被多个源文件包含,而这将导致变量被多次定义。如果变量的初始化只发生一次,ANSIC标准允许变量有多次外部定义;但是,这样做没有任何好处,因此最好避免这样做,以使程序有更强的可移植性。

注意:变量的说明和定义是两个不同的概念,在2.16中将讲解两者之间的区别。

仅供一个文件使用的“全局”变量应该被说明为static,而且不应该出现在头文件中。

C语言编程中,什么时候不应该使用类型强制转换(typecast)?

不应该对用const或volatile说明了的对象进行类型强制转换,否则程序就不能正确运行。
   不应该用类型强制转换把指向一种结构类型或数据类型的指针转换成指向另一种结构类型或数据类型的指针。在极少数需要进行这种类型强制转换的情况下,用共用体(union)来存放有关数据能更清楚地表达程序员的意图。

C语言 什么时候应该使用类型强制转换(typecast)?

在两种情况下需要使用类型强制转换。第一种情况是改变运算分量的类型,从而使运算能正确地进行。下面的程序与2.12中的例子相似,但有不同之处。变量n被赋值为整数i除以整数j的结果,因为是整数相除,所以结果为0。变量f2也被赋值为i除以j的结果,但本例通过(float)类型强制转换把i转换成一个float类型,因此执行的是浮点数除法运算(见2.11),结果为0.75。
#include <stdio.h>
main ( )
{
   int i = 3;
   int j = 4
   float f1 =i/j;
   float f2= (float) i/j;
   printf("3/4== %g or %g depending on the type used. \n",f1, f2);
}

第二种情况是在指针类型和void * 类型之间进行强制转换,从而与期望或返回void指针的函数进行正确的交接。例如,下述语句就把函数malloc()的返回值强制转换为一个指向foo结构的指针:
    struct foo *p=(struct foo *)malloc(sizeof(struct foo));

C语言中的运算符升级(operatorpromotion)是什么?

当两个不同类型的运算分量(operand)进行运算时,它们会被转换为能容纳它们的最小的类型,并且运算结果也是这种类型。下表列出了其中的规则,在应用这些规则时,你应该从表的顶端开始往下寻找,直到找到第一条适用的规则。
-------------------------------------------------------------
    运算分量1          运算分量2           转换结果
-------------------------------------------------------------
    long double      其它任何类型        long double
    double           任何更小的类型      double
    float            任何更小的类        float
    unsigned long    任何整数类          unsigned long
    long             unsigned>LONG_MAX   unsigned long
    long             任何更小的类型      long
    unsigned         任何有符号类型      unsigned
-------------------------------------------------------------

下面的程序中就有几个运算符升级的例子。变量n被赋值为3/4,因为3和4都是整数,所以先进行整数除法运算,结果为整数0。变量f2被赋值为3/4.0,因为4.0是一个float类型,所以整数3也被转换为float类型,结果为float类型0.75。   
#include <stdio.h>
main ()
{
           float f1 = 3/4;
           float f2 = 3/4.0
           printf("3/4== %g or %g depending on the type used. \n",f1, f2);
}

对不同类型的C语言变量进行算术运算会有问题吗?

C有三类固有的数据类型:指针类型、整数类型和浮点类型。

指针类型的运算限制最严,只限于以下两种运算:
    -  两个指针相减,仅在两个指针指向同一数组中的元素时有效。运算结果与对应于两个指针的数组下标相减的结果相同。   
    +  指针和整数类型相加。运算结果为一个指针,该指针与原指针之间相距n个元素,n就是与原指针相加的整数。

浮点类型包括float,double和longdouble这三种固有类型。整数类型包括char,unsigned char,short,unsigned short,int,unsigned int,long和unsigned long。对这些类型都可进行以下4种算术运算:
    +  加   
    -  减
    *  乘   
    /  除

对整数类型不仅可以进行上述4种运算,还可进行以下几种运算:
    %    取模或求余   
    >>    右移
    <<    左移
    &    按位与
    |    按位或
    ^    按位异或
    !    逻辑非
    ~    取反
    尽管C允许你使用“混合模式”的表达式(包含不同类型的算术表达式),但是,在进行运算之前,它会把不同的类型转换成同一类型(前面提到的指针运算除外)。这种自动转换类型的过程被称为“运算符升级(operator promotion)”。

一个C语言的数字型变量,怎样判断它可以容纳的最大值?

要判断某种特定类型可以容纳的最大值或最小值,一种简便的方法是使用ANSI标准头文件limits.h中的预定义值。该文件包含一些很有用的常量,它们定义了各种类型所能容纳的值,下表列出了这些常量:
----------------------------------------------------------------
    常  量                          描    述
----------------------------------------------------------------
  CHAR—BIT       char的位数(bit)   
  CHAR—MAX       char的十进制整数最大值
  CHAR—MIN       char的十进制整数最小值
  MB—LEN—MAX    多字节字符的最大字节(byte)数
  INT—MAX        int的十进制最大值   
  INT—MIN        int的十进制最小值 
  LONG—MAX       long的十进制最大值
  LONG—MIN       long的十进制最小值
  SCHAR—MAX      signedchar的十进制整数最大值
  SCHAR—MIN      signedchar的十进制整数最小值
  SHRT—MIN       short的十进制最小值
  SHRT—MAX       short的十进制最大值
  UCHAR—MAX      unsignedchar的十进制整数最大值
  UINT—MAX       unsignedint的十进制最大值
  ULONG—MAX      unsignedlongint的十进制最大值
  USHRT—MAX      unsignedshortint的十进制最大值
-----------------------------------------------------------------
    对于整数类型,在使用2的补码运算的机器(你将使用的机器几乎都属此类)上,一个有符号类型可以容纳的数字范围为-2 位数-1到(+2 位数-1-1),一个无符号类型可以容纳的数字范围为0到(+2 位数-1)。例如,一个16位有符号整数可以容纳的数字范围为--2 15(即-32768)到(+2 15-1)(即+32767)。

C语言浮点数比较(floating-point comparisons)的可靠性如何?

浮点数是计算机编程中的“魔法(black art)”,原因之一是没有一种理想的方式可以表示一个任意的数字。电子电气工程协会(IEEE)已经制定出浮点数的表示标准,但你不能保证所使用的每台机器都遵循这一标准。

    即使你使用的机器遵循这一标准,还存在更深的问题。从数学意义上讲,两个不同的数字之间有无穷个实数。计算机只能区分至少有一位(bit)不同的两个数字。如果要表示那些无穷无尽的各不相同的数字,就要使用无穷数目的位。计算机只能用较少的位(通常是32位或64位)来表示一个很大的范围内的数字,因此它只能近似地表示大多数数字。

    由于浮点数是如此难对付,因此比较一个浮点数和某个值是否相等或不等通常是不好的编程习惯。但是,判断一个浮点数是否大于或小于某个值就安全多了。例如,如果你想以较小的步长依次使用一个范围内的数字,你可能会编写这样一个程序:
#include <stdio.h>
const float first = O.O;
const float last = 70.0
const float small= O.007
main ( )
{
        float  f;
        for (f=first; f !=last && f<last+1.O; f +=small)
        printf("f is now %g\n", f);
}
 然而,舍入误差(rounding error)和变量small的表示误差可能导致f永远不等于last(f可能会从稍小于last的一个数增加到一个稍大于last的数),这样,循环会跳过last。加入不等式"f<last+1.0"就是为了防止在这种情况发生后程序继续运行很长时间。如果运行该程序并且被打印出来的f值是71或更大的数值,就说明已经发生了这种情况。

一种较安全的方法是用不等式"f<last"作为条件来终止循环,例如:
    float  f;
    for(f=first; f<last; f+=small)
       ;

你甚至可以预先算出循环次数,然后通过这个整数进行循环计数:
    float  f;
    int  count=(last-first)/small;
    for(f=first;count-->0;f+=small)
       ;

C语言编程中,什么时候应该使用const修饰符

使用const修饰符有几个原因:
第一个原因是这样能使编译程序找出程序中不小心改变变量值的错误。

请看下例:
while ( * str=0) / * programmer meant to write * str! =0 * /
{
     / * some code here * /
      strq++;
}

其中的“=”符号是输入错误。如果在说明str时没有使用const修饰符,那么相应的程序能通过编译但不能被正确执行。

第二个原因是效率。如果编译程序知道某个变量不会被修改,那么它可能会对生成的代码进行某些优化。   

如果一个函数参数是一个指针,并且你不希望它所指向的数据被该函数或该函数所调用的函数修改,那么你应该把该参数说明为const指针。如果一个函数参数通过值(而不是通过指针)被传递给函数,并且你不希望其值被该函数所调用的函数修改,那么你应该把该参数说明为const。然而,在实际编程中,只有在编译程序通过指针存取这些数据的效率比拷贝这些数据更高时,才把这些参数说明为const。   

请参见:   
1、一个变量可以同时被说明为const和volatile吗?   
2、什么时候不应该使用类型强制转换(typecast)?   
3、用const说明常量有什么好处?

一个C语言变量可以同时被说明为const和volatile吗

可以。const修饰符的含义是变量的值不能被使用了const修饰符的那段代码修改,但这并不意味着它不能被这段代码以外的其它手段修改。

例如,在2.6的例子中,通过一个volatile const指针t来存取timer结构。函数time_addition()本身并不修改t->value的值,因此t->value被说明为const。不过,计算机的硬件会修改这个值,因此t->value又被说明为volatile。如果同时用const和volatile来说明一个变量,那么这两个修饰符随便哪个在先都行。

请参见:
1、什么时候应该使用volatile修饰符?
2、什么时候应该使用const修饰符?
3、什么时候不应该使用类型强制转换(typecast)?

对于C语言什么时候应该使用volatile修饰符

volatile修饰符告诉编译程序不要对该变量所参与的操作进行某些优化。在两种特殊的情况下需要使用volatile修饰符:第一种情况涉及到内存映射硬件(memory-mapped hardware,如图形适配器,这类设备对计算机来说就好象是内存的一部分一样),第二种情况涉及到共享内存(shared memory,即被两个以上同时运行的程序所使用的内存)。

大多数计算机拥有一系列寄存器,其存取速度比计算机主存更快。好的编译程序能进行一种被称为“冗余装入和存储的删去”(redundant load and store removal)的优化,即编译程序会·在程序中寻找并删去这样两类代码:

一类是可以删去的从内存装入数据的指令,因为相应的数据已经被存放在寄存器中;另一种是可以删去的将数据存入内存的指令,因为相应的数据在再次被改变之前可以一直保留在寄存器中。

如果一个指针变量指向普通内存以外的位置,如指向一个外围设备的内存映射端口,那么冗余装入和存储的优化对它来说可能是有害的。

例如,为了调整某个操作的时间,可能会用到下述函数:   
time_t time_addition(volatile const struct timer * t, int a),
{
         int    n
         int    x
         time_t  then
         x=O;
         then= t->value
         for (n=O; n<1O00; n++)
         {
               x=x+a ;
         }
      return t->value - then;
}

在上述函数中,变量t->value实际上是一个硬件计数器,其值随时间增加。该函数执行1000次把a值加到x上的操作,然后返回t->value在这1000次加法的执行期间所增加的值。

如果不使用volatile修饰符,一个聪明的编译程序可能就会认为t->value在该函数执行期间不会改变,因为该函数内没有明确地改变t->value的语句。这样,编译程序就会认为没有必要再次从内存中读入t->value并将其减去then,因为答案永远是0。因此,编译程序可能会对该函数进行“优化”,结果使得该函数的返回值永远是0。

如果一个指针变量指向共享内存中的数据,那么冗余装入和存储的优化对它来说可能也是有害的,共享内存通常用来实现两个程序之间的互相通讯,即让一个程序把数据存到共享的那块内存中,而让另一个程序从这块内存中读数据。如果从共享内存装入数据或把数据存入共享内存的代码被编译程序优化掉了,程序之间的通讯就会受到影响。   

请参见:   
1、一个变量可以同时被说明为const和volatile吗?
2、什么时候不应该使用类型强制转换(typecast)?

对于C语言什么时候应该使用register修饰符?它真的有用吗?

register修饰符暗示编译程序相应的变量将被频繁使用,如果可能的话,应将其保存在CPU的寄存器中,以加快其存取速度。但是,使用register修饰符有几点限制。

首先,register变量必须是能被CPU寄存器所接受的类型。这通常意味着register变量必须是一个单个的值,并且其长度应小于或等于整型的长度。但是,有些机器的寄存器也能存放浮点数。

其次,因为register变量可能不存放在内存中,所以不能用取址运算符“&”来获取register变量的地址。如果你试图这样做,编译程序就会报告这是一个错误。


register修饰符的用处有多大还受其它一些规则的影响。因为寄存器的数量是有限的,而且某些寄存器只能接受特定类型的数据(如指针和浮点数),因此,真正能起作用的register修饰符的数目和类型都依赖于运行程序的机器,而任何多余的register修饰符都将被编译程序所忽略。

在某些情况下,把变量保存在寄存器中反而会降低运行速度,因为被占用的寄存器不能再用于其它目的,或—者变量被使用的次数不够多,不足以抵消装入和存储变量所带来的额外开销。

那么,什么时候应该使用register修饰符呢?回答是,对现有的大多数编译程序来说,永远不要使用register修饰符。早期的C编译程序不会把变量保存在寄存器中,除非你命令它这样做,这时register修饰符是C语言的一种很有价值的补充。然而,随着编译程序设计技术的进步,在决定哪些变量应该被存到寄存器中时,现在的C编译程序能比程序员作出更好的决定。

实际上,许多C编译程序会忽略register修饰符,因为尽管它完全合法,但它仅仅是暗示而不是命令。

在极罕见的情况下,程序运行速度很慢,而你也知道这是因为有一个变量被存储在内存中,也许你最后会试图在该变量前面加上register修饰符,但是,如果这并没有加快程序的运行速度,你也不要感到奇怪。

请参见:
1、什么时候应该使用volatile修饰符?

C语言 什么是const指针

如果希望一个变量在被初始化后其值不会被修改,程序员就会通过cons,修饰符和编译程序达成默契。编译程序会努力去保证这种默契——它将禁止程序中出现对说明为const的变量进行修改的代码。

const指针的准确提法应该是指向const数据的指针,即它所指向的数据不能被修改。只要在指针说明的开头加入const修饰符,就可说明一个cosnt指针。尽管const指针所指向的数据不能被修改,但cosnt指针本身是可以修改的。

下面给出了const指针的一些合法和非法的用法例子:
    const char *str="hello";
    char c=*str;  /*legal*/
    str++;       /*legal*/
    *str='a';     /* illegal */
    str[1]='b';  /*illegal*/
前两条语句是合法的,因为它们没有修改str所指向的数据;后两条语句是非法的,因为它们要修改str所指向的数据。

在说明函数参数时,常常要使用const指针。例如,一个计算字符串长度的函数不必改变字符串内容,它可以写成这样:
    my_strlen(const char *str)
    {
        int count=0;
        while ( * str++)
        {
            count ++;
        }
        return   count;
    }

注意,如果有必要,一个非const指针可以被隐式地转换为const指针,但一个const指针不能被转换成非const指针。这就是说,在调用my_strlen()时,它的参数既可以是一个const指针,也可以是一个非const指针。

请参见:
1、一个变量可以同时被说明为const和volatile吗?
2、什么时候应该使用const修饰符?
3、什么时候不应该使用类型强制转换(type cast)?
4、用const说明常量有什么好处?、

C语言 什么是页抖动(pagethrashing)

有些操作系统(如UNIX和增强模式下的Windows)使用虚拟内存,这是一种使机器的作业地址空间大于实际内存的技术,它是通过用磁盘空间模拟RAM(random—access memory)来实现的。

在80386和更高级的Intel CPU芯片中,在现有的大多数其它微处理器(如Motorola 68030,sparc和Power PC)中,都有一个被称为内存管理单元(Memory Management Unit,缩写为MMU)的器件。MMU把内存看作是由一系列“页(page)”组成的来处理。一页内存是指一个具有一定大小的连续的内存块,通常为4096或8192字节。操作系统为每个正在运行的程序建立并维护一张被称为进程内存映射(Process Memory Map,缩与为PMM)的表,表中记录了程序可以存取的所有内存页以及它们的实际位置。

每当程序存取一块内存时,它会把相应的地址(虚拟地址,virtualaddress)传送给MMU,MMU会在PMM中查找这块内存的实际位置(物理地址,physical address),物理地址可以是由操作系统指定的在内存中或磁盘上的任何位置。如果程序要存取的位置在磁盘上,就必须把包含该地址的页从磁盘上读到内存中,并且必须更新PMM以反映这个变化(这被称为pagefault,即页错)。

希望你继续读下去,因为下面就要介绍其中的难点了。存取磁盘比存取RAM要慢得多,所以操作系统会试图在RAM中保持尽量多的虚拟内存。如果你在运行一个非常大的程序(或者同时运行几个小程序),那么可能没有足够的RAM来承担程序要使用的全部内存,因此必须把一些页从RAM中移到磁盘上(这被为pagingout,即页出)。   

操作系统会试图去判断哪些页可能暂时不会被使用(通常基于过去使用内存的情况),如果它判断错了,或者程序正在很多地方存取很多内存,那么为了读入已调出的页,就会产生大量页错动作。因为RAM已被全部使用,所以为了调入要存取的一页,必须调出另一页,而这将导致更多的页错动作,因为此时不同的一页已被移到磁盘上。在短时间内出现大量页错动作的情形被称为页抖动,它将大大降低系统的执行效率。

频繁存取内存中大量散布的位置的程序更容易在系统中造成页抖动。如果同时运行许多小程序,而实际上已经不再使用这些程序,也很容易造成页抖动。为了减少页抖动,你应该减少同时运行的程序的数目。对于大的程序,你应该改变它的工作方式,以尽量使操作系统能准确地判断出哪些页不再需要。为此,你可以使用高速缓冲存储技术,或者改变用于大型数据结构的查找算法,或者使用效率更高的malloc()函数。当然,你也可以考虑增加系统的RAM,以减少页出动作。

请参见:
1、怎样说明一个大于640KB的数组?
2、什么是堆(heap)?
3、怎样才能使DOS程序获得超过64KB的可用内存?
4、Windows是怎样组织内存的?

C语言变量必须初始化吗

不。使用变量之前应该给变量一个值,一个好的编译程序将帮助你发现那些还没有被给定一个值就被使用的变量。不过,变量不一定需要初始化。在函数外部定义的变量或者在函数内部用static关键字定义的变量(被定义在数据段中的那些变量,见2.1)在没有明确地被程序初始化之前都已被系统初始化为0了。在函数内部或程序块内部定义的不带static关键字的变量都是自动变量,如果你没有明确地初始化这些变量,它们就会具有未定义值。如果你没有初始化一个自动变量,在使用它之前你就必须保证先给它赋值。

调用malloc()函数从堆中分配到的空间也包含未定义的数据,因此在使用它之前必须先进行初始化,但调用calloc()函数分配到的空间在分配时就已经被初始化为0了。

请参见:
1、什么是局部程序块(10calblock)?
2、什么是栈(stack)?
3、什么是堆(heap)?

C语言变量存储在内存(memory)中的什么地方

变量可以存储在内存中的不同地方,这依赖于它们的生存期。在函数外部定义的变量(全局变量或静态外部变量)和在函数内部定义的static变量,其生存期就是程序运行的全过程,这些变量被存储在数据段(datasegment)中。数据段是在内存中为这些变量留出的一段大小固定的空间,它分为两部分,一部分用来存放初始化变量,另一部分用来存放未初始化变量。

在函数内部定义的auto变量(没有用关键字static定义的变量)的生存期从程序开始执行其所在的程序块代码时开始,到程序离开该程序块时为止。作为函数参数的变量只在调用该函数期间存在。这些变量被存储在栈(stack)中。栈是内存中的一段空间,开始很小,以后逐渐自动增大,直到达到某个预定义的界限。在象DOS这样的没有虚拟内存(virtual memory)的系统中,这个界限由系统决定,并且通常非常大,因此程序员不必担心用尽栈空间。关于虚拟内存 的讨论,请参见2.3。   

第三种(也是最后一种)内存空间实际上并不存储变量,但是可以用来存储变量所指向的数据。如果把调用malloc()函数的结果赋给一个指针变量,那么这个指针变量将包含一块动态分配的内存的地址,这块内存位于一段名为“堆(heap)”的内存空间中。堆开始时也很小,但当程序员调用malloc()或calloc()等内存分配函数时它就会增大。堆可以和数据段或栈共用一个内存段(memorysegment),也可以有它自己的内存段,这完全取决于编译选项和操作系统。

与栈相似,堆也有一个增长界限,并且决定这个界限的规则与栈相同。

请参见:
1、什么是局部程序块(10calblock)?
2、变量必须初始化吗?
3、什么是页抖动(pagethrashing)?
4、什么是栈(stack)?
5、什么是堆(heap)?

C语言变量作用域和生存期

C语言的强大功能之一是可以灵活地定义数据的存储方式。C语言从两个方面控制变量的性质:作用域(scope)和生存期(lifetime)。作用域是指可以存取变量的代码范围,生存期是指可以存取变量的时间范围。

作用域有三种:
  1. extern(外部的)  这是在函数外部定义的变量的缺省存储方式。extern变量的作用域是整个程序。
  2. static(静态的)  在函数外部说明为static的变量的作用域为从定义点到该文件尾部;在函数内部说明为static的变量的作用域为从定义点到该局部程序块尾部。
  3. auto(自动的)  这是在函数内部说明的变量的缺省存储方式。auto变量的作用域为从定义点到该局部程序块尾部。
变量的生存期也有三种,但它们不象作用域那样有预定义的关键字名称。
  1. 第一种是extern和static变量的生存期,它从main()函数被调用之前开始,到程序退出时为止。
  2. 第二种是函数参数和auto变量的生存期,它从函数调用时开始,到函数返回时为止。
  3. 第三种是动态分配的数据的生存期,它从程序调用malloc()或calloc()为数据分配存储空间时开始,到程序调用free()或程序退出时为止。

可以把C语言变量保存在局部程序块中吗

用局部程序块来保存变量是不常见的,你应该尽量避免这样做,但也有极少数的例外。
  1. 例如,为了调试程序,你可能要说明一个全局变量的局部实例,以便在相应的函数体内部进行测试。
  2. 为了使程序的某一部分变得更易读,你也可能要使用局部程序块。
    例如,在接近变量被使用的地方说明一个变量有时就会使程序变得更易读。
然而,编写得较好的程序通常不采用这种方式来说明变量,你应该尽量避免使用局部程序块来保存变量。


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
目 录 第1章 C语言 8 1.1 什么是局部程序块(local block)? 8 1.2 可以把变量保存在局部程序块中吗? 9 1.3 什么时候用一条switch语句比用多条if语句更好? 9 1.4 switch语句必须包含default分支吗? 10 1.5 switch语句的最后一个分支可以不要break语句吗? 11 1.6 除了在for语句中之外,在哪些情况下还要使用逗号运算符? 11 1.7 怎样才能知道循环是否提前结束了? 13 1.8 goto,longjmp()和setjmp()之间有什么区别? 13 1.9 什么是左值(lvaule)? 15 1.10 数组(array)可以是左值吗? 15 1.11 什么是右值(rvaule)? 16 1.12 运算符的优先级总能保证是“自左至右”或“自右至左”的顺序吗? 17 1.13 ++var和var++有什么区别? 17 1.14 取模运算符(modulus operator)“%”的作用是什么? 17 第2章 变量和数据存储 18 2.1. 变量存储在内存(memory)中的什么地方? 18 2.2. 变量必须初始化吗? 19 2.3. 什么是页抖动(pagethrashing)? 19 2.4. 什么是const指针? 20 2.5. 什么时候应该使用register修饰符?它真的有用吗? 21 2.6. 什么时候应该使用volatile修饰符? 21 2.7. 一个变量可以同时被说明为const和volatile吗? 22 2.8. 什么时候应该使用const修饰符? 23 2.9. 浮点数比较(floating-point comparisons)的可靠性如何? 23 2.10. 怎样判断一个数字型变量可以容纳的最大值? 24 2.11. 对不同类型的变量进行算术运算会有问题吗? 25 2.12. 什么是运算符升级(operatorpromotion)? 25 2.13. 什么时候应该使用类型强制转换(typecast)? 26 2.14. 什么时候不应该使用类型强制转换(typecast)? 27 2.15. 可以在头文件中说明或定义变量吗? 27 2.16. 说明一个变量和定义一个变量有什么区别? 27 2.17. 可以在头文件中说明static变量吗? 28 2.18. 用const说明常量有什么好处? 28 第3章 排序与查找 28 排序 28 查找 29 排序或查找性能? 30 3.1. 哪一种排序方法最方便? 32 3.2. 哪一种排序方法最快? 33 3.3. 对外存(磁盘或磁带)中而不是内存中的数据进行排序称为外部排序。 39 3.4. 1哪一种查找方法最方便? 44 3.5. 1哪一种查找方法最快? 46 3.6. 1什么是哈希查找? 51 3.7. 1怎样对链表进行排序? 53 3.8. 1怎样查找链表中的数据? 53 第4章 数据文件 59 4.1. 当errno为一个非零值时,是否有错误发生? 59 4.2. 什么是流(stream)? 59 4.3. 怎样重定向一个标准流? 60 4.4. 怎样恢复一个重定向了的标准流? 60 4.5. stdout能被强制打印到非屏幕设备上吗? 61 4.6. 文本模式(textmode)和二进制模式(binarymode)有什么区别? 61 4.7. 怎样判断是使用流函数还是使用低级函数? 62 4.8. 怎样列出某个目录下的文件? 62 4.9. 怎样列出一个文件的日期和时间? 63 4.10. 怎样对某个目录下的文件名进行排序? 66 4.11. 怎样判断一个文件的属性? 67 4.12. 怎样查看PATH环境变量? 69 4.13. 怎样打开一个同时能被其它程序修改的文件? 69 4.14. 怎样确保只有你的程序能存取一个文件? 71 4.15. 怎样防止其它程序修改你正在修改的那部分文件内容? 71 4.16. 怎样一次打开20个以上的文件? 72 4.17. 怎样避开"Abort,Retry,Fail”消息? 72 4.18. 怎样读写以逗号分界的本? 74 第5章 编译预处理 76 5.1. 什么是宏(macro)?怎样使用宏? 76 5.2. 预处理程序(preprocessor)有什么作用? 77 5.3. 怎样避免多次包含同一个头文件? 79 5.4. 可以用#include指令包含类型名不是".h"的文件吗? 80 5.5. 用#define指令说明常量有什么好处? 80 5.6. 用enum关键字说明常量有什么好处? 81 5.7. 与用#define指令说明常量相比,用enum关键字说明常量有什么好处? 81 5.8. 如何使部分程序在

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值