其他
如何设计一个 C++ 的类?
The following article is from 程序喵大人 Author 程序喵大人
什么是类?
class A {
public:
A() : a_(2) {}// 一种初始化,标准初始化形式
~A() {}
private:
int a_;
int b_ = 3; // 另一种初始化
};
什么是默认构造函数?看下百度百科的定义:
class A {
A(int a) {}
A() = default;
};
class A {
public:
constexpr static int kConstValue = 2;
private:
int a_;
};
这个很明确,如果类会作为基类被派生时,该基类的析构函数就一定要声明为虚函数,如果某个类确定不会被派生,那就不要声明其析构函数为虚函数。
移动构造是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。
函数传参问题?
函数传参无非就是传值还是传引用的选择问题:
参数需要在函数内修改,并在函数外使用修改后的值时:传引用
类的声明和实现要分开写到不同文件中吗?
一般来说类的声明会写到头文件,类的定义会写到源文件中,但也有很多人会把定义写到头文件中,我还见过有人#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();
}
这里抛砖引玉下,如果是服务端编程,建议使用异常处理替代错误码的错误处理方式,关于异常处理有两个常见问题:
构造函数可以使用异常吗
析构函数可以使用异常吗?
结论是构造函数在处理错误时可以使用异常,而且建议使用异常,析构函数中也可以使用异常,但不要让异常从析构函数中逃离,有异常要在析构函数中捕获处理掉。
是否需要标记为inline?
inline的优点是可以减少函数调用的开销,inline的缺点是容易导致代码段体积变大,如果某个函数体非常短,比如两三行代码而且会被频繁调用,可以考虑标记为inline,如果太长的且不追求极致性能的情况下,就没必要标记为inline。
tips:inline关键字只是开发者给编译器的请求,建议编译器做内联处理,编译器具体做不做内联还得看它心情。
如果确定某个类永远不会被其他类继承,那就就明确将该类标记为final,这可防止其他人继承!
如果子类想要重写基类某个虚函数时,可以将此函数标记为override,那该函数必须重写父类虚函数,否则编译器报错。
标明某个函数是虚函数,有子类继承时可以改写此函数的行为。
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。
函数参数个数多少合适?
个人习惯最多四个,超过四个我一般就会封装到一个结构体作为参数传递。
这里我没有学术式的列出面向对象的几大原则,而是把我认为重要的点都列在了这里:
接口一致原则:行为与名字相匹配 误操作防御原则:边界处理,能加const就加const,能用智能指针就用智能指针 依赖倒置原则:针对接口编程,依赖于抽象而不依赖于具体,抽象(稳定)不应依赖于实现细节(变化),实现细节应该依赖于抽象,因为稳定态如果依赖于变化态则会变成不稳定态。 开放封闭原则:对扩展开放,对修改关闭,业务需求是不断变化的,当程序需要扩展的时候,不要去修改原来的代码,而要灵活使用抽象和继承,增加程序的扩展性,使易于维护和升级,类、模块、函数等都是可以扩展的,但是不可修改。 单一职责原则:一个类只做一件事,一个类应该仅有一个引起它变化的原因,并且变化的方向隐含着类的责任。 里氏替换原则:子类必须能够替换父类,任何引用基类的地方必须能透明的使用其子类的对象,开放关闭原则的具体实现手段之一。 接口隔离原则:接口最小化且完备,尽量少public来减少对外交互,只把外部需要的方法暴露出来。 最少知道原则:一个实体应该尽可能少的与其他实体发生相互作用。 将变化的点进行封装,做好分界,保持一侧变化,一侧稳定,调用侧永远稳定,被调用测内部可以变化。 优先使用组合而非继承,继承为白箱操作,而组合为黑箱,继承某种程度上破坏了封装性,而且父类与子类之间耦合度比较高。 针对接口编程,而非针对实现编程,强调接口标准化。
⚡根据实际情况选择遵循某些原则,完善程序。
注意事项
不要引用没有必要的头文件! 暴露给用户的头文件要想清楚该暴露什么,不该暴露什么,外部头文件不要引用内部头文件
类成员变量确保作保初始化工作
不要让异常逃离析构函数
构造函数或析构函数不要调用虚函数
不要返回函数局部对象的指针或引用
尽量不要返回函数内部堆对象的指针或引用,容易产生内存泄漏,尽量遵循谁申请谁释放的原则
参考资料
http://coder.amazingdemo.top/post/cpp_%E8%AE%BE%E8%AE%A1%E9%AB%98%E6%95%88%E7%9A%84%E7%B1%BB/
- EOF -
关注「程序员的那些事」加星标,不错过圈内事
点赞和在看就是最大的支持❤️