1)指向指针的指针
我们已经知道,指针变量是用于储存特定数据类型地址的变量,假如我们定义
int *pInt;
那么,pInt为一个指向整型变量的指针变量。好,我们把前面这句话的主干提取出来,就是:pInt为变量。既然pInt是变量,在内存中就会有与之对应的存放数据的地址值,那么理论上也就应该有对应的指针来存储,嗯,实际上也如此,我们可以向这样来定义可以指向变量pInt的指针:
int **pIntPtr;
按前一章的方法很好理解这样的定义:**pIntPtr是一个int类型,则去掉一个*,*pIntPtr就是指向int的指针,再去一个*,我们最终得到的pIntPtr就是一个“指向int型指针变量的指针变量”,呵呵,是点拗口,不管怎么说我们现在可以写:
pIntPtr = &pInt;
令其指向pInt变量,而*pIntPtr则可以得回pInt变量。假如pInt指向某个整型变量如a,*pInt可以代表a,因此*(*pIntPtr)此时也可以更间接地得到a,当然我们如果省去括号,写成**pIntPtr也是可以的。
以此类推,我们还可以得到int ***p这样的“指向指向指向int型变量的指针的指针的指针”,或者再复杂:int ****p,“指向指向指向指向……”喔,说起来已经很晕了,不过原理摆在这里,自己类比一下即可。
2)指针与常量
C++的常量可以分两种,一种是“文本”常量,比如我们程序中出现的18,3.14,’a’等等;另一种则是用关键字const定义的常量。大多数时候可以把这两种常量视为等同,但还是有一些细微差别,例如,“文本”常量不可直接用&寻找其在内存中对应的地址,但const定义的常量则可以。也就是说,我们不能写&18这样的表达式,但假如我们定义了
const int ClassNumber = 18;
则我们可以通过&ClassNumber表达式得到常量ClassNumber的地址(不是常数18的地址!)。其实在存储特点上常量与变量基本是一样的(有对应的地址,并且在对应地址上存有相应的值),我们可以把常量看作一种“受限”的变量:只可读不可写。既然它们如此相似,而变量有对应的指针,那么常量也应该有其对应的指针。比如,一个指向int型常量的指针pConstInt定义如下:
const int *pConstInt;
它意味着*pConstInt是一个整型常量,因此pConstInt就是一个指向整型常量的指针。我们就可以写
pConstInt = &ClassNumber;
来令pConstInt指向常量ClassNumber. 给你三秒钟,请判断pConstInt是常量还是变量。1,2,3!OK,假如你的回答是变量,那么说明你对常量变量的概念认识得还不错,否则应该翻本C++的书看看const部分的内容。
唔,既然int、float、double甚至我们自己定义的class都可以有对应的常量类型,那么指针应该也有常量才对,现在的问题是,我们应该如何定义一个指针常量呢?我们通常定义常量的作法是在类型名称前面加上const,像const int a等等,但如果在指针定义前面加const,由于*是右结合的,语义上计算机会把const int *p 视为 (const int) (*p)(括号是为了突出其结合形式所用,但不是合法的C++语法),即*p是一个const int型常量,p就为一个指向const int常量的指针。也就是说,我们所加的const并非修饰p,而是修饰*p,换成int const *p又如何呢?噢,这和const int *p没有区别。为了让我们的const能够修饰到p,我们必须越过*号的阻挠将const送到p跟前,假如我们先在前面定义了一个int变量a,则语句
int * const p = &a;
就最终如我们所愿地定义了一个指针常量p,它总是表示a的地址,也就是说,它恒指向变量a.
嗯,小结一下:前面我们讲了两种指针,一种是“指向常量的指针变量”,而之后是“指向变量的指针常量”,它们定义的区别就在于const所修饰的是*p还是p. 同样,还会有“指向常量的指针常量”,显然,必须要有两个const,一个修饰*p,另一个修饰p:
const int * const p = &ClassNumber;
以*为界,我们同样很好理解:*表示我们声明的是指针,它前面的const int表示它指向某个整型常量,后面的const表示它是的个常量指针。为方便区别,许多文章都介绍了“从右到左”读法,其中把“*”读作“指针”:
const int *p1 = &ClassNumber; // p1是一个指针,它指向int型常量
int * const p2 = &a; // p2是一个指针常量,它指向int型变量
const int * const p3 = &ClassNumber; // p3是一个指针常量,它指向int型常量
好了,我们前面定义指针常量时,受到了*号右结合的困扰,使得前置的const修饰不到p,假如*号能与int结合起来(就像前一章所说的“前置派”的理解),成为一种“指向整型指针的类型”,如:
const (int*) p;
const就可以修饰到p了。但C++的括号只能用于改变表达式的优先级而不能改变声明语句的结合次序,能不能想出另一种方法来实现括号的功能呢?答案是肯定的:使用关键字typedef.
typedef的一个主要作用是将多个变量/常量修饰符捆梆起来作为一种混合性的新修饰符,例如要定义一个无符号的整型常量,我们要写
const unsigned int ClassNumber = 18;
但我们也可以先用typedef将“无符号整型常量”定义成一个特定类型:
typedef const unsigned int ConstUInt;
这样我们只须写
ConstUInt ClassNumber = 18;
就可以达到与前面等价的效果。咋看似乎与我们关注的内容没有关系,其实typedef的“捆梆”就相当于加了括号,假如,我们定义:
typedef int * IntPtr;
这意味着什么?这意味着IntPtr是一个“整型指针变量”类型,这可是前面所没有出现过的新复合类型,实际上这才是上章“前置派”所理解的“int*”类型:我们当初即使写
int* p1, p2;
虽然有了空格作为我们视觉上的区分,但不幸的是编译器不吃这一套,仍会把*与p1结合,变成
int (*p1), p2;
所以可怜的p2无依无靠只得成为一个整型变量。但现在我们写
IntPtr p1, p2;
结论就不一样了:有了typedef的捆梆,IntPtr已经成为了名符其实的整型指针类型,所以p1,p2统统成为了货真介实的指针。那么我们写
const IntPtr p;
噢,不好意思,编译出错了:没有初始化常量p……咦,看见了没有?在const IntPtr的修饰下p已经成为指针常量了(而不是const int *p这样的指向常量的指针),哦,明白了,由于typedef的捆梆,const与IntPtr都同心协力地修饰p,即理解为:
(const) (int *) p;
而不是前面的
(const int) (*p);
所以,不要小瞧了typedef,不要随意将它看作是一个简单的宏替换。事实上《C++ Primer》就曾经出了这样的类似考题,大约也是考你:const IntPtr p中的p是指向const int的指针呢还是指向int的指针常量。我知道现在你可以毫不犹豫地正确地回答这个问题了。
BTW:当初第一次看到的时候,我也是毫不犹豫,可惜答错了^_^
3.指针、动态内存、数组
我们上一章谈到变量时已经知道,变量实际上就是编译系统为我们程序分配的一块内存,编译器会将变量名称与这块内存正确地联系起来以供我们方面地读写。设想一下,假如一块这样的存储单元没有“变量名”,我们应该如何访问它呢?噢,如果有这个单元的地址,我们通过*运算符也可以得回该对应的变量。
变量定义可以看作两个功能的实现:1.分配内存;2.将内存与变量名联系起来。
按前面所说,如果知道地址,也可以不需要变量名,所以上两个功能如果变成:1.分配内存;2.将分配所得的内存的地址保存起来;
理论上也可以实现上面的功能。在C++中,我们使用new运算符就可以实现第二种方法。new表达式会为我们分配一适当的内存,并且返会该内存的首地址(确切说应该是一个指针)。在表达式中,关键字new后面通常紧跟着数据类型,以指示分配内存的大小及返回的指针类型,例如new int表达式会为我们分配一块整型变量所需的内存(32位机上通常为4字节),然后这个表达式的值就是一个指向该内存的整型指针值。因此我们可以写:
int *p;
p = new int; // 分配一块用于存储一个整型变量的内存,并将地址赋给指针p
这样我们就可以通过*p来对这块“没有变量名”的内存进行相同的操作。
前面我们仅仅在内存中分配了一个整型存储单元,我们还可以分配一块能存储多个整型值的内存,方法是在int后面加上用“[ ]”括起来的数字,这个数字就是你想分配的单元数目。如:
int *p;
p = new int[18]; // 分配一块用于存储18个整型变量的内存,并将首地址赋给指针p
但这时候我们用*p只能对18个整型单元的第一个进行存取,如何访问其它17个单元呢?由于这些单元都是连续存放的,所以我们只要知道首地址的值以及每个整型变量所占用的空间,就可以计算出其它17个单元的起始地址值。在C++中,我们甚至不必为“每个整形变量所占空间”这样的问题所累,因为C++可以“自动地”为我们实现这一点,我们只需要告诉它我们打算访问的是相对当前指针值的第几个单元就可以了。
这一点通过指针运算可以实现,例如,按前面的声明,现在p已经指向18块存储单元的第一块,如果我想访问第二块,也就是p当前所指的下一块内存呢?很简单,只要写p+1,这个表达式的结果就会神奇地得出第二块内存单元的地址,如果你的机器是32位,那么你感兴趣的话可以打印一下p的地址值与p+1的地址值,你会发现它们之间相差的是4个字节,而不是1个,编译器已经自动为我们做好了转换的工作:它会自动将1乘上指针所指的一个变量(整型变量)所占的内存(4字节)。于是我们如果想要给第二内存单元赋值为3 ,则只须写:
*(p + 1) = 3; // 注意:*号优先级比+号要高,所以要加上括号
要打印的时候就写:
cout << *(p+1); // 输出3
总之这些和一般的变量一样使用没有什么两样了。我们当然也可以将它的地址值赋给另外的指针变量:
int *myPtr;
myPtr = p + 1; // OK,现在myPtr就指向第二内存单元的地址
也可以进行自加操作:
myPtr++; // 按上面的初值,自加后myPtr已经指向第三内存单元的地址
*myPtr = 18; // 现在将第三个内存单元赋予整型值18,也就相当于*(p + 2) = 18
到目前为止一切都很好,但*(p +1)这样的写法太麻烦,C++为此引入了简记的方法,就是“[ ]”运算符(当初定义的时候也用过它哦):要访问第二单元内存,我们只需要写p[1]就可以,它实际上相当于*(p + 1):
p[1] = 3; // *(p + 1) = 3;
cout << p[15]; // cout << *(p + 15);
p[0] = 6; // *(p + 0) = 6; 也就是 *p = 6;
为了说明“[ ]”与*(… + …)的等效性,下面再看一组奇怪的例子:
1[p] = 3; // *(1 + p) = 3;
cout << 15[p]; // cout << *(15 + p);
0[p] = 6; // *(0 + p) = 6; 也就是 *p = 6;
看起来是不是很怪异?其实这一组只不过交换了一下加数位置而已,功能与上一组是完全一样的。
前面我们介绍了一种分配内存的新方法:利用new运算符。new运算符分配的内存除了没有变量分配时附带有的变量名外,它与变量分配还有一个重要的区别:new运算符是在堆(heap)中分配空间,而通常的变量定义是在栈(stack)上分配内存。
堆和栈是程序内存的两大部分,初学可以不必细究其异同,有一点需要明白的是,在栈上分配的内存系统会自动地为其释放,例如在函数结束时,局部变量将不复存在,就是系统自动清除栈内存的结果。但堆中分配的内存则不然:一切由你负责,即使你退出了new表达式的所处的函数或者作用域,那块内存还处于被使用状态而不能再利用。好处就是如果你想在不同模块中共享内存,那么这一点正合你意,坏处是如果你不打算再利用这块内存又忘了把它释放掉,那么它就会霸占你宝贵的内存资源直到你的程序退出为止。
如何释放掉new分配的堆内存?答案是使用delete算符。delete的大概是C++中最简单的部分之一(但也很容易粗心犯错!),你只要分清楚你要释放的是单个单元的内存,还是多个单元的内存,假如:
int *p = new int; // 这里把分配语句与初始化放在一起,效果和前面是一样的
… // 使用*p
delete p; // 释放p所指的内存,即用new分配的内存
如果是多个单元的,则应该是这样:
int *p = new int[18];
… // 使用
delete[] p; // 注意,由于p指向的是一块内存,所以delete后要加“[]”
// 以确保整块内存都被释放,没有“[]”只会释放p指的第一块内存
刚才我们是在堆中分配连续内存,同样,在栈上也可以分配边续内存,例如我们同样要分配18个单元的整型内存空间,并将首地址赋予指针a,则定义如下:
int a[18];
类似于前面用new的版本,系统会在栈上分配18个整型内存单元,并将首地址赋予指针a,我们同样可以通过“[ ]”操作符或者古老的“*(… + …)”来实现对它的访问。需要注意的是a是一个指向整型的指针常量类型,不可以再对a赋值使其指向其它变量。同样,由于是在栈中分配内存,释放工作也不必由我们操心。由于a“看起来”包含了许多个相同类型的变量,因此C++将其称为数组。
由上面看来,栈分配的数组似乎比堆分配要简单好用,但栈分配有一个缺点,就是必须在编译时刻确定内存的大小,也就是说,假如我要写一个排序程序,每次参加排序的元素个数都不一样,但我不能写
int number;
cin >> number;
int a[number]; // 错误,number是变量,而作为栈上分数空间的数组a的大小必须在编译时就决定
但我可以写
int number;
cin >> number;
int *a = new int[number]; // 没有问题,堆空间分配可以在程序运行时才确定
当然最后别忘了释放就成了:
delete[] a;
由于堆内存的分配比栈内存具有更大的灵活性,可以在程序执行期动态决定分配空间的大小,所以又称为动态内存。
C++中动态内存分配引发问题的解决方案http://www.pcdog.com/p/html/2004124/41220041343_1.htm
假设我们要开发一个String类,它可以方便地处理字符串数据。我们可以在类中声明一个数组,考虑到有时候字符串极长,我们可以把数组大小设为200,但一般的情况下又不需要这么多的空间,这样是浪费了内存。对了,我们可以使用new操作符,这样是十分灵活的,但在类中就会出现许多意想不到的问题,本文就是针对这一现象而写的。现在,我们先来开发一个Wrong类,从名称上看出,它是一个不完善的类。的确,我们要刻意地使它出现各种各样的问题,这样才好对症下药。好了,我们开始吧!
Wrong.h:
#ifndef WRONG_H_
#define WRONG_H_
class Wrong
{
private:
char * str; //存储数据
int len; //字符串长度
public:
Wrong(const char * s); //构造函数
Wrong(); // 默认构造函数
~Wrong(); // 析构函数
friend ostream & operator<<(ostream & os,const Wrong& st);
};
#endif
Wrong.cpp:
#include <iostream>
#include <cstring>
#include "wrong.h"
using namespace std;
Wrong::Wrong(const char * s)
{
len = strlen(s);
str = new char[len + 1];
strcpy(str, s);
}//拷贝数据
Wrong::Wrong()
{
len =0;
str = new char[len+1];
str[0]='/0';
}
Wrong::~Wrong()
{
cout<<"这个字符串将被删除:"<<str<<'/n';//为了方便观察结果,特留此行代码。
delete [] str;
}
ostream & operator<<(ostream & os, const Wrong & st)
{
os << st.str;
return os;
}
test_right.cpp:
#include <iostream>
#include <stdlib.h>
#include "Wrong.h"
using namespace std;
int main()
{
Wrong temp("天极网");
cout<<temp<<'/n';
system("PAUSE");
return 0;
}
运行结果:
天极网
请按任意键继续. . .
大家可以看到,以上程序十分正确,而且也是十分有用的。可是,我们不能被表面现象所迷惑!下面,请大家用test_wrong.cpp文件替换test_right.cpp文件进行编译,看看结果。有的编译器可能就是根本不能进行编译!
test_wrong.cpp:
#include <iostream>
#include <stdlib.h>
#include "Wrong.h"
using namespace std;
void show_right(const Wrong&);
void show_wrong(const Wrong);//注意,参数非引用,而是按值传递。
int main()
{
Wrong test1("第一个范例。");
Wrong test2("第二个范例。");
Wrong test3("第三个范例。");
Wrong test4("第四个范例。");
cout<<"下面分别输入三个范例:/n";
cout<<test1<<endl;
cout<<test2<<endl;
cout<<test3<<endl;
Wrong* wrong1=new Wrong(test1);
cout<<*wrong1<<endl;
delete wrong1;
cout<<test1<<endl;//在Dev-cpp上没有任何反应。
cout<<"使用正确的函数:"<<endl;
show_right(test2);
cout<<test2<<endl;
cout<<"使用错误的函数:"<<endl;
show_wrong(test2);
cout<<test2<<endl;//这一段代码出现严重的错误!
Wrong wrong2(test3);
cout<<"wrong2: "<<wrong2<<endl;
Wrong wrong3;
wrong3=test4;
cout<<"wrong3: "<<wrong3<<endl;
cout<<"下面,程序结束,析构函数将被调用。"<<endl;
return 0;
}
void show_right(const Wrong& a)
{
cout<<a<<endl;
}
void show_wrong(const Wrong a)
{
cout<<a<<endl;
}
运行结果:
下面分别输入三个范例:
第一个范例。
第二个范例。
第三个范例。
第一个范例。
这个字符串将被删除:第一个范例。
使用正确的函数:
第二个范例。
第二个范例。
使用错误的函数:
第二个范例。
这个字符串将被删除:第二个范例。
这个字符串将被删除:?=
?=
wrong2: 第三个范例。
wrong3: 第四个范例。
下面,程序结束,析构函数将被调用。
这个字符串将被删除:第四个范例。
这个字符串将被删除:第三个范例。
这个字符串将被删除:?=
这个字符串将被删除:x =
这个字符串将被删除:?=
这个字符串将被删除:
现在,请大家自己试试运行结果,或许会更加惨不忍睹呢!下面,我为大家一一分析原因。
首先,大家要知道,C++类有以下这些极为重要的函数:
一:复制构造函数。
二:赋值函数。
我们先来讲复制构造函数。什么是复制构造函数呢?比如,我们可以写下这样的代码:Wrong test1(test2);这是进行初始化。我们知道,初始化对象要用构造函数。可这儿呢?按理说,应该有声明为这样的构造函数:Wrong(const Wrong &);可是,我们并没有定义这个构造函数呀?答案是,C++提供了默认的复制构造函数,问题也就出在这儿。
(1):什么时候会调用复制构造函数呢?(以Wrong类为例。)
在我们提供这样的代码:Wrong test1(test2)时,它会被调用;当函数的参数列表为按值传递,也就是没有用引用和指针作为类型时,如:void show_wrong(const Wrong),它会被调用。其实,还有一些情况,但在这儿就不列举了。
(2):它是什么样的函数。
它的作用就是把两个类进行复制。拿Wrong类为例,C++提供的默认复制构造函数是这样的:
Wrong(const Wrong& a)
{
str=a.str;
len=a.len;
}
在平时,这样并不会有任何的问题出现,但我们用了new操作符,涉及到了动态内存分配,我们就不得不谈谈浅复制和深复制了。以上的函数就是实行的浅复制,它只是复制了指针,而并没有复制指针指向的数据,可谓一点儿用也没有。打个比方吧!就像一个朋友让你把一个程序通过网络发给他,而你大大咧咧地把快捷方式发给了他,有什么用处呢?我们来具体谈谈:
假如,A对象中存储了这样的字符串:“C++”。它的地址为2000。现在,我们把A对象赋给B对象:Wrong B=A。现在,A和B对象的str指针均指向2000地址。看似可以使用,但如果B对象的析构函数被调用时,则地址2000处的字符串“C++”已经被从内存中抹去,而A对象仍然指向地址2000。这时,如果我们写下这样的代码:cout<<A<<endl;或是等待程序结束,A对象的析构函数被调用时,A对象的数据能否显示出来呢?只会是乱码。而且,程序还会这样做:连续对地址2000处使用两次delete操作符,这样的后果是十分严重的!
本例中,有这样的代码:
Wrong* wrong1=new Wrong(test1);
cout<<*wrong1<<endl;
delete wrong1;
假设test1中str指向的地址为2000,而wrong中str指针同样指向地址2000,我们删除了2000处的数据,而test1对象呢?已经被破坏了。大家从运行结果上可以看到,我们使用cout<<test1时,一点反应也没有。而在test1的析构函数被调用时,显示是这样:“这个字符串将被删除:”。
再看看这段代码:
cout<<"使用错误的函数:"<<endl;
show_wrong(test2);
cout<<test2<<endl;//这一段代码出现严重的错误!
show_wrong函数的参数列表void show_wrong(const Wrong a)是按值传递的,所以,我们相当于执行了这样的代码:Wrong a=test2;函数执行完毕,由于生存周期的缘故,对象a被析构函数删除,我们马上就可以看到错误的显示结果了:这个字符串将被删除:?=。当然,test2也被破坏了。解决的办法很简单,当然是手工定义一个复制构造函数喽!人力可以胜天!
Wrong::Wrong(const Wrong& a)
{
len=a.len;
str=new char(len+1);
strcpy(str,a.str);
}
我们执行的是深复制。这个函数的功能是这样的:假设对象A中的str指针指向地址2000,内容为“I am a C++ Boy!”。我们执行代码Wrong B=A时,我们先开辟出一块内存,假设为3000。我们用strcpy函数将地址2000的内容拷贝到地址3000中,再将对象B的str指针指向地址3000。这样,就互不干扰了。
大家把这个函数加入程序中,问题就解决了大半,但还没有完全解决,问题在赋值函数上。我们的程序中有这样的段代码:
Wrong wrong3;
wrong3=test4;
经过我前面的讲解,大家应该也会对这段代码进行寻根摸底:凭什么可以这样做:wrong3=test4???原因是,C++为了用户的方便,提供的这样的一个操作符重载函数:operator=。所以,我们可以这样做。大家应该猜得到,它同样是执行了浅复制,出了同样的毛病。比如,执行了这段代码后,析构函数开始大展神威^_^。由于这些变量是后进先出的,所以最后的wrong3变量先被删除:这个字符串将被删除:第四个范例。很正常。最后,删除到test4的时候,问题来了:这个字符串将被删除:?=。原因我不用赘述了,只是这个赋值函数怎么写,还有一点儿学问呢!大家请看:
平时,我们可以写这样的代码:x=y=z。(均为整型变量。)而在类对象中,我们同样要这样,因为这很方便。而对象A=B=C就是A.operator=(B.operator=(c))。而这个operator=函数的参数列表应该是:const Wrong& a,所以,大家不难推出,要实现这样的功能,返回值也要是Wrong&,这样才能实现A=B=C。我们先来写写看:
Wrong& Wrong::operator=(const Wrong& a)
{
delete [] str;//先删除自身的数据
len=a.len;
str=new char[len+1];
strcpy(str,a.str);//此三行为进行拷贝
return *this;//返回自身的引用
}
是不是这样就行了呢?我们假如写出了这种代码:A=A,那么大家看看,岂不是把A对象的数据给删除了吗?这样可谓引发一系列的错误。所以,我们还要检查是否为自身赋值。只比较两对象的数据是不行了,因为两个对象的数据很有可能相同。我们应该比较地址。以下是完好的赋值函数:
Wrong& Wrong::operator=(const Wrong& a)
{
if(this==&a)
return *this;
delete [] str;
len=a.len;
str=new char[len+1];
strcpy(str,a.str);
return *this;
}
把这些代码加入程序,问题就完全解决,下面是运行结果:
下面分别输入三个范例:
第一个范例
第二个范例
第三个范例
第一个范例
这个字符串将被删除:第一个范例。
第一个范例
使用正确的函数:
第二个范例。
第二个范例。
使用错误的函数:
第二个范例。
这个字符串将被删除:第二个范例。
第二个范例。
wrong2: 第三个范例。
wrong3: 第四个范例。
下面,程序结束,析构函数将被调用。
这个字符串将被删除:第四个范例。
这个字符串将被删除:第三个范例。
这个字符串将被删除:第四个范例。
这个字符串将被删除:第三个范例。
这个字符串将被删除:第二个范例。
这个字符串将被删除:第一个范例。
关于动态内存分配的问题就介绍到这儿,希望大家都能热爱编程,热爱C++!