Effective C++浅浅学习,很多不太理解
- 尽量用const, enum, inline 替换#define
- 尽可能使用const
- 确认对象在使用前就已经被初始化
- 构造,析构,赋值运算,拷贝构造
- 为多态基类声明virtual析构函数
- 不要让析构函数抛出异常
- 不在构造和析构过程中调用virtual函数
- 令operator= 返回一个指向 *this的引用
- 在operator= 中处理“自我赋值”
- 以对象管理资源/COPY/提供对原始资源的访问
- 用独立语句将new的对象置入智能指针
- 写更加容易被正确使用的接口
- 以pass-by-reference-to-const替换pass-by-value
- 以non-member、non-friend替换member函数
- 若所有参数皆需类型转换,请为此采用non-member函数
- 尽量不要进行强制类型转换
- 异常安全的考虑
- 不要过度使用inline
- 降低文件间编译的依存关系
- 一些需要在继承时注意的
- 考虑virtual函数以外的其他选择
- 复合类
- private继承和多重继承
- 隐式接口和编译期多态
- typename的双重意义
- 学习处理模板化基类内的名称
- 将与参数无关的代码抽离templates
- 运用成员函数模板接受所有兼容类型
- 需要类型转换时请为模板定义非成员函数
- 认识template元编程
- 了解new-handler的行为
- new和delete
尽量用const, enum, inline 替换#define
- define只在预处理阶段起作用,简单的文本替换,因为在预处理阶段就已经做了替换,发生错误的时候,报错信息不好找。
- 可以用const来代替宏定义,const在编译、链接过程中起作用;
- const定义的常量是变量带类型,而#define定义的只是个常数不带类型;
define只是简单的字符串替换没有类型检查。而const是有数据类型的,是要进行判断的,可以避免一些低级错误;
- define预处理后,占用代码段空间,const占用数据段空间;
- const不能重定义,而define可以通过#undef取消某个符号的定义,进行重定义;
- define独特功能,比如可以用来防止文件重复引用。
C++里有一个特殊的:类中的静态数据,在类中声明,在类外定义,其中整数可以不定义就使用。
enum可以替换const和define,优雅的实现一个常量的设置。
class A{
private:
enum{NumTurns =5};
int scores[NumTurns]; //constant
public:
int numTurns(){
return NumTurns;
}
};
使用inline函数,替换宏定义函数
- #define是关键字,inline是函数;
- 宏定义在预处理阶段进行文本替换,inline函数在编译阶段进行替换;
- inline函数有类型检查,相比宏定义比较安全;
#define 更容易出错。inline可以获得宏的所有效率,也可以获得类型安全。
尽可能使用const
const出现在* 左侧,指向的是常量,
const出现在* 右侧,指针本身是常量。
顶层const:指的是const修饰的变量本身是一个常量,星号右边。
底层const:指的是const修饰的变量所指向的对象是一个常量,星号左边。
int a = 10;
int* const b1 = &a; //顶层const,b1本身是一个常量。指针常量
const int* b2 = &a; //底层const,b2本身可变,所指的对象是常量。常量指针
STL中迭代器以指针为模型。
const可以使代码变得更加健壮,确保哪些东西可以修改,哪些东西不可以修改。const还可以定义不同的函数重载。
确认对象在使用前就已经被初始化
内置类型如int,string等在默认构造函数进入构造函数体之前就已经完成了初始化,构造函数体中是在进行赋值,一般采用列表初始化的方法,直接进行初始化,直接调用拷贝构造方法,效率更高一些。
class A{
public:
A():a(0){}
private:
int a;
};
C++中不同编译单元中定义的非局部静态对象的初始化顺序是不固定的。所以定义的时候,最好将非局部静态对象定义为局部静态对象。
构造,析构,赋值运算,拷贝构造
- 如果不自己声明任何函数,编译器会生成默认构造,拷贝构造,赋值运算符和析构函数,也不是任何时候都生成,需要用到才会生成。
- 如果定义了构造函数,编译器就不会再生成默认构造函数
- 如果类中的数据成员为static或者引用,编译器就会拒绝自动生成赋值运算符操作。
- 如果基类的拷贝赋值运算符为private,派生类将无法生成拷贝赋值运算符,无法处理基类部分。
- 如果不想编译器自动生成函数,就该明确拒绝。
- 解法一:将拷贝构造和拷贝赋值运算符都设置为private,只给声明,不写定义,编译的时候没问题,链接的时候会出问题。
- 解法一更进一步:定义一个基类,以后定义的类继承自这个基类,就没有办法拷贝构造和赋值运算。
- 解法二:C++11 直接调用delete。
拷贝构造函数:
- 当编写一个copy或者拷贝构造函数,应该确保复制成员里面的所有变量,以及所有基类的成员
- 不要尝试用一个拷贝构造函数调用另一个拷贝构造函数,如果想要精简代码的话,应该把所有的功能机能放到第三个函数里面,并且由两个拷贝构造函数共同调用
- 当新增加一个变量或者继承一个类的时候,很容易出现忘记拷贝构造的情况,所以每增加一个变量都需要在拷贝构造里面修改对应的方法。
为多态基类声明virtual析构函数
- 类的多态性,基类指针可以指向派生类的对象,如果删除该基类的指针,就会调用该指针指向的派生类析构函数,而
派生类的析构函数又自动调用基类的析构函数,这样整个派生类的对象完全被释放。
如果析构函数不被声明成虚函数,则编译器实施静态绑定,在删除基类指针时,只会调用基类的析构函数而不调用派生类析构函数,这样就会造成派生类对象析构不完全,造成内存泄漏。
如果类不会成为基类,也不要刻意的去写上virtual的析构函数,会引入虚表指针,占据额外的空间。
标准库的string类型不含虚函数,将string作为基类的话,有可能会出现内存泄漏。
想创建一个抽象类,但是没有任何纯虚函数,怎么办?声明纯虚析构函数。
不要让析构函数抛出异常
- 再封装一层中间层,下层抛出异常的话,在这一层捕捉。
class DBConnection{
public:
static DBConnection create();
void close();
};
class DBConn{
public:
~DBConn(){
// db.close();
// 方案1
try
{
db.close();
}
catch(const std::exception& e)
{
std::abort();
// 或者记录下异常
}
}
private:
DBConnection db;
};
- 重新设计接口,使其客户有机会对可能出现的问题作出反应。
class DBConn
{
public:
void close() //给客户使用的
{
db.close();
closed = true;
}
~DBConn()
{
if (!closed)
{
try //如果客户没关闭,再关闭 双保险
{
db.close();
}
catch (const std::exception &e)
{
std::abort();
// 或者记录下异常
}
}
}
private:
DBConnection db;
bool closed;
};
如果某个操作可能在失败时抛出异常,又存在某种需要必须处理该异常,那么这个异常必须来自析构函数以外的某个函数
析构函数不要抛出异常,因该在内部捕捉异常。如果客户需要对某个操作抛出的异常做出反应,应该将这个操作放到普通函数(而不是析构函数)里面
不在构造和析构过程中调用virtual函数
如果在构造函数中执行virtual函数,派生类对象的基类部分,会首先构造,构造基类时,虚函数永远不会进入派生类。所以要避免这种情况。
- 如果在父类的构造函数中,调用子类的虚函数,是没有办法做到的。
- 如果在父类的析构函数中,调用子类的析构函数,也是没有办法的。
令operator= 返回一个指向 *this的引用
该约定主要是为了支持连读和连写,这个操作同样适用于-=,+=,*=等
class A{
public:
A& operator=(const A&rhs){ //返回的是当前类的一个引用。
//..
return *this;
}
A& operator+=(const A&rhs){ //返回的是当前类的一个引用。
//..
return *this;
}
};
在operator= 中处理“自我赋值”
主要是要处理 a[i] = a[j] 或者 *px = *py这样的自我赋值。有可能会出现一场安全性问题,或者在使用之前就销毁了原来的对象,例如
class Bitmap{...}
class Widget{
private:
Bitmap *pb;
};
Widget& Widget::operator=(const Widget& rhs){
delete pb; // 当this和rhs是同一个对象的时候,就相当于直接把rhs的bitmap也销毁掉了
pb = new Bitmap(*rhs.pb);
return *this;
}
改进处理:
class Widget{
void swap(Widget& rhs); //交换this和rhs的数据
};
Widget& Widget::operator=(const Widget& rhs){
Widget temp(rhs) //为rhs数据制作一个副本
swap(temp); //将this数据和上述副本数据交换
return *this;
}//出了作用域,原来的副本销毁
以对象管理资源/COPY/提供对原始资源的访问
为了防止在delete语句执行前return,所以需要用对象来管理这些资源。这样当控制流离开f以后,该对象的析构函数会自动释放那些资源。
- 将资源放进智能指针,如shared_ptr或unique_ptr对象包装中,函数体结束以后,资源就会自动释放,调用析构函数。尽量使用shared_ptr,行为更加符合逻辑一些。
- 为了防止资源泄露,使用RAII对象,在构造函数中获得资源并在析构函数中释放资源。
在资源管理类里面,如果出现了拷贝复制行为的话,需要注意这个复制具体的含义,从而保证和我们想要的效果一样
下面的代码会对锁进行拷贝,但是并没有达到想要的独立类对象的需求。
class Lock{
public:
explicit Lock(Mutex *pm):mutexPtr(pm){
lock(mutexPtr);//获得资源锁
}
~Lock(){unlock(mutexPtr);}//释放资源锁
private:
Mutex *mutexPtr;
}
Lock m1(&m)//锁定m
Lock m2(m1);
//好像是锁定m1这个锁。。而我们想要的是除了复制资源管理对象以外,还想复制它所包括的资源(deep copy)。通过使用shared_ptr可以有效避免这种情况。
- 要么禁止拷贝复制
- 要么用引用计数,添加智能指针。可以作为删除器来使用
class Lock{
public:
explicit Lock(Mutex* pm):mutexPtr(pm,unlock){ //用互斥量和unlock函数初始化shared_ptr
lock(mutexPtr.get());//作为删除器
}
private:
std::shared_ptr<Mutex> mutexPtr;//获取普通指针
};
用独立语句将new的对象置入智能指针
主要是为了防止内存泄漏
考虑以下场景:
processWidget(shared_ptr<Widget>(new Widget), priority()) // 可能会造成内存泄漏
内存泄漏的原因为:先执行new Widget,再调用priority, 最后执行shared_ptr构造函数,那么当priority的调用发生异常的时候,new Widget返回的指针就会丢失了。当然不同编译器对上面这个代码的执行顺序不一样。所以安全的做法是:
shared_ptr<Widget> pw(new Widget)
processWidget(pw, priority())
这就是用独立语句将new的对象置入智能指针。凡是有new语句的,尽量放在单独的语句当中,特别是当使用new出来的对象放到智能指针里面的时候
写更加容易被正确使用的接口
Date(int month, int day, int year);
这一段代码可以有很多问题,例如用户将day和month顺序写反(因为三个参数都是int类型的),可以修改成:
Date(const Month &m, const Day &d, const Year &y);//注意这里将每一个类型的数据单独设计成一个类,同时加上const限定符,为了让接口更加易用,可以对month加以限制,只有12个月份
class Month{
public:
static Month Jan(){return Month(1);}//这里用函数代替对象,主要是方式第四条:non-local static对象的初始化顺序问题
}
对于一些返回指针的问题函数,例如:Investment *createInvestment(); 可以设计成返回类型为智能指针类型:std::shared_ptr<Investment> createInvestment();
以pass-by-reference-to-const替换pass-by-value
有些传递参数的时候用引用和cosnt结合的操作,可以降低很多的构造和析构函数调用。
bool validateStudent(const Student &s);//省了很多构造析构拷贝赋值操作
bool validateStudent(s);
subStudent s;
validateStudent(s);//调用后,则在validateStudent函数内部实际上是一个student类型,如果有重载操作的话会出现问题
对于内置类型和stl标准库中的迭代器和函数对象,pass-by-value更加合适
必须返回对象时,别妄想返回其reference,因为很容易返回一个被删除的局部变量,就算是没被删除,在外部也不好释放堆内存
以non-member、non-friend替换member函数
class WebBrowser{
public:
void clearCache();
void clearHistory();
void removeCookies();
}
member 函数:
class WebBrowser{
public:
......
void clearEverything(){ clearCache(); clearHistory();removeCookies();}
}
non-member non-friend函数:
void clearBrowser(WebBrowser& wb){
wb.clearCache();
wb.clearHistory();
wb.removeCookies();
}
- member可以访问class的private函数,enums,typedefs等,但是non-member函数则无法访问上面这些东西,所以non-member non-friend函数更好。
- 熟悉namespace用法:namespace可以用来对某些便利函数进行分割,将同一个命名空间中的不同类型的方法放到不同文件中,C++标准库就是这么用的。
若所有参数皆需类型转换,请为此采用non-member函数
class Rational{
public:
const Rational operator* (const Rational& rhs)const;
}
Rational oneHalf;
result = oneHalf * 2;
result = 2 * oneHalf;//出错,因为没有int转Rational函数
non-member函数
class Rational{}
const Rational operator*(const Rational& lhs, const Rational& rhs){
return Rational(lhs.numerator()*rhs.numerator(), lhs.denominator()* rhs.denominator());
}
result = 2 * oneHalf; //不会出错
member函数中,无法解析2导致编译出错,non-member函数重新定义,就可以对2进行编译。
尽量不要进行强制类型转换
- 尽可能延后变量定义式的出现时间:主要是防止变量在定义以后没有使用,影响效率,应该在用到的时候再定义,同时通过default构造而不是赋值来初始化。
尽量不要进行强制类型转换:
- double转int会丢失精度。
- 将一个类转换成他的父类也容易出现问题
C++的四种新型类型转换:应优先使用 static_cast。如果需要处理多态性,使用 dynamic_cast。只有在特殊情况下,当其他转换不适用时,才考虑使用 const_cast 或 reinterpret_cast。
- static_cast:
它可以在相关类型之间进行转换,例如将浮点数转换为整数,或将派生类的指针转换为基类的指针。
int i = 10;
float f = static_cast<float>(i); // 将int转换为float
- dynamic_cast :
主要用于处理多态。
它在运行时执行安全的向下转换(由基类指针转换为派生类指针)。如果转换失败(即如果指针不实际指向派生类对象),则返回 nullptr。
Base* b = new Derived();
Derived* d = dynamic_cast<Derived*>(b); // 安全的向下转换
- const_cast:
用来移除或添加 const(或 volatile)属性。它通常用于指针或引用,允许修改被 const 修饰的变量。
const int ci = 10;
int* modifiable = const_cast<int*>(&ci); // 移除const属性
- reinterpret_cast:提供了低级别的强制转换,它可以将任意指针类型转换为另一个指针类型,甚至可以转换为足够大的整数类型。这种转换不做任何类型检查,因此可能导致不安全的行为。
void* p = new int(10);
int* ip = reinterpret_cast<int*>(p); // 将void*转换为int*
异常安全的考虑
- 尽量不要返回指向private变量的指针引用等,如果真的要用,尽量使用const进行限制,同时尽量避免悬吊的可能性。
- 函数提供的异常安全保证,通常最高只等于其所调用各个函数的“异常安全保证”中最弱的那个。即函数的异常安全保证具有连带性
不要过度使用inline
关键字 inline 建议编译器使用函数定义中的代码替换对该函数的每次调用。
理论上,使用内联函数可以加速程序运行,因为它们消除了与函数调用关联的开销。 调用函数需要将返回地址推送到堆栈、将参数推送到堆栈、跳转到函数体,然后在函数完成时执行返回指令。 通过内联函数可以消除此过程。 相对于未内联扩展的函数,编译器还有其他机会来优化内联扩展的函数。
- 内联函数的一个缺点是程序的整体大小可能会增加。
- 内联代码替换操作由编译器自行决定。 例如,如果某个函数的地址已被占用或编译器判定函数过大,则编译器不会内联该函数。
inline 函数的过度使用会让程序的体积变大,内存占用过高
- 编译器是可以拒绝将函数inline的,不过当编译器不知道该调用哪个函数的时候,会报一个warning。
- 尽量不要为template或者构造函数设置成inline的,因为template inline以后有可能为每一个模板都生成对应的函数,从而让代码过于臃肿 同样的道理,构造函数在实际的过程中也会产生很多的代码.
降低文件间编译的依存关系
当有代码:
#include"date.h"
#include"address.h"
class Person{
private
Dates m_data;//Dates类
Addresses m_addr;//Addresses类
}
这种代码情况下,Person和Dates以及Addresses文件之间,形成了编译依存关系。如果这些头文件有任何一个改变,或者这些头文件所依赖的其他头文件有所改变,那么含Person class的文件就得重新编译。程序头文件应该有且仅有声明
如何实现解耦?
- 定义一个接口类Impl:接口类与具体细目结合,用“声明的依存性”替换“定义的依存性”。
- 或者定义一个全虚函数:然后通过继承的子类来实现相关的方法。这种情况下这些virtual函数通常被成为factory工厂函数。
原代码:
class Person{
private
Dates m_data;
Addresses m_addr;
}
添加一个Person的实现类,定义为PersonImpl,修改后的代码:
class PersonImpl;
class Person{
private:
shared_ptr<PersonImpl> pImpl;
}
全虚函数
class Person{
public:
virtual ~Person();
virtual std::string name() const = 0;
virtual std::string birthDate() const = 0;
}
一些需要在继承时注意的
- 保证继承逻辑正确性,企鹅不是鸟类。
- 避免遮掩继承而来的名称:如果继承base类并加上重载函数,而且又希望重新定义或覆写其中的一部分,那么需要加载一个using声明,否则会有些继承函数被覆盖。
- pure virtual 函数式提供了一个接口继承,当一个函数式pure virtual的时候,意味着所有的实现都在子类里面实现。不过pure virtual也是可以有实现的,调用他的方法是在调用前加上基类的名称ps->Shape::draw();
- 接口继承和实现继承不同,在public继承下,derived classes总是继承base的接口。
- pure virtual函数只具体指定接口继承。
- (非纯)impure virtual函数具体指定接口继承以及缺省实现继承。
- non-virtual函数具体指定接口继承以及强制性的实现继承
考虑virtual函数以外的其他选择
模板方法模式:NVI手法:通过public non-virtual成员函数间接调用private virtual函数,即所谓的template method设计模式:这种方法的优点在于事前工作和事后工作,这些工作能够保证virtual函数在真正工作之前之后被单独调用
class GameCharacter{
public:
int healthValue() const{
//做一些事前工作
int retVal = doHealthValue();
//做一些事后工作
return retVal;
}
private:
virtual int doHealthValue() const{
... //缺省算法,计算健康函数
}
}
- 函数指针(strategy设计模式:
class GameCharacter; // 前置声明
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter{
public:
typedef int (*HealthCalcFunc)(const GameCharacter&);//函数指针
explicit GameCHaracter(HealthCalcFunc hcf = defaultHealthCalc):healthFunc(hcf){}//可以换一个函数的
int healthValue()const{return healthFunc(*this);}
private:
HealthCalcFunc healthFunc;
}
不要重新定义继承而来的non-virtual函数
绝不重新定义继承而来的缺省参数值
:virtual函数是动态绑定,但是缺省参数值是静态绑定。
class Shape{
public:
enum ShapeColor {Red, Green, Blue};
virtual void draw(ShapeColor color=Red)const = 0;
};
class Rectangle : public Shape{
public:
virtual void draw(ShapeColor color=Green)const;//和父类的默认参数不同
}
Shape* pr = new Rectangle; // 注意此时pr的静态类型是Shape,但是他的动态类型是Rectangle
pr->draw(); //virtual函数是动态绑定,而缺省参数值是静态绑定,所以会调用Red
复合类
- 复合是一种类型之间的关系,当某种类型的对象内含有其他类型的对象时,就是复合关系。
- 比如一个Person类,包含一个Address对象,一个PhoneNumber对象。
- set并不是一个list,但是set可以has a list:
template<class T>
class Set{
public:
void insert();
//.......
private:
std::list<T> rep;
}
private继承和多重继承
- 因为private继承只是一种实现上的继承,不包含接口继承,即有一部分父类的private成员是子类无法访问的,而且经过private继承以后,子类的所有成员都是private的。
- private继承通常比复合的级别低,但是
当derived class 需要访问protect base class 的成员,或者需要重新定义继承而来的virtual函数时
,这么设计是合理的。
多重继承:
class BorrowableItem{
public:
void checkOut();
};
class ElectronicGadget{
bool checkOut()const;
};
class MP3Player:public BorrowableItem, public ElectronicGadget{...};
MP3Player mp;
mp.checkOut();//歧义,到底是哪个类的函数
只能使用:
mp.BorrowableItem::checkOut();
- 在实际应用中, 经常会出现两个类继承与同一个父类,然后再有一个类多继承这两个类:
class Parent{...};
class First : public Parent(...);
class Second : public Parent{...};
class last:public First, public Second{...};
- 这个时候,last子类会包含两个Parent类中的成员数据,这属于数据冗余,是不能容忍的。可以采用virtual虚继承来避免这种情况。但是virtual继承会带来额外的代价。
隐式接口和编译期多态
- 面向对象编程:以显式接口(explicit interfaces)和运行期多态(runtime polymorphism)解决问题。
class Widget {
public:
Widget();
virtual ~Widget();
virtual std::size_t size() const;
void swap(Widget& other); //第25条
}
void doProcessing(Widget& w){
if(w.size()>10){...}
}
- 在templete编程中:隐式接口(implicit interface)和编译器多态(compile-time polymorphism)更重要。
template<typename T>
void doProcessing(T& w)
{
if(w.size()>10){...}
}
- 在上面这段代码中,w必须支持哪一种接口,由template中执行于w身上的操作来决定,例如T必须支持size等函数。这叫做隐式接口。
- w的任何函数调用,例如operator>,都有可能造成template具现化,使得调用成功,根据不同的T调用具现化出来不同的函数,这叫做编译期多态。
typename的双重意义
- 声明template参数时,前缀关键字class和typename是可以互换的
// class和typename两者有什么不同? 没有不同
template<class T> class A;
template<typename T> class A;
- 需要使用typename标识嵌套从属类型名称,但不能在base class lists(基类列)或者member initialization list(成员初始列)内以它作为base class修饰符:
template<typename C>
void print2nd(const C& container){
if(container.size() >=2)
typename C::const_iterator iter(container.begin());
//这里的typename表示C::const_iterator是一个类型名称,
//因为有可能会出现C这个类型里面没有const_iterator这个类型
//或者C这个类型里面有一个名为const_iterator的变量
}
template class Derived : public typename Base ::Nested{}//错误的!!!!!
学习处理模板化基类内的名称
- class CompanyA和CompanyB都有发送明文的函数:sendCleartext
- 信息类MsgInfo:
- MsgSender:模板类,负责发送消息。根据不同的Company去调用sendCleartext。
- LoggingMsgSender:在MsgSender的基础上又记录了一些日志。但是无法编译,因为找不到一个特例化的MsgSender< company>。
class CompanyA{
public:
void sendCleartext(const std::string& msg);
....
};
class CompanyB{....};
class MsgInfo{....};
template <typename Company>
class MsgSender{
public:
void sendClear(const MsgInfo& info){
std::string msg;
Company c;
c.sendCleartext(msg);
}
}
template<typename Company>//想要在发送消息的时候同时写入log,因此有了这个类
class LoggingMsgSender:public MsgSender<Company>{
public:
void sendClearMsg(const MsgInfo& info){
//记录log
sendClear(info);//无法通过编译,因为找不到一个特例化的MsgSender<company>
// 编译的时候,不会主动的调用基类中的函数,因为存在一个特化版本,不存在这个函数会出问题。
}
}
解决方法:对编译器说:base class template的任何特例化版本都支持其一般版本所提供的接口。
- 方法一:生成一个全特例化的模板
- 方法二:使用this:假设sendClear会被继承,会从基类里面寻找
template<typename Company>
class LoggingMsgSender:public MsgSender<Company>{
public:
void sendClearMsg(const MsgInfo& info){
//记录log
this->sendClear(info);//假设sendClear将被继承
}
}
- 方法三:使用using:告诉编译器,请他假设sendClear位于base class里面
template<typename Company>
class LoggingMsgSender:public MsgSender<Company>{
public:
using MsgSender<Company>::sendClear; //告诉编译器,请他假设sendClear位于base class里面
void sendClearMsg(const MsgInfo& info){
//记录log
sendClear(info);//假设sendClear将被继承
}
}
- 方法四:指明位置,和使用using差不多。
template<typename Company>
class LoggingMsgSender:public MsgSender<Company>{
public:
void sendClearMsg(const MsgInfo& info){
//记录log
MsgSender<Company>::sendClear(info);//假设sendClear将被继承
}
}
将与参数无关的代码抽离templates
- 在下面的代码中,sm1和sm2分别属于两个类,就会产生invert()函数,造成代码冗余。
template<typename T, std::size_t n>
class SquareMatrix{
public:
void invert(); //求逆矩阵
}
SquareMatrix<double, 5> sm1;
SquareMatrix<double, 10> sm2;
- 修改代码:SquareMatrix继承自base类,避免了遮掩base版的invert,n的不同大小,不会产生不同的类,代码冗余降低。
template<typename T>
class SquareMatrixBase{
protected:
void invert(std::size_t matrixSize);
}
template<typename T, std::size_t n>
class SquareMatrix:private SquareMatrixBase<T>{
private:
using SquareMatrixBase<T>::invert; //避免遮掩base版的invert
public:
void invert(){ this->invert(n); } //一个inline调用,调用base class版的invert
}
- templates生成多个classes和多个函数,所以任何template代码都不该与某个造成膨胀的template参数产生依赖关系。
- 因非类型模板参数(non-type template parameters)而造成的代码膨胀,往往可以消除,做法是以函数参数后者class成员变量替换template参数
运用成员函数模板接受所有兼容类型
利用多态性将基类指针指向派生类对象很简单,但是对于包含模板的类型转换是比较麻烦的。因为编译器对于派生类模板参数和基类模板参数,是没有连接关系的。
Top* pt2 = new Bottom; //将Bottom*转换为Top*是很容易的
template<typename T>
class SmartPtr{
public:
explicit SmartPtr(T* realPtr);
};
SmartPtr<Top> pt2 = SmartPtr<Bottom>(new Bottom);//将SmartPtr<Bottom>转换成SmartPtr<Top>是有些麻烦的
可运用模板成员函数,来进行变动:使用成员函数模板生成“可接受所有兼容类型”的函数
template<typename T>
class SmartPtr{
public:
template<typename U>
SmartPtr(const SmartPtr<U>& other) //为了生成copy构造函数
:heldPtr(other.get()){....}
T* get() const { return heldPtr; }
private:
T* heldPtr; //这个SmartPtr持有的内置原始指针
};
需要类型转换时请为模板定义非成员函数
进行混合类型算术运算的时候,会出现编译通过不了的情况
template<typename T>
const Rational<T> operator* (const Rational<T>& lhs, const Rational<T>& rhs){....}
Rational<int> oneHalf(1, 2);
Rational<int> result = oneHalf * 2; //错误,无法通过编译
// no match for 'operator' (operand types are 'Ratinal<int>' and 'int'
解决方法:使用friend声明一个函数,进行混合式调用
这里的friend和非friend函数是没有关联的。类外部的函数,并非为声明函数的实现。
template<typename T>
class Rational{
public:
friend const Rational operator*(const Rational& 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<T>&rhs){....}
当我们编写一个class template, 而他所提供的“与此template相关的”函数支持所有参数隐形类型转换时,请将那些函数定义为classtemplate内部的friend函数
认识template元编程
Template metaprogramming是编写执行于编译期间的程序
,因为这些代码运行于编译器而不是运行期,所以效率会很高,同时一些运行期容易出现的问题也容易暴露出来
。
template<unsigned n>
struct Factorial{
enum{
value = n * Factorial<n-1>::value
};
};
// 类似递归出口
template<>
struct Factorial<0>{
enum{ value = 1 };
}; //这就是一个计算阶乘的元编程
了解new-handler的行为
当new无法申请到新的内存的时候,会不断的调用new-handler,直到找到足够的内存,new_handler是一个错误处理函数:
namespace std{
typedef void(*new_handler)();
new_handler set_new_handler(new_handler p) throw();
}
new_handler是一个函数指针,可以用set_new_handler把函数设置到系统中,返回一个new_handler:set_new_handler允许客户制定一个函数,在内存分配无法获得满足时被调用
一个设计良好的new-handler要做下面的事情:
- 让更多内存可以被使用
- 安装另一个new-handler,如果目前这个new-handler无法取得更多可用内存,或许他知道另外哪个new-handler有这个能力,然后用那个new-handler替换自己
- 卸除new-handler
- 抛出bad_alloc的异常
- 不返回,调用abort或者exit
new-handler无法给每个class进行定制,但是可以重写new运算符,设计出自己的new-handler
此时这个new应该类似于下面的实现方式:
void* Widget::operator new(std::size_t size) throw(std::bad_alloc){
NewHandlerHolder h(std::set_new_handler(currentHandler)); // 安装Widget的new-handler
return ::operator new(size);
//分配内存或者抛出异常,恢复global new-handler
}
new和delete
- 了解new和delete的合理替换时机。
- 重写delete的时候,要保证删除null指针永远是安全的
- 如果operator new接受的参数除了一定会有的size_t之外还有其他的参数delete的时候,也要注意。
参考列表:
Effective C++
https://www.bilibili.com/video/BV1Vs4y1z7zo/