什么意思呢?就是你什么时候用变量再去定义,别一上来定义很多变量,因为有的时候,如果你定义的变量带有构造函数和析构函数,但是变量却没有用到,但依然要承受构造函数和析构函数的成本,这不就低效了吗?
例如
/
// 真正用的时候再定义
std::string func(string& password){
....
// 定义一个变量
string res;
// 下面是一些业务操作
if(...){
// 如果这里抛出异常,那res根本就没有执行到
throw logic_error("erron!");
}
...
// string res 的一些执行动作
....
return res;
}
当然了,并不是让你再使用那个变量的前一刻才定义它
再比如看
// 哪个更加好一点
// 方法A
Widget w;
for(int i = 0;i<n;++i){
w = i;
}
// 方法B
for(int i = 0;i<n;++i){
Widget w(i);
}
答案:方法A 调用一次构造函数,一次析构函数,n次赋值操作
方法B 调用 n 次构造函数,n 次析构函数,你说哪个好?其实这个有分情况,如果赋值成本低于一次构造和一次析构,那A还是比较好,如果 n 特别大的情况,B 可能会更好一些
当然了如果违背这则条款并不是很严重,只是追求一种好的写法却是挺高效的
C++ 提供四种转型方式
没看不懂没关系,下面详细举例以及他们的好坏
/
// static_cast 举例
class Widget{
public:
explicit Widget(int size);
...
};
void dosomething(const Widget& w){
...
}
// 方法一
dosomething(Widget(15));
// 方法二
dosomething(static_cast<Widget>(15));
这两种方法都行
但是下面这种方法是错误的使用
//错误使用
class Window{
public:
virtual void onResize(){...}
...
}
class SpecialWindow:public Window{
public:
virtual void onResize(){
// 将 this 指针转换为 Window 类型去调用 onResize 函数
static_cast<Window>(*this).onResize();
....
}
}
注意:上述代码并非是在当前对象调用 Window::onResize() 函数,而是在当前对象基类成分的副本去调用的,然后回到子类中,在当前对象上执行 Special Window 的专属动作,如果 Window::onResize 修改了对象内容,也不影响子类中对象,因为改动的是一个副本
正确写法为
//正确使用
class Window{
public:
virtual void onResize(){...}
...
}
class SpecialWindow:public Window{
public:
virtual void onResize(){
Window::onResize();
....
}
}
书中还有很多案例,本人能力有限(笑哭),大致意思是:
尽量不要使用转型,而去想一想别的设计方案,如果非要使用的话尽量把它写在函数里面,不要让客户把转型写在他们的代码里,另外不要使用隐式类型转换,而使用显示的那四种
话不多说,先上案例
///
// 涉及矩形设计
class Point{
public:
Point(int x,int y){
}
...
void setX(int newVal);
void setY(int newVal);
};
struct RectDate{
Point ulhc; // 左上角
Point lrhc; // 右下角
};
class Rectangle{
public:
Point& upperLeft()const{
return pDate->ulhc;
}
Point& lowerRight()const{
return pDate->lrhc;
}
private:
std::trl::shared_ptr<Rectangle>pDate;
};
这个设计有什么问题?
如果你没看出来,那我加一段代码进行访问
// 客户调用
Point coord1(0,0);
Point coord2(10,10);
const Rectangle rec(x,y);
rec.upperLeft().setX(100); // 这一行语句会修改 x 的值
由于 Rectangle 返回的是对象成员的引用,所以用户可以随意去修改了
解决办法:在成员函数前面加 const 限制修改就可以了
但是即便如此,依然还是返回了内部对象,有的情况会导致引用对象不存在,而造成空悬,虚吊等情况。
所以我们尽量不要返回 Handles指向对象内部,可以增强封装性(其中Handles 包括引用,指针,迭代器)
异常安全指的是什么?比如一个函数里面抛出了异常,安全的函数需要满足两点
但是看下面这个案例
class PrettyMenu{
public:
PrettyMenu();
// 改变背景图像
void changeBackground(std::istream& image);
~PrettyMenu();
private:
Mutex mutex; // 保证并发控制
Image* bgImage; // 目前的图像
int imageChanges; // 背景图像被改变的次数
};
void PrettyMenu::changeBackground(std::istream& imgsrc){
lock(&mutex); // 取得互斥器
delete bgImage; // 去重之前的背景
++imageChanges; // 修改图像次数
bgImage = new Image(imgsrc); // 安装新的背景
unlock(&mutex); // 释放互斥器
}
上面代码new 函数抛出异常后,资源被泄露(unlock 无法调用),互斥器被锁,然后 bgImage 指向一个删除的对象,次数也被增加,然后图像并没有更换成功,显然数据被破坏
怎么修改?
对于互斥器我们可以使用对象去管理资源,对象释放的时候自动调用析构函数,里面释放 unlock 就行了
void PrettyMenu::changeBackground(std::istream& imgsrc){
Lock m1 (&mutex); // 使用互斥器类构造互斥器
delete bgImage; // 去重之前的背景
++imageChanges; // 修改图像次数
bgImage = new Image(imgsrc); // 安装新的背景
}
但是资源怎么解决呢?
我们只要调换一下语句顺序就可以了
void PrettyMenu::changeBackground(std::istream& imgsrc){
lock m1(&mutex);
PrettyMenu* tmp = new Image(imgsrc); // 临时资源
++imageChanges;
delete bgImage; // 释放之前的对象资源
bgImage = tmp; // 把新对象赋值给 bgImage
}
当然也可以使用智能指针去管理资源,这样就不需要手动delete,这种方法就比较完美了
class PrettyMenu{
public:
// 改变背景图像
void changeBackground(std::istream& image);
void reset(PrettyMenu* bgImage);
private:
Mutex mutex; // 保证并发控制
std::trl::shared_ptr<Image>bgImage; //智能指针管理资源
int imageChanges; // 背景图像被改变的次数
};
void PrettyMenu::changeBackground(std::istream& imgsrc){
lock m1(&mutex);
bgImage.reset(new Image(imgsrc));
++imageChanges;
}
终于到了我比较熟悉的内联函数了
内联函数可以避免函数调用的开销,在函数编译期间对函数调用进行替换为函数本体,以空间换取时间的策略
当然 inline 只是给编译器的一个申请建议,编译器会不会采取,还得看代码的复杂程度,和编译器环境等,如果展开后太多导致代码膨胀带来的效率低于函数调用,那么有的编译器会给你一个警告信息
当然有的时候,编译器会不会把函数设置为内联是根据函数的调用方式决定的,如果函数是通过指针调用,则不会设置内联,因为指针调用是要知道函数地址的,函数一旦设为内联,编译器没有能力提出一个指针指向不存在的函数,如果是函数调用的话,有可能被内联
并不是所有的函数都可以设置为内联函数
1、虚函数
不可以设置为内联的,因为虚函数是在运行期间确定调用哪个接口的,而内联函数意味着 在执行前将调用动作替换为函数本体,所以肯定不行滴
2、构造函数
不行,会导致代码的膨胀,如果一个基类构造函数被内联,那么当派生类调用基类构造函数的时候,也会把基类的函数体插入到派生类中,导致派生类构造函数过于复杂,所以编译器不会那样搞
最后:程序猿需要考虑的是,如果将函数被内联带来的冲击有多大,如果fun函数是程序库的内联函数,当客户端将fun函数编到自己代码中,如果将来fun需要修改,那么用户就要重新编译程序,而如果fun不是内联,如果有改动,用户只需要重新连接就好了,比重新编译负担少很多,有的应用采用动态链接的方式,不知不觉就升级了,所以这些都应该程序猿考虑的因素
文件间的依赖关系,意思就是,A 文件编译的时候需要依赖B 或者 C 其他文件,否则无法编译,看下面代码
// 文件编译依赖关系
#include
#include "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 birthday()const;
std::string address()const;
...
private:
std::string theName;
Date theBirthday;
Address theAddress;
};
// Person 这个类所在的文件需要依赖于 string date.h address.h 这些文件
如果 date.h 文件,或者 address.h 里面某个东西修改了,那么Person文件就需要重新编译,以及 包含Person 的文件都需要重新编译,试想如果这是一个大项目,得多费时
那应该怎么修改把依赖关系降到最低?
声明和定义分开实现
// 文件编译依赖关系
#include
#include // shared_ptr 需要用的头文件
class Date; // 声明Date 否则编译不通过
class Address; // 同上
class PersonImpl; // 负责提供 Person 的接口
// 此文件才是 Person 的真正实现
class Person{
public:
Person(const std::string& name,const Date& birthday,const Address& addr){}
std::string name()const{}
std::string birthday()const{}
std::string address()const{}
...
private:
std::trl::shared_ptr<PersonImpl>pIml; // 使用一个指针指向实现物
};
无论是修改 adress 还是 date 都不会影响到 person 文件
本质:尽可能让头文件可以自己满足,万一做不到就让它与其他文件的声明式(非定义式)相依
当然了还可以为声明式和定义式提供两个不同的头文件,编译的时候只要包含声明式(非定义式)的头文件,这样就不需要类的前置声明了
这是一种设计模式,这种继承关系的设计模式必须是符合逻辑的,举个例子:
/
// 鸟类
class Bird{
//... 鸟的基本特性
};
// 会飞的鸟类
class FlyingBird:public Bird{
public:
// 该鸟有飞的功能
virtual void flay(){
...
}
};
// 现在有一种动物叫企鹅,它要继承鸟类
class Penguin:public FlyingBird{
... // 这就是一个错误的设计
// 因为企鹅根本不会飞,虽然代码通过编译
// 但是不符合逻辑,达不到预期效果
};
/
那应该怎么修改呢?需要单独写一个类,而这个类就是不会飞的鸟类,让它继承不会飞的类,后面会详细总结…
当派生类和基类中的成员对象或者函数名相同时,会发生覆盖(不考虑多态)
class A {
public:
void fun1() {
cout << "基类函数 fun1" << endl;
}
private:
int a = 10;
};
class B :public A {
public:
void fun1() {
cout << a << endl;
cout << "派生类函数 fun1" << endl;
}
private:
int a = 20;
};
B b;
b.fun1();
这样会输出派生类的函数,a 会输出 20,而不是 10,是因为被基类的 a 值被覆盖了
可以通过域名符操作进行制定调用哪个类的函数
B b;
b.A::fun1();
我们先看一个列子
class Shape{
public:
virtual void draw() const = 0;
virtual void error(const Shape&ms);
void obJectID() const;
};
一个重虚函数就可以理解为真正的接口继承,客户不可以创建它的基类实体,只能在派生类创建和定义函数,否则就不能使用,因为基类是不给接口的定义.
对于非重虚函数,可以继承接口,也可以继承缺省实现,非重虚函数在基类是可以被定义的,如果子类没有定义,则会使用基类的函数,表示它的行为和基类一样,如果子类给出定义,则表示子类的行为和基类行为是不同的,这也就是多态的概念
对于非虚函数,则表示它在子类并不打算实现不同行为,它的状态一直是不变 ,所以它不应该在子类中重定义.
这个说的就是设计模式,书上提到的是策略模式
什么是策略模式?
定义一系列的算法,把它们用类封装起来,然后在运行的时候,根据对象不同,互相替换调用。从依赖的角度来看,变化依赖稳定,稳定依赖于抽象
采用的是面向对象设计中的开闭原则,对扩展开放,对修改封闭
主要解决使用 if else 语句,或者 switch 开关语句带来的复杂以及难以维护
举例:比如进行税法的计算,每个国家都有自己的税法计算规律,把这些国家的计算规律封装到不同的类中,当然这些类都是继承一个稳定的抽象类,然后实例化出不同的对象去调用对应的国家税法
因为会发生覆盖,non-virtual 都是静态绑定,在函数编译的时候就知道应该调用哪个函数了,所以它是根据指针类型去决定的,而不是根据对象类型去决定调用哪个函数
因为继承而来的却是参数是静态绑定的,即使这个函数虚函数,虚函数在实现层面是动态绑定,但是参数如果你没有指定,它会继承基类的参数,而不会使用是你给出的参数
class A {
public:
virtual void fun1(int a = 10) {
}
virtual ~A() {
}
private:
int a;
};
class B :public A {
public:
virtual void fun1(int b = 20) {
cout << b << endl;
}
private:
int b;
};
B b;
A* a = &b;
a->fun1(); // 输出的是 10 ,而不是 20
// 就是因为它绑定的是基类的参数
复合是一种类型之间的关系,当然复合也有很多含义,要看你怎么理解,比如可以理解为内嵌,分层,聚合,下面举一个泛型编程的例子
实现:通过 list 容器来实现 set 容器
看代码
template<class T>
class Set {
public:
// 支持插入,删除,操作
bool memset(const T& item)const;
void insert(const T& item);
void remove(const T& item);
std::size_t size()const;
private:
std::list<T> rep;
};
// 判断 Set 中有无相同元素
template<class T>
bool Set<T>::memset(const T& item)const {
auto it = find(rep.begin(), rep.end(), item);
if (it != rep.end()) {
return true;
}
return false;
}
template<class T>
void Set<T>::insert(const T& item) {
if (!memset(item)) {
rep.push_back(item);
}
return;
}
template<class T>
void Set<T>::remove(const T& item) {
// 为什么使用 typename ,后面条款 46 具体讨论
typename std::list<T>::iterator it =
std::find(rep.begin(), rep.end(), item);
if (it != rep.end()) {
rep.erase(it);
}
return;
}
template<class T>
size_t Set<T>::size()const{
return rep.size();
}
当然这样写不代表 C++ set 容器就是 list 实现的,这个条款只是想说明它们之间关系,理解复合的意思
private 继承关系表示,基类和子类完全没有任何观念上的关系,单纯只是一个实现技术,子类只是想采用基类已经具备好的特性
private 在软件设计层面上没有实际意义,所以很少使用
一般用于派生类去访问基类的 protected 成员的时候,或者需要重新定义继承而来多个虚函数的时候,才会用到
多继承的关系特别复杂,尤其是
多继承中的菱形继承会导致歧义性和数据的冗余性
可以通过指定类来调用哪个函数
数据冗余可以采用虚继承的方法,让子类继承它的虚方法
采用虚继承会增加大小,速度,初始化以及复杂度的成本
一般也是很少用的
模板掌握的好与否,是一个程序猿的功力的体现,也是在面试过程中的装逼体现
我们之前都是这样去写代码的
class Widegt{
public:
Widegt();
virtual ~Widegt();
virtual std::size_t size()const;
virtual void normalize();
void swap(Widegt& other);
...
};
void doProcessing(Widegt& w){
//...
}
所有的接口都是显示定义的,函数中的 w 参数类型是 Widget ,所以必须支持 Widegt 接口,这是一个显示接口
隐式接口是啥样的?
template <typename T>
void doProcessing(T& w){
//...
}
T 的类型在编译期间会进行推导,当然这个接口依然支持多态性,这叫函数模板,参数为隐式类型,T 可以自定以的任意类型,提高代码的复用性
以不同的模板参数具现化函数模板,会导致调用不同的函数,这就是编译器的多态
最后稍微总结一下
template<class T> class Widegt;
template<typename T> class Widegt;
class 和 typename 的区别在哪? 没有区别
至少在 template 这里,它们的的意义是相同的
但是并不代表它们两个关键字就是一样的
typename 功能
我们这里用到第二个作用
先看一段代码
template<typename T>
void Print(const T& container){
//...
T::const_iterator iter = container.begin();
T::const_iterator* x;
//...
}
iter 的类型是 T::const_iterator
,这是一个嵌套从属名称,这会导致解析困难
关键就在于 T::const_iterator
,你怎么知道它就是一个类型,如果在T中有一个静态变量的名字是 const_iterator
,那它就变为 const_iterator 乘以 x 的表达式了
所以会导致歧义问题,这个时候我们就可以使用 typename ,声明这是一个类型
注意:typename 不可以出现在基类列或者成员初值列中内作为基类修饰符
class CompanyA{
public:
...
void sendCleartext(const std::string& msg);
void sendEncrypted(const std::string& msg);
...
};
class CompanyB{
public:
...
void sendCleartext(const std::string& msg);
void sendEncrypted(const std::string& msg);
...
};
// 用来保存信息
class MsgInfo{...};
// 模板类发送加密消息已经清空信息
template<typename Company>
class MsgSender{
public:
...
void sendClear(const MsgInfo& info){
std::string msg;
Company c;
c.sendCleartext(msg);
}
void sendSeret(const MsgInfo& info){
std::string msg;
Company c;
c.sendEncrypted(msg);
}
};
// 继承上一个类
template<typename Company>
class LoggingMsgSender: public MsgSender<Company>{
public:
...
void sendClearMsg(const MsgInfo& info){
// 调用基类的函数会报错
// 是因为编译器不知道继承的这个类是什么类型
// 类型不知,所以无法通过编译
sendClear(info);
}
...
};
解决办法:模板特化
指定 typename 中的类型,这样编译器就可以识别了
如果是全特化(typename 所有参数都被指定),那么再没有其他参数可变化
需要注意的是:类模板的基础,不像 C++ 对象那样畅通无阻,比如在模板的基类定义一个 fun1 函数,如果在模板派生类直接访问 fun1 函数是访问不到的,需要在前面加 this->
指涉基类模板中的成员,或者使用 using 把函数引进来
实现固定尺寸的正方形矩阵,编写一个 template
template<typename T,std::size_t n>
class SquareMatrix{
public:
...
void invert();
...
};
// 实例化 5 * 5 矩阵
SquareMatrix<double,5> sm1;
sm1.invert();
// 实例化 10 * 10 矩阵
SquareMatrix<double,10> sm2;
sm2.invert();
实例化出2 个矩阵,一个是 5 * 5 的,还有一个是 10 * 10 的,但是我们发现,这两个矩阵除了常量不一样,其余函数部分都一样,这可能会导致 template 膨胀
解决办法:建立一个带有数值参数的函数,然后以 5 或者 10 来调用这个带参数的函数
template<typename T>
class SquareMateixBase{
protected:
...
// 以给定的尺寸求逆矩阵
void invert(std::size_t matrixSize){
//...
}
...
};
template<typename T,std::size_t n>
class SquareMatrix:private SquareMateixBase<T>{
private:
// 避免遮掩 Base 类的invert
using SquareMateixBase<T>::invert
public:
...
// 这是一个 inline 函数,调用基类的 invert 函数
void invert(){
this->invert(n);
}
...
};
基类只对矩阵元素类型参数化,不对矩阵尺寸参数化
但是还有个问题要解决,就是基类如何查看派生类矩阵的数据,这个也好办,定义一个指针指向内存地址就可以了
template<typename T>
class SquareMateixBase{
protected:
...
// 以给定的尺寸求逆矩阵
void invert(std::size_t n,T* pMem)
:size(n),pData(pMem){
//...
}
void setDataPtr(T* ptr){
pData = ptr;
}
...
private:
std::size_t size; // 矩阵大小
T* pData; // 指针指向矩阵内容
};
template<typename T,std::size_t n>
class SquareMatrix:private SquareMateixBase<T>{
public:
SquareMatrix()
:SquareMateixBase<T>(n,0)
,pData(new T[n*n]){
// 把副本交给 base class
this->setDataPtr(pData.get());
}
private:
boost::scoped_array<T> pData;
};
这是指针之间的隐式转换
class Top{...};
class Middle:public Top{...};
class Bottom:public Middle{...};
Top* pt1 = new Middle; // Middle* 转 Top*
Top* pt2 = new Bottom; // Bottom* 转 Top*
const Top* pct2 = pt1; // Top* 转 const Top*
但是如果引进模板,就比较麻烦了
下面的代码是通过不了的,为什么?
template<typename T>
// 实现一个智能指针
class SmartPtr{
public:
explicit SmartPtr(T* realPtr);
...
};
SmartPtr<Top> pt1 = SmartPtr<Middle>(new Middle);
SmartPtr<Top> pt2 = SmartPtr<Bottom>(new Bottom);
SmartPtr<const Top>pct2 = pt1;
因为虽然是同一个模板类,但是具现出来的类完全扯不上关系,就比如 Smart
所以也就不存在转换这一说法了
那应该怎么做?
把关注点放在 Smart 的构造函数上
下面才是重点,怎么写构造函数?
template<typename T>
// 实现一个智能指针
class SmartPtr{
public:
SmartPtr(const SmartPtr& other);
template<typename U>
// 为了生成拷贝构造函数
// 意思就是对任何类型T和任何类型U,这里可以
// 根据Smartptr 生成一个 Smartptr
// 这被称为泛化拷贝构造函数
SmartPtr(const SmartPtr<U>& other)
// 返回智能指针对象原始指针的副本
:heldPtr(other.get())
{
...
}
T* get()const{
return heldPtr;
}
private:
T* heldPtr;
};
需要注意的是,当你声明一个泛化的拷贝构造或者赋值操作符,同时还需要声明正常的拷贝构造和赋值操作符
template<typename T>
class Rational{
public:
Rational(const T& numerator = 0,
const T& denominator = 1);
const T numerator()const;
const T denominator()const;
...
};
template<typename T>
const Rational<T>operator* (const Rational<T>& lhs,const Rational& rhs){
//...
}
// 实例化
{
Rational<int>onehalf(1,2);
Rational<int> result = onehalf * 2; //会报错
}
为什么会报错,因为编译器无法推导出T类型(对于第一个参数是Rational
解决办法:友元函数
class Rational
因为当对象 oneHalf 被声明为Rational
template<typename T>
class Rational{
public:
friend
const Rational<T>operator* (const Rational<T>& lhs,
const Rational& rhs){
return Rational(lhs.numerator()*rhs.numerator(),
lhs.denominator()*rhs.denominator());
}
...
};
template<typename T>
const Rational<T>operator* (const Rational<T>& lhs,const Rational& rhs){
//...
}
这是一种模板萃取的技术,比如我们对不同的类型有不同的操作,所以可以针对一部分类型进行特化 ,特化的类型执行某一个逻辑程序,没有特化的类型执行另一个逻辑程序
比如我们针对内置类型和非内置类型有不同的操作
// 内置类型
struct TrueType{
static bool Get(){
return true;
}
};
// 自定义类型
struct FalseType{
static bool Get(){
return false;
}
};
// 给出以下模板,将来用户可以按照任意类型实例化该模板
template<class T>
struct TypeTraits{
typedef FalseType IsPODType;
};
// 实例化
template<>
struct TypeTraits<char>{
typedef TrueType IsPODType;
};
template<>
struct TypeTraits<short>{
typedef TrueType IsPODType;
};
template<>
struct TypeTraits<int>{
typedef TrueType IsPODType;
};
template<>
struct TypeTraits<long>{
typedef TrueType IsPODType;
};
template<>
struct TypeTraits<double>{
typedef TrueType IsPODType;
};
template<>
struct TypeTraits<...>{//所有内置类型都特化一下
typedef TrueType IsPODType;
};
// 确认对象的实际类型
template<class T>
void func(T* dst,const T* src,size_t size){
if(TypeTraits<T>::IsPODType::Get()){
// 内置类型逻辑处理程序
}
else{
// 自定义类型处理程序
}
}
简单总结一下,书上案例看不太懂了 …
元编程可将工作由运行期移往编译期间,因而得以实现早期错误侦测和更高的执行效率
C++ 可以手动管理内存,因为系统定制的 new 和 delete 可能不符合客户的需求,所以这章主要讲的是 operator new 和 operator delete
这章写的很简略(因为看不懂),书上还是很详细的
new-handler 是什么?就是当你 new 出的内存不够需求、或者无法分配足够的内存,这个时候定义一个错误处理程序,这个处理程序就是 new-handler ,当然你不定义也行,默认的处理方式是抛出异常
void outOfMem(){
std::cout<<"Unable to satisfy request for memory!"<<endl;
// 这个程序里通常会做 5 件事
// 1 让更多的内存可被使用
// 2 安装另一个 new-handler 如果这个无法获得内存,则会安装另一个替换自己
// 3 卸除 new-handler 也就是交 null 指针传给 set_new_handler
// 4 抛出 bad_alloc 异常
// 5 不返回,调用 abort 函数
}
int main(){
std::set_new_handler(outOfMem);
int* pBigData = new int[1000000000L]; // 会调用outOfMem函数
return 0;
}
以上是基本使用方法
如果你希望以不同的方式处理内存分配失败情况,比如
class X{
public:
static void outOfMemory();
};
class Y{
public:
static void outOfMemory();
};
X* p1 = new X; //如果分配失败,则执行 X::outOfMemory
Y* p2 = new Y; //如果分配失败,则执行 Y::outOfMemory
书上还有很多,我看不懂了,只能写这么多了
为什么需要替换编译器提供的 new operator和 delete operator
在 new operator 内部写一个无穷循环,并在其中尝试分配内存,如果他无法满足内存需求,就调用 new-handler ,它也应该有能力处理 0 byte 字节的申请
delete operator 应该在收到null 指针时不作任何事情
使用 new 关键字,会先调用 new operator 分配空间,在调用构造器进行初始化,但是如果在调用构造器的时候抛出异常, 没有指针可以释放之前申请的资源,就会断断续续的造成内存泄漏的问题
所以在定义的 placement new 后也应该定义 placement delete
把警告当做错误来处理
因为不同的编译器可能会发出不一样的警告,可能在这台编译器是警告,把代码移植到另一个编译器就可能是错误了
C++ 标准程序库
STL、Iostreams 、locales 组成
TRI 添加了智能指针,hash-based 容器、正则表达式、…
Boost 是一个社群,也是一个网站,致力于C++开发,Boost 库提供了 TRI 组件实现品,以及其他程序库