8 ODB 继承 (Inheritance)

In C++ inheritance can be used to achieve two different goals. We can employ inheritance to reuse common data and functionality in multiple classes. For example:

在c++中,继承可以用来实现两个不同的目标。我们可以使用继承来重用多个类中的公共数据和功能。例如:

class person
{
public:
  const std::string& first () const;
  const std::string& last () const;

private:
  std::string first_;
  std::string last_;
};

class employee: public person
{
  ...
};

class contractor: public person
{
  ...
};

In the above example both the employee and contractor classes inherit the first_ and last_ data members as well as the first() and last() accessors from the person base class.在上面的例子中,employee类和contractor类都继承了first_和last_ data成员以及person基类的first()和last()访问器。A common trait of this inheritance style, referred to as reuse inheritance from now on, is the lack of virtual functions and a virtual destructor in the base class. Also with this style the application code is normally written in terms of the derived classes instead of the base.这种继承风格的一个共同特征(从现在开始称为重用继承)是基类中缺乏虚函数和虚析构函数。同样,在这种风格下,应用程序代码通常是根据派生类而不是基类编写的。The second way to utilize inheritance in C++ is to provide polymorphic behavior through a common interface. In this case the base class defines a number of virtual functions and, normally, a virtual destructor while the derived classes provide specific implementations of these virtual functions. For example:c++中利用继承的第二种方法是通过公共接口提供多态行为。在这种情况下,基类定义了许多虚函数,通常还定义了一个虚析构函数,而派生类则提供了这些虚函数的特定实现。例如:

class person
{
public:
  enum employment_status
  {
    unemployed,
    temporary,
    permanent,
    self_employed
  };

  virtual employment_status
  employment () const = 0;

  virtual
  ~person ();
};

class employee: public person
{
public:
  virtual employment_status
  employment () const
  {
    return temporary_ ? temporary : permanent;
  }

private:
  bool temporary_;
};

class contractor: public person
{
public:
  virtual employment_status
  employment () const
  {
    return self_employed;
  }
};

With this inheritance style, which we will call polymorphism inheritance, the application code normally works with derived classes via the base class interface. Note also that it is very common to mix both styles in the same hierarchy. For example, the above two code fragments can be combined so that the person base class provides the common data members and functions as well as defines the polymorphic interface.

使用这种继承风格(我们将其称为多态性继承),应用程序代码通常通过基类接口处理派生类。还请注意,在同一层次结构中混合两种样式是很常见的。例如,上面的两个代码片段可以组合在一起,这样person基类就可以提供公共数据成员和函数,并定义多态接口。

The following sections describe the available strategies for mapping reuse and polymorphism inheritance styles to a relational data model. Note also that the distinction between the two styles is conceptual rather than formal. For example, it is possible to treat a class hierarchy that defines virtual functions as a case of reuse inheritance if this results in the desired database mapping and semantics.

下面的部分描述了将重用和多态继承样式映射到关系数据模型的可用策略。还请注意,这两种风格之间的区别是概念上的,而不是形式上的。例如,可以将定义虚函数的类层次结构视为重用继承,如果这导致所需的数据库映射和语义。

Generally, classes that employ reuse inheritance are mapped to completely independent entities in the database. They use different object id spaces and should always be passed to and returned from the database operations as pointers or references to derived types. In other words, from the persistence point of view, such classes behave as if the data members from the base classes were copied verbatim into the derived ones.

通常,使用重用继承的类被映射到数据库中完全独立的实体。它们使用不同的对象id空间,应该始终作为派生类型的指针或引用传递给数据库操作或从数据库操作返回。换句话说,从持久化的角度来看,这些类的行为就像是将基类中的数据成员逐字复制到派生类中一样。

In contrast, classes that employ polymorphism inheritance share the object id space and can be passed to and returned from the database operations polymorphically as pointers or references to the base class.

相反,使用多态性继承的类共享对象id空间,可以多态地作为基类的指针或引用传递给数据库操作,并从数据库操作中返回。

For both inheritance styles it is sometimes desirable to prevent instances of a base class from being stored in the database. To achieve this a persistent class can be declared abstract using the db abstract pragma (Section 14.1.3, "abstract"). Note that a C++-abstract class, or a class that has one or more pure virtual functions and therefore cannot be instantiated, is also database-abstract. However, a database-abstract class is not necessarily C++-abstract. The ODB compiler automatically treats C++-abstract classes as database-abstract.

对于这两种继承样式,有时最好避免将基类实例存储在数据库中。为了实现这一点,可以使用db abstract pragma(章节14.1.3,"abstract")将一个持久化类声明为抽象类。请注意,c++抽象类,或者具有一个或多个纯虚函数,因此不能实例化的类,也是数据库抽象类。然而,数据库抽象类不一定是c++抽象类。ODB编译器自动地将c++抽象类视为数据库抽象类

8.1 Reuse Inheritance 重用继承

Each non-abstract class from the reuse inheritance hierarchy is mapped to a separate database table that contains all its data members, including those inherited from base classes. An abstract persistent class does not have to define an object id, nor a default constructor, and it does not have a corresponding database table. An abstract class cannot be a pointed-to object in a relationship. Multiple inheritance is supported as long as each base class is only inherited once. The following example shows a persistent class hierarchy employing reuse inheritance:

重用继承层次结构中的每个非抽象类都映射到一个单独的数据库表,该表包含其所有数据成员,包括从基类继承的数据成员。抽象持久类不需要定义对象id,也不需要定义默认构造函数,也不需要相应的数据库表。抽象类不能是关系中的指向对象。只要每个基类只继承一次,就支持多重继承。下面的例子展示了一个使用重用继承的持久类层次结构:

// Abstract person class. Note that it does not declare the
// object id.
//
#pragma db object abstract
class person
{
  ...

  std::string first_;
  std::string last_;
};

// Abstract employee class. It derives from the person class and
// declares the object id for all the concrete employee types.
//
#pragma db object abstract
class employee: public person
{
  ...

  #pragma db id auto
  unsigned long id_;
};

// Concrete permanent_employee class. Note that it doesn't define
// any data members of its own.
//
#pragma db object
class permanent_employee: public employee
{
  ...
};

// Concrete temporary_employee class. It adds the employment
// duration in months.
//
#pragma db object
class temporary_employee: public employee
{
  ...

  unsigned long duration_;
};

// Concrete contractor class. It derives from the person class
// (and not employee; an independent contractor is not considered
// an employee). We use the contractor's external email address
// as the object id.
//
#pragma db object
class contractor: public person
{
  ...

  #pragma db id
  std::string email_;
};

The sample database schema for this hierarchy is shown below.

CREATE TABLE permanent_employee (
  first TEXT NOT NULL,
  last TEXT NOT NULL,
  id BIGINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT);

CREATE TABLE temporary_employee (
  first TEXT NOT NULL,
  last TEXT NOT NULL,
  id BIGINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
  duration BIGINT UNSIGNED NOT NULL);

CREATE TABLE contractor (
  first TEXT NOT NULL,
  last TEXT NOT NULL,
  email VARCHAR (255) NOT NULL PRIMARY KEY);

The complete version of the code presented in this section is available in the inheritance/reuse example in the odb-examples package.

本节提供的代码的完整版本可以在odb-examples包中的继承/重用示例中找到。

8.2 Polymorphism Inheritance多态继承

There are three general approaches to mapping a polymorphic class hierarchy to a relational database. These are table-per-hierarchy, table-per-difference, and table-per-class. With the table-per-hierarchy mapping, all the classes in a hierarchy are stored in a single, "wide" table. NULL values are stored in columns corresponding to data members of derived classes that are not present in any particular instance.

将多态类层次结构映射到关系数据库有三种一般的方法。它们是每个层次的表、每个差异的表和每个类的表。使用逐表层次结构映射,层次结构中的所有类都存储在一个单一的“宽”表中。NULL值存储在与任何特定实例中不存在的派生类的数据成员对应的列中。

In the table-per-difference mapping, each class is mapped to a separate table. For a derived class, this table contains only columns corresponding to the data members added by this derived class.

在逐差表映射中,每个类映射到一个单独的表。对于派生类,此表仅包含与该派生类添加的数据成员对应的列。

Finally, in the table-per-class mapping, each class is mapped to a separate table. For a derived class, this table contains columns corresponding to all the data members, from this derived class all the way down to the root of the hierarchy.

最后,在逐类表映射中,每个类映射到一个单独的表。对于派生类,该表包含与所有数据成员相对应的列,从派生类一直到层次结构的根。

The table-per-difference mapping is generally considered as having the best balance of flexibility, performance, and space efficiency. It also results in a more canonical relational database model compared to the other two approaches. As a result, this is the mapping currently implemented in ODB. Other mappings may be supported in the future.

通常认为逐差表映射具有灵活性、性能和空间效率的最佳平衡。与其他两种方法相比,它还产生了更规范的关系数据库模型。因此,这就是当前在ODB中实现的映射。将来可能会支持其他映射。

A pointer or reference to an ordinary, non-polymorphic object has just one type — the class type of that object. When we start working with polymorphic objects, there are two types to consider: the static type, or the declaration type of a reference or pointer, and the object's actual or dynamic type. An example will help illustrate the difference:

指向普通非多态对象的指针或引用只有一种类型——该对象的类类型。当我们开始处理多态对象时,有两种类型需要考虑:静态类型,或引用或指针的声明类型,以及对象的实际或动态类型。下面的例子将有助于说明两者的区别:

class person {...};
class employee: public person {...};

person p;
employee e;

person& r1 (p);
person& r2 (e);

auto_ptr<person> p1 (new employee);

In the above example, the r1 reference's both static and dynamic types are person. In contrast, the r2 reference's static type is person while its dynamic type (the actual object that it refers to) is employee. Similarly, p1 points to the object of the person static type but employee dynamic type.

在上面的例子中,r1引用的静态和动态类型都是person。相反,r2引用的静态类型是person,而它的动态类型(它引用的实际对象)是employee。类似地,p1指向person静态类型但employee动态类型的对象。

In C++, the primary mechanisms for working with polymorphic objects are virtual functions. We call a virtual function only knowing the object's static type, but the version corresponding to the object's dynamic type is automatically executed. This is the essence of runtime polymorphism support in C++: we can operate in terms of a base class interface but get the derived class' behavior. Similarly, the essence of the runtime polymorphism support in ODB is to allow us to persist, load, update, and query in terms of the base class interface but have the derived class actually stored in the database.

在c++中,处理多态对象的主要机制是虚函数。调用虚函数时,只知道对象的静态类型,但会自动执行与对象的动态类型对应的版本。这是c++中运行时多态性支持的本质:我们可以根据基类接口进行操作,但获得派生类的行为。类似地,ODB中运行时多态性支持的本质是允许我们根据基类接口持久化、加载、更新和查询,但将派生类实际存储在数据库中。

To declare a persistent class as polymorphic we use the db polymorphic pragma. We only need to declare the root class of a hierarchy as polymorphic; ODB will treat all the derived classes as polymorphic automatically. For example:

要将一个持久化类声明为多态类,我们使用db polymorphic pragma。我们只需要声明一个层次结构的根类是多态的;ODB将自动地将所有派生类视为多态类。例如:

#pragma db object polymorphic
class person
{
  ...

  virtual
  ~person () = 0; // Automatically abstract.

  #pragma db id auto
  unsigned long id_;

  std::string first_;
  std::string last_;
};

#pragma db object
class employee: public person
{
  ...

  bool temporary_;
};

#pragma db object
class contractor: public person
{

  std::string email_;
};

A persistent class hierarchy declared polymorphic must also be polymorphic in the C++ sense, that is, the root class must declare or inherit at least one virtual function. It is recommended that the root class also declares a virtual destructor. The root class of the polymorphic hierarchy must contain the data member designated as object id (a persistent class without an object id cannot be polymorphic). Note also that, unlike reuse inheritance, abstract polymorphic classes have a table in the database, just like non-abstract classes.

虚函数。建议根类也声明一个虚析构函数。多态层次结构的根类必须包含指定为对象id的数据成员(没有对象id的持久类不能是多态的)还要注意,与重用继承不同,抽象多态类在数据库中有一个表,就像非抽象类一样。

Persistent classes in the same polymorphic hierarchy must use the same kind of object pointer (Section 3.3, "Object and View Pointers"). If the object pointer for the root class is specified as a template or using the special raw pointer syntax (*), then the ODB compiler will automatically use the same object pointer for all the derived classes. For example:

相同多态层次结构中的持久化类必须使用相同类型的对象指针(第3.3节,“对象和视图指针”)。如果根类的对象指针被指定为模板或使用特殊的原始指针语法(*),那么ODB编译器将自动为所有派生类使用相同的对象指针。例如:

#pragma db object polymorphic pointer(std::shared_ptr)
class person
{
  ...
};

#pragma db object // Object pointer is std::shared_ptr<employee>.
class employee: public person
{
  ...
};

#pragma db object // Object pointer is std::shared_ptr<contractor>.
class contractor: public person
{
  ...
};

Similarly, if we enable or disable session support (Chapter 11, "Session") for the root class, then the ODB compiler will automatically enable or disable it for all the derived classes.

类似地,如果我们为根类启用或禁用会话支持(第11章,“session”),那么ODB编译器将自动为所有派生类启用或禁用会话支持。

For polymorphic persistent classes, all the database operations can be performed on objects with different static and dynamic types. Similarly, operations that load persistent objects from the database (load(), query(), etc.), can return objects with different static and dynamic types. For example:

对于多态持久类,所有的数据库操作都可以在具有不同静态和动态类型的对象上执行。类似地,从数据库加载持久对象的操作(load()、query()等)可以返回不同静态和动态类型的对象。例如:

unsigned long id1, id2;

// Persist.
//
{
  shared_ptr<person> p1 (new employee (...));
  shared_ptr<person> p2 (new contractor (...));

  transaction t (db.begin ());
  id1 = db.persist (p1); // Stores employee.
  id2 = db.persist (p2); // Stores contractor.
  t.commit ();
}

// Load.
//
{
  shared_ptr<person> p;

  transaction t (db.begin ());
  p = db.load<person> (id1); // Loads employee.
  p = db.load<person> (id2); // Loads contractor.
  t.commit ();
}

// Query.
//
{
  typedef odb::query<person> query;
  typedef odb::result<person> result;

  transaction t (db.begin ());

  result r (db.query<person> (query::last == "Doe"));

  for (result::iterator i (r.begin ()); i != r.end (); ++i)
  {
    person& p (*i); // Can be employee or contractor.
  }

  t.commit ();
}

// Update.
//
{
  shared_ptr<person> p;
  shared_ptr<employee> e;

  transaction t (db.begin ());

  e = db.load<employee> (id1);
  e->temporary (false);
  p = e;
  db.update (p); // Updates employee.

  t.commit ();
}

// Erase.
//
{
  shared_ptr<person> p;

  transaction t (db.begin ());
  p = db.load<person> (id1); // Loads employee.
  db.erase (p);              // Erases employee.
  db.erase<person> (id2);    // Erases contractor.
  t.commit ();
}

in addition to those corresponding to the data members. The first, called discriminator, is added to the table corresponding to the root class of the hierarchy. This column is used to determine the dynamic type of each object. The second column is added to tables corresponding to the derived classes and contains the object id. This column is used to form a foreign key constraint referencing the root class table.

除了那些与数据成员相对应的。第一个被称为辨别者,添加到与层次结构的根类对应的表中。此列用于确定每个对象的动态类型。第二列添加到与派生类对应的表中,并包含对象id。此列用于形成引用根类表的外键约束。

When querying the database for polymorphic objects, it is possible to obtain the discriminator value without instantiating the object. For example:

当查询数据库的多态对象时,可以在不实例化对象的情况下获得discriminator的值。例如:

typedef odb::query<person> query;
typedef odb::result<person> result;

transaction t (db.begin ());

result r (db.query<person> (query::last == "Doe"));

for (result::iterator i (r.begin ()); i != r.end (); ++i)
{
  std::string d (i.discriminator ());
  ...
}

t.commit ();

In the current implementation, ODB has limited support for customizing names, types, and values of the extra columns. Currently, the discriminator column is always called typeid and contains a namespace-qualified class name (for example, "employee" or "hr::employee"). The id column in the derived class table has the same name as the object id column in the root class table. Future versions of ODB will add support for customizing these extra columns.

在当前实现中,ODB对定制额外列的名称、类型和值的支持有限。目前,标识符列总是被称为typeid,并包含一个名称空间限定的类名(例如,“employee”或“hr::employee”)。派生类表中的id列与根类表中的对象id列具有相同的名称。ODB的未来版本将增加对定制这些额外列的支持。

The sample database schema for the above polymorphic hierarchy is shown below.

上述多态层次结构的样本数据库模式如下所示。

CREATE TABLE person (
  id BIGINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
  typeid VARCHAR(255) NOT NULL,
  first TEXT NOT NULL,
  last TEXT NOT NULL);

CREATE TABLE employee (
  id BIGINT UNSIGNED NOT NULL PRIMARY KEY,
  temporary TINYINT(1) NOT NULL,

  CONSTRAINT employee_id_fk
    FOREIGN KEY (id)
    REFERENCES person (id)
    ON DELETE CASCADE);

CREATE TABLE contractor (
  id BIGINT UNSIGNED NOT NULL PRIMARY KEY,
  email TEXT NOT NULL,

  CONSTRAINT contractor_id_fk
    FOREIGN KEY (id)
    REFERENCES person (id)
    ON DELETE CASCADE);

The complete version of the code presented in this section is available in the inheritance/polymorphism example in the odb-examples package.

8.2.1 Performance and Limitations性能和限制

A database operation on a non-polymorphic object normally translates to a single database statement execution (objects with containers and eager object pointers can be the exception). Because polymorphic objects have their data members stored in multiple tables, some database operations on such objects may result in multiple database statements being executed while others may require more complex statements. There is also some functionality that is not available to polymorphic objects.

非多态对象上的数据库操作通常转换为单个数据库语句的执行(带有容器和即时对象指针的对象除外)。因为多态对象的数据成员存储在多个表中,对这些对象的一些数据库操作可能导致执行多个数据库语句,而其他对象可能需要更复杂的语句。还有一些功能是多态对象不可用的。

The first part of this section discusses the performance implications to keep in mind when designing and working with polymorphic hierarchies. The second part talks about limitations of polymorphic objects.

本节的第一部分讨论在设计和使用多态层次结构时要牢记的性能影响。第二部分讨论了多态对象的限制。

The most important aspect of a polymorphic hierarchy that affects database performance is its depth. The distance between the root of the hierarchy and the derived class translates directly to the number of database statements that will have to be executed in order to persist, update, or erase this derived class. It also translates directly to the number of SQL JOIN clauses that will be needed to load or query the database for this derived class. As a result, to achieve best performance, we should try to keep our polymorphic hierarchies as flat as possible.

影响数据库性能的多态层次结构最重要的方面是它的深度。层次结构根与派生类之间的距离直接转换为为了持久化、更新或删除这个派生类而必须执行的数据库语句的数量。它还直接转换为加载或查询此派生类的数据库所需的SQL JOIN子句的数量。因此,为了获得最好的性能,我们应该尽量保持我们的多态层次结构尽可能的扁平。

When loading an object or querying the database for objects, ODB will need to execute two statements if this object's static and dynamic types are different but only one statement if they are the same. This example will help illustrate the difference:

当加载对象或查询数据库中的对象时,如果对象的静态和动态类型不同,ODB将需要执行两条语句,但如果对象的静态和动态类型相同,则只需要执行一条语句。下面这个例子将有助于说明两者的区别:

unsigned long id;

{
  employee e (...);

  transaction t (db.begin ());
  id = db.persist (e);
  t.commit ();
}

{
  shared_ptr<person> p;

  transaction t (db.begin ());
  p = db.load<person> (id);   // Requires two statement.
  p = db.load<employee> (id); // Requires only one statement.
  t.commit ();
}

As a result, we should try to load and query using the most derived class possible.

因此,我们应该尝试使用尽可能多的派生类来加载和查询

Finally, for polymorphic objects, erasing via the object instance is faster than erasing via its object id. In the former case the object's dynamic type can be determined locally in the application while in the latter case an extra statement has to be executed to achieve the same result. For example:

最后,对于多态对象,通过对象实例擦除要比通过对象id擦除快在前一种情况下,对象的动态类型可以在应用程序中本地确定,而在后一种情况下,必须执行额外的语句才能达到相同的结果。例如:

shared_ptr<person> p = ...;

transaction t (db.begin ());
db.erase<person> (p.id ()); // Slower (executes extra statement).
db.erase (p);               // Faster.
t.commit ();

Polymorphic objects can use all the mechanisms that are available to ordinary objects. These include containers (Chapter 5, "Containers"), object relationships, including to polymorphic objects (Chapter 6, "Relationships"), views (Chapter 10, "Views"), session (Chapter 11, "Session"), and optimistic concurrency (Chapter 12, "Optimistic Concurrency"). There are, however, a few limitations, mainly due to the underlying use of SQL to access the data.

多态对象可以使用普通对象可用的所有机制。这些包括容器(第5章,“容器”),对象关系,包括多态对象(第6章,“关系”),视图(第10章,“视图”),会话(第11章,“会话”),以及乐观并发(第12章,“乐观并发”)。但是,有一些限制,主要是因为底层使用SQL访问数据。

When a polymorphic object is "joined" in a view, and the join condition (either in the form of an object pointer or a custom condition) comes from the object itself (as opposed to one of the objects joined previously), then this condition must only use data members from the derived class. For example, consider the following polymorphic object hierarchy and a view:

当一个多态对象是“加入”的观点,以及联接条件(对象指针的形式或一个自定义条件)来自于对象本身(而不是一个对象加入之前),那么这种情况必须只使用派生类的数据成员例如,考虑以下多态对象层次结构和视图:

#pragma db object polymorphic
class employee
{
  ...
};

#pragma db object
class permanent_employee: public employee
{
  ...
};

#pragma db object
class temporary_employee: public employee
{
  ...

  shared_ptr<permanent_employee> manager_;
};

#pragma db object
class contractor: public temporary_employee
{
  shared_ptr<permanent_employee> manager_;
};

#pragma db view object(permanent_employee) \
                object(contractor: contractor::manager_)
struct contractor_manager
{
  ...
};

This view will not function correctly because the join condition (manager_) comes from the base class (temporary_employee) instead of the derived (contractor). The reason for this limitation is the JOIN clause order in the underlying SQL SELECT statement. In the view presented above, the table corresponding to the base class (temporary_employee) will have to be joined first which will result in this view matching both the temporary_employee and contractor objects instead of just contractor. It is usually possible to resolve this issue by reordering the objects in the view. Our example, for instance, can be fixed by swapping the two objects:

这个视图不能正常工作,因为连接条件(manager_)来自基类(temporary_employee),而不是派生类(contractor)。这个限制的原因是底层SQL SELECT语句中的JOIN子句顺序。在上面的视图中,对应于基类(temporary_employee)的表必须首先被连接,这将导致这个视图同时匹配temporary_employee和contractor对象,而不仅仅是contractor对象。通常可以通过重新排序视图中的对象来解决这个问题。例如,我们的例子可以通过交换两个对象来修复:

#pragma db view object(contractor) \
                object(permanent_employee: contractor::manager_)
struct contractor_manager
{
  ...
};

The erase_query() database function (Section 3.11, "Deleting Persistent Objects") also has limited functionality when used on polymorphic objects. Because many database implementations do not support JOIN clauses in the SQL DELETE statement, only data members from the derived class being erased can be used in the query condition. For example:

erase_query()数据库函数(章节3.11,“删除持久化对象”)在多态对象上使用时也有一定的功能限制。因为许多数据库实现在SQL DELETE语句中不支持JOIN子句,所以只有派生类中被擦除的数据成员才能在查询条件中使用。例如:

typedef odb::query<employee> query;

transaction t (db.begin ());
db.erase_query<employee> (query::permanent);     // Ok.
db.erase_query<employee> (query::last == "Doe"); // Error.
t.commit ();

8.3 Mixed Inheritance混合继承

It is possible to mix the reuse and polymorphism inheritance styles in the same hierarchy. In this case, the reuse inheritance must be used for the "bottom" (base) part of the hierarchy while the polymorphism inheritance — for the "top" (derived) part. For example:

可以在同一层次结构中混合重用和多态继承样式。在这种情况下,重用继承必须用于层次结构的“底部”(基础)部分,而多态性继承-用于“顶部”(派生)部分例如:

#pragma db object
class person
{
  ...
};

#pragma db object polymorphic
class employee: public person // Reuse inheritance.
{
  ...
};

#pragma db object
class temporary_employee: public employee // Polymorphism inheritance.
{
  ...
};

#pragma db object
class permanent_employee: public employee // Polymorphism inheritance.
{
  ...
};

原文链接: https://www.cnblogs.com/CaiNiaoIceLee/p/15783062.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/184615

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年2月12日 上午10:46
下一篇 2023年2月12日 上午10:46

相关推荐