天天看点

C++笔记(3)18.虚析构关于NULL在C语言中和在C++中的区别:19.接口类20.头文件21.operator重载操作符22.list类23.拷贝构造24.设计模式25.模板

这里写目录标题

  • 18.虚析构
    • 问题提出:在继承关系中构造和析构什么时候被调用?
    • 虚析构
  • 关于NULL在C语言中和在C++中的区别:
  • 19.接口类
    • 纯虚函数
    • 抽象类
    • 抽象方法
    • 抽象类的派生类的使用
    • 接口类
  • 20.头文件
    • 类的头文件
    • 类的源文件
    • 头文件&编译
    • 头文件重复包含问题
  • 21.operator重载操作符
    • operator:
      • 为什么使用重载操作符:
      • 如何重载
        • 1.将操作符重载实现为类成员函数
        • 2.将操作符重载实现为非类成员函数(即全局函数)
        • 3.特殊重载
      • 补充:
  • 22.list类
  • 23.拷贝构造
    • 什么是拷贝构造
    • 拷贝构造的规则:
      • 为什么类中指针成员变量动态分配内存时,我们需要自己写一个拷贝构造?
      • 如何解决?(如何在自定义的拷贝构造中避免此问题的发生?)
    • 哪些地方会存在拷贝构造的现象?
      • 1. 函数值传递
      • 2. list类
    • 重载“=”操作符
  • 24.设计模式
    • Template模式
      • 1.问题:
      • 2.代码实现:
      • 3.代码说明:
      • 4.注意:
    • 单例模式
      • 1.问题:
      • 2.示例代码:
      • 3.代码说明:
  • 25.模板
    • 1.函数模板
      • 问题引出:
      • 函数模板的使用
    • 2.类模板:
      • 类模板的使用:

18.虚析构

问题提出:在继承关系中构造和析构什么时候被调用?

假如当前有类

CSon

继承

CFather

  • 构造:当

    new CSon

    的时候,就会调用

    CSon()

    ,程序跳进

    CSon()

    ,在

    CSon()

    里会先调用

    CFather()

    ,然后执行

    CSon()

    中的程序,然后从

    CSon()

    中结束。
  • 析构:当

    delete

    一个

    CSon

    类型的对象的时候,程序会调用

    ~CSon()

    ,先执行

    ~CSon()

    中的程序,然后在

    ~CSon()

    退出前调用

    ~CFather()

    ,再从

    ~CSon()

    结束。

虚析构

  1. 什么是虚析构?
  • 在面向对象的编程中,给基类的析构函数加上

    virtual

    关键字,使其成为虚函数(注意:只能是析构函数,构造函数不可以是虚函数)
  1. 虚析构用来解决什么问题?
  • 申请子类对象赋值为父类指针时:

    CFather *p = new CSon();

  • 我们知道,

    new CSon()

    的时候,可以调用

    CFather()

    CSon()

    ,但是当

    delete p

    的时候,p是一个

    CFather

    类型,所以系统会直接去调用

    ~CFather()

    而没有调用

    ~CSon()

    ,这种情况 就会导致程序出错以及内存泄漏,而虚析构就是用来解决此问题的。
  1. 为什么虚析构可以解决此问题?
  • 如果

    CFather()

    的析构函数是这样的:

    virtual ~CFather()

    , 当

    delete p

    的时候,默认会调用

    CFather()

    中的析构函数,但是这时候

    vfptr

    指向的析构函数是虚函数,被子类重写,实际上就是调用的是子类的析构函数,程序跳转进入

    ~CSon()

    ,先执行

    ~CSon()

    中的内容,在退出之前调用基类(CFather)的析构函数

    ~CFather()

    ,然后从

    ~CSon()

    中退出。

所以我们可以得到虚析构的作用:通过父类指针完整的删除子类的对象,防止内存泄漏。

关于NULL在C语言中和在C++中的区别:

在stdio.h文件中这样定义:

#ifndef NULL
	#ifdef _cplusplus
		#define NULL 0
	#else
		#define NULL ((void*)0)
	#endif
#endif
           

也就是说,在C++中NULL其实就是0

在C语言中,NULL是将0强转成指针类型之后的结果

19.接口类

纯虚函数

形如

virtual void show() = 0;

的函数

抽象类

  • 包含抽象纯虚函数的类,叫做抽象类,这些抽象类是一些具体的类的基类,可以用抽象类的派生类去定义对象,但是不能使用抽象类去定义对象
  • 例如:鸟这个类就是一个抽象类,而我们所说的麻雀,鹦鹉……这些就是一些具体的类,你能刻画出麻雀等具体的一种鸟是什么样子,但是你不能刻画出鸟类是什么样子

抽象方法

  • 抽象类中的纯虚函数就叫做抽象方法,但是抽象方法在抽象类中的具体作用是确定不了的,并且无法将其实例化
  • 例如:人类都会吃饭,但是具体怎样吃,你无法确定,中国人用筷子,美国人用刀叉,印度人用手抓,视具体人种而定
  • 正是因为这个原因:在C++中对于一个虚函数,“=0”和“{}”是不能共存的,言外之意就是纯虚函数无法实例化

抽象类的派生类的使用

  • 抽象类的派生类被强制要求要对抽象类中的抽象方法进行重写,否则无法使用
  • 因为派生类可以使用基类的方法,基类如果是抽象类,前面说到过抽象方法不能在基类中进行实例化,所以在派生类中必须去重写它,才能够使用。抽象方法在基类中的定义,实际上只是提供了一种接口,告诉子类,我有这个功能,但是这个功能要如何实现,具体要做什么,就需要子类去重写实现了。

接口类

  • 一个类中的所以函数都是纯虚函数,那么这个类就是一个接口类
  • 设计抽象类的目的是为了给其他类提供一个可以继承的适当的基类
  • 面向对象的系统可能会使用一个抽象基类为所有的外部应用程序提供一个适当的、通用的、标准化的接口。然后,派生类通过继承抽象基类,就把所有类的操作都继承下来
  • 外部应用程序提供的功能(即共有函数)在抽象类基类中是以春旭函数的形式存在的。这些纯虚函数在相应的派生类中被实现。这个架构是的新的应用程序可以很容易地被添加到系统中,即使是在系统被定义之后依然可以如此

20.头文件

我们知道,一个项目通常都是由多个文件构成,通常在我们写程序的时候,都是将声明部分和实现不用分分开,一般在头文件中进行声明,在源文件中实现,这样更加符合规范

类的头文件

  • 将类中的成员方法的实现部分去掉,就是类的声明,声明部分放在头文件中,例如:
#pragma once

class CPerson
{
public:
	int a;  //普通变量
	static int b;  //静态变量
	const int c;  //常量
	int* p;  //指针变量
public:
	CPerson(void);  //构造函数
	~CPerson(void);  //析构函数
public:
	void AA();  //普通函数
	static void BB();  //静态函数
	void CC() const;  //常函数
	virtual void DD();  //虚函数
	virtual void EE() = 0;  //纯虚函数(接口函数)
}
           

类的源文件

方法的实现在源文件中进行,需要注意的是:

(1)在源文件中实现的时候要加类名作用域

(2)虚函数、静态函数在源文件中实现的时候需要将标识符去掉

(3)常函数的修饰符const必须保留,因为它表明这个函数的this指针是个常量

(4)纯虚函数不需要实现,也无法实现

例如:

#include "Person.h"
#include <iostream>
using namespace std;

int CPerson::b = 200;  //静态变量可以直接加类名作用域赋值

CPerson::CPerson(void):c(300)  //构造函数
{
	a = 100;
	p = new int;
}

CPerson::~CPerson(void)  //析构函数
{
	delete  p;
	p = 0;
}

void CPerson::AA()  //普通函数
{

}

void CPerson::BB()  //静态函数去掉static标识符
{

}

void CPerson::CC() const  //常函数const标识符保留
{

}

void CPerson::DD()  //虚函数去掉virtual标识符
{

}
           

头文件&编译

  • 编译的时候是跳过头文件的,也就是说,建立一个未被包含的头文件,在里面无论写什么,编译的时候都不会报错
  • 但是我们一般创建头文件的目的就是为了被包含并且使用的,否则就没有意义,如果源文件中包含了该头文件,就相当于将头文件中的内容拷贝到了包含的位置,那么在编译源文件的时候,如果头文件中有错误,编译器这时候就会报错了(编译源文件时报错)

头文件重复包含问题

当各个文件之间头文件引用关系复杂的时候,可能就会出现头文件重复包含的问题,这种情况下编译就会报错。

如何解决?

  • 使用ifndef,他的作用时判断这个宏是否被定义过,如果定义了就跳过#ifndef和#endif之间的内容
#ifndef _AA_H
	#define _AA_H
		//头文件内容
	#endif
           
  • #pragma once,在C++中还提供的一种解决方法时,可以在头文件中写#pragma once,他的意思时该头文件只编译一次

21.operator重载操作符

operator:

operator是C++的一个关键字,它和运算符(如=)一起使用,表示一个运算符重载函数,在理解时可以将operator和运算符(如operator=)视为一个函数名。

使用operator重载运算符,是C++扩展运算符功能的方法。

为什么使用重载操作符:

  • 对于C++提供的所有操作符,通常只支持对于基本数据类型和标准库中提供的类的操作,而对于用户自己定义的类,如果想要通过该操作符实现一些基本操作(比如比较大小,判断是否相等),就需要用户自己来定义这个操作符的具体实现了。
  • 比如,我们要设计一个名为“person”的类,现在要判断person类的的两个对象p1和p2是否一样大,我们设计的比较规则是每个对象中的

    int age

    这一属性去比较,那么,在设计person类的时候,就可以通过对操作符

    ==

    进行重载,来使用操作符

    ==

    对对象p1和p2进行比较了。
  • 我们上面所说的对操作符

    ==

    进行重载。说是“重载”,是由于编译器在实现操作符“==”的功能的时候,已经为我们提供了这个操作符对于一些基本数据类型的操作支持,只不过由于现在该操作符所操作的内容变成了我们自定义的数据类型(如class),而默认情况下,该操作符是不能对我们自定义的class类进行操作的,所以就需要我们通过重载该操作符,该出该操作符操作我们自定义的class类型的方法,从而达到使用该操作符对我们自定义的class类进行运算的目的。

如何重载

1.将操作符重载实现为类成员函数

例如:

class CPerson
{
public:
	int age;
public:
	CPerson():age(18)
	{}
public:
	bool operator==(CPerson& p2)
	{
		if(this->age == p2.age)
			return true;
		return false;
	}
};

int main()
{
	CPerson p1,p2;
	int result = 1 + (p1==p2);
	cout << result << endl;
	system("pause");
}
           

结果输出为2,可以看到:

  • 传入的参数作为“==”的右操作符,自身作为左操作符
  • 返回值还可以参与其他运算符的运算

2.将操作符重载实现为非类成员函数(即全局函数)

当我们要重载的操作符的左操作数不是自身类型,那么类成员就无法实现了,就需要使用全局函数

例如:

class CPerson
{
public:
	int age;
public:
	CPerson():age(18){}
public:
	int operator+(CPerson& p2)
	{
		return this->age + p2.age;
	}
	
	int operator+(int b)
	{
		return this->age + b;
	}
};

int operator+(int a,CPerson& p2)
{
	return a + p2.age;
}

int main()
{
	CPerson p1,p2;

	cout << p1 + p2 << endl;
	cout << p1 + 2 << endl;
	cout << 2 + p2 << endl;
	system("pause");
}
           

输出结果为:

C++笔记(3)18.虚析构关于NULL在C语言中和在C++中的区别:19.接口类20.头文件21.operator重载操作符22.list类23.拷贝构造24.设计模式25.模板

可以看到:

  • 他需要两个参数,一个符号左边的,一个符号右边的
  • 这里我们用这种方式,实现了class + class,class + int,int + class

注意:

(1)重载运算符的优先级不会改变

(2)重载操作符要有返回值,返回值继续和其他的操作符去结合,去运算

3.特殊重载

(1)重载 “<<” 和 “>>”

  • 目的:

cout << 右边是一个CPerson类型的对象的时候,输出CPerson类中的age的值

cin >> 右边是一个CPerson类型的对象的时候,输出CPerson类中的age的值

  • 注意:cout是输出流对象,它的类型是ostream

    cin是输入流的对象,它的类型是istream

  • 实现
class CPerson
{
public:
	int age;
public:
	CPerson(){age = 18;}
};

ostream& operator<<(ostream& os,CPerson& p)
{
	os << p.age;
	return os;
}

istream& operator>>(istream& is,CPerson& p)
{
	is >> p.age;
	return is;
}

int main()
{
	CPerson p1;
	cin >> p1;
	cout << p1 << " " << p1 << endl;
	system("pause");
}
           

运行结果:

C++笔记(3)18.虚析构关于NULL在C语言中和在C++中的区别:19.接口类20.头文件21.operator重载操作符22.list类23.拷贝构造24.设计模式25.模板

(2)重载 ++ 操作符

  • 目的:

Person p;

可以通过p++和++p来让类中的age分别以age++和++age的方式自加

  • 注意:这种单目运算符作为类内成员函数或者作为全局函数实现都可以。

    a++和++a的区别:

    a++需要一个临时变量来存储原来的值,用来返回。

    ++a不需要临时变量,直接a=a+1,然后返回。

实现:

class CPerson
{
public:
	int age;
public:
	CPerson(){age = 10;}
public:
	int operator++() //不带参数的是++a
	{
		age = age + 1;
		return age;
	}
	
	int operator++(int temp)  //带参数的是a++
	{
		//传入的参数不一定要用,只是用来区分a++还是++a,也可以自己定义一个临时变量
		int f1 = age;
		age = age +1;
		return f1;
	}
};

int main()
{
	CPerson p1;
	cout << p1++ << endl;
	cout << ++p1 << endl;
}
           

运行结果:

C++笔记(3)18.虚析构关于NULL在C语言中和在C++中的区别:19.接口类20.头文件21.operator重载操作符22.list类23.拷贝构造24.设计模式25.模板

补充:

这里是后续在使用过程中发现的一些问题:

  1. 有继承关系的两个类之间可以通过虚函数的方法使得derived class重载base class的operator使其具有多态特性。
  2. operator只能对于有自定义的类型参与的系统没有规定的操作符进行重载,经过实验,有以下几种自定义类型可以使用operator,他们是class,struct,enum这三种类型,值得注意的是,才传入对象的时候使用引用传参,而不是指针。

22.list类

说明

该类作为一个容器,是一个用来实现链表功能的类

头文件:

list

用法:

  1. 创建一个装int类型的链表
  1. 尾添加节点:
  1. 头添加节点:
  1. 头删除:
  1. 尾删除:
  1. 链表头:
  1. 链表尾:
  1. 定义一个迭代器:

    每一种容器都会有配套的迭代器。这里是链表类list的迭代器定义

  1. 遍历链表:

    (1)使用迭代器遍历

list<int*>::iterator ite = lst.begin();
while(ite != lst.end())
{
	cout << *ite << end;
	++ite;
}
           

(2)使用for_each函数

for_each()函数:

  • 功能:将指定的函数对象按前向顺序应用到范围内的每个元素,并返回该函数对象。
  • 头文件:
algorithm
  • 原型:
template<class InputIterator, class Function> 
   Function for_each( 
      InputIterator _First,  
      InputIterator _Last,  
      Function _Func 
   );
           
  • 参数:

_First 输入迭代器,寻址要操作的范围内第一个元素的位置。

_Last 输入迭代器,寻址操作范围内最后一个元素后1的位置。

_Func 用户定义的函数对象,应用于范围中的每个元素。

  • 返回值:
函数对象被应用到范围内所有元素后的一个副本。(不用去关心)

所以我们可以这样写,去遍历一个list链表类

::for_each(lst.begin(),lst.end(),&show);
//自定义的show函数
void  show(int nVal) //参数类型取决于list<>尖括号中是什么类型
{
	cout << nVal << " ";
}
           
  1. 查找:

    find()函数:

  • 功能:定位具有指定值的范围中某个元素的第一个匹配项的位置。
  • 头文件:
algorithm
  • 原型:
template<class InputIterator, class Type> 
   InputIterator find( 
      InputIterator _First,  
      InputIterator _Last,  
      const Type& _Val
   );
           
  • 参数:

_First 输入迭代器,寻址要操作的范围内第一个元素的位置。

_Last 输入迭代器,寻址操作范围内最后一个元素后1的位置。

_Val 要搜索的值

  • 返回值:
一个输入迭代器,用于寻址正在搜索范围内指定值的第一个匹配项。如果范围内不存在这样的值,则迭代器返回的地址将指向范围内最后一个位置,即在最后一个元素的下一位置。

所以我们可以这样去写:

  1. 指定位置插入:

    insert()函数:

示例:

  1. 指定位置删除:

    erase()函数:

示例:

23.拷贝构造

什么是拷贝构造

拷贝构造函数是一种特殊的构造函数,它在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象。拷贝构造函数通常用于:

  • 通过使用另一个同类型的对象来初始化新创建的对象。
  • 复制对象把它作为参数传递给函数。
  • 复制对象,并从函数返回这个对象。

例子:

class CPerson
{
public:
	int age;
public:
	CPerson()
	{
		age = 5;
	}
};

int main()
{
	CPerson p1;
	cout << p1.age << endl;
	p1.age = 10;
	CPerson p2(p1);
	cout << p1.age << endl;
	cout << p2.age << endl;

	system("pause");
}
           

运行结果:

C++笔记(3)18.虚析构关于NULL在C语言中和在C++中的区别:19.接口类20.头文件21.operator重载操作符22.list类23.拷贝构造24.设计模式25.模板

上面的例子中,可以看到,我们定义了一个p1,它执行默认正常的构造函数,给age赋值为10,定义p2的时候我们传入的参数是一同类型对象,也是允许的,可以正常运行而没有报错。

拷贝构造的规则:

拷贝构造的规则是这样的:如果在类中没有定义拷贝构造函数,编译器会自行定义一个。如果类带有指针变量,并有动态内存分配,则它必须有一个拷贝构造函数。拷贝构造函数的最常见形式如下:

classname (const classname &obj) {
   // 构造函数的主体
}
           

注意:在这里,==obj 是一个对象引用,该对象是用于初始化另一个对象的。==可以传入多个值,但是第一个值必须为当前这个类的const类型的引用。

为什么类中指针成员变量动态分配内存时,我们需要自己写一个拷贝构造?

看下面例子:

class CPerson
{
public:
	int* value;
public:
	CPerson()
	{
		value = new int(200);
	}
	~CPerson()
	{
		delete value;
		value = NULL;
	}
};

int main()
{
	{
	CPerson p1;
	CPerson p2(p1);
	}
}
           

这个例子在运行时,会发生如下错误:

C++笔记(3)18.虚析构关于NULL在C语言中和在C++中的区别:19.接口类20.头文件21.operator重载操作符22.list类23.拷贝构造24.设计模式25.模板

实际上,编译器自行定义的拷贝构造是这个样子的:

CPerson(const CPerson& p)
{
	this->value = p.value;
}
           

它实际上是将传入的对象的各个成员变量的值通过直接赋值的方式来初始化将要定义的变量来完成构造。

实际上我们将这种拷贝构造的方法叫做浅拷贝。

那么我们可以分析以下上面的代码:

  • 我们定义了

    CPerson p1

    ,他执行普通构造函数,在内存中申请一块空间(假设地址为0x3f)存放了一个200的值,然后

    p1.value

    指向那块空间,即

    p1.value

    中存的数据是

    0x3f

  • 接下来我们通过拷贝构造的方式定义了

    CPerson p2(p1)

    ,然后默认的拷贝构造函数通过赋值的方式初始化了

    p2.value

    ,即

    p2.value = p1.value

    ,现在

    p2.value

    中存到也是

    0x3f

    了,也就是说,

    p1.value

    p2.value

    指向了同一块内存空间
  • 之后当两个对象生命周期结束的时候,p1的析构函数被调用,将

    p1.value

    指向的内存空间释放了,当p2的析构函数被调用,就会再去释放一次该内存空间,这种操作了不属于自己的内存的行为是不被允许的。

如何解决?(如何在自定义的拷贝构造中避免此问题的发生?)

有浅拷贝,那么对应的就有深拷贝

我们将上面拷贝构造这样去写:

CPerson(const CPerson& p)
{
	this->value = new int(*(p.value));
}
           

再去运行就不会出现之前那种错误了。

可以看到我们在这个拷贝构造里做了这样的事情:给我们将要定义的变量申请一块内存空间,这块内存使用传入的变量的

p.value

所指内存空间中的值去初始化。这时候在主函数里面执行

CPerson p2(p1)

之后,p1.value 和 p2.value各自有一块内存空间,各指各的,最后调用析构函数的时候也是各自释放各自的那块内存。

像这种拷贝我们称之为深构造,我们不只是简单地赋值了指针变量中装的值,而是复制出一块一样的内存空间。

哪些地方会存在拷贝构造的现象?

1. 函数值传递

比如我们定义了一个CPerson类(代码略)

我们再去写一个函数:

void Func(CPerson per)
{
}
           

这个函数使用的是值传递,传入一个Person类型的对象,在实参到形参赋值的这个过程中其实就执行的是一个拷贝构造,那么如果我们写的CPerson类中没有去写一个深拷贝的话,就有可能会出现上述错误。

当然这种情况我们实际上是有两种方法去解决的:

(1)在CPerson类中写一个深拷贝,需要注意的是这种情况下,函数里面的对象和函数外面的对象不是一个对象

(2)函数使用引用传递的方式去传参,这个时候才函数里和函数外实际上操作的是同一个对象,没有新的对象产生,自然不会产生拷贝构造了。

2. list类

假如我们定义一个CPerson类型的链表,这样去写:

这里也会出现一个浅拷贝的问题,所以尽量避免这样去写,而是用

// 。。。。。。。。待补充完善。。。。。。

重载“=”操作符

我们去看这样一个代码:

class CPerson
{
public:
	int age;
};

int main()
{
	CPerson p1;
	p1.age = 10;
	CPerson p2;
	p2 = p1;
	cout << p1.age << " " << p2.age << endl;

	system("pause");
}
           

运行结果:

C++笔记(3)18.虚析构关于NULL在C语言中和在C++中的区别:19.接口类20.头文件21.operator重载操作符22.list类23.拷贝构造24.设计模式25.模板

这里我们并没有去重载“=”操作符,但是两个类之间使用“=”操作符确实可以的,说明系统默认对类之间的“=”操作符进行了重载,但是需要注意的是,这种重载还是一种浅拷贝

class CPerson
{
public:
	int* m;
public:
	CPerson()
	{
		m = new int(100);
	}
	~CPerson()
	{
		delete m;
		m = NULL;
	}
};

int main()
{
	{
		CPerson p1;
		CPerson p2;
		p2 = p1;
	}	
}
           

还是这种当类中有指针变量并且动态分配内存的时候,这样去使用“=”操作符,就会报错。

那么解决办法就是自己去重新重载“=”操作符就好了,如下:

class CPerson
{
public:
	int* m;
public:
	CPerson()
	{
		m = new int(100);
	}
	~CPerson()
	{
		delete m;
		m = NULL;
	}

public:
	CPerson& operator=(const CPerson& p)
	{
		//删除原来执行构造函数时为this->m申请的空间
		delete this->m;
		this->m = NULL;
		//为this->m申请新的空间并用*(p.m)的值初始化
		this->m = new int(*(p.m));
		return (*this);
	}
};

int main()
{
	{
		CPerson p1;
		CPerson p2;
		p2 = p1;
	}	
}
           

如此便可避免出错。

所以我们可以总结一下:一个空类当中,默认函数有:构造函数,析构函数,拷贝构造,重载 等号 操作符

24.设计模式

设计模式:处理

Template模式

1.问题:

假设有这样的场景,某一个功能在不同的类中大体要执行的操作是一样的,只有一些细节的差异,最简单的来说,有100行代码,这100行代码在两个类当中都需要执行,但是其中99行都是相同的,只有中间的一行代码不同,如果我们在每个类中定义一个方法,将这100行代码逐一写进去,这样做显得有点傻,所以我们想要更加高效的方法,Template模式就是去解决这样的问题的。

2.代码实现:

#include <iostream>
using namespace std;

class CPerson
{
public:
	CPerson(){};
	virtual ~CPerson(){};
	void Speak()
	{
		cout << "use ";
		this->Use_Language();
		cout << " speaking" << endl;
	}
protected:
	virtual void Use_Language(){}
};

class CChinese : public CPerson
{
public:
	CChinese(){};
	~CChinese(){};
protected:
	void Use_Language()
	{
		cout << "Chinese";
	}
};

class CAmerican : public CPerson
{
public:
	CAmerican(){};
	~CAmerican(){};
protected:
	void Use_Language()
	{
		cout << "English";
	}
};

int main()
{
	CPerson* p1 = new CChinese();
	CPerson* p2 = new CAmerican();
	p1->Speak();
	p2->Speak();

	system("pause");
	return 0;
}
           

3.代码说明:

  • 这里采用一个例子:美国人和中国人都是人这个类的一个派生类,他们都能够讲话,讲话这个功能(算法)大体框架是一样的,但是存在细节上的不同,美国人讲英语,中国人讲汉语,所以我们将那一点不同的细节封装起来提供一个接口(由子类去实现这一细节),让说话这个功能(算法)去调用,这样一来,人类讲话这个功能就相当于执行了相同的操作,在人这个类中去实现即可。

4.注意:

需要注意的是,我们将原语操作(细节算法)定义为保护(Protected)成员,只供模板方法调用(子类可以),说人话就是防止了代码中的

Use_Language()

被用户在

main

函数中调用,你只能去调用

Speak()

Speak()

去调用

Use_Language()

单例模式

1.问题:

单例模式用于解决在一个项目中某一种类型的类只允许申请一个对象的需求。

2.示例代码:

#include <iostream>
using namespace std;

class CSingleton
{
private:
	static bool bflag;
private:
	CSingleton()
	{}
	~CSingleton()
	{}
	CSingleton(const CSingleton& p)
	{}
public:
	static CSingleton* GetSingleton()
	{
		if(bflag == false)
		{
			bflag = true;
			return new CSingleton();
		}
		else
		{
			return NULL;
		}
	}
	void DestroySingleton(CSingleton* p)
	{
		bflag = false;
		delete p;
		p = 0;
	}

};

bool CSingleton::bflag = false;

int main()
{
	CSingleton* p = CSingleton::GetSingleton();
	p->CSingleton::DestroySingleton(p);
	p = NULL;
	CSingleton* p2 = CSingleton::GetSingleton();

	return 0;
}
           

3.代码说明:

  1. 构造函数私有化:我们不想用户在主函数或者其他地方随意调用构造函数去定义CSingleton对象,所以将其私有化
  2. GetSingleton()函数:我们只允许申请一个CSingleton类型的对象,并不是彻底不让申请该类型的对象,所以我们应该去提供一个接口供用户使用,但是需要加特殊的限制
  3. bflag变量:正如第2条所说,改变量用来标记,是否已经有一个CSingleton类型的对象存在了,如果没有(bflag为false)就可以通过接口去定义一个,如果有(bflag为true),接口就返回NULL,不会再申请第二个CSingleton类型变量了
  4. 析构函数私有化:如果我们创建了一个CSingleton类型对象,如果不对析构函数加以限制,用户就可以随意调用析构函数,将该对象销毁,并且我们无法再定义第二个,防止此现象,需要将析构函数私有化
  5. DestroySingleton()函数:既然构造函数被私有化,外界就无法去销毁已经定义了的CSingleton类型的对象,这是不合理的,所以我们应该提供一个销毁该对象的接口,并且在销毁之前要将bflag赋值为false,表示程序中唯一存在的一个CSingleton类型的对象已经被销毁,允许重新定义一个
  6. 拷贝构造私有化:别忘了,一个空类中默认的函数有构造函数,析构函数,拷贝构造,重载等号操作符,其中重载等号操作符不需要去考虑,因为不会有第二个相同类型变量,所以不会用到该操作。但是我们需要考虑拷贝构造,如果我们不去加以限制,那么用户就可以通过现有的一个对象去拷贝构造出新的CSingleton类型的对象了,这是不允许的。

25.模板

1.函数模板

问题引出:

有一系列函数,如下:

void Show(int c)
{
	cout << c << endl;
}
void Show(char c)
{
	cout << c << endl;
}
void Show(double c)
{
	cout << c << endl;
}
void Show(char* c)
{
	cout << c << endl;
}
           

它有如下特点:

  1. 函数名相同
  2. 传入参数不同
  3. 返回值相同
  4. 函数体执行的功能相同

我们可以通过函数重载的方式是实现以上功能,但是代码重复的部分太多,不够简洁

所以提供了另一种方法,叫做函数模板

函数模板的使用

1. 定义函数模板:

//定义一个
template <typename 类型名>
void Show(类型名 变量名)
{
	//函数体
}
//定义多个
template <typename 类型名1,typename 类型名2>
void Show(类型名 变量名)
{
	//函数体
}
           

这里的

类型名

可以当作任意一种数据类型,视传入的具体数据类型而定,比如上面问题中的示例代码现在就可以使用如下代码代替了:

template<typename AB>
void Show(AB c)
{
	cout << c << endl;
}
           

Show函数就可以实现传入任意类型,并且实现输出操作了。

2.注意:

  • template语句只能放在函数定义的上面一行,中间不能有其他代码
  • 函数模板在调用函数的时候确定参数的类型

    3.使用函数模板改写冒泡排序:

    在之前写的冒泡排序算法中,我们只是对int类型进行了排序,现在我们学习了函数模板,我们要求要能够对任意类型进行排序,实现如下:

#include <iostream>
using namespace std;

template<typename AA>
void BubbleSort(AA arr[],int n)
{
	int i,j;
	for(i = 0;i < n-1;i++)
	{
		for(j = 0;j < (n-1)-i;j++ )
		{
			if(arr[j] < arr[j+1])
			{
				AA temp = arr[j];
				arr[j] = arr[j+1];
				arr[j+1] = temp;
			}
		}
	}
}

int main(int agec,char argv[])
{
	char arr[] = {'a','c','r','g','h','s','t'};
	BubbleSort(arr,sizeof(arr)/sizeof(char));
	for(char nVal:arr)
	{
		cout << nVal << " ";
	}

	system("pause");
	return 0;
}
           

运行结果:

C++笔记(3)18.虚析构关于NULL在C语言中和在C++中的区别:19.接口类20.头文件21.operator重载操作符22.list类23.拷贝构造24.设计模式25.模板

以上程序可以在不修改冒泡排序算法的前提下,实现任意类型从大到小的一个排序(前提是这种类型支持比较运算)

现在提高一下要求:要求当输入int时,从大到小排序,输入double时,从小到大排序

代码实现如下:

#include <iostream>
using namespace std;

bool Rule1(int a,int b)
{
	return a < b;
}

bool Rule2(double a,double b)
{
	return a > b;
}

template<typename AA>
void BubbleSort(AA arr[],int n,bool (*pFun)(AA a,AA b))
{
	int i,j;
	for(i = 0;i < n-1;i++)
	{
		for(j = 0;j < (n-1)-i;j++ )
		{
			if((*pFun)(arr[j],arr[j+1]))
			{
				AA temp = arr[j];
				arr[j] = arr[j+1];
				arr[j+1] = temp;
			}
		}
	}
}

template<typename BB>
void Print(BB arr[],int n)
{
	for(int i=0;i<n;i++)
	{
		cout << arr[i] << " ";
	}
	cout << endl;
}

int main()
{
	int arr1[7] = {1,5,8,7,2,8,98};
	double arr2[4] = {2.3,4.5,89.5,2};

	BubbleSort(arr1,7,Rule1);
	Print(arr1,7);

	BubbleSort(arr2,4,Rule2);
	Print(arr2,4);

	system("pause"); 
	return 0;
}
           

通过以上的冒泡排序程序,我们可以做到不去修改冒泡排序函数的前提下,去实现各种数据类型按照自定义的规则进行冒泡排序操作。

2.类模板:

类模板的使用:

1.定义一个类模板:

同样的我们在类的上面去加

template <typename AA>

这样的语句,在类内就可以使用AA 去定义变量,并且这个变量的类型可以在定义类的时候任意给定,值得注意的是我们在定义类的时候,应该这样去写

类名<要替换AA的数据类型> 对象

示例代码如下:

template <typename AA>
class CPerson
{
public:
	AA a;
public:
	CPerson(AA a)
	{
		this->a = a; //使用传入的值去初始化变量a
	}
	void Show()
	{
		cout << a << endl;
	}
	
};

int main()
{
	CPerson<int> per(100); 
	per.a = 89;
	per.Show();
	return 0;
}
           

2.类模板的应用:自己实现list容器

#include <iostream>
using namespace std;

template <typename AA>
class CList
{
private:
	struct Node{
		AA value;
		Node *pNext;
	};

	Node* pHead;
	Node* pEnd;
	int m_nLen;
public:
	CList()
	{
		pHead = NULL;
		pEnd = NULL;
		m_nLen = 0;
	}
	~CList()
	{
		Node* pDel;
		while(pHead)
		{
			pDel = pHead;
			pHead = pHead->pNext;
			delete pDel;
			pDel = NULL;
		}
	}
public:
	void push_back(int n)
	{
		Node* temp = new Node;
		temp->value = n;
		temp->pNext = NULL;
		if(pHead == NULL)
		{
			pHead = temp;
			pEnd = temp;
		}
		else
		{
			pEnd->pNext = temp;
			pEnd = temp;
		}
		m_nLen++;
	}

	void push_front(int n)
	{
		Node* temp = new Node;
		temp->value = n;
		if(pHead == NULL)
		{
			pHead = temp;
			pEnd = temp;
		}
		else
		{
			temp->pNext = pHead;
			pHead = temp;
		}
		m_nLen++;
	}

	void delete_node(int n)
	{
		if(pHead->value == n)
		{
			Node* pDel = pHead;
			pHead = pHead->pNext;
			delete pDel;
			pDel = NULL;
		}
		else if(pEnd->value == n)
		{
			Node* p = pHead;
			while(p->pNext != pEnd)
			{
				p = p->pNext;
			}
			pEnd = p;
			pEnd->pNext = NULL;
			p = p->pNext;
			delete p;
			p = NULL;
		}
		else
		{
			Node* p = pHead;
			while(p != pEnd)
			{
				if(p->pNext->value == n)
				{
					Node* pDel = p->pNext;
					p->pNext = p->pNext->pNext;
					delete pDel;
					pDel = NULL;
				}
				p = p->pNext;
			}
		}
		m_nLen--;
	}

	void show()
	{
		Node* p = pHead;
		while(p != NULL)
		{
			cout << p->value << " ";
			p = p->pNext;
		}
		cout << "size:" << m_nLen;
		cout << endl;
	}
};

int main()
{
	CList<char> lst;

	lst.push_back('a');
	lst.push_back('b');
	lst.push_back('c');
	lst.push_back('3');
	lst.push_back('2');
	lst.show();
	lst.push_front('u');
	lst.show();
	lst.delete_node('u');
	lst.show();
	lst.delete_node('2');
	lst.show();
	lst.delete_node('b');
	lst.show();

	system("pause");
	return 0;
}