跟我一起玩Win32开发(1):关于C++的几个要点
我不知道各位,一提起C++,第一感觉是什么?而据俺的观察,许多人几乎成了“谈C色变”。不管是C还是C++,一直以来都被很多人视为相当难学的玩意儿,幸好只是一个C++,没有C--,C**和C//,不然,那还得了?曾记得,某年某月某日,在某论坛上看到有牛人说“C++++”,当时我猜想这是啥玩意儿,后来经过一番顺虅摸瓜,深入调查发现,原来有人作了这么个有趣的等式:C# == C++++。
显然,这个等式也不太正确,C#不仅继承了C++一些特性,也继承了Delphi中的和VB中的一些优点。
好了,这个等式意义不大,咱们不扯它了。前面我写了许多和移动开发的文章,估计现在移动市场泡沫也差不多膨胀起来了,你说这泡沫,泡到什么程度呢?据说连压根连程序都没写过的人,也嚷着说:移动开发,我要(幸好不是官人,不然动机不纯)。
这很容易让人联想到“全民炒股”的创世纪大笑话,中国人貌似很喜欢这样,一曰跟风,二曰盲从。这二者合并起来,正好为市场本质上的“自发性,盲目性”等特征作了相当有力的诠释,难怪罗斯福总统说必要时还得宏观调控。在1932年如果还不调控的话,估计到了1945年,在太平洋战场上完蛋的不是零式战斗机了,该是地狱猫战斗机了,呵呵。
不管是移动互联网,还是云计算,各位还是理性地考虑一下吧,认为有需要才进行投资,目前来说,移动市场绝大部分还是在娱乐上,要说真要和商业模式融合,估计现在的手机和平板电脑还达不到这个指标,未来几年有可能开始和商业平台对接,今年的话,不太可能,如果你计划把你的商业应用向移动平台扩展(我这里用扩展,千万不要转移,不然会丢失原来的市场),那么,你现在可能要考虑你现有的应用程序框架到底有多少可以进行扩展了。
这扩展一事说起来容易,做起来可不轻松,记得去年我在F公司工作,尝把ERP的功能,从小的模块开始,向Web/电子商务平台整合,技术上是没问题的,但业务逻辑上有可能会一败涂地。所以,有时候,咱们做开发的,学一学市场营销、财务会计、企业管理、HR,甚至是文学艺术,对我们的成长还是有好处的,你只会写程序,有时候很容易“当局者迷”,金庸老先生在小说里常常把这个称为“走火入X”,不知道欧阳锋大哥算不算。
一、指针,真的那么恐怖吗
很多人学C语言,就是败在她的“石榴裙”下的,指针(Pointer),这里我为什么要把英文原名写出来了,我目的想让你思考一下,我们常叫它指针,但是,这个翻译到底合不合理?
在学习C和C++时,很多人会被指针给弄得“六神无主”,虽然大家在许多书上都看到,指针就是用来保存地址的,是吧。然很多人就是无法理解。我这里告诉大家一个技巧,凡是遇到抽象的东西,你就不妨尝试着在客观存在的事物中去寻找与其相似的东西,例如从我们日常生活中入手。我们要明白一个道理,所有抽象的东西,追根到底,都是从客观事物中提取出来的,也就是说,任何抽象的东西,都会在客观世界中找到它的原形。
那么,在我们的日常生活中,有没有与C/C++中指针对应的东西呢?有,多得是:
指南针。
手表。
电流表/电压表。
汽车上用来显示剩余汽油的表。
……
看看,这些物体都有什么共同特点?是不是都有一根或者多根指针?比如,下面图片中的山寨手表。
现在,我要你从山寨手表中读出其指示的时间,那么,你想一想,你会怎么看出来的
这个应该会看吧,小学生都会用了。我们会先看一下时针所指的方向在哪个时刻范围内,如10-11之间,所以我们确定了是10点钟;然后,我们看到分针所指的是第二个刻度,我们读出2,所以一组合,就是10点2。
是不是这样读,没读错吧?
像电流表也是这样,我们就是通过指针所指的方向找到对应的刻度,就知道当前电流是多少安/毫安,家用的应该是以毫安为单位。
我们知道,程序的运行是存在内存中的,因此,我们在代码中声明的所有变量都是存放在内存中的某块区域中,所以,在C语言中,指针用来告诉我们,某个变量在内存中的首地址在哪。注意,是首地址,因为变量的长度不一定就是一个字节,有可能N多个字节,它在内存中是排列在一段连续的区域块中。
比如,某中学的教学楼,每个年级使用一栋楼,初一年级在A栋,初二在B栋,初三在C栋。某学生在校其间,由于多次非礼女同学,老师说要见家长,于是,家长K君来到了某学校,但学校那么大,怎么找到K君的儿子所在的教室呢?这时候,保安人员告诉K君,初二年级在B栋,并用手指着东北方向(指针指向的内存地址块的位置)。
于是,K君顺着保安人员手指的方向找到了B栋,他知道他儿子在3班,而楼上的教室都是按顺序的,1班在第一个教室,2班在第二个教室,以此类推。所以,K君很快就找到他儿子,然后把他教育了两顿(访问或处理指针所指向内存中的数据),然后,K君很郁闷地下楼,离开了学校(发生析构,清理内存)。
因此,我们可以对指针这样宝义:
通过指针中存放的首地址,应用程序顺利地找到某个变量。就好像我最近认识了一位朋友,他叫我有空去他家坐坐,然后,他留下了地址。某个周末我正闲着,忽然想起这位朋友,于是,我就根据他留的地址去找他,结果,当我来到傻B街230号出租房时,里面走出一个我不认识的人,于是,我问他我这位朋友去哪了,陌生人说,我刚租了这房子,你找的可能是前一位租户吧。
所以,指针所指向的地址,有可能是变量B,也有可能是变量F,或者变量S,指针是房东,可以把房子租给B,C,或F,它可以动态为变量分配内存,也可以把变量销毁(delete),交不起房租就滚蛋(析构函数)。
从上面的故事中,我们看到指针的两个用途:索引内存和分配内存。
看看下面这个例子。
- #include <stdio.h>
- void main()
- {
- int* pint = new int(100);
- printf(" *pint的值:%d\n", *pint);
- printf(" pint的值:0x %x\n", pint);
- getchar();
- }
你猜猜,它运行后会出现什么?
我们看到了,pint里面存的就是整型100的首地址,因为它是int*,是指向int的指针,所以指针知道,找到首地址后,我只关注从首地址开始,连续的4个字节,后面的我不管了,因为我只知道int有四个字节。上面的例子,我们看到pint的值就是0x10f1968,这就是整型100在内存中的首地址,所以,100所拥有的内存块可能是:
0x10f1968 , 0x10f1969, 0x10f196A, 0x10f196b
总之是连续的内存块来保存这4个字节。
new int(100),表示指针pint在首地址为0x10f1968的内存区域创建了一个4个字节的区域,里面保存的值就是整型100,所以,pint取得的就是100的首地址,而加上*号就不同了,看看上面的例子,*pint的值就是100了。这样一来,我们又得到一个技巧:
利用指针标识符 * 放在指针变量前即可获得指针所指地址中存储的实际值。
我都大家一个很简单的技巧。看看下面两行代码。
int *p = new int(200);
int p = 200;
因为 * 放在类型后或放在变量名前面都是可以的,即int* pint和int *pint是一个道理。这样一来,我们不妨把int *pint 看作int (*pint),将整个*pint看作一个整体,这样看上去是不是和下面的声明很像?
int a = 30;
所以,int* p = new int(30)中,*p返回的值就是int的本值30,而p则只是返回30的首地址。
再看看下面的代码:
- #include <stdio.h>
- void main()
- {
- int* arrint = new int[3];
- arrint[0] = 20;
- arrint[1] = 21;
- arrint[2] = 22;
- for(int i =0; i < 3; i++)
- {
- printf(" 数组[%d] = %d\n", i, arrint[i]);
- }
- delete [] arrint; // 清理内存
- getchar();
- }
现在你可以猜猜它的运行结果是什么。
从上面的代码我们又看到了指针的第三个功能:创建数组。
上例中,我创建了有三个元素的数组。在使用完成后,要使用delete来删除已分配的内存,所以,我们的第一个例子中,其实不完善,我们没有做内存清理。
int* pint = new int(100);
/****/
delete pint;
为什么指针可以创建数组?前面我提到过,指针是指向首地址的,那么你想想,我们的数组如果在堆上分配了内存,它们是不是也按一定次序存放在一块连续的内存地址中,整个数组同样构成了一段内存块。
二、取地址符号&
很多书和教程都把这个符号叫引用,但我不喜欢翻译为引用,因为引用不好理解,如果叫取地址符,那我估计你就会明白了,它就是返回一个变量的首地址。
看看例子:
- #include <stdio.h>
- void main()
- {
- int a = 50;
- int* p = &a;
- printf(" a的值:%d\n", a);
- printf(" p的值:0x_%x\n", p);
- getchar();
- }
我们不能直接对指针变量赋值,要把变量的地址传给指针,就要用取地址符&。上面的代码中我们声明了int类型的变量a,值为50,通过&符号把变量a的地址存到p指针中,这样,p指向的就是变量a的首地址了,故:a的值的50,而p的值就应该是a的地址。
那么,这样做有啥好处呢?我们把上面的例子再扩展一下,变成这样:
- #include <stdio.h>
- void main()
- {
- int a = 50;
- int* p = &a;
- printf(" a的值:%d\n", a);
- printf(" p的值:0x_%x\n", p);
- /* 改变指针所指向的地址块中的值,就等于改变了变量的值 */
- *p = 250;
- printf(" a的新值:%d\n", a);
- getchar();
- }
先预览一下结果。
不知道大家在这个例子中发现了什么?
我们定义了变量a,值为50,然后指针p指向了a的首地址,但注意,后面我只是改变了p所指向的那块内存中的值,我并没有修改a的值,但是,你看看最后a的值也变为了250,想一想,这是为什么?
三、参数的传递方式
很多书,包括一些计算机二级的考试内容,那些傻S砖家只是想出一大堆与指针相关的莫名其妙的考题,但很让人找不到指针在实际应用到底能干什么,我估计那些砖家自己也不知识吧。所以,我们的考试最大的失败,就是让学生不知识学了有什么用。
上面介绍了指针可以存首地址,可以分配内存,可以创建数组,还说了取地址符&,那么,这些东西有什么用呢?你肯定会问,我直接声明一个变量也是要占用内存的,那我为什么要吃饱了没事干还要用指针来存放首地址呢?
好,我先不回答,我们再说说函数的参数传递。看看下面这样的例子。
- #include <stdio.h>
- void fn(int x)
- {
- x += 100;
- }
- void main()
- {
- int a = 20;
- fn(a);
- printf(" a : %d\n", a);
- getchar();
- }
我们希望,在调用函数fn后,变量a的值会加上100,现在我们运行一下,看看结果:
我们可能会很失望,为什么会这样?我明明是把20传进了fn函数的,为什么a的值还是不变呢?不用急,我们再把代码改一下:
- #include <stdio.h>
- void fn(int x)
- {
- printf(" 参数的地址:0x_%d\n", &x);
- x += 100;
- }
- void main()
- {
- int a = 20;
- fn(a);
- printf(" a : %d\n", a);
- printf(" a的地址:0x_%x\n", &a);
- getchar();
- }
运行结果如下:
看到了吗?变量a和fn函数的参数x的地址是不一样的,这意味着什么呢?这说明,变量a的值虽然传给了参数x,但实际上是声明了一个新变量x,而x的值为20罢了,最后加上100,x的中的值是120,但a的值没有变,因为在函数内被+100的根本不是变量a,而是变量x(参数)。
这样,就解释了为什么么函数调用后a的值仍然不变的原因。
那么,如何让函数调用后对变量a作修改,让它变成120呢?这里有两个方法:
(1)指针法。把参数改为指针类型。
- #include <stdio.h>
- void fn(int* x)
- {
- *x += 100;
- }
- void main()
- {
- int a = 20;
- fn(&a);//用取地址符来传递,因为指针是保存地址的
- printf(" a : %d\n", a);
- getchar();
- }
这里要注意,把变量传给指针类型的参数,要使用取地址符&。
那么,这次运行正确吗?
好了,终于看到想要的结果了。
(2)引用法,就是把参数改为&传递的。
- #include <stdio.h>
- void fn(int& x)
- {
- x += 100;
- }
- void main()
- {
- int a = 20;
- fn(a);//直接传变量名就行了
- printf(" a : %d\n", a);
- getchar();
- }
可以看到,这样的运行结果也是正确的。
四、指针与对象
不管是类还是结构(其实结构是一种特殊的类),它们在创建时还是要创建内存的,但是,创建类的对象也有两种方式,直接声明和用指针来分配新实例。
- #include <iostream>
- using namespace std;
- class Test
- {
- public:
- Test();
- ~Test();
- void Do(char* c);
- };
- Test::Test()
- {
- cout << "Test对象被创建。" << endl;
- }
- Test::~Test()
- {
- cout << "Test对象被销毁。" << endl;
- }
- void Test::Do(char* c)
- {
- cout << "在" << c << "中调用了Do方法。" << endl;
- }
- void Func1()
- {
- Test t;
- t.Do("Func1");
- /*
- 当函数执行完了,t的生命周期结束,发生析构。
- */
- }
- void Func2()
- {
- Test* pt = new Test;
- pt -> Do("Func2");
- /*
- 用指针创建的对象,就算指针变量的生命周期结束,但内存中的对象没有被销毁。
- 因此,析构函数没有被调用。
- */
- }
- int main()
- {
- Func1();
- cout << "---------------------" << endl;
- Func2();
- getchar();
- return 0;
- }
我们来看看这个例子,首先定义了一个类Test,在类的构造函数中输出对象被创建的个息,在发生析构时输出对象被销毁。
接着, 我们分别在两个函数中创建Test类的对象,因为对象是在函数内部定义的,根据其生命周期原理,在函数返回时,对象会释放,在内存中的数据会被销毁。理论上是这样的,那么,程序实际运行后会如何呢?
这时候我们发现一个有趣的现象,在第一个函数直接以变量形式创建的对象在函数执行完后被销毁,因为析构函数被调用;可是,我们看到第二个函数中并没有发生这样的事,用指针创建的对象,在函数完成时居然没有调用析构函数。
直接创建对象,变量直接与类实例关联,这样一来,当变量的生命周期结束时,自然会被处理掉,而用指针创建的实例,指针变量本身并不存储该实例的数据,它仅仅是存了对象实例的首地址罢了,指针并没有与实例直接有联系,所以,在第二个函数执行完后,被销毁的是Test*,而不是Test的对象,仅仅是保存首地址的指针被释放了而已,而Test对象依然存在于内存中,因此,在第二个函数完成后,Test的析构函数不会调用,因为它还没死呢。
那么,如何让第二个函数在返回时也销毁对象实例呢?还记得吗,我前文中提过。对,用delete.。
- void Func2()
- {
- Test* pt = new Test;
- pt -> Do("Func2");
- delete pt;
- }
现在看看,是不是在两个函数返回时,都能够销毁对象。
现在你明白了吧?
由此,可以得出一个结论:指针只负责为对象分配和清理内存,并不与内存中的对象实例有直接关系。
补充:
我只是为了说明,指针其实是一个数字,它动态分配内存后,里面存的就是它指向的内存的首地址,但是,指针变量的生命周期终结后,并没有去清理它所指向的内存,而需要显示使用delete。动态new完后,不要忘了delete,不要误解了,与const* 没关系。