第16章 数据的共享和流通 一、浅拷贝和深拷贝 二、只读成员函数 三、友元friend
一、浅拷贝和深拷贝 对象作为数据的集合,其中有些数据需要与变 量进行流通,有些数据需由不同的对象共享。 本章介绍的深拷贝、友元函数、静态成员和指 向成员的指针就是在不同的方面加快信息流动以及 实施对象的保护的。
存在两种形式的类。 一种形式的类中仅存在变量或对象,不具备指 针成员。缺省的拷贝构造函数和赋值运算符函数是 浅拷贝的方式,该方式通过memcpy函数将源实例 的数据复制给目标实例占住的一片内存空间。 对于这样的类,缺省的浅拷贝方式是安全的。 另一种形式的类含有指针成员,浅拷贝不再胜 任这样的类。
从上可见对于存在指针成员的类,系统提供的浅拷贝导 致指针指向的内存为两个对象共享的格局。 考虑如下说明: 从上可见对于存在指针成员的类,系统提供的浅拷贝导 致指针指向的内存为两个对象共享的格局。 class CDeep { int n; int*p; } a, b; 一个CDeep类的声明和对象定义 a.n a.p a.p=new int[a.n] b.n b.p b.p=new int[b.n] 对象a,b的内存和指针成员动态扩展的内存空间b=a导致[ b.p=a.p; b.n=a.n; ]。指针b.p指向a对象的动态内存。 ??=new int[b.n] a.n a.p a.p=new int[a.n] b.n b.p 中间深资源归口两个对象监控
浅拷贝的不良结果是:b.p原先指向的堆空间悬空----既 无法索引也不能收回这片内存,a或b对象的析构函数诱发中 间共享的深资源的流失。 对于凡是具有指针成员的类,应细致提交两个函数: 一.是拷贝构造函数, 二.是赋值运算符函数,以便进行指针成员的动态资源 的深拷贝。 深拷贝的核心思路是: 1. 目标对象与源对象内存空间独立,相应指针成员指 向的内存空间也彼此独立。 2. 全部拷贝源对象的数据到目标对象,包括分别拷贝 指针成员指向的内存数据。
[例] 深拷贝方式(去掉定制的拷贝构造函数和赋值运算符函 数则变成缺省的浅拷贝) #include <stdio.h> #include<string.h> class CDeep { public: int n; int *p; CDeep (int) ; ~CDeep (); CDeep (const CDeep& r) ; CDeep& operator= (const CDeep& r); };
CDeep::~CDeep() { static int s_num=1; printf ("%d.~CDeep ()\t", s_num++); delete [ ] p; } CDeep::CDeep (int k) { n=k; p=new int [n]; } CDeep& CDeep::operator= (const CDeep& r) { if (n!=r.n) { delete [ ] p; n=r.n; p=new int [n]; } memcpy (p, r.p, n*sizeof (int)); return *this;
CDeep::CDeep (const CDeep& r) { n=r.n; p=new int [n]; memcpy (p, r.p, n*sizeof (int)); } void main () { CDeep a (2), d(3); a.p[0]=1; d.p[0]=666; { CDeep b (d); a.p [0]=88; b=a; printf ("%d;",b.p[0]); printf ("%d; ",d.p[0]); printf ("b fade out away;\t"); printf ("%d; ",a.p[0]);
程序输出: 88;1.~CDeep() 666;b fade out away; 88; 2.~CDeep() 3.~CDeep() 删除上面的拷贝构造函数和等号赋值运算符函数时,程 序运行输出: 88;1.~CDeep() 666;b fade out away; -572662307;2.~CDeep() 3.~CDeep() 之后弹出一个Debug Assertion Failed!的警告对话框。 原因在于b对象退出作用范围后导致析构函数的调用, 析 构函数释放原来由a对象拥有的深部堆中资源,其后对该内 存空间的操作 a.d[0]就等于在没有定义的内存空间进行寻址 访问,因而是运行时的错误。
二、只读成员函数volatile, mutable关键字 1. 只读成员函数 未经const或volatile限制的对象或成员函数是普通的 对象或普通的成员函数,依称为对象或成员函数。 只读对象是关键字const限定的对象。只读成员函数是 const置于成员函数右圆括号之后修饰的成员函数,该成员 函数不修改成员变量的数据状态,即成员函数体中出现的数 据成员仅作为右值。 没有只读的构造函数和析构函数,只读对象和对象都调 用同一构造函数和析构函数。
const用于名称细分,成员函数声明和定义都必须紧跟 只读成员函数的声明和定义格式为:(其中type,t1, t2,tn是已经声明的类型) type f (t1 v1, t2 v2, ...,tn vn) const; type CType:: f(t1 v1, t2 v2, ...,tn vn) const {; ...;} 成员函数可存在两个重载版本: 一个只读版本另一个普通版本。 当两种版本并存时,对象优先调用普通的成员函数,只 读对象或只读的对象引用仅仅调用只读成员函数。
对象可调用只读成员函数。关键字const本质上约束 this形参为只读属性。 只读指针形参可以匹配左值区的地址,普通指针形参不 指向右值区的地址。 只读对象的地址是右值区的地址。 只读对象和只读成员函数不调用普通成员函数。普通的 成员函数可调用只读成员函数。 只读成员函数是只读取对象的数据状态的成员函数。
[例] const对象与只读成员函数。 #include<stdio.h> struct CPoint { long x; long y; } ; class CRect { public: CRect (int l, int t, int r, int b) ; CPoint& TopLeft () ; const CPoint& TopLeft () const ; private: long left; long top; long right; long bottom; }; inline CRect::CRect (int l, int t, int r, int b) { left = l; top = t; right = r; bottom = b; }
inline CPoint& CRect::TopLeft() { return *((CPoint*)this); } inline const CPoint& CRect::TopLeft () const { return *((CPoint*)this); } void main() { CRect r (10,20,30,40); CPoint& rtl = r.TopLeft (); const CRect d (rtl.x+5, rtl.y+5, 35, 45); CPoint dtl (d.TopLeft()); printf ("left=%d, top=%d\t", rtl.x, rtl.y); printf ("left=%d, top=%d\n", dtl.x, dtl.y); } //输出:left=10,top=20 left=15,top=25
成员函数TopLeft存在只读的和非只读的版本,其中的 语句是一样的。 返回引用的函数可以作为左值,本来只读成员函数不改 变对象的数据状态,如果不在返回类型上加const限制将导 致对象外部调用对数据的改变。 于是返回引用的只读成员函数在返回类型上由const前 置限制。 d.TopLeft ()为只读对象d调用只读成员函数。 r.TopLeft ()为对象r调用普通的成员函数。
volatile关键字表示内存数据的变更。 volatile关键字和const关键字的语法是一致的。 const修饰的变量维持恒定或函数不改变相关成员的数 据状态。 与const相反,volatile关键字限定的对象或成员函数 可以有效的变动。 这种变动可以来自其它的外部进程。
[例] volatile关键字的用法 #include <stdio.h> class B { volatile int m_n; public: B (int v=1) { m_n=v; } void Set (int n) volatile { m_n=n; } void Show () const { printf ("Show()const; n=%d\n",m_n); } void Show() volatile { printf ("Show() volatile; n=%d\n",m_n); } void Show() { printf ("Show() ;n=%d\n",m_n); } };
void main() { const B c; c.Show(); volatile B v(2); v.Show(); v.Set(3); B x(4); x.Show(); } /*程序输出结果: */ /*Show() const; n=1 */ /*Show() volatile; n=2*/ /*Show() volatile; n=3 */ /*Show() ; n=4 */
volatile对象操作volatile成员函数, const对象操作 const 成员函数。不妨认为volatile关键字和const关键字是 一对含意相反的语法修饰词,它们常称为c-v限定词。 对象既可操作const成员函数也可操作volatile成员函 数,如果这两个成员函数都存在但不存在普通的版本则导致 调用的歧义。 volatile关键字使用的场合在一般程序中不 多,主要用在系统的程序设计中。 volatile关键字可以和const同时出现。如下: volatile const int g_n=1; 这表示程序不能改变变量g_n的值,但允许系统改变它。
3. mutable关键字 关键字mutable可以局部松动const对象的不变属性。 如果一个对象前加上const关键字限制,则这个对象所 有的成员就冻结为右值。 但有时候对于这种约束期望有所放松,此时只需在相关 的成员前冠以mutable修饰,通知编译器如此成员不受 const的制约。
[例] mutable关键字注册绝对可变的成员 #include <stdio.h> class CType { public: mutable long v; long n; }; void main() { const CType cobj={1,2}; printf ("cobj={%d,%d}; \t",cobj.v,cobj.n); cobj.v=8; printf ("cobj={%d,%d};\n",cobj.v,cobj.n); //cobj.n=2; error : l-value specifies const object } //输出结果:cobj={1,2}; cobj={8,2};
上面的代码中v是一个绝对可变的成员。 n是一个相对可变的成员,n受const对象限制时而间接 的成为右值表达式。 对于本题cobj是一个不变对象,因而cobj.n不能成为左 值,v由于有了mutable的特殊关照,cobj.v则构成左值表达 式。
三、友元friend friend声明的函数称为友元函数,friend 可用于声明一 个类,这个类称为友元类。 友元函数细分为全局友元函数和成员友元函数。友元函 数或友元类在所访问类或当前类中由friend声明。格式为: class CN { private: friend T f (T1, CN&, Tn); friend type A::f (CN*,T2,Tn); protected: friend class B; friend void funct (T v, CN& r) {...} }; //类型名称type,T, T1, T2, …,Tn, A, B在使用点之前 应首先说明
friend关键字无视protected,private的制约,放置在类 声明的任何地方是等效的。 友元可访问当前类的所有成员。友元类所有的成员函数 都是友元函数。 友元类或友元函数一般通过三个途径访问当前类所有的 成员,包括公共属性的保护属性的和私有属性的成员: a. 友元函数的入口形参具有当前类的类名。 b. 友元函数具有当前类的对象作为局部对象或对象指针 作为局部指针。 c. 友元类具有当前类的对象作为其嵌入成员对象。
1. 友元函数 友元函数是一种定义在当前类外部的普通函数,通过 friend在当前类内部声明,但友元函数不是当前类的成员函 数。 在当前类的友元函数获得如此特权: 定义在友元函数形参列表或函数体中的当前类对象、对 象引用或对象指针等可以访问该类所有控制属性的成员包括 私有成员,就好像当前类三个访问控制属性全部是公共的访 问控制属性。
友元是一种解除访问控制约束的机制。 此种破除访问控制约束的友元关系不传递不反身不继 承,即如果类A是类B的友元, 类B是类C的友元,A不会自动 称为类C的友元; 类A是类B的友元但不一定类B是类A的友元; 类A是类B的友元, 类A的派生类D不会自动成为类B的 友元。 友元函数是某个类的特权函数,这个访问当前类所有成 员的特权在该类中由friend声明获得,未由friend声明的函 数操作当前类的公共成员。
[例] 友元函数将类回归到经典的公共访问性质的结构 #include <stdio.h> struct S {int e;}; class B; class A { private: int a; public: void add(B& ); friend void swap(A*,B& ); int& Ea () { return a; } };
class B { friend void swap (A *p,B& r); private: int b; friend void A::add (B& r); public: int& Eb () { return b; } }; void PublicB (S *p,B& r) { r.Eb()=1; //p->e=r.b; //error 'b' : cannot access private member }
void swap (A *p,B& r) { int t=p->a; p->a=r.b; r.b=t; } void A::add (B& r) { a+=r.b; } void main () { A x; x.Ea ()=1; B y; y.Eb ()=2; x.add (y); swap (&x,y); printf ("%d, %d\n", x.Ea(), y.Eb()); //输出2,3
说明:类B的友元函数的声明格式为: class B{ friend void swap(A *p,B& r); //全局函数swap是B类和A类的的友元函数 friend void A:: add(B& r); }; //A类的成员函数add是B类的友元函数 其中值得关注的是当前类的类名B出现在友元函数的形 参类型中,形参r可通过圆点运算符访问B类中的任一成员。 同理全局函数swap (A *p,B& r)是类A的友元函数,这 个函数的A*型形参p可以通过箭头运算符->访问A类的所有 成员包括私有成员。
友元函数或友元类由friend声明,但友元函数或友元类 友元函数的定义应位于当前类的描述之后,以便编译器 必须有足够的信息操作当前的成员。 友元函数的调用维持原来的格式,即全局友元函数按照 全局函数的虚实结合匹配,成员友元函数则根据自身类的对 象外部调用或自身类的this指针内部调用。 另一个全局函数PublicB (S *p,B& r)由于不是类B的友 元函数,B&型的引用形参r只能访问公共的成员,妨碍了数 据的流通。 为解决这种现象可以声明这个全局函数为B类的友元函 数,通知编译器跳过成员属性访问控制检查的机制,抹平精 细封装的界限。或者将B类的数据处理为公共的属性。
2. 友元类 前面说明了友元函数在某一个类中的特权地 位,在友元函数中当前类不过是一个毫不设防的公 共世界,关键字protected和private形如虚设。 一个类可以声明另一个类为其友元,由此形成 友元类。 友元类中的所有成员函数可以访问当前类所有 的成员。
#include <stdio.h> [例] 友元类B将当前类A视为一个公共访问控制属性的结构 #include <stdio.h> class B; class A { friend class B; private: int m_a; A (int k=0) {m_a=k;} public: friend void g_f(); void Show() { printf ("m_a=%d\t", m_a); } };
class B { int m_b; A a; public:B (int k=1) { m_b=k; a.m_a=1+k; } A& Add(A* ); A& B::Add (A* p) { p->m_a+= a.m_a+m_b; return *p; } A* pa; void g_f () { A a; a.m_a=1; a.Show(); static A d(100); pa=&d; } void main() { g_f (); B b; b.Add (pa).Show (); } //输出:m_a=1,m_a=103
说明: 嵌入对象成员a的行为由于类B是类A的友元类而好像类 A回归到传统的公共的结构,访问控制属性这些与解决具体 问题无关的桎梏消失不起作用。 全局函数g_f()是类A的友元函数,在这个友元函数里调 用私有的构造函数,完成对象的定义。 通过一个全局指针pa与静态局部对象d的关联完成数据 封装与共享。
请打开"第16章(2).ppt"