对虚函数的调用可能在运行时才被解析:
当某个虚函数通过指针或引用调用时,编译器产生的代码直到运行时才能确定应该调用哪个版本的函数。被调用的函数是与之绑定到指针或引用上的对象的动态类型相匹配的那一个
#include<bits/stdc++.h>
using namespace std;
class A{
public:
virtual void print(){
puts("A");
}
};
class B:public A{
public :
void print(){
puts("B");
}
};
int main(){
A a;B b;
A *p=&b;
p->print();//输出B
return 0;
}
注意:动态绑定只有当我们通过指针或引用调用虚函数时才会发生。当我们通过一个具有普通类型(非引用非指针)的表达式调用虚函数时,在编译时就会将调用的版本确定下来:
#include<bits/stdc++.h>
using namespace std;
class A{public: virtual void print(){puts("A");}};
class B:public A{public :void print(){ puts("B");}};
int main(){
A a;
B b;
a.print();//编译时已经确定,输出A
}
多态的唯一展现途径:当且仅当对通过指针或引用调用虚函数时,才会在运行时解析该调用,也只有在这种情况下对象的动态类型才有可能与静态类型不同
派生类中的虚函数:
基类中的虚函数在派生类中隐式地也是一个虚函数。当派生类覆盖了某个虚函数时,该函数在基类中的形参必须与派生类中的形参严格匹配(包括 this 参数)。同样,派生类中虚函数的返回类型也必须与基类函数匹配。(当类的虚函数返回类型是类本身的指针或引用时可以返回派生类自己的引用,但要求从派生类到基类的类型转换是可访问的)
final函数
如果将某个函数定义成final的了,那么就无法再重写了
#include<bits/stdc++.h>
using namespace std;
struct B{
virtual void f1(int)const;
virtual void f2()const;
void f3();
};
struct D1:public B{
void f1(int )const final;
};
struct D2:public D1(){//已经定义成final的虚函数,不能被派生覆盖
void f1(int)const override;
};
int main(){
}
虚函数与默认实参:
和其它函数一样,虚函数也可以拥有默认实参。**如果某次函数调用使用了默认实参,则该实参值由本次调用的静态类型决定。**如果虚函数使用默认实参,则基类和派生类中定义的默认实参最好一致
#include<bits/stdc++.h>
using namespace std;
class A{
public :
virtual void f(int x=1){
cout<<x<<endl;
}
};
class B:public A{
public :
virtual void f(int x=2){
cout<<x<<endl;
}
};
int main(){
A a1,*ap;
B b1,*bp;
a1.f();
b1.f();
ap=&a1;
ap->f();
ap=&b1;
ap->f();
return 0;
}
回避虚函数的机制:
某些情况下,我们希望对虚函数的调用不要进行动态绑定,而是强迫其执行虚函数的某个特定版本。使用作用域运算符可以实现这一目的:
强制调用基类中定义的函数版本而不管 baseP 的动态类型到底是什么
double undiscounted = baseP->Quote::net_price(42);//该调用在编译时完成解析
注意:通常情况下,只有成员函数(或友元)中的代码才需要使用作用域运算符来回避虚函数的机制
通常当一个派生类的虚函数调用它覆盖的基类的虚函数版本时才需要回避虚函数的默认机制
如果一个派生类虚函数需要调用它的基类版本,但是没有使用作用域运算符,则在运行时该调用将被析构为派生类版本自身的调用,从而导致无限递归:
#include<bits/stdc++.h>
using namespace std;
class A{
private :int x=1;
public :virtual void f(){cout<<x;}
};
class B:public A{
private:int y=2;
public:void f(){A::f();}
};
int main(){
B *b=new B;
A *a=b;//基类指针指向派生类
a->f();
return 0;
}
抽象基类:
纯虚函数:
我们通过在声明语句的分号之前加 =0 可以将一个虚函数声明成纯虚函数:
#include<bits/stdc++.h>
using namespace std;
//抽象基类
class Quote{
public:
Quote()=default;
Quote(string s,double t):bookNo(s),price(t){};
~Quote()=default;//都需要一个虚函数,
string isbn(){
return bookNo;
}
virtual double net_price(size_t n)const{return n*price;}//总价函数,定义成虚函数,所以可以动态绑定
private:
string bookNo;
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 = 0;//纯虚函数
protected:
std::size_t quantity=0;
double discount=0.0;
};//这是一个抽象基类,
double Disc_quote::net_price(size_t x)const{//纯虚函数可以提供定义,但是函数体必须定义在外部。
return 0.0;
}
int main(){
Disc_quote d;//抽象基类无法创建对象,报错
return 0;
}
注意:纯虚函数可以提供定义,但函数体必须定义在类的外部
含有纯虚函数的类是抽象基类。抽象基类负责定义接口,后继的其他类可以覆盖该接口。我们不能直接创建一个抽象基类对象,我们可以定义抽象基类的派生类对象,前提是这些派生类覆盖了抽象基类中的纯虚函数。
抽象基类的派生类必须覆盖抽象基类中的纯虚函数,否则派生类将仍然是抽象基类,不能创建对象
虽然抽象基类不能创建对象,但是我们仍然需要定义抽象基类的构造函数,因为抽象基类的派生类将会使用抽象基类的构造函数来构造派生类中的抽象基类部分数据成员:
Disc_quote()=default;//虽然抽象基类无法创建对象,但是仍旧需要构造函数,因为派生类要用
访问和继承:
和私有成员类似:受保护的成员对于类的用户来讲是不可访问的
和公用成员类似:受保护的成员对于派生类成员和友元来讲是可以访问的
派生类的友元只能通过派生类对象来访问基类的受保护成员:
#include<bits/stdc++.h>
using namespace std;
class Base{
public :
Base(int a=0):d(a){}
protected:
int d;
};
class Sneaky:public Base{
friend void clobber(Sneaky&);
friend void clobber(Base&);
private:
int j;
public:
Sneaky(int a=0,int b=0):Base(a),j(b){};
ostream&print(ostream&);
};
ostream& Sneaky::print(ostream& os){
os<<d<<" "<<j<<endl;//派生类可以直接使用
}
void clobber(Sneaky & x){//派生类的友元可以通过派生类的对象访问protected
x.j=x.d=1;
}
void clobber(Base&x){//报错,派生类的友元函数不能直接访问基类protected
cout<<x.d;
}
int main(){
}
公有、私有和受保护继承:
某个类对其继承而来的成员的访问权限受两个因素影响:一是基类中该成员的访问说明符,二是在派生类的派生类列表中的访问说明符。与其派生访问说明符无关。派生类访问说明符的目的是控制派生类用户(包括派生类的派生类在内)对于基类成员的访问权限:
#include<bits/stdc++.h>
using namespace std;
class father{
public :
int fpub;
protected:
int fpro;
private:
int fpri;
};
class pub_son:public father{//共有继承:基类的public是son的public。基类的protected是son的protected.基类的private不是son的private
//也就是讲:用户代码(包括son的派生类)可以访问基类(如成员本身可以访问的话)
int f(){
cout<<fpub<<endl;//正确,派生类可以访问
cout<<fpro<<endl;
cout<<fpri<<endl;//错误,不能访问私有成员
}
};
class pro_son:protected father{//受保护继承,父亲的public和protected都是son的protec
int f(){
cout<<fpub<<endl;//正确,派生类可以访问
cout<<fpro<<endl;//正确
cout<<fpri<<endl;//错误,不能访问私有成员
}
};
class pri_son:private father{
int f(){
cout<<fpub<<endl;//正确,派生类可以访问
cout<<fpro<<endl;//正确
cout<<fpri<<endl;//错误,不能访问私有成员
}
};
int main(){
pub_son *s1=new pub_son;//共有继承
cout<<s1->fpub<<endl;//用户代码能够访问public
cout<<s1->fpro<<endl;//错误
cout<<s1->fpri<<endl;//错误
pro_son *s2=new pro_son;//受保护继承
cout<<s2->fpub<<endl;//错误
cout<<s2->fpro<<endl;//错误
cout<<s2->fpri<<endl;//错误
pri_son *s3=new pri_son;//私有继承
cout<<s3->fpub<<endl;//错误
cout<<s3->fpro<<endl;//错误
cout<<s3->fpri<<endl;//错误
}
派生类向基类转换的可访问性:
派生类向基类的转换是否可访问由使用该转换的代码决定,同时派生类的派生访问说明符也会有影响。假定 D 继承自 B:
//只有D公有的继承B,派生类代码才能向用户转换。如果是私有的或者受保护的就不能实现该转换
#include<bits/stdc++.h>
using namespace std;
class A{
public:
virtual void print(){cout<<"我是A\n";}
};
class B:public A{
public:
virtual void print(){cout<<"我是B\n";}
};
class C:private A{
public:
virtual void print(){cout<<"我是C\n";}
};
int main(){
A *p;
B b;
C c;
p=&b;
// p=&c;//报错,因为C继承A是私有的。基类指针无法指向私有继承派生类
p->print();
return 0;
}
不论D以什么方式继承 B,D的成员函数和友元函数都能使用派生类向基类的转换。
派生类向其直接基类的类型转换对于派生类的成员和友员而言永远是可访问的。
//派生类向基类的转换是可以访问的
#include<bits/stdc++.h>
using namespace std;
class B{};
class D:private B{
friend void ff();
void f(){
B *b=new D;
}
};
void ff(){
B *b=new D;
}
int main(){
B *b1=new D;//用户代码是不能够使用非公有转换来实现多态的
B b;
D d;
b=d;//用户代码是不能够使用非公有转换来实现派生类想基类的转换
return 0;
}
如果D继承B 的方式是公有的或者受保护的,则D的派生类的成员和友员可以使用D向B 的类型转换;反之,如果D继承B 的方式时私有的,则不能使用。这句话咋一看和第二句没什么区别…可一定注意!!是D的派生类。
//可以访问
#include<bits/stdc++.h>
using namespace std;
class B{};
class D:protected B{
friend void ff();
void f(){
B *b=new D;
}
};
void ff(){ B *b=new D;}
class E:private D{
friend void ff1();
void f(){
B *b=new D;
}
};
void ff1(){ B *b=new D;}
int main(){
return 0;
}
//可以访问的
#include<bits/stdc++.h>
using namespace std;
class B{};
class D:public B{
friend void ff();
void f(){
B *b=new D;
}
};
void ff(){ B *b=new D;}
class E:private D{
friend void ff1();
void f(){
B *b=new D;
}
};
void ff1(){ B *b=new D;}
int main(){
return 0;
}
//不可以访问的
#include<bits/stdc++.h>
using namespace std;
class B{};
class D:private B{
friend void ff();
void f(){
B *b=new D;
}
};
void ff(){ B *b=new D;}
class E:private D{
friend void ff1();
void f(){
B *b=new D;
}
};
void ff1(){ B *b=new D;}
int main(){
return 0;
}
友元与继承:
不能继承友元关系,每个类负责控制各自成员的访问权限
改变个别成员的可访问性:
我们可以通过 using 声明改变派生类继承的某个名字的访问级别:
#include<bits/stdc++.h>
using namespace std;
class A{
public :
int pub;
protected:
int pro;
private:
int pri;
};
class B:private A{
public :
using A::pro;//改变了pro的访问权限为public
void f(){
cout<<pro<<endl;
}
};
int main(){
A a;
// cout<<a.pro<<endl;//基类对象本身不能访问
B b;
cout<<b.pro;//但是派生类可以
return 0;
}//也就是通过这样的方式派生类对象可以访问基类的保护成员 , 而基类自己的对象却无法访问 .
注意:using 声明语句中名字的访问权限由该 using 声明语句之前的访问说明符决定
派生类只能为那些它可以访问的名字提供 using 声明。即不能对基类中的 private 成员提供 using 声明
默认的继承访问级别:私有继承!
默认的继承级别是私有继承的。