2010-10-18 wcdj
本文接:Dissection C Chapter 1_2
http://blog.csdn.net/delphiwcdj/archive/2010/10/06/5924226.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
(13) const
(14) volatile
(15) extern
(16) struct
(17) union
(18) enum
(19) typedef
(13) const
const是constant的缩写,是恒定不变的意思,也翻译为常量、常数等。
【注意】
【1】 很多人都认为被const修饰的值是常量。这是不精确的,精确地说应该是:只读的变量,其值在编译时不能被使用,因为编译器在编译时不知道其存储的内容。
#include <cstdio> int main() { const int MAX=100; int a[MAX]={0}; return 0; }
想:这样用正确吗?MAX是只读变量,其值在编译时不能被使用。(C中不正确,C++中可以)
【2】 const推出的目的,正是为了取代预编译指令,消除它的缺点,同时继承它的优点。注意,define不是关键字。
PS: 可以参考《Essential C++》其中一条款的详细介绍。
【3】 const修饰的只读变量必须在定义的同时初始化。
【4】 case语句后面可以使用const修饰的只读变量吗?(当然可以)
#include <cstdio> int main() { const int I_AM_CONST=100; switch (I_AM_CONST) { case 1: break; case 2: break; case 100: printf("ok/n"); break; default: NULL; } return 0; }
【5】节省空间,避免不必要的内存分配,同时提高效率。
编译器通常不为普通(全局的?)const只读变量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的值,没有了存储与读内存的操作,使得它的效率也很高。
const定义的只读变量从汇编的角度来看,只是给出了对应的内存地址,而不是像#define一样给出的是立即数,所以,const定义的只读变量在程序运行过程中只有一份拷贝(因为它是全局的只读变量,存放在静态区),而#define定义的宏常量在内存中有若干个拷贝。#define宏是在预编译阶段进行替换,而const修饰的只读变量是在编译的时候确定其值。#define宏没有类型,而const修饰的只读变量具有特定的类型。
为了验证上述的论述,我做了一下的测试:
#include <cstdio> #define M 3// 宏常量 const int N=100;// 此时并未将N放入内存中 int g_val=1; int main() { 0042D600 push ebp 0042D601 mov ebp,esp 0042D603 sub esp,0F0h 0042D609 push ebx 0042D60A push esi 0042D60B push edi 0042D60C lea edi,[ebp-0F0h] 0042D612 mov ecx,3Ch 0042D617 mov eax,0CCCCCCCCh 0042D61C rep stos dword ptr es:[edi] //const int N=100;// 此时并未将N放入内存中 int a=N;// 此时为N分配内存,以后不再分配 0042D61E mov dword ptr [a],64h int b=M;// 预编译期间进行宏替换,分配内存 0042D625 mov dword ptr [b],3 int c=N;// 没有内存分配 0042D62C mov dword ptr [c],64h int d=M;// 再进行宏替换,又一次分配内存 0042D633 mov dword ptr [d],3 return 0; 0042D63A xor eax,eax } 0042D63C pop edi 0042D63D pop esi 0042D63E pop ebx 0042D63F mov esp,ebp 0042D641 pop ebp 0042D642 ret
我的疑问是 :(将const变量作为全局和局部)
当const变量是全局的时候,VS编译器没有为其分配存储空间,即把当做立即数进行替换。
但是,当声明 const变量为局部变量的时候,VS编译器是为其分配了空间的。这样的话,所提的const的这个“优点”是不是特指的全局变量的时候呢。
为讨论这个问题发布的一个帖子:
http://topic.csdn.net/u/20101013/16/bca742a9-b0c4-4d6b-b301-7d273db44c59.html
最后的结论是 :Debug 下,编译器会保存大量运行时不需要的数据,用于调试信息。所以和真正的release版的汇编不同。关于这个问题还没有完全解决,对于如何测试和验证这个问题,现在还没有找到一个好的方法。(如果您知道请您告诉我,Thx)
【6】修饰一般变量。
const int i=2; // the above same as int const i=2;
即,修饰符const可以用在类型说明符前,也可以用在类型说明符后。(习惯多用在类型说明符前,但建议用在类型说明符后,见4.2.5 section in C++ Primer)
【7】修饰数组。
const int a[]={1,2,3,4,5}; // same as int const a[]={1,2,3,4,5};
【8】修饰指针。
const int *p1;// p1可变,p1指向的对象不可变 int const *p2;// 同上 int ival=1; int* const p3=&ival;// p3不可变且必须初始化,p3指向的对象可变 const int* const p4=&ival;// 指针p4和p4指向的对象都不可变,且p4必须初始化
记忆方法1 :const修饰符可以放在所修饰类型的前面或后面,建议放在后面。见4.2.5 section in C++ Primer
记忆方法2 :先忽略类型名(编译器解析的时候也是忽略类型名),我们看const离哪个近,就是修饰它后面的那个类型。
const int *p1;// const修饰*p1
int const *p2;// 同上
int ival=1;
int * const p3=&ival;// const修饰p3
const int * const p4=&ival;// 第一个const修饰*p4,第二个const修饰p4
【9】修饰函数的参数。
const修饰符也可以修饰函数的参数,当不希望这个参数值被函数体内意外改变时使用。
例如:void fun(const int i);
告诉编译器i在函数体中不能改变,从而防止了使用者的一些无意的或错误的修改。
【10】修饰函数的返回值。
const修饰符还可以修饰函数的返回值,返回值不可被改变。
例如:const int fun();
【11】C++中对const进行了扩展,其特性可查相关资料。
(14) volatile
volatile是易变的、不稳定的意思。很多人根本没有见过这个关键字,不知道它的存在,也有很多人知道它的存在,但从来没有用过它。
volatile关键字和const一样是一种类型修饰符,用它修饰的变量表示可以被某些编译器未知的因素更改,比如操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码不再进行优化,从而可以提供对特殊地址的稳定访问。
例子:
int i=10;
int j=i;// (1)
int k=i;// (2)
这时候编译器对代码进行优化,因为在(1)、(2)两条语句中,i没有被用作左值。这个时候编译器认为i的值没有发生改变,所以在(1)语句时从内存中取出i的值赋给j之后,这个值并没有被丢掉,而是在(2)语句时继续用这个值给k赋值。编译器不会重新从内存里取i的值,这样提高了效率。但要注意:(1)、(2)语句之间i没有被用作左值才行。
另一个例子:
volatile int i=10;
int j=i;// (3)
int k=i;// (4)
volatile关键字告诉编译器i是随时可能发生变化的,每次使用它的时候必须从内存中取出i的值,因而编译器生成的汇编代码会重新从i的地址处读取数据放在k中。这样看来,如果i是一个寄存器变量或者表示一个端口数据或者是多个线程的共享数据,就容易出错,所以说volatile可以保证对特殊地址的稳定访问。
【注意】
在VC6中,一般debug模式没有进行代码优化,所以这个关键字的作用有可能看不出来。我们可以同时生成debug版和release版的程序做个测试。
【思考】请问下面代码是否有问题,如果没有,那i到底是什么属性?(i是const属性)
const volatile int i=10;
i=20;
(15) extern
extern是外面、外来的意思。extern可以置于变量或者函数前,以标识变量或者函数的定义在别的文件中,下面的代码用到的这些变量或函数是外来的,不是本文件定义的,提示编译器遇到此变量和函数时在其他模块中寻找其定义。
【问题】extern和包含头文件有什么关系呢?
(16) struct
struct是个神奇的关键字,它将一些相关联的数据打包成一个整体,方便使用。
在网络协议、通信控制、嵌入式系统、驱动开发等地方,我们经常要传送的不是简单的字节流(char型数组),而是多种数据组合起来的一个整体,其表现形式是一个结构体。经验不足的开发人员往往将所有需要传送的内容顺序保存在char型数组中,通过指针偏移的方法传送网络报文等信息(这种方法自己做过,现在看来确实很笨),这样做编程复杂且易出错。
这个时候,只需要一个结构体就能搞定。平时我们要求函数的参数尽量不多于4个,如果函数的参数多于4个使用起来非常容易出错(包括每个参数的意义和顺序),效率也会降低(与具体CPU有关,ARM芯片对于超过4个参数的处理就有讲究,具体请参考相关资料)。这个时候,可以用结构体压缩参数个数。(在MFC下超过4个参数的函数很多)
【问题】空结构体多大?
结构体所占的内存大小是——其成员所占内存之和(注意:关于结构体的内存对齐)。
#include <cstdio> struct student { }; int main() { student stu; printf("%d/n",sizeof(stu));// 1 return 0; }
在VS2008下,测试输出为1。
【注意】struct与class的区别
在C++里struct关键字与class关键字一般可以通用,只有一个很小的区别。(当然,在C++中习惯用class)
struct的成员默认情况下属性是public的,而class成员却是private的。
【测试】通过下面的代码,说明:(1) struct的成员默认为public。(2) struct内可以定义函数。
#include <cstdio> typedef struct st_type { int i; st_type() { j=1; printf("this is constructor./n"); } ~st_type() { printf("this is destructor./n"); } void fun_in() { printf("fun_in is in the struct./n"); } void fun_out(); private: int j; }type_a; void type_a::fun_out() { printf("fun_out is out the struct./n"); } int main() { type_a st; st.fun_in(); st.fun_out(); printf("%d/n",sizeof(type_a)); //printf("%d/n",st.j);// error, can not access the private member j return 0; } /* output: this is constructor. fun_in is in the struct. fun_out is out the struct. 8 this is destructor. */
(17) union
union维护足够的空间来放置多个数据成员中的“一种”,而不是为每一个数据成员配置空间,在union中所有的数据成员共用一个空间,同一时间只能存储其中一个数据成员,所有的数据成员具有相同的起始地址。
【注意】
【1】一个union只配置一个足够大的空间来容纳最大长度的数据成员,以下面这个例子而言,最大长度是double类型,所以StateMachine的空间大小就是double数据类型的大小。(区别:结构体和类的大小和字节对齐有关,见#pragma pack(n)的用法,VS下默认是按4B对齐)
例如:
#include <cstdio> union StateMachine { char character; int number; char *str; double exp;// 8 }; int main() { printf("%d/n",sizeof(StateMachine));// 8 return 0; }
【2】在C++里,union的成员默认属性页为public。
【3】union主要用来压缩空间,如果一些数据不可能同一时间同时被用到,则可以使用union。
【4】大小端模式对union类型数据的影响。
#include <cstdio> union { int i; char a[2]; }*p, u; int main() { p=&u; p->a[0]=0x39; p->a[1]=0x38; printf("%x/n",p->i);// 3839 (hex.) printf("%d/n",p->i);// 111000 00111001=14393 (decimal) return 0; }
分析如下图所示 :
高地址 低地址
—— —— —— —— int
0 | 0 | 56 | 57
—— —— —— ——
—— —— char
56 | 57
—— ——
这里需要考虑存储模式:大端模式和小端模式。
大端模式(Big-endian) :数据的低字节存放在高地址中。
小端模式(Little-endian) :数据的低字节 存放在低地址 中。
union型数据所占的空间等于其最大的成员所占的空间,对union型成员的存取都是相对于该联合体基地址的偏移量为0处开始,即,联合体的访问不论对哪个变量的存取都是从union的首地址位置开始。因此,上面程序输出的结果就显而易见了。
【5】如何用程序确认当前系统的存储模式(大端还是小端)?
【问题】写一个C函数,若处理器是Big-endian的,则返回0;若是Little-endian的,则返回1。
分析:根据大小端模式的定义,假设int类型变量i被初始化为1。
以大端模式存储,其内存布局如下图:
高地址 低地址
—— —— —— —— int i=1;
0x1 | 0x0 | 0x0 | 0x0
—— —— —— ——
以小端模式存储,其内存布局如下图:
高地址 低地址
—— —— —— —— int i=1;
0x0 | 0x0 | 0x0 | 0x1
—— —— —— ——
变量i占4个字节,但只有一个字节的值为1,另外三个字节的值都为0。如果取出低地址上的值为0,则是大端模式;反之为小端模式。
因此,可以利用union类型数据的特点:所有成员的起始地址一致。
方法1:利用union类型
#include <cstdio> int checkSystem() { union check { int i; char ch; }c; c.i=1; return (c.ch==1); } int main() { checkSystem()==1 ? printf("Little-endian/n") : printf("Big-endian/n"); return 0; }
方法2:利用数组类型
#include <cstdio> int checkSystem() { char s[]="1000"; return (s[0]=='1'); } int main() { checkSystem()==1 ? printf("Little-endian/n") : printf("Big-endian/n"); return 0; }
【思考】
在x86系统下(即32位系统下),输出的值为多少?
#include <cstdio> int main() { int a[5]={1,2,3,4,5}; int *ptr1=(int*)(&a+1); // 0x0012ff68 int *ptr2=(int*)((int)a+1); // 0x0012ff55 int *ptr3=(int*)(a+1); // 0x0012ff58 printf("0x%x,0x%x,0x%x",ptr1[-1],*ptr2,*ptr3);// 0x5,0x2000000,0x2 return 0; }
分析如下图:
高地址 低地址
—— | —— —— —— —— | …… —— —— —— —— | —— —— —— ——
? | 0x0 | 0x0 | 0x0 | 0x05 | …… 0x0 | 0x0 | 0x0 | 0x02 | 0x0 | 0x0 | 0x0 | 0x01
—— | —— —— —— —— | …… —— —— —— —— | —— —— —— ——
^(ptr1) ^(ptr2)
因此,
ptr1[-1]=5(dec)=0x5(hex)
*ptr2=0x02 0x0 0x0 0x0=0x2 00 00 00(hex)=33554432(dec)
此题主要考查的是,指针不同偏移量的计算。
int a[5]={1,2,3,4,5}; // a==&a==0x0012ff54 int *ptr1=(int*)(&a+1); // [1] 0x0012ff68 int *ptr2=(int*)((int)a+1); // [2] 0x0012ff55 int *ptr3=(int*)(a+1); // [3] 0x0012ff58
注意区别 :
a==&a==&a[0]==0x0012ff54
但是,
a+1==&a[0]+1==0x0012ff58 此时的偏移量为:一个int型的大小
&a+1==0x0012ff68 此时的偏移量为:整个数组的大小
[1] (&a+1)等于数组的起始地址+数组的整个大小,即,此时为数组最后一个元素的后一个元素的地址。(int*)(&a+1)强制类型转换后,表示指针的偏移量为一个int型的大小。
[2] ((int)a+1)等于数组的起始地址+一个字节的大小。
[3] (a+1)等于数组的起始地址+一个数组元素的大小。
(18) enum
很多初学者对枚举类系感到迷惑,或者认为没有用,其实enum是个很有用的数据类型。
【一般的使用方法】
enum enum_type_name { ENUM_CONST_1, ENUM_CONST_2, // ... ENUM_CONST_n }enum_variable_name;
enum_variable_name是enum_type_name类型的一个枚举变量,它只能取花括号内的(枚举常量,一般用大写)任何一个值。enum变量还可以给其中的常量符号赋值,如果不赋值则会从被赋值的那个常量开始依次加1,如果都不赋值,它们的值从0开始依次递增1。
例如:
#include <cstdio> enum Color { GREEN=1, RED, BLUE, GREEN_RED=10, GREEN_BLUE }ColorVal; int main() { ColorVal=GREEN; if (ColorVal==1) printf("GREEN==1/n"); ColorVal=RED; if (ColorVal==2) printf("RED==2/n"); ColorVal=BLUE; if (ColorVal==3) printf("BLUE==3/n"); ColorVal=GREEN_RED; if (ColorVal==10) printf("GREEN_RED==10/n"); ColorVal=GREEN_BLUE; if (ColorVal==11) printf("GREEN_BLUE==11/n"); printf("%d/n",sizeof(ColorVal));// 4 return 0; }
【enum与#define宏的区别】
[1] #define宏常量是在预编译阶段进行简单替换;枚举常量则是在编译的时候确定其值。
[2] 一般在编译器里,可以调试枚举常量,但是不能调试宏常量。
[3] 枚举可以一次定义大量相关的常量,而#define宏一次只能定义一个。
【问题】sizeof(ColorVal)的值为多少?为什么?(为4)
(19) typedef
用typedef关键字来创建各种——马甲
typedef是用来给一个已经存在的数据类型取一个别名,而非定义一个新的数据类型。这样做的目的是:用新的名字可以更好地表达其意思。
在实际项目中,为了方便,可能很多数据类型(尤其是结构体之类的自定义数据类型)需要我们重新取一个适用实际情况的,这时候typedef可以帮我们的忙。
typedef struct student { // code }stu_st, *stu_pst; struct student stu1;// same as below stu_st stu1; struct student *stu2;// same as below stu_pst *stu2;
【注意一】把typedef与const放在一起的情况
const stu_pst stu3;// [1] stu_pst const stu4;// [2]
方法是,我们看const修饰谁的时候,完全可以将数据类型名视而不见,当它不存在。因此,修饰的是指针,而不是stu3、stu4指向的对象。
【注意二】#define是在预编译的时候进行宏替换,因此是正确的;而typedef取的别名不支持类型扩展。
#define INT32 int unsigned INT32 i=10;// ok typedef int int32; unsigned int32 j=10;// error typedef unsigned int int32; // ok typedef static int int32; // error #define PCHAR char* PCHAR p3, p4;// error, p3 is a pointer, however, p4 is not typedef char* pchar; pchar p1, p2;// ok, p1 and p2 are pointer
第一章关键字 (已总结完)