重载分为两个大类:函数重载和运算符重载。
C++允许为同一个函数定义几个版本,称为函数重载。
函数重载使一个函数名具有多种功能,即具有“多种形态”,又称这种形态为多态性。
函数重载产生多态性的例子:
//利用函数重载的多态性,设计一个求最大值的函数
#include
using namespace std;
double max(double,double); //2个实型参数的函数原型
int max(int,int); //2个整型参数的函数原型
char max(char,char); //2个字符型参数的函数原型
int max(int,int,int); //3个整型参数的函数原型
void main( )
{
cout<<max(2.5, 17.54)<<" " <<max(56,8)<< " "<<max('w','p')<<endl;
cout<<"max(5,9,4)="<<max(5,9,4)<<" max(5,4,9)= "<<max(5,4,9)<<endl;
}
//函数实现
double max(double m1, double m2)
{
return(m1>m2)?m1:m2;
}
int max(int m1, int m2)
{
return(m1>m2)?m1:m2;
}
char max(char m1, char m2)
{
return(m1>m2)?m1:m2;
}
int max(int m1, int m2, int m3)
{
int t=max(m1,m2);
return max(t,m3);
}
C++能够正确调用相应函数, 程序输出结果如下:
17.54 56 w
max(5,9,4)=9 max(5,4,9)=9
从函数原型可见, 它们的区别:
一是参数类型不同,
二是参数个数不同。
编译器在编译时, 能根据源代码调用固定的函数标识符, 然后由连接器接管这些标识符,并用物理地址代替它们, 这就称为静态联编或先期联编。
同理, 可以设计一个求整数之和的函数。 不过, 如果要求4个整数之和, 使用函数重载则需要编写3个函数。这时可编写一个具有默认参数的函数。
编写一个具有默认参数的函数:
#include
using namespace std;
int add(int m1=0, int m2=0, int m3=0, int m4=0)
{
return m1+m2+m3+m4;
}
void main()
{
cout<<add(1,3)<<","<<add(1,3,5)<<","<<add(1,3,5,7)<<endl;
}
程序输出结果如下:
4,9,16
使用默认参数注意事项:
例如, 这里不能重载具有3个整型参数的add函数,因为编译器决定不了是使用3个参数, 还是4个参数的add函数, 只能对多于4个参数的add函数重载。
默认参数设计类的构造函数 特别注意:
一个类可以有多个构造函数, 这也是典型的函数重载。
可以使用域定义符“:: ” 显式地指出调用的是基类还是派生类的重载函数。
如果基类和派生类的成员函数具有相同的参数表, 则不属于函数重载。 这时按名字支配规律调用, 并且使用域定义符“:: ” 防止二义性。
下面是在基类和派生类中使用参数相同的同名函数的例子。
演示在基类和派生类中使用参数相同的同名函数的例子:
#include
using namespace std;
//创建基类
class Point
{
private:
int x,y;
public:
Point(int a, int b)
{
x=a;
y=b;
}
void Show() //基类的Show()函数
{
cout<<"x="<<x<<",y="<<y<<endl;
}
};
//公有派生类
class Rectangle : public Point
{
private:
int H, W;
public:
Rectangle(int, int, int, int); //构造函数原型
void Show()
{
cout<<"H="<<H<<",W="<<W<<endl;
}
void Display()
{
Point::Show(); //使用基类的Show()函数
Rectangle::Show(); //使用派生类的Show()函数
}
};
Rectangle::Rectangle(int a, int b,int h, int w):Point(a,b) //定义构造函数
{
H=h;
W=w;
}
void main()
{
Point a(3,4);
Rectangle r1(3,4,5,6);
a.Show(); //基类对象调用基类Show()函数
r1.Show(); //派生类对象调用派生类Show()函数
r1.Point::Show(); //派生类对象调用基类Show()函数
r1.Display();
}
程序输出如下;
x=3,y=4
H=5,W=6
x=3,y=4
x=3,y=4
H=5,W=6
派生类的Display( )函数使用域定义符“::”指明调用的是基类还是派生类的Show()函数。
其实, 调用派生类本身的Show()函数, 不需要使用“Rectangle::”来限定, 它使用名字支配规律即可正确地调用自己Show()函数, 这里是有意使用显示方式“ Rectangle::”, 以便帮助读者进一步理解域定义符“:: ” 的作用。
如果不需要单独显示派生类的H和W的数值, 可将void()函数直接定义为如下形式:
void Show()
{
Point::Show(); //显示x和y的数值
cout<<"H="<<H<<",W="<<W<<endl;
}
如果同名函数的参数类型不同或者参数个数不同, 则属于重载。 这时, 也可以使用域定义符显式地指明被调用的函数。
因为任何运算都是通过函数来实现的, 所以运算符重载其实就是函数重载, 要重载某个运算符, 只要重载相应的函数就可以了。
与重载函数不同的是:
需要使用新的关键字“operator”,它经常和C++的一个运算符连用, 构成一个运算符函数名。
例如“operator +”。
这种构成方法就可以像重载普通函数那样, 重载运算符函数operator+( )。
由于C++已经为各种基本数据类型定义了该运算符函数, 所以只需要为自己定义的类型重载operator + ( )就可以了。
一般地, 为用户定义的类型重载运算符, 都要求能够访问这个类型的私有成员。 所以只有两条路可走:
为区别这两种情况,
将作为类的成员函数称为类运算符, 而将作为类的友元的运算符称为友元运算符。
C++的运算符大部分都可以重载, 不能重载有“. ”、 “::”、 “*” 和“? :”。
前面3个是因为在C++中有特定的含义, 不准重载可以避免不必要的麻烦;“ ? :”则是因为不值得重载。 另外, “ sizeof”和“ #”不是运算符, 因而不能重载。
= 、 ()、 [] 、 ->这4个运算符只能用类运算符来重载。
C++对用户定义的运算符重载的限制:
使用友元函数重载运算符“<<”和“>>”:
#include
class test
{
private:
int i;
float f;
char ch;
public:
test(int a=0, float b=0, char c='\0')//构造函数
{
i=a;
f=b;
ch=c;
}
friend ostream &operator << (ostream & , test); //友元函数-重载<<
friend istream &operator >> (istream & , test &); //友元函数-重载>>
};
ostream &operator << (ostream & stream, test obj)
{
stream<<obj.i<<",";
stream<<obj.f<<",";
stream<<obj.ch<<endl;
return stream;
}
istream &operator >> (istream & t_stream, test&obj)
{
t_stream>>obj.i;
t_stream>>obj.f;
t_stream>>obj.ch;
return t_stream;
}
void main()
{
test A(45,8.5,'W');
operator <<(cout,A);
test B,C;cout<<"Input as i f ch:";
operator >>(cin,B);
operator >>(cin,C);
operator << (cout,B);
operator << (cout,C);
}
运行示例如下:
45,8.5,W
Input as i f ch:5 5.8 A 2 3.4 a //假设输入两组
5,5.8,A
2,3.4,a
将主函数写成上面的函数调用形式, 是为了演示运算符就是函数重载。 一般在使用时, 则直接使用运算符。
下面是正规的使用方式:
void main()
{
test A(45,8.5,'W');
cout<<A;
test B,C;
cout<<"Input as i f ch:";
cin>>B>>C;
cout<<B<<C;
}
显然, 运算符“ <<”重载函数有两个参数, 第1个是ostream 类的一个引用, 第2个是自定义类型的一个对象。 这个重载方式是友元重载。
另外, 这个函数的返回类型是一个ostream 类型的引用, 在函数中实际返回的是该函数的第1个参数, 这样做是为了使得“<<”能够连续使用。
例如, 对于语句
cout << a << b; //a,b均为自定义类型的对象
第1次, 系统把 cout << a 作为operator << ( cout,a);来处理, 返回cout, 紧接着又把刚返回的cout连同后面的“<< b”一起作为operator << (cout,b);处理, 再返回cout, 从而实现了运算符“ <<”的连续使用。
使用类运算符重载"++"运算符:
#include
using namespace std;
class number
{
int num;
public:
number( int i )
{
num=i;
}
int operator ++ ( ); // 前缀: ++n。参数表里没有int是前++
int operator ++ ( int ); // 后缀: n++。参数表里有int是后++
void print( )
{
cout << "num="<<num << endl;
}
};
int number :: operator ++ ( )
{
num ++;
return num;
}
int number :: operator ++ ( int ) //不用给出形参名
{
int i=num;
num ++;
return i;
}
void main( )
{
number n(10);
int i = ++n; // i=11, n=11
cout <<"i="<<i<<endl; // 输出i=11
n.print(); // 输出n=11
i=n++; // i=11, n=12
cout <<"i="<< i << endl; // 输出i=11
n.print( ); // 输出n=12
}
同理,
如果主函数的第2条和第5条语句使用函数调用方式, 则分别为:
int i=n.operator ++ ( );
i=n.operator ++(0);
由此可见, 只要定义正确, 不必再使用函数调用方式,而直接使用运算符
如果运算符所需的操作数, 尤其是第一个操作数希望进行隐式类型转换, 则该运算符应该通过友元来重载。 另一方面, 如果一个运算符的操作需要修改类对象的状态, 则应当使用类运算符, 这样更符合数据封装的要求。
使用对象作为友元函数参数来定义运算符“+”的例子:
#include
class complex
{
private:
double real, imag;
public:
complex(double r=0, double i=0)//构造函数
{
real=r;
imag=i;
}
//使用对象作为友元函数参数来定义运算符“+”的例子
friend complex operator + (complex, complex);
void show()
{
cout<<real<<"+"<<imag<<"i";
}
};
complex operator + (complex a,complex b)
{
double r = a.real + b.real;
double i = a.imag + b.imag;
return complex(r,i);
}
void main()
{
complex x(5,3), y ;
y =x+7; //语句2
y =7+y; //语句3
y.show();
}
程序运行正常, 因为语句2和语句3可以分别解释为:
y =operator +(x,7);
y =operator +(7,y);
而“ 7”均可通过构造函数转换成complex类型的对象,使其参数匹配, 保证正常工作。 如果使用类运算符,假设为如下形式:
complex operator + (complex a)
{
double r = a.real + real;
double i = a.imag + imag;
return complex(r,i);
}
因为“ y =7+y;”等价为“ y=7.operator+(y);”, 所以系统无法解释这个式子的含义。
由此可见,
如果对象作为重载运算符函数的参数, 则可以使用构造函数将常量转换成该类型的对象。
如果使用引用作为参数, 则这些常量不能作为对象名使用, 编译系统就要报错。
如果将上面友元和类运算符均使用引用作为参数, 则“ y =x+7;”和“ y =7+y;”都不能通过编译。
在使用它们时, 必须分清场合及其使用方法。MFC中大量使用运算符重载, 其中也包括类型转换运算符(该运算符没有返回值)。 典型的是CPoint类和CString类。 CdumpContext和CArchive类中定义了大量“>>”和“<<”运算符的重载版本。
模板有函数模板和类模板两种。
在程序设计时并不给出相应数据的实际类型, 而在实际编译时, 由编译器利用实际的类型给予实例化, 使它满足需要。 由此可见, 可使编译器成为一种在函数模板引导下, 制作符合要求的代码的程序设计辅助工具。
函数模板声明的一般方法如下:
template <函数模板参数>
返回类型 函数名
{
//函数体
};
规定模板以template关键字和一个形参表开头。
在尖括号里只需要说明一个类型参数的标识符,
例如
定义一个求最大值函数:
template <class T>
T max(T m1, T m2)
{
return(m1>m2)?m1:m2;
}
class意为“用户定义的或固有的类型”。
字母T标识这个模板有一个参数类型。
当在程序中使用max(2,5)时,编译器能推断出这个T为int,并使用如下版本产生具体的模板函数:
int max(int m1, int m2)
{
return(m1>m2)?m1:m2;
}
而max(2.5,8.8)则使用如下版本:
double max(double m1, doublem2)
{
return(m1>m2)?m1:m2;
}
由此可见,
在调用函数模板时,函数参数的类型决定到底使用模板的哪个版本。
也就是说,
模板的参数是由函数推断出来的,
这种使用方法称为默认方式。
一般形式为:
函数模板名(参数列表)
也可以使用max(2,5)明确指出类型为int,
这称为显式参数比较准则。
即:
函数模板名<模板参数>(参数列表)
每次调用都显式地给出比较准则,也会使人厌烦。显式规则可以用于特殊场合,一般喜欢使用如下默认方式。
对于一个默认调用,能从函数的参数推断出模板参数的能力是其中最关键的一环。
编译器能够从一个调用推断出类型参数和非类型参数,从而省去显式调用的麻烦。
条件是由这个调用的函数参数表能够惟一地标识出模板参数的一个集合。
另外,
C++还专门定义一个仅仅用在模板中的关键字typename, 它的用途之一是代替template参数列表中的关键字class。
虽然按照默认约定, 定义一个模板, 用户可以使用能想到的任何模板参数(或者模板参数组合), 但用些用户却宁肯选择另外的实现方法。
例如定义的模板函数:
max:
template <typename T> //声明模板
T max(T m1, T m2) //求两个数的最大值
{
return(m1>m2)?m1:m2;
}
它虽然可以处理字符串, 但用户希望换一种处理方法。
用户的方案是:
如果模板参数不是指针, 就是用这个模板;若果是指针, 就使用如下的处理方法:
char *max(char *a, char *b)
{
return (strcmp(a,b)>=0?a:b);
}
由于普通函数优先于模板函数, 在执行如下语句
cout<<max("ABC","ABD")<<",“< ";
时, 第一个字符串参数是调用普通函数, 第二个单字符参数则调用模板函数。
不过,
为了形成完整的模板系, 便于管理, 并保证在无调用时不会生成任何无用代码, 希望仍使用模板形式。
这可以通过提供多个不同的定义方式来处理, 并由编译器基于在使用处提供的的模板参数, 在这些定义中做出选择。
对一个模板的这些可以互相替换的定义称为用户定义的专门化,或简称为用户专门化。
前缀“template <>”说明这是一个专门化, 在描述时不用模板参数。 可以写成:
template <>char *max<char*>(char *a, char *b)
{
return (strcmp(a,b)>=0?a:b);
}
在函数名之后的
template <>char *max<>(char *a, char *b)
{
return (strcmp(a,b)>=0?a:b);
}
给出了template <>前缀, 第二个<>也属多余之举, 可以简单地写成如下形式:
template <>char *max(char *a, char *b)
{
return (strcmp(a,b)>=0?a:b);
}
C++模板的机制也是重载。
模板提供了看起来很像多态性的语法, 当提供细节时, 模板就可以生成模板函数。
因为选择调用哪一个函数是在编译时实现的, 所以是静态联编。
下面通过重载进一步扩大已定义模板max的适用范围。
专门化和重载的例子:
#include
using namespace std;
template <typename T> //声明模板
T max(T m1, T m2) //求两个数的最大值
{
return(m1>m2)?m1:m2;
}
template <typename T> //声明函数模板时需要重写template
T max(T a, T b, T c) //重载
{
return max(max(a,b),c);
}
template <class T> //声明函数模板时, 需要重写template
T max(T a[], int n) //重载, 求数组中的最大值
{
T maxnum=a[0];
for(int i=0; i<n;i++)
if (maxnum<a[i])
maxnum=a[i];
return maxnum;
}
template <> //专门化
char *max(char *a, char *b) //使用指针
{
return (strcmp(a,b)>=0?a:b);
}
int max(int m1, double m2) //普通函数
{
int m3=(int)m2; return(m1>m3)?m1:m3;
}
void main( )
{
cout<<max("ABC","ABD")<<" "; //1
cout<<max("ABC","ABD","ABE")<<" "; //2
cout<<max('W','T','K')<<" "; //3
cout<<max(2.0,5.,8.9)<<" "; //4
cout<<max(2,6.7)<<" "; //5
double d[]={8.2,2.2,3.2,5.2,7.2,-9.2,15.6,4.5,1.1,2.5}; //定义
//实数数组d
int a[]={-5,-4,-3,-2,-1,-11,-9,-8,-7,-6}; //定义整数数组a
char c[]="acdbfgweab"; //定义字符串数组c
cout<<"intMax="<<max(a,10)<<" doubleMax=“< charMax="<<max(c,10)<<endl;
}
程序输出结果为:
ABD ABE W 8.9 6
intMax=-1 doubleMax=15.6 charMax=w
注意执行语句2和3的区别:它们执行重载的过程一样, 但在重载函数调用时, 前者使用专门化(指针参数)模板, 后者使用定义(选择单字符参数)模板。语句5不调用模板, 使用普通的函数。
如果将类看做包含某些数据类型的框架, 把对支持该类型的不同操作理解为:将数据类型从类中分离出来,允许单个类处理通用的数据类型T。
其实, 这种类型并不是类, 而仅仅是类的描述, 常称之为类模板。
在编译时, 由编译器将类模板与某种特定数据类型联系起来, 就产生一个真实的类。
由此可见,
利用类模板进行程序设计, 就如烹调食物一样, 只要购买了原料,就可以做出不同口味的菜肴。
类模板声明的一般方法如下:
template <类模板参数>
class 类名
{
//类体
};
类模板也称为参数化类型。
初始化类模板时, 传给它具体的数据类型, 就产生了模板类。
使用模板类时,编译器自动产生处理具体数据类型的所有成员( 数据成员和成员函数)。
只要赋给模板一种实际数据类型, 就会产生新的类,而且以特定类型替代模板参数。
定义对象的一般格式如下:
类名<模板实例化参数类型>对象名(构造函数实参列表);
类名<模板实例化参数类型>对象名; //默认或者无参数构造函数
模板实例化参数类型包括数据类型和值, 编译器不能从构造函数列表推断出模板实例化参数类型, 所以必须显式地给出它们的参数类型。
在类体外面定义成员函数时, 必须用template重写模板函数声明。 一般格式如下:
template<模板参数>
返回类型 类名<模板类型参数>::成员函数名(函数参数列表)
{
//函数体
}
<模板类型参数>是指template的“< >”内使用class(或typename)声名的类型参数, 也就是使用
演示对4个数字求和的类模板程序:
#include
using namespace std;
template <class T, int size=4> //可以传递程序中的整数参数值
class Sum
{
T m[size]; //数据成员
public:
Sum(T a, T b, T c, T d ) //构造函数
{
m[0]=a; m[1]=b; m[2]=c; m[3]=d;
}
T S() //求和成员函数
{
return m[0]+m[1]+m[2]+m[3];
}
};
void main()
{
Sum<int, 4>num1(-23,5,8,-2); //整数求和
Sum<float, 4>f1(3.5f, -8.5f,8.8f,9.7f); //单精度求和
//使用f显式说明float型
Sum<double,4>d1(355.4, 253.8,456.7,-67.8);
Sum<char,4>c1(‘W’,-2,-1,-1); //字符减, 等效于
//'W'-4, 结果为S
cout<<num1.S()<<" ,"<<f1.S()<<", “<, "<<c1.S()<<endl;
}
输出结果为:
-12 ,13.5 ,998.1, S
模板类的成员函数不能声明为虚函数, 它的基类和派生类都可以是模板(或非模板)类。
前面已经讨论过全是非模板类的情况,
同样,
类模板也可以继承, 继承的方法也一样。
声明模板继承之前, 必须重新声明模板。
一般可以采用如下处理方法:
下面举例说明最后一种情况。
可以设计一个模板类, 但它的派生类并不确定, 而是在实例化时决定, 即基类由模板参数决定。
下面是一个简单形式:
template <typename T>
class C:public T
{
public:
C(int a):T(a){}
};
类C继承由T标识的基类。 C通过构造函数与基类联系,构造函数的多少根据需要决定.这里是以传递一个整数数据为例, 则
C(base)b(85);
是将一个普通类(非模板类)base作为基类, 而
C< Base<int> >y1(125,188);
则将模板类作为基类。 这个未知基类还可以是派生类。
设计非模板类A1, 让它派生非模板类A11。 设计类模板B1, 由它派生类模板B11。 设计模板类C, 让它公有继承未知基类。 为了简单易懂, 直接使用整型数据。
#include
using namespace std;
class A1
{
int x;
public:
A1(int c=8):x(c)
{
cout<<"call A1 "<<x<<endl;
}
};
class A11:public A1
{
int x1;
public:
A11(int a,int b):A1(a),x1(b)
{
cout<<"call A11 "<<x1<<endl;
}
};
template <typename T>
class B1
{
T x;
public:
B1(T a=0):x(a)
{
cout<<"call B1 "<<x<<endl;
}
};
template <typename T>
class B11:public B1<T>
{
T x1;
public:
B11(T a,T b):B1<T>(a),x1(b)
{
cout<<"call A11 "<<x1<<endl;
}
};
template <typename T>
class C:public T
{
public:
C(int a):T(a)
{
cout<<"call C "<<endl;
}
C(int a, int b):T(a,b)
{
cout<<"call C "<<endl;
}
};
void main()
{
C<A1>x(90);
C<A11>x1(25,95);
C< B1<int> >y(189); //右边两个>号之间必须至少有一个空各
C< B11<int> >y1(125,188);
}
程序输出结果如下:
call A1 90
call C
call A1 25
call A11 95
call C
call B1 189
call C
call B1 125
call A11 188
call C
类模板不能重载, 也不能使用虚成员函数, 但可以使用友元。
它也可以像函数模板那样进行专门化。 专门化也是使用前缀templete <>。
本题以一个堆栈类模板为例, 说明专门化和使用的方法。 当要产生int或double类型的模板类时,使用类模板Stack进行实例化。 当使用字符串堆栈时,使用Stack的专门化。 下面不涉及工作原理, 仅从编程和使用方法上进行演示。
#include
using namespace std;
template <class T> //声名类模板
class Stack
{
int counter;
int max;
T *num;
public:
Stack(int a):counter(0),max(a),num(new T[a])
{
}
bool isEmpty()const
{//判断堆栈是否为空
return counter==0;
}
bool isFull()const
{//判断堆栈是否为满
return counter==max;
}
int count()const
{//返回堆栈种数据的个数
return counter;
}
bool push(const T&data)
{ //将数据压进堆栈
if(isFull())
return false;
num[counter++]=data;
return true;
}
bool pop(T&data)
{ //将数据从堆栈弹出并存入data中
if(isEmpty())
return false;
data=num[--counter];
return true;
}
const T&top()const
{//取栈顶数据但并不出栈
return num[counter-1];
}
~Stack()
{
delete[]num;
}
};
//专门化
template <> //专门化前缀
class Stack<char *>
{
int counter;
int max;
char**num;
public:
Stack(int a):counter(0),max(a),num(new char*[a])
{
}
bool isEmpty()const
{
return counter==0;
}
bool isFull()const
{
return counter==max;
}
int count()const
{
return counter;
}
bool push(const char*data)
{
if(isFull())
return false;
num[counter]=new char[strlen(data)+1];
strcpy(num[counter++],data);
return true;
}
bool pop(char data[])
{
if(isEmpty())
return false;
strcpy(data, num[--counter]);
delete []num[counter];
return true;
}
const char *&top()const
{
return num[counter-1];
}
~Stack()
{
while(counter) delete[]num[--counter];
delete[]num;
}
};
void main()
{
Stack<int>st(8); //整数堆栈st
int i=0;
while(!st.isFull())
{ //建栈操作
st.push(10+i++); //将数据压入堆栈
cout<<st.top()<<" "; //显示栈顶数据
}
cout<<endl;int data;
while(!st.isEmpty())
{ //出栈操作
st.pop(data); //弹出堆栈
cout<<data<<" "; //显示出栈数据
}
cout<<endl;
Stack<double>st1(8); //建立double型堆栈st1
i=0;
while(!st1.isFull())
{
st1.push(0.5+i++);
cout<<st1.top()<<" ";
}
cout<<endl;
double data1;
while(!st1.isEmpty())
{
st1.pop(data1);
cout<<data1<<" ";
}
cout<<endl;char*str[]={"1st","2nd","3rd","4th","5th","6th","7th","8th"};
Stack<char *>st2(8); //建立字符串型堆栈st2
i=0;
while(st2.push(str[i++]))
cout<<st2.top()<<" ";
cout<<endl;
char strdata[8];
while(st2.pop(strdata))
cout<<strdata<<" ";
cout<<endl;
}
程序运行结果如下:
10 11 12 13 14 15 16 17
17 16 15 14 13 12 11 10
0.5 1.5 2.5 3.5 4.5 5.5 6.5 7.5
7.5 6.5 5.5 4.5 3.5 2.5 1.5 0.5
1st 2nd 3rd 4th 5th 6th 7th 8th
8th 7th 6th 5th 4th 3rd 2nd 1st
MFC中大量使用模板, 例如文档模板(Document Template)主要用 于 建 立 和 管 理 文 档 、 视 和 框 架 对 象 。 CArray 是 派 生 于CObiect类的数组集合模板等。
多态性也称后约束 (late binding) 或动态约束(dynamic binding) , 它 常 用 虚 函 数 (virtualfunctions) 来实现。
动态联编所支持的多态性称为运行时的多态性, 这由虚函数来支持。
虚函数类似于重载函数, 但与重载函数的实现策略不同, 即对虚函数的调用使用动态联编。
假设下面例子中的类Point和类Circle各有一个area函数, 各自完成不同的功能。
类的对象和调用的函数一一对应, 编译时即可确定调用关系, 从而产生编译时的多态性。
分析下面程序的输出结果。
#include
using namespace std;
const double PI=3.14159;
class Point
{
private:
double x,y;
public:
Point(double i, double j)
{
x=i;
y=j;
}
double area( )
{
return 0;
}
};
class Circle : public Point
{
private:
double radius;
public:
Circle(double a, double b,double r):Point(a,b)
{
radius=r;
}
double area( )
{
return PI*radius*radius;
}
};
void main()
{
Point a(1.5,6.7);
Circle c(1.5, 6.7, 2.5);
cout<<"area of Point is "<<a.area()<<endl; //(1)
cout<<"area of Circle is "<<c.area()<<endl; //(2)
Point *p=&c; //(3)
cout<<"area of Circle is "<<p->area()<<endl; //(4)
Point &rc=c; //(5)
cout<<"area of Circle is "<<rc.area()<<endl; //(6)
}
为了便于理解, 在注释中给出相应编号。 根据编号解释如下:
编译器对(1)的解释是:
显式的a.area()表达式明确告诉编译器, 它调用的是对象a的成员函数area, 输出0。
同理,
对于(2)而言, 显式的c.area()表达式明确表示调用的是对象c的成员函数area, 输出19.6349。
名字支配规律决定它们调用各自的同名函数area。
(3)和(4)的问题实质是:如果基类和派生类都定义了“相同名称之成员函数” , 通过对象指针调用成员函数时, 是决定该指针的基类类型, 还是决定指针实际所指的类型?
也就是说,
表达式“ p->area()”应该调用Point::area(), 还是调Circle::area()?
根据第6章的赋值兼容性规律, 应该调用基类的area函数, 输出为0。
(5)和(6)的道理与此一样, 输出为0。
下面再从内存分配原理入手, 深入讨论一下赋值兼容规律。
图1.1是Point的对象a和Circle对象c的内存分配关系示意图。
由此可见, 对象的内存地址空间中只包含数据成员, 并不存储有关成员函数的信息。 这些成员函数的地址翻译过程与其对象的内存地址无关。 编译器只根据数据类型翻译成员函数的地址并判断调用的合法性。
如果声明如下两个指针:
Point * pPiont;
Circle* pCircle;
图1.2是基类Point和派生类Circle的UML表示图, 类图中只给出同名函数, 并且在它们的右边给出声明的类指针与类的关系。
声明的基类指针只能指向基类, 派生类指针只能指向派生类。
它们的原始类型决定它们只能调用各自的同名函数area。
除非派生类没有基类的同名函数, 派生类的指针才根据继承原则调用基类的函数, 但这已经脱离给定的条件。
对于程序中的如下代码段:
Point *p=&c; //(3)
cout<<"area of Circle is "<<p->area()<<endl; //(4)
虽然p用c的地址初始化, 但也是枉然。
p的原始类型是Point, 使用Point的指针只能调用对象c的基类的area函数。
因此
(4)的输出是0。
引用的情况与指针一样, 所以(6)也输出0。
这完全符合赋值兼容性则。 编译器编译成员函数是根据数据类型, 类型是事先决定了的, 所以由静态联编决定。
如果让编译器动态联编, 也就是在编译“ Point*p=&c; ”语句时, 只根据兼容性规则检查它的合理性, 也就是符合“ 派生类对象的地址可以赋给基类的指针” 。
至于“ p->area()”调用哪个函数, 等程序运行到里时再决定。
说到底, 想让程序给出如下输出:
area of Point is 0
area of Circle is 19.6349
area of Circle is 19.6349
area of Circle is 19.6349
为了实现这一目的,
就要使类Point的指针p指向派生类函数area的地址。
显然, 目前是做不到的。
必须给这两个函数一个新的标识符, 以便使它们与目前介绍的成员函数区别开来
假设使用关键字virtual声明Point类的area函数, 将这种函数称为虚函数。 下面是使用内联函数完成的定义:
virtual double area( )
{
return 0.0;
}
当编译系统编译含有虚函数的类时, 为它建立一个虚函数表, 表中的每一个元素都指向一个虚函数的地址。
此外,
编译器也为类增加一个数据成员, 这个数据成员是一个指向该虚函数表的指针,通常称为vptr。
Point只有一个虚函数, 所以虚函数表里也只有一项。
图1.3给出它的对象示意图。
如果派生类Circle没有重写这个area虚函数, 则派生类的虚函数表里的元素所指向的地址就是基类Point的虚函数area的地址,即派生类仅继承基类的虚函数, 它调用的也是基类的area函数。
现在将它改写如下:
virtual double area( )
{
return PI*radius*radius;
}
这时,编译器也将派生类虚函数表里的元素指向Circle::area(),即指向派生类area函数的地址。
图1.3图示了Circle的对象c和Point的对象a的对象地址分配图
由此可见,
**虚函数的地址翻译取决于对象的内存地址。 **
编译器为含有虚函数类的对象首先建立一个入口地址, 这个地址用来存放指向虚函数表的指针vptr, 然后按照类中虚函数的声明次序, 一 一填入函数指针。
当调用虚函数时, 先通过vptr找到虚函数表,然后再找出虚函数的真正地址。
派生类能继承基类的虚函数表, 而且只要是和基类同名的(参数也相同)成员函数, 无论是否使用virtua声明, 它们都自动成为虚函数。
如果派生类没有改写继承基类的虚函数, 则函数指针调用基类的虚函数。
如果派生类改写了基类的虚函数, 编译器将重新为派生类的虚函数建立地址, 则函数指针调用这个改写过的虚函 数 。
如 图 10.3 所 示 , 派 生 类 Circle 的 函 数 指 针 调 用 的 是
Circle::area()
虚函数的调用规则是:
根据当前对象, 优先调用对象本身的虚成员函数。
这有点像名字支配规律, 不过虚函数是动态绑定的, 是在执行期“间接” 调用实际上欲绑定的函数。 显然, 程序运行到语句
p->area();
时 , 才 能 确 定 p 指 向 的 是 派 生 类 Circle 的 对 象 , 应 该 调 用Circle::area()函数。
为实现某种功能而假设的函数称作虚函数。
虚函数是实现多态性的基础。
一旦基类定义了虚函数, 该基类的派生类中的同名函数也自动成为虚函数。
虚函数只能是类中的一个成员函数, 但不能是静态成员, 关键字virtual用于类中该函数的声明中。
例如:
class A
{
public:
virtual void fun( ); //声明虚函数
};
void A::fun( ) //定义虚函数
{
// ......
}
当在派生类中定义了一个同名的成员函数时, 只要该成员函数的参数个数和相应类型以及它的返回类型与基类中同名的虚函数完全一样(例如void area(void) 函数) , 则无论是否为该成员函数使用 virtual, 它都将成为一个虚函数。
在上节的例子中,基类Point声明成员函数area为“virtual void area(void);”, 则派生类Circle中的area函数自动成为虚函数。
关键字== virtual== 指示 C++编译器对调用虚函数进行动态联编。
这种多态性是程序运行到此处才动态确定的, 所以称为运行时的多态性。
不过,
使用虚函数并不一定产生多态性, 也不一定使用动态联编。
例如,
在调用中对虚函数使用成员名限定, 可以强制 C++ 对该函数的调用使用静态联编。
产生这种多态性的前提有如下3条:
(1) 类之间的继承关系满足赋值兼容性规则;
(2) 改写了同名虚函数;
(3) 根据赋值兼容性规则使用指针(或引用) 。
满足前两条并不一定产生动态联编, 必须有第3条才能保证实现动态联编。
第3条又有两种情况。
第1种是已经演示过的按赋值兼容性定义使用基类指针(或引用)访问虚函数。
第2种是把指针(或引用)作为函数参数。
下面的例子是设计一个外部函数display, 通过指针(或引用)实现多态性的完整程序。
分别使用指针和引用的display函数。
分析下面程序的输出结果:
#include
using namespace std;
const double PI=3.14159;
class Point
{
private:
double x,y;
public:
Point(double i, double j)
{
x=i; y=j;
}
virtual double area( )
{
return 0;
}
};
class Circle : public Point
{
private:
double radius;
public:
Circle(double a, double b,double r):Point(a,b)
{
radius=r;
}
double area( )
{
return PI*radius*radius;
}
};
void display(Point *p)
{
cout<<p->area()<<endl;
}
void display(Point&a)
{
cout<<a.area()<<endl;
}
void main()
{
Point a(1.5,6.7);
Circle c(1.5, 6.7, 2.5);
Point *p=&c;
Point &rc=c;
display(a);
display(p);
display(rc);
}
程序输出如下:
19.6349
19.6349
由于动态联编是在运行时进行的, 相对于静态联编, 它的运行效率比较低, 但它可以使程序员对程序进行高度抽象, 设计出可扩充性好的程序。
假设基类和派生类都只有一个公有的数据成员, 其中类A有vfunc1和vfunc2两个虚函数和func1和func2两个实函数。
类A公有派生类B, 类B改写vfunc1和func1函数, 它又作为类C的基类, 公有派生类C。
类C也改写vfunc1和func1函数。
图1.4给出3个类建立的vptr和vtable之间的关系图解以及实函数与虚函数的区别。
首先给vptr分配地址, 它所占字节数决定对象中最长数据成员的长度。
因为3个类的数据成员都是整型, 所以VC为vptr分配4个字节。 如果有double型的数据, 则要分配8个字节。
【例1.13】 是演示这一关系的程序。
从图1.4中可见,对象的起始地址是vptr。
它指向vtable, vtable为每个虚函数建立一个指针函数,如果只是继承基类的虚函数,则它们调用基类的虚函数,这就是b和c的vtable表中(*vfunc2)( )项所描述的情况。
如果派生类改写了基类的虚函数,则调用自己的虚函数,这就是b和c的vtable表中(*vfunc1)( )项所描述的情况。
实函数不是通过地址调用,用带底纹的方框表示,它们由对象的名字支配规律决定。
【例1.13】 是程序实现。
【 例1.13】 实函数和虚函数调用过程。
#include
using namespace std;
class A
{
public:
int m_A;
A(int a)
{
m_A=a;
}
void func1()
{
cout<<"A::func1( )"<<endl;
}
void func2()
{
cout<<"A::func2( )"<<endl;
}
virtual void vfunc1()
{
cout<<"A::vfunc1( )"<<endl;
}
virtual void vfunc2()
{
cout<<"A::vfunc2( )"<<endl;
}
};
class B:public A
{
public:
int m_B;
B(int a, int b):A(a),m_B(b)
{
}
void func1()
{
cout<<"B::func1( )"<<endl;
}
void vfunc1()
{
cout<<"B::vfunc1( )"<<endl;
}
};
class C:public B
{
public:
int m_C;
C(int a, int b, int c):B(a,b),m_C(c)
{
}
void func1()
{
cout<<"C::func1( )"<<endl;
}
void vfunc1()
{
cout<<"C::vfunc1( )"<<endl;
}
};
void main()
{
//输出类的长度(字节数)
cout<<sizeof(A)<<","<<sizeof(B)<<"."<<sizeof(C)<<endl;
A a(11);
B b(21,22);
C c(31,32,33);
//输出类的首地址及数据成员地址, 验证首地址是vptr地址
cout<<&a<<","<<&(a.m_A)<<endl;
cout<<&b<<","<<&b.m_A<<","<<&b.m_B<<endl;
cout<<&c<<","<<&c.m_A<<","<<&c.m_B<<",“<<&c.m_C<<endl;//使用基类指针
A* pa=&a; //pa指向基类A
pa->vfunc1(); //调用A::vfunc1( )
pa->vfunc2(); //调用A::vfunc2( )
pa->func1(); //调用A::func1( )
pa->func2(); //调用A::vfunc2( )
cout<<endl;
pa=&b; // pa指向派生类B
pa->vfunc1(); //调用B::vfunc1( )
pa->vfunc2(); //调用A::vfunc2( )
pa->func1(); //静态联编, 只能调用A::func1( )
pa->func2(); //静态联编, 只能调用A::func2( )
cout<<endl;
pa=&c; // pa指向派生类C
pa->vfunc1(); //调用C::vfunc1( )
pa->vfunc2(); //调用A::vfunc2( )
pa->func1(); //静态联编, 只能调用A::func1( )
pa->func2(); //静态联编, 只能调用A::func1( )
cout<<endl;//使用类B的指针, 类B是类C的直接基类
B* pb=&b; // pb指向基类B
pb->vfunc1(); //调用B::vfunc1( )
pb->vfunc2(); //调用A::vfunc2( )
pb->func1(); //静态联编, 调用B::func1()
pb->func2(); //静态联编, 只能调用A::func2()
cout<<endl;
pb=&c; // pb指向派生类C
pb->vfunc1(); //调用C::vfunc1()
pb->vfunc2(); //调用A::vfunc2()
pb->func1(); //静态联编, 只能调用B::func1()
pb->func2(); //静态联编, 只能调用A::func2()
cout<<endl;
//使用类C的指针
C* pc=&c; // pc指向派生类C
pc->vfunc1(); //调用C::vfunc1( )
pc->vfunc2(); //调用A::vfunc2( )
pc->func1(); //静态联编, 调用C::func1()
pc->func2(); //静态联编, 只能调用A::func2( )
}
对象a有一个整型数据, 应分配4个字节, vptr也是4个字节, 总共8个字节。 对象b和c依次增加一个整型数据成员, 内存分配也顺增4个字节。
输出结果如下:
8,12.16
0012FF78,0012FF7C //vptr, m_A
0012FF6C,0012FF70,0012FF74 //vptr, m_A, m_B
0012FF5C,0012FF60,0012FF64,0012FF68 //vptr, m_A,
// m_B, m_C
// A* pa=&a;
A::vfunc1()
A::vfunc2()
A::func1()
A::func2()
// pa=&b;
B::vfunc1()
A::vfunc2()
A::func1()
A::func2()//pa=&c;
C::vfunc1()
A::vfunc2()
A::func1()
A::func2()
// B* pb=&b;
B::vfunc1()
A::vfunc2()
B::func1()
A::func2()
// pb=&b;
C::vfunc1()
A::vfunc2()
B::func1()
A::func2()
//C* pc=&c;
C::vfunc1()
A::vfunc2()
C::func1()
A::func2()
在许多情况下, 在基类中不能为虚函数给出一个有意义的定义,这时可以将它说明为纯虚函数。 它的定义留给派生类来做。
说明纯虚函数的一般形式为:
class 类名
{
virtual 函数类型 函数名(参数列表) =0;
};
点没有面积, 可以说明为:
virtual double area( )=0;
一个类可以说明多个纯虚函数, 包含有纯虚函数的类称为抽象类。
一个抽象类只能作为基类来派生新类, 不能说明抽象类的对象。
例如, 将Point类的area( )函数声明为纯虚函数,
则
Point a(1.5,6.7); //就是错误的。
但可以说明指向抽象类对象的指针( 或引用) ,
例如:
Point *pa;
从一个抽象类派生的类必须提供纯虚函数的实现代码,或在该派生类中仍将它说明为纯虚函数,否则编译器将给出错误信息。
说明了
纯虚函数的派生类仍是抽象类。
如果派生类中给出了基类所有纯虚函数的实现,则该派生类不再是抽象类。
抽象类的这一特点保证了进入类等级的每个类都具有(提供) 纯虚函数所要求的行为, 这保证了围绕这个类等级所建立起来的软件能正常运行, 避免了这个类等级的用户由于偶然的失误而影响系统正常运行。
抽象类至少含有一个虚函数, 而且至少有一个虚函数是纯虚函数,以便将它与空的虚函数区分开来。
下面是两种不同的表示方法:
virtual void area( )=0; //纯虚函数
virtual void area( ) { } //空的虚函数
在成员函数内可以调用纯虚函数, 但在构造函数或析构函数内调用一个纯虚函数将导致程序运行错误, 因为没有为纯虚函数定义代码。
编写一个程序, 用于计算正方形、 矩形、 直角三角形和圆的总面积。
class shape
{
public:
virtual double area( )=0; //纯虚函数
};
class square : public shape
{
protected:
double H;
public:
square(double i)
{
H=i;
}
double area( )
{
return H * H;
}
};
class circle : public square
{
public:
circle(double r) : square(r)
{
}
double area( )
{
return H * H * 3.14159;
}
};
class triangle : public square
{
protected:
double W;
public:
triangle(double h, double w):square(h)
{
W=w;
}
double area( )
{
return H * W * 0.5;
}
};
class rectangle : public triangle
{
public:
rectangle(double h, double w) : triangle( h, w )
{
}
double area( )
{
return H * W;
}
};
double total(shape *s[],int n)
{
double sum=0.0;
for(int i=0; i<n; i++)
sum+=s[i]->area();
return sum;
}
#include
using namespace std;
void main( )
{
shape *s[5];
s[0]=new square(4);
s[1]=new triangle(3,6);
s[2]=new rectangle(3,6);
s[3]=new square(6);
s[4]=new circle(10);
for(int i=0; i<5;i++)
cout<<"s["<<i<<"]="<<s[i]->area()<<endl;
double sum=total(s,5);
cout<<"The total area is:"<<sum<<endl;
}
程序输出结果如下:
s[0]=16
s[1]=9
s[2]=18
s[3]=36
s[4]=314.159
The total area is:393.159
shape类中的虚函数area仅起到为派生类提供一个一致的接口的作用, 派生类中重定义的area用于决定以什么样的方式计算面积。
由于在shape类中不能对此做出决定, 因此被说明为纯虚函数。
由此可见,
赋值兼容规则使人们可将正方形、 三角形和圆等都视为形状, 多态性又保证了函数total在对各种形状求面积之和时,无须关心当前正在计算哪种具体形状的面积。
在需要时, 函数total可从这些形状的对象那里获得该对象的面积, 成员函数area保证了这点。
这种情况在MFC中尤为重要, MFC为用户提供虚函数框架, 用户只要按自己的需要实现虚函数即可。
例如MFC的CWinApp的虚函数InitInstance, 它的功能是为每一个例程都做一次初始化, 所以用户从CWinApp派生CMyApp类时, 只需要改写InitInstance虚函数, 并在其中把窗口产生出来即可。
多重继承可以被视为多个单一继承的组合,
因此,
分析多重继承情况下的虚函数调用与分析单一继承有相似之处。
在C++中, 如果在多条继承路径上有一个汇合处, 则称这个汇合处的基类为公共基类。
显然,
可以通过不同的访问路径访问这个基类, 从而使这个公共的基类会产生多个实例, 引起二义性。
如果想使这个公共的基类只产生一个实例, 则可以将这个基类说明为虚基类。
这要求在从这个公共基类派生新类时, 使用关键字virtual将公共基类说明为虚基类。
一般的声明形式如下:
class 派生类名:virtual 访问控制 基类名
一个派生类可以公有或私有地继承一个或多个虚基类, 关键字virtual和关键字public或private的相对位置无关紧要, 但要放在基类名之前, 并且关键字virtual只对紧随其后的基类名起作用。
例如:
class D : virtual public A, private B, virtual public C
{
……// 类体
};
派生类D从虚基类A和C以及非虚基类B派生。
关键字virtual的位置可以在“访问控制” 之后,但必须在基类名之前,
图1.5是虚基类的UML结构示意图。
base1类和base2类在从base类派生时, 使用关键字virtual指明将base类作为它们的虚基类。
这样,
在derived类中, base类就只有一个实例。
当derived类的对象或成员函数在使用虚基类base类中的成员时, 就不会再产生二义性问题。
例如:
derived d;
int i = d.b; //正确
class base{
public:
int b;
base1(int i, int j):base(i),b1(j)
{
cout<<"base1="<<b1<<",base="<<b<<endl;
}
};
class base1 : virtual public base
{
public:
int b1;
base1(int i, int j):base(i),b1(j)
{
cout<<"base1="<<b1<<",base="<<b<<endl;
}
};
class base2 : virtual public base
{
public:
base2(int i, int j):base(i),b2(j)
{
cout<<"base2="<<b2<<",base="<<b<<endl;
}
};
class derived : public base1, public base2
{
float d1;
public:
derived(int a, int b, int c, float e):base1(a,b),base2(b,a),base(c),d1(e)
{
}
void display()
{
cout<<"derived="<<d1<<endl;
cout<<"base="<<b<<endl;
cout<<"base1="<<b1<<endl;
cout<<"base2="<<b2<<endl;
}
};
void main( )
{
derived d(1,2,3,5.5);
d.display();
cout<<d.b<<endl;
cout<<d.base1::b<<endl;
cout<<d.base2::b<<endl;
}
程序执行结果如下:
base=3
base1=2,base=3
base2=1,base=3
derived=5.5
base=3
base1=2
base2=1
3
3
3
由于base类在derived类中只有一个实例, 所以可从任何一条继承路径访问该虚基类base类的成员, 并且都使用相同的base类的实例,
例如,
d.base1 :: b和d.base2 :: b 使用的是同一个虚基类base类中的数据成员b, 因而具有相同的值。
派生类derived的构造函数初始化列表中, 必须单独调用虚基类的构造函数。
例如:
derived(int a, int b, int c, float e):base1(a,b),base2(b,a),base(c),d1(e)
{
}
当初始化列表中同时出现对虚基类和非虚基类构造函数的调用时,虚基类的构造函数先于非基类的构造函数执行, 并且只被调用一次,从而保证只建立一个虚基类对象, 消除了二义性。
对于上面的构造函数derived, 优先执行base©, 当再执行base1(a,b)时, base1不再调用base的构造函数对虚基类初始化。
同理,
base2(b,a)也不调用base的构造函数。
从执行结果中可知看出, 无论从那条路经访问base的数据成员b, 其效果都是一样的。
一个派生类的对象的地址可以直接赋给虚基类的指针,
例如:
base *bptr = &d;
这时不需要进行强制类型转换。 一个虚基类可以引用一个派生类的对象,
例如:
base& ref = d;
反过来则是错误的, 无论在强制类型转换中指定怎样的路径, 一个虚基类的指针或引用不能转换为派生类的指针或引用。
例如, 语句
derived *dptr = (derived *)(base1*)bptr;
将产生编译错误。
虚基类的定义很难处理, 这就是为什么最初的C++语言没有能支持多重继承的原因。
虚基类在派生类中的特殊布局使得不能将指向虚基类的指针重置回指向派生类(引用也一样) 。
支配规则同样可用于分析在有虚基类的情况下的二义性问题。
一个类可以从多个类派生, 当需要从多个概念组合时, 使用多重继承。
虚基类为多条继承路径提供了一个汇合点, 使它们可以共享信息。
在多数情况下, 使用多重继承必须要用到虚基类, 但虚基类的定义很复杂,
因此,
在程序中使用多重继承应慎重。
但MFC在许多地方均使用多种继承和虚基类,以便提高程序设计的灵活性。
使用虚基类的类体系设计实例。
这个实例可以管理大学的学生(student) 、 教员(faculty) 和教授( professor) 3类人员。
它所解决的问题是一个大家比较熟悉的真实世界中的问题, 可以很容易地识别出对象, 建立对象的类描述,图1.6是为这个实例建立的类等级。
person类是所建立的类体系的公共的根, 它封装这个问题域中各类人员的共性。
为简化程序, 只选择在person类中描述人名和年龄。
student类是对一类具体对象的描述, 在这里只关心一个学生的专业方向。
faculty类描述教员, 关心这类人员在哪个系任教。
professor类描述教授, 教授是一个特殊的教员, 不但关心他所任教的系, 同时也关心他是一级教授还是二级教授, 所以从faculty类派生professor类, 在professor类增加faculty类所不具有的特征。
在职学习的教员既是一个教员, 又具有学生的属性, 所以,studentFaculty类应从student类和faculty类派生。
在这个类等级中, studentFaculty类是合并了两个类的概念建立起来的, 而person类则是studentFaculty类的两条继承路径的公共汇合点。
因为studentFaculty类的对象只能有一个名字和一个年龄, 所以person类应说明为虚基类。
因为这个类体系使用了虚基类和string类, 所以简化了类体系的定义。
person类定义了一个默认构造函数, 这个构造函数不会被调用, 它仅是为了简化从虚基类派生的类的构造函数的定义。
student类和faculty类的保护构造函数并不提供给它们的派生类使用, person类的默认构造函数正好为它提供了这方面的便利。
保护的构造函数除能被派生类调用外, 在建立该类的对象时不能被调用, 以防止该类的对象被不正确地初始化。
为防止在建立student类和faculty类时只提供一个string类型的初始值导致对象不能被正确初始化, 将这两个类中仅供派生类使用的构造函数说明为保护的。
虚基类概念和定义比较复杂, 所以当设计一个带有虚基类的类体系, 并将这个类体系交给其他程序员使用时, 必须意识到这个程序员需要对虚基类的类定义有所了解并要小心使用这个类体系,
因此要认真规划所设计的类体系。
//univ.h为类的声明, univ.cpp是类的定义, 在声明时使用内联函数定义构造函数。
//univ.h
#if ! defined(UNIV_H)
#defined UNIV_H
#include
#include
using namespace std;
class person
{
protected:
string name;
int age;
person( )
{
name=""; age=0;
}
person(string n, int a) : name(n), age(a)
{
}
};
class student : virtual public person
{
private:
string major;
protected:
student(string m) : major(m)
{
}
public:
student(string n, int a, string m) : person(n,a), major(m)
{
}
void print( );
};
class faculty : virtual public person
{
protected:
string dept;
faculty(string d) : dept(d)
{
}
public:
faculty(string n, int a, string d) : person(n,a), dept(d)
{
}
void print( );
};
class professor : public faculty
{
private:
int level;
public:
professor(string n, int a, string dept, int h): person(n,a), faculty(dept), level(h)
{
}
void print( );
};
class studentFaculty : public student, public faculty
{
public:
studentFaculty(string n, int a, string m, string d): person(n,a), student(m), faculty(d)
{
}
void print( );
};
//univ.cpp
#endif
#include "univ.h"
void student :: print( )
{
cout << name <<endl<< "Age: " << age<< "\tMajor: " << major << endl;
}
void faculty::print( )
{
cout << name <<endl<<"Age: " << age<< "\tDepartment: " << dept << endl;
}
void professor :: print( )
{
faculty :: print( );cout << "Level: " << level <<endl;
}
void studentFaculty :: print( )
{
student :: print( );cout << "Department: " << dept <<endl;
}
下面给出使用这个类体系的测试程序:
//main.ccp
#include "univ.h"
void main( )
{
student myStudent("Zhang Hong", 25, "Computer");
faculty myFaculty("Wang Yong", 35, "Engineering");
professor myProfessor("Li yu he", 52, "Management", 2);
studentFaculty myStudentFaculty("Zhao xiao ming",22, "English", "Robot");
myStudentFaculty.print( );
myStudent.print( );
myProfessor.print( );
myFaculty.print( );
}
其运行结果:
Zhao xiao ming
Age: 22 Major: English
Department: Robot
Zhang Hong
Age: 25 Major: Computer
Li yu he
Age: 52 Department: Management
Level: 2
Wang Yong
Age: 35 Department: Engineering
MFC利用函数指针和指向类成员指针来实现某些操作, 有时让初学者感到困惑。 例如窗口回调函数等。
本节将回顾一下函数指针和类成员指针的知识, 以方便后续章节的学习。
函数在内存中有确定的物理地址, 该地址能够赋给指针。
这是因为函数在编译时, 它的源代码转换成了目标代码, 同时确定了函数的入口地址。
程序调用函数, 也就是指向了这个入口地址。
因而,
指向函数的指针实际上包含了函数的入口地址, 所以赋给指针的地址就是函数的入口地址, 从而该指针就用来代替函数名。
这就使得函数可以作为实参传递给其他函数, 从而可将一个指向函数的指针传递给函数, 或放置在数组中提供给其他对象使用。
函数指针定义形式如下:
数据类型标识符 (*指针对象名)(函数参数的数据类型列表);
例如语句“int (*p)(int,int );”仅仅说明定义的p是一个指向函数的指针, 此函数返回整型值。
p并不是固定指向哪一个函数的, 而只是表示定义了这样一个类型的对象, 专门用来存放函数的入口地
址。
在程序中把哪一个函数的地址赋给它, 它就指向哪一个函数。
在一个程序中, 一个函数指针对象可以先后指向不同的函数。
从这一点上看, 它跟过去介绍的指针对象具有相同的性质。
声明函数指针时, 只需要给出函数参数的数据类型, 不需要给出参数名。
如果给出也可以, 只不过编译系统不理睬参数名而已,
所以
下面两种形式是等效的:
int( *p )(int ,int ); //只给参数类型
int( *p )(int a,int b ); //给出参数类型和参数名
也可以使用typedef定义, 例如:
typedef int (*FUN) (int a, int b);
FUN p;
则对象p为一个指向原型为 int (int, int) 的函数的指针。
给函数指针对象赋值时, 只需要给出函数名而不必给出参数。
因为
语句p=函数名;
是将函数入口地址赋给指针对象p, 而不涉及到实参与形参的结合问题。
数组名代表数组起始地址, 这里的函数名则代表函数的入口地址。
因为在C++中, 单独一个函数名(其后不跟一对圆括号) 被自动地转换为指向该函数的指针( 函数的第1条指令的地址) 。
当函数地址被置入一个指针对象中时, 可以通过该指针对象调用该函数。
这里的p就是指向该函数的指针对象, 它和函数名都指向函数的开头, 调用p就是调用这个函数。 但它只能指向函数的入口处, 而不能指向函数中间的具体指令。
因此,
*(p+1)、 p+n、 p–及p++等运算对它都是没有意义的。
调用指向函数的格式如下:
(*指针对象名)(函数实参表);
函数指针对象调用函数时, 只需将(*指针对象名)代替函数名即可, 在(*指针对象名)之后的括号中根据需要写上实参。
例如语句(*p)(a,b)将调用由p指向的函数, 把a 和 b作为实参传递给函数。
圆括号内也可不含参数, 但圆括号必须存在, 下列语句以相似的方式调用无参数的函数:
(*p) ( );
p也需要使用圆括号括起来, 以强制运算“ *” 在被调用前使用。
若不加括号, 则
int *p( );
就变成了一个返回整数指针的函数声明。
可以用普通方式使用间接调用的函数返回结果。
例如,
下列语句将把函数调用返回的结果赋值到i对象。
i = (*p) (a,b);
必须声明函数的原型。
现在是用函数名作右值, 后面没有括号和参数, 编译系统无法判断它们究竟是对象名还是函数名, 故要对它们加以声明。
即:
数据类型 函数名(参数的数据类型列表);
本例的声明如下:
int min(int, int);
输出多项式x2+5x+8和x3-6x在区间[-1, +1] 内, 增长步长为0.1时的所有结果。
#include
using namespace std;
double const STEP=0.1;
double f1(double ); //函数f1的原型声明
double f2(double ); //函数f2的原型声明
void main( )
{
double x, (*p)(double); //声明函数指针对象pfor ( int i=0; i<2; i++)
for ( int i=0; i<2; i++)
{
if (i==0) p = f1; //i为0时p指向函数f1
else p = f2; //i为1时p指向函数f2
for( x = -1; x <= 1; x += STEP) //对指定函数完成计算
cout<<x<<"\t"<<(*p)(x)<<endl;
}
}
double f1(double x) //函数f1的定义
{
return ( x*x + 5*x +8);
}
double f2(double x) //函数f2的定义
{
return( x*x*x-6*x );
}
上面的程序使用一个函数指针 p , 完成对f1和f2两个函数的调用,这在那些有规律的多函数调用系统中, 可以大大增强程序的处理能力。
也可以用指向函数的函数指针对象作为参数, 从而实现函数地址的传递(也就是将函数名作为实参) , 达到将函数作为参数传给其他函数的目的。
下面用一个实例来说明具体的含义。 假设已经有分别求两个数的大者、 小者及平均值的3个函数max, min和mean。
现在另外定义一个函数 all如下:
int all(int x, int y, int (*func)(int,int))
{
return (*func)(x,y);
}
函数all共有3个形参, 有2个int形参, 1个为指向函数的指针对象func。
这个对象声明为 int (*func)(int,int ), 可用一个定义的函数代替all中的func, 例如all(a,b,mean)相当于执行mean(a,b), 从而输出a和b的平均值。
同理,
可用相同方法调用min及max, 而all函数的形式一样, 只是在调用时改变实参函数名而已。
这就增加了函数使用的灵活性, 它在大型程序设计, 尤其是模块设计时特别有用。
完整的示例程序。
#include
using namespace std;
int all (int, int, int (*)(int,int)); //含有函数指针的函数原型声明
int max(int,int ),min(int,int ),mean(int,int ); //函数原型声明
void main( )
{
int a, b;
cin>>a>>b;
cout<<"max="<<all(a,b,max)<<endl;
cout<<"min="<<all(a, b, min)<<endl;
cout<<"mean="<<all(a,b,mean)<<endl;
}
int all(int x, int y, int (*func)(int,int))
{
return (*func)(x,y);
}
int max(int x, int y)
{
return (x>y)?x:y;
}
int min(int x, int y)
{
return (x<y)?x:y;
}
int mean(int x, int y)
{
return( (x+y)/2 );
}
输入58 62
输出 max=62
min=58
mean=60
求函数10x2-9x+2在区间 [ 0, 1] 内x以0.01的增量变化的最小值。
#include
using namespace std;
double const s1=0.0;
double const s2=1.0;
double const step=0.01;
double func(double);
double value(double(*)( double));
void main ( )
{
double (*p)(double);
p=func; //指向目标函数
cout<<"最小值是:"<<value(p)<<endl;
}
double func(double x) //目标函数
{
return (10*x*x-9*x+2);
}
double value(double(*f)(double)) //定义求最小值函数, 它包括函数指针
{
double x=s1, y=(*f)(x);
while( x <= s2 )
{
if( y > (*f)(x) )
y=(*f)(x);
x += step;
}
return y;
}
运行结果:
最小值是: - 0.025
函数指针的目标函数必须已经存在, 才能被引用。
本例中p函数指针的目标函数func已经在p被调用之前声明, 也已经通过语句
p=func;
指向它的目标函数func
对象是一个完整的实体。 为了支持这一封装,C++包含了指向类成员的指针。
可以用普通指针访问内存中给定类型的任何对象, 指向类成员的指针则用来访问某个特定类的对象中给定类型的任何成员。
C++既包含指向类数据成员的指针, 又包含指向成员函数的指针。
C++提供一种特殊的指针类型, 指针指向类的成员, 而不是指向该类的一个对象中该成员的一个实例, 这种指针称为指向类成员的指针。
类并不是对象, 但有时可将其视为对象来使用。 可以声明并使用指向数据成员的指针或指向对象数据成员的指针。
指向对象的指针是比较传统的指针, 指向类X中类型为type的数据成员的指针的声明形式为:
type X :: * pointer;
若类X的数据成员member的类型为type, 则语句
pointer = &X :: member;
将该成员的地址存入pointer中。 注意, 取一个类成员的地址使用表达式&X :: member, 这样得到的地址不是真实地址, 而是成员member在类X的所有对象中的偏移。
因此,
若要访问某个对象中pointer所指向的成员, 使用特殊的运算符“ .”和“ ->”。
指向类数据成员的指针可访问该类的任何对象的公有数据成员。
考虑下面的类:
class A
{
public:
int a,b,c;
};
下面的声明表明p是指向类A的整型数据成员的指针:
int A :: *p; // 指向类A的整型数据成员(a,b,c)的指针
虽然“ ::”是作用域分辨符, 但在这种情况下, “A ::”最好读成“ A的成员” 。 这时, 从内往外看, 此声明可读作: p是一个指针, 指向类A的数据成员, 这些数据是整数。
p可以指向仅有的3个数据成员a, b, c中的一个, 即类A的惟一一组整型数据成员。
不过,
p一次只能指向一个数据成员。
从这一点看, 有点“共同体” 类型的味道。
p可以使用以下的赋值指向A的3个合适的数据成员中的任意一个。
p = &A :: 数据成员名;这时还没有建立任何类A的对象, 这是指向类数据成员指针的核心意义。
p将访问任何对象的a, b, c成员变量。
为了实际使用指向成员的指针, 需要使用“ .”和“ ->”运算符, 这是新出现在C++中的运算符。
在下面例题的主程序中, 还定义一个对象x的指针, 以便观察它们的作用的异同。
使用指向类数据成员的指针和类对象指针的比较。
#include
using namespace std;
void main()
{
int A :: *p; // 指向类A
p = &A :: b; //p指向类A的数据成员b
A x; //类A的对象x
A *px = &x; //px是指向对象x的指针
x.*p = 1; //对象x通过运算符“ .*”使用指针p置x.b等于1
p = &A :: c; //p改为指向类A的数据成员c
x.*p=9; //置x.c=9
px ->*p = 2; //对象的指针px使用指针p重置x.c=2
p = &A :: a; //p改为指向类A的数据成员a
x.*p = 8; // 置x.a=8
cout<<x.*p<<" "<<px->b<<" "<<px->c<<endl;
}
运算符“ .”将左值与右值连接起来。 左值必须是类的对象, 右值是该类的一个特定数据成员。 运算符“ ->” 有相似的作用。
它的左值必须是一个指向类对象的指针, 它的右值是该类的一个特定数据成员。
在语句“x.*p = 1;”中, x是类A的对象, p是指向类A的数据成员b, 代表将1赋给b。 语句“px ->*p = 2;”的px是指向A类的对象x的指针, 这时的p已经改为指向类A的数据成员c, 本语句的作用是将2赋给A的数据成员c。
指针p指示的是当前数据成员, 很容易分析出程序的输出为: 8 1 2。
由此可见,
在使用指向类数据成员的指针访问对象的某个数据成员时, 必须指定一个对象。
如果该对象由对象名或引用标识, 则使用运算符“ .* ”;如果是使用指向对象的指针来标识的, 则使用运算符“ ->* ”。
对数据成员的限制是它们必须是公有的。
指向类的静态数据成员的指针的定义和使用, 与一般指针的定义和使用方法一样。
使用指向类的静态数据成员指针。
class A {
public:
A(){}
static int num;
};
int A::num;
void main( )
{
int *p = &A :: num;
*p=56;
cout<<A::num<<endl; //输出56
A a, b;
cout<<a.num<<endl; //输出56
cout<<b.num<<endl; //输出56
}
由于静态数据成员不属于任何对象, 所以访问指向静态数据成员的指针时不需要对象。 由于静态数据成员是在类中说明的, 所以取它的地址时需要进行成员名限定。
指向类成员函数的指针与指向类数据成员的指针工作原理相似,用途也相似。
主要的区别在于语法更复杂。
假设类A的成员函数为“void fa(void);”, 如要建立一个指针pafn, 它可以指向任何无参数和无返回值的类A的成员函数:
void( A ::* pafn )( void );
下面的例子说明了pafn如何被赋值并用以调用函数fa:
pafn = A :: fa; // 指向类A的成员函数fa的指针pafn
A x; // 类A的对象x
A *px = &x ; // 指向类A对象x的指针px
(x.*pafn)( ); // 调用类A的对象x的成员函数fa
(px ->* pafn)( ); // 调用类A的对象x的指针px指向的成员函数fa
指向类X中参数类型列表为list, 返回类型为type的成员函数的指针的声明形式为:
type( X ::* pointer)( list );
如果类X的成员函数fun的原型与pointer所指向的函数的原型一样,则语句
pointer = X :: fun;
将该函数的地址( 即它在该类所有对象中偏移) 置给了指针pointer。 与指向类数据成员的指针类似,使用对象名或引用调用pointer所指向的函数时, 使用运算符“ .* ”, 使用指向对象的指针调用pointer所指向的成员函数时, 使用运算符“ ->*”。
使用指向类成员函数的指针
#include
using namespace std;
class A
{
private:
int val;
public:
A( int i ) { val = i; }
int value( int a ) { return val + a; }
};
void main( )
{
int( A ::*pfun )( int ); //声明指向类A的成员函数的指针
pfun = A :: value; //指针指向具体成员函数value
A obj(10); //创建对象obj
cout << (obj.*pfun)(15) << endl; //对象使用类的函数
//指针, 输出25A *pc = &obj; //对象A的指针pc
cout << (pc->*pfun)(15) << endl; // 对象指针使用类
//的函数指针, 输出25
}
注意: (obj.* pfun)或(pc->* pfun)必须使用括号括起来。在派生类中, 当一个指向基类成员函数的指针指向一个虚函数,并且通过指向对象的基类指针(或引用) 访问这个虚函数时, 仍发生多态性。
使用基类成员函数的指针产生多态型。
#include
using namespace std;
class base
{
public:
virtual void print( )//虚函数
{
cout << "In Base" << endl;
}
};
class derived : public base
{
public:
void print( ) //虚函数
{
cout << "In Derived" << endl;
}
};
void display( base *pb, void( base ::*pf )( ) )
{
( pb->*pf)( );
}
void main( )
{
base b;
derived d;
display( &b, base :: print ); //输出In Base
display( &d, base :: print ); //输出In Deri
}
如果类的数据成员或成员函数使用关键字static进行修饰, 这样的成员称为静态数据成员或静态成员函
数, 统称为静态成员。 静态成员在MFC中扮演着重要角色。
分析下面程序的输出结果
class Test
{
static int x; // 静态数据成员
int n;
public:
Test()
{
}
Test(int a, int b)
{
x=a; n=b;
}
static int func()// 静态成员函数
{
return x;
}
static void sfunc(Test&r,int a)//静态成员函数
{
r.n=a;
}
int Getn()// 非静态成员函数
{
return n;
}
};
Int Test::x=25; //初始化静态数据成员
#include
using namespace std;
void main()
{
cout<<Test::func();//x在产生对象之前即
//存在, 输出25
Test b, c;
b.sfunc(b,58); //设置对象b的数据成员n
cout<<" "<<b.Getn();
cout<<" "<<b.func();//x属于所有对象, 输出25
cout<<" "<<c.func();//x属于所有对象, 输出25
Test a(24,56); //将类的x值改为24
cout <<" "<<a.func()<<" "<< b.func()<<" "<<b.func()<<endl;
}
静态数据成员只能说明一次, 如果在类中仅对静态数据成员进行声明, 则必须在文件作用域的某个地方进行定义。
在进行初始化时, 必须进行成员名限定。
例如:
Test(int a, int b)
{
Test::x=a; n=b;
}
除静态数据成员的初始化之外, 静态成员遵循类的其他成员所遵循的访问限制, 虽然还没有建立对象, 但静态成员已经存在。
由于数据隐藏的需要, 静态数据成员通常被说明为私有的, 而通过定义公有的静态成员函数来访问静态数据成员。
注意:由于static不是函数类型中的一部分, 所以在类声明之外定义静态成员函数时, 不使用static。
在类中定义的静态成员函数是内联的。
一般来说, 通过成员名限定访问静态成员, 比使用对象名访问静态成员要好, 因为静态成员不是对象的成员。
静态成员可以被继承, 这时, 基类对象和派生类的对象共享该静态成员,
除此之外,
在类等级中对静态成员的其他特性(例如, 静态成员在派生类中的访问权限、 在派生类中重载成员函数等) 的分析与一般成员类似。
类中的任何成员函数都可以访问静态成员, 但静态成员函数只能通过对象名(或指向对象的指针) 访问该对象的非静态成员, 因为静态成员函数没有this指针, 例如sfunc()的定义。构造对象a之后, 改变类的x值, 也即所有对象的x值都变成此值。
输出结果如下:
25 58 25 25 24 24 24
由此可见,
静态成员与一般成员有下列不同之处:
① 可以不指向某个具体的对象, 只与类名连用。
② 在没有建立对象之前, 静态成员就已经存在。
③ 静态成员是类的成员, 不是对象的成员。
④ 静态成员为该类的所有对象共享, 它们被存储于一个公用内存中。
⑤ 没有this指针, 所以除非显式地把指针传给它们, 否则不能存取类的数据成员。
⑥ 静态成员函数不能被说明为虚函数。
⑦ 静态成员函数不能直接访问非静态函数。
使用静态类对象的例子。
不要混淆类的静态成员与静态类对象。
静态类对象是使用关键字static声明的类的对象, 所以静态类对象实质上就是静态类变量,但要注意它的构造函数与析构函数的调用特点。
本例说明了静态类对象的特殊性。
#include
using namespace std;
class test
{
private:
int n;
public:
test(int i)
{
n=i;cout<<"constructor:"<<i<<endl;
}
~test()
{
cout<<"destructor:"<<n<<endl;
}
int getn()
{
return n;
}
void inc()
{
++n;
}
};
void main( )
{
cout <<"loop start:" << endl;
for(int i=0;i<3;i++)
{
static test a(3);
test b(3);
a.inc();
b.inc();
cout<<"a.n="<<a.getn()<<endl;
cout<<"b.n="<<b.getn()<<endl;
}
cout<<"loop end."<<endl;
cout<<"Exit main()"<<endl;
}
程序输出结果如下:
loop start:
constructor:3
constructor:3
a.n=4
b.n=4
destructor:4
constructor:3
a.n=5
b.n=4
destructor:4
constructor:3
a.n=6
b.n=4
destructor:4
loop end.
Exit main()
destructor:6
程序中创建两个类对象:静态类对象a和普通类对象b。
由程序输出可见,
对静态类对象a而言, 第一次执行它的定义时, 调用构造函数使得a.n=3, 而a.inc()使得a.n=4, 输出其值4之后, for循环进入下一轮循环。
在以后的循环中, 再没有调用构造函数;
直到所有程序执行结束, 它才在退出时调用析构函数。
由此可见,
它具有如下性质:
① 构造函数在代码执行过程中, 第一次遇到它的变量定义时被调用,但直到整个程序结束之前仅调用一次。
② 析构函数在整个程序退出之前被调用, 同样也只调用一次。对普通类对象b而言, 因为它是for循环语句中的局部类对象, 所以生命期只能与本次循环共存, 每当循环体的本次循环结束时,它都要调用一次析构函数。 通过比较它们的输出结果, 很容易看出它们之间的区别。
可以使用静态成员变量存储所有对象的共有信息。 因为无论是运行时类信息( 包括基类和派生类名等) , 还是类的映射表( 如CWnd的派生类) , 都是为所有对象共有的, 所以提供运行时类信息的宏定义、 消息映射表的宏定义等, 都可以通过在类中使用静态数据成员变量来实现。 其实, 一般使用static const类型。 例如宏定义DECLARE_MESSAGE_MAP()中, 就使用如下定义来存储消息映射表:
ststic const AFX_MSGMAP_ENTRY_messageEntries[];
类的静态成员函数往往是执行与该类相关的功能操作, 但是又不涉及具体的类对象。 例如MFC中的CFile类, 就将一些系统范围内文件操作的功能函数(文件更名、 删除文件、 区的文件信息等) 定义
为静态成员函数。
由句柄(窗口句柄HWND、 设备句柄HDC、 菜单句柄HMENU) 取得或建立(包括建立临时的)封装类对象(CWnd、 CDC、 CMenu等)的功能函数, 都被定义为相应封装类的静态成员。
例如CWnd类中如下成员函数的定义:
static CWnd* PASCAL FormHandle(HWND hWnd);
CWnd类还将一些系统范围内窗口操作的功能函数(取得前台窗口、 取得当前焦点窗口等) 定义为静态成员函数。
下面是CDC类使用的定义:
static CDC* PASCAL FormHandle(HDC hDC);
static void PASCAL DeleteTempMap( );
类与类之间的关系有两大类。 一是继承和派生问题, 二是一个类使用另一个类的问题。
后者的简单用途是把另一个类的对象作为自己的数据成员或者成员函数的参数。
前者称为分类, 运用分类原则也意味着通过不同程度的抽象而形成基类-派生类结构(又称分类结构), 基类比派生类的抽象程度更高。
运用分类原则可以集中地描述对象的共性, 清晰地表示对象与类的关系( 即“ isa”关系) 以及派生类与基类的关系( 即“is-a-kind-of”关系) , 从而使系统的复杂性得到控制。
后者称为聚合( aggregation) , 又称为组装( composition) ,C++有时又称为包含。 它的原则是把一个复杂的事物看成若干比较简单的事物的组装体, 从而简化对复杂事物的描述。
在面向对象分析(OOA)中运用聚合原则就是要区分事物的整体和它的组成部分, 分别用整体对象和部分对象来进行描述, 形成一个整体-部分结构, 以清晰地表示它们之间的组成关系( 称为“ has-a”关系,或反过来称为“is-a-part-of”关系) 。
例如汽车的一个部件是发动机, 在OOA中可以把汽车作为整体对象, 把发动机作为部分对象,通过整体-部分结构表达它们之间的组成关系(汽车带有一个发动机, 或者说发动机是汽车的一部分) 。
在有些文献中, 对“聚合” 和“包含” 这两个术语的解释和用法略有差异。
前者用于比较松散和灵活的整体-部分结构, 后者用于紧密、 固定的整体-部分结构。
在类中也可以嵌入一个类, 把这个嵌入的类称为内嵌类, 包含这个类的类称为包容类。
内嵌类具有自己的成员函数, 并被封装在包容类中, 不仅外部不能对它直接访问, 而且也不受包容类的控制(即两者的访问控制是完全独立的) 。
内嵌类就像一个工具,用户可以通过内嵌类实现对包容类数据的处理。
包容类可以根据不同的处理对象, 封装不同的数据集, 而且代码变动不会影响原来嵌入的类定义。
同理,
内嵌类的定义可以根据用户的需求进行修改、 增加或删除, 这对包容类也没有什么影响。
由此可见,
可以用内嵌类封装一个数据处理的用户接口部分, 使数据处理与数据的表示相分离。
因为内嵌类不能直接访问包容类的非公有成员, 包容类对内嵌类也不存在任何访问特权。
如果需要增进内嵌类和包容类两者之间的关系, 可以将一方定义为另一方的友元类。
使用内嵌类和友元类的例子。
#include
using namespace std;
class Contain
{
protected:
unsigned char m_link;
public:
//定义一个内嵌类作为公有成员
class Embed
{
private:
int m_flag;
public:
Embed()
{
}
Embed(int flag)
{
m_flag=flag;
}
int GetValue()const
{
return m_flag;
}
Embed& operator=(const Embed& bed); //声明,留待外部定义
void ShowLink(Contain* pCon);
};
Embed m_bed ; //同时定义一个该类型的公有成员变量
//将内嵌类定义为父类的友元
friend class Embed;
public:
Contain()
{
}
Contain(int flag,unsigned char cLink):m_bed(flag)
{
m_link=cLink;
}
//在类中可以随意应用自定义的类型成员
Embed GetEmbedMember()const
{
return m_bed;
}
};
//在外部定义内嵌类的成员函数, 要加双标识符限定范围
Contain::Embed& Contain::Embed::operator=(constEmbed& bed)
{
m_flag=bed.m_flag;
return *this;
}
void Contain::Embed::ShowLink(Contain* pCon)
{
if(m_flag)
cout<<(int)pCon->m_link<<endl;
else
cout<<pCon->m_link<<endl;
}
void main( )
{
Contain::Embed bed,bed1,bed2(56);
Contain con(0,'A');con.m_bed.ShowLink(&con);
bed=con.m_bed;
cout<<bed.GetValue()<<endl;
cout<<bed2.GetValue()<<endl;
bed2=bed;
cout<<bed2.GetValue()<<endl;
Contain con1(73,'A'), con2;
con1.m_bed.ShowLink(&con);
bed=con1.m_bed;
cout<<bed.GetValue()<<endl;
bed1=con1.GetEmbedMember();
cout<<bed1.GetValue()<<endl;
bed2=con1.m_bed;
cout<<bed2.GetValue()<<endl;
}
程序运行结果如下:
A
0
56
0
65
73
73
73
由此可见, 如果一个类包含一个内嵌类, 则要保涵该内嵌类的对象作为成员变量对象, 否则内嵌类就失去作用, 变得毫无意义。
从这一点看来, 又与聚合类似。