C++
第11章 使用类
11.6类的自动转换和强制类型转换
数据类型的转换
1
2
3
4double tine = 11;
int side = 3.33; 都是正确的,将进行自动类型转换
int *p = 10; 不会进行自动转换,是不兼容的类型,可进行强制类型转换
int *p = (int *)10;下面的构造函数用于将double类型的值转换为Stonewt类类型,介绍的是转换构造函数
1
2
3
4
5
6
7
8
9Stonewt::Stonewt(double lbs) 只能是接受一个参数的构造函数才能这样 Stonewt::Stonewt(int stn,double lbs=0)可以
{
stone = int(lbs)/14;
pounds = lbs;
}
Stonewt myCat;
myCat = 19.6;
先创建一个临时的Stonewt的对象,并将19.2作为初始值,然后将临时对象的内容复制到myCat中,为类的隐式转换,是自动进行的explicit是关闭隐式转换,但仍然允许显式强制类型转换
1
2
3
4
5
6explicit Stonewt(double lbs);
Stonewt myCat;
myCat = 19.6; NO
myCat = Stonewt(19.6); Yes
myCat = (Stonewt)19.6;转换还存在二义性,还可以用于将double值传递给接受Stonewt参数的函数
1
2
3
4
5
6
7
8void display(const Stonewt & st,int n)
{
for(int i = 0;i < n;i++){
cout<<"Wow"
}
}
display(422,2);如果提供了Stonewt(double)构造函数,并且是成员函数的加法函数则可以这样做:
1
2
3
4Stonewt jennySt(9,12);
double kennyD = 176.0;
Stonewt total;
total = jennySt+kennyD;但只有友元函数才允许这样做:
1
2
3
4Stonewt jennySt(9,12);
double kennyD = 176.0;
Stonewt total;
total = kennyD+jennySt;
11.6.1转换函数
转换函数的概念:是将类类型转换为某种类型,是用户定义的强制类型转换
转换函数必须是类方法,不能指定返回类型,不能有参数
1
2
3
4
5
6
7
8
9
10operator double() const; 转换为double类型的函数
Stonewt::operator double() const
{
return pounds; 返回一个double数
}
Stonewt wolfe(285.7);
double honst = double(wolfe);
double honst = (double)wolfe;
double honst = wolfe;注意虽然没有声明返回类型,但也将返回所需的值,是四舍五入的方式而不是去掉小数部分
类类型转换为某种类型也会存在二义性
explicit 不能用于转换函数,但可以使用非转换函数替换,只能进行强制转换
1
2
3
4int Stonewt::Stone_to_Int() {return int (pounds+0.5);}
int plb = poppins; 是非法的
int plb = poppins.Stone_to_Int(); 可以警告:应谨慎地使用隐式转换函数。通常使用显式的强制类型转换
11.6.2转换函数和友元函数
- 实现加法时的选择,要将double量和Stonewt量相加可以有两种方法
第12章 类和动态内存分配
12.1动态内存和类
12.1.1开发一个动态内存类
使用动态内存分配来开发类
1
char *str;
使用char指针,而不是char数组,这意味着类声明没有为字符串分配存储空间
静态类成员特点:
无论创建了多少对象,所有对象共享同一个静态成员,例如,num_strings成员可以记录所创建的对象数目1
2static int num_strings;
int StringBad::num_strings = 0;不能在类声明中初始化静态成员变量,只能在.c文件中初始化,类外也不可以初始化,但如果静态成员是const整数类型或枚举类型(见第十章),则可以在类声明中初始化
创建构造函数1
2
3
4
5
6
7StringBad::StringBad(const char *s)
{
len = strlen(s); 不会包括末尾的空字符'\0'
str = new char[len+1];
strcpy(str,s);
num_strings++; 记录对象的数量
}字符串并不保存在对象中,而是保存在堆内存中,对象仅保存了指出到哪里去查找字符串的信息。不能这样做:
1
str = s;
这只保存了地址,而没有创建字符串副本
析构函数的使用
1
2
3
4
5
6int main()
{
{
StringBad knot;
}
}对象声明放在一个内部代码块中,因为析构函数将在定义对象的代码块执行完毕时调用,对象的删除的顺序与创建顺序相反
在进行输出类时,是运用了重载运算符<<,注意查看重载运算符<<函数中输出的是什么内容
1
2StringBad knot;
cout << knot << endl;编译器会自动生成成员函数和自动使用你不使用函数:构造函数,析构函数
1
2StringBad(const StringBad &); 为复制构造函数,会创建对象的一个副本
StringBad sailor=sports《《 StringBad sailor=StringBad(sports); 调用了一个函数
12.1.2 特殊成员函数
特殊成员函数是自动定义的,有:
默认构造函数,如果没有定义
默认析构函数,如果没有定义
复制构造函数,如果没有定义
赋值运算符,如果没有定义
地址运算符,如果没有定义,返回调用对象的地址(即this指针的值)
c++11新增:
移动构造函数
移动运算符默认构造函数
如果定义了构造函数,c++将不会定义默认构造函数。如果希望在创建对象时不显示地对它进行初始话,则必须显示地定义默认构造函数,它还可以来设定特定的值1
2
3
4
5Klunk::Klunk()
{
klunk_ct = 0;
}
Klunk lunk; 在创建对象时不显示地对它进行初始话带参数的构造函数也可以是默认构造函数,只要所有参数都有默认值,但只有一个默认构造函数,不然会造成二义性
1
Klunk(int n=0){klunk_ct = n};
复制构造函数
1.它用于初始化过程,而不是常规的赋值过程,每当程序生成了对象副本时,编译器都将使用复制构造函数1
StirngBad * pStringBad = new StringBad(const StringBad &);
使用motto初始化一个匿名对象,并将新对象的地址赋给pstring指针
2.当按值传递和返回对象时以及编译器生成临时对象,例如将3个Vectir对象相加时,编译器可能生成临时的Vector对象来保存中间结果,都将调用复制构造函数1
2void callme1(StringBad n); 复制构造函数初始化callme2()函数的StringBad形参
callme2(headline2);3.由于按值传递对象将调用复制构造函数,在用类为函数的参数时应该按引用传递对象
4.如果成员本身就是类对象,则将使用这个类的复制函数来复制成员对象。静态成员不受影响,因为它们属于整个类显示复制构造函数
1
2
3
4StringBad::StringBad(const StringBad & s)
{
num_string++;
}如果类中包含这样的静态数据成员,即其值将在对象被创建时发生变化,则应该提供一个显示复制构造函数来处理计数问题
12.1.3 回到Stringbad: 复制构造函数的哪里出了问题
这里复制的并不是字符串,而是一个指向字符串的指针,得到两个指向同一个字符串的指针,相当于
1
sailor.str = sport.str;(由于私有成员是无法访问的,因此这些代码是不能通过编译的)
sports.str指向的内容已经被sailor的析构函数释放
1
2delete [] sailor.str;
delete [] sports.str;定义一个显式复制构造函数以解决问题(深度复制)
1
2
3
4
5
6
7StringBad::StringBad(const StringBad & st)
{
num_string++;
len = st.len;
str = new char [len+1];
strcpy(str,st.str);
}该复制构造函数应当复制字符串并将副本的地址赋给str成员,如果类中包含了使用new初始化的指针成员,应当定义一个深度复制函数
12.1.4 StringBad的其他问题:赋值运算符
赋值运算符的功能以及何时使用它
将已有的对象赋给另一个对象时,将使用重载的赋值运算符:1
2
3
4StringBad & StringBad::operator=(const StringBad &);
so = s1;
使用函数表示法时:
so.operator(s1);与复制构造函数相似,赋值运算符也对成员进行逐个复制。如果成员本身就是类对象,则程序将使用为这个类定义的赋值运算符来复制该成员,但静态数据成员不受影响
赋值的问题与复制的问题的一样的
解决赋值的问题
1
2
3
4
5
6
7
8
9
10StringBad & StringBad::operator=(const StringBad &st)
{
if(this==&st)
return *this;
delete [] str;
len = st.len;
str = new char [len+1];
strcpy(str,st.str);
return *this;
}1.函数返回一个指向调用对象的引用(即this指针)
2.代码首先检查自我复制,这是通过查看赋值运算符右边的地址(& s)是否与接收对象的地址(this)相同来完成的
3.赋值操作并不创建新的对象,因此不需要调整静态数据成员num_strings的值
改进后的新Stirng类
标准字符串函数库cstring的功能
1
2
3
4
5
6
7
8int length()const {return len}
friend bool operator<(const String & st,const String & st2);
friend bool operator>(const String & st,const String & st2);
friend bool operator==(const String & st,const String & st2);
friend bool operator>>(istream & st,const String & st2);
char & operator[](int i)const;
const char & operator[](int i)const;
static int HowMany();c++11空指针
1
str = nullptr;
nullptr用于表示空指针
重载>>运算符
1
2
3
4
5
6
7
8
9
10istream & operator>>(istream & is,String & st)
{
char temp[80];
is.get(temp,80)
if(is)
str = temp;
while(is&&is.get()!='\n')
continue;
return is;
}为对象数组输入内容
1
2
3
4
5
6
7
8
9
10
11
12
13String saying[n];
char temp[80];
for(int i=0;i<n;i++)
{
cin.get(temp,80);
while(cin&&cin.get()!='\n'){
continue;
}
if(cin)
saying[i] = temp;
else
break;
}为对象数组输出到屏幕上
1
2
3
4for(i=0;i<n;i++)
{
cout<<saying[i][0]<<":"<<saying[i]<<endl;
}找到对象数组中最短的对象
1
2
3
4
5
6int shortest = 0
for(i=0;i<n;i++)
{
if(saying[i].length()<saying[shortest].length)
shortest = i;
}
12.2.2比较成员函数
- 将比较函数作为友元,有助于String对象与常规的c字符串进行比较
1
2
3
4
5if("love"==answer)
将被转换为:
if(operator==("love",answer))
然后,编译器将使用某个构造函数将代码转换为:
if(operator==(String("love"),answer))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
29bool operator<(const String & st1,const String &st2)
{
if(strcmp(st1.str,st2.str)<0)
{
return 0;
}else{
return false;
}
}
bool operator>(const String & st1,const String &st2)
{
if(strcmp(st1.str,st2.str)>0)
{
return 0;
}else{
return false;
}
}
bool operator==(const String & st1,const String &st2)
{
if(strcmp(st1.str,st2.str)==0)
{
return 0;
}else{
return false;
}
}
12.2.3使用中括号表示法访问字符
一般是在String类这种数组中,opera[4]不是指它包含有四个对象,而是第四个字符
1
2
3
4
5
6String opera("The Magic Flute");
opera[4];
char & String::opera[](int i)
{
return str[i];
}将r赋给指向means.str[0]的引用
1
2
3
4String means("might");
means[0]='r';
means.operator[](0)='r
means.str[0]='r' 访问的是私有数据,但由于operator[]()是类的一个方法,因此能够修改数组的内容后三者是等同的
answer是常量,只能使用常量函数
1
2
3
4
5
6
7
8const String answer("futile");
如果只有operator[]()定义,则下面的代码将出错:
cout<<answer[1];
因此提供常量版本:
const char & String::opera[](int i) const
{
return str[i];
}
12.2.4 静态类成员函数
不能通过对象调用静态成员函数,甚至不能使用this指针,它不属于对象,属于类,调用它的方式:
1
int count = String::HowMany();
可以使用类名和作用域解析运算符调用它,可以访问静态成员num_string,但不能访问str
两种的差别
1
2static int num_strings;
static const int CLNLIM = 80;
12.2.5 进一步重载赋值运算符
- 将常规字符串复制到String对象中一般来说,必须释放str指向的内存
1
2
3
4
5
6
7
8String & String::operator=(const char *s)
{
delete[] str;
len = strlen(s);
str = new char [len+1];
strcpy(str,s);
return *this;
}
12.3 在构造函数中使用new时应注意的事项
- 如果有多个构造函数,则必须以相同的方式使用new,要么带中括号,要么不带中括号。因为只有一个析构函数,然而将指针初始化为空,两种都兼容
12.3.1 包含类成员的类的逐成员复制
1 | class Magazine |
String和string都使用动态内存分配,但不需要为Magazine类编写复制构造函数和赋值运算符,会将使用成员类定义的复制构造函数和赋值运算符
12.4 有关返回对象的说明
12.4.1 返回指向const对象的引用
1 | const Vector & Max(const Vector & v1;const Vector & v2) |
第一个const与返回有关,返回的是v1或v2,v1和v2都被声明为const引用,所有才使用const
12.4.2 返回指向非const对象引用
operator<<()的返回类型必须是ostream &,而不能仅仅是ostream。如果使用返回类型ostream,将调用ostream类的复制构造函数,而ostream类没有公有的复制构造函数
12.4.3 返回对象
如果被返回的对象是被调用函数中的局部变量,则不应按引用方式返回它,只能是返回对象
12.5 使用指向对象的指针
1 | String * shortest = &sayings[0]; |
使用结构体的方式来使用成员
- 使用new初始化对象这里指针favorite指向new创建的未被命名对象,但复制构造函数会给它创建内容
1
2
3String *favorite = new String(saying[choice]);
将调用复制构造函数:
String()
12.5.1 再谈定位new运算符
内存缓冲区实则指的是数组
1 | class JustTesting |
将delete用于pc2,将自动调用为pc2指向的的对象调用析构函数,用于buffer时,不会为使用定位new运算符创建的对象调用析构函数,而是需要显示的调用析构函数
,一般情况下将自动调用析构函数,这是需要显示调用析构函数的少数几种情况之一
1 | p1->~JustTesting(); |
cout对地址输出的不同
1
cout<<(void *)buffer<<pc1<<pc2<<endl;
buffer输出地址的方式不同
定位new运算符创建的对象的删除顺序与创建的顺序相反。原因在于晚创建的对象可能依赖于早创建的对象,另外当所有对象都被消除后,才能释放缓冲区
1
2
3p3->~JustTesting();
p1->~JustTesting();
delete [] buffer;
第13章 类继承
面向对象编程的主要目的之一是提供可重用的代码
- 通过继承完成的一些工作:
可以在已有的基础上添加功能。例如,对于数组类,可以添加数学运算。
可以给类添加数据。例如,对于字符串类,可以添加显示颜色的数据成员。
可以修改类方法的行为。例如,提供给飞机乘客的服务的类,可以提供更高级别服务的类。初始化列表语法可以减少一个步骤,它直接使用string的复制构造函数将firstname初始化为fny1
2
3
4
5Table::Table(const string & fn,const string & ln,bool ht):firstname(fn),lastname(ln),hasTable(ht){}
Table::Table(const string & fn,const string & ln,bool ht)
{
firstname = fn;
}
13.1 一个简单的基类
13.1.1
构造函数必须给新成员和继承的成员提供数据。第二个构造函数使用一个类为参数,包含firstname,lastname,hasTable
1 | RatePlayer(int r=0,const string & fn="none"); |
继承类的构造函数的写法
13.1.2 构造函数:访问权限的考虑
派生类不能直接访问基类的私有成员,而必须通过基类方法进行访问。列如,RatePlayer构造函数不能直接设置继承的成员,派生类构造函数必须使用基类构造函数
创建派生类对象时,程序首先创建基类对象。使用成员初始化列表来完成
1
2
3
4RatePlayer::RatePlayer(int r,const string & fn,const string & ln,bool ht):TableTennisPalyer(fn,ln,ht)
{
rating = r;
}调用了TableTennisPalyer的构造函数
1 | RatePlayer::RatePlayer(int r,const string & fn,const string & ln,bool ht) |
3.第二个构造函数的代码
1 | RatePlayer::RatePlayer(int r=0,const TableTennishPlayer & tp):TableTennishPlayer(tp),rating(r) {} |
上述方法声明是在类外声明的,与在类内声明的形式有很大的不一样
释放对象的顺序与创建对象的顺序相反,先执行派生类的析构函数
13.1.4 派生类和基类之间的特殊关系
基类指针或引用可以指向和引用派生类对象
1
2
3
4
5RatedPlayer rplayer1(1140,"Mallory","Duck",true);
TableTennisPlayer & rt = rplayer;
TableTennisPlayer * pt = &rplayer;
rt.Name();
pt->Name();基类指针或引用只能用于调用基类方法,不能使用基类指针或引用来调用派生类的方法
对于形参为指向基类的指针或引用的函数,也可以使用派生类作为实参,按值传递将派生类对象的基类部分传递给函数
1
2
3
4
5void Show(const TableTennisPlayher & rt)
RatedPlayer rplayer1(1140,"Mallory","Duck",true);
TableTennisPlayer player1(1140,"Mallory","Duck",true);
Show(rplayer1);
Show(player1);引用兼容性属性让你能够将基类对象初始化为派生类对象
1
2RatedPlayer rplayer1(1140,"Mallory","Duck",true);
TableTennisPlayer player1(rplayer1);要初始化player1,基类要调用构造函数的原型:
1
TableTennisPlayer(const RatedPlayer & );
基类定义中没有这样的构造函数,但存在隐式复制构造函数
同样,也可以将派生类对象赋给基类对象:
1
2
3RatedPlayer rplayer1(1140,"Mallory","Duck",true);
TableTennisPlayer player1;
player1 = rplayer1;将使用隐式重载赋值运算符
1
TableTennisPlayher & operator=(const TableTennisPlayher & )const;
13.2 继承:is-a关系
公有继承是最常用的方式,它建立一种is-a关系,新类将继承原始类的所有数据成员
公有继承不建立has-a关系,has-a关系:午餐有水果,将水果的对象作为午餐类的数据成员
公有继承不建立is-like-a关系,即律师就像鲨鱼,不应从鲨鱼类派生出律师类,继承可以在基类的基础上添加基础,但不能删除基类的属性
公有继承不建立is-implemented-as-a关系,即作为···来实现,使用数组来实现栈,不可以因为栈不是数组
所以坚持使用is-a的关系,当满足is-a的关系,就可以使用公有继承
13.3 多态公有继承
概念:同一个方法在派生类和基类中的行为是不同的即称为多态–具有多种形态
两种实现方法:
在派生类中重新定义基类的方法
使用虚方法
13.3.1 开发Brass类和BrassPlus类
虚方法的定义
1
2
3
4
5
6
7
8
9class Brass
{
virtual void ViewAcct() const;
}
class BrassPlus:public Brass
{
virtual void ViewAcct() const;
}会在派生类中重新定义基类的方法,但函数名一样的,关键字virtual只用于类声明的方法原型中
引用类型或指针类型选择方法在继承类中的使用
方法没有使用virtual将根据引用类型或指针类型选择方法1
2
3
4
5
6Brass dom("D",121,22);
BrassPlus dot("D",121,22);
Brass & bl_ref = dom; 本应该这样定义的
Brass & b2_ref = dot;
bl_ref.ViewAcct();
b2_ref.ViewAcct();引用变量的类型为Brass,所以都为Brass::ViewAcct()
方法使用virtual将根据引用类型或指针类型选择方法
1 | Brass dom("D",121,22); |
第二个是BrassPlus::ViewAcct()
可以在派生类方法中调用基类的方法
1
2
3
4
5void BrassPlus::ViewAcct() const
{
Brass::ViewAcct();
cout<<"df";
}如果该方法是虚方法,是使用作用域解析运算符来调用基类方法,而不是派生类对象来调用方法;如果不是虚方法
1
2
3
4
5void BrassPlus::ViewAcct() const
{
ViewAcct();
cout<<"df";
}则不必使用作用域解析运算符
使用格式化方法setf()和precision()将浮点值的输出模式设置为定点
1
cout.precision(2);
创建指向Brass的指针数组,可以使用一个数组来表示多种类型的对象,这也是多态,Brass指针既可以指向Brass对象,也可以指向BrassPlus对象
1
2
3
4
5Brass * p_clients[4]; 与一般的数组定义是完全不一样的
for(int i=0;i<4;i++)
{
p_clients[i]=new Brass(temp,tempnum);
}是Brass的指针数组,所以可以进行new分配内存
类对象的输入与一般的数据输入是不一样的
1
2
3
4string temp;
long tempnum;
getline(cin,temp);
cin>>tempnum;多态是由下述代码提供的:
1
2
3
4for(int i=0;i<n;i++)
{
p_clients[i]->ViewAcct;
}p_clients[i]指的是指针不是值
为何需要虚构函数
13.4 静态联编和动态联编
- 在编译过程就知道使用哪一个函数,是静态联编。因为虚函数的存在编译器不知道用户将选择哪种类型的对象,只能在程序运行的时候确定正确的虚函数方法
叫动态联编,总之,编译器对虚方法使用动态联编,根据对象类型将ViewAcct()关联到Brass::ViewAcct()或BrassPlus::ViewAcct()
13.4.1 指针和引用类型的兼容性
c++不允许将一种类型地址或引用赋给另一种类型的指针或引用
1
2
3double x = 2.5;
int *p = &x;
long & rl = x;但基类和派生类可以,而不必进行类型转换
虚函数的工作原理:
给每个对象添加一个隐藏成员,隐藏成员是一个指向函数地址数组的指针,被称为虚函数表虚析构函数
析构函数应当是虚函数,即使它不执行任何操作,除非类不用做基类,析构函数不应进行delete操作1
2
3
4virtual ~BaseClass() {}
Employee *pe = new Singer;
delete pe;如果没有虚析构函数,delete语句将调用
Employer()析构函数,将释放派生类对象中的基类部分指向的内存,但不会释放新的类成员指向的内存,如果有虚析构函Singer析构函数,在调用~Employer()析构函数
数则先调用虚函数的参数要相同,但返回值可以不同
1
2
3
4
5
6
7
8
9
10
11class Dwelling
{
public:
virtual Dwelling & build(int n);
}
class Hovel:public Dwelling
{
public:
virtual Hovel & build(int n);
}重新定义将隐藏方法
1
2
3
4
5
6
7
8
9
10
11
12
13class Dwelling
{
public:
virtual void showperks(int a) const;
virtual void showperks(long a) const;
}
两个都将被隐藏
class Hovel:public Dwelling
{
public:
virtual void showperks() const;
}重新定义继承的方法并不是重载,将隐藏所有的同名基类的方法
13.5 访问控制:protected
- protected与private相似,在类外只能用公有类成员来访问protected部分中的类成员区别在于继承方面:派生类的成员可以直接访问基类的保护成员,但不能直接访问基类的私有成员,在派生类中与公有成员相识。例如可以编写
1
2
3
4
5
6
7class brass
{
private:
doubloe balance;
protected:
doubloe balance;
}
BrassPlus::Withdraw()只有在派生类中可以这样使用,保护数据成员可以简化代码的编写工作,但又使保护数据成员balance成为公有变量,被轻易修改1
2
3
4void BrassPlus::Withdraw(double amt)
{
if(amt< balance)
}
13.6 抽象基类(ABC)
前面已经接受了简单继承和多态继承
另一种建立继承的方法:Ellipse类和Circle类有共点,可以建立拥有他们共同点的类BaseEllipse,这个类还包含Ellipse类和Circle类不的同的方法,应
被声明为虚函数,但至少应有一个纯虚函数抽象函数通过使用纯虚函数来提供未实现的函数
1
2
3
4
5class BaseEllipse
{
public:
virtual double Area() const = 0;
}当类声明中包含纯虚函数时,则不能创建该类的对象,只能用做基类,因此可以从BaseEllipse类派生出Ellipse类和Circle类
Ellipse类和Circle类被称为具体类,具有相同的基类,可以用BaseEllipse指针数组同时管理这两种对象
BaseEllipse类的纯虚函数也应该定于
1
2
3
4
5
6
7
8
9
10
11virtual void Withdraw(double amt) = 0;
void BaseEllipse::Withdraw(double amt)
{
balance -= amt;
}
void Circle::Withdraw(double amt)
{
balance -= amt;
}
13.6.1 应用ABC概念
- ABC是一种必须实施的接口,这种模式在基于组件的编程模式中很常见,每个ABC或者派生类是组件
13.7 继承和动态内存分配
13.7.1 第一种情况:派生类不使用new
- 基类使用动态内存分配,包含特殊方法:析构函数,复制构造函数,重载赋值运算符,而派生类不需要
13.7.2 第二种情况:派生类使用new
- 必须为派生类定义特殊方法
1
2
3
4
5
6
7
8
9
10派生类的析构函数:
baseDMA::~baseDMA()
{
delete [] label;
}
hasDMA::~hasDMA()
{
delete [] style;
}
派生类的复制构造函数:
1 | hasDMA::hasDMA(const hasDMA &hs) |
派生类的重载赋值运算符:
1 | hasDMA & hasDMA::operator=(const hasDMA &hs) |
13.7.3 友元的继承
- hasDMA类的友元访问label和rating的方法:使用强制类型转换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19class baseDMA
{
private:
char *label;
int rating;
}
ostream & operator<<(ostream & os, const baseDMA & rs)
{
os << rs.rating << endl;
return os;
}
ostream & operator<<(ostream & os, const hasDMA & hs)
{
os << (const baseDMA &)hs;
os << hs.style << endl;
return os;
}
第14章 c++中的代码重用
- 可以定义一个通用的栈模板,然后创建表示int或double值栈的类
14.1 包含对象成员的类
- 对于考试分数,可以使用一个定长数组,这限制了数组的长度;可以使用动态内存分配的指针,并提供大量的支持代码;也可以使用动态内存分配的类表示该数组;
还可以在标准c++库中查找一个表示这种数据的类,自己开发这样的类一点问题也没有
14.1.1 valarray类简介
它支持将数组中所有元素的值相加以及在数组中找出最大和最小的值的操作,提供的算术支持比vector和array的多
几个使用其构造函数的例子:
1
2
3
4
5
6
7
double gpa[5] = {3.1,3.5,3.8,2.9,3.3};
valarray <double> v1;
valarray <int> v2(8); 指定长度的空数组
valarray <int> v3(10,8);
valarray <double> v4(gpa,4);
valarray <int> v5 = {12,32,34}; 初始化列表先有长度再有数值,长度放后面
这个类的方法:
1
2
3
4
5size() 返回数组的长度
length() 返回字符串的长度
sum()
max()
min()
14.1.2 Student类的设计
可以从string和valarray这两个类,派生出Student类,这是多重公有继承(一种is-a关系),但这里并不合适,学生类与这些类不是is-a的关系
模板类一般使用自定义的形式
1
2
3
4
5class Student
{
privatef:
typedef std::valarray<double> ArrayDb; 也在using namespace std;
}放在私有部分意味着可以在Student类的实现中使用它,当在Student类外面不能使用
在Student类中,可以直接使用string和valarray这两个类的方法
1
2
3
4
5
6
7
8
9
10class Student
{
public:
Student():name("Null Student"),scores(){}
explicit Student(const string & s):name(s),scores(){}
explicit Student(int n):name("Nully"),scores(n){}
Studeent(const string & s,int n):name(s),scores(n){}
Studeent(const string & s,const ArrayDb & a):name(s),scores(a){}
Studeent(const char *str,const double *pd,int n):name(str),scores(pd,n){}
}初始化被包含的对象时,构造函数将使用成员名,因为初始化的是成员对象,而不是继承的对象
在构造函数有一个参数时,考虑隐式转换函数,没有使用explicit可以写如下
1
2
3Student(int n):name("Nully"),scores(n){}
Student doh(5);将创建一个Nully,5个元素的doh对象,但着一般是不允许的
如果使用了explicit:
1 | explicit Student(int n):name("Nully"),scores(n){} |
将会发生错误
初始化顺序:它们被声明的顺序,而不是它们在初始化列表中的顺序
1
Studeent(const string & s,int n):scores(n),name(s){}
先初始化name成员而不是scores成员,在一个成员的值作为另一个成员的初始化表达式的一部分使,初始化的顺序就非常重要
stu.name是一个string对象,所以调用函数operator<<(ostream &,const string &)
1
2
3
4
5
6
7ostream & operator<<(ostream & os,const Student & stu)
{
os<<stu.name<<endl;
}
scores.sum()
scores.size()可以使用scores类的方法,同样该函数也可以实现valarray的输出,但scores没有<<重载运算符,因此,Student类定义了一个私有辅助方法
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
28ostream & Student::arr_out(ostram & os) const
{
int i;
int lim = scores.size();
if(lim>0)
{
for(i=0;i<lim;i++)
{
os<<scores[i]<<" ";
if(i%5==4) os<<endl;
}
if(i%5!=0)
os<<endl;
}
return os;
}
ostream & operator<<(ostream & os,const Student & stu)
{
os<<stu.name<<endl;
stu.arr_out(os);
return os;
}
double & Student::operator[](int i)
{
return scores[i]; 引用返回的更快
}在包含main()函数的文件中一般还自定义函数
14.2 私有继承
另一种实现has-a关系的途径–私有继承,基类的公有成员和保护成员都将成为派生类的私有成员,可以在派生类的成员函数中使用它们,
即只能在派生类的方法中使用基类的方法,has-a是使用接口,与包含的特性一致,也是将另外两个类的对象做为Student的成员,is-a是使用实现访问限定符的默认类型是私有private
Student类应从两个类派生而来,使用多个基类的继承被称为多重继承
1
2
3
4class Student : private string,private valarray<double>
{
public:
};包含与私有继承的区别:包含提供了两个对象成员,而私有继承提供了两个无名的子对象成员
14.2.1 初始化基类组件
有隐式地继承组件和显式地包含组件
私有继承类的构造函数将使用类名来初始化
1
Student(const char * str,const double * pd,int n) :string(str),ArrayDb(pd,n) {}
14.2.2 访问基类的方法
1 | double Student::Average() const |
在没有确定对象时可以使用类名和作用域解析运算符来调用基类的方法,但函数的作用域与方法的作用域不一致时,就使用解析运算符
14.2.3 访问基类对象
- 使用强制类型转换,将Student对象转换为string对象,*this表示Student对象为避免调用构造函数创建新的对象,可使用强制类型转换来创建一个引用
1
2
3
4const string & Student::Name() const
{
return (const string &) *this;
}
14.2.3 访问基类的友元函数
用类名显式地限定函数名不合适于友元函数,可以使用显示地转换为基类来调用正确的函数
1 | ostream & operator<<(ostream & os,const Student & stu) |
14.2.4 使用修改后的Student类
两个版本的Student类的公有接口(方法)完全相同,因此可以使用同一个程序测试它们
使用包含比私有继承好,如果某个类需要3个string对象,可以使用包含声明3个独立的string成员,如果新类需要访问原有类的保护成员,
或需要重新定义虚函数,则应使用私有继承
14.2.5 保护继承
第三代类体现出保护继承和私有继承的区别
14.2.6 使用using重新定义访问权限
1.在派生类类外调用基类对象的方法
1 | double Student::sum() const |
- 使用一个using声明来指出派生类可以使用特定的基类成员,即使采用的是私有派生using声明只使用成员名——没有圆括号,函数特征标和返回类型
1
2
3
4
5
6
7
8class Student : private string,private valarray<double>
{
public:
using valarray<double>::min;
using valarray<double>::max;
};
stu.min; 可以这样使用
14.3 多重继承
14.4 类模板
- 不如编写一个泛型栈,然后将具体的类型作为参数传递给这个类,这样就可以使用不同类型的栈,例如int栈和string栈
14.4.1 定义模板类
- templat为函数名,尖括号中的内容相当于函数的参数列表,class/typename看作是变量的类型名,Type看作变量的名称可以使用模板成员函数替换原有类的类方法。每个函数头都将以相同模板声明打头
1
2
3
4
5template <class Type>
template <typename Type>
template <class T>
template <typename T>
应改为
1 | Item items[MAX];-->Type items[MAX]; |
如果在类声明中定义了方法(内联定义),则可以省略模板前缀和类限定符
- 不能将模板成员函数放在独立的实现文件中,由于模板不是函数,它们不能单独编译
14.4.2 使用类模板
- 使用的算法必须与类型一致,一般int与string是可以用在同一个模板类中的,string栈与指针栈有相同的功能,但不能用在同一个模板类中
14.4.3 指针栈/指针模板
使用一个指针数组,其中每个指针都指向不同的字符串,用使用动态数组
返回类型为类时也要使用Stack
1
2
3
4Stack & operator=(const Stack & st); 这是缩写,只能在类中使用
template <class Type>
Stack<Type> & Stack<Type>::operator=(const Stack & st) {}
14.4.4 数组模板示例和非类型参数
模板常用作容器类,主要是为容器类提供可重用代码
数组模板的成员是数组
1
template <class Type>
为类型参数
1
2
3
4
5
6template <class T,int n>
class ArrayTP
{
private:
T ar[n];
};为非类型或表达式参数
1
ArrayTP<double,12>eqqweights;
编译器将使用double替换T,使用12替换n
表达式参数可以是整型,枚举,引用或指针,因此,double m是不合法的,但double *rm是合法的
模板代码不能修改参数的值,也不能使用参数的地址,如n++和&n
用作参数的值必须是常量表达式介绍一个允许指定数组大小的简单数组模板:
第一种:使用动态数组和构造函数参数来提供元素数目
第二种:使用模板参数来提供常规数组的大小,array就是这样做的表达式参数方法的缺点:每种数组大小都将生成自己的模板
1
2ArrayTP<double,12> eqqweights;
ArrayTP<double,13> donuts;将生成两个独立的类声明
使用动态数组和构造函数参数的方法的优点:更通用,数组大小是作为类成员存储在定义中的,可以将一种大小的数组赋给另一种大小的数组
14.4.4 模板的多功能性
模板类可用作基类,也可用作组件类,还可用作其他模板的类型参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17template <typename T>
class Array
{
private:
T entry;
};
template <typename Type>
class GrowArray : public Array<Type> {...}
template <class Tp>
class Stack
{
Array<Tp> ar;
};
Array< Stack<int> > asi;递归使用模板:对于前面的数组模板定义
1
ArrayTP< ArrayTP<int,5> 10> twodee;
这使得twodee是一个包含10个元素的数组,其中每个元素都是一个包含5个int元素的数组,与之等价的常规数组声明
1
int towdee[10][5];
即使没有这样的函数也可以这样使用
1
2
3
4
5
6
7for(i=0;i<10;i++)
{
for(j=0;j<5;j++)
{
twodee[i][j]=12;
}
}控制输出宽度的方法
1
cout.width(2);
使用多个类型参数
1
2template <class T1,class T2>
Pair<string,int>模板类的类名是Pair<string,int>,而不是Pair
默认类型模板参数
1
2
3
4template <class T1,class T2 = int>
class Top{...}
Top<double> m1; T2为int型
Top<double double> m2; T2为double型
14.4.6 模板的具体化
隐式实例化
1
ArrayTP<int,100> stuff;
显式实例化
1
template class ArrayTP<string,100>;
之后将生成一个类
显式具体化
1
2
3template <typename T>
class SortedArray {....}假设模板使用>运算符来对值进行比较,对于数字,这管用;如果T是const char *,将不管用,这将要求类定义使用strcmp(),而不是>来对值进行比较,这种情况下
可以提供一个显式模板具体化,即为一种具体类型定义的模板,而不是泛型定义的模板1
2
3
4template <> class SortedArray<const char *>
SortedArray<int> scores;
SortedArray<const char *>dates;
4.部分具体化
1 | template <class T> class Feed {...} |
第二个声明使用通用模板时,将T转换为char *类型,如果是部分具体化,T将转换为char
- 模板该考虑的类型:常规类型,char,char *a, string,ArrayTP,ArrayTP<int,5>
14.4.7 成员模板
模板可用作结构,类或模板类的成员
- 在beta模板外定义hold类和blah方法,模板是嵌套的,还必须指出hold和blab是beta
类的成员 而不能使用1
2
3template <typename T>
template <typename V>
class beta<T>::hold {...}1
template <typename T,typename V>
14.4.8 将模板用作参数
是可以将类进行更改
1 | template <template <typename T> class Thing> |
14.4.8 模板类和友元
14.4.8 模板别名
第15章 友元、异常和其他
15.1 友元
友元类的所有方法都可以访问原始类的私有成员和保护成员
15.1.1 友元类
- 编写一个模拟电视机和遥控器的简单程序,遥控器可以改变电视机的状态,而不是is-a,has-a的关系,因此将Romote类作为Tv类的一个友元,必须先定义Tv满足的是一种先有电视再有遥控器的关系
1
2
3
4
5
6
7
8
9
10
11
12class Tv
{
public:
friend class Remote;
...
}; 放在公有位置
class Remote
{
public:
bool volup(Tv & t) {return t.volup();}
}
15.1.2 友元成员函数
让特定是类成员成为另一个类的友元,而不必让整个类成为友元
让Remote::set_chan()成为Tv类的友元的方法是,在Tv类声明中将其声明为友元,set_chan()使用的是Tv类的成员,所以必须是友元
1
2
3
4
5
6class Tv
{
public:
friend void Remote::set_chan(Tv & t,int c);
...
};必须使用前向声明(forward declaration)
1
2
3class Tv;
class Remote;
class Tv;Remote声明中只包含方法声明,并将实际的定义放在Tv类之后
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class Tv;
class Remote
{
public:
void set_chan(Tv & t,int c);
void set_mode(Tv &t);
}
class Tv
{
public:
friend void Remote::set_chan(Tv & t,int c);
};
inline void Remote::set_chan(Tv & t,int c) {t.channel = c;}
inline void Remote::set_mode(Tv &t) {t.set_mode();}
15.1.3 其他友元关系
15.1.4 共同的友元
15.2 嵌套类
对类进行嵌套与包含并不同,包含意味着将类对象作为另一个类成员,而对类进行嵌套不创建类成员,而是定义了一种类型程序的其他部分
在方法文件中定义构造函数,则定义必须指出Node类是在Queue类中定义的
1
Queue::Node::Node(const Item &i) : item(i),next(0) {}
15.2.1 嵌套类和访问权限
类的默认访问权限是私有的,Queue队列类是嵌套类
嵌套类是在另一个类的私有部分声明的,只有类成员可以使用对象和指向嵌套类对象的指针,派生类和外部世界不知道它的存在
如果是保护部分声明的,派生类可见并且可以创建这种类型的对象,但对于外部是不可见的
如果是公有部分声明的,允许派生类和外部世界使用它
嵌套结构和枚举的作用也是相同的有一个失业的教练,他不属于任何球队,可以在Team类的外面创建Coach对象,是这种关系的类型
1
2
3
4
5
6
7class Team
{
public:
class Coach {...}
};
Team::Coach forhire;Queue类对象只能显示地访问Node节点类对象的公有成员,因为Node类的所有成员都被声明为公有的
1
2
3
4
5
6
7
8
9
10
11
12
13class Queue
{
private:
class Node
{
public:
Item item;
Node *next:
Node(const Item &i) : item(i),next(0) {}
};
Node *front;
Node *rear;
};
15.2.2 模板中的嵌套
Queue类定义转换为模板,是一种容器类
1 | QueueTp<double> dq; |
Node被定义成用于存储double的值
15.3异常
15.4 RTTI
- RTTI是运行阶段类型识别的简称,旨在为程序在运行阶段确定对象的类型提供一种标准方式
15.4.1 RTTI的用途
有一个类层次结构,其中的类都是从同一个基类派生而来的,则可以让基类指针指向其中任何一个类的对象
15.4.2 RTTI的工作原理
RTTI只适用于包含虚函数的类,是用于基类指针与派生类的转换
dynamic_cast运算符,使一个基类指针指向一个派生类的指针,如果失败将空指针赋给指针,使得进行向上转换(is-a的关系)
1
2
3
4
5
6
7
8
9
10class Grand;
class Supeerd : public Grand;
class Magnificent : public Supeerd;
Grand *pg = new Grand;
Superd *ps = new Supeerd;
pg = GetOne();
ps = dynamic_cast<Superd *>(pg);
ps->Say();这样ps可以在pg的基础上使用Say()成员(Grand类并没有这个函数,Superd有这个函数)
typeid运算符和type_info类
15.5 类型转换运算符
在c语言中都是允许的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15struct Data
{
double data[200];
};
struct Junk
{
int junk[100];
};
Data d = {2.5e33,3.5e-19};
char *pch = (char *) (&d); //可以将结构里的成员类型发生转换,转换为字符串
char ch = char (&d); //将地址转换为字符
Junk *pj = (Junk *) (&d); //将结构里的成员类型发生转换,转换为整型const_cast运算符将改变值为const或volatile,类型的其他方面不能被修改
1
2
3High bar;
const High *pbar = &bar;
High *pb = const_cast<High *>(pbar);static_cast运算符是进行向下转换的
1
2
3
4High bar;
Low blow;
Low *pl = static_cast<Low *>(&bar);reinterpret_cast运算符将进行重新解释,将一种类型转换为另一种类型
第16章 string类和标准模板库
- STL编程是一种泛型编程,STL(标准模板库)是用于处理各种容器对象的模板
16.1 string类
- 头文件string.h和cstring支持对c风格字符串进行操纵,不支持string类
16.1.1 构造字符串
- string类的构造函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20string one("Lottery");
string two(20,'s'); //初始化为ssssssssssssssssss
string three(one);
char alsfs[20] = "Alsfsaell";
one += "oops"; //+=运算符被多次重载,可以使用C-风格字符串或char值
one += alsfs;
string four(one,3,6); //从one中取出从第3个到第6个字符
char alls[20] = "Allwell";
string five(alls,20); //取前20字母
string six(alls+2,alls+4); //取alls[2]到alls[4]
string seven(&five[2],&five[4]); //取five[2]到five[4]
不可string seven(five+2,five+4); //five不是指针地址
....... - C++11 新增的构造函数
1
2
3
4string (string && str); //移动构造函数
string (initializer_list<char> il); //列表初始化
string piano_man = {'L','i'};
16.1.2 string类输入
C-风格字符串的输入
1
2
3
4char info[100];
cin >> info;
cin.getline(info,100);
cin.get(info,100);string对象,可以自动调整对象的大小
1
2
3
4string stuff;
cin >> stuff;
getline(cin,stuff);
getline(cin,stuff[2]); //可以这两种使用两个版本都有一个可选参数,用于指定使用哪个字符来确定输入的边界
1
2cin.getline(info,100,':');
getline(stuff,':');string对象输入的限制
1.string对象的最大允许长度,由常量string::npos指定,通常最大值为unsigned int,如果你将整个文件的内容读取到单个string对象中,这是一个限制
16.1.3 使用字符串
length()成员来自string类,而size()是为STL提供的,可以计算数组的大小
成员函数find()方法
1
2
3size_type find(char c,size_type pos = 0) const; //从pos开始查找字符c,找到返回索引
size_type find(const char* s,size_type pos = 0) const; //从pos开始查找字符串s
size_type find(const string& str,size_type pos = 0) const; //从pos开始查找字符串str删除字符串的内容
capacity()返回当前分配给字符串的内存块的大小,resize()能够请求内存块的最小长度
1
2
3string stuff;
stuff.resize(10); //分配10个字符的内存
int n = stuff.capacity();
16.2 智能指针模板类
智能指针定义了类似于指针的类对象,可以帮助管理动态内存分配的智能指针模板,将new获得的地址赋给这种对象
ps是一个常规指针,不是一个类对象
1
string* ps = new string("hello");
16.2.1 使用智能指针
必须包含头文件
模板auto_ptr,unique_ptr,shared_ptr
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18template<class X>
class auto_ptr
{
public:
explicit auto_ptr(X* p = 0);
auto_ptr(auto_ptr& rhs);
auto_ptr& operator=(auto_ptr& rhs);
~auto_ptr();
X& operator*() const;
X* operator->() const;
private:
X* ptr;
};
auto_ptr<string> p1(new string(s));
str = *p1;
p1->find();new string是new返回的指针,p1可以使用string的成员函数
每一个智能指针都放在代码块中,当离开作用域时,指针自动释放
auto_ptr放弃对象的所有权,变成空指针,unique_ptr放弃对象所有权,shared_ptr共享对象所有权
1
2
3
4
5
6auto_ptr<string> p1(new string(s));
auto_ptr<string> p2; //放弃p1的所有权
p2 = p1; //放弃p2的所有权,p1变为空指针,是不被允许的,unique_ptr这样做会直接编译出错
shared_ptr<string> p3;
p3 = p1; //p1和p3指向同一个对象,p1和p3共享对象的所有权使用new分配内存时,才能使用auto_ptr和shared_ptr,使用new[]时,使用unique_ptr
16.2.3 unique_ptr优于auto_ptr
- 函数返回的临时unique_ptr会被销毁不会编译出错
1
2
3
4
5
6
7
8
9
10
11unique_ptr<string> demo(const char *s)
{
unique_ptr<string> ret(new string(s));
return ret;
}
unique_ptr<string> p1;
p1 = demo("hello"); //编译出错,unique_ptr不能被赋值
unique_ptr<string> p3;
p3 = unique_ter<string>(new string("hello")); //正确
16.2.4 选择智能指针
选择shared_ptr的情况:
1.有一个指针数组,并使用一些辅助指针来标识特定的元素
2.两个对象包含都指向第三个对象的指针
3.STL容器包含指针STL算法都支持赋值和复制操作,可用于shared_ptr,只要不调用将一个unique_ptr复制和赋值给另一个的方法或算法,但不能用于unique_ptr
1
2vector<unique_ptr<int>> vp(size);
vp.push_back(unique_ptr<int>);push_back()调用没有问题,因为它返回一个临时unique_ptr,被赋给vp中的一个unique_ptr
unique_ptr为右值时,可将其赋给dhared_ptr
1
shared_ptr<int> p1(unique_ptr<int>);
shared_ptr包含一个显示构造函数,可将右值unique_ptr转换为shared_ptr
16.3 标准模板库
迭代器能够用来遍历容器的对象,与能够遍历数组的指针类似,是广义指针
容器:数组,队列,链表,是选择一种能存储多种数据类型的数据类型成为容器
操作:搜索,排列和随机排列,适用于所有容器类的非成员函数,省去了大量重复的工作
STL不是面向对象编程,而是一种不同的编程模式——泛型编程
数学矢量与计算矢量不一样
能够分配容器的对象大小的容器,都使用了动态内存分配
16.3.1 模板类vector
分配器:管理内存分配和释放的类
各种STL容器模板都接受一个可选的模板参数,该参数指定使用哪个分配器对象来管理内存
1
2template <class T,class Allocator = allocator<T>>
class vectorr{...};这个类使用new和delete来分配和释放内存, allocator
为分配器
16.3.2 可执行的操作
所有的STL容器都提供了一些基本方法
1
2
3
4size()
swap()交换对象的内容
begin()返回一个指向容器中第一个元素
end()超过容器尾的迭代器迭代器解除引用和递增
1
2operator*()返回指向当前元素的指针
operator++()递增迭代器
3.每个容器类都定义了一个合适的迭代器,该迭代器是一个名为iterator的模板类,作用域为整个类
1 | vector<int>::iterator pd; |
声明了一个迭代器
1 | vector<int> scores; |
一般带有指针的名字都会有指针的性质,只不过在取地址这里有点不一样
迭代器遍历容器内容
1
for(pd = scores.begin();pd!=scores.end();pd++)
vector类的才有的push_back(),在矢量末尾添加元素,它将负责内存管理,增加矢量的长度
1
2
3
4vector<int> scores;
int a=10;
scores.push_back(100);
scores.push_back(a);erase()方法删除矢量中给定区间的元素
vector提供了随机访问功能,因此可以有begin()+2操作
1
scores.erase(scores.begin(),scores.begin()+2);
insert()方法将元素插入到矢量中的指定位置,该区间是另一个容器对象的一部分
1
2
3
4vector<int> old_v;
vector<int> new_v;
...
old_v.insert(old_v.begin()+2,new_v.begin(),new_v.end());swap()交换两个对象的内容
1
2
3vector<int> old_v;
vector<int> new_v;
old_v.swap(new_v);8个容器类,需要支持10中操作,都有自己的成员函数,则要定义80个成员函数,但采用STL方式时,只需要10个非成员函数即可
即使有执行相同任务的非成员函数,STL有时也会定义一个成员函数,因为类特定算法的效率比通用算法高,vector成员函数swap()效率比非成员函数swap()高,但非成员函数让你能够交换两个不同容器的内容
STL函数:for_each(),random_shuffle()和sort(),必须包含
1
2
3
4
5vector<Review>::iterator pr;
for (pr = books.begin(); pr != books.end(); pr++)
ShowReview(*pr);
替换为:
for_each(books.begin(),books.end(),ShowReview);最后一个参数是指向函数的指针(函数对象),该函数不可以修改容器元素的值
1 | random_shuffle(books.begin(),books.end()); |
随机排列该区间中的元素,要求容器类允许随机访问
1 | bool operator<(const Review& r1,const Review& r2) const; //注意是使用的是布尔类型的返回值,现在函数总是用布尔类型的返回值和循环是否结束 |
sort()第一个版本使用为存储在容器中的类型元素定义的<运算符,如果容器元素类型是用户定义的,则要使用sort(),必须定义能够该类型对象的operator<()函数,为Review提供了成员或非成员函数operator<()
1 | bool WorseThan(const Review& r1,const Review& r2) const; |
第二种版本的sort()
16.3.4 基于范围的for循环
- 基于范围的for循环是为用于STL而设计的
1
2
3double prices[5] = {10.0,20.0,30.0,40.0,50.0};
for(double x: prices)
cout<<x<<endl;
2.循环将依次将books中的每个Review对象传递给ShowReview()
1 | for(auto x : books) |
- 不同于for_each(),基于范围的for循环可以修改容器中的元素,但要指定一个引用参数
1
2
3void InflateReview(Review& r){r.rating++;}
for(auto & x : books) //两个地方必须要有引用
InflateReview(x);
16.4 泛型编程
STL是一种泛型编程,面向对象编程关注的是编程的数据方面,使任何数据类型能存在容器中,而泛型编程关注的是算法,使任何容器能运用于算法,共同特定是抽象和创建可重用代码
模板使得算法独立于存储的数据类型,而迭代器使算法独立于使用的容器类型
定义一种链表类型的迭代器类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25struct Node
{
double item;
Node* p_next;
}
class iterator
{
Node* pt;
public:
iterator(0) : pt(0) {};
iterator(Node* pn) : pt(pn) {};
double operator*() {return pt->item;}
iterator operator++() //是返回的是类对象
{
pt = pt->p_next;
return *this;
}
iterator operator++(int)
{
iterator tmp = *this;
pt = pt->p_next;
return tmp;
}
}find_ar()与find_ll几乎相同,区别在于结束的条件不同,这就需要不同的容器了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18typedef double* iterator;
iterator find_ar(iterator begin,iterator end,double & val)
{
iterator ar;
for(ar = begin;ar!=end;ar++)
if(*ar == val)
return ar;
return end;
}
iterator find_ll(iterator head,const double & val)
{
interator start;
for(start = head;start!=0;start++)
if(*start == val)
return start;
return 0;
}find()函数的实现方法
每个容器类定义了相应的迭代器类型,可能是指针,可能是对象,每个容器类都有begin()和end()方法,都使用++,让迭代器递增
C++11新增的自动类型推断
1
2for(auto pr = scores.begin();pr != scores.end();pr++)
cout << *pr << endl;最好避免直接使用迭代器,而尽量使用for_each(),和基于范围的for循环
有了迭代器算法才能通用,基于算法的要求,设计基本迭代器的特征和容器的特征
16.4.2 迭代器类型
不同的算法对迭代器的要求也不同,排序算法需要能够随机访问,可以通过定义+运算符来实现,迭代器也是一个类,含有构造函数
如果两个迭代器相同,则解除引用操作得到的值将相同
1
2iter1 == iter2
*iter1 == *iter2输出迭代器只能修改容器值,而不能读取,程序的输出就是容器的输入,输入和输出迭代器都是单通行,不能保证第二次遍历容器时,顺序不变,也不能保证其先前值仍然可以被解除引用
正向迭代器可以对前面的迭代器值解除引用,可以读取和修改数据,也可以只读取数据
1
2int *pirw;
const int *pirw;双向迭代器中,reverse函数可以交换第一个元素和最后一个元素,将指向第一个元素的指针加1,指向第一个元素的指针减1
sort()函数需要随机访问迭代器,所以只能用于支持这种迭代器的容器,随机访问迭代器实现为一个常规指针,正向迭代器实现为一个类
一种迭代器的类型是不一样的
1
2vector<int>::iterator
vector<double>::iterator //两种类型的迭代器每个容器类都定义了一个类级typedef名称——iterator
9.如果所设计的容器类需要迭代器,可考虑STL,它包含用于标准种类的迭代器模板
16.4.4 概念,改进和模型
将指针用作迭代器,使得STL算法用于常规数组
将一个数组复制到一个矢量中copy()函数
1
2
3int casts[10] = {6,7,8,8,8,8,8,8,};
vector<int> dice[10]; //copy()不能自动根据发送值调整目标容器的长度
copy(casts,casts+10,dice.begin())前两个参数必须是输入迭代器,最后一个必须是输出迭代器
输出流迭代器,STL为这种迭代器提供了ostream_iterator模板,是输出迭代器的一个模型,也是一个适配器——类或函数,可以将一些其他接口转换为STL使用的接口(使得cout可以在算法中使用),迭代器就是STL的接口,要包含头文件iterator
1
2
3
ostream_iterator<int,char> out_iter(cout," "); //构造函数的第一个参数指出了要使用的输出流,第二个参数指出了要使用的分隔符
out_iter++ = 15;out_iter才是迭代器,意味着将15和有空格组成的字符串发送到输出流中,并为下一个输出做准备
copy()函数的另一种用法
1
2
3
4
5copy(dice.begin(),dice.end(),out_iter);
copy(dice.begin(),dice.end(),ostream_iterator<int,char>(cout," ");
copy(istream_iterator<int,char>(cin),istream_iterator<int,char>,dice.begin());将dice的内容复制到cout输出流中,即显示容器的内容
其他有用的迭代器(reverse_iterator,back_insert_iterator,frint_insert_iterator)
1
copy(dice.rbegin(),dice.rend(),out_iter);
这样不必声明反向迭代器reverse_iterator,rbegin()和end()返回的值相同,但类型不同(reverse_iterator,iterator)
反向指针先通过递减,再解除引用来解决rbegin()的超尾的问题,rp指向位置6,则*rp将是位置5的值
1
2
3vector<int>::reverse_iterator ri;
for(ri = dice.rbegin();ri != dice.rend();ri++)
cout << *ri << " ";三种插入迭代器:back_insert_iterator,front_insert_iterator,insert_iterator
back_insert_iterator只能于允许在尾部快速插入的容器(快速插入指的是一个小时固定的算法),vector满足,将容器类型作为模板参数,将实际的容器标识符作为构造函数参数
1
back_insert_iterator<vector<int> > back_iter(dice);
构造函数将假设传递给它的类型有一个push_back()方法
front_insert_iterator,满足queue,不满足vector,完成任务很快
insert_iterator没有这些限制,还需要一个指示插入位置的构造函数参数
1
insert_iterator<vector<int> > insert_iter(dice,dice.begin());
二维数组也是同样的运用
1
2
3string s1[2] = {"fsdaf","ffsadfds"}
vector<string> s2(4);
copy(s1,s1+2,s2.begin()); //stirng没有迭代器可以用insert_iterator将复制数据的算法转换为插入数据的算法
1
copy(s1,s1+2,back_insert_iterator<vector<string> >(words));
这些预定义迭代器增加了函数的功能,比如copy()函数
16.4.5 容器的种类
STL具有容器概念和容器类型,容器类型是可用于创建具体容器对象的模板,有deque,list,queue,priority_queue,stack,vector,map,multimap,set,multiset,bitset
1
2
3
4
5
6
7
8
9
10
11"deque": "双端队列(double-ended queue),可以从中端和末端插入和删除元素,是可以随机访问的",
"list": "链表(list)是一个双向链表,可以随时在任何位置插入或删除元素。",
"queue": "队列(queue)是一种先进先出(FIFO)的数据结构,只能在末端添加元素,在前端删除元素。",
"priority_queue": "优先队列(priority queue)是一种特殊的队列,每个元素都有一个优先级,优先级最高的元素最先出队。",
"stack": "栈(stack)是一种后进先出(LIFO)的数据结构,只能在顶端添加或删除元素。",
"vector": "向量(vector)是动态数组,可以动态地增加和减少元素,在尾部添加和删除元素的时间是固定的,但在头部或中间插入和删除元素为线性时间,还是一种反转容器",
"map": "映射(map)是一种关联数组,它存储的是键值对(key-value pairs)。",
"multimap": "多重映射(multimap)类似于映射,但允许存在多个相同的键。",
"set": "集合(set)是一种不包含重复元素的无序集合。",
"multiset": "多重集合(multiset)类似于集合,但允许存在重复的元素。",
"bitset": "位集(bitset)是一种特殊的数组,它存储的是位(0或1)。"C++11新增的容器类
1
2
3std::unordered_map:基于哈希表的关联容器,用于存储键值对,查找、插入和删除操作的平均时间复杂度为O(1)。
std::unordered_set:基于哈希表的集合容器,用于存储唯一的键,查找、插入和删除操作的平均时间复杂度为O(1)。
std::forward_list:单向链表容器,只支持正序遍历,插入和删除操作在链表头部和尾部速度很快。一些基本的容器特征
1
2
3
4
5
6X a;
(&a)->~X(); //线性时间
a.size(); //固定时间
a.swap(); //固定时间“复杂度”,从快到慢:
1.编译时间,在编译时执行,指向时间为0
2.固定时间,在运行时执行,指向时间为O(1),独立于对象中的元素数目
3.线性时间,时间与元素数目成正比,指向时间为O(n)序列是基本的容器概念的改进,包括deque,list,vector,forward_list,queue,stack,priority_queue,要求是正向迭代器,保证了元素将按特定顺序排序,即除了第一和最后,每个元素前后都分别有一个元素
为list,deque定义了push_front,而没有为vector定义,是因为在矢量前插入一个元素,需要移动大量的元素,而list和deque的允许将元素添加到前端,而不移动其他元素,以固定时间来完成,所以vector没有必要定义push_front
deque和vector都对元素进行随机访问和在中部执行线性时间的插入和删除,但vector容器执行的更快,因为vector的内存是连续的,而deque的内存不是连续的,所以vector的访问速度更快,deque更复杂
双向链表可以双向遍历链表,可以从后面往前面遍历,也是反转容器
list成员函数
1
2
3
4
5void merge(list<T,Alloc>&x); //将两个链表合并,两个链表必须是已经排序,合并后x为空,为线性时间
void remove(const T&value); //删除所有值为value的元素,线性时间
void unique(); //将连续的相同(即相邻的相同值)的元素压缩为单个元素,可以结合sort()来使用,线性时间
void sort(); //使用<运算给发,将元素排序,线性时间为NlogN
void splice(iterator pos,list<T,Alloc> x) //将x中的元素插入到pos之前,x将为空,为固定时间非成员函数sort(),需要随机访问迭代器,不能用于链表,所以只能使用类中的成员函数版本
list工具箱:list方法组成了一个方便的工具箱,例如有两个邮件列表要整理,则可以对每个列表进行排序,合并它们,然后使用unique()来删除重复的元素
C++11新增的forward_list,实现单链表,无反向迭代器,因为每个节点都只链接到下一个节点,而没有链接到前一个节点
queue模板类是一个适配器类,让底层类(deque)展示典型的队列接口,既不允许随机访问,也不允许遍历队列,只允许队列的基本操作,使用这个值与栈一样
1
2
3
4
5
6push:在队列的末尾添加一个元素。
pop:从队列的开头移除一个元素,并返回该元素。
front:返回队列的第一个元素,但不移除该元素。
back:返回队列的最后一个元素,但不移除该元素。
empty:检查队列是否为空,如果为空则返回true,否则返回false。
size:返回队列中的元素个数。priority_queue是另一个适配器类,与queue操作相同,主要是priority_queue最大的元素被移到队首,底层类是vector,可以修改用于确定哪个元素放到队首的比较方式
1
priority_queue<int> pq1;
这用到了构造函数
stack模板类,与queue相同,也是适配器类,底层类是vector,给底层类提供了典型的栈接口,既不允许随机访问,也不允许遍历栈,只允许栈的基本操作:将压入推到栈顶,从栈顶弹出元素,查看栈顶的值,检查元素数目和测试栈是否为空
1
2
3
4
5bool empty()const:判断栈是否为空,如果为空则返回true,否则返回false。
size_type size()const:返回栈的元素个数。
T& top():返回栈顶元素,并弹出该元素。
void push(const T& x):将元素压入栈中。
void pop():弹出栈顶元素。如果要使用栈值,必须首先使用top()来检索这个值,然后使用pop()将它从栈中删除
array模板类并非STL容器,因为其长度是固定的,没有定义调整容器大小的操作,如push_back()和insert(),可将标准STL算法用于array对象,如copy()和for_each()
16.4.6 关联容器
关联容器将值与键冠梁在一起,并使用键来查找值,优点在于它提供了对元素的快速访问,允许插入元素新元素,但不能指定元素的插入位置原因是关联容器通常有用于确定数据放置位置的算法,而不是使用迭代器,以便能够快速找到元素
关联容器通常是使用某种树实现的,4种关联容器:set,multiset,map,multimap,前两种是在头文件set中定义,后两种是在map中定义的
set其值类型与键相同,键是唯一的,不会有多个相同的键,multimap可能有多个值的键相同
map中,值与键的类型不同,键是唯一的,每个键只对应一个值,multimap可能有一个键可以与多个值关联
set模板类可反转,可排序,且键是唯一的,所以不能存储多个相同的值,第二个模板参数,可用于指示用来对键进行的比较函数对象
1
2
3
4
5
6
7
8set<string> A;
set<stirng less<string> > A;
cconst int N = 6;
string s1[N] = {"adsf", "bwe", "cfdsf", "dzv", "ev", "fbn"};
set<string> A(s1, s1 + N);
ostream_iterator<string, char> out(cout, " ");
copy(A.begin(), A.end(), out);set有一个将迭代器区间作为参数的构造函数,键是唯一的,所以”for”在数组两次出现,但在集合中只出现一次,且集合被排序
数学为集合定义了一些标准操作,如并集,交集,这些操作的算法,是通用函数,不是类方法
1
set_union(A.begin(), A.end(), B.begin(), B.end(), ostream_iterator<string,char> out(cout, " "));
显示集合A和B的并集,并进行了排序
multimap的模板参数指定键的类型和存储的值的类型,为将信息结合在一起,实际的值的类型将键类型与数据类型结合为一对,STL使用模板类pair<class T,call U>将两种值存储到一个对象中
1
2
3multimap<int,string> codes;
pair<const int,string>codes对象的值类型为pair<const int,string>
用区号作为键来存储城市名,这恰好与codes值类型一致
1
2pair<const int,string> iteam(231, "New York");
codes.insert(iteam);因为数据项是按键排序的,所以不需要指出插入位置
16.4.7 无序关联容器
- 无序关联容器也将值与键关联起来,并使用键来查找值,是基于哈希表,旨在提高添加和删除元素的速度以及查找算法的效率:unordered_set,unordered_multiset,unordered_map,unordered_multimap
16.5 函数对象
- 很多STL算法使用了函数对象,包括函数名,指向函数的指针和重载了()运算符的类对象(即定义了函数operator()()的类)
1
2
3
4
5
6
7
8
9
10
11class Linear
{
private:
double slope;
double y0;
public:
double operator()(double x) {return y0+slope*x;}
};
Linear f1;
double y1 = f1(2.0);
16.5.1 函数符概念
生成器是不用参数就可以调用的函数符,一元函数是用一个参数就可以调用的函数符,例如,for_each()是一元函数,因为它每次用于一个容器元素,返回bool值的一元函数是谓词
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25bool WorseThan(const Review& r1, const Review& r2);
sort(books.begin(),books.end(),WorseThan);
bool tooBig(int n) {return n > 100;}
list<int> scores;
scores.remove_if(tooBig); //删除链表中所有大于100的元素
template<class T>
bool tooBig2(const T & val,const T & lim) //可以将两个参数的模板函数转换为单个参数的函数对象
{
return val > lim;
}
Tempalte<class T>
class TooBig
{
private:
T cutoff;
public:
TooBig(const T & t) : cutoff(t) {}
bool operator()(const & v){return v > cutoff;}
}
TooBig<int> p(100);
scores.remove_if(p); //直接使用类对象n的值来自链表中,设计一个TooBing类,来控制大于多少的元素将被删除
函数对象是一种适配器,使函数或类成员函数能够满足不同的接口(接口就是函数或类成员函数)
16.5.2 预定义的函数符
STL定义了多个基本函数符,它们执行诸如将两个值相加,比较两个值是否相等操作,transform()有两个版本
1
2
3
4
5transform(v.begin(),v.end(),v.begin(),sqrt); //计算每个元素的平方根
transform(v.begin(),v.end(),v1.begin(),v.begin(),mean); //计算v所有元素和v1的第一个元素的平均值
transform(v.begin(),v.end(),v1.begin(),v.begin(),plus<double>()); //()可有可无,函数对象不会带参数的头文件functional定义了多个模板类函数对象,其中包括plus类
对于所有内置的算术运算符,关系运算符和逻辑运算符,STL都提供了函数对象,例如,+ plus
()和 - minus ()是两个函数对象
16.5.3 自适应函数符和函数适配器
表16.12列出的预定义函数符都是自适应的,函数符自适应性的意义在于:函数适配器对象可以使用函数对象,使函数能匹配不同的接口
函数适配器将接受两个参数的函数符转换为接受1个参数的函数符,前面的TooBig2示例提供了一种方法,但STL使用binder1st和binder2nd类自动完成这个过程,
1
binder1st(f2,val) f1; //f2是一个自适应二元函数
f1对象将与f2的第一个参数val相关联
函数binder1st()与binder1st类的作用相同,binder2st,只是将常数赋给第二个参数
1
transform(gr8.begin(),gr8.end(),out,bind1st(multiplies<int>(),2));
将二元函数multiplies()转换为将参数乘以2的一元函数
16.6 算法
STL的非成员函数:sort(),copy(),find(),random_shuffle(),set_union(),set_intersection(),set_difference(),transform(),有些函数接受一个函数对象
统一的容器设计使得不同类型的容器之间具有明显的关系,例如可以使用copy()将vector对象中的值复制到list对象中,用==来比较不同类型的容器,如deque和vector,之所以能这样做是容器都使用迭代器来提供访问容器中的数据
16.6.1 算法组
- 通用数字运算的算法在头文件numeric中定义,vector最有可能使用这些操作的容器
16.6.2 算法的通用特征
sort()的结果被存放在原始数据的位置上,copy()将结果发送到另一个位置,transform()可以以这两种方式完成工作
copy()的原型
1
2template <class InputIterator,class OutputIterator>
OutputIterator copy(InputIterator first,InputIterator last,OutputIterator result);这包含了两种迭代器
有些算法有两个版本:就地版本和复制版本,复制版本的名称将以_copy()结尾,将接受一个多的输出迭代器参数
第17章 泛型编程注意的事项
1 |
|
- string对象初始化
1
2
3
4
5
6
7
8
9
10string s1[2] = {"fsdaf","ffsadfds"};
vector<string> s2(4); //两个不同表示数组大小的方式
````
2. STL中的类,对象和通常函数都要处理空间名称
```c++
std::set<std::string> B(s2,s2+N);
std::ostream_iterator<std::string,char> out(std::cout," ");
std::copy(B.begin(),B.end(),out);


