#编码风格# #Google C++# 类(Classes)

本文详述了C++类的设计与使用原则,涵盖构造函数、默认构造函数、复制构造函数、继承、多重继承、接口、运算符重载、访问控制等内容,旨在指导开发者如何正确、高效地使用类。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 

目录

在构造函数中完成⼯工作(Doing Work in Constructors)

默认构造函数(Default Constructor)

显式构造函数(Explicit Constructors)

复制构造函数(Copy Constructos)

结构体与类(Structs vs Classes)

继承(Inheritance)

多重继承(Multiple Inheritance)

接口(Interface)

运算符重载(Operator Overloading)

访问控制(Access Control)

声明次序(Declaration Order)

定义简短函数(Write Short Functions)


        类是C++代码的基本单位。自然,其使用也是⼲⼴广泛的。这一部分将告诉你在使用类时应该和不应该做的。

 

在构造函数中完成⼯工作(Doing Work in Constructors)

        通常认为构造函数仅仅完成成员变量的初始化。其他复杂的初始化⼯工作则交给Init()函数。

定义:

        可以在构造函数中实现类的初始化。

  • 利:形式简单,在使用类时不必担⼼心类是否被初始化。
  • 弊:在构造函数中完成初始化⼯工作面临如下问题:
  1. 由于缺少异常处理(在构造函数中不允许使用),构造函数很难发现错误;
  2. 如果初始化失败,继续使用类将进入不可预知状态;
  3. 如果构造函数是虚函数,则其调用不会传至子类的实现。未来对类的修改可能悄悄地引入此问题,甚至类不是其子类时也会引起混乱。
  4. 如果创建全局类变量(虽然违反此指南,但仍有⼈人这么做),构造函数将在main()执行之前被调用,这很可能打破在构造函数中的假设。譬如,gflags还未被初始化。

结论:

        如果初始化⼯工作对于类很重要,考虑使用Init()方法。特别地,构造函数不应该调用虚函数,这很可能引起错误、访问未初始化的全局变量等问题。

 

默认构造函数(Default Constructor)

        如果类定义了成员变量且没有其他构造函数,应定义默认构造函数,这样可以确保新建对象的内部状态一致和有效。否则,编译器将会不安全地初始化类。

 

定义:

        当创建一个类而不传入参数时,编译器便会调用默认构造函数来完成初始化。

        比如使用new[]运算符时总是调用这个构造器。

  • 利:按默认方式初始化结构体,能处理非法值,简化调试⼯工作。
  • 弊:增加代码写书量。

结论:

        即使你没有定义默认构造函数,编译器也会自动产⽣生一个来初始化新对象,这种构造函数通常不能正确地完成初始化⼯工作。

        继承自其他类且未增加成员变量的子类不需要再定义默认构造函数。

 

显式构造函数(Explicit Constructors)

        关键字explicit用于仅有一个参数的构造函数。

定义:

        通常,只接受一个参数的构造函数可用于类型转换。比如,Foo的构造函数:Foo::Foo(string name),当向以Foo类型为参数的函数传递string参数时,将调用这个构造函数完成string到Foo的转换。有时这很方便,但有时会带来⿇麻烦,比如这种机制在违背你本意的情况下完成类型转换并创建新的对象。声明一个构造函数为显式(explicit)可以避免这种转换。

  • 利:避免不希望的类型转换。
  • 弊:⽆无。

结论:

        最好在每一个只有一个参数的构造函数前用explicit进行限制。但复制构造函数例外,在一些极罕见的情况下允许转换。还有一种例外情况是,那些打算作为透明封装的类。这两种情况应该以注释注明。

 

复制构造函数(Copy Constructos)

        在必要时才提供复制构造函数和赋值运算符。否则, 使用DISALLOW_COPY_AND_ASSIGN来禁用它们。

定义:

        复制构造函数和赋值运算符用来创建一个对象的副本。复制构造函数在需要时被自动调用,比如以传值方式传递一个对象时。

  • 利:复制构造函数方便对象的复制。C++标准模板库(STL)中的容器内容必须是可复制和可赋值的。复制构造函数比CopyFrom()这种替代方案更高效,因为它将构造和复制进行了结合,某些情况下,编译器会略去它,它也避免了堆分配的开销。
  • 弊:C++对象的显式复制常会导致缺陷和引起性能问题。它也会降低代码的可读性,与引用相比,传值将使找出到底是哪个对象被来回传递变得困难,因此,找出对象在何处被修改也变得不可映射。

结论:

        只有少量的类具有可复制性。大多数要么有一个复制构造函数,要么支持赋值运算符。通常,指针和引用起到复制的功能,且性能更好。比如,你可以向函数传递对象的引用或者指针而不是对象本⾝身,你也可以在C++ STL标准容器中保存对象的指针。如果类需要复制性,最好提供复制方法,比如CopyFrom()或者Clone()而不要使用不能被显式调用的复制构造函数。如果复制方法不满足要求(比如出去性能的要求或者类需要被保存在STL标准容器中),可以提供复制构造函数和赋值运算符。

        如果你的类不需要复制构造函数或者赋值运算符,必须显式地禁用它们。将它们在类的private部分声明,且不提供任何相关定义(这样,任何试图调用都将导致连接错误)。方便起见,可以使用DISALLOW_COPY_AND_ASSIGN宏:

// 定义⼀一个宏来禁⽤用复制构造函数和赋值运算符
// 这两个⽅方法的声明应该位于类声明的私有部分
#define DISALLOW_COPY_AND_ASSIGN(TypeName) \
TypeName(const TypeName&); \
void operator=(const TypeName&);

        在Foo类中这样声明:

class Foo{
    public:
    Foo(int f);
    ~Foo();
private:
    DISALLOW_COPY_AND_ASSIGN(Foo);
}

 

结构体与类(Structs vs Classes)

        当对象只是用来保存数据时,则使用结构体,其他情况使用类。

        在C++中,structclass几乎是同义词。我们将给它们加上自己的语义,以便于正确地使用它们进行数据定义。

        结构体应该被用来承载数据,也可包含相关的常量,除了数据成员的访问/修改方法外不提供其他方法。而且数据成员的访问/修改也是直接对其数据的访问而不通过方法调用。即使它有其他方法,这些方法也只完成数据成员的修改,比如构造器、析构函数,Initialize()、Reset()、Validate()

        如果还需要其他方法,则使用类。

        为保持与STL的一致性,函数对象(functor)和类型获取器(traits)可以使用结构体。

        注意:在结构体和类中,成员变量的命名方式是不同的。

 

继承(Inheritance)

        组合通常比继承更合适。使用继承时,一般为公有继承。

定义:

        一个子类将继承基类的全部数据和操作。特别地,C++中,继承有两种主要方式:实现继承:实质性代码都被子类继承和接口继承:子类只继承接口名称。

  • 利:实现继承通过重用基类的代码来减小程序规模。由于继承是一个编译时的声明,程序员和编译器可以理解这些操作并检测错误。接口继承则通过编程使一个类对外暴露特定的API。同样,当一个类没有定义必要的API时,编译器可以检测出错误。
  • 弊:对于实现继承,由于子类的实现代码需要在基类和其自⾝身展开,理解这些实现将变得困难。子类不可以覆盖非虚方法,所以它不能改变(基类的)实现。基类也可以定义一些数据成员来规定其物理布局。

结论:

        所有继承应该是公有继承。如果使用私有继承,更好的方法是使用一个基类的实例作为成员

        不要滥用实现继承,类组合常常更合适。只有当Bar有充分的理由说明其is a Foo时,才能说BarFoo的子类

        必要时使析构函数虚化。任何定义了虚函数的类,析构函数都应该被虚化。

        子类需要使用的基类方法最好用protected加以限制。注意,基本的数据成员必要是private

        重写继承的虚函数时,在子类中显式地声明其为virtual。一旦漏掉了virtual,读者必须检测其所有基类来确保它是不是虚函数。

 

多重继承(Multiple Inheritance)

        多重实现继承通常罕有其用。只有当仅一个基类被实现继承,而其他类都是纯接口且以Interface作为后缀声明时才允许多重继承

定义:

        多重继承允许一个类继承多个基类。请注意区别基类、纯接口和实现接口。

  • 利:多重继承允许你更大限度地重用代码。
  • 弊:多重继承只有一种情况才被允许:除了第一基类,其他类都是以Interface作为后缀结束的纯接口。

        注意:在Windows中有一个例外。

 

接口(Interface)

        作为接口的类可以但不必以Interface后缀结束。

定义:

        满足以下条件的类被称为纯接口:

  1. 只有公共的纯虚函数(“=0”)和静态方法(见下文,析构函数例外);
  2. 只能有静态数据成员;
  3. 没有构造函数定义。即使有构造函数, 也仅是默认构造函数且被声明为protected
  4. 如果是子类,只能继承自满足以上条件且以Interface后缀结束的类;

        由于内部都是纯虚函数,接口类不能被直接实例化。为使所有接口的实现都可以被正确地销毁,所有接口类必须定义一个虚析构函数(这与第一条冲突)。详细请参见Stroustup的《The C++ Programming Language》第3版的12.4章节。

  • 利:最好给纯接口类加上Interface后缀以让其他程序员知道此类不能添加任何方法实现和非静态数据成员,这对于多重继承来说很重要。Java程序员可能更了解接口。
  • 弊:Interface后缀使类名变得冗⻓长而难以阅读和理解。而且,接口的特征可能被误解为其具体实现不能暴露给调用者。

结论:

        满足以上条件的类最好以Interface后缀结束。然而,这不是必须的。

 

运算符重载(Operator Overloading)

        只有在很罕见的情况下才会用到运算符重载。

定义:

        类可以重载诸如+/-的运算符以使其能像内建类型一样操作。

  • 利:这些类可以像内建类型一样操作(比如int),代码看上去更直观。相比于那些呆板的命名函数(比如Equals()、Add()),重载的运算符是一种操作性的命名。为确保某些模板函数的正确性,有时必须重载运算符。
  • 弊:运算符有很多弊端:
  1. 它可能使我们误以为大开销的操作(运算符重载实际上是函数调用)是小开销的内建操作;
  2. 找到重载运算符的调用点常常很困难。比如找到函数Equals()的调用处比找到==的简单的多。
  3. 一些运算符也适用于指针,这很容易造成程序缺陷。举个例子:&Foo+4和Foo+4实现的操作完全不同,但编译器不会报错。

    运算符重载也有可能造成歧义。比如,如果一个类重载了单目运算符&,它不可以被安全地前置声明。

结论:

        一般情况下不要重载运算符。尤其赋值运算符,通常很隐蔽。如果需要,可以定义Equals()CopyFrom()等函数。如果一个类需要被前置声明,一定避免危险的单目运算符&的重载。

        然而,在很罕见的情况下,你可能需要重载运算符来与模板和C++标准类( <<(ostream&,const T&))互操作。合理的情况下,可以使用运算符重载,但如果可能,还是应该避免。尤其注意,不要仅仅为了类能在标准容器中作为键而重载==<,相反,你应该创建相等和比较函数对象。

        一些标准模板库算法可能需要你重载==,但必须说明原因。

        参见复制构造函数和函数重写。

 

访问控制(Access Control)

        数据成员应该被定义成私有(private)(静态常数据成员除外),需要时提供访问器(accessor)(出于技术考虑,使用Google Test时,承载测试功能的类可以将其数据成员声明成protected)。通常,名称为foo_的变量其访问函数为foo(),而其修改器(mutator)则为set_foo()

        访问器常在头文件中定义为内联函数。

        参见继承和函数命名。

 

声明次序(Declaration Order)

        请按下面的规则次序来定义类:公共成员位于私有成员前;方法位于数据成员前(变量)等等。

        公共部分位于保护部分前,保护部分位于私有部分前,如果某个部分空,则忽略它。

        在每个部分,请按照下面的次序来声明成员:

  1. 类型定义(Typedefs)和枚举(Enums)
  2. 常量(static const数据成员)
  3. 构造函数;
  4. 析构函数;
  5. 类方法,包括静态方法
  6. 数据成员(静态常量除外);

        友元声明和DISALLOW_COPY_AND_ASSIGN宏调用应该在私有部分。私有部分应该在类定义的最后部分。参见复制构造函数。

        在相关源文件中,方法的实现次序也应尽量与类声明中一致。

        不要将把大函数内联定义在类定义中。通常,只有很短且性能要求高的情况下才将一个函数定义成内联。参见内联函数。

 

定义简短函数(Write Short Functions)

        函数应该尽量简短并功能单一。

        不得不承认,某些场合长函数很合适,所以不太容易去限制其长度。如果一个函数超过40行,考虑可否在不改变程序结构的情况下将其拆分。

        尽管长函数目前工作良好,也许其他程序员日后会修改并给它添加新功能。这会导致难以发现的缺陷。保持你的函数简短可以方便其他程序员阅读和修改你的代码。

        阅读一些代码时,你可能发现长函数。不过,不要害怕修改它们。如果发现使用它们很困难或者调试有难度,或者你想在多处使用部分函数代码,考虑把它拆分成更易管理的片段。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值