Dissection C Chapter 1_2

2010-10-6晚   wcdj

本文接:Dissection C Chapter 1_1

http://blog.csdn.net/delphiwcdj/archive/2010/10/06/5923540.aspx

 

[1] C语言有多少个关键字?sizeof怎么用?它是函数吗?
[2] 什么是定义?什么是声明?它们有何区别?
[3] 关键字角色的逐个分析
      (1) auto
      (2) register
      (3) static
      (4) 基本数据类型
      (5) sizeof
      (6) signed, unsigned
      (7) if, else
      (8) switch, case
      (9) do, while, for
      (10) goto
      (11) void
      (12) return


(6) signed、unsigned关键字
计算机底层只认识0,1。那么负数怎么存储呢?负号“-”是无法存入内存的,怎么办?很好办,做个标记。
把基本数据类型的最高位腾出来,用来存储符号,同时约定如下:
最高位如果是1,表明这个数是负数,其值为除最高位以外的剩余位的值添上这个“-”号;如果最高位是0,表明这个数是正数,其值为除最高位以外的剩余位的值。
一个8位的char类型数,其值表示范围为:-2^7~2^7-1
一个32位的signed int类型整数,其值表示范围为:-2^31~2^31-1
一个32位的unsigned int类型整数,其值表示范围为:0~2^32-1
【注意】编译器默认情况下,数据为signed类型的。

【问题】下面代码输出的结果是什么?
#include <cstdio> #include <cstring> int main() { char a[1000]; int i; for (i=0; i<1000; ++i) { a[i]=-1-i; } printf("%d/n",strlen(a));// 255 return 0; }

【分析】
在计算机系统中,数值一律用补码来表示(存储)。主要原因是使用补码,可以将符号位和其他位统一处理,目的是减法也可按加法来处理。另外,两个用补码表示的数相加时,如果最高位(符号位)有进位,则进位被舍弃。
例如:
-1-1=(-1)+(-1)=1111 1111+1111 1111=1111 1110(补码)=-2      ( -(补码减1再取反) )
正数的补码与原码一致。
负数的补码:符号位为1,其余位为该数绝对值的原码按位取反,然后整个数加1。

按照负数补码的规则,可以知道:
-1的补码为0xff,-2的补码为0xfe……。
当i的值为127时,a[127]的值为-128,而-128是char类型数据能表示的最小的负数(-128~127)。
当i继续增加,a[128]的值肯定不是-129,因为这时候发生了溢出,-129需要9位才能存储下来,而char类型数据只有8位,所以最高位被丢弃。
即,-128-1=1000 0000+1111 1111=0111 1111=127
剩下的8位是原来9位补码的低8位的值,即0x7f=127。
当i继续增加到255的时候,-256的补码的低8位为0=0。
然后,当i增加到256时,-257的补码的低8位全为1=-1,即低8位的补码为0xff。
如此,又开始一轮新的循环……
即:-1,-2,……,-128,127,126,……,1,0,-1,-2……
按照上面的分析,a[0]到a[254]里面的值都不为0,而a[255]的值为0。
strlen函数是计算字符串长度的,并不包含字符串最后的'/0'。而判断一个字符串是否结束的标志就是看是否遇到'/0',如果遇到,则认为本字符串结束。
因此,strlen(a)的值应该为255。
这个问题的关键就是要明白:
(1) char类型默认情况下是有符号的,其表示的值的范围为[-128,127],超出了这个范围的值会产生溢出。
(2) 另外,还要清楚的就是负数的补码怎么表示。
下面代码用于验证上面的分析
#include <cstdio> int main() { char a=-1-127; printf("%d/n",a); // -128 a=(char)(-1-128); printf("%d/n",a); // 127 a=(char)(-1-255); printf("%d/n",a); // 0 a=(char)(-1-256); printf("%d/n",a); // -1 return 0; }

 

【问题扩展】
(1) 按照上面的解释,那-0和+0在内存里面是分别怎么存储的?
#include <iostream> #include <bitset> using std::bitset; using std::cout; using std::endl; int main() { int i=-0,j=+0; unsigned k=-1; bitset<32> bitvec_i(i); bitset<32> bitvec_j(j); bitset<32> bitvec_k(k); cout<<bitvec_i<<endl; // [32](0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0) cout<<bitvec_j<<endl; // [32](0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0) cout<<bitvec_k<<endl; // [32](1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1) cout<<k<<endl; // 4294967295 return 0; }

(2) int i=-20; unsigned j=10;   i+j的值为多少?为什么?
#include <cstdio> int main() { int i=-20; unsigned j=10; printf("%d/n",sizeof(j)); // 4 printf("%d/n",i+j); // -10 printf("%u/n",i+j); // 4294967286 return 0; }

(3) 下面的代码有什么问题?
unsigned i;
for(i=9; i>=0; --i)
{
    printf("%u\n",i);
}

(7) if, else组合
bool变量与“零值”进行比较
if(bTestFlag==TRUE)
if(bTestFlag==FLASE)

float变量与“零值”进行比较
if( fTestVal>=-EPSIONON && fTestVal<=EPSIONON )// EPSIONON为定义好的精度

指针变量与“零值”进行比较
int *p=NULL;
if(NULL==p)

问题:else到底与哪个if配对呢?
建议:程序中尽量用大括号包含独立的语句块

问题:if后面的分号
建议:;代表空语句,为了便于查看,使用NULL;表示空语句。

(8) switch, case组合
if, else一般表示两个分支或是嵌套表示少量的分支,但如果分支很多的话……还是用switch, case组合吧。
【注意】
【一点】:每个case语句的结尾绝对不要忘了加break,否则将导致多个分支重叠(除非你有意这样做)
【二点】:最后必须使用default分支。即使你的程序真的不需要default处理,也应该保留这个语句。这样做并非画蛇添足,可以避免让人误解为你忘了default处理。
【三点】:case关键字后面的值有什么要求吗?记住:case后面只能是整型或字符型的常量或常量表达式(实际上都是整型)。
【四点】:case语句的排列顺序。 当case语句非常多的时候,就需要考虑case语句的排列顺序了。规则一:按字母或数字顺序排列各条case语句;规则二:把正常情况放在前面,而异常情况放在后面。规则三:把执行频率高的语句放在前面,也方便调试。
【五点】:简化每种情况对应的操作。一般,case语句后面的代码尽量不要超过20行。
【六点】:不要为了使用case语句而刻意制造一个变量。
【七点】:把default子句只用于检查真正地默认情况。

(9) do, while, for
C语言中循环语句有三种:while循环、do-while循环和for循环。
思考:在switch case语句中,能否使用continue关键字?为什么?
【注意】
【建议1】在多重循环中,如果有可能,应当将最长的循环放在最内层,最短的循环放在最外层,以减少CPU跨切循环层的次数。
【建议2】最好不要在for循环体内修改循环变量,防止循环失控。
【建议3】循环要尽可能的短,要使代码清晰,一目了然。
【建议4】把循环嵌套控制在3层以内。

(10) goto
请禁止使用goto语句。
【缺点一】它会破坏程序的结构化设计风格。
【缺点二】经常带来错误或隐患,它可能跳过了变量的初始化、重要的计算等语句。
例如:
struct student *p=NULL;
……
goto state;
p=(struct student *)malloc(……);// 被goto跳过,没有初始化
……
state:
// 使用p指向的内存里的值的代码
……
如果编译器不能发觉此类错误,每用一次goto语句都可能留下隐患。

(11) void
void是空类型
void *是空类型指针,它可以指向任何类型的数据。
void真正发挥作用在:(1) 对函数返回的限定。(2) 对函数参数的限定。
float *fp;
int *ip;
fp=(float*)ip;// 不同类型赋值时,需要进行强制类型转换
而void*则不同,任何类型的指针都可以直接赋值给它,无需进行强制类型转换。
void *vp;
int *ip;
vp=ip;// ok,无需强制类型转换
但是,void*不能直接赋给其它类型的指针,必须进行强制类型转换。即,“空类型”可以包容“有类型”,而“有类型”则不能包容“空类型”。
void *vp;
int *ip;
ip=vp;// error, cannot convert from 'void *' to 'int *'

【注意】
【规则1】如果函数没有返回值,那么应声明为void类型。
在C语言中,凡不加返回值类型限定的函数,就会被编译器作为返回整型值处理。

【规则2】如果函数无参数,那么应声明其参数为void。
在C语言中,可以给无参数的函数传送任意类型的参数。
在C++中,不能向无参数的函数传送任何参数。
所以,无论在C还是在C++中,若函数不接受任何参数,一定要指明参数为void。

【规则3】千万小心又小心地使用void指针类型。
按照ANSI标准,不能对void指针进行算法操作。
之所以这样认定,是因为它坚持:进行算法操作的指针必须是确定知道其指向数据类型大小的。也就是说,必须知道内存目的地址的确切值。
但是,在GNU中则不这么认定。它指定void*的算法操作与char*一致。

【规则4】如果函数的参数可以是任意类型指针,那么应声明其参数为void*。
典型的如,内存操作函数memcpy和memset的函数原型分别为:
 void *memcpy(void *dest, const void *src, size_t len); void *memset(void *buffer, int c, size_t num); int IntArray_a[100]; memset(IntArray_a, 0, 100*sizeof(int));// 将IntArray_a清0 int destIntArray_a[100], srcIntArray_a[100]; memcpy(destIntArray_a, srcIntArray_a, 100*sizeof(int));// 将src拷贝给dest  
任何类型的指针都可以传入memcpy和memset中,这也真实地体现了内存操作函数的意义,因为,它操作的对象仅仅是一片内存,而不论这片内存是什么类型。
【规则5】void不能代表一个真实的变量。
void a;// 错误
function(void a);// 错误
因为定义变量时必须分配内存空间,定义void类型变量,编译器到底分配多大的内存呢?
void的出现只是为了一种抽象的需要,如果你正确地理解了面向对象中“抽象基类”的概念,就很容易理解void数据类型了。正如,不能给抽象基类定义一个实例,我们也不能定义一个void变量。

(12) return
return用来终止一个函数并返回其后面跟着的值。
return (val);// 此括号可以省略,但一般不省略,尤其在返回一个表达式的值时。
【注意】
【规则1】return语句不可返回指向“栈内存”的指针,因为该内存在函数体结束时被自动销毁。

 

 

 

你可能感兴趣的:(c,存储,语言,float,编译器)