本文主要介绍类的友元函数、虚函数、静态成员、const对象和volatile对象以及指向类成员的指针。
从之前的文章可知,当把类中的成员的访问权限定义为私有的或者保护的时,在类的外面,只能通过该类的成员函数来访问这些成员,这是由类的封装性所确定的。这种用法往往觉得不够方便,若把类的成员的访问均定义为公有的访问权限时,又损坏了面向对象的封装性。为此,在C++中提供了友元函数,允许在类外访问类中的任何成员(私有的、保护的或公有的成员)。
在定义一个类时,若在类中用friend修饰函数,则该函数就成为该类的友元函数,它可以访问该类中的所有成员。说明一个友元函数的一般格式为:
friend 返回值类型 函数名称(形参列表);
例如:
#include
using namespace std;
const float PI = 3.1415926;
class A {
private:
float r;
float h;
public:
A(float a, float b){
r = a;
h = b;
}
float Getr() { return r; }
float Geth() { return h; }
friend float Volum(A &); //A
};
float Volum(A &a) { //B
return PI*a.r*a.r*a.h; //C
}
int main()
{
A a1(25, 40);
cout << Volum(a1) << endl; //E
cout << PI*a1.Getr()*a1.Getr()*a1.Geth() << endl; //F
system("pause");
return 0;
}
本例程中,将Volum()函数定义为类A的友元函数,在A行给出了友元函数的原型说明。在B行具体定义函数的时候,并不像成员函数那样,用作用域运算符“::”。在E行调用友元函数的时候,没有使用对象来调用函数a1.Volum(a1),而是直接使用函数的。
有关友元函数的使用,必须说明以下几点:
总而言之,友元函数不是类的成员函数,它更类似于一般的函数,只不过它必须在类中进行说明,且必须用对象名或引用作为形参,且它能够访问类中的所有成员。
一个类可以定义若干个友元函数,可以将一个类的任一个成员函数说明为另一个类的友元函数,以便通过该成员函数访问另一个类的成员,亦可以将一个类中的所有成员函数都说明为另一个类的友元函数。
要将类C的一个成员函数(包括析构函数和构造函数)说明为类D的友元函数时,其一般格式如下:
class D; //A:对类D的引用说名,因为D类定义在C类后面,而C类中用到了D类
class C {
...
public:
void fun(D &); //B:类C的成员函数
};
class D{
...
friend void C:: fun(D &); //C:类C的成员函数作为类D的友元函数
};
void C:: fun(D &d){ //D:类C的成员函数的说明
...
}
这段程序将类C的成员函数作为了类D的友元函数。在B行只能给出函数的原型说明,不能给出函数体,因为类D还没有定义。能够用作友元函数的参数可以是类D的引用、类D的对象或者指向类D的指针。
例如:
#include
using namespace std;
class B; //A:对类B的引用性说明
class A {
private:
float x, y;
public:
A(float a, float b){
x = a;
y = b;
}
float Getx() { return x; }
float Gety() { return y; }
void Setxy(B &); //B:类A的成员函数,类B引用作为形参
};
class B {
private:
float c, d;
public:
B(float a, float b) {
c = a;
d = b;
}
float Getc() { return c; }
float Getd() { return d; }
friend void A::Setxy(B &); //C:类B的友元函数
};
void A::Setxy(B &b) { //D:函数的定义
x = b.c;
y = b.d;
}
int main()
{
A a1(25, 40);
B b1(55, 60);
cout << a1.Getx() << ' ' << a1.Gety() << endl;
a1.Setxy(b1); //E:函数的调用
cout << a1.Getx() << ' ' << a1.Gety() << endl;
system("pause");
return 0;
}
若要将一个类M中的所有成员函数都说明成另一个类N的友元时,则不必在类N中一一列出M类的成员函数为友元,可简化为:
class N{
...
friend class M; //说明类M是类N的友元
};
class M{
...
};
在类M中的所有成员函数可以使用类N中的全部成员,成类M为类N的友元。
注意:友元关系是不传递的。例如:类A是类B的友元,类B是类C的友元时,类A并不一定是类C的友元;这种友元关系也不具有交换性。例如:类A是类B的友元时,类B不一定是类A的友元。同样的,友元关系时不继承的。这是因为友元函数不是类的成员函数,当然不存在继承关系。
多态性时实现OOP的关键技术之一。它常用虚函数或重载技术来实现。利用多态性实现技术,可以调用同一个函数名的函数,但实现完全不同的功能。
在C++中,将多态性分为两种:编译时的多态性和运行时的多态性。编译时的多态性是通过函数的重载或运算符的重载来实现的;运行时的多态性是通过类的继承关系和虚函数来实现的。
为实现某一种功能而假设的虚拟函数称为虚函数,虚函数只能是一个类中的成员函数,并且不能是静态的成员函数。定义一个虚函数的一般格式为:
virtual 返回值 函数名(形参列表);
一旦把某一个类的成员函数定义为虚函数,由该类所派生出来的所有派生类中,该函数均保持虚函数的特性。当在派生类中定义了一个与该虚函数同名的成员函数,并且改成原函数的参数个数、参数类型以及函数的返回值类型都与基类中的同名虚函数一样,则无论是否使用virtual修饰该成员函数,它都成为一个虚函数。
也就是说,在派生类中重新定义基类的虚函数时,可以不使用关键字virtual来修饰。
例如:
#include
using namespace std;
class A {
private:
int x;
public:
A() {
x = 100;
}
virtual void print() { //A
cout << x << endl;
}
};
class B:public A {
private:
int y;
public:
B() {
y = 200;
}
void print() {
cout << y << endl;
}
};
class C :public A {
private:
int z;
public:
C() {
z = 300;
}
void print() {
cout << z << endl;
}
};
int main()
{
A a,* p;
B b;
C c;
a.print();
b.print();
c.print();
p = &a;
p->print();
p = &b;
p->print();
p = &c;
p->print();
system("pause");
return 0;
}
这段程序的运行结果为:
100
200
300
100
200
300
请按任意键继续. . .
前三个的输出都是明显的,通过调用三个不同对象的成员函数,分别输出各自的值。因在编译时,根据对象名就可以确定要调用哪一个成员函数,这是编译时的多态性。
而后三个的输出是将三个不同类型的对象起始地址赋给基类的指针变量,这在C++中是允许的,即可以将由基类所派生出来的派生类对象的地址赋给基类类型的指针变量。当基类指针指向不同的对象时,尽管调用的形式完全相同,但却是调用不同对象中的虚函数。因此输出了不同的结果,这是运行时的多态。
为了体会一下虚函数的用法,将上例中的virtual去掉,看一下程序:
#include
using namespace std;
class A {
private:
int x;
public:
A() {
x = 100;
}
void print() {
cout << x << endl;
}
};
class B:public A {
private:
int y;
public:
B() {
y = 200;
}
void print() {
cout << y << endl;
}
};
class C :public A {
private:
int z;
public:
C() {
z = 300;
}
void print() {
cout << z << endl;
}
};
int main()
{
A a,* p;
B b;
C c;
a.print();
b.print();
c.print();
p = &a;
p->print();
p = &b;
p->print();
p = &c;
p->print();
system("pause");
return 0;
}
这段程序的运行结果为:
100
200
300
100
100
100
请按任意键继续. . .
virtual删除前后比较一下,可以看出一些端倪:
关于虚函数,须说明以下几点:
总结起来虚函数的作用就是:
派生类的指针可以赋给基类指针,而通过基类指针调用基类和派生类中的同名虚函数时:
这种机制就叫做多态。
类中的虚函数是动态生成的,由虚函数表的指向进行访问,不为类的对象分配内存,就没有虚函数表就无法访问。类中的普通函数静态生成,不为类的对象分配内存也可访问。
成员函数中调用虚函数
例子:
#include
using namespace std;
class A {
public:
virtual void fun1() {
cout << "A::fun1" << ' ';
fun2();
}
void fun2() {
cout << "A::fun2" << ' ';
fun3();
}
void fun3() {
cout << "A::fun3" << ' ';
fun4();
}
virtual void fun4() {
cout << "A::fun4" << ' ';
fun5();
}
void fun5() {
cout << "A::fun5" << endl;
}
};
class B: public A {
public:
void fun3() {
cout << "B::fun3" << ' ';
fun4();
}
void fun4() {
cout << "B::fun4" << ' ';
fun5();
}
void fun5() {
cout << "B::fun5" << endl;
}
};
int main()
{
B b;
b.fun1();
system("pause");
return 0;
}
这段程序的运行结果为:
A::fun1 A::fun2 A::fun3 B::fun4 B::fun5
请按任意键继续. . .
这一题的主要知识点是:C++的所有成员函数在被调用时都会得到this指针,然后通过this指针去调用个虚函数就是常规的查虚函数表跳转。
例子:
#include
using namespace std;
class A {
public:
virtual void fun() {
cout << "A::fun" << ' ';
}
A() {
fun();
}
};
class B: public A {
public:
B() {
fun();
}
void fun() {
cout << "B::fun" << ' ';
}
void g() {
fun();
}
};
class C : public B {
public:
C() {
fun();
}
void fun() {
cout << "C::fun" << endl;
}
};
int main()
{
C c;
c.g();
system("pause");
return 0;
}
这段程序的运行结果为:
A::fun B::fun C::fun
C::fun
请按任意键继续. . .
这是因为在构造函数中调用虚函数时,只调用自己类中定义的函数(若自己类中没有定义,则调用基类中定义的函数),而不是调用派生类中重新定义的虚函数。
也就是说,在构造函数中调用虚函数是不起作用的!该什么样,就什么样。
如果对这部分的知识不够理解的话,可以参考链接:虚函数的实调用与虚调用。
有一个基类派生出来的类体系中,使用虚函数可对类体系中的任一子类提供一个统一的接口,即用相同的方法来对同一个类体系中的任一子类的对象进行各种操作,并可把接口与实现两者分来,建立基础类库。
在VC++的基础类库正是使用了这种技术。在定义一个基类时,会遇到这样的情况:无法定义基类中虚函数的具体实现,其实现完全依赖于其不同的派生类。这是,可把基类中的虚函数定义为纯虚函数。
定义纯虚函数的一般格式为:
virtual 返回值类型 函数名称(形参列表) = 0;
有关纯虚函数的使用,须说明以下几点:
综上所述:抽象类的唯一用途就是为派生类提供基类,纯虚函数的作用是作为派生类中的成员函数的基础,并实现动态多态性。
在定义一个类时,实际上是定义了一种数据类型,编译程序并不为数据结构分配存储空间。只有在说明类的对象时,才依次为对象的每一个成员分配存储空间,并把对象占用的存储空间看成一个整体对待。
通常,每当说明一个对象时,把该类中的有关成员拷贝到该对象中,即同一类的不同对象,其成员之间是相互独立的。当把类的某一个数据成员的存储空间指定为静态类型时,则由该类所产生的所有对象均共享为静态成员所分配的一个存储空间。
在类定义中,用关键字static修饰的成员数据称为静态成员数据。
有关静态成员数据的使用,须说明以下几点:
例如:
#include
using namespace std;
class A {
private:
int i, j;
public:
static int x;
A(int a, int b) { //一般静态成员数据不通过构造函数赋初值
i = a;
j = b;
}
void Show() {
cout << i << ' ' << j << ' ' << x << endl;
}
};
int A::x = 100; //静态成员数据定义性说明
int main()
{
A a(50, 100);
a.Show();
a.x = 200;
a.Show();
cout << A::x << endl; //直接通过类名作用域运算符调用,无须类的对象
system("pause");
return 0;
}
这段程序的运行结果为:
50 100 100
50 100 200
200
请按任意键继续. . .
与静态的成员数据一样,可以将类的成员函数定义为静态的成员函数。其方法也是使用关键字static来修饰成员函数。
对静态成员函数的用法说明以下几点:
可以用关键字const和volatile来修饰类的成员函数和对象。当用这两个关键字修饰成员函数时,const和volatile对类的成员函数具有特定的语义:
当希望成员函数只能引用成员数据的值,而不允许修改成员数据的值,可用关键字const修饰成员函数,一旦在引用const修饰的成员函数中出现了修改成员数据的值时,将导致编译出错。
在成员函数的前面加上关键字const,表示该函数返回一个常量,其值不可改变。这里讲的const成员函数是指将canst放在参数表之后,函数体之前,其一般格式为:
返回值类型 函数名称(形参列表) const;
表示该函数的this指针所指向的对象是一个常量,即规定了const成员函数不能修改对象的数据成员,在函数体内只能调用const成员函数,不能调用其他成员函数。
用volatile修饰一个成员函数时,其一般格式为:
返回值类型 函数名称(形参列表) volatile;
表示成员函数具有一个易变的this指针,调用该函数时,编译程序把属于此类的所有成员数据都看成是易变的变量,编译器不要对该函数作优化工作。因为这种成员函数的执行速度要慢一点,但可保证易变变量的值是正确的。
也可以用这两个关键字同时修饰一个成员函数,其格式为:
返回值类型 函数名称(形参列表) const volatile;
这两个关键字的顺序无关紧要,其语义时限定成员函数在其函数体内不能修改成员数据的值,同时也不要优化该函数,在函数体内把对象的成员数据作为易变变量来处理。
由于关键字const和volatile是属于数据类型的组成部分,因此若在类定义之外定义const或者volatile成员函数时,则必须用这两个关键字修饰,否则编译器则认为是重载函数,而不是定义const和volatile成员函数!这与static是不一样的,因为static不是数据类型的组成部分!
说明const和volatile对象的方法与说明一般变量的方法相同。说明const对象的一般格式为:
const 类名 对象名;
标示对象的数据成员均是常量,不能改变其成员数据的值。它可以通过成员运算符“.”访问const成员函数,但不能访问其他的成员函数。
说明volatile对象的一般格式为:
volatile类名 对象名;
标示对象的数据成员均是易变的。它可以通过成员运算符“.”访问volatile成员函数,但不能访问其他的成员函数。
简单地说:面向对象程序设计中,为了体现封装性,通常不允许直接修改类某些对象的数据成员。但如若要修改类对象,应调用某些特定的公有成员函数来完成,而不是所有的成员函数都行。所以,编译器须区分不安全与安全的成员函数(即区分试图修改类对象与不修改类对象的函数)。
class A{
public:
void fun1(){
cout << "fun1" <
而volatile的用法也是类似的。
在C++中可以定义一种特殊的指针,它指向类中的成员函数或类中的成员数据,并可通过这样的指针来使用类中的成员数据或者调用类中的成员函数。
定义一个指向类中成员数据的指针变量的一般格式为:
数据类型 类名::* 指针变量名
指向类中成员数据的指针变量的使用方法:
指针变量名 = &类名::成员数据名;
由于编译系统不为类名分配存储空间,也就是代表没有一个绝对的地址。所以这种赋值,是取该成员相对于该类的所在对象的偏移量,即相对地址(距离开始位置的字节数)。
对象名. *指针变量名
指向对象的指针 -> *指针变量名
因为,这种指针变量的值是一个相对地址,不是使用某一个对象中的成员数据的绝对地址,所以不能单独使用这种指针来访问成员数据。比如:
cout<< *指针变量名; //错误
例如:
#include
using namespace std;
class S {
private:
int x;
public:
float y, z;
float a;
S() {
x = y = z = a = 0;
}
S(int b, int c, int d, int e) {
x = b; y = c; z = d; a = e;
}
};
int main()
{
S s(100, 200, 300, 400), *pr;
pr = &s;
float S::*p; //定义指向成员数据的指针变量
p = &S::y; //给指向成员数据的指针变量赋值
cout << s.y << ' ' << pr->y << ' ' << s.*p << ' ' << pr->*p << endl; //引用
system("pause");
return 0;
}
这段程序的运行结果为:
200 200 200 200
请按任意键继续. . .
定义一个指向类中成员函数的指针变量的一般格式为:
返回值类型 (类名::*指针变量名)(形参列表);
在使用这种指向成员函数的指针前,应先对其赋值,其方法与用指向成员数据的指针的方法类同,即:
指针变量名 = 类名::函数名;
因为一个函数的函数名就是该函数的地址,所以不需要取址运算。
指向成员函数的指针变量的使用方法:
(对象名. *指针变量名)(实参列表);
(指向对象的指针 -> *指针变量名)(实参列表);