1.1重载
重载分为两个大类:函数重载和运算符重载。
1.1.1函数重载和默许参数
C++答应为同一个函数界说几个版别,称为函数重载。 函数重载使一个函数名具有多种功用,即具有“多种形状”,又称这种形状为多态性。
函数重载发生多态性的比如:
//运用函数重载的多态性,规划一个求最大值的函数
#include <iostream>
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 <iostream>
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函数重载。
- ==仅有函数回来值不同也差异不了重载函数==。
默许参数规划类的结构函数 特别留意:
-
一个类能够有多个结构函数, 这也是典型的函数重载。
-
能够运用域界说符“:: ” 显式地指出调用的是基类仍是派生类的重载函数。
1.1.2重载与姓名分配规矩的差异
假如基类和派生类的成员函数具有相同的参数表, 则不归于函数重载。 这时按姓名分配规矩调用, 并且==运用域界说符“:: ” 避免二义性==。 下面是在基类和派生类中运用参数相同的同名函数的比如。
演示在基类和派生类中运用参数相同的同名函数的比如:
#include <iostream>
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;
}
假如同名函数的参数类型不同或许参数个数不同, 则归于重载。 这时, 也能够运用域界说符显式地指明被调用的函数。
1.1.3运算符重载
由于任何运算都是经过函数来完结的, 所以运算符重载其实便是函数重载, 要重载某个运算符, 只需重载相应的函数就能够了。
与重载函数不同的是:
需求运用新的要害字“operator”,它常常和C++的一个运算符连用, 构成一个运算符函数名。
例如“operator +”。
这种构成办法就能够像重载一般函数那样, 重载运算符函数operator+( )。
由于C++现已为各种根本数据类型界说了该运算符函数, 所以只需求为自己界说的类型重载operator + ( )就能够了。
一般地, 为用户界说的类型重载运算符, 都要求能够拜访这个类型的私有成员。 所以只需两条路可走:
- 重载为这个类型的成员函数
- 重载为这个类型的友元。
为差异这两种状况, 将作为类的成员函数称为==类运算符==, 而将作为类的友元的运算符称为==友元运算符==。
C++的运算符大部分都能够重载, ==不能重载==有“. ”、 “::”、 “*” 和“? :”。
前面3个是由于在C++中有特定的含义, 禁绝重载能够避免不必要的麻烦;“ ? :”则是由于不值得重载。 别的, “ sizeof”和“ #”不是运算符, 因此不能重载。
= 、 ()、 [] 、 ->这4个运算符只能用==类运算符==来重载。
C++对用户界说的运算符重载的束缚:
- 重载后的运算符有必要至少有一个操作数是用户界说的类型
- 运用运算符时不能违反运算符本来的句法规矩,不能修正运算符的优先级。
- 不能创建新运算符
- 不能重载的运算符“. ”、 “::”、 “*” 和“? :”。
- = 、 ()、 [] 、 ->这4个运算符只能用==类运算符==来重载。
运用==友元函数==重载运算符“<<”和“>>”:
#include <iostream.h>
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 <iostream>
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);
由此可见, 只需界说正确, 不必再运用函数调用办法,而直接运用运算符
1.1.4友元运算符、类运算符及其参数
假如运算符所需的操作数, 尤其是第一个操作数期望进行隐式类型转化, 则该运算符应该经过友元来重载。 另一方面, 假如一个运算符的操作需求修正类方针的状况, 则应当运用类运算符, 这样更符合数据封装的要求。
运用方针作为友元函数参数来界说运算符“+”的比如:
#include <iostream.h>
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类中界说了很多“>>”和“<<”运算符的重载版别。
1.2模板
模板有函数模板和类模板两种。
1.2.1函数模板及其显式调用规矩
在程序规划时并不给出相应数据的实践类型, 而在实践编译时, 由编译器运用实践的类型给予实例化, 使它满意需求。 由此可见, 可使编译器成为一种在函数模板引导下, 制造符合要求的代码的程序规划辅助东西。
函数模板声明的一般办法如下:
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。
1.2.2模板函数专门化和模板重载
1.2.2.1模板函数专门化
尽管依照默许约定, 界说一个模板, 用户能够运用能想到的任何模板参数(或许模板参数组合), 但用些用户却宁肯挑选别的的完结办法。
例如界说的模板函数:
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")<<",“<<max('W','T')<<" ";
时, 第一个字符串参数是调用一般函数, 第二个单字符参数则调用模板函数。
不过,
为了构成完好的模板系, 便于管理, 并确保在无调用时不会生成任何无用代码, 期望仍运用模板办法。
这能够经过供给多个不同的界说办法来处理, 并由编译器依据在运用处供给的的模板参数, 在这些界说中做出挑选。
对一个模板的这些能够互相替换的界说称为用户界说的专门化,或简称为==用户专门化==。
前缀“template <>”阐明这是一个专门化, 在描绘时不必模板参数。 能够写成:
template <>char *max<char*>(char *a, char *b)
{
return (strcmp(a,b)>=0?a:b);
}
在函数名之后的<char*>阐明这个专门化应该在模板参数是char*的状况下运用。 由于模板参数能够从函数的实践参数列表中揣度, 所以不需求显式地描绘它。即能够简化为:
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);
}
1.2.2.2模板重载
C++模板的机制也是重载。
模板供给了看起来很像多态性的语法, 当供给细节时, 模板就能够生成模板函数。
由于挑选调用哪一个函数是在编译时完结的, 所所以静态联编。
下面经过重载进一步扩展已界说模板max的适用规模。
专门化和重载的比如:
#include <iostream>
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=“<<max(d,10) <<" charMax="<<max(c,10)<<endl;
}
程序输出成果为:
ABD ABE W 8.9 6 intMax=-1 doubleMax=15.6 charMax=w
留意履行句子2和3的差异:它们履行重载的进程相同, 但在重载函数调用时, 前者运用专门化(指针参数)模板, 后者运用界说(挑选单字符参数)模板。句子5不调用模板, 运用一般的函数。
1.2.3类模板
假如将类看做包括某些数据类型的结构, 把对支撑该类型的不同操作了解为:将数据类型从类中别离出来,答应单个类处理通用的数据类型T。
其实, 这种类型并不是类, 而仅仅是类的描绘, 常称之为类模板。
在编译时, 由编译器将类模板与某种特定数据类型联络起来, 就发生一个实在的类。
由此可见,
运用类模板进行程序规划, 就如烹调食物相同, 只需购买了原料,就能够做出不同口味的菜肴。
类模板声明的一般办法如下:
template <类模板参数>
class 类名
{
//类体
};
类模板也称为参数化类型。
初始化类模板时, 传给它详细的数据类型, 就发生了模板类。
运用模板类时,编译器主动发生处理详细数据类型的一切成员( 数据成员和成员函数)。
只需赋给模板一种实践数据类型, 就会发生新的类,并且以特定类型替代模板参数。
界说方针的一般格局如下:
类名<模板实例化参数类型>方针名(结构函数实参列表);
类名<模板实例化参数类型>方针名; //默许或许无参数结构函数
模板实例化参数类型包括数据类型和值, 编译器不能从结构函数列表揣度出模板实例化参数类型, 所以有必要显式地给出它们的参数类型。
在类体外面界说成员函数时, 有必要用template重写模板函数声明。 一般格局如下:
template<模板参数>
回来类型 类名<模板类型参数>::成员函数名(函数参数列表)
{
//函数体
}
<模板类型参数>是指template的“< >”内运用class(或typename)声名的类型参数, 也便是运用<T1,T2,…,Tn> 的类型参数列表。 结构函数和析构函数没有回来类型。
演示对4个数字求和的类模板程序:
#include <iostream>
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()<<", “<<d1.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 <iostream>
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 <iostream>
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类的数组调集模板等。
1.3虚函数和多态性
多态性也称后束缚 (late binding) 或动态束缚(dynamic binding) , 它 常 用 虚 函 数 (virtualfunctions) 来完结。
动态联编所支撑的多态性称为运转时的多态性, 这由虚函数来支撑。
虚函数相似于重载函数, 但与重载函数的完结策略不同, 即对虚函数的调用运用动态联编。
1.3.1静态联编中的赋值兼容性及姓名分配规矩
假定下面比如中的类Point和类Circle各有一个area函数, 各自完结不同的功用。
类的方针和调用的函数一一对应, 编译时即可确认调用联系, 然后发生编译时的多态性。
剖析下面程序的输出成果。
#include <iostream>
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。
这完全符合赋值兼容性则。 编译器编译成员函数是依据数据类型, 类型是事前决议了的, 所以由静态联编决议。
1.3.2动态联编的多态性
假如让编译器动态联编, 也便是在编译“ 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()函数。
1.3.3虚函数的界说
为完结某种功用而假定的函数称作虚函数。
虚函数是完结多态性的根底。
一旦基类界说了虚函数, 该基类的派生类中的同名函数也主动成为虚函数。
虚函数只能是类中的一个成员函数, 但不能是静态成员, 要害字virtual用于类中该函数的声明中。
例如:
class A
{
public:
virtual void fun( ); //声明虚函数
};
void A::fun( ) //界说虚函数
{
// ......
}
当在派生类中界说了一个同名的成员函数时, 只需该成员函数的参数个数和相应类型以及它的回来类型与基类中同名的虚函数完全相同(例如void area(void) 函数) , 则不管是否为该成员函数运用 virtual, 它都将成为一个虚函数。
在上节的比如中,基类Point声明成员函数area为“virtual void area(void);”, 则派生类Circle中的area函数主动成为虚函数。
1.3.4虚函数完结多态性的条件
要害字== virtual== 指示 C++编译器对调用虚函数进行动态联编。
这种多态性是程序运转到此处才动态确认的, 所以称为运转时的多态性。
不过,
运用虚函数并不一定发生多态性, 也不一定运用动态联编。
例如,
在调用中对虚函数运用成员名限制, 能够强制 C++ 对该函数的调用运用静态联编。
发生这种多态性的条件有如下3条: (1) 类之间的承继联系满意赋值兼容性规矩; (2) 改写了同名虚函数; (3) 依据赋值兼容性规矩运用指针(或引证) 。
满意前两条并不一定发生动态联编, 有必要有第3条才能确保完结动态联编。
第3条又有两种状况。
第1种是现已演示过的按赋值兼容性界说运用基类指针(或引证)拜访虚函数。
第2种是把指针(或引证)作为函数参数。
下面的比如是规划一个外部函数display, 经过指针(或引证)完结多态性的完好程序。
别离运用指针和引证的display函数。 剖析下面程序的输出成果:
#include <iostream>
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
由于动态联编是在运转时进行的, 相关于静态联编, 它的运转效率比较低, 但它能够使程序员对程序进行高度抽象, 规划出可扩充性好的程序。
1.3.5进一步讨论虚函数与实函数的差异
假定基类和派生类都只需一个公有的数据成员, 其中类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 <iostream>
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()
1.3.6纯虚函数与抽象类
在许多状况下, 在基类中不能为虚函数给出一个有含义的界说,这时能够将它阐明为纯虚函数。 它的界说留给派生类来做。
阐明纯虚函数的一般办法为:
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 <iostream>
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虚函数, 并在其中把窗口发生出来即可。
1.3.7多重承继与虚函数
多重承继能够被视为多个单一承继的组合,
因此,
剖析多重承继状况下的虚函数调用与剖析单一承继有相似之处。
在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(c), 当再履行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 <iostream>
#include <string>
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
1.4函数指针和类成员指针
MFC运用函数指针和指向类成员指针来完结某些操作, 有时让初学者感到困惑。 例如窗口回调函数等。
本节将回忆一下函数指针和类成员指针的常识, 以方便后续章节的学习。
1.4.1函数指针
函数在内存中有确认的物理地址, 该地址能够赋给指针。
这是由于函数在编译时, 它的源代码转化成了方针代码, 一起确认了函数的进口地址。
程序调用函数, 也便是指向了这个进口地址。
因此,
指向函数的指针实践上包括了函数的进口地址, 所以赋给指针的地址便是函数的进口地址, 然后该指针就用来替代函数名。
这就使得函数能够作为实参传递给其他函数, 然后可将一个指向函数的指针传递给函数, 或放置在数组中供给给其他方针运用。
1.4.1.1函数指针界说
函数指针界说办法如下:
数据类型标识符 (*指针方针名)(函数参数的数据类型列表);
例如句子“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) 的函数的指针。
1.4.1.2函数指针方针赋值
给函数指针方针赋值时, 只需求给出函数名而不必给出参数。
由于
句子p=函数名;
是将函数进口地址赋给指针方针p, 而不涉及到实参加形参的结合问题。
数组名代表数组开始地址, 这儿的函数名则代表函数的进口地址。
由于在C++中, 独自一个函数名(其后不跟一对圆括号) 被主动地转化为指向该函数的指针( 函数的第1条指令的地址) 。
当函数地址被置入一个指针方针中时, 能够经过该指针方针调用该函数。
这儿的p便是指向该函数的指针方针, 它和函数名都指向函数的最初, 调用p便是调用这个函数。 但它只能指向函数的进口处, 而不能指向函数中间的详细指令。 因此,
*(p+1)、 p+n、 p–及p++等运算对它都是没有含义的。
1.4.1.3调用指向函数
调用指向函数的格局如下:
(*指针方针名)(函数实参表);
函数指针方针调用函数时, 只需将(*指针方针名)替代函数名即可, 在(*指针方针名)之后的括号中依据需求写上实参。
例如句子(*p)(a,b)将调用由p指向的函数, 把a 和 b作为实参传递给函数。
圆括号内也可不含参数, 但圆括号有必要存在, 下列句子以相似的办法调用无参数的函数:
(*p) ( );
p也需求运用圆括号括起来, 以强制运算“ *” 在被调用前运用。
若不加括号, 则
int *p( );
就变成了一个回来整数指针的函数声明。
能够用一般办法运用间接调用的函数回来成果。
例如,
下列句子将把函数调用回来的成果赋值到i方针。
i = (*p) (a,b);
1.4.1.4函数声明
有必要声明函数的原型。
现在是用函数名作右值, 后边没有括号和参数, 编译体系无法判别它们究竟是方针名仍是函数名, 故要对它们加以声明。
即:
数据类型 函数名(参数的数据类型列表);
本例的声明如下:
int min(int, int);
输出多项式x2+5x+8和x3-6x在区间[-1, +1] 内, 增加步长为0.1时的一切成果。
#include <iostream>
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 <iostream>
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
求函数10×2-9x+2在区间 [ 0, 1] 内x以0.01的增量改动的最小值。
#include <iostream>
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
1.4.2指向类成员的指针
方针是一个完好的实体。 为了支撑这一封装,C++包括了指向类成员的指针。
能够用一般指针拜访内存中给定类型的任何方针, 指向类成员的指针则用来拜访某个特定类的方针中给定类型的任何成员。
C++既包括指向类数据成员的指针, 又包括指向成员函数的指针。
C++供给一种特别的指针类型, 指针指向类的成员, 而不是指向该类的一个方针中该成员的一个实例, 这种指针称为指向类成员的指针。
1.4.2.1指向类数据成员的指针
类并不是方针, 但有时可将其视为方针来运用。 能够声明并运用指向数据成员的指针或指向方针数据成员的指针。
指向方针的指针是比较传统的指针, 指向类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 <iostream>
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
}
由于静态数据成员不归于任何方针, 所以拜访指向静态数据成员的指针时不需求方针。 由于静态数据成员是在类中阐明的, 所以取它的地址时需求进行成员名限制。
1.4.2.2指向类成员函数的指针
指向类成员函数的指针与指向类数据成员的指针作业原理相似,用处也相似。
主要的差异在于语法更杂乱。
假定类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 <iostream>
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 <iostream>
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
}
1.5静态成员
假如类的数据成员或成员函数运用要害字static进行修饰, 这样的成员称为静态数据成员或静态成员函 数, 统称为静态成员。 静态成员在MFC中扮演着重要人物。
1.5.1运用举例
剖析下面程序的输出成果
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 <iostream>
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 <iostream>
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循环句子中的局部类方针, 所以生命期只能与本次循环共存, 每当循环体的本次循环完毕时,它都要调用一次析构函数。 经过比较它们的输出成果, 很简单看出它们之间的差异。
1.5.2静态成员在MFC中的人物
能够运用静态成员变量存储一切方针的共有信息。 由于不管是运转时类信息( 包括基类和派生类名等) , 仍是类的映射表( 如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( );
1.6分类、聚合和嵌套
类与类之间的联系有两大类。 一是承继和派生问题, 二是一个类运用另一个类的问题。
后者的简略用处是把另一个类的方针作为自己的数据成员或许成员函数的参数。
前者称为分类, 运用分类准则也意味着经过不同程度的抽象而构成基类-派生类结构(又称分类结构), 基类比派生类的抽象程度更高。
运用分类准则能够集中地描绘方针的共性, 清晰地标明方针与类的联系( 即“ isa”联系) 以及派生类与基类的联系( 即“is-a-kind-of”联系) , 然后使体系的杂乱性得到操控。
后者称为聚合( aggregation) , 又称为拼装( composition) ,C++有时又称为包括。 它的准则是把一个杂乱的事物当作若干比较简略的事物的拼装体, 然后简化对杂乱事物的描绘。
在面向方针剖析(OOA)中运用聚合准则便是要差异事物的全体和它的组成部分, 别离用全体方针和部分方针来进行描绘, 构成一个全体-部分结构, 以清晰地标明它们之间的组成联系( 称为“ has-a”联系,或反过来称为“is-a-part-of”联系) 。
例如轿车的一个部件是发动机, 在OOA中能够把轿车作为全体方针, 把发动机作为部分方针,经过全体-部分结构表达它们之间的组成联系(轿车带有一个发动机, 或许说发动机是轿车的一部分) 。
在有些文献中, 对“聚合” 和“包括” 这两个术语的解释和用法略有差异。
前者用于比较松懈和灵敏的全体-部分结构, 后者用于紧密、 固定的全体-部分结构。
在类中也能够嵌入一个类, 把这个嵌入的类称为内嵌类, 包括这个类的类称为容纳类。
内嵌类具有自己的成员函数, 并被封装在容纳类中, 不只外部不能对它直接拜访, 并且也不受容纳类的操控(即两者的拜访操控是完全独立的) 。
内嵌类就像一个东西,用户能够经过内嵌类完结对容纳类数据的处理。
容纳类能够依据不同的处理方针, 封装不同的数据集, 并且代码变动不会影响本来嵌入的类界说。
同理,
内嵌类的界说能够依据用户的需求进行修正、 添加或删去, 这对容纳类也没有什么影响。
由此可见,
能够用内嵌类封装一个数据处理的用户接口部分, 使数据处理与数据的标明相别离。
由于内嵌类不能直接拜访容纳类的非公有成员, 容纳类对内嵌类也不存在任何拜访特权。
假如需求增进内嵌类和容纳类两者之间的联系, 能够将一方界说为另一方的友元类。
运用内嵌类和友元类的比如。
#include <iostream>
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 56 65 73 73 73
由此可见, 假如一个类包括一个内嵌类, 则要保涵该内嵌类的方针作为成员变量方针, 不然内嵌类就失掉效果, 变得毫无含义。
从这一点看来, 又与聚合相似。