第1章:C++泛型技术基础:模板——《C++泛型:STL原理和应用》读书笔记整理

时间:2019-11-18
本文章向大家介绍第1章:C++泛型技术基础:模板——《C++泛型:STL原理和应用》读书笔记整理,主要包括第1章:C++泛型技术基础:模板——《C++泛型:STL原理和应用》读书笔记整理使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

第1章:C++泛型技术基础:模板

1.2 关于模板参数
1.2.1 模板参数类型
  1. 类型参数
      typename声明的参数都属于类型参数,它的实参必须为系统内置或者用户自定义的数据类型,包括类模板实体,由类模板产生的类模板实体,本质上就是类。

  2. 非类型参数
      C++允许人们在模板参数列表中像函数参数列表中那样定义普通变量或者对象。定义的普通变量不能被修改,因为模板参数是在预编译期间进行传递并且被编译的。仅支持可以转换为Int类型的变量(double都不行!)、枚举、指针、引用。

     template<typename T, int b>
  3. 模板定义型参数
      C++也允许以类模板的定义作为类模板参数,之所以需要这种参数,其目的就是除了强调这个参数的实参必须为类模板外,还强调这个类模板所具有的参数个数。

     //其中T就是一个单参数类模板
     template<template<typename S>class T>
1.2.2 模板形参和实参的结合
  1. 函数模板实参的隐式提供
      实参的隐式提供需要提供返回值的类型,或者可以使用auto+decltype大法推导返回值的类型。
  2. 指针实参
      指针其实是类型参数的一种,使用4字节的存储空间。
  3. 修饰符const和&的使用
      修饰符const和&只是对模板参数的修饰,对模板参数类型无太大影响
1.3 特化模板和模板具现规则
1.3.1 特化模板(特例化模板)
  1. 函数模板中的特化模板
      没啥好说的就是普通的特化。不过模板函数也是可以偏特化的,比如一个双参数函数,是可以特化第一个参数的。
  2. 类模板的特化和偏特化
      偏特化就是指特化一个模板参数。
  3. 模板的具现
      在源文件中,我们可以使用多种方式编写一个泛型的程序功能模块,他可能是特化模板、偏特化模板以及全特化模板,或者这几种泛型模块共存。显然编译器需要一个模板具现的规则,优先生成哪个模板的实体。编译器对于一个模块调用的具现优先规则应为:
特化模板 > 偏特化模板 > 普通模板 > 系统
1.4 右值引用与模板
1.4.1 右值引用

  左值值既可以出现在赋值运算符的左边和右边,右值只可以出现在赋值运算法的右边。左值之所以可以出现在赋值运算符的左边,就是因为这种表达式代表一块存储空间,可以接受并保存数据。程序可以通过其变量名获取地址,并用这个地址访问数据。右值仅能代表数据。右值表达式要么是数据本身,要么是一个能得出结果的运算表达式,尽管它也占据一定的存储空间,但是因为它没有名字,也不能从其表达式中提取这个空间的地址。因此这种表达式只能出现在赋值运算符的右边,而且仅能代表生命期与其所在语句相同的临时对象
  关于引用,在C++11之前,左值可以定义两种引用:左值常引用和左值引用。对于右值,C++11之前仅定义了一种常量引用。对右值进行常量引用可以延长右值的生命期,从而使后续程序可以利用它的信息,遗憾的就是它是常量,不能满足程序更多的要求。
  为了能充分利用临时对象,C++11标准推出了一种新的数据类型——右值的非常量引用,简称右值引用。

T&& name = rvalue;
1.4.2 右值引用应用1——转移语义

深拷贝与浅拷贝:

#include <iostream> 
using namespace std;
class Student {
private:
    int num;
    char *name;
public:
    Student();
    ~Student();
};
Student::Student(){
    name = new char(20);
    cout << "Student" << endl;
}
Student::~Student(){
    cout << "~Student " << (int)name << endl;
    delete name;
    name = NULL;
} 
int main()
{
    Student s1;
    // 使用了默认拷贝函数,默认拷贝函数进行的是浅拷贝
    // 因此在析构的时候会对同一个内存空间释放两次,造成内存泄漏。
    Student s2(s1);
    return 0;
}

  浅拷贝优点是速度快,节省资源,缺点是共享了资源,容易引起内存泄漏。深拷贝不会引起泄漏,但是每次拷贝都消耗大量资源。

转移语义:

//关闭RVO,return value optimistic,返回值优化
//不关闭在返回右值的时候会跳过移动构造函数,直接构造对象。
g++ -fno-elide-constructors e1_b.cpp -o e1_b && ./e1_b

Foo fuct(){
    Foo foof(100);  //产生局部变量,生命周期只有在fuct()函数中,在函数返回的时候会被析构。
    return foof;    //这里其实是调用了Foo类的拷贝函数,拷贝了一份作为返回值。
}

fuct();         
        Foo(Foo&& r)    //调用拷贝函数,返回一份拷贝
        ~Foo()          //函数中的局部变量生命周期结束
        ~Foo()          //函数返回值的生命周期结束,在主函数return的时候

//为什么会调用了一次析构函数呢?
//因为foo1的生命周期是整个主函数,少的那一次在主函数return的时候会被调用
//不是初始化!没有调用构造函数!声明了一个常引用接受fuct()的返回值。
const Foo& foo1 = fuct();   
        Foo(Foo&& r)    //拷贝局部变量作为返回值
        ~Foo()          //局部变量声明周期结束

//与上相同,只是一个是常引用,另一个是非常量引用
Foo&& foo2 = fuct();
        Foo(Foo&& r)
        ~Foo()

//隐式调用构造函数和显式调用构造函数!
Foo foo3(fuct());
Foo foo3 = Foo(fuct());
        Foo(Foo&& r)    //拷贝作为返回
        ~Foo()          //析构局部变量
        Foo(Foo&& r)    //调用拷贝构造函数,创建对象foo3,
        ~Foo()          //返回值声明周期结束,调用析构函数

return 0!

  在某些情况下,被拷贝的对象是右值,意味着其生命周期即将结束,此时如果我们再去消耗资源开辟新空间就显得浪费了。因为被拷贝对象即将”死亡“,我们不妨借用一下这个右值的内存空间!这就是语义转移,即使用浅拷贝共享内存空间后,将”强迫“原对象放弃资源控制权(指针为空),避免内存空间被释放。通过这种方式,原对象(右值)的内存空间,被我们转移(窃取)出来,用于新对象,通过这种方式,我们极大地节省了开销。

1.4.3 右值引用应用2——转移函数move()

  看到了右值引用的好处,左值引用也想利益共沾。但是右值引用,必须参数要是右值才能完成语义转移,因为左值在逻辑上是不允许被窃取内存空间的。

T&& std::move(T&);
void swap(T& a, T& b){
    T tmp = std::move(a);
    a = std::move(b);
    b = std::move(tmp);
}
1.4.4 右值引用应用3——参数完美转发模板

  在程序设计实践中,函数模板经常需要调用一些外部函数实现它的功能,这时候就需要模板为这些函数传递参数,人们习惯上把模板的这种行为叫做参数转发。对于模板来说,它的任务就是参数的忠实转发,一不能改变参数特性,二不能产生额外开销。如果模板能把所有数据类型都按照上述要求进行转发,那么这个模板就是一个完美参数转发者。

//方案一:
void Func(int);     //目标函数
template<typename T>
Tmp(T);             //转发模板
//结果:这种方式在Tmp(10),参数为右值的时候,在经模板转发后变成了左值

//方案二:
void Func(int v);
template<typename T>
Tmp(T&);
//结果:无法转发右值,Tmp(100)报错,因为模板为左值引用
//改进,Tmp(const T&),此时模板转发后参数变为常引用,可能不符合某些函数要求

//方案三:
void Func(int& v);
template<typename T>
Tmp(T&& a){
    Func(a);
}
//结果:C++11前无法通过,因为C++11前还没有右值引用T&&,以及对多个引用符&连用的数据类型进行推导的功能
/*      多重引用推导表
实参类型    模板参数    推导类型
int&        T&          int&
int&&       T&          int&
int&        T&&         int&
int&&       T&&         int&&

lvalue      T&&         T&      左值 + T&& = T&
rvalue      T&&         T       右值 + T&& = T(左值)
*/
//在这种推导情况下,如果Tmp的参数为右值,在转发后参数类型就会被推导为左值,这与我们的预期不同。
//对此我们在Tmp函数内对参数进行强制的类型转换
Tmp(T&& a){
    Func(static_cast<T&&>(a));
}
//因为左值+T&& = T&&, T& + T&& = T&, 实现了参数的完美转发。
//为了区别于move()和static_cast,并使之更具有语义性,C++11将static_cast()封装在函数模板std:forward()。
Tmp(T&& a){
    func(std::forward<T>(a));
}

原文地址:https://www.cnblogs.com/azhao/p/11881231.html