专栏首页编程学习基地如何设计一个C++的类?

如何设计一个C++的类?

什么是类? 我理解类是现实世界的描述,是对业务的抽象,类设计的好不好多半取决于你抽象的巧不巧。

类的设计最重要的一点是要表示来自某个领域的概念,拿我最近在做的音视频剪辑来举例,剪辑业务中有轨道的概念,也有片段的概念,每个轨道可包含多个片段,这时候就有些问题需要考虑,在现实世界中,轨道可以复制吗?片段可以复制吗?轨道可以移动吗?片段可以移动吗?

然后我们就可以进一步将现实世界中的轨道和片段抽象成类了,可分为两个类,一个轨道类,一个片段类,两个类是否需要提供拷贝构造函数和移动构造函数,完全取决于它们在现实世界的样子。

tips:类的名字应该明确告诉用户这个类的用途。

类需要自己写构造函数和析构函数吗?

反正我每次定义一个类的时候都会明确把构造函数和析构函数写出来,即便它是空实现,即便我不写编译器也会视情况默认生成一个,自动生成的称为默认构造函数。但我不想依赖编译器,也建议大家不要过度依赖编译器,明确写出来构造函数和析构函数也是一个好习惯,多数情况下类没有那么简单,多数情况下编译器默认生成的构造函数和析构函数不一定是我们想要的。默认的构造函数不会给我们的数据成员初始化,所以需要自己写一个构造函数,其实在构造函数里的语句也不能称之为初始化,那是个赋值操作,真正的初始化可以通过初始化列表方式或者声明成员时直接给初值,类似下面的代码。如果我们的类有指针数据成员,我们在某个地方为其分配了一块内存,编译器自动生成的析构函数默认是不会将这块内存释放掉的,为了规避这潜在的风险,还是自己写一个吧!

tips:编译器在某些情况下会生成移动构造函数或移动赋值运算符,但记住这些情况太麻烦了,建议手动控制,明确要的时候就自己写一个,明确不要的时候就delete掉。

class A {
public:
    A() : a_(2) {}// 一种初始化,标准初始化形式
    ~A() {}
private:
    int a_;
    int b_ = 3; // 另一种初始化
};

类需要手动声明默认构造函数吗? 什么是默认构造函数?看下百度百科的定义:

默认构造函数(default constructor)就是在没有显式提供初始化式时调用的构造函数。它由不带参数的构造函数,或者为所有的形参提供默认实参的构造函数定义。如果定义某个类的变量时没有提供初始化时就会使用默认构造函数。

这和上一个问题类似,首先需要了解什么时候需要默认构造函数,看下面这段代码。当已经为一个类提供了带有参数的构造函数,编译器不会为该类再默认的生成构造函数,如果此时在其它地方以无参形式构造了该类的一个对象,编译器就会报错,找不到对应的构造函数,那怎么解决?一种方法是为类设置一个无参的默认构造函数(像下面代码这样),另一种方法是自己提供一个对应的构造函数。我倾向于后一种方式,前一种方式只能解决编译上的问题,但还有可能存在潜在的bug。

class A {
    A(int a) {}
    A() = default;  
};

数据成员是设置private还是public还是protected?

三种访问权限就不过多介绍了,说说我平时是怎么设置数据成员权限的吧!对于普通成员变量,我全是private,除非该类作为基类,而子类也需要访问父类的私有成员,这时候我会将父类的private改为protected。什么时候用public呢?一般情况下只会对某些静态常量我会考虑使用public修饰,前提是外部有访问此常量的需求。

class A {
  public:
    constexpr static int kConstValue = 2;
  private:
    int a_;  
};

类需要虚析构函数吗? 这个很明确,如果类会作为基类被派生时,该基类的析构函数就一定要声明为虚函数,如果某个类确定不会被派生,那就不要声明其析构函数为虚函数。

类需要提供拷贝构造函数吗?

这里需要考虑清楚,需要明确究竟是否提供,这需要结合这个类在现实生活中的实际意义,类是某个领域某个业务某个实物的抽象,假设有一个试卷类,因为试卷可以拷贝,那就明确提供拷贝构造函数,假设有一个Person类,因为不允许克隆人,那就明确禁用拷贝构造函数。这里也可以参考智能指针中的unique_ptr,该智能指针就明确禁用了拷贝操作。

类需要提供移动构造函数吗? 移动构造是C++11引入的新特性,这里涉及到左值右值等概念,具体可以看我这篇文章:《c++11新特性,所有知识点都在这了!

一个类具有移动构造函数才具备移动语义,如果追求资源管理的效率,move资源效率一般会比拷贝一个资源高一些。

这里重点讨论是否需要提供移动构造函数,答案还是,要想清楚,要结合实际情况,假设我们定义了一个美国总统的类,可以提供移动构造函数,因为美国总统几年就会换一个,再假设我们定义了一个美国最傻吊总统的类,那就应该禁用移动构造函数,因为只有懂王一个,永远不可移动。

排坑:赋值运算符需要考虑是否能正确的防止自身给自身赋值?

class A {
   public:
    A();
    A(const A& rhs);
    A& operator=(const A& rhs) {
        if (this == &rhs) return *this; // 必须的
        delete m_ptr;
        m_ptr = new int[5];
        memcpy(m_ptr, rhs.m_ptr, 5); 
       return *this;
    }
   private:
    int* m_ptr;
};

成员函数什么时候使用const修饰? 这里需要知道成员函数使用const修饰代表什么意思,代表在此函数内不能修改类的数据成员,如果在const修饰的成员函数内修改了成员变量,那编译器会编译失败。其实不标const也不会有任何问题,但是如果我们期望某个函数内不会修改任何成员变量时,应该把该成员函数标记为const,这样可以防止自己或者其它程序员误操作,当误更改了某些成员变量时,编译器会报错。

如果你期望在某个成员函数内不更改成员函数,而又没有标记为const,这时自己或者其他人在此函数内改动了某些成员变量,编译器对此没有任何提示,这就有可能产生潜在的bug。

tips:const对象上只能调用const成员函数,非const对象上既可以调用非const成员函数,也可以调用const成员函数。

什么时候需要加noexcept? 如果确认某个函数不会抛出异常,那就标记为noexcept,这样编译器可以对函数做进一步优化(具体做了什么优化,我也不知道),提供程序运行效率,总之,尽量把能标记为noexcept的都标记为noexcept。

函数传参问题? 函数传参无非就是传值还是传引用的选择问题: 参数需要在函数内修改,并在函数外使用修改后的值时:传引用

参数需要在函数内修改,但在函数外使用修改前的值时:传值

参数在函数内不会修改,参数类型如果为基础类型(int等):传值

参数在函数内不会更改,参数类型如果为class类型:传const引用

类的声明和实现要分开写到不同文件中吗? 一般来说类的声明会写到头文件,类的定义会写到源文件中,但也有很多人会把定义写到头文件中,我还见过有人#include "xxx.cpp"呢,这里建议,不想让函数内联,那就把定义写到源文件中。如果非内联函数在头文件中定义,多个源文件都引用此头文件时编译器就会报错。至于类的声明写到头文件还是源文件中,视情况而定,看下面这段代码,某些类的声明写到了头文件中,又有些类的声明写到了源文件中!

// a.h
class AImpl;
class A {
    public:
        A();
        ~A();
        void func();
    private:
        AImpl *impl_;
};

源文件如下:

// a.cc
class AImpl {
    public:
        void func() {
            std::cout << "real func \n";
        }
};

A::A() {
    impl_ = new AImpl;
}
A::~A() {
    delete impl_;
}
void A::func() {
    _impl->func();
}

是否需要异常处理? 关于异常处理详细的介绍可以看我这篇文章:《你的c++团队还在禁用异常处理吗?

这里抛砖引玉下,如果是服务端编程,建议使用异常处理替代错误码的错误处理方式,关于异常处理有两个常见问题:

  • 构造函数可以使用异常吗
  • 析构函数可以使用异常吗?

结论是构造函数在处理错误时可以使用异常,而且建议使用异常,析构函数中也可以使用异常,但不要让异常从析构函数中逃离,有异常要在析构函数中捕获处理掉。

tips:异常处理方式尽量方便好用,但是它会使得程序体积增大10%-20%左右,如果对程序体积敏感的环境,我能想到的主要是嵌入式或者移动端编程环境,需要谨慎考虑下。

是否需要标记为inline? inline的优点是可以减少函数调用的开销,inline的缺点是容易导致代码段体积变大,如果某个函数体非常短,比如两三行代码而且会被频繁调用,可以考虑标记为inline,如果太长的且不追求极致性能的情况下,就没必要标记为inline。

tips:inline关键字只是开发者给编译器的请求,建议编译器做内联处理,编译器具体做不做内联还得看它心情。

final override virtual关键字的使用

  • 如果确定某个类永远不会被其他类继承,那就就明确将该类标记为final,这可防止其他人继承!
  • 如果子类想要重写基类某个虚函数时,可以将此函数标记为override,那该函数必须重写父类虚函数,否则编译器报错。
  • 标明某个函数是虚函数,有子类继承时可以改写此函数的行为。

tips:注意构造函数和析构函数中不要调用虚函数

类内考虑使用智能指针

直接看代码:

class A {
    public:
        A() {
            a_ = new int;
        }
        ~A() {
            delete a_;
        }
     private:
        int* a_;
};

可以考虑改为:

class A {
    public:
        A() {
            a_ = std::make_unique<int>();
        }
        ~A() {}
     private:
        int* a_;
};

使用智能指针来管理类内的内存更方便且更安全。

什么时候使用explict避免隐式转换? explict多数情况下用于修饰只有一个参数的类构造函数,表示拒绝隐式类型转换。那什么时候使用explict关键字呢,还是看情况。

比如vector的单参数构造函数就是explict,而string则不是explict。因为vector接收的单参数类型时int类型,表示vector的容量,如果希望int型隐式自动转换成vector,那这个int是表示容量还是表示vector中的内容呢,有点牵强,所以vector中的单参数构造函数是explict的。而string接收的单参数是const char*类型,一个const char*隐式转换string很正常,也很符合逻辑,所以不需要标记为explict。

函数参数个数多少合适? 个人习惯最多四个,超过四个我一般就会封装到一个结构体作为参数传递。

类设计原则: 这里我没有学术式的列出面向对象的几大原则,而是把我认为重要的点都列在了这里:

  1. 接口一致原则:行为与名字相匹配
  2. 误操作防御原则:边界处理,能加const就加const,能用智能指针就用智能指针
  3. 依赖倒置原则:针对接口编程,依赖于抽象而不依赖于具体,抽象(稳定)不应依赖于实现细节(变化),实现细节应该依赖于抽象,因为稳定态如果依赖于变化态则会变成不稳定态。
  4. 开放封闭原则:对扩展开放,对修改关闭,业务需求是不断变化的,当程序需要扩展的时候,不要去修改原来的代码,而要灵活使用抽象和继承,增加程序的扩展性,使易于维护和升级,类、模块、函数等都是可以扩展的,但是不可修改。
  5. 单一职责原则:一个类只做一件事,一个类应该仅有一个引起它变化的原因,并且变化的方向隐含着类的责任。
  6. 里氏替换原则:子类必须能够替换父类,任何引用基类的地方必须能透明的使用其子类的对象,开放关闭原则的具体实现手段之一。
  7. 接口隔离原则:接口最小化且完备,尽量少public来减少对外交互,只把外部需要的方法暴露出来。
  8. 最少知道原则:一个实体应该尽可能少的与其他实体发生相互作用。
  9. 将变化的点进行封装,做好分界,保持一侧变化,一侧稳定,调用侧永远稳定,被调用测内部可以变化。
  10. 优先使用组合而非继承,继承为白箱操作,而组合为黑箱,继承某种程度上破坏了封装性,而且父类与子类之间耦合度比较高。
  11. 针对接口编程,而非针对实现编程,强调接口标准化。

根据实际情况选择遵循某些原则,完善程序。

tips:对于设计模式而言,不能一步到位,刚开始编程时不要把太多精力放到设计模式上,需求总是变化的,刚开始着重于实现,一般敏捷开发后为了应对变化重构再决定采取合适的设计模式。

注意事项

  • 不要引用没有必要的头文件!
  • 暴露给用户的头文件要想清楚该暴露什么,不该暴露什么,外部头文件不要引用内部头文件
  • 类成员变量确保作保初始化工作
  • 不要让异常逃离析构函数
  • 构造函数或析构函数不要调用虚函数
  • 不要返回函数局部对象的指针或引用
  • 尽量不要返回函数内部堆对象的指针或引用,容易产生内存泄漏,尽量遵循谁申请谁释放的原则

·················END·················

本文分享自微信公众号 - 编程学习基地(LearnBase)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2021-06-15

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 如何设计一个IVR?

    IVR(交互式语音响应)的全称是Interactive Voice Response,它是一种自动电话系统,也就是我们常说的电话语音菜单。FreeSWITCH支...

    用户1506126
  • 如何设计一个经营策略类游戏

    玩家需要在 7 天之内尽量多的搜集食物。而搜集食物的方法有两种,一种是打猎,每天可以获取 2 个食物。另外一种是种田,每天可以增加 1 块田,而每块田都能在将来...

    韩伟
  • 如何设计一个架构

    架构不是像平常写代码一样,对就是对,错就是错,它并无对错之分,是一个取舍的过程。当我们从0开始做架构的时候,的确是比较困难。虽然万事开头难,但是一个好的开始相当...

    哲洛不闹
  • 如何设计一个 A/B test?

    作者:刘健阁(Jiange Liu),PCG 数据分析师 实验设计 AB Test 实验一般有 2 个目的: 判断哪个更好:例如,有 2 个 UI 设计,究竟...

    腾讯大讲堂
  • C++11:如何判断一个类是另一个模板类的子类?

    版权声明:本文为博主原创文章,转载请注明源地址。 https://blog.csdn.net...

    用户1148648
  • 如何设计一个 RPC 系统

    用户1263954
  • 如何设计一个 RPC 系统

    RPC 是一种方便的网络通信编程模型,由于和编程语言的高度结合,大大减少了处理网络数据的复杂度,让代码可读性也有可观的提高。本文就是通过分析几种流行的 RPC ...

    韩伟
  • 如何设计一个 RPC 框架

    尽管不同的分布式服务框架实现细节存在差异,但是核心功能差异不大,下面的架构图描绘了一个分布式服务框架的整体逻辑架构。

    芋道源码
  • 如何设计一个API签名

    大部分情况下,我们使用已有的API签名方案(如腾讯云API签名、阿里云APi签名、亚马逊API签名等等)即可,无需从零开始设计一个API签名方案...

    林喜东
  • 系统设计面试:如何设计一个 Pastebin

    今天分享一下如何设计一个类 Pastebin 的 web 服务,用户可以存储纯文本,然后获得一个随机生成的 URL,其他人可以通过这个 URL 来访问文本内容,...

    somenzz
  • 【系统设计】如何设计一个CMS系统

    在 cms 中,可以创建一个“内容集”(类似于数据表),并且可以修改内容集的字段信息。

    心谭博客
  • 如何设计一个“高大上”的 logo

    来源:CODING-Summer 第一步:搞清楚需求——我们需要一个什么样的 logo 工具:交流(如果这也算的话) 对于整个 Coding 的 logo,老大...

    编程范 源代码公司
  • 如何设计一个良好的接口

    在设计接口时,有很多因素要考虑,如接口的业务定位,接口的安全性,接口的可扩展性、接口的稳定性、接口的跨域性、接口的协议规则、接口的路径规则、接口单一原则、接口过...

    用户1093975
  • 如何设计一个良好的接口

    在设计接口时,有很多因素要考虑,如接口的业务定位,接口的安全性,接口的可扩展性、接口的稳定性、接口的跨域性、接口的协议规则、接口的路径规则、接口单一原则、接口过...

    好好学java
  • 如何设计一个电子计算器

      首先,不要误解,我这里的计算器是指硬件的计算器,至于纯软件的计算程序,乃至有高级功能的,比如可以求解方程甚至可编程之类,我以后找个时间来说说。这两天看到有人...

    窗户
  • 如何设计一款工具类产品

    工具型产品是从用户某个特定使用场景出发,解决用户需求提升效率的产品,比如天气、查询、浏览器、计算器、输入法、翻译、软件优化等等。

    宇相
  • 如何设计一个秒杀系统

    最近在部门内部分享了原来在电商业务做秒杀活动的整体思路,大家对这次分享反馈还不错,所以我就简单整理了一下,分享给大家参考参考。

    Bug开发工程师
  • 如何设计一个本地缓存

    最近在看 Mybatis 的源码,刚好看到缓存这一块,Mybatis 提供了一级缓存和二级缓存;一级缓存相对来说比较简单,功能比较齐全的是二级缓存,基本上满足了...

    java思维导图
  • 如何设计一个秒杀系统

    秒杀已经成为电商不可缺少的一步分了,所谓 买到就是赚到,可以成功吸引到一大堆用户,那程序员面对这些用户该怎么办呢。我们该如何设计秒杀呢?

    憧憬博客

扫码关注云+社区

领取腾讯云代金券