作者用两章分别来介绍object-based(以下简称ob) 和objected-oriented(以下简称oo),弄清并区分这两个概念是理解这两章的基础。首先我们来看这两个术语的概念:
In a technical sense, the term "object-based language" may be used to describe any programming language that is based on the idea of encapsulating state and operations inside objects.
Object-oriented programming (OOP) is a programming paradigm that uses "objects" to design applications and computer programs. It is based on several techniques, including inheritance, modularity, polymorphism, and encapsulation. [Source: Wikipedia]
由概念可见,从技术上讲ob需要支持封装,而oo除了封装之外还要支持多态和继承。也就是说ob是oo的基础,支持oo的语言一定是ob的(如c++),但支持ob的语言不一定要支持oo(比如VB有类和对象的概念但不支持继承,所以不是面向对象的)。
Object-Based Programming这一章主要是熟悉类机制的设计和运用,涉及以下关键概念:
Class constructors and destructors, member initialization list, memberwise initialization,
const member function, mutable data member,
this pointer,
static class members,
friendship,
function object,
overloaded operator,nested types,
pointers to class member functions.
1. 类就是对事物的抽象,C数据结构的概念即来源于此。设计类实质就是设计数据结构。跟C的数据结构相似,类包括数据成员(属性)和一些操作(方法)。以上是从对事物抽象的角度看类,从用户的角度讲类又可看作包括 public interface 和private impementation. 而对用户可见的只有public interface(如function prototype).
2.class和structure都是用户自定义类型,注意二者的定义最后都以一个分号(;)结束。
3.区分class declaration和class definition.如下面是一个class declaration:
class
Stack;
它的作用就是对编译器引入类的名字,但不提供它包括的数据成员和操作的细节。这样一个声明只允许你定义指向该类的指针或在类声明中用作类型说明符。
Stack
*
pt
=
0
;
void
process(
const
Stack
&
);
而在类的定义出现之前不允许你定义该类的对象或引用类的任何成员。比如下面是错误的:
Stack st;
//
invalid definition
![](https://i-blog.csdnimg.cn/blog_migrate/a41954a27d6ad96fa2c2cf816e677448.gif)
void
process(
const
Stack
&
st)
...
{
st.clear(); //invalid operation
}
下面是一个class definition.
class
Stack
...
{
public:
//... public interface
private:
//... private implementation
}
;
4. 区分member function declaration和member function definition.
member function declaration必须出现在class definition之中。如:
class
Stack
...
{
public:
bool push(const string&); //member function declaration.
//... other things go here.
private:
//... private implementation
}
;
member function definition既可以出现在class definition(.h文件)之中,也可以在class definition之外的program text(.cpp文件)中。出现在class definition中的member function definition自动做为inline函数。而出现在program text中的member function definition,函数名字前面要加一个class scope operator.如:
//
Stack.h
![](https://i-blog.csdnimg.cn/blog_migrate/a41954a27d6ad96fa2c2cf816e677448.gif)
class
Stack
...
{
public:
bool push(const string&); //memeber function declaration.
![](https://i-blog.csdnimg.cn/blog_migrate/37c8bf68cdc3cc81759c34160776bc53.gif)
bool full() ...{return _stack.size()==_stack.max_size();} //memeber function definition inside class definition, automatically treated as being inline
//other public things go here..
private:
vector<string> _stack;
}
;
![](https://i-blog.csdnimg.cn/blog_migrate/6810355c2f78c12e91b7997a8e8c583a.gif)
//
Stack.cpp
//
memeber function definition outside class definition.
![](https://i-blog.csdnimg.cn/blog_migrate/a41954a27d6ad96fa2c2cf816e677448.gif)
bool
Stack::push(
const
string
&
elem)
...
{
if(full()) return false;
_stack.push_back(elem);
return true;
}
5. 构造函数和析构函数都没有返回值。构造函数可以有参数,所以可以重载,而析构函数不允许有参数,所以不能重载。定义析构函数不是必须的,它主要用来释放在构造类时申请的空间,从而使类的用户不必知道内存管理的细节。
6.default memberwise initialization的缺陷和解决办法。
当我们用一个类的对象去初始化另一个对象时,类的data members会依次拷贝。 但当类的data members中包含指向在类初始化时申请的空间的指针时,指针值的拷贝只会使两个对象中的指针指向同一空间,而不是申请新的空间,在该空间销毁时会出现问题。比如:
class
Matrix
...
{
public:
Matrix(int row, int col)
:_row(row), _col(col)
![](https://i-blog.csdnimg.cn/blog_migrate/37c8bf68cdc3cc81759c34160776bc53.gif)
...{
_pmat=new double[row*col];
}
~Matrix
![](https://i-blog.csdnimg.cn/blog_migrate/37c8bf68cdc3cc81759c34160776bc53.gif)
...{
delete[] _pmat;
}
private:
int _row,_col;
double *_pmat;
}
;
![](https://i-blog.csdnimg.cn/blog_migrate/6810355c2f78c12e91b7997a8e8c583a.gif)
//
Example
![](https://i-blog.csdnimg.cn/blog_migrate/a41954a27d6ad96fa2c2cf816e677448.gif)
...
{
Matrix mat(4,4);
![](https://i-blog.csdnimg.cn/blog_migrate/37c8bf68cdc3cc81759c34160776bc53.gif)
...{
Matrix mat2=mat;
//default memberwise copy applied
//... use mat2 here
//destructor applied here for mat2
}
// ... use mat here
//destructor applied here for mat
}
default memberwise initialization使两个实例中的_pmat指向了堆中的同一数组。当mat2析构的时候,数组空间被释放。但是之后,mat的_pmat仍然指向和操作已经释放了的数组空间,这是一个严重的bug。
这就要去我们在设计类的时候考虑default memberwise initialization的行为对类来说是否合理,不合理的我们就要提供explicit copy constructor.如
Matrix::Matrix(
const
Matrix
&
rhs)
:_row(rhs._row), _col(rhs._col)
![](https://i-blog.csdnimg.cn/blog_migrate/a41954a27d6ad96fa2c2cf816e677448.gif)
...
{
int elem_cnt=_row*_col;
_pmat=new double[elem_cnt];
for(int ix=0; ix<elem_cnt; ++ix)
_pmat[ix]=rhs._pmat[ix];
}
或者copy assignment operator 如:
Matrix
&
Matrix::
operator
=
(
const
Matrix
&
rhs)
![](https://i-blog.csdnimg.cn/blog_migrate/a41954a27d6ad96fa2c2cf816e677448.gif)
...
{
if(this!=&rhs)
![](https://i-blog.csdnimg.cn/blog_migrate/37c8bf68cdc3cc81759c34160776bc53.gif)
...{
_row=rhs._row; _col=rhs._col;
int elem_cnt=_row*_col;
![](https://i-blog.csdnimg.cn/blog_migrate/6a9c071a08f1dae2d3e1c512000eef41.gif)
delete[] _pmat;
_pmat=new double[elem_cnt];
for(int ix=0; ix<elem_cnt; ++ix)
_pmat[ix]=rhs._pmat[ix];
}
return *this;
}
7.为了保证const class object不被修改,则必须保证它调用的方法不会修改其调用者。为了让编译器知道这些方法不会修改其调用者,类设计者必须对这些方法标为const。注意const修饰符出现在参数列表后面。定义在类体外的成员函数,必须在声明和定义处都标上const修饰符。const class object只能调用标为const的方法,而不能调用公开接口中的non-const成分。
另外,const member function返回data member的non-const reference也是不允许的,因为data member可能在程序的其他地方被修改。member functions可以根据const与否而重载(另外,函数可以根据参数多少和类型不同重载,但不可以根据返回值类型不同重载),所以可以可以提供两个版本的成员函数,一个是const member function返回data member的const reference,另一个是non-const member function返回data member 的non-const reference。这样对于一个non-const class object, non-const 版本被调用。对于const class object, const版本被调用。例如:
class
val_class
...
{
public:
val_classs(const BigClass &v)
![](https://i-blog.csdnimg.cn/blog_migrate/37c8bf68cdc3cc81759c34160776bc53.gif)
:_val(v)...{}
![](https://i-blog.csdnimg.cn/blog_migrate/6a9c071a08f1dae2d3e1c512000eef41.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/37c8bf68cdc3cc81759c34160776bc53.gif)
const BigClass& val() const ...{return _val}
![](https://i-blog.csdnimg.cn/blog_migrate/37c8bf68cdc3cc81759c34160776bc53.gif)
BigClass& val() ...{return _val;}
private:
BigClass _val;
}
;
![](https://i-blog.csdnimg.cn/blog_migrate/6810355c2f78c12e91b7997a8e8c583a.gif)
void
example(
const
BigClass
*
pbc, BigClass
&
rbc)
![](https://i-blog.csdnimg.cn/blog_migrate/a41954a27d6ad96fa2c2cf816e677448.gif)
...
{
pub->val(); //invokes const instance
rbc.val(); // invokes non-const instance
//...
}
把一个data member标为mutable则意味着对它的修改不会影响该类对象的const-ness。这种data member一般起辅助作用,而其本身并不是事物属性的抽象。如果一个member function修改了mutable data member仍可以标为const。
8. 在成员函数内部,this 指针指向调用该成员函数的对象。在内部实现上,编译器为每个成员函数添加一个参数即this指针。该成员函数被调用的时候,编译器会自动添加一个参数即调用该成员函数的对象的地址。如:
Triangular
&
Triangualr::copy(
const
Triangular
&
rhs)
![](https://i-blog.csdnimg.cn/blog_migrate/a41954a27d6ad96fa2c2cf816e677448.gif)
...
{
_length=rhs.length;
_beg_pos=rhs._beg_pos;
_next=rhs.beg_pos-1;
return *this;
}
//
Invoke
Triangular tr1(
8
);
Triangular tr2(
8
,
9
);
tr1.copy(tr2);
![](https://i-blog.csdnimg.cn/blog_migrate/6810355c2f78c12e91b7997a8e8c583a.gif)
//
Internal Member Function Transformation
Triangular
&
Triangualr::copy(Trangular
*
this
,
const
Triangular
&
rhs)
![](https://i-blog.csdnimg.cn/blog_migrate/a41954a27d6ad96fa2c2cf816e677448.gif)
...
{
this->_length=rhs.length;
this->_beg_pos=rhs._beg_pos;
this->_next=rhs.beg_pos-1;
return *this;
}
//
inter code transformation:
//
tr1 become the class object addressed by this pointer
tr1.copy(
&
tr1,tr2);
与此类似,作为函数返回值的对象,也被大多数C++编译器优化为一个额外的引用参数。
9. Static Class Members.(static data members, static function members)
static data members跟非成员函数里的局部静态变量类似,static data members为所有该类的对象所共享的单一实例。因为只有一个实例,我们必须在program text中对该实例进行显式的定义,必要的话也可以对他进行初始化。
class
Triangular
...
{
public:
//...
private:
static vector<int> _elems;
static int _initial_size;
}
;
![](https://i-blog.csdnimg.cn/blog_migrate/6810355c2f78c12e91b7997a8e8c583a.gif)
//
placed in program text file, such as Triangular.cpp
vector
<
int
>
Triangular::elems;
![](https://i-blog.csdnimg.cn/blog_migrate/6810355c2f78c12e91b7997a8e8c583a.gif)
//
an initial value, if desired, can also be specified
int
Triangular::_initial_size
=
8
;
const static data member 可以在它的声明中进行初始化。
class
intBuffer
...
{
public:
//...
private:
static const int _buf_size=1024; //ok
int _buffer[_buf_size]; //ok
}
;
non-static member function通过this指针访问存贮在调用它的对象内部的non-static data member. 而对于那些只访问static data member而不访问non-static data member的成员函数,它的调用可以独立与特定的对象,这就是所谓的static member function。当static class member定义在类体之外时,关键字static不必重复。
10.运算符重载的规则:
(1).不能引入新的运算符
(2)不能改变运算符的元数
(3)不能改变运算符已有的优先级
(4)不能为非class类型重新定义已有的运算符
一个运算符可以被定义为member operator function,也可以被定义为nonmember operator function。nonmember operator function往往比member operator function多一个参数,因为对于member function来说,this指针隐喻代表做操作数。对于iostream operator最好定义为nonmember function。因为memeber function要求左操作数是该类的对象,这样会使用户感到迷惑:比如
ostream
&
Triangular::
operator
<<
(ostream
&
os)
![](https://i-blog.csdnimg.cn/blog_migrate/a41954a27d6ad96fa2c2cf816e677448.gif)
...
{
//...
return os;
}
![](https://i-blog.csdnimg.cn/blog_migrate/6810355c2f78c12e91b7997a8e8c583a.gif)
Triangular tri(
6
,
3
);
tri
<<
cout
<<
'
/n
'
;
//
confusing
下面是定义为nonmember function:
ostream
&
operator
<<
(ostream
&
os,
const
Triangular
&
rhs)
![](https://i-blog.csdnimg.cn/blog_migrate/a41954a27d6ad96fa2c2cf816e677448.gif)
...
{
//.....
return os;
}
![](https://i-blog.csdnimg.cn/blog_migrate/6810355c2f78c12e91b7997a8e8c583a.gif)
Triangular tri(
6
,
3
);
cout
<<
tri
<<
'
/n
'
;
注意函数里,我们返回了传入的同一ostream对象,这样的目的是可以连接使用多个output操作符。
11. increment operator function有prefix(++object)和postfix(object++)两种个版本。prefix版本定义时参数列表为空。但是,每个重载的运算符必须有一个唯一的参数列表,所以postfix版本就不能再用空参数列表。C++语言给出的解决方案是用一个int类型做参数来定义postfix版本。但是在调运的时候我们不必给出这个参数,而是由编译器自动生成。
12. 嵌套类型(Nested Types)其实就是在Class definition内的typedef,其作用范围在只在类内部。可以利用它给一种已有类型起一个别名。这样我们可对不同的类定义用相同的名称嵌套在类里,从而提供了一种抽象机制,让这些类可以有统一的操作接口。如:
class
Triangular
...
{
public:
typedef Triangular_iterator iterator;
//......
private:
//......
}
;
![](https://i-blog.csdnimg.cn/blog_migrate/6810355c2f78c12e91b7997a8e8c583a.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/a41954a27d6ad96fa2c2cf816e677448.gif)
class
Fibonacci
...
{
public:
typedef Fibonacci_iterator iterator;
//......
private:
//......
}
;
![](https://i-blog.csdnimg.cn/blog_migrate/6810355c2f78c12e91b7997a8e8c583a.gif)
Triangular::iterator fit
=
fib.begin();
//
instead of Triangular::Triangular_iterator fit=fib.begin();
Fibonacci::iterator trt
=
tri.begin();
//
instead of Fibonacci::Fibonacci_iterator trt=tri.begin();
13. friend 的位置不受public,private的影响。要想把重载函数的多个版本都声明为某类的友元,必须逐一列出。把类A的几个成员函数声明为类B的友元,必须在定义类B之前给出类A的定义。如果是把整个类A声明为B的友元,则之前只需给出类A声明即可。如:
//
example1
![](https://i-blog.csdnimg.cn/blog_migrate/a41954a27d6ad96fa2c2cf816e677448.gif)
class
Triangular_iterator
...
{
public:
int operator*() const;
//......
private:
void check_integrity() const;
}
;
![](https://i-blog.csdnimg.cn/blog_migrate/6810355c2f78c12e91b7997a8e8c583a.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/a41954a27d6ad96fa2c2cf816e677448.gif)
class
Triangular
...
{
friend int Triangular_iterator::operator*();
friend void Triangular_iterator::check_intergrity();
//......
}
;
//
---------------------------------------------------------------------
//
example2
class
Triangular_iterator;
![](https://i-blog.csdnimg.cn/blog_migrate/6810355c2f78c12e91b7997a8e8c583a.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/a41954a27d6ad96fa2c2cf816e677448.gif)
class
Triangular
...
{
friend class Triangular_iterator;
//......
}
;
![](https://i-blog.csdnimg.cn/blog_migrate/6810355c2f78c12e91b7997a8e8c583a.gif)
应用Friendship基本上是由于性能的原因,比如在非成员函数中Point和Matrix的相乘。如果仅仅是为了读写data member可以为这些data member写一个inline public access function来访问它们,以替代friendship。
14.Function object是一个实现了函数调用运算符重载的类。当编译器遇到一个貌似是函数调用的东西比如:
f可以是函数、函数指针或者Function Object。 如果f是一个类的对象的话,编译器会在内部把这条语句变成:
f.
operator
()(ival);
//
internal transformation
我们通常把function object作为泛型算法的参数使用。
class
LessThan
...
{
public:
![](https://i-blog.csdnimg.cn/blog_migrate/37c8bf68cdc3cc81759c34160776bc53.gif)
LessThan(int val):_val(val)...{}
![](https://i-blog.csdnimg.cn/blog_migrate/37c8bf68cdc3cc81759c34160776bc53.gif)
int comp_val() const ...{return _val;}
![](https://i-blog.csdnimg.cn/blog_migrate/37c8bf68cdc3cc81759c34160776bc53.gif)
void comp_val(int nval)...{_val=nval;}
bool operator() (int value) const;
private:
int _val;
}
;
![](https://i-blog.csdnimg.cn/blog_migrate/6810355c2f78c12e91b7997a8e8c583a.gif)
inline
bool
LessThan::
operator
()(
int
value)
const
![](https://i-blog.csdnimg.cn/blog_migrate/a41954a27d6ad96fa2c2cf816e677448.gif)
...
{return value<_val;}
![](https://i-blog.csdnimg.cn/blog_migrate/6810355c2f78c12e91b7997a8e8c583a.gif)
void
print_less_than(
const
vector
<
int
>
&
vec,
int
comp, ostream
&
os
=
cout)
![](https://i-blog.csdnimg.cn/blog_migrate/a41954a27d6ad96fa2c2cf816e677448.gif)
...
{
LessThan lt(comp);
vector<int>::const_interator iter=vec.begin();
vector<int>::const_interator it_end=vec.end();
os<<"elements less than "<<lt.com_val()<<endl;
while((iter=find_if(iter,it_end,lt))!=it_end)
![](https://i-blog.csdnimg.cn/blog_migrate/37c8bf68cdc3cc81759c34160776bc53.gif)
...{
os<<*iter<<' ';
++iter;
}
}
15. 类成员函数指针跟普通的函数指针类似必须指明返回类型和参数列表,却也有几点不同:
(1)声明的时候必须在指明它是指向哪个类的成员函数,须在前面加一个class scope operator,
void
(num_sequence::
*
pm)(
int
)
=
0
;
pm初始化为0,指明它现在还没指向任何成员函数。也可以用typedef简化声明。
typedef
void
(num_sequence::
*
PtrType)(
int
);
PtrType pm
=
0
;
(2).指向成员函数的指针必须通过该成员函数所在类的实例调用,如:
num_sequence ns;
num_sequence
*
pns
=&
ns;
PtrType pm
&
num_sequence::fibonacci;
![](https://i-blog.csdnimg.cn/blog_migrate/6810355c2f78c12e91b7997a8e8c583a.gif)
(ns.
*
pm)(pos);
//
equivalent to ns.fibonacci(pos);
(pns
->*
pm)(pos);
//
equivalent to pns->fibonacci(pos);