C++ primer 第十五章 面向对象程序设计
面向对象的程序设计(OOP)
OOP:概述
面向对象程序设计的核心思想是数据抽象、继承和动态绑定。
继承
通过继承联系在一起的类构成一种层次关系。通常在类的根部有一个基类,其他类则直接或间接地从基类继承而来,这些继承得到的类称为派生类。基类负责定义在层次关系中所有类共同拥有的成员,而每个派生类定义各自特有的成员。
在 C++ 语言中,基类将类型相关的函数与派生类不做改变直接继承的函数区分对待。对于某些函数,基类希望它的派生类各自定义适合自身的版本,此时基类就将这些函数声明成虚函数。
class Quote { public: std::string isbn() const; // 返回书籍的 isbn 编号 virtual double net_price(std::size_t n) const; // 返回书籍的实际销售价格,前提是用户购买该书的数量达到一定标准 };
派生类必须通过使用类派生列表明确指出它是从哪个(哪些)基类继承而来的。
class Bulk_quote : public Quote { // Bulk_quote 继承了 Quote public: double net_price(std::size_t) const override; }
派生类必须在其内部对所有重新定义的虚函数进行声明。派生类可以在这样的函数之前加上 virtual 关键字,但是并不是非得这么做。
C++11 新标准允许派生类显式地注明它将使用哪个成员函数改写基类的虚函数,具体措施是在该函数的形参列表之后增加一个 override 关键字。
动态绑定
通过使用动态绑定,我们能用同一段代码分别处理 Quote 和 Bulk_quote 的对象。
// 计算并打印销售给定数量的某种书籍所得的费用 double print_total(std::ostream &os, const Quote &item, std::size_t n) { // 根据传入 item 形参的对象类型调用 Quote::net_price // 或者 Bulk_quote::net_price double ret = item.net_price(n); os << "ISBN: " << item.isbn() << " # sold: " << n << " total due: " << ret << endl; return ret; } // basic 的类型是 Quote; bulk 的类型是 Bulk_quote print_total(cout, basic, 20); // 调用 Quote 的 net_price print_total(cout, bulk, 20); // 调用 Bulk_quote 的 net_price
因为在上述过程中函数的运行版本由实参决定,即在运行时选择函数的版本,所以动态绑定有时又被称为运行时绑定。
在 C++ 语言中,当我们使用基类的引用(或指针)调用一个虚函数时将发生动态绑定
定义基类和派生类
定义基类
我们首先完成 Quote 类的定义:
class Quote { public: Quote() = default; Quote(const std::string &book, double sales_price) : bookNo(book), price(sales_price) { } std::string isbn() const; // 返回书籍的 isbn 编号 // 返回给定数量的书籍的销售总额 // 派生类负责改写并使用不同的折扣计算算法 virtual double net_price(std::size_t n) const { return n * price; } virtual ~Quote() = default; // 对析构函数进行动态绑定 private: std::string bookNo; // 书籍的 ISBN 编号 protected: double price = 0.0; // 代表普通状态下不打折的价格 };
基类通常都应该定义一个虚析构函数,即使该函数不执行任何实际操作也是如此
成员函数与继承
基类通过在其成员函数的声明语句之前加上关键字 virtual 使得该函数执行动态绑定。
成员函数如果没有声明为虚函数,则其解析过程发生在编译时而非运行时。
访问控制与继承
我们用受保护的访问运算符说明这样的成员:基类希望它的派生类有权访问该成员,同时禁止其他用户访问。
定义派生类
派生类必须通过使用类派生列表明确指出它是从哪个(哪些)基类继承而来的。
派生类必须将其继承而来的成员函数中需要覆盖的那些重新声明,因此,我们的 Bulk_quote 类必须包含一个 net_price 成员。
class Bulk_quote : public Quote { // Bulk_quote 继承了 Quote public: Bulk_quote() = default; Bulk_quote(const std::string&, double, std::size_t, double); // 覆盖基类的函数版本以实现基于大量购买的折扣政策 double net_price(std::size_t) const override; private: std::size_t min_qty = 0; // 适用折扣政策的最低购买量 double discount = 0.0; // 以小数表示的折扣额 };
派生类中的虚函数
派生类经常(但不总是)覆盖它继承的虚函数。如果派生类没有覆盖其基类中的某个虚函数,则该虚函数的行为类似与其他的普通成员,派生类会直接继承其在基类中的版本。
派生类可以在它覆盖的函数前使用 virtual 关键字,但不是非得这么做。C++11 新标准允许派生类显式地注明它使用某个成员函数覆盖了它继承的虚函数。具体做法是在形参列表后面、或者在 const 成员函数的 const 关键字后面、或者在引用成员函数的引用限定符后面添加一个关键字 override。
派生类对象及派生类向基类的类型转换
一个派生类对象包含多个组成部分:一个含有派生类自己定义的(非静态)成员的子对象,以及一个与该派生类继承的基类对应的子对象,如果有多个基类,那么这样的子对象也有多个。
因为在派生类对象中含有与其基类对应的组成部分,所以我们能把派生类的对象当成基类对象来使用,而且我们也能将基类的指针或引用绑定到派生类对象中的基类部分上。
Quote item; // 基类对象 Bulk_quote bulk; // 派生类对象 Quote *p = &item; // p 指向 Quote 对象 p = &bulk; // p 指向 bulk 的 Quote 部分 Quote &r = bulk; // r 绑定到 bulk 的 Quote 部分
这种转换通常称为派生类到基类的类型转换。和其他类型转换一样,编译器会隐式地执行派生类到基类的转换。
在派生类对象中含有与其基类对应的组成部分,这一事实是继承的关键所在。
派生类的构造函数
每个类控制它自己的成员初始化过程
class Bulk_quote : public Quote { public: Bulk_quote(const std::string& book, double p, std::size_t qty, double disc) : Quote(book, p), min_qty(qty), discount(disc) { } };
首先初始化基类的部分,然后按照声明的顺序依次初始化派生类的成员。
派生类使用基类的成员
派生类可以访问基类的公有成员和受保护成员:
// 如果达到了购买书籍的某个最低限量值,就可以享受折扣价格了 double Bulk_quote::net_price(size_t cnt) const { if (cnt >= min_qty) return cnt * (1 - discount) * price; else return cnt * price; }
对于派生类的一个成员来说,它使用派生类成员的方式与使用基类成员的方式没什么不同。
继承与静态成员
如果基类定义了一个静态成员,则在整个继承体系中只存在该成员的唯一定义。不论从基类中派生出来多少个派生类,对于每个静态成员来说都只存在唯一的实例。
class Base { public: static void statmem(); }; class Derived : public Base { void f(const Derived&); };
静态成员遵循通用的访问控制规则,如果基类中的成员是 private 的,则派生类无权访问它。假设某静态成员是可访问的,则我们既能通过基类使用它也能通过派生类使用它。
void Derived::f(const Derived &derived_obj) { Base::statmem(); // 正确:Base 定义了 statmem Derived::statmem(); // 正确:Derived 继承了 statmem // 正确:派生类的对象能访问基类的静态成员 derived_obj.statmem(); // 通过 Derived 对象访问 statmem(); // 通过 this 对象访问 }
派生类的声明
派生类的声明与其他类差别不大,声明中包含类名但是不包含它的派生列表:
class Bulk_quote : public Quote; // 错误:派生列表不能出现在这里 class Bulk_quote; // 正确:声明派生类的正确方式
被用作基类的类
如果我们想将某个类用作基类,则该类必须已经定义而非仅仅声明:
class Quote; // 声明但未定义 // 错误:Quote 必须被定义 class Bulk_quote : public Quote { ... };
一个类是基类,同时它也可以是一个派生类:
class Base { /* ... */ }; class D1 : public Base { /* ... */ }; class D2 : public D1 { /* ... */ };
在这个继承关系中,Base 是 D1 的直接基类,同时是 D2 的间接基类。直接基类出现在派生列表中,而间接基类由派生类通过其直接基类继承而来。
防止继承的发生
有时我们会定义这样一种类,我们不希望其他类继承它,或者不想考虑它是否适合作为一个基类。为了实现这一目的,C++11 新标准提供了一种防止继承发生的方法,即在类名后跟一个关键字 final:
class NoDerived final { /* */ }; // NoDerived 不能作为基类 class Base { /* */ }; // Last 是 final 的;我们不能继承 Last class Last final : Base { /* */ }; // Last 不能作为基类 class Bad : NoDerived { /* */ }; // 错误:NoDerived 是 final 的 class Bad2 : Last { /* */ }; // 错误:Last 是 final 的
类型转换与继承
可以将基类的指针或引用绑定到派生类对象上有一层极为重要的含义:当使用基类的引用(或指针)时,,实际上我们并不清楚该引用(或指针)所绑定对象的真实类型。该对象可能是基类的对象,也可能是派生类的对象。
和内置指针一样,智能指针类也支持派生类向基类的类型转换,这意味着我们可以将一个派生类对象的指针存储在一个基类的智能指针内。
静态类型与动态类型
当我们使用存在继承关系的类型时,必须将一个变量或其他表达式的静态类型与该表达式表示对象的动态类型区分开来。表达式的静态类型在编译时总是已知的,它是变量声明时的类型或表达式生成的类型;动态类型则是变量或表达式表示的内存中的对象的类型。动态类型直到运行时才可知。
如果表达式既不是引用也不是指针,则它的动态类型永远与静态类型一致。
不存在从基类向派生类的隐式类型转换......
因为一个基类的对象可能是派生类对象的一部分,也可能不是,所以不存在从基类向派生类的自动类型转换:
Quote base; Bulk_quote *bulkP = &base; // 错误:不能将基类转换成派生类 Bulk_quote &bulkRef = base; // 错误:不能将基类转换成派生类
如果上述赋值是合法的,则我们有可能会使用 bulkP 或 bulkRef 访问 base 中本不存在的成员。
除此之外还有一种情况显得有点特别,即使一个基类指针或引用绑定在一个派生类对象上,我们也不能执行从基类向派生类的转换:
Bulk_quote bulk; Quote *itemP = &bulk; // 正确:动态类型是 Bulk_quote Bulk_quote *bulkP = itemP; // 错误:不能将基类转换成派生类
......在对象之间不存在类型转换
当我们用一个派生类对象为一个基类对象初始化或赋值时,只有该派生类对象中的基类部分会被拷贝、移动或赋值,它的派生类部分将被忽略掉。
虚函数
当我们使用基类的引用或指针调用一个虚成员函数时会执行动态绑定。因为我们直到运行时才能知道到底调用了哪个版本的虚函数,所以所有虚函数都必须有定义。
对虚函数的调用可能在运行时才被解析
当某个虚函数通过指针或引用调用时,编译器产生的代码直到运行时才能确定应该调用哪个版本的函数。被调用的函数是与绑定到指针或引用上的对象的动态类型相匹配的那一个。
Quote base("0-201-82470-1", 50); print_total(cout, base, 10); // 调用 Quote::net_price Bulk_quote derived("0-201-82470-1", 50, 5, .19); print_total(cout, derived, 10); // 调用 Bulk_quote::net_price
必须要搞清楚的一点是,动态绑定只有当我们通过指针或引用调用虚函数时才会发生
base = derived; // 把 derived 的 Quote 部分拷贝给 base base.net_price(20); // 调用 Quote::net_price
当且仅当对通过指针或引用调用虚函数时,才会在运行时解析该调用,也只有在这种情况下对象的动态类型才有可能与静态类型不同。
派生类中的虚函数
基类中的虚函数在派生类中隐含地也是一个虚函数。当派生类覆盖了某个虚函数时,该函数在基类中的形参必须与派生类中的形参严格匹配。
final 和 override 说明符
在 C++11 新标准中我们可以使用 override 关键字来说明派生类中的虚函数。这么做的好处是在使得程序员的意图更加清晰的同时让编译器可以为我们发现一些错误,后者在编程实践中显得更加重要。
struct B { virtual void f1(int) const; virtual void f2(); void f3(); }; struct D1 : B { void f1(int) const override; // 正确:f1 与基类中的 f1 相匹配 void f2(int) override; // 错误:B 没有形如 f2(int) 的函数 void f2() override; // 错误:f3 不是虚函数 void f4() override; // 错误:B 没有名为 f4 的函数 };
我们还能把某个函数指定为 final,如果我们已经把函数定义成 final 了,则之后任何尝试覆盖该函数的操作都将引发错误。
struct D2 : B { // 从 B 继承 f2() 和 f3(),覆盖 f1(int) void f1(int) const final; // 不允许后续的其他类覆盖 f1(int) }; struct D3 : D2 { void f2(); // 正确:覆盖从间接基类 B 继承而来的 f2 void f1(int) const; // 错误:D2 已经将 f2 声明成 final };
final 和 override 说明符出现在形参列表(包括任何 const 或引用修饰符)以及尾置返回类型之后。
虚函数与默认实参
如果某次函数调用使用了默认实参,则该实参值由本次调用的静态类型决定。
如果虚函数使用默认实参,则基类和派生类中定义的默认实参最好一致。
回避函数的机制
// 强行调用基类中定义的函数版本而不管 baseP 的动态类型到底是什么 double undiscounted = baseP->Quote::net_price(42);
如果一个派生类虚函数需要调用它的基类版本,但是没有使用作用域运算符,则在运行时该调用将被解析为对派生类版本自身的调用,从而导致无限递归。
抽象基类
纯虚函数
一个纯虚函数无须定义,我们通过在函数体的位置(即在声明语句的分号之前)书写 =0 就可以将一个虚函数说明为纯虚函数。其中,=0 只能出现在类内部的虚函数声明语句处。
// 用于保存折扣值和购买量的类,派生类使用这些数据可以实现不同的价格策略 class Disc_quote : public Quote { public: Disc_quote() = default; Disc_quote(const std::string &book, double price, std::size_t qty, double disc) : Quote(book, price), quantity(qty), discount(disc) { } double net_price(std::size_t) const = 0; protected: std::size_t quantity = 0; // 折扣适用的购买量 double discount = 0.0; // 表示折扣的小数值 };
我们也可以为纯虚函数提供定义,不过函数体必须定义在类的外部。也就是说,我们不能在类的内部为一个 =0 的函数提供函数体。
含有纯虚函数的类是抽象基类
含有(或者未经覆盖直接继承)纯虚函数的类是抽象基类。抽象基类负责定义接口,而后续其他类可以覆盖该接口。
Disc_quote discounted; // 错误:不能定义 Disc_quote 的对象 Bulk_quote bulk; // 正确:Bulk_quote 中没有纯虚函数
Disc_quote 的派生类必须给出自己的 net_price 定义,否则它们仍将是抽象基类。
我们不能创建抽象基类的对象
派生类构造函数只初始化它的直接基类
// 当同一书籍的销售量超过某个值时启用折扣 // 折扣的值是一个小于 1 的正的小数值,以此来降低正常销售价格 class Bulk_quote : public Disc_quote { public: Bulk_quote() = default; Bulk_quote(const std::string &book, double price, std::size_t qty, double disc) : Disc_quote(book, price, qty, disc) { } // 覆盖基类中的函数版本以实现一种新的折扣策略 double net_price(std::size_t) const override; };
访问控制与继承
每个类分别控制自己的成员初始化过程,与之类似,每个类还分别控制着其成员对于派生类来说是否可访问
受保护的成员
- 和私有成员类似,受保护的成员对于类的用户来说是不可访问的。
- 和公有成员类似,收保护的成员对于派生类的成员和友元来说是可访问的。
- 派生类的成员或友元只能通过派生类对象来访问基类的受保护成员。派生类对于一个基类对象中的受保护成员没有任何访问特权。
为了理解最后一条规则,请考虑如下的例子:
class Base { protected: int prot_mem; // protected 成员 }; class Sneaky : public Base { friend void clobber(Sneaky&); // 能访问 Sneaky::prot_mem friend void clobber(Base&); // 不能访问 Base::prot_mem int j; // j 默认是 private }; // 正确:clobber 能访问 Sneaky 对象的 private 和 protected 成员 void clobber(Sneaky &s) { s.j = s.prot_mem = 0; } // 错误:clobber 不能访问 Base 的 protected 成员 void clobber(Base &b) { b.prot_mem = 0; }
公有、私有和受保护继承
class Base { public: void pub_mem(); // public 成员 protected: int prot_mem; // protected 成员 private: char priv_mem; // private 成员 }; struct Pub_Derv : public Base { // 正确:派生类能访问 protected 成员 int f() { return prot_mem; } // 错误:private 成员对于派生类来说是不可访问的 char g() { return priv_mem; } }; struct Priv_Derv : private Base { // private 不影响派生类的访问权限 int f1() const { return prot_mem; } };
派生访问说明符对于派生类的成员(及友元)能否访问其直接基类的成员没什么影响。对基类成员的访问权限只与基类中的访问说明符有关。
派生访问说明符的目的是控制派生类用户(包括派生类的派生类在内)对于基类成员的访问权限。
Pub_Derv d1; // 继承自 Base 的成员是 public 的 Priv_Derv d2; // 继承自 Base 的成员是 private 的 d1.pub_mem(); // 正确:pub_mem 在派生类中是 public 的 d2.pub_mem(); // 错误:pub_mem 在派生类中是 private 的
派生访问说明符还可以控制继承自派生类的新类的访问权限:
struct Derived_from_Public : public Pub_Derv { // 正确:Base::prot_mem 在 Pub_Derv 中仍然是 protected 的 int use_base() { return prot_mem; } }; struct Derived_from_Private : public Priv_Derv { // 错误:Base::prot_mem 在 Priv_Derv 中是 private 的 int use_base() { return prot_mem; } };
派生类向基类转换的可访问性
假定 D 继承自 B:
- 只有当 D 公有地继承 B 时,用户代码才能使用派生类向基类的转换;如果 D 继承 B 的方式是受保护的或者私有的,则用户代码不能使用该转换。
- 不论 D 以什么方式继承 B,D 的成员函数和友元都能使用派生类向基类的转换;派生类向其直接基类的类型转换对于派生类的成员和友元来说永远是可访问的。
- 如果 D 继承 B 的方式是公有的或者受保护的,则 D 的派生类的成员和友元可以使用 D 向 B 的类型转换;反之,如果 D 继承 B 的方式是私有的,则不能使用。
对于代码中的某个给定节点来说,如果基类的公有成员是可访问的,则派生类向基类的类型转换也是可访问的;反之则不行。
友元与继承
就像友元关系不能传递一样,友元关系同样也不能继承。基类的友元在访问派生类成员时不具有特殊性。类似的,派生类的友元也不能随意访问基类的成员。
class Base { friend class Pal; // Pal 在访问 Base 的派生类时不具有特殊性 protected: int prot_mem; }; class Sneaky : public Base { int j; }; class Pal { public: int f(Base b) { return b.prot_mem; } // 正确:Pal 是 Base 的友元 int f2(Sneaky s) { return s.j; } // 错误:Pal 不是 Sneaky 的友元 // 对基类的访问权限由基类本身控制,即使对于派生类的基类部分也是如此 int f3(Sneaky s) { return s.prot_mem; } // 正确:Pal 是 Base 的友元 };
当一个类将另一个类声明为友元时,这种友元关系只对做出声明的类有效。对于原来那个类来说,其友元的基类或者派生类不具有特殊的访问能力。
// D2 对 Base 的 protected 和 private 成员不具有特殊的访问能力 class D2 : public Pal { public: int mem(Base b) { return b.prot_mem; } // 错误:友元关系不能继承 };
不能继承友元关系;每个类负责控制各自成员的访问权限。
改变个别成员的可访问性
有时我们需要改变派生类继承的某个名字的访问级别,通过使用 using 声明可以达到这一目的。
class Base { public: std::size_t size() const { return n; } protected: std::size_t n; }; class Derived : private Base { // 注意 private 继承 public: // 保持对象尺寸相关的成员的访问级别 using Base::size; protected: using Base::n; };
using 声明语句中名字的访问权限由该 using 声明语句之前的访问说明符来决定。
派生类只能为那些它可以访问的名字提供 using 声明。
默认的继承保护级别
默认情况下,使用 class 关键字定义的派生类是私有继承的;而使用 struct 关键字定义的派生类是公有继承的:
class Base { /* ... */ }; struct D1 : Base { /* ... */ }; // 默认 public 继承 class D2 : Base { /* ... */ }; // 默认 private 继承
一个私有派生的类最好显式地将 private 声明出来,而不要仅仅依赖于默认的设置。显式声明的好处是可以令私有继承关系清晰明了,不至于产生误会。
继承中的类作用域
当存在继承关系时,派生类的作用域嵌套在其基类的作用域之内。
恰恰因为类作用域有这种继承嵌套的关系,所以派生类才能像使用自己的成员一样使用基类的成员。
在编译时进行名字查找
一个对象、引用或指针的静态类型决定了该对象的哪些成员是可见的。即使静态类型与动态类型可能不一致(当使用基类的引用或指针时会发生这种情况),但是我们能使用哪些成员仍然是由静态类型决定的。
class Disc_quote : public Quote { public: std::pair<std::size_t, double> discount_policy() const { return {quantity, discount}; } // 其他成员与之前版本一致 };
我们只能通过 Disc_quote 及其派生类的对象、引用或指针使用 discount_policy;
Bulk_quote bulk; Bulk_quote *bulkP = &bulk; // 静态类型与动态类型一致 Quote *itemP = &bulk; // 静态类型与动态类型不一致 bulkP->discount_policy(); // 正确:bulkP 的类型是 Bulk_quote* itemP->discount_policy(); // 错误:itemP 的类型是 Quote*
名字冲突与继承
和其他作用域一样,派生类也能重用定义在其直接基类或间接基类中的名字,此时定义在内层作用域(即派生类)的名字将隐藏定义在外层定义域(即基类的名字):
struct Base { Base() : mem(0) { } protected: int mem; }; struct Derived : Base { Derived(int i) : mem(i) { } int get_mem() { return mem; } protected: int mem; }; int main(int argc, char *argv[]) { Derived d(42); cout << d.get_mem() << endl; return 0; }
派生类的成员将隐藏同名的基类成员。
通过作用域运算符来使用隐藏的成员
struct Derived : Base { int get_mem() { return Base::mem; } // ... };
作用域运算符将覆盖掉原有的查找规则,并指示编译器从 Base 类的作用域开始查找 mem。
除了覆盖继承而来的虚函数之外,派生类最好不要重用其他定义在基类中的名字
一如既往,名字查找先于检查类型
如前所述,声明在内层作用域的函数并不会重载声明在外层作用域的函数。因此,定义派生类中的函数也不会重载基类中的成员。和其他作用域一样,如果派生类(即内层作用域)的成员与基类(即外层作用域)的某个成员同名,则派生类将在其作用域内隐藏该基类成员。即使派生类成员和基类成员的形参列表不一致,基类成员也仍然会被隐藏掉。
struct Base { int memfcn(); }; struct Derived : Base { int memfcn(int); }; int main(int argc, char *argv[]) { Derived d; Base b; b.memfcn(); // 调用 Base::memfcn d.memfcn(10); // 调用 Derived::memfcn d.memfcn(); // 错误:参数列表为空的 memfcn 被隐藏了 d.Base::memfcn(); // 正确:调用 Base::memfcn return 0; }
虚函数与作用域
我们现在可以理解为什么基类与派生类中的虚函数必须有相同的形参列表了。假如基类与派生类的虚函数接受的实参不同,则我们就无法通过基类的引用或指针调用派生类的虚函数了。例如:
class Base { public: virtual int fcn(); }; class D1 : public Base { public: // 隐藏基类的 fcn,这个 fcn 不是虚函数 // D1 继承了 Base::fcn() 的定义 int fcn(int); // 形参列表与 Base 中的 fcn 不一致 virtual void f2(); // 是一个新的虚函数,在 Base 中不存在 }; class D2 : public D1 { public: int fcn(int); // 是一个非虚函数,隐藏了 D1::fcn(int) int fcn(); // 覆盖了 Base 的虚函数 fcn void f2(); // 覆盖了 D1 的虚函数 f2 };
通过基类调用隐藏的虚函数
给定上面定义的这些类后,我们来看几种使用其函数的方法:
Base bobj; D1 d1obj; D2 d2obj; Base *bp1 = &bobj, *bp2 = &d1obj, *bp3 = &d2obj; bp1->fcn(); // 虚调用,将在运行时调用 Base::fcn bp2->fcn(); // 虚调用,将在运行时调用 Base::fcn bp3->fcn(); // 虚调用,将在运行时调用 D2::fcn D1 *d1p = &d1obj; D2 *d2p = &d2obj; bp2->f2(); // 错误:Base 没有名为 f2 的成员 d1p->f2(); // 虚调用,将在运行时调用 D1::f2() d2p->f2(); // 虚调用,将在运行时调用 D2::f2() Base *p1 = &d2obj; D1 *p2 = &d2obj; D2 *p3 = &d2obj; p1->fcn(42); // 错误:Base 中没有接受一个 int 的 fcn p2->fcn(42); // 静态绑定,调用 D1::fcn(int) p3->fcn(42); // 静态绑定,调用 D2::fcn(int)
覆盖重载的函数
一种好的解决方案是为重载的成员提供一条 using 声明语句。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!