因为派生类是从基类继承而来的,所以包含了基类的一些成员,所以在写派生类的构造函数和复制控制函数时,必须考虑基类的影响。
先说构造函数,派生类的构造函数中,并不直接初始化基类的成员,而是调用基类的构造函数初始化基类的部分:
class Item_base
{
public:
//构造函数
Item_base(const std::string &book = "",double sales_price = 0.0):
isbn(book),price(sales_price){ std::cout<<"基类构造函数"<<std::endl;}
//返回isbn号
std::string book(){return isbn;}
//基类不需要折扣策略
virtual double net_price(std::size_t n)const{return n * price;}
//析构函数
virtual ~Item_base(){std::cout<<"基类析构函数"<<std::endl;};
//复制控制函数
Item_base (const Item_base&);
//赋值操作
Item_base& operator=(const Item_base&);
private:
std::string isbn;
protected:
double price;
};
class Bulk_item:public Item_base
{
public:
//构造函数
Bulk_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){ std::cout<<"派生类构造函数"<<std::endl;}
~Bulk_item(){std::cout<<"派生类析构函数"<<std::endl;}
double net_price(std::size_t)const;
//复制控制
Bulk_item (const Bulk_item&);
//赋值操作符
Bulk_item& operator=(const Bulk_item&);
private:
std::size_t quantity;
double discount;
};
而且每次都是先初始化基类部分,在初始化派生类的成员。还有·一点需要注意,就是派生类只能初始化自己的基类,并不需要考虑基类的基类怎么初始化。
对于复制控制函数,我们一个一个看:
先说复制构造函数,它的作用是规定当用一个派生类对象复制给另一个派生类对象时,会发生什么。它需要完成的工作也是两部分:调用基类的复制构造函数完成基类部分的复制,然后再复制派生类的部分。
//基类复制控制
Item_base::Item_base(const Item_base& ib)
{
isbn = ib.isbn;
price = ib.price;
std::cout<<"基类复制构造函数"<<std::endl;
}
//派生类复制构造函数
Bulk_item::Bulk_item (const Bulk_item& b)
{
Item_base::Item_base(b);
quantity = b.quantity;
discount = b.discount;
std::cout<<"派生类复制构造函数"<<std::endl;
}
注意一下作用域操作符:如果不加作用域标示符,会提示你形参b重定义。因为Item_base(b)相当于新建了一个Item_base的对象b,所以是重定义。使用作用域操作符以后,等于显示调用了基类的复制构造函数Item_base,用派生类直接给基类复制,所以不会出现重定义。
同理还有赋值操作符:
//基类的赋值操作
Item_base& Item_base::operator=(const Item_base& rhs)
{
isbn = rhs.isbn;
price = rhs.price;
std::cout<<"基类赋值操作符"<<std::endl;
return *this;
}
//赋值操作符
Bulk_item& Bulk_item::operator=(const Bulk_item& rhs)
{
if(this != &rhs)
Item_base::operator=(rhs);
quantity = rhs.quantity;
discount = rhs.discount;
std::cout<<"派生类赋值操作符"<<std::endl;
return *this;
}
这里要注意的是,我们增加了左右操作数是否相等的判断,只有在不等时,才调用基类的操作。
在析构函数中,我们什么也没有做,却把它定义成了虚函数,这是为什么呢?
通过前面的学习我们知道,如果一个类中有指针成员,那么应该在这个类的析构函数中删除指针成员。我们又知道:由于动态绑定的缘故,我们完全可以让一个静态类型为基类的指针指向一个派生类的对象。当通过基类的指针去删除派 生类的对象,而基类又没有虚析构函数时,会出现问题,举一个例子:
class A
{
public:
A()
{
ptr = new int[10];
cout<<"A构造函数"<<endl;
}
virtual ~A()
{
delete ptr;
cout<<"A析构函数"<<endl;
}
private:
int *ptr;
};
class B:public A
{
public:
B()
{
ptr = new long[10];
cout<<"B构造函数"<<endl;
}
~B()
{
delete ptr;
cout<<"B析构函数"<<endl;
}
private:
long *ptr;
};
int main()
{
A *a=new B();
delete a;
return 0;
}
程序运行依次打印:A构造函数,B构造函数,A析构函数。
可以看出,B构造函数中创建的指针并没有删除,这是因为,当执行delete a时,由于a的静态类型是指向A类的指针,所以会调用A的析构函数删除A构造函数中新建的对象。当我们把A类构造函数设定为虚函数时,由于虚函数是动态绑定的,所以会调用派生类的析构函数,而在派生类的析构函数中,调用基类的析构函数。这样,就能完全删除对象了。
这里还要强调下一构造函数与析构函数的执行顺序:建立一个派生类时,会调用派生类构造函数,然后在这个构造函数中(显示或者隐式的)调用基类构造函数,然后再初始化派生类的其他部分;析构函数中先调用派生类析构函数,先析构派生类自己的成员,最后在这个析构函数中隐式的调用基类的析构函数析构基类的成员。
最后的一点内容是对这一小节内容的总结以及上一小结的补充:
上一小节中并没有给出相关内容的说明程序,下面通过测试程序来具体说明:
首先是定义了3个函数:
void func1(Item_base obj){}
void func2(Item_base& obj){}
Item_base func3()
{
Item_base obj;
return obj;
}
这三个函数看起来并没有做什么实质性的工作,但他们很有代表新:前两个接受的分别是基类的对象和基类对象的引用,第三个的返回值是基类的对象,下面我们看看主程序:
int main()
{
Item_base iobj; //调用基类构造函数,整个函数结束时释放
func1(iobj); //调用基类构造函数创建一个临时对象,整个函数释放
func2(iobj); //使用引用时,并不创建对象
iobj = func3(); //新建一个Item_base对象时调用基类构造函数
//函数返回时调用复制构造函数
//然后调用基类析构函数撤销局部对象
//然后调用基类赋值操作符
//整个函数结束时释放
Item_base *p = new Item_base; //调用基类构造函数创建Item_base对象
delete p; //删除指针时调用析构函数
Bulk_item bobj; //因为构造函数先执行列表,后执行函数体
//打印先执行基类构造函数,然后打印构造派生类
func1(bobj); //由于func1函数的形参是基类,所以先将派生类隐式转化成基类对象
//然后调用基类复制构造函数将实参传给形参
//调用基类析构函数撤销对象
func2(bobj); //引用不存在临时对象的创建
Bulk_item *q = new Bulk_item; //派生类构造函数会调用基类构造函数
delete q; //调用析构函数
Item_base obj(bobj); //派生类转化为基类,然后调用基类复制构造函数
return 0;
}
程序的注释部分说明了具体的功能。值得注意的是:
1.如果将用派生类实参调用形参为基类引用的函数,并不会发生派生类到基类的类型转化,因为引用直接绑定到派生类上,对象并没有“类型转化”,只是将派生类的基类部分的地址传递给基类型的引用。
2.如果将派生类实参传递给一个基类形参的函数,先将派生类隐式转化成基类对象,然后调用基类复制构造函数将实参传给形参。
3.用派生类初始化基类时,派生类将转化为基类,然后调用基类的复制构造函数处理。