C++学习笔记7
1. 防止继承发生
class NoDerived final { }; //NoDerived不能作为基类
class Base { };
//Last是final的,我们不能继承Last
class Last final : Base { }; //Last不能作为基类
class Bad : NoDerived { }; //错误: NoDerived是final的
class Bad2 : Last { }; //错误: Last是final的
2. 继承
总结:
1.基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
2.基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接访问,但需要在派生类中能访问,就定义为protected。可见,保护成员限定符protected是因为继承才出现的。
3.表格里的访问方式都是取最小的“权限”。
4.使用关键字class时默认的继承方式是private,使用struct的默认继承方式是public,不过最好显示地写出继承方式。
5.在实际运用中一般都使用的是public继承,几乎很少去使用protected/private继承,也不提倡去使用。因为protected/private继承下来的成员都只能在派生类的类里面使用,实际中的扩展维护性不强。
3. 纯虚函数和抽象类
3.1 纯虚函数与抽象类
C++中的纯虚函数(或抽象函数)是我们没有实现的虚函数!我们只需声明它!通过声明中赋值0来声明纯虚函数!
/**
* @brief 抽象类
*/
class Test
{
// Data members of class
public:
// Pure Virtual Function
virtual void show() = 0;
/* Other members */
};
- 纯虚函数:没有函数体的虚函数
- 抽象类:包含纯虚函数的类
/**
* @brief 纯虚函数:没有函数体的虚函数
* 抽象类:包含纯虚函数的类
*/
#include<iostream>
using namespace std;
class A
{
private:
int a;
public:
virtual void show()=0; //< 纯虚函数
};
int main()
{
// 抽象类只能作为基类来派生新类使用,不能创建抽象类的对象,抽象类的指针和引用->由抽象类派生出来的类的对象!
// A a; // error 抽象类,不能创建对象
A *a1; // ok 可以定义抽象类的指针
// A *a2 = new A; // error,A是抽象类,不能创建对象
抽象类只能作为基类来派生新类使用,不能创建抽象类的对象,抽象类的指针和引用->由抽象类派生出来的类的对象!
3.2 实现抽象类
抽象类中:在成员函数内可以调用纯虚函数,在构造函数/析构函数内部不能使用纯虚函数。
如果一个类从抽象类派生而来,它必须实现了基类中的所有纯虚函数,才能成为非抽象类。
#include<iostream>
using namespace std;
class A {
public:
virtual void f() = 0;
void g(){ this->f(); }
A(){}
};
class B:public A{
public:
void f(){ cout<<"B:f()"<<endl;}
};
int main(){
B b;
b.g();
return 0;
}
3.3 重要点
-
纯虚函数使一个类变成抽象类
-
抽象类类型的指针和引用
-
如果我们不在派生类中覆盖纯虚函数,那么派生类也会变成抽象类。
-
抽象类可以有构造函数
-
构造函数不能是虚函数,而析构函数可以是虚析构函数。
当基类指针指向派生类对象并删除对象时,我们可能希望调用适当的析构函数。如果析构函数不是虚拟的,则只能调用基类析构函数。
3.4 完整实例
抽象类由派生类继承实现!
#include<iostream>
using namespace std;
class Base
{
int x;
public:
virtual void fun() = 0;
int getX() { return x; }
};
class Derived: public Base
{
int y;
public:
void fun() { cout << "fun() called"; } ///< 实现了fun()函数
};
int main(void)
{
Derived d;
d.fun();
return 0;
}
4. 深入解析c++多态
4.1 为什么需要多态
#include <iostream>
using namespace std;
class Parent{
public:
void printP(){
cout<<"Parent"<<endl;
}
Parent(const Parent& obj){
cout<<"拷贝构造函数"<<endl;
}
private:
int a;
};
class Son:public Parent
{
public:
void printC(){
cout<<"Son"<<endl;
}
private:
int b;
};
void howtoPrint(Parent *base){
base->printP(); //只能执行父类的成员函数
}
void main(int argc, char const *argv[])
{
Parent p1;
Son s1;
Parent *p = NULL;
p1 = &s1;
p1->printP();
howtoPrint(&p1);
howtoPrint(&s1);
return 0;
}
对于上面代码,我们希望Parent的指针或者引用根据其所代表的对象进行调用printP(), 即当是Son时,调用Son的printP(), 当是Parent时调用Parent的printP(), 为了实现这一点,所以需要多态。
4.2 多态基础
多态:就是根据实际的对象类型决定函数调用语句的具体调用目标。
总结一句话就是,同样的调用语句有多种不同的表现形态。
-
要有继承
-
要有虚函数重写
-
父类指针(引用)指向子类对象
4.3 多态的实现原理
-
概念
在一个类中定义一个virtual修饰的函数时,sizeof这个类,发现类的大小多了恰好一个指针的字节大小,它就是c++编译器给我们添加的vptr指针。
当类中声明虚函数时,编译器会在类中生成一个虚函数表;
虚函数表是一个存储成员函数指针的数据结构;
虚函数表是由编译器自动生成与维护的;
virtual成员函数会被编译器放入虚函数表中;
存在虚函数时,每个对象都有一个指向虚函数的指针(vptr指针)
在实现多态的过程中,父类和派生类都有vptr指针。
对象中的vptr指针什么时候被初始化:
对象在创建时,由编译器对vptr指针进行初始化;
只有当对象的构造完全结束后vptr的指向才最终决定下来;
父类对象的vptr指向父类的虚函数表,子类对象的vptr指向子类的虚函数表。
定义子类对象时,vptr先指向父类的虚函数表,在父类构造完成之后,子类的vptr才指向自己的虚函数表。(这也就是在父类或者子类的构造函数中调用虚成员函数不会实现多态的原因,这是一道面试题)
-
内部结构
4.4 多态知识点
1.虚函数默认参数
#include <iostream>
using namespace std;
class Base
{
public:
virtual void fun ( int x = 10 )
{
cout << "Base::fun(), x = " << x << endl;
}
};
class Derived : public Base
{
public:
virtual void fun ( int x=20 )
{
cout << "Derived::fun(), x = " << x << endl;
}
};
int main()
{
Derived d1;
Base *bp = &d1;
bp->fun(); // Derived::fun(), x = 10
return 0;
}
默认参数是静态绑定的,虚函数是动态绑定的。 默认参数的使用需要看指针或者引用本身的类型,而不是对象的类型。
2.静态函数可以声明为虚函数吗?
静态函数不可以声明为虚函数,同时也不能被const 和 volatile关键字修饰
static成员函数不属于任何类对象或类实例,所以即使给此函数加上virutal也是没有任何意义
虚函数依靠vptr和vtable来处理。vptr是一个指针,在类的构造函数中创建生成,并且只能用this指针来访问它,静态成员函数没有this指针,所以无法访问vptr。
3.构造函数可以为虚函数吗?
构造函数不可以声明为虚函数。同时除了inline之外,构造函数不允许使用其它任何关键字。
为什么构造函数不可以为虚函数?
尽管虚函数表vtable是在编译阶段就已经建立的,但指向虚函数表的指针vptr是在运行阶段实例化对象时才产生的。 如果类含有虚函数,编译器会在构造函数中添加代码来创建vptr。 问题来了,如果构造函数是虚的,那么它需要vptr来访问vtable,可这个时候vptr还没产生。 因此,构造函数不可以为虚函数。
我们之所以使用虚函数,是因为需要在信息不全的情况下进行多态运行。而构造函数是用来初始化实例的,实例的类型必须是明确的。 因此,构造函数没有必要被声明为虚函数。
4.析构函数可以为虚函数吗?
析构函数可以声明为虚函数。如果我们需要删除一个指向派生类的基类指针时,应该把析构函数声明为虚函数。 事实上,只要一个类有可能会被其它类所继承, 就应该声明虚析构函数(哪怕该析构函数不执行任何操作)。
5.虚函数可以为私有函数吗?
C++中, 虚函数可以为private, 并且可以被子类覆盖。
#include<iostream>
class Derived;
class Base {
private:
virtual void fun() { std::cout << "Base Fun"; }
friend int main();
};
class Derived: public Base {
public:
void fun() { std::cout << "Derived Fun"; }
};
int main()
{
Base *ptr = new Derived;
ptr->fun();
return 0;
}
输出:
Derived fun()对于上面的程序,下面几点是需要注意的方面:
ptr是一个Base类型的指针,指向的是Derived对象。最终实际调用的是Derived::fun()。
int main()是Base类的友元函数。如果删除这个友元声明,则程序会编译失败。因为在编译期间,会进行权限检查。对于这行代码ptr->fun(), 编译器会检查到fun是私有函数,base类型的对象/指针无权访问。
6.虚函数可以被内联吗?
通常类成员函数都会被编译器考虑是否进行内联。 但通过基类指针或者引用调用的虚函数必定不能被内联。 当然,实体对象调用虚函数或者静态调用时可以被内联,虚析构函数的静态调用也一定会被内联展开。
- 虚函数可以是内联函数,内联是可以修饰虚函数的,但是当虚函数表现多态性的时候不能内联。
- 内联是在编译器建议编译器内联,而虚函数的多态性在运行期,编译器无法知道运行期调用哪个代码,因此虚函数表现为多态性时(运行期)不可以内联。
inline virtual
唯一可以内联的时候是:编译器知道所调用的对象是哪个类(如Base::who()
),这只有在编译器具有实际对象而不是对象的指针或引用时才会发生。
是否内联有编译器控制,所以即使有inline修饰,当编译器判断不能内联的时候,该函数不会被因为加了inline修饰而内联