《Effective C++》《设计与声明——18、让接口容易被正确使用,不易被误用》

1、term18:Make interfaces easy to use correctly and hard to use incorrectly

前言:

C++ 在接口之海漂浮。函数接口,类接口,模板接口……每个接口都是客户与你的代码进行交互的一种方法。假设你正在面对的是一些“讲道理”的人,这些客户尝试把工作做好,他们希望能够正确使用你的接口。在这种情况下,如果接口被误用,你至少负一部分的责任。理想情况下,如果使用一个接口没有做到客户希望做到的,代码应该不能通过编译;如果代码通过了编译,那么它就能做到客户想要的。

1.1 引入新的类型

想要开发出一个容易被正确使用不容易被误用的接口,首先需要考虑客户可能出现的所有类型的错误。举个例子,假设你正在为一个表示日期的类设计一个构造函数:

class Date {
public:
	Date(int month, int day, int year);
	...
};

乍一看,这个接口可能看上去去合理的,但是客户很容易犯下至少两个错误:

第一,他们可能搞错参数的传递顺序:

Date d(30, 3, 1995);

第二,他们可能传递一个无效的月份或者天数:

Date d(2, 30, 1995); 

(上一个例子看上去很蠢,但是不要忘了在键盘上,数字2和3是挨着的,将2错打成3这样的错误并不罕见。)
许多客户端错误可以因为通过引入新的类型获得预防,的确,类型系统(type system)是你阻止不合要求的代码编译通过的主要盟友。在这种情况下,我们可以引入简单的外覆类型来区分天,月和年,然后在Date构造函数中使用这些类型:

struct Day{
	explicit Day(int d): val(d) {}
	int val;
};
struct Month {
	explicit Month(int m): val(m) {}
	int val;
 };
struct Year {
	explicit Year(int y): val(y){}
	int val;
 };
class Date {
public:
	Date(const Month& m, const Day& d, const Year& y);
...
};
Date d(30, 3, 1995); // error! 错误类型
Date d(Day(30), Month(3), Year(1995)); // error!  错误类型
Date d(Month(3), Day(30), Year(1995)); //  OK,正确类型

将Day,Month和Year数据封装在羽翼丰满的类中比上面简单的使用struct要更好(见条款22),但是使用struct就足以证明,明智而谨慎地引入新类型可以很好的阻止接口被误用的问题。

一旦正确的类型准备好了,就可以合理的约束这些类型的值。如,一年只有12个月份应该能够通过Month类型反映出来。方法之一是使用一个枚举类型来表示月份,但是枚举不是我们喜欢的类型安全的类型。例如,枚举可以像int一样使用(见条款2)。一个更加安全的解决方案是预先将所有有效的月份都定义出来。

class Month {
public:
	static Month Jan() { return Month(1); } // 函数,返回有效月份
	static Month Feb() { return Month(2); } 
	...
	static Month Dec() { return Month(12); }
	... // 其它成员函数
private:
	explicit Month(int m); // 阻止生成新的月份
	... // 月份专属数据
};
Date d(Month::Mar(), Day(30), Year(1995));

如果使用函数代替对象来表示指定月份值会让你觉的奇怪的话,可能是因为你忘记了非本地static对象的初始化次序有可能出现问题(见条款4)。
完整可编译代码如下:

#include <iostream>  
using namespace std;

struct Day {  
    explicit Day(int d) : val(d) {}  
    int val;  
     // 重载 operator<< 以便与 std::ostream 一起工作  
    friend std::ostream& operator<<(std::ostream& os, const Day& d) {  
        os << d.val;  
        return os;  
    }  
};  
  
struct Month {  
    static Month Jan() { return Month(1); }  
    static Month Feb() { return Month(2); }  
    static Month Mar() { return Month(3); }  
    static Month Apr() { return Month(4); }  
    static Month May() { return Month(5); }  
    static Month Jun() { return Month(6); }  
    static Month Jul() { return Month(7); }  
    static Month Aug() { return Month(8); }  
    static Month Sep() { return Month(9); }  
    static Month Oct() { return Month(10); }  
    static Month Nov() { return Month(11); }  
    static Month Dec() { return Month(12); }
public:  
    explicit Month(int m) : val(m) {}  
    int val;  
     // 重载 operator<< 以便与 std::ostream 一起工作  
    friend std::ostream& operator<<(std::ostream& os, const Month& m) {  
        os << m.val;  
        return os;  
    }  
};  
  
struct Year {  
    explicit Year(int y) : val(y) {}  
    int val;  
     // 重载 operator<< 以便与 std::ostream 一起工作  
    friend std::ostream& operator<<(std::ostream& os, const Year& y) {  
        os << y.val;  
        return os;  
    }  
};  
  
class Date {  
public:  
    Date(const Month& m, const Day& d, const Year& y)  
        : month(m), day(d), year(y) {  
        // 可以在这里添加一些逻辑来验证日期的有效性  
    }  
     // 获取日期的各个部分  
    Month getMonth() const { return month; }  
    Day getDay() const { return day; }  
    Year getYear() const { return year; }  
  
private:  
    Month month;  
    Day day;  
    Year year;  
};  
  
int main() {  
    // 现在可以正确创建Date对象了  
    Date d(Month::Mar(), Day(30), Year(1995));  
    // 错误的创建方式仍然会导致编译错误  
    // Date d(30, 3, 1995); // error! 错误类型  
    // Date d(Day(30), Month(3), Year(1995)); // error! 错误类型,因为构造函数参数顺序不对  
  
    // 正确创建Date对象的另一种方式(使用静态成员函数)  
    Date d2(Month::Jan(), Day(1), Year(2023));  
  
    // 输出日期以验证  
    cout << "Date: " << d2.getYear() << "-" << d2.getMonth() << "-" << d2.getDay() << endl;  
  
    return 0;  
}

1.2 对类型的操作进行限定

另外一种防止类似错误的方法是对类型能够做什么进行限制。进行限制的一般方法是添加const。举个例子,条款3解释了对于用户自定义的类型,把operator*的返回类型加上const能够防止下面错误的发生:

if (a * b = c) ... //原意是要做一次比较动作

1.3 提供行为一致的接口

事实上,这只是“使类型容易正确使用不容易被误用”的表现形式:除非有更好的理由,让你的自定义类型同内置类型的行为表现一致。客户已经知道像int一样的内置类型的行为是什么样子的,所以在任何合理的时候你应该努力使你的类型表现与其一致。举个例子,如果a和b是int类型,那么赋值给a*b是不合法的,所以除非有一个好的理由偏离这种行为,你应该使你的类型同样不合法。每当你不确定自定义类型的行为时,按照int来做就可以了。
避免自定义类型同内置类型无端不兼容的真正原因是:提供行为一致的接口。很少有其它特征比“一致性”更能使接口容易被使用了,也没有特征比“不一致性”更加导致接口容易被误用了。STL容器的接口基本上(虽然不是完全一致)是一致的,这使得它们使用起来相当容易。举个例子,每个STL容器有一个size成员函数,用来指出容器中的对象数量。与Java相比,arrays使用length属性(property)来表示对象数量,而String使用length方法(method)来表示,List使用size方法来表示;对于.NET来说,Array有一个Length属性,而ArrayList有一个Count属性。一些开发人员认为集成开发环境(IDE)使这种不一致性不再重要,但他们错了。不一致性会将精神摩擦强加到开发人员的工作中,没有任何IDE能够将其擦除。

1.4 使用智能指针消除客户管理资源的责任

1.4.1 让函数返回一个智能指针

一个要让客户记住做某事的接口比较容易被用错,因为客户有可能会忘记做。举个例子,条款13中引入一个工厂函数,在一个Investment继承体系中返回指向动态分配内存的指针:

Investment* createInvestment(); 

为了防止资源泄漏,createInvesment返回的指针最后必须被delete,但是这为至少两类客户错误的出现创造了机会:delete指针失败,多次delete同一个指针。

条款13展示了客户如何将createInvestment的返回值存入像auto_ptr或者tr1::shared_ptr一样的智能指针中,这样就将delete的责任交给智能指针。但是如果客户忘记使用智能指针该怎么办?在许多情况下,更好的接口是要先发制人,让工厂函数首先返回一个智能指针:

std::tr1::shared_ptr<Investment> createInvestment();

这就强制客户将返回值保存在tr1::shared_ptr中,从而完全消除了忘记delete不再被使用的底层Investment对象的可能性。

1.4.2 返回绑定删除器的智能指针

事实上,对于一个接口设计者来说,返回tr1::shared_ptr能够避免许多其他的有关资源释放的客户错误,因为条款14中解释道,在创建智能指针时,tr1::shared_ptr允许将一个资源释放函数——释放器(deleter)——绑定到智能指针上。
  假设客户从createInvestment得到一个Investment*指针,我们通过将这个指针传递给一个叫做getRidOfInvestment的函数来释放资源,而不是直接使用delete。这样的接口开启了另外一类客户错误的大门:客户可能会使用错误的资源析构机制(用delete而不是用提供的getRidOfInvestment接口)。createInvestment的实现者可以先发制人,返回一个tr1::shared_ptr,并将getRidOfInvestment绑定为删除器。
  tr1::shared_ptr提供了一个有两个参数的构造函数:需要被管理的指针和当引用计数为0时需要被调用的删除器。这就提供了一个创建用getRidOfInvestment作为删除器的空tr1::shared_ptr的方法,如:

std::tr1::shared_ptr<Investment> // 视图创建一个 null shared_ptr
pInv(0, getRidOfInvestment); // 并携带一个自定的删除器
							// 此式无法通过编译

上面不是有效的c++,tr1::shared_ptr构造函数的第一个参数必须为指针,但是0不是指针,是个int。虽然它可以转换成指针,但是在此例子中并不够好,因为tr1::shared_ptr坚持使用真实的指针。转型(cast)就能解决问题:

std::tr1::shared_ptr<Investment> // 创建一个 null shared_ptr
pInv( static_cast<Investment*>(0), // 并以getRidOfInvestment作为删除器
getRidOfInvestment); 

这意味着实现一个createInvestment的代码如下(返回值为绑定了getRidOfInvestment作为删除器的tr1::shared_ptr):

std::tr1::shared_ptr<Investment> createInvestment() {
	std::tr1::shared_ptr<Investment> retVal(static_cast<Investment*>(0),
											getRidOfInvestment);
	retVal = ...; // 令retVal指向正确对象
return retVal;
}

当然,如果在创建一个retVal之前就能够决定一个原始指针是不是由reVal来管理,将原始指针直接传递给retVal的构造函数比先将retVal初始化为null然后做一个赋值操作要好。为什么请看 条款26。

1.5 使用智能指针消除交叉-DLL错误

tr1::shared_ptr的一个特别好的性质是它会自动使用它的“每个指针专属的删除器”,因而消除另外一个客户错误——交叉(cross)-DLL错误。当一个对象在一个DLL中使用new被创建,但是在另外一个DLL中被delete时这个问题就会出现。在许多平台中,这样的交叉-DLL new/delete对会导致运行时错误。使用tr1::shared_ptr可以避免这种错误,因为它使用的默认的删除器来自创建tr1::shared_ptr的DLL。这就意味着,例如,如果Stock是一个继承自Investment的类,createInvestment实现如下:

std::tr1::shared_ptr<Investment> createInvestment() {
	return std::tr1::shared_ptr<Investment>(new Stock);
}

补充:
tr1::shared_ptr 是 C++ TR1(Technical Report 1)中的一个智能指针实现,它是 std::shared_ptr 的前身。std::shared_ptr 是 C++11 标准库中的一个重要组件,它实现了共享所有权的智能指针概念。当多个 shared_ptr 实例共享同一个对象时,对象的生命周期将一直持续到最后一个引用它的 shared_ptr 被销毁或重置。目前主要用std::shared_ptr ,这已经够用了。

2、面试相关

接口调用在项目中用的比较多,更偏于实用性,下面是面试中可能会问到的问题。

2.1、说一下异常处理在接口调用中的重要性。

异常处理在接口调用中的重要性不容忽视。它有助于提升代码的稳定性、可读性和可维护性,特别是在处理复杂的接口调用逻辑时。以下是异常处理在接口调用中的重要性体现:
(1)错误处理与程序稳定性:
接口调用过程中,由于网络问题、参数错误、资源限制等多种原因,可能会出现各种异常情况。如果没有适当的异常处理机制,这些异常可能导致程序崩溃或产生不可预测的行为。通过异常处理,我们可以捕获这些异常,并采取适当的措施,如记录日志、回滚事务或提供友好的错误提示,从而确保程序的稳定性和可靠性。
(2)提高代码可读性:
通过合理地使用异常处理,我们可以将错误处理逻辑与正常的业务逻辑分离,使代码结构更加清晰。这有助于其他开发人员更好地理解代码的功能和逻辑,降低维护成本。
(3)便于调试与定位问题:
当接口调用出现问题时,异常处理可以帮助我们快速定位问题所在。通过捕获异常并输出详细的错误信息,我们可以迅速了解问题的原因和发生位置,从而加快问题的解决速度。
(4)业务逻辑的完整性:
在接口调用中,有时候我们可能希望在某些异常情况发生时继续执行后续的逻辑,或者根据不同的异常类型执行不同的操作。通过异常处理,我们可以根据捕获到的异常类型进行条件判断,从而实现更灵活的业务逻辑处理。
(5)用户体验的提升:
对于面向用户的系统来说,友好的错误提示对于提升用户体验至关重要。通过异常处理,我们可以捕获接口调用中的错误,并向用户展示易于理解的错误提示,避免用户因为遇到不明原因的错误而感到困惑或不满。

2.2、在接口调用中,多态性如何帮助你实现灵活的代码复用和扩展?

在接口调用中,多态性是一个核心概念,它允许我们使用统一的接口来处理不同类型的对象,从而实现灵活的代码复用和扩展。以下是多态性在接口调用中如何帮助你实现这些目标的几个关键点:
(1)统一的接口调用:
多态性允许我们定义一个通用的接口,该接口可以被不同的类实现。这意味着,在编写调用接口的代码时,我们不需要关心具体的实现类是什么,只需要按照接口定义的方法进行操作即可。这种统一的接口调用方式大大简化了代码,并提高了代码的可读性和可维护性。
(2)代码复用:
通过多态性,我们可以编写一段通用的代码来处理实现了相同接口的多个对象。这些对象在内部可以有不同的实现细节,但对外都呈现出相同的接口。这使得我们可以在不修改已有代码的情况下,将新的实现类添加到系统中,并通过相同的接口进行调用。这种代码复用不仅减少了冗余代码,还提高了系统的可维护性和可扩展性。
(3)易于扩展:
多态性使得系统更加易于扩展。当需要添加新的功能或支持新的数据类型时,我们只需要创建新的类并实现相同的接口,然后将其注册到系统中即可。已有的调用代码不需要做任何修改,就可以与新的实现类协同工作。这种扩展方式既简单又高效,降低了系统的复杂性,并提高了开发的效率。
(4)开放封闭原则的支持:
多态性是实现开放封闭原则的重要手段之一。开放封闭原则强调软件实体(类、模块、函数等)应该是可扩展的,但是不可修改的。通过多态性,我们可以在不修改已有代码的情况下,通过添加新的实现类来扩展系统的功能。这符合开放封闭原则的精神,有助于提高系统的稳定性和可维护性。
(5)解耦与降低耦合度:
多态性有助于实现模块之间的解耦,降低系统各部件之间的耦合度。通过将接口和实现分离,我们可以使得各个模块更加独立,减少相互之间的依赖关系。这样,当某个模块发生变化时,其他模块受到的影响将最小化,提高了系统的可维护性和可扩展性。

3、总结

天堂有路你不走,地狱无门你自来。

4、参考

4.1《 Effective C++》
4.2 Effective C++条款18:让接口容易被正确使用,不容易被误用

  • 25
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 《更有效的C语言编程与设计的35个有效方法》是一本非常实用的书籍,它总结了35个提高C语言编程和设计能力的有效方法。这本书结合实际编程经验,从不同角度介绍了如何更高效地利用C语言进行软件开发。 该书首先从代码的可读性和可维护性方面提出了一些方法。比如,合理命名变量和函数、遵循一定的代码风格、使用注释等,这些方法可以使代码更易于理解和修改,提高工作效率。 其次,该书讲解了一些关于内存管理和指针的技巧。对于C语言开发者来说,内存管理是一个非常重要的技能。书中通过介绍如何正确使用动态内存分配函数、如何避免内存泄漏等方面来帮助读者提高内存管理的能力。 此外,该书还提供了一些提高代码质量和性能的方法。如代码复用、性能优化等。对于C语言开发者来说,写出高质量、高效率的代码是非常重要的,这本书可以帮助读者掌握一些技巧和原则。 总的来说,这本书内容丰富,通俗易懂,适合C语言的初学者和有一定基础的开发者阅读。它可以帮助读者全面提高C语言编程和设计的能力,提升工作效率。无论是想从事C语言开发还是提升编程技能的人,都可以从中受益匪浅。 ### 回答2: 《more effective c: 35个改善编程与设计的有效方法(中文版) 》是一本非常实用的书籍,它提供了许多改善编程与设计的有效方法。以下是对该书的回答: 这本书共包含了35个方法,旨在帮助读者提高编程和设计的效率。它首先介绍了良好的编程风格和规范,包括命名规则、代码布局、注释等。这些方法可以使代码更易于阅读和维护,并提高代码的可重用性和可扩展性。 接下来,该书介绍了一些常见的编程错误和陷阱,并提供了相应的解决方案。例如,它说明了内存管理的重要性,并给出了避免内存泄漏和悬挂指针的方法。 此外,该书还介绍了一些高级的编程技术和设计模式,如多线程编程、异常处理和继承等。这些方法可以帮助读者编写更健壮和可靠的程序,并提高程序的性能和响应能力。 另外,该书还强调了测试和调试的重要性,并介绍了一些常用的测试工具和技术。它提供了一些测试和调试的实用方法,帮助读者发现和修复程序中的错误和缺陷。 总的来说,《more effective c: 35个改善编程与设计的有效方法(中文版) 》是一本非常实用的书籍,它提供了许多实用的方法和技巧,帮助读者提高编程和设计的效率。无论是初学者还是有经验的开发者,都可以从中受益,并提升自己的编程能力。 ### 回答3: 《more effective c :35个改善编程与设计的有效方法(中文版) .pdf》是一本关于优化编程和设计的有效方法的书籍。 这本书共包含了35个有效的方法,可以帮助程序员和设计师改进他们的工作。在这本书中,作者提供了一些实用的技巧和经验,帮助读者提高他们的编程和设计技能。 这本书的价值在于它提供了实用的方法和步骤,读者可以按照这些方法和步骤进行实施,从而实现更有效的编程和设计。这本书涵盖了多个方面,包括代码的优化、错误的处理、算法的选择、设计模式的应用等等。 通过阅读这本书,读者可以了解到如何更好地组织和管理代码,以及如何选择合适的算法和数据结构来提高程序的效率。此外,这本书还介绍了一些设计模式和原则,读者可以学习如何使用它们来提高程序的灵活性和可维护性。 总之,这本书提供了一些实用的方法和技巧,帮助读者改进他们的编程和设计技能。对于那些希望在编程和设计领域取得更好成果的人来说,这本书是一个很好的参考资料。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值