一、虚函数、覆盖、多态
- 虚函数:成员函数在定义时添加了 virtual 关键字,这种函数叫虚函数。
- 覆盖:如果在子类中实现与父类中的虚函数具有相同的函数,那么子类中的成员函数会覆盖父类中的成员函数。
- 多态:如果子类中的成员函数对父类中的成员进行了覆盖,当一个指向中子类的父类指针或引用了子类的父类引用,当使用它调用虚函数,然后根据实际的调用对象调用子类中的覆盖函数,而不是父类中了虚函数,这种语法现象叫多态。
多态的意义在于,同一种类发出同一种调用,而产生不同的反映。
三、多态的条件
1、多态特性除了父子类之间要构造覆盖,还必须是父类以指针或引用的方式指向子类。
2、当指针或引用已经构造多态时,此时调用成员所传的this指针再调用成员函数时也可以构造多态。
3、在子类的构造函数执行前会先调用父类的构造函数,如果调用被覆盖的虚函数,由于子类还没有构造完成,因此只能是调用父类中的虚函数。
构造函数在进入函数体执行时,类中看的见的资源已经全部构造完成。4、在子类的析构函数执行完成后会再调用父类的析构函数,如果调用被覆盖的虚函数,由于子类已经开始析构完成已经不能算是完整的子类了,因此只能调用父类中的虚函数。
四、纯虚函数和抽象类
1、纯虚函数
class A
{
public:
virtual void test(void) = 0;
virtual void test(void) const = 0;
};
- a、纯虚函数不需要被实现,如果非要实现也不能再类中,必须要在类外(虚函数)。
- b、纯虚函数如果想调用必须在子类中覆盖,然后以多态的方式调用。
2、抽象类
成员函数中有纯虚函数的叫抽象类,这种类不能创建对象。
- 如果子类继承了抽象类,则必须把父类中的纯虚函数覆盖了,否则它也变成了抽象类不能被实例化。
- 因此抽象类只能以指针或引用的方式指向子类来调用自己的非纯虚函数。
3、纯抽象类
所有的成员函数都是纯虚函数,这种类叫纯抽象类。
面向对象的程序设计四大特性:
- 抽象、
定义:对具体问题(对象)进行概括,抽出一类对象的公共性质并加以描述的过程。
- 封装、
定义:将抽象得到的数据和行为相结合,形成一个有机的整体,也就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏.封装是对象和类概念的主要特性. 简单的说,一个类就是一个封装了数据以及操作这些数据的代码的逻辑实体。在一个对象内部,某些代码或某些数据可以是私有的,不能被外界访问。通过这种方式,对象对内部数据提供了不同级别的保护,以防止程序中无关的部分意外的改变或错误的使用了对象的私有部分.
- 继承、
定义:指可以让某个类型的对象获得另一个类型的对象的属性的方法。它支持按级分类的概念。继承是指这样一种能力:它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。 通过继承创建的新类称为“子类”或“派生类”,被继承的类称为“基类”、“父类”或“超类
- 多态
同样的消息被不同类型的对象接收时导致不同的行为。所谓消息是指对类的成员函数的调用,不同的行为是指不同的实现,也就是调用了不同的成员函数。
纯抽象类可以是封装类的一个过程,同时抽象类也可以当作一个统一的接口类。
继承是构成多态的基础;
虚函数是构成多态的关键;
函数覆盖是构成多态的必备条件;
多态的应用:函数参数,函数返回值。
多态的产生必须依靠上面的四点,缺一不可。
4、纯抽象类的应用场景 代码测试
- 回调模式:
函数a由程序员小明实现完成,如果在此时他想调用小光实现的函数b。 1970年
函数b由程序员小光实现,小光在1980年出生。 - 命令模式:
输入一个指使然后执行对应的操作。 - 生产者与消费者模式
- 单例模式(饿汉、懒汉)
- 工厂模式:一个类以专门制造其它类的己任,这种编程模式叫做工厂模式。
五、C++中的强制类型转换
1、C++即支持C风格的类型转换,又有自己风格的类型转换。C风格的转换格式很简单,但是有不少缺点的:
a.转换太过随意,可以在任意类型之间转换。你可以把一个指向const对象的指针转换成指向非const对象的指针,把一个指向基类对象的指针转换成一个派生类对象的指针,这些转换之间的差距是非常巨大的,但是传统的C语言风格的类型转换没有区分这些。
b.C风格的转换没有统一的关键字和标示符。对于大型系统,做代码排查时容易遗漏和忽略。
C++风格完美的解决了上面两个问题。
a.对类型转换做了细分,提供了四种不同类型转换,以支持不同需求的转换;
b.类型转换有了统一的标示符,利于代码排查和检视。
2、C++的强制类型转换使用很麻烦,其实是C++之父不建议使用强制类型转换,一旦代码中需要使用强制类型转换说明代码设计的不合理,强制类型转换是一种亡羊补牢的方法。
3、在C/C++语言中,强制转型是“一个你必须全神贯注才能正确使用”的特性。所以一定要慎用强制转型。
4、 类似的问题还会发生在有符号负数转化为无符号数、双精度类型转化为单精度类型、浮点数转化为整型等时候。以上这些情况都属于数值的强制转型,在转换过程中,首先生成临时变量,然后会进行数值截断。
5 、在标准C中,强制转型还有可能导致内存扩张与截断。这是因为在标准C中,任何非void类型的指针都可以和void类型的指针相互指派,也就可以通过void类型指针这个中介,实现不同类型的指针间接相互转换了。代码如下所示:
double PI = 3.1415926;
double *pd = Π
void *temp = pd;
int *pi = temp; //转换成功
指针pd指向的空间本是一个双精度数据,8字节。但是经过转换后,pi却指向了一个4字节的int类型。这种发生内存截断的设计缺陷会在转换后进行内存访问时存在安全隐患。不过,这种情况只会发生在标准C中。在C++中,设计者为了杜绝这种错误的出现,规定了不同类型的指针之间不能相互转换,所以在使用纯C++编程时大可放心。而如果C++中嵌入了部分C代码,就要注意因强制转型而带来的内存扩张或截断了。
static_cast转换 (静态类型转换 )
1、基本用法:static_cast<目标类型> (原类型 )
2、使用场景:
a、用于类层次结构中基类和派生类之间指针或引用的转换
上行转换(派生类—->基类)是安全的;
下行转换(基类—->派生类)由于没有动态类型检查,所以是不安全的。
b、用于基本数据类型之间的转换,如把int转换为char,这种带来安全性问题由程序员来保证
c、把空指针转换成目标类型的空指针(int* p = static_cast
3、使用特点:
a、主要执行非多态的转换操作,用于代替C中通常的转换操作
b、隐式转换都建议使用static_cast进行标明和替换
class A { // … };
void Function()
{
const A *pConstObj = new A;
A *pObj = pConstObj; //ERROR: 不能将const对象指针赋值给非const对象
pObj = const_cast<A*>( pConstObj); // OK
}
const_cast转换(去常类型转换)
1、基本用法:const_cast<type-id>expression
2、使用场景:
a、常量指针转换为非常量指针,并且仍然指向原来的对象
b、常量引用被转换为非常量引用,并且仍然指向原来的对象
3、使用特点:
a、cosnt_cast是四种类型转换符中唯一可以对常量进行操作的转换符
b、去除常量性是一个危险的动作,尽量避免使用。一个特定的场景是:类通过const提供重载时,一般都是非常量函数调用const_cast将参数转换为常量,然后调用常量函数,然后得到结果再调用const_cast 去除常量性。
reinterpret_cast转换( 重解释类型转换)
1.基本用法:reinterpret_cast<type-id>expression
2.使用场景:不到万不得已,不用使用这个转换符,高危操作
3.使用特点:
a、reinterpret_cast是从底层对数据进行重新解释,依赖具体的平台,可移植性差
b、reinterpret_cast可以将整型转换为指针,也可以把指针转换为数组
c、reinterpret_cast可以在指针和引用里进行肆无忌惮的转换
int num = 0x13141516;
int* p = reinterpret_cast<int*>(num); //0x13141516
cout << p << endl;
cout << reinterpret_cast<int>(p) << endl;
整数与指针之间有类型转换。
代码测试
dynamic_cast转换( 动态类型转换)
1、基本用法:dynamic_cast<type-id> expression
2、使用场景:
只有在派生类之间转换时才使用dynamic_cast,type-id必须是类指针,类引用或者void*。
不能用于内置的基本数据类型的强制转换
3、使用特点:
a、基类必须要有虚函数,因为dynamic_cast是运行时类型检查,需要运行时类型信息,而这个信息是存储在类的虚函数表中,只有一个类定义了虚函数,才会有虚函数表(如果一个类没有虚函数,那么一般意义上,这个类的设计者也不想它成为一个基类)。
b、
对于下行转换:
dynamic_cast是安全的(当类型不一致时,转换过来的是空指针),
static_cast是不安全的(当类型不一致时,转换过来的是错误意义的指针,可能造成踩内存,非法访问等各种问题)
对于上行转换时:
dynamic_cast和static_cast的效果是一样的
c、dynamic_cast还可以进行交叉转换
d、用于父子类的强制转换
e、其他三种都是编译时完成的,而它是运行时处理的,运行时要进行类型检查。
代码测试
用于构成多态的父子类之间指针类型转换。
六、虚函数表
1、什么是虚函数表,当一个类中有虚函数时,编译器会为这个分配一个表专门记录这些虚函数,在类中会一个隐藏的指针成员来指向这张表。
2、如何证明这张表存在。
有虚数的类会比没有虚函数的类(相同中的)多4字节,还会添加补齐和对齐。
3、一个类只有一个张虚函数表,所有的对象共享一张虚函数表。
4、一般对象的前4个字节是指向虚函数表的指针变量。
七、动态类型绑定
1、当使用父类的指针或引用指向子类时,编译器并没有立即生成调用函数的指针,而生成了一段代码,用于检查指针指向的真正的对象是什么类型。
2、在代码真正运行时才通过对象的指针找到指向虚函数的成员指针。
3、再通过成员指针访问到虚函数表,再从中找到调用的函数地址。
4、使用多态会产生额外的一些代码和调用,因此使用多态会降低代码的执行速度。
八、类的信息
1、在C++中,使用typeid可以获取类一些信息,以此来确定指针指向的到底是什么类。
2、typeid可以直接使用来判断是否是同一种类型
- typeid(标识符) == typeid(标识符)
3、typeid(标识符).name() 获取类型名,以字符串的形式呈现。
4、如果typeid(指针)只能获取到指针的类型,typeid(*指针)可以获取到对象实际的类型信息。
九、虚析构
1、如果通过父类指针或引用指向子类对象,当使用delete释放对象时,此时只能调用父类的析构函数,如果在子类中使用new/malloc申请的内存资源,那么将导致内存泄漏。
2、解决方法就是把父类的析构函数设置为虚函数。
3、在设计类时如果析构函数什么都不需要做,编译器也会生成一个空的析构造函数,但这样会让继承它的子类有安全隐患。
4、最好把所有的析构函数都设置为虚函数。
十、C++中的的字符串的使用
仿照string类设计一个String类。
实现String类的
1、构造
2、析构
3、拷贝构造
4、赋值构造
5、+=、+、<<
#include <iostream>
using namespace std;
class Base
{
public:
Base(int c = 2):_c(c){}
public:
int _c;
};
class Derived:public Base
{
public:
int _d;
int _e;
};
int main(void)
{
int tempA = 2;
int tempB = 3;
Base base;
/*1.无编译告警,但是危险操作,譬如说调用drvPtrA->_d会造成不可预知的后果*/
Derived *drvPtrA = static_cast<Derived *>(&base);
drvPtrA->_d = 4;
drvPtrA->_e = 5;
/*2.输出:tempA为4,tempB为5,踩内存了(机器信息:32位ubuntu,编译器c++)*/
cout<<tempA<<endl;
cout<<tempB<<endl;
/*3.Base中没有虚函数,无法查看运行时信息,编译不过*/
Derived *drvPtrB = dynamic_cast<Derived *>(base);
return 0;
}
//在基类派生类互相转换时,虽然static_cast是在编译期完成,效率更高,但是不安全,上例中就示范了一个踩内存的例子。相比之下因为dynamic_cast可以查看运行时信息,上例如果Base含有虚函数,那么drvPtrB就是一个空指针(这可比踩内存什么的好多了),不能操作Derived中_d的数据从而保证安全性,所以应该优先使用dynamic_cast。
#include <iostream>
using namespace std;
class BaseA
{
public:
BaseA(int c = 2):_c(c){}
int _c;
};
class BaseB
{
public:
BaseB(int d = 3):_d(d){}
int _d;
};
int main(void)
{
BaseA baseA;
/*1.编译不过*/
// BaseB *baseB = static_cast<BaseB *>(&baseA);
/*2.无任何编译告警,编译通过,正常运行*/
BaseB *baseC = reinterpret_cast<BaseB *>(&baseA);
cout<<baseC->_d<<endl; //2
return 0;
}
// static_cast虽然也不是一种绝对安全的转换,但是它在转换时,还是会进行必要的检测(诸如指针越界计算,类型检查)。reinterpret_cast完全是肆无忌惮,直接从二进制开始重新映射解释,是极度不安全的,再次提醒,不到万不得已,不要使用
#include <iostream>
using namespace std;
int main()
{
//加上关键字 防止编译器隐士的优化 之前常量放在寄存器中 提高效率 加上以后要去对应的内存中找数据
volatile const int num = 100;
int* p = (int*)#
*p = 200;
cout << num << endl; //200
cout << *p << endl; //200
/*
const int num = 100;
int* p = const_cast<int*>(&num);
*p = 200;
cout << num << endl;//100
cout << *p << endl;*/ 200
}
#include <iostream>
#include <stdlib.h>
using namespace std;
class A
{
};
class B : public A
{
};
int main()
{
int ch = 48;
char num = static_cast<char> (ch);
cout << num << endl;
int* p = static_cast<int*>(malloc(4));
*p = 20;
cout << *p << endl;
A* a = new B;
//B* b = new A; 此行会报错误
B* b = static_cast<B*>(new A);
}
#include <iostream>
#include <typeinfo>
using namespace std;
class A
{
public:
virtual void test(void){}
};
class Base : public A
{
int num;
public:
};
int main()
{
A* a = new Base;
A* a1;
//如果typeid(指针)只能获取到指针的类型,typeid(*指针)可以获取到对象实际的类型信息。
if(typeid(a) == typeid(a1)) //true if(tyepid (a) == typeid(Base)) false
{
cout << "true" << endl;
}
else
{
cout << "false" << endl;
}
cout << typeid(*a).name() << endl;
}
#include <iostream>
using namespace std;
class Base
{
public:
virtual void show(void) = 0;
};
class A : public Base
{
public:
void show(void)
{
cout << "-----我是类A-----" << endl;
}
};
class B : public Base
{
public:
void show(void)
{
cout << "-----我是类B-----" << endl;
}
};
class C : public Base
{
public:
void show(void)
{
cout << "-----我是类C-----" << endl;
}
};
class D : public Base
{
public:
void show(void)
{
cout << "-----我是类D-----" << endl;
}
};
enum CLASS_TYPE {TYPE_A=1,TYPE_B,TYPE_C,TYPE_D};
Base* create_class(CLASS_TYPE type)
{
switch(type)
{
case TYPE_A: return new A;
case TYPE_B: return new B;
case TYPE_C: return new C;
case TYPE_D: return new D;
}
}
int main()
{
Base* b1 = create_class(TYPE_A);
b1->show();
Base* b2 = create_class(TYPE_B);
b2->show();
Base* b3 = create_class(TYPE_C);
b3->show();
Base* b4 = create_class(TYPE_D);
b4->show();
}