Effective C++ 精华版笔记

文章目录


01.让自己习惯C++


01.视C++为一个语言联邦

C++支持多重泛型编程语言,支持面向过程、面向对象、函数形式、泛型形式、元编程形式。
内容:

  • C: 区块、语句、预处理、内置数据类型、数组、指针等。
  • 带有类的C: ,classes(构造函数和析构函数),封装、集成、多态、虚函数等等
  • template C++:TMP,模板元编程
  • STL:容器、迭代器、算法以及函数对象
    C++是这四个次语言的组成

02 尽量以const,enum,inline 替换#define

宁可编译器代替预处理
1.报错的时候会指向宏的内容,预处理会将宏进行替换

技巧:
1.常量字符串

	 const char* const authorName = "xxxxx";
	//声明不变字符串
	const std::string authorName("Scott Meyers");

2.class专属常量,为了作用域

class GamePlayer{
	private :
		static const int NumTurns = 9;//定义该常量
		int socres[NumTurns] ;	//使用常量
}

3.需要取地址需要定义式
const int GamePlayer::NumTurns;由于声明以赋值,所以无需继续赋值

原因:
1.宁愿编译器替换预处理
2.使用宏定义,编译器可能盲目的替换,没有常量产生的代码小
3.可以控制其域
4.enum比较像#define,因为取地址的时候也违法
5.宏的括号,及算法优先级的烦恼

要点:

  • 对于单纯常量,最好以const对象替换#defines
  • 对于形式函数的宏,最好用inline代替宏

03.尽可能使用const

const星号左边:被指物体常量
const星号右边:指针自身常量

  • 对单纯遍历,最好以const对象或enums替换#deines
  • 对于形式函数的宏,最好用inline替换为#define

04.确定对象被使用前已被初始化

  • 变量都要初始化。
  • STL默认已经帮我们初始化好了。
  • 构造函数赋值的行为不能称之为初始化,也不建议这么写
  • 推荐使用初始化成员列表方式初始化
  • 基类多构造函数,可以把数值初始化放到一个私有函数中

static分为函数内static loical

static全局non-local

  • non-loacl static对象声明如果 依赖另一个non-local static对象
    这样不能明确顺序是不好的,可以使用单例模式来解决这个问题

记住:
–为内置型对象进行手工初始化,因为C++不保证初始化他们
– 使用初始化列表初始化,不要在构造函数本体内使用赋值操作
– 为免除“跨单元之初始化次序”,以local static 对象替换non-local static对象

2.构造/析构/赋值

05.了解C++默默编写并调用哪些函数

  • 默认有构造函数,析构函数,拷贝构造函数

06.若不适用编译器自动生成的函数,就该明确拒绝

  • 不适用拷贝构造
class HomeForSale {
public:
    ...
private:
    ...
    HomeForSale(const HomeForSale&);    //只有声明而没有定义
    HomeForSale& operator=(const HomeForSale&);
  • 为了驳回编译器自动(暗自)提供的机能,可以将相应的成员函数声明为private并且不予实现。使用像Uncopyable这样的base class也是一种做法。(就是使用继承的方法,让父类私有化拷贝构造函数)

07.为多态基类声明virtual析构函数

  • 带多态性质的基类应该声明一个虚析构函数,如果类带有任何虚函数,它就应该拥有一个虚析构函数

  • 类的设计目的如果不是作为基类使用,或者不是为了具备多态性,就不应该声明虚析构函数。(浪费内存)
    – 32位计算中将占用64bits~96bits
    – 64位占用 64~128bits

08.别让异常逃离析构函数

  • 析构函数绝对不要吐出异常,如果一个析构函数调用的函数可能抛出异常,
    析构函数应该捕捉任何异常,然后吞下它们或者结束程序。

  • 如果客户需要对某个操作函数运行期间抛出的异常做出反应,
    那么class应该提供一个普通函数(而非在析构函数中)执行该操作。

09.绝不在构造和析构中调用virtual

eg:构造中父类调用了虚函数的print,会打印父类print
析构函数中,先析构子类后析构父类,所以调用虚函数,也会打印父类print函数
C++ 构造和析构禁止调用virtual

10. operator返回一个 reference to *this

int x,y,z;
x=y=z=15

将上述转换为下:

x=(y=(z=15))

推荐代码如下:
适用于+=,-=,*=,等等

Widget& operator=(const Widget&rhs)
{
	...
	return *this
	...
}

11.在operator中处理"自我赋值"

可能导致指针被提前释放的问题

//删除this.pb意味着rhs.pb也可能是个野指针,代码会产生问题
 Widget& Widget::operator=(const Widget& rhs)
 {
  delete pb;
  pb = new Bitmap(*(rhs.pb));
  return *this;
 }

解决方案

Widget& operator=(const Widget&rhs)
{
	if(this == &rhs)
	{
		return *this
	}
	delete pb;
	pb = new Bitmap(*(rhs.pb))
	return *this;
}
  • 确保当对象自我赋值时 operaotr=有良好的行为,处理好源对象和目标对象的处理顺序,以及 copy and swap
  • 确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象是,其还是正确。

12.复制对象时勿忘其每一个成分

eg:

class Defau {
public:
	int i = 10;
};
class A {
public:
	A() = default;
	A(const A&rhs):name(rhs.name),u(rhs.u){}
	A& operator = (const A&rhs) {
		this->name = rhs.name;
		this->u = rhs.u;
		return *this;
	}
	string name;
	Defau u;
};
 
class B : public A {
public:
	B():prio(3){}
	int prio;
	B(const B&ur):prio(ur.prio),A(ur){}
	B& operator =(const B&ur) {
		prio = ur.prio;
		A::operator=(ur);
		return *this;
	}
};
int main() {
	B b;
	b.u.i = 15;
	b.prio = 6;
	b.name = "dewdqw";
	B c(b);
	cout << c.u.i << endl;
	cout << c.prio << endl;
	cout << c.name;
}

Copying函数应该确保复制“对象内的所有成员变量”及“所有base class成分”。

不要尝试以某个copying函数实现另一个copying函数。应该将共同机能放进第三个函数中,并由两个copying函数共同调用。

3.资源管理

13.以对象管理资源

– 使用 auto_ptr /share_ptr

  • 为防止资源泄漏,请使用RAII对象,它们在构造函数中获得资源并在析构函数中释放资源。

  • 两个常被使用的RAII classes分别是tr1::shared_ptr和auto_ptr,前者通常是较佳选择,因为其copy行为比较直观。若选择auto_ptr,复制动作会使它(被指物)指向null。

14.在资源管理类中小心coping行为

RAII(资源获得即是初始化)

  • 复制RAII对象必须一并复制它所管理的资源,所以资源copying行为决定RAII对象的copying行为

  • 普遍而常见的RAII class copying行为是:抑制copying,施行引用计数法(shared_ptr思想),或者是转移底部资源(auto_ptr思想)

15.在资源管理类中提供对原始资源的访问

  • 1、APIs往往要求访问原始资源( raw resources),所以每一个 RAII class 应该提供一个“取得其所管理之资源”的办法。

  • 2、对原始资源的访问可能经由显式转换或隐式转换。一般而言显式转换比较安全, 但隐式转换对客户比较方便。

eg:显示转换

#include "stdafx.h"
#include <iostream>
#include <memory>
using namespace std;
class myClass
{
public :
    myClass()
    {
        cout<<"myClass()"<<endl;
    }
    ~myClass()
    {
        cout<<"~myClass()"<<endl;
    }
    void printFunc()
    {
        cout<<"printFunc()"<<endl;
    }
int getType()const
    {
        return type;
    }
private:
    int type; 
};
myClass* creatMyClass()
{
    myClass *my=new myClass();
    return my;
}
void issue(myClass *my) { 
    delete my;
} 
int _tmain(int argc, _TCHAR* argv[])
{
    auto_ptr<myClass> apMy(creatMyClass());
    myClass* myC=apMy.get();//////auto_ptr 给我们提供的函数,用来访问原始资源
    myC->printFunc();//////调用了myClass的方法
    return 0;
}

eg:隐式转换
通过函数把share_ptr/auto_ptr,转出一个目标值

16.成对使用new和delete时要采取 相同形式

  • 1.如果在new表达式中使用[],必须在相应的delete表达式中使用[]。如果
    在new表达式中不使用[],一定不要再相应的delete表达式中使用[]。
  • 2.new一个对象会有两个行为,第一个是内存被分配出来,第二是针对此内存会有一个
    或多个构造函数被调用。
  • 3.delete一个对象也会有两个行为,第一个是针对此内存会有一个或多个析构函数被调用,

17.以独立语句将newed对象置入智能指针

以独立语句将newed对象存储于(置入)智能指针内。如果不这样做,一旦异常抛出

有可能导致难以察觉的资源泄漏。

processWidget(std::trl::shared_ptr<Widget> (new Widget), property());

①执行new Widget
②调用proprety()函数
③调用tr1::shared_ptr的构造函数
那么proprety()可以在第一个调用,可以第二个调用,也可以第三个调用。
如果函数异常了,new Widget的指针没放入share_ptr,这样就泄漏了.

4.设计与声明

18.让接口容易被正确使用,不易被勿用

struct Day {
    explicit Day(int d) : val(d) { }
    int val;
};

struct Month {
    explicit Month(int m) : val(m) { }
    int val;
};

struct Year {
    explicit Year(int y) : val(y) { }
    int val;
};

class Date {
public:
    Date(const Month& m, const Day& d, const Year& y);
    ...
};

Date d(30, 3, 1995);        //错误的类型!
Date d(Day(30), Month(3), Year(1995));  //错误的类型!
Date d(Month(3), Day(30), Year(1995));  //正确了!

eg:createInvestment使它返回一个tr1::shared_ptr并夹带getRidOfInvestment函数作为删除器

std::tr1::shared_ptr<Investment> createInvestment()
{
    std::tr1::shared_ptr<Investment> retVal(static_cast<Investment*>(0), getRidOfInvestment);
    retVal = ...;    //令retVal指向正确的对象
    return retVal;
}

tr1::shared_ptr的一个优秀的性质是:
它会自动的使用它的“每个指针专属的删除器”

这样的性质消除了一个潜在的可能错误:

“cross-DLL problem”
这个问题发生于“对象在动态链接库(DLL)中被创建,但却在另一个DLL内被delete销毁”。在一些平台上,这一类“跨DLL的new/delete成对运用”会导致运行期间错误。

然而tr1::shared_ptr就没有这个问题,因为它默认的删除器是来自“tr1::shared_ptr诞生所在的那个那个DLL”的delete。举个例子来说,如果Stock派生自Investment,而createInvestment的实现如下:

std::tr1::shared_ptr<Investment> createInvestment()
{
    return std::tr1::shared_ptr<Investment>(new Stock);
}

返回的那个tr1::shared_ptr可被传递给任何其他的DLLs,无需在意“cross-DLL problem”。这个指向Stock的tr1::shared_ptr会追踪记录“当Stock的引用次数变成0时该调用的那个DLL’s delete”。

  • 1,好的接口很容易被正确使用,不容易被误用。应该在实现的所有接口中努力达成这些性质。
  • 2,“促进正常使用”的办法包括接口的一致性,以及与内置类型的行为兼容。
  • 3,“阻止误用”的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除用户的资源管理责任。
  • 4,tr1::shared_ptr支持定制型删除器(custom deleter)。这可防范DLL问题,可被用来自动解锁互斥锁(mutexes)等等。

19.设计class犹如设计type

灵魂十二问啊!!

  • 1.新的type对象该如何被创建销毁? 这会影响到class的构造函数和析构函数以及内存的分配函数和释放函数的设计。

  • 2.对象初始化和对象赋值应该有什么差别不要混淆初始化和赋值,这将决定构造函数和赋值操作符的行为。

  • 3.新type对象如果被值传递,这将意味着什么? copy构造函数用来定义一个type的值传递应该如何实现。

  • 4.什么是新type的合法值? 对于成员变量而言,只有有些数值集是有效的。

  • 5.你的新type需要配合继承图系吗?

  • 继承:受到基类的束缚

  • 被继承:影响你所声明的函数——尤其是析构函数——是否为virtual。

  • 6. 你的新type需要什么样的转换?

  • 7.什么样的操作符核函数对此新的type是合理的? 这个问题将决定你的class声明哪些函数

  • 8.什么样的标准函数应该被驳回 这些正是你必须声明为private的

  • 9.谁改取用新type的成员? 这些帮助决定那些成员为public,哪些为protedted,哪些为private。也帮助决定哪一个class和/或function应该是友元,以及将他们嵌套到另一个之内是否合理。

  • 10.什么是type的“未声明接口”? 他对效率、异常安全性以及资源运用提供何种保证?你讲在这些方面提供的保证将为你的class实现代码加上相应的约束条件

  • 11.你的type有多么一般化 或许你并非定义了一个新type,而是定义了一整个types家族。如果果真如此,你就不应该定义一个新的class,而是定义一个新的class template(模板类)

  • 12.你真的需要一个新type吗? 如果只是定义一个新的子类以便为既有的class添加机能,说不定单纯定义一个或多个non-number函数或者templates更能够达到目标。

20.宁以pass-by-reference-to-const替换pass-by-value

尽量传引用,效率高。不需要修改的 引用就给个const
不包含STL的迭代器和函数对象

  • 1,尽量以pass-by-reference-to-const替换pass-by-value。前者通常比较高效,并可以避免切割问题(slicing problem)。
  • 2,以上的规则并不适用于内置类型,以及STL的迭代器和函数对象。对它们而言,pass-by-value往往比较适当。

21.必须返回对象时,别妄想返回reference

该错误尝试过了,不在解释。新手易犯错误

绝不要返回pointer或reference指向一个local stack对象,或返回reference指向一个heap-allocated对象,或返回pointer指向一个local static对象而有可能同时需要多个这样的对象。

22.将成员变量声明为private

  • 1,切记将成员变量声明为private。这可以赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供class作者以充分的实现弹性。
  • 2,protected并不比public根据封装性。
    一旦你将一个成员变量声明为public或protected而用户开始使用它,就很难改变这个变量所涉及的一切!总结说来,其实只有两种访问权限:
    private(提供封装)
    其他(不提供任何封装)

23.宁以non-member、non-friend替换member函数

封装性优先于类的内聚逻辑
采用namespace可以对内聚性进行良好的折中。
“愈多东西被封装,愈少人可以看到它,而愈少人看到它,我们就有愈大的弹性去改变它,因为我们的改变仅仅影响看到改变的那些人或事物”
code:

void ClearWebBrowser(WebBrower& w)
{
    w.ClearCach();
    w.ClearHistory();
    w.RemoveCookies();
}
  • 尽量用non-member non-friend函数替换member函数。可以增加封装性、包裹弹性(packaging flexibility)、和机能扩充性。

24.若所有参数皆需类型转换,请以此采用non-member函数

https://www.bbsmax.com/A/lk5aEoE051/
不太理解

25.考虑写出一个不抛出异常的swap函数

  • todo

5.实现

26.尽可能延后变量定义式的出现时间

  • 尽可能延后变量定义式。这样做可增加程序清晰度和改善效率。
  • 用到变量在定义就行,防止没有必要的生命成本

27.尽量少做类型转换动作

C风格的转型动作:

(T)expression

函数风格的转型动作:

T(expression)

C++ 提供四种新类型转换

  • const_cast (expression)
    • 通常是将对象的长良性消除,是唯一有此能力的C++ style转型操作符
  //1. 指针指向类 
  const A *pca1 = new A; 
  A *pa2 = const_cast<A*>(pca1); //常量对象转换为非常量对象 
  pa2->m_iNum = 200; //fine 
	   //2. 指针指向基本类型 
  const int ica = 100; 
  int * ia = const_cast<int *>(&ica); 
  *ia = 200; 
     
     3.常量引用转为非常量引用 
    A a0; 
  const A &a1 = a0; 
  A a2 = const_cast<A&>(a1); //常量引用转为非常量引用 

	
    //常量对象被转换成非常量对象时出错 
  const A ca; 
  A a = const_cast<A>(ca); //不允许 
  
  const int i = 100; 
  int j = const_cast<int>(i); //不允许 

     const int a = 1;//允许
	int * b = const_cast<int*>(&a);
	*b = 2;
	
	const int a = 1;//允许
	int & b = const_cast<int&>(a);
	b = 2;

网上看到的一个比较好的代码

const string& shorter(const string& s1, const string& s2) {
	return s1.size() <= s2.size() ? s1 : s2;
}

string& shorter(string& s1, string& s2) {
	//重载调用到上一个函数,它已经写好了比较的逻辑
	auto &r = shorter(const_cast<const string&>(s1), const_cast<const string&>(s2));
	//auto等号右边为引用,类型会忽略掉引用
	return const_cast<string&>(r);
}

  • dynamic_cast(expression)
    • 是将一个基类对象指针(或引用)转换到继承类指针,dynamic_cast会根据基类指针是否真正指向继承类指针来做相应处理。(基类转子类)
    • 前提条件:当我们将dynamic_cast用于某种类型的指针或引用时,只有该类型含有虚函数时,才能进行这种转换。否则,编译器会报错。
      dynamic_cast运算符的调用形式如下所示:
dynamic_cast<type*>(e)  //e是指针
dynamic_cast<type&>(e)  //e是左值
dynamic_cast<type&&>(e)//e是右值

e能成功转换为type*类型的情况有三种:

  • 1)e的类型是目标type的公有派生类:派生类向基类转换一定会成功。

  • 2)e的类型是目标type的基类,当e是指针指向派生类对象,或者基类引用引用派生类对象时,类型转换才会成功,当e指向基类对象,试图转换为派生类对象时,转换失败。

  • 3)e的类型就是type的类型时,一定会转换成功。

如果一条dynamic_cast语句的转换目标是指针类型并且转换失败了,会返回一个空指针,则判断条件为0,即为false;如果转换成功,指针为非空,则判断条件为非零,即true。

补充:dynamic_cast 有个普通版本来说,他会调用strcmp,用以比较class名称,所以效率较低

  • reinterpret_cast(expression)
    T必须是一个指针、引用、算术类型、函数指针或者成员指针。它可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针(先把一个指针转换成一个整数,再把该整数转换成原类型的指针,还可以得到原先的指针值)。

    之前一篇博客介绍的这个用法,使用不是很多,点击前往
    参考代码,不好用的东西,慎用吧。reinterpret_cast体现了 C++ 语言的设计思想:用户可以做任何操作,但要为自己的行为负责。

#include <iostream>
using namespace std;
class A
{
public:
    int i;
    int j;
    A(int n):i(n),j(n) { }
};
int main()
{
    A a(100);
    int &r = reinterpret_cast<int&>(a); //强行让 r 引用 a
    r = 200;  //把 a.i 变成了 200
    cout << a.i << "," << a.j << endl;  // 输出 200,100
    int n = 300;
    A *pa = reinterpret_cast<A*> ( & n); //强行让 pa 指向 n
    pa->i = 400;  // n 变成 400
    pa->j = 500;  //此条语句不安全,很可能导致程序崩溃
    cout << n << endl;  // 输出 400
    long long la = 0x12345678abcdLL;
    pa = reinterpret_cast<A*>(la); //la太长,只取低32位0x5678abcd拷贝给pa
    unsigned int u = reinterpret_cast<unsigned int>(pa);//pa逐个比特拷贝到u
    cout << hex << u << endl;  //输出 5678abcd
    typedef void (* PF1) (int);
    typedef int (* PF2) (int,char *);
    PF1 pf1;  PF2 pf2;
    pf2 = reinterpret_cast<PF2>(pf1); //两个不同类型的函数指针之间可以互相转换
}
  • static_cast(expression)
    ①用于类层次结构中基类(父类)和派生类(子类)之间指针或引用的转换。
    进行上行转换(把派生类的指针或引用转换成基类表示)是安全的;
    进行下行转换(把基类指针或引用转换成派生类表示)时,由于没有动态类型检查,所以是不安全的。
    ②用于基本数据类型之间的转换,如把int转换成char,把int转换成enum。这种转换的安全性也要开发人员来保证。
    ③把空指针转换成目标类型的空指针。
    ④把任何类型的表达式转换成void类型。

可以将non_const转为const,相反则不能(相反使用const_cast)

小结:

  • 如果可以,尽量避免转型,特别是注重效率的代码避免使用dynamiy_cast
  • 如果转型是必要的,试着将它隐藏域某个函数背后,客户随后可以调用该函数,而不需将转型放进他们代码内
  • 宁可使用C++ style(新式)转型,不要使用旧式转型。

28.避免返回handles指向对象内部成分

class Point{ 
public: 
    Point(int x, int y); 
    ... 
    void setX(int newVal); 
    void setY(int newVal); 
    ... 
}; 

struct RectData{ 
    Point ulhc; 
    Point lrhc; 
}; 

class Rectangle{ 
    ... 
    Point& upperLeft()const {return pData->ulhc;} 
    Point& lowerRight()const {return pData->lrhc;} 

private: 
    std::tr1::shared_ptr<RectData> pData; 
};

上述代码是不好的,因为客户可以通过upperLeft或是lowerRight返回一个内部的引用,进而修改了内部值

使用如下是更好的

class Rectangle{ 
    ... 
    const Point& upperLeft()const {return pData->ulhc;} 
    const Point& lowerRight()const {return pData->lrhc;} 

private: 
    std::tr1::shared_ptr<RectData> pData; 
};

第二个例子

class GUIObject{...}; 

const Rectangle boundingBox(const GUIObject& obj);
GUIObject *pgo; 

... 

const Point* pUpperLeft = &(boundingBox(*pgo).upperLeft());//取得一个指针指向外框左上点

boundingBox(*pgo).函数调用结束后会析构Rectangle ,导致代码产生问题。

避免返回 handle(包括 reference、指针、迭代器)指向对象内部。遵守这个条款可增加封装性,帮助 const 成员函数的行为像个 const,并将发生“虚吊号码牌”的可能性降至最低。

29.为异常安全而努力是值得的

30.透彻了解inling的里里外外

inline的代码会提高效率,编译器会最优化,但也可能会导致程序体检太大

class Person
{
public:
int age() const{return theAge;} //一个隐喻的inline申请
private:
 int theAge;
}
  • 将template 生命为inline,所有的具现化才都是inlined,否则不是。
  • 虚函数inlinehi没有作用的
  • 编译器会把inline函数copy多份,这也是为啥会程序会变大
  • 如果f是程序库内的inline函数,客户修改了inline函数,所有用到inline的都会重新编译
    那么如果不是inline函数,只需要重连接一下。

请记住:

  • 将大多数inline限制在小型、被频繁调用的函数身上。这可使日后的调试过程和二进制
    升级更容易,也可以使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。
    • 不要只因为function template出现在文件,就将它inline

31.将文件的编译依赖关系降到最低

6.继承与面向对象设计

32.确定你的public继承塑模出is-a关系

public意味着,基类的事情也一定适用于子类

33.避免遮掩继承而来的名称

34.区分接口继承和实现继承

class Base { 
private: 
    int x; 
public: 
    virtual void mf1() = 0; 
    virtual void mf1(int); 
    virtual void mf2(); 
    void mf3(); 
    void mf3(double); 
}; 
class Derived : public Base { 
public: 
    virtual void mf1(); 
    void mf3(); 
    void mf4();
Derived d;
int x;
d.mf1();// ok 调用 Derived::mf1
d.mf1(x);//error  Derived::mf1掩盖了Base::mf1
d.mf2();// ok 调用Base::mf2()
d.mf3();//ok 调用Derived::mf3()
d.mf3(x);//error Derived::mf3遮掩了 Base::mf3

1.派生类内的名称会遮掩基类内的名称。
2.可以使用using 声明式或者转交函数。
-纯虚函数只继承接口
-虚函数既继承接口,也提供了一份默认实现;调用基类虚函数使用Base::fun()
-普通函数既继承接口,也强制继承实现。这里假定讨论的成员函数都是public的。
NVI:该设计是令客户通过public non-virtual成员函数间接调用private virtual函数,称为non-virtual interface(NVI)手法。
它是模板方法设计模式的一个独特表示;相当对virtual函数进行一层的包装,可以称为是virtual函数的外覆器它是模板方法设计模式的一个独特表示;相当对virtual函数进行一层的包装,可以称为是virtual函数的外覆器(warpper).
non-virtual函数,采用就近调用原则。virtual函数系动态绑定,而缺省参数值却是静态绑定。
Base* ps = new Derived;ps->func(defaultParam);ps的静态类型就是Base*,而动态类型则是Derived*。

35.考虑virtual函数以外的其他选择

36.绝不重新定义继承而来的non-virtual

37.绝不重新定义继承而来的non-virtual function

38.通过复合塑模出has-a或根据某物实现出

39.明智而审慎地使用private继承

private继承根据某物实现

40.明智而审慎地使用多重继承

多重继承可能导致歧义性,带有virtual会增加成本。可以做虚接口继承类或是private继承

7.模板与泛型编程

41.了解隐式接口和编译期多态

对于 classes(类),interfaces(接口)是 explicit(显式)的并以 function signatures(函数识别特征)为中心的。polymorphism(多态性)通过 virtual functions(虚拟函数)出现在运行期。

class Widget {
public:
   Widget();
   virtual ~Widget();
   virtual std::size_t size() const;
   virtual void normalize();
   void swap(Widget& other); // see Item 25
   ...
};
void doProcessing(Widget& w)
{
   if (w.size() > 10 && w != someNastyWidget) {
   	Widget temp(w);
   	temp.normalize();
   	temp.swap(w);
   }
}

Templates及泛型编程,包含显示接口和运行期多态。也包含了
隐式接口和编译器多态。


template<typename T>
void doProcessing(T& w)
{
	if (w.size() > 10 && w != someNastyWidget) {
		T temp(w);
		temp.normalize();
		temp.swap(w);
	}
}

T的隐式接口似乎有这些约束
1、它必须提供一个size函数,该函数返回一个整数值。
2、必须支持一个operator!= 函数,用于比较两个T对象。这里我们假设someNastyWidget类型为T

从上述代码我们可以知道:
1、w支持哪种接口,由模版执行于w上的操作决定。如本例中w必须支持size,normalize,swap,copy构造、不等比较等(虽然不完全正确)。这组表态式(对该模板而言必须有效编译)是T必须支持的一组隐式接口。
2、涉及w的调用,可能实例化模板。“以不同的模板参数实例化函数模版”会导致不同的函数调用。这就是所谓的编译期多态。
类似于“哪一个重载函数被调用”(发生在编译期)和“哪一个虚函数被绑定”(运行期)。

需要记住的:
1、类和模版都支持接口和多态。
2、类接口是显示的,以函数声明为中心。多态通过虚函数发生于运行期。
3、对模版参数而言,接口是隐式的,基于有效表达式。多态通过模版实例化和函数重载解析,发生于编译期。

42.了解tyename的双重含义

  1. 在template的声明式中,typename的使用和class完全相同,即以下两种声明方式完全相同
template<typename T>
template<class T>

然而typename还有其他用途:指明嵌套从属类型名称.

template <typename C> 
void print2nd(const C& container) 
{ 
    if (container.size() >= 2) 
    { 
        C::const_iterator iter(container.begin()); 
        ++iter; 
        int value = *iter; 
        std::cout << value; 
    } 
}

嵌套从属名称,可能会导致解析困难,出现二义性。比如:C::const_iterator* x; 有两种意思。
一是:模版形参C中有个静态字段const_iterator,然后计算它与x的乘积;
二是:模版形参C中有个嵌套类型const_iterator,定义指向它的指针。默认情况下,C++编译器按第一种意思解释,也就是说,把它当成静态字段,而不是类型。如果我想告诉编译器,这是个嵌套类型,该怎么办?使用typename C::const_iterator* x;

请记住

  • 声明template参数时,前缀关键字class和typename可互换
    请使用typename标识嵌套从属类型的名称;但不得在base class lists(基类列)或
  • member initalization list(成员初值列)内以它作为base标识符

43.学习模板化基类内名称

44将与参与无关的代码抽离templates

45.运用成员函数模板接受所有兼容类型

46.需要类型转换时请为模板定义非成员函数

47.请使用 traits classes表现类型信息

48.认识template元编程

X

8.定制new和delete

49.了解new-handler的行为

operator new无法满足内存申请时,它会不断调用new-handler函数,直到找到足够内存。
标准函数如下:

namespace std
{
	typedef void (*new_handler)();
	new_handler set_new_handler(new_handler p) throw;
}

eg


void outOfMen()
{
	std::cerr<<"unable tosatisfy requesest for memeory \n"
	std::abort();
}

int main()
{
	std::set_new_handler(outOfMem);
	int *p = new int[100000000000L]
}

设计一个new-handler函数必须做以下事情:
1.让更多内存可以被使用,以便于下一次new可以分配内存
2.set_new_handler传递一个可以替换原有的函数指针,以便内存申请
3.set_new_handler传递空指针,这样以便于抛出异常
4.抛出bad_alloc的异常,会在存储所求处抛出异常
5.不返回,通常调用abort或exit

50.了解new和delete的合理替换时机

51.编写new和delete时固守常规

52.写了placement new 也要写placement delete

9.杂项讨论

53.不要忽略编译期的警告

class B {
public:
virtual void f() const;
};

class D: public B {
public:
virtual void f();
};

以上代码会有警告,原因参考50

1、严肃对待编译器发出的警告信息,因为编译器作者对所发生的事情有更好的领悟,尽量争取“无任何警告”。

2、不要过度依赖编译器的警告,因为不同编译器对待事情的态度不同。

54.让自己熟悉包括TR1在内的标准程序库

C++ 98标准:STL、Iostreams、国际化支持、数值处理、异常阶层体系、C89标准程序库。

TR1 14个新组件:智能指针(shared_ptr和tr1:weak_ptr)、tr1:function、tr1::bind、Hash tables/
正则表达式、Tuples、tr1::array、tr1::men_fn、reference_wrapper、随机数、数学特殊函数、C99兼容扩展
第二组TR1:Type traits、tr1::result_of.

请记住
1、C++标准程序的主要机能是由STL、iostreams、locales组成,并包含C99标准程序库;
2、TR1添加了诸如智能指针、一般化函数指针、哈希容器、正则表达式及其他10个组件;
3、TR1只是一个规范,为了熟悉此规范你必须熟悉源码,Boost是个不错的源码库。

55.让自己熟悉Boost

Boost很多的特性都可能会加入C++标准,Boost
包含 文字字符串与文本处理、容器、函数对象和高级编程、泛型编程、模板元编程
等等
1、Boost是一个社群,一个网站。它致力于免费、源码开放、同僚复审的C++程序开发。Boost在C++标准化过程中扮演了重要的角色。
2、Boost提供了许多TR1组件实现品,以及其他许多程序库。

发布了243 篇原创文章 · 获赞 80 · 访问量 28万+

猜你喜欢

转载自blog.csdn.net/hiwoshixiaoyu/article/details/102976920