[学习笔记] 3. C++ / CPP提高
Posted Le0v1n
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[学习笔记] 3. C++ / CPP提高相关的知识,希望对你有一定的参考价值。
本阶段主要针对C++泛型编程和STL技术做详细讲解,探讨C++更深层的使用。
[学习笔记] 3. C++ / CPP提高
- 1. 模板
- 2. STL(Standard Template Library,标准模板库)初识
- 3. STL —— 常用容器
- 4. STL:函数对象
- 5. STL的常用算法
STL(Standard Template Library,标准模板库)是 C++ 标准库的一部分,也是 C++ 中非常重要的一个技术。它是由 Alexander Stepanov 和 Meng Lee 于 1994 年开发的,旨在提供一组通用的、高效的数据结构和算法,能够满足大多数程序的需求。
STL 技术的核心是模板(template)和泛型编程(generic programming)。模板是一种 C++ 语言特性,可以让程序员编写通用的代码,而不用为不同的类型分别编写不同的代码。泛型编程则是一种编程理念,强调代码的通用性和复用性,通过使用模板来实现。
STL 中提供了多种容器(container)和算法(algorithm),容器是一种用于存储数据的对象,包括 vector
、list
、set
、map
等;算法则是一些用于对容器中的数据进行操作的函数,包括排序、查找、遍历等。
此外,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
- 使用函数模板有两种方式:
- 自动类型推导:
函数名()
——swap_fn(a, b);
- 显示指定类型:
函数名<指定数据类型>()
——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 普通函数与函数模板的调用规则
调用规则如下:
- 如果函数模板和普通函数都可以实现(函数模板和普通函数是重载关系),优先调用普通函数(即便普通函数是空函数,也会优先调用普通函数)
- 可以通过空模板参数列表
<>
来强制调用函数模板 - 函数模板也可以发生重载
- 如果函数模板可以产生更好的匹配,优先调用函数模板
总结:既然提供了函数模板,最好就不要提供普通函数,否则容易出现二义性。
C++函数重载的条件如下:
- 函数名称必须相同
- 函数参数个数不同
- 或者参数类型不同
- 或者参数顺序不同
函数的返回类型可以相同也可以不同(不能作为重载的条件!)。
当函数满足以上条件时,可以使用函数重载,也就是在同一作用域内使用同一个函数名定义多个函数。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;
在上述代码中提供的赋值操作,如果传入的a
和b
是一个数组,就无法实现了。
再例如:
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提高的主要内容,如果未能解决你的问题,请参考以下文章