条款2-3:大小写不敏感的字符串
要求实现一个大小写不敏感的string类
这里主要利用traits技术
从STL的源码当中可以看到string是一个模板生成的类型
typedef basic_string<char> string; //进一步看basic_string的声明如下 template<class charT,class traits=char_traits<charT>,class Allocator = allocator<charT>> class basic_string;
关于traits技术还不是很理解,后面学了在补上
basic_string提供了一些判断字符串大于小于等于的函数,这些函数建立在traits上,如果希望改变比较多的行为可以通过提供自定义的traits来实现
```cpp
//通过继承去改变那些需要替换的函数,其安全性后面讨论
struct my_char_traits:public char_traits{
static bool eq(char c1,char c2){
return toupper(c1)==toupper(c2);
}static bool lt(char c1,char c2){
return toupper(c1)<toupper(c2);
}static int compare(const char* s1,const char* s2,size_t n){
for(size_t i=0;i<n;++i){
if(toupper(*s1)!=toupper(*s2))
return i;
++s1,++s2;
}
}static const char* find(const char* s,int n,char c){
while(n– > 0&&toupper(*s)==toupper(c)){
++s;
}
return n>=0?s:0;
}
};
//现在只需像string一样产生一个类型即可
typedef basic_string<char,my_char_traits> Mystring;
#### 条款4-5:可用性最高的泛型容器
* 模板构造函数不是构造函数,因此即使在类里面定义了模板构造函数,编译器仍会生成对应的隐式构造函数,但就算是模板拷贝函数也会阻止编译器对默认构造函数的生成,
#### 条款26- :编译防火墙和Pimpl技法
> 习惯性地#include一些不必要的头文件会严重降低编译的效率和依赖性,尤其是这些头文件又包含了其它的头文件
~~简单地理解就是在你给客户看的头文件里能少用#include就少用#include,能不包含定义就不包含定义~~
*这里插一段可以帮助理解但不一定正确的描述:在设计一个东西的时候,就假设一个类,你要把接口的这些声明放在头文件里,而具体的实现是放在.c文件里,因为这里面有你的"核心"算法之类的东西,然后你编译好后这个.c文件就成了一个.o文件(这里很有可能不对,但可以先暂时这样理解-_-),类的使用者也就是客户#include你给的头文件然后编译的时候去链接这个.o文件就行了,因此客户能了解到的这个类的所有信息都来自你的头文件*
```cpp
//本章将优化下面的代码
//下面的内容是x.h最初的内容,也即客户可以看到的代码
#include<iostream>
#include<ostream>
#include<list>
//A,B,C,D,E都不是模板
//只有A和C定义了虚函数
#include"a.h"
#include"b.h"
#include"c.h"
#include"d.h"
#include"e.h"
//包含的头文件里有相关类的声明与定义
class X:public A,private B{
public:
X(const C&);
B f(int,char*);
C f(int,C);
C& g(B);
E h(E);
virtual std::ostream& print(std::ostream& )const;
private:
std::list<C> clist_;
D d_;
};
inline std::ostream& operator<<(std::ostream& os,const X& x){
return x.print(os);
}
只需要声明就不要#include其定义
比较明显的,E的定义在此头文件内是不需要的,函数返回类型,参数类型,引用类型,指针类型这些都不需要看到类的定义,只要有一个声明即可#include”e.h”可以改成Class E;
另外应该也清楚<iostream>里面是包含<ostream>的,所以#include<ostream>也是可以删除的.但是,
//下面的代码会报错,说fstream这个类型是不完整的,因为fstream的定义在<fstream>里面
#include<iostream>
int main()
{
std::fstream out;
//<iostream>里肯定有一个#include<iosfwd>,而<iosfwd>里面有fstream这个组件的声明,所以不会报no declaration的错误而是incomplete type的错误
return 0;
}
所以经过初步优化,代码是这样的
#include<iosfwd>
#include<list>
#include"a.h"
#include"b.h"
#include"c.h"
#include"d.h"
Class E;
class X:public A,private B{
public:
X(const C&);
B f(int,char*);
C f(int,C);
C& g(B);
E h(E);
virtual std::ostream& print(std::ostream& )const;
private:
std::list<C> clist_;
D d_;
};
inline std::ostream& operator<<(std::ostream& os,const X& x){
return x.print(os);
}
现在的代码达到了只要X的基类和公有接口不变,即使对X的结构做一些修改(比如加了一个private变量),使用了X的用户代码也不会受影响,只需要重新编译一下的地步,下面看能不能继续删除一些#include
- 首先a.h和b.h现在不能删除的原因是X继承于A跟B,因此在此头文件内必须看到A与B的定义才能知道X的大小及其它一些必要信息如虚函数等
- list,c.h和d.h现在不能删除的原因是list
和D是X的私有数据成员(因为作者写这本书是很久之前了,其实作为模板参数的C现在也不需要看到其定义了,因为在这个头文件里这里的list 其实也只是一个”声明”类似的东西,模板还没有真正实例化的,这个模板的实例化应该在X对象的实例化那里,所以其实这里的#include”c.h”也可以用Class C代替)必须要知道其定义才能确定X的大小
- Pimpl(Pointer to implemention)出现
先上代码
跟前面的代码相比,又少了三个#include分别是c,d跟list的头文件,然后private部分变成了一个指针,再看下现在x.cpp里的内容#include<iosfwd> #include"a.h" #include"b.h" class C; class E; class X:public A,private B{ public: //内容不变 private: struct Ximpl; Ximpl* pimpl_; };
```cpp
#include”x.h”
#include”c.h”
#include”d.h”
#include
/*
public接口的实现细目
*/
//对X::Ximpl的定义
struct X::Ximpl{
std::list
D d;
};
可以看到删掉的三个#include被搬到x.cpp文件里来了,以及在X里的那个Ximpl的定义,其实就是把以前的private数据成员捆成了一个struct,然后让原来的类里面只有一个指针来访问这些成员
看完了变化现在再来说原因
> cpp设置private是为了防止客户进行一些未授权的访问
举个例子,假设你写了一个表示圆的类,它现在用的是xy垂直坐标系,然后你定义它的数据成员是一个圆心的坐标和半径,你也设计了供客户使用的计算面积的函数Square。在你提供给客户的头文件内,客户是可以看到你通过一个坐标和半径来定义一个圆,然后如果没有private,也就是说客户可以直接访问这两个数据成员,那这个时候有的客户就觉得自己很牛,要自己写一个函数计算面积,反正他可以直接访问这些成员的,然后他就写好了计算面积的函数,并且用得很开心,但是有一天你说你觉得用xy垂直坐标不好,想用极坐标来表示一个圆,然后你更改了圆这个类的定义当然之前那些数据成员也肯定改了,而客户的那个计算面积的函数是严重依赖这些内部"细节"的,你用垂直坐标的方法计算极坐标肯定是错的,也就导致客代码中所有依赖直接访问数据成员的地方全部需要重写,这肯定是一个噩梦
上面的例子使用private就很好解决,将数据成员变成private,客户就算能看到细节也只能乖乖用提供的Square函数去,也就避免了客户写出一些依赖于这些细节的代码。
而上面优化后的头文件,直接把private的内容也封装起来了,直接让客户看都看不到实现细节,更进一步地解决了这个依赖问题,同时在这个头文件里不需要看到D跟list,这里是声明都不需要看到,D被替换成class D;是因为前面有个函数使用了D类型。现在随便你怎么改X的私有成员,客户代码甚至连编译都不需要重新编译了(必须感叹一下这个方法很牛(⌐■_■))但这个方法也有一些不足的地方,放到最后再讨论,下面再再看下能不能优化
上面不能优化的第二点已经被Pimpl给解决掉了,现在就剩a和b这两个头文件了
注意到B是私有继承,且B没有虚函数,这就有机会了
一般情况下,私有继承是能通过复合对象的方式给替换掉的,偏偏要使用私有继承一般都是为了能访问基类的protected成员和*重写其虚函数*
又举个例子,假设你要设计一个青蛙类,这个类的对象要每秒调用一次自己的GuaGua函数,这个时候你翻到了一个写好的定时器类,定义如下
```cpp
class Timer{
public:
explicit Timer(int tickFrequency);//它有一个自己频率
virtual void onTick()const; //依据频率,每多少秒就调用一次该函数
}
//似乎可以这样定义Frog
class Frog:public Timer{
private:
virtual void ontick()const{
GuaGua();
}
};
//首先这个ontick一定要变成private,如果变成public会让用户认为他们可以调用它,而对于一个青蛙来说它不应该有一个tick接口,其次采用继承的方式可以去重写那个onTick函数
上面的代码就说明了一种偏偏要使用私有继承的情况,但这种方式也绝非必要,仍然可以通过一些方式来达到相同的效果,如下:
class Frog{
private:
class WidgetTimer:public Timer{
public:
virtual void onTick()const;
};
WigetTimer timer_;
};
现在回归正题,B这个类并没有虚函数,如果我们再假设B没有保护成员,那私有继承这种关系就太”强”了我们用不到这么”强”的关系,将其作为私有成员即可,这样就能把B纳入struct XImpl_中了
下面是最终的x.h文件
#include<iosfwd>
#include"a.h"
class B;
class C;
class E;
class X:public A
{
public:
X(const C&);
B f(int,char*);
C f(int,C);
C& g(B);
E h(E);
virtual std::ostream& print(std::ostream& os,const X& x)const;
private:
struct XImpl_;
XImpl_ * pimpl_;
};
inline std::operator<<(std::ostream& os,const X& x){
return x.print(os);
}
现在这个头文件只有两个#include了!比起最初的代码真是非常大的改进( ⓛ ω ⓛ *)
但相应的还有一些代价问题需要讨论
- 哪些部分应该放入XImpl?
- 在XImpl中是否应该加入一个指向X的回指指针?
第一个问题的常见做法有两种
- 将所有的私有成员放进XImpl中,但要注意不能将虚函数放到这里面来。因为如果派生类需要对基类的虚函数进行覆盖(这里提一下子类就算不能访问基类的private函数但能覆盖private函数),那这个虚函数就必须出现在实际的派生类当中,当然如果你不想一个函数被覆盖,把这个函数放进XImpl就起到了一个类似final的效果
- 将XImpl写成与原来的X完全一样的形式,X的作用就是去调用XImpl对应的接口,这种方法实现的X在继承时没什么大用,因为什么东西都在XImpl中,子类没法去覆盖相应的函数
现在考虑第二个问题,首先想一想为什么要加一个回指指针,因为有些private函数操作会去调用public函数,如果没有这个回指指针XImpl就调用不了相应的函数在上面的方法一中有一个折中的方法就是把只有私有函数才会调用的非私有函数放到XImpl中这样就不需要一个回指指针了,这样的被调用的函数不一起放在XImpl的原因就是要给派生类覆盖.同理,方式2直接把所有的资源都放XImpl了更不需要回指指针了
现在考虑一下Pimpl的性能问题
1.最明显的就是内存分配需要的时间
//下面是可能的客户代码y.h与y.cpp
//1
#include"x.h"
class Y{
X x_;
};
Y::Y(){}
//上面要求X是明确可见的,所以有下面这种代码
//2,y.h
class X;
class Y{
X *px_;
};
//y.cpp
#include"x.h"
Y::Y():px_(new X){}
Y::~Y(){delete px_;px_=0;}
//第2种方法很好地隐藏了X但如果程序里大量使用Y那动态分配内存可能降低程序的性能
2.其次是由于资源的由一个指针访问多了至少一层间接操作,同时空间上的开销也至少多了一个指针的开销,如果考虑到对齐多出来的空间是大于等于一个指针的大小的
条款43:正确地使用const
- void f(int x)与void f(const int x)对编译器来说是同一个函数,但后者在定义体当中仍不可修改x,如果同时定义上述两个函数编译会报错
- 但void f(int&)与void f(const int&)对编译器来说不一样
- 对于非内置类型的返回类型,一般加一个const修饰,以帮助客户检查出类似A+B=C(实际想要操作A+B==C)的错误
条款44:类型转换
- 新形式的类型转化中,只有dynamic_cast与C语言不等价
- 如果没有虚函数,那dynamic_cast的操作都是错误的
区分几大内存区域
内存区域 | 特性和对象的生存期 |
---|---|
常量数据 | 常用于存储字符串和一些编译时就知道的常量。程序的整个生存期内数据都有效且只读,因为编译器可能会 对这个区域的数据进行一些特殊的存储优化,擅自修改可能会出现一些未定义的行为 |
栈 | 存自动变量。自动变量在定义时被立即构造,并在其作用域结束时被立即销毁。 栈内存的分配通常比动态内存快很多,因为每次栈的分配只涉及到栈指针的自增操作 |
自由存储 | 两种动态内存区域之一,通过new/delete来分配/释放 。 对象的生存周期可能小于所分配的的存储空间的生存期,因为这里的的对象不要求分配内存时立即进行初始化,销毁对象时也不要求立即归还空间 |
堆 | 另一种动态内存区域,通过malloc/free函数进行分配和释放。堆还是不同于自由存储的,尽管可能有些地方默认的new和delete会调用malloc和free实现。但堆中的内存不能在自由内存中安全释放,反之亦然。也就是说new出来的内存不能用free安全释放。 |
全局/静态 | 程序启动时,全局和静态变量就已经分配了内存,但只有程序执行时才会进行初始化。 对于跨域多个编译单元的全局变量,它们的初始化顺序是未定义的,因此在管理时要特别小心它们之间的依赖性 |
实现自己特定的内存管理
如果要重载operator new与operator delete,记得显式地将它们声明为static,虽然会默认它们是static但这样可以增加可读性,因为一个对象是由它的类创建的,而不是一个对象去创建的,所以operator new函数一定是早于对象它不可能是一个非静态的成员函数
在此之前应该先了解一些placement new相关的知识,该方面的内容在Effective C++和More EffectiveC++中有详细描述
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 2128099421@qq.com