本节知识点:
1.函数重写:
a.通过上篇文章<c++学习笔记(11.继承的构造与析构)> 我们可以知道,子类与父类可以定义同名成员变量,子类依然继承了父类中的同名变量,并且在默认情况下子类中的同名成员变量隐藏了父类的同名成员变量,如果想访问父类的同名成员变量,就要使用作用域分别符号。
b.子类和父类中的同名成员函数:
第一,函数参数相同的情况,这样就在子类和父类之间出现了,除了函数体以外,原型完全相同的函数。这种情况叫做函数的重写。对于重写,子类中的同名成员函数会隐藏父类中的同名成员函数,如果想调用父类中的同名成员函数,就要使用作用域分别符号。注意:函数重写只发生在父类与子类之间
第二,函数参数不相同的情况,切记,此时即不发生函数的重写,也不发生函数的重载(子类与父类之间是不发生重载的),但是因为函数名字相同,子类中的函数会隐藏父类中的同名成员函数
总结下:子类与父类中,只要存在同名成员,子类中的成员就会隐藏父类中的成员。同名成员函数和同名成员变量都一样。函数参数相同的同名成员函数还会发生函数的重写
示例代码:
#include <iostream>
using namespace std;
class parent
{
public:
void fun(int a) //这两个函数发生了重载
{
cout << "parent fun() " << a << endl;
}
void fun()
{
cout << "parent fun() " << endl;
}
};
class child : public parent
{
public:
void fun() //与上面父类中的函数发生了重写
{
cout << "child fun() " << endl;
}
};
int main()
{
child c;
c.parent::fun(4); //这里发生了函数的重载
c.parent::fun();
c.fun(); //发生了函数的重写 隐藏了父类中的同名函数
//c.fun(4); //这里会报错 因为没有发生函数重载 被子类的函数隐藏了
return 0;
}
c.父类中被重写的函数依然会继承给子类,仅仅是被子类中的函数隐藏了。但是可以通过作用域分辨符:: 来访问父类中被隐藏的函数
2.函数重写与赋值兼容性:
a.对于类的赋值兼容性,父类的指针或引用,指向子类的对象,如:parent *p = &c 或 parent &p = c 此时的p它不能访问子类中独特的成员,它只能访问父类继承给子类的那些成员。其实在赋值的过程中,应该是隐藏了一个类似强制类型转换的过程,就像把一个int的变量赋值给一个char的变量一样,要进行切割的。如果父类和子类之间存在同名成员,能访问的应该都是父类的同名成员(前提是没有虚函数设置)。当p进行访问的时候,先去判断访问成员,是不是父类继承给子类的,不是,直接就会报错,说成员不存在(因为p的类型是parent类型) 。然后再去判断父类子类直接有没有同名的,不同名的,子类会赋值给父类。同名的,会直接调用父类的同名成员,忽略子类的成员。
示例代码:
#include <iostream>
using namespace std;
class parent
{
public:
int a;
int b;
void fun()
{
cout << "parent fun " << endl;
}
};
class child : public parent
{
public:
int a;
void fun()
{
cout << "child fun " << endl;
}
};
int main()
{
child c;
c.a = 10;
c.b = 100;
parent *p = &c;
parent &p1 = c;
cout << p->a << endl;
cout << p1.a << endl;
cout << p1.b << endl;
cout << p->b << endl;
p->fun();
p1.fun();
return 0;
}
注意:如果子类与父类存在同名成员,child c子类对象去访问同名成员,访问到的一定是子类中的同名成员,把父类继承过来的同名成员隐藏(此时是子类的类型)。如果父类的指针或引用指向子类对象的时候,存在同名成员,通过指针或引用访问到的一定是父类的同名成员,子类的同名成员不是被隐藏了,而是根本就没有赋值过来(此时是父类的类型)。这里就说明一个问题,变量的类型决定变量的行为!!!(但是前提一定是没有设置虚函数)
b.c语言和c++语言都是静态编译型语言,所谓静态编译型语言就是在编译前清楚变量函数的类型,根据确定下来的变量函数的类型进行编译。所以说,在默认的情况下,由于程序没有运行,不可能知道父类指针指向的具体是父类对象还是子类对象,一律认为父类指针指向的是父类对象,因此编译的结果为调用父类的成员函数。
3.多态的本质:
a.如何根据不同对象的类型来判断重写函数的调用,这是面向对象中多态的概念。如果父类指针指向的是父类对象,则调用父类中定义的函数。如果父类指针指向的是子类对象,则调用子类中定义的重写函数。
b.多态,根据实际的对象类型决定函数调用语句的具体调用目标,打破了c++静态编译的弊端。使得同样的调用语句有多种不同的表现形式。
c.使用虚函数,让重写后的函数具有多态的特性。c++中通过virtual关键字对多态进行支持,使用virtual声明的函数被重写后即可展现多态特性
示例代码:
#include <iostream>
using namespace std;
class parent
{
public:
virtual void fun()
{
cout << "parent fun() " << endl;
}
};
class child : public parent
{
public:
virtual void fun()
{
cout << "child fun() " << endl;
}
};
int main()
{
child c;
parent *p = &c;
p->fun(); //同样的语句 有不同的表现形式
parent d;
p = &d;
p->fun();//次条语句 具有多态的特性
return 0;
}
注意:对于虚函数的声明,其实仅仅在父类中声明就可以了,不用再在子类中进行声明了。也可以即在父类中声明,也在子类中声明。
4.重写与重载:
a.在阐述重写与重载之前,要先说明一些概念。在一个类里面,肯定是不允许存在同原型函数或同名变量的。但是父类和子类对象之间,虽然说子类是特殊的父类,但是毕竟他俩是相对独立的,这样就允许子类与父类之间存在同名成员了,同时子类还会默认隐藏父类的同名成员。子类中依然继承了父类的同名成员,可以通过作用域分别符进行访问。对于同名成员函数,就存在两种情况,参数相同的就发生了函数的重写。对于参数不相同的,切记,这里没有发生重载(因为子类和父类之间是不可以发生重载的),虽然说参数不相同就是两个完全不相同的函数,但是函数名字相同就发生了隐藏。两者的区别就在,函数的重写是允许父类和子类之间存在完全相同的函数结构。总之,c++允许子类与父类之间,存在完全相同的同名成员函数(即函数的重写)和同名成员变量,也允许存在参数不同的同名成员函数(其实允许是合理的),但不发生函数重载,对于同名成员一律进行对父类的隐藏,并可以通过作用域分别符进行访问。
b.比较重载与重写:
函数重载:必须在同一个作用域。子类无法重载父类函数,父类同名函数将被隐藏。重载是在编译期间静态的根据参数类型和个数决定调用哪个函数的。
函数重写:必须发生在父类与子类之间。并且父类与子类之间的函数必须有完全相同的函数原型。使用virtual关键字声明后能够产生多态。多态是在运行期间动态的根据具体对象的类型决定调用函数的。
5.深入理解虚函数:
a.c++中多态的实现原理:当类中声明虚函数时,编译器会在类中生成一个虚函数表。虚函数表是一个存储类成员虚函数的函数指针的数据结构(实际上是一个链表)。虚函数表是由编译器自动生成与维护的。virtual成员函数会被编译器放入虚函数表中。存在虚函数时,每个对象都有一个指向虚函数表的指针(即VPTR指针)
b.多态实现的过程:首先,得有两个以上具有继承关系的类(一个父类与多个子类是个经典的例子)。然后,类之间存在着函数的重写。再把重写的函数定义为虚函数。因为定义了virtual成员函数,编译器就会在这些类创建对象时分别产生各自的虚函数表。当然,各自的虚函数表需要各自的虚函数表的指针(VPTR指针是类的一个成员,且还是第一个成员)。最重要的是,在调用多态的这些函数时,编译器首先判断函数是否为虚函数。如果是,就利用此时的对象的VPTR指针所指向的虚函数表中查找这个函数,并调用,查找和调用是在程序运行时完成的(注意对象是如何找到VPTR指针的,这个指针首先是不可以外界调用的,当类中有函数声明为virtual属性时,类中就会多一个成员变量,即VPTR指针,且这个指针放在了类的第一个成员的位置,也就是对象的地址就是VPTR指针的地址,当对象创建时,也就是调用了类的构造函数之后就创建了这个对象的虚函数表,此时就给VPTR指针进行了赋值,使其指向对象的虚函数表)。如果不是,编译器就可以直接调用本类型类的成员函数。
如图:
示例代码:
#include <iostream>
using namespace std;
class parent
{
public:
int a;
virtual void fun()
{
cout << "parent fun()" <<endl;
}
};
class child : public parent
{
public:
child()
{
cout << "~~~~~~~~~~~~" <<endl;
}
int b;
void fun()
{
cout << "child fun()" <<endl;
}
};
class a
{
public:
int q;
void fun()
{
}
};
int main()
{
a a1;//证明没有virtual属性,即没有虚函数 对象中就没有VPTR指针
cout << &a1 << endl;
cout << &a1.q<< endl;
child c;//证明有virtual属性,有虚函数 对象的首地址就为VPTR指针
cout << &c << endl;
cout << &(c.a) <<endl;//地址差了4个字节
parent p;
cout << &p << endl;
cout << &(p.a) << endl;
parent *q = &c;
q->fun();
return 0;
}
注意:出于效率的考虑,没有必要将所有成员函数都声明为虚函数。通过虚函数表指针VPTR调用重写函数是在程序运行时进行的,因此需要通过寻址操作才能确定真正应该调用的函数。而普通成员函数是在编译时就确定了调用的函数。在效率上,虚函数的效率要低很多。
c.虚函数与多态的关系:多态是依靠虚函数实现的。但是就算我不想实现多态,依然可以在类中定义虚函数的,类创建对象时,依然产生虚函数表,依然存在VPTR指针。
d.对象在创建的时候由编译器对VPTR指针进行初始化,只有当对象的构造完全结束后VPTR的指向才最终确定,父类对象的VPTR指向父类虚函数表,子类对象的VPTR指向子类虚函数表。
e.构造函数中调用虚函数无法实现多态:说明下这个知识点,是本节的一个疑问。如果有大神路过,希望大神指点下。也希望再日后的学习中,自己可以补充上来!!!对于唐老师的 构造函数与VPTR指针之间的关系分析(唐老师的白板图)、还有那个this指针的举例的观点(唐老师的代码),我有所保留!!!待日后提高!!!这里面有一个不知对错的反例,代码如下:
#include <iostream>
using namespace std;
class parent
{
public:
parent()
{
cout << "hello parent()" << endl;
}
virtual void fun()
{
cout << "parent fun() " <<endl;
}
};
class child : public parent
{
public:
child(parent* p)
{
cout << "hello child()" <<endl;
p->fun();
}
void fun()
{
cout << "child fun() " << endl;
}
};
int main()
{
parent p;
child c(&p);
child c1(&c);
}
虽然不确定,但是依然记住构造函数中无法使用虚函数完成多态。可能语法编译通过了,但是根本无法实现你想要的目的。
6.纯虚函数:
a.面向对象中的抽象类:抽象类可以用于表示现实世界中的抽象概念。抽象类是一种只能定义类型,而不能产生对象的类。抽象类只能被继承并重写相关函数。抽象类的直接特征是纯虚函数。
b.纯虚函数:只声明函数原型,而故意不定义函数体的虚函数
c.抽象类与纯虚函数:
第一:抽象类不能定义对象(因为抽象类,就是利用继承,让自己这个父类的纯虚函数进行重写,使得多个子类产生多态的现象),抽象类的目的不是创建对象,而是使子类产生多态。
第二:抽象类只能定义指针和引用
第三:抽象类中的纯虚函数必须被子类重写
第四:因为有纯虚函数的存在,所以类变成了抽象类,因为纯虚函数没有函数体且不能被调用,所以抽象类也不能定义对象(纯虚函数存在的目的,就是让抽象类的子类进行函数重写,从而实现多态),抽象类定义了对象就有可能调用纯虚函数!
d.纯虚函数的示例:
class shape
{
public:
virtual double area() = 0;
};
注意:area为纯虚函数,= 0 是告诉编译器,这个函数是纯虚函数,故意只声明不定义函数体
示例代码(这是一个很好的抽象类的例子,也是一个很不错的多态的例子):
#include <iostream>
using namespace std;
class Shape
{
public:
virtual double area() = 0;
};
class Rectangle : public Shape
{
double m_a;
double m_b;
public:
Rectangle(double a, double b)
{
m_a = a;
m_b = b;
}
double area()
{
return m_a * m_b;
}
};
class Circle : public Shape
{
double m_r;
public:
Circle(double r)
{
m_r = r;
}
double area()
{
return 3.14 * m_r * m_r;
}
};
void area(Shape* s)
{
cout<<s->area()<<endl;
}
int main(int argc, char *argv[])
{
Rectangle rect(2, 3);
Circle circle(4);
area(&rect);
area(&circle);
return 0;
}
本节问题补充:
1.为什么最好不要在构造函数中使用虚函数实现多态:
a.首先对于this指针来说:this指针是指向当前对象的指针,child c在创建这个c对象的时候,即使在父类中的构造函数中this指针依然是指向c对象的(这个可以根据&c和this指针的打印情况来看)。但是此时的this指针只能访问父类继承给子类的成员,而不能访问子类特有的成员(如果访问会报错),而且访问到的都是父类中的值。总结,其实完全可以当做,this指针在那个类的内部,就是那个类的this指针(虽然this指针实际上的地址是指向不一样的,但是表现出来的现象就是这句话的意思)。
b.根本在父类中就实现不了,当创建不同的子类对象时候,在父类的构造函数中,调用不同的多态的函数。
c.如果真想在构造函数中,调用多态的函数,一定要在子类中使用parent *p这样的父类指针或者父类引用,通过这个指针来调用多态的函数。切记千万别用this指针(直接调用虚函数是和使用this指针是相同的),那根本就实现不了。当然虽然说现在,可以在子类的构造函数中通过虚函数实现多态了,但其实没有解决根本的问题,也没简便反而麻烦了!!!所有不推荐在构造函数中通过虚函数实现多态!!!
示例代码:
#include <iostream>
using namespace std;
class parent
{
public:
parent()
{
// fun();
}
virtual void fun() = 0;
};
class child : public parent
{
public:
child()
{}
child(parent *p)
{
p -> fun();
// fun();
// this -> fun();
}
virtual void fun()
{
cout << "child " << endl;
}
};
class child1 : public parent
{
public:
child1()
{
}
child1(parent *p)
{
p ->fun();
//fun();
//this->fun();
}
virtual void fun()
{
cout << "child1 " << endl;
}
};
int main()
{
child c;
child1 d;
child c1(&d);
child1 d1(&c);
return 0;
}