C++学习笔记day3

目录

前言

一、类继承

1.1 继承概念(基类、派生类)

1.2 类创建顺序

1.3 继承权限

1.4 重定义(遮蔽)

二、虚函数

2.1 纯虚函数、虚析构

2.2 重写(覆盖)

2.3 多态

三、多重继承

3.1 虚基类、虚继承、虚表

3.2 计算类大小(继承关系)

3.3 多次继承初始化顺序

四、C++异常机制

4.1 异常、检测步骤、处理异常

4.2 内联函数、隐式转换与explicit

4.3 虚析构、纯虚析构

五、模板

5.1 模板函数

5.2 类模板

六、STL(标准模板)

6.1 STL 6大组件

6.2 容器(序列、关联)

总结



前言

例如:本章主要总结了类继承的权限,格式、虚函数、虚基类、虚析构、纯虚函数、纯虚析构的作用与区别;重写、重定义、重载的区别及代码举例;多重继承的格式、初始化列表顺序及注意事项以及多次继承下,如何计算所占内存空间大小;C++的异常处理、包括检测、抛出、处理以及多个多种异常的处理情况;同时对模板函数进行了细致分类,举例如何操作;STL标准模板库的 6大组件、容器介绍,明天将进行深入探索!!


提示:以下是本篇文章正文内容,下面案例可供参考

一、类继承

继承的概念:继承允许依据一个类来定义另一个类,使创建和维护一个应用程序变得更容易
优点:提高代码的复用率、执行效率

当创建一个类时,不用重新编写新的数据成员、函数,只需指定新建的类继承了一个已有的类的成员即可。

1.1 继承概念(基类、派生类)

当创建一个类时,指定新建的类继承了一个已有的类的成员即可,已有的类称为基类或父类,新建的类称为派生类或子类。
一个子类可以有多个父类,它继承了多个父类的特性。继承可以实现面向对象代码重用,继承也是实现多态的必要语法
使用方法:class 子类名 : 继承方式 父类名1,继承方式 父类名2,继承方式 父类名3....

示例1:
#include <iostream>
using namespace std;

class Food
{
public:
};
//添加继承关系:冒号: 继承权限 父类
//kfc:子类  派生类  
//Food:基类 父类
class KFC : public Food
{
    string chips;
public:
    KFC(string chips)
    {
        this->chips = chips;
    }
    void show()
    {
        cout<<"kfc: "<<chips<<endl;
    }
};
int main()
{
    KFC kfc("薯条");
    kfc.show();
    return 0;
}

思考:引伸1 如果父类中定义了同名的变量呢?
     引伸2:子类中没有成员变量,父类中定义? 

1.2 类创建顺序

父子类创建顺序:先父后子

1.当父类没有显示的定义构造,那么会调用父类默认的构造函数
2.父类显示的定义了构造,那么会调用父类显示的无参的构造
3.先创建父类,后创建子类
示例:
#include <iostream>
using namespace std;

class Food
{
public:
     string chips;
    Food(string chips):chips(chips)
    {
        cout<<"food create"<<endl;
}
Food()
    {
        cout<<"food 无参构造"<<endl;
    }
};
//添加继承关系:冒号: 继承权限 父类
//kfc:子类  派生类  Food:基类 父类
class KFC : public Food
{
string chips;
public://子类的初始化列表中,先创建父类,即调用父类的无参构造函数
    KFC(string chips):chips(chips)
    {
       // this->chips = chips;
        cout<<"kfc create"<<endl;
    }
    void show()
    {
        cout<<"kfc: "<<chips<<endl;
    }
};
int main()
{
KFC kfc("薯条");
    kfc.show();
    return 0;
}
输出:
food 无参构造


结论:如果没有显式的调用父类构造函数,会调用父类默认无参的构造函数

思考:如果子类无成员,如何通过参数列表给父类成员初始化?
子类的初始化列表,可以通过父类的构造函数来初始化父类的成员

注意:如果父类中没有无参构造函数,子类的初始化列表中必须显式的调用父类带参的构造函数
初始化顺序:先初始化父类,然后初始化自己成员
#include <iostream>
using namespace std;

class Food
{
public:
     string chips;
    Food(string chips):chips(chips)
    {
        cout<<"food create"<<endl;
    }
};
class KFC : public Food
{
public://子类的初始化列表中,初始化父类的构造函数
    KFC(string chips):Food(chips) //父类的匿名对象
    {
       // this->chips = chips;
        cout<<"kfc create"<<endl;
    }
    void show()
    {
        cout<<"kfc: "<<chips<<endl;
    }
};
int main()
{
    KFC kfc("薯条");
    kfc.show();
    return 0;
}

1.3 继承权限

继承权限:
public继承     子类继承后的访问权限不变
protected继承  子类继承后,父类中的public变成protected,其它不变(protected:只有子类可以用),类外不可用
private继承    子类继承后,父类中的public和protected变成private,其他不变

1.4 重定义(遮蔽)

继承时的重定义(遮蔽问题)
如果派生类中的成员(包括成员变量和成员函数)和基类中的成员重名,那么就会遮蔽从基类继承过来的成员。所谓遮蔽,是派生类中使用该成员时,实际上使用的是派生类新增的成员,而不是从基类继承来的。

注意:基类成员函数和派生类成员函数不构成重载

基类成员和派生类成员名字一样时会造成遮蔽,不管函数的参数如何,只要名字一样就会造成遮蔽。
基类成员函数和派生类成员函数不会构成重载,如果派生类有同名函数,那么就会遮蔽基类中的所有同名函数,不管它们的参数是否一样。

重定义(遮蔽):1.继承关系下(父子类中) 2.函数名相同  (参数是否一样 返回值是否一样 )
重载(overload):1.同一个类中           2.函数名相同  3.参数列表不同(参数个数,参数类型)

父类中的函数 和 子类中的函数  不可以构成函数的重载

调用:
1、如果子类中调用函数,父子类中都有时,优先使用子类自己的(内部优先原则)
如果要调用父类成员函数、变量,调用方法为:类名::函数名/变量名

2、如果子类对象没有,则会调用父类的

二、虚函数

c++中定义成员函数时 前边加关键字virtual 这个函数就是虚函数  virtual void eat();

2.1 纯虚函数、虚析构

纯虚函数、抽象类
1.格式: 
	virtual 返回值类型 函数名(形参列表) = 0 ; //声明此函数是纯虚函数
2.特点(记住)
	1)在基类中没有实现 
	2)必须在派生类中加以实现 (需要派生类实现其函数定义)
	3)有纯虚函数的类相当于接口 抽象类 不能直接实例化

3.抽象类:包含纯虚函数的类  不允许创建抽象类对象。
4.抽象类作用:就是用来被继承的,纯虚函数就是用来被重写的。

5.定义纯虚函数的目的: 是为了可以通过抽象类的指针去调用子类对象中重写的函数。
示例代码1:
#include <iostream>
using namespace std;
//抽象类:包含了纯虚函数,那么这个类就变成了抽象类
        //特点:1.不可以单独的实例化(创建)对象,
        //作用:用来被继承,重写纯虚函数
         //2.有类继承了抽象类之后,必须重写抽象类中的纯虚函数
class Person
{
public:
    //纯虚函数: 虚函数 = 0;
    virtual void show(int a) = 0;
};
class B : public Person
{
public:
    void show(int a)
    {
        cout<<a<<endl;
    }
};
int main()
{
   Person *p = new B;
   p->show(12);
    
/*  B b;//可以的
    Person *p = new Person;//不行的
    Person a;//不行
*/
    return 0;
}
析构函数:作用就是在类对象结束时,释放空间回收资源
调用顺序:
1、成员变量和构造函数:先成员 后构造  
2、父类的成员变量和构造函数: 先父类中的成员 后构造 
3、析构函数的顺序:同构造的顺序相反(对象都在栈空间)
4、继承关系时:先父类,后构造 ,先父后子

虚析构:在多重继承中,析构函数函数释放空间时,不会释放子类空间,所以虚析构的作用就是在父类对象调用子类函数或变量结束时(抽象类),先释放子类空间,再释放父类空间

2.2 重写(覆盖)

重写 override: 子类中定义和父类中虚函数同名,同返回值类型,同参数列表的函数,叫重写。

对比重载:overload 在同一个类中,函数名相同,参数列表不同,返回值无所谓,叫重载.

代码示例:重写
#include <iostream>
using namespace std;
//函数的重写override:1.发生在继承关系中  2.函数名 3.参数列表相同
//函数的重载overload: 1.发生在同一个类中  2.函数名 3.参数列表不同
class Person
{
public:
    //虚函数:virtual
    virtual void show()
    {
        cout<<"person  show..."<<endl;
    }
};
class Student : public Person
{
public:
    void show()
    {
        cout<<"stu  show..."<<endl;
    }
};
int main()
{
    Person p;
    p.show();
    Student stu;
    stu.show();

    return 0;
}

重载、重写、重定义
重载:同一个类中,函数名相同、函数参数不同(参数格式、类型)
重写:继承关系下,父类中有虚函数、子类与父类的函数名相同、参数返回值都相同(覆盖)
重定义:继承关系下,主要函数名相同即为重定义(遮蔽)

2.3 多态

多态:指的是同一名字的事物可以完成不同的功能。

编译时多态:函数的重载(包括运算符的重载)、对重载函数的调用,在编译时就能根据实参确定应该调用哪个函数,因此叫编译时的多态;

运行时多态:和继承、虚函数等概念有关,重写

c++的虚函数,主要作用是 实现了运行时多态。
父类中虚函数的实现,是为子类提供默认的函数实现;子类可以重写父类的虚函数实现子类的特殊化。

#include <iostream>
using namespace std;

//一个事务(函数),多种表现形态(多个不同的功能)
class Hero
{

public:
    //进攻的函数:提供的默认功能
    virtual void attack()
    {
        cout<<"小刀拉拉"<<endl;
    }
    void fun()
    {
        cout<<"hero  fun..."<<endl;
    }
};
class SheShou : public Hero
{
public:
    //不重写:拿到父类给你提供的默认功能
    //重写(父类提供的默认功能,不能满足当前类对象的功能):子类功能 的特殊化
    void attack()
    {
        cout<<"点点点"<<endl;
    }
    void fun1()
    {
        cout<<"fun1..."<<endl;
    }
};
class FaShi : public Hero
{
public:
    void attack()
    {
        cout<<"东东冻"<<endl;
    }
};
//父类指针调用函数时:如果调用父类普通的函数  --》 父类普通的函数
//                 如果调用父类虚函数 --》 子类重写的函数
//                  不可以访问 子类特有的变量或者函数
/*
 *多态有两种:编译时  运行时
 * 运行时多态 实现步骤:
 * 1.继承关系下,父类有虚函数
 * 2.子类选择 是否重写
 * 3.父类指针  指向 子类对象
 * 4.父类指针 可以访问 子类重写的函数
 * */
int main()
{
    //父类指针 指向 子类对象
    Hero* h ;//指针 并没有 创建父类对象

    h = new SheShou;
    h->attack();//子类重写的
    h->fun();//父类普通的函数
    h->fun1();//不可以的,访问子类特有的函数,只能反问子类重写的

    h = new FaShi;
    h->attack();//法师类 重写函数
    return 0;
}

多态实现步骤
1)父类中包含虚函数
	如果使用这种指针方式,若调用父类普通函数,则调用函数时依然是父类的方法;
	若调用父类的虚函数,调用函数时调用的是子类重写之后的函数。

2)父类指针指向子类对象

	父类* 指针 = new 子类();
	Shape* shape = new Rectangle();
	使用这种方式,父类的指针可以指向自己的所有派生类对象。
		
	Student xiaoming;
	Person& p1 = xiaoming;//父类引用指向子类对象
	Person* p2 = &xiaoming;//指针的用法同引用
			
**********父类类型的指针 可以指向子类类型的对象*******重要 
1.画图演示
2. 说明调用顺序  1)看父类有没有虚函数
				 2)如果有,去子类中找有没有重写,有就执行子类重写的方法;
				 3)如果子类没有重写,就去执行父类的虚函数。

注意:
父类指针 指向子类对象时,可以调用父类继承的和子类重写的
不能使用 子类特有的变量和函数

引用也可以实现多态
由于引用类似于常量,只能在定义的同时初始化,不能再引用其他数据,所以必须要定义两个引用变量,一个用来引用基类对象,一个用来引用派生类对象。
当基类的引用指代基类对象时,调用的是基类的成员,而指代派生类对象时,调用的是派生类的成员。

不过引用不像指针灵活,指针可以随时改变指向,而引用只能指代固定的对象,在多态性方面缺乏表现力,所以谈及多态时一般是说指针。

三、多重继承

3.1 虚基类、虚继承、虚表

虚基类:以virtual声明的基类 使得在继承间接共同基类时 只对共同的基类初始化一次

虚继承:格式:class 派生类名:virtual 继承方式 基类名
这种继承称作虚继承,只能在派生的时候声明虚基类
作用:
1、为了解决多继承时的命名冲突和冗余数据问题,C++ 提出了虚继承,使得在派生类中只保留一份间接基类的成员。
2、在继承方式前面加上 virtual 关键字就是虚继承,使得在继承间接共同基类时,只对共同的基类初始化一次
3、消除二义性
***************************错误代码***********************
class A
{
protected:
    int money;
public:
    A(int money):money(money){ cout<<"a create"<<endl;}
};//先父后子
class B : public A
{
public:
    B(int money):A(money){ cout<<"b create"<<endl; }
};
class C : public A
{
public:
    C(int money):A(money){ cout<<"c create"<<endl; }
};
        //    A B      A C
class D : public B, public C
{
public:     //        1         3
    D(int money):B(money-1),C(money+1)
    {
        cout<<"d create"<<endl;
    }
    void show(){cout<<C::money<<endl;}
};
int main()
{
    D d(2);
    d.show();
    return 0;
}

***************************正确代码***********************
#include <iostream>

using namespace std;
//菱形继承
//虚继承的那个类,被叫做虚基类
class A
{
protected:
    int money;//1
public:

    A(int money):money(money){cout<<"A"<<endl;}
};
//虚继承: B 我将共享我的父类A   并没有真实的创建类A,构造函数传值都是失效的
class B:virtual public A
{
public:
    B(int money): A(money) {
        cout<<"B"<<endl;
        cout<<money<<endl;
    }
};
//虚继承: C 我将共享我的父类A, 并没有真实的创建类A,构造函数传值都是失效的
class C:virtual public A
{
public:
     C(int money): A(money) {
         cout<<"C"<<endl;
          cout<<money<<endl;
     }
};
//最底层派生类中,初始化列表中 首先创建 共同的类A
class D: public B,public C
{
public://    创建的A(创建虚基类),传值有效      失效       失效
    D(int money):A(money),       B(money-1),C(money+1) {cout<<"D"<<endl;}
    void show(){
        cout<<money<<endl;
    }
};

int main()
{
    D d(1);
    d.show();
    return 0;
}

总结:
虚基类:
   以virtual继承, 使得在继承间接共同基类时 只对共同的基类初始化一次
在哪初始化:最底层(最远派生类)的派生类 初始化列表中,首先初始化虚基类
   作用:消除二义性

虚继承:菱形继承
  以virtual修饰, 类声明为 我可以共享我的父类,并不以当前类的初始化列表的赋值为准,默认失效的赋值
	
虚函数:
  C++的虚函数主要作用是“运行时多态”,父类中提供虚函数的实现,为子类提供默认的函数实现。
子类可以重写父类的虚函数实现子类的功能特殊化。
	
父类指针指向子类对象与虚函数实现多态:
	父类* 指针 = new 子类();
	Shape* shape = new Rectangle();
	使用这种方式,父类的指针可以指向自己的所有派生类对象。
	如果使用这种指针方式,若调用父类普通函数,则调用函数时依然是父类的方法;
	若调用父类的虚函数,调用函数时调用的是子类重写之后的函数。

多态:
	 1.子类可以重写父类中virtual函数,
	 2.用父类指向子类的指针或引用可以访问子类中重写的方法:为了将逻辑(类)当做参数进行传递
     3.父类可以调用子类重写的方法

菱形继承,是非常不科学的继承逻辑,类的关系设计的合理,是不可能出现菱形继承的。

但是C++的语法允许我们出现菱形继承,并且菱形继承逻辑必然出问题。

虚函数表(虚表):当类中定义虚函数后,编译器会生成一个虚函数表(虚表,相当于一堆函数指针),
父类指针指向子类对象时,通过虚表去锁定调用子类中重写的函数。

class Shape//抽象类,因为有纯虚函数
{
public:
    virtual int getPerimeter() = 0;//纯虚函数
    virtual string getType() = 0;
};


int main()
{
    //输出4,因为类中有虚函数,所以会生成一个指向虚表的指针
    cout<<sizeof(Shape)<<endl;
    return 0;
}	

1)每个虚继承的子类都有一个虚基类指针(占用一个指针的存储空间,4字节)和虚基类表(不占用类对象的存储空间);
2)当虚继承的子类被当做父类继承时,虚基类指针也会被继承。
实际上,vbptr指的是虚基类表指针(virtual base table pointer),该指针指向了一个虚基类表(virtual table),虚表中记录了虚基类与本类的偏移地址;通过偏移地址,这样就找到了虚基类成员,
虚基类依旧存在继承类中,只占用存储空间;虚函数不占用存储空间。
3)虚基类表存储的是虚基类相对直接继承类的偏移;而虚函数表存储的是虚函数地址。 

3.2 计算类大小(继承关系)

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

空类可以实例化,每个实例在内存中都有一个独一无二的地址,为了达到这个目的,编译器往往会给一个空类隐含的加一个字节,这样空类在实例化后在内存得到了独一无二的地址.所以sizeof( A )的大小为1

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

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

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

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

示例1:
class G : public E
{
 virtual int VirtualMemFuncTest1(int para)=0;
 int m_Int;
};
class H : public G
{
 int m_Int;
};
sizeof( G ) = ?  
sizeof( H ) = ?


可以看出子类的大小是本身成员的大小再加上父类成员的大小.如果父类还有父类,也加上父类的父类。
sizeof( G ) = 16
sizeof( H ) = 20

示例2:
class D
{
 D(){}
 virtual ~D() {}
 virtual int VirtualMemFuncTest1()=0;
 virtual int VirtualMemFuncTest2()=0;
 virtual int VirtualMemFuncTest3()=0;
};

class I : public D
{
 virtual int VirtualMemFuncTest1()=0;
 virtual int VirtualMemFuncTest2()=0;
};
sizeof( I ) = ?


父类子类工享一个虚函数指针,虚函数指针保留一个即可。
sizeof( I ) = 4


总结:
空的类也是会占用内存空间的,而且大小是1,原因是C++要求每个实例在内存中都有独一无二的地址。
(一)类内部的成员变量:
普通的变量:是要占用内存的,但是要注意内存对齐(这点和struct类型很相似)。
static修饰的静态变量:不占用内存,原因是编译器将其放在全局变量区。
从父类继承的变量:计算进子类中。

(二)类内部的成员函数:
非虚函数(构造函数、静态函数、成员函数等):不占用内存。
虚函数:要占用4个字节(32位的操作系统),用来指定虚拟函数表的入口地址。跟虚函数的个数没有关系。父类子类工享一个虚函数指针。

    构成对象本身的只有数据,任何成员函数都不隶属于任何一个对象,非静态成员函数与对象的关系就是绑定,绑定的中介就是this指针。成员函数为该类所有对象共享,不仅是处于简化语言实现、节省存储的目的,而且是为了使同类对象有一致的行为。同类对象的行为虽然一致,但是操作不同的数据成员。

3.3 多次继承初始化顺序

初始化顺序和继承的顺序为准,不按照初始化列表来, 和初始化成员变量时相同。
#include <iostream>
using namespace std;
class A
{
public:
	A(int a){cout<<"A create"<<endl;}
	~A(){cout<<"A delete"<<endl;}
};

class B
{
public:
	B(int a){cout<<"B create"<<endl;}
	~B(){cout<<"B delete"<<endl;}
};
//以继承的先后顺序为基准 
class C : public A, public B
{
	int num1;
	int num2;
public:
	//D d;
	C():B(1),A(1)//匿名对象
	{
		cout<<"C create"<<endl;
	}
	~C()
	{
		cout<<"C delete"<<endl;
	}
};

int main()
{
	C c;
	return 0;
}
 A  B  C  C  B  A

四、C++异常机制

4.1 异常、检测步骤、处理异常

异常是程序在执行期间产生的问题。C++ 异常是指在程序运行时发生的特殊情况,比如尝试除以零的操作。

检测异常的三个步骤:检查(try) 抛出(throw) 捕捉(catch)
	try: try 块中的代码标识将被激活的特定异常。它后面通常跟着一个或多个 catch 块。 
	throw: 当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。
	catch: 在您想要处理问题的地方,通过异常处理程序捕获异常。catch 关键字用于捕获异常。

try...catch语法:

try 
{
	被检查语句
}
catch(异常处理的类型 变量名)
{
	进行异常处理的语句;
}

**********************示例1:*******************
#include<iostream>
using namespace std;

int mydiv(int a,int b)
{
	if(b==0)   //分母为0,说明发生了异常
	{
		cout<<"throw....."<<endl;
		throw b;//抛出一个异常对象,异常对象可以是基本类型,也可以是类类型。
	}
	return a/b;
}
int main()  
{
	int a=10;
	int b=0;
	try
	{
//因为mydiv函数可能会抛出异常,所以写在try语句块中用来捕捉异常。
		int result = mydiv(10,0);
如果mydiv抛出异常,会马上终止try语句块
如果mydiv没有抛出异常,会把try语句块执行完
		cout<<result<<endl;
	}
//在catch中处理try中抛出的异常,catch()中的对象,就是try中抛出的异常对象
	catch(int e){
		cout<<"catch int "<<e<<endl;
	}
}
*************************示例2:捕捉多异常****************************
#include <iostream>
using namespace std;

class ExceptA//专门用来表示异常的类
{
public:
	void show()
	{
		cout<<"异常A"<<endl;
	}
};

class ExceptB//专门用来表示异常的类
{
public:
	void show()
	{
		cout<<"异常B"<<endl;
	}
};
int div(int a, int b)
{
	if(b == 0)
	{
		ExceptA e;
		throw e;//抛出异常对象ExceptA 类型
	}
	return a/b;
}
double div(double a, double b)
{
	if(b == 0)
	{
		ExceptB e;
		throw e;//抛出异常对象ExceptB 类型
	}
	return a/b;
}
int main()
{
	try
	{
//同一时刻只能抛出一个异常
		div(1, 0);//当这里抛出ExceptA 类型异常时,整个try就结束了
		div(1.0, 0.0);
	}
	catch(ExceptA e)//专门用来处理ExceptA 类型异常的catch
	{
		e.show();	
	}
	catch(ExceptB e)//专门用来处理ExceptB 类型异常的catch
	{
		e.show();
	}
	catch(...)//处理其他类型的异常
	{
		cout<<"other e"<<endl;	
	}
}

4.2 内联函数、隐式转换与explicit

内联函数格式:
class Person
{
public:
    inline void show()//内联函数的声明和定义不能分开
    {
        
    }
};

内联函数、普通函数区别:当编译器处理调用内联函数的语句时,不会将该语句编译成函数调用的指令,而是直接将整个函数体的代码插人调用语句处,就像整个函数体在调用处被重写了一遍一样。好处就是省去切换函数所需要的开销。
比较小的函数(三两行代码的函数),又会被频繁调用的函数,建议定义成内联函数。
如果编译器判断函数不适合展开,不会将函数当做内联函数处理。

声明内联函数,这是向编译器建议把函数在调用处展开,最终有编译器决定要不要展开。
#include <iostream>
using namespace std;

inline int max(int a, int b)//内联函数,内联函数的声明和定义不能分开。
{
    if(a>b)
        return a;
    else 
        return b;
}

int main()
{
    	cout<<max(10, 20)<<endl;//内联函数会在调用处将函数的代码展开,省去切换函数的开销。
	return 0;
}

注意:内联函数的函数逻辑必须很简单。

4.3 虚析构、纯虚析构

虚析构和纯虚析构		
问题:多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构函数;
解决方式:将父类中的析构函数改为虚析构或者纯虚析构;

示例代码1:
#include <iostream>
using namespace std;

class A
{
public :
    A(){cout<<"A create"<<endl;}
   virtual ~A(){cout<<"A delete"<<endl;}
};
class B : public A
{
public :
    B(){cout<<"B create"<<endl;}
    ~B(){cout<<"B delete"<<endl;}
};
class C : public B
{
public :
    C(){cout<<"C create"<<endl;}
    ~C(){cout<<"C delete"<<endl;}
};

int main(int argc, char * argv[]) {

	//先调用子类的析构,释放子类空间
	//后调用父类的析构,释放父类的空间
    A *pa =  new C;
    delete pa;

    return 0;
}
A  B  C
析构: C  B  A

总结:查看三种情况下的输出
1.没在析构函数前加virtual时:只调用父类析构,没有调用子类析构
2.在析构函数前加virtual,调用虚析构时:先调用子类析构,后调用父类析构
3.在析构函数前加virtual,调用纯虚析构时:先调用子类析构,后调用父类析构

注意:
1、虚析构或纯虚析构就是用来解决通过父类指针释放子类对象
2、如果子类中没有堆区数据,可以不写为虚析构或纯虚析构
3、拥有纯虚析构函数的类也属于抽象类,无法实例化对象,子类必须重写,不重写的话仍旧为抽象类,无法实例化对象。

虚析构和纯虚析构共性:可以解决父类指针释放子类对象,都需要有具体的函数实现

虚析构和纯虚析构区别:如果是纯虚析构,该类属于抽象类,无法实例化对象

析构语法:virtual ~类名(){}
纯虚析构语法:virtual ~类名()=0; 
类名::~类名(){  }

虚函数 虚基类 纯虚函数 虚析构
虚函数:1.格式 virtual  2.作用:实现动态 
虚基类:1.作用:最后继承的类初始化虚基类,只初始化一次
        2.菱形继承 3.虚继承 4。在那里创建的虚基类

纯虚函数:1.格式 2.抽象类 3.特点 4.普通纯虚函数没有功能实现 = 0  4.就是用来重写的
纯虚析构:virtual ~A() = 0;  作用:避免内存泄漏
类外实现(保留析构函数的功能):A::~A(){   析构函数的功能 }
虚析构:1.作用 :避免内存泄漏
explicit:explicit用来防止由构造函数定义的隐式转换。
**************************示例1:***********************************
#include <iostream>
#include <string>
using namespace std;
class Person
{
private:
    int age;
    string name;
public:
    Person()
    {
        cout<<"no arg"<<endl;
    }
    
    Person(int age):age(age)
    {
        cout<<"int arg"<<endl;
    }
    
    Person(const char* name):name(name)
    {
        cout<<"string arg"<<endl;
    }
	void show()
	{
		cout<<age<<endl;
	}
};

void fun(Person p)
{
	p.show();
}
int main()
{
    Person p = 10;   //触发隐式转换 调用Person(int age)
Person p2 = "farsight";//触发隐式转换  调用Person(const char* name)
fun(100);//触发隐式转换 调用Person(int age)
    return 0;
}

总结:
构造函数的实参只需要一个参数时会触发隐式转换。

***********************禁止隐式转换示例:********************************
#include <iostream>
#include <string>

using namespace std;

class Person
{
private:
    int age;
    string name;
public:
    Person()
    {
        cout<<"no arg"<<endl;
    }
    
    explicit Person(int age):age(age)
    {
        cout<<"int arg"<<endl;
    }
    
    explicit Person(string name):name(name)
    {
        cout<<"string arg"<<endl;
    }
};

int main()
{
    Person p = 10;//Person p(10);
    Person p2 = "farsight";//Person p1("farsight");
    return 0;
}
explicit的优点是可以避免不合时宜的类型变换,缺点:无。
被声明为explicit的构造函数通常比其non-explicit兄弟更受欢迎。
因为它们禁止编译器执行非预期(往往也不被期望)的类型转换。
除非有一个好理由允许构造函数被用于隐式类型转换,否则要把它声明为explicit。

五、模板

模板:泛型编程(可以将类型(基本类型、自定义类型)当做参数, 进行传递)

5.1 模板函数

只适用于函数体相同 参数个数相同 参数的类型不同的情况
*************************示例1:函数模板
#include <iostream>
using namespace std;
//模板函数
template<typename T> //<>放的是 类型参数 int   string  char.....
void show(T s,T a)
{
    cout<<"T .... "<<s<<endl;
}
int main() {
   show<string>("hello","world");//<>放的是类型参数
    show<int>(10,20);//<>放的是类型参数
    return 0;
}
*************************示例2:类型当做返回值类型
#include <iostream>
using namespace std;
//模板函数
template<typename T> //<>放的是 类型参数 int   string  char.....
T show(T s,T a)
{
    cout<<"T .... "<<s<<endl;
    return s;
}

int main() {

	cout<<show<string>("hello","world")<<endl;//<>放的是类型参数
    cout<<show<int>(10,20)<<endl;//<>放的是类型参数

    return 0;
}
*************************示例3:类类型
#include <iostream>
using namespace std;


class Person
{
    int age;
   public:
       Person(int age):age(age){}
       void show(){cout<<age<<endl;}
};

//函数模板:函数名相同,参数类型不同,个数相同
template<class T,typename U>//Person
void div(T a,T b,U num)//()参数列表  形参
{
  a.show();
  b.show();
  T c(122);
  c.show();
}
int main()
{
    Person p(1);
    //模板类型:自定义类型
    div<Person,int>(Person(12),p,10);

    return 0;
}

函数模板和普通函数的区别:
	1) 普通函数可以自动类型转换 函数模板必须显示指定类型
	2) 同名的普通函数和模板函数出现,优先调用普通函数
	3) 函数模板也可以发生重载

5.2 类模板

使用方法:显示指定类型 Person<int,string> p(10,"王美丽");
泛型编程:类型参数化
注意:类模板不建议分文件编写,所有的函数实现都写在头文件中

******************************示例1:成员变量
	#include <iostream>
	using namespace std;

	template <typename T1,typename T2>
 class Person{
	private:
		T1 age;
		T2 name;
	public:
		Person(T1 a,T2 n):age(a),name(n){}
		void show(){
			cout << name << ":" << age << endl;
		}
	};

	int main()
	{
		Person<int,string> p(10,"王美丽");
		p.show();
		return 0;
	}
******************************示例2:
#include <iostream>
using namespace std;

template<typename T>
class Person
{
public:
    Person(int age):age(age)
    {   
    }
	void eat(T food)
	{
		cout<<food<<age<<endl;
	}
private:
    int age;
};
int main()
{
	Person<string> xiaoming(2);	
	xiaoming.eat("牛排");
	
	Person<string>* p = new Person<string>(10);
	p->eat("鸡排");
	delete p;
}

六、STL(标准模板)

STL标准模板库:即标准模板库或者泛型库,其包含有大量的模板类和模板函数,是 C++ 提供的一个基础模板的集合,用于完成诸如输入/输出、数学计算等功能;该库包含了诸多在计算机科学领域里所常用的基本数据结构和基本算法。
	为广大C++程序员们提供了一个可扩展的应用框架,高度体现了软件的可复用性。

1.在 C++ 中如果定义一个数组,可以采用如下方式:int a[n];
这种定义数组的方法需要事先确定好数组的长度,即 n 必须为常量,这意味着,如果在实际应用中无法确定数组长度,则一般会将数组长度设为可能的最大值,但这极有可能导致存储空间的浪费。

2.还可以采用在堆空间中动态申请内存的方法,此时长度可以是变量:
int *p = new int[n];
这种定义方式可根据变量 n 动态申请内存,不会出现存储空间浪费的问题。
但是,如果程序执行过程中出现空间不足的情况时,则需要加大存储空间,此时需要进行扩容:

3.新申请一个较大的内存空间,即执行int * temp = new int[m];
将原内存空间的数据全部复制到新申请的内存空间中,即执行memecpy(temp, p, sizeof(int)*n);
将原来的堆空间释放,即执行delete [] p; p = temp;

4.完成相同的操作,如果采用 STL 标准库,则会简单很多,因为大多数操作细节将不需要程序员关心。
示例:
vector <int> a; //定义 a 数组,当前数组长度为 0,但和普通数组不同的是,		
此数组 a 可以根据存储数据的数量自动变长。
//向数组 a 中添加 10 个元素
for (int i = 0; i < 10 ; i++)
    a.push_back(i)
//还可以手动调整数组 a 的大小
a.resize(100);
a[90] = 100;
//还可以直接删除数组 a 中所有的元素,此时 a 的长度变为 0
a.clear();
//重新调整 a 的大小为 20,并存储 20 个 -1 元素。
a.resize(20, -1)

对比以上两种使用数组的方式,使用 STL 可以更加方便灵活地处理数据。所以,只需要系统地学习 STL,集中精力去实现程序的功能,而无需纠结某些细节如何用代码实现。

优点:安全、方便、快捷简单

6.1 STL 6大组件

STL:由容器、算法、迭代器、函数对象、适配器、内存分配器这 6 部分构成,其中后面 4 部分是为前 2 部分服务的。

STL的组成

含义

容器

一些封装数据结构的模板类,例如 vector 向量容器、list 列表容器等。

算法

STL 提供了非常多(大约 100 个)的数据结构算法,它们都被设计成一个个的模板函数,这些算法在 std 命名空间中定义,其中大部分算法都包含在头文件 <algorithm> 中,少部分位于头文件 <numeric> 中。

迭代器

在 C++ STL 中,对容器中数据的读和写,是通过迭代器完成的,扮演着容器和算法之间的胶合剂。

函数对象

如果一个类将 () 运算符重载为成员函数,这个类就称为函数对象类,这个类的对象就是函数对象(又称仿函数)。

适配器

可以使一个类的接口(模板的参数)适配成用户指定的形式,从而让原本不能在一起工作的两个类工作在一起。值得一提的是,容器、迭代器和函数都有适配器。

内存分配器

为容器类模板提供自定义的内存申请和释放功能,由于往往只有高级用户才有改变内存分配策略的需求,因此内存分配器对于一般用户来说,并不常用。

 C++ STL头文件:

<iterator>

<functional>

<vector>

<deque>

<list>

<queue>

<stack>

<set>

<map>

<algorithm>

<numeric>

<memory>

<utility>

6.2 容器(序列、关联)

STL容器:它就是一些模板类的集合,但和普通模板类不同的是,容器中封装的是组织数据的方法(也就是数据结构)。STL 提供有 3 类标准容器,分别是序列容器、排序容器和哈希容器,其中后两类容器有时也统称为关联容器。
两大类:序列容器  关联容器

STL中的容器有:
序列容器     vector(向量容器)  list(双向链表)  deque(双端队列容器)
关联容器   set(单重集合) multiset(双重集合) map(单重映射表) multimap(多重映射表)
容器适配器   stack(栈) queue(队列) prority_queue(优先级队列)

1、顺序容器(序列式容器):每个元素都有固定位置——取决于插入时机和地点,和元素值无关,vector、deque、list;

vector : 将元素置于一个动态数组中加以管理,可以随机存取元素(用索引直接存取),
		 数组尾部添加或移除元素非常快速。但是在中部或头部安插元素比较费时;
	
List : 双向链表,不提供随机存取(按顺序走到需存取的元素,O(n)),
		在任何位置上执行插入或删除动作都非常迅速,内部只需调整一下指针;
	 
Deque : 是double-ended queue的缩写,可以随机存取元素(用索引直接存取),
	数组头部和尾部添加或移除元素都非常快速。但是在中部安插元素比较费时;

 STL 容器种类和功能

容器种类

功能

序列容器

主要包括 vector 向量容器、list 列表容器以及 deque 双端队列容器。之所以被称为序列容器,是因为元素在容器中的位置同元素的值无关,即容器不是排序的。将元素插入容器时,指定在什么位置,元素就会位于什么位置。

排序容器

包括 set 集合容器、multiset多重集合容器、map映射容器以及 multimap 多重映射容器。排序容器中的元素默认是由小到大排序好的,即便是插入元素,元素也会插入到适当位置。所以关联容器在查找时具有非常好的性能。

哈希容器

C++ 11 新加入 4 种关联式容器,分别是 unordered_set 哈希集合、unordered_multiset 哈希多重集合、unordered_map 哈希映射以及 unordered_multimap 哈希多重映射。和排序容器不同,哈希容器中的元素是未排序的,元素的位置由哈希函数确定。

STL中的容器有:
序列容器     vector(向量容器)  list(双向链表)  deque(双端队列容器)
关联容器   set(单重集合) multiset(双重集合) map(单重映射表) multimap(多重映射表)
容器适配器   stack(栈) queue(队列) prority_queue(优先级队列)

1、顺序容器(序列式容器):每个元素都有固定位置——取决于插入时机和地点,和元素值无关,vector、deque、list;

vector : 将元素置于一个动态数组中加以管理,可以随机存取元素(用索引直接存取),
		 数组尾部添加或移除元素非常快速。但是在中部或头部安插元素比较费时;
	
List : 双向链表,不提供随机存取(按顺序走到需存取的元素,O(n)),
		在任何位置上执行插入或删除动作都非常迅速,内部只需调整一下指针;
	 
Deque : 是double-ended queue的缩写,可以随机存取元素(用索引直接存取),
	数组头部和尾部添加或移除元素都非常快速。但是在中部安插元素比较费时;

总结

这里对文章进行总结:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值