【C++】模板/继承/多态

2024-09-05 13:54:28 浏览数 (2)

函数模板

  • 意义:对类型进行参数化 模板的实参推演:可以根据用户传入的实参类型,来推导出模板类型。 函数模板 不会参与编译,在函数调用点,实例化/推导出类型,模板函数再进行编译。
  • 模板代码是不能在一个文件中定义,在另一个文件中使用 模板代码调用之前,一定要看到模板定义的地方,这样的话,模板才能进行正常的实例化,产生能够被编译器编译的代码。
  • 模板一般都是放在头文件中的,在源文件中展开
  • 函数模板的非类型参数 必须是整数类型(整数/地址/引用)都是常量,只能使用

继承

  1. 继承的本质和原理 继承·的·本质·: a.代码复用 b.在基类中给所有派生类提供统一的虚函数接口,让派生类重写虚函数,然后就可以使用多态
  • 类和类之间的关系:组合and继承 组合:a part of …一部分的关系 继承:a kind of… 一种·的关系
  • 总结: 外部只能访问对象public的成员,protected和private的成员无法直接访问。 在继承结构中,派生类从基类可以继承过来private的成员,但是派生类却无法直接访问。
  1. protected和private的区别?
  • 在基类中·定义的成员,想被派生类访问,但是不想被外界访问,那么在基类中,把相关成员定义成protected保护的,如果派生类和外部都不打算访问,那么在基类中,就把相关成员定义成private私有的
  1. 默认继承方式: class定义派生类,默认继承方式就是private私有的 struct定义派生类,默认方式就是public
  • 派生类从继承可以继承所有的成员(变量和方法),除过构造函数和析构函数
  1. 派生类怎么初始化从基类继承来的成员变量呢? 通过调用·基类相应的构造函数来初始化
  • 派生类的构造函数和析构函数,负责初始化和清理派生类部分
  1. 派生类从基类继承来的成员的初始化和清理谁来负责?
  • 是由基类的构造和析构来负责
  1. 派生类对象构造和析构的过程是:
  • 派生类调用基类的构造函数,初始化从基类继承来的成员。 调用派生类自己的构造函数。初始化派生类自己特有的成员 派生类对象的作用域到期了 1.调用派生类的析构函数,释放派生类成员可能占用的外部资源(堆内存,文件) 2.调用基类的析构函数,释放派生类内存中,从基类继承来的成员可能占用的外部资源(堆内存,文件)
  • 重载:一组函数要重载,必须处在同一个作用域当中,而且函数名字相同,参数列表不同
  • 隐藏(作用域的隐藏)的关系: 在继承结构当中,派生类的同名成员,把基类的同名成员给隐藏调用了
  • 覆盖:基类和派生类的方法,返回值,函数名以及参数列表都相同,而且基类的方法是虚函数,那么派生类的方法就自动处理成虚函数,他们之间成为覆盖关系。
  1. 把继承结构,也就是说成从上(基类)到下(派生类)的结构

基类对象 < -派生类对象 类型从下到上的转换(可以) 派生类对象 <- 基类对象 类型从上到下的转换(不可以) 基类指针(引用)<- 派生类对象 类型从下到上的转换(可以) 派生类指针(引用)<-基类对象 类型从上到下的转换(不可以)

  • 在继承结构中进行上下的类型转换,默认只支持从下到上的类型转换。

虚函数,静态绑定/动态绑定

总结一: 如果类里面定义了虚函数,那么编译阶段,编译器给这个类类型产生一个唯一的vftable虚函数表,虚函数表中主要存储的内容就是RTTI指针(运行时的类型信息)和虚函数的地址。当程序运行时,每一张虚函数表都会加载到内存的.rodata区。

二: 一个类里面定义了虚函数,那么这个类定义的对象,其运行时,内存中开始的部分,多存储一个vfptr虚函数指针,指向相应类型的虚函数表vfptable。 一个类型定义的n个对象,他们的vfptr指向的都是同一张虚函数表。

三: 一个类里面虚函数的个数,不影响对象内存大小(vfptr)影响的是虚函数白表的大小。

四: 如果派生类中的方法和基类继承来的某个方法,返回值,函数名,参数列表都相同,而且基类的方法是virtual虚函数,那么派生类的这个方法,自动处理成虚函数。

静态绑定/动态绑定

在编译时期的绑定(函数的调用) 只call指令-静态绑定

在运行时期的绑定(函数的调用)

代码语言:javascript复制
  mov eax,dword ptr[pd] 
  mov ecx,dword ptr[eax]
  	call ecx(虚函数的地址) 动态绑定
  1. 那些函数不能实现成虚函数? 虚函数能产生地址,存储在vftable当中 对象必须存在(vfptr -》vftable -》虚函数地址)
  2. 构造函数 virtual 构造函数 (错误) 构造函数中(调用的任何函数,都是静态绑定的)调用虚函数,也不会发生静态绑定。派生类对象构造过程:先调用的是基类的构造函数 再调用派生类的构造函数。 static静态成员方法 (错误) 虚析构函数 (可以) 析构函数调用时,对象是存在的 基类的虚函数是虚函数,派生类的析构函数自动变成虚函数 当基类指针(引用)指向堆上new出来的派生类对象的时候,delete 基类指针,它调用析构函数时,必须发生动态绑定,否则会导致派生类的析构函数无法调用。
  3. 虚函数和动态绑定 问题是不是虚函数的调用一定就是动态绑定? 在类的构造函数当中,调用虚函数,也是静态绑定(构造函数中调用其他 函数(虚)不会发生动态绑定) 如果不是通过指针或者引用变量来调用虚函数,那就是静态绑定。

如何解释多态

静态(编译时期)的多态:函数重载,模板(函数模板,类模板)

代码语言:javascript复制
bool compare(int , int){};
bool compare(double,double){};

compare(10,20); /在编译阶段就确定了调用的函数版本

template<typename T>
bool compare(T a,T b){};

compare<int>(10,30); int 实例化一个 compare<int>
compare(1.2,5.1);	推导出double实例化一个 compare<double>
  • 动态(运行时期)的多态: 在继承结构中,基类指针(引用)指向派生类对象,通过该指针(引用)调用同名覆盖方法(虚函数),基类指针指向哪个派生类对象,就会调用哪个派生类对象的同名覆盖方法。 多态底层是通过动态绑定来实现的。pbase 指向谁就访问谁的vfptr,从而继续访问谁的vftable,也就调用对应的派生类对象的方法了。

抽象类

拥有纯虚函数的类,叫做抽象类 抽象类不能再实例化对象了,但是可以定义指针和引用变量。 一般情况会把基类定义成抽象类。

多重继承

代码复用 一个派生类有多个基类

virtual可以修饰继承方式,是虚继承,虚继承的类是虚基类

基类指针指向派生类对象,永远指向的是派生类基类部分数据的起始地址。

代码语言:javascript复制
#include <iostream>

// 虚基类
class Base {
public:
    int data;
};

// 第一个派生类
class Derived1 : virtual public Base {
public:
    void setData(int value) {
        data = value;
    }
};

// 第二个派生类
class Derived2 : virtual public Base {
public:
    void displayData() {
        std::cout << "Data: " << data << std::endl;
    }
};

// 最终派生类
class FinalDerived : public Derived1, public Derived2 {
public:
    // 使用虚基类的成员函数和数据
    void accessData() {
        setData(42); // 通过Derived1访问Base的成员函数
        displayData(); // 通过Derived2访问Base的数据
    }
};

int main() {
    FinalDerived obj;
    obj.accessData();
    return 0;
}

虚基类是用于解决多重继承中的菱形继承问题的一种机制。当一个类同时继承了两个或更多个共同基类,而这些基类又继承自同一个共同的基类时,就会形成菱形继承结构。为了解决由此可能产生的二义性和数据重复的问题,可以将这些共同的基类声明为虚基类。

在声明虚基类时,需要在派生类的继承列表中使用关键字 virtual。这样做可以确保每个派生类只包含一份虚基类的实例,从而避免了数据重复和二义性。

面试题

一:

代码语言:javascript复制
class Base
{
public:
virtual void show(){ cout<<"call Base::show"<<endl;}
};
class Derive : public Base
{
private: //编译阶段不看它
void show()
{
cout<<"call Derive::show"<<endl;
}
};
int main()
{
Base *p = new Derive();
p->show(); //最终能调用到Derive::show,是在运行时期才确定的
delete p;
return 0;
}

对于private:

代码语言:javascript复制
void show()
{
cout<<"call Derive::show"<<endl;
}可以正常调用

成员方法能不能调用,就是说方法的访问权限是不是public的,是在编译阶段就需要确定的。

编译阶段:Base::show (call Base::show (静态绑定)/ call ecx(动态绑定))

也就是说在执行 p->show(); //最终能调用到Derive::show,是在运行时期才确定的 时看的是Base基类的访问权限,不看派生类的权限。

二:

代码语言:javascript复制
class Base
{
public:
virtual void show(int i = 10){ cout<<"call Base::show i="<< i <<endl;}
};
class Derive : public Base
{
private: //编译阶段不看它
void show(int i = 20)
{
cout<<"call Derive::show i = "<< i <<endl; //i的值是10
}
};
int main()
{
Base *p = new Derive();
/*
因为p是Base类型
push 0Ah 参数压栈(Base里面的参数)
mov eax,dword ptr[p]
mov ecx,dword ptr[eax]
call ecx
*/
p->show(); //动态绑定 p->Derive vfptr -》Derive vftable
delete p;
return 0;
}

对于有默认值的基类和派生类发生多态时,参数压栈是在编译时期就会确定好的,所以派生类的默认参数根本不会起作用,永远用不到

三:

代码语言:javascript复制
class Base
{
public:
Base()
{
/*
push ebp
mov ebp , esp
sub esp,...开辟空间
rep stos esp <->ebp (windows下默认初始化为0×CCCCCCCC)(Linux g  /gcc不做此步骤)
vfptr 《- &Base::vftable
*/
cout<<"Call Base()"<<endl;
clear();
}
void clear(){memset(this,0,sizeof(*this));}
virtual void show()
{
cout<<"CAll Base::show()"<<endl;
}
};

class Derive : public Base
{
public:
Derive()
{
cout<<"call Derive()"<<endl;
}
void show()
{
cout<<"Call Derive::show()"<<endl;
}
};

int main()
{
//错误代码
Base *pb1 = new Base();
pb1->show();
delete pb1;

//正确
Base *pb2 = new Derive();
pd2 ->show();
delete pb2;

return 0;
}

解释:

代码语言:javascript复制
//错误代码
Base *pb1 = new Base();
pb1->show();
delete pb1;

因为pb1调用了clear函数,相当于把vfptr置成0地址了,vfptr已经不再指向Base::vfptable了,当再次调用pb1->show();,从而找不到,发生异常错误。

代码语言:javascript复制
//正确
Base *pb2 = new Derive();
pd2 ->show();
delete pb2;

首先vfptr同样被置0地址,因为Base先进行构造函数,vfptr 《- &Base::vftable,再调用clear函数,vfptr被置成0地址。 然后Derive再构造,同样会执行vfptr 《- &Base::vftable,但是不会调用clear函数,所有此时vfptr 就会指向Derive::vftable,从而正常运行。

四种类型转换方式

  1. 语言级别的转换方式 const_cast
  2. 去掉常量属性的一个类型转换 static_cast
  3. 提供编译器认为安全的类型转换(没有任何联系的类型之间的转换不会成功) reinterpret_cast
  4. 类似于c风格的强制类型转换 dynamic_cast

主要用字继承结构中,可以支持RTTI类型识别的上下类型转化

代码语言:javascript复制
int main()
{
const int a = 10;
int *p1 = (int*)&a;

int *p2 = const_cast<int*>(&a);
/*不考虑const,左右两边类型要保持一致,体现了安全性
const_cast《》里面必须是指针或引用类型
*/
int a = 10;
char b = static_cast<int>(a); //char与int有联系

int*p = nullptr;
double *b = reinterpret_cast<double*>(p);//可以转换,但是不安全
}

dynamic_cast 是 C 中用于安全地进行基类指针或引用向派生类指针或引用的类型转换的一种运算符。它主要用于在运行时检查类型安全性,只能用于具有虚函数的类层次结构中。如果尝试转换失败,dynamic_cast 将返回一个空指针(对指针进行转换)或引发 std::bad_cast 异常(对引用进行转换)。

下面是 dynamic_cast 的一般语法:

代码语言:javascript复制
dynamic_cast<new_type>(expression)

其中 new_type 是要转换的目标类型,expression 是要转换的指针或引用。

代码语言:javascript复制
#include <iostream>

// 基类
class Base {
public:
    virtual ~Base() {} // 虚析构函数确保多态性
};

// 派生类
class Derived : public Base {
public:
    void derivedMethod() {
        std::cout << "Derived method called." << std::endl;
    }
};

int main() {
    Base* basePtr = new Derived();
    
    // 使用 dynamic_cast 进行类型转换
    Derived* derivedPtr = dynamic_cast<Derived*>(basePtr);
    
    if (derivedPtr) {
        // 转换成功,可以安全地调用 Derived 类的方法
        derivedPtr->derivedMethod();
    } else {
        // 转换失败
        std::cout << "Dynamic cast failed." << std::endl;
    }
    
    delete basePtr;
    
    return 0;
}

在这个例子中,basePtr 是一个指向基类 Base 的指针,但实际上指向了一个派生类 Derived 的对象。通过使用 dynamic_cast 将 basePtr 转换为 Derived* 类型的指针 derivedPtr,我们可以安全地调用 Derived 类的方法。如果转换失败(例如 basePtr 指向的对象不是 Derived 类型的),dynamic_cast 将返回 nullptr。

0 人点赞