LucienXian's Blog


  • 首页

  • 归档

  • 标签

Effective-cpp-#10&11

发表于 2017-11-29

Have assignment operators return a reference to *this

为什么

其实返回void,返回对象本身等都是可以的,返回引用是为了一下两个原因:

  • 允许连续赋值
  • 防止返回对象时因为调用拷贝构造函数和析构函数带了不必要的开销(而且如果对象没有自定义拷贝构造函数,可能会产生错误)

场景

  • 连续赋值如下,因此为了实现连续赋值,必须返回一个引用指向自身,这样后面的赋值才能生效;
1
2
int x, y, z;
x = y = z = 15;//等价于x = (y = (z = 15))
  • 有些场景下,必须返回指向自身的引用
1
2
3
4
5
6
7
8
9
10
11
12
class Widget{
public:
...
Widget& operator+=(const Widget& rhs){//此举是因为该操作符本身的意义就是自增,然后返回自身值
...
return *this;
}
Widget& operator=(const Widget& rhs){
...
return *this;
}
}

Handle assignment to self in operator=

动机

client在操作时很可能会出现,自我赋值的情况,比如:

1
2
a[i] = a[j]; // i == j时
*px = *py; //px与py恰好指向同一对象时

这种情况下,往往会出现安全性缺失的问题

1
2
3
4
5
6
7
8
9
10
class Bitmap{};
class Widget{
public:
Widget& operator=(const Widget& rhs){
delete pb;//如果是自我赋值,删除pb之后会连rhs里的内容也删除了
pb = new Bitmap(*rhs.pb);
return *this;
}
Bitmap* pb;
};

方法

  • 检查合法性
1
2
3
4
5
6
Widget& operator=(const Widget& rhs){
if (&rhs == this) return *this;//如果是自我赋值,不做任何操作
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}

但这个版本仍具有安全问题,如果new时抛出了异常,这样this会指向一块被删除了的内存;

  • 保证异常安全性
1
2
3
4
5
6
7
Widget& operator=(const Widget& rhs){
Bitmap* temp = pb;
pb = new Bitmap(*rhs.pb);
//即便new出现了异常,但由于我们生成了副本指针,因此能够保证this指向有效内存并且影响自我赋值的安全性
delete temp;
return *this;
}

这个方法会降低效率,因为需要拷贝构造的函数

  • copy&swap

这是一种更加高效的做法

1
2
3
4
5
Widget& operator=(const Widget& rhs){
Widget temp(rhs);
swap(temp);//交换temp与this的指针
return *this
}

建议

  • 令赋值操作返回一个reference to *this
  • 确保自我赋值时的安全性,可以采用比较两个对象的地址是否相同,调整操作里的语句顺序,使用copy&swap的方法

Prototype(DesignPattern)

发表于 2017-11-28

Prototype

目的

用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象

动机

假设有一个框架,并希望增加一些表示符、休止符等对象来构造。我们可以为每个对象构造一个子类,但如果对象太多,子类太多不方便管理。因此为了解决这个问题,我们可以让框架通过拷贝或克隆一个graphic子类的实例来创建一个graphic。

使用范围

  • 希望动态加载类的实例时;
  • 为了避免创建一个与产品类层次平行的工厂类层次;
  • 当一个类的实力只能有几个不同状态组合中的一种时,可以通过建立一定的数量的原型,并通过克隆其建立对象;

效果

  • 动态添加和删除产品(客户能直接接触产品)
  • 随时改变结构,只需要通过修改参数值就可以生成新的对象;
  • 与factory method相比,子类的构造会更少;

由于是基于clone操作,因此内部存在不支持拷贝的对象会难以实现

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include <iostream>

class Prototype {
protected:
Prototype() { std::cout << "Prototype" << std::endl; }
public:
virtual Prototype* clone() const= 0;
virtual ~Prototype() { std::cout << "~Prototype" << std::endl; }
};

class ConcretePrototype1 : public Prototype{
public:
ConcretePrototype1(){ std::cout << "Prototype---A" << std::endl; }
virtual ~ConcretePrototype1() { std::cout << "~Prototype---A" << std::endl; }
ConcretePrototype1(const ConcretePrototype1&) { std::cout << "copy PrototypeA" << std::endl; }
virtual Prototype* clone() const{
return new ConcretePrototype1(*this);
}
};

class ConcretePrototype2 : public Prototype {
public:
ConcretePrototype2() { std::cout << "Prototype---B" << std::endl; }
virtual ~ConcretePrototype2() { std::cout << "~Prototype---B" << std::endl; }
ConcretePrototype2(const ConcretePrototype2&) { std::cout << "copy PrototypeB" << std::endl; }
virtual Prototype* clone() const {
return new ConcretePrototype2(*this);
}
};

class Manager {
public:
Prototype* _prototype1;
Prototype* _prototype2;
public:
Manager(){}
void register_protype(Prototype* p1, Prototype* p2) {
_prototype1 = p1->clone();
_prototype2 = p2->clone();
}
~Manager() {
/***It's safe to delete null pointer******/

/* _GLIBCXX_WEAK_DEFINITION void
* operator delete(void* ptr) throw ()
* {
* if (ptr)
* std::free(ptr);
* }
*/
delete _prototype1;
delete _prototype2;
}
};

int main()
{
Manager manager;
Prototype* p1 = new ConcretePrototype1();
Prototype* p2 = new ConcretePrototype2();
manager.register_protype(p1, p2);
delete p1;
delete p2;
return 0;
}


Effective-cpp-#9

发表于 2017-11-27

Never call virtual functions during constructions or destructions

声明结论:不应该在构造函数和析构函数执行期间调用虚函数

动机

如果基类中定义了一个虚函数,那么在派生类中

  • 如果是构造函数调用了该虚函数
    • 那么虚函数起不到多态作用,原因是派生类的构造顺序是先基类后派生类,而此时基类中如果调用了虚函数,那么该虚函数的调用版本为基类的版本。因为在基类构造时,派生类的成员变量还没初始化,因此C++拒绝调用派生类版本的虚函数;
  • 如果是析构函数调用了该虚函数
    • 同理,析构函数的析构顺序是先派生类后基类,如果派生类析构完成员变量后,这些成员变量变成了未定义的。而此时如果到基类的析构函数,那么调用的版本变成了基类版本;

方法

如果真的要在构造函数中调用一个动态函数,那么可以把该函数改造为non-virtual,然后将该函数的信息向上传给基类的构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
class Transaction{
public:
explict Transaction(const std::string& logInfo){logTransaction(logInfo);}
void logTransaction(const std::string& logInfo) const;
};

class BuyTransaction:public Transaction{
public:
BuyTransaction(para):Transaction(createLogString(parameters)){}//向上传递信息
private:
static std::string createLogString(para)
}

建议

  • 构造和析构函数期间不要调用virtual函数。。。。

Effective-cpp-#8

发表于 2017-11-26

Prevent exceptions from leaving destructions

动机

  • 一种情况下,如果析构函数抛出了异常,则该异常点后面的程序将无法执行,而如果这些对象不能正常释放,则有可能造成内存泄漏的情况;
  • 另一种情况则是,假设前面有异常被抛出,而后面的对象仍在执行正常的销毁,而此时后面的对象也抛出了异常,在两个异常同时存在的情况下,会导致不明确行为,程序崩溃。

方法

如果析构函数不得已执行一个有可能抛出异常的动作,那可以用两个方法解决:

  • 让析构函数自行处理
1
2
3
4
5
6
7
DBConn::~DBConn()
{
try{db.close();}
catch(...){
....//处理掉异常,并做记录
}
}
  • 直接结束程序
1
2
3
4
5
6
7
8
DBConn::~DBConn()
{
try{db.close();}
catch(...){
....//处理掉异常,并做记录
std::abort();
}
}

有时候,析构函数处理掉异常不是一件好事,因为我们不知道“某些行为失败了”;但有时为了让程序忽略掉错误继续运行,这又是一件好事。

  • 也可以重新设计接口,让客户自身去处理异常,比如上面代码的close()函数;
1
2
3
4
5
6
7
8
9
10
11
12
class DBConn{
public:
void close(){
db.close();
closed = true;
}
~DBCconn(){
if (!closed){
try....
}
}
}

建议

  • 析构函数不能抛出异常。如果实在要抛出异常,可以选择让析构函数捕捉该异常,并使其限制在析构函数内或者直接结束函数;
  • 如果客户需要对某个操作函数的异常做出反应,我们应该为其提供一个普通函数,而不是在析构函数中执行该操作;

Factory method(DesignPattern)

发表于 2017-11-25

Factory method

目的

定义一个用于创建对象的接口,让子类去决定实例化哪一个类

动机

一个框架通过抽象类来定义对象之间的关系,并且客户通过子类来定义相关对象的实现。假设有一个应用框架来显示文档,画图框架显示画图文档等,我们需要具体的子类来重定义创建文档的操作,而这个操作就叫做抽象方法。

使用范围

  • 当一个类不知道它所创建的对象的类的时候;
  • 当一个类希望它的子类来指定创建哪个对象的时候;

效果

  • 工厂方法不会将与特定的应用有关的类绑定到代码中,代码仅仅处理product的接口;
  • 客户为了创建一个product,往往需要创建一个具体的creator;

与abstract factory不同的是,factory method的工厂仅仅是一个方法,而抽象工厂里的工厂是一个类。因此抽象工厂里,客户在使用时,产品本身是固定死的;而工厂方法则需要使用到产品本身(接口)。总结就是,抽象工厂看不到产品,因此需要使用客户端和更换工程,而工厂方法直接使用产品,从而生产产品;

代码示范

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// Factory_method.cpp : 定义控制台应用程序的入口点。
//

#include <iostream>

using namespace std;

class Product {
public:
Product() { cout << "Product..." << endl; }
virtual void partA() { cout << "partA..." << endl; }
virtual void partB() { cout << "partB..." << endl; }
};

class derivedPro : public Product {
public:
derivedPro() { cout << "derived Product..." << endl; }
virtual void partA() { cout << "derived partA..." << endl; }
virtual void partB() { cout << "derived partB..." << endl; }
};

class Creator {
public:
Product* getProduct() {
if (_product == 0) {
_product = createProduct();
}
return _product;
}
protected:
virtual Product* createProduct() = 0;
private:
Product* _product;
};

template <class TheProduct>
class standardCreator :public Creator {
public:
virtual Product* createProduct();
};

template <class TheProduct>
Product* standardCreator<TheProduct>::createProduct() {
return new TheProduct;
}

int main()
{
standardCreator<Product> myCreator1;
Product* p = myCreator1.getProduct();
p->partA();
p->partB();

standardCreator<derivedPro> myCreator2;
Product* q = myCreator2.getProduct();
q->partA();
q->partB();
return 0;
}

Effective-cpp-#7

发表于 2017-11-25

Declare destructions virtual in polymorphic base classes

动机

在某些情况下,当一个derived class对象是经由一个base class指针被删除时,而base class着一个non-virtual 的析构函数。此时实际执行的效果是,对象的derived成分并有被销毁,销毁的是base成分。这种局部销毁会造成资源泄漏。

方法

  • 一个意图被用作base class的class,应该令其析构函数为virtual。

具体用法和原因参考:虚函数的学习

  • 另外需要注意的是,一个不打算被用作base class的class,不应该令其析构函数为virtual。原因是:
    • 实现virtual函数的类的体积往往会增大50%到100%,这就造成一个问题是,该类的对象不能被塞到一个预先定义好的缓冲器里面,并传给其它语言的函数,也不能和其他语言(如C)的相同声明有着同样的结构;
    • 因此往往是只有当class内至少包含一个virtual函数,才为其声明virtual析构函数;

建议

  • 多态性质的基类应该声明virtual 函数;
  • 不作为基类使用的类,就不应该声明virtual函数;

Effective_cpp-#6

发表于 2017-11-24

Explicitly disallow the use of complier-generated functions you do not want

动机

在某些时候,我们不希望某个对象能被拷贝或者赋值,因此我们需要某些方法来阻止对象被拷贝或复制。

方法

  • 将成员函数定义为private,并且不去具体实现它们;这样可以避免friend函数和member函数取调用它们;
1
2
3
4
5
6
7
8
class myClass{
public:
...
private:
...
myClass(const myClass&);
myClass& operator=(const myClass&);
};

此时如果client试图拷贝类对象,编译器会报错;而当friend函数和member函数试图去调用,链接器则会报错;

  • 另一种方法则是定义一个uncopyable的基类,让那些不希望被拷贝的类继承该基类
1
2
3
4
5
6
7
8
9
10
11
12
13
class unCopyable{
public:
unCopyable();
~unCopyable();//允许派生对象去构造和析构
private:
...
unCopyable(const unCopyable&);
unCopyable& operator=(const unCopyable&);
};

class myClass : private unCopyable{
... //不再声明copy构造函数和copy赋值函数
}

这种操作可以使得任何调用(包括member函数和friend函数)尝试去拷贝的都会被编译器驳回,因为这种函数的“编译器生成版本”在尝试调用其基类的对应生成时发现基类的拷贝构造和拷贝赋值都是private的

建议

  • 为驳回编译自动生成的函数,可以将相应的成员函数声明为private并且不予实现;或者继承一个如上的unCopyable的基类;

Builder(DesignPattern)

发表于 2017-11-22

Builder

目的

构造一个复杂的对象,并且构造与表示分开,同一个构建方式能达到不同的表示

动机

假设有一个富文本的编辑器,你需要将RTF转换为多种不同的正文格式(比如普通的ASCII文本或者能进行交互的窗口),倘若需要转换很多种文本格式,那么我们就需要快速实现新的转换方式。因此我们可以构建一个文本转换器,在读入不同的标记时进行不同的转换。

使用范围

  • 构建对象的算法与对象的组成部分无关;
  • 构造的对象要有不同表示,即可能由不同的组件组成;
  • 角色:
    • builder:创建一个product的各个组件时,提供接口;
    • concretebuilder:具体实现builder的接口并提供返回产品的接口;
    • director:蓝图,指导builder如何构造产品;
    • product:被构造的对象;

效果

  • 可以根据蓝图(director)随机修改产品的内部表示;
  • director可以复用builder封装的接口去生成product;
  • 精装控制product的构造,这也是与abstract factor不同的地方,后者着重在生成一系列统一风格的product,前者可以一步一步地控制product的生成;

代码示范

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// Builder.cpp : 定义控制台应用程序的入口点。
//

#include <iostream>

using namespace std;

class Product {
public:
Product() { cout << "A product..." << endl; }
};

class Builder {
public:
Builder() = default;

virtual void BuildA(){}
virtual void BuildB(){}
virtual void BuildC(){}

virtual void buildPro(){}

virtual Product* GetProduct() { return NULL; }

};

class concreteBuilder : public Builder{
public:
concreteBuilder() { product = 0; }
virtual void buildPro() { product = new Product; }
virtual void BuildA() {
cout << "build a" << endl;
}
virtual void BuildB() {
cout << "build b" << endl;
}
virtual void BuildC() {
cout << "build c" << endl;
}

virtual Product* GetProduct() {
return product;
}
private:
Product* product;
};

class Director {
public:
Product* create(Builder& builder) {
builder.buildPro();
builder.BuildA();
builder.BuildA();
builder.BuildB();
builder.BuildC();

return builder.GetProduct();
}
Director(){}
};


int main()
{
Director director;
concreteBuilder concretebuilder;
director.create(concretebuilder);
Product* pro = concretebuilder.GetProduct();

delete pro;
return 0;
}


Effective-cpp-#5

发表于 2017-11-22

Know what functions C++ silently writes and calls

关于类的构造、析构、和拷贝构造、拷贝赋值函数

  • 如果自定义类时没有创建构造函数、析构函数、拷贝构造函数和拷贝赋值函数,编译器会默认为类创建;

  • copy构造函数和copy赋值函数,编译器创建的版本只是简单地将类中每一个non-static对象拷贝到目标对象;

问题

  1. 类中的成员变量为引用类型,此时使用默认函数容易出错;
1
2
3
4
5
6
7
8
9
10
11
12
template<class T>
class NamedObj{
public:
NamedObj(std::string& name, const T& value);
private:
std::string& nameValue;
const T& objValue;
};

NamedObj<int> p("dog", 2);
NamedObj<int> q("cat", 3);
p = s;
  • 倘若该例子没有错,那么p.nameValue和s.nameValue是否指向同一个string呢?如果指向同一个对象,你们就意味着reference被改变了,但是C++不允许reference改变指向不同的对象;
  • 此时,编译器会拒绝编译这一行的赋值;
  1. 同理,对面对const成员变量也一样。由于更改const是不合法的,因此编译器也会拒绝进行赋值;

  2. 如果某个基类将copy赋值函数声明为private的,那么编译器会拒绝为派生类生成一个copy赋值函数,因为编译器需要权限去处理基类的成分(在编译时,编译器会假设copy赋值函数需要处理基类的成分)。

提醒

  • 编译器会暗自创建构造函数、析构函数、拷贝构造函数和拷贝赋值函数;

Effective-cpp-4

发表于 2017-11-21

Make sure that objects are initialized before they're used

尽量使用member initialization list

  • 如果只是在构造函数体内进行赋值,非内置类型会在default构造函数中先被初始化
1
2
3
4
5
6
7
8
9
10
class myClass{
public:
myClass(std::string& address, std::list<myClass>& l){
addr = address;//只是赋值,早在默认构造函数已经被初始化
list = l;
}
private:
std::string addr;
std::list<myClass> list;
};
  • 如果使用member initialization list,会更加高效,避免了内置类型进行结果相同的初始化和赋值;而且member initialization list也能通过默认构造函数初始化成员变量;
1
2
3
4
myClass::myClass()
:addr(),
list()
{}
  • C++的成员初始化次序是固定
    • base class-> derived class
    • class的成员变量按照声明次序初始化;

不同编译单元的non-local static对象

  • 这个问题的起源是C++不保证不同的编译单元的初始化次序。
  • 例如
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//file1.cpp
class FileSystem{
public:
std::size_t numDisks() const;
};
extern FileSystem tfs;

//file2.cpp
class Dir{
public:
Dir(params){
std::size_t disks = tfs.numDisks();
};
};
  • 在这种情况,倘若tfs在tempDir初始化并没有完成初始化,这样就会出现问题;

为了解决这个问题,我们采取singleton模式,把每个non-local static放到专属函数中。以local static对象替代non-local static对象。因为C++保证一个local static对象会在函数调用期间/首次遇到该对象被定义时进行初始化。

1
2
3
4
5
6
FileSystem& tfs()
{
static FileSystem fs;
return fs;
}//第一行是定义并初始化一个对象,第二行是返回该对象的引用。
//而且如果函数被反复调用,可以声明为inline

建议

  • 构造函数最好使用member initialization list;
  • 以local static对象替代non-local static对象,保证跨编译单元的对象初始化;
<i class="fa fa-angle-left"></i>1…242526…28<i class="fa fa-angle-right"></i>

278 日志
29 标签
RSS
© 2025 LucienXian
由 Hexo 强力驱动
主题 - NexT.Pisces