http://liyanblog.cn/李岩的博客 李岩 java lucene 搜索 nosql hadoop 博客 mongodb, c++2024-03-19T10:43:10+08:00李岩的博客c++程序员如何成长savagertnullhttp://liyanblog.cn/articles/2012/09/19/1348047284109.html2012-09-19T17:34:44+08:00<div class="content-head clearfix">
<h2 class="title content-title">c++程序员如何成长</h2>
</div>
<div id="content" class="content mod-cs-content text-content clearfix">
<p>先把C++的语法全面复习一下,建议看看《Thinking in C++》,也就是《C++编程思想》,这本书并不是像有些人说的那么烂,对掌握基本C++语法来说足够了。<br /><br />然后你去买一本清华大学出版社出版的《数据结构(C++版)》,扎扎实实从第一章学到最后一章,把每道习题都做一下,遇到C++编程方面的问题就去回过头翻C++基础书,比如说《C++编程思想》。这时候你遇到的问题应该出不了C++基础语法的范围。<br /><br />然后你可以看一些提高类的书,比如说《C++ Primer中文版》。<br /><br />然后你重点看看用C++处理字符串。char*、wchar_t*、TCHAT、std::string、std::wstring、CString、LPSTR、LPCSTR、BSTR、LPCWSTR……用这些数据类型怎么做字符串处理,Unicode(UTF-8、UTF-16)相互之间怎么转换,MBCS,SBCS、DBCS是怎么回事。<br /><br />然后你再看看《Windows核心编程》。不要被这本书的名字给吓到了,它主要讲Windows API的、线程机制的。<br /><br />然后再看看C++怎么操作数据库,怎么写dll、lib,怎么写COM,怎么写线程安全的COM。<br /><br />然后再看看Socket编程,这时候可以结合着看一下《TCP/IP原理及协议分析》,这本书分上、中、下三卷。按顺序看下来,你会成为TCP/IP协议方面的专家。<br /><br />然后你可以看看《Linux内核源代码情景分析》,了解一下Linux操作系统是怎么工作的。这将不仅有利于你认识Linux,还有助于你提高对其他操作系统(比如说Windows、FreeBSD、MacOS)的认识。<br /><br />这时候,你应该可以成为一个成熟的C++程序员了。结合一些项目经验,税后1万的工作任你挑。找一个行业去做,比如说网络安全行业,干上两年,年薪二十万应该是有指望的。干到5年以上,年薪50万对你来说是正常的事情。</p>
</div>C++程序设计从零开始之何谓变量savagertnullhttp://liyanblog.cn/articles/2012/09/19/1348046197962.html2012-09-19T17:16:38+08:00<div class="content-head clearfix">
<h2 class="title content-title">C++程序设计从零开始之何谓变量</h2>
</div>
<div id="content" class="content mod-cs-content text-content clearfix">
<p> 本篇说明内容是C++中的关键,基本大部分人对于这些内容都是昏的,但这些内容又是编程的基础中的基础,必须详细说明。</p>
<p> 数字表示</p>
<p> 数学中,数只有数值大小的不同,绝不会有数值占用空间的区别,即数学中的数是逻辑上的一个概念,但电脑不是。考虑算盘,每个算盘上有很多列算子,每列都分成上下两排算子。上排算子有2个,每个代表5,下排算子有4个,每个代表1(这并不重要)。因此算盘上的每列共有6个算子,每列共可以表示0到14这15个数字(因为上排算子的可能状态有0到2个算子有效,而下排算子则可能有0到4个算子有效,故为3×5=15种组合方式)。</p>
<p> 上面的重点就是算盘的每列并没有表示0到14这15个数字,而是每列有15种状态,因此被人利用来表示数字而已(这很重要)。由于算盘的每列有15个状态,因此用两列算子就可以有15×15=225个状态,因此可以表示0到224。阿拉伯数字的每一位有0到9这10个图形符号,用两个阿拉伯数字图形符号时就能有10×10=100个状态,因此可以表示0到99这100个数。</p>
<p> 这里的算盘其实就是一个基于15进制的记数器(可以通过维持一列算子的状态来记录一位数字),它的一列算子就相当于一位阿拉伯数字,每列有15种状态,故能表示从0到14这15个数字,超出14后就必须通过进位来要求另一列算子的加入以表示数字。电脑与此一样,其并不是数字计算机,而是电子计算机,电脑中通过一根线的电位高低来表示数字。一根线中的电位规定只有两种状态——高电位和低电位,因此电脑的数字表示形式是二进制的。</p>
<p> 和上面的算盘一样,一根电线只有两个状态,当要表示超出1的数字时,就必须进位来要求另一根线的加入以表示数字。所谓的32位电脑就是提供了32根线(被称作数据总线)来表示数据,因此就有2的32次方那么多种状态。而16根线就能表示2的16次方那么多种状态。<br />所以,电脑并不是基于二进制数,而是基于状态的变化,只不过这个状态可以使用二进制数表示出来而已。即电脑并不认识二进制数,这是下面“类型”一节的基础。</p>
<p> 内存</p>
<p> 内存就是电脑中能记录数字的硬件,但其存储速度很快(与硬盘等低速存储设备比较),又不能较长时间保存数据,所以经常被用做草稿纸,记录一些临时信息。</p>
<p> 前面已经说过,32位计算机的数字是通过32根线上的电位状态的组合来表示的,因此内存能记录数字,也就是能维持32根线上各自的电位状态(就好象算盘的算子拨动后就不会改变位置,除非再次拨动它)。不过依旧考虑上面的算盘,假如一个算盘上有15列算子,则一个算盘能表示15的15次方个状态,是很大的数字,但经常实际是不会用到变化那么大的数字的,因此让一个算盘只有两列算子,则只能表示225个状态,当数字超出时就使用另一个或多个算盘来一起表示。</p>
<p> 上面不管是2列算子还是15列算子,都是算盘的粒度,粒度分得过大造成不必要的浪费(很多列算子都不使用),太小又很麻烦(需要多个算盘)。电脑与此一样。2的32次方可表示的数字很大,一般都不会用到,如果直接以32位存储在内存中势必造成相当大的资源浪费。于是如上,规定内存的粒度为8位二进制数,称为一个内存单元,而其大小称为一个字节(Byte)。就是说,内存存储数字,至少都会记录8根线上的电位状态,也就是2的8次方共256种状态。所以如果一个32位的二进制数要存储在内存中,就需要占据4个内存单元,也就是4个字节的内存空间。</p>
<p> 我们在纸上写字,是通过肉眼判断出字在纸上的相对横坐标和纵坐标以查找到要看的字或要写字的位置。同样,由于内存就相当于草稿纸,因此也需要某种定位方式来定位,在电脑中,就是通过一个数字来定位的。这就和旅馆的房间号一样,内存单元就相当于房间(假定每个房间只能住一个人),而前面说的那个数字就相当于房间号。为了向某块内存中写入数据(就是使用某块内存来记录数据总线上的电位状态),就必须知道这块内存对应的数字,而这个数字就被称为地址。而通过给定的地址找到对应的内存单元就称为寻址。</p>
<p> 因此地址就是一个数字,用以唯一标识某一特定内存单元。此数字一般是32位长的二进制数,也就可以表示4G个状态,也就是说一般的32位电脑都具有4G的内存空间寻址能力,即电脑最多装4G的内存,如果电脑有超过4G的内存,此时就需要增加地址的长度,如用40位长的二进制数来表示。</p>
<p> 类型</p>
<p> 在本系列最开头时已经说明了何谓编程,而刚才更进一步说明了电脑其实连数字都不认识,只是状态的记录,而所谓的加法也只是人为设计那个加法器以使得两个状态经过加法器的处理而生成的状态正好和数学上的加法的结果一样而已。这一切的一切都只说明一点:电脑所做的工作是什么,全视使用的人以为是什么。</p>
<p> 因此为了利用电脑那很快的“计算”能力(实际是状态的变换能力),人为规定了如何解释那些状态。为了方便其间,对于前面提出的电位的状态,我们使用1位二进制数来表示,则上面提出的状态就可以使用一个二进制数来表示,而所谓的“如何解释那些状态”就变成了如何解释一个二进制数。</p>
<p> C++是高级语言,为了帮助解释那些二进制数,提供了类型这个概念。类型就是人为制订的如何解释内存中的二进制数的协议。C++提供了下面的一些标准类型定义。</p>
<p> ·signed char 表示所指向的内存中的数字使用补码形式,表示的数字为-128到+127,长度为1个字节</p>
<p> ·unsigned char 表示所指向的内存中的数字使用原码形式,表示的数字为0到255,长度为1个字节</p>
<p> ·signed short 表示所指向的内存中的数字使用补码形式,表示的数字为–32768到+32767,长度为2个字节</p>
<p> ·unsigned short 表示所指向的内存中的数字使用原码形式,表示的数字为0到65535,长度为2个字节</p>
<p> ·signed long 表示所指向的内存中的数字使用补码形式,表示的数字为-2147483648到+2147483647,长度为4个字节</p>
<p> ·unsigned long 表示所指向的内存中的数字使用原码形式,表示的数字为0到4294967295,长度为4个字节</p>
<p> ·signed int 表示所指向的内存中的数字使用补码形式,表示的数字则视编译器。如果编译器编译时被指明编译为在16位操作系统上运行,则等同于signed short;如果是编译为32位的,则等同于signed long;如果是编译为在64位操作系统上运行,则为8个字节长,而范围则如上一样可以自行推算出来。</p>
<p> ·unsigned int 表示所指向的内存中的数字使用原码形式,其余和signed int一样,表示的是无符号数。</p>
<p> ·bool 表示所指向的内存中的数字为逻辑值,取值为false或true。长度为1个字节。</p>
<p> ·float 表示所指向的内存按IEEE标准进行解释,为real*4,占用4字节内存空间,等同于上篇中提到的单精度浮点数。</p>
<p> ·double 表示所指向的内存按IEEE标准进行解释,为real*8,可表示数的精度较float高,占用8字节内存空间,等同于上篇提到的双精度浮点数。</p>
<p> ·long double 表示所指向的内存按IEEE标准进行解释,为real*10,可表示数的精度较double高,但在为32位Windows操作系统编写程序时,仍占用8字节内存空间,等效于double,只是如果CPU支持此类浮点类型则还是可以进行这个精度的计算。</p>
<p>标准类型不止上面的几个,后面还会陆续提到。</p>
<p> </p>
<p> 上面的长度为2个字节也就是将两个连续的内存单元中的数字取出并合并在一起以表示一个数字,这和前面说的一个算盘表示不了的数字,就进位以加入另一个算盘帮助表示是同样的道理。</p>
<p> 上面的signed关键字是可以去掉的,即char等同于signed char,用以简化代码的编写。但也仅限于signed,如果是unsigned char,则在使用时依旧必须是unsigned char。</p>
<p> 现在应该已经了解上篇中为什么数字还要分什么有符号无符号、长整型短整型之类的了,而上面的short、char等也都只是长度不同,这就由程序员自己根据可能出现的数字变化幅度来进行选用了。</p>
<p> 类型只是对内存中的数字的解释,但上面的类型看起来相对简单了点,且语义并不是很强,即没有什么特殊意思。为此,C++提供了自定义类型,也就是后继文章中将要说明的结构、类等。</p>
<p> 变量</p>
<p> 在本系列的第一篇中已经说过,电脑编程的绝大部分工作就是操作内存,而上面说了,为了操作内存,需要使用地址来标识要操作的内存块的首地址(上面的long表示连续的4个字节内存,其第一个内存单元的地址称作这连续4个字节内存块的首地址)。为此我们在编写程序时必须记下地址。</p>
<p> 做5+2/3-5*2的计算,先计算出2/3的值,写在草稿纸上,接着算出5*2的值,又写在草稿纸上。为了接下来的加法和减法运算,必须能够知道草稿纸上的两个数字哪个是2/3的值哪个是5*2的值。人就是通过记忆那两个数在纸上的位置来记忆的,而电脑就是通过地址来标识的。但电脑只会做加减乘除,不会去主动记那些2/3、5*2的中间值的位置,也就是地址。因此程序员必须完成这个工作,将那两个地址记下来。<br />问题就是这里只有两个值,也许好记一些,但如果多了,人是很难记住哪个地址对应哪个值的,但人对符号比对数字要敏感得多,即人很容易记下一个名字而不是一个数字。为此,程序员就自己写了一个表,表有两列,一列是“2/3的值”,一列是对应的地址。如果式子稍微复杂点,那么那个表可能就有个二三十行,而每写一行代码就要去翻查相应的地址,如果来个几万行代码那是人都不能忍受。</p>
<p> C++作为高级语言,很正常地提供了上面问题的解决之道,就是由编译器来帮程序员维护那个表,要查的时候是编译器去查,这也就是变量的功能。</p>
<p> 变量是一个映射元素。上面提到的表由编译器维护,而表中的每一行都是这个表的一个元素(也称记录)。表有三列:变量名、对应地址和相应类型。变量名是一个标识符,因此其命名规则完全按照上一篇所说的来。当要对某块内存写入数据时,程序员使用相应的变量名进行内存的标识,而表中的对应地址就记录了这个地址,进而将程序员给出的变量名,一个标识符,映射成一个地址,因此变量是一个映射元素。而相应类型则告诉编译器应该如何解释此地址所指向的内存,是2个连续字节还是4个?是原码记录还是补码?而变量所对应的地址所标识的内存的内容叫做此变量的值。</p>
<p> 有如下的变量解释:“可变的量,其相当于一个盒子,数字就装在盒子里,而变量名就写在盒子外面,这样电脑就知道我们要处理哪一个盒子,且不同的盒子装不同的东西,装字符串的盒子就不能装数字。”上面就是我第一次学习编程时,书上写的(是BASIC语言)。对于初学者也许很容易理解,也不能说错,但是造成的误解将导致以后的程序编写地千疮百孔。</p>
<p> 上面的解释隐含了一个意思——变量是一块内存。这是严重错误的!如果变量是一块内存,那么C++中著名的引用类型将被弃置荒野。变量实际并不是一块内存,只是一个映射元素,这是致关重要的。</p>
<p> 内存的种类</p>
<p> 前面已经说了内存是什么及其用处,但内存是不能随便使用的,因为操作系统自己也要使用内存,而且现在的操作系统正常情况下都是多任务操作系统,即可同时执行多个程序,即使只有一个CPU。因此如果不对内存访问加以节制,可能会破坏另一个程序的运作。比如我在纸上写了2/3的值,而你未经我同意且未通知我就将那个值擦掉,并写上5*2的值,结果我后面的所有计算也就出错了。</p>
<p> 因此为了使用一块内存,需要向操作系统申请,由操作系统统一管理所有程序使用的内存。所以为了记录一个long类型的数字,先向操作系统申请一块连续的4字节长的内存空间,然后操作系统就会在内存中查看,看是否还有连续的4个字节长的内存,如果找到,则返回此4字节内存的首地址,然后编译器编译的指令将其记录在前面提到的变量表中,最后就可以用它记录一些临时计算结果了。</p>
<p> 上面的过程称为要求操作系统分配一块内存。这看起来很不错,但是如果只为了4个字节就要求操作系统搜索一下内存状况,那么如果需要100个临时数据,就要求操作系统分配内存100次,很明显地效率低下(无谓的99次查看内存状况)。因此C++发现了这个问题,并且操作系统也提出了相应的解决方法,最后提出了如下的解决之道。</p>
<p> 栈(Stack) 任何程序执行前,预先分配一固定长度的内存空间,这块内存空间被称作栈(这种说法并不准确,但由于实际涉及到线程,在此为了不将问题复杂化才这样说明),也被叫做堆栈。那么在要求一个4字节内存时,实际是在这个已分配好的内存空间中获取内存,即内存的维护工作由程序员自己来做,即程序员自己判断可以使用哪些内存,而不是操作系统,直到已分配的内存用完。</p>
<p> 很明显,上面的工作是由编译器来做的,不用程序员操心,因此就程序员的角度来看什么事情都没发生,还是需要像原来那样向操作系统申请内存,然后再使用。</p>
<p> 但工作只是从操作系统变到程序自己而已,要维护内存,依然要耗费CPU的时间,不过要简单多了,因为不用标记一块内存是否有人使用,而专门记录一个地址。此地址以上的内存空间就是有人正在使用的,而此地址以下的内存空间就是无人使用的。之所以是以下的空间为无人使用而不是以上,是当此地址减小到0时就可以知道堆栈溢出了(如果你已经有些基础,请不要把0认为是虚拟内存地址,关于虚拟内存将会在《C++从零开始(十八)》中进行说明,这里如此解释只是为了方便理解)。而且CPU还专门对此法提供了支持,给出了两条指令,转成汇编语言就是push和pop,表示压栈和出栈,分别减小和增大那个地址。</p>
<p> 而最重要的好处就是由于程序一开始执行时就已经分配了一大块连续内存,用一个变量记录这块连续内存的首地址,然后程序中所有用到的,程序员以为是向操作系统分配的内存都可以通过那个首地址加上相应偏移来得到正确位置,而这很明显地由编译器做了。因此实际上等同于在编译时期(即编译器编译程序的时候)就已经分配了内存(注意,实际编译时期是不能分配内存的,因为分配内存是指程序运行时向操作系统申请内存,而这里由于使用堆栈,则编译器将生成一些指令,以使得程序一开始就向操作系统申请内存,如果失败则立刻退出,而如果不退出就表示那些内存已经分配到了,进而代码中使用首地址加偏移来使用内存也就是有效的),但坏处也就是只能在编译时期分配内存。</p>
<p> 堆(Heap) 上面的工作是编译器做的,即程序员并不参与堆栈的维护。但上面已经说了,堆栈相当于在编译时期分配内存,因此一旦计算好某块内存的偏移,则这块内存就只能那么大,不能变化了(如果变化会导致其他内存块的偏移错误)。比如要求客户输入定单数据,可能有10份定单,也可能有100份定单,如果一开始就定好了内存大小,则可能造成不必要的浪费,又或者内存不够。</p>
<p> 为了解决上面的问题,C++提供了另一个途径,即允许程序员有两种向操作系统申请内存的方式。前一种就是在栈上分配,申请的内存大小固定不变。后一种是在堆上分配,申请的内存大小可以在运行的时候变化,不是固定不变的。</p>
<p> 那么什么叫堆?在Windows操作系统下,由操作系统分配的内存就叫做堆,而栈可以认为是在程序开始时就分配的堆(这并不准确,但为了不复杂化问题,故如此说明)。因此在堆上就可以分配大小变化的内存块,因为是运行时期即时分配的内存,而不是编译时期已计算好大小的内存块。</p>
<p>变量的定义</p>
<p> </p>
<p> 上面说了那么多,你可能看得很晕,毕竟连一个实例都没有,全是文字,下面就来帮助加深对上面的理解。</p>
<p> 定义一个变量,就是向上面说的由编译器维护的变量表中添加元素,其语法如下:</p>
<p>long a;</p>
<p> 先写变量的类型,然后一个或多个空格或制表符(\t)或其它间隔符,接着变量的名字,最后用分号结束。要同时定义多个变量,则各变量间使用逗号隔开,如下:</p>
<p>long a, b, c; unsigned short e, a_34c;</p>
<p> 上面是两条变量定义语句,各语句间用分号隔开,而各同类型变量间用逗号隔开。而前面的式子5+2/3-5*2,则如下书写。</p>
<p>long a = 2/3, b = 5*2; long c = 5 + a – b;</p>
<p> 可以不用再去记那烦人的地址了,只需记着a、b这种简单的标识符。当然,上面的式子不一定非要那么写,也可以写成:long c = 5 + 2 / 3 – 5 * 2; 而那些a、b等中间变量编译器会自动生成并使用(实际中编译器由于优化的原因将直接计算出结果,而不会生成实际的计算代码)。</p>
<p> 下面就是问题的关键,定义变量就是添加一个映射。前面已经说了,这个映射是将变量名和一个地址关联,因此在定义一个变量时,编译器为了能将变量名和某个地址对应起来,帮程序员在前面提到的栈上分配了一块内存,大小就视这个变量类型的大小。如上面的a、b、c的大小都是4个字节,而e、a_34c的大小都是2个字节。</p>
<p> 假设编译器分配的栈在一开始时的地址是1000,并假设变量a所对应的地址是1000-56,则b所对应的地址就是1000-60,而c所对应的就是1000-64,e对应的是1000-66,a_34c是1000-68。如果这时b突然不想是4字节了,而希望是8字节,则后续的c、e、a_34c都将由于还是原来的偏移位置而使用了错误的内存,这也就是为什么栈上分配的内存必须是固定大小。</p>
<p> 考虑前面说的红色文字:“变量实际并不是一块内存,只是一个映射元素”。可是只要定义一个变量,就会相应地得到一块内存,为什么不说变量就是一块内存?上面定义变量时之所以会分配一块内存是因为变量是一个映射元素,需要一个对应地址,因此才在栈上分配了一块内存,并将其地址记录到变量表中。但是变量是可以有别名的,即另一个名字。这个说法是不准确的,应该是变量所对应的内存块有另一个名字,而不止是这个变量的名字。</p>
<p> 为什么要有别名?这是语义的需要,表示既是什么又是什么。比如一块内存,里面记录了老板的信息,因此起名为Boss,但是老板又是另一家公司的行政经理,故变量名应该为Manager,而在程序中有段代码是老板的公司相关的,而另一段是老板所在公司相关的,在这两段程序中都要使用到老板的信息,那到底是使用Boss还是Manager?其实使用什么都不会对最终生成的机器代码产生什么影响,但此处出于语义的需要就应该使用别名,以期从代码上表现出所编写程序的意思。</p>
<p> 在C++中,为了支持变量别名,提供了引用变量这个概念。要定义一个引用变量,在定义变量时,在变量名的前面加一个“&”,如下书写:</p>
<p>long a; long &a1 = a, &a2 = a, &a3 = a2;</p>
<p> 上面的a1、a2、a3都是a所对应的内存块的别名。这里在定义变量a时就在栈上分配了一块4字节内存,而在定义a1时却没有分配任何内存,直接将变量a所映射的地址作为变量a1的映射地址,进而形成对定义a时所分配的内存的别名。因此上面的Boss和Manager,应该如下(其中Person是一个结构或类或其他什么自定义类型,这将在后继的文章中陆续说明):</p>
<p>Person Boss; Person &Manager = Boss;</p>
<p> 由于变量一旦定义就不能改变(指前面说的变量表里的内容,不是变量的值),直到其被删除,所以上面在定义引用变量的时候必须给出欲别名的变量以初始化前面的变量表,否则编译器编译时将报错。</p>
<p> 现在应该就更能理解前面关于变量的红字的意思了。并不是每个变量定义时都会分配内存空间的。而关于如何在堆上分配内存,将在介绍完指针后予以说明,并进而说明上一篇遗留下来的关于字符串的问题。</p>
</div>50条重要的C++学习建议savagertnullhttp://liyanblog.cn/articles/2012/09/19/1348046176608.html2012-09-19T17:16:16+08:00<div class="content-head clearfix">
<h2 class="title content-title">50条重要的C++学习建议</h2>
</div>
<div id="content" class="content mod-cs-content text-content clearfix">
<p> 1.把C++当成一门新的语言学习(和C没啥关系!真的);</p>
<p> 2.看《Thinking In C++》,不要看《C++变成死相》(C++编程思想,翻译的非常差);</p>
<p> 3.看《The C++ Programming Language》(这本东西有影印板的)和《Inside The C++ Object Model》 ,不要因为他们很难而 我们自己是初学者所以就不看;</p>
<p> 4.不要被VC、BCB、BC、MC、TC等词汇所迷惑——他们都是集成开发环境,而我们要学的是一门语言;</p>
<p> 5.不要放过任何一个看上去很简单的小编程问题——他们往往并不那么简单,或者可以引伸出很多知识点;</p>
<p> 6.会用Visual C++,并不说明你会C++;</p>
<p> 7.学class并不难,template、STL、generic programming也不过如此——难的是长期坚持实践和不遗余力的博览群书;</p>
<p> 8.如果不是天才的话,想学编程就不要想玩游戏——你以为你做到了,其实你的C++水平并没有和你通关的能力一起变高——其实可以时刻记住:学C++是为了编游戏的;</p>
<p> 9.看Visual C++的书,是学不了C++语言的;</p>
<p> 10.浮躁的人容易说:XX语言不行了,应该学YY;——是你自己不行了吧!?</p>
<p> 11.浮躁的人容易问:我到底该学什么;——别问,学就对了;</p>
<p> 12.浮躁的人容易问:XX有钱途吗;——建议你去抢银行;</p>
<p> 13.浮躁的人容易说:我要中文版!我英文不行!——不行?学呀!</p>
<p> 14.浮躁的人容易问:XX和YY哪个好;——告诉你吧,都好——只要你学就行;</p>
<p> 15.浮躁的人分两种:a)只观望而不学的人;b)只学而不坚持的人;</p>
<p> 16.把时髦的技术挂在嘴边,还不如把过时的技术记在心里;</p>
<p> 17.C++不仅仅是支持面向对象的程序设计语言;</p>
<p> 18.学习编程最好的方法之一就是阅读源代码;</p>
<p> 19.在任何时刻都不要认为自己手中的书已经足够了;</p>
<p> 20.请阅读《The Standard C++ Bible》(中文版:标准C++宝典),掌握C++标准;</p>
<p> 21.看得懂的书,请仔细看;看不懂的书,请硬着头皮看;</p>
<p> 22.别指望看第一遍书就能记住和掌握什么——请看第二遍、第三遍;</p>
<p> 23.请看《Effective C++》(这本书刚出,也是候sir翻译的)和《More Effective C++ 》以及《Exceptional C++》;</p>
<p> 24.不要停留在集成开发环境的摇篮上,要学会控制集成开发环境,还要学会用命令行方式处理程序;</p>
<p> 25.和别人一起讨论有意义的C++知识点,而不是争吵XX行不行或者YY与ZZ哪个好</p>
<p> 26.请看《程序设计实践》,并严格的按照其要求去做;</p>
<p> </p>
<p> 27.不要因为C和C++中有一些语法和关键字看上去相同,就认为它们的意义和作用完全一样;</p>
<p> 28.C++绝不是所谓的C的“扩充”——如果C++一开始就起名叫Z语言,你一定不会把C和Z 语言联系得那么紧密;</p>
<p> 29.请不要认为学过XX语言再改学C++会有什么问题——你只不过又在学一门全新的语言而已;</p>
<p> 30.读完了《Inside The C++ Object Model》以后再来认定自己是不是已经学会了C++;</p>
<p> 31.学习编程的秘诀是:编程,编程,再编程;</p>
<p> 32.请留意下列书籍:《C++面向对象高效编程(C++ Effective Object-Oriented Software Construction)》《面向对象软件构造(Object-Oriented Software Construction)》《设计模式(Design Patterns)》《The Art of Computer Programming》;</p>
<p> 33.记住:面向对象技术不只是C++专有的;</p>
<p> 34.请把书上的程序例子亲手输入到电脑上实践,即使配套光盘中有源代码;</p>
<p> 35.把在书中看到的有意义的例子扩充;</p>
<p> 36.请重视C++中的异常处理技术,并将其切实的运用到自己的程序中;</p>
<p> 37.经常回顾自己以前写过的程序,并尝试重写,把自己学到的新知识运用进去;</p>
<p> 38.不要漏掉书中任何一个练习题——请全部做完并记录下解题思路;</p>
<p> 39.C++语言和C++的集成开发环境要同时学习和掌握;</p>
<p> 40.既然决定了学C++,就请坚持学下去,因为学习程序设计语言的目的是掌握程序设计技术,而程序设计技术是跨语言的;</p>
<p> 41.就让C++语言的各种平台和开发环境去激烈的竞争吧,我们要以学习C++语言本身为主;</p>
<p> 42.当你写C++程序写到一半却发现自己用的方法很拙劣时,请不要马上停手;请尽快将余下的部分粗略的完成以保证这个设计的完整性,然后分析自己的错误并重新设计和编写(参见43);</p>
<p> 43.别心急,设计C++的class确实不容易;自己程序中的class和自己的class设计水平是在不断的编程实践中完善和发展的;</p>
<p> 44.决不要因为程序“很小”就不遵循某些你不熟练的规则——好习惯是培养出来的,而不是一次记住的;</p>
<p> 45.每学到一个C++难点的时候,尝试着对别人讲解这个知识点并让他理解——你能讲清楚才说明你真的理解了;</p>
<p> 46.记录下在和别人交流时发现的自己忽视或不理解的知识点;</p>
<p> 47.请不断的对自己写的程序提出更高的要求,哪怕你的程序版本号会变成Version 100.XX;</p>
<p> 48.保存好你写过的所有的程序——那是你最好的积累之一;</p>
<p> 49.请不要做浮躁的人;</p>
<p> 50.请热爱C++!</p>
</div>C++编程:C++的底层机制savagertnullhttp://liyanblog.cn/articles/2012/09/19/1348046153248.html2012-09-19T17:15:53+08:00<div class="content-head clearfix">
<h2 class="title content-title">C++编程:C++的底层机制</h2>
</div>
<div id="content" class="content mod-cs-content text-content clearfix"> C++为我们所提供的各种存取控制仅仅是在编译阶段给我们的限制,也就是说是编译器确保了你在完成任务之前的正确行为,如果你的行为不正确,那么你休想构造出任何可执行程序来。
<p> 但如果真正到了产生可执行代码阶段,无论是c,c++,还是pascal,大家都一样,你认为c和c++编译器产生的机器代码会有所不同吗,你认为c++产生的机器代码会有访问限制吗?那么你错了。什么const,private,统统没有(const变量或许会放入只读数据段),它不会再给你任何的限制,你可以利用一切内存修改工具或者是自己写一个程序对某一进程空间的某一变量进行修改,不管它在你的印象中是private,还是public,对于此时的你来说都一样,想怎样便怎样。</p>
<p> 另外,你也不要为c++所提供的什么晚期捆绑等机制大呼神奇,它也仅仅是在所产生的代码中多加了几条而已,它远没有你想象的那么智能,所有的工作都是编译器帮你完成,真正到了执行的时候,计算机会完全按照编译器产生的代码一丝不苟的执行。</p>
<p> (以下的反汇编代码均来自visial c++ 7.0)</p>
<p><strong> 一.让我们从变量开始-----并非你想象的那么简单</strong></p>
<p> 变量是什么,变量就是一个在程序执行过程中可以改变的量。换一个角度,变量是一块内存区域的名字,它就代表这块内存区域,当我们对变量进行修改的时候,会引起内存区域中内容的改变。但是你若是学习过汇编或是计算机组成原理,那么你就会清楚对于一块内存区域来说,根本就不存在什么名字,它所仅有的标志就是他的地址,因此我们若想修改一块内存区域的内容,只有知道他的地址方能实现。看来所谓的变量一说只不过是编译器给我们进行的一种抽象,让我们不必去了解更多的细节,降低我们的思维跨度而已。例如下面这条语句:</p>
<p> int a=10;</p>
<p> 按照我们的思维习惯来讲,就是“存在一个变量a,它的值是10”,一切都显得那么的自然。我们不必去在乎什么所谓的地址以及其他的一些细节。然而在这条语句的底层实现中,a已经不能算是一个变量了,它仅仅是一个标记,代表一个地址的标记:</p>
<p> mov dWord ptr[a],0Ah;</p>
<p> 怎么样,这条语句不像上面那条易于接受吧,因为它需要了解更多的细节,你几乎不能得到编译器的任何帮助,一切思维上的跨越必须由你自己完成。这条语句应该解释为“把10写入以a为地址的内存区域”。你说什么?a有些像指针?对,的确像,但还不是,只不过他们的过程似乎是类似的。这里所说的跨越实际上就是从一个现实问题到具体地址以及内存区域的跨越。</p>
<p><strong> 二.引用:你可以拥有引用,但编译器仅拥有指针(地址)</strong></p>
<p> 看过了第一条,你一定对编译器的工作有了一定的了解,实际上编译器就是程序员与底层之间的一个转换层,它把一个高级语言代码转换为低级语言代码,一个编译器完成的转换跨度越大,那么它也就会越复杂,因为程序员的工作都由他代为完成了。C++编译器必然比汇编编译器复杂就是这个道理。如果我问你引用和指针是一样的吗?你或许会说当然不一样了,指针容易产生不安全的因素,引用却不会,真的不会吗?我们来看下面这段代码:</p>
<p> int *e=new int(10);</p>
<p> int &f=*e;</p>
<p> delete e;</p>
<p> f=30;</p>
<p> 你认为上面这段代码怎么样,我感觉就不很安全,它和指针有相同的隐患。因为它所引用的内存区域就不合法。</p>
<p> 我个人认为,所谓的引用其实就是一种指针,只不过二者的接口并不相同,引用的接口有一定的限制。指针可以一对多,而引用却只能一对一,即&refer不能被改变,但却并不能说一对一就是安全的,只不过危险的系数降低罢了。引用比指针更容易控制</p>
<p>下面来说说指针,曾经有过汇编经验的人一定会说,恩,指针的某些地方有些像汇编,尤其是那个“*”,怎么就那么像汇编中的“[]”啊。的确,它也涵盖了一个寻址的过程。看来指针的确是个比较低级的东西。然而引用却并不那么直接,虽然程序员用起来方便安全了许多。但是你要清楚,只有你可以拥有引用,编译器可没有这个工具,计算机并不认识这个东西。因此,它的底层机制实际上是和指针一样的。不要相信只有一块内存拷贝,不要认为引用可以为你节省一个指针的空间,因为这一切不会发生,编译器还是会把引用解释为指针。不管你相不相信,请看下面这段代码:</p>
<p> </p>
<p> int& b=a;</p>
<p> lea eax,[a];</p>
<p> mov dword ptr[b],eax;把a的地址赋给地址为b的一块内存</p>
<p> b=50;</p>
<p> mov eax,dword ptr[b];</p>
<p> mov dword ptr[eax],32h;</p>
<p> int *d=&a;</p>
<p> lea eax,[a];</p>
<p> mov dword ptr[d],eax</p>
<p> *d=60;</p>
<p> mov eax,dword ptr[d]</p>
<p> mov dword ptr[eax],3ch;</p>
<p> 以上的代码均来自具体的编译器,怎么样,相信了吧,好,让我再来做一个或许不怎么恰当的比拟,你一定编过有关线性表和栈的程序吧,线性表是一个非常灵活的数据结构,在他上面有许多的操作,然而栈呢,它是一个限制性操作的线性表,它的底层操作实际上是由线性表操作实现的。就好比stack与vector的关系,因此指针和引用的关系就好比线性表和栈的关系,引用也就是受限的指针,它对外的接口和指针虽然并不一样,但底层是相同的。</p>
<p> 下面再来看看引用的一个重要用途,作为函数的参数传递的时候是怎样的情形:</p>
<p> void swapr(int &a, int &b);</p>
<p> void swapr(int* a, int *b);</p>
<p> int a=10;</p>
<p> int b=20;</p>
<p> swapr(a, b);</p>
<p> lea eax,[a];</p>
<p> push eax; //把a的地址压入堆栈</p>
<p> lea ecx,[b];</p>
<p> push ecx;</p>
<p> call swapr;</p>
<p> swapr(&a, &b);</p>
<p> lea eax,[a];</p>
<p> push eax;</p>
<p> lea ecx,[b];</p>
<p> push ecx;</p>
<p> call swapr;</p>
<p> 怎么样,用引用和指针传递参数无论是在效率上还是在空间上都是完全一样的,如果妄想不传入地址就修改实参的值,简直就是天方夜谭,这就说明引用的本质就是指针。毕竟它们的行为都太相似了,如果不是这样,你还有什么方法去实现引用吗?记住,引用只不过是编译器为你提供的一个有用且安全的工具,对于机器代码可无法表示它,它把指针一对多的缺点去除,禁止了你的不安全的操作。但回到问题的本源,他们没有任何区别。</p>
</div>汇编简介savagertnullhttp://liyanblog.cn/articles/2012/09/19/1348046135898.html2012-09-19T17:15:35+08:00<div class="content-head clearfix">
<h2 class="title content-title">汇编简介</h2>
</div>
<div id="content" class="content mod-cs-content text-content clearfix"><a name="1"></a><br />汇编语合中,用助记符(Memoni)代替操作码,用地址符号(Symbol)或标号(Label)代替地址码。这样用符号代替机器语言的二进制码,就把机器语言变成了汇编语言。于是汇编语言亦称为符号语言。<br /><br />用汇编语言编写的程序,机器不能直接识别,要由一种程序将汇编语言翻译成机器语言,这种起翻译作用的程序叫汇编程序,汇编程序是系统软件中语言处理的系统软件。汇编程序把汇编语言翻译成机器语言的过程称为汇编。<br /><br />汇编语言比机器语言易于读写、易于调试和修改,同时也具有机器语言执行速度快,占内存空间少等优点,但在编写复杂程序时具有明显的局限性,汇编语言依赖于具体的机型,不能通用,也不能在不同机型之间移植。<br /><br />是能完成一定任务的机器指令的集合。 <br /><br />常说汇编语言过时,是低级语言,并不是说汇编语言要被弃之,相反,汇编语言仍然是程序员必须了解的语言,在某些行业与领域,汇编是必不可少的,非它不可适用。只是,现在计算机最大的领域为IT软件,也是我们常说的 Windows 编程,在熟练的程序员手里,使用汇编语言编写的程序,运行效率与性能比其它语言写的程序是成倍的优秀,但是代价是需要更长的时间来优化,如果对计算机原理及编程基础的扎实,实在是得不尝失,对比现在的软件开发,已经是市场化的软件行业,加上高级语言的优秀与跨平台,一个公司不可以让一个团队使用汇编语言来编写所有的东西,花上几倍甚至几十倍的时间,不如使用其它语言来完成,只要最终结果不比汇编语言编写的差太多,就能抢先一步完成,这是市场经济下的必然结果。<br /><br />但是,至今为止,还没有程序员敢断定汇编语言是不需要学的,一个不懂汇编语言的程序员,只是三流的程序,这是大部分人的共识,同时,技术精湛的汇编程序员,已经脱离软件开发,挤身于工业电子编程中,一个电子工程师,主要开发语言就中汇编,c语言使用只占极少部分,而电子开发工程师是千金难求,在一些工业公司,一个核心的电子工程师比其它任何职员待遇都高,对比起来,一般电子工程师待遇是程序员的十倍以上。这种情况是因为现在学习汇编的人虽然也不少,但是真正能学到精通的却不多,它难学,难用,适用范围小,虽然简单,但是过于灵活,学习过高级语言的人去学习汇编比一开始学汇编的人难得多,但是学过汇编的人学习高级语言却很容易,简从繁易,繁从简难。<br /><br />总之,汇编语言是程序员的必修语言。<br /><br />目前国内最好的汇编网站是:http://www.aogosoft.com 其站点aogo,就是一个在工业方面有所成就的工程师,有意者可多参考。<br />其次就是罗云彬的汇编站点:http://asm.yeah.net 这个大概是国内建站时间最长的汇编站点,其编写的《Windows下汇编语言程序设计》一书。是站长十几年的经验的集合,不妨看看。<br />熟悉指令,可以尝试破解,加强兴趣,参考看雪学院:http://www.pediy.com,国内最好的破解组织,其中看雪与众高手打造的破解书《加密 解密完全方案》非常有名。</div>动态链接库dll,静态链接库libsavagertnullhttp://liyanblog.cn/articles/2012/09/18/1347936141460.html2012-09-18T10:42:21+08:00<div class="content-head clearfix">
<h2 class="title content-title">动态链接库dll,静态链接库lib</h2>
</div>
<div id="content" class="content mod-cs-content text-content clearfix">
<p>目前以lib后缀的库有两种,一种为静态链接库(Static Libary,以下简称“静态库”),另一种为动态连接库(DLL,以下简称“动态库”)的导入库(Import Libary,以下简称“导入库”)。 <br /><br />静态库是一个或者多个obj文件的打包,所以有人干脆把从obj文件生成lib的过程称为Archive,即合并到一起。比如你链接一个静态库,如果其中有错,它会准确的找到是哪个obj有错,即静态lib只是壳子。 <br /><br />动态库一般会有对应的导入库,方便程序静态载入动态链接库,否则你可能就需要自己LoadLibary调入DLL文件,然后再手工GetProcAddress获得对应函数了。有了导入库,你只需要链接导入库后按照头文件函数接口的声明调用函数就可以了。 <br /><br />导入库和静态库的区别很大,他们实质是不一样的东西。静态库本身就包含了实际执行代码、符号表等等,而对于导入库而言,其实际的执行代码位于动态库中,导入库只包含了地址符号表等,确保程序找到对应函数的一些基本地址信息。</p>
<p> </p>
<p>DLL: <br />动态链接库 (DLL) 是作为共享函数库的可执行文件。动态链接提供了一种方法,使进程可以调用不属于其可执行代码的函数。函数的可执行代码位于一个 DLL 中,该 DLL 包含一个或多个已被编译、链接并与使用它们的进程分开存储的函数。DLL 还有助于共享数据和资源。多个应用程序可同时访问内存中单个 DLL 副本的内容。 <br /><br />动态链接与静态链接的不同之处在于它允许可执行模块(.dll 文件或 .exe 文件)仅包含在运行时定位 DLL 函数的可执行代码所需的信息。在静态链接中,链接器从静态链接库获取所有被引用的函数,并将库同代码一起放到可执行文件中。 <br /><br />使用动态链接代替静态链接有若干优点。DLL 节省内存,减少交换操作,节省磁盘空间,更易于升级,提供售后支持,提供扩展 MFC 库类的机制,支持多语言程序,并使国际版本的创建轻松完成。</p>
<p> </p>
<p>API 就是应用程序编程接口。它是能用来操作组件、应用程序或者操作系统的一组函数。典型的情况下,API 由一个或多个提供某种特殊功能的 DLL 组成。 <br /><br />DLL 是一个文件,其中包含了在 Microsoft? Windows? 下运行的任何应用程序都可调用的函数。运行时,DLL 中的函数动态地链接到调用它的应用程序中。无论有多少应用程序调用 DLL 中的某个函数,在磁盘上只有一个文件包含该函数,且只在它调入内存时才创建该 DLL。 <br /><br />您听到最多的 API 可能是 Windows API,它包括构成 Windows 操作系统的各种 DLL。每个 Windows 应用程序都直接或间接地与 Windows API 互动。Windows API 保证 Windows 下运行的所有应用程序的行为方式一致。 <br /><br />注意 随着 Windows 操作系统的发展,现已发布了几个版本的 Windows API。Windows 3.1 使用 Win16 API。Microsoft? Windows NT?、Windows 95 和 Windows 98 平台使用 Microsoft? Win32? API。 <br />除 Windows API 外,其他一些 API 也已发布。例如,邮件应用程序编程接口 (MAPI) 是一组可用于编写电子邮件应用程序的 DLL。 <br /><br />API 传统上是为开发 Windows 应用程序的 C 和 C++ 程序员编写的,但其他的编程语言(包括VBA)也可以调用 DLL 中的函数。因为大部分 DLL 主要是为 C 和 C++ 程序员编写和整理说明的,所以调用 DLL 函数的方法与调用 VBA 函数会有所不同。在使用 API 时必须了解如何给 DLL 函数传递参数。 <br /><br />警告 调用 Windows API 和 其他 DLL 函数可能会给您的应用程序带来不良影响。从自己的代码中直接调用 DLL 函数时,您绕过了 VBA 通常提供的一些安全机制。如果在定义或调用 DLL 函数时出现错误(所有程序员都不可避免),可能会在应用程序中引起应用程序错误(也称为通用性保护错误,或 GPF)。最好的解决办法是在运行代码以前保存该项目,并确保了解 DLL 函数调用的原理。</p>
<p> </p>
<p>LIB 创建标准库、导入库和导出文件,在生成 32 位程序时可将它们与 LINK 一起使用。LIB 从命令提示运行。 <br /><br />可在下列几种模式下使用 LIB: <br /><br />生成或修改 COFF 库 <br />将成员对象提取到文件中 <br />创建导出文件和导入库 <br />这些模式是互斥的;每次只能以一种模式使用 LIB。</p>
</div>C++的预处理 savagertnullhttp://liyanblog.cn/articles/2012/09/18/1347936088856.html2013-02-28T15:49:13+08:00<div class="content-head clearfix">
<h2 class="title content-title">C++的预处理</h2>
</div>
<div id="content" class="content mod-cs-content text-content clearfix"><br /> 在编译一个程序源文件时,C++编译器首先进行预处理。预处理的作用是通过执行源文件中包含的预处理指令将源文件转换为一个等价文件,常见的预处理指令有:文件包含,条件编译和宏替换。<br /> 图1示例了对一个简单源文件进行预处理的过程,包括:<br /> ★去掉注释,用一个空白字符代替。<br /> ★执行文件包含(#include)和条件编译指令(#ifdef等)。<br /> ★对源文件进行宏替换。<br /> 因为预处理只是对文本内容进行简单操作,它不可能针对C++语言的语法进行错误检查,这也导致它只能进行极少的错误检查。 图1 编译预处理的过程 <img src="http://blogimg.chinaunix.net/blog/upfile2/080403150332.jpg" alt="" /> <br /><br /><strong>预处理指令</strong><br />声明预处理指令的一般形式为:<br /># directive tokens<br />符号“#”必须是该行第一个非空白字符,但前面有空白符或退格符都可以,#与directive之间也可以有多个空白符,如下代码具有完全相同的效果:
<p>#define size 100<br /> #define size 100<br /> # define size 100</p>
<br />一般一行声明一条预处理指令,但不排除用多行声明一条指令,字符“\”用在一行的末尾表示下行仍然接着该行,比如下面两个声明是完全对等的:
<p>#define CheckError \<br /> if (error) \<br /> exit(1)<br /><br />#define CheckError if (error) exit(1)</p>
<br />预处理指令声明中出现的注释以及一行单独一个#符号的情况在预编译处理过程中都会被忽略掉。<br />表1是所有预处理指令和意义
<p>指令 意义<br />#define 定义宏<br />#undef 取消定义宏<br />#include 包含文件<br />#ifdef 其后的宏已定义时激活条件编译块<br />#ifndef 其后的宏未定义时激活条件编译块<br />#endif 中止条件编译块<br />#if 其后表达式非零时激活条件编译块<br />#else 对应#ifdef, #ifndef, 或 #if 指令<br />#elif #else 和 #if的结合<br />#line 改变当前行号或者文件名<br />#error 输出一条错误信息<br />#pragma 为编译程序提供非常规的控制流信息</p>
<br /><strong>宏定义</strong><br />#define指令定义宏,宏定义可分为两类:简单宏定义,带参数宏定义。<br />简单宏定义有如下一般形式:<br />#define 名字 替换文本<br />它指示预处理器将源文件中所有出现名字记号的地方都替换为替换文本,替换文本可以是任何字符序列,甚至可以为空(此时相当于删除掉文件中所有对应的名字)。<br />简单宏定义常用于定义常量符号,如:<br />#define size 512<br />#define word long<br />#define bytes sizeof(word)<br />因为宏定义对预编译指令行也有效,所以一个前面已经被定义的宏能被后来的宏嵌套定义(如上面的bytes定义用到了word)。对于下面这句代码:<br />word n = size * bytes;<br />它的宏扩展就是:<br />long n = 512 * sizeof(long);<br />使用简单宏定义定义常量符号起源于C语言,但在C++中,定义常量可以用const关键字,并且还附加类型检查的功能,因此C++中已经尽量避免使用宏定义来定义常量了。<br />带参数宏定义的一般形式为:<br />#define 名字(参数) 替换文本<br />其中参数是一个或多个用逗号分割的标识符;在“名字”和“(”之间不允许有空格,否则整个宏定义将退化为一个置换文本为“(参数) 替换文本”的简单宏定义。下例表示定义一个求两数中较大者的带参数宏Max。<br />#define Max(x,y) ((x) > (y) ? (x) : (y))<br />带参数宏的调用有点类似于函数调用,实参数目必须匹配形参。首先,宏的替换文本部分置换掉调用的代码,接着,替换文本部分的形参又被置换为相应的实参,这个过程叫做宏扩展。见下例:<br />n = Max (n - 2, k +6);<br />的宏扩展为:<br />n = (n - 2) > (k + 6) ? (n - 2) : (k + 6);<br />注意,宏扩展时有可能发生不预期的运算符优先级的变化,这时如果定义宏时将替换文本里出现的每个形参都用括号括起来就不会出现问题(如上述宏MAX所示)。<br />仔细考察带参数宏与函数调用的异同可以发现,由于宏工作在文本一层,相同功能的宏和函数调用产生的语义有时是不完全相同的,比如:<br />Max(++i, j)<br />扩展为<br />((++i) > (j) ? (++i) : (j))<br />可见i最后自增了两次,但相同功能的函数能够保证只自增一次。<br />带参数宏定义在C++中的使用同样也在减少,因为:1,C++的内联函数提供了和带参数宏同样高的代码执行效率,同时没有后者那样的语义歧义;2,C++模板提供了和带参数宏同样高的灵活性,还能够执行语法分析和类型检查。<br />最后讨论一点内容是宏能够被重定义,在重定义前,必须使用#undef指令取消原来的宏定义,#undef如果取消的是一个原本不存在的宏定义则视为无效。如:<br />#undef size<br />#define size 128<br />#undef Max<br /><br /><strong>引用操作符和拼接操作符</strong><br />预处理提供了两个特殊操作符操作宏内的参数。引用操作符(#)是一元的,后跟一个形参作为运算对象,它的作用是将该运算对象替换为带引号的字符串。 <br />如有一个调试打印宏检查指针是否为空,为空时输出警告信息:<br />#define CheckPtr(ptr) \<br />if ((ptr) == 0) cout << #ptr << " is zero!\n"<br />此时#操作符将表达式中的变量ptr当成字符串输出为警告信息的一部分。因此,如下的调用:<br />CheckPtr(tree->left);<br />扩展为:<br />if ((tree->left) == 0) cout << "tree->left" << " is zero!\n";<br />注意:如果按照下面这样定义宏<br />#define CheckPtr(ptr) \<br />if ((ptr) == 0) cout << "ptr is zero!\n"<br />是不会得到期望结果的,因为宏不能在字符串内部进行置换。<br />拼接操作符(##)是二元的,被用来连接宏中两个实际参数,比如,如下宏定义<br />#define internal(var) internal##var<br />如果执行<br />long internal(str);<br />则被扩展为:<br />long internalstr;<br />在一般编程时很少用到拼接操作符,但在编写编译器程序或源代码生成器时特别有用,因为它能轻易的构造出一组标识符。<br /><br />----------------------------------------------<br />版权所有:朱科 欢迎光临我的网站:www.goodsoft.cn,各位转贴别删,劳动成果啊<br />----------------------------------------------<br /><br /><strong>文件包含</strong><br />#include指令实现将一个文件包含在另外一个中,如:<br />#include "constants.h"<br />被包含的文件一般要求和源文件在同一个目录下,否则必须指定一个完整或相对的路径。如:
<p>#include "../file.h" // 从父目录中包含文件 (UNIX)<br />#include "/usr/local/file.h" // 完整路径 (UNIX)<br />#include "..\file.h" // 从父目录中包含文件(DOS)<br />#include "\usr\local\file.h" // 完整路径(DOS)</p>
当要包含的文件是标准库中的头文件时,文件名用<>符号括起来。如:
<p>#include <iostream.h></p>
<br />编译预处理中遇到<>符号包含的文件时,处理程序将搜索一至多个系统中的特定目录,如UNIX系统中的/usr/include/cpp目录。用户也可通过执行编译命令或改变系统环境变量来自定义这些特定目录。<br />文件包含可以嵌套,如,文件f包含了文件g,文件g又包含了文件h,则文件f也包含了文件h。<br />编译预处理并没有限定包含的文件类型,可以是.h,.cpp或.cc文件,但习惯上只包含头文件.h。<br />重复包含头文件也许会造成编译错误,视情况而定。如,如果头文件中只有一些宏定义和声明,则重复包含没有问题。但如果它包含了一些变量定义,则编译器视重复包含为错误。<br /><br /><strong>条件编译</strong><br />条件编译控制某段代码是否能够被编译,它常用于为了适应不同的软硬件环境而进行的代码裁剪。表2列出了所有条件编译指令的一般形式。 表2 条件编译指令的一般形式
<p>形式 解释<br />#ifdef 名字<br /> 代码段<br />#endif 如果名字被定义,代码段参与编译,否则不参与<br />#ifndef名字<br /> 代码段<br />#endif 如果名字未被定义,代码段参与编译,否则不参与<br />#if 表达式<br /> 代码段<br />#endif 如果表达式非0,代码段参与编译,否则不参与<br />#ifdef名字<br /> 代码段1<br />#else<br /> 代码段2<br />#endif 如果名字被定义,代码段1参与编译,代码段2不参与;否则相反。<br /> 类似,#else也可与#if配合使用<br /><br />#if 表达式1<br /> 代码段1<br />#elif 表达式2<br /> 代码段2<br />#else<br /> 代码段3<br />#endif 如果表达式1非0,代码段1参与编译,否则,如果表达式2非0,<br /> 代码段2参与编译,再否则,代码段3参与编译</p>
<br />下面是两个例子
<p>// 测试版和最终发行版本使用不同的代码:<br />#ifdef BETA<br /> DisplayBetaDialog();<br />#else<br /> CheckRegistration();<br />#endif<br /><br />// 确保Unit有至少4个字节宽:<br />#if sizeof(int) >= 4<br /> typedef int Unit;<br />#elif sizeof(long) >= 4<br /> typedef long Unit;<br />#else<br /> typedef char Unit[4];<br />#endif</p>
<br /><br />#if指令的一个用处是临时忽略部分代码,当程序员在测试某段可疑代码时常常用到这招,当然你也可以用/*…*/将代码注释掉,但因为注释是不可以嵌套的,如果代码中已有/*…*/注释这种方法就不奏效了。<br />下面这段代码表示永久删除中间的代码段:
<p>#if 0<br /> ...要删除的代码段<br />#endif</p>
<br />另外预处理提供了一个defined运算符与#if和#elif配合,如:<br />#if defined BETA<br />等价于:<br />#ifdef BETA<br />它的妙处是可以写出组合逻辑表达式(#ifdef就不能),如:<br />#if defined ALPHA || defined BETA<br />条件编译的另一个用处是可以防止重复包含头文件。如,有一个file.h的头文件,为了避免重复包含,可以在将该文件的内容包含在下列形式的条件语句中:
<p>#ifndef _file_h_<br />#define _file_h_<br /> 这里是头文件内容<br />#endif</p>
<br />当编译预处理首次包含头文件file.h时,将定义名字_file_h_,其后准备再次包含file.h时,会发现该名字已经定义,这样就直接跳转到#endif处,就不会包含该头文件了。<br /><br /><strong>其他预编译指令</strong><br />还有三种不常用的指令。#line指令可改变当前行号和文件名,一般形式为:<br />#line 行号 文件名<br />其中 “文件名”可选,举例如下:<br />#line 20 "file.h"<br />上句告诉编译器当前行号为20,文件名为file.h,直至遇到下一个#line指令。它在编写C++编译器程序的中有些用处,因为编译器对C++源码编译过程中会产生一些中间文件,通过这条指令,可以保证文件名是固定的,不会被这些中间文件代替,有利于进行分析。<br />#error用于在预处理时输出错误信息,一般形式为:<br />#error 错误信息<br />当预处理时遇到这个指令,它将输出错误信息并中止编译,因此最好保证确实无法进一步编译时才书写这个指令代码,如:<br />#ifndef UNIX<br />#error This software requires the UNIX OS.<br />#endif<br />#pragma指令执行一些非标准的预编译命令,不同厂商的编译器有不同的解释执行方法,如对于SUN C++编译器有:
<p>// name和val必须在8个字节的倍数上对齐:<br />#pragma align 8 (name, val)<br />char name[9];<br />double val;<br /><br />// 程序启动即运行MyFunction:<br />#pragma init (MyFunction)</p>
<br /><br /><strong>预定义标识符</strong><br />预处理器提供了一些非常有用的预定义标识符,表3列出了标准预定义标识符。 表3 标准预定义标识符
<p>标识符 表示<br />_FILE_ 正在编译的文件的文件名<br />_LINE_ 正在编译的文件的当前行<br />_DATE_ 字符串表示的当前日期(如“25 Dec 2000”)<br />_TIME_ 字符串表示的当前时间(如“12:30:55”)</p>
<br />预定义标识符在程序中可以像普通常量一样使用。如:<br />#define Assert(p) \<br />if (!(p)) cout << __FILE__ << ": assertion on line " \<br /><< __LINE__ << " failed.\n"<br />定义了一个测试用的宏(断言)。假设有如下调用:<br />Assert(ptr != 0);<br />出现在prog.cpp的第50行,当条件成立(ptr==0),输出信息为:<br /><strong>prog.cpp: assertion on line 50 failed.</strong><br /><br />完成。hoho <strong>例外引用一篇写得简约明了的文章:</strong> #include文件的一个不利之处在于一个头文件可能会被多次包含,为了说明这种错误,考虑下面的代码:<br />#include "x.h"<br />#include "x.h"<br /><br />显然,这里文件x.h被包含了两次,没有人会故意编写这样的代码。但是下面的代码:<br />#include "a.h"<br />#include "b.h"<br /><br />看上去没什么问题。如果a.h和b.h都包含了一个头文件x.h。那么x.h在此也同样被包含了两次,只不过它的形式不是那么明显而已。<br /><br />多重包含在绝大多数情况下出现在大型程序中,它往往需要使用很多头文件,因此要发现重复包含并不容易。要解决这个问题,我们可以使用条件编译。如果所有的头文件都像下面这样编写:<br />#ifndef _HEADERNAME_H<br />#define _HEADERNAME_H<br /><br />...<br /><br />#endif<br /><br />那么多重包含的危险就被消除了。当头文件第一次被包含时,它被正常处理,符号_HEADERNAME_H被定义为1。如果头文件被再次包含,通过条件编译,它的内容被忽略。符号_HEADERNAME_H按照被包含头文件的文件名进行取名,以避免由于其他头文件使用相同的符号而引起的冲突。<br /><br />但是,你必须记住预处理器仍将整个头文件读入,即使这个头文件所有内容将被忽略。由于这种处理将托慢编译速度,所以如果可能,应该避免出现多重包含。
<p> </p>
</div>学牛c++savagertnullhttp://liyanblog.cn/articles/2012/09/18/1347936070045.html2012-09-18T10:41:10+08:00<div class="content-head clearfix">
<h2 class="title content-title">学牛c++</h2>
</div>
<div id="content" class="content mod-cs-content text-content clearfix">关键还是要看你怎么学C++。 <br /><br />如果你学好C++,只是学好C++的语法,为了将来做一个Coder,那么自然没那个研究生有前途(不过那研究生也就北航而已,学校也一般般)。 <br /><br />如果你学好了C++中的指针、数组,学好了数据结构,也学好了算法,或者模式识别,或者信安,或者2D、3D图形,那么C++便可以给你带来更多的价值。 <br /><br />如果你学好了C++,学好了如何用C++来做出很有效率的程序,并且能够知道C++里的每句代码会在内存里面做什么事情,会让CPU执行哪条指令,那么你可以成为一个系统分析师,很有前途的。 <br /><br />如果你学好了C++的设计思想,知道编译器如何去分析每句C++代码来产生汇编码,知道C++中的虚方法、虚继承、成员指针是怎样在最底层的实现的,学好编译原理,那么你将更有前途。无论是C++,还是Java,一门成功的语言会改变整个技术界!</div>c++指针savagertnullhttp://liyanblog.cn/articles/2012/09/18/1347936056376.html2012-09-18T10:40:56+08:00<div class="content-head clearfix">
<h2 class="title content-title">c++指针</h2>
</div>
<div id="content" class="content mod-cs-content text-content clearfix"><strong>[转]</strong>这篇文章摘自网易广州社区的C语言版精华区。文章不错,不敢独享!作者girlrong是以前C语言版版主,她乐于助人,虚心诚恳,颇受网友欢迎。只可惜现在已退隐江湖了。 <br /> <br /><br />第一章。指针的概念 <br /><br />指针是一个特殊的变量,它里面存储的数值被解释成为内存里的一个地址。要搞清一个指针需要搞清指针的四方面的内容:指针的类型,指针所指向的类型,指针的值或者叫指针所指向的内存区,还有指针本身所占据的内存区。让我们分别说明。<br /><br />先声明几个指针放着做例子: <br /><br />例一: <br /><br />(1)int *ptr; <br /><br />(2)char *ptr; <br /><br />(3)int **ptr; <br /><br />(4)int (*ptr)[3]; <br /><br />(5)int *(*ptr)[4]; <br /><br />如果看不懂后几个例子的话,请参阅我前段时间贴出的文章 < <如何理解c和c <br /><br />++的复杂类型声明>>。 <br /><br /> <br /><br />1。 指针的类型。 <br /><br />从语法的角度看,你只要把指针声明语句里的指针名字去掉,剩下的部分就是这个指针的类型。这是指针本身所具有的类型。让我们看看例一中各个指针的类型: <br /><br />(1)int *ptr; //指针的类型是int * <br /><br />(2)char *ptr; //指针的类型是char * <br /><br />(3)int **ptr; //指针的类型是 int ** <br /><br />(4)int (*ptr)[3]; //指针的类型是 int(*)[3] <br /><br />(5)int *(*ptr)[4]; //指针的类型是 int *(*)[4] <br /><br />怎么样?找出指针的类型的方法是不是很简单? <br /><br /> <br /><br />2。指针所指向的类型。 <br /><br />当你通过指针来访问指针所指向的内存区时,指针所指向的类型决定了编译器将把那片内存区里的内容当做什么来看待。 <br /><br />从语法上看,你只须把指针声明语句中的指针名字和名字左边的指针声明符*去掉,剩下的就是指针所指向的类型。例如: <br /><br />(1)int *ptr; //指针所指向的类型是int <br /><br />(2)char *ptr; //指针所指向的的类型是char <br /><br />(3)int **ptr; //指针所指向的的类型是 int * <br /><br />(4)int (*ptr)[3]; //指针所指向的的类型是 int()[3] <br /><br />(5)int *(*ptr)[4]; //指针所指向的的类型是 int *()[4] <br /><br />在指针的算术运算中,指针所指向的类型有很大的作用。 <br /><br />指针的类型(即指针本身的类型)和指针所指向的类型是两个概念。当你对C越来越熟悉时,你会发现,把与指针搅和在一起的“类型”这个概念分成“指针的类型”和“指针所指向的类型”两个概念,是精通指针的关键点之一。我看了不少书,发现有些写得差的书中,就把指针的这两个概念搅在一起了,所以看起书来前后矛盾,越看越糊涂。 <br /><br />3。 指针的值,或者叫指针所指向的内存区或地址。 <br /><br />指针的值是指针本身存储的数值,这个值将被编译器当作一个地址,而不是一个一般的数值。在32位程序里,所有类型的指针的值都是一个32位整数,因为32位程序里内存地址全都是32位长。 <br /><br />指针所指向的内存区就是从指针的值所代表的那个内存地址开始,长度为sizeof(指针所指向的类型)的一片内存区。以后,我们说一个指针的值是XX,就相当于说该指针指向了以XX为首地址的一片内存区域;我们说一个指针指向了某块内存区域,就相当于说该指针的值是这块内存区域的首地址。 <br /><br />指针所指向的内存区和指针所指向的类型是两个完全不同的概念。在例一中,指针所指向的类型已经有了,但由于指针还未初始化,所以它所指向的内存区是不存在的,或者说是无意义的。 <br /><br />以后,每遇到一个指针,都应该问问:这个指针的类型是什么?指针指向的类型是什么?该指针指向了哪里? <br /><br />4。 指针本身所占据的内存区。 <br /><br />指针本身占了多大的内存?你只要用函数sizeof(指针的类型)测一下就知道了。在32位平台里,指针本身占据了4个字节的长度。 <br /><br />指针本身占据的内存这个概念在判断一个指针表达式是否是左值时很有用。 <br /><br />第二章。指针的算术运算 <br /><br /> <br /><br />指针可以加上或减去一个整数。指针的这种运算的意义和通常的数值的加减运算的意义是不一样的。例如: <br /><br />例二: <br /><br />1。 char a[20]; <br /><br />2。 int *ptr=a; <br /><br />... <br /><br />... <br /><br />3。 ptr++; <br /><br />在上例中,指针ptr的类型是int*,它指向的类型是int,它被初始化为指向整形变量a。接下来的第3句中,指针ptr被加了1,编译器是这样处理的:它把指针ptr的值加上了sizeof(int),在32位程序中,是被加上了4。由于地址是用字节做单位的,故ptr所指向的地址由原来的变量a的地址向高地址方向增加了4个字节。 <br /><br />由于char类型的长度是一个字节,所以,原来ptr是指向数组a的第0号单元开始的四个字节,此时指向了数组a中从第4号单元开始的四个字节。 <br /><br />我们可以用一个指针和一个循环来遍历一个数组,看例子: <br /><br />例三: <br /><br />int array[20]; <br /><br />int *ptr=array; <br /><br />... <br /><br />//此处略去为整型数组赋值的代码。 <br /><br />... <br /><br />for(i=0;i <20;i++) <br /><br />{ <br /><br />(*ptr)++; <br /><br />ptr++; <br /><br />} <br /><br />这个例子将整型数组中各个单元的值加1。由于每次循环都将指针ptr加1,所以每次循环都能访问数组的下一个单元。再看例子: <br /><br />例四: <br /><br />1。 char a[20]; <br /><br />2。 int *ptr=a; <br /><br />... <br /><br />... <br /><br />3。 ptr+=5; <br /><br />在这个例子中,ptr被加上了5,编译器是这样处理的:将指针ptr的值加上5乘sizeof(int),在32位程序中就是加上了5乘4=20。由于地址的单位是字节,故现在的ptr所指向的地址比起加5后的ptr所指向的地址来说,向高地址方向移动了20个字节。在这个例子中,没加5前的ptr指向数组a的第0号单元开始的四个字节,加5后,ptr已经指向了数组a的合法范围之外了。虽然这种情况在应用上会出问题,但在语法上却是可以的。这也体现出了指针的灵活性。 <br /><br />如果上例中,ptr是被减去5,那么处理过程大同小异,只不过ptr的值是被减去5乘sizeof(int),新的ptr指向的地址将比原来的ptr所指向的地址向低地址方向移动了20个字节。 <br /><br />总结一下,一个指针ptrold加上一个整数n后,结果是一个新的指针ptrnew,ptrnew的类型和ptrold的类型相同,ptrnew所指向的类型和ptrold所指向的类型也相同。ptrnew的值将比ptrold的值增加了n乘sizeof(ptrold所指向的类型)个字节。就是说,ptrnew所指向的内存区将比ptrold所指向的内存区向高地址方向移动了n乘sizeof(ptrold所指向的类型)个字节。一个指针ptrold减去一个整数n后,结果是一个新的指针ptrnew,ptrnew的类型和ptrold的类型相同,ptrnew所指向的类型和ptrold所指向的类型也相同。ptrnew的值将比ptrold的值减少了n乘sizeof(ptrold所指向的类型)个字节,就是说,ptrnew所指向的内存区将比ptrold所指向的内存区向低地址方向移动了n乘sizeof(ptrold所指向的类型)个字节。 <br /><br /> <br /><br />第三章。运算符&和* <br /><br /> <br /><br />这里&是取地址运算符,*是...书上叫做“间接运算符”。&a的运算结果是一个指针,指针的类型是a的类型加个*,指针所指向的类型是a的类型,指针所指向的地址嘛,那就是a的地址。*p的运算结果就五花八门了。总之*p的结果是p所指向的东西,这个东西有这些特点:它的类型是p指向的类型,它所占用的地址是p所指向的地址。 <br /><br />例五: <br /><br />int a=12; <br /><br />int b; <br /><br />int *p; <br /><br />int **ptr; <br /><br />p=&a;//&a的结果是一个指针,类型是int*,指向的类型是int,指向的地址是a的地址。 <br /><br />*p=24;//*p的结果,在这里它的类型是int,它所占用的地址是p所指向的地址,显然,*p就是变量a。 <br /><br />ptr=&p;//&p的结果是个指针,该指针的类型是p的类型加个*,在这里是int**。该指针所指向的类型是p的类型,这里是int*。该指针所指向的地址就是指针p自己的地址。 <br /><br />*ptr=&b;//*ptr是个指针,&b的结果也是个指针,且这两个指针的类型和所指向的类型是一样的,所以?amp;b来给*ptr赋值就是毫无问题的了。 <br /><br />**ptr=34;//*ptr的结果是ptr所指向的东西,在这里是一个指针,对这个指针再做一次*运算,结果就是一个int类型的变量。<br /><br /> <br /><br />第四章。指针表达式。 <br /><br /> <br /><br />一个表达式的最后结果如果是一个指针,那么这个表达式就叫指针表达式。下面是一些指针表达式的例子: <br /><br />例六: <br /><br />int a,b; <br /><br />int array[10]; <br /><br />int *pa; <br /><br />pa=&a;//&a是一个指针表达式。 <br /><br />int **ptr=&pa;//&pa也是一个指针表达式。 <br /><br />*ptr=&b;//*ptr和&b都是指针表达式。 <br /><br />pa=array; <br /><br />pa++;//这也是指针表达式。 <br /><br />例七: <br /><br />char *arr[20]; <br /><br />char **parr=arr;//如果把arr看作指针的话,arr也是指针表达式 <br /><br />char *str; <br /><br />str=*parr;//*parr是指针表达式 <br /><br />str=*(parr+1);//*(parr+1)是指针表达式 <br /><br />str=*(parr+2);//*(parr+2)是指针表达式 <br /><br />由于指针表达式的结果是一个指针,所以指针表达式也具有指针所具有的四个要素:指针的类型,指针所指向的类型,指针指向的内存区,指针自身占据的内存。 <br /><br />好了,当一个指针表达式的结果指针已经明确地具有了指针自身占据的内存的话,这个指针表达式就是一个左值,否则就不是一个左值。 在例七中,&a不是一个左值,因为它还没有占据明确的内存。*ptr是一个左值,因为*ptr这个指针已经占据了内存,其实*ptr就是指针pa,既然pa已经在内存中有了自己的位置,那么*ptr当然也有了自己的位置。 <br /><br /> <br /><br />第五章。数组和指针的关系 <br /><br /> <br /><br />如果对声明数组的语句不太明白的话,请参阅我前段时间贴出的文章 < <如何理解c和c++的复杂类型声明>>。 数组的数组名其实可以看作一个指针。看下例: <br /><br />例八: <br /><br />int array[10]={0,1,2,3,4,5,6,7,8,9},value; <br /><br />... <br /><br />... <br /><br />value=array[0];//也可写成:value=*array; <br /><br />value=array[3];//也可写成:value=*(array+3); <br /><br />value=array[4];//也可写成:value=*(array+4); <br /><br />上例中,一般而言数组名array代表数组本身,类型是int [10],但如果把array看做指针的话,它指向数组的第0个单元,类型是int *,所指向的类型是数组单元的类型即int。因此*array等于0就一点也不奇怪了。同理,array+3是一个指向数组第3个单元的指针,所以*(array+3)等于3。其它依此类推。 <br /><br />例九: <br /><br />char *str[3]={ <br /><br />"Hello,this is a sample!", <br /><br />"Hi,good morning.", <br /><br />"Hello world" <br /><br />}; <br /><br />char s[80]; <br /><br />strcpy(s,str[0]);//也可写成strcpy(s,*str); <br /><br />strcpy(s,str[1]);//也可写成strcpy(s,*(str+1)); <br /><br />strcpy(s,str[2]);//也可写成strcpy(s,*(str+2)); <br /><br />上例中,str是一个三单元的数组,该数组的每个单元都是一个指针,这些指针各指向一个字符串。把指针数组名str当作一个指针的话,它指向数组的第0号单元,它的类型是char**,它指向的类型是char *。 <br /><br />*str也是一个指针,它的类型是char*,它所指向的类型是char,它指向的地址是字符串"Hello,this is a sample!"的第一个字符的地址,即'H'的地址。 str+1也是一个指针,它指向数组的第1号单元,它的类型是char**,它指向的类型是char *。 <br /><br />*(str+1)也是一个指针,它的类型是char*,它所指向的类型是char,它指向"Hi,good morning."的第一个字符'H',等等。 <br /><br />下面总结一下数组的数组名的问题。声明了一个数组TYPE array[n],则数组名称array就有了两重含义:第一,它代表整个数组,它的类型是TYPE [n];第二,它是一个指针,该指针的类型是TYPE*,该指针指向的类型是TYPE,也就是数组单元的类型,该指针指向的内存区就是数组第0号单元,该指针自己占有单独的内存区,注意它和数组第0号单元占据的内存区是不同的。该指针的值是不能修改的,即类似array++的表达式是错误的。 <br /><br />在不同的表达式中数组名array可以扮演不同的角色。 <br /><br />在表达式sizeof(array)中,数组名array代表数组本身,故这时sizeof函数测出的是整个数组的大小。 <br /><br />在表达式*array中,array扮演的是指针,因此这个表达式的结果就是数组第0号单元的值。sizeof(*array)测出的是数组单元的大小。 <br /><br />表达式array+n(其中n=0,1,2,....。)中,array扮演的是指针,故array+n的结果是一个指针,它的类型是TYPE*,它指向的类型是TYPE,它指向数组第n号单元。故sizeof(array+n)测出的是指针类型的大小。 <br /><br />例十: <br /><br />int array[10]; <br /><br />int (*ptr)[10]; <br /><br />ptr=&array; <br /><br />上例中ptr是一个指针,它的类型是int (*)[10],他指向的类型是int [10],我们用整个数组的首地址来初始化它。在语句ptr=&array中,array代表数组本身。 <br /><br />本节中提到了函数sizeof(),那么我来问一问,sizeof(指针名称)测出的究竟是指针自身类型的大小呢还是指针所指向的类型的大小?答案是前者。例如: <br /><br />int (*ptr)[10]; <br /><br />则在32位程序中,有: <br /><br />sizeof(int(*)[10])==4 <br /><br />sizeof(int [10])==40 <br /><br />sizeof(ptr)==4 <br /><br />实际上,sizeof(对象)测出的都是对象自身的类型的大小,而不是别的什么类型的大小。 <br /><br />第六章。指针和结构类型的关系 <br /><br /> <br /><br />可以声明一个指向结构类型对象的指针。 <br /><br />例十一: <br /><br />struct MyStruct <br /><br />{ <br /><br />int a; <br /><br />int b; <br /><br />int c; <br /><br />} <br /><br />MyStruct ss={20,30,40};//声明了结构对象ss,并把ss的三个成员初始化为20,30和40。 <br /><br />MyStruct *ptr=&ss;//声明了一个指向结构对象ss的指针。它的类型是 <br /><br />MyStruct*,它指向的类型是MyStruct。 <br /><br />int *pstr=(int*)&ss;//声明了一个指向结构对象ss的指针。但是它的类型和它指向的类型和ptr是不同的。 <br /><br />请问怎样通过指针ptr来访问ss的三个成员变量? <br /><br />答案: <br /><br />ptr->a; <br /><br />ptr->b; <br /><br />ptr->c; <br /><br />又请问怎样通过指针pstr来访问ss的三个成员变量? <br /><br />答案: <br /><br />*pstr;//访问了ss的成员a。 <br /><br />*(pstr+1);//访问了ss的成员b。 <br /><br />*(pstr+2)//访问了ss的成员c。 <br /><br />呵呵,虽然我在我的MSVC++6.0上调式过上述代码,但是要知道,这样使用pstr来访问结构成员是不正规的,为了说明为什么不正规,让我们看看怎样通过指针来访问数组的各个单元: <br /><br />例十二: <br /><br />int array[3]={35,56,37}; <br /><br />int *pa=array; <br /><br />通过指针pa访问数组array的三个单元的方法是: <br /><br />*pa;//访问了第0号单元 <br /><br />*(pa+1);//访问了第1号单元 <br /><br />*(pa+2);//访问了第2号单元 <br /><br />从格式上看倒是与通过指针访问结构成员的不正规方法的格式一样。 <br /><br />所有的C/C++编译器在排列数组的单元时,总是把各个数组单元存放在连续的存储区里,单元和单元之间没有空隙。但在存放结构对象的各个成员时,在某种编译环境下,可能会需要字对齐或双字对齐或者是别的什么对齐,需要在相邻两个成员之间加若干个“填充字节”,这就导致各个成员之间可能会有若干个字节的空隙。 <br /><br />所以,在例十二中,即使*pstr访问到了结构对象ss的第一个成员变量a,也不能保证*(pstr+1)就一定能访问到结构成员b。因为成员a和成员b之间可能会有若干填充字节,说不定*(pstr+1)就正好访问到了这些填充字节呢。这也证明了指针的灵活性。要是你的目的就是想看看各个结构成员之间到底有没有填充字节,嘿,这倒是个不错的方法。 <br /><br />通过指针访问结构成员的正确方法应该是象例十二中使用指针ptr的方法。 <br /><br /> <br /><br />第七章。指针和函数的关系 <br /><br /> <br /><br /> <br /><br />可以把一个指针声明成为一个指向函数的指针。 <br /><br />int fun1(char*,int); <br /><br />int (*pfun1)(char*,int); <br /><br />pfun1=fun1; <br /><br />.... <br /><br />.... <br /><br />int a=(*pfun1)("abcdefg",7);//通过函数指针调用函数。 <br /><br />可以把指针作为函数的形参。在函数调用语句中,可以用指针表达式来作为实参。 <br />例十三: <br /><br />int fun(char*); <br /><br />int a; <br /><br />char str[]="abcdefghijklmn"; <br /><br />a=fun(str); <br /><br />... <br /><br />... <br /><br />int fun(char*s) <br /><br />{ <br /><br />int num=0; <br /><br />for(int i=0;i <strlen(s);i++) <br /><br />{ <br /><br />num+=*s;s++; <br /><br />} <br /><br />return num; <br /><br />} <br /><br />这个例子中的函数fun统计一个字符串中各个字符的ASCII码值之和。前面说了,数组的名字也是一个指针。在函数调用中,当把str作为实参传递给形参s后,实际是把str的值传递给了s,s所指向的地址就和str所指向的地址一致,但是str和s各自占用各自的存储空间。在函数体内对s进行自加1运算,并不意味着同时对str进行了自加1运算。 <br /><br /> <br /><br />第八章。指针类型转换 <br /><br /> <br /><br />当我们初始化一个指针或给一个指针赋值时,赋值号的左边是一个指针,赋值号的右边是一个指针表达式。在我们前面所举的例子中,绝大多数情况下,指针的类型和指针表达式的类型是一样的,指针所指向的类型和指针表达式所指向的类型是一样的。 <br /><br />例十四: <br /><br />1。 float f=12.3; <br /><br />2。 float *fptr=&f; <br /><br />3。 int *p; <br /><br />在上面的例子中,假如我们想让指针p指向实数f,应该怎么搞?是用下面的语句吗? <br /><br />p=&f; <br /><br />不对。因为指针p的类型是int*,它指向的类型是int。表达式&f的结果是一个指针,指针的类型是float*,它指向的类型是float。两者不一致,直接赋值的方法是不行的。至少在我的MSVC++6.0上,对指针的赋值语句要求赋值号两边的类型一致,所指向的类型也一致,其它的编译器上我没试过,大家可以试试。为了实现我们的目的,需要进行“强制类型转换”: <br /><br />p=(int*)&f; <br /><br />如果有一个指针p,我们需要把它的类型和所指向的类型改为TYEP*和TYPE,那么语法格式是: <br /><br />(TYPE*)p; <br /><br />这样强制类型转换的结果是一个新指针,该新指针的类型是TYPE*,它指向的类型是TYPE,它指向的地址就是原指针指向的地址。而原来的指针p的一切属性都没有被修改。 <br /><br />一个函数如果使用了指针作为形参,那么在函数调用语句的实参和形参的结合过程中,也会发生指针类型的转换。 <br /><br />例十五: <br /><br />void fun(char*); <br /><br />int a=125,b; <br /><br />fun((char*)&a); <br /><br />... <br /><br />... <br /><br />void fun(char*s) <br /><br />{ <br /><br />char c; <br /><br />c=*(s+3);*(s+3)=*(s+0);*(s+0)=c; <br /><br />c=*(s+2);*(s+2)=*(s+1);*(s+1)=c; <br /><br />} <br /><br />注意这是一个32位程序,故int类型占了四个字节,char类型占一个字节。函数fun的作用是把一个整数的四个字节的顺序来个颠倒。注意到了吗?在函数调用语句中,实?amp;a的结果是一个指针,它的类型是int *,它指向的类型是int。形参这个指针的类型是char*,它指向的类型是char。这样,在实参和形参的结合过程中,我们必须进行一次从int*类型到char*类型的转换。结合这个例子,我们可以这样来想象编译器进行转换的过程:编译器先构造一个临时指针 char*temp,然后执行temp=(char*)&a,最后再把temp的值传递给s。所以最后的结果是:s的类型是char*,它指向的类型是char,它指向的地址就是a的首地址。 <br /><br />我们已经知道,指针的值就是指针指向的地址,在32位程序中,指针的值其实是一个32位整数。那可不可以把一个整数当作指针的值直接赋给指针呢?就象下面的语句: <br /><br />unsigned int a; <br /><br />TYPE *ptr;//TYPE是int,char或结构类型等等类型。 <br /><br />... <br /><br />... <br /><br />a=20345686; <br /><br />ptr=20345686;//我们的目的是要使指针ptr指向地址20345686(十进制) <br /><br />ptr=a;//我们的目的是要使指针ptr指向地址20345686(十进制) <br /><br />编译一下吧。结果发现后面两条语句全是错的。那么我们的目的就不能达到了吗?不,还有办法: <br /><br />unsigned int a; <br /><br />TYPE *ptr;//TYPE是int,char或结构类型等等类型。 <br /><br />... <br /><br />... <br /><br />a=某个数,这个数必须代表一个合法的地址; <br /><br />ptr=(TYPE*)a;//呵呵,这就可以了。 <br /><br />严格说来这里的(TYPE*)和指针类型转换中的(TYPE*)还不一样。这里的(TYPE*)的意思是把无符号整数a的值当作一个地址来看待。 <br /><br />上面强调了a的值必须代表一个合法的地址,否则的话,在你使用ptr的时候,就会出现非法操作错误。 <br /><br />想想能不能反过来,把指针指向的地址即指针的值当作一个整数取出来。完全可以。下面的例子演示了把一个指针的值当作一个整数取出来,然后再把这个整数当作一个地址赋给一个指针: <br /><br />例十六: <br /><br />int a=123,b; <br /><br />int *ptr=&a; <br /><br />char *str; <br /><br />b=(int)ptr;//把指针ptr的值当作一个整数取出来。 <br /><br />str=(char*)b;//把这个整数的值当作一个地址赋给指针str。 <br /><br />好了,现在我们已经知道了,可以把指针的值当作一个整数取出来,也可以把一个整数值当作地址赋给一个指针。 <br /><br /> <br /><br />第九章。指针的安全问题 <br /><br />看下面的例子: <br /><br />例十七: <br /><br />char s='a'; <br /><br />int *ptr; <br /><br />ptr=(int*)&s; <br /><br />*ptr=1298; <br /><br />指针ptr是一个int*类型的指针,它指向的类型是int。它指向的地址就是s的首地址。在32位程序中,s占一个字节,int类型占四个字节。最后一条语句不但改变了s所占的一个字节,还把和s相临的高地址方向的三个字节也改变了。这三个字节是干什么的?只有编译程序知道,而写程序的人是不太可能知道的。也许这三个字节里存储了非常重要的数据,也许这三个字节里正好是程序的一条代码,而由于你对指针的马虎应用,这三个字节的值被改变了!这会造成崩溃性的错误。让我们再来看一例: <br /><br />例十八: <br /><br />1。 char a; <br /><br />2。 int *ptr=&a; <br /><br />... <br /><br />... <br /><br />3。 ptr++; <br /><br />4。 *ptr=115; <br /><br />该例子完全可以通过编译,并能执行。但是看到没有?第3句对指针ptr进行自加1运算后,ptr指向了和整形变量a相邻的高地址方向的一块存储区。这块存储区里是什么?我们不知道。有可能它是一个非常重要的数据,甚至可能是一条代码。而第4句竟然往这片存储区里写入一个数据!这是严重的错误。所以在使用指针时,程序员心里必须非常清楚:我的指针究竟指向了哪里。 <br /><br />在用指针访问数组的时候,也要注意不要超出数组的低端和高端界限,否则也会造成类似的错误。 <br /><br />在指针的强制类型转换:ptr1=(TYPE*)ptr2中,如果sizeof(ptr2的类型)大于sizeof(ptr1的类型),那么在使用指针ptr1来访问ptr2所指向的存储区时是安全的。如果sizeof(ptr2的类型)小于sizeof(ptr1的类型),那么在使用指针ptr1来访问ptr2所指向的存储区时是不安全的。至于为什么,读者结合例十七来想一想,应该会明白的。 <br /><br /><br />请写出以下程序的运行结果: <br /><br />#include <stdio.h> <br />int *p; <br />pp(int a,int *b); <br />main() <br />{ <br />int a=1,b=2,c=3; <br />p=&b; <br />pp(a+c,&b); <br />printf("(1)%d%d%dn",a,b,*p); <br />} <br />pp(int a,int *b) <br />{int c=4; <br />*p=*b+c; <br />a=*p-c; <br />printf("(2)%d%d%dn",a,*b,*p); <br />} </div>关于#pragma warningsavagertnullhttp://liyanblog.cn/articles/2012/09/18/1347935979857.html2013-02-28T15:48:03+08:00<div class="content-head clearfix">
<h2 class="title content-title">关于#pragma warning</h2>
</div>
<div id="content" class="content mod-cs-content text-content clearfix">关于#pragma warning
<p>1. #pragma warning只对当前文件有效(对于.h,对包含它的cpp也是有效的),而不是对整个工程的所有文件有效。当该文件编译结束,设置也就失去作用。</p>
<p>2. #pragma warning(push)</p>
<p>存储当前报警设置。</p>
<p>#pragma warning(push, n)</p>
<p>存储当前报警设置,并设置报警级别为n。n为从1到4的自然数。</p>
<p>3. #pragma warning(pop)</p>
<p>恢复之前压入堆栈的报警设置。在一对push和pop之间作的任何报警相关设置都将失效。</p>
<p>4. #pragma warning(disable: n)</p>
<p>将某个警报置为失效</p>
<p>5. #pragma warning(default: n)</p>
<p>将报警置为默认</p>
<p>6. 某些警告如C4309是从上到下生效的。即文件内#pragma warning从上到下遍历,依次生效。</p>
<p>例如:</p>
<p>void func()</p>
<p>{</p>
<p> #pragma warning(disable: 4189)</p>
<p> char s;</p>
<p> s = 128;</p>
<p> #pragma warning(default: 4189)</p>
<p> char c;</p>
<p> c = 128;</p>
<p>}</p>
<p>则s = 128不会产生C4309报警,而C4309会产生报警。</p>
<p>7. 某些警告例如C4189是以函数中最后出现的#pragma warning设置为准的,其余针对该报警的设置都是无效的。</p>
<p>例如:</p>
<p>void func()</p>
<p>{</p>
<p> #pragma warning(disable: 4189)</p>
<p> int x = 1;</p>
<p> #pragma warning(default: 4189)</p>
<p>}</p>
<p>则C4189仍然会出现,因为default指令是函数的最后一条。在该文件内的其他函数中,如果没有重新设置,C4189也是以#pragma warning(default: 4189)为准。如果重新设置,同样是按照其函数中的最后一个#pragma warning为准。</p>
<p>8. 某些警告(MSDN认为是大于等于C4700的警告)是在函数结束后才能生效。</p>
<p>例如:</p>
<p>#pragma warning(disable:4700)</p>
<p>void Func()</p>
<p>{</p>
<p>int x;</p>
<p>int y = x; </p>
<p> #pragma warning(default:4700) </p>
<p> int z= x;</p>
<p>}</p>
<p>则y = x和z = x都不会产生C4700报警。只有在函数结束后的后的另外一个函数中,#pragma warning(default:4700)才能生效。</p>
</div>
<div class="mod-post-info clearfix">
<div class="tag-box clearfix"> </div>
</div>