C++高级教程(中)
# 泛型编程
泛型编程的基础是模板,泛型编程即以一种独立于任何特定类型的方式编写代码。
同样在Swift中也支持泛型编程
但是在OC中是不支持泛型编程的,虽然不支持,但是OC的 id 能满足所有对象的通用性,一定程度也是实现了类似泛型能力,对数据类型是不支持的。
究其原因是因为,c/c++、Swift都是抢强型语言,
强类型语言必须一一匹配,否则编译阶段就会失败
而OC是弱类型语言,动态语言,所以其对对象甚至数据类型的匹配是模糊的,对数据类型的确认从编译阶段延迟到运行阶段,从而实现动态语言的特性。
# 函数模板
先看一个比较示例:
#include <iostream>
#include <string>
using namespace std;
class Person
{
public:
int age;
Person(int a) {
age = a;
}
};
int max(int a, int b)
{
return a > b ? a : b;
}
double max(double a, double b)
{
return a > b ? a : b;
}
string max(string a, string b)
{
return a > b ? a : b;
}
Person * max(Person a, Person b)
{
return a.age > b.age ? &a : &b;
}
int main ()
{
cout << "int 类型比较大小 :max(3, 1) = " << max(3, 1) << endl;
cout << "double 类型比较大小 :max(3.88, 8.88) = " << max(3.88, 8.88) << endl;
cout << "string 类型比较大小 :max(a, d) = " << max("a", "d") << endl;
Person per1 = Person(20);
Person per2 = Person(14);
cout << "Person 类对象比较大小 :max(per1, per2) = " << max(per1, per2) << endl;
return 0;
}
结果
int 类型比较大小 :max(3, 1) = 3
double 类型比较大小 :max(3.88, 8.88) = 8.88
string 类型比较大小 :max(a, d) = a
Person 类对象比较大小 :max(per1, per2) = 0x7ffc59264e9c
小结:
我们发现上面示例中比较不同类型的大小方法存在很多通用型,假如将相识的实现函数通过定义一个通用模版函数来实现,那么是否会更简单呢?
模板函数定义:
/// template : 模版声明关键字
/// type 只是一个占位符名称,在函数定义中使用
/// func-name 模版方法名称
/// list 模版函数参数
template <typename type> ret-type func-name(parameter list)
{
// 函数的主体
}
下面通过一个示例的演变来解释泛型概念
将前面的示例进行优化:
#include <iostream>
#include <string>
using namespace std;
class Person
{
public:
int age;
Person(int a) {
age = a;
}
// 重载 < 运算符
bool operator<(const Person& p)
{
return this->age < p.age ? true : false;
}
};
template <typename T> inline T const& Max (T a, T b) // const&
{
return a < b ? b : a;
}
int main ()
{
cout << "int 类型比较大小 :Max(3, 1) = " << Max(3, 1) << endl;
cout << "double 类型比较大小 :max(3.88, 8.88) = " << Max(3.88, 8.88) << endl;
cout << "string 类型比较大小 :max(a, d) = " << Max("a", "d") << endl;
Person per1 = Person(20);
Person per2 = Person(14);
Person max_p = Max(per1, per2);
cout << "Person 类对象比较大小 :max(per1, per2) 的 age = " << max_p.age << endl;
return 0;
}
结果:
int 类型比较大小 :Max(3, 1) = 3
double 类型比较大小 :max(3.88, 8.88) = 8.88
string 类型比较大小 :max(a, d) = a
Person 类对象比较大小 :max(per1, per2) 的 age = 20
⚠️ 上面示例中我们泛型没有限定,如果参数是对象,那么返回的也是对象,如果是普通的函数这是不允许的,因为通常函数只能返回对象的内存地址引用,而不能返回对象变量
我将上述示例打印对象参数前后的内存地址:
#include <iostream>
#include <string>
using namespace std;
class Person
{
public:
int age;
Person(int a) {
age = a;
}
// 重载 < 运算符
bool operator<(const Person& p)
{
return this->age < p.age ? true : false;
}
};
template <typename T> inline T const& Max (T a, T b) // const&
{
cout << "第一个对象参数地址 = " << &a << endl;
cout << "第二个对象参数地址 = " << &b << endl;
return a < b ? b : a;
}
int main ()
{
Person per1 = Person(20);
Person per2 = Person(14);
cout << "第一个对象地址 = " << &per1 << endl;
cout << "第二个对象地址 = " << &per2 << endl;
Person max_p = Max(per1, per2);
cout << "max(per1, per2) 的 age = " << max_p.age << endl;
return 0;
}
结果
第一个对象地址 = 0x7ffdc02eca3c
第二个对象地址 = 0x7ffdc02eca38
第一个对象参数地址 = 0x7ffdc02eca1c
第二个对象参数地址 = 0x7ffdc02eca18
max(per1, per2) 的 age = 20
结果发现,直接传入对象变量时,对象的内存地址是不一样的(直接拿参数对象修改,函数外定义的对象不受影响),其实C++中是将这些对象都复制了一份,
称其为【副本】,后面解释什么是副本。
# 模版函数-引用传参
如果想传入参数是对象本身(在函数内修改,函数外定义的对象同时被修改),那么需要使用内存地址引用传值
如果是内存地址对象数据本身比较,可以设置参数是地址:
template <typename T> inline T const& Max (T const& a, T const& b)
{
return a < b ? b:a;
}
# 类成员函数-泛型
类成员函数模板-泛型
泛型类声明的一般形式如下所示:
template <class type> class class-name {
}
栈:
入栈出栈操作示例:
⚠️⚠️ ⚠️ 必须要注意的一点:vector 操作需要使用副本 ,否则取值和修改都是不对的
#include <iostream>
#include <vector>
#include <cstdlib>
#include <string>
#include <stdexcept>
using namespace std;
template <class T>
class Stack {
private:
vector<T> elems; // 元素
public:
void push(T const&); // 压栈
void pop(); // 出栈
T top() const; // 返回栈顶元素
bool empty() const{ // 如果为空则返回真。
return elems.empty();
}
};
template <class T>
void Stack<T>::push (T const& elem)
{
// 压栈(使用副本)
elems.push_back(elem);
}
template <class T>
void Stack<T>::pop ()
{
if (elems.empty()) {
throw out_of_range("出栈异常 Stack<>::pop(): empty stack");
}
cout << "出栈元素:" << elems.back() <<endl;
// 出栈(使用副本)
elems.pop_back();
}
template <class T>
T Stack<T>::top () const
{
if (elems.empty()) {
throw out_of_range("Stack<>::top(): empty stack");
}
// 栈顶元素(副本)
return elems.back();
}
int main()
{
try {
// int 类型的栈
Stack<int> intStack;
// 压栈
intStack.push(1);
intStack.push(5);
intStack.push(9);
cout << "intStack 栈顶元素:" << intStack.top() <<endl;
// string 类型的栈
Stack<string> stringStack;
// 压栈
stringStack.push("one");
stringStack.push("five");
stringStack.push("nine");
cout << "stringStack 栈顶元素:" << stringStack.top() << std::endl;
stringStack.pop();
stringStack.pop();
stringStack.pop();
stringStack.pop(); // 越界异常
}
catch (exception const& ex) {
cout << "Exception: " << ex.what() <<endl;
}
}
结果:
intStack 栈顶元素:9
stringStack 栈顶元素:nine
出栈元素:nine
出栈元素:five
出栈元素:one
Exception: 出栈异常 Stack<>::pop(): empty stack
# STL 标准模板(泛型)库
标准库查询:
https://www.apiref.com/cpp-zh/cpp/header.html
STL
简单理解就是对一系列方法和算法函数进行封装,并使用模版函数(泛型)的方式,创建一个标准的功能库,称STL
# STL泛型库
C++ 对模板(Template)支持得很好,STL 就是借助模板把常用的数据结构及其算法都实现了一遍,并且做到了数据结构和算法的分离。
例如,vector 的底层为顺序表(数组),list 的底层为双向链表,deque 的底层为循环队列,set 的底层为红黑树,hash_set 的底层为哈希表。
通常认为,STL 是由容器、算法、迭代器、函数对象、适配器、内存分配器这 6 部分构成,其中后面 4 部分是为前 2 部分服务的,它们各自的含义如表 1 所示。
STL的组成 | 含义 |
---|---|
容器 | 一些封装数据结构的模板类,例如 vector 向量容器、list 列表容器等。 |
算法 | STL 提供了非常多(大约 100 个)的数据结构算法,它们都被设计成一个个的模板函数,这些算法在 std 命名空间中定义,其中大部分算法都包含在头文件 algorithm 中,少部分位于头文件 numeric 中。 |
迭代器 | 在 C++ STL 中,对容器中数据的读和写,是通过迭代器完成的,扮演着容器和算法之间的胶合剂。 |
函数对象 | 如果一个类将 () 运算符重载为成员函数,这个类就称为函数对象类,这个类的对象就是函数对象(又称仿函数)。 |
适配器 | 可以使一个类的接口(模板的参数)适配成用户指定的形式,从而让原本不能在一起工作的两个类工作在一起。值得一提的是,容器、迭代器和函数都有适配器。 |
内存分配器 | 为容器类模板提供自定义的内存申请和释放功能,由于往往只有高级用户才有改变内存分配策略的需求,因此内存分配器对于一般用户来说,并不常用。 |
# map
用于存储和检索集合(类似array) 中的数据,此集合中的每个元素均为包含数据值和排序键的元素对。 键的值是唯一的,用于自动排序数据。
可以直接更改映射中的元素值。 键值是常量,不能更改。 必须先删除与旧元素关联的键值,才能为新元素插入新键值。
# map构造函数
名称 | 说明 |
---|---|
map (opens new window) | 构造特定大小的列表、包含具有特定值的元素的列表、包含特定 allocator 的列表或作为其他某个映射的副本的列表。 |
# map的Typedef
名称 | 说明 |
---|---|
allocator_type (opens new window) | 映射对象的 allocator 类的 typedef。 |
const_iterator (opens new window) | 可读取 const 中 map 元素的双向迭代器的 typedef。 |
const_pointer (opens new window) | 指向映射中的 const 元素的指针的 typedef。 |
const_reference (opens new window) | 对存储在映射中的 const 元素的引用(用于读取和执行 const 操作)的 typedef。 |
const_reverse_iterator (opens new window) | 一种类型,此类型提供可读取 const 中的任何 map 元素的双向迭代器。 |
difference_type (opens new window) | 映射中迭代器指向的元素间范围内元素数量的有符号整数 typedef。 |
iterator (opens new window) | 可读取或修改映射中任何元素的双向迭代器的 typedef。 |
key_compare (opens new window) | 可比较两个排序键以确定 map 中两个元素的相对顺序的函数对象的 typedef。 |
key_type (opens new window) | 存储在映射内每个元素中的排序键的 typedef。 |
mapped_type (opens new window) | 存储在映射内每个元素中的数据的 typedef。 |
pointer (opens new window) | 指向映射中的 const 元素的指针的 typedef。 |
reference (opens new window) | 对映射中存储的元素的引用的 typedef。 |
reverse_iterator (opens new window) | 可读取或修改反向映射中的元素的双向迭代器的 typedef。 |
size_type (opens new window) | 映射中元素数量的无符号整数 typedef。 |
value_type (opens new window) | 作为元素存储在映射中的对象类型的 typedef。 |
# map成员函数
成员函数 | 说明 |
---|---|
at (opens new window) | 查找具有指定键值的元素。 |
begin (opens new window) | 返回一个迭代器,此迭代器指向 map 中的第一个元素。 |
cbegin (opens new window) | 返回一个常量迭代器,此迭代器指向 map 中的第一个元素。 |
cend (opens new window) | 返回一个超过末尾常量迭代器。 |
clear (opens new window) | 清除 map 的所有元素。 |
contains (opens new window)C++20 | 检查 map 中是否包含具有指定键的元素。 |
count (opens new window) | 返回映射中其键与参数中指定的键匹配的元素数量。 |
crbegin (opens new window) | 返回一个常量迭代器,此迭代器指向反向 map 中的第一个元素。 |
crend (opens new window) | 返回一个常量迭代器,此迭代器指向反向 map 中最后一个元素之后的位置。 |
emplace (opens new window) | 将就地构造的元素插入到 map 。 |
emplace_hint (opens new window) | 将就地构造的元素插入到 map ,附带位置提示。 |
empty (opens new window) | 如果 map 为空,则返回 true 。 |
end (opens new window) | 返回超过末尾迭代器。 |
equal_range (opens new window) | 返回一对迭代器。 此迭代器对中的第一个迭代器指向 map 中其键大于指定键的第一个元素。 此迭代器对中的第二个迭代器指向 map 中其键等于或大于指定键的第一个元素。 |
erase (opens new window) | 从指定位置移除映射中的元素或元素范围。 |
find (opens new window) | 返回一个迭代器,此迭代器指向 map 中其键与指定键相等的元素的位置。 |
get_allocator (opens new window) | 返回用于构造 allocator 的 map 对象的副本。 |
insert (opens new window) | 将一个或一系列元素插入到 map 中的指定位置。 |
key_comp (opens new window) | 返回用于对 map 中的键进行排序的比较对象副本。 |
lower_bound (opens new window) | 返回一个迭代器,此迭代器指向 map 中其键值等于或大于指定键的键值的第一个元素。 |
max_size (opens new window) | 返回 map 的最大长度。 |
rbegin (opens new window) | 返回一个迭代器,此迭代器指向反向 map 中的第一个元素。 |
rend (opens new window) | 返回一个迭代器,此迭代器指向反向 map 中最后一个元素之后的位置。 |
size (opens new window) | 返回 map 中的元素数量。 |
swap (opens new window) | 交换两个映射的元素。 |
upper_bound (opens new window) | 返回一个迭代器,此迭代器指向 map 中其键值大于指定键的键值的第一个元素。 |
value_comp (opens new window) | 检索用于对 map 中的元素值进行排序的比较对象副本。 |
# map运算符
名称 | 说明 |
---|---|
operator[\] (opens new window) | 将元素插入到具有指定键值的映射。 |
operator= (opens new window) | 将一个映射中的元素替换为另一映射副本。 |
# vector
相比 array 容器,vector 提供了更多了成员函数供我们使用,如表 1 所示:
函数成员 | 函数功能 |
---|---|
begin() | 返回指向容器中第一个元素的迭代器。 |
end() | 返回指向容器最后一个元素所在位置后一个位置的迭代器,通常和 begin() 结合使用。 |
rbegin() | 返回指向最后一个元素的迭代器。 |
rend() | 返回指向第一个元素所在位置前一个位置的迭代器。 |
cbegin() | 和 begin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。 |
cend() | 和 end() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。 |
crbegin() | 和 rbegin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。 |
crend() | 和 rend() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。 |
size() | 返回实际元素个数。 |
max_size() | 返回元素个数的最大值。这通常是一个很大的值,一般是 232-1,所以我们很少会用到这个函数。 |
resize() | 改变实际元素的个数。 |
capacity() | 返回当前容量。 |
empty() | 判断容器中是否有元素,若无元素,则返回 true;反之,返回 false。 |
reserve() | 增加容器的容量。 |
shrink _to_fit() | 将内存减少到等于当前元素实际所使用的大小。 |
operator[ ] | 重载了 [ ] 运算符,可以向访问数组中元素那样,通过下标即可访问甚至修改 vector 容器中的元素。 |
at() | 使用经过边界检查的索引访问元素。 |
front() | 返回第一个元素的引用。 |
back() | 返回最后一个元素的引用。 |
data() | 返回指向容器中第一个元素的指针。 |
assign() | 用新元素替换原有内容。 |
push_back() | 在序列的尾部添加一个元素。 |
pop_back() | 移出序列尾部的元素。 |
insert() | 在指定的位置插入一个或多个元素。 |
erase() | 移出一个元素或一段元素。 |
clear() | 移出所有的元素,容器大小变为 0。 |
swap() | 交换两个容器的所有元素。 |
emplace() | 在指定的位置直接生成一个元素。 |
emplace_back() | 在序列尾部生成一个元素。 |
# 预处理器
预处理器就是将相同常量、逻辑代码、代码块等等抽出供使用。
宏不同于函数,它提供方便,但是不会减少代码量,因为在编译的时候会将代码重复的复制到使用的每一个地方
前面在c语言也有介绍。这里简单列举
# 预处理器
以井号(#)开头
比如 #include、#define、#if、#else、#line 等,让我们一起看看这些重要指令。
# #define 预处理
#include <iostream>
using namespace std;
#define kPI 3.14
int main ()
{
cout << "半径5的面积约 = " << kPI*5*5 << endl;
return 0;
}
半径5的面积约 = 78.5
# 函数宏
#include <iostream>
using namespace std;
#define Max(a,b) (((a)>=(b)) ? a : b)
int main ()
{
cout << "5和8的最大值 = " << Max(5, 8) << endl;
return 0;
}
结果:
5和8的最大值 = 8
# 条件编译
if条件编译宏
// ifndef 判断如果没有定义过,那么开始定义
#ifndef AUTO
#define AUTO 0
#endif
#ifdef DEBUG
// 调试模式的代码
#else
// 其他,或者生产环境代码
#endif
# # 和 ## 运算符
1、宏定义中【 # 运算符】为内容添加为被引号的字符串,比如直接打印枚举名称,而不是显示对应的value:
#include <iostream>
using namespace std;
enum RGB{
red, green, yellow
};
// 将参数“名称”内容直接替换成对应字符串
#define MKSTR(x) #x
int main ()
{
cout << MKSTR(RGB::green) << endl;
cout << RGB::green << endl;
return 0;
}
结果:
RGB::green
1
2、宏定义中【## 运算符】用于连接两个令牌,这很有用,你可以将两个名称拼接成一个变量名称
实例:
#define CONCAT( x, y ) x ## y
实例:
#include <iostream>
using namespace std;
#define concat(a, b) a ## b
int main()
{
int xy = 98;
cout << concat(x, y); // 等同于将x、y拼接成变量 xy 使用
return 0;
}
结果:
98
# 预定义宏
预定义宏:
宏 | 描述 |
---|---|
LINE | 这会在程序编译时包含当前行号。 |
FILE | 这会在程序编译时包含当前文件名。 |
DATE | 这会包含一个形式为 month/day/year 的字符串,它表示把源文件转换为目标代码的日期。 |
TIME | 这会包含一个形式为 hour:minute:second 的字符串,它表示程序被编译的时间。 |
让我们看看上述这些宏的实例:
#include <iostream>
using namespace std;
int main ()
{
cout << "当前代码行 : " << __LINE__ << endl;
cout << "当前函数名称 : " << __FUNCTION__ << endl;
cout << "当前文件名称 : " << __FILE__ << endl;
cout << "当前年月日 : " << __DATE__ << endl;
cout << "当前时分秒 : " << __TIME__ << endl;
return 0;
}
结果:
当前代码行 : 7
当前代码行 : main
当前文件名称 : main.cpp
当前年月日 : Jul 6 2023
当前时分秒 : 11:01:35