再读C++ Primer
第十三章:复制控制
复制构造函数是一种特殊构造函数,具有单个形参,该形参是对该类类型的引用。当定义一个新对象并用一个同类型的对象对它进行初始化时,将显式使用复制构造函数。当将该类型的对象传递给函数或从函数返回该类型的对象时,将隐式使用复制构造函数。
析构函数是构造函数的互补:当对象超出作用域或动态分配的对象被删除时,将自动应用析构函数。析构函数可用于释放对象时构造或在对象的生命期中所获取的资源。不管类是否定义了自己的析构函数,编译器都自动执行类中非static数据成员的析构函数。
赋值操作符:赋值操作符可以通过指定不同类型的右操作数而重载。右操作数为类类型的版本比较特殊:如果我们没有编写这种版本,编译器会为我们合成一个。
赋值构造函数、赋值操作符和析构函数总称为复制控制。编译器自动实现这些操作,但类也可以定义自己的版本。
有一种特别常见的情况需要类定义自己的复制控制成员:类具有指针成员。
复制构造函数:
只有单个形参,而且该形参是对本类类型对象的引用(常用const修饰),这样的构造函数称为复制构造函数。与默认构造函数一样,复制构造函数可由编译器隐式调用。
常用于一下几种情况:
1.根据另一个同类型的对象显式或隐式初始化一个对象。
初始化支持两种形式:直接初始化和复制初始化。复制初始化使用=符号,而直接初始化将初始化式放在圆括号中。
直接初始化直接调用与实参匹配的构造函数,复制初始化总是调用复制构造函数。复制初始化首先使用指定构造函数创建一个临时对象,然后复制构造函数将那个临时对象复制到正在创建的对象。
通常不支持复制的类型(比例IO)或者使用非explicit构造函数(阻止构造函数隐式调用)的时候,不能用复制构造函数。
2.复制一个对象,将它作为实参传递给一个函数
正如我们所知,当形参是非引用类型时,将复制实参的值。类似地,以非引用做类型返回时,将返回return语句中的值的副本。
当形参或返回值为类类型时,由复制构造函数进行隐式复制。
3.从函数返回时复制一个对象
4.初始化顺序容器中的元素
5.根据初始化式列表初始化数组元素
合成复制构造函数:
与合成的默认构造函数不同,即使我们定义了其他的构造函数,也会合成赋值构造函数。合成复制构造函数的行为是,执行逐个成员初始化,将新对象初始化为原对象的副本。所谓逐个成员,指的是编译器将现有对象的每个非static成员,一次复制到正在创建的对象。对于数组,给每个数组成员赋值。
定义自己的复制构造函数:
虽然也可以定义接受非const引用的复制构造函数,但形参通常是一个const引用。因为用于向函数传递对象和从函数返回对象,所以该构造函数一般不应该设置为explicit。
有些类必须对赋值对象时发生的事情加以控制。这样的类经常有一个数据成员是指针,或者成员表示在构造函数中分配的其他资源。而另一些类在创建新对象时必须做一些特定的工作。这两张情况下都必须定义复制构造函数。
禁止复制:
有些类需要完全禁止复制,例如iostream类不允许复制。如果想要禁止复制,似乎可以省略复制构造函数,然后不定义赋值构造函数,编译器会自动合成一个。
为了防止复制,类必须显式声明复制构造函数为private。如复制构造函数是私有的,将不允许用户代码复制该类类型的对象,编译器将拒绝任何进行复制的尝试。
然而,类的友元和成员仍可以进行复制。如果想禁止友元和成员中复制,可以声明一个复制构造函数但不对其定义。声明而不定义成员函数是合法的,但是使用未定义成员的任何操作都将导致链接失败。通过声明但不定义private复制构造函数,可以禁止任何复制类类型
如果定义了复制构造函数,那么最好也定义默认构造函数。因为只有不存在其他构造函数时才合成默认构造函数。
赋值操作符:
与复制构造函数一样,如果没有定义自己的赋值操作符,则编译器会合成一个。
大多数操作符可以定义为成员函数或非成员函数。当操作符为成员函数时,它的第一个操作数隐式绑定到this指针。有些操作符(包括赋值操作符)必须是定义自己的类的成员。因为赋值必须是类的成员,所以this绑定到指向左操作数的指针。因此赋值操作符接收单个形参,且该形参是同一类类型的对象。右操作数一般作为const引用传递。
赋值操作符的返回类型应该与内置类型赋值运算返回值相同。内置类型的赋值运算返回对右操作数的引用,因此赋值操作符也返回同一类类型的引用。
合成的赋值操作符:
与合成复制构造函数一样,他也会执行逐个成员赋值。对于数组,给每个数组成员赋值。
复制和赋值常一起使用。
析构函数;
构造函数的一个用途是自动获取资源。例如:构造函数可以分配一个缓冲区或打开一个文件,在构造函数中分配了资源之后,需要一个对应操作自动回收或释放资源。析构函数就是这样一个的特殊函数,它可以完成所需资源的资源回收。
当对象的引用或指针超出作用域时,不会运行析构函数。只有删除指向动态分配对象的指针或实际对象(而不是对象的引用)超出作用域时,才会运行析构函数。
撤销一个容器时,也会运行容器中的类类型元素的析构函数。容器中的元素总是按逆序撤销:首先撤销size()-1的元素,直到撤销下标为0的元素。
何时编写显式的析构函数:
如果类需要析构函数,则他也需要赋值操作符和赋值构造函数。
合成析构函数:
编译器总会为我们合成一个析构函数。合成析构函数按对象创建时的逆序撤销每个非static成员,因此,它按成员在类中声明的次序的逆序撤销成员。
析构函数没有返回值也没有形参。因为不能指定任何形参,所以不能重载析构函数。
析构函数与复制构造函数或赋值操作符之间的重要区别:即使我们编写了自己的析构函数,合成析构函数仍然运行。
管理指针成员:
包含指针的类需要特别注意复制控制,原因是复制指针只复制指针中的地址,而不会复制指针指向的对象。
将一个指针复制到另一个指针时,两个指针指向同一对象。当两个指针指向同一对象时,可能使用任一指针改变基础对象。类似地,很可能一个指针删除了一个对象,另一个指针的用户还认为基础对象仍然存在。
大多数C++成员采用下面三种方法管理指针成员:
1.指针成员采取常规指针型行为。这样的类具有指针的所有缺陷单无需特殊的复制控制。
2.类可以实现所谓的智能指针行为。指针所指向的对象时共享的,但类能防止悬垂指针。
3.采取值行为。指针所指向的对象时唯一的,有每个类对象独立管理。
1.指针成员采取常规指针型行为。
class HasPtr{
public:
HasPtr(int *p, int i):ptr(p),val(i){}
int *get_ptr() const{return ptr;}
int get_int() const{return val;}
void set_ptr(int *p){ptr = p;}
void set_int(int i){val = i;}
int get_ptr_val() const {return *ptr;}
void set_ptr_val() const {*ptr = val;}
private:
int *ptr;
int val;
}
指针共享一个对象。如:
int *ip = new int(42);
HasPtr ptr(ip,10);
delete ip;
ptr.set_ptr_val(0); //这里的问题是ip和ptr中的指针指向同一个对象。删除了该对象时,ptr中的指针不在指向有效的对象,出现悬垂指针。
2.定义智能指针类:本例中让智能指针删除共享对象。用户将动态分配一个对象并将给对象的地址传给新的HasPtr类。用户仍然可以通过普通指针访问对象,但绝不删除指针。HasPtr类将保证在撤销指向对象时删除对象。定义智能指针通用技术是采用一个使用计数。智能指针将yield计数器与类指向的对象相关联。使用计数跟踪该类有多少个对象共享同一指针。当计数为0时,删除对象。其他方面和普通指针一样,具体而言复制对象时也将指向同一个基础对象。
class U_Ptr{
friend class HasPtr;
int *ip;
size_t use;
U_Ptr(int *p):ip(p),use(1){}
~U_Ptr(){delete ip;}
}
这个类的所有成员均为private,我们不希望普通用户使用U_Ptr类,所以它没有任何public成员。将HasPtr类设置为友元,使其成员可以访问U_Ptr的成员。U_Ptr的构造函数复制指针,而析构函数删除指针。构造函数将使用计数值为1,表示HasPtr对象指向这个U_Ptr对象。
class HasPtr{
public:
int *get_ptr() const {return ptr->ip;}
int *get_int const{return val;}
void set_ptr(int *p){ptr->ip = p;}
void set_int(int i){val = i;}
int get_ptr_val()const{return *ptr->ip;}
void set_ptr_val(int i){*ptr->ip = i;}
HasPtr(int *p,int i):ptr(new U_Ptr(p)),val(i){}
HasPtr(const HasPtr &orig):ptr(orig.ptr),val(orig.val){++ptr->use;}
HasPtr& operator = (const HasPtr&);
~HasPtr(){if(--ptr-use == 0) delete ptr;}
private:
U_Ptr *ptr;
int val;
}
HasPtr& HasPtr::operator=(const HasPtr &rhs)
{
++rhs.ptr->use;
if(--ptr->use == 0)
delete ptr; //赋值运算法,先删除自己原来的值,在赋值,所以总体使用指针的个数不增加。先++rhs.ptr->use保证自身赋值正确运行。
ptr=rhs.ptr;
val = rhs.val;
return *this;
}
为了管理指针成员的类,必须定义三个复制控制成员:赋值构造函数,赋值操作符和析构函数。
3.定义值型类:复制值型对象时,会得到一个不同的新副本。
class HasPtr{
public:
HasPtrr(const int &p,int i):ptr(new int(p)),val(i){}
HasPtr(const HasPtr&orig):ptr(new int (*orig.ptr)),val(orig.val){}
HasPtr& operator=(const HasPtr&);
~HasPtr(){delete ptr;}
int get_ptr_val() const{return *ptr;}
int get_int() const {return val;}
void set_ptr(int *p){ptr = p;}
void set_int(int i){val = i;}
int *get_ptr() const {return ptr;}
int set_ptr_val(int p) const{*ptr = p;}
private:
int *ptr;
int val;
}
HasPtr:: HasPtr::operator=(const HasPtr&rhs)
{
*ptr = *rhs.ptr;
val=rhs.val;
return *this;
}
复制构造函数不再复制指针,它将分配一个新的int对象,并初始化对象以保存与被复制对象相同的值。每个对象都保存属于自己的int值的不同副本。因为每个对象保存自己的副本,所以析构函数将无条件删除指针。
第十四章:重载操作符与转换
重载操作符必须具有至少一个类类型或枚举类型的操作数。这条规则强制重载操作符不能重新定义用于内置类型对象的操作符含义。
除了函数调用操作符operator()之外,重载操作符时使用默认实参是非法的。
作为成员函数的操作符有一个隐含的this形参,限定为第一个操作数,故形参看起来比操作数数目少1.
重载一元操作符作为成员函数就没有(显式)形参,如果作为非成员函数就有一个形参。
一般将算术和关系操作符定义为非成员函数,而将赋值操作符定义为成员。
操作符定义为非成员函数时,通过必须将他们设置为所操作类的友元,因为操作符通常需要访问类的私有部分。
选择成员或非成员实现操作符重载指导原则:
1.赋值,下标,函数调用,成员访问箭头等操纵必须定义为成员。
2.复合赋值操作符通常定义为成员函数
3.改变对象状态或与给定类型紧密联系的其他操作符,如自增,自减,解引用等通常定义为类成员。
4.对称的操作符,如算术操作付,相等操作服务,关系操作符和位操作符最好定义为非成员函数。
输入和输出操作符:
输出操作符重载:
为了保持与IO标准库一致,操作符应该接受ostream&作为第一个形参,对类类型const对象的引用作为第二个形参,并返回读ostream形参的引用。
简单的定义:
ostream& operator << (ostream &os,const ClassType &boject)
{
os <<.....
return os;
}
第一个形参是对ostream对象的引用,在该对象上将产出输出。ostream为非const,因为写入到流会改变流的状态。该形参是一个引用,因为不能复制ostream对象。
第二个形参一般应该是对输出的类类型的引用。该形参是一个引用以避免复制实参。他可以是const,因为输出一个对象不应该改变该对象。使形参成为const引用,就可以使用同一个定义来输出const和非const对象。
返回类型是一个ostream引用。
IO操作符必须定义为非成员函数。否则,左操作数将只能是该类类型的对象。
如 item << cout; 这与标准库定义的使用方式相反。
IO操作符通常对非公有数据成员进行读写,因此,类通常将IO操作符设为友元。
输入操作付>>的重载:
与输出操作符类似,输入操作符的第一个形参是一个引用,指向他要读取的流,并且返回的也是对同一个流的引用。它的第二个形参是对要读入的对象的非const引用,该形参必须为非const,因为输入操作符的目的是将数据读到这个对象中。
更重要的是输入操作符必须处理错误和文件结束的可能性。
如: istream& operator>>(istream &in,Sales_item &s)
{
double price;
in >> s.isbn >> s.units >> price;
if(in)
s.revenue = s.units * price;
else
s=Sales_item(); //input failed:reser object to default
return in;
}
算术操作符和关系操作符:一般而言将算术和关系操作符定义为非成员函数。
Sales_item operator+(const Sales_item &lhs,const Sales_item &rhs)
{
Sales_item ret(lhs);
ret += rhs;
return ret;
}
为了与内置操作符保持一致,加法返回一个右值,而不是一个引用。
相等操作符:
inline bool operator==(const Sales_item &lhs, cosnt Sales_item &rhs)
{
return lhs.units_sold == rhs.units_sold && lhs.revenue == rhs.revenue &&
lhs.sme_isbn(rhs);
}
inline bool operator!=(const Sales_item &lhs, cosnt Sales_item &rhs)
{
return !(lhs == rhs);
}
赋值操作符:赋值必须返回对*this的引用
一般而言,赋值和复合赋值操作符应返回左操作数的引用。
下标操作符:下标操作符必须定义为类成员函数
一般需要定义两个版本:一个非const成员并返回引用,另一个为const成员并返回const引用。
class Foo{
public:
int &operator[] (const size_t);
const int &operator[] (const size_t) const;
private:
vector<int> date;
}
int& Foo::operator[] (const size_t index)
{
return data[index];
}
const int& Foo::operator[](const size_t index)const
{
return data[index];
}
成员访问操作符:C++语言允许重载解引用和箭头操作符
箭头操作符必须定义为类成员函数。解引用不要求为成员函数,但一般将它作为成员函数也是正确的。
解引用操作符和箭头操作符常用在实现智能指针的类中。作为例子,假定想要定义一个类类型表示指向编写Screen类型对象的指针,将该类命名为ScreenPtr。ScreenPtr的用户将会传递一个指针,该指针指向动态分配的Screen。ScreenPtr类将对其指针进行计数。我们定义一个伙伴类来保存指针及其相关使用计数。
Class ScrPtr{
friend class ScreenPtr;
Screen *sp;
size_t use;
ScrPtr(Screen *p):sp(p),use(1){}
~ScrPtr(){detele sp;}
}
class ScreenPtr{
ScreenPtr(Screen *p):ptr(new ScrPtr(p)){}
ScreenPtr(const ScreenPtr &org):ptr(org.ptr){++ptr->use;}
ScreenPtr &operator=(const ScreenPtr&);
~ScreenPtr() {if(--ptr->use==0) delete ptr;}
private:
ScrPte *Ptr;
}
使用重载箭头:
ScreenPtr p(&myScreen);
p->display(cout); //因为p是一个ScreenPtr对象,p->display的含义与对(p.operator->())->display求值相同。对p.operator->()求值将调用ScreenPtr类的operator->,它的返回值指向Screen对象的指针,该指针用于获取并运行ScreenPtr所指向对象的display成员。
重载箭头操作符必须返回指向类类型的指针,或者返回定义了自己的箭头操作符的类类型对象。
自增操作符和自减操作符:
自增和自减操作符经常由诸如迭代器这样的类实现。
class CheckedPtr{
public:
CheckedPtr(int *b,int*e):beg(b),end(e),curr(b){}
CheckPtr &operator++(); //prefix operator
CheckPtr &operator--();
CheckPtr operator++(int); //postfix operator同时为前置和后置操作符重载,但他们在标准库中定义的参数数目和类型相同。普通重载不能区别定义是前置还是后置。所以后缀操作符函数接受一个额外的int型参数。使用后缀操作符时,编译器提供0作为这个形参的实参
CheckPtr operator--(int); //后缀应该返回旧值,并且应作为值返回,不应该是返回引用。
private:
int *beg;
int *end;
int *curr;
}
Checked &CheckedPtr::operator++()
{
if(curr==end)
throw out_of_range("increment past the end of CheckPtr");
++curr;
return *this;
}
Checked &CheckedPtr::operator--()
{
if(curr==beg)
throw out_of_range("decrement past the beginning of CheckPtr");
--curr;
return *this;
}
Checked CheckedPtr::operator++(int)
{
CheckedPtr ret(*this);
++*this;
return ret;
}
Checked CheckedPtr::operator--(int)
{
CheckedPtr ret(*this);
--*this;
return ret;
}
显示调用前缀和后缀
CheckedPtr parr(ia,ia+size);
parr.operator++(0);
parr.operator++();
调用操作符和函数操作对象:
struct absInt{
int operator()(int val){
return val <0 ? -val:val;
}
}
用法:
int i=-42;
absInt absObj;
unsigned int ui = absObj(i); //判断i的正负
函数调用操作符必须声明为成员函数。一个类可以定义函数调用操作符的多个版本,有形参的数目或类型加以区别。
定义了调用操作符的类,其对象称为函数对象,即它们是行为类似的函数的对象。
第十五章 面向对象编程
面向对象编程基于三个概念:数据抽象、继承、多态
在C++中基类必须指出希望派生类中定义哪些函数,定义为virtual的函数时基类期待派生类重新定义的,基类希望派生类继承的函数不能定义为虚函数。
在C++中,通过基类的引用(或指针)调用虚函数时,发生动态绑定。引用(或指针)既可以指向基类对象也可以指向派生类对象,这一事实是动态绑定的关键。用引用(或指针)调用的虚函数在运行时确定,被调用的函数是引用(或指针)所指对象的实际类型所定义的。
定义基类:
对非虚函数的调用在编译时确定。为了指明函数为虚函数,在其返回类型前面加上保留字virtual。除了构造函数之外,任意非static成员函数都可以是虚函数。保留字virtual只在类内部的成员函数声明中出现,不能用在类定义体外部出现的函数定义上。
基类通常将派生类需要重定义的任意函数定义为虚函数。
如: class Item_base{
public:
Item_base(const std::string &book = " ", double sales_price = 0.0):isbn(book),price(sales_price){}
std::string book() const {return isbn;}
virtual double net_price(std::size_t n) const {return n * price;}
virtual ~Item_base(){}
private:
std::string isbn;
protected:
double price; //因为派生类需要更改price
}
protected成员:
1.像private一样,protected成员不能被类的用户访问
2.像public一样,protected成员可被该类的派生类访问
3.派生类只能通过派生类对象访问基类的protected成员,派生类对基类类型对象的protected成员没有特殊访问权限。
例如:Bulk_item是Item_base的派生类
void Bulk_item::memfun(const Bulk_item &d, const Item_base &b)
{
double ret = price; //ok,use this -> price
ret = d.pricce; //ok,使用派生类对象作为形参的protected成员
ret = b.price; //error,不能使用基类对象作为形参的protected成员
}
派生类:
class Bulk_item : public Item_base{
public:
double net_price(std::size_t)const;
private:
std::size_t min_qty; //至少购买min_qty才能打折
double discount; //折扣
}
double Bulk_item::net_price(size_t cnt) const
{
if (cnt > min_qty)
return cnt * price *(1-discount);
else
return cnt * price;
}
派生类一般会重定义所继承的虚函数。如果派生类没有重定义某个虚函数,则使用基类中定义的版本。
派生类必须对想要重定义的每个继承成员进行声明。Bulk_item类指出,它将重定义net_price函数但将使用book的继承版本
派生类中虚函数的声明必须与基类的定义方式完全匹配,但有一个例外:返回对基类的引用(或指针)的虚函数。派生类中的虚函数可以返回基类所返回类型派生类的引用(或指针)。
例如,Item_base类可以定义返回Item_base*的虚函数,如果这样,Bulk_item类中定义的实例可以定义为返回Item_base*h或Bulk_item*。
一旦函数在基类中被声明为虚函数,他就一直为虚函数,派生类无法改变函数为虚函数这一事实。派生类重定义虚函数时,可以使用virtual保留字,但不是必须这样做。
已定义的类可以用作基类,只声明未定义的类不能用作基类。
如果需要声明(但不实现)一个派生类,则声明包含类名但不包含派生列表
如 class Bulk_item : pubic Item_base; //error
class Bulk_item; //OK
C++中要触发动态绑定必须满足两个条件:
1.只有指定虚函数的成员才能进行动态绑定
2.必须通过基类类型的引用或指针进行函数调用
基类类型的引用或指针可以引用基类类型对象,也可以引用派生类型对象,所以对象的实际类型可能不同于该对象引用或指针的静态类型。
任何可以在基类对象上执行的操作也可以通过派生类对象使用。因为派生类对象用于基类部分。
引用和指针的静态类型和动态类型可以不同。但对象是非多态的,对象类型已知且不变。对象的动态类型总是与静态类型相同,这一点与引用或指针相反。
通过引用或指针调用虚函数时,编译器将生成代码,在运行时确定调用哪个函数,被调用的是与动态类型相对应函数。如果调用的非虚函数,则无论实际对象是什么类型,都执行基类类型所定义的函数。
不管动态类型是基类还是派生类的引用或指针,如果静态类型是基类,那么对book的调用总会调用Item_base的book(即使引用的是派生类对象,并且派生类中定义了book函数)
覆盖虚函数机制:某些情况下,希望覆盖虚函数机制并强制调用使用虚函数的特定版本,这是可以使用作用域操作符
Item_base *baseP = &derived;
double d = baseP->Item_base::net_price(42);
虚函数与默认实参:
虚函数也可以有默认实参。通过基类的引用或指针调用虚函数时,默认实参为在基类虚函数声明中的值,如果通过派生类的指针或引用调用虚函数,则默认实参是在派生类的版本中声明的值。
在同一虚函数的基类版本和派生类版本中使用不同的默认实参几乎一定会引起麻烦。如果通过基类的引用或指针调用虚函数,但实际执行的是派生类中定义的版本,这时就可能会出现问题。在这种情况下,为虚函数的基类版本定义的默认实参将传给派生类定义的版本,而派生类版本是用不同默认实参定义的。
公有、私有和受保护的继承:
基类本身指定对自身成员的最小访问控制。如果成员在基类中为private,则只有基类和基类的友元才可以访问该成员。派生类不能访问基类的private成员,也不能使自己的用户能够访问那些成员。如果基类成员为public或protected,则派生列表中使用的访问标号决定该成员在派生类中的访问级别:
如果是公用继承,基类成员保持自己的访问级别:基类的public成员为派生类的public成员,基类的protected成员为派生类的protected成员
如果是受保护继承,基类的public和protected成员在派生类中为protected成员
如果是私有继承,基类的所有成员在派生类中为private成员
Class Base{
public:
std::size_t size() const {return n;}
protected:
std::size_t n;
}
class Derived : private Base{...}
class Derived : private Base{
public:
using Base::size; //size仍为public
protected:
using Base::n; //n为protected
}
友元关系与继承:
友元不能继承。基类的友元对派生类的成员没有特殊访问权限。如果基类被授予友元关系,则只有基类具有特殊访问权限,该基类的派生类不能访问友元关系的类。
继承与静态成员:
如果成员在基类中为private,则派生类不能访问它。假定可以访问成员,则既可以通过基类访问static成员,也可以通过派生类访问static成员。一般而言,既可以使用作用域操作符也可以使用点或箭头成员访问操作符。
如: struct Base{
static void statmen();
};
struct Derived : Base{
void f(const Derived&);
};
void Derived::f(const Derived &derived_obj)
{
Base::statmen();
Derived::statmen();
derived_obj.statmen();
statmem();
}
转换与继承:
派生类到基类的转换:
1.引用转化不同于转换对象
将派生类的对象传递给希望接受基类引用的函数时,引用直接绑定到该对象,虽然看起来在传递对象,实际上实参是对该对象的引用,对象本身未被复制,并且,转换不会在任何方面改变派生类对象,该对象仍是派生类型对象。
将派生类对象传递给希望接受基类类型对象(而不是引用)的函数时,情况完全不同。在这种情况下,形参的类型是固定的——在编译时和运行时形参都是基类类型对象。如果用派生类型对象调用这样的函数,则该派生类对象的基类部分被复制到形参。
一个是将派生类对象转换为基类类型的引用,一个是用派生类对象对基类对象进行初始化或赋值。
2.用派生类对象对基类对象进行初始化或赋值
用派生类对象对基类对象进行初始化或赋值实际上是调用函数:初始化调用构造函数,赋值时调用赋值操作符。
用派生类对象对基类对象进行初始化或赋值时,存在两种可能性。
第一种是基类显式定义将派生类对象复制或赋值给基类对象,不过并不常见。相反基类一般预定义自己的复制构造函数和赋值操作符,这些成员接受一个形参,该形参时基类类型的(const)引用。因为存在从派生类引用到基类的转换,这些复制控制成员可用于从派生类 对象对基类对象进行初始化或赋值。
如: Item_base item;
Bulk_item bulk;
Item_base item(bulk);
item=bulk;
将发生下面的步骤:
1)将Bulk_item对象转换为Item_base引用,这仅仅以为着一个Item_base引用绑定到Bulk_item对象。
2)将该引用作为实参传给复制构造函数或赋值操作符
3)那些操作符使用Bulk_item的Item_base部分分别对调用构造函数或赋值的Item_base对象的成员初始化或赋值。
4)一旦操作符执行完毕,对象即为Item_base。它包含Bulk_item的Item_base部分的副本,但实参的Bulk_item部分被忽略。
派生类到基类转换的可访问性:
假定D继承B:
1.只有当D公有地继承B时,用户代码才能使用派生类向基类的转换;如果D继承B的方式是受保护的或者私有的,则用户代码不能使用该转换。
2.不论D以什么方式继承B,D的成员函数和友元都能使用派生类向基类的转换;派生类向其直接基类的类型转换对于派生类的成员和友元来说是永远可访问的。
3.如果D继承B的方式是公有的或者是受保护的,则D的派生类的成员和友元可以使用D向B的类型转换;反之,如果D继承B的方式是私有的,则D的派生类的成员和友元不能使用D向B的类型转换。
基类到派生类的转换:
从基类到派生类的自动转换是不存在的。需要派生类对象是不能使用基类对象。
如 Item_base base;
Bulk_item *bulkP = &base; //error
Bulk_item &bulkP = base; //error
Bulk_item bulk = base; //error
当基类指针或引用时间绑定到派生类对象时,从基类到派生类的转换也存在限制;
如 Bulk_item bulk;
Item_base *itemP = &bulk;
Bulk_item *bulkP = itemP; //error
编译器在编译时无法知道特定转换在运行实际上是安全的。在这些情况下,如果知道基类到派生类的转换是安全的,就可以使用static_cast强制编译器转换。或者可以用dynamic_cast申请在运行时进行检查。
构造函数和复制控制
每个派生类对象由派生类中定义的成员加上一个或多个基类子对象构造。所有当构造、复制、赋值和撤销派生类型对象时,也会构造、复制、赋值和撤销这些基类子对象。
构造函数和复制控制成员不能继承。
基类构造函数和复制控制:
本身不是派生类的基类,其构造函数和复制控制基本上不受继承影响。但某些类希望派生类使用特殊的构造函数,这样的构造函数应定义为protected。
派生类构造函数:
派生类构造函数受继承关系的影响,每个派生类构造函数除了初始化自己的数据成员之外,还要初始化基类。
1.合成的派生类默认构造函数
除了初始化派生了的数据成员外,还会调用基类的默认构造函数初始化基类部分
对于Bulk_item类,合成的默认构造函数会这样执行
1).调用Item_base的默认构造函数,将isbn成员初始化为空串,将price成员初始化为0
2).用常规变量的初始化规则初始化Bulk_item的成员,也就是说,min_qty和discount成员会是为初始化的。
2.定义默认构造函数
如: class Bulk_item : public Item_base{
public:
Bulk_item() : min_qty(0),discount(0.0){}
};
3.向基类构造函数传递实参
派生类构造函数的初始化列表只能初始化派生类的成员,不能直接初始化继承成员。相反,派生类构造函数通过将基类包含在构造函数初始化列表中来间接初始化继承成员。
class Bulk_item : public Item_base{
public:
Bulk_item(const std::string &book,double sales_price,std::size_t qty = 0,double disc_rate = 0.0):Item_base(book,sales_price),min_qty(qty),discount(dis_rate){}
};
Bulk_item bulk("0-201-8737-1",50,5, 0.19);
构造函数初始化列表为类的基类和成员提供初始值,它并不指定初始化的执行次序。首先初始化基类,然后根据声明次序初始化派生类的成员。
4.在派生类中使用默认实参
class Bulk_item : public Item_base{
public:
Bulk_item(const std::string &book = " ",double sales_price = 0,std::size_t qty = 0,double disc_rate = 0.0):Item_base(book,sales_price),min_qty(qty),discount(dis_rate){}
};
5.只能初始化直接基类
一个类只能初始化自己的直接基类,不能初始化两代和两代以上的基类。
class Disc_item : public Item_base{
public:
Dics_item(const std::string &book = " ",double sales_price = 0.0,
std::size_t qty = 0, double disc_rate = 0.0):
Item_base(book,sales_price),
quantity(qty),discount(disc_rate){}
protected:
std::size_t quantity;
double discount;
};
在定义Bul_item继承Disc_item
class Bulk_item : public Disc_item{
public:
Bulk_item(const std::string& book = " ",
double sales_price = 0.0,
std::size_t qty = 0,double disc_rate =0.0):
Disc_item(book,sales_price,qty,disc_rate){}
double net_price(std::size_t)const;
};
复制控制和继承
1.定义派生类复制构造函数
如果派生类定义了自己的复制构造函数,该复制狗杂函数一般应显式使用基类复制构造函数初始化对象的基类部分
如 class Base{}
class Derived: public Base{
public:
Derived(const Derived& d):
Base(d)
}
初始化函数Base(d)将派生类对象d转换为它的基类部分的引用,并调用基类复制构造函数
如果省略基类初始化函数:
如
class Derived: public Base{
public:
Derived(const Derived& d):
}
奇怪的配置:运行效果是运行Base的默认构造函数初始化对象的 基类部分,而他的Derived成员是另一对象d的副本。
2.派生类赋值操作符:
与复制构造函数类似:如果派生类定义了自己赋值操作符,则该操作符必须对基类部分进行显式赋值。
Derived &Derived::operator=(const Derived &rhs)
{
if(this != this){
Base::operator=(rhs);
//再做对Derived的成员复制;
}
return *this;
}
赋值操作符必须防止自身赋值。假定左右操作数不同,则调用Base类的赋值操作符给基类部分赋值。基类操作符将释放左操作数中的基类部分的值,并辅以来自rhs的新值。该操作符执行完毕后,接着要做的是为派生类中的成员赋值。
3.派生类析构函数
析构函数的工作于复制构造函数和赋值操作符不同。派生类析构函数不负责撤销基类对象的成员。编译器总是显式调用派生类对象基类部分的析构函数。每个析构函数指清除自己的成员。
class Derived:public Base{
public:
~Derived()
};
对象的撤销顺序与构造顺序相反:首先运行类析构函数,然后按继承层次一次向上调用个基类析构函数。
虚析构函数:
如果删除基类指针,则需要运行基类析构函数并清除基类的成员,如果对象实际是派生类型的,则没有定义该行为。要保证运行适当的析构函数,基类中的析构函数必须为虚函数。
如果析构函数为虚函数,那么通过指针调用时,运行哪个析构函数将因指针所指对象类型的不同而不同:
如: Item_base *itemP = new Item_base;
delete itemP;
itemP = new Bulk_item;
delete itemP;
像其他虚函数一样,析构函数的虚函数性质都将继承。因此,如果根类的析构函数为虚函数,则派生类析构函数也将是虚函数,无论派生类显式定义析构函数还是使用合成析构函数,派生类析构函数都是虚函数。
构造函数不能为虚函数,因为构造函数是在对象完全构造之前运行的,在构造函数运行的时候,对象的动态类型还不完整。
构造函数和析构函数中的虚函数:
在基类构造函数或析构函数中,将派生类对象当做基类类型对象对待。如果在构造函数或析构函数中调用虚函数,在运行的是构造函数或析构函数自身定义的版本。比如A继承B,A、B中的构造函数和析构函数都调用析构函数。但在创建A调用B中的构造函数时,B中的构造函数会调用B中的虚函数,而不是A中的虚函数。
继承情况下的类作用域:
在基类和派生类总使用同一名字的成员hanukkah,其行为与数据成员一样:在派生类作用域中派生类成员将屏蔽积累成员。即使函数原型不同,基类成员 也会被屏蔽。
如: struct Base{
int memfcn();
};
struct erived : Basw{
int memfcn(int);
};
Derived d, Base b;
b.memfcn(); //calls Base::memfcn
d.memfcn(10); //calls Derived::memfcn
d.memfcn(); //error,memfcn with no arguments is hidden
d.Base::memfcn(); //ok,calls base::memfcn
如果派生类重定义了重载成员,则通过派生类型只能访问派生类中重定义的那些成员,当然派生类也可以通过基类类型访问基类中重定义的那些成员。如果派生类想通过自身类型使用所有的重载版本,则派生类要么定义所有成员,要么一个也不从定义。
有时类需要仅仅重定义一个重载集中某些版本的行为,并且想要继承其他版本的含义,这种情况下,重定义每个基类版本,可能会令人厌烦。这时,派生类可以为重载成员提供using声明,一个using声明只能制定一个名字,不能指定形参表,。将所有名字加入作用域之后,派生类只需重定义本类型必须定义的那些函数,对其他版本可以使用继承的定义。
虚函数与作用域:
class Base{
public:
virtual int fcn();
};
class D1:public Base{
public:
int fcn(int);
};
class D2:public D1{
public:
int fcn(int);
int fcn();
}
D1中的fcn版本没有重定义Base的虚函数fcn,相反,它屏蔽了基类的fcn。结果D1有两个名为fcn的函数:类从Base继承了一个名为fcn的虚函数,类又定义了自己的名为fcn的非成员函数,该函数接受一个int形参。但是,从Base继承的虚函数不能通过D1对象(或D1的引用或指针)调用,因为该函数被fcn(int)的定义屏蔽了。
类D2重定义了它继承的两个函数,它重定义了Base中定义的fcn的原始版本并重定义了D1中定义的非虚版本。
通过基类调用被屏蔽的虚函数:
Base bobj; D1 d1obj; D2 d2obj;
Base *bp1 = &bobj, *bp2 = &d1obj, *bp3 = &d2obj;
bp1->fcn(); //ok Base::fcn
bp2->fcn(); //ok,Base::fcn
bp3->fcn(); //ok,D2::fcn
纯虚函数:在函数形参表后面加上=0以指定纯虚函数。
class Disc_item : public Item_base{
public:
virtual double net_price(std::siez_t) const = 0;
}
将函数定义为纯虚函数能够说明,该函数为后代类型提供了可以覆盖的接口,但是这个类中的版本绝不会调用。重要的是,用户将不能创建Disc_item类型的对象。
含有(或继承)一个或多个虚函数的类是抽象基类。除了作为抽象基类的派生类的对象的组成部分,不能创建抽象类型的对象。
容器与继承:
我们希望使用容器(或内置数组)保存因继承而相关联的对象。但是,对象不是多态。
multiset<Item_base> basket;
Item_base base;
Bulk_item bulk;
basket.insert(base);
basket.insert(bulk);
则加入派生类的的对象时,只将对象的基类部分保存在容器中。记住,将派生类对象复制到积累对象时,派生类对象将被切掉。
所以不能用容器同时存储基类对象和派生类对象。唯一可行的选择是使用容器保存对象的指针。但代价是需要用户保证容器存在,被指向的对象就存在。如果对象是动态分配的,用户必须保证容器消失时适当的释放对象。
句柄类与继承:
句柄类存储和管理基类指针。因为句柄类使用基类指针执行操纵,虚成员的行为将在运行时根据句柄实际绑定的对象的类型而变化。
包装了继承层次的句柄有两个重要的设计考虑因素:
像对任何保存指针的类一样,必须对复制控制做些什么。包装了继承层次的句柄通常表现的像一个智能指针或像一个值。
句柄类决定句柄接口屏蔽还是不屏蔽继承层次,如果不屏蔽继承层次,用户必须了解和使用基本层次中的对象。
指针型句柄:
我们定义一个名为Sales_item的指针型句柄,表示Item_base层次。Sales_item的用户将想使用指针一样使用它:用户将Sales_item绑定到Item_base类型的对象并使用*和->操作符执行Item_base的操作。
Sales_item item(Bulk_item("0-201-82470-1",35,3, .20));
item -> net_price();
但用户不必管理句柄指向的对象,Sales_item类将完成这部分工作。
1.定义句柄:
Sales_item有三个构造函数:默认构造函数、复制构造函数和接受Item_base对象的构造函数。第三个构造函数将复制Item_base对象,并保证:只要Sales_item对象存在副本就存在。当复制Sales_item对象或给Sales_item赋值时,将复制指针而不是复制对象。像对其他指针型句柄一样,将用使用技术来管理副本。
但不使用伙伴类类存储指针和相关计数。Sales_item类将有两个数据成员,都是指针:一个指针指向Item_base对象,而另一个将指向使用计数。Item_base指针可以指向Item_base对象也可以指向Item_base派生类型的对象。通过指向使用计数,多个Sales_item对象可以共享同一计数器。
除了管理使用技术外,Sales_item类还将定义解引用和箭头操作符:
class Sales_item{
public:
Sales_item():p(0),use(new std::size_t(1)){}
Sales_item(const Item_base&);
Sales_item(const Sales_item &i): p(i.p),use(i.use){++*use;}
~Sales_item(){decr_use();}
Sales__item &operator=(const Sales_item&);
const Item_base *operator ->()const{if(p) return p;
else throw std::logic_error("unbound Sales_item");}
const Item_base &operator*() const{if(p) return *p;
else throw std::logic_error("unbound Sales_item");}
private:
Item_base *p;
std::size_t *use;
void decr_use()
{if(--*use == 0){delete p;delete use;}}
};
Sales_item& Sales_item::operator=(const Sales_item &rhs)
{
++*rhs.use;
derc_use();
p=rhs.p;
use=rhs.use;
return *this;
}
复制未知类型:
要实现Item_base对象的构造函数,必须解决一个问题:我们不知道给予构造函数的对象的实际类型。我们知道他是一个Item_base对象或者是一个Item_base派生类型对象。句柄经常需要在不知道对象的确切类型时分配已知对象的新副本。
为了支持句柄类,需要从基类开始,在继承层次的每个类型中增加clone,基类必须将该函数定义为虚函数:
class Item_base{
public:
virtual Item_base* clone() const
{return new Item_base(*this);}
};
class Bulk_item:public Item_base{
public:
Bulk_item* clone() const
{return new Bulk_item(*this);}
};
然后可以定义句柄构造函数:
Sales_item::Sales_item(const Item_base &item):
p(item.clone()),use(new std::size_t(1)){}
想默认构造函数一样,这个构造函数分配并初始化使用计数,它调用形参的clone产生那个对象的副本。如果实参是Item_base对象,则运行Item_base的clone函数;如果是Bulk_item对象,则执行Bulk_item的clone函数。
第十三章:复制控制
复制构造函数是一种特殊构造函数,具有单个形参,该形参是对该类类型的引用。当定义一个新对象并用一个同类型的对象对它进行初始化时,将显式使用复制构造函数。当将该类型的对象传递给函数或从函数返回该类型的对象时,将隐式使用复制构造函数。
析构函数是构造函数的互补:当对象超出作用域或动态分配的对象被删除时,将自动应用析构函数。析构函数可用于释放对象时构造或在对象的生命期中所获取的资源。不管类是否定义了自己的析构函数,编译器都自动执行类中非static数据成员的析构函数。
赋值操作符:赋值操作符可以通过指定不同类型的右操作数而重载。右操作数为类类型的版本比较特殊:如果我们没有编写这种版本,编译器会为我们合成一个。
赋值构造函数、赋值操作符和析构函数总称为复制控制。编译器自动实现这些操作,但类也可以定义自己的版本。
有一种特别常见的情况需要类定义自己的复制控制成员:类具有指针成员。
复制构造函数:
只有单个形参,而且该形参是对本类类型对象的引用(常用const修饰),这样的构造函数称为复制构造函数。与默认构造函数一样,复制构造函数可由编译器隐式调用。
常用于一下几种情况:
1.根据另一个同类型的对象显式或隐式初始化一个对象。
初始化支持两种形式:直接初始化和复制初始化。复制初始化使用=符号,而直接初始化将初始化式放在圆括号中。
直接初始化直接调用与实参匹配的构造函数,复制初始化总是调用复制构造函数。复制初始化首先使用指定构造函数创建一个临时对象,然后复制构造函数将那个临时对象复制到正在创建的对象。
通常不支持复制的类型(比例IO)或者使用非explicit构造函数(阻止构造函数隐式调用)的时候,不能用复制构造函数。
2.复制一个对象,将它作为实参传递给一个函数
正如我们所知,当形参是非引用类型时,将复制实参的值。类似地,以非引用做类型返回时,将返回return语句中的值的副本。
当形参或返回值为类类型时,由复制构造函数进行隐式复制。
3.从函数返回时复制一个对象
4.初始化顺序容器中的元素
5.根据初始化式列表初始化数组元素
合成复制构造函数:
与合成的默认构造函数不同,即使我们定义了其他的构造函数,也会合成赋值构造函数。合成复制构造函数的行为是,执行逐个成员初始化,将新对象初始化为原对象的副本。所谓逐个成员,指的是编译器将现有对象的每个非static成员,一次复制到正在创建的对象。对于数组,给每个数组成员赋值。
定义自己的复制构造函数:
虽然也可以定义接受非const引用的复制构造函数,但形参通常是一个const引用。因为用于向函数传递对象和从函数返回对象,所以该构造函数一般不应该设置为explicit。
有些类必须对赋值对象时发生的事情加以控制。这样的类经常有一个数据成员是指针,或者成员表示在构造函数中分配的其他资源。而另一些类在创建新对象时必须做一些特定的工作。这两张情况下都必须定义复制构造函数。
禁止复制:
有些类需要完全禁止复制,例如iostream类不允许复制。如果想要禁止复制,似乎可以省略复制构造函数,然后不定义赋值构造函数,编译器会自动合成一个。
为了防止复制,类必须显式声明复制构造函数为private。如复制构造函数是私有的,将不允许用户代码复制该类类型的对象,编译器将拒绝任何进行复制的尝试。
然而,类的友元和成员仍可以进行复制。如果想禁止友元和成员中复制,可以声明一个复制构造函数但不对其定义。声明而不定义成员函数是合法的,但是使用未定义成员的任何操作都将导致链接失败。通过声明但不定义private复制构造函数,可以禁止任何复制类类型
如果定义了复制构造函数,那么最好也定义默认构造函数。因为只有不存在其他构造函数时才合成默认构造函数。
赋值操作符:
与复制构造函数一样,如果没有定义自己的赋值操作符,则编译器会合成一个。
大多数操作符可以定义为成员函数或非成员函数。当操作符为成员函数时,它的第一个操作数隐式绑定到this指针。有些操作符(包括赋值操作符)必须是定义自己的类的成员。因为赋值必须是类的成员,所以this绑定到指向左操作数的指针。因此赋值操作符接收单个形参,且该形参是同一类类型的对象。右操作数一般作为const引用传递。
赋值操作符的返回类型应该与内置类型赋值运算返回值相同。内置类型的赋值运算返回对右操作数的引用,因此赋值操作符也返回同一类类型的引用。
合成的赋值操作符:
与合成复制构造函数一样,他也会执行逐个成员赋值。对于数组,给每个数组成员赋值。
复制和赋值常一起使用。
析构函数;
构造函数的一个用途是自动获取资源。例如:构造函数可以分配一个缓冲区或打开一个文件,在构造函数中分配了资源之后,需要一个对应操作自动回收或释放资源。析构函数就是这样一个的特殊函数,它可以完成所需资源的资源回收。
当对象的引用或指针超出作用域时,不会运行析构函数。只有删除指向动态分配对象的指针或实际对象(而不是对象的引用)超出作用域时,才会运行析构函数。
撤销一个容器时,也会运行容器中的类类型元素的析构函数。容器中的元素总是按逆序撤销:首先撤销size()-1的元素,直到撤销下标为0的元素。
何时编写显式的析构函数:
如果类需要析构函数,则他也需要赋值操作符和赋值构造函数。
合成析构函数:
编译器总会为我们合成一个析构函数。合成析构函数按对象创建时的逆序撤销每个非static成员,因此,它按成员在类中声明的次序的逆序撤销成员。
析构函数没有返回值也没有形参。因为不能指定任何形参,所以不能重载析构函数。
析构函数与复制构造函数或赋值操作符之间的重要区别:即使我们编写了自己的析构函数,合成析构函数仍然运行。
管理指针成员:
包含指针的类需要特别注意复制控制,原因是复制指针只复制指针中的地址,而不会复制指针指向的对象。
将一个指针复制到另一个指针时,两个指针指向同一对象。当两个指针指向同一对象时,可能使用任一指针改变基础对象。类似地,很可能一个指针删除了一个对象,另一个指针的用户还认为基础对象仍然存在。
大多数C++成员采用下面三种方法管理指针成员:
1.指针成员采取常规指针型行为。这样的类具有指针的所有缺陷单无需特殊的复制控制。
2.类可以实现所谓的智能指针行为。指针所指向的对象时共享的,但类能防止悬垂指针。
3.采取值行为。指针所指向的对象时唯一的,有每个类对象独立管理。
1.指针成员采取常规指针型行为。
class HasPtr{
public:
HasPtr(int *p, int i):ptr(p),val(i){}
int *get_ptr() const{return ptr;}
int get_int() const{return val;}
void set_ptr(int *p){ptr = p;}
void set_int(int i){val = i;}
int get_ptr_val() const {return *ptr;}
void set_ptr_val() const {*ptr = val;}
private:
int *ptr;
int val;
}
指针共享一个对象。如:
int *ip = new int(42);
HasPtr ptr(ip,10);
delete ip;
ptr.set_ptr_val(0); //这里的问题是ip和ptr中的指针指向同一个对象。删除了该对象时,ptr中的指针不在指向有效的对象,出现悬垂指针。
2.定义智能指针类:本例中让智能指针删除共享对象。用户将动态分配一个对象并将给对象的地址传给新的HasPtr类。用户仍然可以通过普通指针访问对象,但绝不删除指针。HasPtr类将保证在撤销指向对象时删除对象。定义智能指针通用技术是采用一个使用计数。智能指针将yield计数器与类指向的对象相关联。使用计数跟踪该类有多少个对象共享同一指针。当计数为0时,删除对象。其他方面和普通指针一样,具体而言复制对象时也将指向同一个基础对象。
class U_Ptr{
friend class HasPtr;
int *ip;
size_t use;
U_Ptr(int *p):ip(p),use(1){}
~U_Ptr(){delete ip;}
}
这个类的所有成员均为private,我们不希望普通用户使用U_Ptr类,所以它没有任何public成员。将HasPtr类设置为友元,使其成员可以访问U_Ptr的成员。U_Ptr的构造函数复制指针,而析构函数删除指针。构造函数将使用计数值为1,表示HasPtr对象指向这个U_Ptr对象。
class HasPtr{
public:
int *get_ptr() const {return ptr->ip;}
int *get_int const{return val;}
void set_ptr(int *p){ptr->ip = p;}
void set_int(int i){val = i;}
int get_ptr_val()const{return *ptr->ip;}
void set_ptr_val(int i){*ptr->ip = i;}
HasPtr(int *p,int i):ptr(new U_Ptr(p)),val(i){}
HasPtr(const HasPtr &orig):ptr(orig.ptr),val(orig.val){++ptr->use;}
HasPtr& operator = (const HasPtr&);
~HasPtr(){if(--ptr-use == 0) delete ptr;}
private:
U_Ptr *ptr;
int val;
}
HasPtr& HasPtr::operator=(const HasPtr &rhs)
{
++rhs.ptr->use;
if(--ptr->use == 0)
delete ptr; //赋值运算法,先删除自己原来的值,在赋值,所以总体使用指针的个数不增加。先++rhs.ptr->use保证自身赋值正确运行。
ptr=rhs.ptr;
val = rhs.val;
return *this;
}
为了管理指针成员的类,必须定义三个复制控制成员:赋值构造函数,赋值操作符和析构函数。
3.定义值型类:复制值型对象时,会得到一个不同的新副本。
class HasPtr{
public:
HasPtrr(const int &p,int i):ptr(new int(p)),val(i){}
HasPtr(const HasPtr&orig):ptr(new int (*orig.ptr)),val(orig.val){}
HasPtr& operator=(const HasPtr&);
~HasPtr(){delete ptr;}
int get_ptr_val() const{return *ptr;}
int get_int() const {return val;}
void set_ptr(int *p){ptr = p;}
void set_int(int i){val = i;}
int *get_ptr() const {return ptr;}
int set_ptr_val(int p) const{*ptr = p;}
private:
int *ptr;
int val;
}
HasPtr:: HasPtr::operator=(const HasPtr&rhs)
{
*ptr = *rhs.ptr;
val=rhs.val;
return *this;
}
复制构造函数不再复制指针,它将分配一个新的int对象,并初始化对象以保存与被复制对象相同的值。每个对象都保存属于自己的int值的不同副本。因为每个对象保存自己的副本,所以析构函数将无条件删除指针。
第十四章:重载操作符与转换
重载操作符必须具有至少一个类类型或枚举类型的操作数。这条规则强制重载操作符不能重新定义用于内置类型对象的操作符含义。
除了函数调用操作符operator()之外,重载操作符时使用默认实参是非法的。
作为成员函数的操作符有一个隐含的this形参,限定为第一个操作数,故形参看起来比操作数数目少1.
重载一元操作符作为成员函数就没有(显式)形参,如果作为非成员函数就有一个形参。
一般将算术和关系操作符定义为非成员函数,而将赋值操作符定义为成员。
操作符定义为非成员函数时,通过必须将他们设置为所操作类的友元,因为操作符通常需要访问类的私有部分。
选择成员或非成员实现操作符重载指导原则:
1.赋值,下标,函数调用,成员访问箭头等操纵必须定义为成员。
2.复合赋值操作符通常定义为成员函数
3.改变对象状态或与给定类型紧密联系的其他操作符,如自增,自减,解引用等通常定义为类成员。
4.对称的操作符,如算术操作付,相等操作服务,关系操作符和位操作符最好定义为非成员函数。
输入和输出操作符:
输出操作符重载:
为了保持与IO标准库一致,操作符应该接受ostream&作为第一个形参,对类类型const对象的引用作为第二个形参,并返回读ostream形参的引用。
简单的定义:
ostream& operator << (ostream &os,const ClassType &boject)
{
os <<.....
return os;
}
第一个形参是对ostream对象的引用,在该对象上将产出输出。ostream为非const,因为写入到流会改变流的状态。该形参是一个引用,因为不能复制ostream对象。
第二个形参一般应该是对输出的类类型的引用。该形参是一个引用以避免复制实参。他可以是const,因为输出一个对象不应该改变该对象。使形参成为const引用,就可以使用同一个定义来输出const和非const对象。
返回类型是一个ostream引用。
IO操作符必须定义为非成员函数。否则,左操作数将只能是该类类型的对象。
如 item << cout; 这与标准库定义的使用方式相反。
IO操作符通常对非公有数据成员进行读写,因此,类通常将IO操作符设为友元。
输入操作付>>的重载:
与输出操作符类似,输入操作符的第一个形参是一个引用,指向他要读取的流,并且返回的也是对同一个流的引用。它的第二个形参是对要读入的对象的非const引用,该形参必须为非const,因为输入操作符的目的是将数据读到这个对象中。
更重要的是输入操作符必须处理错误和文件结束的可能性。
如: istream& operator>>(istream &in,Sales_item &s)
{
double price;
in >> s.isbn >> s.units >> price;
if(in)
s.revenue = s.units * price;
else
s=Sales_item(); //input failed:reser object to default
return in;
}
算术操作符和关系操作符:一般而言将算术和关系操作符定义为非成员函数。
Sales_item operator+(const Sales_item &lhs,const Sales_item &rhs)
{
Sales_item ret(lhs);
ret += rhs;
return ret;
}
为了与内置操作符保持一致,加法返回一个右值,而不是一个引用。
相等操作符:
inline bool operator==(const Sales_item &lhs, cosnt Sales_item &rhs)
{
return lhs.units_sold == rhs.units_sold && lhs.revenue == rhs.revenue &&
lhs.sme_isbn(rhs);
}
inline bool operator!=(const Sales_item &lhs, cosnt Sales_item &rhs)
{
return !(lhs == rhs);
}
赋值操作符:赋值必须返回对*this的引用
一般而言,赋值和复合赋值操作符应返回左操作数的引用。
下标操作符:下标操作符必须定义为类成员函数
一般需要定义两个版本:一个非const成员并返回引用,另一个为const成员并返回const引用。
class Foo{
public:
int &operator[] (const size_t);
const int &operator[] (const size_t) const;
private:
vector<int> date;
}
int& Foo::operator[] (const size_t index)
{
return data[index];
}
const int& Foo::operator[](const size_t index)const
{
return data[index];
}
成员访问操作符:C++语言允许重载解引用和箭头操作符
箭头操作符必须定义为类成员函数。解引用不要求为成员函数,但一般将它作为成员函数也是正确的。
解引用操作符和箭头操作符常用在实现智能指针的类中。作为例子,假定想要定义一个类类型表示指向编写Screen类型对象的指针,将该类命名为ScreenPtr。ScreenPtr的用户将会传递一个指针,该指针指向动态分配的Screen。ScreenPtr类将对其指针进行计数。我们定义一个伙伴类来保存指针及其相关使用计数。
Class ScrPtr{
friend class ScreenPtr;
Screen *sp;
size_t use;
ScrPtr(Screen *p):sp(p),use(1){}
~ScrPtr(){detele sp;}
}
class ScreenPtr{
ScreenPtr(Screen *p):ptr(new ScrPtr(p)){}
ScreenPtr(const ScreenPtr &org):ptr(org.ptr){++ptr->use;}
ScreenPtr &operator=(const ScreenPtr&);
~ScreenPtr() {if(--ptr->use==0) delete ptr;}
private:
ScrPte *Ptr;
}
使用重载箭头:
ScreenPtr p(&myScreen);
p->display(cout); //因为p是一个ScreenPtr对象,p->display的含义与对(p.operator->())->display求值相同。对p.operator->()求值将调用ScreenPtr类的operator->,它的返回值指向Screen对象的指针,该指针用于获取并运行ScreenPtr所指向对象的display成员。
重载箭头操作符必须返回指向类类型的指针,或者返回定义了自己的箭头操作符的类类型对象。
自增操作符和自减操作符:
自增和自减操作符经常由诸如迭代器这样的类实现。
class CheckedPtr{
public:
CheckedPtr(int *b,int*e):beg(b),end(e),curr(b){}
CheckPtr &operator++(); //prefix operator
CheckPtr &operator--();
CheckPtr operator++(int); //postfix operator同时为前置和后置操作符重载,但他们在标准库中定义的参数数目和类型相同。普通重载不能区别定义是前置还是后置。所以后缀操作符函数接受一个额外的int型参数。使用后缀操作符时,编译器提供0作为这个形参的实参
CheckPtr operator--(int); //后缀应该返回旧值,并且应作为值返回,不应该是返回引用。
private:
int *beg;
int *end;
int *curr;
}
Checked &CheckedPtr::operator++()
{
if(curr==end)
throw out_of_range("increment past the end of CheckPtr");
++curr;
return *this;
}
Checked &CheckedPtr::operator--()
{
if(curr==beg)
throw out_of_range("decrement past the beginning of CheckPtr");
--curr;
return *this;
}
Checked CheckedPtr::operator++(int)
{
CheckedPtr ret(*this);
++*this;
return ret;
}
Checked CheckedPtr::operator--(int)
{
CheckedPtr ret(*this);
--*this;
return ret;
}
显示调用前缀和后缀
CheckedPtr parr(ia,ia+size);
parr.operator++(0);
parr.operator++();
调用操作符和函数操作对象:
struct absInt{
int operator()(int val){
return val <0 ? -val:val;
}
}
用法:
int i=-42;
absInt absObj;
unsigned int ui = absObj(i); //判断i的正负
函数调用操作符必须声明为成员函数。一个类可以定义函数调用操作符的多个版本,有形参的数目或类型加以区别。
定义了调用操作符的类,其对象称为函数对象,即它们是行为类似的函数的对象。
第十五章 面向对象编程
面向对象编程基于三个概念:数据抽象、继承、多态
在C++中基类必须指出希望派生类中定义哪些函数,定义为virtual的函数时基类期待派生类重新定义的,基类希望派生类继承的函数不能定义为虚函数。
在C++中,通过基类的引用(或指针)调用虚函数时,发生动态绑定。引用(或指针)既可以指向基类对象也可以指向派生类对象,这一事实是动态绑定的关键。用引用(或指针)调用的虚函数在运行时确定,被调用的函数是引用(或指针)所指对象的实际类型所定义的。
定义基类:
对非虚函数的调用在编译时确定。为了指明函数为虚函数,在其返回类型前面加上保留字virtual。除了构造函数之外,任意非static成员函数都可以是虚函数。保留字virtual只在类内部的成员函数声明中出现,不能用在类定义体外部出现的函数定义上。
基类通常将派生类需要重定义的任意函数定义为虚函数。
如: class Item_base{
public:
Item_base(const std::string &book = " ", double sales_price = 0.0):isbn(book),price(sales_price){}
std::string book() const {return isbn;}
virtual double net_price(std::size_t n) const {return n * price;}
virtual ~Item_base(){}
private:
std::string isbn;
protected:
double price; //因为派生类需要更改price
}
protected成员:
1.像private一样,protected成员不能被类的用户访问
2.像public一样,protected成员可被该类的派生类访问
3.派生类只能通过派生类对象访问基类的protected成员,派生类对基类类型对象的protected成员没有特殊访问权限。
例如:Bulk_item是Item_base的派生类
void Bulk_item::memfun(const Bulk_item &d, const Item_base &b)
{
double ret = price; //ok,use this -> price
ret = d.pricce; //ok,使用派生类对象作为形参的protected成员
ret = b.price; //error,不能使用基类对象作为形参的protected成员
}
派生类:
class Bulk_item : public Item_base{
public:
double net_price(std::size_t)const;
private:
std::size_t min_qty; //至少购买min_qty才能打折
double discount; //折扣
}
double Bulk_item::net_price(size_t cnt) const
{
if (cnt > min_qty)
return cnt * price *(1-discount);
else
return cnt * price;
}
派生类一般会重定义所继承的虚函数。如果派生类没有重定义某个虚函数,则使用基类中定义的版本。
派生类必须对想要重定义的每个继承成员进行声明。Bulk_item类指出,它将重定义net_price函数但将使用book的继承版本
派生类中虚函数的声明必须与基类的定义方式完全匹配,但有一个例外:返回对基类的引用(或指针)的虚函数。派生类中的虚函数可以返回基类所返回类型派生类的引用(或指针)。
例如,Item_base类可以定义返回Item_base*的虚函数,如果这样,Bulk_item类中定义的实例可以定义为返回Item_base*h或Bulk_item*。
一旦函数在基类中被声明为虚函数,他就一直为虚函数,派生类无法改变函数为虚函数这一事实。派生类重定义虚函数时,可以使用virtual保留字,但不是必须这样做。
已定义的类可以用作基类,只声明未定义的类不能用作基类。
如果需要声明(但不实现)一个派生类,则声明包含类名但不包含派生列表
如 class Bulk_item : pubic Item_base; //error
class Bulk_item; //OK
C++中要触发动态绑定必须满足两个条件:
1.只有指定虚函数的成员才能进行动态绑定
2.必须通过基类类型的引用或指针进行函数调用
基类类型的引用或指针可以引用基类类型对象,也可以引用派生类型对象,所以对象的实际类型可能不同于该对象引用或指针的静态类型。
任何可以在基类对象上执行的操作也可以通过派生类对象使用。因为派生类对象用于基类部分。
引用和指针的静态类型和动态类型可以不同。但对象是非多态的,对象类型已知且不变。对象的动态类型总是与静态类型相同,这一点与引用或指针相反。
通过引用或指针调用虚函数时,编译器将生成代码,在运行时确定调用哪个函数,被调用的是与动态类型相对应函数。如果调用的非虚函数,则无论实际对象是什么类型,都执行基类类型所定义的函数。
不管动态类型是基类还是派生类的引用或指针,如果静态类型是基类,那么对book的调用总会调用Item_base的book(即使引用的是派生类对象,并且派生类中定义了book函数)
覆盖虚函数机制:某些情况下,希望覆盖虚函数机制并强制调用使用虚函数的特定版本,这是可以使用作用域操作符
Item_base *baseP = &derived;
double d = baseP->Item_base::net_price(42);
虚函数与默认实参:
虚函数也可以有默认实参。通过基类的引用或指针调用虚函数时,默认实参为在基类虚函数声明中的值,如果通过派生类的指针或引用调用虚函数,则默认实参是在派生类的版本中声明的值。
在同一虚函数的基类版本和派生类版本中使用不同的默认实参几乎一定会引起麻烦。如果通过基类的引用或指针调用虚函数,但实际执行的是派生类中定义的版本,这时就可能会出现问题。在这种情况下,为虚函数的基类版本定义的默认实参将传给派生类定义的版本,而派生类版本是用不同默认实参定义的。
公有、私有和受保护的继承:
基类本身指定对自身成员的最小访问控制。如果成员在基类中为private,则只有基类和基类的友元才可以访问该成员。派生类不能访问基类的private成员,也不能使自己的用户能够访问那些成员。如果基类成员为public或protected,则派生列表中使用的访问标号决定该成员在派生类中的访问级别:
如果是公用继承,基类成员保持自己的访问级别:基类的public成员为派生类的public成员,基类的protected成员为派生类的protected成员
如果是受保护继承,基类的public和protected成员在派生类中为protected成员
如果是私有继承,基类的所有成员在派生类中为private成员
Class Base{
public:
std::size_t size() const {return n;}
protected:
std::size_t n;
}
class Derived : private Base{...}
class Derived : private Base{
public:
using Base::size; //size仍为public
protected:
using Base::n; //n为protected
}
友元关系与继承:
友元不能继承。基类的友元对派生类的成员没有特殊访问权限。如果基类被授予友元关系,则只有基类具有特殊访问权限,该基类的派生类不能访问友元关系的类。
继承与静态成员:
如果成员在基类中为private,则派生类不能访问它。假定可以访问成员,则既可以通过基类访问static成员,也可以通过派生类访问static成员。一般而言,既可以使用作用域操作符也可以使用点或箭头成员访问操作符。
如: struct Base{
static void statmen();
};
struct Derived : Base{
void f(const Derived&);
};
void Derived::f(const Derived &derived_obj)
{
Base::statmen();
Derived::statmen();
derived_obj.statmen();
statmem();
}
转换与继承:
派生类到基类的转换:
1.引用转化不同于转换对象
将派生类的对象传递给希望接受基类引用的函数时,引用直接绑定到该对象,虽然看起来在传递对象,实际上实参是对该对象的引用,对象本身未被复制,并且,转换不会在任何方面改变派生类对象,该对象仍是派生类型对象。
将派生类对象传递给希望接受基类类型对象(而不是引用)的函数时,情况完全不同。在这种情况下,形参的类型是固定的——在编译时和运行时形参都是基类类型对象。如果用派生类型对象调用这样的函数,则该派生类对象的基类部分被复制到形参。
一个是将派生类对象转换为基类类型的引用,一个是用派生类对象对基类对象进行初始化或赋值。
2.用派生类对象对基类对象进行初始化或赋值
用派生类对象对基类对象进行初始化或赋值实际上是调用函数:初始化调用构造函数,赋值时调用赋值操作符。
用派生类对象对基类对象进行初始化或赋值时,存在两种可能性。
第一种是基类显式定义将派生类对象复制或赋值给基类对象,不过并不常见。相反基类一般预定义自己的复制构造函数和赋值操作符,这些成员接受一个形参,该形参时基类类型的(const)引用。因为存在从派生类引用到基类的转换,这些复制控制成员可用于从派生类 对象对基类对象进行初始化或赋值。
如: Item_base item;
Bulk_item bulk;
Item_base item(bulk);
item=bulk;
将发生下面的步骤:
1)将Bulk_item对象转换为Item_base引用,这仅仅以为着一个Item_base引用绑定到Bulk_item对象。
2)将该引用作为实参传给复制构造函数或赋值操作符
3)那些操作符使用Bulk_item的Item_base部分分别对调用构造函数或赋值的Item_base对象的成员初始化或赋值。
4)一旦操作符执行完毕,对象即为Item_base。它包含Bulk_item的Item_base部分的副本,但实参的Bulk_item部分被忽略。
派生类到基类转换的可访问性:
假定D继承B:
1.只有当D公有地继承B时,用户代码才能使用派生类向基类的转换;如果D继承B的方式是受保护的或者私有的,则用户代码不能使用该转换。
2.不论D以什么方式继承B,D的成员函数和友元都能使用派生类向基类的转换;派生类向其直接基类的类型转换对于派生类的成员和友元来说是永远可访问的。
3.如果D继承B的方式是公有的或者是受保护的,则D的派生类的成员和友元可以使用D向B的类型转换;反之,如果D继承B的方式是私有的,则D的派生类的成员和友元不能使用D向B的类型转换。
基类到派生类的转换:
从基类到派生类的自动转换是不存在的。需要派生类对象是不能使用基类对象。
如 Item_base base;
Bulk_item *bulkP = &base; //error
Bulk_item &bulkP = base; //error
Bulk_item bulk = base; //error
当基类指针或引用时间绑定到派生类对象时,从基类到派生类的转换也存在限制;
如 Bulk_item bulk;
Item_base *itemP = &bulk;
Bulk_item *bulkP = itemP; //error
编译器在编译时无法知道特定转换在运行实际上是安全的。在这些情况下,如果知道基类到派生类的转换是安全的,就可以使用static_cast强制编译器转换。或者可以用dynamic_cast申请在运行时进行检查。
构造函数和复制控制
每个派生类对象由派生类中定义的成员加上一个或多个基类子对象构造。所有当构造、复制、赋值和撤销派生类型对象时,也会构造、复制、赋值和撤销这些基类子对象。
构造函数和复制控制成员不能继承。
基类构造函数和复制控制:
本身不是派生类的基类,其构造函数和复制控制基本上不受继承影响。但某些类希望派生类使用特殊的构造函数,这样的构造函数应定义为protected。
派生类构造函数:
派生类构造函数受继承关系的影响,每个派生类构造函数除了初始化自己的数据成员之外,还要初始化基类。
1.合成的派生类默认构造函数
除了初始化派生了的数据成员外,还会调用基类的默认构造函数初始化基类部分
对于Bulk_item类,合成的默认构造函数会这样执行
1).调用Item_base的默认构造函数,将isbn成员初始化为空串,将price成员初始化为0
2).用常规变量的初始化规则初始化Bulk_item的成员,也就是说,min_qty和discount成员会是为初始化的。
2.定义默认构造函数
如: class Bulk_item : public Item_base{
public:
Bulk_item() : min_qty(0),discount(0.0){}
};
3.向基类构造函数传递实参
派生类构造函数的初始化列表只能初始化派生类的成员,不能直接初始化继承成员。相反,派生类构造函数通过将基类包含在构造函数初始化列表中来间接初始化继承成员。
class Bulk_item : public Item_base{
public:
Bulk_item(const std::string &book,double sales_price,std::size_t qty = 0,double disc_rate = 0.0):Item_base(book,sales_price),min_qty(qty),discount(dis_rate){}
};
Bulk_item bulk("0-201-8737-1",50,5, 0.19);
构造函数初始化列表为类的基类和成员提供初始值,它并不指定初始化的执行次序。首先初始化基类,然后根据声明次序初始化派生类的成员。
4.在派生类中使用默认实参
class Bulk_item : public Item_base{
public:
Bulk_item(const std::string &book = " ",double sales_price = 0,std::size_t qty = 0,double disc_rate = 0.0):Item_base(book,sales_price),min_qty(qty),discount(dis_rate){}
};
5.只能初始化直接基类
一个类只能初始化自己的直接基类,不能初始化两代和两代以上的基类。
class Disc_item : public Item_base{
public:
Dics_item(const std::string &book = " ",double sales_price = 0.0,
std::size_t qty = 0, double disc_rate = 0.0):
Item_base(book,sales_price),
quantity(qty),discount(disc_rate){}
protected:
std::size_t quantity;
double discount;
};
在定义Bul_item继承Disc_item
class Bulk_item : public Disc_item{
public:
Bulk_item(const std::string& book = " ",
double sales_price = 0.0,
std::size_t qty = 0,double disc_rate =0.0):
Disc_item(book,sales_price,qty,disc_rate){}
double net_price(std::size_t)const;
};
复制控制和继承
1.定义派生类复制构造函数
如果派生类定义了自己的复制构造函数,该复制狗杂函数一般应显式使用基类复制构造函数初始化对象的基类部分
如 class Base{}
class Derived: public Base{
public:
Derived(const Derived& d):
Base(d)
}
初始化函数Base(d)将派生类对象d转换为它的基类部分的引用,并调用基类复制构造函数
如果省略基类初始化函数:
如
class Derived: public Base{
public:
Derived(const Derived& d):
}
奇怪的配置:运行效果是运行Base的默认构造函数初始化对象的 基类部分,而他的Derived成员是另一对象d的副本。
2.派生类赋值操作符:
与复制构造函数类似:如果派生类定义了自己赋值操作符,则该操作符必须对基类部分进行显式赋值。
Derived &Derived::operator=(const Derived &rhs)
{
if(this != this){
Base::operator=(rhs);
//再做对Derived的成员复制;
}
return *this;
}
赋值操作符必须防止自身赋值。假定左右操作数不同,则调用Base类的赋值操作符给基类部分赋值。基类操作符将释放左操作数中的基类部分的值,并辅以来自rhs的新值。该操作符执行完毕后,接着要做的是为派生类中的成员赋值。
3.派生类析构函数
析构函数的工作于复制构造函数和赋值操作符不同。派生类析构函数不负责撤销基类对象的成员。编译器总是显式调用派生类对象基类部分的析构函数。每个析构函数指清除自己的成员。
class Derived:public Base{
public:
~Derived()
};
对象的撤销顺序与构造顺序相反:首先运行类析构函数,然后按继承层次一次向上调用个基类析构函数。
虚析构函数:
如果删除基类指针,则需要运行基类析构函数并清除基类的成员,如果对象实际是派生类型的,则没有定义该行为。要保证运行适当的析构函数,基类中的析构函数必须为虚函数。
如果析构函数为虚函数,那么通过指针调用时,运行哪个析构函数将因指针所指对象类型的不同而不同:
如: Item_base *itemP = new Item_base;
delete itemP;
itemP = new Bulk_item;
delete itemP;
像其他虚函数一样,析构函数的虚函数性质都将继承。因此,如果根类的析构函数为虚函数,则派生类析构函数也将是虚函数,无论派生类显式定义析构函数还是使用合成析构函数,派生类析构函数都是虚函数。
构造函数不能为虚函数,因为构造函数是在对象完全构造之前运行的,在构造函数运行的时候,对象的动态类型还不完整。
构造函数和析构函数中的虚函数:
在基类构造函数或析构函数中,将派生类对象当做基类类型对象对待。如果在构造函数或析构函数中调用虚函数,在运行的是构造函数或析构函数自身定义的版本。比如A继承B,A、B中的构造函数和析构函数都调用析构函数。但在创建A调用B中的构造函数时,B中的构造函数会调用B中的虚函数,而不是A中的虚函数。
继承情况下的类作用域:
在基类和派生类总使用同一名字的成员hanukkah,其行为与数据成员一样:在派生类作用域中派生类成员将屏蔽积累成员。即使函数原型不同,基类成员 也会被屏蔽。
如: struct Base{
int memfcn();
};
struct erived : Basw{
int memfcn(int);
};
Derived d, Base b;
b.memfcn(); //calls Base::memfcn
d.memfcn(10); //calls Derived::memfcn
d.memfcn(); //error,memfcn with no arguments is hidden
d.Base::memfcn(); //ok,calls base::memfcn
如果派生类重定义了重载成员,则通过派生类型只能访问派生类中重定义的那些成员,当然派生类也可以通过基类类型访问基类中重定义的那些成员。如果派生类想通过自身类型使用所有的重载版本,则派生类要么定义所有成员,要么一个也不从定义。
有时类需要仅仅重定义一个重载集中某些版本的行为,并且想要继承其他版本的含义,这种情况下,重定义每个基类版本,可能会令人厌烦。这时,派生类可以为重载成员提供using声明,一个using声明只能制定一个名字,不能指定形参表,。将所有名字加入作用域之后,派生类只需重定义本类型必须定义的那些函数,对其他版本可以使用继承的定义。
虚函数与作用域:
class Base{
public:
virtual int fcn();
};
class D1:public Base{
public:
int fcn(int);
};
class D2:public D1{
public:
int fcn(int);
int fcn();
}
D1中的fcn版本没有重定义Base的虚函数fcn,相反,它屏蔽了基类的fcn。结果D1有两个名为fcn的函数:类从Base继承了一个名为fcn的虚函数,类又定义了自己的名为fcn的非成员函数,该函数接受一个int形参。但是,从Base继承的虚函数不能通过D1对象(或D1的引用或指针)调用,因为该函数被fcn(int)的定义屏蔽了。
类D2重定义了它继承的两个函数,它重定义了Base中定义的fcn的原始版本并重定义了D1中定义的非虚版本。
通过基类调用被屏蔽的虚函数:
Base bobj; D1 d1obj; D2 d2obj;
Base *bp1 = &bobj, *bp2 = &d1obj, *bp3 = &d2obj;
bp1->fcn(); //ok Base::fcn
bp2->fcn(); //ok,Base::fcn
bp3->fcn(); //ok,D2::fcn
纯虚函数:在函数形参表后面加上=0以指定纯虚函数。
class Disc_item : public Item_base{
public:
virtual double net_price(std::siez_t) const = 0;
}
将函数定义为纯虚函数能够说明,该函数为后代类型提供了可以覆盖的接口,但是这个类中的版本绝不会调用。重要的是,用户将不能创建Disc_item类型的对象。
含有(或继承)一个或多个虚函数的类是抽象基类。除了作为抽象基类的派生类的对象的组成部分,不能创建抽象类型的对象。
容器与继承:
我们希望使用容器(或内置数组)保存因继承而相关联的对象。但是,对象不是多态。
multiset<Item_base> basket;
Item_base base;
Bulk_item bulk;
basket.insert(base);
basket.insert(bulk);
则加入派生类的的对象时,只将对象的基类部分保存在容器中。记住,将派生类对象复制到积累对象时,派生类对象将被切掉。
所以不能用容器同时存储基类对象和派生类对象。唯一可行的选择是使用容器保存对象的指针。但代价是需要用户保证容器存在,被指向的对象就存在。如果对象是动态分配的,用户必须保证容器消失时适当的释放对象。
句柄类与继承:
句柄类存储和管理基类指针。因为句柄类使用基类指针执行操纵,虚成员的行为将在运行时根据句柄实际绑定的对象的类型而变化。
包装了继承层次的句柄有两个重要的设计考虑因素:
像对任何保存指针的类一样,必须对复制控制做些什么。包装了继承层次的句柄通常表现的像一个智能指针或像一个值。
句柄类决定句柄接口屏蔽还是不屏蔽继承层次,如果不屏蔽继承层次,用户必须了解和使用基本层次中的对象。
指针型句柄:
我们定义一个名为Sales_item的指针型句柄,表示Item_base层次。Sales_item的用户将想使用指针一样使用它:用户将Sales_item绑定到Item_base类型的对象并使用*和->操作符执行Item_base的操作。
Sales_item item(Bulk_item("0-201-82470-1",35,3, .20));
item -> net_price();
但用户不必管理句柄指向的对象,Sales_item类将完成这部分工作。
1.定义句柄:
Sales_item有三个构造函数:默认构造函数、复制构造函数和接受Item_base对象的构造函数。第三个构造函数将复制Item_base对象,并保证:只要Sales_item对象存在副本就存在。当复制Sales_item对象或给Sales_item赋值时,将复制指针而不是复制对象。像对其他指针型句柄一样,将用使用技术来管理副本。
但不使用伙伴类类存储指针和相关计数。Sales_item类将有两个数据成员,都是指针:一个指针指向Item_base对象,而另一个将指向使用计数。Item_base指针可以指向Item_base对象也可以指向Item_base派生类型的对象。通过指向使用计数,多个Sales_item对象可以共享同一计数器。
除了管理使用技术外,Sales_item类还将定义解引用和箭头操作符:
class Sales_item{
public:
Sales_item():p(0),use(new std::size_t(1)){}
Sales_item(const Item_base&);
Sales_item(const Sales_item &i): p(i.p),use(i.use){++*use;}
~Sales_item(){decr_use();}
Sales__item &operator=(const Sales_item&);
const Item_base *operator ->()const{if(p) return p;
else throw std::logic_error("unbound Sales_item");}
const Item_base &operator*() const{if(p) return *p;
else throw std::logic_error("unbound Sales_item");}
private:
Item_base *p;
std::size_t *use;
void decr_use()
{if(--*use == 0){delete p;delete use;}}
};
Sales_item& Sales_item::operator=(const Sales_item &rhs)
{
++*rhs.use;
derc_use();
p=rhs.p;
use=rhs.use;
return *this;
}
复制未知类型:
要实现Item_base对象的构造函数,必须解决一个问题:我们不知道给予构造函数的对象的实际类型。我们知道他是一个Item_base对象或者是一个Item_base派生类型对象。句柄经常需要在不知道对象的确切类型时分配已知对象的新副本。
为了支持句柄类,需要从基类开始,在继承层次的每个类型中增加clone,基类必须将该函数定义为虚函数:
class Item_base{
public:
virtual Item_base* clone() const
{return new Item_base(*this);}
};
class Bulk_item:public Item_base{
public:
Bulk_item* clone() const
{return new Bulk_item(*this);}
};
然后可以定义句柄构造函数:
Sales_item::Sales_item(const Item_base &item):
p(item.clone()),use(new std::size_t(1)){}
想默认构造函数一样,这个构造函数分配并初始化使用计数,它调用形参的clone产生那个对象的副本。如果实参是Item_base对象,则运行Item_base的clone函数;如果是Bulk_item对象,则执行Bulk_item的clone函数。