文章目录
- 01.让自己习惯C++
- 2.构造/析构/赋值
- 05.了解C++默默编写并调用哪些函数
- 06.若不适用编译器自动生成的函数,就该明确拒绝
- 07.为多态基类声明virtual析构函数
- 08.别让异常逃离析构函数
- 09.绝不在构造和析构中调用virtual
- 10. operator返回一个 reference to *this
- 11.在operator中处理"自我赋值"
- 12.复制对象时勿忘其每一个成分
- 3.资源管理
- 13.以对象管理资源
- 14.在资源管理类中小心coping行为
- 15.在资源管理类中提供对原始资源的访问
- 16.成对使用new和delete时要采取 相同形式
- 17.以独立语句将newed对象置入智能指针
- 4.设计与声明
- 18.让接口容易被正确使用,不易被勿用
- 19.设计class犹如设计type
- 20.宁以pass-by-reference-to-const替换pass-by-value
- 21.必须返回对象时,别妄想返回reference
- 22.将成员变量声明为private
- 23.宁以non-member、non-friend替换member函数
- 24.若所有参数皆需类型转换,请以此采用non-member函数
- 25.考虑写出一个不抛出异常的swap函数
- 5.实现
- 26.尽可能延后变量定义式的出现时间
- 27.尽量少做类型转换动作
- 28.避免返回handles指向对象内部成分
- 29.为异常安全而努力是值得的
- 30.透彻了解inling的里里外外
- 31.将文件的编译依赖关系降到最低
- 6.继承与面向对象设计
- 32.确定你的public继承塑模出is-a关系
- 33.避免遮掩继承而来的名称
- 34.区分接口继承和实现继承
- 35.考虑virtual函数以外的其他选择
- 36.绝不重新定义继承而来的non-virtual
- 37.绝不重新定义继承而来的non-virtual function
- 38.通过复合塑模出has-a或根据某物实现出
- 39.明智而审慎地使用private继承
- 40.明智而审慎地使用多重继承
- 7.模板与泛型编程
- 41.了解隐式接口和编译期多态
- 42.了解tyename的双重含义
- 43.学习模板化基类内名称
- 44将与参与无关的代码抽离templates
- 45.运用成员函数模板接受所有兼容类型
- 46.需要类型转换时请为模板定义非成员函数
- 47.请使用 traits classes表现类型信息
- 48.认识template元编程
- 8.定制new和delete
- 49.了解new-handler的行为
- 50.了解new和delete的合理替换时机
- 51.编写new和delete时固守常规
- 52.写了placement new 也要写placement delete
- 9.杂项讨论
- 53.不要忽略编译期的警告
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用于某种类型的指针或引用时,只有该类型含有虚函数时,才能进行这种转换。否则,编译器会报错。
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的双重含义
- 在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组件实现品,以及其他许多程序库。