OOP2(虚函数/抽象基类/访问控制与继承)

时间:2023-03-09 17:15:46
OOP2(虚函数/抽象基类/访问控制与继承)

通常情况下,如果我们不适用某个函数,则无需为该函数提供定义。但我们必须为每个虚函数都提供定义而不管它是否被用到了,这因为连编译器也无法确定到底会适用哪个虚函数

对虚函数的调用可能在运行时才被解析:

当某个虚函数通过指针或引用调用时,编译器产生的代码直到运行时才能确定应该调用哪个版本的函数。被调用的函数是与之绑定到指针或引用上的对象的动态类型相匹配的那一个

注意:动态绑定只有当我们通过指针或引用调用虚函数时才会发生。当我们通过一个具有普通类型(非引用非指针)的表达式调用虚函数时,在编译时就会将调用的版本确定下来:

 Quote base;
Bulk_quote derived;
base = derived;//将derived的Quote部分拷贝给base
base.net_price();//调用Quote::net_prive

注意:对非虚函数的调用在编译时进行绑定。通过对象进行的函数(虚函数或非虚函数)调用也在编译时绑定。

当且仅当对通过指针或引用调用虚函数时,才会在运行时解析该调用,也只有在这种情况下对象的动态类型才有可能与静态类型不同

派生类中的虚函数:

基类中的虚函数在派生类中隐式地也是一个虚函数。当派生类覆盖了某个虚函数时,该函数在基类中的形参必须与派生类中的形参严格匹配(包括 this 参数)。同样,派生类中虚函数的返回类型也必须与基类函数匹配。(当类的虚函数返回类型是类本身的指针或引用时可以返回派生类自己的引用,但要求从派生类到基类的类型转换是可访问的)

final 和 override 说明符:

派生类如果定义一个函数与基类中函数的名字相同但是形参列表不同,这仍然是合法的行为。编译器将认为新定义的这个函数与基类中原有的函数是相互独立的。这时,派生类的函数没有覆盖掉基类中的版本。就实际的编程习惯而言,这种声明往往是错误的,因为我们可能原本希望派生类能覆盖掉基类的虚函数,但是一不小心把形参列表写错了

我们可以通过 override 关键字来发现这种错误。如果我们使用 override 标记了某个函数,但该函数并没有覆盖已存在的虚函数,此时编译器将报错:

 #include <iostream>
using namespace std; struct B{
virtual void f1(int) const;
virtual void f2();
void f3();
}; struct D : B{
void f1(int) const override;//正确,f1与基类中的f1匹配
// void f1(int) override;//错误,this参数应该是const的
// void f2(int) override;//错误,f2没有形如f2(int)的函数
// void f3() override;//错误,f3不是虚函数
// void f4() override;//错误,B没有名为f4的函数
}; int main(void){ }

final 关键字作用和 override 恰好相反,如果我们已经把函数定义成 final 了,则之后任何尝试覆盖该函数的操作都将引发错误:

 #include <iostream>
using namespace std; struct B{
virtual void f1(int) const;
virtual void f2();
void f3();
}; struct D1 : B{
//从B继承f2(),f3(),覆盖f1(int)
void f1(int) const final;//不允许后继的其它类覆盖f1(int)
}; struct D2 : D1{
void f2();//正确,覆盖从间接基类B继承而来的f2
// void f1(int) const;//错误,D1已经将f1声明成final的
}; int main(void){ }

虚函数与默认实参:

和其它函数一样,虚函数也可以拥有默认实参。如果某次函数调用使用了默认实参,则该实参值由本次调用的静态类型决定。如果虚函数使用默认实参,则基类和派生类中定义的默认实参最好一致

回避虚函数的机制:

某些情况下,我们希望对虚函数的调用不要进行动态绑定,而是强迫其执行虚函数的某个特定版本。使用作用域运算符可以实现这一目的:

强制调用基类中定义的函数版本而不管 baseP 的动态类型到底是什么

double undiscounted = baseP->Quote::net_price(42);//该调用在编译时完成解析

注意:通常情况下,只有成员函数(或友元)中的代码才需要使用作用域运算符来回避虚函数的机制

通常当一个派生类的虚函数调用它覆盖的基类的虚函数版本时才需要回避虚函数的默认机制

如果一个派生类虚函数需要调用它的基类版本,但是没有使用作用域运算符,则在运行时该调用将被析构为派生类版本自身的调用,从而导致无限递归:

 #include <iostream>
using namespace std; class A{
protected:
int x; public:
// A();
// ~A();
virtual ostream& f(ostream &os) const {
os << x;
return os;
}
}; class B : public A{
private:
int y; public:
// B();
// ~B();
ostream& f(ostream &os) const {
// return f(os) << " " << y;//错误,调用该函数时会无限递归
return A::f(os) << " " << y;
}
}; int main(void) {
B *b = new B;
A *a = b;
a->f(cout);//动态类型为B,调用ostream& B::f(ostream&) const
delete b; return ;
}

抽象基类:

纯虚函数:

我们通过在声明语句的分号之前加 =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 = ;//纯虚函数 protected:
std::size_t quantity = ;
double discount = 0.0;
}; double Disc_quote::net_price(std::size_t sz) const {
//纯虚函数可以提供定义,但函数体必须定义在类的外部
}

注意:纯虚函数可以提供定义,但函数体必须定义在类的外部

含有纯虚函数的类是抽象基类。抽象基类负责定义接口,后继的其他类可以覆盖该接口。我们不能直接创建一个抽象基类对象,我们可以定义抽象基类的派生类对象,前提是这些派生类覆盖了抽象基类中的纯虚函数。

抽象基类的派生类必须覆盖抽象基类中的纯虚函数,否则派生类将仍然是抽象基类,不能创建对象

虽然抽象基类不能创建对象,但是我们仍然需要定义抽象基类的构造函数,因为抽象基类的派生类将会使用抽象基类的构造函数来构造派生类中的抽象基类部分数据成员:

 #include <iostream>
using namespace std; 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 {//定义成虚函数,运行2时进行动态绑定
return n * price;
} virtual ~Quote() = default;//基类通常都应该定义一个虚析构函数,即使该函数不执行任何实际操作 private:
std::string bookNo;//书籍的isbn编号 protected://可被派生类访问
double price = 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 = ;//纯虚函数 protected:
std::size_t quantity = ;
double discount = 0.0;
}; double Disc_quote::net_price(std::size_t sz) const {
//纯虚函数可以提供定义,但函数体必须定义在类的外部
} class Bulk_quote : public Disc_quote {
public:
Bulk_quote() = default;
Bulk_quote(const std::string&, double, std::size_t, double); double net_price(std::size_t) const override;//override显式注明该成员函数覆盖它继承的虚函数 // ~Bulk_quote(); private:
std::size_t min_qty = ;//适用折扣政策的最低购买量
double discount = 0.0;//以小数表示的折扣额 }; Bulk_quote::Bulk_quote(const std::string &book, double price, std::size_t qty, double disc) :
//调用抽象基类的构造函数来构造派生类中的抽象基类部分数据成员
Disc_quote(book, price, qty, disc) {} double Bulk_quote::net_price(size_t cnt) const {
if(cnt >= min_qty) return cnt * ( - discount) * price;
return cnt * price;
} int main(void){
// Disc_quote discount;//错误,不能创建抽象类基类的对象
Bulk_quote bqt;//正确,该派生类中覆盖了抽象基类中的纯虚函数,可以创建对象
return ;
}

访问控制与继承:

受保护的成员:

和私有成员类似,受保护的成员对于类的用户来说是不可访问的

和公有成员类似,受保护的成员对于派生类成员和友元来说是可访问的

派生类的友元只能通过派生类对象来访问基类的受保护成员:

 #include <iostream>
using namespace std; class Base{
public:
Base(int a = ) : prot_mem(a) {}
// ~Base(); protected:
int prot_mem; }; class Sneaky : public Base{
friend void clobber(Sneaky&);
friend void clobber(Base&);
int j; public:
//如果使用了默认实参,则派生类和基类的默认实参应该保持一致
Sneaky(int a = , int b = ) : Base(a), j(b) {}//调用基类的构造函数来构造派生类对象中的基类部分
ostream& print(ostream&) const; }; ostream& Sneaky::print(ostream &os) const {
os << prot_mem << j;//派生类成员中可以直接使用基类中的受保护数据成员
return os;
} // 注意:clobber(Sneaky&)和clobber(Base&)是派生类的友元,但不是基类的友元,因此我们可以通过派生类对象来访问基类中的受保护数据成员,
// 但不能直接通过基类对象来访问基类中的受保护成员。该函数相对于基类仅仅是一个用户,只能直接访问基类的公共成员
void clobber(Sneaky &s) {
s.j = s.prot_mem = ;
} void clobber(Base &b) {
// b.prot_mem = 0;//错误,该函数不是Base类的友元,只能访问Base类中的公共成员
} int main(void){
Sneaky s();
s.print(cout) << endl; return ;
}

公有、私有和受保护继承:

某个类对其继承而来的成员的访问权限受两个因素影响:一是基类中该成员的访问说明符,二是在派生类的派生类列表中的访问说明符。与其派生访问说明符无关。派生类访问说明符的目的是控制派生类用户(包括派生类的派生类在内)对于基类成员的访问权限:

 #include <iostream>
using namespace std; class Base {
public:
void pub_mem(); protected:
int prot_mem; private:
char priv_mem;
}; void Base::pub_mem() { } struct Pub_Derv : public Base {//公有继承,用户代码(包括Pub_Derv类的派生类)可以访问基类(如果基类的成员本身可以被访问的话)
int f() {
return prot_mem;//正确,派生类能访问protected成员
} char g() {
// return priv_mem;//错误,派生类不能访问private成员
}
}; struct Priv_Derv : private Base{//私有继承,用户代码(包括Priv_Derv类的派生类)可以访问基类
int f1() const {
return prot_mem;//正确,private不影响派生类的访问权限
}
}; //派生类访问说明符还可以控制继承自派生类的新类的访问权限
struct Derived_from_public : public Pub_Derv {
int use_base() {
return prot_mem;
}
}; struct Derived_from_private : public Priv_Derv {
int use_base() {
// return prot_mem;//Priv_Derv中继承自Base的成员都变成private了,不能被派生类调用
}
}; int main(void) {
Pub_Derv d1;//继承自Base的成员是public的
Priv_Derv d2;//继承自Base的成员是private的
d1.pub_mem();//正确,pub_mem在派生类中是public的
// d2.pub_mem();//错误,pub_mem在派生类中是private的 // Base *b = &d2;//只有公有继承才能在用户代码中使用派生类像基类转换
// b->pub_mem(); return ;
}

注意:

1)    public 继承:基类成员保持自己的访问级别

2)    protected 继承:基类的 public 和 protected 成员在派生类中为 protected 成员

3)    private 继承:基类所有成员在派生类中为 private 成员

派生类向基类转换的可访问性:

派生类向基类的转换是否可访问由使用该转换的代码决定,同时派生类的派生访问说明符也会有影响。假定 D 继承自 B:

只有当 D 公有地继承 B 时,用户代码才能使用派生类向基类转换;如果 D 继承 B 的方式是受保护的或私有的,则用户代码不能使用该转换

不论 D 以什么方式继承 B,D 的成员函数和友元函数都能使用派生类向基类的转换

如果 D 继承 B 的方式是公有的或者受保护的,则 D 的派生类的成员和友元可以使用 D 向 B 的类型转换,反之则不行

友元与继承:

不能继承友元关系,每个类负责控制各自成员的访问权限

改变个别成员的可访问性:

我们可以通过 using 声明改变派生类继承的某个名字的访问级别:

 class Base{
public:
// Base();
// ~Base();
std::size_t size() const {
return n;
} protected:
std::size_t n;
}; class Derived : private Base{
public:
// Derived();
// ~Derived();
using Base::size;//该成员被标记为public的 protected:
using Base::n;//该成员被标记为protected的 };

注意:using 声明语句中名字的访问权限由该 using 声明语句之前的访问说明符决定

派生类只能为那些它可以访问的名字提供 using 声明。即不能对基类中的 private 成员提供 using 声明

默认的继承保护级别:

默认情况下,使用 class 关键字定义的派生类是私有继承的;而使用 strcut 关键字定义的派生类是公有继承的:

 class Base {};
struct D1 : Base {};//默认public继承
class D2 : Base {};//默认private继承