【编程基础】C++类的其他特性(包括:友元函数、虚函数)

本文主要介绍类的友元函数、虚函数、静态成员、const对象和volatile对象以及指向类成员的指针。


友元函数

从之前的文章可知,当把类中的成员的访问权限定义为私有的或者保护的时,在类的外面,只能通过该类的成员函数来访问这些成员,这是由类的封装性所确定的。这种用法往往觉得不够方便,若把类的成员的访问均定义为公有的访问权限时,又损坏了面向对象的封装性。为此,在C++中提供了友元函数,允许在类外访问类中的任何成员(私有的、保护的或公有的成员)。

友元函数的说明及使用

在定义一个类时,若在类中用friend修饰函数,则该函数就成为该类的友元函数,它可以访问该类中的所有成员。说明一个友元函数的一般格式为:

friend 返回值类型 函数名称(形参列表);

例如:

#include <iostream>
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),而是直接使用函数的。

有关友元函数的使用,必须说明以下几点:

  • 友元函数不是类的成员函数;
  • 由于友元函数并不是类的成员函数,它不带有this指针,因此在友元函数函数体中不能直接使用类的成员,而是要将对象名或者对象的引用作为友元函数的参数,再使用运算符“.”来访问对象的成员。同时,在调用友元函数的时候,不需要类的对象来调用,而是可以和一般函数一样直接调用;
  • 友元函数与一般函数的不同点:友元函数必须在类的定义中说明,其函数体可在类内定义,也可以在类外定义;它可以访问该类中的所有成员(公有的、私有的、保护的),而一般函数只能访问类中的公有成员。
  • 在类中对友元函数指定访问权限无效。正因为友元函数不是对应类的成员函数,所以它不受类中访问权限关键字的限定,可以把它放在类的任何一个位置;
  • 友元函数的作用域与一般的函数作用域相同,一般具有文件作用域;
  • 由于友元函数破坏了类的封装性,所以谨慎使用友元函数。

总而言之,友元函数不是类的成员函数,它更类似于一般的函数,只不过它必须在类中进行说明,且必须用对象名或引用作为形参,且它能够访问类中的所有成员。

成员函数用作友元

一个类可以定义若干个友元函数,可以将一个类的任一个成员函数说明为另一个类的友元函数,以便通过该成员函数访问另一个类的成员,亦可以将一个类中的所有成员函数都说明为另一个类的友元函数。

要将类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 <iostream>
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 <iostream>
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 <iostream>
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删除前后比较一下,可以看出一些端倪:

  • 当无虚函数时,遵循以下规则:C++规定,定义为基类的指针,也能作指向派生类的指针使用,并可以用这个指向派生类对象的指针访问继承来的基类成员;但不能用它访问派生类的成员。
  • 而使用虚函数实现运行时的多态性的关键在于:必须通过基类指针访问这些函数。也就是说,一旦定义为虚基类,只要定义一个基类的指针,就可以指向派生类的对象。

关于虚函数,须说明以下几点:

  • 当在基类中把成员函数定义为虚函数后,在其派生类中定义的虚函数必须与基类中的虚函数同名,参数的类型、顺序、参数的个数必须一一对应,函数的返回的类型也相同;
  • 实现这种动态的多态性时,必须使用基类类型的指针变量(引用对象也可以),使该指针指向不同派生类的对象,并通过调用指针所指向的虚函数才能实现动态的多态性;
  • 虚函数必须是类的一个成员函数,不能使友元函数,也不能是静态的成员函数;
  • 在派生类中没有重新定义虚函数时,与一般的成员函数一样,当调用这种派生类对象的虚函数时,则调用其基类中的虚函数;
  • 可把析构函数定义为虚函数,但是不能将构造函数定义为虚函数。通常在释放基类中和其派生类中的动态申请的存储空间时,也要把析构函数定义为虚函数,以便完成撤销对象时的多态性;
  • 虚函数与一般的成员函数相比较,调用时的执行速度要慢一些。因为为了实现多态性,在每一个派生类中均要保存相应虚函数的入口地址表,函数的调用机制也是间接实现的。

总结起来虚函数的作用就是:

派生类的指针可以赋给基类指针,而通过基类指针调用基类和派生类中的同名虚函数时:

  • 若该指针指向一个基类的对象,那么被调用是基类的虚函数;
  • 若该指针指向一个派生类的对象,那么被调用的是派生类的虚函数。

这种机制就叫做多态。

类中的虚函数是动态生成的,由虚函数表的指向进行访问,不为类的对象分配内存,就没有虚函数表就无法访问。类中的普通函数静态生成,不为类的对象分配内存也可访问。

成员函数中调用虚函数

例子:

#include <iostream>
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 <iostream>
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;

有关纯虚函数的使用,须说明以下几点:

  • 在定义纯虚函数时,不能定义虚函数的实现部分;
  • 把函数名赋值为0,本质上是将指向函数体的指针值赋值为0。所以与定义空函数不一样,空函数的函数体为空,即调用该函数时,不执行任何动作。在没有重新定义纯虚函数之前,是不能调用这种函数的;
  • 把至少包含一个纯虚函数的类称为抽象类。这种类只能作为派生类的基类,不能用来说明这种类的对象。其理由很明显:因为虚函数没有实现部分,不能产生对象。但可以定义指向抽象类的指针,即指向这种基类的指针。当用这种基类指针指向其派生类的对象时,必须在派生类中重载纯虚函数;
  • 在以抽象类为基类的派生类中必须有纯虚函数的实现部分,即必须有重载纯虚函数的函数体。否则,这样的派生类也无法产生对象。

综上所述:抽象类的唯一用途就是为派生类提供基类,纯虚函数的作用是作为派生类中的成员函数的基础,并实现动态多态性。


静态成员

在定义一个类时,实际上是定义了一种数据类型,编译程序并不为数据结构分配存储空间。只有在说明类的对象时,才依次为对象的每一个成员分配存储空间,并把对象占用的存储空间看成一个整体对待。

通常,每当说明一个对象时,把该类中的有关成员拷贝到该对象中,即同一类的不同对象,其成员之间是相互独立的。当把类的某一个数据成员的存储空间指定为静态类型时,则由该类所产生的所有对象均共享为静态成员所分配的一个存储空间。

静态数据成员

在类定义中,用关键字static修饰的成员数据称为静态成员数据。

有关静态成员数据的使用,须说明以下几点:

  • 类的静态成员数据是静态分配存储空间的,而其他成员时动态分配存储空间的(全局变量除外)。当类中没有定义静态成员数据时,在程序执行期间遇到说明类的对象时,才为对象的所有成员一次分配存储空间(动态的);当类中定义了静态成员数据时,在编译时,就要为类的静态成员数据分配存储空间;
  • 必须在文件作用域中,对静态成员数据作一次且只能作一次定义性说明。由于类是一种数据结构,所以在定义类时,并不为类分配存储空间,这种说明属于引用性说明。只有遇到定义性说明,编译程序才能为静态成员数据分配存储空间;
  • 只要对静态成员数据进行了定义性说明,就可以直接通过类名加上作用域运算符引用静态成员数据,而不需要类的对象来引用;
  • 一般情况下,在构造函数中不给静态成员数据赋初值,而是在定义性说明的时候指定初值。

例如:

#include <iostream>
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来修饰成员函数。

对静态成员函数的用法说明以下几点:

  • 与静态成员数据一样,在类外的程序代码中,可以直接通过类名加上作用域运算符调用静态成员函数,而不需要类的对象来引用;
  • 静态成员函数只能直接使用本类的静态成员数据或静态成员函数,但不能直接使用非静态的成员数据。这是因为静态成员函数可被其他程序直接调用,所以它不包含对象地址的this指针;
  • 静态成员函数的实现部分在类定义之外定义时,其前面不能加修饰词static;
  • 不能将静态成员函数定义为虚函数。静态成员函数也是在编译时分配存储空间的,所以在程序的执行过程中不能提供多态性;
  • 可将静态成员函数定义为内联的(inline)。


const、volatile对象和成员函数

可以用关键字const和volatile来修饰类的成员函数和对象。当用这两个关键字修饰成员函数时,const和volatile对类的成员函数具有特定的语义:

  • 用const修饰的对象只能访问该类中用const修饰的成员函数,而不能访问其他成员函数;
  • 用volatile修饰的对象,只能访问该类中用volatile修饰的成员函数,而不能访问其他成员函数。

当希望成员函数只能引用成员数据的值,而不允许修改成员数据的值,可用关键字const修饰成员函数,一旦在引用const修饰的成员函数中出现了修改成员数据的值时,将导致编译出错。

const和volatile成员函数

在成员函数的前面加上关键字const,表示该函数返回一个常量,其值不可改变。这里讲的const成员函数是指将canst放在参数表之后,函数体之前,其一般格式为:

返回值类型 函数名称(形参列表) const;

表示该函数的this指针所指向的对象是一个常量,即规定了const成员函数不能修改对象的数据成员,在函数体内只能调用const成员函数,不能调用其他成员函数。

用volatile修饰一个成员函数时,其一般格式为:

返回值类型 函数名称(形参列表) volatile;

表示成员函数具有一个易变的this指针,调用该函数时,编译程序把属于此类的所有成员数据都看成是易变的变量,编译器不要对该函数作优化工作。因为这种成员函数的执行速度要慢一点,但可保证易变变量的值是正确的。

也可以用这两个关键字同时修饰一个成员函数,其格式为:

返回值类型 函数名称(形参列表) const volatile;

这两个关键字的顺序无关紧要,其语义时限定成员函数在其函数体内不能修改成员数据的值,同时也不要优化该函数,在函数体内把对象的成员数据作为易变变量来处理。

由于关键字const和volatile是属于数据类型的组成部分,因此若在类定义之外定义const或者volatile成员函数时,则必须用这两个关键字修饰,否则编译器则认为是重载函数,而不是定义const和volatile成员函数!这与static是不一样的,因为static不是数据类型的组成部分!

const和volatile对象

说明const和volatile对象的方法与说明一般变量的方法相同。说明const对象的一般格式为:

const 类名 对象名;

标示对象的数据成员均是常量,不能改变其成员数据的值。它可以通过成员运算符“.”访问const成员函数,但不能访问其他的成员函数。

说明volatile对象的一般格式为:

volatile类名 对象名;

标示对象的数据成员均是易变的。它可以通过成员运算符“.”访问volatile成员函数,但不能访问其他的成员函数。

简单地说:面向对象程序设计中,为了体现封装性,通常不允许直接修改类某些对象的数据成员。但如若要修改类对象,应调用某些特定的公有成员函数来完成,而不是所有的成员函数都行。所以,编译器须区分不安全与安全的成员函数(即区分试图修改类对象与不修改类对象的函数)。

class A{
    public:
        void fun1(){
            cout << "fun1" <<endl;
        }
        void fun2() const{
            cout << "fun2" <<endl;
        }
}

A a1;                        //可调用任何的成员函数
const A a2;                  //只能调用fun2()函数,不能调用fun1()函数

而volatile的用法也是类似的。


指向类中成员数据的指针变量

在C++中可以定义一种特殊的指针,它指向类中的成员函数或类中的成员数据,并可通过这样的指针来使用类中的成员数据或者调用类中的成员函数。

指向类中成员数据的指针变量

定义一个指向类中成员数据的指针变量的一般格式为:

数据类型 类名::* 指针变量名

指向类中成员数据的指针变量的使用方法:

  • 指向类中成员数据的指针变量不是类中的成员,应在类外说明;
  • 与指向类中成员数据的指针变量同类型的任一成员数据,可将其地址赋给该指针变量,赋值的一般格式为:
指针变量名 = &类名::成员数据名;

由于编译系统不为类名分配存储空间,也就是代表没有一个绝对的地址。所以这种赋值,是取该成员相对于该类的所在对象的偏移量,即相对地址(距离开始位置的字节数)。

  • 用这种指针访问成员数据时,必须指明是使用哪一个对象的数据成员。当与对象结合使用时,其用法为:
对象名. *指针变量名
指向对象的指针 -> *指针变量名

因为,这种指针变量的值是一个相对地址,不是使用某一个对象中的成员数据的绝对地址,所以不能单独使用这种指针来访问成员数据。比如:

cout<< *指针变量名;                    //错误

  • 由于这种指针变量不是类的成员,所以使用它只能访问对象的公有成员数据。若要访问对象的私有成员数据,必须通过成员函数来实现。

例如:

#include <iostream>
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
请按任意键继续. . .

指向类中成员函数的指针变量

定义一个指向类中成员函数的指针变量的一般格式为:

返回值类型 (类名::*指针变量名)(形参列表);

在使用这种指向成员函数的指针前,应先对其赋值,其方法与用指向成员数据的指针的方法类同,即:

指针变量名 = 类名::函数名;

因为一个函数的函数名就是该函数的地址,所以不需要取址运算。

指向成员函数的指针变量的使用方法:

  • 不能将任一成员函数的地址赋给指向成员函数的指针变量,只有成员函数的参数个数、参数类型、参数顺序与函数的类型均与这种指针变量相同时,才能将成员函数的地址赋给该变量;
  • 使用这种指针变量来调用成员函数时,必须指明调用哪一个对象的成员函数,这种指针变量不能单独使用的。其用法为:
(对象名. *指针变量名)(实参列表);
(指向对象的指针 -> *指针变量名)(实参列表);

  • 由于这种指针变量不是类的成员,所以用它只能调用公有的成员函数;
  • 当一个成员函数的指针指向一个虚函数,且通过指向对象的基类指针或对象的引用来访问该成员函数的指针时,同样会产生运行时的多态;
  • 当用这种指针指向静态成员函数时,可直接使用类名而不要列举对象名。这是由静态成员函数的特性决定的。

猜你喜欢

转载自blog.csdn.net/qq_38410730/article/details/80594421