Day 20 C++ 模板

C++ 模板

  • 定义
  • 特点
    • 通用性(Genericity)
    • 静态类型检查(Static Type Checking)
    • 延迟实例化(Deferred Instantiation)
    • 元编程(Metaprogramming)
    • 注意
  • 范式编程
    • 核心思想是将算法和数据结构与数据类型分离
    • 重要优势是可以提供高效的代码复用
    • 可以实现许多强大的功能
    • 总结
  • 分类
    • 函数模板
      • 定义
      • 语法
      • 使用函数模板有两种方式
        • 自动类型推导
        • 显示指定类型
      • 注意
        • 自动类型推导,必须推导出一致的数据类型T,才可以使用
        • 模板必须要确定出T的数据类型,才可以使用
        • 模板参数列表中,是不允许有默认参数的
        • 函数模板也可以发生重载
      • 普通函数与函数模板的区别
      • 普通函数与函数模板的调用规则
        • 如果函数模板和普通函数都可以实现,优先调用普通函数
        • 可以通过空模板参数列表来强制调用函数模板
        • 如果函数模板可以产生更好的匹配,优先调用函数模板
    • 类模板
      • 作用
      • 语法
      • 类模板与函数模板区别
        • 类模板没有自动类型推导的使用方式
        • 类模板在模板参数列表中可以有默认参数
      • 类模板中成员函数创建时机
        • 普通类中的成员函数一开始就可以创建
        • 类模板中的成员函数在调用时才创建
      • 类模板对象做函数参数
        • 指定传入的类型 ( 直接显示对象的数据类型)
        • 参数模板化 (将对象中的参数变为模板进行传递)
        • 整个类模板化 --- 将这个对象类型 模板化进行传递**
      • 类模板继承
        • 注意
      • 类模板成员函数类外实现
      • 类模板与友元
        • 全局函数类内实现 - 直接在类内声明友元即可
        • 全局函数类外实现 - 需要提前让编译器知道全局函数的存在
      • 类模板分文件编写
        • 问题
        • 解决
          • 直接引入.cpp源文件
          • 将声明和实现写到同一个文件中,并更改后缀名为.hpp(非强制)
          • 示例

定义

C++ 模板是一种通用编程工具,它允许程序员编写可重用的代码,以在不同的数据类型上进行操作。
(简单来说,模板就是建立通用的模具,大大提高复用性

特点

通用性(Genericity)

模板允许程序员编写通用的代码,以适应多种数据类型和需求。通过使用模板参数,可以在函数模板或类模板中处理不同类型的数据,而无需为每种类型编写单独的实现。这极大地提高了代码的可重用性和灵活性。

静态类型检查(Static Type Checking)

C++编译器可以在编译时对模板进行类型检查,确保代码在运行时没有类型错误。这使得在使用模板时可以避免一些常见的运行时错误,如类型不匹配、类型转换错误等。静态类型检查还可以提供更好的编程安全性和代码质量。

延迟实例化(Deferred Instantiation)

C++模板使用延迟实例化的机制,即只有在实际使用模板时才会进行实例化。这意味着模板不会引入额外的开销,直到真正需要使用模板的功能时才会生成相应的代码。这种延迟实例化的机制使得模板可以有效地处理各种数据类型而不会产生额外的性能损失。

元编程(Metaprogramming)

C++模板还提供了一种元编程的能力,即在编译时生成代码。利用模板的特性,程序员可以在编译期间进行一些复杂的计算和逻辑操作,并将其结果作为编译时常量或类型的一部分。这种元编程能力为编写高度灵活和性能优化的代码提供了很大的空间

注意

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

范式编程

泛型编程是C++中另一种重要的编程思想,它主要利用了模板技术。泛型编程旨在实现通用的、与具体数据类型无关的算法和数据结构。通过使用模板,我们可以编写可以适用于不同数据类型的通用代码,从而提高代码的可重用性和灵活性。

核心思想是将算法和数据结构与数据类型分离

通过使用模板参数,我们可以将类型相关的细节推迟到编译时处理,并通过实例化模板生成针对特定类型的代码。这种方式使得我们能够以一种抽象的方式编写代码,从而实现对不同类型的操作。

重要优势是可以提供高效的代码复用

通过编写通用的数据结构和算法,我们可以在不需要重复编写代码的情况下针对不同的数据类型进行操作。这不仅减少了代码的冗余,还提高了开发效率。

可以实现许多强大的功能

如容器类(例如vector、list等)、算法库(例如排序、搜索等)和函数对象(Functors)等。STL(Standard Template Library)就是C++标准库中基于泛型编程思想的一个重要组成部分。

总结

泛型编程是利用模板技术实现的一种编程思想,它旨在实现通用的算法和数据结构。通过将类型相关的细节推迟到编译期处理,泛型编程提供了高效的代码复用和灵活性。

分类

函数模板

定义

函数模板是C++中一种特殊类型的模板,它允许我们编写通用的函数定义,以处理不同类型的数据。函数模板使用了参数化的方式,使得函数可以接受不同类型的参数,从而实现代码的通用性和灵活性。

函数模板通过在函数声明和定义中使用通用的类型参数(也称为模板参数)来实现。这些模板参数可以用作函数的参数类型、返回类型或局部变量类型等。当我们调用函数模板时,编译器会根据实际传入的参数类型实例化模板,并生成对应的函数定义。

语法

template <typename T>
返回类型 函数名(参数列表) {
    // 函数体
}

其中,template 关键字用于声明一个模板,并且 指定了模板参数列表,T 是我们自己定义的类型参数。可以使用关键字 class 替代 typename

使用函数模板有两种方式

自动类型推导更为简洁,而显式指定类型可以提供更明确的类型控制

自动类型推导

编译器会根据函数参数的类型推断出要使用的模板类型。在这种情况下,不需要显式地指定模板类型,编译器会自动选择合适的函数定义。

template <typename T>
T add(T a, T b) {
    return a + b;
}

int main() {
    int result1 = add(3, 5);        // 自动类型推导为 add(3, 5),结果为 8
    double result2 = add(4.2, 1.5); // 自动类型推导为 add(4.2, 1.5),结果为 5.7

    return 0;
}

显示指定类型

通过在函数调用时显式地指定模板类型,来实例化特定的函数定义。

template <typename T>
T add(T a, T b) {
    return a + b;
}

int main() {
    int result1 = add<int>(3, 5);        // 显式指定类型为 int,结果为 8
    double result2 = add<double>(4.2, 1.5); // 显式指定类型为 double,结果为 5.7

    return 0;
}

注意

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

在使用自动类型推导实例化函数模板时,编译器会根据传入的参数类型来推断模板参数的数据类型 T。如果无法推导出一致的数据类型 T,则会导致编译错误。

template <typename T>
T maximum(T a, T b) {
    return a > b ? a : b;
}

模板必须要确定出T的数据类型,才可以使用

使用自动类型推导实例化该函数模板时,参数的类型必须一致,在数据类型不同的情况下会导致编译错误

int main() {
    int result1 = maximum(3, 5);        // 自动类型推导为 maximum(3, 5),结果为 5
    double result2 = maximum(4.2, 1.5); // 自动类型推导为 maximum(4.2, 1.5),结果为 4.2

    char result3 = maximum('a', 'b');   // 错误!无法推导出一致的数据类型T

    return 0;
}

模板参数列表中,是不允许有默认参数的

模板参数列表是指在函数模板定义中用于声明和定义模板参数的部分。函数模板的模板参数列表中只能包含模板参数的名称和模板参数的约束(如果有的话),而不可以指定默认参数值。这是因为在使用函数模板时,编译器需要根据参数的类型进行模板实例化,并根据实际提供的参数来确定模板参数的具体值。然而,对于函数模板的函数参数列表(即在模板定义中的函数参数部分),可以有默认参数。默认参数允许在调用函数模板时省略某些参数,而使用预先定义的默认值。

#include 
template <typename T>
void print(T value, int width = 10) {
    std::cout.width(width);
    std::cout << value << std::endl;
}
int main() {
    print("Hello"); // 使用默认的宽度(10)进行输出
    print(3.14159, 5); // 使用自定义的宽度(5)进行输出
    return 0;
}

函数模板也可以发生重载

函数模板的重载规则与普通函数的重载规则类似。

template <typename T>
T maximum(T a, T b) {
    return a > b ? a : b;
}

template <typename T>
T maximum(T a, T b, T c) {
    return maximum(maximum(a, b), c);  // 调用了上面定义的函数模板
}

int maximum(int a, int b) {
    return a > b ? a : b;
}

int main() {
    int result1 = maximum<int>(3, 5);                         // 实例化第一个函数模板,结果为 5
    double result2 = maximum<double>(4.2, 1.5);                // 实例化第一个函数模板,结果为 4.2
    int result3 = maximum<int>(3, 5, 8);                       // 实例化第二个函数模板,结果为 8
    int result4 = maximum(3, 5);                               // 调用了普通函数 maximum(int, int),结果为 5

    return 0;
}

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

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

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

匹配度/空模板参数列表 > 普通函数 > 函数模板

如果函数模板和普通函数都可以实现,优先调用普通函数

可以通过空模板参数列表来强制调用函数模板

通过使用空模板参数列表,我们可以明确地指示编译器实例化函数模板,以确保调用的是函数模板而不是普通函数。

myPrint<>(a, b);

如果函数模板可以产生更好的匹配,优先调用函数模板

类模板

作用

建立一个通用类,类中的成员 数据类型可以不具体制定,用一个虚拟的类型来代表。

语法

template <typename T>
class ClassName {
    // 类成员声明和定义
};

其中,template 表示定义一个类模板,并使用 T 作为类型参数。T 可以被替换为任意类型名称,用于表示类中的成员类型、函数参数类型或返回类型等。

在类模板中,可以像定义普通类一样,声明和定义成员函数、数据成员和类型。在这些成员的声明和定义中,可以使用 T 来表示类型参数。




类模板与函数模板区别

类模板没有自动类型推导的使用方式

类模板在模板参数列表中可以有默认参数




类模板中成员函数创建时机

普通类中的成员函数一开始就可以创建

类模板中的成员函数在调用时才创建

类模板对象做函数参数

一共有三种传入方式

指定传入的类型 ( 直接显示对象的数据类型)

void printPerson1(Person<string, int>& p) {
    // 函数实现
}

参数模板化 (将对象中的参数变为模板进行传递)

template <class T1, class T2>
void printPerson2(Person<T1, T2>& p) {
    // 函数实现
}

整个类模板化 — 将这个对象类型 模板化进行传递**

template<typename T>
class MyClass {
    // 类模板定义
};

template<typename T>
void myFunction(MyClass<T> obj) {
    // 函数操作 MyClass 类型的对象
}

int main() {
    MyClass<int> myObj;
    myFunction(myObj); // 将 MyClass 作为函数参数传递
    return 0;
}

类模板继承

类模板的继承和普通类的继承基本相同,只不过需要指明基类是一个类模板

注意

  • 当子类继承的父类是一个类模板时,子类在声明的时候,要指定出父类中T的类型
  • 如果不指定,编译器无法给子类分配内存
  • 如果想灵活指定出父类中T的类型,子类也需变为类模板
// 定义一个类模板
template<typename T>
class BaseClass {
public:
    T data;

    BaseClass(T value) : data(value) {}

    void printData() {
        cout << "Base Data: " << data << endl;
    }
};

// 派生类从类模板 BaseClass 继承
template<typename T>
class DerivedClass : public BaseClass<T> {
public:
    DerivedClass(T value) : BaseClass<T>(value) {}

    void printDerivedData() {
        cout << "Derived Data: " << (this->data * 2) << endl;
    }
};

int main() {
    // 实例化一个基类对象
    BaseClass<int> baseObj(10);
    baseObj.printData();  // 输出: Base Data: 10

    // 实例化一个派生类对象
    DerivedClass<int> derivedObj(5);
    derivedObj.printData();         // 输出: Base Data: 5
    derivedObj.printDerivedData();  // 输出: Derived Data: 10

    return 0;
}

类模板成员函数类外实现

类模板中成员函数类外实现时,需要加上模板参数列表

// 定义一个类模板
template<typename T>
class MyClass {
public:
    T data;

    MyClass(T value) : data(value) {}

    void printData(); // 在类外声明成员函数
};

// 在类外实现成员函数,需要在函数名称前加上模板参数列表
template<typename T>
void MyClass<T>::printData() {
    cout << "Data: " << data << endl;
}

int main() {
    // 实例化一个类模板对象
    MyClass<int> obj(10);

    // 调用成员函数
    obj.printData(); // 输出: Data: 10

    return 0;
}

类模板与友元

全局函数类内实现 - 直接在类内声明友元即可

class MyClass {
public:
    // 定义全局函数,并将其声明为友元函数
    friend void myGlobalFunction(MyClass& obj) {
        // 在函数内部可以直接访问类的私有成员和保护成员
        obj.privateVar = 10;
    }

private:
    int privateVar;
public:
    void setPrivateVar(int value) {
        privateVar = value;
    }
};

全局函数类外实现 - 需要提前让编译器知道全局函数的存在

class MyClass;

// 声明全局函数的原型
void myGlobalFunction(MyClass& obj);

class MyClass {
public:
    // 其他类成员和方法的定义

private:
    int privateVar;
public:
    void setPrivateVar(int value) {
        privateVar = value;
    }

    // 友元函数的声明
    friend void myGlobalFunction(MyClass& obj);
};

// 在类外实现全局函数
void myGlobalFunction(MyClass& obj) {
    // 在全局函数内部可以访问类的私有成员和保护成员
    obj.privateVar = 10;
}

类模板分文件编写

问题

类模板中成员函数创建时机是在调用阶段,导致分文件编写时链接不到

解决

直接引入.cpp源文件
将声明和实现写到同一个文件中,并更改后缀名为.hpp(非强制)

将文件后缀名更改为.hpp 是一种常见的约定,用于表示该文件是一个 C++ 头文件。虽然这只是一种约定,而不是强制要求,但它有助于标识文件类型和提高代码的可读性。在C++开发中,通常使用不同的文件后缀名来区分源代码文件和头文件。常见的约定是将源代码文件命名为以.cpp或.c为后缀的文件,并将头文件命名为以.hpp或.h为后缀的文件。

示例

person.hpp

#pragma once
#include 
using namespace std;
#include 

template<class T1, class T2>
class Person {
public:
	Person(T1 name, T2 age);
	void showPerson();
public:
	T1 m_Name;
	T2 m_Age;
};

//构造函数 类外实现
template<class T1, class T2>
Person<T1, T2>::Person(T1 name, T2 age) {
	this->m_Name = name;
	this->m_Age = age;
}

//成员函数 类外实现
template<class T1, class T2>
void Person<T1, T2>::showPerson() {
	cout << "姓名: " << this->m_Name << " 年龄:" << this->m_Age << endl;
}

person.cpp

#include
using namespace std;

//#include "person.h"
#include "person.cpp" //解决方式1,包含cpp源文件

//解决方式2,将声明和实现写到一起,文件后缀名改为.hpp
#include "person.hpp"
void test01()
{
	Person<string, int> p("Tom", 10);
	p.showPerson();
}

int main() {

	test01();

	system("pause");

	return 0;
}

#pragma once 是一个预处理指令,用于确保头文件只被编译一次。在C++中,当头文件被包含到多个源代码文件中时,可能会导致多重包含(multiple inclusion)的问题。多重包含指的是同一个头文件在同一个编译单位中多次被包含,这可能导致符号重定义和编译错误。为了解决多重包含的问题,可以使用 #pragma once 指令。当编译器遇到 #pragma once 时,它会将该指令所在的头文件标记为只能被编译一次。这意味着在同一个编译单位中,当多次包含同一个头文件时,只有第一次包含会起作用,后续的包含将被忽略。

PS:又是7000多字大论文QAQ

你可能感兴趣的:(C,++,c++)