C++ Primer 16-18

再读C++ Primer


第十六章:模板与泛型编程:
模板定义:
int compare(const string &v1,const string &v2)
{
if(v1<v2)  return -1;
if(v2<v1)  return 1;
return 0;
}
int compare(const double &v1,const double &v2)
{
if(v1<v2) return -1;
if(v2<v1) return 1;
return 0;
}
定义函数模板:
template <typename T>
int cpmpare(const T &v1,const T &v2)
{
if(v1 < v2) return -1;
if(v2< v1) return 1;
return 0;
}
模板定义以关键字template开始,后接模板形参表,某班形参表是用尖括号括住的一个或多个模板的列表,形参之间以逗号分隔。




1.模板形参表
模板形参可以是表示类型的类型形参,也可以是表示常量表达式的非类型形参。非类型形参跟在类型说明符之后声明。类型形参跟在关键字class或typename之后定义。在这类class和typename没有区别。
2.使用函数模板
编译器会确定用什么类型代替每个类型形参,以及用什么值代替每个非类型形参。
int main()
{
cout << compare(1,0) << endl;
string s1 = "hi", s2 = "world";
cout << compare(s1,s2) << endl;
return 0;
}
3.inline函数模板
函数模板可以用于非模板函数一样的方式声明inline。说明符放在模板形参表之后,返回类型之前,不能放在关键字template之前。
template<typename T> inline T min(const T&,const T&); //ok


定义类模板:
示例:定义queue,这里先定义接口
template <class Type> class Queue{
public:
Queue();
Type &front();
const Type &front() const;
void push (const Type &);
void pop();
bool empty() const;
private:
...
}




模板形参:
像函数形参一样,程序员为模板选择的名字没有本质含义。在compare函数中的T可以换成其他的名字。
可以给模板形参赋予的唯一含义是区别形参是类型形参还是非类型形参。如果是类型形参,我们就知道该形参表示未知类型;如果是非类型形参,我们就知道他是一个未知值。如果希望使用模板形参所表示的类型或值,可以使用与对应模板形参相同的名字。
1.模板形参作用域:与函数中的变量一样
2.使用模板形参名字的限制:用作模板形参的名字不能在模板内部重用
template <class T> T calc(const T &a,const T &b)
{
typedef double T; //error
T tmp = a;
return tmp;
}
template <class V, class V> V calc(const v&,const v&);






模板声明:同一模板的声明和定义中,模板形参的名字不必相同。
template <class V, class V> V calc(const v&,const v&);
每个模板类型形参前面必须带上关键字class或typename,每个非类型形参面前必须带上类型名字,省略关键字或类型说明符是错误的。
template <typename T, U> T calc(const T&,const U&);


模板类型形参:
类型形参由关键字class或typename后接说明符构成。在模板形参中,这两个关键字具有相同的含义,都指出后面接的名字表示一个类型。


typename和class的区别:
typename更直观,是作为标准C++的组成部分加入到C++中的,因此旧的程序更有可能只用关键字class。




在模板定义内部指定类型:
template <class Parm, class U>
Parm fcn(Parm* array, U value)
{
Parm::size_type *p; //size_t不知道是类型成员的名字还是一个数据成员的名字。默认情况下嘉定这样的名字是数据成员。
}


如果希望编译器将size_t当做类型,则必须显式指定
template <class Parm, class U>
Parm fcn(Parm* array, U value)
{
typename Parm::size_type *p;
}




非类型模板形参:
模板形参不必都是类型。在调用函数时非类型形参将要值代替:
如; template <class T,size_t N> void array_init(T (&parm)(N))
{
for (size_t i=0;i!=N;i++ ){
parm[i]=0;
}
}
int x[42];
array_init[x]; //array_init(int (&x)[42]) 




实例化:
类模板的每次实例化都会产生一个独立的类类型。
Queue<int> qi;
Queue<string> qs;




模板实参推断:
1.多个类型形参的实参必须完全匹配
template <typename T>
int compare(const T& v1, const T& v2)
{
if(v1<v2) return -1;
if(v2<v1) return 1;
return 0;
}
int main()
{
short si;
compare(si,1024); //error.nust be compare(short,short) or compare(int,int)
return 0;
}
2.类型形参的实参的首先转换
const转换:接受const引用或const指针的函数可以分别用非const对象的引用或指针来调用,无须产生新的实例化。如果函数接受非引用类型,形参类型和实参都忽略const,即,无论传递const或非const对象给接受非引用类型的函数,都使用相同的实例化。
数组或函数到指针的转换:如果模板形参不是引用类型,则对数组或函数类型的实参应用常规指针转换。数组实参将当做指向其第一个元素的指针,函数实参当做指向函数类型的指针。


template <typename T> T fobj(T,T);
template <typename T>
T fref(const T&, const T&);
string s1("a value");
const string s2("another value");
fobj(s1,s2); //OK
fref(s1,s2); //OK
int a[10],b[42];
fobj(a,b); //OK
fref(a,b); //error
第一种情况下,传递string对象和const string对象作为实参,即使这些类型不完全匹配,两个调用也都是合法的。在fobj的调用中,实参被复制,因此原来的对象是否为const无关紧要。在fref的调用中,形参类型是const引用,对引用形参而言,转换为const是可以接受的转换。
第二中情况下,将传递不同长度的数组实参。fobj的调用中,数组不同无关紧要,两个数组都转换为指针,fobj的模板形参类型是int*。但是fref的调用时非法的,当形参为引用时,数组不能转换为指针,a和b的类型不匹配,所以调用将出错。




模板实参推断与函数指针:
可以使用函数模板对函数指针进行初始化或赋值,这样做的时候,编译器使用指针的类型实例化具有适当模板实参的模板的版本。
例如:
template <typename T> int compare(const T&,const T&);
int(*pf1)(const int&,const int&)= compare; //指针pf1引用的是将T绑定到int的实例化。




指定显式模板实参:
template <class T1, class T2, class T3>
T1 sum(T2,T3);
long val3 = sum<long>(i,lng); //ok,省略最右边的两个形参


template <class T1, class T2, class T3>
T3 alternative_sum(T2,T1)
long val3 = alternative_sum<long>(i,lng); //error
long val2 = alternative_sum<long,int,long>(i,lng); //ok
显式模板实参从左到右与对应模板形参相匹配,第一个模板实参与第一个模板形参匹配,第二个模板实参与第二个模板形参匹配,依次类推。假如可以从函数形参推断,则只有结尾(最右边)形参的显式模板实参可以省略。




显式实参与函数模板的指针:
template <typename T> int compare(const T&, const T&);
void func(int(*)(const string &,const string &));
void func(int(*)(const int &,const int &));
func(compare<int>); //ok,explicitly specify which version of compare




模板编译模型
[1] 当编译器看到模板定义的时候,它不立即产生代码。 只有在看到用到模板时 ,如调用了函数模板或定义了类模板的对象的时候,编译器才产生特定类型的模板实例 。
[2] 一般而言,当调用函数的时候,编译器只需要看到函数的声明。类似地,定义类类型的对象时,类定义必须可用,但成员函数的定义不是必须存在的。因此,应该将类定义和函数声明放在头文件中,而普通函数和类成员函数的定义放在源文件中。
[3] 模板则不同:要进行实例化,编译器必须能够访问定义模板的源代码。 当调用函数模板或类模板的成员函数的时候,编译器需要函数定义,需要哪些通常放在源文件中的代码。
[4] 标准C++为编译模板代码定义了两种模型。 所有编译器都支持第一种模型,称为“包含”模型( inclusion compilation model) ;只有一些编译器支持第二种模型,“分别编译”模型( separate compilation model) 。
[5] 在两种模型中,构造程序的方式很大程度上是相同的:类定义和函数声明放在头文件中,而函数定义和成员定义放在源文件中。两种模型的不同在于,编译器怎样使用来自源文件的定义 。
[6] 在包含编译模型,编译器必须看到用到的所有模板的定义。一般而言,可以通过在声明函数模板或类模板的头文件中添加一条#include指示使定义可用,该#include引入了包含相关定义的源文件 。
[7] 在分别编译模型中,编译器会为我们跟踪相关的模板定义。但是,我们必须让编译器知道要记住给定的模板定义,可以使用export关键字来做这件事 。export关键字能够指明给定的定义可能会需要在其他文件中产生实例化 。
[8] 在一个程序中,一个模板只能定义为导出一次。 一般我们在函数模板的定义中指明函数模板为导出的 ,这是通过在关键字template之前包含export关键字而实现的。对类模板使用export更复杂一些 ,记得应该在类的实现文件中使用export,否者如果在头文件中使用了export,则该头文件只能被程序中的一个源文件使用。
[9] 导出类的成员将自动声明为导出的。也可以将类模板的个别成员声明为导出的,在这种情况下,关键字export不在类模板本身指定,而是只在被导出的特定成员定义上指定。任意非导出成员的定义必须像在包含模型中一样对待:定义应放在定义类模板的头文件中。






类模板成员:
1.QueueItem类表示Queue的链表中的节点,该类有两个数据成员item和next
item保存Queue中元素的值,它的类型随Queue的每个实例而变化
next是队列中指向下一QueueItem对象的指针
Queue中的每个元素将保存在一个QueueItem对象中
2.Queue类将提供两个数据成员head和tail,这些成员是QueueItem指针。


QueueItem类:
template <class Type> class QueueItem{
QueueItem(const Type &t):item(t),next(0){}
Type item;
QueueItem *next;
};


Queue类:
template <class Type> class Queue{
public:
Queue():head(0),tail(0){}
Queue(const Queue &Q):head(0),tail(0){copy_elems(Q);}
Queue& operator=(const Queue&);
~Queue(){destroy();}
Type& front() {return head->item;}
const Type &front() const{return head->item;}
void push(const Type&);
void pop();
bool empty() const{retur head==0;}
private:
QueueItem<Type> *head;
QueueItem<Type> *tail;
void destoy();
void copy_elems(const Queue&);
};


template <class Type> void Queue<Type>::destoy()
{
while(!empty())
pop();
}




template <class Type> void Queue<Type>::pop()
{
QueueItem<Type>*p = head;
head=head->next;
delete p;
}




template <class Type> void Queue<Type>::push(const Type &val)
{
QueueItem<Type> *pt = new QueueItem<Type>(val);
if(empty())
head=tail=pt;
else{
tail->next=pt;
tail=pt;
}
}




template <class Type> void Queue<Type>::copy_elems(const Queue &orid)
{
for(QueueItem<Type> *pt = orig.head;pt;pt=pt->next)
push(pt->item);
}








类模板中的友元声明:
1)普通非模板类或函数的友元声明
2)类模板或函数的友元声明,授予对友元所有势力的访问权限
3)指收于对类模板或函数模板的特定实例的访问权的友元声明




第十七章部分:
多重继承与虚继承:
定义多个类:
class Bear :public ZooAnimal{
}
class Panda : public Bear,public Endangered{
}


构造顺序:按照基类构造函数在类派生列表中的出现次序调用,与构造函数初始化列表中基类出现的顺序无关。
1.ZooAnimal,从Panda的直接基类Bear沿层次向上的最终基类
2. Bear,第一个直接基类
3.Endangered
4.Panda


析构函数的次序:
按构造函数允许的逆序调用析构函数




多个基类可能导致二义性:
假定Bear类和Endangered类都定义了名为print的成员,如果Panda类没有定义该成员
Panda ying_yang("Panda");
这样的语句将导致编译错误。但更令人惊讶的是,即使两个继承的函数有不同的形参表,也会产生错误。类似地,即使函数在一个类中是私有的而在另一个类中是公有的或受保护的,也是错误的。最后,如果在ZooAnmial类中定义了print而Bear类中没有定义,调用仍然错误。
但如果没有Parada没有调用print就可以避免二义性。如果每个print调用明确指出想要哪个版本--------Bear::print还是Endangered::print也可以避免错误。




虚继承:
虚继承:在继承定义中包含了virtual关键字的继承关系;
虚基类:在虚继承体系中的通过virtual继承而来的基类。


假定通过多个派生路径继承名为 X 的成员,有下面三种可能性:
1、如果在每个路径中 X 表示同一虚基类成员,则没有二义性,因为共享该成员的单个实例。
2、如果在某个路径中 X 是虚基类的成员,而在另一路径中 X 是后代派生类的成员,也没有二义性——特定派生类实例的优先级高于共享虚基类实例。
3、如果沿每个继承路径 X 表示后代派生类的不同成员,则该成员的直接访问是二义性的


特殊的初始化:
class Bear:public virtual ZooAnimal{};
class Raccoon:public virtual ZooAnimal{};
class Panada : public Bear,public Raccoon,public Endangered{};
通常,每个类值初始化自己的基类。在应用于虚基类的时候,这个初始化策略会失败。如果使用常规规则,就可能会多次初始化虚基类。类将沿着包含该虚基类的每个继承路径初始化。在ZooAnimal示例中,使用常规规则将导致Bear类和Raccoon类都试图初始化Panada对象的ZooAnimal部分。
为解决重复初始化问题,在虚派生中,由最底层派生类的构造函数初始化虚基类。
在这种情况下,Bear(或Raccoon)构造函数想平常一样直接初始化他们的ZooAnimal基类:
Bear::Bear(std::string name,bool onExhibit):ZooAnimal(name,onExhibit,"Bear"){}
Raccoon::Raccoon(std::string name,bool onExhibit):ZooAnimal(name,onExhibit,"Raccoon"){}
虽然ZooAnimal不是Panada的直接基类,但是Panada构造函数也初始化ZooAnimal基类:
Panada::Panada(std::string name,bool onExihibit):ZooAnimal(name,onExihibit,"Panada"),Bear(name,onExhibit),Raccoon(name,onExhibit),Endangered(Endangered::critical),sleeping_flag(false){}
则当创建Panda对象时,将按以下顺序:
1)首先使用构造函数初始化列表中指定的初始化式构造ZooAnimal部分
2)接下来,构造Bear部分。忽略Bear的用于ZooAnimal构造函数初始化列表的初始化式。
3)然后,构造Raccoon部分。忽略Raccoon的用于ZooAnimal构造函数初始化列表的初始化式。
4)最后,构造Panda部分。
如果Panda构造函数不显式初始化ZooAnimal基类,就使用ZooAnimal默认构造函数。




构造函数与析构函数:
Class Character{};
Class BookCharacter : public Character {}; //不是虚继承
Class ZooAnimal{};
class Bear:public virtual ZooAnimal{};//虚继承
Class ToyAnimal{};


Class TeddyBear : public BookCharacter,public Bear,public virtual ToyAnimal {};
上式调用构造函数的顺序如下:
ZooAnimal();
ToyAnimal();
Character();
BookCharacter();
Bear();
TeddyBear();
在合成复制构造函数和合成赋值操作符中使用同样的顺序。
析构顺序与构造顺序相反。






第十八章小部分:
我们知道extern "C"的真实目的是实现类C和C++的混合编程。在C++源文件中的语句前面加上extern "C",表明它按照类C的编译和连接规约来编译和连接,而不是C++的编译的连接规约。这样在类C的代码中就可以调用C++的函数or变量等。(注:我在这里所说的类C,代表的是跟C语言的编译和连接方式一致的所有语言)


C和C++互相调用
我们既然知道extern "C"是实现的类C和C++的混合编程。下面我们就分别介绍如何在C++中调用C的代码、C中调用C++的代码。首先要明白C和C++互相调用,你得知道它们之间的编译和连接差异,及如何利用extern "C"来实现相互调用。


C++的编译和连接
C++是一个面向对象语言(虽不是纯粹的面向对象语言),它支持函数的重载,重载这个特性给我们带来了很大的便利。为了支持函数重载的这个特性,C++编译器实际上将下面这些重载函数:
void print(int i);
void print(char c);
void print(float f);
void print(char* s);
编译为:
_print_int
_print_char
_print_float
_pirnt_string
这样的函数名,来唯一标识每个函数。注:不同的编译器实现可能不一样,但是都是利用这种机制。所以当连接是调用print(3)时,它会去查找_print_int(3)这样的函数。下面说个题外话,正是因为这点,重载被认为不是多态,多态是运行时动态绑定(“一种接口多种实现”),如果硬要认为重载是多态,它顶多是编译时“多态”。
C++中的变量,编译也类似,如全局变量可能编译g_xx,类变量编译为c_xx等。连接是也是按照这种机制去查找相应的变量。


C的编译和连接
C语言中并没有重载和类这些特性,故并不像C++那样print(int i),会被编译为_print_int,而是直接编译为_print等。因此如果直接在C++中调用C的函数会失败,因为连接是调用C中的print(3)时,它会去找_print_int(3)。因此extern "C"的作用就体现出来了。


C++中调用C的代码
假设一个C的头文件cHeader.h中包含一个函数print(int i),为了在C++中能够调用它,必须要加上extern关键字(原因在extern关键字那节已经介绍)。它的代码如下:


#ifndef C_HEADER
#define C_HEADER
extern void print(int i);
#endif C_HEADER
相对应的实现文件为cHeader.c的代码为:
#include <stdio.h>
#include "cHeader.h"
void print(int i)
{
    printf("cHeader %d\n",i);
}
现在C++的代码文件C++.cpp中引用C中的print(int i)函数:
extern "C"{
#include "cHeader.h"
}
 
int main(int argc,char** argv)
{
    print(3);
    return 0;
}
执行程序输出:
cHeader 3






C中调用C++的代码
现在换成在C中调用C++的代码,这与在C++中调用C的代码有所不同。如下在cppHeader.h头文件中定义了下面的代码:
#ifndef CPP_HEADER
#define CPP_HEADER
extern "C" void print(int i);
#endif CPP_HEADER
相应的实现文件cppHeader.cpp文件中代码如下:
#include "cppHeader.h"
#include <iostream>
using namespace std;
void print(int i)
{
    cout<<"cppHeader "<<i<<endl;
}
在C的代码文件c.c中调用print函数:
extern void print(int i);
int main(int argc,char** argv)
{
    print(3);
    return 0;
}
注意在C的代码文件中直接#include "cppHeader.h"头文件,编译出错。而且如果不加extern int print(int i)编译也会出错。






extern "C"函数指针
C函数的指针和C++函数的指针具有不同的类型,不能将C函数的指针初始化或赋值为C++函数的指针,反之亦然































































































  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值