1 数据抽象
- 为类型选择一个描述性的名字
- 列出所有可能执行的操作,避免set get
- 为类型设计接口
- 实现类型:不要让实现影响类型的接口,要实现接口所承诺的约定
2 多态
将一个多态基类想象成一个契约,包括郑重的语法承诺和不易验证的语义承诺。派生类称为转包者,实现与其客户签订的契约。
用抽象基类指针指向派生类:原则上,基类可以不知道除自己以外所有的事物,从实践角度看,对其接口的设计要考虑用户的需求,并且应该这样设计:派生类可以很容易的推知并实现基类契约,然而,基类应该对其派生类的具体细节全然不知。
和生活一样,OOP中,不知情也是一种天赐之福。
3 设计模式
设计模式不仅仅是对技术的简单描述,还是从现有的成功实践一点一滴汇集起来的设计智慧的具体封装,并以容易交流和复用的方式编写而成
使用一种设计模式时,不仅包括其中用到的技术,还包括应用该模式的动因以及应用后达到的效果
Ps..为了和别人交流,一些标准算法必须会!
在设计模式出现后,他为我们提供了一种高效,毫无歧义的描述我们的设计的方法。
设计模式的描述包含下列四个组成部分:
- 设计模式必须有一个毫无歧义的名字
- 模式描述必须定义该模式所能解决的问题
- 模式描述要说明将该模式应用于某个上下文的后果
4 STL
**STL并不仅仅是一个库,它更是一种优秀的思想以及一套约定。 **
- 容器:容纳和组织元素
- 算法:执行操作
- 迭代器:访问容器中的元素
STL的优秀思想体现在:
容器与算法之间无需彼此了解(迭代器实现),通过迭代器,容器和算法可以紧密的
协作,同时还可以保持彼此不知情
- 迭代器类似于指针
- 容器是对数据结构的一种抽象,以类模板的方式实现
- 算法是对函数的一种抽象,采用函数模板实现
5 引用时别名而非指针
我们常用指针来实现引用的功能,其实引用不是指针,他们的区别:
- 不存在空引用
- 所有引用都要初始化
- 一个引用永远指向用来对他初始化的对象
重要: - 一个引用就是引用的那个对象的别名,这个别名的属性使得引用常常成为函数形参的优秀选择
2. 我的问题:构造函数可否使用引用来接受初始化的值
我的答案:可以,要在声明时写好默认初值 构造函数:ans( const int & num = 0 ) ;
6 数组形参
- C++中不存在数组形参,在使用数组做形参时,只是将首元素的指针传入了。(术语:退化)
- 传入时,数组的边界被忽略了,如果数组边界非常重要,可以使用一个引用形参:
- 如果使用指针初始化数组那个这个技巧非法
void average( int (&ary)[12] );
int anArray[12];
int *anOther = new int[12];
average( anArray ); //正确
average( anOther ); //错误,anOther 是一个指针
- 出于这些原因,经常采用标准容器Vector,String来代替数组。
数组形参尝试使用以下用法:(使用模板)
template <int n>
void average(int (&ary)[n]) {}
int t3[12];
average<12>(t3);
7 常量指针(const pointer)与指向常量的指针(pointer to const)
T *pt = new T;
const T *pct = pt; //指向常量的指针
T *const cpt = pt; //常指针,指向pt
- const出现在星号左面是指向常量的指针,出现在右面是修饰指针的,说明一个常指针
- 上述代码的第二行:指向常量的指针现在指向一个非常量,这是合法的,因为这样不会造成危害 ,相反的转换是非法的(可是使用const_cast)
8 指向指针的指针
9 新式转型操作符
旧式转型下面隐藏着一些见不得人,鬼鬼祟祟的东西,它们的语法形式使其在一段代码中通常很难引起人们的注意,但是它们会做出一些很大的破坏,应该像躲避瘟疫一样躲避它们。
下面介绍新式的转型 符
- const_cast 添加或移除const或volatile
const Person *getEmployee( ) { ... }
...
Person *anEmployee = const_cast<Person *>(getEmployee( )); //新式转换
anEmployee = (Person *)getEmployee( ); //旧式转换
使用const_cast更好,原因: - 任何形式的转型都存在危险,它应该看上去醒目,丑陋
- o 使用旧式转型就等于告诉编译器:“你给我闭嘴,给我将getEmployee的返回类型转换成Person*”。而是用const_cast则等于告诉编译器:“我只希望将getEmployee的返回类型const去掉”
- 还有些内容..............................暂时略过
10 重要:常量成员函数的含义
深层含义:
在类的一般成员函数中使用的this指针类型为X const
在类的常量成员函数中使用的this指针类型为const X const**
11 编译器会在类中放的东西o_0?
注意:请不要对类的内部结构做低级的假定,类成员的偏移量都认为是未知
12 赋值和初始化并不相同
初始化和赋值是不同的操作,它们具有不同的用途和实现
- 赋值发生与当你赋值时,使用对象的operator =
- 除此之外所有的复制情形均为初始化:声明、函数返回、参数传递、异常中的初始化,使用对象的构造函数
13 复制操作
复制构造和复制赋值是两种不同的操作:
Handle( const Handle &); //复制构造
Handle &operator =( const Handle &); //复制赋值
他们产生的结果不应该有差别!
Ps. 编译器自动为你添加的:复制构造函数,复制赋值函数,析构函数
14 函数指针
- 用法
- 声明一个指向特定类型函数的指针:
void ( *fp ) ( int );
这个指针必须指向同类型的函数:
void h( int );
fp = h;
fp = &h; //等价
使用时:
( *fp ) ( 12 ); //显式
fp( 12 ); //隐式等价
注意:非静态成员函数的地址不是一个指针,因此不可以将一个函数指针指向一个非静态成员函数。作用
实现回调:
void stopDropRoll();
inline void jumpIn();
//...
void ( *fireAction ) () = 0;
//...
if( !fatalist ) { //如果你关心失火
//设置适当的动作,以防万一
if( nearWater )
fireAction = jumpIn;
else
fireAction = stopDropRoll;
}
一旦决定了要执行的操作,代码中的一部分就可以专注于是否以及何时去执行该活动,而无需关心这个动作到底是什么:
if( ftemp >= 451 ) { //着火啦
if( fireAction )
fireAction();
}
Ps.函数指针指向inline函数,这会导致这个函数不再被内联调用
Pss.函数指针可以指向重载函数,用来挑选匹配的函数
15 指向类成员的指针并非指针
16 指向成员函数的指针并非指针
17 处理函数和数组声明
18 函数对象(STL中侯捷翻译为仿函数functor)
有时需要一些行为类似于函数指针的东西,但函数指针显得笨拙、危险而且过时。通常最佳的方式是使用函数对象取代函数指针。
与智能指针一样,函数对象也是一个普通的类对象。智能指针重载->和*操作符,来模仿指针的行为;而函数对象类型则重载函数调用操作符( ),来创建类似于函数指针的东西。
举例:斐波那契数列
class Fib {
public:
Fib() : a0_(1),a1_(1) {}
int operator ()();
private:
int a0_,a1_;
};
int Fib::operator () {
int temp = a0_;
a0_ = a1_;
a1_ = temp + a0_;
return temp;
}
使用标准函数的调用语法来调用:
Fib fib;
cout << fib() << fib() << endl;
优势:类可以储存值得状态,如果采用函数则必须使用全局或静态变量
STL中的仿函数
传递给算法的函数性参数并不一定得是函数,可以使行为类似函数的对象,STL大量运用仿函数,也提供了一些很有用的仿函数。
1. 什么是仿函数:仿函数是泛型编程强大威力和纯粹抽象概念的又一个例证。可以说,任何东西,只要行为像函数,他就是个函数。因此,如果你定义了一个对象,行为像函数,他就可以被当做函数来用。好,那么什么才算是具备函数行为?所谓函数行为,是指可以“使用小括号传递参数,以调用某个东西”,例如
function( arg1,arg2 );
如果你指望对象也可以如此这般,就必须让他们也有可能被调用——通过小括号的运用和参数的传递。没错,这是可能的(在C++中,很少有什么是不可能的),你只需定义operator(),并给与何时的参数型别:
class X {
public:
return-value operator() (arguments) const;
...
};
2.例子与困惑:
class PrintInt {
public:
void operator() (int elem) const {
cout << elem << ' ';
}
};
int main()
{
vector<int> coll;
// insert elements from 1 to 9
for (int i=1; i<=9; ++i) {
coll.push_back(i);
}
// print all elements
for_each (coll.begin(), coll.end(), // range
PrintInt()); // operation
cout << endl;
}
注意:上面令人困惑的一句话
for_each (coll.begin(), coll.end(), PrintInt());
for_each的第三个参数是一个类的构造函数,传入的是构造好的一个类实例的指针,并不是类中重载的()方法!
进一步讨论:PrintInt是一个类型,但是我们必须传入一个该类型的对象作为函数的参数。通过在PrintInt类型名字后面附加一对圆括号,就创建了一个没名字的临时PrintInt对象,此对象仅存活于函数条用期间(匿名临时对象),也可以声明并传入一个具名对象:
PrintInt comp;
for_each (coll.begin(), coll.end(), comp);
然而,传入一个匿名临时对象更简单、更符合习惯。
3.仿函数的优点:
- 仿函数是智能型函数:仿函数可拥有成员函数和变量,这意味着仿函数拥有状态。你可以再执行期(runtime)初始化他们
- 每个仿函数都有自己的型别:由仿函数定义的每一个函数行为都有自己的型别,如此一来,你甚至可以设计自己的仿函数集成体系来完成某些特别的事情,例如在一个总体原则下确立某些特殊情况
- 仿函数通常比一般函数速度快: 就template而言,由于许多细节在编译器就已确定,所以传入一个仿函数可能获得更好的性能
19 Command模式与好莱坞法则 IMP
- 函数回调:面对一个任务,下面两者共同完成一个应用程序
- 框架知道何时去干一些事情,但具体干什么一无所知
- 框架的用户知道具体做什么,但不知道何时去做
下面看一段代码:
class Button {
public:
Button( const string &label )
: label_( label ) , action_( 0 ) {}
void setAction( void ( *newAction )( ) )
{ action_ = newAction; }
void onClick() const
{ if( action_ ) action_(); }
private:
string label_;
void ( *action_ ) ();
}
Button的用户设置回调函数,然后将Button移交给框架代码,后者可以侦测到Button何时被点击了,并执行指定的动作。上面的做法是回调的一个简单地实例,上面做法使用一个简单的函数指针作为回调有很多的限制:函数往往需要一些数据才能工作,但一个函数指针没有相关的数据。下面介绍一种更好的方式,进而产生出一种新的模式——Command模式(使用函数对象)
使用这种面相对象的方式显而易见的好处是函数对象可以封装数据,另一个好处是函数对象可以通过虚拟成员表现出动态行为。换句话说,可以拥有一个相关的函数对象的层次结构(18),第三个好处稍后再谈。
class Action {
public:
virtual ~Action();
virtual void operator ()() = 0;
virtual Action *clone() const = 0;
}
class Button {
public:
Button( const std::string &label )
: label_( label ), action_( 0 ) {}
void setAction( const Action *newAction ) {
Action *temp = newAction->clone();
delete action_;
action_ = temp;
}
void onClick() const
{ if( action_ ) ( *action_ )(); }
private:
std::string label_;
Action *action_;
};
现在Button可以喝任何一个is-a的Action的函数对象协作:
class PlayMusic : public Action {
public:
PlayMusic( const string &songFile )
: song_( song ) {}
void operator ()(); //播放歌曲
private:
MP3 song_;
};
被封装的数据既保持了PlayMusic函数对象的灵活性,也保持了它的安全性。
Button *b = new Button( "Anoko" );
auto_ptr<PlayMusic> song( new PlayMusic( "Anoko.mp3" ) );
b->setAction( song );
第三个神秘的好处是什么呢?简单地说,就是可以处理类层次结构而不是较为原始的、缺乏灵活性的结构(函数指针)。有了Command层次结构,就可以将Prototype模式和Command模式组合,从而产生可克隆的命令。按照这种套路,我们可以继续将其他模式与Command模式、Prototype模式组合使用,从而获得更大的灵活性
20 STL函数对象
没有STL,我们的日子该怎么过?STL不但使我们能够更轻松、更快捷地编写复杂的代码,而且编写的代码既标准又高度优化
std::vector<std::string> names;
//...
std::sort( name.begin(),name.end() );
STL另一个优雅之处在于高度可配置。在以上的代码中,使用string的小于操作对names进行排序,但在其他场合下,未必总有一个小于操作符可供使用,而且有时并不希望以升序方式进行排序。
进一步请参考上面18
21 重载与重写并不相同
重载与重写彼此之间没有任何关系。他们是完全不同的概念。对他们之间区别的不了解,以及对这两个术语的马虎使用,已经导致数不清的混乱和不计其数的BUG
重载发生于一个作用域内有两个或以上个同名函数,单形参不同时
重写发生于派生类函数和基类虚函数具有相同的名字和形参时
22 Template Method模式
模板方法模式和C++模板一点关系都没有。实际上,它是基类设计者为派生类设计者提供清晰指示的一种方式,这个指示就是“应该如何实现基类所规定的契约”。即使你认为这个模式应该起个不同的名字,我还是请你继续使用这个名字Template Method,使用源自标准技术术语表的模式名字,会给你带来许多好处。
基类的可以自由的通过其公有成员函数指定与外界的契约关系,并通过受保护的成员函数为派生类指明额外的细节。私有成员函数也可以用作类实现的一部分。
一个基类成员函数是否应该为非虚拟的、虚拟的或纯虚拟的,这样的决策主要是基于该函数的行为如何被派生类定制。毕竟,使用基类接口的代码并不关心对象是怎么实现一个特定操作的,它只关心对一个对象执行该操作,而恰当地实现该操作则是对象自己的事情。
如果基类成员是非虚拟的,那么基类设计者就为以该基类为跟所确立的层次结构指明了一个不变式invariant。派生类不应该用同名的派生类成员去隐藏基类的非虚函数。如果不喜欢基类所指定的契约,可以去寻找另一个合乎心意的基类,而不要试图去改写基类的契约。
虚函数和纯虚函数指定的操作,其实现可以由派生类通过重写机制定义。一个非纯虚的函数提供了一个默认实现,并且不强求派生类一定要重写它,而一个纯虚函数则必须在具体派生类中进行重写。这两种虚函数都允许派生类插入并取代其整个实现,同时保持接口不变。
Template Method模式赋予基类设计者一种中级控制机制,该控制机制介于非虚函数提供的“占有它或离开它”和虚函数提供的“如果你不喜欢就替换掉所有的东西”这两种机制之间。Template Method确立了其实现的整体架构,同时将部分实现延迟到派生类中进行。通常来说,Template Method被实现为一个公有非虚函数,它调用被保护的虚函数。派生类必须接受它所继承的非虚基类函数所指明的全部实现,同时还可以通过重写该公有函数所调用的被保护的虚函数,以有限的方式来定制起行为。
class App {
public:
virtual ~App();
//...
void startup() { //Template Method
initialize();
if ( !validate() )
altInit();
}
protected:
virtual bool validate() const = 0;
virtual void altInit();
//...
private:
void initialize();
//...
};
非虚拟的startup TemplateMethod可以向下调用派生类提供的定制实现:
class MyApp : public App {
public:
//...
private:
bool validate() const;
void altInit();
//...
};
TemplateMethod是一个是一个好莱坞法则的例子,即不要call我们,我们会Call你。startup函数的整体流程由基类决定,客户通过基类的接口来调用startup,因此派生类设计者不知道validate或aliInit何时会被调用。但他们知道当这两个方法被调用时,它们各自应该做什么。因此我们说,基类和派生类同心协力打造了了一个完整的函数实现。
23 命名空间
24 成员函数查找
编译器查找函数的名字
从可用候选者中选择最佳匹配函数
检查是否具有访问该匹配函数的权限
25 实参相依ADL技术
26 操作符函数查找
27 能力查询
28 指针比较的含义
29 虚构造函数与Prototype模式
30 Factory Method模式
31 协变返回类型
32 进制复制
复制构造函数声明为私有
33 制造抽象基类
抽象基类通常用于表示目标问题领域的抽象概念,创建这种类型的对象时没有什么意义的。我们通过至少声明一个纯虚函数使得一个基类成为抽象的,编译器将确保无人能够创建该抽象基类的任何对象。
典型方法:人为的将该类的一个虚函数指定为纯虚的。通常来说,析构函数是最佳候选者
class ABC {
public:
virtual ~ABC() = 0;
//...
};
//...
ABC::~ABC() { }
注意: 在这个例子中,为该纯虚函数提供一个实现是必不可少的,因为派生类的析构函数将会隐式地调用基类的析构函数(注意,从一个派生类析构函数内部对一个基类析构函数的隐式调用)