第15章:面向对象编程
1. 简介
面向对象编程基于三个基本概念:数据抽象,继承,动态绑定。在C++中,用类进行数据抽象,用类派生从一个类继承另一个类:派生类继承基类的成员。动态绑定使编译器能够在运行时决定是使用基类中定义的函数还是派生类中定义的函数。
2. 面向对象编程
2.1 继承
派生类能够继承基类定义的成员,派生类可以无须改变而使用那些与派生类型具体特性不相关的操作,派生类可以重定义那些与派生类型相关的成员函数,将函数特化,考虑派生类型的特性,最后,除了基类继承的成员外,派生类还可以定义更多的成员。
在C++中,基类必须指出希望派生类重定义哪些函数,定义为virtual的函数是基类期待派生类重新定义的,基类希望派生类继承的函数不能定义为虚函数。
2.2 动态绑定
通过动态绑定,我们能够编写程序使用继承层次中任意类型的对象,无须关心对象具体类型。使用这些类的程序无须区分函数是在基类还是在派生类中定义的。
在C++中,通过基类引用或者指针调用虚函数时,发生动态绑定。引用或者指针既可以指向基类对象也可以指向派生类对象,这一事实是动态绑定的关键。用引用或者指针调用的虚函数在运行时确定,被调用的函数是引用或指针所指对象的实际类型所定义的。
动态绑定需要符合两个条件:调用函数必须是virtual ;必须要通过指针或引用调用虚函数。 动态绑定时执行函数取决于实际执行的类型,而不取决于指针或引用变量类型。
// item 是子类
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函数版本如:
item b;
base *a = &b;
a->base::show(); // 指针指定调用base版方法
base &a = b;
a.base::show(); // 引用指定调用base版方法
2.3 定义基类和派生类
函数可以设定默认默认参数,默认参数定义的顺序为自右到左。即如果一个参数设定了缺省值时,其右边的参数都要有缺省值
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);
派生类可以恢复继承的成员访问级别(只能恢复子类可访问的成员级别),但不能使被恢复成员的级别比他原来的还大。
class base
{
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函数。
2.4 构造函数和复制控制
缺省情况下派生类创建对象时会先调用基类的默认构造函数,然后再调用自己的构造函数。
也可以在派生类构造函数中显示调用基类某个构造函数,甚至给基类构造函数传参。调用语法是
class item : public base
{
public:
item (int age,string name) : base(age,name),prage(age),prname(name) {}; // 调用基类构造函数并传参, 初始化本类成员
}
派生类只能调用直接基类构造函数。 如果不显示调用基类构造函数则基类一定要有默认构造函数否则会产生编译错误。
复制构造函数有点不同:子类使用合成复制构造函数则先调用基类默认构造函数再调用子类合成复制构造函数。如果定义了子类的复制构造则一定要显示调用基类赋值构造函数。否则会出现 子类成员是被复制对象副本,而基类成员却未初始化。
class item : public base
{
public:
item (cosnt item &it) : base(it) ... {}; // 一定要调用基类复制构造函数base(it)
}
赋值操作同复制类似,如果派生类定义了自己的赋值操作一定要显示为基类进行赋值
class item : public base
{
public:
item &operator=(const item &it)
{
base:: operator=(it); // 显示调用基类赋值操作
//...
};
}
析构函数无论如何总是会调用父类的析构函数。析构函数运行顺序和构造函数相反,总是先运行子类析构函数再运行父类析构函数。
class one
{
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(){...} 。
构造函数和赋值函数不要定义成虚函数,因为会让人混淆且没有什么用处。
2.5 继承情况下的类作用域
子类可以定义和父类一样的非虚函数,此时子类会覆盖父类函数。
和虚函数动态绑定不同,调用版本并不是由指向的数据类型决定,而是由申明变量类型决定。
如果想调用父类成员需要如此调用
itm a;
a.base::show(); // 调用base类的show方法
item *b =&a;
b->base::show(); // 调用base类的show方法
base *k = &a;
k->base::show(); // 变量类型是base,但实际对象是item 所以需要b->base::show();
2.6 纯虚函数
纯虚函数申明很简单 void show()=0;拥有纯虚函数的类无法定义对象,但可以定义指针或引用。假设基类 base 定义了纯虚函数。
base c ; // 错误
base *c = &b ; // 正确
base &c = b ; // 正确
2.7 容器与继承
容器对象可以定义成存放基类对象,但可以给容器加入子类对象,这时候子类会被转换成基类对象,或者说基类部分会被系统删除。
可以定义基类指针或引用类型容器,再增加子类指针活引用,这时候会更具实际内容不同执行不同代码(动态绑定)。
2.8 句柄类与继承
我们知道C++中最令人头疼的当属指针,如果您申请了对象却没有释放它,时间一长就会造成系统崩溃,大量的内存溢出使得您的程序的健壮性出现问题而句柄类就是为了能够解决这一问题而出现的,句柄类有点类似于智能指针。
好了,废话不多说,我们来看代码,首先我们来看 head.h文件的代码:
#ifndef HEAD_H
#define HEAD_H
#include
#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 << "空句柄无需释放任何资源"< 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的代码:
#include"head.h"
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
在 dec_use函数中计数器减为0,释放对象
此时已经删除了被包装对象(item的副本)
最后item 对象被释放
结论:我们可以看到,句柄类能够很方便并且能够很安全地释放内存,不会导致内存的泄露