1. 1. 内存的分区模型
  2. 2. 函数进阶
    1. 2.0.1. 函数的默认参数
    2. 2.0.2. 函数的占位参数
    3. 2.0.3. 函数重载
  • 3. 面向对象
    1. 3.0.1. 构造函数的分类和调用
    2. 3.0.2. 拷贝构造函数的调用时机
    3. 3.0.3. 构造函数调用规则
    4. 3.0.4. 深拷贝和浅拷贝
    5. 3.0.5. 初始化列表
    6. 3.0.6. 类对象作为类成员
    7. 3.0.7. 静态成员
    8. 3.0.8. C++对象内存模型和this指针
    9. 3.0.9. const修饰成员函数
  • 4. 友元
  • 5. 运算符重载
  • 6. 继承
    1. 6.0.1. 继承方式
    2. 6.0.2. 继承中的对象模型
    3. 6.0.3. 继承中构造和析构顺序
    4. 6.0.4. 继承中同名成员的处理方式
    5. 6.0.5. 菱形继承
  • 7. 多态
    1. 7.0.1. 内部实现
    2. 7.0.2. 纯虚函数
    3. 7.0.3. 虚析构和纯虚析构
  • 8. C++ 文件操作
  • C++提高编程
    1. 1. 泛型编程(模板)
    2. 2. 函数模板
    3. 3. 类模板
      1. 3.0.1. 类模板和函数模板的区别
      2. 3.0.2. 成员函数创建时期
      3. 3.0.3. 类模板对象作函数参数
      4. 3.0.4. 类模板与继承
      5. 3.0.5. 类模板成员函数的类外实现
      6. 3.0.6. 类模板分文件编写
      7. 3.0.7. 类模板与友元
  • 有人查漏补缺,有人精卫填海,有人开天辟地(bushi

    内存的分区模型

    代码区:存放函数体的二进制代码
    全局区:存放全局变量、静态变量、常量
    栈区:由编译器自动分配/释放,存放函数参数和局部变量等
    堆区:由程序员分配和释放,若未释放则在程序结束时由操作系统回收

    代码区的特点:共享(多次执行只需要一份代码即可),只读(防止意外修改)
    全局区的特点:数据在程序结束后由操作系统释放
    栈区的特点:保存在栈区的数据在函数执行完即自动释放
    堆区的特点:用new开辟,由程序员指定分配

    1
    2
    3
    4
    int *p=new int(10);//在堆区开辟了一块int的内存,里面存的值为10
    delete p;//释放了这一块内存
    int *p=new int[10];//开辟一块10个整型数据的数组(连续)

    引用(给变量起别名),指向同一片内存空间
    意义:
    要求:

    • 一定要初始化
    • 一旦初始化了,就不可以更改了
      1
      2
      int a;
      int b=&a;

    参数传递

    1. 值传递
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      void swap(int a,int b)
      {
      int temp=a;
      a=b;
      b=temp;
      }
      int a=10;
      int b=10;
      swap(a,b);
      //实参的值不会被改变
    2. 地址传递
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      void swap(int* a,int* b)
      {
      int temp=a;
      a=b;
      b=temp;
      }
      int a=10;
      int b=10;
      swap(&a,&b);
      //实参的值会被改变
    3. 引用传递
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      void swap(int& a,int& b)
      {
      //使用引用接收参数
      int temp=a;
      a=b;
      b=temp;
      }
      int a=10;
      int b=10;
      swap(a,b);
      //实参的值会被改变

    引用作函数返回值
    含义:如果一个函数的返回值是引用,那么这个函数可以作为左值

    • 不要返回局部变量的引用
      1
      2
      3
      4
      5
      6
      7
      8
      9
      int& yy()
      {
      static int a=10;
      return a;
      }
      int& ref=yy()
      yy()=1000;
      cout<<ref;
      //此时输出的是1000
      引用的本质:一个指针常量,指针不可以修改,但是指针指向的值可以修改
      由编译器完成(引用→指针常量)的过程
      常量引用:
      使用场景:用来修饰形参,使其只读,防止误操作
      1
      void yy(const int &val);

    函数进阶

    函数的默认参数

    类型 名(参数名=默认值){}

    1
    2
    3
    4
    5
    void qwq(int a,int b=20){return a+b;}
    //优先使用传入的数据
    cout<<qwq(20);
    //传回40

    • 若某位置有默认值,则该位置往后必须全部有默认参数(规定)
    • 声明和实现只可以有一个有默认参数(规定)
    • 如果函数参数值>传入参数值,则从第一个参数开始向后填充,若某参数未被填充且没有默认值,则会报错

    函数的占位参数

    调用函数时必须填补该位置

    1
    2
    3
    4
    void qwq(int a,int){return a+b;}
    cout<<qwq(2040);
    //传回40

    函数重载

    满足条件:

    • 同一个作用域下
    • 函数名称相同
    • 函数参数(类型/个数/顺序)不同
    • 函数返回值并不作为函数重载的条件
      注意事项:
    • 引用可以作为重载的条件
    • 重载遇到默认参数时,警惕UB(二义性)报错

    面向对象

    基本特性:继承、多态、封装

    • struct和class的区别:
      • struct的默认权限为公共
      • class默认权限为私有

    构造函数的分类和调用

    调用方式:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    class qwq{
    private:
    int a;
    int b;
    qwq(int a,int b){
    //有参构造函数
    this.a=a;
    this.b=b;
    }
    qwq(const qwq &q)
    {
    //拷贝构造函数
    this.a=q.a;
    this.b=q.b;
    }
    ~qwq()
    {
    //析构函数
    }
    };
    //括号法
    //如果使用默认构造,不要写括号
    qwq q=new qwq(10,20);
    //显示法(相关知识点:匿名对象)
    qwq q=qwq(10,20);
    qwq q1=qwq(q);
    //不要利用拷贝构造函数初始化匿名对象(qwq(q1)会被视为qwq q1)
    //隐式转换法
    qwq q=10;//适用于构造函数单参数的情况
    qwq q1=q;

    拷贝构造函数的调用时机

    • 使用一个对象来初始化另一个对象
    • 以值传递的方式给函数参数赋值
      自动调用拷贝构造函数创造副本
    • 以值方式返回局部对象
      此时被返回的对象会被拷贝构造

    构造函数调用规则

    • 默认存在构造函数、析构函数、拷贝构造函数
    • 如果定义有参构造函数,只会提供默认拷贝构造
    • 如果定义拷贝构造函数,不会提供其他构造函数

    深拷贝和浅拷贝

    浅拷贝:简单的复制拷贝,默认的拷贝构造函数拷贝方法
    容易带来的问题:若存在堆区空间的操作,容易造成重复释放与重复操作
    深拷贝:在堆区重新申请空间,进行数据拷贝

    初始化列表

    qwq():a(10),b(20){}
    //一个让a的默认值为10,b的默认值为20的无参构造函数
    qwq(int a,int b):a(a),b(b){}
    //一个让a的值为a,b的值为b的有参构造函数

    类对象作为类成员

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class A{}
    class B
    {
    A a;
    }
    /*
    构造和析构顺序:
    A构造
    B构造
    B析构
    A析构
    即先构造类对象,再构造自身,先析构自身,再析构类对象
    析构顺序与构造相反
    */

    静态成员

    静态成员变量特点:

    • 所有对象共享统一数据
    • 在编译阶段即分配内存
    • 类内声明,类外初始化
    • 仍然存在访问权限
      1
      2
      3
      4
      //类内:
      static int a;
      //类外:
      int qwq::a=100;
      静态成员变量访问方式:
    • 通过对象进行访问
      1
      2
      qwq q;
      cout<<q.a;
    • 通过类名进行访问
      1
      cout<<qwq::q;

    静态成员函数特点:

    • 所有对象共享同一个函数
    • 静态成员函数只能访问静态成员变量
    • 存在访问权限
      静态成员函数调用方法:
    • 通过对象进行访问
      1
      2
      qwq q;
      cout<<q.a();
    • 通过类名进行访问
      1
      cout<<qwq::q();

    C++对象内存模型和this指针

    只有非静态成员变量才属于类的对象
    成员变量和成员函数是分开存储的
    静态成员变量和成员函数只有一份拷贝
    this指针:指向被调用的成员函数所属的对象
    奇怪的用法:可以使用空指针调用成员函数

    1
    2
    3
    4
    qwq* p=NULL;
    p->any();
    //如果调用的函数没有调用实体成员,则可以被正常运行
    //但是很不推荐这么干

    const修饰成员函数

    • const修饰的函数成为常函数
    • 常函数不可以修改成员属性
    • 可以修改有关键字mutable的成员变量

    友元

    关键字: friend
    作用:让一个函数或者类区访问另一个类的私有成员
    三种实现方式:

    • 全局函数作友元
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      class qwq
      {
      friend void visit(qwq *q);
      //此时,visit函数可以访问qwq的私有成员
      public:
      ...
      private:
      ...
      }
      void visit(qwq *q)
      {
      ...
      }
    • 类作友元
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      class qaq;
      //提前声明,之后实现
      class qwq
      {
      friend class qaq;
      //qaq类作为qwq类的友元,可以访问私有成员
      public:
      ...
      private:
      int a;
      }
      class qaq
      {
      public:
      void visit(qwq *q)
      {
      cout<<q->a;
      }
      private:
      ...
      }
    • 成员函数作友元
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      class qaq;
      //提前声明,之后实现
      class qwq
      {
      friend void qaq::visit();
      //qaq类下的visit作为qwq类的友元,可以访问私有成员
      public:
      ...
      private:
      int a;
      }
      class qaq
      {
      public:
      void visit(qwq *q)
      {
      cout<<q->a;
      }
      private:
      ...
      }

    运算符重载

    作用:对已有的运算符进行重新定义,以适应不同的数据类型
    函数名:operator*(被重载的运算符)
    注意:

    1. 对于内置的数据类型的运算是不可以重载的
    2. 不要滥用,尽量让运算符和实际作用相符
    • 加号运算符重载
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      class Person
      {
      //通过成员函数重载
      Person operator+(Person *p)
      {
      //左值为this指针指向的类
      //右值为p指针指向的类
      Person temp;
      temp.p_a=this->p_a+p->p_a;
      temp.p_b=this->p_b+p->p_b;
      return temp;
      }
      private:
      int p_a;
      int p_b;
      }
      //通过全局函数重载
      Person operator+(Person &p1,Person &p2)
      {
      Person temp;
      temp.p_a=this->p_a+p->p_a;
      temp.p_b=this->p_b+p->p_b;
      return temp;
      }
      Person p3 = p1 + p2;
    • 左移运算符重载
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      ostream operator(ostream &cout,Person &p)
      {
      //有一点像是java中.toString()的意思
      cout<<p.a<<" "<<p.b;
      return cout;
      //返回类型为ostream
      //如果不是ostream,不可以在cout中继续往后追加
      //那么如果输出的时候要访问私有函数呢?
      //友元!friend!
      //在类内写friend ostream operator(ostream &cout,Person &p),使该方法成为原有类的友元
      }
    • 递增运算符重载
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      //重载前置++运算符
      //写在类内
      Person& operator++()
      {
      p_a++;
      return *this;
      }
      //重载后置++运算符
      Person& operator++(int)
      //int是占位参数
      //c++无法通过返回类型来实现多态,因此需要以参数类型来区分函数
      {
      Person temp=*this;
      //先记录,在自增,再返回记录值
      p_a++;
      return temp;
      }
      //前置递增返回引用,后置递增返回值
      //原因:如果后置递增返回对象,那么temp会在函数执行后被销毁,引用会成为野指针
    • 赋值运算符重载
      知识点:C++编译器会给一个类添加赋值运算符=对属性进行值拷贝(默认为浅拷贝)
    • 复习!浅拷贝在什么情况下会出问题!
    • 答:涉及到堆区内存申请/释放的时候
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    //赋值运算符注意事项
    //应该先检查是否有在堆区的内存,如果有,先释放,再申请新的
    //因为赋值运算符并不一定只在拷贝构造的时候被调用,还有可能会出现两个现有对象之间互相等来等去
    class Person
    {
    //通过成员函数重载
    Person operator+(Person *p)
    {
    //左值为this指针指向的类
    //右值为p指针指向的类
    Person temp;
    temp.p_a=this->p_a+p->p_a;
    temp.p_b=this->p_b+p->p_b;
    return temp;
    }
    private:
    int *p_a;
    int p_b;
    Person& operator=(Person *p)
    {
    if(p_a!=NULL)
    {
    //检查是否有已经占有的堆区内存,若有则释放
    delete p_a;
    p_a=NULL;
    }
    p_a=new int(*p.p_a);
    return *this;
    }
    }

    • 函数调用运算符()重载
      //因为长得真的很像自定义函数,又称之为仿函数
      //仿函数没有固定写法,非常灵活
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      class Person
      {
      //通过成员函数重载
      Person operator()(String text)
      {
      cout<<text;
      }
      }
      int main()
      {
      Person p;
      p("qwq!");
      //此时会打印qwq!
      //注意上文中括号前为对象名,不是类名
      }

    继承

    基本语法:
    class 派生类名:继承方式1 基类1,继承方式2 基类2……
    C++中一个类可以由多个类派生而来
    多继承中如果涉及到同名问题,加作用域以访问不同父类中的成员
    但是在实际开发中不建议使用多继承

    继承方式

    继承后private成员不可用,成员权限会有所改变

    • 公有继承
      不改变权限
    • 私有继承
      public&protected->private
    • 保护继承
      public->protected

    继承中的对象模型

    基类所有的非静态成员变量,在派生类中仍然存在,只是被编译器隐藏了

    继承中构造和析构顺序

    继承后在类初始化时,基类和派生类的构造函数和析构函数都会被分别调用
    具体调用顺序:先调用基类构造函数,再调用派生类构造函数,析构函数顺序相反

    继承中同名成员的处理方式

    在继承后,同名成员会共存(静态成员也是)
    访问派生类中同名成员,可以直接访问
    访问基类中同名成员,需要加作用域
    如果基类和派生类中存在同名函数,父类中同名函数会被隐藏,如果要访问,需要加作用域

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    class qaq 
    {
    public:
    int a;
    int b;
    qaq()
    {
    a = 20;
    b = 10;
    }
    };
    class qwq :public qaq
    {
    public:
    int a;
    int b;
    qwq() {
    this->a = 30;
    this->b = 240;
    }
    ~qwq()
    {
    //析构函数
    }
    };
    int main()
    {
    //Solution s = Solution();
    //cout << func(10,40);
    qwq q;
    cout << q.a;//此时输出的是qwq中的a
    cout << q.qaq::a;//此时输出的是qaq中的a
    //cout << s.maxProduct(v);

    }

    菱形继承

    定义:
    两个派生类继承同一个基类,某个类同时继承这两个派生类
    可能导致的问题:数据重复,资源浪费
    解决方案:虚继承(在第一次继承的两个派生类上发生)(关键词virtual)
    vbptr(Virtual Base Pointer):虚基类指针,指向虚基类表

    • 指针占4个内存嗷
      采用虚继承后,来自同一个上层基类的数据会只存在一份拷贝,对于直接继承的两个基类,存在一个vbptr,指向虚基类表中的偏移量,即为那一份拷贝在多少位之后

    多态

    • 静态多态:函数重载和运算符重载,复用函数名,依靠传入参数类型和个数区分
      编译阶段确定函数地址
    • 动态多态:派生类和虚函数实现运行时多态
      运行阶段确认函数地址
      如何实现动态多态?
      函数前加virtual关键字,使其变成虚函数
      class animal
      {
      public:
      virtual void speak(){
      cout<<”animal speak”;
      }
      }
      class cat: public animal
      {
      public:
      virtual void speak(){
      cout<<”cat speak”;
      }
      }
      class dog: public animal
      {
      public:
      virtual void speak(){
      cout<<”dog speak”;
      }
      }
      void doSpeak(Animal &animal)//也可以是指针
      {
      animal.spaek();
      //可以传入animal,dog,cat
      //此时若调用doSpeak函数,会根据传入的参数选择调用哪一个类中的speak函数
      }

    内部实现

    vfptr(Virtual Function Pointer):虚函数指针
    指向虚函数表vftable(Virtual Function Table)
    虚函数表内容:函数->入口地址
    动态多态的原理:每个类都维护一个虚函数表(函数->入口地址),如果它重写了父类的函数,那么会用新函数的地址替代旧函数的地址
    以上文的代码为例,cat类重写了speak函数,当cat被传入doSpeak函数中时,调用animal.speak,
    此时animal是对该cat对象的引用,所以会从cat类的虚函数表中查找speak函数,查找到的就是输出”cat speak”

    纯虚函数

    virtual 返回值类型 函数名 (参数) =0;
    有纯虚函数的类一定是抽象类,无法被实例化,子类必须重写纯虚函数
    可以类比java中的abstract方法

    虚析构和纯虚析构

    现存的问题:使用多态时,若子类有属性在堆区需要在析构函数时调用delete释放,那么父类指针在释放时(delete 父类;)无法调用到子类的析构代码,只会调用父类的析构函数
    解决方法:将父类的析构函数改为虚析构或者纯虚析构

    1
    2
    3
    4
    //虚析构
    vritual ~类名(){};
    //纯虚构造析构
    virtual ~类名()=0;

    虚析构和纯虚析构的共性:

    • 可以解决堆区内存释放问题
    • 需要有具体的函数实现
      区别:
    • 有纯虚析构的类为抽象类,无法被实例化,派生类必须重写析构函数

    C++ 文件操作

    包含库:
    文件分为两种:

    1. 文本文件
    2. 二进制文件
      操作文件的三大类:
    3. ofstream 写操作
    4. ifstream 读操作
    5. fstream 读写操作
      总体操作流程
      创建流对象->open(“path”,way)->写入数据->close
      |—-|—-|
      |打开方式|解释|
      |ios::in|为读文件而打开文件|
      |ios::out|为写文件而打开文件|
      |ios::ate|初始位置:文件尾|
      |ios::app|追加方式写文件|
      |ios::trunc|先删除,再创建新的|
      |ios::bindry|以二进制方式打开文件|
    • 可以使用|运算符,同时以多种方式打开文件

    C++提高编程

    泛型编程(模板)

    建立一个通用模具,提高代码复用性

    函数模板

    1
    2
    3
    4
    5
    6
    7
    8
    9
    //template 创建模板的声明
    //typename 声明抽象类型
    template<typename T>
    void swap(T &a, T &b)
    {
    T temp=a;
    a=b;
    b=temp;
    }

    使用方法:

    1
    2
    3
    4
    5
    //自动类型推导
    //直接使用
    swap(a,b);
    //显式指定类型
    swap<int,int>(a,b)

    注意事项:

    • 自动类型推导,必须推导出一致的数据类型t才可以使用
      类型的一致性
    • 模板必须要确定出T的数据类型才可以使用
      模板对应的函数必须指出T的数据类型

    函数模板和普通函数的区别:

    • 普通函数调用可以发生隐式类型转换
    • 函数模板如果使用自动类型推导,不会发生隐式类型转换
    • 函数模板如果使用显式指定类型,可以发生隐式类型转换

    函数模板调用规则

    • 如果函数模板和普通函数都可以实现,优先调用普通函数
    • 可以通过空模板参数列表来强制调用函数模板
    • 函数模板也可以发生重载
    • 如果函数模板可以更好的匹配,优先调用函数模板
      总结:尽量不要让模板和普通函数功能重合,容易产生二义性

    模板的局限性
    对于特定数据类型(class等),需要具体化方式做特殊实现
    常见相关解决方法:运算符重载(治标不治本)、具体化实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    //具体化实现
    class A{

    }
    template<typename T>
    bool isEqual(T &a,T &b)
    {
    if(a==b) return true;
    else return false;
    }
    //上段代码如果传入class A会出错
    //解决办法如下
    template<> bool isEqual(A &a,A &b)
    {
    if(...) return true;
    else return false;
    }
    //这样传入A类时就会自动调用该方法

    类模板

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    template<class NameType, ClassAgeType>
    class person
    {
    public:
    Person(NameType name,AgeType age)
    {
    this->m_name=name;
    this->m_age=age
    }
    NameType m_name;
    AgeType m_age;
    };
    int main()
    {
    Person<string, int>("张三"20);
    }

    类模板和函数模板的区别

    1. 类模板不可以采用自动类型推导的使用方式
      1
      2
      3
      4
      5
      6
      //以上文代码为例
      int main()
      {
      Person("张三"20);// 不可以
      Person<string, int>("张三"20);//可以
      }
    2. 类模板在模板参数列表可以有默认参数
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      template<class NameType, Class AgeType=int>//设置默认参数类型
      class person
      {
      public:
      Person(NameType name,AgeType age)
      {
      this->m_name=name;
      this->m_age=age
      }
      NameType m_name;
      AgeType m_age;
      };
      int main()
      {
      Person<string>("张三"20);//参数二采用了默认值int
      }

    成员函数创建时期

    • 成员函数在调用时才被创建
      可能出现的情况:模板类中定义的函数,在模板被实现后无法被调用
      换种说法,若出现该类问题,不会在编译时被编译器查出

    类模板对象作函数参数

    传入方式:

    1. 指定传入的类型 直接显示对象的数据类型
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      template<class T1,class T2>
      class Person
      {
      T1 name;
      T2 age;

      }
      void showPerson(Person<string,int>&p)
      {
      p.show();
      }
    2. 参数模板化 将对象中的参数变为模板进行传递
      1
      2
      3
      4
      5
      template<class T1,class T2>
      void showPerson(Person<T1,T2>&p)
      {
      p.show();
      }
    3. 整个类模板化 将这个对象类型模板化进行传递
      1
      2
      3
      4
      5
      template<class T>
      void showPerson(T &p)
      {
      p.showperson();
      }

    类模板与继承

    1. 当子类继承的父类是一个类模板时,子类在声明时需要指定T的类型
      1
      2
      3
      4
      5
      6
      7
      8
      9
      template<class T>
      class Base
      {
      T m;
      }
      class Son:public Base<int>
      {
      //可以正常写了
      }
    2. 如果不指定,编译器无法给子类分配内存
    3. 如果想灵活指定T的类型,子类也需要为类模板
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      template<class T>
      class Base
      {
      T m;
      }
      template<class T1,class T2>
      class Son:public Base<T2>
      {
      T1 m;
      //可以正常写了
      }

    类模板成员函数的类外实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    template<class T1,class T2>
    class qwq
    {
    T1 m;
    T2 n;
    qwq(T1 m1,T2,n1);
    }
    template<class T1,class T2>
    qwq<T1,T2>::qwq(T1 m1,T2,n1)//类外实现,成员函数同
    {
    this->m=m1;
    this->n=n1;
    }

    类模板分文件编写

    问题:成员函数在调用阶段生成,导致分文件编写时无法被链接
    解决方案:

    1. 直接包含cpp源文件(会通过.cpp的头文件自动包含.h文件)
    2. 将声明和实现写到同一个文件中,并更改后缀名为hpp(约定)

    类模板与友元

    类内实现:直接在类内声明友元即可
    类外实现:提前让编译器知道全局函数存在

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    template<class T1,class T2>
    class Person;//提前声明类的存在

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

    template<class T1,class T2>
    ckass Person
    {
    //类内实现
    friend void printPerson(Person<T1,T2> p)
    {
    cout<<p.name;
    }
    //类外实现
    //加空模板参数列表(括号前面的<>)
    friend void printPerson2<>(Person<T1,T2> p);
    public:
    string p.name;
    }