Effective CPP(五): 设计接口的原则

文章目录

  • 一、设计接口的原则
  • 二、使用常量对象引用做参数,而不是使用值传递做参数
  • 三、减少能够访问类的私有变量的成员函数的数目
  • 四、运算符重载函数有的时候不应该作为类的成员函数
  • 五. 自定义 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++ 名称查找法则:编译器会从使用名字的地方开始向上查找,由内向外查找各级作用域(命名空间)直到全局作用域(命名空间),找到同名的声明即停止,若最终没找到则报错。 函数匹配优先级:普通函数 > 特化函数 > 模板函数


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/204643.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

数字营销影响消费者行为的 6 种方式

如果您正在考虑转向在线市场&#xff0c;那么这个决定就好了&#xff01;没有什么比数字营销更强大的了。但是&#xff0c;在开始之前&#xff0c;请了解数字营销如何影响消费者行为。由于客户是任何企业的基石&#xff0c;因此跟踪消费者行为至关重要。 数据分析在识别潜在客…

3DMAX UV贴图修改插件安装卸载方法

3DMAX UV贴图修改插件安装卸载方法 3dMax贴图修改插件PolyUnwrapper是为纹理艺术家设计的一整套专业工具&#xff0c;尤其适用于建筑和游戏行业。 它包含许多功能&#xff0c;将大大帮助您改进UV展开的工作流程。 【主要功能特点】 -多重缝合。一次缝合多个壳 -自定义打包算…

在Ubuntu上搭建RiscV交叉编译环境

参考文档 安装 RISC-V 交叉编译工具链 - USTC CECS 2023 安装依赖库 sudo apt updatesudo apt -y install autoconf automake autotools-dev curl python3 python3-pip sudo apt -y install libmpc-dev libmpfr-dev libgmp-dev gawk sudo apt -y install build-essential bi…

基于LangChain+LLM的本地知识库问答:从企业单文档问答到批量文档问答

前言 过去半年&#xff0c;随着ChatGPT的火爆&#xff0c;直接带火了整个LLM这个方向&#xff0c;然LLM毕竟更多是基于过去的经验数据预训练而来&#xff0c;没法获取最新的知识&#xff0c;以及各企业私有的知识 为了获取最新的知识&#xff0c;ChatGPT plus版集成了bing搜索…

我也不想说啊,可这东西行政用能保命啊!

行政人姐妹在哪里啊&#xff01;在处理工作报告&#xff0c;行政报告等文章的时候&#xff0c;毫无头绪&#xff0c;速度还慢&#xff0c;容易被领导批评。 最近挖到了个抄好用的AI智能写作工具 用它写报告&#xff0c;写总结、写会议记录&#xff0c;写方案等等......写啥都…

深入React Flow Renderer(二):构建拖动操作栏

在上一篇博客中&#xff0c;我们介绍了如何启动React Flow Renderer并创建一个基本的工作流界面。本文将进一步深入&#xff0c;着重讨论如何构建一个可拖动的操作栏&#xff0c;它是用户与工作流交互的入口之一。 引言 操作栏是工作流界面的一部分&#xff0c;通常位于界面的…

Comparator Comparators Comparable Collections排序源码解析

问题引出 起初&#xff0c;写了一行排序代码&#xff0c;空指针异常。有判空思想但对nullsLast理解是错误的&#xff0c;于是阅读了一下相关源码。 result.sort(Comparator.nullsLast(Comparator.comparing(StationPointDataZoneVO::getDv)));以下写法是正确的&#xff1a; …

再见了 shiro

前言 作为一名后台开发人员&#xff0c;权限这个名词应该算是特别熟悉的了。就算是java里的类也有 public、private 等“权限”之分。之前项目里一直使用shiro作为权限管理的框架。说实话&#xff0c;shiro的确挺强大的&#xff0c;但是它也有很多不好的地方。shiro默认的登录…

Python优雅重启谷歌游览器并过cf

python如何优雅的重启谷歌游览器&#xff1f; 代码很简单&#xff1a; import subprocesshomepage "about:blank" # 结束已经启动的谷歌游览器 subprocess.run("taskkill /f /im chrome.exe", shellTrue) # debug启动谷歌游览器 subprocess.run(["…

docker批量删除退出状态的容器

sudo docker rm $(docker ps -a -q -f statusexited)

移动云“遇见大咖”|玻色量子副总裁巨江伟:超越摩尔定律的新型计算革命

移动云MVP&#xff0c;作为产品共建专家、关键意见领袖及技术布道者&#xff0c;帮助开发者更好地了解和使用移动云。开发者社区希望携手移动云MVP&#xff0c;与开发者共生、共赢、共成长。 8月31日&#xff0c;移动云开发者社区“遇见大咖”系列活动第2期——“[量子计算]超越…

了解c++11新特性-智能指针

c智能指针概念 1 智能指针的思想--CRAII机制 RAII是Resource Acquisition Is Initialization&#xff08;wiki上面翻译成 “资源获取就是初始化”&#xff09;的简称&#xff0c;是C语言的一种管理资源、避免泄漏的惯用法&#xff0c;利用的就是C构造的对象最终会被析构销毁的…

如何在Go中编写包

包由位于同一目录中的Go文件组成,这些文件在开头具有相同的package语句。你可以从包中包含额外的功能,使程序更复杂。有些包可以通过Go标准库获得,因此与Go安装一起安装。其他可以使用Go的go get命令安装。您还可以通过使用必要的package语句在要共享代码的相同目录中创建Go…

【Vue第3章】使用Vue脚手架_Vue2_笔记

笔记 脚手架文件结构 ├── node_modules ├── public │ ├── favicon.ico: 页签图标 │ └── index.html: 主页面 ├── src │ ├── assets: 存放静态资源 │ │ └── logo.png │ │── component: 存放组件 │ │ └── HelloWorld.vue …

英语学习(衣服与服装篇)

一、购买服装 1.有关时尚的形容词 1&#xff09;有许多可用于形容 fashion 和 clothes 的形容词。 cool 酷的 stylish 时髦的 in style 时髦 fashionable 时髦的&#xff0c;流行的 2&#xff09;描述不喜欢的衣服 out of style 过时的 dre…

springboot和swagger版本不兼容问题解决

1.错误提示 org.springframework.context.ApplicationContextException: Failed to start bean documentationPluginsBootstrapper; nested exception is java.lang.NullPointerExceptionat org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLi…

Java程序员,你掌握了多线程吗?

文章目录 01 多线程对于Java的意义02 为什么Java工程师必须掌握多线程03 Java多线程使用方式04 如何学好Java多线程写作末尾 摘要&#xff1a;互联网的每一个角落&#xff0c;无论是大型电商平台的秒杀活动&#xff0c;社交平台的实时消息推送&#xff0c;还是在线视频平台的流…

ConcurrentHashMap实现线程安全原理

我们知道&#xff0c;在日常开发中使用的HashMap是线程不安全的&#xff0c;而线程安全类HashTable只是简单的在方法上加锁实现线程安全&#xff0c;效率低下&#xff0c;所以在线程安全的环境下我们通常会使用ConcurrentHashMap。 1. 初始化数据结构时的线程安全 HashMap的底…

【51单片机系列】矩阵按键扩展实验

本文对矩阵按键的一个扩展&#xff0c;利用矩阵按键和动态数码管设计一个简易计算器。代码参考&#xff1a;https://blog.csdn.net/weixin_47060099/article/details/106664393 实现功能&#xff1a;使用矩阵按键&#xff0c;实现一个简易计算器&#xff0c;将计算数据及计算结…

【Proteus仿真】【51单片机】简易计算器

文章目录 一、功能简介二、软件设计三、实验现象联系作者 一、功能简介 本项目使用Proteus8仿真51单片机控制器&#xff0c;使动态数码管、矩阵按键、蜂鸣器等。 主要功能&#xff1a; 系统运行后&#xff0c;数码管默认显示0&#xff0c;输入对应的操作数进行四则运算&#x…