虚函数、纯虚函数详解
ios
1.首先:强调一个概念
定义一个函数为虚函数,不表明函数为不被实现的函数。定义他为虚函数是为了容许用基类的指针来调用子类的这个函数。
定义一个函数为纯虚函数,才表明函数没有被实现。定义他是为了实现一个接口,起到一个规范的做用,规范继承这个。类的程序员必须实现这个函数。c++
2.关于实例化一个类:
有纯虚函数的类是不可能生成类对象的,若是没有纯虚函数则能够。好比:
class CA
{
public:
virtual void fun() = 0; // 说明fun函数为纯虚函数
virtual void fun1();
};程序员
class CB
{
public:
virtual void fun();
virtual void fun1();
};web
// CA,CB类的实现
...面试
void main()
{
CA a; // 不容许,由于类CA中有纯虚函数
CB b; // 能够,由于类CB中没有纯虚函数
...
}编程
3.虚函数在多态中间的使用:
多态通常就是经过指向基类的指针来实现的。
多态性(polymorphism)是面向对象程序设计的一个重要特征。若是一种语言只支持类而不支持多态,是不能被称为面向对象语言的,只能说是基于对象的,如Ada、VB就属此类。C++支持多态性,在C++程序设计中可以实现多态性。利用多态性能够设计和实现一个易于扩展的系统。数组
顾名思义,多态的意思是一个事物有多种形态。多态性的英文单词polymorphism来源于希腊词根poly(意为“不少”)和morph(意为“形态”)。在C ++程序设计中,多态性是指具备不一样功能的函数能够用同一个函数名,这样就能够用一个函数名调用不一样内容的函数。在面向对象方法中通常是这样表述多态性的:向不一样的对象发送同一个消息, 不一样的对象在接收时会产生不一样的行为(即方法)。也就是说,每一个对象能够用本身的方式去响应共同的消息。所谓消息,就是调用函数,不一样的行为就是指不一样的实现,即执行不一样的函数。安全
其实,咱们已经屡次接触过多态性的现象,例如函数的重载、运算符重载都是多态现象。只是那时没有用到多态性这一专门术语而已。例如,使用运算符“+”使两个数值相加,就是发送一个消息,它要调用operator +函数。实际上,整型、单精度型、双精度型的加法操做过程是互不相同的,是由不一样内容的函数实现的。显然,它们以不一样的行为或方法来响应同一消息。ide
在现实生活中能够看到许多多态性的例子。如学校校长向社会发布一个消息:9月1日新学年开学。不一样的对象会做出不一样的响应:学生要准备好课本准时到校上课;家长要筹集学费;教师要备好课;后勤部门要准备好教室、宿舍和食堂……因为事先对各类人的任务已做了规定,所以,在获得同一个消息时,各类人都知道本身应当怎么作,这就是 多态性。能够设想,若是不利用多态性,那么校长就要分别给学生、家长、教师、后勤部门等许多不一样的对象分别发通知,分别具体规定每一种人接到通知后应该怎么作。显然这是一件十分复杂而细致的工做。一人包揽一切,吃力还不讨好。如今,利用了多态性机制,校长在发布消息时,没必要一一具体考虑不一样类型人员是怎样执行的。至于各种人员在接到消息后应气作什么,并非临时决定的,而是学校的工做机制事先安排决定好的。校长只需不断发布各类消息,各类人员就会按预约方案有条不紊地工做。函数
一样,在C++程序设计中,在不一样的类中定义了其响应消息的方法,那么使用这些类 时,没必要考虑它们是什么类型,只要发布消息便可。正如在使用运算符“ ”时没必要考虑相加的数值是整型、单精度型仍是双精度型,直接使用“+”,不论哪类数值都能实现相加。能够说这是以不变应万变的方法,不论对象变幻无穷,用户都是用同一形式的信息去调用它们,使它们根据事先的安排做出反应。
从系统实现的角度看,多态性分为两类:静态多态性和动态多态性。之前学过的函数重载和运算符重载实现的多态性属于静态多态性,在程序编译时系统就能决定调用的是哪一个函数,所以静态多态性又称编译时的多态性。静态多态性是经过函数的重载实现的(运算符重载实质上也是函数重载)。动态多态性是在程序运行过程当中才动态地肯定操做所针对的对象。它又称运行时的多态性。动态多态性是经过虚函数(Virtual fiinction)实现的。
有关静态多态性的应用,即函数的重载(请查看:C++函数重载)和运算符重载(请查看:C++运算符重载),已经介绍过了,这里主要介绍动态多态性和虚函数。要研究的问题是:当一个基类被继承为不一样的派生类时,各派生类可使用与基类成员相同的成员名,若是在运行时用同一个成员名调用类对象的成员,会调用哪一个对象的成员?也就是说,经过继承而产生了相关的不一样的派生类,与基类成员同名的成员在不一样的派生类中有不一样的含义。也能够说,多态性是“一个接口,多种 方法”。
下面是一个承上启下的例子。一方面它是有关继承和运算符重载内容的综合应用的例子,经过这个例子能够进一步融会贯通前面所学的内容,另外一方面又是做为讨论多态性的一个基础用例。
但愿你们耐心、深刻地阅读和消化这个程序,弄清其中的每个细节。
[例12.1] 先创建一个Point(点)类,包含数据成员x,y(坐标点)。以它为基类,派生出一个Circle(圆)类,增长数据成员r(半径),再以Circle类为直接基类,派生出一个Cylinder(圆柱体)类,再增长数据成员h(高)。要求编写程序,重载运算符“<<”和“>>”,使之能用于输出以上类对象。
这个例题难度不大,但程序很长。对于一个比较大的程序,应当分红若干步骤进行。先声明基类,再声明派生类,逐级进行,分步调试。
1) 声明基类Point
类可写出声明基类Point的部分以下:
#include <iostream>
//声明类Point
class Point
{
public:
Point(float x=0,float y=0); //有默认参数的构造函数
void setPoint(float ,float); //设置坐标值
float getX( )const {return x;} //读x坐标
float getY( )const {return y;} //读y坐标
friend ostream & operator <<(ostream &,const Point &); //重载运算符“<<”
protected: //受保护成员
float x, y;
};
//下面定义Point类的成员函数
Point::Point(float a,float b) //Point的构造函数
{ //对x,y初始化
x=a;
y=b;
}
void Point::setPoint(float a,float b) //设置x和y的坐标值
{ //为x,y赋新值
x=a;
y=b;
}
//重载运算符“<<”,使之能输出点的坐标
ostream & operator <<(ostream &output, const Point &p)
{
output<<"["<<p.x<<","<<p.y<<"]"<<endl;
return output;
}
以上完成了基类Point类的声明。
为了提升程序调试的效率,提倡对程序分步调试,不要将一个长的程序都写完之后才统一调试,那样在编译时可能会同时出现大量的编译错误,面对一个长的程序,程序人员每每难以迅速准确地找到出错位置。要善于将一个大的程序分解为若干个文件,分别编译,或者分步调试,先经过最基本的部分,再逐步扩充。
如今要对上面写的基类声明进行调试,检查它是否有错,为此要写出main函数。实际上它是一个测试程序。
int main( )
{
Point p(3.5,6.4); //创建Point类对象p
cout<<"x="<<p.getX( )<<",y="<<p.getY( )<<endl; //输出p的坐标值
p.setPoint(8.5,6.8); //从新设置p的坐标值
cout<<"p(new):"<<p<<endl; //用重载运算符“<<”输出p点坐标
return 0;
}
getX和getY函数声明为常成员函数,做用是只容许函数引用类中的数据,而不容许修改它们,以保证类中数据的安全。数据成员x和y声明为protected,这样能够被派生类访问(若是声明为private,派生类是不能访问的)。
程序编译经过,运行结果为:
x=3.5,y=6.4
p(new):[8.5,6.8]
测试程序检查了基类中各函数的功能,以及运算符重载的做用,证实程序是正确的。
2)声明派生类Circle
在上面的基础上,再写出声明派生类Circle的部分:
class Circle:public Point //circle是Point类的公用派生类
{
public:
Circle(float x=0,float y=0,float r=0); //构造函数
void setRadius(float ); //设置半径值
float getRadius( )const; //读取半径值
float area ( )const; //计算圆面积
friend ostream &operator <<(ostream &,const Circle &); //重载运算符“<<”
private:
float radius;
};
//定义构造函数,对圆心坐标和半径初始化
Circle::Circle(float a,float b,float r):Point(a,b),radius(r){}
//设置半径值
void Circle::setRadius(float r){radius=r;}
//读取半径值
float Circle::getRadius( )const {return radius;}
//计算圆面积
float Circle::area( )const
{
return 3.14159*radius*radius;
}
//重载运算符“<<”,使之按规定的形式输出圆的信息
ostream &operator <<(ostream &output,const Circle &c)
{
output<<"Center=["<<c.x<<","<<c.y<<"],r="<<c.radius<<",area="<<c.area( )<<endl;
return output;
}
为了测试以上Circle类的定义,能够写出下面的主函数:
int main( )
{
Circle c(3.5,6.4,5.2); //创建Circle类对象c,并给定圆心坐标和半径
cout<<"original circle:\\nx="<<c.getX()<<", y="<<c.getY()<<", r="<<c.getRadius( )<<", area="<<c.area( )<<endl; //输出圆心坐标、半径和面积
c.setRadius(7.5); //设置半径值
c.setPoint(5,5); //设置圆心坐标值x,y
cout<<"new circle:\\n"<<c; //用重载运算符“<<”输出圆对象的信息
Point &pRef=c; //pRef是Point类的引用变量,被c初始化
cout<<"pRef:"<<pRef; //输出pRef的信息
return 0;
}
程序编译经过,运行结果为:
original circle:(输出原来的圆的数据)
x=3.5, y=6.4, r=5.2, area=84.9486
new circle:(输出修改后的圆的数据)
Center=[5,5], r=7.5, area=176.714
pRef:[5,5] (输出圆的圆心“点”的数据)
能够看到,在Point类中声明了一次运算符“ <<”重载函数,在Circle类中又声明了一次运算符“ <<”,两次重载的运算符“<<”内容是不一样的,在编译时编译系统会根据输出项的类型肯定调用哪个运算符重载函数。main函数第7行用“cout<< ”输出c,调用的是在Circle类中声明的运算符重载函数。
请注意main函数第8行:
Point & pRef = c;
定义了 Point类的引用变量pRef,并用派生类Circle对象c对其初始化。前面咱们已经讲过,派生类对象能够替代基类对象为基类对象的引用初始化或赋值(详情请查看:C++基类与派生类的转换)。如今 Circle是Point的公用派生类,所以,pRef不能认为是c的别名,它获得了c的起始地址, 它只是c中基类部分的别名,与c中基类部分共享同一段存储单元。因此用“cout<<pRef”输出时,调用的不是在Circle中声明的运算符重载函数,而是在Point中声明的运算符重载函数,输出的是“点”的信息,而不是“圆”的信息。
3) 声明Circle的派生类Cylinder
前面已从基类Point派生出Circle类,如今再从Circle派生出Cylinder类。
class Cylinder:public Circle// Cylinder是Circle的公用派生类
{
public:
Cylinder (float x=0,float y=0,float r=0,float h=0); //构造函数
void setHeight(float ); //设置圆柱高
float getHeight( )const; //读取圆柱高
loat area( )const; //计算圆表面积
float volume( )const; //计算圆柱体积
friend ostream& operator <<(ostream&,const Cylinder&); //重载运算符<<
protected:
float height;//圆柱高
};
//定义构造函数
Cylinder::Cylinder(float a,float b,float r,float h):Circle(a,b,r),height(h){}
//设置圆柱高
void Cylinder::setHeight(float h){height=h;}
//读取圆柱高
float Cylinder::getHeight( )const {return height;}
//计算圆表面积
float Cylinder::area( )const { return 2*Circle::area( )+2*3.14159*radius*height;}
//计算圆柱体积
float Cylinder::volume()const {return Circle::area()*height;}
ostream &operator <<(ostream &output,const Cylinder& cy)
{
output<<"Center=["<<cy.x<<","<<cy.y<<"],r="<<cy.radius<<",h="<<cy.height <<"\\narea="<<cy.area( )<<", volume="<<cy.volume( )<<endl;
return output;
} //重载运算符“<<”
能够写出下面的主函数:
int main( )
{
Cylinder cy1(3.5,6.4,5.2,10);//定义Cylinder类对象cy1
cout<<"\\noriginal cylinder:\\nx="<<cy1.getX( )<<", y="<<cy1.getY( )<<", r="
<<cy1.getRadius( )<<", h="<<cy1.getHeight( )<<"\\narea="<<cy1.area()
<<",volume="<<cy1.volume()<<endl;//用系统定义的运算符“<<”输出cy1的数据
cy1.setHeight(15);//设置圆柱高
cy1.setRadius(7.5);//设置圆半径
cy1.setPoint(5,5);//设置圆心坐标值x,y
cout<<"\\nnew cylinder:\\n"<<cy1;//用重载运算符“<<”输出cy1的数据
Point &pRef=cy1;//pRef是Point类对象的引用变量
cout<<"\\npRef as a Point:"<<pRef;//pRef做为一个“点”输出
Circle &cRef=cy1;//cRef是Circle类对象的引用变量
cout<<"\\ncRef as a Circle:"<<cRef;//cRef做为一个“圆”输出
return 0;
}
运行结果以下:
original cylinder:(输出cy1的初始值)
x=3.5, y=6.4, r=5.2, h=10 (圆心坐标x,y。半径r,高h)
area=496.623, volume=849.486 (圆柱表面积area和体积volume)
new cylinder: (输出cy1的新值)
Center=[5,5], r=7.5, h=15 (以[5,5]形式输出圆心坐标)
area=1060.29, volume=2650.72(圆柱表面积area和体积volume)
pRef as a Point:[5,5] (pRef做为一个“点”输出)
cRef as a Circle:Center=[5,5], r=7.5, area=176.714(cRef做为一个“圆”输出)
说明:在Cylinder类中定义了 area函数,它与Circle类中的area函数同名,根据前面咱们讲解的同名覆盖的原则(详情请查看:C++多重继承的二义性问题),cy1.area( ) 调用的是Cylinder类的area函数(求圆柱表面积),而不是Circle类的area函数(圆面积)。请注意,这两个area函数不是重载函数,它们不只函数名相同,并且函数类型和参数个数都相同,两个同名函数不在同 —个类中,而是分别在基类和派生类中,属于同名覆盖。重载函数的参数个数和参数类型必须至少有一者不一样,不然系统没法肯定调用哪个函数。
main函数第9行用“cout<<cy1”来输出cy1,此时调用的是在Cylinder类中声明的重载运算符“<<”,按在重载时规定的方式输出圆柱体cy1的有关数据。
main函数中最后4行的含义与在定义Circle类时的状况相似。pRef是Point类的引用变量,用cy1对其初始化,但它不是cy1的别名,只是cy1中基类Point部分的别名,在输出pRef时是做为一个Point类对象输出的,也就是说,它是一个“点”。一样,cRef是Circle类的引用变量,用cy1对其初始化,但它只是cy1中的直接基类Circle部分的别名, 在输出 cRef 时是做为Circle类对象输出的,它是一个"圆”,而不是一个“圆柱体”。从输 出的结果能够看出调用的是哪一个运算符函数。
在本例中存在静态多态性,这是运算符重载引发的(注意3个运算符函数是重载而不是同名覆盖,由于有一个形参类型不一样)。能够看到,在编译时编译系统便可以断定应调用哪一个重载运算符函数。
4.有一点你必须明白,就是用父类的指针在运行时刻来调用子类:
例如,有个函数是这样的:
void animal::fun1(animal *maybedog_maybehorse)
{
maybedog_maybehorse->born();
}
参数maybedog_maybehorse在编译时刻并不知道传进来的是dog类仍是horse类,因此就把它设定为animal类,具体到运行时决定了才决定用那个函数。也就是说用父类指针经过虚函数来决定运行时刻究竟是谁而指向谁的函数。
5.用虚函数
#include <iostream.h>
class animal
{
public:
animal();
~animal();
void fun1(animal *maybedog_maybehorse);
virtual void born();
};
void animal::fun1(animal *maybedog_maybehorse)
{
maybedog_maybehorse->born();
}
animal::animal() { }
animal::~animal() { }
void animal::born()
{
cout<< "animal";
}
///////////////////////horse
class horse:public animal
{
public:
horse();
~horse();
virtual void born();
};
horse::horse() { }
horse::~horse() { }
void horse::born()
{
cout<<"horse";
}
///////////////////////main
void main()
{
animal a;
horse b;
a.fun1(&b);
}
上面定义的horse类是基类animal的派生,animal是他的继承现讲解继承和派生
假设已经声明了一个基类Student(基类Student的定义见上节:C++继承与派生的概念),在此基础上经过单继承创建一个派生类Student1:
class Student1: public Student //声明基类是Student
{
public:
void display_1( ) //新增长的成员函数
{
cout<<"age: "<<age<<endl;
cout<<"address: "<<addr<<endl;
}
private:
int age; //新增长的数据成员
string addr; //新增长的数据成员
};
仔细观察第一行:
class Student1: public Student
在class后面的Student1是新建的类名,冒号后面的Student表示是已声明的基类。在Student以前有一关键宇public,用来表示基类Student中的成员在派生类Studeml中的继承方式。基类名前面有public的称为“公用继承(public inheritance)”。
请你们仔细阅读以上声明的派生类Student1和基类Student,并将它们放在一块儿进行分析。
声明派生类的通常形式为:
class 派生类名:[继承方式] 基类名
{
派生类新增长的成员
};
继承方式包括public (公用的)、private (私有的)和protected(受保护的),此项是可选的,若是不写此项,则默认为private(私有的)。
采用公用继承方式时,基类的公用成员和保护成员在派生类中仍然保持其公用成员和保护成员的属性,而基类的私有成员在派生类中并无成为派生类的私有成员,它仍然是基类的私有成员,只有基类的成员函数能够引用它,而不能被派生类的成员函数引用,所以就成为派生类中的不可访问的成员。
私有基类的公用成员和保护成员在派生类中的访问属性至关于派生类中的私有成员,即派生类的成员函数能访问它们,而在派生类外不能访问它们。私有基类的私有成员在派生类中成为不可访问的成员,只有基类的成员函数能够引用它们。一个基类成员在基类中的访问属性和在派生类中的访问属性多是不一样的。
//output: horse
6.不用虚函数
#include <iostream.h>
class animal
{
public:
animal();
~animal();
void fun1(animal *maybedog_maybehorse);
void born();
};
void animal::fun1(animal *maybedog_maybehorse)
{
maybedog_maybehorse->born();
}
animal::animal() { }
animal::~animal() { }
void animal::born()
{
cout<< "animal";
}
////////////////////////horse
class horse:public animal
{
public:
horse();
~horse();
void born();
};
horse::horse() { }
horse::~horse() { }
void horse::born()
{
cout<<"horse";
}
////////////////////main
void main()
{
animal a;
horse b;
a.fun1(&b);
}
//output: animal
运算符重载
运算符重载的方法是定义一个重载运算符的函数,在须要执行被重载的运算符时,系统就自动调用该函数,以实现相应的运算。也就是说,运算符重载是经过定义函数实现的。运算符重载实质上是函数的重载。
重载运算符的函数通常格式以下:
函数类型 operator 运算符名称 (形参表列)
{
// 对运算符的重载处理
}
例如,想将”+”用于Complex类(复数)的加法运算,函数的原型能够是这样的:
Complex operator+ (Complex& c1, Complex& c2);
在上面的通常格式中,operator是关键字,是专门用于定义重载运算符的函数的,运算符名称就是C++提供给用户的预约义运算符。注意,函数名是由operator和运算符组成,上面的operator+就是函数名,意思是“对运算符+重载”。只要掌握这点,就能够发现,这 类函数和其余函数在形式上没有什么区别。两个形参是Complex类对象的引用,要求实参为Complex类对象。
在定义了重载运算符的函数后,能够说,函数operator +重载了运算符+。在执行复数相加的表达式c1 + c2时(假设c1和c2都已被定义为Complex类对象),系统就会调用operator+函数,把c1和c2做为实参,与形参进行虚实结合。
为了说明在运算符重载后,执行表达式就是调用函数的过程,能够把两个整数相加也想像为调用下面的函数:
int operator + (int a, int b)
{
return (a+b);
}
若是有表达式5+8,就调用此函数,将5和8做为调用函数时的实参,函数的返回值为13。这就是用函数的方法理解运算符。能够在例10.1程序的基础上重载运算符“+”,使之用于复数相加。
[例10.2] 改写例10.1,重载运算符“+”,使之能用于两个复数相加。
#include <iostream>
using namespace std;
class Complex
{
public:
Complex( ){real=0;imag=0;}
Complex(double r,double i){real=r;imag=i;}
Complex operator+(Complex &c2);//声明重载运算符的函数
void display( );
private:
double real;
double imag;
};
Complex Complex::operator+(Complex &c2) //定义重载运算符的函数
{
Complex c;
c.real=real+c2.real;
c.imag=imag+c2.imag;
return c;
}
void Complex::display( )
{
cout<<"("<<real<<","<<imag<<"i)"<<endl;
}
int main( )
{
Complex c1(3,4),c2(5,-10),c3;
c3=c1+c2; //运算符+用于复数运算
cout<<"c1=";c1.display( );
cout<<"c2=";c2.display( );
cout<<"c1+c2=";c3.display( );
return 0;
}
运行结果与例10.1相同:
c1=(3+4i)
c2=(5-10i)
c1+c2=(8,-6i)
请比较例10.1和例10.2,只有两处不一样:
1) 在例10.2中以operator+函数取代了例10.1中的complex_add函数,并且只是函数名不一样,函数体和函数返回值的类型都是相同的。
2) 在main函数中,以“c3=c1+c2;”取代了例10.1中的“c3=c1.complex_add(c2);”。在将运算符+重载为类的成员函数后,C++编译系统将程序中的表达式c1+c2解释为
c1.operator+(c2) //其中c1和c2是Complex类的对象
即以c2为实参调用c1的运算符重载函数operator+(Complex &c2),进行求值,获得两个复数之和。
能够看到,两个程序的结构和执行过程基本上是相同的,做用相同,运行结果也相同。重载运算符是由相应的函数实现的。有人可能说,既然这样,何须对运算符重载呢?咱们要从用户的角度来看问題,虽然重载运算符所实现的功能彻底能够用函数实现,可是使用运算符重载能使用户程序易于编写、阅读和维护。在实际工做中,类的声明和类的使用每每是分离的。假如在声明Complex类时,对运算符+, -, *, /都进行了重载,那么使用这个类的用户在编程时能够彻底不考虑函数是怎么实现的,放心大胆地直接使用+, -, *, /进行复数的运算便可,十分方便。
对上面的运算符重载函数operator+还能够改写得更简练一些:
Complex Complex::operator + (Complex &c2)
{return Complex(real+c2.real, imag+c2.imag);}
return语句中的Complex( real+c2.real, imag+c2.imag)是创建一个临时对象,它没有对名,是一个无名对象。在创建临时对象过程当中调用构造函数。return语句将此临时对象做为函数返回值。
请思考,在例10.2中可否将一个常量和一个复数对象相加?如
c3=3+c2; //错误,与形参类型不匹配
应写成对象形式,如
c3 = Complex (3,0) +c2; //正确,类型均为对象
须要说明的是,运算符被重载后,其原有的功能仍然保留,没有丧失或改变。经过运算符重载,扩大了C++已有运算符的做用范围,使之能用于类对象。
运算符重载对C++有重要的意义,把运算符重载和类结合起来,能够在C++程序中定义出颇有实用意义而使用方便的新的数据类型。运算符重载使C++具备更强大的功能、更好的可扩充性和适应性,这是C++最吸引人的特色之一。
引用
C++中的引用
【导读】介绍C++引用的基本概念,经过详细的应用分析与说明,对引用进行全面、透彻地阐述
引用是C++引入的新语言特性,是C++经常使用的一个重要内容之一,正确、灵活地使用引用,可使程序简洁、高效。
引用简介
引用就是某一变量(目标)的一个别名,对引用的操做与对变量直接操做彻底同样。
引用的声明方法:类型标识符 &引用名=目标变量名;
【例1】:int a; int &ra=a; //定义引用ra,它是变量a的引用,即别名
说明:
(1)&在此不是求地址运算,而是起标识做用。
(2)类型标识符是指目标变量的类型。
(3)声明引用时,必须同时对其进行初始化。
(4)引用声明完毕后,至关于目标变量名有两个名称,即该目标原名称和引用名,且不能再把该引用名做为其余变量名的别名。
ra=1; 等价于 a=1;
(5)声明一个引用,不是新定义了一个变量,它只表示该引用名是目标变量名的一个别名,它自己不是一种数据类型,所以引用自己不占存储单元,系统也不给引用分配存储单元。故:对引用求地址,就是对目标变量求地址。&ra与&a相等。
(6)不能创建数组的引用。由于数组是一个由若干个元素所组成的集合,因此没法创建一个数组的别名。
引用应用
一、引用做为参数
引用的一个重要做用就是做为函数的参数。之前的C语言中函数参数传递是值传递,若是有大块数据做为参数传递的时候,采用的方案每每是指针,由于这样能够避免将整块数据所有压栈,能够提升程序的效率。可是如今(C++中)又增长了一种一样有效率的选择(在某些特殊状况下又是必须的选择),就是引用。
【例2】:
void swap(int &p1, int &p2) //此处函数的形参p1, p2都是引用
{ int p; p=p1; p1=p2; p2=p; }
为在程序中调用该函数,则相应的主调函数的调用点处,直接以变量做为实参进行调用便可,而不须要实参变量有任何的特殊要求。如:对应上面定义的swap函数,相应的主调函数可写为:
main( )
{
int a,b;
cin>>a>>b; //输入a,b两变量的值
swap(a,b); //直接以变量a和b做为实参调用swap函数
cout<<a<< ' ' <<b; //输出结果
}
上述程序运行时,若是输入数据10 20并回车后,则输出结果为20 10。
由【例2】可看出:
(1)传递引用给函数与传递指针的效果是同样的。这时,被调函数的形参就成为原来主调函数中的实参变量或对象的一个别名来使用,因此在被调函数中对形参变量的操做就是对其相应的目标对象(在主调函数中)的操做。
(2)使用引用传递函数的参数,在内存中并无产生实参的副本,它是直接对实参操做;而使用通常变量传递函数的参数,当发生函数调用时,须要给形参分配存储单元,形参变量是实参变量的副本;若是传递的是对象,还将调用拷贝构造函数。所以,当参数传递的数据较大时,用引用比用通常变量传递参数的效率和所占空间都好。
(3)使用指针做为函数的参数虽然也能达到与使用引用的效果,可是,在被调函数中一样要给形参分配存储单元,且须要重复使用"*指针变量名"的形式进行运算,这很容易产生错误且程序的阅读性较差;另外一方面,在主调函数的调用点处,必须用变量的地址做为实参。而引用更容易使用,更清晰。
若是既要利用引用提升程序的效率,又要保护传递给函数的数据不在函数中被改变,就应使用常引用。
二、常引用
常引用声明方式:const 类型标识符 &引用名=目标变量名;
用这种方式声明的引用,不能经过引用对目标变量的值进行修改,从而使引用的目标成为const,达到了引用的安全性。
【例3】:
int a ;
const int &ra=a;
ra=1; //错误
a=1; //正确
这不光是让代码更健壮,也有些其它方面的须要。
【例4】:假设有以下函数声明:
string foo( );
void bar(string & s);
那么下面的表达式将是非法的:
bar(foo( ));
bar("hello world");
缘由在于foo( )和"hello world"串都会产生一个临时对象,而在C++中,这些临时对象都是const类型的。所以上面的表达式就是试图将一个const类型的对象转换为非const类型,这是非法的。
引用型参数应该在能被定义为const的状况下,尽可能定义为const 。
三、引用做为返回值
要以引用返回函数值,则函数定义时要按如下格式:
类型标识符 &函数名(形参列表及类型说明)
{函数体}
说明:
(1)以引用返回函数值,定义函数时须要在函数名前加&
(2)用引用
返回一个函数值的最大好处是,在内存中不产生被返回值的副本。
【例5】如下程序中定义了一个普通的函数fn1(它用返回值的方法返回函数值),另一个函数fn2,它以引用的方法返回函数值。
#i nclude <iostream.h>
float temp; //定义全局变量temp
float fn1(float r); //声明函数fn1
float &fn2(float r); //声明函数fn2
float fn1(float r) //定义函数fn1,它以返回值的方法返回函数值
{
temp=(float)(r*r*3.14);
return temp;
}
float &fn2(float r) //定义函数fn2,它以引用方式返回函数值
{
temp=(float)(r*r*3.14);
return temp;
}
void main() //主函数
{
float a=fn1(10.0); //第1种状况,系统生成要返回值的副本(即临时变量)
float &b=fn1(10.0); //第2种状况,可能会出错(不一样 C++系统有不一样规定)
//不能从被调函数中返回一个临时变量或局部变量的引用
float c=fn2(10.0); //第3种状况,系统不生成返回值的副本
//能够从被调函数中返回一个全局变量的引用
float &d=fn2(10.0); //第4种状况,系统不生成返回值的副本
//能够从被调函数中返回一个全局变量的引用
cout<<a<<c<<d;
}
引用做为返回值,必须遵照如下规则:
(1)不能返回局部变量的引用。这条能够参照Effective C++[1]的Item 31。主要缘由是局部变量会在函数返回后被销毁,所以被返回的引用就成为了"无所指"的引用,程序会进入未知状态。
(2)不能返回函数内部new分配的内存的引用。这条能够参照Effective C++[1]的Item 31。虽然不存在局部变量的被动销毁问题,可对于这种状况(返回函数内部new分配内存的引用),又面临其它尴尬局面。例如,被函数返回的引用只是做为一个临时变量出现,而没有被赋予一个实际的变量,那么这个引用所指向的空间(由new分配)就没法释放,形成memory leak。
(3)能够返回类成员的引用,但最好是const。这条原则能够参照Effective C++[1]的Item 30。主要缘由是当对象的属性是与某种业务规则(business rule)相关联的时候,其赋值经常与某些其它属性或者对象的状态有关,所以有必要将赋值操做封装在一个业务规则当中。若是其它对象能够得到该属性的很是量引用(或指针),那么对该属性的单纯赋值就会破坏业务规则的完整性。
(4)引用与一些操做符的重载:
流操做符<<和>>,这两个操做符经常但愿被连续使用,例如:cout << "hello" << endl; 所以这两个操做符的返回值应该是一个仍然支持这两个操做符的流引用。可选的其它方案包括:返回一个流对象和返回一个流对象指针。可是对于返回一个流对象,程序必须从新(拷贝)构造一个新的流对象,也就是说,连续的两个<<操做符其实是针对不一样对象的!这没法让人接受。对于返回一个流指针则不能连续使用<<操做符。所以,返回一个流对象引用是唯一选择。这个惟一选择很关键,它说明了引用的重要性以及无可替代性,也许这就是C++语言中引入引用这个概念的缘由吧。 赋值操做符=。这个操做符象流操做符同样,是能够连续使用的,例如:x = j = 10;或者(x=10)=100;赋值操做符的返回值必须是一个左值,以即可以被继续赋值。所以引用成了这个操做符的唯一返回值选择。
【例6】 测试用返回引用的函数值做为赋值表达式的左值。
#i nclude <iostream.h>
int &put(int n);
int vals[10];
int error=-1;
void main()
{
put(0)=10; //以put(0)函数值做为左值,等价于vals[0]=10;
put(9)=20; //以put(9)函数值做为左值,等价于vals[9]=10;
cout<<vals[0];
cout<<vals[9];
}
int &put(int n)
{
if (n>=0 && n<=9 ) return vals[n];
else { cout<<"subscript error"; return error; }
}
(5)在另外的一些操做符中,却千万不能返回引用:+-*/ 四则运算符。它们不能返回引用,Effective C++[1]的Item23详细的讨论了这个问题。主要缘由是这四个操做符没有side effect,所以,它们必须构造一个对象做为返回值,可选的方案包括:返回一个对象、返回一个局部变量的引用,返回一个new分配的对象的引用、返回一个静态对象引用。根据前面提到的引用做为返回值的三个规则,第二、3两个方案都被否决了。静态对象的引用又由于((a+b) == (c+d))会永远为true而致使错误。因此可选的只剩下返回一个对象了。
四、引用和多态
引用是除指针外另外一个能够产生多态效果的手段。这意味着,一个基类的引用能够指向它的派生类实例。
【例7】:
class A;
class B:public A{……};
B b;
A &Ref = b; // 用派生类对象初始化基类对象的引用
Ref 只能用来访问派生类对象中从基类继承下来的成员,是基类引用指向派生类。若是A类中定义有虚函数,而且在B类中重写了这个虚函数,就能够经过Ref产生多态效果。
引用总结
(1)在引用的使用中,单纯给某个变量取个别名是毫无心义的,引用的目的主要用于在函数参数传递中,解决大块数据或对象的传递效率和空间不如意的问题。
(2)用引用传递函数的参数,能保证参数传递中不产生副本,提升传递的效率,且经过const的使用,保证了引用传递的安全性。
(3)引用与指针的区别是,指针经过某个指针变量指向一个对象后,对它所指向的变量间接操做。程序中使用指针,程序的可读性差;而引用自己就是目标变量的别名,对引用的操做就是对目标变量的操做。
(4)使用引用的时机。流操做符<<和>>、赋值操做符=的返回值、拷贝构造函数的参数、赋值操做符=的参数、其它状况都推荐使用引用。
什么是变量的引用
对一个数据可使用“引用(reference)”,这是C++对C的一个重要扩充,引用是一种新的变量类型,它的做用是为一个变量起一个别名。假若有一个变量a,想给它起一个别名b,能够这样写:
int a; //定义a是整型变量
int &b=a; //声明b是a的引用
以上语句声明了b是a的引用,即b是a的别名。通过这样的声明后,a或b的做用相同,都表明同一变量。
注意: 在上述声明中,&是引用声明符,并不表明地址。不要理解为“把a的值赋给b的地址”。声明变量b为引用类型,并不须要另外开辟内存单元来存放b的值。b和a占内存中的同一个存储单元,它们具备同一地址。声明b是a的引用,能够理解为: 使变量b具备变量a的地址。见图6.26,若是a的值是20,则b的值也是20。
图6.26
在声明一个引用类型变量时,必须同时使之初始化,即声明它表明哪个变量。在声明变量b是变量a的引用后,在它们所在函数执行期间,该引用类型变量b始终与其表明的变量a相联系,不能再做为其余变量的引用(别名)。下面的用法不对:
int a1, a2;
int &b=a1;
int &b=a2; //企图使b又变成a2的引用(别名)是不行的
引用的简单使用
【例6.17】引用和变量的关系。
#include <iostream>
#include <iomanip>
using namespace std;
int main( )
{
int a=10;
int &b=a; //声明b是a的引用
a=a*a; //a的值变化了,b的值也应一块儿变化
cout<<a<<setw(6)<<b<<endl;
b=b/5; //b的值变化了,a的值也应一块儿变化
cout<<b<<setw(6)<<a<<endl;
return 0;
}
a的值开始为10,b是a的引用,它的值固然也应该是10,当a的值变为100(a*a的值)时,b的值也随之变为100。在输出a和b的值后,b的值变为20,显然a的值也应为20。运行记录以下:
100 100 (a和b的值都是100)
20 20 (a和b的值都是20)
对象引用
在程序中常常须要访问对象中的成员。访问对象中的成员能够有3种方法:
经过对象名和成员运算符访问对象中的成员;
经过指向对象的指针访问对象中的成员;
经过对象的引用变量访问对象中的成员。
经过对象名和成员运算符访问对象中的成员
例如在程序中能够写出如下语句:
stud1.num=1001; //假设num已定义为公用的整型数据成员
表示将整数1001赋给对象stud1中的数据成员num。其中“.”是成员运算符,用来对成员进行限定,指明所访问的是哪个对象中的成员。注意不能只写成员名而忽略对象名。
访问对象中成员的通常形式为:
对象名.成员名
不只能够在类外引用对象的公用数据成员,并且还能够调用对象的公用成员函数,但一样必须指出对象名,如:
stud1.display( ); //正确,调用对象stud1的公用成员函数
display( ); //错误,没有指明是哪个对象的display函数
因为没有指明对象名,编译时把display做为普通函数处理。应该注意所访问的成员是公用的(public )仍是私有的(private ),只能访问public成员,而不能访问private成员。若是已定义num为私有数据成员,下面的语句是错误的:
stud1.num=10101; //num是私有数据成员,不能被外界引用
在类外只能调用公用的成员函数。在一个类中应当至少有一个公用的成员函数,做为对外的接口,不然就没法对对象进行任何操做。
经过指向对象的指针访问对象中的成员
前面已经介绍了指向结构体变量的指针(详情请猛击:指向结构体变量的指针),能够经过指针引用结构体中的成员。用指针访问对象中的成员的方法与此相似。若是有如下程序段:
class Time
{
public : //数据成员是公用的
int hour;
int minute;
};
Time t, *p; //定义对象t和指针变量p
p=&t; //使p指向对象t
cout<<p->hour; //输出p指向的对象中的成员hour
在p指向t的前提下,p->hour,(*p).hour和t.hour三者等价。
经过对象的引用变量来访问对象中的成员
若是为一个对象定义了一个引用变量,它们是共占同一段存储单元的,实际上它们是同一个对象,只是用不一样的名字表示而已。所以彻底能够经过引用变量来访问对象中的成员。
若是已声明了Time类,并有如下定义语句:
Time t1; //定义对象t1
Time &t2=t1; //定义Time类引用变量t2,并使之初始化为t1
cout<<t2.hour; //输出对象t1中的成员hour
因为t2与t1共占同一段存储单元(即t2是t1的别名),所以t2.hour就是t1.hour。
结构体变量引用
在定义告终构体变量之后,固然能够引用这个变量,经常使用的方法有如下几种。
1) 能够将一个结构体变量的值赋给另外一个具备相同结构的结构体变量。
如上面的student1和student2都是student类型的变量,能够这样赋值:
student1= student2;
2) 能够引用一个结构体变量中的一个成员的值。
例如, student1.num表示结构体变量student1中的成员的值,若是student1的值如图7.2所示,则student1.num的值为10001。
引用结构体变量中成员的通常方式为:
结构体变量名.成员名
例如能够这样对变量的成员赋值:
student1.num=10010;
3) 若是成员自己也是一个结构体类型,则要用若干个成员运算符,一级一级地找到最低一级的成员。
例如,对上面定义的结构体变量student1,能够这样访问各成员:
student1.num (引用结构体变量student1中的num成员)
若是想引用student1变量中的birthday成员中的month成员,不能写成student1.month,必须逐级引用,即
student1.birthday.month=12; (引用结构体变量student1中的birthday成员中的month成员)
4) 不能将一个结构体变量做为一个总体进行输入和输出。
例如,已定义student1和student2为结构体变量,而且它们已有值。不能企图这样输出结构体变量中的各成员的值
cin>>student1;
只能对结构体变量中的各个成员分别进行输入和输出。
5) 对结构体变量的成员能够像普通变量同样进行各类运算(根据其类型决定能够进行的运算种类)。例如:
student2.score=student1.score;
sum=student1.score+student2.score;
student1.age++;
++student1.age;
因为“.”运算符的优先级最高,student1.age++至关于(student1.age)++ 。++是对student1.age进行自加运算,而不是先对age进行自加运算。
6) 能够引用结构体变量成员的地址,也能够引用结构体变量的地址。如:
cout<<&student1; //输出student1的首地址
cout<<&student1.age; //输出student1.age的地址
结构体变量的地址主要用做函数参数,将结构体变量的地址传递给形参。
【例7.1】引用结构体变量中的成员。
#include <iostream>
using namespace std;
struct Date//声明结构体类型Date
{
int month;
int day;
int year;
};
struct Student//声明结构体类型Student
{
int num;
char name[20];
char sex;
Date birthday; //声明birthday为Date类型的成员
float score;
}student1,student2={10002,"Wang Li",'f',5,23,1982,89.5};
//定义Student 类型的变量student1,student2,并对student2初始化
int main( )
{
student1=student2; //将student2各成员的值赋予student1的相应成员
cout<<student1.num<<endl; //输出student1中的num成员的值
cout<<student1.name<<endl; //输出student1中的name成员的值
cout<<student1.sex<<endl; //输出student1中的sex成员的值
cout<<student1.birthday.month<<'/'<<student1.birthday.day<<'/' <<student1.birthday.year<<endl; //输出student1中的birthday各成员的值
cout<<student1.score<<endl;
return 0;
}
运行结果以下:
10002
Wang Li
f
5/23/1982
89.5
关于引用和指针的区别的文章不少不少,可是老是找不到他们的根本区别,偶然在codeproject上看到这篇文章,以为讲的挺好的,
因此翻译了下,但愿对你们有帮助。
原文地址: http://www.codeproject.com/KB/cpp/References_in_c__.aspx
引言
我选择写 C++ 中的引用是由于我感受大多数人误解了引用。而我之因此有这个感觉是由于我主持过不少 C++ 的面试,而且我不多从面试者中获得关于 C++ 引用的正确答案。
那么 c++ 中引用到底意味这什么呢?一般一个引用让人想到是一个引用的变量的别名,而我讨厌将 c++ 中引用定义为变量的别名。这篇文章中,我将尽可能解释清楚, c++ 中根本就没有什么叫作别名的东东。
背景
在 c/c++ 中,访问一个变量只能经过两种方式被访问,传递,或者查询。这两种方式是:
1. 经过值 访问 / 传递变量
2. 经过地址 访问 / 传递变量 – 这种方法就是指针
除此以外没有第三种访问和传递变量值的方法。引用变量也就是个指针变量,它也拥有内存空间。最关键的是引用是一种会被编译器自动解引用的指针。很难相信么?让咱们来看看吧。。。
下面是一段使用引用的简单 c++ 代码
引用其实就是 c++ 中的常量指针。表达式 int &i = j; 将会被编译器转化成 int *const i = &j; 而引用之因此要初始化是由于 const 类型变量必须初始化,这个指针也必须有所指。下面咱们再次聚焦到上面这段代码,并使用编译器的那套语法将引用替换掉。
读者必定很奇怪为何我上面这段代码会跳过打印地址这步。这里须要一些解释。由于引用变量时会被编译器自动解引用的,那么一个诸如 cout << &j << endl; 的语句,编译器就会将其转化成语句 cout << &*j << endl; 如今 &* 会相互抵消,这句话变的毫无心义,而 cout 打印的 j 值就是 i 的地址,由于其定义语句为 int *const j = &i;
因此语句 cout << &i << &j << endl; 变成了 cout << &i << &*j << endl; 这两种状况都是打印输出 i 的地址。这就是当咱们打印普通变量和引用变量的时候会输出相同地址的缘由。
下面给出一段复杂一些的代码,来看看引用在级联 (cascading) 中是如何运做的。
下面这段代码是将上面代码中的引用替换以后代码,也就是说明咱们不依赖编译器的自动替换功能,手动进行替换也能达到相同的目标。
咱们经过下面代码能够证实 c++ 的引用不是神马别名,它也会占用内存空间的。
结论
我但愿这篇文章能把 c++ 引用的全部东东都解释清楚,然而我要指出的是 c++ 标准并无解释编译器如何实现引用的行为。因此实现取决于编译器,而大多数状况下就是将其实现为一个 const 指针。
引用支持 c++ 虚函数机制的代码
上述代码使用引用支持虚函数机制。若是引用仅仅是一个别名,那如何实现虚函数机制,而虚函数机制所须要的动态信息只能经过指针才能实现,因此更加说明引用其实就是一个 const 指针。
函数指针与指针函数
1、
在学习arm过程当中发现这“指针函数”与“函数指针”容易搞错,因此今天,我本身想一次把它搞清楚,找了一些资料,首先它们之间的定义:
一、指针函数是指带指针的函数,即本质是一个函数。函数返回类型是某一类型的指针
类型标识符 *函数名(参数表)
int *f(x,y);
首先它是一个函数,只不过这个函数的返回值是一个地址值。函数返回值必须用同类型的指针变量来接受,也就是说,指针函数必定有函数返回值,并且,在主调函数中,函数返回值必须赋给同类型的指针变量。
表示:
float *fun();
float *p;
p = fun(a);
注意指针函数与函数指针表示方法的不一样,千万不要混淆。最简单的辨别方式就是看函数名前面的指针*号有没有被括号()包含,若是被包含就是函数指针,反之则是指针函数。
来说详细一些吧!请看下面
指针函数:
当一个函数声明其返回值为一个指针时,实际上就是返回一个地址给调用函数,以用于须要指针或地址的表达式中。
格式:
类型说明符 * 函数名(参数)
固然了,因为返回的是一个地址,因此类型说明符通常都是int。
例如:int *GetDate();
int * aaa(int,int);
函数返回的是一个地址值,常用在返回数组的某一元素地址上。
int * GetDate(int wk,int dy);
main()
{
int wk,dy;
do
{
printf(Enter week(1-5)day(1-7)\n);
scanf(%d%d,&wk,&dy);
}
while(wk<1||wk>5||dy<1||dy>7);
printf(%d\n,*GetDate(wk,dy));
}
int * GetDate(int wk,int dy)
{
static int calendar[5][7]=
{
{1,2,3,4,5,6,7},
{8,9,10,11,12,13,14},
{15,16,17,18,19,20,21},
{22,23,24,25,26,27,28},
{29,30,31,-1}
};
return &calendar[wk-1][dy-1];
}
程序应该是很好理解的,子函数返回的是数组某元素的地址。输出的是这个地址里的值。
二、函数指针是指向函数的指针变量,即本质是一个指针变量。
int (*f) (int x); /* 声明一个函数指针 */
f=func; /* 将func函数的首地址赋给指针f */
指向函数的指针包含了函数的地址,能够经过它来调用函数。声明格式以下:
类型说明符 (*函数名)(参数)
其实这里不能称为函数名,应该叫作指针的变量名。这个特殊的指针指向一个返回整型值的函数。指针的声明笔削和它指向函数的声明保持一致。
指针名和指针运算符外面的括号改变了默认的运算符优先级。若是没有圆括号,就变成了一个返回整型指针的函数的原型声明。
例如:
void (*fptr)();
把函数的地址赋值给函数指针,能够采用下面两种形式:
fptr=&Function;
fptr=Function;
取地址运算符&不是必需的,由于单单一个函数标识符就标号表示了它的地址,若是是函数调用,还必须包含一个圆括号括起来的参数表。
能够采用以下两种方式来经过指针调用函数:
x=(*fptr)();
x=fptr();
第二种格式看上去和函数调用无异。可是有些程序员倾向于使用第一种格式,由于它明确指出是经过指针而非函数名来调用函数的。下面举一个例子:
void (*funcp)();
void FileFunc(),EditFunc();
main()
{
funcp=FileFunc;
(*funcp)();
funcp=EditFunc;
(*funcp)();
}
void FileFunc()
{
printf(FileFunc\n);
}
void EditFunc()
{
printf(EditFunc\n);
}
程序输出为:
FileFunc
EditFunc
主要的区别是一个是指针变量,一个是函数。在使用是必要要搞清楚才能正确使用
2、指针的指针
指针的指针看上去有些使人费解。它们的声明有两个星号。例如:
char ** cp;
若是有三个星号,那就是指针的指针的指针,四个星号就是指针的指针的指针的指针,依次类推。当你熟悉了简单的例子之后,就能够应付复杂的状况了。固然,实际程序中,通常也只用到 二级指针,三个星号不常见,更别说四个星号了。
指针的指针须要用到指针的地址。
char c='A';
char *p=&c;
char **cp=&p;
经过指针的指针,不只能够访问它指向的指针,还能够访问它指向的指针所指向的数据。下面就是几个这样的例子:
char *p1=*cp;
char c1=**cp;
你可能想知道这样的结构有什么用。利用指针的指针能够容许被调用函数修改局部指针变量和处理指针数组。
void FindCredit(int **);
main()
{
int vals[]={7,6,5,-4,3,2,1,0};
int *fp=vals;
FindCredit(&fp);
printf(%d\n,*fp);
}
void FindCredit(int ** fpp)
{
while(**fpp!=0)
if(**fpp<0) break;
else (*fpp)++;
}
首先用一个数组的地址初始化指针fp,而后把该指针的地址做为实参传递给函数FindCredit()。FindCredit()函数经过表达式**fpp间接地获得数组中的数据。为遍历数组以找到一个负值,FindCredit()函数进行自增运算的对象是调用者的指向数组的指针,而不是它本身的指向调用者指针的指针。语句(*fpp)++就是对形参指针指向的指针进行自增运算的。可是由于*运算符高于++运算符,因此圆括号在这里是必须的,若是没有圆括号,那么++运算符将做用于二重指针fpp上。
3、指向指针数组的指针
指针的指针另外一用法旧处理指针数组。有些程序员喜欢用指针数组来代替多维数组,一个常见的用法就是处理字符串。
char *Names[]=
{
Bill,
Sam,
Jim,
Paul,
Charles,
0
};
main()
{
char **nm=Names;
while(*nm!=0) printf(%s\n,*nm++);
}
先用字符型指针数组Names的地址来初始化指针nm。每次printf()的调用都首先传递指针nm指向的字符型指针,而后对nm进行自增运算使其指向数组的下一个元素(仍是指针)。注意完成上述认为的语法为*nm++,它首先取得指针指向的内容,而后使指针自增。
注意数组中的最后一个元素被初始化为0,while循环以次来判断是否到了数组末尾。具备零值的指针经常被用作循环数组的终止符。程序员称零值指针为空指针(NULL)。采用空指针做为终止符,在树种增删元素时,就没必要改动遍历数组的代码,由于此时数组仍然以空指针做为结束。