一、OOP
-
动态绑定:直到运行时才确定到底执行函数的哪个版本。在C++语言中,动态绑定的意思是在运行时根据引用或指针所绑定对象的实际类型来选择执行虚函数的某一个版本。(只作用于虚函数)
#include<bits/stdc++.h> using namespace std; class A { public: string isbn() const; virtual double x(size_t n) const { return 1; } }; class B : public A { public: double x(size_t) const override { return 2; } }; double print(ostream &os, const A &item,size_t n) { double ret = item.x(n); //os << ret << endl; return ret; } int main() { A a; B b; ostream &os = cout; cout << print(os,a,10) << endl; cout << print(os,b,10) << endl; }
二、基类和派生类
-
派生类对象及派生类向基类的类型转换
假设A是B的基类
A a;//基类对象 B b;//派生类对象 A *p = &a;//p指向a对象 p = &b;//p指向b的a部分 A &r = b;//r绑定到b的a部分
-
继承与静态成员
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_obj.statmem(); statmem(); //通过this }
-
防止继承的发生
class NoDerived final{}; class Base; class Last final : Base{}; class Bad : NoDerived{};//错误 class Bad2 : Last{};//错误
-
静态类型与动态类型
当我们使用存在继承关系的类型时,必须将一个变量或其他表达式的静态类型与该表达式表示对象的动态类型区分开来。表达式的静态类型在编译时总是已知的,它是变量声明时的类型或表达式生成的类型;动态类型则是变量或表达式表示的内存中的对象的类型。动态类型直到运行时才可知。
当调用x时:
double ret = item.x(n);
我们知道item的静态类型是A&,它的动态类型则依赖于item绑定的实参,动态类型直到在运行时调用该函数时才会知道。如果我们传递一个B对象给print,则item的静态类型将与它的动态类型不一致。
如果表达式既不是引用也不是指针,则它的动态类型永远与静态类型一致。
-
不存在基类向派生类的隐式类型转换
A a; B* b = &a;//错误 不能将基类转换成派生类 B& br = base;//错误 不能将基类转换成派生类
B b; A* a = b;//动态类型是b B *bp = a;//错误 不能将基类转换成派生类
-
在对象之间不存在类型转换
B b; A item(b);//调用A::A(const A&) item = b; //调用A::operator=(const A&)
可以说b的B部分被切掉了。
三、虚函数
-
对虚函数的调用可能在运行时才被解析
动态绑定只有当我们通过指针或引用调用虚函数时才会发生。
print(cout,a,10);//调用A::print print(cout,b,10);//调用B::print a = b; a.print(); //调用A::print
当我们通过一个具有普通类型(非引用非指针)的表达式调用虚函数时,在编译时就会将调用的版本确定下来。例如,如果我们使用a调用print,则应该运行print版本是显而易见的。我们可以改变a表示的对象的值,但是不会改变该对象的类型。因此,在编译时就调用就会被解析成A的print()。
四、抽象基类
-
含有纯虚函数的类是抽象基类
A a;//wrong B b;
我们不能创建抽象基类的对象。
五、访问控制与继承
-
受保护的成员
- 和私有成员类似,受保护的成员对于类的用户来说是不可访问的。
- 和共有成员类似,受保护的成员对于派生类的成员和有元来说是可访问的。
- 派生类的成员或友元只能通过派生类对象来访问基类的受保护成员。派生类对于一个基类对象中的受保护成员没有任何访问特权。
class Base { protected: int prot_mem; }; class Sneaky : public Base { friend void clobber(Sneaky&);//能访问Sneaky::prot_mem friend void clobber(Base&);//不能访问Base::prot_mem int j;//j默认是private }; //正确 void clobber(Sneaky &s) { s.j = s.prot_mem = 0; } //错误 void clobber(Base &b) { b.prot_mem = 0; }
-
共有、私有和受保护继承
派生访问说明符的目的是控制派生类用户(包括派生类的派生类在内)对于基类成员的访问权限:
#include<bits/stdc++.h> using namespace std; struct Base { public: void pub_mem(); protected: int prot_mem; private: char priv_mem; }; struct Pub_Derv : public Base { int f() { return prot_mem; } //错误:private成员对于派生类来说是不可访问的 char g() { return priv_mem; } }; struct Priv_Derv : private Base { //private不影响派生类的访问权限 int f1() const { return prot_mem; } }; int main() { Pub_Derv d1;//继承自Base的成员是public的 Priv_Derv d2;//继承自Base的成员是private的 d1.pub_mem();//正确:pub_mem在派生类中是public的 d2.pub_mem();//错误:pub_mem在派生类中是private的 }
-
派生类向基类转换的可访问性
- 只有当D共有地继承B时,用户代码才能使用派生类向基类的转换;如果D继承B的方式是受保护的或者私有的,则用户代码不能使用该转换
- 不论D以什么方式继承B,D的成员函数和友元都能使用派生类向基类的转换:派生类向其直接基类的类型转换对于派生类的成员和友元来说永远是可访问的。
- 如果D继承B的方式是共有的或者受保护的,则D的派生类的成员和友元可以使用D向B的类型转换;反之,如果D继承B的方式是私有的,则不能使用。
-
友元与继承
class Base { //添加friend声明,其他成员与之前的版本一致 friend class Pal;//Pal在访问Base的派生类时不具有特殊性 }; 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的友元 };
每个类负责控制自己的成员的访问权限,因此尽管看起来有点儿奇怪,但f3确实是正确的。Pal是Base的友元,所有Pal能够访问Base对象的成员,这种可访问性包括了Base对象内嵌在其派生类对象中的情况。
当一个类将另一个类声明为友元时,这种友元关系只对做出声明的类有效。对于原来那个类来说,其友元的基类或者派生类不具有这种特殊的访问能力:
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 { public: //保持对象尺寸相关的成员的访问级别 using Base::size; protected: using Base::n; }
通过在类的内部使用using声明语句,我们可以将该类的直接或间接类中的任何可访问成员标记出来。using声明语句中名字的访问权限由该using声明语句之前的访问说明符来决定。也就是说,如果一条using声明语句出现在类的private部分,则该名字只能被类的成员和友元访问;如果using声明语句位于public部分,则类的所有用户都能访问它;如果using声明语句位于protected部分,则该名字对于成员、友元和派生类是可访问的。
派生类只能为那些它可以访问的名字提供using声明。
-
默认的继承保护级别
struct 默认共有继承,内部默认public
class默认私有继承,内部默认private
六、继承中的类作用域
-
名字冲突与继承
struct Base { Base(): mem(0){} protected: int mem; }; struct Derived : Base { Derived(int i) : mem(i) {} int get_mem() {return mem;} protected: int mem; //隐藏基类中的mem } Derived d(42); cout << d.get_mem() << endl; //打印42
派生类的成员将隐藏同名的基类成员
通过作用域运算符来使用隐藏的成员
struct Derived : Base { int get_base_mem() return Base::mem; }//结果是0
和其他作用域一样,如果派生类的成员与基类的某个成员同名,则派生类将在其作用域内隐藏该基类成员。即使派生类成员和基类成员的形参列表不一致,基类成员也仍然会被隐藏掉:
#include<bits/stdc++.h> using namespace std; struct Base { int memfcn(); }; struct Derived : Base { int memfcn(int); //隐藏基类的memfcn }; int main() { Derived d; Base b; b.memfcn(); d.memfcn(10); d.memfcn();//错误 d.Base::memfcn(); }
-
虚函数与作用域
#include<bits/stdc++.h> using namespace std; class Base { public: virtual int fcn(); }; class D1 : public Base { public: int fcn(int); virtual void f2(); }; class D2 : public D1 { public: int fcn(int); int fcn(); void f2(); }; int main() { 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(); d2p->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) }
指针都指向了D2类型的对象,但是由于我们调用的是非虚函数,所以不会发生动态绑定。实际调用的函数版本由指针的静态类型决定。
七、构造函数与拷贝控制
如果基类含有几个构造函数,则除了两个例外情况,大多数时候派生类会继承所有这些构造函数。第一个例外是派生类可以继承一部分构造函数,而为其他构造函数定义自己的版本。如果派生类定义的构造函数与基类的构造函数具有相同的参数列表,则该构造函数将不会被继承。定义在派生类中的构造函数将替换继承而来的构造函数。
第二个例外是默认、拷贝和移动构造函数不会被继承。这些构造函数按照正常规则被合成。继承的构造函数不会被作为用户定义的构造函数来使用,因此如果一个类只含有继承的构造函数,则它也将拥有一个合成的默认构造函数。