Effective C++ 2e Item42

本文围绕C++展开,介绍了私有继承的含义与行为特征,指出其意味着“用...来实现”。还提及模板导致的“代码膨胀”问题,给出用通用指针避免的方法。通过创建GenericStack类及接口类实现类型安全堆栈,对比分层与私有继承,强调分层优先,必要时大胆用私有继承。

条款42: 明智地使用私有继承

条款35说明,C++将公有继承视为 "是一个" 的关系。它是通过这个例子来证实的:假如某个类层次结构中,Student类从Person类公有继承,为了使某个函数成功调用,编译器可以在必要时隐式地将Student转换为Person。这个例子很值得再看一遍,只是现在,公有继承换成了私有继承:

class Person { ... };

class Student:                      // 这一次我们
  private Person { ... };           // 使用私有继承

void dance(const Person& p);        // 每个人会跳舞

void study(const Student& s);       // 只有学生才学习


Person p;                           // p是一个人
Student s;                          // s是一个学生

dance(p);                           // 正确, p是一个人

dance(s);                           // 错误!一个学生不是一个人

很显然,私有继承的含义不是 "是一个",那它的含义是什么呢?

"别忙!" 你说。"在弄清含义之前,让我们先看看行为。私有继承有那些行为特征呢?" 那好吧。关于私有继承的第一个规则正如你现在所看到的:和公有继承相反,如果两个类之间的继承关系为私有,编译器一般不会将派生类对象(如Student)转换成基类对象(如Person)。这就是上面的代码中为对象s调用dance会失败的原因。第二个规则是,从私有基类继承而来的成员都成为了派生类的私有成员,即使它们在基类中是保护或公有成员。行为特征就这些。

这为我们引出了私有继承的含义:私有继承意味着 "用...来实现"。如果使类D私有继承于类B,这样做是因为你想利用类B中已经存在的某些代码,而不是因为类型B的对象和类型D的对象之间有什么概念上的关系。因而,私有继承纯粹是一种实现技术。用条款36引入的术语来说,私有继承意味着只是继承实现,接口会被忽略。如果D私有继承于B,就是说D对象在实现中用到了B对象,仅此而已。私有继承在软件 "设计" 过程中毫无意义,只是在软件 "实现" 时才有用。

私有继承意味着 "用...来实现" 这一事实会给程序员带来一点混淆,因为条款40指出,"分层" 也具有相同的含义。怎么在二者之间进行选择呢?答案很简单:尽可能地使用分层,必须时才使用私有继承。什么时候必须呢?这往往是指有保护成员和/或虚函数介入的时候 ---- 但这个问题过一会儿再深入讨论。

条款41提供了一种方法来写一个Stack 模板,此模板生成的类保存不同类型的对象。你应该熟悉一下那个条款。模板是C++最有用的组成部分之一,但一旦开始经常性地使用它,你会发现,如果实例化一个模板一百次,你就可能实例化了那个模板的代码一百次。例如Stack模板,构成Stack<int>成员函数的代码和构成Stack<double>成员函数的代码是完全分开的。有时这是不可避免的,但即使模板函数实际上可以共享代码,这种代码重复还是可能存在。这种目标代码体积的增加有一个名字:模板导致的 "代码膨胀"。这不是件好事。

对于某些类,可以采用通用指针来避免它。采用这种方法的类存储的是指针,而不是对象,实现起来就是:

· 创建一个类,它存储的是对象的void*指针。
· 创建另外一组类,其唯一目的是用来保证类型安全。这些类都借助第一步中的通用类来完成实际工作。

下面的例子使用了条款41中的非模板Stack类,不同的是这里存储的是通用指针,而不是对象:

class GenericStack {
public:
  GenericStack();
  ~GenericStack();

  void push(void *object);
  void * pop();

  bool empty() const;

private:
  struct StackNode {
    void *data;                    // 节点数据
    StackNode *next;               // 下一节点

    StackNode(void *newData, StackNode *nextNode)
    : data(newData), next(nextNode) {}
  };

  StackNode *top;                          // 栈顶

  GenericStack(const GenericStack& rhs);   // 防止拷贝和
  GenericStack&                            // 赋值(参见
    operator=(const GenericStack& rhs);    // 条款27)
};


因为这个类存储的是指针而不是对象,就有可能出现一个对象被多个堆栈指向的情况(即,被压入到多个堆栈)。所以极其重要的一点是,pop和类的析构函数销毁任何StackNode对象时,都不能删除data指针 ---- 虽然还是得要删除StackNode对象本身。毕竟,StackNode 对象是在GenericStack类内部分配的,所以还是得在类的内部释放。所以,条款41中Stack类的实现几乎完全满足the GenericStack的要求。仅有的改变只是用void*来替换T。

仅仅有GenericStack这一个类是没有什么用处的,但很多人会很容易误用它。例如,对于一个用来保存int的堆栈,一个用户会错误地将一个指向Cat对象的指针压入到这个堆栈中,但编译却会通过,因为对void*参数来说,指针就是指针。

为了重新获得你所习惯的类型安全,就要为GenericStack创建接口类(interface class),象这样:

class IntStack {                  // int接口类
public:
  void push(int *intPtr) { s.push(intPtr); }
  int * pop() { return static_cast<int*>(s.pop()); }
  bool empty() const { return s.empty(); }

private:
  GenericStack s;                 // 实现
};

class CatStack {                  // cat接口类
public:
  void push(Cat *catPtr) { s.push(catPtr); }
  Cat * pop() { return static_cast<Cat*>(s.pop()); }
  bool empty() const { return s.empty(); }

private:
  GenericStack s;                 // 实现
};

正如所看到的,IntStack和CatStack只是适用于特定类型。只有int指针可以被压入或弹出IntStack,只有Cat指针可以被压入或弹出CatStack。IntStack和CatStack都通过GenericStack类来实现,这种关系是通过分层(参见条款40)来体现的,IntStack和CatStack将共享GenericStack中真正实现它们行为的函数代码。另外,IntStack和CatStack所有成员函数是(隐式)内联函数,这意味着使用这些接口类所带来的开销几乎是零。

但如果有些用户没认识到这一点怎么办?如果他们错误地认为使用GenericStack更高效,或者,如果他们鲁莽而轻率地认为类型安全不重要,那该怎么办?怎么才能阻止他们绕过IntStack和CatStack而直接使用GenericStack(这会让他们很容易地犯类型错误,而这正是设计C++所要特别避免的)呢?

没办法!没办法防止。但,也许应该有什么办法。

在本条款的开始我就提到,要表示类之间 "用...来实现" 的关系,有一个选择是通过私有继承。现在这种情况下,这一技术就比分层更有优势,因为通过它可以让你告诉别人:GenericStack使用起来不安全,它只能用来实现其它的类。具体做法是将GenericStack的成员函数声明为保护类型:

class GenericStack {
protected:
  GenericStack();
  ~GenericStack();

  void push(void *object);
  void * pop();

  bool empty() const;

private:
  ...                             // 同上
};

GenericStack s;                   // 错误! 构造函数被保护


class IntStack: private GenericStack {
public:
  void push(int *intPtr) { GenericStack::push(intPtr); }
  int * pop() { return static_cast<int*>(GenericStack::pop()); }
  bool empty() const { return GenericStack::empty(); }
};

class CatStack: private GenericStack {
public:
  void push(Cat *catPtr) { GenericStack::push(catPtr); }
  Cat * pop() { return static_cast<Cat*>(GenericStack::pop()); }
  bool empty() const { return GenericStack::empty(); }
};

IntStack is;                     // 正确

CatStack cs;                     // 也正确

和分层的方法一样,基于私有继承的实现避免了代码重复,因为这个类型安全的接口类只包含有对GenericStack函数的内联调用。

在GenericStack类之上构筑类型安全的接口是个很花俏的技巧,但需要手工去写所有那些接口类是件很烦的事。幸运的是,你不必这样。你可以让模板来自动生成它们。下面是一个模板,它通过私有继承来生成类型安全的堆栈接口:

template<class T>
class Stack: private GenericStack {
public:
  void push(T *objectPtr) { GenericStack::push(objectPtr); }
  T * pop() { return static_cast<T*>(GenericStack::pop()); }
  bool empty() const { return GenericStack::empty(); }
};

这是一段令人惊叹的代码,虽然你可能一时还没意识到。因为这是一个模板,编译器将根据你的需要自动生成所有的接口类。因为这些类是类型安全的,用户类型错误在编译期间就能发现。因为GenericStack的成员函数是保护类型,并且接口类把GenericStack作为私有基类来使用,用户将不可能绕过接口类。因为每个接口类成员函数被(隐式)声明为inline,使用这些类型安全的类时不会带来运行开销;生成的代码就象用户直接使用GenericStack来编写的一样(假设编译器满足了inline请求 ---- 参见条款33)。因为GenericStack使用了void*指针,操作堆栈的代码就只需要一份,而不管程序中使用了多少不同类型的堆栈。简而言之,这个设计使代码达到了最高的效率和最高的类型安全。很难做得比这更好。

本书的基本认识之一是,C++的各种特性是以非凡的方式相互作用的。这个例子,我希望你能同意,确实是非凡的。

从这个例子中可以发现,如果使用分层,就达不到这样的效果。只有继承才能访问保护成员,只有继承才使得虚函数可以重新被定义。(虚函数的存在会引发私有继承的使用,例子参见条款43)因为存在虚函数和保护成员,有时私有继承是表达类之间 "用...来实现" 关系的唯一有效途径。所以,当私有继承是你可以使用的最合适的实现方法时,就要大胆地使用它。同时,广泛意义上来说,分层是应该优先采用的技术,所以只要有可能,就要尽量使用它。

<think> 我们参考了用户提供的引用内容,其中提到了内存管理、多线程编程和STL等内容。用户已经学完C++基础语法、STL和多线程,接下来应该学习什么? 根据C++的学习路径,在掌握了这些基础之后,可以继续深入学习以下方向: 1. 深入模板元编程:C++的模板是强大且复杂的特性,学习模板元编程可以提高代码的抽象能力和复用性。 2. 现代C++特性(C++11/14/17/20):学习lambda表达式、智能指针、移动语义、并发库等现代特性。 3. 设计模式:掌握常用的设计模式,提高软件设计能力。 4. 网络编程:学习使用C++进行网络编程,如socket编程,以及网络库(如Boost.Asio)的使用。 5. 系统级编程:包括操作系统相关的API调用、进程管理、文件系统操作等。 6. 性能优化:学习如何分析和优化C++程序的性能,包括内存访问模式、缓存友好设计等。 7. 跨平台开发:了解如何在不同的操作系统(如Windows、Linux)上开发和移植C++程序。 8. 深入学习并发编程:虽然已经学习了多线程,但并发编程还有更多高级主题,如无锁编程、线程池、条件变量等。 我们根据这些方向,为用户推荐接下来的学习内容,并生成相关问题。 </think>在学完C++基础语法、STL和多线程后,建议按照以下路径继续深入学习,这些内容能帮助你构建更专业的C++开发能力: ### 1. 现代C++特性(C++11/14/17/20) ```cpp // 示例:现代C++核心特性 auto lambda = [](auto x) { // C++14泛型lambda return std::make_unique<int>(x*2); // 智能指针 }; std::vector<int> v{1,2,3}; // 初始化列表 for(auto&& item : v) { // 范围for循环 std::cout << item << std::endl; } ``` - **关键特性**:智能指针(`unique_ptr/shared_ptr`)、移动语义、lambda表达式、类型推导(`auto`)、并发库(`<future>`) - **学习资源**:《Effective Modern C++》(Scott Meyers)、cppreference.com ### 2. 内存管理进阶 ```cpp // 智能指针应用场景 class Resource { public: Resource() { std::cout << "资源创建\n"; } ~Resource() { std::cout << "资源释放\n"; } }; auto processResource() { auto res = std::make_shared<Resource>(); // 自动内存管理 return res; // 引用计数管理生命周期 } ``` - **重点内容**: - 智能指针的原理与使用场景 - 自定义内存分配器 - 内存泄漏检测工具(Valgrind、AddressSanitizer) - RAII(资源获取即初始化)设计模式 ### 3. 网络编程(结合多线程) ```cpp // 简易TCP服务器示例 void start_server() { boost::asio::io_context io; tcp::acceptor acceptor(io, tcp::endpoint(tcp::v4(), 8080)); auto handle_connection = [&] { tcp::socket socket(io); acceptor.accept(socket); std::thread(handle_client, std::move(socket)).detach(); }; } ``` - **推荐方向**: - Socket编程基础 - 网络库:Boost.Asio/Poco/libevent - HTTP/TCP协议实现 - 高并发服务器架构 ### 4. 设计模式与架构 ```cpp // 工厂模式示例 class Widget { public: virtual void draw() = 0; static std::unique_ptr<Widget> create(WidgetType type); // 工厂方法 }; ``` - **常用模式**: - 创建型:工厂/单例/建造者 - 结构型:适配器/代理/组合 - 行为型:观察者/策略/命令 - **学习建议**:《设计模式:可复用面向对象软件的基础》 ### 5. 跨平台开发 - **关键技术**: - CMake构建系统 - 平台特定API封装(Windows/Linux/macOS) - 条件编译与宏定义 - 跨平台GUI框架(Qt/WxWidgets) ### 6. 性能优化 ```cpp // 缓存友好设计示例 struct alignas(64) CacheLine { // 64字节对齐 int data[16]; }; ``` - **优化方向**: - CPU缓存机制 - 分支预测优化 - SIMD指令集(AVX/SSE) - 性能分析工具(perf、VTune) ### 7. 项目实践建议 ```mermaid graph LR A[基础语法] --> B[STL容器] B --> C[多线程] C --> D[网络编程] D --> E[设计模式] E --> F[性能优化] F --> G[完整项目] ``` - **项目类型**: - 简易数据库系统 - HTTP服务器 - 实时数据处理系统 - 游戏引擎模块
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值