c++源码高级反思(04)
时间:2010-12-12 来源:yuzhou133
C++高级部分
结构体默认情况下,其成员是共有(pubilic)的,类默认情况下,其成员是私有的,这是结构体和类的区别之一。
Visual c++中类名的风格是,所有类的名字都以大写字母C开头,以表示这个类的名字。
C++中类体被分成三类:
共有成员:以关键字public指明
私有成员:以关键字private指明
保护成员:以关键字protected指明
类的共有成员,在程序的任何位置都能够以正确的方式引用它
类的私有成员只能被其自身成员所访问,即私有成员的名字只能出现在所属类类体、成员函数中,不能出现在其它函数中。
类的保护成员只能在该类的派生类类体中使用
类的说明通常放在一个以.h为扩展名的文件中,称为头文件,其中定义类的接口(iterface),可以同其他类的说明同其他类的说明同放在一个文件。
如果类的说明程序行较多,那么应该将其他放在一个独立文件中,visual C++的风格是以主文件为类名去掉前面的字符C,例如Cbook的类说明可以放在文件book.h中,将类体的定义放于一个以.cpp为扩展名的文件中, 这称为类的实现文件。在这个文件的开始部分应该用文件包含指令将类说明文件包含进来。
对象之间的联系通过消息来传递,消息机制是对象之间相互联系和相互作用的方式。
对象数组是以数组元素为对象的数组。该数组中若干个元素必须是同一个类的若干个对象。
构造函数(constructor)是在类中定义的一种特殊的函数,它的函数的名称和类的名称相同。构造函数的主要功能是为对象分配空间,也可用来为类成员变量赋初值,因此构造函数不能有返回类型,甚至不能有return语句。
构造函数可以在类中声明,在外部定义。
在类的外部定义构造函数时,需要在函数前加上类名和域运算符,否则在编译时系统不能识别其属于哪个类。
实际应用中,一般都要给类定义构造函数,如果没有定义,编译系统就自动生成一个默认的构造函数,这个默认的构造函数不带任何参数,只能给对象开辟一个存储空间,而不能为对象中的数据成员赋初值。
不带参数的构造函数对象的初始化是固定的,如希望在建立对象时通过参数初始化数据成员,应使用带参数的构造函数
当构造函数带有参数时,在定义对象时必须给构造函数传递参数,否则,构造函数将不被执行。
创建对象调用的构造函数是否带参数,其创建的对象成员变量的初始化程度是不一样的。
一个类可以有多个不同参数形式的构造函数。
C++允许对构造函数重载,即定义多个参数及参数类型不同的构造函数,用多种方法对构造函数进行初始化。
拷贝构造函数是一种特殊的构造函数,它的作用是用一个已经存在的对象来初始化该类的新对象,用户可根据需要定义拷贝构造函数,也可以由系统生成一个默认的拷贝构造函数。
在创建对象时,调用的是构造函数还是拷贝构造函数,这是由编译系统根据需要创建对象的参数来确定的
析构函数作用时释放分配给对象的内存空间,并作一些善后工作,析构函数的名字必须与类名相同,但在名字的前面要加波浪号(“~”),析构函数没有参数,没 有返回值,不能重载,在一个类中只能有一个析构函数。当被撤销对象时,系统会自动调用析构函数完成空间的释放和善后工作。
在使用析构函数中,需要注意一下几个问题:
每个类必须有一个析构函数,若没有显式地定义,则系统会自动生成一个默认的析构函数,它是一个空函数。
对于大多数类而言,默认的析构函数就能满足要求,但如果对象在完成操作前需要做内部处理,则应显式地定义析构函数。
构造函数和析构函数的常见的用法是,在构造函数中用new运算对为对象分配空间,在析构函数中用delete运算符释放空间。
C++中为了使得类的私有成员和保护成员能够被其他类或其他成员函数访问,引入了友元的概念。友元提供了不同类或对象的成员函数之间、类的成员函数与一般 函数之间进行数据共享的机制。如果友元是一般成员函数或类的成员函数,则称为友元函数,如果友元是一个类,则称为友元类,友元类的所有成员函数都称为友元 函数。
友元函数与普通成员函数不同,它不是当前类的成员函数,而是独立于当前类的外部函数;它可以是普通函数或其他类的成员函数。友元函数定义后可以访问该类的所有对象的成员,包括私有成员、保护成员和公有成员。
友元函数使用前必须要在类定义时声明,声明时在其函数名前加上关键字friend。该声明可以放在公有成员中,也可以放在私有成员中。
友元函数不是类的成员函数,因此,在类外定义友元函数时,不必像成员函数那样,在函数名前加“类名::”
友元函数不是类的成员,因而不能直接引用对象成员的名字,也不能通过this指针引用对象的成员,必须通过作为入口参数传递进来的对象名或对象指针来引用 该对象的成员。为此,友元函数一般都有一个该类的入口参数。如distance(point &p1,point &p2)
当一个函数需要访问多个类时,应该把这个函数同时定义为这些类的友元函数,这样,这个函数才能访问这些类的数据。
如果一个类的成员函数是另一个类的友元函数,则称这个成员函数为友元函数。
通过友元成员函数,不仅可以访问自己所在类对象中的私有和公有成员,还可以访问有关键字friend声明语句所在类对象中的私有和公有成员,从而可使两个类可以互相访问,从而共同完成某个任务。
当一个类的成员函数作为另一个类的友元函数时,必须先定义成员函数所在的类
如果在类定义前要使用到该类的成员,需要事先在使用前对该类进行声明。
当一个类做为另一个类的友元时,称这个类为友元类。当一个类成为另一个类的友元类时,这个类的所有成员函数都成为另一个类的友元函数,因此,友元类中的所 有成员函数都可以通过对象名直接访问另一个类中的私有成员,从而实现不同类之间的数据共享。friend class <友元类名>;或friend <友元类名>
友元关系是不能传递的。类B是类A的友元,类C是类B的友元,类C与类A之间,除非特别说明,没有任何关系,不能进行数据的共享。友元关系是单向的。类B 是类A的友元,类B的成员函数可以访问类A的私有成员和保护成员,反之,类A的成员函数却不可以访问类B的私有成员和保护成员
继承是指一个类除了得到另外一个类的所有性质,还具有自身独特的性质,则该类称为派生类,而另一个类称为基类,这种行为称为继承
class <派生类名>:<派生方式><基类名>
{
派生类声明;
}
继承方式关键字为:private、public和protected,分别表示私有继承、公有继承和保护继承。默认的继承方式是私有继承,。继承方式规定了派生类成员和类外对象访问基类成员的权限。
派生类成员是指除了从基类继承来的成员外,新增的数据成员和成员函数。
派生类访问基类的能力:
基类中的私有成员在派生类中是隐藏的,只能在基类内部访问
派生类中的成员不能访问基类中的私有成员,但可以访问基类中的共有成员和保护成员
派生类从基类公有继承时,基类的共有成员和保护成员在派生类中仍为公有成员和保护成员。
派生类从基类私有继承时,基类的公有成员和保护成员在派生类中都改变为私有成员
派生类从基类保护继承时,基类的公有成员在派生类中该变为保护成员,基类的而保护成员在派生类中 则认为保护成员
如果要访问类中的私有成员和保护成员,必须通过公有成员函数来访问
派生类不能直接访问基类的私有成员,若要访问,必须使用基类的接口,即通过其成员函数。实现方法如下:
在基类的声明中增加保护成员,将基类中提供给派生类访问的私有成员定义为保护成员
将需要访问基类私有成员的派生类成员函数声明为友元
C++中规定,基类成员的初始化工作由基类的构造函数完成,而派生类的初始化工作由派生类的构造函数完成。
在构建派生类的构造函数和析构函数时,需要注意一下原则:
基类的构造函数和析构函数不能被派生类继承。
如果基类没有定义构造函数,派生类也可以不定义构造函数,全都采用默认的构造函数,此时,派生类新增成员的初始化工作可用其他公有函数来完成。
如果基类定义了带有行参表的构造函数,派生类就必须定义一个新的构造函数,提供一个将参数传递给基类构造函数的途径,以便保证在基类进行初始化时能获得必需的数据。
如果派生类的基类也是派生类,则每个派生类只需负责其直接基类的构造,不负责自己的简介基类的构造。
派生类是否要定义析构函数与所属的基类无关,如果派生类对象在撤销时需要做清理善后工作,就需要定义新的析构函数。
派生类的数据成员由所有基类的数据成员和派生类新增的数据成员共同组成,如果派生类新增成员中还有对象成员,那派生类的数据成员中间接含有这些对象的数据 成员,应此要对派生类对象初始化,就要对基类数据成员、新增数据成员和对象成员的数据进行初始化,这样,派生类的构造函数需要以合适的初值作为参数,隐含 调用基类的构造函数和新增对象成员的构造函数来初始化各自的数据成员,再用新加的语句新增数据成员进行初始化。
派生类构造函数声明的形式:
<派生类名>::<派生类名>(参数总表):基类名(参数表),对象成员名1(参数表1),................,对象成员名n(参数表n){
//派生类新增成员的初始化语句
}
使用作用域运算符来指明调用的某个成员属于哪个基类是很直观的方法,因此在多重继承中使用非常广泛。
引入虚基类的主要原因是为了解决基类中由于同名成员的问题而产生的二义性问题。
一般来说,在多重继承中,当派生类有多条路径去访问基类,即非虚基类时,编译系统会给出错误问题。
虚基类的初始化与一般的多继承的初始化在语法上是一样的,但构造函数的执行顺序不同:
虚基类的构造函数的执行在非虚基类的构造函数之前。
若同一层次中包含多个虚基类,这些虚基类的构造函数按对他们说明的先后顺序执行
若虚基类由非虚基类派生而来,则仍然先执行基类的构造函数,再执行派生类的构造函数
只有能够解决多态问题的语言,才是真正支持面向对象的开发语言。
在面向对象的过程中,多态性是指不同对象接受到相同消息时,根据对象的不同而产生不同的动作。多态性提供了同一个接口可以用多种方法进行调用的机制,从而可以通过相同的接口访问不同的函数,具体说,就是同一个函数名称,作用在不同的对象将产生不同的操作。
简单的说,多态就是“一个接口,多种实现”
面向对象的多态性可以严格的分为四类:重载多态、强制多态、包含多态和参数多态。前两种称为专用多态,后面两种称为通用多态
包含多态是指:定义于不同类中的同名成员函数的多态行为,主要通过虚函数来实现。
参数多态于类模板相关联,其是一个可以参数化的模板,其中包含的操作所涉及的类型必须用类型参数进行实例化。这样,由类模板实例化的各类都具有相同的操作,而操作对象的类型却各不相同。
多态从实现的角度来讲可以划分为两类,编译时的多态和运行时的多态。前者是在编译的过程中确定了同名操作的具体操作对象,而后者则是在程序运行过程中才动态地确定操作所针对的具体对象,这种确定操作的具体对象的过程就是联编。
联编是指计算机程序自身彼此关联的过程,即把一个标识符名和一个存储地址联系在一起的过程,用面向对象的术语讲,就是把一条消息和一个对象的方法相结合的过程。按照联编进行阶段的不同,可以分为两种不同的联编方法,静态联编和动态联编。
多态性提供了把接口与实现分开的另一种方法,提供了代码的组织性和可读性,更重要的是提高了软件的可扩充性。
//***********************************************************************************
#include <iostream>
using namespace std;
class animal
{
public:
void sleep()
{
cout<<"animal sleep"<<endl;
}
void breathe()
{
cout<<"animal breathe"<<endl;
}
};
class fish:public animal
{
public:
void breathe()
{
cout<<"fish bubble"<<endl;
}
};
int main()
{
fish fh;
fh.breathe();
animal *pAn=&fh;
pAn->breathe();
fish *pBn=&fh;
pBn->breathe();//you should know why the result is .
return 0;
}
//*******************************************************************************************
由静态联编支持的多态性称为编译时的多态性或静态多态性,也就是说,确定同名操作的具体操作对象的过程是在编译过程中完成的。在C++中,可以用函数重载和运算符重载来实现编译时的多态性。
由动态联编支持的多态性称为运行时的多态性或动态多态性,也就是说,确定同名操作的具体操作对象的过程是在运行过程中完成的。在C++中,可以用继承和虚函数来实现运行时的多态性。
函数的重载也称为多态函数,是实现编译时的多态性的形式之一,它使程序能用同一个名字来访问一组相关的函数,提高了程序的灵活性。函数重载时,函数名相同,但函数所带的参数个数或数据类型不同,编译系统会根据参数来决定调用哪个同名函数。
面向对象程序设计中,函数的重载表现为两种情况:
1、第一种是参数个数或类型有所差别的重载。
2、第二种是函数的参数完全相同但属于不同的类。
当函数的参数完全相同但属于不同的类时,为了让编译能正确区分调用哪个类的同名函数,采用以下两种方法:
对象名区别:在函数名前加上对象名来限制。如对象名.函数名
用类名和作用域运算符::加以区别;在函数名前加“类名::”来限制,如类名::函数名
//***************************************************************************************
#include<iostream>
using namespace std;
class point
{
int x,y;
public:
point(int xx,int yy)
{
x=xx;
y=yy;
}
double area()
{
return 0.0;
}
};
class circle:public point
{
int r;
public:
circle(int xx,int yy,int rr):point(xx,yy)
{
r=rr;
}
double area()
{
return 3.1415*r*r;
}
};
int main()
{
point pob(15,15);
circle cob(20,20,10);
cout<<"pob.area()= "<<pob.area()<<endl;
cout<<"cob.area()= "<<cob.area()<<endl;
cout<<"cob.point::area()= "<<cob.point::area()<<endl;
return 0;
}
//*********************************************************************************
虚函数是重载的另一种形式,实现的是动态地重载,即函数调用与函数体之间的联系是在运行时才建立,也就是动态联编。
一般对象的指针之间没有联系,彼此独立,不能混用。但派生类是由基类派生出来的,它们之间有继承关系,因此,指向基类和派生类的指针之间的指针之间也有一定的联系。如果使用不当,将会出现一些问题。
//**********************************************************
#include<iostream>
using namespace std;
class base
{
private:
int x,y;
public:
base(int xx=0,int yy=0)
{
x=xx;
y=yy;
}
void disp()
{
cout<<"base: "<<x<<" "<<y<<endl;
}
};
class base1:public base
{
private:
int z;
public:
base1(int xx,int yy,int zz):base(xx,yy)
{
z=zz;
}
void disp()
{
cout<<"base1:"<<z<<endl;
}
};
int main()
{
base obj(3,4),*objp;
base1 obj1(1,2,3);
objp=&obj;
objp->disp();
objp=&obj1;
objp->disp();
return 0;
}
//****************************************************************************************
声明为指向基类对象的指针可以指向它的共有派生类的对象,但不允许指向它的私有派生类的对象。
允许声明为指向基类对象的指针指向它的公有派生类的对象,但不允许将一个声明为指向派生类对象的指针指向基类的对象。
声明为指向基类对象的指针,当其指向它的派生类的对象时,只能直接访问派生类中从基类继承下来的成员,不能直接访问公有派生类中定义的成员,要访问其公有派生类中的成员,可将基类指针用显式类型转换为派生类指针。
在C++中引入虚函数的概念主要是为了解决在调用既有继承关系的类的成员函数时能够正确地被执行。
虚函数的定义时在基类中进行的,即把基类中需要定义为虚函数的成员函数声明为virtual。当基类中的某个成员被声明为虚函数后,其就可以在派生类中被 重新定义。在派生类中重新定义时,其函数原型,包括返回类型、函数名、参数个数和类型、参数的顺序都必须与基类中的原型完全一致。
//******************************************************************************************
#include<iostream>
using namespace std;
class animal
{
public:
void sleep()
{
cout<<"animal sleep"<<endl;
}
virtual void breathe()
{
cout<<"animal breathe"<<endl;
}
};
class fish:public animal
{
public:
void breathe()
{
cout<<"fish bubble"<<endl;
}
};
int main()
{
fish fh;
fh.breathe();
animal *pAn=&fh;
animal fs;
fs.breathe();
pAn->breathe();
fish *pBn=&fh;
pBn->breathe();
return 0;
}
//************************************************************************************
虚函数的定义必须在基类中进行,即只有基类中的函数才能被定义为虚函数,在派生类中不能定义某个函数为虚函数。
在派生类中被重新定义的基类中的虚函数,是函数重载的另一种形式,但它与函数重载又有如下的区别:一般函数的重载,要求其函数的参数或参数类型必须有所不 同,函数的返回类型也可以不同,但重载一个虚函数时,要求函数名、返回类型、参数个数、参数的类型和参数的顺序必须与基类中的虚函数的原型完全相同。
如果仅返回类型不同,其余相同,则系统会给出错误信息。
如果函数名相同,而参数个数、参数的类型或参数的顺序不同,系统认为是普通的函数重载,虚函数的特性将丢失。
//**********************************************************************************
#include<iostream>
using namespace std;
class base
{
private:
int x,y;
public:
base(int xx=0,int yy=0)
{
x=xx;
y=yy;
}
virtual void disp()
{
cout<<"base:"<<x<<", "<<y<<endl;
}
};
class base1:public base
{
private:
int z;
public:
base1(int xx,int yy,int zz):base(xx,yy)
{
z=zz;
}
void disp()
{
cout<<"base1:"<<z<<endl;
}
};
int main()
{
base obj(3,4),*objp;
base1 obj1(1,2,5);
objp=&obj;
objp->disp();
objp=&obj1;
objp->disp();
return 0;
}
//*******************************************************************************************
对于虚函数调用来说,每一个对象内部都有一个虚表指针,该虚表指针被初始化为本类的虚表。所以在程序中,不管你的对象类型如何转换,但该对象内部的虚表指针式固定的,所以才能实现动态地对象函数调用,这就是C++多态性实现的原理
一个虚函数无论被继承多少次,仍保持其虚函数的特性,与继承的次数无关。
//******************************************************************************************
#include<iostream>
using namespace std;
class base
{
public:
virtual ~base(){};
virtual void func() const
{
cout<<"base output!"<<endl;
}
};
class derive1:public base
{
public:
void func() const
{
cout<<"derive1 output!"<<endl;
}
};
class derive2:public derive1
{
public:
void func() const
{
cout<<"derive2 output!"<<endl;
}
};
void test(const base & rBase)
{
rBase.func();
};
int main()
{
base bObj;
derive1 d1Obj;
derive2 d2Obj;
test(bObj);
test(d1Obj);
test(d2Obj);
return 0;
}
//******************************************************************************************
在使用虚析构函数时,要注意一下几点:
只要基类的析构函数被声明为虚函数,则派生类的析构函数,无论是否使用virtual关键字进行声明,都自动成为虚函数。
如果基类的析构函数为虚函数,则当派生类未定义析构函数时,编译器所生成的析构函数也为虚函数。
使用虚函数时应注意如下问题:
虚函数的声明只能出现在类函数原型的声明中,不能出现在函数体实现的时候,而且,基类中只要保护成员或公有成员才能被声明为虚函数。
在派生类中重新定义虚函数时,关键字virtual可以写也可以不写,但在容易引起混乱时,应写上该关键字。
动态联编只能通过成员函数调用或通过指针、引用来访问虚函数,如果以对象名的形式来访问虚函数,将采用静态联编。
抽象类是一种特殊的类,其为类提供统一的操作界面。建立抽闲类的原因就是为了通过抽象类多态使用其中的成员函数,抽象类是带有纯虚函数的类
当在基类中不能为虚函数给出一个有意义的实现时,可以将其声明为纯虚函数,纯虚函数的实现可以留给派生类完成,纯虚函数的作用是为派生类提供一个一致的接 口,一般来说,一个抽象类带有至少一个纯虚函数,纯虚函数是爱一个基类中说明的虚函数,它在该基类 中没有具体的操作内容,要求各派生类在重新定义时根据自己的需要定义实际的操作内容。
Virtual<虚函数><函数名>(参数表)=0
//************************************************************************
#include<iostream>
using namespace std;
class point
{
public:
point(int i=0,int j=0)
{
x0=i;
y0=j;
}
virtual void set()=0;
virtual void draw()=0;
protected:
int x0,y0;
};
class line:public point
{
public:
line(int i=0,int j=0,int m=0,int n=0):point(i,j)
{
x1=m;
y1=n;
}
void set()
{
cout<<"line::set() called. ";
}
void draw()
{
cout<<"line::draw() called. ";
}
protected:
int x1,y1;
};
class ellipse:public point
{
public:
ellipse(int i=0,int j=0,int p=0,int q=0):point(i,j)
{
x2=p;
y2=q;
}
void set()
{
cout<<"ellipse::set() called. ";
}
void draw()
{
cout<<"ellipse::draw() called. ";
}
protected:
int x2,y2;
};
void drawobj(point *p)
{
p->draw();
}
void setobj(point *p)
{
p->set();
}
int main()
{
line *lineobj=new line;
ellipse *elliobj=new ellipse;
drawobj(lineobj);
drawobj(elliobj);
cout<<endl;
setobj(lineobj);
setobj(elliobj);
cout<<" Redraw the object... ";
drawobj(lineobj);
drawobj(elliobj);
return 0;
}
结构体默认情况下,其成员是共有(pubilic)的,类默认情况下,其成员是私有的,这是结构体和类的区别之一。
Visual c++中类名的风格是,所有类的名字都以大写字母C开头,以表示这个类的名字。
C++中类体被分成三类:
共有成员:以关键字public指明
私有成员:以关键字private指明
保护成员:以关键字protected指明
类的共有成员,在程序的任何位置都能够以正确的方式引用它
类的私有成员只能被其自身成员所访问,即私有成员的名字只能出现在所属类类体、成员函数中,不能出现在其它函数中。
类的保护成员只能在该类的派生类类体中使用
类的说明通常放在一个以.h为扩展名的文件中,称为头文件,其中定义类的接口(iterface),可以同其他类的说明同其他类的说明同放在一个文件。
如果类的说明程序行较多,那么应该将其他放在一个独立文件中,visual C++的风格是以主文件为类名去掉前面的字符C,例如Cbook的类说明可以放在文件book.h中,将类体的定义放于一个以.cpp为扩展名的文件中, 这称为类的实现文件。在这个文件的开始部分应该用文件包含指令将类说明文件包含进来。
对象之间的联系通过消息来传递,消息机制是对象之间相互联系和相互作用的方式。
对象数组是以数组元素为对象的数组。该数组中若干个元素必须是同一个类的若干个对象。
构造函数(constructor)是在类中定义的一种特殊的函数,它的函数的名称和类的名称相同。构造函数的主要功能是为对象分配空间,也可用来为类成员变量赋初值,因此构造函数不能有返回类型,甚至不能有return语句。
构造函数可以在类中声明,在外部定义。
在类的外部定义构造函数时,需要在函数前加上类名和域运算符,否则在编译时系统不能识别其属于哪个类。
实际应用中,一般都要给类定义构造函数,如果没有定义,编译系统就自动生成一个默认的构造函数,这个默认的构造函数不带任何参数,只能给对象开辟一个存储空间,而不能为对象中的数据成员赋初值。
不带参数的构造函数对象的初始化是固定的,如希望在建立对象时通过参数初始化数据成员,应使用带参数的构造函数
当构造函数带有参数时,在定义对象时必须给构造函数传递参数,否则,构造函数将不被执行。
创建对象调用的构造函数是否带参数,其创建的对象成员变量的初始化程度是不一样的。
一个类可以有多个不同参数形式的构造函数。
C++允许对构造函数重载,即定义多个参数及参数类型不同的构造函数,用多种方法对构造函数进行初始化。
拷贝构造函数是一种特殊的构造函数,它的作用是用一个已经存在的对象来初始化该类的新对象,用户可根据需要定义拷贝构造函数,也可以由系统生成一个默认的拷贝构造函数。
在创建对象时,调用的是构造函数还是拷贝构造函数,这是由编译系统根据需要创建对象的参数来确定的
析构函数作用时释放分配给对象的内存空间,并作一些善后工作,析构函数的名字必须与类名相同,但在名字的前面要加波浪号(“~”),析构函数没有参数,没 有返回值,不能重载,在一个类中只能有一个析构函数。当被撤销对象时,系统会自动调用析构函数完成空间的释放和善后工作。
在使用析构函数中,需要注意一下几个问题:
每个类必须有一个析构函数,若没有显式地定义,则系统会自动生成一个默认的析构函数,它是一个空函数。
对于大多数类而言,默认的析构函数就能满足要求,但如果对象在完成操作前需要做内部处理,则应显式地定义析构函数。
构造函数和析构函数的常见的用法是,在构造函数中用new运算对为对象分配空间,在析构函数中用delete运算符释放空间。
C++中为了使得类的私有成员和保护成员能够被其他类或其他成员函数访问,引入了友元的概念。友元提供了不同类或对象的成员函数之间、类的成员函数与一般 函数之间进行数据共享的机制。如果友元是一般成员函数或类的成员函数,则称为友元函数,如果友元是一个类,则称为友元类,友元类的所有成员函数都称为友元 函数。
友元函数与普通成员函数不同,它不是当前类的成员函数,而是独立于当前类的外部函数;它可以是普通函数或其他类的成员函数。友元函数定义后可以访问该类的所有对象的成员,包括私有成员、保护成员和公有成员。
友元函数使用前必须要在类定义时声明,声明时在其函数名前加上关键字friend。该声明可以放在公有成员中,也可以放在私有成员中。
友元函数不是类的成员函数,因此,在类外定义友元函数时,不必像成员函数那样,在函数名前加“类名::”
友元函数不是类的成员,因而不能直接引用对象成员的名字,也不能通过this指针引用对象的成员,必须通过作为入口参数传递进来的对象名或对象指针来引用 该对象的成员。为此,友元函数一般都有一个该类的入口参数。如distance(point &p1,point &p2)
当一个函数需要访问多个类时,应该把这个函数同时定义为这些类的友元函数,这样,这个函数才能访问这些类的数据。
如果一个类的成员函数是另一个类的友元函数,则称这个成员函数为友元函数。
通过友元成员函数,不仅可以访问自己所在类对象中的私有和公有成员,还可以访问有关键字friend声明语句所在类对象中的私有和公有成员,从而可使两个类可以互相访问,从而共同完成某个任务。
当一个类的成员函数作为另一个类的友元函数时,必须先定义成员函数所在的类
如果在类定义前要使用到该类的成员,需要事先在使用前对该类进行声明。
当一个类做为另一个类的友元时,称这个类为友元类。当一个类成为另一个类的友元类时,这个类的所有成员函数都成为另一个类的友元函数,因此,友元类中的所 有成员函数都可以通过对象名直接访问另一个类中的私有成员,从而实现不同类之间的数据共享。friend class <友元类名>;或friend <友元类名>
友元关系是不能传递的。类B是类A的友元,类C是类B的友元,类C与类A之间,除非特别说明,没有任何关系,不能进行数据的共享。友元关系是单向的。类B 是类A的友元,类B的成员函数可以访问类A的私有成员和保护成员,反之,类A的成员函数却不可以访问类B的私有成员和保护成员
继承是指一个类除了得到另外一个类的所有性质,还具有自身独特的性质,则该类称为派生类,而另一个类称为基类,这种行为称为继承
class <派生类名>:<派生方式><基类名>
{
派生类声明;
}
继承方式关键字为:private、public和protected,分别表示私有继承、公有继承和保护继承。默认的继承方式是私有继承,。继承方式规定了派生类成员和类外对象访问基类成员的权限。
派生类成员是指除了从基类继承来的成员外,新增的数据成员和成员函数。
派生类访问基类的能力:
基类中的私有成员在派生类中是隐藏的,只能在基类内部访问
派生类中的成员不能访问基类中的私有成员,但可以访问基类中的共有成员和保护成员
派生类从基类公有继承时,基类的共有成员和保护成员在派生类中仍为公有成员和保护成员。
派生类从基类私有继承时,基类的公有成员和保护成员在派生类中都改变为私有成员
派生类从基类保护继承时,基类的公有成员在派生类中该变为保护成员,基类的而保护成员在派生类中 则认为保护成员
如果要访问类中的私有成员和保护成员,必须通过公有成员函数来访问
派生类不能直接访问基类的私有成员,若要访问,必须使用基类的接口,即通过其成员函数。实现方法如下:
在基类的声明中增加保护成员,将基类中提供给派生类访问的私有成员定义为保护成员
将需要访问基类私有成员的派生类成员函数声明为友元
C++中规定,基类成员的初始化工作由基类的构造函数完成,而派生类的初始化工作由派生类的构造函数完成。
在构建派生类的构造函数和析构函数时,需要注意一下原则:
基类的构造函数和析构函数不能被派生类继承。
如果基类没有定义构造函数,派生类也可以不定义构造函数,全都采用默认的构造函数,此时,派生类新增成员的初始化工作可用其他公有函数来完成。
如果基类定义了带有行参表的构造函数,派生类就必须定义一个新的构造函数,提供一个将参数传递给基类构造函数的途径,以便保证在基类进行初始化时能获得必需的数据。
如果派生类的基类也是派生类,则每个派生类只需负责其直接基类的构造,不负责自己的简介基类的构造。
派生类是否要定义析构函数与所属的基类无关,如果派生类对象在撤销时需要做清理善后工作,就需要定义新的析构函数。
派生类的数据成员由所有基类的数据成员和派生类新增的数据成员共同组成,如果派生类新增成员中还有对象成员,那派生类的数据成员中间接含有这些对象的数据 成员,应此要对派生类对象初始化,就要对基类数据成员、新增数据成员和对象成员的数据进行初始化,这样,派生类的构造函数需要以合适的初值作为参数,隐含 调用基类的构造函数和新增对象成员的构造函数来初始化各自的数据成员,再用新加的语句新增数据成员进行初始化。
派生类构造函数声明的形式:
<派生类名>::<派生类名>(参数总表):基类名(参数表),对象成员名1(参数表1),................,对象成员名n(参数表n){
//派生类新增成员的初始化语句
}
使用作用域运算符来指明调用的某个成员属于哪个基类是很直观的方法,因此在多重继承中使用非常广泛。
引入虚基类的主要原因是为了解决基类中由于同名成员的问题而产生的二义性问题。
一般来说,在多重继承中,当派生类有多条路径去访问基类,即非虚基类时,编译系统会给出错误问题。
虚基类的初始化与一般的多继承的初始化在语法上是一样的,但构造函数的执行顺序不同:
虚基类的构造函数的执行在非虚基类的构造函数之前。
若同一层次中包含多个虚基类,这些虚基类的构造函数按对他们说明的先后顺序执行
若虚基类由非虚基类派生而来,则仍然先执行基类的构造函数,再执行派生类的构造函数
只有能够解决多态问题的语言,才是真正支持面向对象的开发语言。
在面向对象的过程中,多态性是指不同对象接受到相同消息时,根据对象的不同而产生不同的动作。多态性提供了同一个接口可以用多种方法进行调用的机制,从而可以通过相同的接口访问不同的函数,具体说,就是同一个函数名称,作用在不同的对象将产生不同的操作。
简单的说,多态就是“一个接口,多种实现”
面向对象的多态性可以严格的分为四类:重载多态、强制多态、包含多态和参数多态。前两种称为专用多态,后面两种称为通用多态
包含多态是指:定义于不同类中的同名成员函数的多态行为,主要通过虚函数来实现。
参数多态于类模板相关联,其是一个可以参数化的模板,其中包含的操作所涉及的类型必须用类型参数进行实例化。这样,由类模板实例化的各类都具有相同的操作,而操作对象的类型却各不相同。
多态从实现的角度来讲可以划分为两类,编译时的多态和运行时的多态。前者是在编译的过程中确定了同名操作的具体操作对象,而后者则是在程序运行过程中才动态地确定操作所针对的具体对象,这种确定操作的具体对象的过程就是联编。
联编是指计算机程序自身彼此关联的过程,即把一个标识符名和一个存储地址联系在一起的过程,用面向对象的术语讲,就是把一条消息和一个对象的方法相结合的过程。按照联编进行阶段的不同,可以分为两种不同的联编方法,静态联编和动态联编。
多态性提供了把接口与实现分开的另一种方法,提供了代码的组织性和可读性,更重要的是提高了软件的可扩充性。
//***********************************************************************************
#include <iostream>
using namespace std;
class animal
{
public:
void sleep()
{
cout<<"animal sleep"<<endl;
}
void breathe()
{
cout<<"animal breathe"<<endl;
}
};
class fish:public animal
{
public:
void breathe()
{
cout<<"fish bubble"<<endl;
}
};
int main()
{
fish fh;
fh.breathe();
animal *pAn=&fh;
pAn->breathe();
fish *pBn=&fh;
pBn->breathe();//you should know why the result is .
return 0;
}
//*******************************************************************************************
由静态联编支持的多态性称为编译时的多态性或静态多态性,也就是说,确定同名操作的具体操作对象的过程是在编译过程中完成的。在C++中,可以用函数重载和运算符重载来实现编译时的多态性。
由动态联编支持的多态性称为运行时的多态性或动态多态性,也就是说,确定同名操作的具体操作对象的过程是在运行过程中完成的。在C++中,可以用继承和虚函数来实现运行时的多态性。
函数的重载也称为多态函数,是实现编译时的多态性的形式之一,它使程序能用同一个名字来访问一组相关的函数,提高了程序的灵活性。函数重载时,函数名相同,但函数所带的参数个数或数据类型不同,编译系统会根据参数来决定调用哪个同名函数。
面向对象程序设计中,函数的重载表现为两种情况:
1、第一种是参数个数或类型有所差别的重载。
2、第二种是函数的参数完全相同但属于不同的类。
当函数的参数完全相同但属于不同的类时,为了让编译能正确区分调用哪个类的同名函数,采用以下两种方法:
对象名区别:在函数名前加上对象名来限制。如对象名.函数名
用类名和作用域运算符::加以区别;在函数名前加“类名::”来限制,如类名::函数名
//***************************************************************************************
#include<iostream>
using namespace std;
class point
{
int x,y;
public:
point(int xx,int yy)
{
x=xx;
y=yy;
}
double area()
{
return 0.0;
}
};
class circle:public point
{
int r;
public:
circle(int xx,int yy,int rr):point(xx,yy)
{
r=rr;
}
double area()
{
return 3.1415*r*r;
}
};
int main()
{
point pob(15,15);
circle cob(20,20,10);
cout<<"pob.area()= "<<pob.area()<<endl;
cout<<"cob.area()= "<<cob.area()<<endl;
cout<<"cob.point::area()= "<<cob.point::area()<<endl;
return 0;
}
//*********************************************************************************
虚函数是重载的另一种形式,实现的是动态地重载,即函数调用与函数体之间的联系是在运行时才建立,也就是动态联编。
一般对象的指针之间没有联系,彼此独立,不能混用。但派生类是由基类派生出来的,它们之间有继承关系,因此,指向基类和派生类的指针之间的指针之间也有一定的联系。如果使用不当,将会出现一些问题。
//**********************************************************
#include<iostream>
using namespace std;
class base
{
private:
int x,y;
public:
base(int xx=0,int yy=0)
{
x=xx;
y=yy;
}
void disp()
{
cout<<"base: "<<x<<" "<<y<<endl;
}
};
class base1:public base
{
private:
int z;
public:
base1(int xx,int yy,int zz):base(xx,yy)
{
z=zz;
}
void disp()
{
cout<<"base1:"<<z<<endl;
}
};
int main()
{
base obj(3,4),*objp;
base1 obj1(1,2,3);
objp=&obj;
objp->disp();
objp=&obj1;
objp->disp();
return 0;
}
//****************************************************************************************
声明为指向基类对象的指针可以指向它的共有派生类的对象,但不允许指向它的私有派生类的对象。
允许声明为指向基类对象的指针指向它的公有派生类的对象,但不允许将一个声明为指向派生类对象的指针指向基类的对象。
声明为指向基类对象的指针,当其指向它的派生类的对象时,只能直接访问派生类中从基类继承下来的成员,不能直接访问公有派生类中定义的成员,要访问其公有派生类中的成员,可将基类指针用显式类型转换为派生类指针。
在C++中引入虚函数的概念主要是为了解决在调用既有继承关系的类的成员函数时能够正确地被执行。
虚函数的定义时在基类中进行的,即把基类中需要定义为虚函数的成员函数声明为virtual。当基类中的某个成员被声明为虚函数后,其就可以在派生类中被 重新定义。在派生类中重新定义时,其函数原型,包括返回类型、函数名、参数个数和类型、参数的顺序都必须与基类中的原型完全一致。
//******************************************************************************************
#include<iostream>
using namespace std;
class animal
{
public:
void sleep()
{
cout<<"animal sleep"<<endl;
}
virtual void breathe()
{
cout<<"animal breathe"<<endl;
}
};
class fish:public animal
{
public:
void breathe()
{
cout<<"fish bubble"<<endl;
}
};
int main()
{
fish fh;
fh.breathe();
animal *pAn=&fh;
animal fs;
fs.breathe();
pAn->breathe();
fish *pBn=&fh;
pBn->breathe();
return 0;
}
//************************************************************************************
虚函数的定义必须在基类中进行,即只有基类中的函数才能被定义为虚函数,在派生类中不能定义某个函数为虚函数。
在派生类中被重新定义的基类中的虚函数,是函数重载的另一种形式,但它与函数重载又有如下的区别:一般函数的重载,要求其函数的参数或参数类型必须有所不 同,函数的返回类型也可以不同,但重载一个虚函数时,要求函数名、返回类型、参数个数、参数的类型和参数的顺序必须与基类中的虚函数的原型完全相同。
如果仅返回类型不同,其余相同,则系统会给出错误信息。
如果函数名相同,而参数个数、参数的类型或参数的顺序不同,系统认为是普通的函数重载,虚函数的特性将丢失。
//**********************************************************************************
#include<iostream>
using namespace std;
class base
{
private:
int x,y;
public:
base(int xx=0,int yy=0)
{
x=xx;
y=yy;
}
virtual void disp()
{
cout<<"base:"<<x<<", "<<y<<endl;
}
};
class base1:public base
{
private:
int z;
public:
base1(int xx,int yy,int zz):base(xx,yy)
{
z=zz;
}
void disp()
{
cout<<"base1:"<<z<<endl;
}
};
int main()
{
base obj(3,4),*objp;
base1 obj1(1,2,5);
objp=&obj;
objp->disp();
objp=&obj1;
objp->disp();
return 0;
}
//*******************************************************************************************
对于虚函数调用来说,每一个对象内部都有一个虚表指针,该虚表指针被初始化为本类的虚表。所以在程序中,不管你的对象类型如何转换,但该对象内部的虚表指针式固定的,所以才能实现动态地对象函数调用,这就是C++多态性实现的原理
一个虚函数无论被继承多少次,仍保持其虚函数的特性,与继承的次数无关。
//******************************************************************************************
#include<iostream>
using namespace std;
class base
{
public:
virtual ~base(){};
virtual void func() const
{
cout<<"base output!"<<endl;
}
};
class derive1:public base
{
public:
void func() const
{
cout<<"derive1 output!"<<endl;
}
};
class derive2:public derive1
{
public:
void func() const
{
cout<<"derive2 output!"<<endl;
}
};
void test(const base & rBase)
{
rBase.func();
};
int main()
{
base bObj;
derive1 d1Obj;
derive2 d2Obj;
test(bObj);
test(d1Obj);
test(d2Obj);
return 0;
}
//******************************************************************************************
在使用虚析构函数时,要注意一下几点:
只要基类的析构函数被声明为虚函数,则派生类的析构函数,无论是否使用virtual关键字进行声明,都自动成为虚函数。
如果基类的析构函数为虚函数,则当派生类未定义析构函数时,编译器所生成的析构函数也为虚函数。
使用虚函数时应注意如下问题:
虚函数的声明只能出现在类函数原型的声明中,不能出现在函数体实现的时候,而且,基类中只要保护成员或公有成员才能被声明为虚函数。
在派生类中重新定义虚函数时,关键字virtual可以写也可以不写,但在容易引起混乱时,应写上该关键字。
动态联编只能通过成员函数调用或通过指针、引用来访问虚函数,如果以对象名的形式来访问虚函数,将采用静态联编。
抽象类是一种特殊的类,其为类提供统一的操作界面。建立抽闲类的原因就是为了通过抽象类多态使用其中的成员函数,抽象类是带有纯虚函数的类
当在基类中不能为虚函数给出一个有意义的实现时,可以将其声明为纯虚函数,纯虚函数的实现可以留给派生类完成,纯虚函数的作用是为派生类提供一个一致的接 口,一般来说,一个抽象类带有至少一个纯虚函数,纯虚函数是爱一个基类中说明的虚函数,它在该基类 中没有具体的操作内容,要求各派生类在重新定义时根据自己的需要定义实际的操作内容。
Virtual<虚函数><函数名>(参数表)=0
//************************************************************************
#include<iostream>
using namespace std;
class point
{
public:
point(int i=0,int j=0)
{
x0=i;
y0=j;
}
virtual void set()=0;
virtual void draw()=0;
protected:
int x0,y0;
};
class line:public point
{
public:
line(int i=0,int j=0,int m=0,int n=0):point(i,j)
{
x1=m;
y1=n;
}
void set()
{
cout<<"line::set() called. ";
}
void draw()
{
cout<<"line::draw() called. ";
}
protected:
int x1,y1;
};
class ellipse:public point
{
public:
ellipse(int i=0,int j=0,int p=0,int q=0):point(i,j)
{
x2=p;
y2=q;
}
void set()
{
cout<<"ellipse::set() called. ";
}
void draw()
{
cout<<"ellipse::draw() called. ";
}
protected:
int x2,y2;
};
void drawobj(point *p)
{
p->draw();
}
void setobj(point *p)
{
p->set();
}
int main()
{
line *lineobj=new line;
ellipse *elliobj=new ellipse;
drawobj(lineobj);
drawobj(elliobj);
cout<<endl;
setobj(lineobj);
setobj(elliobj);
cout<<" Redraw the object... ";
drawobj(lineobj);
drawobj(elliobj);
return 0;
}
相关阅读 更多 +