cpp primer 第十五章笔记
终于到了第十五章了,复习大计实施的还不错~~
- 动态绑定可以用指针或者引用实现
- 基类的析构函数一般都应该是virtual修饰的,因为通常我们会在一个基类指针中存放一个派生类的指针,delete这个指针时编译器要调用合适的析构函数,讲基类的析构函数定义为virtual的使编译器自动调用派生类的析构函数。不用担心,析构时仍然会调用基类的析构函数
- 在用于覆盖虚函数的函数后面可以添加关键字override来显示注明,位置是const和引用限定符后面
- 静态成员可以被继承切在整个继承体系中只有一个
- 防止继承,在类定义后面加final关键字
- 基类的拷贝构造函数不是虚函数,因为用一个派生类给基类赋值只会赋值派生类中的基类部分
- 类中普通的成员函数我们可以只提供声明而不定义,只要不使用即可,但是必须为所有的虚函数提供定义,因为直到运行时才会知道哪个虚函数被使用了
- 引用和指针的静态类型与动态类型不同这一事实正是C++语言支持多态的根本所在
- 如果派生类中覆盖了某个虚函数,它可以再一次使用virtual指定其虚函数属性,然而这是没必要的,因为一旦某个函数被声明为虚函数,在整个继承体系中它都是虚函数。
- 派生类要覆盖虚函数,新函数必须有与虚函数一样的参数和返回值,唯一的例外是,如果虚函数返回的是类本身(基类)的指针或引用,那么覆盖函数允许返回派生类的指针或引用
- 也可以将某个函数指定为final,防止后续派生类覆盖它(一般用于间接派生中,自己定义了一个覆盖父类的虚函数,但是不希望后续子类覆盖它)
- 虚函数中使用默认实参是依赖与当前的静态类型的!!!所以最好将一个虚函数所有的覆盖函数中的默认实参定义成一样的,防止出错
- 使用作用域运算符可以回避虚函数机制,静态指定调用目标,这一般用在派生类的虚函数需要调用基类的虚函数做一些操作的时候,此时如果不指定作用域,运行时会被解析为调用自身,从而无限递归
- 纯虚函数,带 =0 的虚函数,可以不提供定义(一般不提供),也可以提供,但是必须在类外定义。
- 含有(或者直接继承,没有覆盖)纯虚函数的类是抽象基类,抽象基类不能创建对象
- protected成员:
- 用户不可访问,但是对其派生类和其友元是可以访问的(注意是基类的友元)
- 对于派生类的友元,想要访问基类的protected成员,只能通过派生类来“代理”访问
关于派生类向基类转换的可访问性,引用primer的总结,
对于代码中的某个给定节点来说,如果基类的公有成员是可访问的,则派生类向基类的类型转换也是可访问的;反之则不行。
友元没有传递性,友元也不能继承
可以用using声明改变继承来的可访问成员的可访问性(比如把private继承来的某父类成员用using声明变成public的)
struct默认访问说明符为public,默认继承保护级别也是public;而class默认访问说明符是private的,默认继承保护级别也是private(but还是显示声明吧。。。)
指针和引用的静态类型决定哪些成员可见
名字查找先于类型检查,所以派生类中的函数会隐藏掉基类中的同名函数,即使二者函数签名不同,也不会构成函数重载
所以说,如果在覆盖虚函数的时候写错了参数,不但不能覆盖虚函数(用基类指针调用虚函数调用的是基类的虚函数),而且还会在派生类中隐藏掉该虚函数(用该派生类指针调用虚函数会报错,因为虚函数被隐藏了),比如:
#include <iostream> class base{ public: virtual int func(){std::cout<<"base v"<<std::endl;} }; class d1:public base{ public: int func(int n){std::cout<<"d1 func"<<std::endl;} }; int main(){ base *bp=new d1; d1 *dp=bp; bp->func(); //调用的是base的虚函数func //dp->dunc(); //编译错误,因为对d1而言,base的虚函数func被d1的func(int)隐藏了 delete bp; return 0; }起始对于派生类来说,基类的虚函数和非虚函数都可以覆盖,只不过覆盖虚函数可以在其基类指针中动态调用,而非虚函数不行。但是,一旦定义一个名字与基类中某个函数一样的函数,基类中的函数就被覆盖了,基类中的所有的该函数的重载函数也一样。所以如果想在派生类中使用基类的所有重载函数,要不就不要覆盖,要不就把每个重载函数都覆盖。或者,用一条using声明将基类中的函数“拿”到派生类的public域中,然后再覆盖其中要覆盖的函数。
#include <iostream> class base{ public: void func(){std::cout<<"bfunc"<<std::endl;} void func(int n){std::cout<<"bfunc2"<<std::endl;} int x=0; }; class d1:public base{ public: using base::func; void func(){std::cout<<"dfunc"<<std::endl;} using base::x; int x=1; }; int main(){ d1 d; d.func(); d.func(3); // std::cout<<"d.x="<<d.x<<std::endl; //二义性错误 return 0; }派生类的构造函数、拷贝构造函数、拷贝赋值运算符以及移动构造函数、移动赋值运算符都应该显式调用基类的相关函数,传入复制/移动对象即可,因为基类的这些函数参数一定是个引用,基类可以引用派生类。而析构函数则不需要,只释放自己的资源就好。
如果在构造或析构函数中调用了虚函数,调用的是当前函数对应类的虚函数版本,不会调用派生类的版本,因为此时派生类部分还没有初始化活着已经析构了,不存在(换句话说,此时虚表中不存在派生类的信息)。
#include <iostream> using namespace std; class base{ public: base(){vfunc();} virtual void vfunc(){cout<<"base n: "<<n<<endl;} virtual ~base(){cout<<"d base ";vfunc();} private: int n=0; }; class d1:public base{ public: d1(){vfunc();} void vfunc(){cout<<"d1 x: "<<x<<endl;} ~d1(){cout<<"d d1";vfunc();} private: int x=1; }; int main(){ // d1 d; base *bp=new d1; delete bp; return 0; }$ ./dcvirtual.out base n: 0 d1 x: 1 d d1d1 x: 1 d base base n: 0因为向一个基类赋值派生类会产生“截断”现象(因为调用的是基类的拷贝函数),所以不要在容器中直接存储基类对象,这样一旦你向其中放入派生类,就会截断,切编译器不会报错。应在容器中存储基类指针。
