【C++知识点总结全系列 (07)】:模板与泛型编程详细总结与分析

1、概述

(1)What(什么是模板、泛型编程)

模板:

模板分为函数模板和类模板,其类内部的类型和函数的形参类型不具体指定,用一个 虚拟的类型来代表,在具体使用的时候在具体化

泛型编程:

以一种独立于任何特定类型的方式编写代码,模板是泛型编程的基础

(2)Why

实现代码的重用

(3)Which

  • 模板函数
  • 模板类

(4)模板参数

A.What

模板参数是在 C++ 模板中使用的类型或非类型实体的占位符,分为类型模板参数和非 类型模板参数

B.How

template <typename T> T calc(const T&, const T&); //模板的声明

注意:通常一个文件所有模板的声明放在文件的开始位置

C.模板参数的类型成员

  • T::value_type():必须显式地告诉编译器该名字是一个类型,且只能使用关键字 typename(而非 class)

D.默认模板参数

与函数默认实参一样,对于一个模板参数,只有当它的右侧都有默认参时,它才可以有默认参数

在这里插入图片描述

2、模板函数

#include <sstream>
using namespace std; 
template <class T>//T 是类型模板参数
string tTostring(T t)
{
	std::ostringstream osstream; 
	osstream << t;
	return osstream.str(); 
}

template <typename T, typename U>//模板参数列表 
auto add(T t, U u){
	if constexpr (std::is_same<T, std::string>::value)
		return t + tTostring(u);
	if constexpr (std::is_same<U, std::string>::value)
		return tTostring(t) + u;
	if constexpr (std::is_arithmetic<T>::value && std::is_arithmetic<U>::value)
		return t + u;
}

3、模板类

(1)How(如何定义和使用模板类)

template <class T> 
class Blob {
public: 
typedef typename std::vector<T>::size_type size_type;
private:
std::vector<T> *data_; void check(size_type i, const std::string &msg) const; public:
Blob(){ data_ = new std::vector<T>();} 
Blob(std::initializer_list<T> il)
{ 
data_ = new std::vector<T>(il); 
} 
Blob(const Blob &blob) {//在一个类模板作用域内,可直接使用模板名,而不必指定模板参数
data_ = new std::vector<T>(*blob.getData());
// 也可 blob.data_,在类的成员函数内部,可直接访问同类的其他对象私有成员 }
~Blob()
{ 
delete data_;
}
std::vector<T> *getData() const
{
return data_;
} 
size_type size() const 
{
	return data_->size(); 
} 
bool empty() const 
{ 
	return data_->empty(); 
} 
void push_back(const T &t) 
{ 
	data_->push_back(t); 
} 
void push_back(T &&t)
{ 
 	data_->push_back(std::move(t)); 
} 
};
void main()
{
	Blob<double> blob; //实例化一个blob对象,用域处理double类型的数据
	...
}

(2)成员模板

本身是模板函数的成员函数,成员模板不能是虚函数

在这里插入图片描述

4、模板实参和完美转发

(1)模板实参推断

A.What(什么是模板实参推断)

在实例化模板函数或模板类的时候,进行模板实参推断

B.Which(有哪些常见的实参推断)

  • (a) 无法推断时必须显式地指定模板实参类型

    template <typename T1, typename T2, typename T3>
    T1 sum(T2, T3);
    auto res = sum<long>(i, lng); //实例化:long sum(int, long)
    
  • (b) 尾置返回类型

    template <typename It>
    ??? &func(It begin, It end)
    {
    	//TODO:业务逻辑处理
    	return *begin; //返回序列中的引用
    }
    //为了避免不必要的歧义,一般定义成如下形式
    auto &func(It begin, It end) -> decltype(*begin)
    {
    	//TODO:业务逻辑
    	return *begin;
    }
    auto &str = func(vecStr.begin(),vecStr.end());
    auto &i  = func(vecInt.begin(), vecInt.end());
    
  • (c ) STL中的标准类型转换

    如下图所示,展示了如何将引用传递改为值传递:

    在这里插入图片描述
    常见的类型转换模板有如下:
    在这里插入图片描述

  • (d) 实参推断和引用

    如果一个函数参数的类型是 constT&,正常绑定规则告诉我们可以传递给它任 意类型的实参—对象、临时变量、字面值常量

    从左值引用推断模板参数:
    在这里插入图片描述
    从右值引用推断模板参数:
    在这里插入图片描述

  • (e) 引用折叠和右值引用参数

    • 将左值传递给右值参数引用时(且函数模板参数类型为 T&&),编译器推断模 板类型参数为实参的左值引用类型

    下面程序中:T 的类型为 string&,参数类型为 string& &&,根据折叠原则,为 string &

    template <class T> 
    void show(T &&t){
    	cout << t << endl;
    }
    string zs = "张三同学"; 
    show(zs);//将左值引用传递给右值引用参数
    
    • 我们不能直接定义一个引用的引用,但是,通过类型别名或模板类型参数可以 间接定义引用的引用;此时会进行引用折叠

    X& &、X& &&和X&& &都会折叠成类型X&
    类型X&& &&折叠成X&&

    • 总结:当模板函数的参数为T&&,则可以传递任意类型的实参给它

    传递左值时:结果为int& && -> int &
    传递右值时:结果为int&& && -> int &&

(3)完美转发

A.What(什么是完美转发)

某些函数将其一个或多个实参连同其类型不变地转发给其它函数,我 们需要保证被转发实参的所有性质,这种保证转发实参所有性质的事情,我们称之为“完美转发

B.How(如何进行完美转发)

常见的模板函数定义形式如下:

template <typename F, typename T1, typename T2>
auto flip(F f, T1 &&t1, T2 &&t2)
{
	return f(t1, t2);
}

假如存在函数func()如下:

int func(int& iVal01, int iVal02)
{
	return iVal01 + iVal02;
}
int main()
{
	int iVal = 10;
	auto res = flip(func, iVal, 30); //res为40
	return 1;
}

在上述例子中,T1为int& &&,折叠为int& t1,从而实现了完美转发

C. forward()和move()函数介绍

  • forward 可以保持给定实参的左值/右值属性
forward<T>(t); //使用方式,保持t的左值或右值属性
  • move函数将左值转换为右值
std::string str = "Hello, ThreeBody!";
std::string &&ref = std::move(str); //将左值强转为右值

(4)可变参数模板

A.What(什么是可变参数模板)

void show() // 终止条件 
{ 
	cout << endl; 
}
template <typename T, typename... Args> // Args 为模板参数包,表示 0 个或多个模板参数 
void show(const T &t, const Args &...args) // args 为函数参数包,表示 0 个或多个函数参数
{
	cout << t << " " << endl; 
	cout << sizeof...(Args) << endl; // 2 1 0 ;sizeof...运算符,返回包中参数的个数 cout << sizeof...(args) << endl; // 2 1 0
	show(args...); 
}
string zs = "张三"; 
double defict = 128.1; 
int year = 1981;
show(zs, defict, year);

B.包拓展

包拓展就是将它分解成构成元素

C.转发参数包

void show() // 终止条件
{
	cout << endl; 
}
template <typename T, typename... Args> // Args 为模板参数包,表示 0 个或多个模板参数 
void show(T &&t, Args &&...args) // args 为函数参数包,表示 0 个或多个函数参数
{
	cout << t << " " << endl; 
	cout << sizeof...(Args) << endl; // 2 1 0 ;sizeof...运算符,返回包中参数的个数 
	cout << sizeof...(args) << endl; // 2 1 0
	show(std::forward<Args>(args)...);
}

(5)模板特例化

A.What

对特定的模板参数提供定制实现;分为完全特化和偏特化

  • 完全特化:所有模板参数都提供定制
  • 偏特化:部分模板参数提供定制-只允许类模板偏特化,函数模板不允许

B.Why

为了针对特定类型或条件提供特殊的实现或行为。模板特例化可以在需要时覆盖通用 模板,以针对某些特定情况做出额外的处理或提供定制化的实现

C.How

template <class T1, class T2, class T3> 
void display(T1 t1, T2 t2, T3 t3)
{
	cout << "通用模板版本 display" << endl; 
	cout << t1 << "," << t2 << "," << t3 << endl; 
}
template <> //完全特例化模板函数 display,函数模板不允许偏特例化 void display(double d, string str, int a)
{
	cout << "特化版本 display" << endl; 
	cout << a << "," << d << "," << str << endl;
}
int main(){
	string zs = "张三"; 
	double defict = 128.1; 
	int year = 1981;
	display(defict, zs, year); // 调用特例化版本的 
	display display(128.1, "李四", 1981); // 调用通用版本的 display 
	return 0;
}

5、动态内存分配

(1)动态内存分配概述

A. C++内存空间模型

在这里插入图片描述

B. What(什么是动态内存分配)

一种程序在 运行时 内存申请和释放的机制

C. Why(为什么要进行动态内存分配)

  • 提升灵活性:静态内存分配需要编译器在程序编译时知道分配内存的大小,而动态内存分配机制则允许在程序运行时分配内存空间
  • 便于内存管理:动态地创建和销毁对象,提供了灵活的内存管理方式
  • 动态调整数据结构:如vector数据结构会在不足时重新分配内存空间

D. How(如何进行动态内存分配)

在C语言中,使用malloc和free来申请和释放堆栈中的内存空间
在C++中,使用new和delete来申请和释放堆栈中的内存空间

//使用C格式管理动态内存的分配和释放
const int SIZE = 1024;
int * ptrArrC = (int *)malloc(SIZE * sizeof(int));
free(ptrArrC);
//使用C++格式管理动态内存的分配和释放
int * ptrArrCPlus = new int[SIZE];
delete [] ptrArrCPlus;

(2)定位new运算符

A. What(什么是定位 new 运算符)

在指定的内存中创建对象

double dArr[1024];
 double ptrArr = new (&dArr[0]) double(100.0);

B. Why(定位 new 运算符的作用)

  • 在预先分配的内存区域上创建对象,而不一定是在堆中开辟动态内存空间
  • 特定的内存位置上创建对象,并根据需要提供参数来初始化对象
  • 将对象放置在满足对齐要求的合适内存地址上,以满足底层系统的需求

C. How(如何使用定位 new 运算符)

在这里插入图片描述
需要注意的是:当管理的是类对象或结构体对象时,调用它的析构函数并不会释放动态内存空间
在这里插入图片描述
因此,正确的做法是调用delete运算符,该运算符会首先调用类对象的析构函数来销毁对象,然后回收动态内存
在这里插入图片描述

(3)重载new和delete

下图展示了重载new和delete运算符的方式,值得一提的是:当我们将下列运算符函数定义成类的成员函数时,它们都是隐式静态的(即默认是静态成员函数)

在这里插入图片描述
实例说明:
在这里插入图片描述
在这里插入图片描述
注意:上述代码在使用malloc存在一定问题,因为malloc分配的是字节数,所以在具体使用的时候应该乘以sizeof(obj),即乘以所存对象占用的内存大小

6、智能指针模板

(1)概述

A.Why(C++为什么引入智能指针)

C++引入智能指针的根本原因就是解决手动管理动态内存所带来的问题,手动管理动态内存常见的问题如下:内存泄漏、悬挂指针、释放操作未定义等

内存泄漏问题:

当程序用光了它可用的动态内存空间,new就会报错

int *p = new int; //*p的值未定义
int *p2 = new int(); //*p的值初始化为0
//程序用光了它可用的堆内存空间
int *pn = new int; //此时将会抛出异常:std::bad_alloc
int *pn_noThrow = new (nothrow) int; //如果分配失败,将返回空指针nullptr

悬挂指针问题:

当使用delete释放了指向动态内存空间的指针p时,指针p就成为了一个悬挂指针(即在有的机器中,指针p还保存着曾经的内存地址),因此在手动管理动态内存空间的时候,一定要在delete之后将对应指针置为nullptr或0

int *ptr = new int();
delete ptr;
ptr = nullptr; //避免悬挂指针从而引起的未定义定位(不可预知行为)

释放操作未定义问题:

释放一块非new分配的内存或将相同的指针释放多次,其行为是未定义的

B.What(什么是智能指针)

本质是一个封装了原始指针的模板类,在适当的时候(生命周期结束时)自动释放所管理的动态内存

C.Which(有哪些智能指针)

  • shared_ptr
  • weak_ptr
  • unique_ptr
智能指针所有权内存释放
shared_ptr多个 std::shared_ptr 共享所有权通过内部的引用计数机制,当引用计数为 0 时,自动释放内存
weak_ptr不拥有所指向对象的所有权,它只是对 std::shared_ptr 的一种弱引用-
unique_ptr独占所有权当 std::unique_ptr 超出其作用域时,自动释放所管理的内存

(2)How(如何使用智能指针)

C++中的智能指针,其本质都是类模板,因此在学习智能指针的时,我们可以像对待普通类对象一样使用智能指针对象。正是因为它的本质是类对象,因此使用智能指针的一般流程如下:创建智能指针对象和调用智能指针对象的方法

A.智能指针类的成员变量

shared_ptr智能指针:

成员数据说明
T *ptr存放实际数据的动态内存地址
int iCount计数器,用于记录有多少个shared_ptr共享同一块内存空间
deleter删除器,本质是一个函数对象,自定义资源释放的行为

unique_ptr智能指针:

成员数据说明
T *ptr存放实际数据的动态内存地址
deleter删除器,本质是一个函数对象,自定义资源释放的行为

weak_ptr智能指针:

weak_ptr 是一种不控制所指向对象生存期的智能指针,它指向由一个 shared_ptr 管理 的对象。将一个 weak_ptr 绑定到一个 shared_ptr 不会改变 sharedptr 的引用计数。一 旦最后一个指向对象的 shared_ptr 被销毁,对象就会被释放。即使有 weakptr 指向对象, 对象也还是会被释放,因此,weak_ptr 的名字抓住了这种智能指针“弱”共享对象

所以说weak_ptr可以说没有成员数据

B.智能指针类的成员函数

shared_ptr智能指针:

成员函数说明
constexpr shared_ptr() noexcept = default;默认的构造函数
constexpr shared_ptr(nullptr_t) noexcept {}带空指针的构造函数
explicit shared_ptr(T* ptr)传入指向堆内存的一般指针
void swap(shared_ptr& _Other) noexcept交换两个智能指针所管理的内存
void reset() noexcept释放资源
remove_extent_t<_Ty>* get() const noexcept得到所管理动态内存的地址
bool unique() const noexcept是否只有一个引用
long use_count() const noexcept返回shared_ptr对象的引用数

在这里插入图片描述

unique_ptr智能指针:

成员函数说明
constexpr unique_ptr() noexcept默认的构造函数
constexpr unique_ptr(nullptr_t) noexcept带空指针的构造函数
explicit unique_ptr(pointer _Ptr) noexcept传入指向堆内存的一般指针
void swap(shared_ptr& _Right ) noexcept交换两个智能指针所管理的内存
_Dx& get_deleter() noexcept得到删除器对象
pointer release() noexcept释放所管理动态内存的地址
void reset(pointer _Ptr = nullptr) noexcept如果reset函数带有参数,则会释放当前指向的对象,并将unique_ptr指向新的对象

在这里插入图片描述

weak_ptr智能指针:

成员函数说明
constexpr weak_ptr() noexcept = default;默认的构造函数
weak_ptr(const weak_ptr& _Other) noexcept拷贝构造函数
weak_ptr(const shared_ptr<_Ty2>& _Other) noexcept带shared_ptr对象的构造函数
void swap(weak_ptr& _Other) noexcept交换两个智能指针所管理的内存
void reset() noexcept释放资源
bool expired() const noexceptuse_count()为0时返回true
shared_ptr<_Ty> lock() const noexcept转为shared_ptr对象
long use_count() const noexcept返回对应shared_ptr对象的引用数

在这里插入图片描述
在这里插入图片描述

(3)使用智能指针需要注意的事项

  • 不要混合使用常规指针和智能指针

    使用内置指针访问智能指针管理的对象是危险的,无法知道对象何时被销毁

  • 不要使用get()函数初始化另一个智能指针或为另一个智能指针赋值
    在这里插入图片描述

  • 不 deleteget()返回的指针

  • 不使用相同的内存指针初始化(reset)多个智能指针

  • 如果智能指针所管理的内存不是 new 分配的内存,要传递一个删除器

================================================================================

C++知识点总结全系列文章索引:
【C++知识点总结全系列 (01)】:数据类型、数据类型转换和变量
【C++知识点总结全系列 (02)】:C++中的语句、运算符和表达式详细总结
【C++知识点总结全系列 (03)】:函数(函数参数传递、可变参数、函数返回值等详细介绍)
【C++知识点总结全系列 (04)】:C++类的详细总结与分析
【C++知识点总结全系列 (05)】:IO 类的详细总结和分析
【C++知识点总结全系列 (06)】:STL六大组件总结- 配置器、容器、迭代器、适配器、算法和仿函数
【C++知识点总结全系列 (07)】:模板与泛型编程详细总结与分析 ==》 当前位置
【C++知识点总结全系列 (08)】:面向对象编程OOP

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值