C++面向对象

C++面向对象知识

内存字节对齐

  • #pragma pack(n) 表示的是设置n字节对齐,windows默认是8字节,linux是4字节,鲲鹏是4字节
struct A{char a;int b;short c;
};
  • char占一个字节,起始偏移为零,int占四个字节,min(8,4)=4;所以应该偏移量为4,所以应该在char后面加上三个字节,不存放任何东西,short占两个字节,min(8,2)=2;所以偏移量是2的倍数,而short偏移量是8,是2的倍数,所以无需添加任何字节,所以第一个规则对齐之后内存状态为0xxx|0000|00
  • 此时一共占了10个字节,但是还有结构体本身的对齐,min(8,4)=4;所以总体应该是4的倍数,所以还需要添加两个字节在最后面,所以内存存储状态变为了 0xxx|0000|00xx,一共占据了12个字节
  • 内存对齐规则

    • 对于结构的各个成员,第一个成员位于偏移为0的位置,以后的每个数据成员的偏移量必须是 min( #pragma pack()指定的数, 这个数据成员的自身长度 )的倍数
    • 在所有的数据成员完成各自对齐之后,结构或联合体本身也要进行对齐,对齐将按照 #pragam pack指定的数值和结构或者联合体最大数据成员长度中比较小的那个,也就是 min(#pragram pack() , 长度最长的数据成员)
  • 需要对齐的原因

    • 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常
    • 硬件原因:经过内存对齐之后,CPU的内存访问速度大大提升。访问未对齐的内存,处理器要访问两次(数据先读高位,再读低位),访问对齐的内存,处理器只要访问一次,为了提高处理器读取数据的效率,我们使用内存对齐

面向对象三大特性

通过类创建一个对象的过程叫实例化,实例化后使用对象可以调用类成员函数和成员变量,其中类成员函数称为行为,类成员变量称为属性。类和对象的关系:类是对象的抽象,对象是类的实例

  • 封装
    • 把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。
    • public,private,protected
  • 继承
    • 基类(父类)——> 派生类(子类)
  • 多态

双冒号、using和namespace

  • namespace主要用来解决命名冲突的问题

    • 必须在全局作用域下声明
    • 命名空间下可以放函数,变量、结构体和类
    • 命名空间可以嵌套命名空间
    • 命名空间是开放的,可以随时加入新成员(添加时只需要再次声明namespace,然后添加新成员即可
  • 双冒号::作用域运算符

    • 全局作用域符(::name):用于类型名称(类、类成员、成员函数、变量等)前,表示作用域为全局命名空间
    • 类作用域符(class::name):用于表示指定类型的作用域范围是具体某个类的
    • 命名空间作用域符(namespace::name):用于表示指定类型的作用域范围是具体某个命名空间的
  • using分为using声明和using编译指令

    • using std::cout; //声明
    • using namespace std; //编译指令
    • 尽量使用声明而不是编译指令,不同命名空间中可能会有相同的变量名,编译指令执行两个命名空间后,会产生二义性

内联函数和函数重载

  • 内联函数

    • 相当于把内联函数里面的内容写在调用内联函数处;
    • 相当于不用执行进入函数的步骤,直接执行函数体;
    • 相当于宏,却比宏多了类型检查,真正具有函数特性;
    • 不能包含循环、递归、switch 等复杂操作;
    • 在类声明中定义的函数,除了虚函数的其他函数都会自动隐式地当成内联函数,内联函数对于编译器而言只是一个建议,编译器不一定会接受这种建议,即使没有声明内联函数,编译器可能也会内联一些小的简单的函数。
  • C++的函数名称可以重复,称为函数重载。

    • 其中必须在同一作用域下的函数名称相同,不能是一个在全局,一个局部,或者不同的代码块中
    • 可以根据函数参数的个数、类型(const也可以作为重载条件)、顺序不同进行函数重载,但不能用函数返回值进行重载
    • 当函数重载遇到函数默认参数时,要注意二义性。

虚函数可以是内联函数吗

  • 虚函数可以是内联函数,内联是可以修饰虚函数的,但是当虚函数表现多态性的时候不能内联。
  • 内联是在编译期内联,而虚函数的多态性在运行期,编译器无法知道运行期调用哪个代码,因此虚函数表现为多态性时(运行期)不可以内联。
  • inline virtual 唯一可以内联的时候是:编译器知道所调用的对象是哪个类(如 Base::who()),这只有在编译器具有实际对象而不是对象的指针或引用时才会发生。

构造函数/析构函数

构造函数和析构函数,分别对应变量的初始化和清理,变量没有初始化,使用后果未知;没有清理,则会内存管理出现安全问题。
当对象结束其生命周期,如对象所在的函数已调用完毕时,系统会自动执行析构函数

  • 构造函数:与类名相同,没有返回值,不写void,可以发生重载,可以有参数,编译器自动调用,只调用一次。
  • 析构函数:~类名,没有返回值,不写void,不可以发生重载,不可以有参数,编译器自动调用,只调用一次。
  • 构造函数

    • 系统会默认给一个类提供三个函数:默认构造函数(无参,函数体为空)、默认拷贝构造和析构函数(无参,函数体为空),其中默认拷贝构造可以实现简单的值拷贝。
    • 提供了有参构造函数,就不提供默认构造函数;提供了拷贝构造函数,就不会提供其他构造函数,若自己定义可有参构造,也需要自定义无参构造函数
  • 析构函数

    • 如果一个类中有指针,且在使用的过程中动态的申请了内存,那么最好自定义析构函数,在销毁类之前,释放掉申请的内存空间,避免内存泄漏

拷贝构造函数与深浅拷贝

拷贝构造函数的参数必须加const,因为防止修改,本来就是用现有的对象初始化新的对象。

  • 拷贝构造函数的使用时机

    • 使用已经创建好的对象初始化新对象 A a; A b = a; A c(a); b = c;//b = c不是初始化,调用赋值运算符
    • 以值传递的方式来给函数参数传值
    • 以值方式返回局部对象(不常用,一般不返回局部对象)
  • 深拷贝和浅拷贝
    只有当对象的成员属性在堆区开辟空间内存时,才会涉及深浅拷贝,如果仅仅是在栈区开辟内存,则默认的拷贝构造函数和析构函数就可以满足要求。

    • 浅拷贝:使用默认拷贝构造函数,拷贝过程中是按字节复制的,对于指针型成员变量只复制指针本身,而不复制指针所指向的目标,因此涉及堆区开辟内存时,会将两个成员属性指向相同的内存空间,从而在释放时导致内存空间被多次释放,使得程序down掉。
    • 浅拷贝的问题:当出现类的等号赋值时,系统会调用默认的拷贝函数——即浅拷贝,它能够完成成员的一一复制。当数据成员中没有指针时,浅拷贝是可行的。但当数据成员中有指针时,如果采用简单的浅拷贝,则两类中的两个指针将指向同一个地址,当对象快结束时,会调用两次free函数,指向的内存空间已经被释放掉,再次free会报错;另外,一片空间被两个不同的子对象共享了,只要其中的一个子对象改变了其中的值,那另一个对象的值也跟着改变了所以,这时,必须采用深拷贝
    • 深拷贝:自定义拷贝构造函数,在堆内存中另外申请空间来储存数据,从而解决指针悬挂的问题。需要注意自定义析构函数中应该释放掉申请的内存

我们在定义类或者结构体,这些结构的时候,最后都重写拷贝函数,避免浅拷贝这类不易发现但后果严重的错误产生

只在堆上/栈上创建对象

  • 只能在堆上生成对象:将析构函数设置为私有。
    原因:C++是静态绑定语言,编译器管理栈上对象的生命周期,编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性。若析构函数不可访问,则不能在栈上创建对象。
  • 只能在栈上生成对象:将new 和 delete 重载为私有。
    原因:在堆上生成对象,使用new关键词操作,其过程分为两阶段:第一阶段,使用new在堆上寻找可用内存,分配给对象;第二阶段,调用构造函数生成对象。
    将new操作设置为私有,那么第一阶段就无法完成,就不能够在堆上生成对象。

this指针

  • 为什么会有this指针
    在类实例化对象时,只有非静态成员变量属于对象本身,剩余的静态成员变量,静态函数,非静态函数都不属于对象本身,因此非静态成员函数只会实例一份,多个同类型对象会共用一块代码,由于类中每个实例后的对象都有独一无二的地址,因此不同的实例对象调用成员函数时,函数需要知道是谁在调用它,因此引入了this指针。this指针是对象的首地址。

  • this指针的作用
    this指针是隐含在对象成员函数内的一种指针。当一个对象被创建后,它的每一个成员函数都会含有一个系统自动生成的隐含指针this。this指针指向被调用的成员函数所属的对象(谁调用成员函数,this指向谁),*this表示对象本身,非静态成员函数中才有this,静态成员函数内部没有

    this指针实际上是编译器对非静态成员函数做出的操作,在定义函数时会往函数里传入class *this这个参数,在函数调用时会传入对象的地址。静态成员函数之所以没有this指针是因为静态成员函数先于对象产生,并且是所有对象共享的。

    • this 并不是一个常规变量,而是个右值,所以不能取得 this 的地址(不能 &this)。
    • 对非静态成员函数默认添加了this指针,类型为classname *const this
  • this指针使用

    • 当形参与成员变量名相同时,用this指针来区分

    • 为实现对象的链式引用,在类的非静态成员函数中返回对象本身,可以用return *this,this指向对象,

      *this表示对象本身。

常函数和常对象

void func() const //常函数,此处func为类成员函数
const Person p2; //常对象

  • 常函数修饰的是this指针,不允许修改this指针指向的值,如果执意要修改常函数,可以在成员属性前加mutable
  • 常对象不允许修改属性,不可以调用普通成员函数,可以调用常函数。

delete this合法吗

合法,但有前提:

  • 必须保证 this 对象是通过 new(不是 new[]、不是 placement new、不是栈上、不是全局、不是其他对象成员)分配的
  • 必须保证调用 delete this 的成员函数是最后一个调用 this 的成员函数
  • 必须保证成员函数的 delete this 后面没有调用 this 了
  • 必须保证 delete this 后没有人使用了

为什么空类大小不为0

sizeof(空class) = 1,为了确保两个不同对象的地址不同。

静态成员变量与静态成员函数

若将成员变量声明为static,则为静态成员变量,与一般的成员变量不同,无论建立多少对象,都只有一个静态成员变量的拷贝,静态成员变量属于一个类,所有对象共享。静态变量在编译阶段就分配了空间,对象还没创建时就已经分配了空间,放到全局静态区。

  • 静态成员变量
    • 最好是类内声明,类外初始化(以免类名访问静态成员访问不到)
    • 无论公有,私有,静态成员都可以在类外定义,但私有成员仍有访问权限
    • 非静态成员类外不能初始化
    • 静态成员数据是共享的。
  • 静态成员函数
    • 静态成员函数可以直接访问静态成员变量,不能直接访问普通成员变量,但可以通过参数传递的方式访问
    • 普通成员函数可以访问普通成员变量,也可以访问静态成员变量
    • 静态成员函数没有this指针。非静态数据成员为对象单独维护,但静态成员函数为共享函数,无法区分是哪个对象,因此不能直接访问普通变量成员,也没有this指针。

初始化列表的好处和使用条件

  • 初始化列表的使用条件
    • const类型的数据
    • 引用类型的数据
  • 好处
    • 初始化是直接初始化成员
    • 赋值是初始化再赋值

能否通过初始化列表初始化静态成员变量

不能,静态成员变量最好类内声明,类外初始化。静态成员是单独存储的,并不是对象的组成部分。如果在类的内部进行定义,在建立多个对象时会多次声明和定义该变量的存储位置。在名字空间和作用域相同的情况下会导致重名的问题。

友元全局函数、友元类、友元成员函数

友元主要是为了访问类中的私有成员(包括属性和方法),会破坏C++的封装性,尽量不使用

  • 友元全局函数
    • 友元函数声明可以在类中的任何地方,一般放在类定义的开始或结尾
    • 一个函数可以是多个类的友元函数,只需要在各个类中分别声明
    • 友元函数在类内声明,类外定义,定义和使用时不需加作用域和类名,与普通函数无异。
class Building
{friend void goodGay(Building * building); //goodGay是Building的友元函数,因此goodGay可以访问building的任意成员
public:Building(){m_Sittingroom = "客厅";m_Bedroom = "卧室";}string m_Sittingroom;
private:string m_Bedroom;
};//和C语言结构体同,传参时尽量不要传递值,尽量传递指针
void goodGay(Building * building){cout << "别人在访问" << building->m_Sittingroom << endl;cout << "别人在访问" << building->m_Bedroom << endl; //当不是友元函数时,不能访问私有成员
}void test01(){Building building; //或者Building *build = new Building;这里如果定义指针,需要new,否则未初始化goodGay(&building);
}
  • 友元类
    • 友元不可继承
    • 友元是单向的,类A是类B的友元类,但类B不一定是类A的
    • 友元不具有传递性,类A是类B的友元类,类B是类C的友元类,但类A不一定是类C的友元类。
class Building{friend class Person; //Person是Building的友元函数,因此Person可以访问Building的任意成员
public:Building(){this->m_Sittingroom = "客厅";this->m_Bedroom = "卧室";}string m_Sittingroom;
private:string m_Bedroom;};class Person{public:void test(Building *building){cout << building->m_Bedroom << endl;}
};void test01(){Building *build = new Building;//可以在这里写定义,也可以将定义写在Person的构造函数中Person p;p.test(build);
}
  • 友元成员函数
    • 使类B中的成员函数成为类A的友元函数,这样类B的该成员函数就可以访问类A的所有成员
    • 当用到友元成员函数时,需注意友元声明和友元定义之间的相互依赖,在该例子中,类Person必须先定义,否则类Building就不能将一个Person的函数指定为友元。然而,只有在定义了类Person之后,才能定义类Person的该成员函数。更一般的讲,必须先定义包含成员函数的类,才能将成员函数设为友元。另一方面,不必预先声明类和非成员函数来将它们设为友元。
//和C语言中结构体的互引用类似,需要先声明一个Building类
class Building;class Person{public:void test(Building *building);void test1(Building *building);
};class Building{friend void Person::test1(Building *building); //先将Person类定义,类友元成员函数声明后,再使用friend
public:Building(){this->m_Sittingroom = "客厅";this->m_Bedroom = "卧室";}string m_Sittingroom;
private:string m_Bedroom;};//定义Building类后才能定义Person成员函数
void Person::test1(Building *building){cout << building->m_Bedroom << endl;
}void Person::test(Building *building){cout << building->m_Sittingroom << endl;
}void test01(){Building *build = new Building;Person p;p.test1(build);
}

运算符重载及++重载实现

运算符重载基本属性

  • 运算符重载的目的是扩展C++中提供的运算符的适用范围,使之能作用于对象,或自定义的数据类型
  • 运算符重载的实质是函数重载,可以重载为普通函数,也可以重载为成员函数
  • 运算符重载也是多态的一种,和函数重载称为静态多态,表示函数地址早绑定,在编译阶段就确定好了地址

运算符重载总结

  • 重载运算符(),[] ,->, =的时候,运算符重载函数必须声明为类的成员函数
  • 重载运算符<<,>>的时候,运算符只能通过全局函数配合友元函数进行重载
  • 不要重载 &&|| 运算符,因为无法实现短路原则。
// 重载 << 运算符
ostream & operator<<(ostream & os, cont A & a) {// ...return os;
}

不能被重载的运算符

  • sizeof
  • . 成员运算符
  • :: 作用域解析运算符
  • ?: 条件运算符
  • typeid 一个RTTI运算符
  • 4种强制类型转换运算符

i++和++i实现

C++内置类型的后置++返回的是变量的拷贝,也就是不可修改的值;前置++返回的是变量的引用,因此可以作为修改的左值。即++(++a)或(++a)++都可以,但++(a++)不可以,(C++默认必须修改a的值,如果不修改则报错)。

//++i
int&  int::operator++()
{*this += 1;return *this;
}//i++,注意后置++有占位参数以区分跟前置++不同
const int  int::operator++(int)
{int oldValue = *this;++(*this);return oldValue;
}

继承方式、对象模型、同名处理

继承主要是为了减少代码的重复内容,解决代码复用问题。通过抽象出一个基类(父类),将重复代码写到基类中,在派生类(子类)中实现不同的方法。

继承方式

  • 公有继承:保持父类中的访问属性
  • 私有继承:将父类中的所有访问属性改为private
  • 保护继承:除父类中的私有属性,其他改为保护属性

继承的对象模型

  • 子类中会继承父类的私有成员,只是被编译器隐藏起来了,无法访问到,通过sizeof(子类class)可以检查出。
  • 子类创建对象时,先调用父类的构造函数,然后再调用自身的构造,析构顺序与构造顺序相反
    • 由于继承中父类和子类的构造、析构顺序原因,当父类中只提供了有参构造(默认构造等函数会被隐藏),而子类仅仅调用默认构造时,会因为子类创建对象时无法调用父类构造函数而报错,这里可以让子类利用初始化列表来显式调用父类有参构造函数来进行父类构造,然后进行子类构造。
  • 子类会继承父类的成员属性和成员函数,但子类不会继承父类构造函数和析构函数

继承中的同名处理

  • 父类和子类成员属性同名,用子类声明对象调用子类属性,若想调用父类成员,则加上父类的作用域
  • 父类和子类成员函数同名,子类函数不会覆盖父类的成员,只是隐藏起来,用子类声明对象调用子类成员函数,若想调用父类函数(包括重载),则加上父类的作用域
  • 若子类中没有与父类同名的成员函数,子类声明对象后,可以直接调用父类成员函数。

多继承和菱形继承

多继承

多继承会产生二义性的问题。如果继承的多个父类中有同名的成员属性和成员函数,在子类调用时,需要指定作用域从而确定父类。

菱形继承

两个子类继承于同一个父类,同时又有另外一个类多继承于两个子类,这种继承称为菱形继承。比如羊和驼继承于动物类,同时羊驼继承于羊和驼。

菱形继承会产生问题

  • **浪费空间。**羊驼继承了两份动物类中的某些数据和函数,但只需要一份即可
  • 二义性。从不同途径继承来的同名的数据成员在内存中有不同的拷贝造成数据不一致问题。 羊驼调用数据和函数时,会出现二义性,通过sheep类得到一个age,通过carmel类得到一个age,两个数据不会相互影响,相互修改,导致同一份数据不一致。

解决菱形继承的问题

使用虚继承,在继承方式前加virtual,这样的话羊驼可以直接访问m_Age,不用添加作用域,且这样操作的是共享的一份数据

class Animal{
public:int m_Age;
};
class Sheep:virtual public Animal{int m_sheep;
};
class Camel :virtual public Animal{int m_camel;
};class Son :public Sheep, public Camel{int m_son;
};
void test01(){Son son;son.m_Age = 10;cout << sizeof(Animal) << endl; // 4:m_Agecout << sizeof(Sheep) << endl;  // 12:sheep-Vbptr,m_sheep,m_Agecout << sizeof(Camel) << endl;  // 12:camel-Vbptr,m_camel,m_Agecout << sizeof(Son) << endl;    // 24:sheep-Vbptr,m_sheep,camel-Vbptr,m_camel,m_son,m_Age
}

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • **特别注意:**此时son没有自己的虚基类表和虚基类指针,只是继承了sheep和camel的虚基类指针和虚基类表,只是修改了两个虚基类表中的值,修改为当前类中,如何通过继承的虚基类指针查找虚基类数据
  • Son继承Sheep父类,父类中有虚基类指针vbptr(virtual base pointer),对象结构类似结构体,首元素是虚基类指针,其余为自身数据(不包括静态成员和成员函数)
  • Sheep的虚基类指针vbptr指向下面Sheep的虚基类表vbtale@Sheep(virtual base table),虚基类表是一个整型数组,数组第二个元素值为20,即Sheep的虚指针地址偏移20指向Animal的m_Age地址。Camel父类同理,因此,类中只有一个m_Age元素。
  • Son中包含了两个指针和四个int类型,所以大小为24。
class Animal{
public:int m_Age;
};
class Sheep:virtual public Animal{int m_sheep;
};
class Camel :virtual public Animal{int m_camel;
};class Son :virtual public Sheep, virtual public Camel{int m_son
};
void test01(){Son son;son.m_Age = 10;cout << sizeof(Animal) << endl; // 4:m_Agecout << sizeof(Sheep) << endl;  // 12:sheep-Vbptr,m_sheep,m_Agecout << sizeof(Camel) << endl;  // 12:camel-Vbptr,m_camel,m_Agecout << sizeof(Son) << endl;    // 28:son-vbptr,m_son,m_Age,sheep-Vbptr,m_sheep,camel-Vbptr,m_camel,
}

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 注意跟上面的区别,一个是son类中的元素顺序,一个是son类有了自己的虚基类指针和虚基类表
  • 虚继承
    • 一般通过虚基类指针和虚基类表实现,将共同基类设置为虚基类
    • **每个虚继承的子类(虚基类本身没有)**都有一个虚基类指针(占用一个指针的存储空间)和虚基类表(不占用类对象的存储空间),虚基类指针属于对象,虚基类表属于类
    • 当虚继承的子类被当做父类继承时,虚基类指针也会被继承。
    • 虚表中只记录了虚基类数据在派生类对象中与派生类对象首地址(虚基类指针)之间的偏移量,以此来访问虚基类数据
    • 虚继承不用像普通多继承那样维持着公共基类(虚基类)的两份同样的拷贝,节省了存储空间。
    • 虚基类表本质是一个整型数组

静态函数可以是虚函数吗

不可以,因为虚函数属于对象,不属于类,静态函数属于类

类型兼容性原则 为什么会有多态

类型兼容规则是指在需要基类对象的任何地方,都可以使用公有派生类的对象来替代,如使用子类对象可以直接赋值给父类对象或子类对象可以直接初始化父类对象时,对于同样的一条语句,不管传入子类还是父类对象,都是调用的父类函数,但我们想实现的是同样的一条语句,传入不同的对象,调用不同的函数.

class Animal{
public:void speak(){cout << "Animal speak" << endl;}
};class Sheep :public Animal{
public:void speak(){ //重定义,子类重新定义父类中有相同名称的非虚函数 cout << "Sheep speak" << endl;}
};void doSpeak(Animal &animal){animal.speak();
}//想通过父类引用指向子类对象
void test01(){Sheep sheep;doSpeak(sheep); //Animal speak;sheep.speak();  //sheep speaksheep.Animal::speak();  //Animal speak; //继承中的重定义可以通过作用域
}

但我们想传入子类对象调用子类函数,传入父类对象调用父类函数,即同样的调用语句有多种不同的表现形态,这样就出现了多态

重载、覆盖、重写

  • 重载(overload):
    • 是函数名相同,参数列表不同。
    • 重载只是在同一个类的内部存在,但是不能靠返回类型来判断
    • 重载是在编译器期间根据参数类型和个数决定函数调用(静态联编)
  • 重写(override):也叫覆盖,子类重新定义父类中有相同名称参数的虚函数。两者的函数特征相同。
    • 函数重写必须发生在父类与子类之间
    • 被重写的函数不能是static的。必须是virtual的
    • 重写函数必须有相同的类型,名称和参数列表
    • 重写函数的访问权限可以不同。尽管virtual是private的,子类中重写改写为public,protected也是可以的。
  • 重定义(overwrite):也叫做隐藏。子类重新定义父类中有相同名称的非虚函数 ( 参数列表可以不同 ) 。如果一个类,存在和父类相同的函数,那么,这个类将会隐藏其父类的方法,除非你在调用的时候,强制转换为父类类型或加上父类作用域

多态实现的基础

  • 继承
  • 虚函数覆盖
  • 父类指针或引用指向子类对象访问虚函数
class Animal{
public:virtual  void speak(){ //在父类中声明虚函数,可以实现多态,动态联编cout << "Animal speak" << endl;}int m_age = 0;
};class Sheep :public Animal{
public:void speak(){ //发生多态时,子类对父类中的成员函数进行重写,virtual可写可不写cout << "Sheep speak" << endl;}int m_age = 1;
};void doSpeak(Animal &animal){animal.speak();
}void test01(){//传入子类对象调用子类成员函数Sheep sheep;doSpeak(sheep); //sheep speak;//子类对象直接调用子类成员函数sheep.speak();  //sheep speak;//子类对象通过作用域调用父类成员函数sheep.Animal::speak();  //animal speak;//基类成员不能转换为子类成员,即不能向下转换//Animal *animal0 = new Animal();//Sheep * sheep0 = animal0;//sheep0->speak();//同样不能向下转换//Animal animal0;//Sheep sheep0 = animal0;//父类指针指向子类对象Sheep *sheep1 = new Sheep();Animal *animal1 = sheep1;animal1->speak(); //sheep speak;//父类引用指向子类对象Sheep sheep2;Animal &animal2 = sheep2;animal2.speak();    //sheep speak;//子类对象直接赋值给父类对象,不符合多态条件,符合类型兼容性原则Sheep sheep0;Animal animal0 = sheep0;animal0.speak();    //animal speak;
}

静态多态和动态多态

  • 静态多态(运算符重载、函数重载)
  • 动态多态(继承、虚函数)

两者主要的区别:函数地址是早绑定(静态联编)还是晚绑定(动态联编)。即,在编译阶段确定好地址还是在运行时才确定地址。

虚函数指针和虚函数表

  • 前提发生了多态,每个类中都有虚函数表,最开始的父类创建虚函数表,后面的子类继承父类的虚函数表,然后对虚函数重写
  • 虚函数重写(覆盖)的实质就是重写父类虚函数表中的父类虚函数地址;
  • 实现多态的流程:虚函数指针->虚函数表->函数指针->入口地址,虚函数表(vftable)属于类,或者说这个类的所有对象共享一个虚函数表;虚函数指针(vfptr)属于单个对象
  • 在程序调用时,先创建对象,编译器在对象的内存结构头部添加一个虚函数指针,进行动态绑定,虚函数指针指向对象所属类的虚函数表。
  • 虚函数表是一个指针数组,其元素是虚函数的指针,每个元素对应一个函数的指针。如果子类对父类中的一个或多个虚函数进行重写,子类的虚函数表中的元素顺序,会按照父类中的虚函数顺序存储,之后才是自己类的函数顺序。
  • 编译器根本不会去区分,传进来的是子类对象还是父类对象,而是关心调用的函数是否为虚函数。如果是虚函数,就根据不同对象的vptr指针找属于自己的函数。父类对象和子类对象都有vfptr指针,传入对象不同,编译器会根据vfptr指针,到属于自己虚函数表中找自己的函数。即:vptr—>虚函数表------>函数的入口地址,从而实现了迟绑定(在运行的时候,才会去判断)。

函数指针与指针函数

  • 指针函数int* f(int x, int y)本质是函数,返回值为指针,函数指针int (*f)(int x)本质是指针,指向函数的指针

  • 通常我们可以将指针指向某类型的变量,称为类型指针(如,整型指针)。若将一个指针指向函数,则称为函数指针。

  • 函数名代表函数的入口地址,同样的,我们可以通过根据该地址进行函数调用,而非直接调用函数名。

void test001(){printf("hello, world");
}int main(){void(*myfunc)() = test001;//将函数写成函数指针myfunc(); //调用函数指针 hello world
}

test001的函数名与myfunc函数指针都是一样的,即都是函数指针。test001函数名是一个函数指针常量,而myfunc是一个函数指针变量,这是它们的关系。

  • 函数指针多用于回调函数,回调函数最大的优势在于灵活操作,可以实现用户定制的函数,降低耦合性,实现多样性,如STL中

C语言实现多态

可以让函数指针指向参数类型相同、返回值类型也相同的函数。通过函数指针我们也可以实现C++中的多态。

#include<iostream>
typedef int (*func)();int print1(){printf("hello, print1 \n");return 0;
}int print2(){printf("hello, print2 \n");return 0;
}int main(int argc, char * argv[]){func fp = print1;fp();fp = print2;fp();return 0;
}

怎么理解多态和虚函数

  • 多态的实现主要分为静态多态和动态多态,静态多态主要是重载,在编译的时候就已经确定;动态多态是用虚函数机制实现的,在运行期间动态绑定。

  • 举个例子:一个父类类型的指针指向一个子类对象时候,使用父类的指针去调用子类中重写了的父类中的虚函数的时候,会调用子类重写过后的函数

  • 虚函数的实现:在有虚函数的类中,类的最开始部分是一个虚函数表的指针,这个指针指向一个虚函数表,表中放了虚函数的地址,实际的虚函数在代码段(.text)中。当子类继承了父类的时候也会继承其虚函数表,当子类重写父类中虚函数时候,会将其继承到的虚函数表中的地址替换为重新写的函数地址。使用了虚函数,会增加访问内存开销,降低效率。

构造函数能否实现多态/虚函数指针什么时候初始化

两个问题本质是一样的,构造函数不能实现多态

  • 对象在创建时,由编译器对VPTR指针进行初始化,只有当对象的构造完全结束后VPTR的指向才最终确定。

  • 子类中虚函数指针的初始化过程
    当定义一个子类对象的时候比较麻烦,因为构造子类对象的时候会首先调用父类的构造函数然后再调用子类的构造函数。当调用父类的构造函数的时候,此时会创建Vptr指针,该指针会指向父类的虚函数表;然后再调用子类的构造函数,子类继承父类的虚函数指针,此时Vptr又被赋值指向子类的虚函数表。

    也就是说,会先调用父类构造函数,再调用子类构造函数,并不会只调用子类构造函数,是没法实现多态的

构造函数能否是虚函数

不能,因为在调用构造函数时,虚表指针并没有在对象的内存空间中,必须要构造函数调用完成后才会形成虚表指针

不要在构造函数中调用虚函数的原因

第一个原因,在概念上,构造函数的工作是为对象进行初始化。在构造函数完成之前,被构造的对象被认为“未完全生成”。当创建某个派生类的对象时,如果在它的基类的构造函数中调用虚函数,那么此时派生类的构造函数并未执行,所调用的函数可能操作还没有被初始化的成员,这将导致灾难的发生。

第二个原因,即使想在构造函数中实现动态联编,在实现上也会遇到困难。这涉及到对象虚指针(vptr)的建立问题。在Visual C++中,包含虚函数的类对象的虚指针被安排在对象的起始地址处,并且虚函数表(vtable)的地址是由构造函数写入虚指针的。所以,一个类的构造函数在执行时,并不能保证该函数所能访问到的虚指针就是当前被构造对象最后所拥有的虚指针,因为后面派生类的构造函数会对当前被构造对象的虚指针进行重写,因此无法完成动态联编。

#include <iostream>
using namespace std;class A{
public:A() {show();}virtual void show(){cout<<"in A"<<endl;}virtual ~A(){}
};class B:public A{
public:void show(){cout<<"in B"<<endl;}
};int main(){A a;B b;
}

不要在析构函数中调用虚函数的原因

析构函数是用来销毁一个对象的,在销毁一个对象时,先调用该对象所属类的析构函数,然后再调用其基类的析构函数,所以,在调用基类的析构函数时,派生类对象的“善后”工作已经完成了,这个时候再调用在派生类中定义的函数版本已经没有意义了。

#include <iostream>
using namespace std;class A{
public:virtual void show(){cout<<"in A"<<endl;}virtual ~A(){show();}
};class B:public A{
public:void show(){cout<<"in B"<<endl;}
};int main(){A a;B b;
}
/*
in A
in A
*/

抽象类和纯虚函数

在程序设计中,如果仅仅为了设计一些虚函数接口,打算在子类中对其进行重写,那么不需要在父类中对虚函数的函数体提供无意义的代码,可以通过纯虚函数满足需求。

  • 纯虚函数的语法格式:virtual 返回值类型 函数名 () = 0; 只需要将函数体完全替换为 =0即可,纯虚函数必须在子类中进行实现,在子类外实现是无效的。

  • 注意

    • 如果父类中出现了一个纯虚函数,则这个类变为了抽象类,抽象类不可实例对象
    • 如果父类为抽象类,子类继承父类后,必须实现父类所有的纯虚函数,否则子类也为抽象类,也无法实例对象但纯虚析构函数例外,因为子类不会继承父类的析构函数

虚析构和纯虚析构

  • 仅仅发生继承时,创建子类对象后销毁,函数调用流程为:父类构造函数->子类构造函数->子类析构函数->父类析构函数;
  • 当发生多态时(父类指针或引用指向子类对象),通过父类指针在堆上创建子类对象,然后销毁,调用流程为:父类构造函数->子类构造函数->父类析构函数,不会调用子类析构函数,因此子类中会出现内存泄漏问题。

解决方法:将父类中的析构函数设置为虚函数,设置后会先调用子类析构函数,再调用父类析构函数

  • 纯虚析构
    • 纯虚析构需要类内声明,类外实现
    • 纯虚析构也是虚函数,该类也为抽象类
    • 子类不会继承父类的析构函数,当父类纯虚析构没有实现时,子类不是抽象类,可以创建创建对象。

为什么析构函数必须是虚函数

因为当发生多态时,父类指针在堆上创建子类对象,销毁时会内存泄漏

为什么C++默认的析构函数不是虚函数

因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存

类模板和函数模板

通过template或template实现,主要用于数据的类型参数化,简化代码,有类模板和函数模板,函数模板是用于生成函数的,类模板则是用于生成类的

  • 类模板和函数模板定义
  • template声明下面是函数定义,则为函数模板,否则为类模板。
  • 注意:每个函数模板前必须有且仅有一个template声明,不允许多个template声明后只有一个函数模板,也不允许一个template声明后有多个函数模板(类模板同理)。
  • 类模板与函数模板的区别
    • 类模板不支持自动类型推导
    • 数据类型可以有默认参数.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/130384.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

计算机毕设之基于Hadoop+springboot的物品租赁系统的设计与实现(前后端分离,内含源码+文档+教程)

该系统基于Hadoop平台&#xff0c;利用Java语言、MySQL数据库&#xff0c;结合目前流行的 B/S架构&#xff0c;将物品租赁管理的各个方面都集中到数据库中&#xff0c;以便于用户的需要。在确保系统稳定的前提下&#xff0c;能够实现多功能模块的设计和应用。该系统由管理员功能…

小白备战大厂算法笔试(六)——堆

文章目录 堆常用操作堆的实现存储与表示访问堆顶元素元素入堆元素出堆 常见应用建堆操作自上而下构建自下而上构建 TOP-K问题遍历选择排序堆 堆 堆是一种满足特定条件的完全二叉树&#xff0c;主要可分为下图所示的两种类型。 大顶堆&#xff1a;任意节点的值 ≥ 其子节点的值…

Android MeidiaCodec之OMXPluginBase与QComOMXPlugin实现本质(四十)

简介: CSDN博客专家,专注Android/Linux系统,分享多mic语音方案、音视频、编解码等技术,与大家一起成长! 优质专栏:Audio工程师进阶系列【原创干货持续更新中……】🚀 人生格言: 人生从来没有捷径,只有行动才是治疗恐惧和懒惰的唯一良药. 更多原创,欢迎关注:Android…

tomcat的优化

TOMCAT的优化 tomcat的优化主要是从三个方面进行的&#xff0c;第一个是 tomcat配置的优化第二是对JVM虚拟机的优化第三是对Linux系统内核的优化&#xff0c;配置文件中的优化主要在tomcat中server.xml文件夹内 tomcat配置文件的优化 1、 maxThreads&#xff1a; Tomcat 使用…

Java笔记:线程池

一. 正确使用ThreadPoolExecutor创建线程池 1.1、基础知识 Executors创建线程池便捷方法列表&#xff1a;下面三个是使用ThreadPoolExecutor的构造方法创建的 方法名功能newFixedThreadPool(int nThreads)创建固定大小的线程池newSingleThreadExecutor()创建只有一个线程的线…

HTML5Plus

之前写过在 vue 中使用 mui 框架的方法&#xff0c;因为用 vue 开发后打包 5App 会有一些问题&#xff0c;所以当时用到了&#xff0c;最近又一次开发移动端&#xff0c;不同的是这次使用的是 vue3 开发的&#xff0c;导致之前使用的 vue-awesome-mui 依赖不能使用了&#xff0…

java 工程管理系统源码+项目说明+功能描述+前后端分离 + 二次开发

Java版工程项目管理系统 Spring CloudSpring BootMybatisVueElementUI前后端分离 功能清单如下&#xff1a; 首页 工作台&#xff1a;待办工作、消息通知、预警信息&#xff0c;点击可进入相应的列表 项目进度图表&#xff1a;选择&#xff08;总体或单个&#xff09;项目显示…

华为OD机考算法题:分奖金

题目部分 题目分奖金难度难题目说明公司老板做了一笔大生意&#xff0c;想要给每位员工分配一些奖金&#xff0c;想通过游戏的方式来决定每个人分多少钱。按照员工的工号顺序&#xff0c;每个人随机抽取一个数字。按照工号的顺序往后排列&#xff0c;遇到第一个数字比自己数字…

YOLOv5:对yolov5n模型进一步剪枝压缩

YOLOv5&#xff1a;对yolov5n模型进一步剪枝压缩 前言前提条件相关介绍具体步骤修改yolov5n.yaml配置文件单通道数据&#xff08;黑白图片&#xff09;修改models/yolo.py文件修改train.py文件 剪枝后模型大小 参考 前言 由于本人水平有限&#xff0c;难免出现错漏&#xff0c;…

chrome_elf.dll丢失怎么办?修复chrome_elf.dll文件的方法

Chrome是目前最受欢迎的网络浏览器之一&#xff0c;然而有时用户可能会遇到Chrome_elf.dll丢失的问题。该DLL文件是Chrome浏览器的一个重要组成部分&#xff0c;负责启动和管理程序的各种功能。当Chrome_elf.dll丢失时&#xff0c;用户可能无法正常启动Chrome或执行某些功能。本…

springboot jpa手动事务

创建springboot项目 搭建最简单的SpringBoot项目_Steven-Russell的博客-CSDN博客 引入jpa和数据据依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId> </dependency>…

手搓消息队列【RabbitMQ版】

什么是消息队列&#xff1f; 阻塞队列&#xff08;Blocking Queue&#xff09;-> 生产者消费者模型 &#xff08;是在一个进程内&#xff09;所谓的消息队列&#xff0c;就是把阻塞队列这样的数据结构&#xff0c;单独提取成了一个程序&#xff0c;进行独立部署~ --------&…

yolov8 模型部署--TensorRT部署-c++服务化部署

写目录 yolov8 模型部署--TensorRT部署1、模型导出为onnx格式2、模型onnx格式转engine 部署 yolov8 模型部署–TensorRT部署 1、模型导出为onnx格式 如果要用TensorRT部署YOLOv8&#xff0c;需要先使用下面的命令将模型导出为onnx格式&#xff1a; yolo export modelyolov8n.p…

【算法】常见位运算总结

目录 1.基础位运算2. 给一个数n&#xff0c;确定它的二进制表示中的第x位是0还是13.将一个数n的二进制表示的第x位修改成14.将一个数n的二进制表示的第x位修改成0、5. 位图的思想6.提取一个数(n)二进制表示中最右侧的17.干掉一个数(n)二进制表示中最右侧的18.位运算的优先级9.异…

华为OD七日集训第4期 - 按算法分类,由易到难,循序渐进,玩转OD

目录 一、适合人群二、本期训练时间三、如何参加四、7日集训第4期五、精心挑选21道高频100分经典题目&#xff0c;作为入门。第1天、数据结构第2天、滑动窗口第3天、贪心算法第4天、二分查找第5天、分治递归第6天、深度优先搜索dfs算法第7天、宽度优选算法&#xff0c;回溯法 六…

SpringBoot2.0(过滤器,监听器,拦截器)

目录 一&#xff0c;过滤器1.1&#xff0c;自定义Filter1.2&#xff0c;启动类代码1.2&#xff0c;创建filter类和LoginFilter包1.2.1&#xff0c;编写loginFilter类 过滤器代码1.2.2&#xff0c;创建二个Controller类 二&#xff0c;监听器2.1&#xff0c;自定义监听器2.2&…

Linux 操作系统云服务器安装部署 Tomcat 服务器详细教程

Tomcat 基本概述 Tomcat 服务器是Apache软件基金会&#xff08;Apache Software Foundation&#xff09;的 Jakarta 项目中的一个核心项目&#xff0c;由 Apache、Sun 和其他一些公司及个人共同开发而成。它是一个免费的开放源代码的 Web 应用服务器&#xff0c;属于轻量级应用…

DNS (Domain Name System) 域名解析过程

一、域名与IP地址 通常情况下一台电脑都会有一个IPv4和IPv6地址&#xff08;由数字和字母组成&#xff0c;难以记忆&#xff09;&#xff0c;所以日常访问网站时我们通常都是采用输入域名&#xff08;方便记忆&#xff09;的方式来访问。 二、域名结构树 www 主机名bilibil…

宋浩高等数学笔记(十二)无穷级数

完结&#xff0c;宋浩笔记系列的最后一更~ 之后会出一些武忠祥老师的错题&笔记总结&#xff0c;10月份就要赶紧做真题了

End-to-End Object Detection with Transformers(论文解析)

End-to-End Object Detection with Transformers 摘要介绍相关工作2.1 集合预测2.2 transformer和并行解码2.3 目标检测 3 DETR模型3.1 目标检测集设置预测损失3.2 DETR架构 摘要 我们提出了一种将目标检测视为直接集合预测问题的新方法。我们的方法简化了检测流程&#xff0c…