C++编程规范 4 类
来源:互联网 发布:阿里云 代码托管 编辑:程序博客网 时间:2024/06/05 06:01
4 类
4.1 类的设计
类是面向对象设计的基础,一个好的类应该职责单一,接口清晰、少而完备,类间低耦合、类内高内
聚,并且很好地展现封装、继承、多态、模块化等特性。
原则4.1 类职责单一
说明:类应该职责单一。如果一个类的职责过多,往往难以设计、实现、使用、维护。
随着功能的扩展,类的职责范围自然也扩大,但职责不应该发散。
用小类代替巨类。小类更易于编写,测试,使用和维护。用小类体现简单设计的概念;巨类会削弱封
装性,巨类往往承担过多职责,试图提供“完整”的解决方案,但往往难以真正成功。
如果一个类有10个以上数据成员,类的职责可能过多。
原则4.2 隐藏信息
说明:封装是面向对象设计和编程的核心概念之一。隐藏实现的内部数据,减少调用者代码与具体实
现代码之间的依赖。
尽量减少全局和共享数据;
禁止成员函数返回成员可写的引用或者指针;
将数据成员设为私有的(struct除外),并提供相关存取函数;
避免为每个类数据成员提供访问函数;
运行时多态,将内部实现(派生类提供)与对外接口(基类提供)分离。
原则4.3 尽量使类的接口正交、少而完备
说明:应该围绕一个核心去定义接口、提供服务、与其他类合作,从而易于实现、理解、使用、测试
和维护。接口函数功能正交,尽量避免一个接口功能覆盖另一个接口功能。接口函数太多,会难以理
解、使用和维护。如果一个类包含20个以上的非私有成员函数,类的接口可能不够精简。
规则4.1 模块间对外接口类不要暴露私有和保护成员
说明:对外接口类暴露受保护或者私有成员则破坏了封装,一旦因为类的设计变更(增加,删除,修改
内部成员)会导致关联组件或系统的代码重新编译,从而增加系统编译时间,也产生了二进制兼容问题,
导致关联升级和打补丁。所以除非必要,不要在接口类中暴露私有和保护成员。
有如下几种做法:
使用纯虚类作为接口类,用实现类完成实现,使用者只看到接口类,这种做法缺点是:
代码结构相对复杂。
新增接口必须放在原有接口后面,不能改变原有接口的顺序。否则,因为虚函数表的原因,
会导致客户代码重新编译。
接口类使用PIMPL模式(只有一个指向实现类指针的私有数据成员),所有私有成员都封装在实现类
中(实现类可以不暴露为头文件,直接放在实现文件中)。
代码结构简单,容易理解。
可以节省虚函数开销,但是有间接访问开销。
修改实现不会导致客户代码重新编译。
class Interface
{
public:
void function();
private:
Implementation* impl_;
};
class Implementation
{
public:
int i;
int j;
};
void Interface:: function ()
{
++impl_->i;
}
规则4.2 避免成员函数返回成员可写的引用或者指针
说明:破坏了类的封装性,对象本身不知道的情况下对象的成员被修改。
示例:不好的例子
class Alarm
{
public:
string& getname(){return name;} //破坏类的封装性,成员name被暴露
private:
string name;
};
例外:某些情况下确实需要返回可写引用或者指针的,例如单件模式的一种写法:
Type& Type::Instance()
{
static Type instance;
return instance;
}
规则4.3 禁止类之间循环依赖
说明:循环依赖会导致系统耦合度大大增加,所以类之间禁止循环依赖。类A依赖类B,类B依赖类A。
出现这种情况需要对类设计进行调整,引入类C:
升级:将关联业务提到类C,使类C依赖类A和类B,来消除循环依赖
降级:将关联业务提到类C,使类A和类B都依赖类C,来消除循环依赖。
示例:类Rectangle和类Window互相依赖
class Rectangle
{
public:
Rectangle(int x1, int y1, int x2, int y2);
Rectangle(const Window& w);
};
class Window
{
public:
Window(int xCenter, int yCenter, int width, int height);
Window(const Rectangle& r);
};
可以增加类BoxUtil做为转换,不用产生相互依赖
class BoxUtil
{
public:
static Rectangle toRectangle(const Window& w);
static Window toWindow(const Rectangle& r);
};
建议4.1 将数据成员设为私有的(struct除外),并提供相关存取函数
说明:信息隐藏是良好设计的关键,应该将所有数据成员设为私有,精确的控制成员变量的读写,对
外屏蔽内部实现。否则意味类的部分状态可能无法控制、无法预测,原因是:
非private成员破坏了类的封装性,导致类本身不知道其数据成员何时被修改;
任何对类的修改都会延伸影响到使用该类的代码。
将数据成员私有化,必要时提供相关存取函数,如定义变量foo_及取值函数foo()、赋值操作符
set_foo()。 存取函数一般内联在头文件中定义成内联函数。如果外部没有需求,私有数据成员可以
不提供存取函数,以达到隐藏和保护的目的。不要通过存取函数来访问私有数据成员的地址(见规则
4.2)。
建议4.2 使用PIMPL模式,确保私有成员真正不可见
说明:C++将私有成员成员指定为不可访问,但还是可见的,可以通过PIMPL惯用法使私有成员在当前
类的范围中不可见。PIMPL主要是通过前置声明,达到接口与实现的分离的效果,降低编译时间,降低
耦合。
示例:
class Map
{
private:
struct Impl;
shared_ptr<Impl> pimpl_;
};
4.2 构造、赋值和析构
规则4.4 包含成员变量的类,须定义构造函数或者默认构造函数
说明:如果类有成员变量,没有定义构造函数,又没有定义默认构造函数,编译器将自动生成一个构
造函数,但编译器生成的构造函数并不会对成员变量进行初始化,对象状态处于一种不确定性。
例外:如果这个类是从另一个类继承下来,且没有增加成员变量,则不用提供默认构造函数
示例:如下代码没有构造函数,私有数据成员无法初始化:
class CMessage
{
public:
void ProcessOutMsg()
{
//…
}
private:
unsigned int msgid;
unsigned int msglen;
unsigned char *msgbuffer;
};
CMessage msg; //msg成员变量没有初始化
msg.ProcessOutMsg(); //后续使用存在隐患
//因此,有必要定义默认构造函数,如下:
class CMessage
{
public:
CMessage ():
msgid(0),
msglen (0),
msgbuffer (NULL)
{
}
//...
};
规则4.5 为避免隐式转换,将单参数构造函数声明为explicit
说明:单参数构造函数如果没有用explict声明,则会成为隐式转换函数。
示例:
class Foo
{
public:
explicit Foo(const string &name):m_name(name)
{
}
private:
string m_name;
};
ProcessFoo("zhangsan"); //函数调用时,编译器报错,因为显式禁止隐式转换
定义了Foo::Foo(string &name),当形参是Foo对象实参为字符串时,构造函数Foo::Foo(string &name)
被调用并将该字符串转换成一个Foo临时对象传给调用函数,可能导致非预期的隐式转换。解决办法:
在构造函数前加上explicit限制隐式转换。
规则4.6 包含资源管理的类应自定义拷贝构造函数、赋值操作符和析构函数
说明:如果用户不定义,编译器默认会生成拷贝构造函数、赋值操作符和析构函数。自动生成的拷贝
构造函数、赋值操作符只是将所有源对象的成员简单赋值给目的对象,即浅拷贝(shallow copy);自
动生成析构函数是空的。这对于包含资源管理的类来说是不够的:比如从堆中申请的资源,浅拷贝会
使得源对象和目的对象的成员指向同一内存,会导致资源重复释放。空的析构函数不会释放已申请内
存。
如果不需要拷贝构造函数和赋值操作符,可以声明为private属性,让它们失效。
示例:如果结构或对象中包含指针,定义自己的拷贝构造函数和赋值操作符以避免野指针。
class GIDArr
{
public:
GIDArr()
{
iNum = 0;
pGid = NULL;
}
~GIDArr()
{
if (pGid)
{
delete [] pGid;
}
}
private:
int iNum;
char *pGid;
GIDArr(const GIDArr& rhs);
GIDArr& operator = (const GIDArr& rhs) ;
}GIDArr;
规则4.7 让operator=返回*this的引用
说明:符合连续赋值的常见用法和习惯。
示例:
String& String::operator=(const String& rhs)
{
//...
return *this; //返回左边的对象
}
string w, x, y, z;
w = x = y = z = "Hello";
规则4.8 在operator=中检查给自己赋值的情况
说明:自己给自己赋值和普通赋值有很多不同,若不防范会出问题。
示例:
class String
{
public:
String(const char *value);
~String();
String& operator=(const String& rhs);
private:
char *data;
};
//自赋值,合法
String a;
a=a;
//不好的例子:忽略了给自己赋值的情况,导致访问野指针
String& String::operator=(const String& rhs)
{
delete [] data; //删除data
//分配新内存,将rhs的值拷贝给它
data = new char[strlen(rhs.data) + 1]; //rhs.data已经删除,变成野指针
strcpy(data, rhs.data);
return *this;
}
//好的例子:检查给自己赋值的情况
String& String::operator=(const String& rhs)
{
if(this != &rhs)
{
delete [] data;
data = new char[strlen(rhs.data) + 1];
strcpy(data, rhs.data);
}
return *this;
}
规则4.9 在拷贝构造函数、赋值操作符中对所有数据成员赋值
说明:确保构造函数、赋值操作符的对象完整性,避免初始化不完全。
规则4.10 通过基类指针来执行删除操作时,基类的析构函数设为公有且虚拟的
说明:只有基类析构函数是虚拟的,才能保证派生类的析构函数被调用。
示例:基类定义中无虚析构函数导致的内存泄漏。
//如下平台定义了基类A,完成获取版本号的功能。
class A
{
public:
virtual std::string getVersion()=0;
};
//产品派生类B,实现其具体功能,其定义如下:
class B:public A
{
public:
B()
{
cout<<"B()"<<endl;
m_int = new int [100];
}
~B()
{
cout<<"~B()"<<endl;
delete [] m_int;
}
std::string getVersion(){ return std::string("hello!");}
private:
int *m_int;
};
//模拟该接口的调用代码如下:
int main(int argc, char* args[])
{
A *p = new B();
delete p;
return 0;
}
派生类B虽然在析构函数中进行了资源清理,但不幸的是该派生类析构函数永远不会被调用。由于基类
A没有定义析构函数,更没有定义虚析构函数,当对象被销毁时,只会调用系统默认的析构函数,故导
致内存泄漏。
规则4.11 避免在构造函数和析构函数中调用虚函数
说明:在构造函数和析构函数中调用虚函数,会导致未定义的行为。
在C++中,一个基类一次只构造一个完整的对象。
示例:类BaseA是基类,DeriveB是派生类
class BaseA //基类BaseA
{
public:
BaseA();
virtual void log() const=0; //不同的派生类调用不同的日志文件
};
BaseA::BaseA() //基类构造函数
{
log(); //调用虚函数log
}
class DeriveB:public BaseA //派生类
{
public:
virtual void log() const;
};
当执行如下语句:
DeriveB B;
会先执行DeriveB的构造函数,但首先调用BaseA的构造函数,由于BaseA的构造函数调用虚函数log,
此时log还是基类的版本,只有基类构造完成后,才会完成派生类的构造,从而导致未定义的行为。
同样的道理也适用于析构函数。
建议4.3 拷贝构造函数和赋值操作符的参数定义成const引用类型
说明:拷贝构造函数和赋值操作符不可以改变它所引用的对象。
建议4.4 在析构函数中集中释放资源
说明:使用析构函数来集中处理资源清理工作。如果在析构函数之前,资源被释放 (如release函数),
请将资源设置为NULL,以保证析构函数不会重复释放。
4.3 继承
继承是面向对象语言的一个基本特性。理解各种继承的含义: “public继承”意味"是...一个",纯虚
函数只继承接口,一般的虚函数继承接口并提供缺省实现,非虚函数继承接口和实现但不允许修改。
继承的层次过多导致理解困难;多重继承会显著增加代码的复杂性,还会带来潜在的混淆。
原则4.4 用组合代替继承
说明:继承和组合都可以复用和扩展现有的能力。如果组合能表示类的关系,那么优先使用组合。
继承实现比较简单直观,但继承在编译时定义,无法在运行时改变;继承对派生类暴露了基类的实现
细节,使派生类与基类耦合性非常强。一旦基类发生变化,派生类随着变化,而且因为派生类无法修
改基类的非虚函数,导致修改基类会影响到各个派生类。
而组合的灵活性较高,代码耦合小,所以优先考虑组合。
但是并非绝对,往往组合和继承是一起使用的,例如组合的元素是抽象的,通过实现抽象来修改组合
的行为。
继承在一般情况下有两类:实现继承(implementation inheritance)和接口继承(interface
inheritance),尽可能不要使用实现继承而考虑用组合替代。
接口继承:只继承成员函数的接口(也就是声明),例如纯虚(pure virtual)函数;实现继承:继
承成员函数的接口和实现,例如虚函数同时继承接口和缺省实现,又能够覆写它们所继承的实现;非
虚函数继承接口,强制性继承实现。
示例:组合是指一个类型嵌入另一个类型的成员变量,即"有一个" 或 "由...来实现"。例如:
class Address{ ... }; //某人居住之处
class PhoneNumber{ ... }; //某人电话号码
class Person
{
private:
string name; //组合成员变量
Address address; //同上
PhoneNumber voiceNumber; //同上
PhoneNumber faxNumber; //同上
};
原则4.5 避免使用多重继承
说明: 相比单继承,多重实现继承可重用更多代码;但多重继承会显著增加代码的复杂性,程序可维
护性差,且父类转换时容易出错,所以除非必要,不要使用多重实现继承,使用组合来代替。
多重继承中基类都是纯接口类,至多只有一个类含有实现。
规则4.12 使用public继承而不是protected/private继承
说明:public继承与private继承的区别:
private继承体现"由...来实现"的关系。编译器不会把private继承的派生类转换成基类,也就是
说,私有继承的基类和派生类没有"是...一个"的关系。
public继承体现"是...一个"的关系,即类B public继承于类A,则B的对象就是A的对象,反之则
不然。例如“白马是马,但马不是白马”。
对继承而言,努力做到"是...一个"的关系,否则使用组合代替。
private继承意味"由...来实现",它通常比组合的级别低,与组合的区别:
private继承可以访问基类的protected成员,而组合不能。
private继承可以重新定义基类的虚函数,而组合不能。
尽量用组合代替private继承,因为private继承不如组合简单直观,且容易和public继承混淆。
规则4.13 继承层次不超过4层
说明:当继承的层数超过4层时,对软件的可维护性大大降低,可以尝试用组合替代继承。
规则4.14 虚函数绝不使用缺省参数值
说明:在C++中,虚函数是动态绑定的,但函数的缺省参数却是在编译时就静态绑定的。这意味着你最
终执行的函数是一个定义在派生类,但使用了基类中的缺省参数值的虚函数。因此只要在基类中定义
缺省参数值即可,绝对不要在派生类中再定义缺省参数值。
示例:虚函数display缺省参数值strShow是由编译时刻决定的,而非运行时刻,没有达到多态的目的:
class Base
{
public:
virtual void display(const std::string& strShow = "I am Base class !")
{
std::cout << strShow << std::endl;
}
virtual ~Base(){}
};
class Derive: public Base
{
public:
virtual void display(const std::string& strShow = "I am Derive class !")
{
std::cout << strShow << std::endl;
}
virtual ~Derive(){}
};
int main()
{
Base* pBase = new Derive();
Derive* pDerive = new Derive();
pBase->display(); //程序输出结果: I am base class !而期望输出:I am Deriveclass !
pDerive->display();//程序输出结果: I am Derive class !
delete pBase;
delete pDerive;
return 0;
};
规则4.15 绝不重新定义继承而来的非虚函数
说明:因为非虚函数无法实现动态绑定,只有虚函数才能实现动态绑定:只要操作基类的指针,即可
获得正确的结果。
示例:pB->mf()和pD->mf()两者行为不同。
class B
{
public:
void mf();
//...
};
class D:public B
{
public:
void mf();
//...
};
D x; //x is an object of type D
B *pB = &x; //get pointer to x
D *pD = &x; //get pointer to x
pB->mf(); //calls B::mf
pD->mf(); //calls D::mf
建议4.5 避免派生类中定义与基类同名但参数类型不同的函数
说明:参数类型不同的函数实际是不同的函数。
示例:如下三个类,类之间继承关系如下:类Derive2继承类Derive1,类Derive1继承类Base。
三个类之中均实现了FOO函数,定义如下:
Base类:virtual long FOO(const A , const B , const C)=0;
Derive1类:long FOO(const A , const B , const C);
Derive2类:long FOO(const A , B , const C);
代码中存在如下的调用:
Base* baseptr = new Derive2();
baseptr -> FOO(A,B,C);
代码原意是期望通过如上的代码调用Derive2::FOO函数,但是由于Derive2::FOO与Base::FOO的
参数类型不一致,Derive2::FOO对Base类来说不可见,导致实际运行的时候,调用到
Derive1::FOO,出现调用错误。使得代码逻辑异常。
解决方案:确保派生类Derive2::FOO定义和Base::FOO一致。
建议4.6 派生类重定义的虚函数也要声明virtual关键字
说明:当重定义派生的虚函数时,在派生类中明确声明其为virtual。如果遗漏virtual声明,阅读者
需要检索类的所有祖先以确定该函数是否为虚函数。
4.4 重载
C++的重载功能使得同名函数可以有多种实现方法,以简化接口的设计和使用。但是,要合理运用防止带
来二义性以及潜在问题。保持重载操作符的自然语义,不要盲目创新。
原则4.6 尽量不重载操作符,保持重载操作符的自然语义
说明:重载操作符要有充分理由,而且不要改变操作符原有语义,例如不要使用 ‘+’操作符来做减运算。
操作符重载令代码更加直观,但也有一些不足:
混淆直觉,误以为该操作和内建类型一样是高性能的,忽略了性能降低的可能;
问题定位时不够直观,按函数名查找比按操作符显然更方便。
重载操作符如果行为定义不直观(例如将‘+’操作符来做减运算),会让代码产生混淆。
赋值操作符的重载引入的隐式转换会隐藏很深的bug。可以定义类似Equals()、CopyFrom()等函数来
替代=,==操作符。
规则4.16 仅在输入参数类型不同、功能相同时重载函数
说明:使用重载,导致在特定调用处很难确定到底调用的是哪个函数;当派生类只重载函数的部分变
量,会对继承语义产生困惑,造成不必要的费解。
如果函数的功能不同,考虑让函数名包含参数信息,例如,使用AppendName()、AppendID()而不是
Append()。
建议4.7 使用重载以避免隐式类型转换
说明:隐式转换常常创建临时变量;如果提供类型精确匹配的重载函数,不会导致转换。
示例:
class String
{
//…
String( const char* text ); //允许隐式转换
};
bool operator==( const String&, const String& );
//…代码中某处…
if( someString == "Hello" ) {... }
上述例子中编译器进行隐式转换,好像someString == String( "Hello")一样,形成浪费,因为并不需
要拷贝字符。使用操作符重载即可消除这种隐式转换:
bool operator==( const String& lhs, const String& rhs ); //#1
bool operator==( const String& lhs, const char* rhs ); //#2
bool operator==( const char* lhs, const String& rhs ); //#3
建议4.8 C/C++混用时,避免重载接口函数
说明:目前很多产品采用C模块与C++模块混合的方式,在这个情况下,应该避免模块之间接口函数的
重载。比如:传递给用C语言实现的组件的函数指针。
stPdiamLiCallBackFun.pfCreateConn = PDIAM_COM_CreatConnect;
stPdiamLiCallBackFun.pfDeleteConn = PDIAM_COM_DeleteConnect;
stPdiamLiCallBackFun.pfSendMsg = PDIAM_COM_SendData;
stPdiamLiCallBackFun.pfRematchConn = PDIAM_COM_RematchConn;
stPdiamLiCallBackFun.pfSuCloseAcceptSocket = PDIAM_COM_CloseAcceptSocket;
//注册系统底层通讯函数
ulRet = DiamRegLiFunc(&stPdiamLiCallBackFun);
if (DIAM_OK != ulRet)
{
return PDIAM_ERR_FAILED_STACK_INIT;
}
上面的代码中,由于组件通过C语言实现,如果重载PDIAM_COM_CreatConnect等函数,将会导致该组件
无法初始化。
- C语言编程规范4: 命名规则
- C/C++编程规范
- C/C++ 编程规范
- C#.NET编程规范
- C#.NET编程规范
- c语言编程规范
- c/c++编程规范
- c编程注意规范
- C 语言编程 规范
- C/C++编程规范
- C/C++ 编程规范
- C#.NET 编程规范
- C 编程规范要求
- C语言编程规范
- C语言编程规范
- C/C++编程规范
- C语言编程规范
- C语言编程规范
- 关于爱情
- HDU 2544 最短路(各种最短路算法的实现)
- C++编程规范 3 函数
- Linux内核数据包处理流程-数据包接收(1)
- 解决tomcat启动时隐藏命令行
- C++编程规范 4 类
- C++编程规范 5 作用域、模板和C++其他特性
- ZZ 常用算法经典代码(C++版)
- 使用AES算法对文件进行加密/解密的操作(JAVA)
- C++ 6 资源分配和释放
- 一个是阆苑仙葩,一个是美玉无瑕
- C++ 编程规范 7 异常与错误处理
- C++编程规范 8 标准库
- C++编程规范 9 程序效率