C++ 小白 学习记录16
2021/9/30 17:11:04
本文主要是介绍C++ 小白 学习记录16,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
第十六章 模版与泛型编程
OOP: 能处理类型在程序运行之前都未知的情况
泛型编程: 在编译时就能获知类型
泛型代码的两个重要原则:
- 模板中的函数参数是const的应用(使用const可以接受更多的类型, 拷贝/不可拷贝/const/非const的)
- 函数体重的条件判断仅使用<比较运算符(减少对类型的要求, <可以计算出!=, >, ==).
16.1 定义模版
16.1.1 函数模版
代表 任意类型. 类型参数
template <typename T, typename W, typename A > // 指定一个类型为T int compare(const T& v1, const W& v2) { // 模版中指定的类型 if (v1 < v2) return -1; if (v2 < v1) return 1; return 0; }
模版参数列表的作用很像函数参数列表
类型参数前必须使用class 或 typename, 在模板参数列表中, 这俩没有什么不同.
类型已经指定了, 代表任意值. 非类型参数, 要求必须是常量表达式.
template<unsigned N, unsigned M> int compare(const char(&p1)[N], const char(&p2)[m]) { return strcmp(p1, p2); }
inline和constexpr 的函数模板:
inline或constexpr放在模板参数之后, 返回类型之前
template <typename T> inline T min(const T&, const T&);
函数模板和类模板成员函数的定义通常放在头文件中.
16.1.2 类模板
编译器不能为类模板推断模板参数类型. 感觉就像 需要使用string的地方全部换成T.
template<typename T> class Blob { public: typedef T value_type; typedef typename std::vector<T>::size_type size_type; Blob():data(std::make_shared<std::vector<T>>()) {}; Blob(std::initializer_list<T> il) :data(std::make_shared<std::vector<T>>(il)) {}; 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 pop_back(); T& back(); T& operator[](size_type i); private: std::shared_ptr<std::vector<T>> data; Blob check(size_type i, const T& msg) const; };
模板形成的各个类 之间没有任何关系.
当在类外完成内类声明的函数时, 仍然需要使用template<typename T> 声明类型T, 并且 需要用到类名的地方, 必须在类名后面增加<T>, 如 check函数的定义为:
template<typename T> Blob<T> Blob<T>::check(size_type i, const T& msg) const { // 类模板外使用 类模板名时需要带<T> // xx }
使用类模板时:
int main(int argc, char** argv) { Blob<int> ia = { 0,1,2,3,4 }; }
模板嵌套, 和在类模板作用域内时可以省略<T>, 在类模板外使用类模板名时需要BlobPtr<T>
template <typename T> class BlobPtr { public: BlobPtr() :curr(0) {} BlobPtr(Blob<T> &a, size_t sz=0):wptr(a.data), curr(sz) {} // 使用别的类模板时, 需要Blob<T> T& operator*() const { auto p = check(curr, "dereference past end"); return (*p)[curr]; } BlobPtr& operator++(); // 类模板作用域内 不需要使用BlobPtr<T>& operator++() BlobPtr& operator--(); // 等同于 BlobPtr<T>& operator--() private: std::shared_ptr<std::vector<T>>check(std::size_t, const std::string&) const; std::weak_ptr<std::vector<T>> wptr; std::size_t curr; };
类模板和友元:
类模板包含的非模板友元, 则友元可以访问所有模板实例
类模板包含模板友元, 则所有友元实例可以访问所有模板实例, 也可以只授权特定友元.
一对一友好关系:
template <typename> class BlobPtr; // Blob内定义友元类 使用 template<typename> class Blob; // 用于operator==的函数声明 template<typename T> // 用于operator==的函数声明 bool operator==(const Blob<T>&, const Blob<T>&); // 函数声明 template<typename T> class Blob { public: // typedef T value_type; typedef typename std::vector<T>::size_type size_type; friend class BlobPtr<T>; //友元类 friend bool operator==<T>(const Blob<T>&, const Blob<T>&); // 友元函数声明 // xx }
上述代码中, 友元类的声明中 BlobPtr<T> 和友元函数 在Blob中的声明中, 使用的类型都是T, 如此则限定了, BlobPtr, operator= 中使用的类型必须和Blob中的类型一致. 即: 友好关系 被限定在所有使用相同类型的Blob, BlobPtr, operator== 中.
通用和特定的模板友好关系:
分为两种, 一种是非模板类 对模板类:
template<typename T> class Pal; //类模板的前置声明 class C { friend class Pal<C>; // 只用用C实例化的类Pal才是C的一个友元. template <typename T> friend class Pal2; // Pal2的所有实例都是C的友元, 此种情况不需要前置声明 };
另一种, 模板类对 其他:
template<typename T> class Pal; // 类模板的前置声明 template<typename T> class C2 { friend class Pal<T>; // 类型为T的C2的实例和Pal的实例才是友好关系. 一对一 template <typename X> friend class Pal2; // X可能等于T,也可能不等于T, 所有就是任意类型的C2的实例和Pal2实例都是友元关系, 多对多 friend class Pal3; // Pal3 不需要前置声明, 所有类型的C2实例都与Pal3 是友元关系, 多对一. };
上面代码中, 两种情况下都是一对一的需要前置声明, 其余的则不需要.
模板参数也可以声明为友元:
template<typename T> class B { friend T; };
模板类型别名: 因为Blob<T>不是一个确切的类型, 所有不能使用typedef 重命名, 必须是确定的类型才可以重命名; 但是 可以这么命名
typedef Blob<T> StrBlob; // 错误 typedef Blob<string> StrBlob; template<typename T> using twin = pair<T, T>; // 定义 twin<string>authors; // == pair<string, string> 使用方法 template<typename T> using twin2 = pair<int, T>; // 也可以固定一个类型
类模板的static成员: 所有的类模板实例 都共享相同的static成员
template<typename T> class Foo { public: static std::size_t cout() { return ctr; } private: static std::size_t ctr; }; template<typename T> size_t Foo<T>::ctr = 0; // 定义 跟非static成员定义相似 int main(int argc, char** argv) { Foo<int> fi; auto ct = Foo<int>::cout(); ct = fi.cout(); ct = Foo::cout(); // 错误, 无法确定模板实例, 所以无法使用cout函数 }
static 成员需要在模板外定义初始值
模板外部使用模板名时需要携带类型列表
// 16.16 template <typename T> class Vec { public: Vec() :elements(nullptr), first_free(nullptr), cap(nullptr) {} Vec(const Vec&); Vec(const std::initializer_list<T>&); Vec& operator=(const Vec&); Vec& operator=(Vec&&) noexcept; Vec(Vec&&) noexcept; ~Vec(); void push_back(const T&); void push_back(const T&&); size_t size() const { return first_free - elements; } size_t capacity() const { return cap - elements; } void resize(size_t); void resize(size_t, const T&); // 若 new_cap 大于当前的 capacity() ,则分配新存储,否则该方法不做任何事。 void reserve(size_t); // 增加vector的容量到大于或者等于 指定的值 T* begin() const { return elements; } T* end() const { return first_free; } private: static std::allocator<T> alloc; // 用于分配元素内存 // 如果没有空间容纳新元素了, 则重新分配内存 void chk_n_alloc() { if (size() == capacity()) reallocate(); } // 用于分配足够的内存保存给定范围的元素, 并将这些元素拷贝到新分配的内存中.返回的pair中的两个指针 // 分别指向新空间的开始位置和尾后位置. std::pair<T*, T*>alloc_n_copy(const T*, const T*); void free(); // 销毁元素并释放内存 void reallocate(size_t); // 获得更多内存并拷贝已有元素 T* elements; // 指向首元素的指针 T* first_free; // 指向第一个空闲元素的指针 T* cap; // 指向尾后位置的指针 }; template<typename T> std::allocator<T> Vec<T>::alloc = std::allocator<T>(); template<typename T> void Vec<T>::push_back(const T& s) { chk_n_alloc(); alloc.construct(first_free++, s); } template<typename T> void Vec<T>::push_back(const T&& s) { chk_n_alloc(); alloc.construct(first_free++, std::move(s)); } template<typename T> std::pair<T*, T*>Vec<T>::alloc_n_copy(const T* b, const T* e) { auto data = alloc.allocate(e - b); return { data, uninitialized_copy(b,e, data) }; } template<typename T> void Vec<T>::free() { // elements 如果为空 说明该vec为空, 则不必释放内存 if (elements) { for_each(elements, first_free, [this](T& s) { alloc.destroy(&s); }); alloc.deallocate(elements, cap - elements); } } template<typename T> Vec<T>::Vec(const Vec<T>& s) { auto newData = alloc_n_copy(s.begin(), s.end()); elements = newData.first; first_free = cap = newData.second; } template<typename T> Vec<T>::~Vec() { free(); } template<typename T> Vec<T>& Vec<T>::operator=(const Vec<T>& rhs) { auto data = alloc_n_copy(rhs.begin(), rhs.end()); free(); elements = data.first; first_free = cap = data.second; return *this; } template<typename T> Vec<T>& Vec<T>::operator=(Vec<T>&& rrs) noexcept { if (this != &rrs) { free(); elements = rrs.elements; first_free = rrs.first_free; cap = rrs.cap; rrs.elements = rrs.first_free = rrs.cap = nullptr; } return *this; }; template<typename T> void Vec<T>::reallocate(size_t new_size) { size_t newcapacity = 0; if (new_size == 0) newcapacity = size() ? 2 * size() : 1; else newcapacity = new_size; auto newdata = alloc.allocate(newcapacity); auto dest = newdata; auto elem = elements; for (size_t i = 0; i != size(); ++i) alloc.construct(dest++, std::move(*elem++)); free(); elements = newdata; first_free = dest; cap = elements + newcapacity; } template<typename T> void Vec<T>::reserve(size_t new_cap) { if (new_cap > capacity()) { reallocate(new_cap); } } template<typename T> void Vec<T>::resize(size_t count) { resize(count, T()); } template<typename T> void Vec<T>::resize(size_t count, const T& s) { if (count > size()) { if (count > capacity()) reallocate(2 * count); for (size_t t = size(); t != count; ++t) { alloc.construct(first_free++, s); } } else if (count < size()) { while (first_free != elements + count) alloc.destroy(--first_free); } } template<typename T> Vec<T>::Vec(const std::initializer_list<T>& lst) { auto newdata = alloc_n_copy(lst.begin(), lst.end()); elements = newdata.first; first_free = cap = newdata.second; } template<typename T> Vec<T>::Vec(Vec<T>&& s) noexcept :elements(s.elements), first_free(s.first_free), cap(s.cap) { s.elements = s.first_free = s.cap = nullptr; }
16.1.3 模板参数
- 模板参数名的可用范围为 其声明之后 至 模板声明或定义结束之前
- 模板参数隐藏外层作用域中声明的相同名字
- 参数名不能被重新定义
模板声明时必须包含模板参数, 声明和定义中使用typename 后的名字可以不同
使用类的类型成员:
默认通过作用域运算符访问的名字不是类型, 是静态成员, 如vec模板中的alloc
template<typename T> std::allocator<T> Vec<T>::alloc = std::allocator<T>();
如果希望使用一个模板类型参数的类型成员, 则必须使用typename 明确
template<typename T> typename T::value_type top(const T& c) { if (!c.empty()) return c.back(); else return typename T::value_type(); }
当希望通知编译器一个名字表示类型时, 必须使用typename, 而不能使用class.
// 这个需要练习, 琢磨
默认模板实参:
template<typename T, typename F=less<T>()> // less<T> less<int> 比较函数 bool compare(const T& v1, const T& v2, F f) { if (f(v1, v2)) return -1; if (f(v2, v1)) return 1; return 0; }
模板默认实参与类模板:
无论何时使用一个类模板(类作用于内 可以不用) , 都必须在模板名后接上尖括号.
template<typename T = int> class Numbers { // 默认为int类型 typename 也可以用class public: Numbers(T v = 0) :val(v) {}; private: T val; }; int main(int argc, char** argv) { Numbers<long double> lots; Numbers<> aaa; // 默认int }
16.1.4 成员模板
类内是模板的成员函数叫成员模板, 不能是虚函数. 成员模板 有自己的参数类型
template<typename T> class NewCall { template<typename TT> void funInC(TT& a) { //xx } template<typename TT> void funInC2(TT&); }; template<typename T> // 需要类 类型参数, 同时需要成员模板 类型参数 template<typename WW> // 此处的类型参数可以与 类内定义的类型参数不同 void NewCall<T>::funInC2(WW& w) { //xx };
实例化与成员模板: 编译器会根据传入的类 类型推断 模板类的类型, 会根据成员函数传入的参数推断 成员模板 的类型.
16.1.5 控制实例化
当模板被使用时才会进行实例化. 显式实例化
extern template declaration; // 实例化声明 template declaration; // 实例化定义 // 如 extern template class Blob<string>; // 声明 template int compare(const int&, const int&); // 定义
当编译器遇到extern声明时, 不会在本文件中实例化代码, 并且承诺在程序其他位置有该实例化的一个非extern声明/定义. 可以有多个extern声明, 必须只有一个定义.
extern 必须出现在任何使用模板之前, 否则会自动实例化.
实例化定义 会实例化所有成员.
可能模板会被实例化的几种情况
- 声明一个类模板的指针和引用,不会引起类模板的实例化,因为没有必要知道该类的定义。
- 定义一个类类型的对象时需要该类的定义,因此类模板会被实例化。
- 在使用sizeof()时,因为需要计算对象的大小,编译器必须根据类型将其实例化出来
- new 类模板被实例化
- 引用类模板的成员会导致类模板被编译器实例化。
- 需要注意的是,类模板的成员函数本身也是一个模板。标准C++要求这样的成员函数只有在被调用或者取地址的时候,才被实例化
16.1.6 效率和灵活性
//16.28 练习题 参考 https://github.com/pezy/CppPrimer/tree/master/ch16
16.2 模版实参推断
16.2.1 类型转换与模板类型参数
const转换: 非const的引用/指针 可以传递给一个const的
template<typename T> T fobj(T, T); template<typename T> T fref(const T&, const T&); string s1("aaaaaa"); const string s2("bbbb"); fobj(s1, s2); // s2 的const被忽略 fref(s1, s2); // s1 非const转为const
数组或函数指针: 如果形参不是引用类型, 则数组/函数类型的实参可以应用正常的指针转换, 即 数组实参转换为一个指向首元素的指针, 函数实参转换为一个指向该函数类型的指针.
template<typename T> T fobj(T, T); template<typename T> T fref(const T&, const T&); int a[10], b[11]; fobj(a, b); // a[10] -> int*, b[11]-> int* fobj(int*, int*) fref(a, b); // 错误的 引用类型的形参时, 实参不转换 a[10], b[11],不是一种类型
将实参传递给带模板类型的函数形参时, 只有const转换, 数组和函数到指针的转换.
如果函数参数类型不是模板参数, 则对实参进行正常的类型转换.
// 16.34 template<typename T> int compare2(const T&, const T&); compare2("hi", "world"); // 不合法, "hi" 转化为const char[3], "world" 转化为const char[6] 两者类型不同 compare2("bye", "daa"); // 合法, 都转化为const char(&)[4]
16.2.2 函数模板显示实参
在函数名的后面跟上<类型> 可以显式声明模板实参 如:
template<typename T> bool compare(const T& v1, const T& v2) { if (v1<v2) return -1; if (v2<v1) return 1; return 0; } // 使用 auto a = compare<std::string>("helo", "world");
16.2.3 尾置返回类型与类型转换
当出现 需要返回值类型, 但是在没确定参数类型时 无法确定返回值类型的 情况时, 需要使用尾置指明返回值类型:
template<typename It> auto fcn(It beg, It end) -> decltype(*beg) { return *beg; }
尾置返回 允许我们在参数列表之后 声明返回类型.
上面的代码只能返回元素的引用, 如果想返回一个元素的拷贝 则需要使用下面的代码
template<typename It> auto fcn2(It beg, It end) -> typename remove_reference<decltype(*beg)>::type { return *beg; }
remove_reference 为标准库函数, 其作用是 脱去引用类型的 引用部分, 其中的type 则返回脱去引用部分后剩余的类型.
如: remove_reference<int&>::type 的值为int.
类似remove_reference<T>::type 的函数还有:
16.2.4 函数指针和实参推断
可以通过函数指针 实例化 函数模板. 重载的函数需要明确究竟是哪个版本的模板.
template<typename T> int compare(const T&, const T&); // 函数重载 void func(int(*)(const string&, const string&)); void func(int(*)(const int&, const int&)); // 使用 int (*pf1)(const int&, const int&) = compare; // T = int func(compare<int>); // 调用重载的func, 但是需要指明compare到底是哪个? 是string版本还是int版本
当参数是一个 函数模板实例的地址 时, 程序必须满足 对每个模板参数,能唯一确定其类型或值
16.2.5 模板实参推断和引用
从左值引用函数参数推断类型:
template<typename T> void f1(T&) 需要传入的类型 必须是一个左值. 如果传入的const int, 则T就是const int
template<typename T>void f2(const T&) 可以传入任何参数包括一个右值. 此时会忽略掉顶层const. 如f2(ci) 如果ci 是const int的, 则T 仍然是int的
template<typename T>void f3(T&&) 右值推断和左值相同
引用折叠和右值引用参数:
两种列外:
- 当我们将一个左值(如i)传递给函数的右值引用参数(如f3), 且此右值引用指向模板类型参数(如T&&)时, 编译器推断模板类型参数为实参(i)的左值引用类型. 所以f3(i) 中 T的类型为int&
- 引用会折叠. 如 X& &, X& &&, X&& & 都会折叠成类型X&, X&& && 折叠成 X&&
引用折叠只能应用于简介创建的引用的引用, 如果类型别名或模板参数.
16.2.6 std::move
static_cast 可以显式的将一个左值转换为一个右值引用.
template<typename T> typename remove_reference<T>::type&& move(T&& t) { return static_cast<typename remove_reference<T>::type&&>(t); } // 当move传入的是一个左值时, 就变成string&& move(string &t) // static_cast<string&&>(t) t的类型为string&, cast 将一个左值引用转化为右值引用
16.2.7 转发
可以利用 模板参数 是右值引用的特性, 保留 函数的参数的所有属性, 即: 如果一个函数参数是指向模板类型参数的右值引用(T&&), 它对应的实参的const属性和左值/右值属性将得到保持. 当用于一个指向模板参数类型的右值引用函数参数(T&&)时, forward 会保持实参类型的所有细节.
template<typename F, typename T1, typename T2> void flip(F f, T1&& t1, T2&& t2) { f(std::forward<T2>(t2), std::forward<T1>(t1)); } // 此种定义可以完美保存t1, t2的属性, 并实现反转
16.3 重载与模版
函数模板可以被另一个模板或普通非模板函数重载.
重载后的匹配规则:
- 对于一个调用, 其候选函数包括所有模板参数推断成功的函数模板实例
- 候选的函数模板总是可行的, 因为模板实参推断会排除任何不可行的模板.
- 可行函数(模板和非模板)按类型转换来排序
- 如果有一个函数提供比任何其他函数都更好的匹配, 则选择此函数.如果有多个函数提供同样好的匹配, 则:
- 如果同样好的函数中只有一个是非模板函数, 则选择此函数.
- 如果同样好的函数中没有非模板函数, 而有多个函数模板, 且其中一个模板比其他模板更特例化, 则选择此模板
- 否则, 此调用有歧义
16.4 可变参数模版
可以接收可变数目参数的模板函数或模板类. 可变数目的参数 -- 参数包. 符号 ... 省略号,
typename... T
两种参数包:
- 模板参数包 -- 零或多个 模板参数
- 函数参数包 -- 零或多个函数参数
template<typename T, typename ... Args> void foo(const T& t, const Args& ... rest) {}
sizeof... 运算符
当我们需要知道包中有多少元素时, 可以使用sizeof... 运算符
template<typename T, typename ... Args> void foo(const T& t, const Args& ... rest) { cout << sizeof...(Args) << endl; }
16.4.1 编写可变参数函数模板
可变参数函数通常是递归的. 每次处理包中的第一个实参, 然后用剩余的实参调用自身.
template<typename T> ostream& print(ostream& os, const T& t) { return os << t; } template<typename T, typename ... Args> ostream &print(ostream &os, const T &t, const Args& ... rest) { os << t << ", "; return print(os, rest...); // 此处, 每次解析rest中的第一个实参 绑定到t, 剩余的部分 继续作为rest 绑定到形参中的rest上 }
16.4.2 包扩展
即: 模板参数包/模板函数参数包 的解开操作, 如上面的print可变参数模板函数, 当传入
print(cout, i, s, 42) 时, 模板函数将会被实例化成:
print(ostream&, const int&, const string&, const int&);
当需要调用函数对包内元素逐个执行函数的操作时, 需要:
template <typename ... Args> ostream& errorMsg(ostream& os, const Args& ... rest) { return print(os, debug_rep(rest)...) // debug_rep 也是一个模板函数, 其接受一个参数, 并打印输出参数的内容 }
16.4.3 转发参数包
转发时 定义形参为 右值版本
template<typename ... Args> inline void StrVec::emplace_back(Args&& ... args) { chk_n_alloc(); alloc.construct(first_free++, std::forward<Args>(args)...); }
16.5 模版特性化
当特例化一个函数模板时, 必须为原模板中的每个模板参数提供实参. 还应在template周免跟上<>. 也就是说 此处特例化定义时 使用的类型 必须是前面定义的模板的类型相匹配.
// 特例化一个compare模板 template<> int compare(const char* const& p1, const char* const& p2) { return strcmp(p1, p2); }
template后面应该跟的typename,用于指定模板中使用的类型, 但是因为是特例化的, 本身指定了模板中使用的类型, 所以, template后面只跟<> 而未定义类型.
const char* const & 一个指向const char的const指针的引用
函数重载与模板特例化:
一个特例化版本 本质上是一个实例, 而非函数名的一个重载版本.
模板及其特例化版本应该声明在同一个头文件中, 所有同名模板的声明应该放在前面, 然后是这些模板的特例化版本. 否则编译器很难发现没有特例化声明的错误.
类模板特例化:
类模板部分特例化:
部分特例化的类 仍然是模板类
特例化成员而不是类: 只特例化成员函数而非整个模板.
template<typename T> struct Foo2 { Foo2(const T &t = T()):mem(t) {} void Bar(); T mem; }; template<> void Foo2<int>::Bar(){} // 特例化Foo<int>的成员Bar // 特例化以后 当使用int以外的类型实例化Foo时, 跟其他情况一样 // 当使用int类型实例化Foo时, 除了Bar其余跟其他一样
这篇关于C++ 小白 学习记录16的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-11-22怎么实现ansible playbook 备份代码中命名包含时间戳功能?-icode9专业技术文章分享
- 2024-11-22ansible 的archive 参数是什么意思?-icode9专业技术文章分享
- 2024-11-22ansible 中怎么只用archive 排除某个目录?-icode9专业技术文章分享
- 2024-11-22exclude_path参数是什么作用?-icode9专业技术文章分享
- 2024-11-22微信开放平台第三方平台什么时候调用数据预拉取和数据周期性更新接口?-icode9专业技术文章分享
- 2024-11-22uniapp 实现聊天消息会话的列表功能怎么实现?-icode9专业技术文章分享
- 2024-11-22在Mac系统上将图片中的文字提取出来有哪些方法?-icode9专业技术文章分享
- 2024-11-22excel 表格中怎么固定一行显示不滚动?-icode9专业技术文章分享
- 2024-11-22怎么将 -rwxr-xr-x 修改为 drwxr-xr-x?-icode9专业技术文章分享
- 2024-11-22在Excel中怎么将小数向上取整到最接近的整数?-icode9专业技术文章分享