C++学习记录(一)

虚函数、纯虚函数详解

1.首先:强调一个概念
       定义一个函数为虚函数,不代表函数为不被实现的函数。定义他为虚函数是为了允许用基类的指针来调用子类的这个函数。
       定义一个函数为纯虚函数,才代表函数没有被实现。定义他是为了实现一个接口,起到一个规范的作用,规范继承这个。类的程序员必须实现这个函数。

2.关于实例化一个类:
有纯虚函数的类是不可能生成类对象的,如果没有纯虚函数则可以。比如:
class CA
{
public:
    virtual void fun() = 0;  // 说明fun函数为纯虚函数
    virtual void fun1();
};

class CB
{
public:
   virtual void fun();
   virtual void fun1();
};

// CA,CB类的实现
...

void main()
{
    CA a;   // 不允许,因为类CA中有纯虚函数
    CB b;   // 可以,因为类CB中没有纯虚函数
    ...
}

3.虚函数在多态中间的使用:
   多态一般就是通过指向基类的指针来实现的。
多态性(polymorphism)是面向对象程序设计的一个重要特征。如果一种语言只支持类而不支持多态,是不能被称为面向对象语言的,只能说是基于对象的,如Ada、VB就属此类。C++支持多态性,在C++程序设计中能够实现多态性。利用多态性可以设计和实现一个易于扩展的系统。

顾名思义,多态的意思是一个事物有多种形态。多态性的英文单词polymorphism来源于希腊词根poly(意为“很多”)和morph(意为“形态”)。在C ++程序设计中,多态性是指具有不同功能的函数可以用同一个函数名,这样就可以用一个函数名调用不同内容的函数。在面向对象方法中一般是这样表述多态性的:向不同的对象发送同一个消息, 不同的对象在接收时会产生不同的行为(即方法)。也就是说,每个对象可以用自己的方式去响应共同的消息。所谓消息,就是调用函数,不同的行为就是指不同的实现,即执行不同的函数。

其实,我们已经多次接触过多态性的现象,例如函数的重载、运算符重载都是多态现象。只是那时没有用到多态性这一专门术语而已。例如,使用运算符“+”使两个数值相加,就是发送一个消息,它要调用operator +函数。实际上,整型、单精度型、双精度型的加法操作过程是互不相同的,是由不同内容的函数实现的。显然,它们以不同的行为或方法来响应同一消息。

在现实生活中可以看到许多多态性的例子。如学校校长向社会发布一个消息: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)不能建立数组的引用。因为数组是一个由若干个元素所组成的集合,所以无法建立一个数组的别名。

引用应用

1、引用作为参数

引用的一个重要作用就是作为函数的参数。以前的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)使用指针作为函数的参数虽然也能达到与使用引用的效果,但是,在被调函数中同样要给形参分配存储单元,且需要重复使用"*指针变量名"的形式进行运算,这很容易产生错误且程序的阅读性较差;另一方面,在主调函数的调用点处,必须用变量的地址作为实参。而引用更容易使用,更清晰。

如果既要利用引用提高程序的效率,又要保护传递给函数的数据不在函数中被改变,就应使用常引用。


2、常引用

常引用声明方式: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 。

3、引用作为返回值

要以引用返回函数值,则函数定义时要按以下格式:

类型标识符 &函数名(形参列表及类型说明)

{函数体}

说明:

(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分配的对象的引用、返回一个静态对象引用。根据前面提到的引用作为返回值的三个规则,第2、3两个方案都被否决了。静态对象的引用又因为((a+b) == (c+d))会永远为true而导致错误。所以可选的只剩下返回一个对象了。

4、引用和多态

引用是除指针外另一个可以产生多态效果的手段。这意味着,一个基类的引用可以指向它的派生类实例。

【例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

深入分析C++引用

 关于引用和指针的区别的文章很多很多,但是总是找不到他们的根本区别,偶然在codeproject上看到这篇文章,觉得讲的挺好的,

所以翻译了下,希望对大家有帮助。

原文地址: http://www.codeproject.com/KB/cpp/References_in_c__.aspx

 

引言

      我选择写 C++ 中的引用是因为我感觉大多数人误解了引用。而我之所以有这个感受是因为我主持过很多 C++ 的面试,并且我很少从面试者中得到关于 C++ 引用的正确答案。

       那么 c++ 中引用到底意味这什么呢?通常一个引用让人想到是一个引用的变量的别名,而我讨厌将 c++ 中引用定义为变量的别名。这篇文章中,我将尽量解释清楚, c++ 中根本就没有什么叫做别名的东东。

 

背景

 c/c++ 中,访问一个变量只能通过两种方式被访问,传递,或者查询。这两种方式是:

1. 通过值 访问 / 传递变量

2. 通过地址 访问 / 传递变量 – 这种方法就是指针

 

       除此之外没有第三种访问和传递变量值的方法。引用变量也就是个指针变量,它也拥有内存空间。最关键的是引用是一种会被编译器自动解引用的指针。很难相信么?让我们来看看吧。。。

 

下面是一段使用引用的简单 c++ 代码

 

  204226_mprg_1269935.jpg

引用其实就是 c++ 中的常量指针。表达式   int &i = j; 将会被编译器转化成 int *const i = &j; 而引用之所以要初始化是因为 const 类型变量必须初始化,这个指针也必须有所指。下面我们再次聚焦到上面这段代码,并使用编译器的那套语法将引用替换掉。

 204343_Aoa6_1269935.jpg

    读者一定很奇怪为什么我上面这段代码会跳过打印地址这步。这里需要一些解释。因为引用变量时会被编译器自动解引用的,那么一个诸如   cout << &j << endl; 的语句,编译器就会将其转化成语句   cout << &*j << endl;   现在 &* 会相互抵消,这句话变的毫无意义,而 cout 打印的 j 值就是 i 的地址,因为其定义语句为 int *const j = &i;

 

      所以语句 cout << &i << &j << endl; 变成了 cout << &i << &*j << endl; 这两种情况都是打印输出 i 的地址。这就是当我们打印普通变量和引用变量的时候会输出相同地址的原因。

 

      下面给出一段复杂一些的代码,来看看引用在级联 (cascading) 中是如何运作的。

 204508_PdIG_1269935.png

 

下面这段代码是将上面代码中的引用替换之后代码,也就是说明我们不依赖编译器的自动替换功能,手动进行替换也能达到相同的目标。

 204708_icdy_1269935.png

         我们通过下面代码可以证明 c++ 的引用不是神马别名,它也会占用内存空间的。

204806_5NGP_1269935.jpg

结论

我希望这篇文章能把 c++ 引用的所有东东都解释清楚,然而我要指出的是 c++ 标准并没有解释编译器如何实现引用的行为。所以实现取决于编译器,而大多数情况下就是将其实现为一个 const 指针。

 

 

引用支持 c++ 虚函数机制的代码

 204904_51V0_1269935.jpg

 

上述代码使用引用支持虚函数机制。如果引用仅仅是一个别名,那如何实现虚函数机制,而虚函数机制所需要的动态信息只能通过指针才能实现,所以更加说明引用其实就是一个 const 指针。

 函数指针与指针函数

 

一、

在学习arm过程中发现这指针函数函数指针容易搞错,所以今天,我自己想一次把它搞清楚,找了一些资料,首先它们之间的定义:

1、指针函数是指带指针的函数,即本质是一个函数。函数返回类型是某一类型的指针

     类型标识符    *函数名(参数表)

      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];
        }
        
程序应该是很好理解的,子函数返回的是数组某元素的地址。输出的是这个地址里的值。

 

 

 

2、函数指针是指向函数的指针变量,即本质是一个指针变量。

 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

 

主要的区别是一个是指针变量,一个是函数。在使用是必要要搞清楚才能正确使用

 

二、指针的指针
    
指针的指针看上去有些令人费解。它们的声明有两个星号。例如:
        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上。

三、指向指针数组的指针
    
指针的指针另一用法旧处理指针数组。有些程序员喜欢用指针数组来代替多维数组,一个常见的用法就是处理字符串。

        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++,它首先取得指针指向的内容,然后使指针自增。
    
注意数组中的最后一个元素被初始化为0while循环以次来判断是否到了数组末尾。具有零值的指针常常被用做循环数组的终止符。程序员称零值指针为空指针(NULL)。采用空指针作为终止符,在树种增删元素时,就不必改动遍历数组的代码,因为此时数组仍然以空指针作为结束。

 

 

 ‍‍

转载于:https://my.oschina.net/u/1269935/blog/367345

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值