1. 面向对象程序设计
面向对象程序设计的核心思想是数据抽象、继承和动态绑定。通过数据抽象,我们可以将类的接口与实现相分离;使用继承。可以使用相似的类型对其关系建模;使用动态绑定,可以在一定程度上忽略相似类型的区别。
- 1)继承:根部类为基类(相似于java的父类),其他继承于基类的类为派生类。
- 2)虚函数:某些函数,基类希望它的派生类各自定义适合其自生的函数
- 3)动态绑定:能使用同一代码分别处理基类和派生类。如虚函数运行版本由实参决定。
1.1 定义基类
- 作为继承关系中根结点的类通常有定义了一个虚析构函数。
- 基类中的成员函数分为两种:一是派生类要进行重写覆盖的函数,称为虚函数;二是希望直接继承而不改变的函数。当我们使用指针或引用调用虚函数时,该调用将被动态绑定。
- 基类通过在其成员函数的声明语句之前加上关键字
virtual
使得函数执行动态绑定。其只能出现在类内部声明或定义中。 - 派生类可以继承定义在基类中的成员,但是派生类的的成员函数不一定有权访问从基类继承而来的成员。派生类能访问公有成员,但不能访问私有成员。为解决这一问题,引入了新访问运算符:
protected
(派生类有权访问,禁止用户访问)
1 | class Quote{ //定义基类 |
1.2 定义派生类
派生类需要使用类派生列表来指出它是从哪个基类继承而来的,每个基类前面可以有三种访问说明符public、private、protected
中的一个。派生类需要将继承来的成员函数需要覆盖的进行重新声明。如果一个派生是公有的,则基类的公有成员也是派生类接口的组成部分,也能将公有类型的对象绑定到基类的引用或者指针上。 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19class Bulk_Book:public Quote{
public:
Bulk_Book()=default;
Bulk_Book(const Bulk_Book& bb):Quote(bb.isbn(),bb.price),
min_qty(bb.getMin_qty()),discount(bb.getDiscount()){}
Bulk_Book(const string& _book,double p,size_t qty,double disc):Quote(_book,p),min_qyt(qyt),discount(disc){}
double net_price(size_t n)const override{
if(n>min_qyt)
return n*price*discount;
else
return n*price;
}
size_t getMin_qty()const{return min_qty;}
double getDiscpunt()const{return discount;}
virtual ~Bulk_Book()=default;
private:
size_t min_qty=0;
double discount=0;
};
- 派生类经常(但不总是)覆盖虚函数,派生类如果没有覆盖虚函数,则会直接继承其在基类中的版本。
- 派生类可以在它要覆盖的函数前面使用
virtual
,但不是非得这样做。C++11新标准允许派生类显式地注明它使用某个成员函数覆盖了它继承的虚函数,既添加一个override
。 - 因为派生类对象中含有与基类对应得组成部分,所以我们能把派生类对象当成基类对象来使用(java的多态),同时也可把基类的指针或引用绑定到派生类对象得基类部分上。这种转换被称为派生类到基类的转换
1 | Quote item; //基类对象 |
1.2.1 派生类构造函数
派生类需要用基类的构造函数来初始化,每个类控制它自己的成员初始化过程。基类部分和自己的数据成员都在构造函数初始化阶段执行初始化操作,只不过由基类构造函数来初始化Bulk_quote的基类部分 1
2
3Bulk_Book(const Bulk_Book& bb):Quote(bb.isbn(),bb.price),
min_qty(bb.getMin_qty()),discount(bb.getDiscount()){}
Bulk_Book(const string& _book,double p,size_t qty,double disc):Quote(_book,p),min_qyt(qyt),discount(disc){}
1.2.2 派生类使用基类成员
派生类可以访问由基类继承来的的公有成员和受保护成员 1
2
3
4
5
6double net_price(size_t n)const override{
if(n>min_qyt)
return n*price*discount;
else
return n*price;
}
1.2.3 继承与静态成员
如果基类定义了一个静态成员,则在整个继承体系中都只存在该成员的唯一定义,无论从基类中派生出多少个类,对每个静态成员来说都只存在唯一的实例,属于一个类。静态成员遵循同样的访问控制规则,如果基类中成员private,派生类无权访问(只能由接口访问、或将该派生类声明为友元),如果可访问,那么可通过基类或者派生类使用它。 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class Base{
public:
static void statmen();
};
class Derived:public Base{
public:
void f(const Derived&);
};
void Derived::f(const Derived &s){
Base::statmen(); //Base定义了statmen
Derived::statmen(); //Derived继承了statmen
s.statmen(); //通过Derived对象调用
statmen(); //通过this对象调用
}
其他规则:
- 如果想将某个类用作基类,那么这个类必须已经定义而非仅仅声明,因为派生类要使用基类当中的成员,另外,这个规定隐含表示了:一个类不能派生它本身。
- 一个类可以同时是基类和派生类。
- 可以通过
class Base final {};
来阻止其他类来继承该类
1 | //Base是D1的直接基类,是D2的间接基类(D1既是派生类又是基类)。 |
1.3 类型转换与继承
存在继承关系,我们可以将一个基类的引用或者指针绑定到派生类对象上。当使用存在继承关系的类型时,需将静态类型与动态类型区分开来:表达式的静态类型在编译时总是已知的,是变量声明时的类型或表达式生成的类型;动态类型是变量或者表达式表示的内存中对象的类型。但如果表达式既不是引用也不是指针,那么静态类型永远与动态类型一致。
- 不存在从基类向派生类的隐式类型转换(不安全)。
每个派生类对象包含一个基类部分,才能实现派生类向基类的隐式转换,所以一个基类不能隐式地转换为派生类:
1
2
3Quote base;
Bulk_Book& bulkr=base; //错误
Bulk_Book* bulkp=&base; //错误另外一个特殊的是即使一个基类指针绑定在派生类对象上,我们也不能执行基类向派生类的转换,因为编译器判断的是指针或者引用的静态类型:
1
2
3Bulk_Book bulk;
Quote *itemp=&bulk; //正确
Bulk_Book *bulkp=itemp; //错误,不允许对象之间不存在类型转换。派生类向基类的自动类型转换只针对指针或者引用,我们初始化或者赋值一个类类型的对象,实际上是在调用某个函数,这些函数可以接受一个引用作为参数,允许我们向基类形参传递一个派生类的对象,但又由于这些不是虚函数,显然这些函数只能处理基类的对象,派生类自有的成员被切掉了。
1
2
3
4//因为拷贝和赋值的参数为const Quote&的引用类型
Bulk_Book bulk; //派生类对象
Quote item(bulk); //允许,只不过在Bulk_Book上新增的被切掉
item=bulk; //允许
1.4 虚函数
当我们使用基类的引用或指针调用虚函数时会执行动态绑定(直到运行时,我们才会知道调用的是哪个版本---动态类型)。
1. 某个虚函数(参数)通过指针或者引用调用时,直到运行时才能确定调用哪个版本的函数,被调用的是与指针或者引用的动态类型匹配的哪个,需要注意的是只有通过指针或者引用调用虚函数才会发生动态绑定。通过对象调用的虚函数版本在编译时确定。 1
2
3
4
5
6
7
8
9
10
11double print_total(ostream &os,const Quote &items,size_t n){
//根据传入的items调用net_price,允许时才知道时基类的net_price还是派生类的
double ret= items.net_price(n);
os<<"isbn:"<<items.isbn()<<" sold_num:"<<n<<" total_price:"<<ret<<endl;
return ret;
}
//调用
Quote base("0-2121-755",50);
print_total(cout,base,10); //item引用调用的是quote版本的net_price
Bulk_quote bu_base("021-1141-143",20,5,0.9);
print_total(cout,bu_base,10); //item引用调用的是Bulk_quote版本的net_price
2. 一旦某个函数被声明为虚函数,则在所有派生类中都是虚函数(可加virtual也可不加),覆盖的版本形参类型、返回类型必须一致,(但如果返回的是类的引用或者指针时,可以不一样,但须保证基类和派生类的类型是可转换的,这将在后面简述:注意处) 1
2
3
4
5
6
7
8
9
10
11
12
13
14class B{
public:
virtual void f1(int )const;
virtual void f2();
virtual B& f3();
void f4(); //可覆盖,但是不能加override
};
struct D1:public B{
void f1(int)const override;
//void f2(int)override; //错误,参数不一样
D1& f3()override; //正确
void f4();
};
3.final和override说名符:c++新标准中的override
是为了说明派生类的中的虚函数,能让编译器为程序员发现一些错误:如参数并不匹配,返回类型不一致等,同时也让编译者知道它是一个(基类虚函数)到派生类重写的虚函数。函数被指定为final
,则之后任何尝试覆盖该函数的操作都将引发错误 1
2
3struct B{
void f1(int)const final; //不允许后续的其他继承类覆盖
};
4.虚函数运行默认实参:虚函数也可以有默认实参,但是实参值由该次调用的静态类型决定,也就是如果通过基类的引用或指针调用虚函数,则使用基类虚函数定义的默认实参,不管动态类型如何。因此虚函数如果使用实参,最好基类和派生类的中定义的默认实参是一致的。 1
2
3
4
5
6
7
8
9
10
11
12
13class A{
public:
virtual void f(int a=0,int b=3);
};
class B:public A{
public:
void f(int a=1,int b=4);
};
//调用
B b;
A *P=&B;
p->f(); //调用B中的f,但是传入参数为A的默认参数,即B::f(0,3);
5. 回避虚函数的动态绑定机制:有些情况下希望强制执行虚函数的某个特定版本,可以使用作用域运算符,强制调用某个派生类或者基类的虚函数。如上述的item
,如果是要Quote
的: 1
doeble ret=item.Queto::net_price(42);
1.5 抽象基类
含有纯虚函数的类是抽象基类。纯虚函数无需定义,通过在函数声明语句的分号之前书写=0就可以将一个虚函数说明为纯虚函数,它告诉用户该函数对于该类没有实际意义。不能在类的内部为一个=0的函数提供函数体。 1
2
3
4
5
6
7
8
9
10class Disc_quote:public Quote{
public:
Disc_quote()=default;
Dis_quote(const string& book,double price,std::size_t qty,double disc):
Queto(book,price),quantity(qty),discount(disc){}
double net_prince(std::size_t n)=0; //纯虚函数
protected:
std:size_t quantity=0;
double discount=0.0;
};
这个时候我们讲Bulk_queto
直接继承与Disc_quote
,而不是Quot
e,值得一提的是,派生类构造函数只初始化它的直接基类: 1
2
3
4
5
6
7class Bulk_quote :public Disc_quote
{ //继承了isbn,prince,bookNo
Bulk_quote() = default;
Bulk_quote(const std::string& book, double p, std::size_t qty, double disc)
:Disc_quote(book,p,qty,disc) {}
double net_price(std::size_t)const override;
};
1.6 访问控制与继承
每个类分别控制自己成员的初始化过程和成员可访问特性
1.6.1 protected
protected
受保护的成员,对类用户来说是不可访问的,对于派生类和友元来说是可访问的。派生类的成员或者友元只能通过派生类对象来访问基类的受保护成员! 1
2
3
4
5
6
7
8
9
10
11class Base{
protected:
int prot_mem;
};
class Sneaky:public Base{
friend void clobber(Sneaky&); //能访问Sneaky::port_mem
//friend void clobber(Base&); //不能访问Base::port_,e,
int j;
};
friend void clobber(Sneaky& s){s.j=s.port_mem=0;}friend void clobber(Base&)
;是不允许的,因为void clobber(Base&)
;不是Base
类的友员,试想一下如果这种方法可行,那么就意味着用户可以自己制作一个派生类,此时就很容易规避了protected不能被用户访问的特性,这是一个弊端,所以说派生类的成员或者友元只能通过派生类对象来访问继承自基类的受保护成员!
1.6.2 公有、私有和受保护继承
类对其继承而来的成员的访问权限收到两个因素影响:一是基类中该成员的访问说明符;二是派生类的派生列表中的说明符。 1
2
3
4
5
6
7
8class Base{
public:
void pub_mem();
protected:
int port_mem;
private:
char priv_mem;
};public
的,则尊循原来的访问说明符,如果继承是private
的,则所有继承成员都是私有的,如果继承是受保护的,则继承的公有成员都是受保护的: 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
26class pub_exam:public Base{
//成员性质保持不变
int f(){return port_mem;} //可被访问
char g(){return priv_mem;} //错误,private不可访问(可用基类接口、或声明为友员)
};
class priv_exam:private Base{
//private只是不会影响派生类对基类的原有访问。
//该继承类的成员都是private
int f(){return port_mem;} //可被访问
char g(){return priv_mem;} //错误,private不可访问(可用基类接口、或声明为友员)
};
使用:
pub_exam d1;
priv_exam d2;
d1.pub_mem(); //正确,该函数再派生类中是public
d2.pub_mem(); //错误,该函数再派生类中是private
//所以派生访问说明符对于派生类的成员能否访问其直接基类的成员没什么影响,
//只是影响派生类用户对于基类成员的访问权限。同时也可控制继承自派生类的新类的访问权限。
class ds:public pub_exam{
//成员性质不变
}
class as:public priv_exam{
//都是private,无法直接访问
}
注意:
- 当派生类公有继承基类时,用户代码才能实现派生类向基类的转换,否则不能;
- 无论什么方式继承,派生类的成员函数和友元都可以使用派生类向基类的转换;
- 如果D是公有或者受保护继承B,则D的派生类的成员和友元可以使用向基类B的类型转换,否则不能
1.6.3 友员和继承
就如友员关系无法传递一样,友员关系同样无法被继承。既基类的友员再访问派生类成员时不具有特殊性,同理派生类的友员也不买随意访问基类成员。
1.6.4 改变个别成员的可访问性
当需要改变派生类继承的某个名字的访问级别,通过使用using声明: 1
2
3
4
5
6
7
8
9
10
11
12
13class Base{
public:
std::size_t size()const{return n;}
protected:
std::size_t n;
};
class Derived:private Base{ //private继承,则默认下的继承类成员为私有
public:
usnig Base::size; //保持与Base基类一样的public
protected:
using Base::n; //保持与基类一样的protected
};
1.7 继承中类的作用域
每个类都有自己的作用域,在这个域中我们声明和定义类的成员。当存在继承时,派生类的作用域嵌套在其基类作用域之内。如果有多次继承,则一环一环嵌套下去。
1.7.1 编译时进行名字查找
一个对象、引用和指针的静态类型决定了该对象的哪些成员是可见的,我们使用哪些成员仍然是由静态成员类型所决定的:如我们在上面的Disc_quote添加一新成员: 1
2
3
4
5
6
7
8
9
10
11
12
13class Disc_quote:public Quote{
public:
std::pair<size_t,double> discount_policy()const{
return {quanlity,discount};
}
//其他成员
};
//使用:Quote不包含有新成员;而Bulk_quote继承自Disc_quote
Bulk_quote bulk;
Bulk_quote* blukp=&bulk; //静态类型于动态类型一致
Quote* itemp=&bulk; //基类指针指向派生类对象,即静态与动态不一致
bulkp->discount_policy(); //正确
itemp->discount_policy(); //错误
1.7.2 名字冲突与继承
与其他作用域一样,派生类也能重用定义在其直接或间接基类中的名字,此时定义在内存作用域的名字将会隐藏定义在外层(即基类)的名字: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17class A{
punlic:
A()=default;
int getMem()const{return mem;}
protected:
int mem;
};
class B:public A{
public:
B()=default;
int getMem()const{return mem;} //隐藏了基类中的getMem
//那么我们在需要被隐藏成员时,可以通过**使用域作用运算符**获取:
int get_A_mem(){return A::mem}
private:
int mem; //隐藏了基类中的mem
};p->getMem()
或者obj.getMem()
,则以此执行下面四个步骤:
- 首先确定
p
或obj
的静态类型。因为我们调用的是一个成员,所有它们必定是类类型- 在
p
或obj
的静态类型对应的类中寻找getMem
成员,如果找不到则以此在它的基类中寻找,直到继承链的顶端。若到顶端都没找到,则编译器报错。- 一旦找到了
getMmem
,就进行常规的类型检查(参数个数和类型)以确认找到的getMem
,对于本次调用是否合法。- 假设调用合法,则编译器将根据调用是否是虚函数而产生不同代码:如果
getMem
是虚函数且我们是通过引用或指针形式进行调用,则编译器产生的代码将在运行时确定到底是运行该虚函数的哪一个版本。反之,则常规调用函数
1.7.3 名字查找先于类型检查
声明在内层作用域中的函数不会重载声明在外层作用域的函数。因此派生类的成员也不会重载基类中的成员,而是隐藏掉外层作用域成员。所以下面的调用是错误的: 1
2
3
4
5
6
7
8
9
10
11
12
13
14class A{
public:
int memfcn();
};
class B:public A{
int memfcn(int);
};
//调用
B d;
A c;
c.memfvn(); //正确
d.memfcn(10); //正确
d.memfcn(); //错误,参数列表为空的memcn被隐藏了
d.A::memfcn(); //正确,通过作用域访问
1.7.4 虚函数与作用域
现在我们应该能够理解为什么虚函数必须要有相同的形参列表。因为假如不同,就会被隐藏,我们就无法通过基类引用或指针调用派生类的虚函数了,而是通过作用域运算符::
访问,这就失去了多态这个重要性质。
1.8 构造函数与拷贝控制
继承体系中的类也需要控制当其对象执行一系列操作时发生什么样的行为。
1.8.1 虚析构函数
继承关系对基类拷贝控制最直接的影响是基类通常应该定义一个虚析构函数,这样我们就能动态分配继承体系中的对象了:假如当我们要delete
一个动态Quote*
指针时,该指针实际可能是Bulk_quote
类型的对象。那么此时编译器就必须清楚它应该执行的是Bulk_quote
的析构函数,所以应该在基类定义一个虚析构函数 1
2
3
4class Quote{
public:
virtual ~Quote()=default;
};
1.8.2 基类合成拷贝控制与继承
基类或者派生类的合成拷贝控制成员的行为与其他类似:
- 合成派生类默认构造函数运行了直接基类的默认构造函数,直接基类又运行了间接基类的构造函数(如上面的
Quote、Disc_quote、Bulk_quote
的构造函数),顺序时会先先执行基类的默认构造函数,再执行派生类构造函数 - 在上文继承体系中所有类都使用合成的析构函数,派生类隐式使用,基类显式使用,派生类的析构函数释放成员,销毁直接基类
1. 派生类中删除的拷贝控制与基类的关系:
- 如果基类的默认构造函数、拷贝控制成员、析构函数是删除或者不可访问,则派生类中对应的成员也是删除的,因为没办法执行对基类的操作;
- 如果基类的析构函数是删除的,则派生类拷贝控制成员和移动构造函数是删除的,因为没法销毁基类对象
- 编译器不会合成删除掉的移动操作。如果基类的移动操作是删除的,则派生类当中的也是删除的
1 | class B{ |
2. 移动操作与继承
多数基类定义一个虚析构函数,因此默认下基类不含合成的移动操作,这导致派生类也没有移动操作(没有移动操作,但使用的时候用到移动操会默认使用拷贝构造函数)。当确实需要移动操作时应在基类中定义,并同时定义拷贝操作 1
2
3
4
5
6
7
8
9class Quote{
public:
Quote()=default;
Quote(const Quote&)=default;
Quote(Quote&&)=default;
Quote& operator=(const Quote&)=default;
Quote& operator=(Quote&&)=default;
virtual ~Quote()=default;
}
1.8.3 派生类的拷贝控制成员
正如派生类构造函数一样要初始化基类部分的成员,派生类的拷贝控制成员不仅拷贝自身成员,也负责了拷贝基类部分的成员。 1. 当为派生类定义拷贝或移动构造函数时,我们通常使用对应得函数初始化对象得基类部分。 1
2
3
4
5
6calss base{/*...*/};
class D:public base{
public:
D(const D& d):Base(d){}
D(D&& d):Base(std::move(d)){}
};
2. 派生类赋值运算符,与拷贝和移动构造函数一样,派生类赋值运算符也必须显式地为其基类部分赋值。 1
2
3
4
5
6
7
8
9
10calss base{/*...*/};
class D:public base{
public:
D(const D& d):Base(d){}
D(D&& d):Base(std::move(d)){}
D& operator=(const D& d){
Base::operator=(d);
return *this;
}
};
3. 继承的构造函数.在c++新标准中,允许派生类能够直接用基类定义的构造函数,但不能继承默认构造函数和拷贝,移动构造函数,如果不定义,编译器负责合成。 1
2
3
4class Bulk_quote:public Disc_qupte{
using Disc_quote::Disc_quote; //继承Disc_Quot的构造函数
double net_price(std::size_t)const override;
};
- 构造函数的
using
不会改变构造函数的访问级别; - 当基类构造函数含有默认实参,实参不会被继承,而是生成一个形参列表中除去默认实参的构造函数(例如,若基类有一个接受两个形参的构造函数,其中第二个有默认实参,则派生类将获得两个构造函数:一个是构造函数接受两个形参,另一个只接受一个形参,该形参是没有默认实参的那个)。P558 ;
- 如果派生类定义了与基类构造函数形参相同的构造函数,则不会继承该构造函数,而是替换
1.9 单例模式
定义:单例模式是一个类只能实例化一个对象,实现单例模式的思路:
- 1).把无参构造函数和拷贝构造函数私有化
- 2).定义一个类内的类静态成员指针
- 3)在类外初始化时,
new
一个对象 - 4)把指针的权限设置为私有,然后提供一个静态成员函数让外面获取这个指针
1 | class Maker |
2. 模板与泛型编程
面向对象编程(OOP)和泛型编程都能处理在编写程序时不知道类型的情况,不同之处在于:OOP能处理类型在程序运行之前都未知的情况;而泛型编程,在编译时就能知道类型。之前介绍过的容器、迭代器和泛型算法都是泛型编程的例子
必须知道的原理:模板定义并不是真正的定义了一个函数或者类,而是编译器根据程序员缩写的模板和形参来自己写出一个对应版本的定义,这个过程叫做模板实例化。编译器成成的版本通常被称为模板的实例。编译器为程序员生成对应版本的具体过程。类似宏替换。即模板类在没有调用之前是不会生成代码的。 注意:要区分类实例化和模板实例化,类实例即为创建对象,模板实例化为定义一个实例类
2.1 函数模板
一个函数模板就是一个公式,可以用来针对特定类型的函数版本。比如我们定义一个比较函数模板: 1. 定义函数模板
1 | template <typename T>int compare(const T&v1,const T& v2){ |
模板定义以关键字template
开始,后跟一模板参数类型列表(由逗号分隔的一个或多个模板参数,由<>括起)。 当我们使用模板时,显式或隐式的指定模板实参,将其绑定到模板参数上。
2.模板实例
我们调用该模板时候编译器就会根据我们提供的实参来实例化一个特定版本的函数: 1
cout<<compare(1,0)<<endl; //实例化了一int compare(const int& v1,const int& v2)函数
3.模板类型参数
上面自定义的模板函数有模板参数列表,这些参数可以指定函数参数列表、返回类型、函数体内的变量声明和类型转换,其内的类型参数前必须使用class
或者typename
,在这里两个的含义完全相同。 1
2
3
4template<typename T,class U> T(const T& v1,const U& v2){
T tep=v1;
U up=v2;
}
4. 非类型模板参数
除了定义模板类型参数,还可以定义非类型模板参数。非类型参数表示一个值而非一个类型,通过一个特定的类型名而不是关键字typename
、class
来指定非类型参数。例如:编写一个compare
版本处理字符串字面常量,这种字面常量是const char
数组:(因为数组是无法拷贝的,所以采用引用),该非类型模板定义了两个非类型参数,第一个将要表示第一个数组长度,第二个表示第二个数组长度: 1
2
3
4
5//非类型模板参数
template<unsigned N,unsigned M>
int compare(const char (&p1)[N],const char (&p2)[M]){
return strcmp(p1,p2);
}1
2
3
4
5//调用
compare("Hi","Hello");
//编译器会将字面常量大小代替`N\M`,从而实例模板。
//编译器会在字符串字面常量末尾插入一个空字符作为终结符,所以编译器实例的版本是:
int compare(const char (&p1)[3],const char (&p2)[6]);
2.2 类模板
函数模板不同的是,编译器不能为类模板推断模板参数类型。为了使用类模板,我们必须在模板名后面的<>中提供额外信息----作为类模板参数的实参
类模板中的声明和实现必须放在同一头文件,因为链接器(linker)会找不到实例化的函数模板的入口地址。链接器之所以会去找这个函数入口,是因为编译器(compiler)告诉他这里有一个函数入口。“因为C++标准明确表示,当一个模板不被用到的时侯它就不该被实例化出来”。模板分离编译(分别放在.h和.cpp中),VS会报错:
c++编译器为什么不支持模板的分离式编译error LNK2001: unresolved external symbol
。没有报错的,要么是没有使用这个模板,要么是在实现的.cpp文件中有实际使用。- C++支持类模板虚函数,但是不支持模板虚函数
- ①这是由C++多态的实现机制决定的:**每个有虚函数的类都拥有一张虚函数表(虚函数表的大小取决于类内有多少个虚函数,比如有N个,就是4*N),虚函数表内存储着指向各自的虚函数入口地址;当我们实例化一个类的对象时,就会对该对象生成一个虚表指针,指向这个类的虚函数表,这样我们就能够知道在继承中子类调用子类的虚函数,而不是父类的虚函数。**
- ②我们知道类模板不是类的实例化,模板类只要在我们显示定义类时,才会实例化一个类。
- 因此知道上面的①②点,很容易知道为什么。因为类模板不是实例化类,那么当然就不会有虚函数表,它的虚函数表只要在类实例化后才生成,并不会冲突,因此类模板可以有虚函数;但是,当我们在一个实例化类定义一个模板函数是不可行的,因为编译器在编译的时候就得确定虚函数表的大小,而模板函数只又在实例化后才会生成一个真正的函数。
2.2.1 定义类模板
1 |
|
从上面一个完整的类模板可以看到:
- 类模板的成员函数可定义在类内也可在类外,类内定义的成员函数默认内联(inline)。类模板成员函数具有和模板相同的模板参数。因此,在类外定义成员函数必须以关键字
template
关键字开始,后接类模板参数列表:template<class T>
inline
关键字放在模板之后,函数之前即可- 我们使用一个类模板类型必须提供模板参数。但有一个例外:在模板类内作用域可以直接使用模板名而不需要要提供参数
<T>
,但在类外定义的时候需要提供参数<T>
- 在函数体内,我们进入了类作用域,所以可以不用再提供模板参数。
- 模板和非模板友类:如果一类模板包含了一个非类模板友元,则友元被授权可以访问所有模板实例;如果是包含模板友元,类可以授权给所有友元模板实例,也可以只授权特定实例:
1
2
3
4
5
6
7
8
9template<class T>class C2{
//只要pal和C2的实例类型是一致的,那么实例化后的pal就是它的友类,
//跟friend class BlobPtr<T>;一样
friend class pal<T>;
//pal2的所有实例都是C2的友类,不管类型是否一样
template<class x> friend class pal2;
//对于非模板类,时C2所有实例的友类
friend class pal3;
}
2.2.2 类模板的静态成员
- 对于任意给定的模板参数
X
,其都有count_object、getStatic、addStatic
成员,所有的foo<x>
类型对象都共享相同的count_object、getStatic、addStatic
。注意时相同的类型参数X
情况下共享,不同类型各自拥有 - 类外初始化要加上
tempalte<class T>
,如template<class T>size_t Blob<T>::count_object = 0;
- 可以使用实例类访问
Blob<String>::getStatic()
,也可以使用实例化的对象访问blob.getStatic()
2.3 模板参数
摸版参数(如上面一直用到的T
)遵循普通作用域规则,一个模板参数的可用范围是其声明之后至模板声明或定义结束之前。它也会隐藏外层作用域中声明的相同的名字。
- 模板声明需要包含模板参数,如:
template <typename T>int compare(const T&, const T&);
- 由于我们通过作用域运算符来访问静态成员和类型成员,但是在模板中,编译器无法区分访问的是类型还是静态成员。如
T::size_type *p
;到底是定义p
变量还是将size_type
这个静态成员与p
相乘。为了区分,编译器是认为在模板中通过作用域访问的是变量。所以必须通过关键字typename
来告诉编译器现在使用的是个类型,如前面类模板中的typedef typename std::vector<T>::size_type size_type;
1. 默认模板实参:
1 | tempalte<class T,class F=less<T>> |
我们为模板参数提供了默认实参,指出compare
默认将使用标准库的less
函数对象类,但用户调用该函数时可以提供自己的比较操作,但该操作接受的实参类型应当与compare
的前两个类型兼容。对于一个模板参数,只有当它右侧所有参数都有默认实参时,它才可以有默认实参
如果一个类模板为其所有模板参数提供了默认实参,且希望使用默认实参,那么就必须在模板名后跟一个空尖括号。用int实例化的average_precision模板名后跟的是尖括号。 1
2
3
4
5
6
7
8template<class T=int>class Numbers{
public:
private:
};
//调用
Numbers<doubel> los;
Numebers<>average_precision; //使用默认实参
2.4 控制实例化
当多个或多个独立的编译的源文件使用了相同的模板,并提供了相同的模板参数时,每个文件就都会有该模板的一个实例类。在多个文件实例化相同模板类的额外开销可能非常严重。我们可以通过显式实例化来避免这种开销: 1
extern template declaration; //声明
declaration
是一个类或函数的声明,其中所有模板参数已被替换为实参。遇到extern
模板声明,编译不会在本文件中生成实例化代码,而是去别处寻找。声明必须出现在使用实例化版本的代码,否则会自动实例化,对于一个给定的实例化版本,可能有多个extern
声明但只有一个定义。 1
2
3
4
5
6
7
8//extern声明,那么接下来实例化一个对应对象和函数并不会在本文件生成一个实例化类和函数
//简而言之,就是我这些类型的类和函数在别的地方定义,在这只是调用
extern template class Blob<string>;
extern template int conpare(const int&,const int &);
Blob<String> sa1,sa2;
//下面会在本文件生成一个int类型的实例化类(你看不到,但是编译器生成知道)
Blob<int> a1{0,1,2,3,4,5,6};
Blob<int> a2(a1)
2.5 模板参数和返回值的推断
从函数实参来确定模板实参的过程称为模板实参推断,编译器使用函数调用中的实参类型来寻找模板实参,用这些模板实参生成的函数版本,与给定的函数调用最为匹配。
2.5.1 类型转换与模板类型参数
- 模板函数对const的转换要求很低。向函数模板传递参数时允许
const
转换:可以将非const对象
的引用(或指针)传递给一个const
引用(或指针)形参,也可以将const
传递给非const
,只不过const会被忽略 - 数组或函数指针转换,如果函数形参不是引用类型,数组实参转换为指向数组首元素的指针,函数实参转换为指向该函数的指针
- 其他算术类型转换、派生类到基类的转换不能应用于函数模板。
1 | template<typename T> T fobj(T,T); |
2.5.2 函数模板必需显示实参情况
某些情况,编译器无法推断类型,我们希望用户指定template <typename T1, typename T2, typename T3> T1 sum(T2, T3)
; 每次调用sum时调用者必须为T1
提供一个显式模板实参(因为我们在实例化这个模板函数时,提供的实参只是T2、T3
的,编译器能推断它们的类型,但无法推断T1的. 1
auto val3 = sum<long long>(i,lng); //此时T1是long long型的,
2.5.3 尾置返回类型与转换
有时需返回一个元素的值,但是迭代器操作只能生成元素的引用而不是元素。为了获得元素类型,可以使用标准库的类型转换:remove_reference<int&>
则它的type
成员会是int
。更一般地remove_reference<decltype(beg*)>::type
将获得beg引用元素的类型,组合使用remove_reference
尾置返回和decltyp
e,就可以在函数中返回元素值的拷贝: 1
2
3
4
5
6
7
8
9
10
11
12
13
14//此例中我们通知编译器fcn2的返回类型与解引用beg的参数的结果类型相同
template<class T>
auto fcn2(T beg,T end)->typename remove_reference<decltype(*beg)>::type
{
return *beg;
}
//等价于下面这个,但是不能用,因为此时*beg标识符未定义,只能后置
/*
template<class T>
typename remove_reference<decltype(*beg)>::type fcn(T beg,T end)
{
return *beg;
}
*/
type
是一个类的成员,所以必须在返回类型的声明中用typename
显式告知编译器返回的是一个类型。decltype
关键字,它的作用是选择并返回操作数的数据类型,在此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值
2.5.4 模板函数的例外规则(即对称)对move的支持
模板库例外规则支持move标准库的正确工作: - 第一个例外规则影响右值引用参数的推断如何进行。当我们将一个左值i
传递给函数的右值引用参数,且此右值引用为模板参数类型T&&
时,编译器推断模板类型参数为实参的左值引用类型(即右值引用的这种函数模板支持传入左值) - 在第一例外规则的基础上,第二个例外绑定规则:如果我们间接创建一个引用的引用(实参传入形参),则这些引用形成“折叠”:在所有情况下(除一个例外),引用会折叠成一个普通左值引用类型: 1
2
3
4
5
6
7template<class T> func(T&&);
//实参传入为左值引用X&
int i=42;
int& m=i;
int&& a=move(i);
func(i); //传入左值i,则int&和int&&折叠成int&
func(a); //传入右值a,则int&&和int&&折叠成int&&
即函数参数时T&&
,就意味着我们既可以传递给move一个右值,也可以是左值,下面以move
函数源码为例子探索模板函数的该原理:
std::move(string("byte"))
向move
传递的是一个右值,推断出的T
的类型为string
因此将模板实例化,type
类型为string
,t
的类型为string&&
,move的返回类型为string&&
,函数体返回static_cast<string&&> (t)
然而t
的类型已经是string&&
所以类型转换什么都不做,因此调用结果就是右值引用。- 如果向
move
传递一个左值,推断出T类型为string&
,remove_reference
用string&
进行实例化,故其type
成员为string
,返回类型string&&
,t
实例化为string&
,string& string&&
折叠为string&
故,string&& move(string& t)
这也正是我们希望的:将一个右值引用绑定到一个左值,返回static_cast<string&&>(t)
- 通常情况下,
static_cast
只能用于合法的类型转换,但是针对右值引用,可以显式地将左值转换为右值引用,而对于操作右值的代码来说,将右值绑定到左值特性允许它们截断左值,尽管编译器允许这种用法,但是要求我们static_cast
以防止意外地转换。
1 | template<typename T> |
2.6 重载与模板
函数模板可以被另一个模板或一个普通非模板函数重载,名字相同的函数必须有不同数量或类型的参数。匹配规则:
- 和往常一样,如果恰有一个函数提供比任何其他函数更好的匹配,则选择此函数,但是,如果有多个函数提供同样好的匹配,则:
- 如果同样好的函数中只有一个是非模板函数,则选在此函数
- 如果同样好的函数中没有非模板,而有多个函数模板,且其中一个模板比其他模板更特例化,则选择此模板,否则,此调用有歧义
注意:在定义任何函数之前,记得声明所有重载的函数版本,这样就不必担心编译器由于未遇到希望调用的函数而实例化一个并非你所需的版本
2.7 可变参数模板
可变参数模板,即一个接受可变数目参数的模板函数或模板类。可变数目的参数被称为参数包,存在两种参数包,一是模板参数包,表示零个或多个模板参数;二是函数参数包,表示零个或多个函数参数。通过用一个省略号来指出一个模板参数或函数参数表示一个包:
class…
或typename…
指出接下来表示零个或多个类型的列表;- 一个类型名后面跟一个省略号表示零个或多个给定类型的非类型参数的列表
- 在一个函数参数列表中,如果一个参数的类型是模板参数包,则此参数也是一个函数参数包
1 | // Args 是一个模板参数包;rest 是一个函数参数包 |
2.7.1 sizeof…运算符
当想要知道包中有多少元素时,可以使用 sizeof… 运算符,其返回一个常量表达式,且不会对其实参求值 1
2
3
4template<typename...Args> void g(Args...args){
cout << sizeof...(Args) << endl; //类型参数的数目
cout << sizeof...(args) << endl; //函数参数的数目
}
2.7.2 编写可变参数函数模板
- 可变参数函数通常是递归的,第一步调用处理包中的第一个实参,然后用剩余实参调用自身
- 为了终止递归,还需要定义一个非可变参数的函数
1 | //用来终止递归并打印最后一个元素的函数,其实可变参数模板也能匹配,但是非可变模板更特例化,因此编译器会选择非可变参数版本 |
这里涉及到了一个专业化名词包扩展,即是指可变参数函数模版在一次次调归后对模板参数包和函数参数包的展开,下列代码是对上述的解释: 1
2
3
4
5
6
7
8
9
10
11
12
13
14template<typename T, typename...Args>
ostream &print(ostream &os, const T &t, const Args&...rest) //扩展Args,为print生成函数参数列表
{
os << t << ",";
return print(os, rest...); //扩展rest,为print递归调用生成实参列表
}
//对于Args的扩展
print(cout, i, s, 42); //包中有两个参数
//实例化为ostream& print(ostream&, const int&, const string&, const int&);
//对于第二个扩展,发生在对print的递归调用中
//模式是函数参数包的名字(即rest),此模式扩展出一个由包中元素组成的、逗号分隔的列表,因此等价于
print(os, s, 42);
2.8 函数模板特例化
在某些情况下,通用模板的定义对特定类型是不适合的,但通用定义又可能编译失败或做得不正确。因此,有时可以利用特定知识编写更高效的代码,而非从通用模板实例化,因此当不能(或不希望)使用模板版本时,可以定义类或函数模板的一个特例化版本。
注意函数模板只有全特例化,不存在偏特化,你认为的偏特化只是重载函数模板
2.8.1 定义函数模板特例化
- 当特例化一个函数模板时,必须为原模板中的每一个模板参数提供实参。
- 为了指出正在实例化一个模板,应该使用关键字
template<>
,指出将为原模板所有模板参数提供实参
1 | template<typename T> int compare(const T&, const T&); //声明放前面 |
2.8.2 函数重载与模板特例化
- 当定义函数模板的特例化版本时,本质上是接管了编译器的工作,为原模板的一个特殊实例提供了定义
- 特例化的本质是实例化一个模板,而非重载,因此特例化影响象函数匹配
- 为了特例化一个模板,原模板的声明必须在作用域中,而且,在任何使用模板实例的代码之前,特例化版本的声明也必须在作用域中
- 模板及其特例化版本应该声明在同一个头文件中,所有同名模板的声明应该放在前面,然后是这些模板的特例化版本
2.9 类模板部分特例化
- 与函数模板不同,类模板的特例化不必为所有模板参数提供实参,可以只指定一部分而非所有模板参数,或是参数的一部分而非全部特性,即类模板支持偏特化。
- 我们只能部分特例化类模板,而不能部分特例化函数模板,函数模板不支持偏特化
- 我们可以只特例化特定成员函数而不是特例化整个类模板
- 类模板即可以全特化,也可以偏特化
2.9.1 类全特化
类模板全特化比较好理解,跟函数模板一样,全特化已经是一个类实例了,当编译器匹配时会优先匹配参数一致的实例 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18template <typename T> struct Foo{
Foo(const T &t ): mem(t){ }
void Bar() {/* ... */}
T mem;
//Foo的其他成员
};
//全特化类模板
template<>struct Foo(string*)
{
....
}
Foo<string> fs; //实例化Foo<string>::Foo()
fs.Bar(); //实例化Foo<string>::Bar()
Foo fi; //调用全特化实例化Foo<string*>::Foo()
fi.Bar(); //使用全特化的类
2.9.2 类偏特化
对类模板我们可以进行偏特化,比如下面这个类 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24template<class T1, class T2> // 普通类模板,有两个模板参数
class B {
void Bar() {/* ... */}
};
//指定一部分参数
template<class T2> // 偏特化版本,指定其中一个参数,即指定了部分类型
class B<int , T2> { ..... }; // 当实例化时的第一个参数为int 则会优先调用这个版本
//或者只特化里面的函数
template<> //正在特例化一个函数成员模板
void B<int>::Bar //正在特例化B<int>的成员Bar
{
//进行应用于int的特例化处理
}
````
还有一种更为重要的特例化形式,在traits编程技法中会用到,以达到完美解决对于原生指针无法进行返回值说明的问题(详间STL源码剖析中的迭代器部分)
```cpp
template<class T> //这个偏特化版本只接收指针类型的模板实参
class B<T*> { ..... };
template<class T>
class B<T&> { ..... }; // 这个偏特化版本只接受引用类型的模板实参