事先声明,本文只代表程序喵个人观点,文中肯定会有部分或大多数观点和大家的想法不一致,大家可以在评论区交流!

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

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

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

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

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

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

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


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

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

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

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

一个类具有移动构造函数才具备移动语义,如果追求资源管理的效率,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();
}

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

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

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

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

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

注意事项

  • 不要引用没有必要的头文件!

  • 暴露给用户的头文件要想清楚该暴露什么,不该暴露什么,外部头文件不要引用内部头文件

  • 类成员变量确保作保初始化工作

  • 不要让异常逃离析构函数

  • 构造函数或析构函数不要调用虚函数

  • 不要返回函数局部对象的指针或引用

  • 尽量不要返回函数内部堆对象的指针或引用,容易产生内存泄漏,尽量遵循谁申请谁释放的原则


 今天的分享就到这里了,大家要好好学C语言/C++哟~
对于准备学习C/C++编程的小伙伴,如果你想更好的提升你的编程核心能力(内功)不妨从现在开始!

C语言C++编程学习交流圈子,企鹅群:【点击进入】
整理分享(多年学习的源码、项目实战视频、项目笔记,基础入门教程)

欢迎转行和学习编程的伙伴,利用更多的资料学习成长比自己琢磨更快哦!

如何设计一个C++的类?相关推荐

  1. 为什么写了value属性 jq赋值value值不显示_为什么 String 要设计成 final,又如何设计一个不可变类呢?...

    前面聊了聊面试必考 String 的坑,具体可以细看<你真的懂 Java 的 String 吗?>,也留下了一个疑问,为什么 String 要被设计成 final 呢?其实,如果你读的认真 ...

  2. 设计一个长方形的类,成员的变量有长与宽,成员函数要求周长与面积,然后进行测试。

    设计一个长方形的类,成员的变量有长与宽,成员函数要求周长与面积,然后进行测试. #include using namespace std; class cfx { private: float len ...

  3. c++_设计一个 Studnet(学生)类

    设计一个 Studnet(学生)类 1.基本信息:学号.姓名.性别.出生日期.年级.班级.院系.专业: 其中:基本信息为 private 属性,成员函数为 public 属性: 2.Student 类 ...

  4. 设计一个圆形的类即Circle类。

    题目内容: 设计一个圆形的类即Circle类,具体要求如下: (1) 创建一个圆形的类,即Circle类,添加radius一个成员变量(数据类型为double ),表示圆形的半径: (2) 定义一个有 ...

  5. 设计一个抽象类图形类,在该类中包含有至少两个抽象方法求周长和求面积,分别定义圆形类、长方形类、正方形类、三角形类来继承图形类,并实现上述两个方法

    设计一个抽象类图形类,在该类中包含有至少两个抽象方法求周长和求面积,分别定义圆形类.长方形类.正方形类.三角形类来继承图形类,并实现上述两个方法 设计抽象类 就要使用abstract关键字,抽象类中的 ...

  6. 7-3 三维向量运算设计一个三维向量类,实现向量加法、减法以及向量与标量的乘法和除法运算。

    7-3 三维向量运算 设计一个三维向量类,实现向量加法.减法以及向量与标量的乘法和除法运算.后面添加下面代码完成: 天杀的出题人,非得放个图片在这,放个代码块会死吗? 运行的时候,要把这张图片里的内容 ...

  7. 1.设计一个长方形的类,成员变量有长与宽,成员函数有求周长与面积,然后进行测试。要求有构造函数、析造函数和复制构造函数。

    1.设计一个长方形的类,成员变量有长与宽,成员函数有求周长与面积,然后进行测试.要求有构造函数.析造函数和复制构造函数. 代码比较简单,论坛有坛友问,顺手写一下当复习 #include <bit ...

  8. 信息系统开发(JAVA)设计一个银行账户类

    实验要求: 设计一个银行账户类,其中包括: • 账户信息,如帐号.姓名.开户时间.身份证号码等. • 存款方法. • 取款方法. • 其他方法如"查询余额"和"显示账户信 ...

  9. python定义一个圆类_python设计一个Circle(圆)类,包括圆心位置`,半径,颜色等属性。编写构造方法和其他方法,计算周长和面积。...

    ** Python:设计一个Circle(圆)类,包括圆心位置`,半径,颜色等属性.编写构造方法和其他方法,计算周长和面积.请编写程序验证类的功能. class Circle: def -init-( ...

  10. 使用策略模式设计一个动作冒险类游戏

    使用策略模式设计一个动作冒险类游戏 背景 ClassDiagram图 部分代码展示 Client运行展示 背景 动作冒险游戏: 游戏中有一系列角色(Character),包括国王(King).皇后(Q ...

最新文章

  1. python使用open打开文件时显示文件不存在-Python打开文件open()的注意事项
  2. 机器人学习--路径规划--A*算法实现
  3. MySQL / 索引模型
  4. php win2003 下载,64位windows2003iis安装包|《win2003 iis安装包》64位完整版附安装PHP教材...
  5. boost::rational用法的测试程序
  6. 深夜,先给自己记录个东西
  7. git clone remote: HTTP Basic: Access denied
  8. 查看安卓APK源码破解
  9. kafka java 生产消费程序demo示例
  10. 影响程序运行速度的几个因素
  11. win10配置python_win10中的Python安装与环境配置
  12. 网站域名后缀index.html的去除方法
  13. origin数据平滑_科学网-关于origin曲线平滑处理 lowess-叶小球的博文
  14. STM32F4 GPIO模式及工作原理详解
  15. 计算机保研面试英文,计算机保研面试英文自我介绍范文
  16. left join一对多只保留一条结果的解决方法
  17. UE5 建模(一)Shapes
  18. 空间句法(二)——Axwoman 6.0
  19. 《认文识字•简述》【小结】
  20. [转][汇编] 汇编语言实现简易文本编辑器(光标移动、上卷和退格删除)

热门文章

  1. 浓淡相宜间,是灵魂的默契;
  2. SQL语句常见面试题(一)
  3. 足球大师服务器维护,球员能力提升终极密法《足球大师》详细突破攻略
  4. Redis 线程模型
  5. 最漂亮的人是为梦想而努力的人
  6. flask框架----跨域
  7. 二十:让行内元素在div中垂直居中
  8. 关于tv app的一些想法
  9. 文件夹成变成.exe 解决办法
  10. android开发面试题!微信小程序趋势及前景,社招面试心得