目录
不存在从基类向派生类的隐式类型转换、dynamic_cast 、static_cast (534P)
只有通过基类的引用或指针进行调用虚函数时,才会发生动态绑定,才能在运行时解析该调用 ( 537P)
将函数定义 final 和 override 说明符 (538P)
使用 作用域运算符来 回避(绕过)虚函数的机制 ( 539P)
派生类公有、私有、保护成员对基类成员的可访问性 (543P)
从 struct 和 class 关键字创建的类的默认的继承保护级别 (546P)
在基类中的构造函数 和 析构函数中调用派生类虚函数会发生的情况 (556P)
使用 using 声明使派生类“ 继承”基类的构造函数 (557)
OPP:概述
面向对象程序设计的核心思想是:
- 数据抽象(封装)—— 可以将类的接口与实现分离
- 继承—— 可以定义与其他类相似但完全不相同的新类
- 动态绑定(虚函数)—— 在使用这些彼此相似的类时,在一定程度上忽略他们的区别,统一使用它们的对象
- 每个派生类除了定义了各自的特有的成员之外, 每个派生类还包含了基类的成员。
- 派生类public 继承 基类, 派生类对象可以当作基类对象来使用。 但是如果是 protected 或 private 继承的话,就不可以。
- 一个派生类必须在其类体中声明所有其需要定义的虚函数。 C++11 新标准允许派生类显式地指出哪个成员函数是重写基类中的虚函数,具体的方法是在派生类函数的形参列表之后增加一个 override 关键字
有时候 我们可以通过动态绑定,我们可以使用相同的代码来分别处理基类或派生类的对象。例如下面程序:
class Quote
{
public:
Quote() = default;
Quote(const std::string &book, double sales_price) :bookNo(book), price(sales_price) { }
std::string isbn() const { return bookNo; }
virtual double net_price(std::size_t n) const
{
cout << "调用的是基类的net_price 函数" << endl;
return n * price;
}
virtual ~Quote() = default; // dynamic binding for the destructor
private:
std::string bookNo;
protected:
double price = 0.0;
};
class Bulk_quote : public Quote
{
public:
Bulk_quote() = default;
Bulk_quote(const std::string& book, double p, std::size_t qty, double disc) :
Quote(book, p), min_qty(qty), discount(disc) { }
double net_price(std::size_t) const override;
private:
std::size_t min_qty = 0;
double discount = 0.0;
};
double Bulk_quote::net_price(size_t cnt) const
{
cout << "调用的是派生类的net_price 函数" << endl;
if (cnt >= min_qty)
return cnt * (1 - discount) * price;
else
return cnt * price;
}
double print_total(std::ostream &os,const Quote &item, size_t n) // 使用相同的代码来分别处理基类或派生类的对象
{
// 根据传入 item 形参的对象类型调用 Quote::net_price 或者 net_price:: net_price
double ret = item.net_price(n);
os << "ISBN: " << item.isbn() << " # sold: " << n << " total due: " << ret << endl;
return ret;
}
int main()
{
Quote basic;
Bulk_quote bulk;
print_total(cout, basic, 20); // calls Quote version of net_price
print_total(cout, bulk, 20); // calls Bulk_quote version of net_price
system("pause");
return 0;
}
输出结果为:
调用的是基类的net_price 函数
ISBN: # sold: 20 total due: 0
调用的是派生类的net_price 函数
ISBN: # sold: 20 total due: 0
仔细观察输出结果。
- 注意: 在C++语言中, 当我们使用基类的引用(或指针)调用一个虚函数时将发生动态绑定。根据引用或指针所绑定的对象类型不同, 该调用可能执行基类的版本, 也可能执行某个派生类的版本。
定义基类
注意: 在作为继承关系中的基类都应该定义一个虚析构函数,即使该函数不执行任何实际操作也是如此。
class Quote
{
public:
Quote() = default;
Quote(const std::string &book, double sales_price) :bookNo(book), price(sales_price) { }
std::string isbn() const { return bookNo; }
virtual double net_price(std::size_t n) const
{
return n * price;
}
virtual ~Quote() = default; // dynamic binding for the destructor
private:
std::string bookNo;
protected:
double price = 0.0;
};
在C++ 语言中的基类中有两种成员函数:
- 一种是基类希望其派生类进行覆盖的函数( 其派生类定义为 virtual 函数)
- 另一种是基类希望派生类直接继承而不要改变的函数
- 任何构造函数之外的非静态函数都可以是虚函数。意思就是说一个基类中除了构造函数和静态函数不可以是虚函数,其他都可以。
- 如果基类把一个函数声明成虚函数,则该函数在派生类中隐式地也是虚函数( 也可以使用 virtual 显式指出,但是该关键字只能出现在类内部的声明语句之前,而不能用于类外部的函数定义)。还可以通过override 关键字显式地指出覆盖了基类中的某个函数。
- 只要成员函数没有被声明为虚函数,那么不管用什么样的方式调用该函数都将是静态联编 ,而不是动态联编。
练习题15.1:
- 就是该函数 的声明处 用 virtual 关键字声明的成员函数,基类希望该成员在派生类中重定义。 还有就是任何除构造函数之外的非 static 成员函数 都可以是 虚函数。
练习题15.2:
- 当在继承关系时, 基类的某些成员希望在派生类中也被访问,但是在类外不可访问。我们可以把这样的成员声明为 protected 成员。声明为 protected 的成员,只可以被其派生类,其自身的成员、友元访问, 类外是不可访问的。
- 虽然派生类在什么样的继承方式下都可以继承基类中的成员, 但是派生类中的成员函数不可以访问基类中的 private 成员。声明为 private 的成员只可以让 友元 或者该类自己的成员函数访问。 类外也是不可访问的。
定义派生类
class Bulk_quote : public Quote
{
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;
};
三个访问说明符的作用:
- 是控制派生类从基类继承而来的成员是否对派生类的用户可见。
如果一个派生类是公有继承于基类,那么该基类的公有成员也是派生类接口的组成部分。此外, 我们就可以将公有派生类型的对象绑定到基类的引用或指针上。如前所述,在任何需要 Quote 的引用或指针的地方我们都能使用 Bulk_quote的对象。
派生类中的虚函数 、override 关键字(530P)
- 派生类经常(但不总是)覆盖它继承的虚函数。
- 如果派生类没有覆盖其基类中的某个虚函数, 则该虚函数的行为与任何普通成员一样 —— 派生类会直接继承其在基类中的版本 ( 就是默认使用基类中虚函数版本)。
派生类可以在它覆盖的函数前使用virtual关键字,但不是非得这么做。
- 因为如果基类将一个函数声明为虚函数时,当派生类重写该函数时会隐式地成为虚函数。
派生类到基类的类型转换 (530P)
一个派生类对象的成员组成部分为:
- 派生类新增加的(非静态)成员 + 继承其基类的成员 +如果这个派生类有多个基类,成员就会从各个基类继承过来
class Quote
{
public:
Quote() = default;
Quote(const std::string &book, double sales_price) :bookNo(book), price(sales_price) {}
std::string isbn() const { return bookNo; }
virtual double net_price(std::size_t n) const
{
cout << "调用的是基类的net_price 函数" << endl;
return n * price;
}
virtual ~Quote() = default; // dynamic binding for the destructor
private:
std::string bookNo;
protected:
double price = 0.0;
};
class Bulk_quote : public Quote
{
public:
Bulk_quote() = default;
Bulk_quote(const std::string& book, double p,std::size_t qty, double disc) :
Quote(book, p), min_qty(qty), discount(disc) { }
double net_price(std::size_t) const override;
private:
std::size_t min_qty = 0;
double discount = 0.0;
};
double Bulk_quote::net_price(size_t cnt) const
{
cout << "调用的是派生类的net_price 函数" << endl;
if (cnt >= min_qty)
return cnt * (1 - discount) * price;
else
return cnt * price;
}
int main()
{
Quote item; // object of base type
Bulk_quote bulk; // object of derived type
// 在C++语言中,当我们使用了基类的引用(或指针)调用一个虚函数时将发生动态绑定
Quote *p = &item; // p points to a Quote object
p->net_price(2); // 调用的是基类的该函数
p = &bulk; // p points to the Quote part of bulk
p->net_price(2); // 调用的是派生类类的该函数
Quote &r = bulk; // r bound to the Quote part of bulk
r.net_price(2); // 调用的是派生类类的该函数
system("pause");
return 0;
}
输出结果为:
调用的是基类的net_price 函数
调用的是派生类的net_price 函数
调用的是派生类的net_price 函数
这种转换通常称为 派生类到基类的(derived-to-base )类型转换。和其他类型转换一样,编译器会隐式地执行派生类到基类的转换。
这种隐式特性意味着我们可以把派生类对象(本人注:会调用拷贝构造函数,而且其中派生类的部分会被 “ 切掉”,但是派生类对象的引用或指针不会调用拷贝构造函数,其中派生类的部分就不会被 “ 切掉”, )或者派生类对象的引用用在需要基类引用的地方; 同样的, 我们也可以把派生类对象的指针用在需要基类指针的地方。
派生类 公有继承基类,派生类对象可以当作基类对象来使用。而且此时我们也能将基类的指针或引用绑定到派生类对象中的基类部分上( 如果该派生类是 private 和 protected 继承与基类, 那么就不可以将基类的指针或引用绑定到派生类对象中的基类部分上。看下图,派生类 protected 继承与 基类,然后编译后的截图)。虽然不可以,但是可以访问派生类自己新增加的成员函数。
派生类的构造函数 (531P)
派生类并不能在其构造函数中直接初始化从基类继承而来的成员,但是派生类必须使用基类的构造函数来初始化它的基类部分。所以说每个类都控制它自己的成员初始化过程。 详细请看上面的程序。
- 如果在派生类的构造函数并没有显式调用基类中的某一个构造函数,那么派生类构造函数将隐式调用基类中默认构造函数(前提是基类中有默认构造函数,不管是显式的还是隐式的。 如果是隐式的,说明该基类中没有任何构造函数; 如果是显式的,那么还想有默认构造函数,那么必须再声明一个,可以像上面程序那样。像 这样 “ Quote() = default; ” 写一个默认构造函数, 该形式的构造函数表明我们既需要其它形式的构造函数,也需要默认的构造函数。 该函数的作用相当于合成的默认构造函数)。那么此时,派生类对象的基类部分的数据成员将执行默认初始化。
- 如果我们想在派生类的构造函数的初始化列表使用基类其它构造函数来显式初始化基类的成员,我们需要以类名加圆括号内的实参列表的形式为构造函数提供初始值( 在 派生类中的构造函数中)。这些实参将帮助编译器决定到底应该选用基类中哪个构造函数来初始化派生类对象的基类部分。
- 注意: 当我们创建一个派生类对象的实例时, 首先初始化是基类的部分,当基类的构造函数的函数体完毕后。然后按照声明的数据成员的初始化顺序依次初始化派生类的成员。
在派生类中使用继承其基类的成员 (531P)
- 不管派生类通过什么方式继承( 例如: public、protected、private)自基类, 派生类都可以访问基类中的公有成员和保护成员,但是私有成员不可以。
继承与静态成员 ( 532P )
下列是静态成员被继承的示例:
class Base
{
public:
static void statmem()
{
cout << "调用的是基类的statmem 函数" << temp << endl;
}
int result()
{
temp = 20000000;
return temp;
}
protected:
static int temp;
};
int Base::temp = 100;
class Derived : public Base
{
public:
int result()
{
return temp;
}
void f(const Derived &derived_obj)
{
temp = 10;
cout << "调用的是派生类的f 函数" << endl;
Base::statmem();
Derived::statmem();
derived_obj.statmem();
this->statmem();
}
};
int main()
{
Base myBase;
Derived myDerived;
cout << " 输出未修改的 数据成员的值:" << myBase.result() << endl;
cout << " 输出未修改的 数据成员的值:" << myDerived.result() << endl;
cout << endl;
myDerived.f(myDerived);
cout << endl;
cout << " 输出修改后的static 数据成员的值:" << myDerived.result() << endl;
cout << " 输出修改后的static 数据成员的值:" << myBase.result() << endl;
cout << " 输出修改后的static 数据成员的值:" << myDerived.result() << endl;
system("pause");
return 0;
}
输出结果为:
输出未修改的 数据成员的值:20000000
输出未修改的 数据成员的值:20000000
调用的是派生类的f 函数
调用的是基类的statmem 函数10
调用的是基类的statmem 函数10
调用的是基类的statmem 函数10
调用的是基类的statmem 函数10
输出修改后的static 数据成员的值:10
输出修改后的static 数据成员的值:20000000
输出修改后的static 数据成员的值:20000000
- 如果基类定义了一个静态成员,则在整个继承体系中只存在该成员的唯一定义。不论从基类中派生出来多少个派生类, 对于每个静态成员来说都只存在唯一的实例。
仔细观察上面的程序可以发现当我们在派生类中的 f() 函数 修改了 temp 数据成员的值时, 不管是调用 派生类的result 函数 还是 调用 基类的 result 函数,输出的结果都是10。 说明了 如果在基类有一个静态成员,那么在整个继承体系中, 不管是派生类对象还是基类对象 都共享此成员 ( 不管派生类通过什么方式继承基类)。
书上还说假设基类的静态成员是可访问的 ( 可访问的 是 public 和 protected ), 则我们既能通过基类使用它也能通过派生类使用它。
派生类的声明需要注意的地方 (532P)
被用作基类的类需要注意的地方 (533P)
如果我们想将某个类用作基类, 则该类必须已经定义而非仅仅声明。
class Quote; // declared but not defined
// error: Quote must be defined
class Bulk_quote : public Quote { ... };
因为派生类包含了并且可以使用从基类继承而来的成员,要使用这些成员,派生类必须知道它们是什么( 指的是基类必须被定义)。这个规则的一个含义是,即一个类不能派生自身。
通过 final 来 禁止类被继承
有时我们定义了一个不希望别人继承的类。或者我们可以定义一个我们不想考虑它是否适合作为基类的类。在新的C++11标准下,我们可以通过在类名后面加上final来防止类被用作基类:
class NoDerived final { /* */ }; // NoDerived can't be a base class
class Base { /* */ };
// Last is final; we cannot inherit from Last
class Last final : Base { /* */ }; // Last can't be a base class
class Bad : NoDerived { /* */ }; // error: NoDerived is final
class Bad2 : Last { /* */ }; // error: Last is final
类型转换与继承 (534P)
可以将基类的指针或引用绑定到派生类对象上有一层极为重要的含义:
- 当使用基类的引用(或指针)时, 实际上我们并不清楚该引用(或指针) 绑定对象的真实类型。该对象可能是基类的对象,也可能是派生类的对象。
- 所以说当我们使用基类的引用(或指针)时调用一个虚函数时将发生动态绑定,调用的是可能是基类的虚函数,也可能是派生类的虚函数, 到底调用谁取决于基类的引用(或指针)绑定对象的真实类型。
Note: 和内置指针一样, 智能指针类也可以支持派生类向基类的类型转换, 这意味着我们可以将一个派生类对象的地址存储在一个基类的智能指针内。
静态类型 和 动态类型 (534P)
本节中讲的静态类型和动态类型指的是 静态联编 和 动态联编的意思。
不存在从基类向派生类的隐式类型转换、dynamic_cast 、static_cast (534P)
为什么 派生类可以隐式转换为基类呢?
- 是不存在从基类向派生类的隐式类型的转换(意识就是说显式转换可以, 比如说用 dynamic_cast 或者 static_cast ),之所以存在派生类向基类的类型转换是因为每个派生类对象都包含一个基类部分。 而基类的引用或指针可以绑定到该基类部分上。 但是基类对象并没有派生类部分 。
书上有这么一句话 “ 一个基类的对象既可以以独立的形式存在, 也可以作为派生类对象的一部分存在。如果基类对象不是派生类对象的一部分, 则它只含有基类定义的成员, 而不含有派生类定义的成员。” 我是这样理解的:
“ 一个基类的对象既可以以独立的形式存在 ”的时候:
指的是基类的成员都是private的,那么派生类不管用什么方式继承基类,派生类的成员都不可以访问基类的成员。这样基类的对象就算是独立的形式存在了。那么此时基类对象就不是派生类对象的一部分了。因为一个基类的对象可能是派生类对象的一部分, 也可能不是, 所以不存在从基类向派生类的自动类型转换。