关闭

C语言的历史以及疑点解惑

1090人阅读 评论(2) 收藏 举报

C语言的历史以及疑点解惑

    C语言是一个无限广阔的世界,你刚开始睁眼看它的时候以为视线的尽头就是边界,但当你慢慢走去,才发现天外有天。这或许就是江湖传说只有C才有真正高手的原因,或许我们也可以反过来说:C没有高手——因为没有人能够真正走到尽头。
    C语言是一门放荡不羁的语言,它给你最大自由,也容易让你摔跤,甚至迷失方向。
 
    通俗地来讲,学习大概可以归结为以下几种途径:A.学校老师的教 B.工作中的需要 C.自己平时要。我的大学专业是数学,因此当初开的C语言只是一门象征性的扫盲课程,老师象征性地教,我们象征性地学。大学毕业后从事的是Web编程,主要用PHP,虽然PHP跟C有点像,但毕竟不是C;再后来研究生阶段10个月的实习用过C,但也并不多。因此,我就是第三者了:平时比较留心C的学习,包括C++。我现在仍然可以归为C的初学者之列,但是毕竟经历了一个过程,还是有一些自己的积累和总结,现列出,以资勉励。本人也是初学者,因此难免有谬误之处,还请知情者斧正。
 
    本文列出的疑点包括初级的,也包括不初级的,并没有仔细分类。本文灵感来自于《C缺陷与陷阱》,参考过《ASNI C标准文档》、《The C++ Programming Language》。感谢Ken Thompson和Dennis Ritchie为我们提供了C,感谢Bjarne Stroustrup为我们提供了C++。
 
  了解一种东西必须首先了解其历史:
 
  1963年,剑桥大学将ALGOL 60语言发展成为CPL(Combined Programming Language)语言。
  1967年,剑桥大学的Matin Richards对CPL语言进行了简化,于是产生了BCPL语言。
  1970年美国贝尔实验室的Ken Thompson以BCPL语言为基础,又作了进一步简化,设计出了很简单的而且很接近硬件的B语言(取BCPL的第一个字母),并用 B语言写第一个UNIX操作系统,在PDP-7上实现。
  1972年至1973年间,贝尔实验室的 Dennis Ritchie在B语言的基础上设计出了C语言(取 BCPL的第二个字母)。C语言既保持了BCPL和B语言的优点(精练、接近硬件),又克服了它们的缺点(过于简单、数据无类型等)。最初的C语言只是为描述和实现UNIX操作系统提供一种工作语言而设计的。
  1973年,Ken Thompson和Dennis Ritchie两人合作把UNIX的90%以上用 C改写(UNIX第5版。原来的UNIX操作系统是1969年由美国的贝尔实验室的Ken Thompson和Dennis Ritchie用汇编开发完成的)。
  1978年Brian Kernighian和Dennis Ritchie出版了名著《The C Programming Language》(取两人名字首字母,也就是通常所说的K&R),这本书中介绍的C语言成为后来广泛使用的C语言版本的基础,它被称为标准C。
  1983年,美国国家标准化协会(ANSI)根据C语言问世以来各种版本对C的发展和扩充,制定了新的标准,称为ANSI C。ANSI C比原来的标准C有了很大的发展。
  K&R在1988年修改了他们的经典著作《The C Programming Language》,按照ANSI C的标准重新写了该书。
  1987年,ANSI C又公布了新标准--87 ANSI C 。目前流行的C编译系统都是以它为基础的。
 
  1980年,Bjarne Stroustrup出于工作需要借鉴Simula的思想为C添加了类的概念,称为C with Classes。他同时获得了C的效率和类的方便。
  1983年C with Classes有了一个新的名字:C++。此间,C++一直被作为一种非标志语言在“民间”流传。
  1998年,C++已经是一门非常热门的语言,而且版本不一,国际标准化组织(ISO)对其进行了标准化,统一了语言风格,增加了一些重要特性(模版、异常处理机制等),丰富了标志类库。C++从此成为一门全新的语言。
 
  下面转入正文:
   
  疑点1:if ( 0<=x<=99 ) ?
  解惑1:这个疑点是针对刚接触编程语言的新手。我们习惯了数学中的那种自然表达,却忘记了在自然空间和程序空间之间还有一个真空地带,需要我们去映射。
  if ( 0<=x && x<=99 )
 
 
  疑点2:if ( a=7 ) ?
  解惑2:这符合语法,有时候也符合语义比如:if ( ch=getch() ) 用于判断用户输入是否结束。但用户往往本意是比较相等。这也是针对刚接触编程语言的新手,因为数学中的等号就是“=”。但是即便是一个C老手,有时也会犯这种低级错误,而且这种错误比较隐讳,不容易被查出。为了避免这种情况的发生,凡是比较相等的地方我们都可以这样写:
  if ( 7==a )
  如果我们误写成了if ( 7=a ),编译器就会告诉我们,因为这是语法错误。


  疑点3:*p++
  解惑3:其实我们都知道这个表达式的意思是“取出p指向的内容,然后p自增”。但通常我们潜意识中是 (*p)++,其实按照运算符优先级应该是 *(p++)。由于 *p++用的非常普遍,因此也就不需要再去加括号了,但我们应该清楚其真正表达的意思。

 

  疑点4:& 和 &&
  解惑4:一开始&和|既用于位运算,又用于逻辑运算,后来为了区分两种不同的运算才引入了&&和||,但&和|仍然可用于逻辑运算,虽然运算结果不会有错,但仍然不推荐。



  疑点5:if ( val & mask ==0 )?
  解惑5:应该是:if ( (val & mask) ==0 ),因为&的优先级比==的要低,这跟&&是一样的情况。
 
 
  疑点6:亦或(^)的作用是什么?
  解惑6:亦或运算是用来计算两个位串的不同位。用的不多,只在特点场景下使用。但可以用来把一个变量清零。
  int val = val^val;
  就相当于:
  int val = 0;
 
 
  疑点7:变量名中是否可以有美元符号($)?
  解惑7:可以。该符号可以出现在C标识符中的任何位置。但是由于该符号可能在某些系统中有特殊用途,因此建议不要使用。
  int $x=0;
   

  疑点8:指针定义中的星号(*)到底应该放哪边?
  解惑8:
  char *p=0;
  char* p=0;
  char * p=0;
  以上三种都符合语法,但是第三种不常用,因为容易跟乘法混淆。
  如果是这样定义:
  char* p, q;
  容易误认为q也是一个字符指针,但实际上q只是一个字符。因此我更倾向于使用第一种定义。
 

  疑点9:声明和定义区别何在?
  解惑9:声明:declaration,定义:definition。我不打算去咬文嚼字,只要搞清楚以下几点:
  A. 对一个实体的定义(变量、函数等)只能有一个地方,如果在别处或者其定义前用到了该实体,则需要进行声明。因为编译器需要在使用一个实体的地方知道其确切信息——对于变量来说就是其类型,对于函数来说就是其原型。
  B. 一个.c的源码文件是一个编译单元。如果在一个编译单元内需要使用另外一个编译单元的全局变量,则需要使用extern对该变量进行声明,以便编译器去别的编译单元去查找这样的变量;如果需要使用另外一个编译单元的函数,则可以不进行声明,因为函数默认是对外可见的,如果要声明也不需要extern,只需要函数原型即可。
  C. 如果一个函数在使用的之前没有声明,则默认返回int型,因此如果不在使用前进行原型的声明是很危险的事情。
  D. 如果一个全局变量或函数被声明成了static,则对外是不可见的。但是通常我们很少会把一个全局变量或函数声明成static,因此一个编译单元可以很容易地访问到其他编译单元的全局变量或函数,这不是我们希望看到的,也就催生了信息隐藏的思想。

   
 
  疑点10:前后缀自增的本质区别?
  解惑10:自增运算符是C的一个特色,也是其灵活性的具体体现,但是也容易让人混淆。我们通常看到的解释是:++x和x++的区别是“前者是先参与运算,而后自增;后者则是先自增,而后参与运算”。理解这一点其实也就基本可以应付日常应用了,但还没有切中要害。前缀运算符只是简单的对变量增1,而后缀运算符则先把变量的值保存起来,然后把变量的值增1,变量原来的值作为结果。很明显,后者比前者多做了些事情,因此如果只是简单的增1,则选择前缀形式会更合适,比如在for循环中:for(i=0; i<10; ++i)。下面是我在VS2005下编译的两句程序,二者区别一看便知。
 
  printf("%p/n",++x);
004117D5  mov         eax,dword ptr [x]
004117D8  add         eax,1
004117DB  mov         dword ptr [x],eax
004117DE  mov         esi,esp
004117E0  mov         ecx,dword ptr [x]
004117E3  push        ecx 
004117E4  push        offset string "%p/n" (415650h)
004117E9  call        dword ptr [__imp__printf (4182B4h)]
004117EF  add         esp,8
004117F2  cmp         esi,esp
004117F4  call        @ILT+440(__RTC_CheckEsp) (4111BDh)
  printf("%p/n",x++);
004117F9  mov         eax,dword ptr [x]           ; 多出的语句
004117FC  mov         dword ptr [ebp-0D0h],eax    ; 多出的语句,多了两次访存

00411802  mov         ecx,dword ptr [x]
00411805  add         ecx,1
00411808  mov         dword ptr [x],ecx
0041180B  mov         esi,esp
0041180D  mov         edx,dword ptr [ebp-0D0h]
00411813  push        edx 
00411814  push        offset string "%p/n" (415650h)
00411819  call        dword ptr [__imp__printf (4182B4h)]
0041181F  add         esp,8
00411822  cmp         esi,esp
00411824  call        @ILT+440(__RTC_CheckEsp) (4111BDh)
 
  C++中的自增运算符跟C中的有一个明显区别就是:++x本身就是一个变量,是自增后的x,可以作为左值,因此我们可以进行下面的运算:(++x)=5,++(++x),&(++x);而x++仅是一个值,是x自增前的值,因此不能作为左值参与运算。
  当然,前后缀自减运算的情况是一样的,不再多做说明。
 
   
  疑点11:while () {...}和 do {...} while()到底有什么重要区别?
  解惑11:在初次学习C语言控制语句时恐怕多数人会有如此疑惑:到底二者有什么重大区别,值得把这么相似的东西搞两个控制结构出来?我们当时被告知二者的区别“后者表示语句至少要执行一次”,或者这么说“需要先判断的就用前者,需要先执行动作的就用后者”。可以肯定的说前者更普遍,但后者在某些情况下更自然,后者的存在是必要的,至于具体什么情况需要在实践中去体会。



  疑点12:C这么多基本类型,该怎么选?
  解惑12:在C中,bool,char,int都属于整型,float,double属于浮点型,另外还有short int,long int,long double,整型中除了bool外其他还都有signed和unsigned。这么看来的确不少。但是实际上我们常用的就是:bool,char,int,double,其中char默认是unsigned,int默认是signed。
  不少先驱在不同场合以不同的腔调告诉过我们:少用unsigned int。为什么?因为我们用的最多的是signed int,也就是int,如果有一个unsigned int参与运算,它会被默默地转换成int,如果这个unsigned int大于int的最大值,转换后它就变成了一个负数,这种问题通常会在软件运行较长时间后出现,并且是无规律重现,很难定位,因此干脆就不要用unsigned int。
  关于各种类型的长度问题也是一个问题。我们不要对各种数据类型的具体长度做任何假设,但是我们可以有如下的保证:
  1) char的长度是作为基准的,因此恒为1;int的长度通常是机器字长(也就是CPU可以同时处理的位数)。
  2)
  sizeof(char) >= 8 bits
  sizeof(short) >= 16 bits
  sizeof(long) >= 32 bits
  3)
  1 = sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long)
  1 <= sizeof(bool) <= sizeof(long)
  sizeof(char) <= sizeof(wchar_t) <= sizeof(long)
  sizeof(float) <= sizeof(double) <= sizeof(long double)
  sizeof(T) = sizeof(signed T) = sizeof(unsigned T)
  其中T可能是:char,short,int
   
  我们还可以列出通常情景:
  sizeof(char) = sizeof(bool) = 1
  sizeof(short) = 2
  sizeof(int) = 4
  你可以使用sizeof运算确定你的具体环境下各种类型的长度。
 
     
  疑点13:指针到底是个什么东西?
  解惑13:从对指针的掌握程度基本可以看出对C语言的掌握程度,这就是指针在C语言中的特殊地位。
  指针也是一个变量,只是它的内容是一个变量的地址。既然指针的内容是一个地址,那么其长度就是固定的,通常跟int型一样,在32电脑上是32位,也就是4个字节。既然指针是一个变量,那么它也有自己的地址,于是也就存在指针的指针(二级指针),三级指针。。
  指针可以有减法运算,但只限于指向同一个变量的指针,其值是两个指针间元素的个数。指针可以跟一个int型变量进行加减运算,也就是指针的移动,其他运算对指针没有意义。指针的移动正是其灵魂所在,通过这个移动的精灵,我们可以跟我们的内存空间进行亲密接触。C中的指针跟汇编中的地址是近亲,因此从C翻译成汇编就不需要太多的转换,这也是C高效的一个重要原因。
  指针有几种典型应用:
  A. 在一块连续的空间中(通常是一个数组)随机访问其中的元素
  B. 在链表中顺序访问元素
  C. 用于传递大块数据(作为函数参数或返回值)
  需要注意的是:指针加1表示的是其值要变成下一个元素的地址,因此增加的具体数字随指针所指向元素的类型不同而不同,这是在翻译成汇编的过程中确定的,这也是指针一定要有一个类型的原因。减1也是同样道理。
  指针最容易造成的问题有两种:
  A. 越界
  B. 访问空指针或未初始化指针所指向的内容
  因此,遇到使用指针的地方就需要多加小心。
 
 
  疑点14:函数指针到底有什么用处?
  解惑14:函数指针是指针应用的升华。其内容仍然是一个地址,这个地址是一个函数的地址。与普通指针不同的是,其类型是函数原型。事实上,函数名作为一个变量,其内容就是函数地址,只是其内容不能改变,这跟数组名是同样道理。当我们进行函数调用的时候,也是根据其参数列表来准备参数,根据函数名中的函数地址进行跳转,最后根据其返回值类型获取返回值。当我们使用函数指针的时候只是把一个函数名中存放的函数地址放在了一个同类型的变量(函数指针)中而已。在标志C中,使用函数指针调用函数需要这样:(*pFunc)(...),但后来就演变成了普通函数的调用方式:pFunc(...),后者更为自然。
  函数指针应用的一个典型场景:一个服务器程序会收到不同类型的消息,它首先从消息中取出消息类型,然后调用相应的消息处理函数。如果消息类型比较少,可以使用switch/case语句,但如果超过比如3种,使用函数指针数组就是一个最好的选择,这样使得程序清晰易读。具体实现如下:
  1) 我们定义出了消息类型:
  typedef enum _E_MsgType { MSG_GET, MSG_POST, MSG_PUT, MSG_HEAD, MSG_DELETE, MSG_TRACE, MSG_ILLEGAL } E_MsgType;
  还定义了处理函数的函数指针类型:
  typedef bool (*pfProcType) (...);
  还定义了一个函数指针数组:
  pfProcType Procs[MSG_ILLEGAL];
  2) 我们已经编写好了相应的消息处理函数,而且除函数名外,参数和返回值都相同。
  bool doGet(...)
  bool doPost(...)
  bool doPut(...)
  bool doHead(...)
  bool doDelete(...)
  bool doTrace(...)
  3) 在初始化函数中我们进行了如下注册
  Procs[MSG_GET] = doGet;
  Procs[MSG_POST] = doPost;
  ..
  Procs[MSG_TRACE] = doTrace;
  4) 在消息处理模块中,假设我们已经取出了消息类型,就可以这样调用相应的消息处理函数:  
  if (eMsgType >= MSG_ILLEGAL)
  {
    return false;
  }
 
  if(!Procs[eMsgType](...))
  {
    return false;
  }
 
  函数指针还有另外一个应用:
  在嵌入式应用中,定义一个函数指针,然后将某个地址处的代码作为函数进行调用。
  typedef void (*pfProcType)();
  (* ((pfProcType)0) )();
  其中pfProcType指向一个这样的函数:没有参数,没有返回值。
  调用的时候,用pfProcType把地址0处的代码看作这种类型的函数去调用。
 
 
  疑点15:常量指针?指针常量?
  解惑15:
  const char *p;
  char const *p;
  char * const p;
  const char * const p;
  这都是些什么东西?第一个看着最眼熟。
  其实前两个意思都是一样的,表明:p指向的内容不能被修改。第二个其实更符合实际意义,但我们已经习惯了第一种用法。const在*前面,就表示其内容是const,不能被修改。
  第三个:const在*后面,表示它修饰的是指针p,也就是说:指针p的内容,也就是其指向不能被修改。
  第四个我就不再罗嗦了。

疑点16:void *是做什么的?
  解惑16:void *也是一种指针,但是它没有任何类型信息,除了可以跟其他类型指针进行转换外不能做其他任何事情,它不是一个完整的指针。其最典型的应用就是作为函数参数类型或者返回值类型。当一个函数需要接受一个指针参数,但不能明确到底是什么类型时void *就派上用场了,返回值类型也是同样的情况。malloc()的返回值就是void *,其函数原型为:void *malloc( size_t size ); 因为它只是返回已经分配的内存的首地址,具体这块空间用作什么用途,那是用户的事情。
   
 
 
疑点17:数组下标为什么要从0开始?
  解惑17:刚开始接触编程时肯定会不习惯,为什么数组下标要从0开始。首先计算机底层世界就只有0和1,其次0在编程中的作用之大是不可想象的,它的身影几乎随处可见,作用不一。最后让我们来看一下如下一个边界判断:
  const int MAX_SIZE = 10;
  int arr[MAX_SIZE];
  ..
  int i=0;
  for(i=0; i<MAX_SIZE; ++i)
  {
  ///
  }
  整形变量往往被初始化成0,因此循环时如果从1开始就会非常别扭;而且上界刚好是数组大小MAX_SIZE。 
  总之,使用0作为起始下标是一种技巧,而不是缺陷,一旦你适应了这种思维,你会写出非常优美的算法。
 
 
 
疑点18:为什么数组之间不能赋值?
  解惑18:首先要明确,在C中数组之间的确是不能赋值的。我想这跟C“小巧、高效”的原则是一致的,数组赋值是一种复杂的运算,不小心就会造成混乱,因此C不能引进这种特性。如果两个数组都是int型,并且大小一样,直接使用C的赋值语句会让人感觉很舒服,但是如果两个数组大小不一样呢?如果类型不一样呢?如果其中一个数组是指针数组呢?所以如果你想在C中对数组进行赋值,那就循环吧。或者使用memcpy,因为数组是一段连续的空间。
 

  疑点19:如何传递数组作为参数?
  解惑19:
#include <stdio.h>

void prtIntArr(int arr[], int size)
{
  int i=0;
  for(i=0; i<size; ++i)
    printf("%d ", arr[i]);

}

int main()
{
  int arr[] = {10, 2, 5, 63, 90};
  prtIntArr(arr, sizeof(arr)/sizeof(arr[0]));
}

看到上面的程序你会不会替我捏一把汗?不会是把整个数组传进去了吧?放心兄弟,没问题的。其实上面的程序跟下面的没有本质区别:
#include <stdio.h>

void prtIntArr(int *pArr, int size)
{
  int i=0;
  for(i=0; i<size; ++i)
    printf("%d ", pArr[i]);

}

int main()
{
  int arr[] = {10, 2, 5, 63, 90};
  prtIntArr(arr, sizeof(arr)/sizeof(arr[0]));
}
也就是说,我们传递的只是数组的首地址,如果你还不信的话我们看看汇编代码:
  prtIntArr(arr, sizeof(arr)/sizeof(arr[0]));
00411411  push        5   
00411413  lea         eax,[arr]
00411416  push        eax
 
00411417  call        @ILT+460(_prtIntArr) (4111D1h)
0041141C  add         esp,8
你看,它只是把arr的地址压栈给prtIntArr()了。



  疑点20:字符串后面那个诡异的影子!
  解惑20:C中的字符串最后总是有一个结尾符,其二进制是0,字符形式为'/0'。没有这个结尾符我们无从知道该字符串在什么地方结束,因为我们没有任何关于其长度的信息。C中要定义一个字符串通常这样:char str[] = "hello"; 这个时候字符串跟字符数组没有区别。
  或许你一下手就这样定义了:char str[5] = "hello";因为hello长度不就是5嘛,可是你错了,你忘记了那个影子,你要给他一个容身之地。不过没关系,编译器或许会放你一马,或者只是口头警告一下。但是当你进行strlen或者strcpy时它就会给你惹麻烦了。或许它返回给你的结果是17,或许是117,或许是911,也或许因为它跑到了别人的地盘而被操作系统遣送回来,并且对你进行严重警告:不要擅闯别人的地盘,这是游戏规则。出现上面的不和谐局面就是因为strlen()一直在找那个尾巴,strlen()的实现或许是这样的:
  int strlen(const char *pstr)
  {
    int len=0;
    for(len=0; *pstr; ++pstr) ++len;
   
    return len;
  }
  因此,我们在为字符串分配空间的时候一定要给结尾符预留出空间,通常可以这样:
  const int MAX_STR_LEN = 255;
  char *str[MAX_STR_LEN+1];
 

  疑点21:你可以返回一个字符串常量。
  解惑21:正如上面所说,字符串常量是在代码段中,而代码段是可读的,因此我们完全可以在函数中返回一个字符串常量,但实际上返回的是它的地址。
#include <stdio.h>

const char *sayError()
{

  return "error occured.";

}

int main()
{
  printf("%s/n", sayError());
}

 

  疑点22:堆和栈区别何在?
  解惑22:我们通常是把堆和栈相提并论的,称作“堆栈”。能够认识到二者有区别就是一个提高。其实二者的区别相当大。
  一个进程(运行期的程序)在内存中通常包括以下几部分:
  1) 代码段:存放程序代码,如果程序有几个副本(进程)在运行,则他们共享该段。代码段是只读的,可执行的。程序中使用的一些常量字符串通常在代码段。
  2) 静态数据段:这里包括了程序中的全局变量、静态变量。只是初始化的和未初始化的会分开存放。为初始化的会被统一初始化为0。
  3) 动态数据段:包括堆和栈。
  程序显式申请的动态内存在堆空间中,由程序自己释放。堆对于整个进程可见,也就是说同一个进程中的一个函数申请的空间在别的函数中可以正常访问。
  栈是函数调用发生的地方。每一次函数调用都会有一块栈空间被分配(占用),我们称为“桢”,或者“栈桢”,而每一次函数返回都会有一个桢被释放。栈中所谓的申请和释放其实只是栈底与栈顶寄存器内容的修改,在栈中分配空间时栈顶寄存器的值减去要分配的字节数即可。
  每种操作系统对于进程空间的安排不一,我们没必要都拿出来研究,但是他们还是有共同点的:
  1) 进程空间基本都包括以上几类
  2) 堆和栈通常“相邻”,这里的相邻是指中间没有其他类型的空间,实际上对和栈之间的空间通常非常大,以防止二者之一的溢出会破坏对方的空间,因为堆通常向高低址“生长”,而栈相反。这种情景有点像我们用一个数组来模拟两个栈的情景。
  3) 代码段和静态数据段通常离的不太远。
 
  局部变量的空间是编译器根据其定义自动在栈中进行的,其释放也是随着函数调用的返回而释放。一个函数中的局部变量对于其他函数是不可见的。有一个比较有意思的地方就是:被调用函数的参数实际是在其调用函数的桢中,但是被调用函数仍然可以访问,并且把其作为自己的局部变量看待。
  而如果我们需要一个不同函数间共享的空间就需要用到堆了,但是一定要记得释放,否则就会造成所谓的内存泄漏。一个比较可行的方法就是在哪个函数中申请的就在哪个函数中释放(使用完毕后)。
 
  我们可以用如下程序来解释各种变量的分配情况:
#include <stdio.h>
#include <malloc.h>

int gInitialized = 10;     // 全局变量,已初始化,在已初始化静态数据段中
long gUnInitialized[10];   // 全局变量,未初始化,在未初始化静态数据段中

int main()
{
  char *pstr = "hello";             // 常量字符串,在代码段尾部

  char str[] = "hello";             // 局部变量,在main的栈桢中
  int iLocal = 0;                   // 局部变量,在main的栈桢中
 
  static int sInt;                  // 静态变量,但没初始化,在未初始化静态数据段中,切被初始化为0

  char *pMem = (char *)malloc(100); // 程序自己申请的空间,在堆中

  free(pMem);                       // 程序自己申请的空间,要自己释放
}


 

  疑点23:在C++中如何使用C中的头文件?
  解惑23:有两种方式:一种是跟在标准C中一样,另外一种就是include里面的不是文件名,而是前面加了一个c,后面去掉了.h,并且需要指明名字空间std方可使用。举个例子:
#include <cstdio>
using namespace std;

int main()
{
  printf("hello C++!");
 
}
 
 
  疑点24:少用#define
  解惑24:#define在C中用处颇多,也颇显另类,但它破坏了语言的整体性,因为这种预编译语句对翻译器(编译器中把C翻译成汇编的部分)实际是不可见的,在编译器对源文件进行预处理后它们就被化解地无影无踪。因此,为了维护C的纯洁,我们呼吁少用#define(因为像#include这种预编译语句是不可缺少的,但#define多数情况下可以被替代)。
  1) 当你打算用#define 定义常量时马上改成const
  2) 当你打算用#define 定义函数时马上改成inline
  3) 当你打算用#define 定义类型时马上改成typedef
  C++已经为#define准备了完整的替换方案,如果在C++中你还用#define,那么说明你有严重的C依赖症,应该及时就诊了。
 
 
  疑点25:我怎么知道某个函数的用法以及在哪个头文件?
  解惑25:唯一的答案就是MSDN。MSDN是Win32程序员的开发宝典,这也是Microsoft用于吸引并粘住程序员的一块蜜糖。Win32程序开发中用到的几乎所有信息都可以在MSDN上面找到。对于一个函数来说,通常有以下信息:函数原型、参数以及返回值说明、函数功能说明以及描述、所在头文件、库文件等,很多还包括示例。尤其当你在Linux下开发找不到函数使用说明时Microsoft的温暖就会一下子涌上你的心头,当你热泪盈眶的时候,你会从泪花中看到Bill.Gates孩童般灿烂的笑容。
 

  疑点26:在VC中如何编译C程序?
  解惑26:一开始我就没搞清楚该如何在VC中编译C程序,害得我用了一段时间的C-Free。
  在VC中创建“Win32控制台应用程序”,创建一个空的工程,然后新建.c文件即可。注意一定是.c的,而不是.cpp的。
   
 
  疑点27:基于对象?面向对象?
  解惑27:C语言的世界基本上已经实现了半共产主义:虽然它给每个家庭(一个编译单元)提供了一个防护门(static),但是很少有人关门,因为人们相信天下无贼,家家户户也都安居乐业,好不快活。但是突然有一天一个叫Bjarne Stroustrup的家伙请了个异国焊工在门上装了把锁,在窗户上装了窗帘。这个事情随后作为一个头号新闻在江湖流传开来。于是家家户户纷纷效仿,图的安心。后来人们才知道,Stroustrup原来早已不满时政,决心要“改造旧世界、建立新世界”,但是不改国号。由于该策略比较温和,不会跟现状产生冲突,反而可能还会有好处,于是当Stroustrup举起起义大旗的时候,越来越多的人尾随其后,决心跟他干。后来Stroustrup还真是不负众望,把原来C的世界纳入囊中,还占领了大块新的领地,自己做了盟主,旗号“C++”。再后来,听说Stroustrup还给自己的卧室装了个门,只在门上留了个小窗口。想必他对自己的孩子也是起了戒心,毕竟江湖无父子。
  上面这个传说发生在公元20世纪80年代。下面我们言规正传。
  当Stroustrup为C引入simula中的类的概念后,也就是产生了C with Classes后,就已经从原来的基于过程的程序开发模式转到了基于对象的模式。也就是说通过对象的概念实现信息的隐藏。
  而当后来又引入继承、多态等概念后,才真正是进行面向对象的开发。这个时候的主要目的是通过类的继承实现代码复用,提高开发效率。


 

  疑点28:C中有没有引用?
  解惑28:如果使用习惯了C++,可能会误认为C中也有引用,但是情况不是这样。C中函数调用时实参都是传值方式进行的(包括指针)。
  让我们看看下面这段程序: 
#include <stdio.h>

typedef struct _person {
  int age;
  int height;
} person;

void f(person per)
{
  printf("age: %d/n", per.age);
}

int main()
{
  person per;
  per.age = 27;
  per.height = 170;

  f(per);
}

  调用f()附近的汇编代码如下:
  person per;
  per.age = 27;
0041140E  mov         dword ptr [per],1Bh
  per.height = 170;
00411415  mov         dword ptr [ebp-8],0AAh

  f(per);
0041141C  mov         eax,dword ptr [ebp-8]  ; per.height
0041141F  push        eax 
00411420  mov         ecx,dword ptr [per]    ; per.age
00411423  push        ecx 
00411424  call        @ILT+455(_f) (4111CCh)
00411429  add         esp,8  

可以看到:调用函数f()前,直接把per两个子域的值压栈。
 
 
  疑点29:表达式中子表达式的运算顺序。
  解惑29:除了逗号表达式(,)、逻辑与(&&)、逻辑或(||)、三目运算符(?:)可以保证其子表达式是按照从左向右的顺序进行运算外,其余情况我们都不能想当然认为运算是从左向右的。其中逗号表达式最后一个子表达式的值作为整个运算结果;逻辑与只有当前面子表达式结果为真时才计算后面的子表达式;逻辑或只有当前面子表达式结果为假时才计算后面的子表达式,这称为“短路”。
  下面的情况都无法知道确切的运算顺序:
  int x = f(2) + g(3);
  int i=1; v[i] = i++;
   

  疑点30:一个在C++中运行正常的C程序在C中编译就提示类似下面的错误“错误 1 error C2143: 语法错误 : 缺少“;”(在“类型”的前面)”
  解惑30:C要求所有变量定义必须在执行语句之前,而C++没有这个要求,C++中的变量可以随用随定义。C也不允许这种for循环:
  for (int i=0; i<10; ++i)
  一旦发现这种错误就需要检查一下程序中的这种问题。
 

0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:437301次
    • 积分:6216
    • 等级:
    • 排名:第4112名
    • 原创:111篇
    • 转载:290篇
    • 译文:1篇
    • 评论:94条
    最新评论