[学习笔记] 3. C++ / CPP提高

Posted Le0v1n

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[学习笔记] 3. C++ / CPP提高相关的知识,希望对你有一定的参考价值。

本阶段主要针对C++泛型编程和STL技术做详细讲解,探讨C++更深层的使用。

[学习笔记] 3. C++ / CPP提高

STL(Standard Template Library,标准模板库)是 C++ 标准库的一部分,也是 C++ 中非常重要的一个技术。它是由 Alexander Stepanov 和 Meng Lee 于 1994 年开发的,旨在提供一组通用的、高效的数据结构和算法,能够满足大多数程序的需求。

STL 技术的核心是模板(template)和泛型编程(generic programming)。模板是一种 C++ 语言特性,可以让程序员编写通用的代码,而不用为不同的类型分别编写不同的代码。泛型编程则是一种编程理念,强调代码的通用性和复用性,通过使用模板来实现。

STL 中提供了多种容器(container)和算法(algorithm),容器是一种用于存储数据的对象,包括 vectorlistsetmap 等;算法则是一些用于对容器中的数据进行操作的函数,包括排序、查找、遍历等。

此外,STL 还提供了迭代器(iterator)、函数对象(function object)和适配器(adapter)等辅助组件,帮助程序员更方便地使用容器和算法。

STL 技术的优点包括:

  • 代码通用且重用性高,可以大大提高开发效率;
  • STL 中的容器和算法都经过了充分测试和优化,具有高效性和可靠性;
  • STL 技术为 C++ 中的面向对象编程和泛型编程提供了支持,使得程序设计更加灵活和可扩展。

因此,STL 技术已经成为了 C++ 程序员必备的技能之一,广泛应用于各种领域的软件开发中。

1. 模板

1.1 模板的概念

在 C++ 中,模板(template)是一种通用的代码结构,可以用于生成特定类型或值的代码。通过模板,程序员可以编写一次代码,然后使用不同的数据类型或值来实例化该模板,生成不同的代码实例。这种方式被称为泛型编程(generic programming),它可以提高代码的可重用性和通用性。

generic: 英[dʒəˈnerɪk] 美[dʒəˈnerɪk]
adj. 通用的; 一般的; 普通的; 无厂家商标的; 无商标的;

模板就是建立通用的模具,大大提高复用性。

例如生活中的模板:一寸照片模板、PPT模板。

模板的特点:

  • 模板不可以直接使用,它只是一个框架
  • 模板的通用并不是万能的

1.2 函数模板

  • C++另一种编程思想称为泛型编程(generic programming),主要利用的技术就是模板。
  • C++提供两种模板机制:①函数模板和②类模板

1.2.1 函数模板语法

函数模板作用:建立一个通用函数,其函数返回值类型形参类型可以不具体制定,用一个虚拟的类型来代表。

语法:

template<typename T>
函数的声明或定义

解释:

  • template —— 声明创建模板
  • typename —— 表面其后面的符号是一种数据类型,可以用class代替
  • T —— 通用的数据类型,名称可以替换,通常为大写字母T(Template)。

可以换别的名称,但一般用T比较清晰明了

总结:

  • 函数模板利用关键字template
  • 使用函数模板有两种方式:
    1. 自动类型推导:函数名() —— swap_fn(a, b);
    2. 显示指定类型:函数名<指定数据类型>() —— swap_fn<int>(a, b);
  • 模板的目的是为了提高复用性,将类型参数化
#include <iostream>
using namespace std;


// 函数模板


// [传统方法]交互两个整型函数
void swap_int(int& a, int& b) 
	int tmp = a;
	a = b;
	b = tmp;



// [传统方法]交换两个浮点型函数
void swap_double(double& a, double& b) 
	double tmp = a;
	a = b;
	b = tmp;



void test01_01() 
	int a = 10;
	int b = 20;
	swap_int(a, b);
	cout << "a: " << a << "\\tb: " << b << endl;  // a: 20   b: 10

	double c = 1.1;
	double d = 2.2;
	swap_double(c, d);
	cout << "c: " << c << "\\td: " << d << endl;  // c: 2.2  d: 1.1



/*
	我们可以发现,如果我们想把所有的数据类型都写完,要写好久。
	如果还有自定义数据类型,那就需要随时再写交换函数,这样太麻烦了。

	我们看上面写的两个交互代码,其实可以发现,两个函数很像,不同点:
		1. 函数名不同
		2. 参数的数据类型不一样
	剩下的都一样,所以我们使用模板来实现各种数据类型的交换。
*/


// 函数模板
template<typename T>  // 声明一个模板,告诉编译器,后面代码中紧跟着的T不要报错。T是一个通用的数据类型
void swap_fn(T& a, T& b) 
	T tmp = a;
	a = b;
	b = tmp;



void test01_02() 
	/*
		利用函数模板实现交换,有两种使用方式:
			1. 自动类型推导:函数名()
			2. 显示指定类型:函数名<指定数据类型>()
	*/
	int a = 10;
	int b = 20;

	// 1. 自动类型推导
	swap_fn(a, b);
	cout << "a: " << a << "\\tb: " << b << endl;  // a: 20   b: 10

	// 2. 显示指定类型
	swap_fn<int>(a, b);  // <T>直接指明T的数据类型
	cout << "a: " << a << "\\tb: " << b << endl;  // a: 10   b: 20



int main() 

	test01_01();

	test01_02();

	system("pause");
	return 0;

1.2.2 函数模板注意事项

注意事项:

  • 自动类型推导,必须推导出一致的数据类型T,才可以使用
  • 模板必须要确定出T的数据类型,才可以使用

示例:

#include <iostream>
using namespace std;


/*
	函数模板注意事项:
		1. 自动类型推导,必须推导出一致的数据类型T才可以使用
		2. 模板必须要确定出T的数据类型,才可以使用
*/


template<typename T>  // typename可以替换成class(效果是一样的)
void swap_02(T& a, T& b) 
	T tmp = a;
	a = b;
	b = tmp;



void test02_01() 
	int a = 10;
	int b = 20;
	float c = 1.1f;

	// 1. 自动类型推导,必须推导出一致的数据类型T才可以使用

	swap_02(a, b);  // a和b的数据类型一致,没问题

	// swap_02(a, c);  // Error: 没有与参数列表匹配的函数模板"swap_02"实例
	// 推导不出一致的数据类型


// 2. 模板必须要确定出T的数据类型,才可以使用
template<class T>
void func() 
	cout << "func函数的调用" << endl;


void test02_02() 
	// 2. 模板必须要确定出T的数据类型,才可以使用

	// func();  // 没有与参数列表匹配的函数模板"func"实例

	// 这种情况下,要想成功调用,只能在调用的时候指定数据类型
	func<int>();  // 随便给它一个数据类型就可以正常调用了



int main() 

	test02_01();

	test02_02();

	system("pause");
	return 0;

总结:使用模板时必须确定出通用数据类型T,并且能够推导出一致的类型。

1.2. 3函数模板案例

案例描述:

利用函数模板封装一个排序的函数,可以对不同数据类型数组进行排序。排序规则从大到小,排序算法为选择排序。

分别利用char数组和int数组进行测试。

示例:

#include <iostream>
using namespace std;


/*
	实现通用 对数组进行排序的函数
	规则:从大到小
	算法:选择排序
	测试:char数组、int数组
*/


// 交换函数模板
template<class T>
void swap_two_element(T& a, T& b) 
	T tmp = a;
	a = b;
	b = tmp;



// 利用模板写排序算法
template<class T>
void select_sort(T arr[], int len) 
	/*
	* arr: 数组
	* len: 数组的长度
	*/
	
	for (int i = 0; i < len; i++)
	
		int max_idx = i;  // 最大值的idx
		for (int j = i + 1; j < len; j++)
		
			if (arr[max_idx] < arr[j])
			
				// 更新最大值下标
				max_idx = j;
			
		
		if (max_idx != i)
		
			// 交换元素
			swap_two_element(arr[max_idx], arr[i]);
		
	



// 打印数组的模板
template<class T>
void print_array(T arr[], int len) 
	for (int i = 0; i < len; i++)
	
		cout << arr[i] << " ";
	
	cout << endl;



void test03_01() 
	// 测试:char数组
	char char_arr[] = "badcfe";

	int len = sizeof(char_arr) / sizeof(char);
	select_sort(char_arr, len);
	print_array(char_arr, len);  // f e d c b a



void test03_02() 
	// 测试:int数组
	int int_arr[] =  7, 5, 1, 3, 9, 2, 4, 6, 8 ;
	int len = sizeof(int_arr) / sizeof(int);

	select_sort(int_arr, len);
	print_array(int_arr, len);  // 9 8 7 6 5 4 3 2 1



int main() 

	test03_01();
	test03_02();

	system("pause");
	return 0;

1.2.4 普通函数与函数模板的区别

普通函数与函数模板区别:

  • 普通函数调用时可以发生自动类型转换(隐式类型转换)
  • 函数模板调用时,如果利用自动类型推导,不会发生隐式类型转换
  • 如果利用显示指定类型的方式,可以发生隐式类型转换

隐式类型转换是指在表达式中自动地将一种类型转换为另一种类型,而无需显式地使用类型转换运算符。例如,将整数类型转换为浮点数类型,或将字符类型转换为整数类型。隐式类型转换可以简化代码编写,但有时也可能导致意外的错误。因此,在进行隐式类型转换时需要格外小心。

下面是一个隐式类型转换的例子:

int a = 10;
double b = a; // 将整数类型a隐式转换为浮点数类型b

在上面的代码中,a是整数类型,b是浮点数类型。当将a赋值给b时,C++会自动将a转换为浮点数类型,这个过程就是隐式类型转换。


示例:

#include <iostream>
using namespace std;

/*
	普通函数与函数模板区别:
		1. 普通函数调用时可以发生自动类型转换(隐式类型转换)
		2. 函数模板调用时,如果利用自动类型推导,不会发生隐式类型转换
		3. 如果利用显示指定类型的方式,可以发生隐式类型转换
*/


// 普通函数
int add_fn_01(int a, int b) 
	return a + b;



void test04_01() 
	int a = 10;
	int b = 20;

	cout << add_fn_01(a, b) << endl;  // 30

	// 1. 普通函数调用时可以发生自动类型转换(隐式类型转换)
	char c = 'c';  // a -> 97
	cout << add_fn_01(a, c) << endl;  // 109



template<class T>
T add_fn_02(T a, T b) 
	return a + b;



void test04_02() 
	int a = 10;
	int b = 20;

	// 自动类型推导
	cout << add_fn_02(a, b) << endl;

	char c = 'c';
	// cout << add_fn_02(a, c) << endl;  // 没有与参数列表匹配的函数模板"add fn 02"实例

	// 显式指定类型
	// (告诉编译器不用推导了,都是int类型,不是int类型就强转到int,转不过去再报错!)
	cout << add_fn_02<int>(a, c) << endl;  // 109



int main() 

	test04_01();
	test04_02();

	system("pause");
	return 0;

总结:建议使用显示指定类型的方式调用函数模板,因为可以自己确定通用类型T

1.2.5 普通函数与函数模板的调用规则

调用规则如下:

  1. 如果函数模板和普通函数都可以实现(函数模板和普通函数是重载关系),优先调用普通函数(即便普通函数是空函数,也会优先调用普通函数)
  2. 可以通过空模板参数列表<>来强制调用函数模板
  3. 函数模板也可以发生重载
  4. 如果函数模板可以产生更好的匹配,优先调用函数模板

总结:既然提供了函数模板,最好就不要提供普通函数,否则容易出现二义性。


C++函数重载的条件如下:

  1. 函数名称必须相同
  2. 函数参数个数不同
  3. 或者参数类型不同
  4. 或者参数顺序不同

函数的返回类型可以相同也可以不同(不能作为重载的条件!)。

当函数满足以上条件时,可以使用函数重载,也就是在同一作用域内使用同一个函数名定义多个函数。C++编译器会根据调用时传入的参数类型、个数和顺序来区分不同的函数,并选择合适的函数进行调用。函数重载可以提高程序的可读性和可维护性。


示例:

#include <iostream>
using namespace std;

/*
	普通函数与函数模板的调用规则
		1. 如果函数模板和普通函数都可以实现,优先调用普通函数
		2. 可以通过空模板参数列表来强制调用函数模板
		3. 函数模板也可以发生重载
		4. 如果函数模板可以产生更好的匹配,优先调用函数模板
*/


void my_print(int a, int b) 
	cout << "调用普通函数" << endl;



template<class T>
void my_print(T a, T b)   // 函数overloading
	cout << "调用函数模板" << endl;



// 3. 函数模板也可以发生重载
template<class T>
void my_print(T a, T b, T c) 
	cout << "调用重载的函数模板" << endl;



void test05_01() 
	int a = 10;
	int b = 20;

	// 1. 如果函数模板和普通函数都可以实现,优先调用普通函数
	my_print(a, b);

	// 2. 可以通过空模板参数列表来强制调用函数模板
	my_print<>(a, b);  // 调用函数模板

	// 3. 函数模板也可以发生重载
	my_print(a, b, 100);  // 调用重载的函数模板

	// 4. 如果函数模板可以产生更好的匹配,优先调用函数模板
	char c1 = 'a';
	char c2 = 'b';
	my_print(c1, c2);  // 调用函数模板



int main() 

	test05_01();  // 调用普通函数

	system("pause");
	return 0;

1.2.6 模板的局限性

局限性:

  • 模板的通用性并不是万能的

例如:

template<class T>
void f(T a, T b) 
    a = b;

在上述代码中提供的赋值操作,如果传入的ab是一个数组,就无法实现了。

再例如:

template<class T>
void f(T a, T b) 
    if (a > b) ...

在上述代码中,如果T的数据类型传入的是像Person这样的自定义数据类型,也无法正常运行。

因此C++为了解决这种问题,提供模板的重载,可以为这些特定的类型提供具体化的模板。

语法:template<> 返回值类型 同名模板(具体参数类型 参数名称, ...)

// 对比两个数据是否相等的函数模板
template<class T>
bool my_compare(T& a, T& b) 
	if (a == b)
	
		cout << "两者相等" << endl;
		return true;
	
	else
	
		cout << "两者不相等" << endl;
		return false;
	


// 利用具体化的Person的版本实现代码,具体化会优先调用
template<> bool my_compare(Person& p1, Person& p2) 
	if (p1.name == p2.name && p1.age == p2.age)
	
		cout << "两者相等" << endl;
		return true;
	
	else
	
		cout << "两者不相等" << endl;
		return false;
	

总结:

  • 利用具体化的模板,可以解决自定义类型的通用化
  • 学习模板并不是为了写模板,而是在STL能够运用系统提供的模板

示例:

#include <iostream>
using namespace std;
#include <string>

/*
	模板并不是万能的,有些特定的数据类型,需要使用具体化方式做特殊实现
*/


// 对比两个数据是否相等的函数
template<class T>
bool my_compare(T& a, T& b) 
	if (a == b)
	
		cout << "两者相等" << endl;
		return true;
	
	else
	
		cout << "两者不相等" << endl;
		return false;
	

小白自我提高学习设计模式笔记—模板模式

前言

         结合着Android源码把所有的设计模式总结一下。

        小白自我提高学习设计模式笔记(一)—代理模式

        小白自我提高学习设计模式笔记(二)—装饰者模式

        小白自我提高学习设计模式笔记(三)—装饰者模式在Android开发的小试

        小白自我提高学习设计模式笔记(四)—责任链模式

        小白自我提高学习设计模式笔记(五)—模板模式


        在小白自我提高学习设计模式笔记(四)—责任链模式中通过模板模式将所有的ConcreteHandler将每个ConcreteHandler的逻辑进行模板化。

一 模板模式

1.定义

        模板模式(Template Pattern):通过抽象类来定义执行行为的方式或模板。子类只需要复写该抽象类就可以实现该行为的整个执行过程。

        行为型模式。

        主要组成部分:

  • (1)抽象类/抽象模板(Abstract Class):抽象模板类,负责实现行为的轮廓或骨架。有模板方法和若干基本方法组成。
    • 模板方法:定义了行为的骨架,里面实现了调用基本方法的逻辑;为了防止恶意操作,一般模板方法都会加上final关键词;
    • 基本方法:构成行为的每个步骤,其中可以为:
      • 抽象方法:子类必须复写,用来实现子类自己的逻辑;
      • 钩子方法:通常给到是一个空方法,主要是用于判断的逻辑方法或需要子类复写的空方法,例如可以在模板方法的调用期间增加一个调用钩子方法,那么当子类复写该方法的时候,就可以增加到了整个行为执行过程,和抽象方法的区别在于子类不一定需要实现;
      • 具体方法:辅助其他方法的方法,子类不需要实现或复写
  • (2)具体实现类(Concrete Class):具体的实现类。继承抽象类/抽象模板。实现父类的抽象方法和钩子方法。   

2.优缺点

优点

  • (1)将公共部分提取成抽象方法,提高代码复用,便于维护;
  • (2)将不变部分封装到父类实现,子类扩展可变部分,子类也可通过扩展方式增加其他功能;
  • (3)父类实现具体的行为,子类无需关心具体逻辑。

缺点

  • (1)每个不同的实现都需要一个子类来实现,使得系统类膨胀;
  • (2)父类修改抽象方法,所有的子类都需要修改。

3.与策略模式区别

  • (1)策略模式和模板模式都是用来封装算法的,而策略模式用的是组合,而模板模式使用的是继承;
  • (2)策略模式关注的是多种算法;而模板模式关注的是一种算法

二 应用场景

        模板模式主要用来解决下面的问题:

  • (1)有多个子类共有方法,且逻辑相同
  • (2)重要复杂方法可以有模板方法来计算核心内容,细节功能通过子类实现

1.在Android开发中的应用

        在小白自我提高学习设计模式笔记(四)—责任链模式中的3.对APP启动逻辑的优化在每个具体的ConcreteHandler的时候由于有共同的特点:都是需要判断下符合该业务逻辑的则交给本业务逻辑的ConcreteHandler进行处理,否则转交给下一个Handler进行处理,并且都需要有一个判断跳转的条件,所以将所有的ConcreteHandler进行模板化:

  • (1)模板方法handlerAppLauncherEvent()

        将上述的逻辑放到模版方法handlerAppLauncherEvent()中,代码如下:

    @Override
    //模版方法
    final public void handlerAppLauncherEvent(Activity context) {
        //符合该条件的直接交给该Handler进行处理
        if (isSelfAppLauncherEventHandler(context)) {
            Log.v(String.format("~~~~~~~~~~~~ 进入到 \\"%s\\" 处理逻辑", getClass().getSimpleName()));
            handlerSelfAppLauncherEvent(context);
            return;
        }
        Log.d(String.format("~~~~~~~~~~~~ 交给下一个 \\"%s\\" 处理逻辑", getClass().getSimpleName()));
        //否则交给下一个Handler进行处理
        goToNextAppLauncherEventHandler(context);
    }

        可以看到这个模版方法中实现了根据是否符合本业务逻辑的条件,将具体的处理过程交给本ConcreteHandler还是下一个Handler

  • (2)基本方法中的抽象方法handlerSelfAppLauncherEvent()

           每个具体的ConcreteHandler必须要实现handlerSelfAppLauncherEvent(),用于处理本ConcreteHandler的业务逻辑


    /**
     * 显示本Handler处理的广告逻辑
     */
    //基本方法
    public abstract void handlerSelfAppLauncherEvent(Activity context);

          具体的代码已经上传到github:地址为https://github.com/wenjing-bonnie/pattern.git的com.android.pattern.template下的相关代码。 可结合小白自我提高学习设计模式笔记(四)—责任链模式看这些代码

2.在Android源码的模板模式

(1)Android源码中的AsyncTask类  

         在Android源码中的AsyncTask类采用的就是模板模式。可以通过创建一个AsyncTask类轻松实现子线程的操作。当执行task.execute()会依次调用onPreExecute()、 doInBackground()、onPostExecute(),并且可以通过在onProgressUpdate()来更新进度,整个执行过程就是一个模板模式的体现。        

        但是从Android11开始,该API废弃,推荐使用Executor、ThreadPoolExecutor、FutureTask或者kotlin的协程Coroutines。官方指出在使用AsyncTask类的时候,容易造成context内存泄漏、回调失败或则在改变配置的时候造成崩溃。

        遗留问题:ThreadPoolExecutor、FutureTask两个区分。

(2)Activity生命周期加载

        在Android 跨进程通信-(三)Binder机制之Client三 从Native看APP进程的初始化中在APP进程加载过程中会执行ActivityThread的main()函数的时候会通过消息循环队列的方式,依次加载Activity的生命周期方法。同样也是采用模版方式。

三 总结

        模板模式在平时用的还是比较多的,简单总结下:

  • 1.模板模式主要通过抽象类来定义执行行为的方式或者模板,子类只需要继承抽象类就可以拥有该行为;
  • 2.模板模式主要有模板方法和基本方法组成:
    • (1)模板方法:实现了行为的主要逻辑,为了防止恶意修改,通常加上final;
    • (2)基本方法:抽象方法、钩子方法和具体方法。其中抽象方法必须复写,钩子方法通常是一个空方法,可选择复写,具体方法通常不复写
  • 3.模板模式主要作用就是将通用的部分抽象成抽象方法,提高代码复用率;
  • 4.将不变的部分封装到父类,子类只需要实现变化的部分

以上是关于[学习笔记] 3. C++ / CPP提高的主要内容,如果未能解决你的问题,请参考以下文章

CPP复习笔记 3

CPP 学习笔记随笔

C++ 学习笔记

《Exceptional c++》和《提高c++性能的编程技术》学习笔记

《C++ 并发编程实战 第二版》学习笔记目录

《C++ 并发编程实战 第二版》学习笔记目录