Cata1yst's blog

祇今尚有清流月,曾照高王万马过

0%

C++拾遗

引用

引用其实就是变量的别名,二者共用一个存储空间

1
2
3
4
5
6
7
int b=1;
int & a=b;
b=2;
cout<<a<<endl;
//output: 2
cout<<&a<<endl<<&b<<endl;
//output: 0x70fddc 0x70fddc

引用一旦被声明就不可以改变其所代表的参数,任何对引用变量的操作实际上都是对原变量的操作。

这里我们声明了 y​x 的引用,之后我们希望 y 变成 z 的引用,但是结果却是我们只是改变了 x 的值,二者地址仍然是一样的。

1
2
3
4
5
6
7
8
9
10
11
12
int x=10;
int z=15;
int & y=x;
y=z;
cout<<&x<<" "<<x<<endl;
cout<<&y<<" "<<y<<endl;
cout<<&z<<" "<<z<<endl;
/*
0x70fdcc 15
0x70fdcc 15
0x70fdc8 15
*/

于是我们更加深入地思考引用的本质,其本质就是一个常指针。

注意这里 const​ 的位置,不变的是 p 而不是 *p

1
2
int & b = a ;// int * const b = & a;
b=100; // * b = 100;

由于引用本质上是对地址的作用,因此如果我们希望直接引用一个常数是做不到的。

1
2
int & a =100;//错误的。因为常数100没有其地址
const int & a =100;//正确的。这里分两步走 const int x=100; int & a=x;

引用和指针有相似之处也有不同点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//引用声明时必须赋值,指针不必
int & a; // ×
int * p; // √

//引用不能指向NULL
int & a =NULL; // ×
int * p =NULL; // √

//引用和指针的大小不同,引用的大小即其引用对象的大小,指针的大小是地址的大小
int & a = b;
int * p = &b;
cout<<sizeof(a)<<" "<<sizeof(p); // 4 8

//对自增运算的意义不同
int & a = b;
int *p = & b;
a++; //b++
p++; //使p指向之后一个地址

//引用没有多级
**p; // √
&&a; //×

最后是引用的一些用法

  1. 作为参数

    1
    2
    3
    4
    5
    6
    void swap (int & a,int & b){
    int c=a;
    a=b;
    b=a;
    return;
    }
  2. 作为函数返回值

    此时返回值不能是局部变量,否则函数结束后会释放该地址的内容!

    1
    2
    3
    4
    int & fun(){
    static int c=10;
    return c;
    }

Class

数据分配

  1. 非静态成员变量,在每一个对象实例中被分配单独空间

  2. 静态成员变量,会被分配空间,但是不在对象实例所占用的空间内而是处于堆区,所有对象实例共用

  3. 成员函数,仅是声明,不在对象实例的空间内。

    因此 sizeof()​ 函数作用于某个对象实例时的返回值是该类所有非静态成员变量内存之和

构造函数快速初始化

1
2
3
4
5
6
7
class C{
private:
int a;
int b;
public:
C(x,y):a(x),b(y){}
}

拷贝构造函数

使用情景

  1. 根据已经建立的实例来初始化另一个实例。
  2. 函数传参
  3. 函数返回局部对象
深拷贝与浅拷贝

浅拷贝即直接将原来参数的地址传给新参数。

深拷贝是指重新开辟空间,之后在空间中写入参数值。

一般情况下两种拷贝没必要区分,但是在需要反复开辟释放空间的地方要格外小心。

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
class C{
private:
int a;
int* b;
public:
C(int a,int b):a(a),b(new int(b)){}
C(const C &C1){
this->a=C1.a;
//this->b = C1.b; 浅拷贝
this->b=new int (*C1.b);
}
~C(){
cout<<&b<<endl;
delete b;
}
};
int main(){
C C1(1,2);
C C2(C1);
return 0;
}
/*
0x70fdf8
0x70fe08
采用浅拷贝就会报错,因为这时C1,C2的成员变量b是同一块地址,C1执行析构函数后该地址被释放,C2再执行就找不到释放的空间了
*/
注意事项

拷贝构造函数的应当使用引用

1
2
3
C (const C& C1){
//...
}

这和拷贝构造函数的使用情景相关,假使我们选用的参数是 const C C1,则根据拷贝构造函数的使用规则,传入参数是需要调用该函数的,那么就会发生无穷递归最终程序崩溃。而通过引用相当于直接传入地址,就可以避免这一情况。至于const则是为了防止不必要的失误导致传入的实例被更改。

静态成员变量

static作为关键字的成员变量,其特点是类内声明,类外初始化

1
2
3
4
5
6
7
8
9
10
class C{
private:
int a;
static int sa;
public:
//C(int a,int b):a(a),sa(b){} ×
//[Error] 'int C::sa' is a static data member; it can only be initialized at its definition
C(int a):a(a){}
};
int C::sa=10; //√

static为关键字的成员函数,其特点是只能调用对象的静态成员变量。这是因为静态成员函数的初始化是在编译阶段进行的,此时计算机只给静态变量分配了空间而对于一般变量,程序还不知道其地址。

对于public 的静态成员变量,可以在类外内通过C::sa来访问,而private类型的静态成员变量则不可在类外访问。

this指针

this指针是每一个对象实例被初始化时自带的一个指针,不需要自己定义。其使用场景如下

  1. 形参和成员变量同名

    this->a=a

  2. 函数返回值是当前对象

    return *this

空指针访问成员函数

​ 如果该成员函数没有用到this指针,则访问没问题,否则失败。

​ 判断一个成员函数中有没有用到this指针最直接的方法就是观察函数中是否出现成员变量。

​ 综上,如果我们用一个对象指针变量访问类函数时,需要保证该指针不是空指针,或者更规范地,在函数中加入

1
if (p==NULL) return ;

const修饰的成员函数

常函数是指在函数后面添加const关键字的成员函数

1
2
void fun() const {
}
1
2
3
4
5
6
7
8
9
10
11
class C{
private:
int a;
mutable int b;
public:
void fun() const{
a=10;
b=20;
}
};
//[Error] assignment of member 'C::a' in read-only object

可以看出,常函数是不能改变成员变量的,但是如果成员变量前面加了mutable关键字,就可以在这里改变。

常函数通常存在于常对象。常对象只能调用常函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class C{
private:
int a;
mutable int b;
public:
void fun2(){
a=100;
b=110;
}
};
int main(){
const C a;
a.fun2();
return 0;
}
//[Error] passing 'const C' as 'this' argument of 'void C::fun2()' discards qualifiers [-fpermissive]

友元

通过添加友元的方式,可以直接访问对象的私有成员。(在类A中声明B是友元,相当于B能访问A的私有成员,反之不能)

友元的使用情形大致分为三种:

  1. 全局函数作为友元

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class C{
    private:
    int a;
    int b;
    public:
    C():a(1),b(2){}
    friend void Print(C & c);
    };
    void Print(C & c){
    cout<<c.a<<" "<<c.b<<endl;
    }
    int main(){
    C c;
    Print(c);
    return 0;
    }
  2. 类作友元

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    class C{
    friend class B;
    private:
    int a;
    int b;
    public:
    C():a(1),b(2){}
    };
    class B{
    public:
    void print(C& c){
    cout<<c.a<<" "<<c.b<<endl;
    }

    };
    int main(){
    C c;
    B b;
    b.print(c);
    return 0;
    }
  3. 成员函数作友元

    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 C;
    class B{
    public:
    void print1(C& c);
    void print2(C& c);
    };

    class C{
    friend void B::print1(C& c);
    private:
    int a;
    int b;
    public:
    C():a(1),b(2){}
    };

    void B::print1(C& c){
    cout<<c.a<<" "<<c.b<<endl;
    }

    void B::print2(C& c){
    cout<<c.a<<" "<<c.b<<endl;
    }

    int main(){
    C c;
    B b;
    b.print1(c);
    b.print2(c);
    return 0;
    }

    这里只有B::print1()可以正常输出,b::print2()因为没有声明友元而报错。

    这种对于类的某些成员函数的友元声明在之后重载cout函数中还会用到。

运算符重载

  1. 自增(减)

  2. coutcin

  3. ()

    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
    36
    37
    38
    39
    40
    41
    #include<iostream>
    using namespace std;
    class B{
    friend ostream& operator << (ostream& cout,B b);
    private:
    int a;
    int b;
    public:
    B():a(2),b(3){
    }
    B& operator ++ ();//前置
    B operator ++ (int);//后置
    void operator () ();//函数调用符
    };
    B& B::operator ++(){
    cout<<"++b : ";
    this->a++;
    this->b++;
    return *this;
    }
    B B::operator ++(int){
    cout<<"b++ : ";
    B temp=*this;
    this->a++;
    this->b++;
    return temp;
    }
    void B::operator()(){
    cout<<"load operator () : "<<this->a<<" "<<this->b<<endl;
    }
    ostream& operator <<(ostream& cout,B b){
    cout<<b.a<<" "<<b.b<<endl;
    }
    int main(){
    B b;
    b();
    cout<<b;
    cout<<b++;
    cout<<++b;
    return 0;
    }
注意事项
  1. 前置++由于是先加后用,因此返回的是当前对象自增后的引用。
  2. 后置++是先用后加,因此函数返回值应该是自增前自身的一个拷贝,该拷贝在函数结束时释放,故不能返回其引用。
  3. cout<<b++在使用时要注意自己重载时的数据类型,比如重载时参数为B& b,则不能输出,因为b++的返回值不是引用。
  4. 重载类Boperator << ()是重载左移运算符,想要用cout输出对象,要重载的应该是ostream的左移运算符。
  5. ()重载在之后伪函数还会用到。

继承

继承的特点
  1. 父类的私有成员子类一律不能访问。不同的继承形态只决定父类的公用成员和保护成员在子类中的属性。
  2. 构造析构函数的调用顺序为:父类构造–>子类构造–>父类析构–>子类析构
  3. 访问父类的同名函数成员变量需要加作用域,包括参数不同的重载函数。
  4. 直接访问同名成员时,返回子类成员。
  5. 多重继承时,子类只能访问到其直接父类,父类的父类是不能访问的。
继承的成员变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class A{
public:
int a;
int b;
int c;
};
class B: public A{
public:
int b;
int d;
};
class C: public A{
public:
int c;
int d;
};
class D: public B, public C{
public:
int d;
};

以上述代码为例,子类D的空间大致可分为BCD三块

成员变量 来源 访问方式
B::a 继承自B(进一步继承自A) d.B::a
B::b 继承自B d.B::b
B::c 继承自B(进一步继承自A) d.B::c
B::d 继承自B d.B::d
C::a 继承自C(进一步继承自A) d.C::a
C::b 继承自C (进一步继承自A) d.C::b
C::c 继承自C d.C::c
C::d 继承自C d.C::d
D::d 自身 d.d

此外还有D::B::bD::C::c两个变量,但是无法访问。

虚继承

虚继承是为了减少多重继承中二义性的出现而产生的。

如上图,在菱形继承下,最终子类D有着11个成员变量,但是成员变量的表示符号只用了abcd四种,这就产生了二义性。

如果我们采用虚继承,即在BC类的定义中加上virtual关键字

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
36
37
38
class A{
public:
int a;
int b;
int c;
A():a(1),b(2),c(3){
}
};
class B: virtual public A{
public:
int b;
int d;
B():b(4),d(5){
}
};
class C: virtual public A{
public:
int c;
int d;
C():c(6),d(7){
}
};
class D: public B, public C{
public:
int d;
D():d(8){
}
};
int main(){
D d;
cout<<d.a<<endl;
cout<<d.b<<endl;
cout<<d.c<<endl;
cout<<d.d<<endl;
cout<<d.B::d<<endl;
return 0;
}
//output: 1 4 6 8 5

有了虚继承以后,二义性就消失了。

再来看看最终子类中保存的数据

1
2
3
4
5
a=1 //该数据来自A
b=4 //该数据来自B
c=6 //该数据来自C
d=8 //该数据来自D
B::d=5 //该数据来自B

同名成员变量以最新修改的值为准

最后讨论一些虚继承的规则(仍然以ABCD的菱形继承为例)

  1. A中定义了变量a,而BC均没有该变量,则D直接访问a时以A中为准,无二义性。
  2. A中定义了变量a,而B(或C)存在同名成员变量,则D中直接访问aB(或C)为准,无二义性。
  3. A中定义了变量aBC都包含同名成员变量,D中直接访问a会产生二义性。

可以看出,一旦出现了多重继承,会大大降低程序的可读性,因此非必要不要出现多继承

虚继承内存分配

还是上面的菱形继承,我们输出一下内存

1
2
3
4
5
6
7
8
int main(){
cout<<sizeof(A)<<" ";
cout<<sizeof(B)<<" ";
cout<<sizeof(C)<<" ";
cout<<sizeof(D)<<" ";
return 0;
}
//output : 12 24 24 40

我们通过调试可以查看各个类的内存结构。

A包含 a、b、c三个int变量

B包含A的a、b、c以及自身的b、d,另外还有一个__vptr变量。

CB类似。

D则分别从BC中继承了这个__vptr

其他都好理解,关键是这个__vptr(也有的编译器叫他它__vbptr)。

在虚继承中,内存区域被划分为不变区和共享区。共享区也就是所谓虚基类所在的内存地址。共享区会随着子类数据的更新而更新,那么显然,子类需要一个指针来从当前地址找到虚基类的地址然后更新数据,这就是__vbptr的作用。

__vbptr理解成编译器在编译阶段额外添加如虚基类的子类中的一个成员变量就可以了。

虚函数

虚函数有点像虚继承,只不过它是“虚继承”了父类的某些函数,这些函数以virtual作为关键字。

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
class A{
public:
int a;
A(){
cout<<"A()\n";
}
virtual void fun(){
cout<<"fun() in A\n";
}
void fun1(){
cout<<"fun1() in A\n";
}
~A(){
cout<<"~A()\n";
}
};
class B : public A{
public:
int b;
B(){
cout<<"B()\n";
}
void fun(){
cout<<"fun() in B\n";
}
void fun1(){
cout<<"fun1() in B\n";
}
~B(){
cout<<"~B()\n";
}
};

我们通过对AB占用内存(此处用32,类的定义如上)的测试来引出接下来的内容

1
2
3
4
5
int main(){
cout<<sizeof(A)<<endl;
cout<<sizeof(B)<<endl;
}
// 8 12

正常来说,A有一个int型变量,其余都是函数,应该占用4字节;BA的基础上又加了一个int变量,应该是8字节。但是测试结果正好是二者分别加4。这里留一个悬念。

__vfptr指针

之后再测试如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int main(){
A* p;
p=new B;
p->fun();
p->fun1();
delete p;
return 0;
}
/*
A()
B()
fun() in B
fun1() in A
~A()
*/

这里产生了两个问题

  1. 为什么virtual关键字修饰前后,同一个指针p访问到的函数不是同一个类?
  2. 子类的析构函数?

我们引入刚才多余的四字节变量——__vfptr,这个指针的数据类型是**void,而它所指向的*void数组就是解决上述问题的关键——虚函数表。

虚函数表里存放了类中所有virtual修饰的虚函数,当程序需要执行的函数是虚函数时,__vfptr指针会根据程序指令从虚函数表中找到对应的虚函数然后执行。

__vfptr相当于是编译系统额外加入的一个成员变量,它所对应的虚函数表也是根据所在类生成的。

下面我们回答第一个问题。

p是一个类A的指针。行fun1()时,系统根据p的数据类型执行,因此类A的指针自然执行类Afun1()

但是当执行虚函数fun()时,程序先找出p所指地址中的__vfptr变量,然后由它去查虚函数表。由于p地址存放的是类B,因此__vfptr也属于类B,所查的虚函数表自然也是类B的表,所以程序最终执行类Bfun()

第二个问题也容易理解。delete p时,系统是不知道这个指针里面装了什么,它只知道这是一个类A的指针,所有只执行类A的析构函数,这就产生了隐患。

根据前面提到的,也很容易想到对应方法。只要我们把~A()也写成一个虚函数,程序在删除p时遇到了virtual ~A(),就又去找__vfptr了。找到以后自然是执行~B()

上述情况的症结在于,指针的数据类型和指针所指地址的实际类型不一致,一般成员函数的调用是根据指针本身的类型决定的,而虚函数则是依赖于地址内数据的类型。

于是我们想到另外一种错误。如果我们在类B中添加函数virtual fun2(),再给一个B* p赋值一个类A的实例a然后调用fun2()。显而易见,编译器找不到对应函数。(事实上,有些编译器不能接受将父类对象实例赋值给子类对象指针

虚函数内存分配

不同编译器对虚函数的处理不同可能导致内存分配有所不同,下面都以 TDM-GCC 4.9.2 32bit Debug 为例

  1. 单继承中,最终子类只会有一个虚函数表,占用内存应该加上sizeof(**void)
  2. 普通多继承中,子类会从每一个父类中继承一个__vfptr变量。
  3. 虚继承的虚函数比较复杂。它既有__vfptr又有__vbptr,不过一些编译器会将二者合成为一个新指针__vptr,所以这类编译器在内存分配上和上一种情况类似。
纯虚函数

纯虚函数是特殊的虚函数,它在父类中没有定义函数体,只是声明。

纯虚函数的基本语法为

1
virtual 返回类型 函数名称()=0;

纯虚函数在被重写前不能调用,因为它只是函数声明。

子类可以重新父类的纯虚函数,如果不重写,则该函数在子类中仍然是纯虚函数。

包含纯虚函数的类称为抽象类抽象类不能实例化

可以看出,纯虚函数有类似于模板的功能。

最后是一些关于虚函数的使用习惯

  1. 基类的析构函数最好是虚析构。
  2. 不能将静态成员函数定义为虚函数。
  3. 在类外定义虚函数的函数体时不需要加virtual关键字。
  4. 构造函数和友元函数不能是虚函数。

文件读写

添加头文件<iostream>

ios
1
2
3
4
5
6
7
8
ios::in //读文件
ios::out //写文件
ios::app //在末尾追加
ios::trunc //删除原文件后创建
ios::ate //打开时定位到末尾
ios::binary //二进制读写
ios::nocreate //不创建文件,文件不存在时打开失败
ios::noreplace //不覆盖原文件,文件存在时打开失败
写文件

文本文件

  1. f<<data;

    写入一个const char* 变量

  2. f.put();

    写入一个字符

1
2
3
4
5
6
7
8
9
10
11
fstream f;
f.open("t.txt",ios::out);
//用<<写文件
f<<"sentence 1\n";
const char* str= "sentence 2\n";
f<<str;
//用put()写文件
char c = 'd';
f.put(d);
f.close();
return 0;

二进制文件

  1. f.write((char*)& data , sizeof(data) );

    写入指定长度数据

  2. f.put(bytes);

    写入一个字节

1
2
3
4
f.open("b.txt",ios::out|ios::binary);
int data[3]={1,2,3};
f.write((char*)data,sizeof(data));
f.close();
读文件

文本文件

  1. f>>buf;

    读取到空格、制表、换行停止或sizeof(buf)大小的数据

  2. getline(f,s)

    读取一行存入string变量

  3. f.get()或``f.get(c)`

    读取一个字符存入char变量

  4. f.getline(buf,size)

    读取size大小或一行数据到buf数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
f.open("t.txt",ios::in);

//第一种方法,读取一行或到buf数组被写满
char buf[1024];
f.getline(buf,1024);
cout<<buf<<endl;

//第二种方法,读取一行
string s;
getline(f,s);
cout<<s<<endl;

//第三种方法,读取一个字符(结束字符为EOF)
char c;
while((c=f.get())!='\n'){
cout<<c;
}
cout<<endl;

//逐个读取,遇到空格、制表、回车等停止
f>>buf;
cout<<buf<<endl;
f.close();

二进制文件

  1. f.read((char*)& data ,sizeof(data));
1
2
3
4
5
6
fstream f;
f.open("t.txt",ios::in|ios::binary);
int input[3];
f.read((char*)input,sizeof(input));
for(int i=0;i<3;i++) cout<<input[i]<<" ";
f.close();
文件检验函数
  1. f.bad() 读写错误
  2. f.fail() 读写及格式错误
  3. f.eof() 结束标志
  4. f.good() 以上三个的集合
  5. f.is_open() 文件打开判定
位置指针

位置指针主要用于二进制文件读写,因为二进制文件内部每个字符大小一样,而文本文件有些特殊字符占位不同。

文件指针主要有两套

  1. seekp()/tellp()

    这两个是对写入指针的操作,前者是改变当前指针位置,后者是获取。

  2. seekg()/tellg()

    这两个是对读取指针的操作,同样前者是修改,后者是获取。

函数参数主要是一个整型和一个ios

1
2
3
f.seekg(n,ios::beg); //从开头向后移动n字节
f.seekg(n,ios::cur); //从当前位置向后移动n字节
f.seekg(n,ios::end); //从末尾向前移动n字节

模板

模板声明格式
1
template <class T>

其中class可以用typename替换。

注意事项
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
template <class T>
//无参模板函数
void fun(){
cout<<"fun()\n";
}
//普通函数
void add(int a,int b){
cout<<"add(),result is :"<<a+b<<endl;
}
//模板函数
template <class T>
void add(T a,T b){
cout<<"add() in template,result is :"<<a+b<<endl;
}
int main(){
//fun();
fun<char>();
int a=1;
int b=2;
float bb=2.0;
//不能匹配模板函数
add(a,bb);
//优先调用普通函数
add(a,b);
//优先匹配模板函数
add(1.1,2.2);
//调用模板函数时发生强制类型转换
add<char>(114514,1919810);
return 0;
}
  1. 在调用模板函数时,最好用<>指定数据类型;如果没有事先声明,则编译器根据传入参数的类型自动推导
  2. 对于无参模板函数必须指定数据类型。
  3. 编译器自动推导时,同一个参数模板T的形参必须是同一类型,否则编译器无法分辨应该用哪种数据类型。
  4. 指定数据类型后,编译器在执行函数时会隐含强制数据类型转换
  5. 能直接匹配普通函数的情况下会先调用普通函数
  6. 不能直接匹配普通函数,但是匹配模板函数时,才调用模板函数。
  7. 两种函数都不能匹配,则进行强制类型转换后调用普通函数
类模板
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
template <class T1,class T2=int>
class C{
public:
T1 name;
T2 age;
C(T1 name ,T2 age);
void fun(){
cout<<"name: "<<name<<endl<<"age: "<<age<<endl;
}
void add(){
name=name+"world";
}
};
template<class T1, class T2>
C<T1,T2>::C(T1 name ,T2 age){
this->name=name;
this->age=age;
}
int main(){
C<char*,int> c1("hello",10);
c1.fun();
//c1.add();
C<char*> c2("world",12);
c2.fun();
return 0;
}
  1. 类模板在初始化实例时必须指明数据类型。

  2. 类模板的成员函数可以任意定义,但是实例化后一些函数可能会因为语法问题不能调用。

  3. 类模板可以有默认数据类型。(模板函数在一些版本中也可以,但为了兼容性,不推荐这么干)

  4. 在类外定义类模板的成员函数时,也需要用到模板

    1
    2
    3
    4
    5
    template <class T1,class T2,...>;
    ReturnType ClassName<T1,T2...>::FunName(T1 a1,T2,a2,...){
    ...;
    ...;
    }

类模板也可以作为函数参数传入,此时可以有三种写法

  1. ReturnType FunName(ClassName<Type1,Type2> & p)

  2. ```C++
    template<class T1,class T2>;
    ReturnType FunName(ClassName<T1,T2> & p)

    1
    2
    3
    4

    3. ```C++
    template<class T>;
    ReturnType FunName(T & p)

类模板作为父类继承时,有以下规则

  1. 子类是具体类时,必须指定父类的类型。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    template <class T>;
    class F(){
    public:
    T a;
    }

    class S: public F<int>{
    public:
    int b;
    }
  2. 子类也是模板类,则可以继续用模板表示父类。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    template <class T>;
    class F(){
    public:
    T a;
    }

    template <class T1,class T2>
    class S: public F<T2>{
    public:
    T1 b;
    }