跳转到内容

C++ 编程/类

来自 Wikibooks,开放世界中的开放书籍

类用于创建用户定义类型。类的一个实例称为对象,程序可以包含任意数量的类。与其他类型一样,对象类型区分大小写。

类提供封装,如面向对象编程 (OOP) 范式中所定义的那样。类可以与其关联数据成员和函数成员。与内置类型不同,类可以包含多个变量和函数,这些称为成员。

类还在程序编写中提供“分而治之”方案的灵活性。换句话说,一位程序员可以编写一个类并保证一个接口。另一个程序员可以使用该预期接口编写主程序。这两部分组合在一起并编译以供使用。

注意
从技术角度来看,结构体和类实际上是一样的。结构体可以在任何可以放置类的任何地方使用,反之亦然,唯一的技术差异是,类成员默认设置为private,而结构体成员默认设置为public。结构体可以通过在结构体开头添加关键字 private 来使其像类一样工作。除此之外,这主要是一种约定差异。

C++ 标准没有对方法的定义。在与其他语言用户讨论时,使用方法来表示成员函数有时会让人感到困惑或出现解释问题,例如将静态成员函数称为静态方法。一些 C++ 程序员在非正式场合甚至经常使用方法一词来专门指代虚拟成员函数。

定义

class MyClass
{
 /* public, protected and private
 variables, constants, and functions */
};

MyClass 类型的对象(区分大小写)声明使用

MyClass object;
  • 默认情况下,所有类成员最初都是private
  • 关键字publicprotected 允许访问类成员。
  • 类不仅包含数据成员,还包含用于操作这些数据的函数。
  • 类用作 OOP 的基本构建块(这是约定上的区别,而不是语言强制语义的区别)。
可以创建类
  • 在调用 main() 之前。
  • 在调用声明对象的函数时。
  • 当使用“new”运算符时。
类名
  • 根据类本身的名称为类命名。如果无法确定名称,则设计系统不够完善。
  • 超过三个词的复合名称表明您的设计可能会混淆系统中的各种实体。重新审视您的设计。尝试使用 CRC 卡会话来查看您的对象是否承担了超出其应有范围的责任。
  • 避免将类命名为类似于其派生类的名称的诱惑。类应独立存在。声明具有类类型的对象不依赖于类的派生位置。
  • 后缀或前缀有时很有用。例如,如果您的系统使用代理,则将某物命名为 DownloadAgent 会传达真实信息。
数据抽象

面向对象 (OO) 的基本概念建议对象不应该暴露其任何实现细节。这样,您可以在不更改使用该对象的代码的情况下更改实现。类通过设计允许其程序员隐藏(以及防止更改)类的实现方式。此强大的工具允许程序员构建“预防性”措施。类中的变量在类的功能中经常发挥着非常重要的作用,因此变量可以在类的private 部分中得到保护。

访问标签

[编辑 | 编辑源代码]

访问标签PublicProtectedPrivate 用于类中,用于设置该部分中成员的访问权限。默认情况下,所有类成员最初都是private。标签可以以任何顺序排列。这些标签可以在类声明中多次使用,适用于需要多个此类类型组的情况。访问标签将保持活动状态,直到使用另一个访问标签更改权限。

我们已经提到类可以在其“内部”包含成员函数;我们将在后面详细了解它们。这些成员函数可以访问和修改类中所有的数据和成员函数。因此,权限标签用于限制对位于类外部的成员函数以及其他类的访问权限。

例如,类“Bottle”可以包含一个私有变量fill,表示 0-3 dl 的液体水平。fill 无法直接修改(编译器错误),但Bottle 提供成员函数 sip() 以将液体水平降低 1。Mywaterbottle 可以是该类的实例,即对象。

/* Bottle - Class and Object Example */
#include <iostream>
#include <iomanip>
 
using namespace std;
 
class Bottle
{
  private:      // variables are modified by member functions of class
  int iFill;    // dl of liquid
     
  public:
    Bottle()    // Default Constructor
    : iFill(3)  // They start with 3 dl of liquid
      {
        // More constructor code would go here if needed.
      }
     
     bool sip() // return true if liquid was available
     {

       if (iFill > 0)
       {
         --iFill;
         return true;
       }
       else
       {
         return false;
       }

     }
 
     int level() const  // return level of liquid dl
     {
         return iFill;
     }
 };  // Class declaration has a trailing semicolon
 
int main()
{
  // terosbottle object is an instance of class Bottle
  Bottle terosbottle;
  cout << "In the beginning, mybottle has "
       << terosbottle.level()
       << "  dl of liquid"
       << endl;
   
  while (terosbottle.sip())
  {
     cout << "Mybottle has "
          << terosbottle.level()
          << " dl of liquid"
          << endl;
  }
   
  return 0;
}

这些关键字private、public 和 protected 影响成员的权限——无论是函数还是变量。

此标签表示“public”部分中的任何成员可以在声明对象的任何范围内自由访问。

注意
避免声明公共数据成员,因为这样做会导致不可预见的问题。

定义为私有的成员只能在定义它们的类或友元类中访问。通常是成员变量和辅助函数的领域。通常将函数放在这里,然后根据需要将它们移动到更高的访问级别,以降低复杂性。

注意
经常被忽视的是,同一个类的不同实例可以访问彼此的私有或受保护的变量。这种情况在复制构造函数中很常见。

(这是一个默认复制构造函数将执行相同操作的示例。)

class Foo
{
 public:
   Foo(const Foo &f)
   {
     m_iValue = f.m_iValue; // perfectly legal
   }
 
 private:
   int m_iValue;
};

protected

[编辑 | 编辑源代码]

受保护的标签对继承具有特殊含义,受保护的成员在定义它们的类中以及在从该基类继承的类或它的友元中都可以访问。在关于继承的部分,我们将看到更多关于它的内容。

注意
同一个类的其他实例可以访问受保护的字段 - 只要两个类是相同类型。但是,子类的实例不能访问父类实例的受保护字段或方法。

继承(派生)

[edit | edit source]

如早前介绍编程范式时所见,继承是一种描述对象类型或类之间关系的属性。它是 OOP 的一个特性,在 C++ 中,类共享此属性。

派生是使用继承属性创建新类的行为。可以从另一个类甚至多个类派生一个类(多重继承),就像一棵树,我们可以将基类称为根,将子类称为任何叶子;在任何其他情况下,派生自另一个类的每个类都将存在父子关系。

基类

基类是一个类,它是在有意地从它派生其他类的情况下创建的。

子类

子类是从另一个类派生的类,现在将成为它的父类。

父类

父类是我们用来创建作为子类引用的类的最近类。

例如,假设您正在创建一个游戏,其中使用不同的汽车,并且您需要为警察和玩家使用特定类型的汽车。两种汽车类型都具有类似的属性。主要区别(在本示例情况下)是警车的顶部会有警笛,而玩家的汽车不会。

准备警车和玩家车的其中一种方法是为警车和玩家车创建单独的类,如下所示

class PlayerCar {
   private:
     int color;
  
   public:
     void driveAtFullSpeed(int mph){
       // code for moving the car ahead
     }
};

class PoliceCar {
private:
  int color;
  bool sirenOn;  // identifies whether the siren is on or not
  bool inAction; // identifies whether the police is in action (following the player) or not
  
public:
  bool isInAction(){
    return this->inAction;
  }

  void driveAtFullSpeed(int mph){
    // code for moving the car ahead
  }
  
};

然后为两辆车创建单独的对象,如下所示

PlayerCar player1;
PoliceCar policemen1;

所以,除了您很容易注意到的一个方面:上述两个类中有一些代码部分非常相似(如果不是完全相同)。本质上,您必须在两个不同的位置输入相同的代码!当您更新代码以包含用于handBrake()pressHorn() 的方法(函数)时,您必须在上面的两个类中都这样做。

因此,为了避免在单个项目中的多个位置编写相同代码的这种令人沮丧(且令人困惑)的任务,您使用继承。

现在您已经了解了继承在 C++ 中解决哪些问题,让我们检查如何在程序中实现继承。顾名思义,继承允许我们创建新的类,这些类自动具有现有类中的所有代码。这意味着,如果有一个名为MyClass 的类,就可以创建一个名为MyNewClass 的新类,该类将具有MyClass 类中存在的所有代码。以下代码段显示了所有内容

class MyClass {
  protected:
         int age;
  public:
         void sayAge(){
             this->age = 20;
             cout << age;
         }
};

class MyNewClass : public MyClass {

};

int main() {
  
  MyNewClass *a = new MyNewClass();
  a->sayAge();
  
  return 0;
  
}

如您所见,使用冒号“:”我们可以从现有类中继承一个新类。就这么简单!MyClass 类中的所有代码现在都可供MyNewClass 类使用。如果您足够聪明,您已经可以看出它提供的优势。如果您像我一样(即不太聪明),您可以查看以下代码段来了解我的意思

class Car {
  protected:
         int color;
         int currentSpeed;
         int maxSpeed;
  public:
         void applyHandBrake(){
             this->currentSpeed = 0;
         }
         void pressHorn(){
             cout << "Teeeeeeeeeeeeent"; // funny noise for a horn
         }
         void driveAtFullSpeed(int mph){
              // code for moving the car ahead;
         }
};

class PlayerCar : public Car {

};

class PoliceCar : public Car {
  private:
         bool sirenOn;  // identifies whether the siren is on or not
         bool inAction; // identifies whether the police is in action (following the player) or not
  public:
         bool isInAction(){
             return this->inAction;
         }
};

在上面的代码中,新创建的两个类PlayerCarPoliceCar 都是从Car 类继承的。因此,Car 类中的所有方法和属性(变量)都可供玩家车和警车的这两个新创建的类使用。从技术上讲,在本例中,C++ 中的Car 类是我们的“基类”,因为它是其他两个类所基于(或继承自)的类。

这里需要注意的一件事是关键字protected 而不是通常的private 关键字。这没什么大不了的:当我们想确保我们在基类中定义的变量应该在从该基类继承的类中可用时,我们使用protected。如果您在Car 类的类定义中使用private,则您将无法在继承的类中继承这些变量。

类继承有三种类型:public、private 和 protected。我们使用关键字public 来实现公有继承。使用关键字 public 从基类继承的类将所有公有成员作为公有成员继承,受保护的数据作为受保护数据继承,私有数据被继承,但不能直接被类访问。

以下示例显示了从基类 Form “公开”继承的 Circle 类

class Form {
private:
  double area;

public:
  int color;

  double getArea(){
    return this->area;
  }

  void setArea(double area){
    this->area = area;
  }

};

class Circle : public Form {
public:
  double getRatio() {
    double a;
    a = getArea();
    return sqrt(a / 2 * 3.14);
  }

  void setRatio(double diameter) {
    setArea( pow(diameter * 0.5, 2) * 3.14 );
  }

  bool isDark() {
    return (color > 10);
  }

};

新类 Circle 继承了来自基类 Form 的属性 area(属性 area 隐式地是 Circle 类的属性),但不能直接访问它。它是通过函数 getArea 和 setArea 来实现的(这些函数在基类中是公有的,并且在派生类中保持为公有)。然而,color 属性被继承为一个公有属性,并且该类可以直接访问它。

下表显示了三种不同类型的继承中属性的继承方式

基类中的访问说明符
private protected public
私有继承 该成员不可访问。 该成员是私有的。 该成员是私有的。
受保护继承 该成员不可访问。 该成员是受保护的。 该成员是受保护的。
公有继承 该成员不可访问。 该成员是受保护的。 该成员是公有的。

如上表所示,受保护的成员在公有继承中被继承为受保护的方法。因此,当我们想要声明一个方法在类外部不可访问,并且不希望在派生类中丢失对它的访问权限时,我们应该使用受保护标签。但是,有时失去可访问性可能很有用,因为我们正在封装基类中的详细信息。

让我们假设我们有一个类,其中包含一个非常复杂的方法“m”,它调用在类中声明为私有的许多辅助方法。如果我们从它派生一个类,我们不应该理会那些方法,因为它们在派生类中不可访问。如果一个不同的程序员负责派生类的设计,允许访问这些方法可能会导致错误和混乱。因此,每当我们可以使用私有标签来设计具有相同结果的设计时,最好避免使用受保护标签。

现在还有一个额外的“语法技巧”。如果基/父类有一个需要参数的构造函数,那么我们遇到了麻烦,你可能会认为。当然,直接调用构造函数是被禁止的,但是我们有一种特殊的语法用于此目的。方式就是当您定义传递类的构造函数时,您像这样调用父构造函数

ChildClass::ChildClass(int a, int b) : ParentClass(a, b)
{
  //Child constructor here
}

注意
避免在父构造函数调用中引用子类内部,因为关于类创建顺序没有保证,并且父类仍然需要初始化。一种解决方法是在父类中创建一个“初始化器”方法,这样任何对其的调用都将提供这些保证。这不是最好的解决方案,通常表明设计中存在错误,但有时是必需的。

多重继承

[edit | edit source]

多重继承允许构建从多个类型或类继承的类。这与单继承形成对比,在单继承中,一个类只从一个类型或类继承。

多重继承可能会导致一些令人困惑的情况,并且比单继承复杂得多,因此关于其优势是否超过其风险存在一些争论。多年来,多重继承一直是一个敏感问题,反对者指出其复杂性增加以及在“菱形问题”等情况下的模糊性。大多数现代 OOP 语言不允许多重继承。

声明的派生顺序与确定构造函数的默认初始化顺序和析构函数清理顺序相关。

class One
{
  // class internals
};

class Two
{
  // class internals
};

class MultipleInheritance : public One, public Two
{
  // class internals
};

注意
请记住,在创建将被派生的类时,析构函数可能需要进一步考虑。

数据成员

[edit | edit source]

数据成员在类定义中以与全局变量或函数变量相同的方式声明。它们的作用是存储该类的信息,并且可以包括任何类型的成员,甚至其他用户定义的类型。它们通常对外部使用隐藏,具体取决于所采用的编码风格,外部使用通常通过特殊成员函数来完成。

注意
在类定义中不允许显式初始化程序,除非它们是const static int 或枚举类型,这些类型可以具有显式初始化程序。


Clipboard

待办事项
添加更多信息


this 指针

[edit | edit source]

this 关键字充当指向所引用类的指针。this 指针的行为类似于任何其他指针,尽管您无法更改指针本身。请阅读有关指针和引用的部分,以了解有关一般指针的更多信息。

this 指针只能在联合体结构体的非静态成员函数中访问,在静态成员函数中不可用。不需要为this 指针编写代码,因为编译器会隐式地执行此操作。在使用调试器时,在程序单步执行到非静态类函数时,您可以在某些变量列表中看到this 指针。

在以下示例中,编译器在非静态成员函数 int getData() 中插入了一个隐式参数this。此外,启动调用的代码传递了一个隐式参数(由编译器提供)。

class Foo
{
private:
    int iX;
public:
    Foo(){ iX = 5; };

    int getData() 
    {   
        return this->iX;  // this is provided by the compiler at compile time
    }
};

int main()
{
    Foo Example;
    int iTemp;

    iTemp = Example.getData(&Example);  // compiler adds the &Example reference at compile time

    return 0;
}

在某些情况下,程序员应该了解并使用this 指针。在重载赋值运算符时,应使用this 指针来防止灾难。例如,在上面的代码中添加一个赋值运算符。

class Foo
{
private:
    int iX;
public:
    Foo() { iX = 5; };

    int getData()          
    {                            
        return iX;  
    }

    Foo& operator=(const Foo &RHS);
};

Foo& Foo::operator=(const Foo &RHS)
{
    if(this != &RHS)
    {    // the if this test prevents an object from copying to itself (ie. RHS = RHS;)
        this->iX = RHS.iX;     // this is suitable for this class, but can be more complex when
                               // copying an object in a different much larger class
    }

    return (*this);            // returning an object allows chaining, like a = b = c; statements
}

无论您对this 的了解多么少,它在实现任何类中都很重要。

静态数据成员

[edit | edit source]

在数据成员中使用static 说明符,将导致该成员被所有拥有者类的实例和派生类共享。要使用静态数据成员,必须将数据成员声明为 static 并在类声明之外的 文件范围 内初始化它。

在类数据成员中使用时,该类的所有实例共享该变量的一个副本。

class Foo {
public:
  Foo() {
    ++iNumFoos;
    cout << "We have now created " << iNumFoos << " instances of the Foo class\n";
  }
private:
  static int iNumFoos;
};

int Foo::iNumFoos = 0;  // allocate memory for numFoos, and initialize it

int main() {
  Foo f1;
  Foo f2;
  Foo f3;
}

在上面的示例中,静态类变量 numFoos 在Foo 类的所有三个实例(f1f2f3)之间共享,并跟踪Foo 类实例化的次数。


成员函数

[edit | edit source]

成员函数可以(也应该)用于与用户定义类型中包含的数据进行交互。用户定义类型在程序编写中的"分而治之" 方案中提供灵活性。换句话说,一个程序员可以编写一个用户定义类型并保证一个接口。另一个程序员可以用该预期的接口编写主程序。这两部分被放在一起并编译以供使用。用户定义类型提供了面向对象编程 (OOP) 范式中定义的封装

在类中,为了保护数据成员,程序员可以定义函数来对这些数据成员执行操作。成员函数和函数是用于引用类的名称。函数原型在类定义中声明。这些原型可以采用非类函数的形式,也可以采用类适用的原型。函数可以在类定义中声明和定义。但是,大多数函数可能具有非常大的定义,并且会使类非常难以阅读。因此,可以使用作用域解析运算符“::”在类定义之外定义函数。此作用域解析运算符允许程序员在其他地方定义函数。这可以使程序员提供一个定义类的头文件.h 和一个从编译后的.cpp 文件构建的.obj 文件,该文件包含函数定义。这可以隐藏实现并防止篡改。用户将必须重新定义每个函数才能更改实现。类中的函数可以访问和修改(除非该函数是常量)数据成员,而无需声明它们,因为数据成员已经在类中声明。

简单示例

文件:Foo.h

// the header file named the same as the class helps locate classes within a project
// one class per header file makes it easier to keep the 
// header file readable (some classes can become large)
// each programmer should determine what style works for them or what programming standards their
// teacher/professor/employer has
 
#ifndef FOO_H
#define FOO_H
 
class Foo{
public:
  Foo();                  // function called the default constructor
  Foo( int a, int b );    // function called the overloaded constructor
  int Manipulate( int g, int h );
 
private:
  int x;
  int y;
};
 
#endif

文件:Foo.cpp

#include "Foo.h"
 
/* these constructors should really show use of initialization lists
Foo::Foo() : x(5), y(10)
{
}
Foo::Foo(int a, int b) : x(a), y(b)
{
}
*/
Foo::Foo(){
  x = 5;
  y = 10;
}
Foo::Foo( int a, int b ){
  x = a;
  y = b;
}

int Foo::Manipulate( int g, int h ){
  x = h + g*x;
  y = g + h*y;
}

重载

[edit | edit source]

成员函数可以被重载。这意味着多个成员函数可以在同一个作用域内存在相同的名称,但必须具有不同的签名。成员函数的签名由成员函数的名称、成员函数的参数类型和顺序组成。

由于名称隐藏,如果派生类中的成员与基类中的成员共享相同的名称,它们将对编译器隐藏。要使这些成员可见,可以使用声明从基类作用域中引入它们。

构造函数和其他类成员函数(析构函数除外)可以被重载。

构造函数

[edit | edit source]

构造函数是一个特殊的成员函数,它在每次创建类的新的实例时都会被调用。编译器在新的对象已在内存中分配并且将该“原始”内存转换为适当的类型化对象后调用构造函数。构造函数的声明方式与普通成员函数非常相似,但它将共享类名并且没有返回值。

构造函数负责类操作所需的几乎所有运行时设置。它的一般主要目的是在对象实例化(声明对象时)定义数据成员,它们也可以具有参数,如果程序员选择的话。如果构造函数具有参数,那么在使用new 运算符时,也应将它们添加到该类任何其他对象的声明中。构造函数也可以被重载。

Foo myTest;                 // essentially what happens is:  Foo myTest = Foo();
Foo myTest( 3, 54 );        // accessing the overloaded constructor
Foo myTest = Foo( 20, 45 ); // although a new object is created, there are some extra function calls involved
                            // with more complex classes, an assignment operator should
                            // be defined to ensure a proper copy (includes ''deep copy'')
                            // myTest would be constructed with the default constructor, and then the
                            // assignment operator copies the unnamed Foo( 20, 45 ) object to myTest

使用new 以及构造函数

Foo* myTest = new Foo();           // this defines a pointer to a dynamically allocated object
Foo* myTest = new Foo( 40, 34 );   // constructed with Foo( 40, 34 )
// be sure to use delete to avoid memory leaks

注意

虽然使用new 创建对象没有风险,但通常最好避免在对象的构造函数中使用内存分配函数。具体来说,使用 new 创建一个对象数组,其中每个对象也使用 new 在其构造期间分配内存,通常会导致运行时错误。如果类或结构包含必须指向动态创建对象的成员,最好依次初始化这些父对象的数组,而不是将任务留给它们的构造函数。
这在使用异常(在异常处理中)编写代码时尤其重要,如果在构造函数完成之前抛出异常,则不会为该对象调用关联的析构函数。

构造函数可以委托给另一个(在 C++ 11 中引入)。也认为减少默认参数的使用是可取的,如果维护者必须编写和维护多个构造函数,这会导致代码重复,从而降低可维护性,因为可能会引入不一致甚至导致代码膨胀。

默认构造函数

默认构造函数是可以不带任何参数调用的构造函数。最常见的情况是,默认构造函数在没有参数的情况下声明,但如果所有这些参数都被赋予默认值,则具有参数的构造函数也可以是默认构造函数。

为了创建类类型的对象数组,该类必须有一个可访问的默认构造函数;C++ 没有语法指定数组元素的构造函数参数。

重载构造函数

[edit | edit source]

当类的对象被实例化时,类编写者可以提供各种构造函数,每个构造函数都有不同的目的。一个大型类将具有许多数据成员,其中一些数据成员在对象被实例化时可能被定义,也可能不被定义。无论如何,每个项目都会有所不同,因此程序员在提供构造函数时应该调查各种可能性。

这些都是类 myFoo 的构造函数。

 
myFoo(); // default constructor, the user has no control over initial values
         // overloaded constructors

myFoo( int a, int b=0 ); // allows construction with a certain 'a' value, but accepts 'b' as 0
                         // or allows the user to provide both 'a' and 'b' values
 // or
 
myFoo( int a, int b ); // overloaded constructor, the user must specify both values

class myFoo {
private:
  int Useful1;
  int Useful2;

public:
  myFoo(){                     // default constructor
           Useful1 = 5;
           Useful2 = 10;  
          };

  myFoo( int a, int b = 0 ) { // two possible cases when invoked
         Useful1 = a;
         Useful2 = b;
  };

};
 
myFoo Find;           // default constructor, private member values Useful1 = 5, Useful2 = 10
myFoo Find( 8 );      // overloaded constructor case 1, private member values Useful1 = 8, Useful2 = 0
myFoo Find( 8, 256 ); // overloaded constructor case 2, private member values Useful1 = 8, Useful2 = 256

构造函数初始化列表

[edit | edit source]

构造函数初始化列表(或成员初始化列表)是使用非默认构造函数初始化数据成员和基类的唯一方法。成员的构造函数包含在参数列表和构造函数主体之间(由冒号与参数列表隔开)。使用初始化列表不仅在效率方面更好,而且是保证所有数据成员初始化在进入构造函数主体之前完成的最简单方法。

// Using the initialization list for myComplexMember_ 
MyClass::MyClass(int mySimpleMember, MyComplexClass myComplexMember)
: myComplexMember_(myComplexMember) // only 1 call, to the copy constructor
{
 mySimpleMember_=mySimpleMember; // uses 2 calls, one for the constructor of the mySimpleMember class
                                 // and a second for the assignment operator of the MyComplexClass class
}

这比在构造函数主体内部为复杂数据成员赋值更有效,因为在这种情况下,变量将使用其对应的构造函数进行初始化。

请注意,提供给成员构造函数的参数不需要是类构造函数的参数;它们也可以是常量。因此,您可以为包含具有非默认构造函数的成员的类创建默认构造函数。

示例

MyClass::MyClass() : myComplexMember_(0) { }

在构造函数中使用初始化列表初始化成员非常有用。这使得读者很清楚地看到构造函数没有执行逻辑。初始化的顺序应该与定义基类和成员的顺序一致,否则编译时可能会出现警告。一旦开始初始化成员,请确保在所有构造函数中都保持一致,以避免混淆和潜在的 0xbaadfood 错误。

使用与成员同名的构造函数参数是安全的。

示例

class MyClass : public MyBaseClassA, public MyBaseClassB {
  private:
    int c;
    void *pointerMember;
  public:
    MyClass(int,int,int);
};
/*...*/
MyClass::MyClass(int a, int b, int c):
 MyBaseClassA(a)
,MyBaseClassB(b)
,c(c)
,pointerMember(NULL)
,referenceMember()
{
 //logic
}

请注意,此技术也适用于普通函数,但现在已过时,在这种情况下被视为错误。

注意
一个常见的误解是,数据成员的初始化可以在构造函数体中完成。所有这种所谓的“初始化”实际上都是赋值。C++ 标准规定,所有数据成员的初始化都必须在进入构造函数体之前完成。这就是为什么某些类型(const 类型和引用)不能被赋值,而必须在构造函数初始化列表中初始化的原因。

还应记住,类成员的初始化顺序是按照它们声明的顺序,而不是在初始化列表中出现的顺序。避免鸡生蛋还是蛋生鸡的悖论的一种方法是始终按照声明顺序将成员添加到初始化列表中。

析构函数

[edit | edit source]

析构函数与构造函数一样,被声明为任何正常的成员函数,但会使用与类相同的名称,区别在于析构函数的名称前面带有“~”,它不能带参数,也不能被重载。

析构函数在销毁类对象时被调用。析构函数在避免资源泄漏(通过释放内存)和实现 RAII 惯用法方面至关重要。在类构造函数中分配的资源通常在该类的析构函数中释放,以便在类不再存在后将系统恢复到已知或稳定的状态。

当对象被销毁时,析构函数被调用,在它们被声明的函数返回后,在使用delete操作符或程序结束时被调用。如果派生类型的对象被销毁,首先执行最派生对象的析构函数。然后,成员对象和基类对象以其对应构造函数完成的相反顺序递归销毁。与结构体一样,如果类没有用户声明的析构函数,编译器会隐式地将析构函数声明为其类的内联公共成员。

对象的动态类型将从最派生类型变为析构函数运行,与构造函数执行时的变化对称。这会影响构造和销毁过程中虚拟调用调用的函数,并导致常见的(并且合理的)建议,避免直接或间接地从对象的构造函数或析构函数调用对象的虚拟函数。

在关于内联函数的介绍中,我们已经看到了大部分概念,在处理成员函数时,这些概念得到了扩展,并有一些额外的注意事项。

如果成员函数的定义包含在类的声明中,则该函数默认情况下被隐式地设为内联。编译器选项可能会覆盖此行为。

如果在编译时不知道对象的类型,则不能内联对虚拟函数的调用,因为我们不知道要内联哪个函数。

static关键字可以用四种不同的方式使用


Clipboard

待办事项
在结构固定后,将上述链接从子部分更改为书籍位置。


静态成员函数
[edit | edit source]

声明为静态的成员函数或变量在对象类型的所有实例之间共享。这意味着对于任何对象类型,成员函数或变量都只存在一个副本。

无需对象即可调用的成员函数

在类函数成员中使用时,该函数不接受实例作为隐式的this参数,而是像自由函数一样运行。这意味着可以调用静态类函数而不创建类的实例。

class Foo {
public:
  Foo() {
    ++numFoos;
    cout << "We have now created " << numFoos << " instances of the Foo class\n";
  }
  static int getNumFoos() {
    return numFoos;
  }
private:
  static int numFoos;
};

int Foo::numFoos = 0;  // allocate memory for numFoos, and initialize it

int main() {
  Foo f1;
  Foo f2;
  Foo f3;
  cout << "So far, we've made " << Foo::getNumFoos() << " instances of the Foo class\n";
}
命名构造函数
[edit | edit source]

命名构造函数是使用静态成员函数的一个很好的例子。命名构造函数是用来创建类对象而不(直接)使用其构造函数的函数的名称。这可能用于以下情况

  1. 绕过构造函数只能在签名不同时重载的限制。
  2. 通过将构造函数设为私有,使类不可继承。
  3. 通过将构造函数设为私有,防止堆栈分配

声明一个使用私有构造函数创建对象并返回它的静态成员函数。(它也可以返回指针或引用,但这种复杂性似乎毫无用处,并将此转变为工厂模式,而不是传统的命名构造函数。)

以下是一个存储可以在任何温度刻度中指定的温度的类的示例。

class Temperature
{
    public:
        static Temperature Fahrenheit (double f);
        static Temperature Celsius (double c);
        static Temperature Kelvin (double k);
    private:
        Temperature (double temp);
        double _temp;
};

Temperature::Temperature (double temp):_temp (temp) {}

Temperature Temperature::Fahrenheit (double f)
{
    return Temperature ((f + 459.67) / 1.8);
}

Temperature Temperature::Celsius (double c)
{
    return Temperature (c + 273.15);
}

Temperature Temperature::Kelvin (double k)
{
    return Temperature (k);
}

const

[edit | edit source]

这种类型的成员函数不能修改类的成员变量。它既是给程序员,又是给编译器的一个提示,表明给定的成员函数不会改变类的内部状态;但是,任何声明为mutable的变量仍然可以修改。

例如

 class Foo
 {
  public:
    int value() const
    {
      return m_value;
    }
 
    void setValue( int i )
    {
      m_value = i;
    }
 
  private:
    int m_value;
 };

这里value()显然不会改变 m_value,因此可以并且应该设为 const。但是setValue()确实修改了 m_value,因此不能设为 const。

另一个经常被忽略的微妙之处是const成员函数不能调用非 const 成员函数(如果你尝试,编译器会抱怨)。该const成员函数不能改变成员变量,而非 const 成员函数可以改变成员变量。由于我们假设非 const 成员函数会改变成员变量,const成员函数被认为永远不会改变成员变量,并且不能调用改变成员变量的函数。

下面的代码示例解释了const根据放置位置的不同,可以做什么。

 class Foo
 {
 public:
    /*
     * Modifies m_widget and the user
     * may modify the returned widget.
     */
    Widget *widget();
 
    /*
     * Does not modify m_widget but the
     * user may modify the returned widget.
     */
    Widget *widget() const;
 
    /*
     * Modifies m_widget, but the user
     * may not modify the returned widget.
     */
    const Widget *cWidget();
 
    /*
     * Does not modify m_widget and the user
     * may not modify the returned widget.
     */
    const Widget *cWidget() const;
 
 private:
    Widget *m_widget;
 };

访问器和修改器(Setter/Getter)

[edit | edit source]
什么是访问器?
访问器是一个不修改对象状态的成员函数。访问器函数应声明为const
Getter是访问器的另一个常见定义,因为这种类型的成员函数的命名(GetSize())。
什么是修改器?
修改器,也称为修改函数,是一个至少更改一个数据成员值的成员函数。换句话说,修改对象状态的操作。修改器也称为“变异器”。
Setter是修改器的另一个常见定义,因为这种类型的成员函数的命名(SetSize( int a_Size ))。

注意
这些是常用的引用标签(在标准语言中没有定义)。

动态多态性(覆盖)

[edit | edit source]

到目前为止,我们已经了解到可以通过继承向类添加新的数据和函数。但是,如果我们想让派生类继承基类的方法,但要使用不同的实现呢?这就是我们谈论多态性的时候,它是 OOP 编程中的一个基本概念。

如之前在编程范式部分中所见,多态性细分为两个概念静态多态性动态多态性。本节重点介绍动态多态性,它在派生类覆盖基类中声明的函数时适用于 C++。

我们在派生类中重新定义方法来实现此概念。但是,我们在这样做时需要考虑一些因素,因此现在必须介绍动态绑定、静态绑定和虚拟方法的概念。

假设我们有两个类,ABB派生自A,并重新定义了类A中存在的方法c()的实现。现在假设我们有一个类B的对象b。如何解释指令b.c()

如果b在堆栈中声明(没有声明为指针或引用),编译器将应用静态绑定,这意味着它会(在编译时)解释我们指的是B中存在c()的实现。

然而,如果我们声明b为类A的指针或引用,编译器在编译时将无法知道调用哪个方法,因为b可以是AB类型。如果在运行时解决这个问题,将调用B中驻留的方法。这称为动态绑定。如果在编译时解决这个问题,将调用A中驻留的方法。这同样是静态绑定。

虚拟成员函数

[编辑 | 编辑源代码]

虚拟成员函数相对简单,但经常被误解。该概念是设计类层次结构的必要部分,涉及子类化类,因为它决定了特定上下文中重写方法的行为。

虚拟成员函数是类成员函数,可以在从声明它们的类派生的任何类中重写。然后用派生类中的一组新的实现替换成员函数体。

注意
重写虚拟函数时,可以更改派生类成员函数的私有、受保护或公共状态访问状态。

通过在方法声明之前放置关键字虚拟,我们指示当编译器必须决定是应用静态绑定还是动态绑定时,它将应用动态绑定。否则,将应用静态绑定。

注意
虽然在子类定义中使用虚拟关键字不是必需的(因为如果基类函数是虚拟的,那么它所有子类的覆盖也将是虚拟的),但在为将来的重复使用(用于同一项目之外)生成代码时,这样做是一种良好的风格。

再说一次,通过一个例子应该更清楚

class Foo
{
public:
  void f()
  {
    std::cout << "Foo::f()" << std::endl;
  }
  virtual void g()
  {
    std::cout << "Foo::g()" << std::endl;
  }
};
 
class Bar : public Foo
{
public:
  void f()
  {
    std::cout << "Bar::f()" << std::endl;
  }
  virtual void g()
  {
    std::cout << "Bar::g()" << std::endl;
  }
};
 
int main()
{
  Foo foo;
  Bar bar;

  Foo *baz = &bar;
  Bar *quux = &bar;

  foo.f(); // "Foo::f()"
  foo.g(); // "Foo::g()"
 
  bar.f(); // "Bar::f()"
  bar.g(); // "Bar::g()"

  // So far everything we would expect...
 
  baz->f();  // "Foo::f()"
  baz->g();  // "Bar::g()"

  quux->f(); // "Bar::f()"
  quux->g(); // "Bar::g()"
 
  return 0;
}

我们对f()g()在两个对象上的第一次调用很简单。然而,事情在我们的 baz 指针变得有趣,它是一个指向 Foo 类型的指针。

f()不是虚拟因此,对f()的调用将始终调用与指针类型关联的实现——在本例中是从 Foo 的实现。

注意
请记住,重载重写是不同的概念。

虚拟函数调用在计算上比常规函数调用更昂贵。虚拟函数使用指针间接寻址、调用,并且需要比普通成员函数多执行几条指令。它们还要求包含虚拟函数的任何类/结构的构造函数初始化一个指向其虚拟成员函数的指针表。

所有这些特性都将表明性能和设计之间的权衡。应该避免在没有现有结构需求的情况下先发制人地声明函数为虚拟。请记住,只有在运行时才能解析的虚拟函数无法内联。


Clipboard

待办事项
虚拟和内联问题示例。


注意
使用类模板可以解决使用虚拟函数的一些需求。当我们介绍模板时,将涵盖这一点。

纯虚拟成员函数

[编辑 | 编辑源代码]

还有一种有趣的可能性。有时我们根本不想提供函数的实现,而是想要求对我们的类进行子类化的人自己提供实现。这就是纯虚拟的情况。

为了指示纯虚拟函数而不是实现,我们只需在函数声明后添加一个“= 0”。

再说一次——一个例子

class Widget
{
public:
   virtual void paint() = 0;
};

class Button : public Widget
{
public:
   void paint() // is virtual because it is an override
   {
       // do some stuff to draw a button
   }
};

因为paint()是纯虚拟函数在 Widget类中,我们必须在所有具体子类中提供实现。如果我们不这样做,编译器将在构建时给出错误。

这对于提供接口很有帮助——我们期望从基于特定层次结构的所有对象中获得的东西,但当我们想要忽略实现细节时。

那么,这有什么用呢?

让我们以我们上面的例子为例,我们有一个纯虚拟用于绘画。在很多情况下,我们希望能够使用小部件而不必担心它是什么类型的小部件。绘画是一个简单的例子。

假设我们的应用程序中有一些东西会在小部件变为活动时重新绘制它们。它只会使用指向小部件的指针——即Widget *activeWidget() const可能是一个可能的函数签名。因此,我们可能会做类似的事情

Widget *w = window->activeWidget();
w->paint();

我们实际上想要调用适合“真实”小部件类型的 paint 成员函数——而不是Widget::paint()(这是一个“纯”虚拟,如果使用虚拟分派调用,会导致程序崩溃)。通过使用虚拟函数,我们确保调用子类的成员函数实现——Button::paint()在本例中——将被调用。


Clipboard

待办事项
提到接口类


协变返回类型

[编辑 | 编辑源代码]

协变返回类型是派生类中的虚拟函数能够返回指向自身实例的指针或引用,如果基类中的方法版本也这样做。例如

class base
{
public:
  virtual base* create() const;
};

class derived : public base
{
public:
  virtual derived* create() const;
};

这允许避免强制转换。

注意
一些较旧的编译器不支持协变返回类型。对于此类编译器,存在解决方法。

虚拟构造函数

[编辑 | 编辑源代码]

有一个以基类Foo为基础的类层次结构。给定一个属于该层次结构的对象bar,希望能够执行以下操作

  1. 创建一个对象baz,该对象与bar(例如,类Bar)相同,使用该类的默认构造函数进行初始化。通常使用的语法是
    Bar* baz = bar.create();
  2. 创建一个对象baz,该对象与bar,它是bar的副本。通常使用的语法是
    Bar* baz = bar.clone();

在类Foo中,方法Foo::create()Foo::clone()声明如下

class Foo
{
    // ...

    public:
        // Virtual default constructor
        virtual Foo* create() const;

        // Virtual copy constructor
        virtual Foo* clone() const;
};

如果Foo将用作抽象类,则这些函数可以被设置为纯虚拟

class Foo
{
   // ...

    public:
        virtual Foo* create() const = 0;
        virtual Foo* clone() const = 0;
};

为了支持创建默认初始化的对象和创建副本对象,每个类Bar在层次结构中都必须具有公有的默认构造函数和复制构造函数。Bar的虚拟构造函数定义如下

class Bar : ... // Bar is a descendant of Foo
{
    // ...

    public:
    // Non-virtual default constructor
    Bar ();
    // Non-virtual copy constructor
    Bar (const Bar&);

    // Virtual default constructor, inline implementation
    Bar* create() const { return new Foo (); }
    // Virtual copy constructor, inline implementation
    Bar* clone() const { return new Foo (*this); }
};

上面的代码使用协变返回类型。如果您的编译器不支持Bar* Bar::create(),请使用Foo* Bar::create()代替,对于clone().

也是如此。在使用这些虚拟构造函数时,您必须通过调用delete baz;手动释放创建的对象。如果在返回类型中使用智能指针(例如std::unique_ptr<Foo>)而不是普通的Foo*.

,可以避免这种麻烦。请记住,无论Foo是否使用动态分配的内存,您必须定义析构函数virtual ~Foo ()并将其设置为虚拟,以处理使用指向祖先类型的指针的对象的释放。

虚拟析构函数

[编辑 | 编辑源代码]

尤其要记住在任何基类中定义虚拟析构函数,即使它为空,因为如果不这样做,将导致编译器生成的默认析构函数出现问题,该析构函数将不是虚拟的。

在派生类中重新定义时,不会重写虚拟析构函数,每个析构函数的定义是累积的,它们从最后一个派生类开始,一直到第一个基类。

纯虚拟析构函数

[编辑 | 编辑源代码]

每个抽象类都应包含纯虚拟析构函数的声明。

纯虚拟析构函数是纯虚拟函数(旨在在派生类中重写)的特例。它们必须始终被定义,并且该定义应始终为空。

class Interface {
public:
  virtual ~Interface() = 0; //declaration of a pure virtual destructor 
};

Interface::~Interface(){} //pure virtual destructor definition (should always be empty)

三法则

[编辑 | 编辑源代码]

“三法则”并不真正是一条法则,而是一项准则:如果类需要显式声明的复制构造函数、复制赋值运算符或析构函数,那么它通常需要这三者。

这个规则有一些例外(或者,换个角度来说,是改进)。例如,有时会显式声明析构函数只是为了使其成为virtual;在这种情况下,不一定需要声明或实现复制构造函数和复制赋值运算符。

大多数类不应该声明任何“三大”操作;管理资源的类通常需要这三者。

子包容属性

[编辑 | 编辑源代码]

子类型化是所有位于类层次结构中的对象都必须满足的属性:基类的对象可以被从它派生的对象替换(直接或间接)。所有哺乳动物都是动物(它们是从动物派生的),所有猫都是哺乳动物。因此,由于子类型化属性,我们可以将任何哺乳动物“视为”动物,将任何猫“视为”哺乳动物。这意味着抽象,因为当我们将哺乳动物“视为”动物时,我们应该知道的唯一信息是它活着,它生长等等,但与哺乳动物无关的信息除外。

当我们使用指向位于类层次结构中的对象的指针或引用时,将在 C++ 中应用此属性。换句话说,指向动物类的指针可以指向动物类、哺乳动物类或猫类的对象。

让我们继续我们的例子

 //needs to be corrected
  enum AnimalType {
       Herbivore,
       Carnivore,
       Omnivore,
  };
 
  class Animal {
        public:
               AnimalType Type;
               bool bIsAlive;
               int iNumberOfChildren;
  };
 
  class Mammal : public Animal{
        public:
               int iNumberOfTeats;
  };
 
  class Cat : public Mammal{
        public:
              bool bLikesFish;  // probably true
  };
 
  int main() {
      Animal* pA1 = new Animal;
      Animal* pA2 = new Mammal;
      Animal* pA3 = new Cat;
      Mammal* pM  = new Cat;
      
      pA2->bIsAlive = true;     // Correct
      pA2->Type = Herbivore;    // Correct
      pM->iNumberOfTeats = 2;   // Correct
 
      pA2->iNumberOfTeats = 6;  // Incorrect
      pA3->bLikesFish = true;   // Incorrect
      
      Cat* pC = (Cat*)pA3;      // Downcast, correct (but very poor practice, see later)
      pC->bLikesFish = false;   // Correct (although it is a very awkward cat)
   }

在示例的最后几行中,有一个指向动物的指针强制转换为指向的指针。这被称为“向下强制转换”。向下强制转换是有用的,应该使用,但首先我们必须确保我们正在强制转换的对象确实是我们要强制转换到的类型。将基类向下强制转换到不相关的类是一个错误。为了解决这个问题,应该使用强制转换运算符dynamic_cast<> 或 static_cast<>。它们会正确地将对象从一个类强制转换为另一个类,如果类类型不相关,则会抛出异常。例如,如果你尝试

Cat* pC = new Cat;

motorbike* pM = dynamic_cast<motorbike*>(pC);

那么应用程序将抛出异常,因为猫不是摩托车。Static_cast 非常相似,只是它会在编译时执行类型检查。如果您有一个不确定其类型的对象,那么您应该使用dynamic_cast,并准备好处理强制转换时的错误。如果您正在向下强制转换您知道类型的对象,那么您应该使用static_cast。不要使用旧式的 C 强制转换,因为如果强制转换的类型不相关,它们只会给你一个访问冲突。

局部类

[编辑 | 编辑源代码]

局部类是指在特定语句块中定义的任何类,例如在函数中。这就像定义任何其他类一样,但是局部类不能访问非静态局部变量,也不能用于定义静态数据成员。这些类型的类在模板函数中特别有用,我们将在后面看到。

void MyFunction()
{
   class LocalClass
   {
   // ... members definitions ...
   };
   
   // ... any code that needs the class ...

}

用户定义的自动类型转换

[编辑 | 编辑源代码]

我们已经介绍了自动类型转换(隐式转换),并提到了一些可以由用户定义。

从一个类到另一个类的用户定义的转换可以通过在目标类中提供一个将源类作为参数的构造函数来完成,Target(const Source& a_Class) 或者通过为目标类提供一个转换运算符,如 operator Source()

确保类的对象永远不会被复制

[编辑 | 编辑源代码]

这需要例如防止内存相关问题,这些问题会在默认的复制构造函数或默认的赋值运算符意外应用于类的情况下导致。C它使用动态分配的内存,其中复制构造函数和赋值运算符可能是一种过度设计,因为它们不会经常被使用。

一些样式指南建议默认情况下使所有类不可复制,并且只有在有意义的情况下才启用复制。其他(不好的)指南说你应该始终明确编写复制构造函数和复制赋值运算符;这实际上是一个糟糕的主意,因为它增加了维护工作量,增加了阅读类的工作量,比使用隐式声明的运算符更容易引入错误,并且对大多数对象类型来说没有意义。一个明智的指南是思考复制对类型是否有意义;如果有意义,那么首先更喜欢安排编译器生成的复制操作能做正确的事情(例如,通过资源管理类而不是通过原始指针或句柄来保存所有资源),如果这不可行,那么遵守三法则。如果复制没有意义,您可以通过以下两种惯用法中的任何一种来禁止它。

只需声明复制构造函数和赋值运算符,并使它们private. 不要定义它们。由于它们不是protectedpublic,因此它们在类外部不可访问。在类内部使用它们会给出链接器错误,因为它们没有定义。

class C
{
  ...
 
  private:
    // Not defined anywhere
    C (const C&);
    C& operator= (const C&);
};

请记住,如果类为数据成员使用动态分配的内存,则必须在析构函数中定义内存释放过程~C ()来释放已分配的内存。

一个只声明了这两个函数的类可以用作私有基类,因此所有私有继承该类的类都将禁止复制。

注意
Boost 库的一部分,实用类boost:noncopyable执行类似的功能,更易于使用,但由于需要派生,会增加成本。

容器类

[编辑 | 编辑源代码]

一个用于在内存或外部存储中保存对象的类通常被称为容器类。容器类充当通用持有者,具有预定义的行为和众所周知的接口。它也是一个支持类,其目的是隐藏用于维护内存中对象列表的拓扑。当它包含一组混合对象时,容器被称为异构容器;当容器保存一组完全相同的对象时,容器被称为同构容器。

接口类

[编辑 | 编辑源代码]
Clipboard

待办事项
完整


单例类

[编辑 | 编辑源代码]

单例 类是一个只能实例化一次的类(类似于静态变量或函数的使用)。它是创建模式的可能实现之一,在本书的设计模式部分中得到了全面介绍。

华夏公益教科书