【黑马程序员 C++教程从0到1入门编程】【笔记8】 泛型编程——模板
https://www.bilibili.com/video/BV1et411b73Z?p=167
C++泛型编程是一种编程范式,它的核心思想是编写通用的代码,使得代码可以适用于多种不同的数据类型。
而模板是C++中实现泛型编程的一种机制,它允许我们编写通用的代码模板,然后在需要使用时根据具体的数据类型进行实例化,生成具体的代码。
因此,可以说模板是C++中实现泛型编程的基础。通过使用模板,我们可以编写出具有高度通用性和可重用性的代码,从而提高代码的效率和可维护性。
文章目录
- 1 模板
- 1.1 模板的概念
- 1.2 函数模板
- 1.2.1 函数模板语法
- 语法
- 解释
- 注意
- 示例:自动类型推导、显式指定类型
- 1.2.2 函数模板注意事项
- 示例:模板必须要确定出T的数据类型,才可以使用
- 示例:多类型参数模板
- 1.2.3 函数模板案例(不同数据类型数组排序)
- 1.2.4 普通函数与函数模板的区别
- 示例:建议调用模板函数时,使用显式指定类型的方式,不容易出错
- 1.2.5 普通函数与函数模板的调用规则
- 示例:当普通函数与模板有冲突时,如果函数模板可以产生更好的匹配,优先调用函数模板(考虑到普通函数存在隐式转换的情况)
- 1.2.6 模板的局限性
- 特化(specialization)模板函数
- 特化模板函数的作用
- 特化模板函数、常规模板函数、普通函数的优先级(普通函数 > 特化的模板函数 > 模板函数)
- 示例:特化的模板函数示例
- 总结
- 1.3 类模板
- 1.3.1 类模板语法
- 示例
- 总结:类模板和函数模板语法相似,在声明模板template后面加类,此类称为类模板
- 1.3.2 类模板与函数模板区别(类模板的实例化在编译时完成,函数模板的实例化在调用时完成)
- 关于“类模板的实例化在编译时完成”
- 关于“类模板没有自动类型推导的使用方式”
- 示例
- 1.3.3 类模板中成员函数创建时机(普通类中的成员函数一开始就可以创建,类模板中的成员函数在调用时才创建)
- 示例
- 类模板除了静态成员函数外,普通成员函数在模板实例化时会生成新的函数,即使这个函数使用时不依赖于模板参数类型
- 1.3.4 类模板对象做函数参数(注意`类模板对象`这几个字,对象不是普通对象,而是`类模板对象`)
- 三种传参方式示例:指定传入的类型、参数模板化、整个类模板化(在下面代码中,Person就是一个`类模板对象`)
- 1.3.5 类模板与继承(当子类继承的父类是一个类模板时,子类在声明的时候,要指定出父类中T的类型;如果想灵活指定出父类中T的类型,子类也需变为类模板)
- 示例
- 1.3.6 类模板成员函数类外实现(居然都需要把模板加上...)
- 示例
- 1.3.7 类模板分文件编写(注意`类模板中成员函数创建时机是在调用阶段,导致分文件编写时链接不到`问题)
- 问题产生原因
- 解决方法
- 示例
- 1.3.8 类模板与友元(1. 友元全局函数类内实现 2. 友元全局函数类外实现)
- 示例
- 1.3.9 类模板案例(用类模板构建一个泛型数组)
- 示例
1 模板
1.1 模板的概念
模板就是建立通用的模具,大大提高复用性
例如生活中的模板
一寸照片模板:
PPT模板:
模板的特点:
- 模板不可以直接使用,它只是一个框架
- 模板的通用并不是万能的
1.2 函数模板
-
C++另一种编程思想称为 泛型编程 ,主要利用的技术就是模板
-
C++提供两种模板机制:函数模板和类模板
1.2.1 函数模板语法
函数模板作用:
建立一个通用函数,其函数返回值类型和形参类型可以不具体制定,用一个虚拟的类型来代表。
语法
template<typename T>
函数声明或定义
解释
template — 声明创建模板
typename — 表明其后面的符号是一种数据类型,可以用class代替(貌似用其他名字不行,只能用typename或者class)
T — 通用的数据类型,名称可以替换,通常为大写字母
注意
template <typename T>
跟后面的代码void mySwap(T &a, T &b)
要连着的,中间可以添加注释,但是不能添加无关的代码
template <typename T> void mySwap(T &a, T &b)
示例:自动类型推导、显式指定类型
(test.cpp)
#include <iostream>
#include <cstdlib>
#include <limits>
using namespace std;// 交换整型函数
void swapInt(int &a, int &b)
{int temp = a;a = b;b = temp;
}// 交换浮点型函数
void swapDouble(double &a, double &b)
{double temp = a;a = b;b = temp;
}// 利用模板提供通用的交换函数
template <typename T>
void mySwap(T &a, T &b)
{T temp = a;a = b;b = temp;
}void test01()
{int a = 10;int b = 20;// swapInt(a, b);// 利用模板实现交换// 1、自动类型推导mySwap(a, b);cout << "a = " << a << endl;cout << "b = " << b << endl;double c = 1.2;double d = 2.3;mySwap(c, d);cout << "c = " << c << endl;cout << "d = " << d << endl;// 3、显示指定类型int e=3;int f=4;mySwap<int>(e, f);cout << "e = " << e << endl;cout << "f = " << f << endl;
}int main()
{test01();// system("pause");// return 0;std::cout << "Press ENTER to continue...";std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // 读取内容直到用户输入enter键return EXIT_SUCCESS;
}
编译:
g++ test.cpp
运行结果:
总结:
- 函数模板利用关键字 template
- 使用函数模板有两种方式:自动类型推导、显式指定类型
- 模板的目的是为了提高复用性,将类型参数化
1.2.2 函数模板注意事项
注意事项:
-
自动类型推导,必须推导出一致的数据类型T,才可以使用
-
模板必须要确定出T的数据类型,才可以使用
示例:模板必须要确定出T的数据类型,才可以使用
#include <iostream>
#include <cstdlib>
#include <limits>
using namespace std;// 利用模板提供通用的交换函数
template <typename T>
void mySwap(T &a, T &b)
{T temp = a;a = b;b = temp;
}// 1、自动类型推导,必须推导出一致的数据类型T,才可以使用
void test01()
{int a = 10;int b = 20;char c = 'c';mySwap(a, b); // 正确,可以推导出一致的T// mySwap(a, c); // 错误,推导不出一致的T类型
}// 2、模板必须要确定出T的数据类型,才可以使用
template <class T>
void func()
{cout << "func 调用" << endl;
}void test02()
{// func(); //错误,模板不能独立使用,必须确定出T的类型func<int>(); // 利用显示指定类型的方式,给T一个类型,才可以使用该模板
}int main()
{test01();test02();// system("pause");// return 0;std::cout << "Press ENTER to continue...";std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // 读取内容直到用户输入enter键return EXIT_SUCCESS;
}
总结:
- 使用模板时必须确定出通用数据类型T,并且能够推导出一致的类型
示例:多类型参数模板
#include <iostream>
#include <cstdlib>
#include <limits>
using namespace std;// 利用模板提供通用的交换函数
template <typename T1, typename T2>
void mySwap(T1 &a, T2 &b)
{T1 temp = a;a = b;b = temp;
}// 1、自动类型推导,必须推导出一致的数据类型T,才可以使用
void test01()
{int a = 10;double b = 20.1;char c = 'c';mySwap(a, b); // 正确,可以推导出一致的T// mySwap(a, c); // 错误,推导不出一致的T类型
}int main()
{test01();std::cout << "Press ENTER to continue...";std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // 读取内容直到用户输入enter键return EXIT_SUCCESS;
}
1.2.3 函数模板案例(不同数据类型数组排序)
案例描述:
- 利用函数模板封装一个排序的函数,可以对不同数据类型数组进行排序
- 排序规则从大到小,排序算法为选择排序
- 分别利用char数组和int数组进行测试
示例:
#include <iostream>
#include <cstdlib>
#include <limits>
using namespace std;// 交换的函数模板
template <typename T>
void mySwap(T &a, T &b)
{T temp = a;a = b;b = temp;
}template <class T> // 也可以替换成typename
// 利用选择排序,进行对数组从大到小的排序
void mySort(T arr[], int len)
{for (int i = 0; i < len; i++){int max = i; // 最大数的下标for (int j = i + 1; j < len; j++){if (arr[max] < arr[j]){max = j;}}if (max != i) // 如果最大数的下标不是i,交换两者{mySwap(arr[max], arr[i]);}}
}template <typename T>
void printArray(T arr[], int len)
{for (int i = 0; i < len; i++){cout << arr[i] << " ";}cout << endl;
}void test01()
{// 测试char数组char charArr[] = "bdcfeagh";int num = sizeof(charArr) / sizeof(char);mySort(charArr, num);printArray(charArr, num);
}void test02()
{// 测试int数组int intArr[] = {7, 5, 8, 1, 3, 9, 2, 4, 6};int num = sizeof(intArr) / sizeof(int);mySort(intArr, num);printArray(intArr, num);
}int main()
{test01();test02();std::cout << "Press ENTER to continue...";std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // 读取内容直到用户输入enter键return EXIT_SUCCESS;
}
运行结果:
1.2.4 普通函数与函数模板的区别
普通函数与函数模板区别:
- 普通函数调用时可以发生自动类型转换(隐式类型转换)
- 函数模板调用时,如果利用自动类型推导,不会发生隐式类型转换
- 如果利用显示指定类型的方式,可以发生隐式类型转换
示例:建议调用模板函数时,使用显式指定类型的方式,不容易出错
#include <iostream>
#include <cstdlib>
#include <limits>
using namespace std;// 普通函数
int myAdd01(int a, int b)
{return a + b;
}// 函数模板
template <typename T>
T myAdd02(T a, T b) //这个表示返回类型也自动推导为 T
{return a + b;
}// 使用函数模板时,如果用自动类型推导,不会发生自动类型转换,即隐式类型转换
void test01()
{int a = 10;int b = 20;char c = 'c';cout << "myAdd01(a, c): " << myAdd01(a, c) << endl; // 正确,将char类型的'c'隐式转换为int类型 'c' 对应 ASCII码 99// myAdd02(a, c); // 报错,使用自动类型推导时,不会发生隐式类型转换cout << "myAdd02<int>(a, c): " << myAdd02<int>(a, c) << endl; // 正确,如果用显式指定类型,可以发生隐式类型转换
}int main()
{test01();std::cout << "Press ENTER to continue...";std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // 读取内容直到用户输入enter键return EXIT_SUCCESS;
}
总结:建议使用显示指定类型的方式,调用函数模板,因为可以自己确定通用类型T
1.2.5 普通函数与函数模板的调用规则
调用规则如下:
- 如果函数模板和普通函数都可以实现,优先调用普通函数
- 可以通过空模板参数列表来强制调用函数模板
- 函数模板也可以发生重载
- 如果函数模板可以产生更好的匹配,优先调用函数模板(比如普通函数需要隐式转换的情况)
示例:当普通函数与模板有冲突时,如果函数模板可以产生更好的匹配,优先调用函数模板(考虑到普通函数存在隐式转换的情况)
#include <iostream>
#include <cstdlib>
#include <limits>
using namespace std;// 普通函数与函数模板调用规则
void myPrint(int a, int b)
{cout << "调用的普通函数" << endl;
}template <typename T>
void myPrint(T a, T b)
{cout << "调用的模板" << endl;
}template <typename T>
void myPrint(T a, T b, T c)
{cout << "调用重载的模板" << endl;
}void test01()
{// 1、如果函数模板和普通函数都可以实现,优先调用普通函数// 注意 如果告诉编译器 普通函数是有的,但只是声明没有实现,或者不在当前文件内实现,就会报错找不到int a = 10;int b = 20;myPrint(a, b); // 调用普通函数// 2、可以通过空模板参数列表来强制调用函数模板myPrint<>(a, b); // 调用函数模板// 3、函数模板也可以发生重载int c = 30;myPrint(a, b, c); // 调用重载的函数模板// 4、 如果函数模板可以产生更好的匹配,优先调用函数模板char c1 = 'a';char c2 = 'b';myPrint(c1, c2); // 调用函数模板
}int main()
{test01();std::cout << "Press ENTER to continue...";std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // 读取内容直到用户输入enter键return EXIT_SUCCESS;
}
总结:既然提供了函数模板,最好就不要提供普通函数,否则容易出现二义性
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++为了解决这种问题,提供模板的重载,可以为这些特定的类型提供具体化的模板
特化(specialization)模板函数
template <>
bool myCompare(Person &p1, Person &p2){}
特化是指针对某些特定的类型,提供一种特殊的实现方式。在这个例子中,我们对模板函数myCompare进行了特化,针对Person类型的参数提供了一种特殊的实现方式。
在这个特化的模板函数中,我们使用了template <>
语法来表示这是一个特化的模板函数。在尖括号中不需要指定模板参数,因为我们已经针对特定的类型进行了特化。在函数参数列表中,我们使用了Person &p1
和Person &p2
来表示这两个参数是Person类型的引用。在函数体中,我们可以根据Person类型的特性来实现具体的比较逻辑。
这个特化的模板函数里面是空的,是因为这个函数的实现方式是根据Person类型的特性来实现的,而不是根据模板参数来实现的。因此,在这个特化的模板函数中,我们不需要再次定义模板参数。
特化模板函数的作用
特化(specialization)模板函数的作用是针对某些特定的类型,提供一种特殊的实现方式。通常情况下,模板函数的实现方式是通用的,适用于多种类型。但是,有些类型可能需要特殊的处理方式,这时候就可以使用特化模板函数来提供特殊的实现方式。
特化模板函数的使用场景比较多,例如:
- 对于某些类型,通用的实现方式可能不够高效,需要针对特定类型进行优化。
- 对于某些类型,通用的实现方式可能不够准确,需要针对特定类型进行特殊处理。
- 对于某些类型,通用的实现方式可能不可用,需要针对特定类型提供特殊的实现方式。
例如,我们可以针对std::string
类型提供一个特化的模板函数,来实现字符串的比较:
template <>
bool myCompare(std::string &s1, std::string &s2) {return s1.length() < s2.length();
}
在这个特化的模板函数中,我们使用了template <>
语法来表示这是一个特化的模板函数。在尖括号中不需要指定模板参数,因为我们已经针对特定的类型进行了特化。在函数参数列表中,我们使用了std::string &s1
和std::string &s2
来表示这两个参数是std::string类型的引用。在函数体中,我们根据字符串长度来实现比较逻辑。
特化模板函数可以提高代码的效率和可读性,使得代码更加灵活和易于维护。
特化模板函数、常规模板函数、普通函数的优先级(普通函数 > 特化的模板函数 > 模板函数)
在 C++ 中,函数模板、特化模板函数和普通函数的优先级如下:
- 普通函数(非模板函数)
- 特化模板函数
- 常规模板函数
当编译器在寻找匹配的函数时,它会首先查找普通函数,然后查找特化模板函数,最后查找常规模板函数。这种顺序可以确保在有多个候选函数时,选择最合适的函数。下面是一个例子:
#include <iostream>// 常规模板函数
template <typename T>
void foo(T t) {std::cout << "常规模板函数" << std::endl;
}// 特化模板函数
template <>
void foo<int>(int t) {std::cout << "特化模板函数" << std::endl;
}// 普通函数
void foo(int t) {std::cout << "普通函数" << std::endl;
}int main() {int a = 1;foo(a); // 输出:普通函数return 0;
}
在这个例子中,我们有一个常规模板函数、一个特化模板函数和一个普通函数。当我们调用 foo(a) 时,编译器首先查找普通函数,然后查找特化模板函数,最后查找常规模板函数。因此,输出结果是 “普通函数”。
测试,我们编译运行,然后注释掉普通函数,再编译运行,运行结果:
可以看出普通函数优先级最高,特化模板函数次之,常规模板函数最低
示例:特化的模板函数示例
#include <iostream>
#include <cstdlib>
#include <limits>
#include <string>
using namespace std;class Person
{
public:Person(string name, int age){this->m_Name = name;this->m_Age = age;}string m_Name;int m_Age;
};// 普通函数模板
template <class T>
bool myCompare(T &a, T &b)
{cout << "普通函数模板" << endl;if (a == b){return true;}else{return false;}
}// 具体化,显示具体化的原型和定意思以template<>开头,并通过名称来指出类型
// 具体化优先于常规模板
template <>
bool myCompare(Person &p1, Person &p2)
{cout << "特化函数模板" << endl;if (p1.m_Name == p2.m_Name && p1.m_Age == p2.m_Age){return true;}else{return false;}
}void test01()
{int a = 10;int b = 20;// 内置数据类型可以直接使用通用的函数模板bool ret = myCompare(a, b);if (ret){cout << "a == b " << endl;}else{cout << "a != b " << endl;}
}void test02()
{Person p1("Tom", 10);Person p2("Tom", 10);// 自定义数据类型,不会调用普通的函数模板// 可以创建具体化的Person数据类型的模板,用于特殊处理这个类型bool ret = myCompare(p1, p2);if (ret){cout << "p1 == p2 " << endl;}else{cout << "p1 != p2 " << endl;}
}int main()
{test01();test02();std::cout << "Press ENTER to continue...";std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // 读取内容直到用户输入enter键return EXIT_SUCCESS;
}
总结
- 利用具体化的模板,可以解决自定义类型的通用化
- 学习模板并不是为了写模板,而是在STL能够运用系统提供的模板
1.3 类模板
1.3.1 类模板语法
类模板作用:
- 建立一个通用类,类中的成员 数据类型可以不具体制定,用一个虚拟的类型来代表。
C++类模板是一种通用的编程工具,它可以让我们编写一种通用的类,可以用来处理多种不同类型的数据。类模板可以让我们编写一次代码,然后可以用来创建多个不同类型的类,这样可以大大减少代码的重复性,提高代码的可重用性和可维护性。
类模板可以用来实现容器类,如vector、list、map等,这些容器类可以存储不同类型的数据,而且可以动态地增加或删除元素。类模板还可以用来实现算法类,如排序、查找等,这些算法可以用来处理不同类型的数据。
总之,C++类模板是一种非常强大的编程工具,可以让我们编写通用的代码,提高代码的可重用性和可维护性,同时也可以让我们更加高效地编写程序。
语法:
template <typename T>
类
或者:
template <class T>
类
解释:
-
template — 声明创建模板
-
typename — 表面其后面的符号是一种数据类型,可以用class代替
-
T — 通用的数据类型,名称可以替换,通常为大写字母(这个在类模板中体现,就是构造类的参数)
示例
#include <iostream>
#include <cstdlib>
#include <limits>
#include <string>
using namespace std;// 类模板
template <typename NameType, typename AgeType>
class Person
{
public:Person(NameType name, AgeType age){this->mName = name;this->mAge = age;}void showPerson(){cout << "name: " << this->mName << " age: " << this->mAge << endl;}public:NameType mName;AgeType mAge;
};void test01()
{// 指定NameType 为string类型,AgeType 为 int类型Person<string, int> P1("孙悟空", 999);P1.showPerson();// 指定NameType 为string类型,AgeType 为 int类型Person<int, string> P2(5000, "999");P2.showPerson();
}int main()
{test01();std::cout << "Press ENTER to continue...";std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // 读取内容直到用户输入enter键return EXIT_SUCCESS;
}
总结:类模板和函数模板语法相似,在声明模板template后面加类,此类称为类模板
1.3.2 类模板与函数模板区别(类模板的实例化在编译时完成,函数模板的实例化在调用时完成)
类模板与函数模板区别主要有两点:
- 类模板没有自动类型推导的使用方式
- 类模板在模板参数列表中可以有默认参数
类模板和函数模板都是C++中的模板,但它们的作用和使用方式有所不同:
类模板是用来生成类的模板,它可以定义一个通用的类,可以用来处理多种不同类型的数据。类模板中可以包含成员函数、成员变量、静态成员等,可以像普通类一样使用。类模板的实例化是在编译时完成的,即在使用时根据模板参数生成具体的类。
函数模板是用来生成函数的模板,它可以定义一个通用的函数,可以用来处理多种不同类型的数据。函数模板中可以包含函数参数、返回值、局部变量等,可以像普通函数一样使用。函数模板的实例化是在调用时完成的,即在使用时根据模板参数生成具体的函数。
因此,类模板和函数模板的区别在于,类模板用来生成类,函数模板用来生成函数。类模板的实例化是在编译时完成的,函数模板的实例化是在调用时完成的。
关于“类模板的实例化在编译时完成”
类模板是一种通用的类,它可以用来处理多种不同类型的数据。在使用类模板时,需要提供具体的类型参数,然后编译器会根据这些类型参数生成具体的类。这个过程就叫做类模板的实例化。
类模板的实例化是在编译时完成的,也就是说,在编译器编译源代码时,会根据模板参数生成具体的类代码,然后将其编译成可执行文件。这样,在程序运行时,就可以直接使用这些已经生成的具体类,而不需要再进行实例化。
例如,我们定义了一个通用的类模板:
template<typename T>
class MyVector {// ...
};
当我们使用这个类模板时,需要提供具体的类型参数,例如:
MyVector<int> v1;
MyVector<double> v2;
在编译时,编译器会根据这些类型参数生成具体的类代码,例如:
class MyVector_int {// ...
};class MyVector_double {// ...
};
然后将其编译成可执行文件。这样,在程序运行时,就可以直接使用这些已经生成的具体类,而不需要再进行实例化。
关于“类模板没有自动类型推导的使用方式”
C++11引入了自动类型推导的功能,可以让编译器根据变量的初始化表达式自动推导出变量的类型。但是,类模板不支持自动类型推导的使用方式。
类模板的类型参数必须在编译时确定,而自动类型推导是在运行时确定的。因此,类模板不支持自动类型推导的使用方式。
例如,下面的代码是错误的:
template<typename T>
class MyVector {// ...
};auto v = MyVector{1, 2, 3}; // 错误:类模板不支持自动类型推导的使用方式
MyVector v{1, 2, 3}; // 错误:类模板不支持自动类型推导的使用方式
在这个例子中,我们使用了自动类型推导的方式来定义变量v,但是由于MyVector是一个类模板,编译器无法根据初始化表达式推导出MyVector的类型参数,因此会报错。
如果要使用类模板,必须显式地指定类型参数,例如:
MyVector<int> v{1, 2, 3}; // 正确:显式指定了类型参数
MyVector< int > v = MyVector< int >{1, 2, 3}; // 正确:显式指定了类型参数
在这个例子中,我们显式地指定了MyVector的类型参数为int,因此可以正确地使用类模板。
示例
#include <iostream>
#include <cstdlib>
#include <limits>
#include <string>
using namespace std;// 类模板
template <class NameType, class AgeType = int>
class Person
{
public:Person(NameType name, AgeType age){this->mName = name;this->mAge = age;}void showPerson(){cout << "name: " << this->mName << " age: " << this->mAge << endl;}public:NameType mName;AgeType mAge;
};// 1、类模板没有自动类型推导的使用方式
void test01()
{// Person p("孙悟空", 1000); // 错误 类模板使用时候,不可以用自动类型推导Person<string, int> p("孙悟空", 1000); // 必须使用显示指定类型的方式,使用类模板p.showPerson();
}// 2、类模板在模板参数列表中可以有默认参数
void test02()
{Person<string> p("猪八戒", 999); // 类模板中的模板参数列表 可以指定默认参数p.showPerson();
}int main()
{test01();test02();std::cout << "Press ENTER to continue...";std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // 读取内容直到用户输入enter键return EXIT_SUCCESS;
}
运行结果:
总结:
- 类模板使用只能用显示指定类型方式
- 类模板中的模板参数列表可以有默认参数
1.3.3 类模板中成员函数创建时机(普通类中的成员函数一开始就可以创建,类模板中的成员函数在调用时才创建)
类模板中成员函数和普通类中成员函数创建时机是有区别的:
- 普通类中的成员函数一开始就可以创建
- 类模板中的成员函数在调用时才创建
示例
#include <iostream>
#include <cstdlib>
#include <limits>
#include <string>
using namespace std;class Person1
{
public:void showPerson1(){cout << "Person1 show" << endl;}
};class Person2
{
public:void showPerson2(){cout << "Person2 show" << endl;}
};template <class T>
class MyClass
{
public:T obj;// 类模板中的成员函数,并不是一开始就创建的,而是在模板调用时再生成void fun1() { obj.showPerson1(); }void fun2() { obj.showPerson2(); }
};void test01()
{MyClass<Person1> m;m.fun1();// m.fun2();//编译会出错,说明函数调用才会去创建成员函数
}int main()
{test01();std::cout << "Press ENTER to continue...";std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // 读取内容直到用户输入enter键return EXIT_SUCCESS;
}
运行结果:
类模板除了静态成员函数外,普通成员函数在模板实例化时会生成新的函数,即使这个函数使用时不依赖于模板参数类型
如果类模板中的成员函数是一个普通成员函数,那么它在模板实例化时会生成新的函数。
例如:
template<typename T>
class MyClass {
public:void print() {std::cout << "Hello, world!" << std::endl;}
};
在这个例子中,print
函数不再依赖于模板参数类型,但它是一个普通成员函数,因此在模板实例化时会生成新的函数。无论我们使用MyClass<int>
还是MyClass<double>
,都会生成不同的函数。
MyClass<int> myInt;
myInt.print(); // 生成一个函数 void MyClass<int>::print()MyClass<double> myDouble;
myDouble.print(); // 生成一个函数 void MyClass<double>::print()
因此,如果类模板中的成员函数是一个普通成员函数,那么它在模板实例化时会生成新的函数。
1.3.4 类模板对象做函数参数(注意类模板对象
这几个字,对象不是普通对象,而是类模板对象
)
学习目标:
- 类模板实例化出的对象,向函数传参的方式
一共有三种传入方式:
- 指定传入的类型 — 直接显示对象的数据类型
- 参数模板化 — 将对象中的参数变为模板进行传递
- 整个类模板化 — 将这个对象类型 模板化进行传递
三种传参方式示例:指定传入的类型、参数模板化、整个类模板化(在下面代码中,Person就是一个类模板对象
)
#include <iostream>
#include <cstdlib>
#include <limits>
#include <string>
using namespace std;// 类模板
template <class NameType, class AgeType = int>
class Person
{
public:Person(NameType name, AgeType age){this->mName = name;this->mAge = age;}void showPerson(){cout << "name: " << this->mName << " age: " << this->mAge << endl;}public:NameType mName;AgeType mAge;
};// 1、指定传入的类型
void printPerson1(Person<string, int> &p)
{p.showPerson();
}
void test01()
{Person<string, int> p("孙悟空", 100);printPerson1(p);
}// 2、参数模板化
template <class T1, class T2>
void printPerson2(Person<T1, T2> &p)
{p.showPerson();cout << "T1的类型为: " << typeid(T1).name() << endl;cout << "T2的类型为: " << typeid(T2).name() << endl;
}
void test02()
{Person<string, int> p("猪八戒", 90);printPerson2(p);
}// 3、整个类模板化
template <class T>
void printPerson3(T &p)
{cout << "T的类型为: " << typeid(T).name() << endl;p.showPerson();
}
void test03()
{Person<string, int> p("唐僧", 30);printPerson3(p);
}int main()
{test01();test02();test03();std::cout << "Press ENTER to continue...";std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // 读取内容直到用户输入enter键return EXIT_SUCCESS;
}
运行结果:
1.3.5 类模板与继承(当子类继承的父类是一个类模板时,子类在声明的时候,要指定出父类中T的类型;如果想灵活指定出父类中T的类型,子类也需变为类模板)
当类模板碰到继承时,需要注意一下几点:
- 当子类继承的父类是一个类模板时,子类在声明的时候,要指定出父类中T的类型,如果不指定,编译器无法给子类分配内存
- 如果想灵活指定出父类中T的类型,子类也需变为类模板
示例
#include <iostream>
#include <cstdlib>
#include <limits>
#include <string>
using namespace std;template <class T>
class Base
{T m;
};// class Son:public Base //错误,c++编译需要给子类分配内存,必须知道父类中T的类型才可以向下继承
class Son : public Base<int> // 必须指定一个类型
{
};
void test01()
{Son c;
}// 类模板继承类模板 ,可以用T2指定父类中的T类型
template <class T1, class T2>
class Son2 : public Base<T2>
{
public:Son2(){cout << typeid(T1).name() << endl;cout << typeid(T2).name() << endl;}
};void test02()
{Son2<int, char> child1;
}int main()
{test01();test02();std::cout << "Press ENTER to continue...";std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // 读取内容直到用户输入enter键return EXIT_SUCCESS;
}
运行结果:
1.3.6 类模板成员函数类外实现(居然都需要把模板加上…)
学习目标:能够掌握类模板中的成员函数类外实现
示例
#include <iostream>
#include <cstdlib>
#include <limits>
#include <string>
using namespace std;// 类模板中成员函数类外实现
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;
}void test01()
{Person<string, int> p("Tom", 20);p.showPerson();
}int main()
{test01();std::cout << "Press ENTER to continue...";std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // 读取内容直到用户输入enter键return EXIT_SUCCESS;
}
运行结果:
1.3.7 类模板分文件编写(注意类模板中成员函数创建时机是在调用阶段,导致分文件编写时链接不到
问题)
学习目标:
- 掌握类模板成员函数分文件编写产生的问题以及解决方式
问题产生原因
问题:
- 类模板中成员函数创建时机是在调用阶段,导致分文件编写时链接不到
这个问题是因为C++的类模板中的成员函数的创建时机是在调用阶段,而不是在编译阶段。这意味着,如果你在一个源文件中定义了一个类模板,但是在另一个源文件中使用了这个类模板的成员函数,那么编译器在编译第二个源文件时会找不到这些成员函数的定义,从而导致链接错误。
为了解决这个问题,你需要将类模板的定义和实现都放在头文件中,然后在需要使用这个类模板的源文件中包含这个头文件。这样,编译器在编译每个源文件时都能够看到类模板的定义和实现,从而避免了链接错误。
示例(参考cg3.5):
假设我们有一个类模板 MyVector,用于表示一个动态数组。它的定义和实现分别在 MyVector.h 和 MyVector.cpp 中:
MyVector.h:
#ifndef MYVECTOR_H
#define MYVECTOR_Htemplate<typename T>
class MyVector {
public:MyVector();void push_back(const T& value);
private:T* data;int size;int capacity;
};#endif
MyVector.cpp:
#include "MyVector.h"template<typename T>
MyVector<T>::MyVector() {data = nullptr;size = 0;capacity = 0;
}template<typename T>
void MyVector<T>::push_back(const T& value) {// ...
}
现在我们在另一个源文件 main.cpp 中使用了 MyVector 的成员函数:
#include "MyVector.h"int main() {MyVector<int> vec;vec.push_back(1);return 0;
}
当我们编译 main.cpp 时,编译器会找不到 MyVector<T>::MyVector()
和 MyVector<T>::push_back()
的定义,从而导致链接错误。
为了解决这个问题,我们需要将 MyVector.h 中的类模板定义和实现都放在头文件中:
MyVector.h:
#ifndef MYVECTOR_H
#define MYVECTOR_Htemplate<typename T>
class MyVector {
public:MyVector() {data = nullptr;size = 0;capacity = 0;}void push_back(const T& value) {// ...}
private:T* data;int size;int capacity;
};#endif
这样,在 main.cpp 中包含 MyVector.h 就可以了:
#include "MyVector.h"int main() {MyVector<int> vec;vec.push_back(1);return 0;
}
编译运行:
解决方法
解决:
- 解决方式1:直接包含.cpp源文件
- 解决方式2:将声明和实现写到同一个文件中,并更改后缀名为.hpp,hpp是约定的名称,并不是强制(主流的解决方法)
示例
person.hpp中代码:
#pragma once
#include <iostream>
using namespace std;
#include <string>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;
}
类模板分文件编写classTemplateSubFile.cpp中代码:
#include<iostream>
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;
}
编译运行:
1.3.8 类模板与友元(1. 友元全局函数类内实现 2. 友元全局函数类外实现)
关于友元,不懂的可以参考之前的笔记:【黑马程序员 C++教程从0到1入门编程】【笔记4】C++核心编程(类和对象——封装、权限、对象的初始化和清理、构造函数、析构函数、深拷贝、浅拷贝、初始化列表、友元friend、运算符重载)
学习目标:
- 掌握类模板配合友元函数的类内和类外实现
全局函数类内实现 - 直接在类内声明友元即可
全局函数类外实现 - 需要提前让编译器知道全局函数的存在
示例
#include <iostream>
#include <cstdlib>
#include <limits>
#include <string>
using namespace std;// 2、全局函数配合友元 类外实现 - 先做函数模板声明,下方在做函数模板定义,在做友元
template <class T1, class T2>
class Person;// 如果声明了函数模板,可以将实现写到后面,否则需要将实现体写到类的前面让编译器提前看到
// template<class T1, class T2> void printPerson2(Person<T1, T2> & p);template <class T1, class T2>
void printPerson2(Person<T1, T2> &p)
{cout << "类外实现 ---- 姓名: " << p.m_Name << " 年龄:" << p.m_Age << endl;
}template <class T1, class T2>
class Person
{// 1、全局函数配合友元 类内实现friend void printPerson(Person<T1, T2> &p){cout << "类内实现 ---- 姓名: " << p.m_Name << " 年龄:" << p.m_Age << endl;}// 全局函数配合友元 类外实现friend void printPerson2<>(Person<T1, T2> &p);public:Person(T1 name, T2 age){this->m_Name = name;this->m_Age = age;}private:T1 m_Name;T2 m_Age;
};// 1、全局函数在类内实现
void test01()
{Person<string, int> p("Tom", 20);printPerson(p);
}// 2、全局函数在类外实现
void test02()
{Person<string, int> p("Jerry", 30);printPerson2(p);
}int main()
{test01();test02();std::cout << "Press ENTER to continue...";std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // 读取内容直到用户输入enter键return EXIT_SUCCESS;
}
运行结果:
1.3.9 类模板案例(用类模板构建一个泛型数组)
案例描述: 实现一个通用的数组类,要求如下:
- 可以对内置数据类型以及自定义数据类型的数据进行存储
- 将数组中的数据存储到堆区
- 构造函数中可以传入数组的容量
- 提供对应的拷贝构造函数以及operator=防止浅拷贝问题
- 提供尾插法和尾删法对数组中的数据进行增加和删除
- 可以通过下标的方式访问数组中的元素
- 可以获取数组中当前元素个数和数组的容量
示例
myArray.hpp
#pragma once
#include <iostream>
using namespace std;template <class T>
class MyArray
{
public:// 构造函数MyArray(int capacity){this->m_Capacity = capacity;this->m_Size = 0;pAddress = new T[this->m_Capacity]; // pAddress = new T[this->m_Capacity]; 是在堆内存中分配了一块大小为 m_Capacity * sizeof(T) 的连续内存空间,并将其首地址赋值给指针 pAddress。这里使用了 new 运算符来动态分配内存,T 是模板参数,可以是任意类型。}// 拷贝构造MyArray(const MyArray &arr){this->m_Capacity = arr.m_Capacity;this->m_Size = arr.m_Size;this->pAddress = new T[this->m_Capacity];for (int i = 0; i < this->m_Size; i++){// 如果T为对象,而且还包含指针,必须需要重载 = 操作符,因为这个等号不是 构造 而是赋值,// 普通类型可以直接= 但是指针类型需要深拷贝this->pAddress[i] = arr.pAddress[i];}}// 重载= 操作符 防止浅拷贝问题MyArray &operator=(const MyArray &myarray){if (this->pAddress != NULL){delete[] this->pAddress; // 在 C++ 中,使用 new 运算符动态分配数组内存空间时,编译器会在分配内存空间时,额外存储一个整数,用于记录数组的大小。这个整数通常被称为“数组长度”,它存储在数组内存空间的前面,占用 4 个字节(32 位系统)或 8 个字节(64 位系统)的空间。当我们使用 delete[] 运算符释放动态分配的数组内存空间时,编译器会自动读取数组长度,并根据数组长度计算出整个数组所占用的内存空间的大小,然后释放该内存空间。因此,我们不需要手动计算数组的大小,也不需要手动释放数组内存空间,编译器会自动完成这些工作。// 这里的 delete[] this->pAddress; 是用来释放 this->pAddress 所指向的动态数组内存空间的语句。delete[] 是 C++ 中用来释放动态分配的数组内存空间的运算符,它会调用数组元素的析构函数,并释放数组所占用的内存空间。在这个代码中,this->pAddress 指向的是一个动态分配的数组,通过 delete[] this->pAddress; 释放该数组所占用的内存空间,避免了内存泄漏的问题。this->m_Capacity = 0;this->m_Size = 0;}this->m_Capacity = myarray.m_Capacity;this->m_Size = myarray.m_Size;this->pAddress = new T[this->m_Capacity];for (int i = 0; i < this->m_Size; i++){this->pAddress[i] = myarray[i];}return *this;}// 重载[] 操作符 arr[0]T &operator[](int index){return this->pAddress[index]; // 不考虑越界,用户自己去处理}// 尾插法void Push_back(const T &val){if (this->m_Capacity == this->m_Size){return;}this->pAddress[this->m_Size] = val;this->m_Size++;}// 尾删法void Pop_back(){if (this->m_Size == 0){return;}this->m_Size--;}// 获取数组容量int getCapacity(){return this->m_Capacity;}// 获取数组大小int getSize(){return this->m_Size;}// 析构~MyArray(){if (this->pAddress != NULL){delete[] this->pAddress;this->pAddress = NULL;this->m_Capacity = 0;this->m_Size = 0;}}private:T *pAddress; // 指向一个堆空间,这个空间存储真正的数据int m_Capacity; // 容量int m_Size; // 大小
};
arrayClassEncapsulation.cpp
#include "myArray.hpp"
#include <string>
#include <limits>void printIntArray(MyArray<int> &arr)
{for (int i = 0; i < arr.getSize(); i++){cout << arr[i] << " ";}cout << endl;
}// 测试内置数据类型
void test01()
{MyArray<int> array1(10);for (int i = 0; i < 10; i++){array1.Push_back(i);}cout << "array1打印输出:" << endl;printIntArray(array1);cout << "array1的大小:" << array1.getSize() << endl;cout << "array1的容量:" << array1.getCapacity() << endl;cout << "--------------------------" << endl;MyArray<int> array2(array1);array2.Pop_back();cout << "array2打印输出:" << endl;printIntArray(array2);cout << "array2的大小:" << array2.getSize() << endl;cout << "array2的容量:" << array2.getCapacity() << endl;cout << "--------------------------" << endl;
}// 测试自定义数据类型
class Person
{
public:Person() {}Person(string name, int age){this->m_Name = name;this->m_Age = age;}public:string m_Name;int m_Age;
};void printPersonArray(MyArray<Person> &personArr)
{for (int i = 0; i < personArr.getSize(); i++){cout << "姓名:" << personArr[i].m_Name << " 年龄: " << personArr[i].m_Age << endl;}
}void test02()
{// 创建数组MyArray<Person> pArray(10);Person p1("孙悟空", 30);Person p2("韩信", 20);Person p3("妲己", 18);Person p4("王昭君", 15);Person p5("赵云", 24);// 插入数据pArray.Push_back(p1);pArray.Push_back(p2);pArray.Push_back(p3);pArray.Push_back(p4);pArray.Push_back(p5);printPersonArray(pArray);cout << "pArray的大小:" << pArray.getSize() << endl;cout << "pArray的容量:" << pArray.getCapacity() << endl;cout << "--------------------------" << endl;
}int main()
{test01();test02();std::cout << "Press ENTER to continue...";std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // 读取内容直到用户输入enter键return EXIT_SUCCESS;
}
运行结果:
相关文章:
【黑马程序员 C++教程从0到1入门编程】【笔记8】 泛型编程——模板
https://www.bilibili.com/video/BV1et411b73Z?p167 C泛型编程是一种编程范式,它的核心思想是编写通用的代码,使得代码可以适用于多种不同的数据类型。 而模板是C中实现泛型编程的一种机制,它允许我们编写通用的代码模板,然后在需…...
分享10个精美可视化模板,解决95%的大屏需求!
前段时间和朋友一起喝茶,我吐槽着excel表格做报表的繁琐,他惊讶的问我竟然不知道大屏模板这种东西,说是直接套用数据就可以,我震惊的同时吃下了这个安利。 回来之后,我好好研究了一番这个叫可视化大屏的“新鲜玩意儿”…...
好用的项目管理软件的具体功能有哪些
随着企业规模不断的扩大,项目管理往往会面临更多的挑战与难题,最常见的会出现以下几个问题:资源消耗失控,而项目部门和相关部门之间沟通越来越困难;团队凝聚力下降、项目进度难以把控,项目成本几乎失控&…...
< 每日小技巧: 基于Vue状态的过渡动画 - Transition 和 TransitionGroup>
》基于Vue状态的过渡动画 - Transition 和 TransitionGroup 👉 一、Vue Transition 简介> Transition 和 TransitionGroup 之间的区别 👉 二、<Transition> 组件> 触发 <Transition> 组件的场景:> 基于 CSS 的过渡效果&…...
vmware安装redhat 8
vmware安装redhat 8 1、下载镜像文件1.1 镜像文件 2、安装系统2.1、选择自定义安装2.2、兼容性选择2.3、选择镜像文件导入2.4、设置用户名密码2.5、选择虚拟机在磁盘上的位置2.6、选择处理器数量2.7、选择内存大小2.8、选择桥接或NAT2.9、选择SCSI控制器类型2.10、选择虚拟机磁…...
OpenCV C++案例实战三十一《动态时钟》
OpenCV C案例实战三十一《动态时钟》 前言一、绘制表盘二、绘制刻线三、获取系统时间四、结果展示五、源码总结 前言 本案例将使用OpenCV C实现动态时钟效果。原理也很简单,主要分为绘制表盘、以及获取系统时间两步。 一、绘制表盘 首先为了效果显示美观一点&…...
字节后端入门 - Go 语言原理与实践
1.1什么是Go语言 1.2Go语言入门 环境 1.3基础语法 1.3.1变量 var name"value" 自己推断变量类型; 也可以显式类型 var c int 1 name: type(value) 常量: const name "value" g : a"foo" 字符串拼接 1.3.2 if else {}花括号…...
锂电材料浆料匀浆搅拌设备轴承经常故障如何处理?
锂电材料浆料匀浆搅拌设备是锂电池生产中重要的设备之一,用于将活性材料、导电剂、粘结剂和溶剂混合成均匀的浆料,是电极制备过程中不可或缺的步骤。然而,由于高速搅拌和化学腐蚀等因素的影响,轴承经常会出现故障,导致…...
设计模式——设计模式介绍和单例设计模式
导航: 【黑马Java笔记踩坑汇总】JavaSEJavaWebSSMSpringBoot瑞吉外卖SpringCloud黑马旅游谷粒商城学成在线设计模式牛客面试题 目录 一、设计模式概述和分类 1.1 设计模式介绍 1.2 设计模式分类 二、创建型设计模式-单例模式 2.1 介绍 2.2 八种单例模式的创…...
利用Iptables构建虚拟路由器
利用Iptables构建虚拟路由器 (1)修改网络类型 在VMware Workstation软件中选择“编辑→虚拟网络编辑器”菜单命令,在虚拟网络列表中选中VMnet1,将其配置为“仅主机模式(在专用网络内连接虚拟机)”&#x…...
C++——类和对象[中]
0.关注博主有更多知识 C知识合集 目录 1.类的默认成员函数 2.构造函数和析构函数基础 3.构造函数进阶 4.析构函数进阶 5.拷贝构造函数 6.运算符重载 7.日期类 7.1输入&输出&友元函数 8.赋值运算符重载 9.const成员函数 9.1日期类完整代码 10.取地址重载 …...
Symbol.iterator和Symbol.asyncIterator
Symbol是什么? symbol是ES6标准中新增的一种基本数据类型,symbol 的值是通过 Symbol()函数返回的,每一个 symbol 的值都是唯一的,即使传入相同的描述值。 注:Symbol 函数不允许通过 new 的方式调用 Symbol的作用是什…...
忆暖行动|“他一个人推着老式自行车在厚雪堆的道路上走,车上带着学生考试要用的司机”
忆暖行动|“他一个人推着老式自行车在厚雪堆的道路上走,车上带着学生考试要用的sj” 一头白发,满山青葱 在那斑驳的物件褶皱中,透过泛黄的相片,掩藏着岁月的冲刷和青葱的时光。曾经的青年早已经不复年轻,但是那份热爱…...
Python中True、False、None的判断(避坑)
2.4 Python中True、False、None的判断 在Python中,所有的空值和0在作为条件表达式时,隐式的进行bool转换后都是False,比如:空列表:[]、空字符串:‘’、空字典:{}等等。 from icecream import …...
Spring Bean定义有哪些方式?
概述 对于学习Spring的兄弟姐妹来说,觉得这个问题很熟悉,若是要把它回答得很清楚,却是很为难?平时写代码的时候,不会在意这些概念问题,但面试时这个问题出现的频率却是很高,所以还是必须要掌握…...
JVM内存模型的演变
1,背景 class文件、类的加载过程。我们的class文件就要进入到JVM内存里,我们沿着经典的JDK1.6,JDK1.7,JDK1.8看看在其中都经历了哪些改变 概念的统一: 方法区: 方法区可以看作是JVM逻辑上管理一片区域的…...
DataX3同步Mysql数据库数据到Mysql数据库和DataX3同步mysql数据库数据到Starrocks数据库
DataX3同步Mysql数据库数据到Mysql数据库和DataX3同步mysql数据库数据到Starrocks 一、认识DataX二、DataX3概览三、DataX3框架设计四、DataX3插件体系五、DataX3核心架构六、DataX 3六大核心优势1.可靠的数据质量监控2.丰富的数据转换功能3.精准的速度控制4.强劲的同步性能5.健…...
你是否曾经为自己写的代码而感到懊恼?那如何才能写出高质量代码呢?
这里写目录标题 一、 前言二、高质量代码的特征三、编程实践技巧1. 遵循编码规范2. 使用有意义的变量名和函数名3. 减少代码重复4. 使用注释5. 编写单元测试6. 使用设计模式7. 使用版本控制工具8. 保持代码简洁9. 优化代码性能10. 学习和借鉴他人的代码总结 一、 前言 写出高质…...
常用 Composition API【VUE3】
二、常用 Composition API 7. 计算属性与监视 7.1 computed函数 与Vue2.x中computed配置功能一致写法 <template><h1>一个人的信息</h1>姓:<input type"text" v-model"person.firstName"><br><br>名&a…...
--商业模式--
O2O O2O,网络用语中指Online To Offline的缩写,即在线离线/线上到线下,是指将线下的商务机会与互联网结合,让互联网成为线下交易的平台。 O2O概念最早来源于美国。O2O的概念非常广泛,既可涉及到线上,又可…...
JavaWeb《HTML基础标签》
本笔记学习于Acwing平台 MDN官方文档https://developer.mozilla.org/zh-CN/ 目录 1. html文件结构 2. 文本标签 3. 图片 4. 音频和视频 5. 超链接 6. 表单 7. 列表 8. 表格 9. 语义标签 10. 特殊符号 1. html文件结构 文档结构 html的所有标签为树形结构ÿ…...
ChatGpt 能取代人类吗?
目录 前言 一、ChatGpt是什么? 二、ChatGpt能做什么 总结 前言 随着人工智能的不断发展,很多人都开启了学习机器学习,以及现在ChatGpt的出现,对人类社会带来了很多变化。 智能化交流方式:ChatGpt的出现为人们提供了…...
PHP内存溢出Allowed memory size of 解决办法
以前追踪过这个问题,但是那个时候工具用的不太好,没看的这么细,这次搞的比较细,修正了偶以前的看法 .于是写小文一篇总结一下. PHP偶尔会爆一下如下 错误Allowed memory size of xxx bytes exhausted at xxx:xxx (tried to allocate xxx bytes) 不想看原理的,直接跳到最后…...
重回代码,学习总结
回顾加总结 2021年 自动化测试 1.ETL 数据库开发维护(oracle pl/sql) 2.自动化测试(javaseleniumcucumber) 2022年 功能测试 1.功能测试(学习测试用例,postman测试) 2.性能测试(jmeter初学) 2023年 测试开发 1.学习了…...
【Leetcode -86.分隔链表 -92.反转链表Ⅱ】
Leetcode Leetcode -86.分隔链表Leetcode -92.反转链表Ⅱ Leetcode -86.分隔链表 题目:给你一个链表的头节点 head 和一个特定值 x ,请你对链表进行分隔,使得所有 小于 x 的节点都出现在 大于或等于 x 的节点之前。 你应当 保留 两个分区中每…...
算法记录 | 48 动态规划
198.打家劫舍 思路: 1.确定dp数组(dp table)以及下标的含义:dp[i]:前 i 间房屋所能偷窃到的最高金额。 2.确定递推公式:dp[i] max(dp[i - 2] nums[i-1], dp[i - 1]) i间房屋的最后一个房子是nums[i−…...
CRM部署Always on 后 CRM报无法更新数据库,数据库只读,且读写分离不正常
CRM部署Always on 后 CRM报无法更新数据库,数据库只读,读写分离不正常 问题描述背景信息问题原因解决方案 问题描述 CRM部署Always on 后 CRM报无法更新数据库,数据库只读 读写分离不正常,出现错乱链接。 背景信息 1.2个节点配置SQL serve…...
麓言信息设计创意思维,打开设计师思路
在现在快速发展的时代,信息纷杂繁琐,如果一个设计不能让人眼前一亮,印象深刻,只会沦为平凡作品,无亮点无用处。正所谓,无设计不创意,这句口号正是喊出对设计的要求。 伴随着时代的发展、…...
POJ3704 括号匹配问题 递归方法
目录 题目 算法 完整代码 题目 参考 递归: https://blog.csdn.net/qq_45272251/article/details/103257953 利用了递归, 但思路稍复杂了 循环: https://blog.csdn.net/weixin_50340097/article/details/114579805 (看起来是递归其实是循环. 每次递归其实是循环内一次迭…...
leetcode — JavaScript专题(三):完全相等的 JSON 字符串、复合函数、 分组、柯里化、将对象转换为 JSON 字符串
专栏声明:只求用最简单的,容易理解的方法通过,不求优化,不喜勿喷 2628. 完全相等的 JSON 字符串 题面 给定两个对象 o1 和 o2 ,请你检查它们是否 完全相等 。 对于两个 完全相等 的对象,它们必须包含相…...
沈阳市住房和城乡建设厅网站/上海互联网管理系统推广公司
OSPF(Open Shorterst Path First)是一种根据OSI的IS-IS协议提出来的一种链路状态型路由协议。它是IGP协议的一种。由于采用最小生成树算法,因此不会产生回路。另外,OSPF支持可变长子网掩码。但这种链路状态型路由协议也有其问题&a…...
兼职做任务赚钱的网站有哪些/最新搜索引擎排名
1638: [Usaco2007 Mar]Cow Traffic 奶牛交通 Time Limit: 5 Sec Memory Limit: 64 MBSubmit: 618 Solved: 217[Submit][Status]Description 农场中,由于奶牛数量的迅速增长,通往奶牛宿舍的道路也出现了严重的交通拥堵问题.FJ打算找出最忙碌的道路来重点整治. 这个牧区包括一个…...
iapp怎么把网站做软件/最新国际新闻热点事件
众所周知,Go 在做依赖管理时会创建两个文件,go.mod 和 go.sum。相比于 go.mod,关于 go.sum 的资料明显少得多。自然,go.mod 的重要性不言而喻,这个文件几乎提供了依赖版本的全部信息。而 go.sum 看上去就是 go module …...
网站的日志文件/磁力狗最佳搜索引擎
1、C中有两个方面体现重用: (1)面向对象的思想:继承和多态,标准类库。 (2)泛型程序设计(generic programming) 的思想: 模板机制,以及标准模板库 STL。 将一些常用的数据…...
wordpress免费插件分享/建网站建设
CAS 详解Ⅰ CAS 是什么?Ⅱ CAS 底层原理Ⅲ CAS 的缺点A. 循环时间长开销很大B. 只能保证一个共享变量的原子操作C. ABA 问题① ABA 问题是什么② ABA 问题的解决Ⅰ CAS 是什么? CAS(Compare and Swap),即比较再交换。…...
网站设计论文前言怎么写/世界杯32强排名
public class Test { //外部类public static void main(String[] args) {EnclosedClazz enclosedClazz new EnclosedClazz();//这里返回的实际是一个InnerSon对象Father f enclosedClazz.test();//定义在test()里的内部类对象is,存活到现在//父类引用指向子类对…...