文章目录
- 一、设计接口的原则
- 二、使用常量对象引用做参数,而不是使用值传递做参数
- 三、减少能够访问类的私有变量的成员函数的数目
- 四、运算符重载函数有的时候不应该作为类的成员函数
- 五. 自定义 Swap 函数的艺术
一、设计接口的原则
在设计接口的时候,尽量要明确声明参数的类型,避免不必要的类型转换。
一个错误的接口设计是:
//不优雅的接口设计,参数类型相同,导致容易造成误用
Date(int month,int day, int year) {}
一个正确的接口设计是:
//优雅的接口设计,为每一个参数限定好类型,同时这也符号 struct 中的 explicit 关键字,同时这里使用了常引用传参数来代替传统的按值传参数,如果按值传参数,程序会调用在这里插入代码片
对象的拷贝构造函数构建一个在函数内作用的局部对象,这个过程的开销会非常昂贵。
Date(const Month& m, const Day& d,const Year& y): month(m.value), day(d.value), year(y.value) {
if(!isValidDate()) {
throw std::invalid_argument(“Invalid Date”);
}
}
二、使用常量对象引用做参数,而不是使用值传递做参数
使用常量对象引用做参数而不是使用值传递做参数有这么一些优点:
1.防止不必要的拷贝构造函数开销,传值做参数就会使用拷贝构造函数创造一个局部变量,而拷贝构造函数带来的开销是巨大的。
2.防止对象切片问题
结合第一点,这里给出一段实例代码:
#include<iostream>
#include<string>
#include<stdexcept>struct Month {explicit Month(int m) : value(m) {} int value;
};struct Day {explicit Day(int d) : value(d) {} int value;
};struct Year {explicit Year(int y) : value(y) {}int value;
};class Date {
public://不优雅的接口设计,参数类型相同,导致容易造成误用Date(int month,int day, int year) {} //优雅的接口设计,为每一个参数限定好类型,同时这也符号 struct 中的 explicit 关键字,同时这里使用了常引用传参数来代替传统的按值传参数,如果按值传参数,程序会调用对象的拷贝构造函数构建一个在函数内作用的局部对象,这个过程的开销会非常昂贵。Date(const Month& m, const Day& d,const Year& y): month(m.value), day(d.value), year(y.value) {if(!isValidDate()) {throw std::invalid_argument("Invalid Date");}}Date(const Date& other)=delete;Date& operator=(const Date& other)=delete;bool isValidDate() const {return month > 0 && month <= 12 && day > 0 && day <= 31 && year > 0;}void print() const {std::cout<< year << "-" << month << "-" << day << std::endl;}
private:int month, day, year;
};class Base {
public:std::string getName() const{return "Base";}virtual void Display() const {std::cout << "Display Base" << std::endl;}
};class Son : public Base{
public:std::string getName() const{return "Son";}void Display() const override{std::cout<< "Display Son" << std::endl;}
};void ValueCopy(Base w) {std::cout<< w.getName()<<std::endl;w.Display();
}void ReferenceCopy(const Base& w) {std::cout<<w.getName()<<std::endl;w.Display();
}int main() {try {Date date(Month(12), Day(12),Year(2023));date.print();} catch (const std::exception& e) {std::cerr << e.what() <<std::endl;}Base* bb = new Son;ValueCopy(*bb);ReferenceCopy(*bb);delete bb;return 0;
}
三、减少能够访问类的私有变量的成员函数的数目
在类中重要的数据通常会使用 private 关键字进行修饰来保持其良好的封装性,很多时候我们为了方便喜欢使用成员函数对成员的 private 变量进行访问。然而,我们应该在设计的时候尽可能减少一些没有必要的,且能够访问类的私有变量的成员函数的数目,这是因为这种设计出现的次数越少, 我们类的封装性就越强。
假设有这样一个类:
class WebBrowser {
public:...void ClearCache();void ClearHistory();void RemoveCookies();...
};
如果想要一次性调用这三个函数,那么需要额外提供一个新的函数:
void ClearEverything(WebBrowser& wb) {wb.ClearCache();wb.ClearHistory();wb.RemoveCookies();
}
注意,虽然成员函数和非成员函数都可以完成我们的目标,但此处更建议使用非成员函数,这是为了遵守一个原则:越少的代码可以访问数据,数据的封装性就越强。此处的ClearEverything函数仅仅是调用了WebBrowser的三个public成员函数,而并没有使用到WebBrowser内部的private成员,因此没有必要让其也拥有访问类中private成员的能力。
这个原则对于友元函数也是相同的,因为友元函数和成员函数拥有相同的权力,所以在能使用非成员函数完成任务的情况下,就不要使用友元函数和成员函数。
如果你觉得一个全局函数并不自然,也可以考虑将ClearEverything函数放在工具类中充当静态成员函数,或与WebBrowser放在同一个命名空间中:
namespace WebBrowserStuff {class WebBrowser { ... };void ClearEverything(WebBrowser& wb) { ... }
}
四、运算符重载函数有的时候不应该作为类的成员函数
假如我们有这一个类Rational类,并且它可以和int隐式转换:
class Rational {
public:
Rational(int n,int d) : numerator(n), denominator(d) {}
Rational(int d) : numerator(0), denominator(d) {}
private:
int numerator;
int denominator;
};
当然,我们需要重载乘法运算符来实现Rational对象之间的乘法:
class Rational {
public:
…
const Rational operator*(const Rational& rhs) const;
};
将运算符重载放在类中是行得通的,至少对于Rational对象来说是如此。但当我们考虑混合运算时,就会出现一个问题:
Rational oneEight(1, 8);
Rational oneHalf(1, 2);
Rational result = oneHalf / oneEight;
result = oneHalf * 2; // 正确
result = 2 * oneHalf; // 报错
假如将乘法运算符写成函数形式,错误的原因就一目了然了:
result = oneHalf.operator*(2); // 正确
result = 2.operator*(oneHalf); // 报错
在调用operator*时,int类型的变量会隐式转换为Rational对象,因此用Rational对象乘以int对象是合法的,但反过来则不是如此。
所以,为了避免这个错误,我们应当将运算符重载放在类外,作为非成员函数:
const Rational operator*(const Rational& lhs, const Rational& rhs);
五. 自定义 Swap 函数的艺术
由于std::swap函数在 C++11 后改为了用std::move实现,因此几乎已经没有性能的缺陷,也不再有像原书中所说的为自定义类型去自己实现的必要。不过原书中透露的思想还是值得一学的。
如果想为自定义类型实现自己的swap方法,可以考虑使用模板全特化,并且这种做法是被 STL 允许的:
class Widget {
public:
void swap(Widget& other) {
using std::swap;
swap(pImpl, other.pImpl);
}
…
private:
WidgetImpl* pImpl;
};
namespace std {
template<>
void swap(Widget& a, Widget& b) {
a.swap(b);
}
}
注意,由于外部函数并不能直接访问Widget的private成员变量,因此我们先是在类中定义了一个 public 成员函数,再由std::swap去调用这个成员函数。
然而若Widget和WidgetImpl是类模板,情况就没有这么简单了,因为 C++ 不支持函数模板偏特化,所以只能使用重载的方式:
namespace std {
template
void swap(Widget& a, Widget& b) {
a.swap(b);
}
}
但很抱歉,这种做法是被 STL 禁止的,因为这是在试图向 STL 中添加新的内容,所以我们只能退而求其次,在其它命名空间中定义新的swap函数:
namespace WidgetStuff {
…
template
class Widget { … };
…
template3
void swap(Widget& a, Widget& b) {
a.swap(b);
}
}
我们希望在对自定义对象进行操作时找到正确的swap函数重载版本,这时候如果再写成std::swap,就会强制使用 STL 中的swap函数,无法满足我们的需求,因此需要改写成:
using std::swap;
swap(obj1, obj2);
这样,C++ 名称查找法则能保证我们优先使用的是自定义的swap函数而非 STL 中的swap函数。
C++ 名称查找法则:编译器会从使用名字的地方开始向上查找,由内向外查找各级作用域(命名空间)直到全局作用域(命名空间),找到同名的声明即停止,若最终没找到则报错。 函数匹配优先级:普通函数 > 特化函数 > 模板函数