C++ Primer 第十五章 面向对象编程
15.1 面向对象编程:概述
继承:
虚函数:virtual
动态绑定:
15.2 定义基类和派生类
成员限制符:public private protected
protected:在子类中可访问,派生类内部可以访问本类对象protected成员,不能访问基类对象protected成员
class item : public base
{
void test(item &a , base &b)
{
a.name; // 可以访问本类对象protected成员
b.name; // 错误,不能访问基类对象protected成员
}
}
C++允许多重继承,例如 class item : public base1, base2...
原则上子类重写父类虚函数时声明和定义要于父类完全一致,但有一个例外:虚函数返回值是父类的指针或引用 可以在子类中将返回改成子类的指针或引用:比如父类有虚函数:base *test(); 子类可重写成: item *test();
声明一个包含派生列表的类(而不实现)是错误的。
class item: public base;
动态绑定需要符合两个条件:调用函数必须是virtual ;必须要通过指针或引用调用虚函数。 动态绑定时执行函数取决于实际执行的类型,而不取决于指针或引用变量类型。
item b;
base a = b;
a.show(); // 不会动态绑定,a不是指针也不是引用。调用变量a的类方法(a类是base 所以调用base版方法)
base *a = &b;
a->show(); // 动态绑定,调用item版方法,调用实际数据的类方法(实际数据b是item类)
base &a = b;
a.show(); // 动态绑定,调用item版方法,调用实际数据的类方法(实际数据b是item类)
virtual函数版本是在运行时确定,非virtual函数是在编译时确定。
也可以指定执行virtual函数版本如:
base *a = &b;
a->base::show(); // 指针指定调用base版方法
base &a = b;
a.base::show(); // 引用指定调用base版方法
函数可以设定默认默认参数,默认参数定义的顺序为自右到左。即如果一个参数设定了缺省值时,其右边的参数都要有缺省值
c++三种继承方式:public, private, protected 假设B类继承A类,即B类是A类的直接子类。
public继承:A的访问属性在B类保持不变。
A的public-----------B仍是public;
A的protected-------B仍是protected;
A的private----------B无法访问(仍是private);
protected继承:
A的public-----------B变成protected;
A的protected-------B仍是protected;
A的private----------B无法访问(仍是private);
private继承:
A的public-----------B无法访问(变成private);
A的protected-------B无法访问(变成private);
A的private----------B无法访问(仍是private);
派生类可以恢复继承的成员访问级别(只能恢复子类可访问的成员级别),但不能使被恢复成员的级别比他原来的还大。
{
public:
void show(){};
void show(int i){};
protected:
void log(){};
};
class item : private base
{
public:
using base::show; // 可以恢复所有重载版本到子类
using base::log; // 错误不能使被恢复成员的级别比他原来的还大
}
派生类继承基类默认级别是由派生类决定,如果派生类是struct则默认是public,若是class则是private。
class a : b // prvate 继承
struct a : b // public 继承
基类的友元关系是无法被子类继承的,所以要想基类的友元类访问子类的私有成员需要在子类中定义友元关系。
15.3 基类到派生类的转换
基类对象和派生类之间有单向转换关系。派生类可以转换成基类反过来则不允许。因为基类里的成员派生类中都包含所以转换无错,但派生类中所有对象基类并不全部包含所以转化会失败。
一个基类引用或指针指向派生类时实际执行的是派生类的代码。
一个基类对象指向派生类时会发生拷贝赋值操作,用派生类中数据成员初始化或赋值基类对应成员,而方法成员还是使用基类版本。所以这种情况下不会发生动态绑定virtual函数。
15.4 构造函数和复制控制
缺省情况下派生类创建对象时会先调用基类的默认构造函数,然后再调用自己的构造函数。
也可以在派生类构造函数中显示调用基类某个构造函数,甚至给基类构造函数传参。调用语法是
{
public:
item (int age,string name) : base(age,name),prage(age),prname(name) {}; // 调用基类构造函数并传参, 初始化本类成员
}
派生类只能调用直接基类构造函数。 如果不显示调用基类构造函数则基类一定要有默认构造函数否则会产生编译错误。
复制构造函数有点不同:子类使用合成复制构造函数则先调用基类默认构造函数再调用子类合成复制构造函数。如果定义了子类的复制构造则一定要显示调用基类赋值构造函数。否则会出现 子类成员是被复制对象副本,而基类成员却未初始化。
{
public:
item (cosnt item &it) : base(it) ... {}; // 一定要调用基类复制构造函数base(it)
}
赋值操作同复制类似,如果派生类定义了自己的赋值操作一定要显示为基类进行赋值
{
public:
item &operator=(const item &it)
{
base:: operator=(it); // 显示调用基类赋值操作
//...
};
}
析构函数无论如何总是会调用父类的析构函数。析构函数运行顺序和构造函数相反,总是先运行子类析构函数再运行父类析构函数。
{
public: ~one(){ cout << "end one" << endl;}; one{ cout << "init one" << endl;};
}
class two : public one
{
public: ~tow(){ cout << "end two" << endl;}; tow(){ cout << "init two" << endl;};
}
class three : public two
{
public: ~three(){ cout << "end three" << endl;}; three(){ cout << "init three" << endl;};
}
three b; // 此时依次输出 "init one" "init two" "init three"
one *a = &b;
// 当超过作用域时对象 b 被释放,依次输出 "end three" "end two" "end one"
当定义three *a =&b, a在回收时不会调用任何方法因为它是指针,只有释放对象b析构才能执行。
但是有一种情况输出层级和指针有直接关系:动态对象,下面代码只会执行指针对象的析构函数。
one *a = new three() ;
delete a ; // 只输出"end one"
如何才能输出 "end three" "end two" "end one"呢? 只要将类 one 中析构函数设置成虚析构函数即可 virtual ~one(){...} 。
构造函数和赋值函数不要定义成虚函数,因为会让人混淆且没有什么用处。
15.5 继承情况下的类作用域
子类可以定义和父类一样的非虚函数,此时子类会覆盖父类函数。
和虚函数动态绑定不同,调用版本并不是由指向的数据类型决定,而是由申明变量类型决定。
如果想调用父类成员需要如此调用
a.base::show(); // 调用base类的show方法
item *b =&a;
b->base::show(); // 调用base类的show方法
base *k = &a;
k->base::show(); // 变量类型是base,但实际对象是item 所以需要b->base::show();
15.6 纯虚函数
纯虚函数申明很简单 void show()=0;拥有纯虚函数的类无法定义对象,但可以定义指针或引用。假设基类 base 定义了纯虚函数。
base c ; // 错误
base *c = &b ; // 正确
base &c = b ; // 正确
15.7 容器与继承
容器对象可以定义成存放基类对象,但可以给容器加入子类对象,这时候子类会被转换成基类对象,或者说基类部分会被系统删除。
可以定义基类指针或引用类型容器,再增加子类指针活引用,这时候会更具实际内容不同执行不同代码(动态绑定)。
15.8 句柄类与继承
我们知道C++中最令人头疼的当属指针,如果您申请了对象却没有释放它,时间一长就会造成系统崩溃,大量的内存溢出使得您的程序的健壮性出现问题而句柄类就是为了能够解决这一问题而出现的,句柄类有点类似于智能指针。
好了,废话不多说,我们来看代码,首先我们来看 head.h文件的代码:
#define HEAD_H
#include<iostream>
#include<string>
using std::cout;
using std::cin;
using std::endl;
using std::string;
//基类
class Item_base
{
public:
//基类的虚函数,用于智能地复制对象
virtual Item_base* clone() const
{
return new Item_base(*this);
}
};
//子类
class Bulk_item: public Item_base
{
//子类的虚函数的重载,用于智能地复制对象
virtual Bulk_item* clone() const
{
return new Bulk_item(*this);
}
};
//句柄类
class Sales_item
{
public:
//默认构造函数,用来初始化一个引用计数器(句柄类未绑定任何对象)
Sales_item(): p(0), use(new size_t(0)) { cout << "Sales_item定义了空句柄" << endl;};
//带有一个参数的,且该参数为基类引用的构造函数
Sales_item( const Item_base &i): p(i.clone()), use(new size_t( 1 )) { cout << "Sales_item的引用计数器初始化为1" << endl; };
//复制构造函数,需要注意的是,每复制一次就需要增加引用计数一次
Sales_item( const Sales_item &i ): p(i.p), use(i.use) { ++*use;};
void show(){cout<< "user: " << *use << endl;};
//析构函数,析构的时候会判断是否能够释放指针所指向的数据
~Sales_item() { decr_use();};
//赋值操作符重载
Sales_item& operator= ( const Sales_item& );
//访问操作符重载
const Item_base* operator-> () const
{
if( p )
{
return p;
}
else
{
cout << "p指针错误" << endl;
}
};
//解引用操作符重载
const Item_base& operator* () const
{
if( p )
{
return *p;
}
else
{
//重载虚函数,用于智能地复制对象
cout << "p指针错误" << endl;
}
};
private:
//两个指针存储着引用计数器以及数据的指针
Item_base *p;
size_t *use;
//减少引用
void decr_use()
{
if(*use == 0 && p == 0)
{
cout << "空句柄无需释放任何资源"<<endl;
return;
}
cout << "在 dec_use函数中引用计数减少了,当前计数值为:" << *use - 1 << endl;
if( --*use == 0 )
{
delete p;
delete use;
cout << "在 dec_use函数中计数器减为0,释放对象" << endl;
}
};
};
//赋值操作符重载,每次复制都会增加引用计数
Sales_item& Sales_item::operator= ( const Sales_item &si )
{
//这里需要特别注意的就是待复制的对象的计数器需要加1而被赋值的对象需要减1
//增加被复制对象的引用计数
++*si.use;
//将即将被赋值的对象的引用计数减1
decr_use();
//复制指针
p = si.p;
use = si.use;
//返回
return *this;
};
#endif //HEAD_H
接下来我们来看mail.cc的代码:
int main()
{
// 被包装类(实际上包装的是这个对象的副本)
Bulk_item item;
Sales_item a(item); // 输出 : Sales_item的引用计数器初始化为1
a.show(); // 输出 : user:1
Sales_item b(a);
a.show(); // 输出 : user:2
b.show(); // 输出 : user:2
Sales_item c; // 输出 : Sales_item定义了空句柄
c.show(); // 输出 : user:0
c = b; // 输出 : 空句柄无需释放任何资源
c.show(); // 输出 : user:3
b.show(); // 输出 : user:3
a.show(); // 输出 : user:3
}
当main函数执行完毕,c最先被释放:
// 输出 : 在 dec_use函数中引用计数减少了,当前计数值为: 2
b被释放:
// 输出 : 在 dec_use函数中引用计数减少了,当前计数值为: 1
a被释放:
// 输出 : 在 dec_use函数中引用计数减少了,当前计数值为: 0
此时已经删除了被包装对象(item的副本)
最后item 对象被释放
结论:我们可以看到,句柄类能够很方便并且能够很安全地释放内存,不会导致内存的泄露。