本文为Effective C++的学习笔记,第一遍学习有很多不理解的地方,后续需要复习。
声明(declaration)
告诉编译器某个东西的名称和类型,但略去细节;
每个函数的声明揭示其签名式(signature),也就是参数和返回类型。
定义(definition)
初始化(initialization)
给予对象初值的过程;
对用户自定义的类型而言,初始化由构造函数执行。
C++是多重范型编程语言,包括4种次语言:
C++高效编程视状况而变化,取决于使用C++的哪一部分。
如果关键词const出现在星号左边,表示被指物是常量(底层const);如果出现在右边,表示指针自身是常量(顶层const);
令函数返回一个常量值,可以预防无意义的赋值动作;
const成员函数:
两个成员函数如果只是常量性不同,可以被重载;
//const成员函数说明隐式this指针指向一个const
const char& operator[](std::size_t position) const//常量性声明,注意位置
const对象只能访问const成员函数,而非const对象可以访问任意的成员函数;
const成员函数不能修改对象的数据成员,const对象的成员变量不可以修改(mutable修饰的数据成员除外)。
当const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本避免代码重复(使用转型,条款27提及)。
对象成员变量的初始化动作发生在进入构造函数本体之前;
为内置型对象进行手工初始化,内置类型以外构造函数负责初始化;
构造函数最好使用成员初值列 ,而不使用赋值操作 ,最好总是以声明次序为其次序;
为免除“跨编译单元之初始化次序”问题,最好以local static对象替换non-local static对象。
为驳回编译器暗自提供的机能,可以将相应的成员函数声明为private而且不实现他们,或者使用像Uncopyable这样的base class;
class HomeForSale
{
public:
...
private:
...
HomeForSale(const HomeForSale&); //只有声明
HomeForSale& operator=(const HomeForSale&);
}
class Uncopyable
{
protected:
Uncopyable(){} //允许派生类对象构造和析构
~Uncopyable(){}
private:
Uncopyable(const Uncopyable&); //但阻止copying
Uncopyable& operator=(const Uncopyable&);
};
//为求阻止HomeForSale对象被拷贝,我们可以继承Uncopyable:
class HomeForSale: private Uncopyable
{
};
get成员函数实现显式转换(安全,受欢迎):
隐式转换函数(方便)。
在一个语句中编译器拥有重新排列操作的自由,如此一来可能被异常干扰,发生资源泄露,因此以独立语句将newed对象置入智能指针内:
processWidget (std::trl::shared_ptr<Widget>(new Widget), prority());
//若先执行new,再调用priority,再调用shared_ptr构造函数:万一对priority的调用导致异常,则new返回的指针将会遗失,因为它尚未置入shared_ptr内;
std::trl::shared_ptr<Widget> pw(new Widget);//在单独语句内以智能指针存储new对象
processWidget(pw, priority());//不会导致泄露
好的接口很容易被正确使用,不容易被误用:
“促进正确使用”的办法包括接口的一致性,以及与内置类型的行为兼容;“防治误用”的办法包括建立新类型,限制类型上的操作,束缚对象值,以及消除用户的资源管理责任;
tr1::shared_ptr支持定制型删除器,可预防DLL问题,可被用来自动解除互斥锁等等==(没太看懂)==。
pass-by-value的缺点
尽量以pass-by-reference-to-const替换pass-by-value,前者通常更加高效,可以避免切割问题;
以上规则并不适用于内置类型,STL的迭代器和函数对象,对他们而言,pass-by-value比较合适。
假设有这样的有理数类:
class Rational
{
public:
//构造函数不为explicit;允许int-to-Rational的隐式转换(分子为某int,分母为1)
Rational (int numerator = 0, int denominator = 1);
int numerator() const;//分子和分母的访问函数
int denominator() const;
private:
...
}
若operator*为成员函数:
class Rational
{
.....
// 运算符重在,定义两个Rational对象的乘法
const Rational operator*(const Rational& rhs)const;
}
此时若调用
Rational oneHalf(1,2);
Rational result1=oneHalf*2; // => 正确
Rational result2=2*oneHalf; // => 错误
本质上,上述两个调用过程是这样的:
result1=oneHalf.operator*(2);
result2=2.operator*(oneHalf);
/*重载的运算符其实有两个参数,一个是传入的`rhs`,另一个是隐含的`this`;
第一个调用直接将2隐式转换为了Rational对象;
但是第二个调用时2在前面,它并没有相应的class,也就没有operator*成员函数,从而无法进行类型转换。*/
解决方法:将operator*()这个成员函数改为non-member函数:
// 不属于任何类/对象
const Rational operator*(const Rational& lhs,const Rational& rhs){
// ................
}
于是就可以调用result2=2.operator*(oneHalf);
了。
menber函数的反面是non-member函数,而不是friend函数;
无论何时,都该尽量避免friend函数
;
swap是STL中的标准函数,用于交换两个对象的数值。后来swap成为异常安全编程(exception-safe programming,条款29)的脊柱,也是实现自我赋值(条款11)的一个常见机制。swap的实现如下:
namespace std
{
template<typename T>
void swap(T& a, T& b)
{
T temp(a);
a=b;
b=temp;
}
}
只要T支持copying函数(copy构造函数和copy assignment操作符)就能允许swap函数。
首先,如果如果swap的缺省实现对我们的class或class template效率可以接受,那么无需做任何事;
其次,如果swap缺省实现版 的效率不足(例如,你的class或template使用了某种pimple(pointer to implementation)手法),试着做以下事情:
调用swap时,针对std::swap使用using形式,然后调用swap并且不带任何命名空间资格修饰。
为“用户定义类型”进行std template全特化时,不要试图在std内加入某些对std而言是全新的东西。
C++中的四种转型动作:
C++单一对象可能拥有多个地址
class Base{....}
class Derived:public Base{...}
Derived d;
Base* pd=&d;
pd和&d的地址可能并不相同,这种情况几乎在多重继承上总是会发生。
尽量避免类型转换,尤其是dynamic_cast,因为其效率很低;
如果转型是必要的,试着将它隐藏与某个函数背后,客户随后可以调用该函数,而不需将转型放进他们自己的代码内。
编译依存关系:如果对C++程序的某个类做了轻微的修改,修改的不是接口而是实现,当编译时,很多关联的文件都需要重新编译。
编译依存性最小化的核心:接口与实现分离:
Handle Classes:可以将对象实现细目隐藏在一个指针背后。针对Person,可以把它分为两个classes,一个负责提供接口,另一个负责实现该接口。
#include
#include
class PersonImpl; //前置声明
class Date;
class Address;
class Person
{
public:
Person(const std::string& name, const Date& birthday, const Address& addr);
std::string name() const;
std::string birthDate() const;
std::string address() const;
……
private:
//实现细目
std::tr1::shared_ptr<PersonImpl> pImpl;//指针,指向实现
};
Interface class:令Person成为一种特殊的abstract base class(抽象基类),称作Interface class,只是描述derived classes接口。
编译依存性最小化的本质:用“声明的依存性”替换了“定义的依存性”,让头文件尽可能自我满足,万一做不到,则让它与其他文件内的声明(不是定义)相依;
is a
鸟,但鸟可以飞,企鹅却不能飞?
名称查找规则:先查局部,找不到再查更大范围 ;
继承中的名称遮掩示例:
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(); // 将遮盖父类中的两个mf1函数(即使参数类型不同)
void mf3(); // 将遮盖父类中的两个mf3函数(即使参数类型不同)
void mf4();
};
解决方法:
using 声明式:指定子类中可见父类的某些名称;
class Derived:public Base{
public:
using Base::mf1; // 让Base内的名为mf1和mf3的所有东西,在Derived作用域内都可见
using Base::mf3;
virtual void mf1();
void mf3();
void mf4();
};
转交函数:不想继承base class中的所有函数(使用private继承+inline);
class Derived:private Base{
public:
virtual void mf1()
{Base::mf1();}; // 此时相当于继承了base class中的mf1()版本,而mf1(int) 重载版本被遮掩。
......
};
当在为解决问题寻找某个设计方法时,可以考虑virtual的几种替代方案;
NVI(non-virtual interface)手法:令客户通过public non-virtual成员函数间接调用private virtual函数,相当对virtual函数进行一层的包装,可以称为是virtual函数的外覆器(warpper)。
将virtual函数替换为“函数指针成员变量”:让每个任务的构造函数接受一个函数指针,这样可以更容易实现动态的改变;(没太看懂)
由tr1::function完成的Strategy模式:用function定义一个可调用类型,这样我们能便能传入更多的可调用对象,只要这些可调用对象能够转换为我们定义的可调用对象类型即可。
古典的Strategy模式:将一个继承体系中的virtual函数替换为另一个继承体系的中的virtual函数,然后再用一个基类的指针指向另一个体系:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7aBqCxSn-1653957385741)(img/Effective C++/2021033116260655.png)]
//情况一
class B{
public:
void mf();
...
};
class D:public B{...};
D x;
B *pB = &x;
pB -> mf();//经由该指针调用 B::mf
D *pD = &x;
pD -> mf();//经由该指针调用B::mf
//情况二
class B{
public:
void mf();
...
};
class D: public B{
public:
void mf(); //遮掩了B::mf;
...
}
D x;
B *pB = &x;
pB -> mf();//调用B::mf
D *pD = &x;
pD -> mf();//调用D::mf
静态类型和动态类型
non-virtual:
virtual:
复合
复合是类型之间的一种关系,当某种类型的对象内含其他类型的对象,便是复合关系;
在应用域,复合意味着“has-a” :
class Person{
private:
Address addr;
PhoneNumber voiceNumber;
PhoneNumber faxNumber;
// ......
};
在实现域,复合意味着 “is-implemented-in-terms-of”:
比如用list实现Set,Set类中对类型std::list< T> 的包含就属于“is-implemented-in-terms-of”
template<class T>
class Set{
public:
void member(const T& item) const;
void insert(const T& item);
void remove(const T& item);
std::size_t size()const;
private:
std::list<T> myset;
};
多重继承:
class A {
public:
void check() {
cout << "A" << endl;
}
};
class B {
private:
void check() const {
cout << "B" << endl;
}
};
class C : public A, public B {
};
int main(){
C c;
c.check();
}
多重继承带来的问题
歧义:程序可能从一个以上的base classes继承相同名称(如函数、typedef等),那会导致较多的歧义,此时需要通过类名指明要调用的具体成员;
空间冗余:如果不是virtual继承方式,哪么某个父类的成分可能经过多条路径出现在子类中,造成空间冗余。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5cDPQWwM-1653957385743)(img/Effective C++/20200726104440612.png)]
virtual继承:会在对象大小、速度、初始化、赋值造成成本增加;
多重继承比单一继承复杂,可能导致新的歧义性,以及对virtual继承的需要;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-50VH2DN9-1653957385743)(img/Effective C++/20200726105644910.png)]
多重继承的确有正当用途,其中一个情节涉及“public继承某个Interface class”和“priavate继承某个协助实现的class”的两相组合。
classe和template都支持接口和多态;
对class而言:
接口是显式的,由函数**签名式(函数名称、参数类型、返回类型)**构成;
class Widget {
public:
Widget();
virtual ~Widget();
virtual std::size_t size()const;
virtual void normalize();
void swap(Widget& other);
};
多态是通过virtual函数发生于运行期。
对template而言
接口是隐式的,由有效表达式组成;
template<typename T>
void doProcessing(T& w)
{
if (w.size() > 10 && w != someNastyWidget) {
......
}
}
多态是通过template具现化和函数重载解析发生于编译期。
声明template参数时,前缀关键字class和typename可以互换;
template<class T> class Widget;
template<typename T> class Widget;
使用typename标识嵌套从属类型名称
从属名称:模板内的一个名称依赖于template的某个参数;
嵌套从属名称:当从属名称属于class,如T::const_iterator
;
编译器在template中遭遇一个嵌套从属名称,它便假设这名称不是个类型,而是个变量名;
**为嵌套从属名称加上关键字typename,**以显式地告诉编译器某种东西是一个类型:
template<typename C>
void print2nd(const C& container)
{
if (container.size() >= 2) {
//使用typename,显式告诉编译器,const_iterator是一个类型
typename C::const_iterator iter(container.begin());
++iter;
int value = *iter;
std::cout << value;
}
}
不得在base class lists或member initialization list内作为base class修饰符。
在模板类中,如果一个派生类在其方法中调用了基类的接口,那么这段代码可能无法编译通过:
解决方法:
**使用this指针:**使用this指针调用这些函数,告诉编译器这些函数是属于自身类的;
this->sendClear(info);
使用using声明式:让编译器去基类中查找这个函数;
using MsgSender<Company>::sendClearMsg;
明确指出被调用的函数位于base class中:**不太建议,因为:**被调用的函数可能是virtual函数,这种修饰符会关闭“virtual绑定行为”:
Base<Company>::sendClear(info);
本文中部分内容系转载,原文地址为:https://blog.csdn.net/qq_41453285/article/details/104856465
例:设计一个模板,用来模仿智能指针类,并且希望智能指针能像普通指针一样进行类型转换:
**一种低效的做法:**在SmartPtr模板中针对于每一个派生类定义一个拷贝构造函数和拷贝赋值运算符。这种做法针对每一个派生类设计相对应的拷贝构造函数和拷贝赋值运算符会使class膨胀,并且如果将来加入新的派生类,那么还需要继续添加新的成员函数。
为SmartPtr模板添加一个成员函数模板:根据下面的拷贝构造函数,我们可以对任何类型T和任何类型U,将一个SmartPtr
转换为SmartPtr
:
template<typename T>
class SmartPtr
{
public:
//拷贝构造函数,是一个成员函数模板
typename<typename U>
SmartPtr(const SmartPtr<U>& other);
};
约束成员函数模板的行为
成员函数模板不改变语言规则,在class内声明一个泛化的拷贝构造函数(是个成员模板)并不会阻止编译器生成自己的拷贝构造函数。
如果你声明member templates用于“泛化copy构造”或“泛化assignment操作”,你还是需要声明正常的copy构造函数和copy assignment操作符。
函数模板在混合式运算时可能出错:
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<T>& rhs)
{}
Rational<int> oneHalf(1, 2);
//在条款24中,Rational和operator*为非模板,此处代码可编译通过
//此处,Rational和operator*为模板,onehalf和2类型不同,为混合运算,编译不通过
Rational<int> result = oneHalf * 2;
解决方法一:将模板函数声明为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()*lhs.denominator());
}
};
解决方法二:令friend函数调用辅助函数(与inline相关)
//声明
template<typename T> class Rational
//函数声明(定义在下)
template<typename T>
const Rational<T> doMultiply(const Rational<T>& lhs, const Rational<T>& rhs);
template<typename T>
class Rational {
public:
friend const Rational operator*(const Rational& lhs, const Rational& rhs)
{
//在其中调用doMultiply(),可使inline最优化
return doMultiply(lhs, rhs);
}
};
//完成原本operator*()要完成的功能
template<typename T>
const Rational<T> doMultiply(const Rational<T>& lhs, const Rational<T>& rhs)
{
return Rational<T>(lhs.numerator()*rhs.numerator(),
lhs.denominator()*lhs.denominator());
}
迭代器分为五种:
traits技术,它们允许你在编译期间获取某些类型信息;
对于迭代器,标准库又定义了一个名为iterator_traits的类,用来萃取迭代器的类型:
//用来处理迭代器的信息
template<typename T>
struct iterator_traits;
其根据传入的迭代器,然后萃取迭代器中的iterator_category成员,然后定义为自己的iterator_category成员;
iterator_category成员最初是迭代器的类型,因此,iterator_traits中的iterator_category就代表迭代器的类型;
template
struct iterator_traits
{
//因为iterator_category为数据类型,因此需要用typename
typedef typename IterT::iterator_category iterator_category;
};
如何萃取:
使用traits class的总结:
Traits广泛应用于标准程序库:
Traits classes使得“类型相关信息”在编译期可用,它们以templates和“templates特化”完成实现;
整合重载技术后,traits classes有可能在编译期对类型执行if…else测试。
set_new_handler
**该函数的作用是:**当new操作或new[]操作失败时调用参数所指的new_p函数
该函数定义于头文件中,实际上是一个typedef;
Typedef void (*new_handler)();
new_handler set_new_handler(new_handler new_p) throw(); //C++98
new_handler set_new_handler (new_handler new_p) noexcept; //C++11
该函数接受一个new_handler参数(这个参数通常指向一个函数),并返回一个new_handler数据;
new_p为当new操作或new[]操作失败时调用的函数,参数列表为空,返回值类型为void;
演示案例:定义一个函数,在operator new无法分配足够内存时被调用:
void outOfMem()
{
std::cerr << "Unable to satisfy request for memory\n";
std::abort();
}
int main()
{
//绑定函数
std::set_new_handler(outOfMem);
//如果此处内存分配失败,将会调用outOfMem()
int *pBigDataArray = new int[100000000L];
return 0;
}
new_handler函数的设计原则
为类设置new-handler行为:每个类单独设置new-handler行为,不同的类分配操作失败就调用相对应的new-handler函数。
实现class的new-handler行为:
class Widget {
public:
static std::new_handler set_new_handler(std::new_handler p)throw();
static void* operator new(std::size_t size)throw(std::bad_alloc);
private:
static std::new_handler currentHandler;//static变量需在类外定义
};
std::new_handler Widget::currentHandler = 0;
std::new_handler Widget::set_new_handler(std::new_handler p)throw()
{
std::new_handler oldHandler = currentHandler;
currentHandler = p;
return oldHandler;
}
如何使用:
void outOfMem();
int main()
{
//设定outOfMem()为Widget的new-handler函数
Widget::set_new_handler(outOfMem);
//如果内存分配失败,调用outOfMem()
Widget* pw1 = new Widget;
//Widget::operator new()执行结束后还会将全局new-handler设置回来
//如果内存分配失败,调用全局new-handler函数
std::string* ps = new std::string;
//设定Widget的new-handler函数为null
Widget::set_new_handler(0);
//如果内存分配失败,立刻抛出异常(因为Widget的new-handler函数置空了)
Widget* pw2 = new Widget;
return 0;
}
封装一个new-handler行为的base class:为了使每种class拥有自己的new-handler行为,我们将该类设置为模板,然后让各自的class继承于这个模板,当class在定义时,就会实例化模板,因此不同的类将会拥有各自的new-handler行为。
nothrow说明:
set_new_handler允许客户指定一个函数,在内存分配无法获得满足时被调用。
Nothrow new是一个颇为局限的工具,因为它只适用于内存分配;后继的构造函数调用还是可能会抛出异常。
operator new所要遵守的规则
必须拥有正确的返回值:如果申请成功就返回指针指向于那块内存,如果不足就抛出bad_alloc异常(未设置new-handler的情况下);
内存不足时调用new-hander函数;
必须针应对申请零内存的情况:C++允许用户申请0byte的内存,如返回1byte的内存;
operator new的伪代码形式:
总结:operator new内含一个无限循环,并在其中尝试分配内存,如果它无法满足内存需求,就该调用new-handler。它也应该有能力处理0 bytes申请。class专属版本则还应该处理**“比正确大小更大的(错误)申请”**。
operator new在继承中的注意事项:
对于一个成员函数版本的operator new来说,如果其有派生类,那么派生类的new可能会产生不明确的行为:如Base中的new(),函数中的相关操作是针对于参数为sizeof(Base)大小而设定的,但如果是Derived调用new(),传入的大小为sizeof(Derived),但是该new()不是针对于参数大小为sizeof(Derived)而设定的。
因此需要在基类的operator new()函数中对申请进行判断:
void* Base::operator new(std::size_t size)throw(std::bad_alloc)
{
//如果不是Base调用,那么调用标准库的operator new
if (size != sizeof(Base))
return ::operator new(size);
//...
}
operator new[]:分配一块未加工内存。
operator delete所要遵守的规则:保证“删除null指针”是正确的。
operator delete在继承中的注意事项:
成员函数版本的**delete函数需要两个参数,**其中第二个参数用来检查删除的大小;
第二个参数与成员函数版本的operator new的参数概念相同,需要用这个大小来比较删除对象的大小。**因为其delete可能会被派生类使用,**而派生类的大小与基类的大小不一致。
void Base::operator delete(void *rawMemory, std::size_t size)throw()
{
if (rawMemory == 0) //删除空指针,直接返回
return;
//删除的不是Base,可能是其派生类调用
//那么就调用全局delete进行删除
if (size != sizeof(Base)) {
::operator delete(rawMemory);
return;
}
//...执行内存归还
}
new表达式先后调用operator new和default构造函数;
**placement new:**对于一般的new函数而言,其只有一个参数(size_t),但是如果我们为new()函数多添加一个参数,那么我们就称这个new为placement new;
例如下面是一个Widget,其有:一个placement new(非正常形式的),其参数2用来记录相关分配信息:
class Widget {
public:
//placement new(因为其带有一个ostream的参数)
static void* operator new(std::size_t size,std::ostream& logStream)
throw(std::bad_alloc);
//正常签名式的delete
static void operator delete(void *rawMemory, std::size_t size)
throw();
};
placement new可能导致内存泄漏:运行期系统寻找“参数个数和类型都与operator new相同”的某个operator delete,若没有对应的placement delete,那么当new的内存分配动作需要取消并恢复旧观时就没有任何operator delete会被调用,导致内存泄漏。
对于自定义的placement new,我们也需要自定义一个placement delete:
class Widget {
public:
//placement new
static void* operator new(std::size_t size, std::ostream& logStream)
throw(std::bad_alloc);
//placement delete(与上面的new是配对的)
static void operator delete(void* pMemory, std::ostream& logStream)throw();
//其他代码同上
};
对于new来说:
可以封装一个Base class,使其包含所有正常形式的new和delete:
class StandardNewDeleteForms{
public:
//正常签名式的new和delete
static void* operator new(std::size_t size)throw(std::bad_alloc){
return ::operator new(size);
}
static void operator delete(void* pMemory)throw() {
::operator delete(pMemory);
}
//placement new和placement delete
static void* operator new(std::size_t size,void* ptr)throw(std::bad_alloc){
return ::operator new(size,ptr);
}
static void operator delete(void* pMemory, void* ptr)throw() {
::operator delete(pMemory, ptr);
}
//nothrow new/nothrow delete
static void* operator new(std::size_t size, const std::nothrow_t& nt)throw(){
return ::operator new(size,nt);
}
static void operator delete(void* pMemory, const std::nothrow_t& nt)throw() {
::operator delete(pMemory);
}
};
客户端自己的类可以继承于这个类,获取其所有new与delete,如果自己定义了new和delete,为了防止隐藏StandardNewDeleteForms中的new与delete,可以使用using声明:
class Widget :public StandardNewDeleteForms {
public:
//使基类中所有new和delete在派生类中可见
using StandardNewDeleteForms::operator new;
using StandardNewDeleteForms::operator delete;
//再定义自己的new与delete
static void* operator new(std::size_t size, std::ostream& logStream)throw(std::bad_alloc);
static void operator delete(void* pMemory, std::ostream& logStream)throw();
};