IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> 【阅读笔记】《Effective C++》 -> 正文阅读

[C++知识库]【阅读笔记】《Effective C++》

#! https://zhuanlan.zhihu.com/p/537589122

Effective C++ 55

导读

  • 声明式(declaration)
  • 签名式(signature)
  • 定义式(definition)
  • 初始化(initialization)
  • explicit关键字:阻止隐式类型转换
  • 接口(interface)
  • lhs与rhs

一 让自己习惯C++

1 视C++为一个语言联邦

2 尽量以const,enum,inline替换#define

  • enum hack是模板元编程的基础技术
class GamePlayer{
private:
    enum { NumTurns = 5};
    // static const int NumTurns = 5;
    int scores[NumTurns];
};

3 尽可能使用const

  • const与mutable

4 确定对象被使用前已先被初始化

  • 别混淆赋值和初始化,初始化效率高于赋值。eg.类的成员初始化列表与构造函数内部赋值
  • 单例模式

二 构造/析构/赋值运算

5 了解c++ 默默编写并调用哪些函数

  • 自动生成:构造函数、拷贝构造函数、拷贝赋值函数、析构函数
  • 另外:移动构造函数、移动赋值函数也很重要
class Test{
public:
    // 空类自带
    Test() = default;
    Test(const Test& t) = default;
    Test& operator=(const Test& t)= default;
    ~Test() = default;

    // 移动(右值引用),需要清除输入资源
    Test(const Test&& t) noexcept{}
    Test& operator=(const Test&& t) noexcept{}
};

6 若不想使用编译器自动生成的函数,就该明确拒绝

  • 声明为私有,或者继承私有class,或者=delete
class Uncopyable{
private:
    Uncopyable(const Uncopyable&);
    Uncopyable& operator=(const Uncopyable&);
};
class HomeForSale : private Uncopyable{};

7 为多态基类声明virtual析构函数

  • 基类析构函数不加virtual会造成“局部销毁”,eg.Base* ptr = New Derived(); delete ptr;中只会调用base的析构,而derived类很可能没有被销毁。
  • 虚函数表及指针,X64占8字节,X86占4字节,参考
  • 纯虚析构函数(virtural ~Test()=0) = 抽象类 = 不能单独实例化 = 派生类必须要定义该函数(如果基类未定义)

8 别让异常逃离析构函数

  • 析构函数绝对不要吐出异常

9 绝不在构造和析构过程中调用virtual函数

  • 这类调用从不下降到派生类

10 令operator=返回一个*this引用

  • 可实现连锁赋值(a=b=c=20)
  • 只是个协议,并无强制性
class Widget{
    Widget& operator=(const Widget& rhs){
        ...
        return *this;
    }
}

11 在operator=中处理“自我赋值”

class BitMap{};
class Widget{
    // 1.不安全实现版本,不具备异常安全性,
    // 如果new操作异常(内存不足或者拷贝构造函数异常),pb会指向一个删除了的地址。
    Widget& operator=(const Widget& rhs){
        if(this == &rhs) return *this; // 证同测试
        delete pb;
        pb = new BitMap(*rhs.pb);
        return *this;
    }
    // 2.异常安全性,非最高效,但行得通
    // 如果很关心效率,可以加入证同测试
    Widget& operator=(const Widget& rhs){
        BitMap* pOring = pb;
        pb = new BitMap(*rhs.pb);
        delete pOring;
        return *this;
    }
    // 3.copy and swap技术。异常安全+自我复制安全。一般用这个
    Widget& operator=(const Widget& rhs){
        Widget temp(rhs);
        swap(temp);
        return *this;
    }
    // 4.传值
    Widget& operator=(Widget rhs){
        swap(rhs);
        return *this;
    }
    void swap(Widget& rhs);// 详见第29条


private:
    BitMap* pb; //从heap分配的指针
};

12 复制对象时勿忘其每一个成分

  • 复制所有成员变量和基类的所有成元
  • 深拷贝和浅拷贝
  • 拷贝构造和拷贝赋值不要相互调用,共同机能放入第三个函数

三 资源管理

13 以对象管理资源

  • c++11 智能指针:shared_ptr, unique_ptr, weak_ptr
  • RAII(Resource Acquisition Is Initialization) 资源取得时便是初始化时刻
  • 多个auto_ptr不能指向同一个对象(auto_ptr被销毁时会自动删除所指对象),unique_ptr已替代auto_ptr
  • weak_ptr专门为避免shared_ptr环形引用问题而设计,算是shared_ptr的辅助工具

14 在资源管理类中小心copying行为

  • 禁止复制:将类的copy函数声明为私有
  • 对底层资源祭出“引用计数法”:共享指针
  • 复制底部资源:深拷贝
  • 转移底部资源的拥有权:auto_ptr,std::move()

15 在资源管理类中提供对原始资源的获取

  • shared_ptr::get()
  • shared_ptr/auto_ptr 重载->和* 运算符

16 成对使用new和delete时要采取相同形式

  • new/delete 与 new[]/delete[]

17 以独立语句将newed对象置入智能指针

如下第二个有风险的函数调用,在调用之前,编译器必须创建代码:

  • 执行new Widget
  • 调用priority()
  • 调用shared_ptr构造函数
    以上三件事,c++编译器以什么次序完成并不知道,弹性很大,如果以上面顺序执行,在对priority()的调用导致异常,new Widget将会遗失,引发资源泄露。所以要分离语句。
int priority();
void processWidget(std::shared_ptr<Widget> pw, int priority);

// 错误,不支持隐式转换
processWidget(new Widget, priority());

// 会有风险
processWidget(std::shared_ptr<Widget>(new Widget), priority());

// OK,new指针独立语句
std::shared_ptr<Widget> pw(new Widget)
processWidget(pw, priority());

四 设计与声明

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

  • STL容器的接口十分一致,size函数等等。相反java就比较混乱
  • 年月日的示例,防止接口误用
class Date{
public:
    // 不行:1.容易输入错误次序,2.容易传递无效月份或天数
    Date(int month, int day, int year);
    // 可以:使用外覆类型(wrapper types)来区分年月日(构造函数设为显示)
    Date(const Month& m, const Day& d, const Year& y);
}
  • 让types容易被正确使用,动态分配对象的示例
// 不行:容易内存泄露
Investment* createInvestment();
// 可以:使用智能指针
std::shared_ptr<Investment> createInvestment();

19 设计class犹如设计type

注意class的重载、操作符、控制内存的分配和归还、传参、继承、类型转换、定义对象的初始化和终结等的设计,和默认类型一样设计。

20 宁以 传常引用 替换 传值

  • 为避免不必要的重复内存构造和析构,传常引用。
  • 内置类型(如int)、STL的迭代器和函数对象传值比传引用效率高。

21 必须返回对象时,谨慎返回其引用

  • 函数内不能返回临时值的引用,当函数结束时,内存就被销毁了,返回引用就会报错
int& func(){
    // 栈:编译就会产生warning:reference to local variable returned [-Wreturn-local-addr]
    int a = 99;
    return a;

    // 堆:能用,但是没有delete,容易导致内存泄漏
    int* a = new int(99);
    return *a;
}

22 将成员变量声明为私有

  • 保证封装性,成员变量私有更安全,可以设置setter和getter函数访问私有
  • protected不比public更具封装性

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

  • 以网页浏览器class为例探讨删除缓存:clearEverythingclearBrowser的封装性低。
  • 越少代码可以看到数据,越多数据可以被封装。越多函数可以访问它,数据的封装性就越低。
  • 成员函数可以访问所有成员,非成员非友元函数不能访问任何成员,但两者提供相同机能。所有后者封装性高
class WebBrowser {
 public:
  void clearCache() {}
  void clearHistory() {}
  void removeCookies() {}

  // 1. 没2封装性好,用2
  void clearEverything() {
    clearCache();
    clearHistory();
    removeCookies();
  }
};
// 2.
void clearBrowser(WebBrowser& wb){
    wb.clearCache();
    wb.clearHistory();
    wb.removeCookies();
}
  • 比较好的做法是类和非成员非友元函数在一个namespace中,同时可以在其他头文件中增加其他的功能函数,需要该功能调用头文件即可,减少编译的依赖关系
  • 把所有便利功能函数放在多个头文件但同一个namespace中,意味着客户可以轻松扩展功能。和C++标准库一样的组织方式
#include "webbrowser.h"
namespace WebBrowserStuff {
class WebBrowser {
 public:
  void clearCache() {}
  void clearHistory() {}
  void removeCookies() {}
};
// 核心机能等
void clearBrowser(WebBrowser& wb) {
  wb.clearCache();
  wb.clearHistory();
  wb.removeCookies();
}
}  // namespace WebBrowserStuff


#include "webbrowserbookmarks.h"
namespace WebBrowserStuff {
    // 与书签相关的便利函数(非成员非友元)
}  // namespace WebBrowserStuff

24 若所有参数皆需类型转换,请为此采用non-member函数

  • 设计有理数支持混合式运算,所以允许隐式转换。示例中有两种乘法设计方式,一种是成员函数,一种是非成员函数。
  • 对于成员函数1,res = 2 * one_half,相当于2.operator*(one_half),中的2没有相应的class,也就没有operator* 成员函数。同事也没有非成员函数满足operator*(2, one_half)
  • 对于非成员函数2,满足operator*(2, one_half),其中2进行了隐式转换Rational(2)
// 有理数
class Rational {
 public:
  // 允许隐式转换
  Rational(int numerator = 0, int denominator = 1)
      : _numerator(numerator), _denominator(denominator) {}

  // 1.
  // const Rational operator*(const Rational& rhs) {
  //   Rational res(this->numerator() * rhs.numerator(),
  //                this->denominator() * rhs.denominator());
  //   return res;
  // }
  int numerator() const { return _numerator; }
  int denominator() const { return _denominator; }

 private:
  // 分子
  int _numerator;
  // 分母
  int _denominator;
};
// 2.
const Rational operator*(const Rational& lhs, const Rational& rhs) {
  Rational res(lhs.numerator() * rhs.numerator(),
               lhs.denominator() * rhs.denominator());
  return res;
}

int main() {
  Rational one_half(1, 2);
  Rational res;

  res = one_half * 2; // 1和2都可以
  res = 2 * one_half; // 只有2可以

  return 0;
}

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

  • 标准程序库提供的算法如下,只需要T支持拷贝构造函数和赋值函数
namespace std{
    template<typename T>
    swap(T a, T b){
        T tmp(a);
        a = b;
        b = tmp;
    }
}
// a复制到tmp,b复制到a,tmp复制到b
  • 简单类型不用考虑,主要是指针类型,常见表现形式是pipml手法(pointer to implementation,见条款31)

  • 下面Widget类使用自带的swap时,复制三遍Widget,还复制三个WidgetImpl对象,缺乏效率。而实际只需要交换指针。因此设计如下,跟STL容器有一致性

class WidgetImpl {
  private:
  // 可能数据很多,复制时间很长
  int a, b, c;
  std::vector<double> vec;
};

class Widget {
  public:
  Widget(const Widget& rhs) {}
  Widget& operator=(const Widget& rhs) {
    *pImpl = *(rhs.pImpl);
    return *this;
  }
  void swap(Widget& other) { std::swap(pImpl, other.pImpl); }

  private:
  WidgetImpl* pImpl;
};

// 全特化模板
namespace std {
template <>
void swap<Widget>(Widget& a, Widget& b) {
  a.swap(b);
}
}  // namespace std
  • 而如果Widget设计为模板类,类设计的没错,但是c++只允许偏特化类模板,不允许偏特化函数模板。尤其是管理规则比较特殊的std内的模板,只支持全特化
template<typename T>
class WidgetImpl {
  private:
  std::vector<T> vec;
};
template<typename T>
class Widget {
  ...
  private:
  WidgetImpl<T>* pImpl;
};

namespace std {
//error:此声明中不允许使用显式模板参数列表
template <typename T>
void swap<Widget<T>>(Widget<T>& a, Widget<T>& b) {
  a.swap(b);
}

// 重载版本,但是也不合法,std标准委员会禁止膨胀已经声明好的东西,能编译运行,但它的行为没有明确定义
// 如果希望你的软件有可预期的行为,请不要添加任何新的东西到std里头。
template <typename T>
void swap(Widget<T>& a, Widget<T>& b) {
  a.swap(b);
}
}
  • 那么可以重新设计一个域,加入重载版本。
namespace WidgetStuff{
    
template <typename T>
class WidgetImpl;
    
template <typename T>
class Widget;

// 非成员函数,不在std内
template <typename T>
void swap(Widget<T>& a, Widget<T>& b) {
  a.swap(b);
}
}
  

五 实现

  • 适当提出class和function的声明
  • 太快定义变量会拖延效率
  • 过度使用转型(cast)可能导致代码变慢又难维护
  • 未考虑异常导致资源泄露
  • 过度inline导致代码膨胀
  • 过度耦合导致编译时间冗长

26 尽可能延后变量定义式的出现时间

  • 密码加密的示例
std::string encryptPassword(const std::string& password){
  // 过早,如果丢出异常,造成不必要的encrypted构造和析构
  // std::string encrypted;

  if(password.length() < 8){
    throw logic_error("Password is too short");
  }
  
  // 可以,且拷贝构造初始化了
  std::string encrypted(password);
  
  // 加密函数
  encrypt(encrypted);
  return encrypted;
}
  • 循环示例
// 这种好,尤其是n很大的时候,1构造 + 1析构 + n赋值
Widget w;
for(int i = 0; i < n; ++i){
  w = 某个值;
}

// n构造 + n析构。如果Widget很大,构造相当耗时
for(int i = 0; i < n; ++i){
  Widget w(某个值);
}

27 尽量少做转型动作

  • 不要使用c旧式转型,使用c++新式转型
  • const_cast 改变常量性和可变性
  • dynamic_cast 可以将一个指向基类的指针或者引用转换成指向其派生类的指针或者引用。耗时很慢,尽量避免
  • reinterpret_cast 将任意的指针类型转换成其他任意的指针类型,容易出错,不具有移植性
  • static_cast 普通类型转换

28 避免返回handles指向对象内部成分

  • handles指包括引用、指针、迭代器。

  • 返回指针指向某个成员函数

struct Point {
  int x = 0;
  int y = 0;
};

class Test {
 public:
 // 错误,虽然能通过编译。声明了const函数,返回了引用指向私有成员,从而调用者可以更改内部数据
  int& GetX() const { return _pt->x; }
  
  // 可以,但也有风险,见下
  const int& GetX() const { return _pt->x; }

 private:
  std::shared_ptr<Point> _pt;
};
  • 假设去掉智能指针,并且调用临时对象
struct Point {
    int x = 99;
    int y = 66;
};

class Test {
public:
    Test() {
        cout << "Test()" << endl;
        _pt = new Point();
    }

    ~Test() {
        cout << "~Test()" << endl;
        delete _pt;
    }

    const int &GetX() const { return _pt->x; }

private:
    // 假设这里是用的普通指针
    Point *_pt;
};

int main() {
    const int *ptr = nullptr;
    {
        // Test临时对象,出作用域后会析构,那么x指针也会被释放
        Test t;
        auto x = &(t.GetX());
        cout << "1 dir:" << x << " value:" << *x << endl;
        ptr = x;
        cout << "2 dir:" << ptr << " value:" << *ptr << endl;
    }

    // 能编译运行,但运行时该指针指向一个不存在的对象,造成指针空悬、虚吊(dangling)
    // 共享指针没有不会出现该问题
    cout << "3 dir:" << ptr << " value:" << *ptr << endl;

    return 0;
}

//1 dir:0x21987721a70 value:99
//2 dir:0x21987721a70 value:99
//~Test()
//3 dir:0x21987721a70 value:-2022565648
  • 所以一般情况下尽量避免返回handles(包括引用、指针、迭代器)指向对象内部,遵守该条款可增加封装性。

29 为异常安全而努力是值得的

  • 当异常被抛出时,带有异常安全性的函数会:1. 不泄露任何资源, 2.不允许数据破坏。以表现夹带背景图案的class为例:
class Image{
public:
    Image(const std::istream& img_src){}
};
class PrettyMenu{
public:
    // 这个函数很糟糕,异常安全的两个条件都没有满足
    void ChangeBackground(std::istream& img_src){
        mtx.lock();
        delete bg_image;
        ++image_changes;
        bg_image = new Image(img_src); // 一旦new失败,不会unlock(资源泄漏),且指针被delete,changes累加了但是图像没有被修改(数据破坏)。
        mtx.unlock();
    }
    // 修改后,可解决资源泄漏问题,数据破坏没解决
    void ChangeBackground2(std::istream& img_src){
        std::lock_guard<std::mutex> auto_lock(mtx);
        delete bg_image;
        ++image_changes;
        bg_image = new Image(img_src);
    }
private:
    std::mutex mtx;
    Image* bg_image;
    int image_changes;
};
  • 异常安全函数(Exception-safe functions)提供以下三个保证之一:
    1. 基本承诺:如果异常被抛出,程序内的任何事物仍然保持在有效状态下。
    2. 强烈保证:如果异常被抛出,程序状态不改变。函数调用成功则是完全成功,失败则要回复到调用之前的状态。
    3. 不抛掷(nothrow)保证。承诺绝不抛出异常,
  • 使用智能指针管理,保证基本的异常安全性。1基本承诺
class Image{
public:
    explicit Image(const std::istream& img_src){}
};
class PrettyMenu{
public:
    void ChangeBackground(std::istream& img_src){
        std::lock_guard<std::mutex> auto_lock(mtx);
        bg_image.reset(new Image(img_src));
        ++image_changes;
    }
private:
    std::mutex mtx;
    shared_ptr<Image> bg_image;
    int image_changes;
};
  • copy and swap,和pimpl idiom手法(将所有“隶属对象的数据”从原对象放进另一个对象内,然后赋予原对象一个指针,指向新的实现对象,即副本)。2强烈保证
class Image{
public:
    explicit Image(const std::istream& img_src){}
};
struct PMImpl{
    shared_ptr<Image> bg_image;
    int image_changes;
};
class PrettyMenu{
public:
    void ChangeBackground(std::istream& img_src){
        std::lock_guard<std::mutex> auto_lock(mtx);
        shared_ptr<PMImpl> p_new(new PMImpl(*p_impl));
        p_new->bg_image.reset(new Image(img_src));
        ++p_new->image_changes;
        std::swap(p_impl, p_new);
    }
private:
    std::mutex mtx;
    shared_ptr<PMImpl> p_impl;
};

30 透彻了解inlining的里里外外

  • 一个inline函数取决于 编译环境,取决于编译器
  • 构造函数和析构函数不适合inline
  • inline限制在小型、被频繁调用的函数上。使得调试和二进制升级更容易,使得代码膨胀问题最小化
  • 不要因为函数模板出现在头文件,就声明为inline。(std::max是内联函数模板)

31 将文件间的编译依存关系降至最低

  • 接口和实现的解耦,降低文件间的编译依存性。

  • 个人觉得这节很重要,值得仔细看看。有时候大工程因为改了一点小地方,却发现要重新编译整个工程浪费很长时间。我们要做的就是降低文件间的编译依存性,解决这个问题。

  • 假设三个类A,B,C,C依赖B,B依赖A,六个类文件和一个main文件。通常的做法:

// a.h
#pargma once
class A{
public:
    A();
};
// a.cpp
#include "a.h"
A::A(){}

/***************/
// b.h
#pargma once
#include "a.h"
class B{
public:
    B();
    A _a;
};
// b.cpp
#include "b.h"
B::B(){}

/***************/
// c.h
#pargma once
#include "b.h"
class C{
public:
    C();
    B _b;
};
// c.cpp
#include "c.h"
C::C(){}
    
/***************/    
//mian.cpp
#include "c.h"
int main(){
    C c;
    return 0;
}
  • 接口是指.h,实现是指.cpp。c++编译分两步编译.o文件链接.o成执行文件(exe/其他)。接口和实现改变都需要重新编译.o,但两者也有区别,最好只修改实现。

  • 一般来说,我们改cpp也就是实现的地方,只会需要重新编译该文件的.o,而依赖该文件的接口也就是.h没变,所以依赖的文件是不需要重新编译的。

  • 也就是说,修改a.cpp,只需要重新编译a.cpp.o和链接成执行文件。这也是类设计的时候需要接口和实现分离两个文件的原因之一!

  • 但是当我们修改接口a.h 时,需要重新编译a.cpp.ob.cpp.oc.cpp.omain.cpp.o和链接成执行文件。这就会导致浪费时间编译整个工程!

  • 编译依存最小化有两种方法:Handle classesInterface classes

(1) Handle classes

前置声明+指针

  • 对于上面的示例可以更改为如下示例。主要改动有两点

  • 一是在有依赖的接口前面去掉依赖的头文件,加上依赖类前置声明,在实现里加入依赖头文件:如b.h去掉了#include "a.h",前置声明了class A,在b.cpp里面加入了#include "a.h"

  • 二是在有依赖的接口里面,使用指针声明成员变量:如B类里面使用了A* _a声明成员。因为A只有声明,此时这个地方没有定义,编译器不知道_a具体分配多少内存,但是使用指针就可以。“将对象实现细目隐藏于一个指针背后的游戏”,这也是该方法的巧妙之处!

  • 此时我们修改a.h时,需要重新编译a.cpp.ob.cpp.o和链接成执行文件,b.cpp.o是因为b.cpp里面包含了A的接口,A接口变了,所以需要重新编译,但是B的接口没变,所以后面的两个都不需要重新编译!

// a.h
#pargma once
class A{
public:
    A();
};
// a.cpp
#include "a.h"
A::A(){}

/***************/
// b.h
#pargma once
class A; //前置声明
class B{
public:
    B();
    A* _a;
};

// b.cpp
#include "b.h"
#include "a.h" //cpp中加入头文件
B::B(){}

/***************/
// c.h
#pargma once
class B;
class C{
public:
    C();
    B* _b;
};
// c.cpp
#include "c.h"
#include "b.h"
C::C(){}
    
/***************/    
//mian.cpp
#include "c.h"
int main(){
    C c;
    return 0;
}
  • 书上的示例如下,设计原理差不多。对外是person类,而具体是由personimpl类来实现,这种设计就是pimpl idiom(pointer to implementation),一般来说,person不会改动,主要是personimpl来修改,封装的很严实,也是真正的接口与实现分离

  • 分离的关键就在于以声明的依存性替换定义的依存性

person.h

#ifndef PERSON_H
#define PERSON_H
#include <memory>

class PersonImpl;
class Person
{
public:
    Person(const std::string& name);
    std::string name() const;
private:
    std::shared_ptr<PersonImpl> pImpl;
};
#endif // PERSON_H

person.cpp

#include "person.h"
#include "personimpl.h"

Person::Person(const std::string& name) : pImpl(new PersonImpl(name)){}

std::string Person::name() const{
    return pImpl->name();
}

personimpl.h

#ifndef PERSONIMPL_H
#define PERSONIMPL_H

#include <iostream>

class PersonImpl
{
public:
    PersonImpl(const std::string& name);
    std::string name() const;
private:
    std::string _name;
};

#endif // PERSONIMPL_H

personimpl.cpp

#include "personimpl.h"

PersonImpl::PersonImpl(const std::string& name):_name(name){}

std::string PersonImpl::name() const{
    return _name;
}
  • C++标准库里的内含iostream各组件的声明式,定义

(2) Interface classes

抽象基类+继承+指针

  • 用基类指针指向派生类
  • 只需要提供基类头文件person.h给使用者即可,PersonImpl类是完全封装的,别人看不见。和上面是一样的。

person.h

#ifndef PERSON_H
#define PERSON_H
#include <memory>
class Person
{
public:
    virtual ~Person() = default;
    static std::shared_ptr<Person> create();
    virtual std::string name() const = 0;
};
#endif // PERSON_H

person.cpp

#include "person.h"
#include "personimpl.h"

std::shared_ptr<Person> Person::create(){
    return std::shared_ptr<Person>(new PersonImpl);
}

personimpl.h

#ifndef PERSONIMPL_H
#define PERSONIMPL_H

#include <iostream>
#include "person.h"

class PersonImpl : public Person
{
public:
    PersonImpl();
    ~PersonImpl() = default;
    std::string name() const;
private:
    std::string _name;
};
#endif // PERSONIMPL_H

personimpl.cpp

#include "personimpl.h"

PersonImpl::PersonImpl():_name("admin"){}

std::string PersonImpl::name() const{
    return _name;
}

main.cpp

#include <iostream>
#include "person.h"
using namespace std;

int main()
{
    std::shared_ptr<Person> per(Person::create());
    cout << per->name() << endl;
    
    return 0;
}

六 继承与面向对象设计

32 确定你的public继承塑模出is-a关系

  • Is-a,公开继承是“is a”的关系。
class Base{
    virtual ~Base(){}
};
class Derived : public Base{
    ~Derived(){}
}
  • 除此之外,还有has ais-implemented-in-terms-of(根据某物实现出)

33 避免遮掩继承而来的名称

  • 继承时的名称遮掩问题可以使用using声明式私有继承+转交函数解决。
class Base{
    virtura ~Base(){}
    virtual void f1();
    virtual void f1(int);
    void f2();
    void f2(float);
};
class Derived : public Base{
    ~Derived(){}
    // 遮掩了base的所有f1 f2函数  
    virtual void f1();
    void f2();
}

Derived d; 
d.f1(); //Derived::f1
d.f2(); //Derived::f2
d.f1(1); //error,被遮掩
d.f2(1.3f); //error,被遮掩
  • using声明式
class Derived : public Base{
    ~Derived(){}
    using Base::f2;
    using Base::f1;
    virtual void f1();
    void f2();
}

d.f1(1); //Base::f1(int)
d.f2(1.3f); //Base::f2(float)
  • 私有继承+转交函数
class Derived : private Base{
    ~Derived(){}
    virtual void f1(){
        Base::f1();
    }
    ...
}

34 区分接口继承和实现继承

  • public继承了两种:函数接口函数实现
  • 基类声明纯虚函数virtual void f() = 0;,派生类只继承函数接口,且需要强制性重新实现
  • 基类声明纯虚函数virtual void f();,派生类继承函数接口和默认函数实现
  • 基类的纯虚函数也可以拥有实现

35 考虑virtual函数以外的其他选择

NVI + Strategy

  • 使用非virtual 接口手法实现Template Method模式

  • Template Method模式之一:使用普通成员函数调用私有虚函数,non-virtual interface(NVI)手法

  • 派生类需要重新定义虚函数时,可以设为protected

    • 多态性质的基类,不能使用NVI
class GameCharacter{
public:
  // 虚函数的wrapper
  int healthValue() const{
    return doHealthValue();
  }
// private:
protected:
  virtual int doHealthValue() const{
    return health_value;
  }
  int health_value;
};
  • 使用函数指针实现Strategy模式:就是构造函数使用函数指针传递,派生类继承基类,在初始化的时候可以使用不同的健康计算函数
class GameCharacter;
int defaultHealthCalc(const GameCharacter& gc) { return 1; }

// 两种函数指针设计都可以,第二种是c++11新特性
// typedef int (*HealthCalCFunc)(const GameCharacter&);
typedef std::function<int(const GameCharacter&)> HealthCalCFunc;

class GameCharacter {
  public:
  explicit GameCharacter(HealthCalCFunc hcf = defaultHealthCalc)
      : healthFuncPtr(hcf) {}
  virtual ~GameCharacter() {}

  int healthValue() const { return healthFuncPtr(*this); }

  private:
  HealthCalCFunc healthFuncPtr;
};

class EvilBadGuy : public GameCharacter {
  public:
  explicit EvilBadGuy(HealthCalCFunc hcf = defaultHealthCalc)
      : GameCharacter(hcf) {}
};
int main() {
  GameCharacter gc(defaultHealthCalc);

  // 派生类可以传递其他的健康函数
  EvilBadGuy ebg1(defaultHealthCalc);
  cout << ebg1.healthValue() << endl;

  return 0;
}
  • 传统古典的Strategy模式是将健康计算函数设计成一个基类,不同的健康计算函数派生。然后用类指针替换任务里的函数指针,本质是一样的。设计模式里面的内容。

36 绝不重新定义继承来的non-virtual函数

  • 非虚函数(普通成员函数)静态绑定,虚函数动态绑定。前面遮掩继承有提到过
class Base {
  public:
  virtual ~Base() {}

  void f() { cout << "base f()" << endl; }
};

class Derived : public Base {
  public:
  // 遮掩了Base::f(),所以不要重新定义
  void f() { cout << "Derived f()" << endl; }
};

int main() {
  Derived x;
  Base* pb = &x;
  Derived* pd = &x;

  pb->f();  // base f()
  pd->f();  // Derived f()

  return 0;
}

37 绝步重新定义继承而来的缺省参数值

  • virtual函数动态绑定,缺省参数值静态绑定
class Base {
  public:
  virtual ~Base() {}

  virtual void f(int num = 1) { cout << "base : " << num << endl; }
};

class Derived1 : public Base {
  public:
  // 赋予不同的默认值,不行
  virtual void f(int num = 2) { cout << "Derived1 : " << num << endl; }
};
class Derived2 : public Base {
  public:
  // 静态绑定时需要传入参数,动态绑定时这个函数就会从基类中继承默认值
  virtual void f(int num) { cout << "Derived2 : " << num << endl; }
};
int main() {
  Derived1 d;
  Base* pb1 = new Derived1();
  Base* pb2 = new Derived2();

  // 没问题,静态类型是Derived1,所以默认参数也是2
  d.f();      // Derived1 : 2
  // 大问题,静态类型是Base*,虽然调用了派生类的函数,但是默认参数却是使用了静态绑定的1
  pb1->f();   // Derived1 : 1
  // 没问题,继承静态绑定的1
  pb2->f();   // Derived2 : 1

  delete pb1;
  delete pb2;
  return 0;
}

38 通过复合塑造出has-a根据某物实现出

  • is a 公有继承(私有继承不是)
  • has a 复合(应用域:仅类成员)
  • is-implemented-in-terms-of 复合(实现域:由类成员实现该类的内容,比如用一个链表成员实现平衡二叉树)、私有继承

39 明智而审慎地使用private继承

  • 编译器不会自动把私有继承的派生类转换为基类,继承的成员全部变成私有属性
class Person {
  public:
  virtual ~Person() {}
  virtual void f() const { cout << "person f()" << endl; }
};

class Student : private Person {
  public:
  virtual void f() const { cout << "Student f()" << endl; }
};

void doSomething(const Person& p) { p.f(); }

int main() {
  Person p;
  Student s;
  doSomething(p);
  // 私有继承编译报错,公有继承可以运行且多态
  doSomething(s);

  return 0;
}
  • 私有继承是is-implemented-in-terms-of,比复合的级别低,但是当派生类需要访问基类的protected成员,或者重新定义virtual函数时,这么设计是合理的。
  • 与复合不同,私有继承可以造成empty base最优化,对象尺寸最小化

40 明智而审慎地使用多重继承

  • 歧义
class A {
 public:
  void checkout() { cout << "A" << endl; }
};
class B {
 public:
  void checkout() { cout << "B" << endl; }
};
class C : public A, public B {};

int main() {
  C c;

  // 编译错误 "C::checkout" 不明确
  // c.checkout();
  
  // 可以
  c.A::checkout();
  c.B::checkout();
  return 0;
}
  • 钻石型多重继承问题
class File {
 public:
  string file_name;
};
class InputFile : public File {};
class OutputFile : public File {};
class IOFile : public InputFile, public OutputFile {};

int main() {
  IOFile io;
  // 编译错误 "IOFile::file_name" 不明确
  io.file_name;
  return 0;
}
// 继承了两份file_name,但实际IOFile对象只改有一个文件名称。解决方法就是virtual继承
// c++标准库中的basic_ios, basic_istream, basic_ostream, basic_iostream就是钻石型多重继承
class File {
 public:
  string file_name;
};
class InputFile : virtual public File {};
class OutputFile : virtual public File {};
class IOFile : public InputFile, public OutputFile {};

int main() {
  IOFile io_file;
  io_file.file_name;
  return 0;
}
// 
// 
  • virtual继承的类,产生的对象体积大,访问成员变量速度慢等。virtual继承的代价。所以避免在virtual base class中放置数据。
  • 虚拟继承(共享继承)是多重继承中特有的概念,解决多重继承中数据的二义性。

七 模板与泛型编程

41 了解隐式接口和编译器多态

  • class和template都支持接口和多态
  • class的接口是显示,以函数签名为中心。多态通过virtual发生在运行期
  • template的接口是隐式, 奠基于有效表达式。多态通过模板具现化和函数重载解析发生于编译期
// T类型需要支持size函数,编译期时就回检查
template<typename T>
void doSomething(T& t){
    t.size();
}

42 了解typename的双重意义

  • 声明template参数时,class和typename都可以
template<class T> 		f(T& t);
template<typename T> 	f(T& t);
  • 嵌套从属类型名称时要使用typename,其他名称不用
template<typename C>
void print2nd(const C& container){
    if(container.size() >= 2){
        // 模板类出现某个名称依赖于某个模板参数C,则是从属名称。从属名称在class内呈嵌套状,则是嵌套从属名称

        // 1. 编译不过,C::const_iterator是嵌套从属名称。const_iterator无法确认是类型还是变量
        // C::const_iterator iter(container.begin());

        // 2. 可以。
        typename C::const_iterator iter(container.begin());

        ++iter;
        cout << *iter << endl;
    }
}

template<typename C>                // class和typename都可以
void f(const C& container,          // 不允许使用typename
       typename C::iterator iter)   // 必须使用typename
{}
  • 基类列和成员初始列的嵌套类型不能使用typename
template<typename T>
class Base{
public:
    Base() = default;
    virtual ~Base() = default;
    class Nested{
    public:
        explicit Nested(int x){}
        Nested() = default;
    };
};

template<typename T>
class Derived : public Base<T>::Nested{             // 基类列(base classes list)不允许typename
public:
    explicit Derived(int x) : Base<T>::Nested(x){   // 成员初始化(member initialization list)不允许typename
        typename Base<T>::Nested temp;              // 嵌套从属名称必须使用typename
    }
};

43 学习处理模板化基类内的名称

  • 派生类模板使用基类模板函数时要加this或者明确域,否则基类模板的函数会被派生类覆盖遮掩。
class CompanyA{
public:
    void sendClearText(const string& msg){}
};
class CompanyB{
public:
    void sendEncrypted(const string& msg){}
};

// 基类模板
template<typename T>
class MsgSender{
public:
    void sendClear(const string& info){
        T company;
        company.sendClearText(info);
    }
};
// 基类模板全特化
template<>
class MsgSender<CompanyB>{
public:
    void sendSecret(const string& info){
        CompanyB company;
        company.sendEncrypted(info);
    }
};

// 派生类模板
template<typename T>
class LoggingMsgSender: public MsgSender<T>{
public:

    void SendClearMsg(const string& info){
        // 1. 编译错误,进入templated base classes观察的行为失效
        // 无法知道MsgSender<T>是否有sendClear函数。尤其是存在特化版本MsgSender<CompanyB>时,并没有该函数,
        // sendClear(info);

        // 2. 可以,加this, 假设成功继承。但是使用特化版本基类没有sendClear会编译错误,下同。
        // this->sendClear(info);

        // 3. 可以,明确指出域(或者使用函数外使用using MsgSender<T>::sendClear)
        MsgSender<T>::sendClear(info);
    }
};

44 将与参数无关的代码抽离templates

  • 模板可以节省时间和避免代码重复,也可能会导致代码膨胀(似乎影响不大,见C++ 模板带来的代码膨胀有多少影响?)。进行共性与变性分析,抽离无关的代码

  • 以设计某动态尺寸的正方矩阵模板为例,该矩阵支持求逆矩阵运算。为了减少求逆运算的代码膨胀,设计一个基类并私有继承,如下:

template<typename T>
class SquareMatrixBase{
protected:
    SquareMatrixBase(size_t n, T* p_mem) : size(n), p_data(p_mem){}
    void invert(size_t matrix_size){}
private:
    size_t size;
    T* p_data; // 获取派生类矩阵数据的指针
};

// 私有继承,意味Base类只是个辅助类,不是is-a关系
// T是模板类型参数,n是非模板类型参数
template<typename T, size_t n>
class SquareMatrix : private SquareMatrixBase<T>{
public:
    SquareMatrix() : SquareMatrixBase<T>(n, data){}
    void invert(){
        SquareMatrixBase<T>::invert(n);
    }
private:
    T data[n*n]; // 存储矩阵数据
};
  • 上述设计的优点有:

    • 如果不用基类实现求逆,不同大小的矩阵都会具现化不同大小的invert函数,导致代码膨胀
    • 私有继承,辅助派生类实现功能
    • 基类获取派生类的成员指针,减少内存拷贝
  • 非类型模板参数造成的代码膨胀,可以消除,用函数参数或者class成员变量替换非类型模板参数,如上面n的设置

  • 类型模板参数造成的代码膨胀,可以降低,用完全相同二进制表述的具现类型共享实现码。如上面invert函数的设计

45 运用成员函数模板接受所有兼容类型

  • 以设计自定义的智能指针模板为例,支持基类和派生类指针的转换。因为具现化了不同的两个类,所以需要设计拷贝构造函数
class Base{
public:
    virtual ~Base() = default;
};
class Derived : public Base{};

template<typename T>
class SmartPtr{
public:
    explicit SmartPtr(T* ptr) : _ptr(ptr){} // 以原始指针初始化
    
    // 成员模板,泛化拷贝构造函数:不声明explicit,因为派生类指针转基类指针是隐式转换,是合理的
    template<typename U>
    SmartPtr(const SmartPtr<U>& other){}
    
    T* get(){return _ptr;}

private:
    T* _ptr;
};
int main() {
    Base* ptr = new Derived;
    SmartPtr<Base> pt1 = SmartPtr<Derived>(new Derived);
    return 0;
}
  • 使用成员模板生成了泛化拷贝构造泛化赋值操作,正常的拷贝构造和赋值操作函数还是需要声明的。

46 需要类型转换时请为模板定义非成员函数

  • 使用24条的有理数例子进行模板化设计,为了支持模板的混合式算数运算,设计如下:
template<typename T>
class Rational {
public:
    Rational(const T& numerator = 0, const T& denominator = 1)
            : _numerator(numerator), _denominator(denominator) {}
    T numerator() const { return _numerator; }
    T denominator() const { return _denominator; }

    // 保证混合运算,定义在class内的非成员-友元函数
    // 如果仅声明,定义在class外,能编译,不能链接,报错
    friend Rational operator*(const Rational& lhs, const Rational& rhs){
        return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
    }
private:
    T _numerator;
    T _denominator;
};
int main() {
    Rational<int> one_half(1, 2);
    Rational<int> result = one_half * 2;
    return 0;
}

47 请使用traits classes表示类型信息

  • STL主要由容器、迭代器、算法等的模板组成,也包括若干工具类模板,例如advance,用来将某个迭代器移动某个给定距离,但迭代器有5种,移动方式也不同:

    • Input迭代器,只能向前移动,一次一步,只读
    • Output迭代器,只能向前移动,一次一步,只写
    • forward迭代器,继承Input迭代器,一次一步,读写
    • Bidirectional迭代器,继承forward迭代器,双向单次移动。
    • random access迭代器,继承Bidirectional迭代器,常量时间内向前或向后跳跃任意距离,包含上述功能
  • 以上五种分类,C++标准程序库分别提供了专属卷标结构(tag struct)加以确认

    ...\c++\bits\stl_iterator_base_types.h

  /**
   *  @defgroup iterator_tags Iterator Tags
   *  These are empty types, used to distinguish different iterators.  The
   *  distinction is not made by what they contain, but simply by what they
   *  are.  Different underlying algorithms can then be used based on the
   *  different operations supported by different iterator types.
  */
  ///@{
  ///  Marking input iterators.
  struct input_iterator_tag { };

  ///  Marking output iterators.
  struct output_iterator_tag { };

  /// Forward iterators support a superset of input iterator operations.
  struct forward_iterator_tag : public input_iterator_tag { };

  /// Bidirectional iterators support a superset of forward iterator
  /// operations.
  struct bidirectional_iterator_tag : public forward_iterator_tag { };

  /// Random-access iterators support a superset of bidirectional
  /// iterator operations.
  struct random_access_iterator_tag : public bidirectional_iterator_tag { };
  • 如果设计advance函数(用来将某个迭代器移动某个给定距离),就需要知道是什么类型迭代器。而traits允许在编译器取得某些类型信息,Traits不是c++关键字或一个定义好的构建,是一种技术,是c++程序员共同遵守的协议,设计如下:
template<typename IterT, typename DistT>
void MyAdvance(IterT &iter, DistT d) {
    if (typeid(typename std::iterator_traits<IterT>::iterator_category) ==
        typeid(std::random_access_iterator_tag)) {
        iter += d;
    } else {
        if (d > 0) { while (d--) ++iter; }
        else { while (d++) --iter; }
    }
}
  • 但是上面存在编译问题,类型可以在编译器确定,但是if else是在运行期确定,为什么要把编译器可以确定的事延到运行期呢?使用重载可以解决,如下:
template<typename IterT, typename DistT>
void MyAdvance(IterT &iter, DistT d) {
    if (typeid(typename std::iterator_traits<IterT>::iterator_category) ==
        typeid(std::random_access_iterator_tag)) {
        iter += d;
    } else {
        if (d > 0) { while (d--) ++iter; }
        else { while (d++) --iter; }
    }
}
template<typename IterT, typename DistT>
void DoAdvance(IterT &iter, DistT d, std::random_access_iterator_tag){
    iter += d;
}
template<typename IterT, typename DistT>
void DoAdvance(IterT &iter, DistT d, std::bidirectional_iterator_tag){
    if (d > 0) { while (d--) ++iter; }
    else { while (d++) --iter; }
}
// forward_iterator_tag公有继承自input_iterator_tag,所以该函数也能处理forward_iterator_tag
template<typename IterT, typename DistT>
void DoAdvance(IterT &iter, DistT d, std::input_iterator_tag){
    if (d < 0) {
        throw std::out_of_range("Negative distance");
    }
    while (d--) ++iter;
}
  • 另外,来看c++官方的实现,和书上的代码是差不多的。
  template<typename _InputIterator, typename _Distance>
    inline _GLIBCXX17_CONSTEXPR void
    advance(_InputIterator& __i, _Distance __n)
    {
      // concept requirements -- taken care of in __advance
      typename iterator_traits<_InputIterator>::difference_type __d = __n;
      std::__advance(__i, __d, std::__iterator_category(__i));
    }
  
  // 下面是五种迭代器的重载函数,截取了两种
  template<typename _InputIterator, typename _Distance>
    inline _GLIBCXX14_CONSTEXPR void
    __advance(_InputIterator& __i, _Distance __n, input_iterator_tag)
    {
      // concept requirements
      __glibcxx_function_requires(_InputIteratorConcept<_InputIterator>)
      __glibcxx_assert(__n >= 0);
      while (__n--)
	++__i;
    }

  template<typename _BidirectionalIterator, typename _Distance>
    inline _GLIBCXX14_CONSTEXPR void
    __advance(_BidirectionalIterator& __i, _Distance __n,
	      bidirectional_iterator_tag)
    {
      // concept requirements
      __glibcxx_function_requires(_BidirectionalIteratorConcept<
				  _BidirectionalIterator>)
      if (__n > 0)
        while (__n--)
	  ++__i;
      else
        while (__n++)
	  --__i;
    }

48 认识模板元编程

  • Template meta programming(TMP,模板元编程)是编写基于模板的C++程序并执行于编译期的过程
    • 模板元编程执行于编译期,原来在运行期才能侦查的错误,编译期就可以发现。(缺点是编译时间变长了,编译通过但是运行报错不容易找到bug)
    • 模板元编程高效,较小的可执行文件、较短的运行期、较少的内存需求。
  • 模板元编程的起手程序是在编译期计算阶乘。使用模板具现化每个阶乘的struct,并且使用enum hack(是模板元编程的基础技术)声明模板元编程的变量
template<unsigned n>
struct Factorial {
    enum {
        value = n * Factorial<n - 1>::value
    };
};
template<>
struct Factorial<0> {
    enum {
        value = 1
    };
};
int main() {
    cout << Factorial<5>::value << endl;    // 120
    cout << Factorial<10>::value << endl;   // 3628800
    return 0;
}
  • 模板元编程的内容很多很深,书上并没有多做深入,但是对于程序库的开发人员,模板元编程还是很重要的

八 定制new和delete

主要是C++内存管理,在《STL源码剖析》中的空间配置器(allocator)篇章也着重讲了内存管理的内容,回头发现这一章还是很重要的。

  • 两个主角:分配例程归还例程(allocation and deallocation routines),即operator newoperator delete

  • 配角:new-handler

  • 多线程环境下的内存管理很重要。由于heap是一个可被改动的全局性资源,如果没有适当的同步控制,一旦使用无锁算法或精心防止并发访问,调用内存例程可能很容易导致管理heap的数据结构内容败坏。

  • operator newoperator delete分配单一对象,operator new[]operator delete[]分配一组对象

  • STL容器所使用的的heap内存由容器自带或分配的allocator来管理,而不是new和delete直接管理,这也是《STL源码剖析》第二章的主要分析内容。

  • 区分newoperator newplacement new,delete亦是。

    • new = operator new + 构造函数
    • delete = 析构函数 + operator delete
    • placement new是operator new的重载版本,解决内存分配慢的问题

49 了解new-handler的行为

  • 当operator new无法满足某一内存分配需求时,会先调用客户端指定的错误处理函数(new-handler),再抛出异常。所以要调用set_new_handler函数传入函数指针,设置好错误处理函数(new-handler)
// 标准库中的定义
namespace std 
{
  // 函数指针
  typedef void (*new_handler)();
  // 设置错误处理函数(new-handler)
  new_handler set_new_handler(new_handler) throw();
}

// 举例
void OutOfMem(){
  std::cerr << "Unable to satisfy request for memory\n";
  std::abort();
}
int main{
  // 两者都可以
  // 1.使用空值,也会抛出异常
  // terminate called after throwing an instance of 'std::bad_alloc'
  // what():  std::bad_alloc
  std::set_new_handler(nullptr);
  
  // 2.使用自己定义的
  // Unable to satisfy request for memory
  std::set_new_handler(OutOfMem);
  int* p_big_data_array = new int[100000000000L];
  return 0}

50 了解new和delete的合理替换时机

替换编译器提供的operator new和operator delete的理由如下:

  • 用来检测运用上的错误:如new的内存delete时不幸失败会导致内存泄漏,new的内存多次delete会导致不确定行为等
  • 为了强化效能:大块内存、小块内存、大小混合型内存的需求。存在破碎问题(总量足够但分散为小区块的自由内存),这时候无法满足大区块内存要求
  • 为了收集使用上的统计数据

51 编写new和delete时需固守常规

52 写了placement new也要写placement delete

九 杂项讨论

53 不要轻忽编译器的警告

  • 废话

54 让自己熟悉包括在TR1在内的标准程序库

  • 也就是现在C++11的一些新特性,智能指针、STL、lambda等

55 让自己熟悉Boost

  • C++标准程序库的测试场或后花园,好的程序经过审核评估后会加入到C++标准程序库中
  • 功能广泛,值得学习和使用
  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-07-05 23:22:06  更:2022-07-05 23:24:21 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/12 5:57:22-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码