- 多态
-
动态多态
**动态多态**则是利用虚函数实现了运行时的多态,也就是说在系统编译的时候并不知道程序将要调用哪一个函数,只有在运行到这里的时候才能确定接下来会跳转到哪一个函数的栈帧。 模式套路可归纳为“**继承**+**虚函数**+**重写**” 派生类对象的地址可以赋值给基类指针。对于通过基类指针调用基类和派生类中都有的同名、同参数表的虚函数的语句,编译时并不确定要执行的是基类还是派生类的虚函数;而当程序运行到该语句时,如果基类指针指向的是一个基类对象,则基类的虚函数被调用,如果基类指针指向的是一个派生类对象,则派生类的虚函数被调用。这种机制就叫作“多态(polymorphism)” 所谓“虚函数”,就是在声明时前面加了 virtual 关键字的成员函数。virtual 关键字只在类定义中的成员函数声明处使用,不能在类外部写成员函数体时使用。静态成员函数不能是虚函数。 包含虚函数的类称为“多态类”。 多态可以简单地理解为同一条函数调用语句能调用不同的函数;或者说,对不同对象发送同一消息,使得不同对象有各自不同的行为。 例子:
#include <iostream>
using namespace std;
//基类
class Shape {
public:
Shape() {};
~Shape() {};
virtual int area() {
std::cout << "Parent area" << std::endl; //1
return 0;
}
};
//派生类Rectangle
class Rectangle:public Shape
{
public:
Rectangle() {};
~Rectangle() {};
int area() {
std::cout << "Rectangle area" << std::endl; //2
return 0;
}
};
//派生类Triangle
class Triangle :public Shape
{
public:
Triangle() {};
~Triangle() {};
int area() {
std::cout << "Triangle area" << std::endl; //3
return 0;
}
};
int main() {
Shape *shape;
Rectangle rec;
Triangle tri;
shape = &rec; //基类指向派生类Rectangle
shape->area(); //输出2
shape = &tri; //基类指向派生类Triangle
shape->area(); //输出3
system("pause");
return 0;
}
如果Shape的area()去掉virtual关键字,将都输出1
-
静态多态
**静态多态**就是在系统编译期间就可以确定程序执行到这里将要执行哪个函数,例如:函数的重载,对象名加点操作符执行成员函数等,都是静态多态,其中,重载是在形成符号表的时候,对函数名做了区分,从而确定了程序执行到这里将要执行哪个函数,对象名加点操作符执行成员函数是通过this指针来调用的。
##函数重载
参见下面重载
##泛型编程
#include <iostream>
using namespace std;
class Car
{
public:
void run() const {
cout << "run a car\n"; //1
}
};
class Airplane
{
public:
void run() const {
cout << "run a airplane\n"; //2
}
};
template <typename Vehicle>
void run_vehicle(const Vehicle& vehicle) {
vehicle.run();
}
int main() {
Car car;
Airplane airplane;
run_vehicle(car); //编译连接的时候就知道运行到1
run_vehicle(airplane); //编译连接的时候就知道运行到2
system("pause");
return 0;
}
上面代码中,Vehicle被修改后用作模版参数而不是公共基类对象,通过编译器进行处理后,最终得到run_vehicle< car >()和run_vehicle< airplane >()这两个不同的函数,这是和动态多态不同的。
#联编
百度科普:
联编是指一个程序自身彼此关联的过程。按照联编所进行的阶段不同,可分为静态联编和动态联编。
静态联编又称静态绑定,指在调用同名函数(即重载函数)时编译器将根据调用时所使用的实参在编译时就确定下来应该调用的函数实现。它是在程序编译连接阶段进行联编的,这种联编又称为早期联编,这是因为这种联编工作是在程序运行之前完成的。它的优点是速度快,效率高,但灵活性不够。编译时所进行的联编又称为静态束定。束定是指确定所调用的函数与执行该函数代码之间的关系。
动态联编也称动态绑定,是指在程序运行时,根据当时的情况来确定调用的同名函数的实现,实际上就是在运行时选择虚函数的实现。这种联编又称为晚期联编或动态(束定。实现条件:①要有继承性且要求创建子类型关系;)②要有虚函数;③通过基类的对象指针或引用访问虚函数。继承是动态联编的基础,虚函数是动态联编的关键,虚函数经过派生之后,在类族中就可以实现运行过程中的多态。动态联编要求在运行时解决程序中的函数调用与执行该函数代码间的关系,调用虚函数的对象是在运行时确定的。对于同一个对象的引用,采用不同的联编方式将会被联编到不同类的对象上。即不同联编可以选择不同的实现,这便是多态性。它的优点是灵活性强,但效率较低。
父类子类指针函数调用注意事项
1,如果以一个基础类指针指向一个衍生类对象(派生类对象),那么经由该指针只能访问基础类定义的函数(静态联翩)
2,如果以一个衍生类指针指向一个基础类对象,必须先做强制转型动作(explicit cast),这种做法很危险,也不符合生活习惯,在程序设计上也会给程序员带来困扰。(一般不会这么去定义)
3,如果基础类和衍生类定义了相同名称的成员函数,那么通过对象指针调用成员函数时,到底调用那个函数要根据指针的原型来确定,而不是根据指针实际指向的对象类型确定。
#重载、重写(覆盖)、重定义(同名隐藏)
我们知道C++中虚函数允许子类重新定义成员函数,而子类重新定义父类的做法称为覆盖(override),或者称为重写。在这里我觉得有必要要明白几个概念的区别:即重载,重写(覆盖),以及重定义(同名隐藏)。
重载:指在同一作用域中允许有多个同名函数,而这些函数的参数列表不同,包括参数个数不同,类型不同,次序不同,需要注意的是返回值相同与否并不影响是否重载。
重写(覆盖)和重定义(同名隐藏)则有点像,区别就是在写重写的函数是否是虚函数,只有重写了虚函数的才能算作是体现了C++多态性,否则即为重定义,在之前的代码中,我们看到子类继承了基类的函数,若是子类(派生类)没有函数,依旧会调用基类的函数,若是子类已重定义,则调用自己的函数,这就叫做同名隐藏。
(当然此时如果还想调用基类的TestFunc函数,只需在调用TestFunc函数前加基类和作用域限定符即可)
class A {
public:
int i;
virtual void func1() { std::cout << "A func1" << std::endl; };
void func2() { std::cout << "A func2" << std::endl; };
void func3() { std::cout << "A func3" << std::endl; };
};
//覆盖:基类方法不在了; 隐藏:基类方法存在
class B : public A {
public:
int j;
void func1() { std::cout << "B func1" << std::endl; }; //覆盖
void func2(const std::string& str) { std::cout << "B func2" << std::endl; }; //隐藏
void func3() { std::cout << "B func3" << std::endl; }; //隐藏
};
int main(int argc, char* argv[]) {
std::cout << "Size of A: " << sizeof(A) << ",Size of B: " << sizeof(B) << std::endl;
B* p = new B();
p->func1(); //b func1
p->func2("str"); //b func2
p->func3(); //b func3
A* p1 = new B();
p1->func1(); //b func1
p1->func2(); //a func2
p1->func3(); //a func3
A* p2 = new A();
p2->func1(); //a func1
A* p3 = static_cast<A*>(p); //向上转型,安全
p3->func1(); //B func1
p3->func2(); //A func2
p3->func3(); //A func3
system("pause");
return 0;
}
对于隐藏的调用,由当时的调用对象决定,p是指向B的指针,因此调用的是B的方法,p1,p2,p3指向A,即使当时new的实例是B,但还是调用A类的方法
#虚函数、纯虚函数、抽象类,具体类、接口
##虚函数
虚函数 是在基类中使用关键字 virtual 声明的函数。在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数。
我们想要的是在程序中任意点可以根据所调用的对象类型来选择调用的函数,这种操作被称为动态链接,或后期绑定。
virtual int area() {
std::cout << "Parent area" << std::endl; //1
return 0;
}
##纯虚函数
您可能想要在基类中定义虚函数,以便在派生类中重新定义该函数更好地适用于对象,但是您在基类中又不能对虚函数给出有意义的实现,这个时候就会用到纯虚函数。
virtual int area() = 0;
##抽象类
含有至少一个纯虚函数的类称为抽象类,不能被实例化;反之能被实例化的类称为具体类
##具体类
如果子类不实现接口,则子类仍为抽象类,只有实现了所有的纯虚函数才能成为具体类
##接口
纯虚函数也可称为接口函数,即父类提供方法,没有实现,但是由各自的子类实现,利用这实现多态和封装。
##虚函数表
class A
{
public:
int i;
virtual void func() { cout << "A func" << endl; }
virtual void func2() { cout << "A func2" << endl; }
void func3() { cout << "A func3" << endl; }
};
class B : public A
{
int j;
void func() { cout << "B func" << endl; }
void func3() { cout << "B func3" << endl; }
};
int main()
{
cout << sizeof(A) << ", " << sizeof(B); //输出 8,12,占用内存空间大小的有i(4B),j(4B),其中还有4B大小的虚函数表指针(32位程序下,64位程序输出16,24)
A* p = new B();
p->func(); //B func
p->func3(); //A func3
p->func2(); //A func
return 0;
}
##C++ 函数调用操作符 ()
函数调用操作符是(),因此,此操作符的函数重载是operator()()。重载函数调用操作符的类对象称为函数对象或仿函数(functor),因为我们可以像使用函数名一样使用对象名。先来看一个简单的例子。下面是重载了函数调用操作符的一个类:
class Area
{
public:
int operator()(int length, int width) { return length*width; }
};
此类中的操作符函数计算一个面积,它是两个整数实参的乘积。为了使用此操作符函数,只需要创建一个类型为Area的对象,例如:
Area area; // Create function object
int pitchLength(100), pitchWidth(50);
int pitchArea = area(pitchLength, pitchWidth); // Execute function call overload
第一条语句创建第三条语句中使用的area对象,第三条语句使用此对象来调用对象的函数调用操作符。在此例中,返回的是足球场的面积。
当然,也可以将一个函数对象传递给另一个函数,就像传递任何其他对象一样。看看下面这个函数:
void printArea(int length, int width, Area& area)
{
cout << "Area is " << area(length, width);
}
printArea(20, 35, Area());
-
模版元编程
- 可变参数列表
https://eli.thegreenplace.net/2014/variadic-templates-in-c/
https://blog.csdn.net/hyl999/article/details/106261528
https://blog.csdn.net/zhouguoqionghai/article/details/45789295
原理深入
- 可变参数列表
-
左值引用和右值引用
https://zhuanlan.zhihu.com/p/97128024
github上的一个thread poo
C++对于左值和右值没有标准定义,但是有一个被广泛认同的说法:
可以取地址的,有名字的,非临时的就是左值;
不能取地址的,没有名字的,临时的就是右值;
可见立即数,函数返回的值等都是右值;而非匿名对象(包括变量),函数返回的引用,const对象等都是左值。
从本质上理解,创建和销毁由编译器幕后控制,程序员只能确保在本行代码有效的,就是右值(包括立即数);而用户创建的,通过作用域规则可知其生存期的,就是左值(包括函数返回的局部变量的引用以及const对象)。