cpp面向对象

面向对象

三大特性

C++ 面向对象的三大特征是:封装、继承、多态。

封装

就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让信任的类或者对象操作,对不可信的进行信息隐藏。一个类就是一个封装了数据以及操作这些数据的代码的逻辑实体。

在一个对象内部,某些代码或某些数据可以是私有的,不能被外界访问。通过这种方式,对象对内部数据提供了不同级别的保护,以防止程序中无关的部分意外的改变或错误的使用了对象的私有部分。

继承

是指可以让某个类型的对象获得另一个类型的对象的属性的方法。它支持按级分类的概念。继承是指这样一种能力:它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。

通过继承创建的新类称为“子类”或者“派生类”,被继承的类称为“基类”、“父类”或“超类”。继承的过程,就是从一般到特殊的过程。要实现继承,可以通过“继承”和“组合”来实现。

继承概念的实现方式有两类:

实现继承:实现继承是指直接使用基类的属性和方法而无需额外编码的能力。

接口继承:接口继承是指仅使用属性和方法的名称、但是子类必需提供实现的能力。

多态

就是向不同的对象发送同一个消息,不同对象在接收时会产生不同的行为(即方法)。即一个接口,可以实现多种方法。(重载实现编译时多态,虚函数实现运行时多态)

多态分为两类:

  • 静态多态:函数重载和运算符重载属于静态多态,复用函数名;
  • 动态多态:派生类和虚函数实现运行时多态。

静态多态动态多态区别:

  • 静态多态的函数地址早绑定 - 编译阶段确定函数地址
  • 动态多态的函数地址晚绑定 - 运行阶段确定函数地址

多态满足条件:有继承关系;子类重写父类中的虚函数;

多态使用条件:父类指针或引用指向子类对象;

多态的优点:代码组织结构清晰;可读性强;利于前期和后期的扩展以及维护

继承

C++之继承_浮沉一只白的博客-CSDN博客_c++ 继承

三种继承方式

private, public, protected的访问范围:

private: 只能由该类中的函数、其友元函数访问,不能被任何其他访问,该类的对象也不能访问;

protected: 可以被该类中的函数、子类的函数、以及其友元函数访问,但不能被该类的对象访问;

public: 可以被该类中的函数、子类的函数、其友元函数访问,也可以由该类的对象访问;

类继承后方法属性变化:

使用private继承,父类的所有方法在子类中变为private;

使用protected继承,父类的protected和public方法在子类中变为protected,而private方法不变;

使用public继承,父类中的方法属性不发生改变;

派生类的访问方式会变为基类的访问方式和继承方式取权限最小的那个。

总结:

基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。

如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。

class的默认继承方式为private,而struct的默认继承方式为public。

实际应用多为public继承。

菱形继承

单继承:一个子类只有一个直接父类时称这个继承关系为单继承。

多继承:一个子类有两个或两个以上直接父类时称这个继承关系称为多继承。

两个派生类继承同一个基类,又有某个类同时继承者两个派生类,这种继承被称为菱形继承

菱形继承带来的主要问题是子类继承两份相同的数据,导致数据的冗余性和二义性,可以用虚继承来解决。

可以通过指定访问哪个父类来解决二义性:

class Person{
    
    
public:
	string _name; // 姓名
};

class Student : public Person{
    
    
protected:
	int _num; //学号
};

class Teacher : public Person{
    
    
protected:
	int _id; // 职工编号
};

class Assistant : public Student, public Teacher{
    
    
protected:
	string _Course; // 课程
};

void Test(){
    
    
	Assistant a;
	a._name = "peter";	// 这样会出现二义性无法明确知道访问的是哪一个
	a.Student::_name = "Tom"; // 正确
	a.Teacher::_name = "Jack"; // 正确
}

而为了解决冗余性,则出现了菱形虚继承

在继承方式前面加上 virtual 关键字就是虚继承,

class Person{
    
    
public:
	string _name; // 姓名
};

class Student : virtual public Person{
    
    
protected:
	int _num; //学号
};

class Teacher : virtual public Person{
    
    
protected:
	int _id; // 职工编号
};

class Assistant : public Student, public Teacher{
    
    
protected:
	string _Course; // 课程
};

int main(){
    
    
	Assistant a;
	a.Student::_name = "Tom";
	a.Teacher::_name = "Jack";
	a._name = "peter"; // 正确
	return 0;
}

这段代码使用虚继承重新实现了菱形继承,这样在派生类 Assistant 中就只保留了一份成员变量 _name,直接访问就不会再有歧义了。

虚继承的目的是让某个类做出声明,承诺愿意共享它的基类。其中,这个被共享的基类就称为虚基类,本例中的 Person 就是一个虚基类。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含一份虚基类的成员。

友元

在程序里有些私有属性也想让类外特殊的一些函数或者类进行访问,就需要用到友元。友元的目的就是让一个函数或者类访问另一个类中私有成员。友元的关键字为 friend,友元的三种实现:全局函数做友元、类做友元、成员函数做友元。

论C++的自我修养(4)友元_海岸星的清风的博客-CSDN博客

(虚)继承类的内存占用大小

首先,平时所声明的类只是一种类型定义,它本身是没有大小可言的。 因此,如果用sizeof运算符对一个类型名操作,那得到的是具有该类型实体的大小。

计算一个类对象的大小时的规律:

  1. 空类、单一继承的空类、多重继承的空类所占空间大小为:1(字节,下同);

  2. 一个类中,虚函数本身、成员函数(包括静态与非静态)和静态数据成员都是不占用类对象的存储空间的;

  3. 当类中声明了虚函数(不管是1个还是多个),那么在实例化对象时,编译器会自动在对象里安插一个指针vPtr指向虚函数表VTable;

  4. 虚承继的情况:由于涉及到虚函数表和虚基表,会同时增加一个(多重虚继承下对应多个)vfPtr指针指向虚函数表vfTable和一个vbPtr指针指向虚基表vbTable,这两者所占的空间大小为:8(或8乘以多继承时父类的个数);

  5. 在考虑以上内容所占空间的大小时,还要注意编译器下的“补齐”的影响,即编译器会插入多余的字节补齐;

  6. 类对象的大小=各非静态数据成员(包括父类的非静态数据成员但都不包括所有的成员函数)的总和+ vfptr指针(多继承下可能不止一个)+vbptr指针(多继承下可能不止一个)+编译器额外增加的字节。

常见数据类型的内存占用:

  1. 指针的大小一定是4个字节,而不管是什么类型的指针;
  2. char型占1个字节,bool型占1个字节,int占4个字节,short int占2个字节,long int占4个字节,float占4字节,double占8字节,string占4字节,一个空类占1个字节,单一继承的空类占1个字节,虚继承涉及到虚指针所以占4个字节,void * 万能指针在32位系统上是4字节,64位系统上是8字节;
  3. 数组的长度:若指定了数组长度,则不看元素个数,总字节数 = 数组长度 * sizeof(元素类型);若没有指定长度,则按实际元素个数来确定。Ps:若是字符数组,则应考虑末尾的空字符。
  • 派生类有两个基类,每个基类都有虚函数,派生类对象内存中有几个虚表指针

对于多继承的情况,假如派生类有n个直接基类,那么派生类对象中就有n个虚表指针。

例子1:

class A{
    
    };
class B{
    
    
public:
    virtual void b1(){
    
    }
    virtual void b2(){
    
    }
};
class C : public B{
    
    
public:
    virtual void c(){
    
    }
};
class D : public virtual B{
    
     //虚继承
public:
    virtual void d1(){
    
    }
    virtual void d2(){
    
    }
};
class E : public virtual D{
    
    // 虚继承
public:
    virtual void e1(){
    
    }
    virtual void e2(){
    
    }
};

int _tmain(int argc, _TCHAR* argv[]){
    
    
    cout<<sizeof(A)<<endl<<sizeof(B)<<endl<<sizeof(C)<<endl<<sizeof(D)<<endl<<sizeof(E);
    // 1 4 4 12 20
    return 0;
}

例子2:

class CBase {
    
     
	int a; 
	char p; 
}; 
sizeof(CBase)=8;

class CBase {
    
     
public: 
	CBase(void); 
	virtual ~CBase(void); 
private: 
	int  a; 
	char *p; 
}; 
sizeof(CBase)=12

class CChild : public CBase {
    
     
public: 
	CChild(void); 
	~CChild(void); 
	virtual void test();
private: 
	int b; 
}; 
sizeof(CChild)=16class a{
    
    }; // 1
class b{
    
    }; // 1
class c : public a{
    
     // 4
	virtual void fun()=0;
};
class d : public b,public c{
    
    }; // 8

重载、重写和隐藏

隐藏:是指派生类的函数屏蔽了与其同名的基类函数,主要只要同名函数,不管参数列表是否相同,基类函数都会被隐藏。

#include <iostream>
using namespace std;

class Base{
    
    
public:
    void fun(int tmp, float tmp1) {
    
     cout << "Base::fun(int tmp, float tmp1)" << endl; }
};

class Derive : public Base{
    
    
public:
    void fun(int tmp) {
    
     cout << "Derive::fun(int tmp)" << endl; } // 隐藏基类中的同名函数
};

int main(){
    
    
    Derive ex;
    ex.fun(1);       // Derive::fun(int tmp)
    ex.fun(1, 0.01); // error: candidate expects 1 argument, 2 provided
    return 0;
}

说明:上述代码中 ex.fun(1, 0.01); 出现错误,说明派生类中将基类的同名函数隐藏了。若是想调用基类中的同名函数,可以加上类型名指明 ex.Base::fun(1, 0.01);,这样就可以调用基类中的同名函数。

问题:继承中同名的静态成员在子类对象上如何进行访问?

静态成员和非静态成员出现同名,处理方式一致

  • 访问子类同名成员 直接访问即可
  • 访问父类同名成员 需要加作用域

重载:是指同一可访问区内被声明几个具有不同参数列(参数的类型、个数、顺序)的同名函数,根据参数列表确定调用哪个函数,重载不关心函数返回类型。

class A{
    
    
public:
    void fun(int tmp);
    void fun(float tmp);        // 重载 参数类型不同(相对于上一个函数)
    void fun(int tmp, float tmp1); // 重载 参数个数不同(相对于上一个函数)
    void fun(float tmp, int tmp1); // 重载 参数顺序不同(相对于上一个函数)
    int fun(int tmp);            //错误:注意重载不关心函数返回类型
};

重写:是指派生类中存在重新定义的函数。函数名、参数列表、返回值类型都必须同基类中被重写的函数一致,只有函数体不同。派生类调用时会调用派生类的重写函数,不会调用被重写函数。重写的基类中被重写的函数必须有 virtual 修饰。

#include <iostream>
using namespace std;

class Base{
    
    
public:
    virtual void fun(int tmp) {
    
     cout << "Base::fun(int tmp) : " << tmp << endl; }
};

class Derived : public Base{
    
    
public:
    virtual void fun(int tmp) {
    
     cout << "Derived::fun(int tmp) : " << tmp << endl; } // 重写基类中的 fun 函数
};

int main(){
    
    
    Base *p = new Derived();
    p->fun(3); // Derived::fun(int) : 3
    return 0;
}

重写和重载的区别:

  • 范围区别:对于类中函数的重载或者重写而言,重载发生在同一个类的内部,重写发生在不同的类之间(子类和父类之间)。
  • 参数区别:重载的函数需要与原函数有相同的函数名、不同的参数列表,不关注函数的返回值类型;重写的函数的函数名、参数列表和返回值类型都需要和原函数相同,父类中被重写的函数需要有 virtual 修饰。
  • virtual 关键字:重写的函数基类中必须有 virtual关键字的修饰,重载的函数可以有 virtual 关键字的修饰也可以没有。

隐藏和重写,重载的区别:

  • 范围区别:隐藏与重载范围不同,隐藏发生在不同类中。
  • 参数区别:隐藏函数和被隐藏函数参数列表可以相同,也可以不同,但函数名一定相同;当参数不同时,无论基类中的函数是否被 virtual修饰,基类函数都是被隐藏,而不是重写。

构造函数和析构函数

  • 构造函数:主要作用在于创建对象时,为对象的成员属性赋值。构造函数由编译器自动调用,无须手动调用。

构造函数语法:类名(){}

  1. 构造函数,没有返回值也不写void
  2. 函数名称与类名相同
  3. 构造函数可以有参数,因此可以发生重载
  4. 程序在调用对象时候会自动调用构造函数,无须手动调用,而且只会调用一次
  • 析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作。

析构函数语法: ~类名(){}

  1. 析构函数,没有返回值也不写void
  2. 函数名称与类名相同,在名称前加上符号 ~
  3. 析构函数不可以有参数,因此不可以发生重载
  4. 程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次
  • 构造函数的两种分类方式:
  1. 按参数分为: 有参构造和无参构造

  2. 按类型分为: 普通构造和拷贝构造

此外,C++还提供了初始化列表语法,用来初始化属性。语法:构造函数():属性1(值1),属性2(值2)... {}

所以类成员初始化方式有两种:1. 赋值初始化,通过在函数体内进行赋值初始化;2. 列表初始化,在冒号后使用初始化列表进行初始化。

这两种方式的主要区别在于:初始化列表是初始化,而函数体内是赋值操作。

对于在函数体中初始化,是在所有的数据成员被分配内存空间后才进行的。而列表初始化是给数据成员分配内存空间时就进行初始化,也就是说分配一个数据成员只要冒号后有此数据成员的赋值表达式(此表达式必须是括号赋值表达式),那么分配了内存空间后在进入函数体之前给数据成员赋值,初始化这个数据成员,此时函数体还未执行。

对于普通的数据类型两种操作只有资源消耗的区别。但引用和const常量都是不能被赋值的,它们在类内只能在构造函数的参数初始化列表中被初始化。

对于对引用变量和const变量的初始化问题:在进入构造函数体内时,实际上变量都已经初始化完毕了,即引用变量和const变量都已经用不确定的值初始化好了,构造函数内能做的只有赋值,而const类型和引用类型是不可以赋值的。所以,需要在初始化列表中初始化。

类的静态成员变量不能用参数初始化表初始化;对于类的const成员,只能使用初始化列表,而不能在构造函数内部进行赋值操作。

那为什么用成员初始化列表会快一些呢?

因为方法一(赋值初始化)是在构造函数当中做赋值的操作,而方法二(列表初始化)是做纯粹的初始化操作。C++的赋值操作是会产生临时对象的,临时对象的出现会降低程序的效率。

  • 执行顺序

构造函数顺序

  1. 基类构造函数。如果有多个基类,则构造函数的调用顺序是某类在类派生表中出现的顺序,而不是它们在成员初始化表中的顺序。

  2. 成员类对象构造函数。如果有多个成员类对象则构造函数的调用顺序是对象在类中被声明的顺序,而不是它们出现在成员初始化表中的顺序。

  3. 派生类构造函数。

析构函数顺序

  1. 调用派生类的析构函数;

  2. 调用成员类对象的析构函数;

  3. 调用基类的析构函数。

  • 构造函数、析构函数可否抛出异常

析构函数:

如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如资源泄漏的问题。通常异常发生时,C++机制会调用已经构造对象的析构函数来释放资源,此时如析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序崩溃的问题。

那么如果无法保证在析构函数中不发生异常该怎么办?那就是把异常完全封装在析构函数内部,决不让异常抛出析构函数之外。这是一种非常简单,也非常有效的方法。

构造函数:

构造函数中抛出异常,会导致析构函数不能被调用,但对象本身已申请到的内存资源会被系统释放(已申请到资源的内部成员变量会被系统依次逆序调用其析构函数)。在构造函数中抛出异常,在概念上将被视为该对象没有被成功构造,因此当前对象的析构函数就不会被调用。因为析构函数不能被调用,所以可能会造成内存泄露或系统资源未被释放。

构造函数中可以抛出异常,但必须保证在构造函数抛出异常之前把系统资源释放掉,防止内存泄露。构造函数中尽量不要抛出异常,能避免的就避免,如果必须,要考虑不要内存泄露。

解决办法:在Catch 块里面释放已经申请的资源 或者 用智能指针把资源当做对象处理。

深拷贝和浅拷贝

深拷贝与浅拷贝之间的区别就在于深拷贝会在堆内存中另外申请空间来存储数据,从而也就解决来野指针的问题。浅拷贝仅进行数据的一一复制,新数据和旧数据共用一块堆内存,会出现同一块内存空间被释放两次的情况。当数据成员中有指针时,必需要用深拷贝,更加安全。

class Person {
    
    
public:
	//无参(默认)构造函数
	Person() {
    
    
		cout << "无参构造函数!" << endl;
	}
	//有参构造函数
	Person(int age ,int height) {
    
    
		cout << "有参构造函数!" << endl;
		m_age = age;
		m_height = new int(height);
	}
	//拷贝构造函数  
	Person(const Person& p) {
    
    
		cout << "拷贝构造函数!" << endl;
		//如果不利用深拷贝在堆区创建新内存,会导致浅拷贝带来的重复释放堆区问题
		m_age = p.m_age;
		m_height = new int(*p.m_height);
	}
	//析构函数
	~Person() {
    
    
		cout << "析构函数!" << endl;
		if (m_height != NULL){
    
    
			delete m_height;
		}
	}
public:
	int m_age;
	int* m_height;
};

  • 什么时候需要自定义拷贝构造函数?

当类中有指针类型成员变量的时候,一定要自定义拷贝构造和赋值运算符。

原因:当有指针类成员变量时,还是用默认拷贝构造函数(拷贝构造函数执行的时候会调用赋值符),默认赋值为浅拷贝,会导致两个对象指向同一块堆区空间,在最后析构的时候导致内存二次析构而出错!

虚函数

  • 虚函数的实现原理

首先C++中多态的实现是在基类的函数前加上 virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数,如果是基类,就调用基类的函数。

实际上,当一个类中包含虚函数时,编译器会为该类生成一个虚函数表,保存该类中虚函数的地址。同样,派生类继承基类,派生类中自然一定有虚函数,所以编译器也会为派生类生成自己的虚函数表。当我们定义一个派生类对象时,编译器检测该类型有虚函数,所以为这个派生类对象生成一个虚函数指针,指向该类型的虚函数表,这个虚函数指针的初始化是在构造函数中完成的。

后续如果有一个基类类型的指针指向派生类,那么当调用虚函数时,就会根据所指真正对象的虚函数表指针去寻找虚函数的地址,也就可以调用派生类的虚函数表中的虚函数,从而实现多态。

  • 多态的实现原理

多态一般就是指继承加虚函数实现的多态,多态可以分为静态多态和动态多态。

  • 静态多态其实就是重载,静态多态在编译时期就决定了调用哪个函数,根据参数列表来决定;
  • 动态多态是指通过子类重写父类的虚函数来实现的,因为是在运行期间决定调用的函数,所以称为动态多态。

一般情况下我们不区分这两个时所说的多态就是指动态多态。动态多态的实现与虚函数表,虚函数指针相关。

静态多态动态多态区别:静态多态的函数地址早绑定 - 编译阶段确定函数地址;动态多态的函数地址晚绑定 - 运行阶段确定函数地址

多态满足条件:有继承关系;子类重写父类中的虚函数;

多态使用条件:父类指针或引用指向子类对象;

  • 存放位置
  1. 虚函数表指针位置取决于对象在哪。如果是new的对象,则存在堆上,如果是直接声明,则存在栈上;

  2. 虚函数表位于只读数据段(.rodata),即C++内存模型中的常量区;

  3. 虚函数代码则位于代码段(.text),即C++内存模型中的代码区。

  • 虚函数表什么时候生成,虚表指针什么时候赋值

虚函数表创建时机是在编译期间。编译期间编译器就为每个类确定好了对应的虚函数表里的内容。

vptr跟着对象走,所以对象什么时候创建出来,vptr就什么时候创建出来,也就是运行的时候。当程序在编译期间,编译器会为构造函数中增加为vptr赋值的代码(这是编译器的行为),当程序在运行时,遇到创建对象的代码,执行对象的构造函数,那么这个构造函数里有为这个对象的vptr赋值的语句。

  • 编译器如何建立虚函数表

对于派生类来说,编译器建立虚函数表有三个步骤:

  1. 拷贝基类的虚函数表,如果是多继承,就拷贝每个有虚函数基类的虚函数表;

  2. 还有一个基类的虚函数表和派生类自身的虚函数表共用了一个虚函数表,称这个基类为派生类的主基类;

  3. 查看派生类中是否有重写基类中的虚函数, 如果有,就替换成已经重写的虚函数地址;查看派生类是否有自身的虚函数,如果有,就追加自身的虚函数到自身的虚函数表中。

  • 析构函数一般写成虚函数的原因

为了防止内存泄漏。一个基类的指针指向一个派生类的对象,在使用完毕准备销毁时,如果基类的析构函数没有定义成虚函数,那么编译器根据指针类型就会认为当前对象的类型是基类,调用基类的析构函数 (该对象的析构函数的函数地址早就被绑定为基类的析构函数),仅执行基类的析构,派生类的自身内容将无法被析构,造成内存泄漏。

如果基类的析构函数定义成虚函数,那么编译器就可以根据实际对象,执行派生类的析构函数,再执行基类的析构函数,成功释放内存。

  • 构造函数为什么一般不定义为虚函数

而且从目前编译器通过虚函数来实现多态的方式来看,虚函数的调用是通过实例化之后对象的虚函数表指针来找到虚函数的地址进行调用的。如果构造函数是虚函数,那么虚函数表指针则是不存在的,无法找到对应的虚函数表来调用虚函数,那么这个调用实际上也是违反了先实例化后调用的准则。

调用构造函数后, 才能生成一个对象。 假设构造函数是虚函数, 虚函数存在于虚函数表中, 而去找虚函数表又需要虚函数表指针, 而虚函数表指针又存在于对象中, 这样就矛盾了: 都没有生成对象, 哪有什么虚函数表指针呢?

  • 纯虚函数

声明一个纯虚函数的目的就是为了让派生类只继承成员函数的接口,而且派生类中必需提供一个这个纯虚函数的实现,否则含有纯虚函数的类将是抽象类,不能进行实例化。

对于纯虚函数来说,我们其实是可以给它提供实现代码的,但是由于抽象类不能实例化,调用这个实现的唯一方式是在派生类对象中指出其 class 名称来调用。

纯虚函数语法:virtual 返回值类型 函数名 (参数列表)= 0 ,当类中有了纯虚函数,这个类也称为抽象类。

抽象类特点:无法实例化对象;子类必须重写抽象类中的纯虚函数,否则也属于抽象类。

  • 哪些函数不能是虚函数

构造函数,构造函数初始化对象,派生类必须知道基类函数干了什么,才能进行构造;当有虚函数时,每一个类有一个虚函数表,每一个对象有一个虚表指针,虚表指针在构造函数中初始化;

内联函数,内联函数表示在编译阶段进行函数体的替换操作,而虚函数意味着在运行期间进行类型确定,所以内联函数不能是虚函数;

静态函数,静态函数不属于对象属于类,静态成员函数没有this指针,因此静态函数设置为虚函数没有任何意义。

友元函数,友元函数不属于类的成员函数,不能被继承。对于没有继承特性的函数没有虚函数的说法。

普通函数,普通函数不属于类的成员函数,不具有继承特性,因此普通函数没有虚函数。

  • 实例化一个对象需要哪几个阶段

1、分配空间

创建类对象首先要为该对象分配内存空间。不同的对象,为其分配空间的时机未必相同。全局对象、静态对象、分配在栈区域内的对象,在编译阶段进行内存分配;存储在堆空间的对象,是在运行阶段进行内存分配。

2、初始化

首先明确一点:初始化不同于赋值。初始化发生在赋值之前,初始化随对象的创建而进行,而赋值是在对象创建好后,为其赋上相应的值。这一点可以联想下上一个问题中提到:初始化列表先于构造函数体内的代码执行,初始化列表执行的是数据成员的初始化过程,这个可以从成员对象的构造函数被调用看的出来。

3、赋值

对象初始化完成后,可以对其进行赋值。对于一个类的对象,其成员变量的赋值过程发生在类的构造函数的函数体中。当执行完该函数体,也就意味着类对象的实例化过程完成了。

总结:构造函数实现了对象的初始化和赋值两个过程,对象的初始化是通过初始化列表来完成,而对象的赋值则才是通过构造函数的函数体来实现。对于拥有虚函数的类的对象,还需要给虚表指针赋值。

没有继承关系的类,分配完内存后,首先给虚表指针赋值,然后再列表初始化以及执行构造函数的函数体,即上述中的初始化和赋值操作。

有继承关系的类,分配内存之后,首先进行基类的构造过程,然后给该派生类的虚表指针赋值,最后再列表初始化以及执行构造函数的函数体,即上述中的初始化和赋值操作。

类对象的初始化顺序:基类构造函数–>派生类成员变量的构造函数–>派生类自身构造函数

delete this

  • 成员函数中调用delete this会出现什么问题?对象还可以使用吗?

在类对象的内存空间中,只有数据成员和虚函数表指针,并不包含代码内容,类的成员函数单独放在代码段中。在调用成员函数时,隐含传递一个this指针,让成员函数知道当前是哪个对象在调用它。当调用delete this时,类对象的内存空间被释放。在delete this之后进行的其他任何函数调用,只要不涉及到this指针的内容,都能够正常运行。一旦涉及到this指针,如操作数据成员,调用虚函数等,就会出现不可预期的问题。

  • 如果在类的析构函数中调用delete this,会发生什么?

会导致堆栈溢出。原因很简单,delete的本质是“为将被释放的内存调用一个或多个析构函数,然后,释放内存”。显然,delete this会去调用本对象的析构函数,而析构函数中又调用delete this,形成无限递归,造成堆栈溢出,系统崩溃。

函数模板和类模板

  1. 什么是模板?如何实现?

模板:创建类或者函数的蓝图或者公式,分为函数模板和类模板。

实现方式:模板定义以关键字 template 开始,后跟一个模板参数列表。

  • 模板参数列表不能为空;
  • 模板类型参数前必须使用关键字 class 或者 typename,在模板参数列表中这两个关键字含义相同,可互换使用。
template <typename T, typename U, ...>

函数模板:通过定义一个函数模板,可以避免为每一种类型定义一个新函数。

  • 对于函数模板而言,模板类型参数可以用来指定返回类型或函数的参数类型,以及在函数体内用于变量声明或类型转换。
  • 函数模板实例化:当调用一个模板时,编译器用函数实参来推断模板实参,从而使用实参的类型来确定绑定到模板参数的类型。
#include<iostream>

using namespace std;

template <typename T>
T add_fun(const T & tmp1, const T & tmp2){
    
    
    return tmp1 + tmp2;
}

int main(){
    
    
    int var1, var2;
    cin >> var1 >> var2;
    cout << add_fun(var1, var2);

    double var3, var4;
    cin >> var3 >> var4;
    cout << add_fun(var3, var4);
    return 0;
}

类模板:类似函数模板,类模板以关键字 template 开始,后跟模板参数列表。但是,编译器不能为类模板推断模板参数类型,需要在使用该类模板时,在模板名后面的尖括号中指明类型。

#include <iostream>

using namespace std;

template <typename T>
class Complex
{
    
    
public:
    //构造函数
    Complex(T a, T b)
    {
    
    
        this->a = a;
        this->b = b;
    }

    //运算符重载
    Complex<T> operator+(Complex &c)
    {
    
    
        Complex<T> tmp(this->a + c.a, this->b + c.b);
        cout << tmp.a << " " << tmp.b << endl;
        return tmp;
    }

private:
    T a;
    T b;
};

int main()
{
    
    
    Complex<int> a(10, 20);
    Complex<int> b(20, 30);
    Complex<int> c = a + b;

    return 0;
}
  1. 函数模板和类模板的区别?

实例化方式不同:函数模板实例化由编译程序在处理函数调用时自动完成,类模板实例化需要在程序中显式指定。

实例化的结果不同:函数模板实例化后是一个函数,类模板实例化后是一个类。

默认参数:类模板在模板参数列表中可以有默认参数。

特化:函数模板只能全特化;而类模板可以全特化,也可以偏特化。

调用方式不同:函数模板可以隐式调用,也可以显式调用;类模板只能显式调用。

函数模板调用方式举例:

#include<iostream>

using namespace std;

template <typename T>
T add_fun(const T & tmp1, const T & tmp2){
    
    
    return tmp1 + tmp2;
}

int main(){
    
    
    int var1, var2;
    cin >> var1 >> var2;
    cout << add_fun<int>(var1, var2); // 显式调用

    double var3, var4;
    cin >> var3 >> var4;
    cout << add_fun(var3, var4); // 隐式调用
    return 0;
}

  1. 什么是可变参数模板?

可变参数模板:接受可变数目参数的模板函数或模板类。将可变数目的参数被称为参数包,包括模板参数包和函数参数包。

  • 模板参数包:表示零个或多个模板参数;
  • 函数参数包:表示零个或多个函数参数。

用省略号来指出一个模板参数或函数参数表示一个包,在模板参数列表中,class… 或 typename… 指出接下来的参数表示零个或多个类型的列表;一个类型名后面跟一个省略号表示零个或多个给定类型的非类型参数的列表。当需要知道包中有多少元素时,可以使用 sizeof… 运算符。

template <typename T, typename... Args> // Args 是模板参数包
void foo(const T &t, const Args&... rest); // 可变参数模板,rest 是函数参数包

#include <iostream>

using namespace std;

template <typename T>
void print_fun(const T &t)
{
    
    
    cout << t << endl; // 最后一个元素
}

template <typename T, typename... Args>
void print_fun(const T &t, const Args &...args)
{
    
    
    cout << t << " ";
    print_fun(args...);
}

int main()
{
    
    
    print_fun("Hello", "wolrd", "!");
    return 0;
}
/*运行结果:
Hello wolrd !
*/

说明:可变参数函数通常是递归的,第一个版本的 print_fun 负责终止递归并打印初始调用中的最后一个实参。第二个版本的 print_fun 是可变参数版本,打印绑定到 t 的实参,并用来调用自身来打印函数参数包中的剩余值。

  1. 什么是模板特化?为什么特化?

模板特化的原因:模板并非对任何模板实参都合适、都能实例化,某些情况下,通用模板的定义对特定类型不合适,可能会编译失败,或者得不到正确的结果。因此,当不希望使用模板版本时,可以定义类或者函数模板的一个特例化版本。

模板特化:模板参数在某种特定类型下的具体实现。分为函数模板特化和类模板特化

  • 函数模板特化:将函数模板中的全部类型进行特例化,称为函数模板特化。
  • 类模板特化:将类模板中的部分或全部类型进行特例化,称为类模板特化。

特化分为全特化和偏特化:

  • 全特化:模板中的模板参数全部特例化。
  • 偏特化:模板中的模板参数只确定了一部分,剩余部分需要在编译器编译时确定。

说明:要区分下函数重载与函数模板特化。定义函数模板的特化版本,本质上是接管了编译器的工作,为原函数模板定义了一个特殊实例,而不是函数重载,函数模板特化并不影响函数匹配。

#include <iostream>
#include <cstring>

using namespace std;
//函数模板
template <class T>
bool compare(T t1, T t2)
{
    
    
    cout << "通用版本:";
    return t1 == t2;
}

template <> //函数模板特化
bool compare(char *t1, char *t2)
{
    
    
    cout << "特化版本:";
    return strcmp(t1, t2) == 0;
}

int main(int argc, char *argv[])
{
    
    
    char arr1[] = "hello";
    char arr2[] = "abc";
    cout << compare(123, 123) << endl;
    cout << compare(arr1, arr2) << endl;

    return 0;
}
/*
运行结果:
通用版本:1
特化版本:0
*/

猜你喜欢

转载自blog.csdn.net/weixin_42461320/article/details/129158137
今日推荐