const char * p //常量指针,不能通过p改变变量的值,但可以通过别的方式改变,p可以变。
char const * p //跟第一个一样
char * const p //指针常量,p不能指向别处了
const int* const p=&n2; //指向的变量值不能变,也不能指向别处
const_iterator //相当于常量指针
将东西声明为const可以帮助编译器检查出错误用法。const可以施加于任何作用域的对象,函数参数,函数返回类型,成员函数本体
区别bitwise constness和conceptual constness。前者是编译器强制实现,后者是概念上,成员函数声明了const就不应该以任何方式修改成员变量。
class CTextBlock{
public:
...
char* operator[](std::size_t position)const
{return pText[position];}
private:
char* pText;
};
//上述类可以通过编译,但在这样调用下,仍会更改成员变量的值
const CTextBlock cwd("hello");
char* pc = &cwd[0];
*pc = 'j';
//这样就产生了"jello"
当const和non-const成员函数有实质等价的实现的时候,让non-const版本调用const版本避免代码重复。
为内置型对象进行手工初始化,因为c++不保证初始化它们
构造函数最好使用初值列,而不要再构造函数北部体内使用赋值操作。(那样的话是先用默认构造函数,然后赋新值)初值列列出的成员变量,其排列次序应该和它们在class中的声明次序相同。
为免除“跨编译单元的初始化顺序”问题,用local static对象替换non-local static对象
一般来说,在函数内部的static对象都叫做”local-static”,以外的叫做”non-local-static”。现假设有两个cpp源文件,各自的类里有一个non-local-static对象,其中一个的初始化动作会使用到另一个static,但是被使用的对象可能还未初始化,此时会发生错误。
将两个类各自的non-local-static搬到属于自己的函数里,将它们变成local-static,函数会返回对象的引用。此操作保证了调用函数时,此函数包含的static对象一定会被初始化。很棒的是,只要你还未调用此函数,就不会有构造与析构成本。因为c++保证,函数内的local static对象会在该函数被调用器件,首次遇上该对象之定义式时被初始化。(对标单例模式的懒汉模式)
class Empty
{
public:
Empty(); //缺省默认构造函数
Empty(const Empty&);//拷贝构造函数
~Empty(); //析构函数
Empty& perator=(const Empty&); //赋值运算符
Empty* operator&(); //取址运算符
const Empty* operator&() const; // 取址运算符
};
为了不使用编译器自动提供的函数,可以将其声明为private并且不予实现。
在构造和析构过程中,因为derived的成员变量处于未定义状态,虚函数并不会像期望的那样调用derived class的版本。故而出现混乱。
reference to *this就是指向自己的对象。就是把自己本身返回回来。这样做的目的是使这样的连锁赋值合理
x=y=z=15;
例如a[i]=a[j];
就存在潜在的自我赋值的可能。
//假设有这样一个类
class Bitmap {...};
class Widget{
...
private:
Bitmap* pb;
}
Widget& Widget::operator=(const Widget& rhs){
if(this==&rhs) return *this;//证同测试,若在自我赋值,直接返回就完了
delete pb; //若没有上一行,此处就把要赋值的东西给删掉了。
pb= new Bitmap(*rhs.pb); //若没有上一行,这里的pb是一个野指针
return *this;
}
解决办法一个是证同测试,或者先copy再swap
当编写一个copy函数的时候,确保
class PriorityCustomer : public Customer {
public:
PriorityCustomer(const PriorityCustomer& rhs)
: Customer(rhs), // 调用base class的copy构造函数
priority(rhs.priority)
{
logCall("PriorityCustormer copy constructor");
}
....
};
为防止资源泄露,使用RAII对象,它们在构造函数中获得资源并在析构函数中释放资源
Resource-Acquisition Is Initialization;资源取得时机就是初始化时机。RAII的存在就是为了确保资源释放一定会发生。比如智能指针(shared_ptr, auto_ptr),懒汉单例模式。我们在获得资源的同一个语句内以它初始化某个管理对象或者赋值给某个管理对象,总之资源在获得的同时立刻被放进管理对象中。一旦对象被销毁,其析构函数会自动调用,于是资源被释放。
RAII的一个问题就是,被复制会发生什么事。一般有两种处理方式,禁止复制或者在底层加上引用计数法。
API往往要求访问原始资源(shared_ptr指向的东西),所以每一个RAII class应该提供一个“取得其管理的资源”的方法
对原始资源的访问可能经由显示转换或者隐式转换。一般而言显示转换比较安全,但隐式转换对客户比较方便
//有这样的api函数
FontHandle getFont();
void releaseFont(FontHandle fh);
void changeFontSize(FontHandle f,int newsize);
class Font{
public:
explicit Font(FontHandle fh):f(fh){}
~Font() {releaseFont (f);}
FontHandle get() const { return f;}//显式转换函数
operator FontHandle() const { return f;}//隐式转换函数
private:
FontHandle f;
}
//客户可以这样调用
Font f(getFont());
int newFontSize;
changeFontSize(f.get(),newFontSize);//调用显式转换函数
changeFontSize(f,newFontSize);//调用隐式转换函数
对原始资源的访问与class的封装是相悖的,但是RAII的类用意是在资源管理而不是封装。
new[]对应delete[]
new对应delete
processWidget(std::shared_ptr<Widget>(new Widget),priority());
考虑上述语句,编译器可能是这样的执行顺序
那么当priority()调用导致异常,那么new Widget返回的指针丢失了,就发生资源泄露。应改为
std::shared_ptr<Widget> pw(new Widget);
processWidget(pw,priority());
尽量以pass-by-reference-to-const替换pass-by-value,比较高效(pass-by-value方式的参数传递成本是一次构造函数和一次析构函数的调用),并且可以避免切割问题
以上规则并不使用内置类型,以及STL迭代器,和函数对象。它们采用pass-by-value更合适(其实采用pass-by-reference-to-const也可以)
pass-by-reference往往以指针的形式实现
void diaplay(baseclass a){
...
}
//这样一个函数,传进去的哪怕是一个derivedclass,里面的虚函数调用,仍然是调用的基类版本,因为已经复制过了。
void diaplay(const baseclass& a){
...
}
//这样以引用调用,就会调用子类的函数版本。
//const用于修饰参数不会在函数内被修改
不要返回pointer或者reference指向一个on stack对象(被析构)
不要返回pointer或者reference指向一个on heap对象
书上举的例子是operator * 无法处理 w=x*y*z
这样的情况。
不要返回pointer或者reference指向local static对象,却需要多个这样的对象(static只能有一份,后面的使用会覆盖前面的)
修改了一个public成员变量,所有使用它的用户代码都需要改变。
修改了一个protected成员变量,所有derived classes都需要重构。
以上二者都有不可预知的大量代码被改变。
而修改private成员变量,只需要修改使用它的成员函数。客户最多只需要重新编译。
可增加封装性和包裹弹性以及机能扩充性。
class Rational{
public:
Rational(int numerator = 0,int denominator = 1);//故意不放explicit,允许从int隐式转换
int numerator() const;
int denominator() const;
const Rational operator*(const Rational& rhs) const;//以成员函数形式定义
private:
...
}
//客户调用如下
Rational oneEight(1,8);
Rational oneHalf(1,2);
Rational result;
result = oneHalf*2;//可以调用
result = 2*oneHalf;//不可以
//本条款给出的解决办法
const Rational operator*(const Rational& lhs,const Rational& rhs);//non-member函数
result = oneHalf*2;//可以调用
result = 2*oneHalf;//也可以
当std::swap效率不高(std::swap调用拷贝构造函数和赋值操作符,如果是深拷贝,效率不会高),提供一个swap成员函数,并确定不会抛出异常。
class Obj{
Obj(const Obj&){//深拷贝}
Obj& operator= (const Obj&){深拷贝
private:
OtherClass *p;//深拷贝时会拷贝OtherClass里的内容,实则没有必要,此时就需要自己写swap
};
如果提供一个member swap,也该提供一个non-member swap用来调用前者.对于class而不是class template,还需要特化std::swap
class WidgetImpl{
public:
...
private:
int a,b,c;
std::vector<double> v;//此数据在swap时应避免交换
};
class Widget{
public:
Widget (const Widget& rhs);
Widget& operator=(const Widget& rhs){
...
*pImpl = *(rhs.pImpl);
...
}
void swap(Widget& other){
using std::swap;//这样可以让编译器自己决定调用哪个swap,万一用户没有实现针对Obj的swap,还能调用std::swap
swap(pImpl,other.pImpl);
}
...
private:
WidgetImpl* pImpl;
};
//特化版本
template<>
void swap<Widget>(Widget& a,Widget& b){
a.swap(b);//置换Widget,调用membet swap就可以了
}
调用swap时应该针对std::swap使用using声明式,然后调用swap不带任何"命名空间修饰”
不要往std命名空间里面加东西
请记住:
第一,成员变量的封装性最多只等于“返回其reference”的函数的访问级别。第二,如果const成员函数传出一个reference,后者所指数据与对象自身有关联,而它又被存储于对象之外,那么这个函数的调用者可以修改那笔数据。
避免返回handles(包括references、指针、迭代器)指向对象内部。遵守这个条款可增加封装性,帮助 const 成员函数的行为像个 const,并将发生“虚吊号码牌”(dangling handles)的可能性降至最低。
异常安全函数(Exception-safe functions)提供以下三个保证之一:
copy and swap。原则很简单:为你打算修改的对象(原件)做出一份副本,然后在那副本身上做一切必要修改。若有任何修改动作抛出异常,原对象仍保持未改变状态。待所有改变都成功后,再将修改过的那个副本和原对象在一个不抛出异常的操作中置换(swap)。
考虑这样一个类
#include
#inlcude "date.h"
#include "address.h"
class Person{
public:
Person(const std::string & name,const Date & birthday,const Address &addr);
std::string name() const;
std::string birthDate() const;
std::string address() const;
...
private:
std::string theName;
Date theBirthdate;
Address theAddress;
}
这样写在person定义文件和其含入文件之间形成了一种编译依存关系,如果这些头文件中的任何一个改变,或者头文件所依赖的其他头文件有任何改变,每一个含入Person class的文件也必须重新编译。
可以将其分隔为两个class,一个提供接口,一个负责实现
#include
#include
class PersonImpl;
class Date;
class Addressl;
class Person{
public:
Person(const std::string & name,const Date & birthday,const Address &addr);
std::string name() const;
std::string birthDate() const;
std::string address() const;
...
private:
std::tr1::shared_ptr<PersonImpl> pImpl;//指针,指向实现的类
}
实现程序这样写
#include "Person.h"
#include "PersonImpl.h"
Person::Person(const std::string & name,const Date & birthday,const Address &addr):
pImpl(new PersonImpl(name,birthday,addr))
{}
std::string Person::name() const{
return pImpl->name();
}
这样Dates,Address以及Persons的实现分离,任何修改都不需要person的客户端重新编译。
class Person{
public:
virtual ~Person();
virtual std::string name() const = 0;
virtual std::string birthDate() const = 0;
virtual std::string address() const = 0;
...
static std::tr1::shared_ptr<Person>
create(const std::string&name,
const Date& birthday,
const Address &addr);
//create函数就是工厂函数,返回指针指向动态分配的对象,该对象他又支持person(Interface class)的接口
...
};
客户可以这样使用
std::string name;
Date dateOfBirth;
Address address;
...
std::tr1::shared_ptr<Person> pp(Person::create(name,dateOfBirth,address));
...
std::cout<<pp->name()<<"was born on"<<pp->birthDate()<<"and now lives at"<<pp->address();
...
//当pp离开作用域,会自动消除
Interface class会有一个或多个derived class.如RealPerson
class RealPerson: public Person{
public:
RealPerson(const std::string & name,const Date & birthday,const Address &addr)
:thename(name),theBirthDate(birthday),theAddress(addr)
{}
virtual ~RealPerson() {}
std::string name() const;
std::string birthDate() const;
std::string address() const;
...
private:
std::string theName;
Date theBirthDate;
Address theAddress;
}
//有了RealPerson之后,写create就简单了
std::tr1::shared_ptr<Person> create(const std::string&name,
const Date& birthday,
const Address &addr)
{
return std::tr1::shared_ptr<Person>(new RealPerson(name,birthday,addr));
//真正的实现不会这么简单,应该有不同的derived class对象,在不同的情况下创建,均在该函数内实现。
}
public inheritance(公开继承)意味 “is-a”(是一种)的关系。
“public继承”意味is-a。适用于base classes身上的每一件事情一定也适用于derived classes身上,因为每一个derived class对象也都是一个base class对象。
class Base{
private:
int x;
public:
virtual void mf1() = 0;
virtual void mf1(int);
virtual void mf2(int);
...
};
class Derived:public Base{
public:
//使用using 声明式让Base class内名为mf1,mf2的所有东西在Derived作用域内都可见。
using Base::mf1;
using Base::mf2;
void mf1();
}
//客户使用
Derived d;
int x;
d.mf1();//调用Derived::mf1
d.mf1(x);//调用Base::mf1;若没有using声明则会报错,因为被mf1被覆盖了,而参数列表不同。
/*------------------------------------------------------------------------------------------------------------------------------------*/
//转交函数的设计方法
class Derived:private Base{
public:
virtual void mf1(){
Base::mf1();
}
void mf1();
}
//这样可以只继承无参数版本
//客户使用
Derived d;
int x;
d.mf1();//调用Derived::mf1
d.mf1(x);//错误!
函数接口(function interfaces)继承和函数实现(functionimplementations)继承。
pure virtual函数、simple (impure) virtual函数、non-virtual函数之间的差异,使你得以精确指定你想要derived classes继承的东西:只继承接口,或是继承接口和一份缺省实现,或是继承接口和一份强制实现。
接口继承和实现继承不同。在public继承之下,derived classes总是继承baseclass的接口。
注意,纯虚函数要求其在derived classes中重新声明,但是也可以有自己的实现。
这是一种设计手法,令客户通过调用public non-virtual成员函数间接调用private virtual函数,称为non-virtual interface(NVI)手法.是模板方法设计模式的一个表现形式。
我们来看下面这段短小的C++程序
#include
using namespace std;
class Base {
public:
void f() {
...
//可以进行事前工作,在virtual函数真正工作之前设定场景,锁定互斥锁,设定日志,验证约束条件等
g();
...
//可以进行事后工作,在virtual函数真正工作之后收尾
}
private:
virtual void g() {
cout << "Hi, MorningStar! I am g() of Base!." << endl;
}
};
class Derived : public Base {
private:
virtual void g() {
//在继承类里重新定义virtual函数的实现
cout << "Hi, MorningStar! I am g() of Derived." << endl;
}
};
int main() {
Base *pB = new Derived();
pB->f();
delete pB;
return 0;
}
程序很简单,我们在基类“Base”中,通过公有方法f调用了虚拟的私有方法g,而继承自Base的Derived类中覆写(override)了私有的方法g。
先不考虑其他,语法上有问题吗?没有;编译能通过吗?能。那运行结果是什么呢?既然编译能通过,那么结果也就能猜到了,应该是:
运行结果:Hi, MorningStar! I am g() of Derived.
从结果来看,对私有方法g的调用在运行时绑定到了对象实际类型(Derived)的方法上,这符合虚拟函数的语义。f()更像是一个包覆器,确保在virtual函数进行真正工作之前和之后环境的设定。
注意Startegy设计模式也可以参考。例子如下:
class GameCharacter;//前置声明
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter{
public:
//定义参数为(const GameCharacter&),返回值为int的函数指针名字叫HealthCalFunc
typedef int (*HealthCalFunc) (const GameCharacter&);
explicit GameCharacter(HealthCalFunc hcf = defaultHealthCalc): healthFunc (hcf){}
int healthValue() const{
return healthFunc(*this);
}
...
private:
HealthCalFunc healthFunc;
}
上例中,同一类的不同对象,也可以有不一样的HealthCalFunc实现,可以增加如set_HealthCalFunc()函数,在运行期间修改对象的HealthCalFunc函数,完成策略模式。
引发奇异性。虽编译通过,但语义上不对。若需要覆盖,为什么不直接在base上加上virtual呢
#include
using namespace std;
class base{
public:
virtual void show(int a = 0){
cout<<"pure virtual shows a="<<a<<endl;
};
};
class derived1:public base{
public:
void show(int a = 1){
cout<<"derived1 shows a="<<a<<endl;
}
};
int main(int argc, char const *argv[])
{
base *p1=new derived1();
p1->show();
return 0;
}
//derived1 shows a=0
//虚函数和继承函数各出一半力。因为虚函数是动态绑定而缺省参数是静态绑定。
绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定,而virtual函数——你唯一应该覆写的东西——却是动态绑定。
复合(composition)是类型之间的一种关系,当某种类型的对象内含它种类型的对象,便是这种关系。
当复合发生于应用域内的对象之间,表现出has-a的关系;当它发生于实现域内则是表现is-implemented-in-terms-of的关系。
复合的典型表现是STL里面的stack里面有vector。map里面有RB-tree
Private 继承意味 implemented-in-terms-of(根据某物实现出)。如果你让class D以private形式继承class B,你的用意是为了采用class B内已经备妥的某些特性,不是因为B对象和D对象存在有任何观念上的关系。private继承纯粹只是一种实现技术(这就是为什么继承自一个privatebase class的每样东西在你的class内都是private:因为它们都只是实现枝节而已)。借用条款34 提出的术语,private 继承意味只有实现部分被继承,接口部分应略去。如果D以private形式继承B,意思是D对象根据B对象实现而得,再没有其他意涵了。Private继承在软件“设计”层面上没有意义,其意义只及于软件实现层面。继承的编译依赖性也较高。
virtual继承
class File{};
class Inputfile:public file{};
class Outputfile:public file{};
class IOFike: public InputFile,
public OutFile{};
考虑上述继承关系,当file类里面有一个filename()函数的话,IOfile里应该有几份呢。为解决这个问题,采用virtual public继承,则只会含有一份,但是需要付出virual的代价,就是访问成员变量的速度变慢,大小变大初始化变复杂等成本。
class File{};
class Inputfile:virtual public file{};
class Outputfile:virtual public file{};
class IOFike: public InputFile,
public OutFile{};
面向对象编程世界总是以显式接口(explicit interfaces)和运行期多态(runtimepolymorphism)解决问题。Templates 及泛型编程的世界,与面向对象有根本上的不同。在此世界中显式接口和运行期多态仍然存在,但重要性降低。反倒是隐式接口(implicit interfaces)和编译期多态(compile-time polymorphism)移到前头了。
template内出现的名称如果相依于某个template参数,称之为从属名称(dependent names)。
C++有个规则可以解析(resolve)此一歧义状态:如果解析器在template中遭遇一个嵌套从属名称,它便假设这名称不是个类型,除非你告诉它是。
一般性规则很简单:任何时候当你想要在template中指涉一个嵌套从属类型名称,就必须在紧临它的前一个位置放上关键字 typename。
“typename 必须作为嵌套从属类型名称的前缀词”这一规则的例外是,typename 不可以出现在 base classes list 内的嵌套从属类型名称之前,也不可在member initialization list(成员初值列)中作为base class修饰符。
声明template参数时,前缀关键字class和typename可互换。
请使用关键字typename标识嵌套从属类型名称;但不得在base class lists(基类列)或member initialization list(成员初值列)内以它作为base class修饰符。
stl里面经典的用法:
typedef typename std::iterator_traits<IterT>::value_type value_type;
typename用于向编译器说明,std::iterator_traits::value_type 是一个typename而不是iterator_traits里面的static成员变量。
可在derived class templates内通过 “this->” 指涉base class templates内的成员名称,或藉由一个明白写出的“base class资格修饰符”完成。
//方法1
this->sendClear(info);
//方法2
using MsgSender<Company>::sendClear;//告诉编译器,假设sendClear在base class里面
//方法3
MsgSender<Company>::sendClear(Info);//调用的时候,直接指定,但这个相当于拒绝了virtual发生作用
template<typename T>
class SmartPtr{
public:
//以下就是成员函数模板
template<typename U>
SmartPtr(const SmartPtr<U>& other);
...
};
//对于任何类型T和任何类型U,可以根据SmartPtr构造出SmartPtr
//这个做法又叫泛化构造函数
当我们编写一个class template,而它所提供之“与此template相关的”函数支持“所有参数之隐式类型转换”时,请将那些函数定义为“class template 内部的friend函数”。
如何使用一个traits class了:
请记住
Template metaprogramming(TMP,模板元编程)是编写template-basedC++程序并执行于编译期的过程。所谓templatemetaprogram(模板元程序)是以C++写成、执行于C++编译器内的程序。一旦TMP程序结束执行,其输出,也就是从templates具现出来的若干C++源码,便会一如往常地被编译。
可以生成客户定制之设计模式(custom designpattern)实现品。设计模式如Strategy(见条款35),Observer,Visitor等等都可以多种方式实现出来。运用所谓policy-based design之TMP-based技术,有可能产生一些templates用来表述独立的设计选项(所谓 “policies”),然后可以任意结合它们,导致模式实现品带着客户定制的行为。
考虑以下这个递归
#include
template <int n>
struct Factorial{
enum {value = n*Factorial<n-1>::value};
};
template <>
struct Factorial<0>{
enum {value = 1};
};
int main(int argc, char const *argv[])
{
std::cout<<Factorial<10>::value;//3628800
return 0;
}
set_new_handler允许客户指定一个函数,在内存分配无法获得满足时被调用。
可以用类似于RAII的方式,为一些类设置类专属的new-handling。用这样的方式是为了避免忘记把new-handler改回来。(set_new_handler的修改是全局性质的)这个书上写了好几页,但感觉太细节了,不在本笔记里体现。
new-handler一般做一下几个事情
Nothrow new是一个颇为局限的工具,因为它只适用于内存分配;后继的构造函数调用还是可能抛出异常
new完成三个工作
注意到,若2出问题,那么new就应该负责把第一部里面开辟的空间归还。否则就会有内存泄漏。
如果operator new接受的参数除了一定会有的那个size_t之外还有其他,这便是个所谓的placement new。众多 placement new版本中特别有用的一个是“接受一个指针指向对象该被构造之处”。如果一个带额外参数的 operator new没有“带相同额外参数”的对应版operator delete,那么当new的内存分配动作需要取消并恢复旧观时就没有任何 operator delete 会被调用。
这意味如果要对所有与 placement new相关的内存泄漏宣战,我们必须同时提供一个正常的operator delete(用于构造期间无任何异常被抛出)和一个placement版本(用于构造期间有异常被抛出)。后者的额外参数必须和operatornew一样。
时间原因,TR1已经在现在的c++11里面实现,包括但不限于shared_ptr等等
Boost是一个社群,也是一个网站。致力于免费、源码开放、同僚复审的C++程序库开发。Boost在C++标准化过程中扮演深具影响力的角色。