Effective C++ 4 设计与声明 条款18-条款25

Effective C++系列文章:

Effective C++ 1自己习惯C++ 条款01-条款04
Effective C++ 2 构造/析构/赋值运算 条款5-条款12
Effective C++ 3 资源管理 条款13-条款17
Effective C++ 4 设计与声明 条款18-条款25
Effective C++ 5 实现 条款26-条款31
Effective C++ 6 继承与面向对象设计 条款32-条款40
Effective C++ 7 模板与泛型编程 条款41-条款52
Effective C++ 8 杂项讨论 条款53-条款55

条款18 让接口容易被正确使用,不易被误用

在这里插入图片描述

条款19 设计class犹如设计type

  1. 新type的对象应该如何被创建和销毁:operator new,operator new[],operator delete,operator delete[]
  2. 对象的初始化和对象的复制应该有什么样的差别:构建函数、赋值操作符
  3. 新类型的对象如果被值传播(passed by value)意味着什么:copy构造函数定义
  4. 什么是新type的合法值:
  5. 新type继承图系:特别注意virtual和non-virtual;如果允许其他函数继承,决定你的析构函数是否应该为virtual
  6. 你的新type需要什么样的转换:关注一下explicit关键字
  7. 谁该取用新type的成员:决定public/protected/private
  8. 什么是新type的“未声明接口”:接口对效率/异常安全性/资源运用提供何种保证

条款20 传参:宁以只读引用替换值传参

  1. 函数参数都是以实际实参的副本为初值,调用端所获得的亦是函数返回值的一个副本,这些副本是对象拷贝构造函数产生,使得值传参十分耗时。
    在这里插入图片描述
    在这里插入图片描述
    调用6次拷贝构造和6次析构函数(Student,Person,string),所以建议使用只读引用:
    在这里插入图片描述
  2. 以引用方式传递参数可以避免对象切割问题,当一个继承类对象以值参方式传递并被是为一个基类对象,基类的拷贝构造函数就会被调用,导致继承类对象只有基类特性。
    在这里插入图片描述
    在这里插入图片描述

解决方法:以只读引用方式传递参数:
在这里插入图片描述
3. 对与内置类型(如int),可能使用值传参会更加高效。

条款21 返回值:必须返回对象时,别返回其引用

  1. on-the-stack:如果直接返回对象引用,因为返回之前就已经销毁了引用变量,导致返回的是一个随机值。总之就是别返回在方法中构建的对象。
    在这里插入图片描述

  2. on-the-heap:如果使用一个指针指向被引用的对象,会导致因为无法获得被指向的引用指针对new的内容进行delete进而导致内存泄漏。
    在这里插入图片描述
    在这里插入图片描述
    但是考虑一下拷贝构造函数,返回的就是引用,温故而知新。

条款22 将成员变量声明为private

  1. 使用函数可以让你对成员变量的处理有更加精确的控制,实现”不准访问“、”只读访问“、”读写访问“等。然后为每一个成员变量设置一个get和set函数。
  2. 有利于封装性,你对成员变量的任何改变行为都来自成员函数,

条款23 宁以non-member、non-friend、替换menber函数

需要好好学学

能够访问private成员变量的函数只有class的member函数和friend函数, 非成员函数可以提提供更大的封装性(成员函数可以访问任何类内的成员变量,导致封装性降低)。

class WebBrowser{
public:
    ...
    void clearCache();
    void clearHistory();
    void removeCookies();
    ..
};
void clearBrowser(WebBrowser & wb){
    wb.clearCache();
    wb.clearHistory();
    wb.removeCookies();
}

比较自然的做法是将clearBrower成为一个non-member函数并位于WebBrowser所在的同一个namespace中,namespace可以跨越多个源码文件

namespace WebBroserStuff{
	class WebBrowser{...};
	void clearBrowser(WebBrowser & wb);
}

一个WebBroserStuff类可能有很多与某些功能相关的函数,比如与打印有关、与cookie有关、与管理有关等,分离它们的直接做法是将他们分别声明到不同头文件中。

//头文件webbrowser.h
//针对class WebBrowser自身和WebBrowser核心机能
namespace WebBroserStuff{
    class WebBrowser{...};
    ...//核心机能,例如几乎素有客户都需要的非成员函数
}
//头文件webbrowsercookies.h
namespace WebBrowserStuff{
    ...//与书签相关的便利函数
}
//头文件WebBrowsercookies.h
namespace WebBrowserStuff{
	...//与cookie有关的便利函数
}
...

在这里插入图片描述
在这里插入图片描述

条款24 若所有参数都需类型转换,请为此采用非成员函数

class Rational{
public:
    // ...
    //构造函数不是explicit,允许int-to-Tational隐式转换
    Rational(int numerator = 0, int denominator = 1);
    const Rational operator *(const Rational & rhs) const;
};
Rational oneEighth(1, 8);
Rational oneHalf(1, 2);
Rational result = oneEighth * oneHalf;//可以
result = result * oneEighth;//可以
result = oneHalf * 2;//可以(隐式类型转换
result = 2 * oneHalf;//不可以
//转换成operator清楚看到为啥错
result = oneHalf.operator*(2);//可以
result = 2.operator*(oneHalf);//不可以
//不可以,因为该类不接受int和Rational作为参数的非成员函数operator *   
result = operator*(2, oneHalf);

所有设置为non-member函数就可以解决这个问题:

const Rational operator *(const Rational & lhs, const Rational & rhs)

条款25 考虑写出一个不抛异常的swap函数

经典版本:

namespace std{
    template<typename T>//std::swap的经典实现
    void swap(T& a, T& b){
        T temp(a);
        a = b;
        b = temp;
    }
}

如果swap缺省版的效率不足,比如只需要交换指针即可,但是默认swap版本先拷贝了成员函数然后交换了成员内容实现两个对象之间的交换。
在这里插入图片描述
在这里插入图片描述
还需要特别注意的是:成员版swap绝不可以抛出异常,因为swap的一个最好的应用是帮助calsses(和class templates)提供强烈的异常安全性保障。
在这里插入图片描述

swap函数在C++中是一个非常重要的函数,但实现也非常复杂。
看一个缺省的std::swap函数的实现

 namespace std {
	 template<typename T>
	 void swap( T& a , T& b)
	 {
		 T temp(a);
		 a = b;
		 b = temp
	 }
 }

①内置类型的调用

int a = 2;
int b =3;
std::swap(a, b);
cout<<“a:”<<a<<" b:"<<b<<endl;

②非内置类型的调用
自我定义的类类型需要实现拷贝构造函数和拷贝赋值函数才能执行swap。
swap的实现:a先调用拷贝构造函数创建temp,通过拷贝赋值函数将b赋给a,再通过赋值函数将temp赋给b;
但对于另一种类的定义:“以指针指向一个对象,内含真正数据"那种类型,这种设计的常见表现形式是所谓"pimpl 手法”,即把数据寄托在另外一个类中。如果采用默认的swap函数,会出现什么状况?

class WigetImpl
{
public:
	WigetImpl(const int v) : m_value(v){ }
	int GetValue() const {return m_value;}
private:
	int m_value;
};
 
class Wiget
{
public:
	Wiget(const int v) {m_impl = new WigetImpl(v);}
	~Wiget() {if (m_impl != NULL) delete m_impl; m_impl = NULL;}
 
	Wiget(const Wiget &wiget)
	{
		this->m_impl = new WigetImpl(wiget.m_impl->GetValue());
	}
 
	Wiget& operator = (const Wiget &wiget)
	{
		if (this != &wiget) {   //采用copy-and-swap技术会更好,参考前面条款
			*m_impl = *wiget.m_impl;
		}
 
		return *this;
	}
private:
	WigetImpl *m_impl;
};

调用

Wiget w1(2);
Wiget w2(3);
std::swap(w1, w2);

调用缺省的swap,不但需要调用三次Wiget的copy函数,还调用三次WigetImpl的copy函数,效率非常低。
我们希望能够告诉std:: swap: 当Widgets 被置换时真正该做的是置换其内部的m_impl指针。确切实践这个思路的一个做法是将std: :swap 针对Widget 特化,即全特化。

namespace std  
{  
	template<>  
	void swap<Wiget>(Wiget &a,Wiget &b)  
	{  
		a.swap(b);  //调用Wiget的swap函数
	}  
} 

在Wiget中定义一个swap函数,实现对m_impl指针交换

void swap(Wiget & rhl)
{
std::swap(this->m_impl, rhl.m_impl);
}

这样调用只是调用三次WigetImpl的copy函数。
假设Wiget和WigetImpl都是一个类模板,如下面

template<typename T>
class WigetImpl {};
template<typename T>
class Wiget{};

我们对函数进行偏特化

namespace std
{
	template<typename T>
	void swap<Wiget<T> >(Wiget<T> &lhs, Wiget<T> &rhs)
	{
		lhs.swap(rhs);
	}
}

但C++只对类偏特化,函数会报错,如:“std::swap”: 非法使用显式模板参数
解决函数偏特化问题,即修改为函数重载,如下:

namespace std
{
	template<typename T>
	void swap(Wiget<T> &lhs, Wiget<T> &rhs)
	{
		lhs.swap(rhs);
	}
}

但std是个特殊的命名空间,可以全特化std内的template,但不能添加新的template到std。
这些,都可以通到定义一个no-member function解决,不属于std的命名空间

template<typename T>
void swap(Wiget<T> &lhs, Wiget<T> &rhs)
{
	lhs.swap(rhs); //调用Wiget的swap函数
}

上面介绍了3中swap:
①std的缺省swap
②std的特化版本
③class命名空间的no-member swap
那么它们的调用顺序是怎么样呢?为了保证调用顺序是③②①,需要在函数中声明std命名空间

template<typename T>
void CallSwap(T &obj1, T &obj2)
{
<span style="white-space:pre">	</span>using std::swap;
<span style="white-space:pre">	</span>swap(obj1, obj2);
}

相关的原理,可以查看C++名称查找法则

总结swap的使用规则:
首先,如果swap的缺省实现为你的类或类模板提供了可接受的性能,你不需要做任何事。任何试图交换类型的对象的操作都会得到缺省版本的支持,而且能工作得很好。
第二,如果swap缺省实现效率不足(这几乎总是意味着你的类或模板使用了某种pimpl手法),就按照以下步骤来做:
1.提供一个public的swap成员函数,能高效地交换你的类型的两个对象值,这个函数应该永远不会抛出异常。
2.在你的类或模板所在的同一个namespace中,提供一个非成员的swap,用它调用你的swap成员函数。
3.如果你写了一个类(不是类模板),为你的类特化std::swap,并令它调用你的swap 成员函数。
最后,如果你调用swap,确保在你的函数中包含一个using 声明式使std::swap可见,然后在调用swap时不使用任何namespace修饰符

绝不要让swap的成员版本抛出异常。这是因为swap非常重要的应用之一是为类(以及类模板)提供强大的异常安全(exception-safety)保证。
贴出最终的测试代码

template<typename T>
class WigetImpl
{
public:
	WigetImpl(const T v) : m_value(v){ }
	T GetValue() const {return m_value;}
private:
	T m_value;
};
 
template<typename T>
class Wiget
{
public:
	Wiget(const T v) {m_impl = new WigetImpl<T>(v);}
	~Wiget() {if (m_impl != NULL) delete m_impl; m_impl = NULL;}
 
	Wiget(const Wiget &wiget)
	{
		this->m_impl = new WigetImpl<T>(wiget.m_impl->GetValue());
	}
 
	Wiget& operator = (const Wiget &wiget)
	{
		if (this != &wiget) {   //采用copy-and-swap技术会更好,参考前面条款
			*m_impl = *wiget.m_impl;
		}
 
		return *this;
	}
 
	void swap(Wiget & rhl)
	{
		std::swap(this->m_impl, rhl.m_impl);
	}
 
private:
	WigetImpl<T> *m_impl;
};
 
namespace std  
{  
	template<>  
	void swap<Wiget<int> >(Wiget<int> &a,Wiget<int> &b)  
	{  
		a.swap(b);  //调用Wiget的swap函数
	}  
} 
 
template<typename T>
void swap(Wiget<T> &lhs, Wiget<T> &rhs)
{
	lhs.swap(rhs);
}
 
template<typename T>
void CallSwap(T &obj1, T &obj2)
{
	using std::swap;
	swap(obj1, obj2);
}

记住
①当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常.
②如果你提供一个member swap,也该提供一个non-member swap用来调用前者.对于classes(而非template),
也请特化std::swap.
③调用swap时应针对std::swap使用using声明式,然后调用swap并且不带任何""命名空间资格修饰.
④为"用户定义类型"进行std templates全特化是好的,但千万不要尝试在std内加入某些std而言全新的东西.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值