定义基类和派生类
定义基类
class Quote
{
public:
Quote() = default;
Quote(const string &book, double sales_price):
bookNo(book), price(sales_price) {}
string isbn() const { return bookNo; }
// 返回给定数量的书籍的销售总额
// 派生类负责改写并使用不同的折扣计算算法
virtual double net_price(size_t n) const
{ return n*price; }
virtual ~Quote() = default;
private:
string bookNo; // 书籍的ISBN编号
protected:
double price = 0.0; // 代表普通状态下不打折的价格
};
注意:基类通常都应该定义个虚析构函数,即使该函数不执行任何实际操作也是如此。
成员函数与继承
派生类可以继承其基类的成员,然而当遇到如net_price这样与类型相关的操作时,派生类必须对其重新定义(覆盖 override)。
基类希望派生类进行覆盖的成员函数,一般被定义为虚函数(virtual)。除了构造函数,任何非静态函数都可以是虚函数。
关键字virtual
只能出现在类内部的声明语句之前而不能用于类外部的函数定义。如果基类把一个函数声明成虚函数,则该函数的派生类中隐式地也是虚函数。
访问控制与继承
- public:public表明该数据成员、成员函数是对所有用户开放的,所有用户都可以直接进行调用
- private:private表示私有,私有的意思就是除了class自己之外,任何人都不可以直接使用,即便是子女,朋友,都不可以使用。
- protected:protected对于子女、朋友来说,就是public的,可以自由使用,没有任何限制,而对于其他的外部class,protected就变成private。
读写权限 | 当前类 | 派生类 | 外部类 |
---|---|---|---|
public | √ | √ | √ |
protected | √ | √ | × |
private | √ | × | × |
friend | √ | × | × |
定义派生类
class Bulk_quote : public Quote
{
public:
Bulk_quote() = default;
Bulk_quote(const string&, double, std::size_t, double);
// 覆盖基类的函数版本以实现基于大量购买地折扣政策
double net_price(size_t) const override;
private:
// 实用折扣政策地最低购买量
size_t min_qty = 0;
// 以小数表示的折扣额
double discount = 0.0;
};
- 派生类声明基类的虚函数的新版本时,必须加上关键字
override
- 基类的虚析构函数不需要覆盖
- 注意:直接编译上述代码是会报错:
C:\MinGW\bin\mingw32-g++.exe -g E:\C++\opp.cpp -o E:\C++\opp.exe c:/mingw/bin/../lib/gcc/mingw32/9.2.0/../../../../mingw32/bin/ld.exe: C:\Users\12435\AppData\Local\Temp\cc34XfTn.o: in function
ZN10Bulk_quoteD1Ev’:
E:/C++/opp.cpp:31: undefined reference tovtable for Bulk_quote' c:/mingw/bin/../lib/gcc/mingw32/9.2.0/../../../../mingw32/bin/ld.exe: C:\Users\12435\AppData\Local\Temp\cc34XfTn.o: in function
ZN10Bulk_quoteC1Ev’:
E:/C++/opp.cpp:27: undefined reference tovtable for Bulk_quote' collect2.exe: error: ld returned 1 exit status
\- 原因:父类中的虚函数,在子类中需要重新实现它们。对于在上述代码中的
net_price()
函数,在子类中仅仅声明了,但是没有定义,因此我们需要进一步定义函数:
double Bulk_quote::net_price(size_t n) const{
return 0.0;
};
派生类中的虚函数
为了所设计的应用适应新版本的功能,派生类经常(但不总是)覆盖基类中已定义的函数。一个派生类的函数如果覆盖了某个继承而来的虚函数,则它的形参类型必须与被它覆盖的基类函数完全一致,而且虚函数的返回类型也必须与基类函数匹配。
基类中的虚函数在派生类中隐含地也是一个虚函数
必须要清晰的概念:虚函数只能借助于指针或者引用来达到多态的效果。
final
和override
说明符
值得一提的是,派生类如果定义了一个函数与基类中虚函数的名字相同但是形参列表不同,这仍然是合法的行为。编译器将认为新定义的这个函数与基类中原有的函数是相互独立的(不是覆盖)。
为了避免上述规则造成编程的混乱,我们可以适应override
关键字标记派生类中的虚函数。
class B
{
public:
virtual void f1(int) const;
virtual void f2();
void f3();
}
class D1 : public B
{
public:
//True
void f1(int) const override;
//False,函数不匹配,编译报错
void f2(int) override;
//False, 函数不匹配,编译报错
void f3() override;
//False, 函数不匹配,编译报错
void f4() override;
}
如果不希望某个虚函数被覆盖,我们可以将其设定为final
class D2 : public B
{
public:
// 从B继承f2()和f3(),覆盖f1(int)
void f1(int) const final;
}
class D3 : public D2
{
public:
void f2(); // 正确,覆盖从间接基类B继承而来的f2
void f1(int) const; // 错误,D2不允许f1被覆盖
}
虚函数与默认实参
如果某次函数调用使用了默认实参,则该实参值由本次调用的静态类型决定。如果我们通过基类的引用或指针调用函数,则使用基类中定义的默认实参,即使实际允许的是派生类中的函数版本也是如此。
如果虚函数使用默认实参,则基类和派生类中定义的默认实参最好一致。
回避虚函数的机制
在某些情况下,我们希望对虚函数的调用不要进行动态绑定,而是强迫其执行虚函数的某个版本。
强行调用基类中定义的函数版本而不管BaseP的动态类型到底是什么
double undiscounted = baseP->Quote::net_price(42);
通常情况下,只有成员函数(或友元)中的代码才需要使用作用域运算符来回避虚函数的机制。
当一个类有子类时,该类的析构函数必须是虚函数,否则会有资源释放不完全的情况
#include<iostream>
using namespace std;
class IDelegate
{
public:
IDelegate(){}
~IDelegate() //非虚析构函数
{
std::cout << "~IDelegate()!" << std::endl;
}
};
class CStaticDelegate : public IDelegate
{
public:
CStaticDelegate() {}
~CStaticDelegate()
{
std::cout << "~CStaticDelegate()!" << std::endl;
}
};
template<class T>
class CMethodDelegate : public IDelegate
{
public:
CMethodDelegate() {}
~CMethodDelegate()
{
std::cout << "~CMethodDelegate()!" << std::endl;
}
private:
T m;
};
int main()
{
IDelegate* demo1 = new CStaticDelegate();
注意这里是用父类的指针指向子类
IDelegate* demo2 = new CMethodDelegate<int>();
delete demo1;
delete demo2;
}
注意这里是用父类的指针指向子类
结果:
~IDelegate()!
~IDelegate()!
这里可以看到,对象销毁时只调用了父类的析构函数。如果这时子类的析构函数中有关于内存释放的操作,将会造成内存泄露。所以需要给父类的析构函数加上virtual
。
class IDelegate
{
public:
IDelegate(){}
virtual ~IDelegate() //非虚析构函数
{
std::cout << "~IDelegate()!" << std::endl;
}
};
结果:
~CStaticDelegate()!
~IDelegate()!
~CMethodDelegate()!
~IDelegate()!
内存清理干净了,舒服~~
子类不需要声明函数覆盖基类的虚析构函数
子类不是必须覆盖基类的虚函数
被覆盖的情况:
#include<iostream>
using namespace std;
class A
{
public:
A(){}
virtual void print_hello();
virtual ~A() //非虚析构函数
{
// std::cout << "~A()!" << std::endl;
}
private:
int a = 10;
};
void A::print_hello()
{
cout<<"a = "<<a<<endl;
}
class B : public A
{
public:
B() {}
void print_hello() override;
~B()
{
// std::cout << "~B()!" << std::endl;
}
private:
int b = 20;
};
void B::print_hello()
{
cout<<"b = "<<b<<endl;
}
int main()
{
A* demo1 = new A();
B* demo2 = new B();
demo1->print_hello();
demo2->print_hello();
delete demo1;
delete demo2;
}
结果:
a = 10
b = 20
不覆盖:
class B : public A
{
public:
B() {}
// void print_hello() override;
~B()
{
// std::cout << "~B()!" << std::endl;
}
private:
int b = 20;
};
// void B::print_hello()
// {
// cout<<"b = "<<b<<endl;
// }
结果:
a = 10
a = 10
如果print_hello()
不是虚函数呢?能不能被覆盖?或者说被重载?(答案是可以的)
#include<iostream>
using namespace std;
class A
{
public:
A(){}
void print_hello();
virtual ~A() //非虚析构函数
{
// std::cout << "~A()!" << std::endl;
}
private:
int a = 10;
};
void A::print_hello()
{
cout<<"a = "<<a<<endl;
}
class B : public A
{
public:
B() {}
void print_hello(int para);
~B()
{
// std::cout << "~B()!" << std::endl;
}
private:
int b = 20;
};
void B::print_hello(int para)
{
cout<<"para = "<<para<<", ";
cout<<"b = "<<b<<endl;
}
int main()
{
A* demo1 = new A();
B* demo2 = new B();
demo1->print_hello();
demo2->print_hello(30);
delete demo1;
delete demo2;
}
结果:
a = 10
para = 30, b = 20
抽象基类
纯虚函数
首先:强调一个概念
-
定义一个函数为虚函数,不代表函数为不被实现的函数。
-
定义他为虚函数是为了允许用基类的指针来调用子类的这个函数。
-
定义一个函数为纯虚函数,才代表函数没有被实现。
定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。
一般虚函数清晰的范例:
#include<iostream>
#include<memory>
using namespace std;
class A
{
public:
A(){}
virtual void print_hello();
~A() //非虚析构函数
{
std::cout << "~A()!" << std::endl;
}
private:
int a = 10;
};
void A::print_hello()
{
cout<<"a = "<<a<<endl;
}
class B : public A
{
public:
B() {}
void print_hello();
~B()
{
std::cout << "~B()!" << std::endl;
}
private:
int b = 20;
};
void B::print_hello()
{
cout<<"b = "<<b<<endl;
}
int main()
{
// 基类指针 -> 基类对象
shared_ptr<A> demo1 = make_shared<A>(A());
// 子类指针 -> 子类对象
shared_ptr<B> demo2 = make_shared<B>(B());
// 基类指针 -> 子类对象
shared_ptr<A> demo3 = make_shared<B>(B());
// 绑定到基类的函数
demo1->print_hello();
// 绑定到子类的函数
demo2->print_hello();
// 绑定到子类的函数
demo3->print_hello();
// 强制使用基类版本的函数
demo3->A::print_hello();
}
结果:
~A()!
~B()!
~A()!
~B()!
~A()!
a = 10
b = 20
b = 20
a = 10
~B()!
~A()!
~B()!
~A()!
~A()!
虚函数只能借助于指针或者引用来达到多态的效果。
纯虚函数是在基类中声明的虚函数,它在基类中没有定义(仅声明,没定义),但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加 =0
virtual void funtion1()=0
引入纯虚函数的意义
- 为了方便使用多态特性,我们常常需要在基类中定义虚拟函数。
- 在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。
为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:virtual ReturnType Function()= 0;
),则编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述两个问题。
声明了纯虚函数的类是一个抽象类。所以,用户不能创建类的实例,只能创建它的派生类的实例。
纯虚函数最显著的特征是:它们必须在继承类中重新声明函数(不要后面的=0
,否则该派生类也不能实例化),而且它们在抽象类中往往没有定义。
定义纯虚函数的目的在于,使派生类仅仅只是继承函数的接口。
纯虚函数的意义,让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但类无法为纯虚函数提供一个合理的默认实现。所以类纯虚函数的声明就是在告诉子类的设计者,“你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它”。
容器与继承
因为C++不允许在容器中保存不同类型的元素,因此通常必须采取间接存储的方式。
class A
{
public:
A(){}
virtual void print_hello();
~A() //非虚析构函数
{
std::cout << "~A()!" << std::endl;
}
private:
int a = 10;
};
void A::print_hello()
{
cout<<"a = "<<a<<endl;
}
class B : public A
{
public:
B() {}
void print_hello();
~B()
{
std::cout << "~B()!" << std::endl;
}
private:
int b = 20;
};
void B::print_hello()
{
cout<<"b = "<<b<<endl;
}
int main()
{
vector<A> Avec;
Avec.push_back(A());
// 合法,但是只能把对象的基类部分拷贝给Avec
Avec.push_back(B());
Avec.back().print_hello();
}
结果:
~A()!
~A()!
~B()!
~A()!
a = 10
~A()!
~A()!
在容器中放置(智能)指针而非对象
当我们希望在容器中存放具有继承关系的对象时,我们实际上存放的通常是基类的指针(更好的选择是智能指针)。这些指针所指对象的动态类型可能是基类类型,也可能是派生类类型:
vector<shared_ptr<A>> Avec;
Avec.push_back(make_shared<A>(A()));
Avec.push_back(make_shared<B>(B()));
Avec.back()->print_hello();
结果:
~A()!
~B()!
~A()!
b = 20
~A()!
~B()!
~A()!