C/C++程序设计
1
一、浅拷贝和深拷贝二、只读成员函数三、友元 friend
C/C++程序设计
2
一、浅拷贝和深拷贝对象作为数据的集合,其中有些数据需要与变量进行流通,有些数据需由不同的对象共享。
本章介绍的深拷贝、友元函数、静态成员和指向成员的指针就是在不同的方面加快信息流动以及实施对象的保护的。
C/C++程序设计
3
存在两种形式的类。
一种形式的类中仅存在变量或对象,不具备指针成员。缺省的拷贝构造函数和赋值运算符函数是浅拷贝的方式,该方式通过 memcpy函数将源实例的数据复制给目标实例占住的一片内存空间。
对于这样的类,缺省的浅拷贝方式是安全的。
另一种形式的类含有指针成员,浅拷贝不再胜任这样的类。
C/C++程序设计
4
考虑如下说明:
从上可见对于存在指针成员的类,系统提供的浅拷贝导致指针指向的内存为两个对象共享的格局。
一个 CDeep类的声明和对象定义
a.n a.p a.p=new int[a.n] b.n b.p b.p=new int[b.n]
=newint[b.n]a.n a.p a.p=newint[a.n] b.n b.p
对象 a,b的内存和指针成员动态扩展的内存空间 b=a导致
[ b.p=a.p; b.n=a.n; ]。指针 b.p指向 a对象的动态内存。
classCDeep{ int n; int*p; } a,b;
中间深资源归口两个对象监控
C/C++程序设计
5
浅拷贝的不良结果是,b.p原先指向的堆空间悬空 ----既无法索引也不能收回这片内存,a或 b对象的析构函数诱发中间共享的深资源的流失。
对于凡是具有指针成员的类,应细致提交两个函数:
一,是拷贝构造函数,
二,是赋值运算符函数,以便进行指针成员的动态资源的深拷贝。
深拷贝的核心思路是:
1,目标对象与源对象内存空间独立,相应指针成员指向的内存空间也彼此独立。
2,全部拷贝源对象的数据到目标对象,包括分别拷贝指针成员指向的内存数据。
C/C++程序设计
6
[例 ] 深拷贝方式(去掉定制的拷贝构造函数和赋值运算符函数则变成缺省的浅拷贝)
#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);
};
C/C++程序设计
7
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;
}
C/C++程序设计
8
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]);
}
C/C++程序设计
9
程序输出,
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]就等于在没有定义的内存空间进行寻址访问,因而是运行时的错误。
C/C++程序设计
10
二、只读成员函数 volatile,mutable关键字
1,只读成员函数未经 const或 volatile限制的对象或成员函数是普通的对象或普通的成员函数,依称为对象或成员函数。
只读对象是关键字 const限定的对象。只读成员函数是
const置于成员函数右圆括号之后修饰的成员函数,该成员函数不修改成员变量的数据状态,即成员函数体中出现的数据成员仅作为右值。
没有只读的构造函数和析构函数,只读对象和对象都调用同一构造函数和析构函数。
C/C++程序设计
11
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 {;,..;}
成员函数可存在两个重载版本:
一个只读版本另一个普通版本。
当两种版本并存时,对象优先调用普通的成员函数,只读对象或只读的对象引用仅仅调用只读成员函数。
C/C++程序设计
12
对象可调用只读成员函数。关键字 const本质上约束
this形参为只读属性。
只读指针形参可以匹配左值区的地址,普通指针形参不指向右值区的地址。
只读对象的地址是右值区的地址。
只读对象和只读成员函数不调用普通成员函数。普通的成员函数可调用只读成员函数。
只读成员函数是只读取对象的数据状态的成员函数。
C/C++程序设计
13
[例 ] 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;
}
C/C++程序设计
14
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
C/C++程序设计
15
成员函数 TopLeft存在只读的和非只读的版本,其中的语句是一样的。
返回引用的函数可以作为左值,本来只读成员函数不改变对象的数据状态,如果不在返回类型上加 const限制将导致对象外部调用对数据的改变。
于是返回引用的只读成员函数在返回类型上由 const前置限制。
d.TopLeft ()为只读对象 d调用只读成员函数。
r.TopLeft ()为对象 r调用普通的成员函数。
C/C++程序设计
16
2,volatile关键字
volatile关键字表示内存数据的变更。
volatile关键字和 const关键字的语法是一致的。
const修饰的变量维持恒定或函数不改变相关成员的数据状态。
与 const相反,volatile关键字限定的对象或成员函数可以有效的变动。
这种变动可以来自其它的外部进程。
C/C++程序设计
17
[例 ] 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); }
};
C/C++程序设计
18
void main()
{ const B c;
c.Show();
volatile B v(2);
v.Show();
v.Set(3);
v.Show();
B x(4);
x.Show();
}
/*程序输出结果,*/
/*Show() const; n=1 */
/*Show() volatile; n=2*/
/*Show() volatile; n=3 */
/*Show() ; n=4 */
C/C++程序设计
19
volatile对象操作 volatile成员函数,const对象操作
const 成员函数。不妨认为 volatile关键字和 const关键字是一对含意相反的语法修饰词,它们常称为 c-v限定词。
对象既可操作 const成员函数也可操作 volatile成员函数,如果这两个成员函数都存在但不存在普通的版本则导致调用的歧义。 volatile关键字使用的场合在一般程序中不多,主要用在系统的程序设计中。
volatile关键字可以和 const同时出现。如下:
volatile const int g_n=1;
这表示程序不能改变变量 g_n的值,但允许系统改变它。
C/C++程序设计
20
3,mutable关键字关键字 mutable可以局部松动 const对象的不变属性。
如果一个对象前加上 const关键字限制,则这个对象所有的成员就冻结为右值。
但有时候对于这种约束期望有所放松,此时只需在相关的成员前冠以 mutable修饰,通知编译器如此成员不受
const的制约。
C/C++程序设计
21
[例 ] 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};
C/C++程序设计
22
上面的代码中 v是一个绝对可变的成员。
n是一个相对可变的成员,n受 const对象限制时而间接的成为右值表达式。
对于本题 cobj是一个不变对象,因而 cobj.n不能成为左值,v由于有了 mutable的特殊关照,cobj.v则构成左值表达式。
C/C++程序设计
23
三、友元 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 在使用点之前应首先说明
C/C++程序设计
24
friend关键字无视 protected,private的制约,放置在类声明的任何地方是等效的。
友元可访问当前类的所有成员。友元类所有的成员函数都是友元函数。
友元类或友元函数一般通过三个途径访问当前类所有的成员,包括公共属性的保护属性的和私有属性的成员:
a,友元函数的入口形参具有当前类的类名。
b,友元函数具有当前类的对象作为局部对象或对象指针作为局部指针。
c,友元类具有当前类的对象作为其嵌入成员对象。
C/C++程序设计
25
1,友元函数友元函数是一种定义在当前类外部的普通函数,通过
friend在当前类内部声明,但友元函数不是当前类的成员函数 。
在当前类的友元函数获得如此特权:
定义在友元函数形参列表或函数体中的当前类对象,对象引用或对象指针等可以访问该类所有控制属性的成员包括私有成员,就好像当前类三个访问控制属性全部是公共的访问控制属性 。
C/C++程序设计
26
友元是一种解除访问控制约束的机制 。
此种破除访问控制约束的友元关系不传递不反身不继承,即如果类 A是类 B的友元,类 B是类 C的友元,A不会自动称为类 C的友元;
类 A是类 B的友元但不一定类 B是类 A的友元;
类 A是类 B的友元,类 A的派生类 D不会自动成为类 B的友元 。
友元函数是某个类的特权函数,这个访问当前类所有成员的特权在该类中由 friend声明获得,未由 friend声明的函数操作当前类的公共成员 。
C/C++程序设计
27
[例 ] 友元函数将类回归到经典的公共访问性质的结构
#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; }
};
C/C++程序设计
28
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
}
C/C++程序设计
29
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
C/C++程序设计
30
说明,类 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类的所有成员包括私有成员。
C/C++程序设计
31
友元函数或友元类由 friend声明,但友元函数或友元类在当前类外的定义不使用关键字 friend。
友元函数的定义应位于当前类的描述之后,以便编译器必须有足够的信息操作当前的成员。
友元函数的调用维持原来的格式,即全局友元函数按照全局函数的虚实结合匹配,成员友元函数则根据自身类的对象外部调用或自身类的 this指针内部调用。
另一个全局函数 PublicB (S *p,B& r)由于不是类 B的友元函数,B&型的引用形参 r只能访问公共的成员,妨碍了数据的流通。
为解决这种现象可以声明这个全局函数为 B类的友元函数,通知编译器跳过成员属性访问控制检查的机制,抹平精细封装的界限。或者将 B类的数据处理为公共的属性。
C/C++程序设计
32
2,友元类前面说明了友元函数在某一个类中的特权地位,在友元函数中当前类不过是一个毫不设防的公共世界,关键字 protected和 private形如虚设。
一个类可以声明另一个类为其友元,由此形成友元类。
友元类中的所有成员函数可以访问当前类所有的成员。
C/C++程序设计
33
[例 ] 友元类 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); }
};
C/C++程序设计
34
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
C/C++程序设计
35
说明:
嵌入对象成员 a的行为由于类 B是类 A的友元类而好像类
A回归到传统的公共的结构,访问控制属性这些与解决具体问题无关的桎梏消失不起作用。
全局函数 g_f()是类 A的友元函数,在这个友元函数里调用私有的构造函数,完成对象的定义。
通过一个全局指针 pa与静态局部对象 d的关联完成数据封装与共享。
C/C++程序设计
36