容器和算法
顺序容器的操作
容器元素的初始化:
C<T> c; //创建一个名为c的空容器。C是容器类型名,T是元素类型,适用于所有容器
C<T> c2(c);//创建容器c的副本,c2和c必须具有相同的容器类型,并存放相同类型的元素。适用于所有容器
C<T> c(b,e);//创建c,其元素是迭代器b和e表示的范围内元素的副本。适用于所有容器
C<T> c(n,t);//用n个值为t的元素创建容器c,其中t必须是容器类型c的元素类型的值,或者可转换为该类型的值。只适用于顺序容器
C<T> c(n); //创建有n个值初始化元素的容器c。只适用于顺序容器
vector和deque类型迭代器支持的操作:
iter+n; iter-n; iter+=iter; iter-=iter; iter-iter; <=, <, >=, >
关系操作符只适用于vector和deque容器,为其提供随即快速的访问元素。
list容器的迭代器既不支持算术运算(加法或减法),也不支持关系运算(<=,<,>=,>)它只提供前置和后置的自增自减及相等(不等)运算。
在顺序容器中添加元素操作:
c.push_back(t);在容器尾部添加值为t的元素,返回void类型。
c.push_front(t);在容器前端添加值为t的元素,返回void类型。只适用于list,deque
c.insert(p,t); 在迭代器p所指向的元素前面插入t元素,返回指向新元素的迭代器。等效于push_front函数。
c.insert(p,n,t); 在迭代器p所指向的元素前面插入n个值为t的元素,返回void类型。
c.insert(p,b,e); 在迭代器p所指向的元素前面插入由迭代器b和e表决范围内的元素。
容器元素都是副本。在添加元素时,系统是将元素值赋值到容器中,容器元素的改变不影响被复制的原值。
避免存储end操作返回的迭代器。添加或删除deque或vector容器内的元素都会导致存储的迭代器失效。
访问顺序容器内元素的操作:
c.back() 返回容器c的最后一个元素的引用。如果c为空,则该操作未定义。
c.front() 返回容器c的第一个元素的引用。如果c为空,则该操作未定义。
c[n] 返回下标为n的元素的引用。如果n<0或n>c.size(),则该操作未定义,只适用于deque和vector容器。
c.at(n) 返回下标为n的元素的引用,如果小标越界,则该操作未定义,只适用于deque和vector容器。
删除顺序容器内元素的操作:
c.erase(p) 删除迭代器p所指向的元素。返回一个迭代器,指向被删除元素后面的元素。如果p本身是指向超出末端的下一个位置的迭代器,则该函数未定义
c.erase(b,e) 删除迭代器b和e所标记范围内的所有元素。
c.clear() 删除c内的所有元素,返回void
c.pop_back() 删除c的最后一个元素,返回void。如果c为空,则该函数未定义
c. pop_back() 删除c的第一个元素,返回void。如果c为空,则该函数未定义。只适用于list或deque容器
erase,pop_back,pop_back函数是指向被删除元素的所有迭代器失效。对于vector容器,指向删除点后面的元素的迭代器通常也会失效。而对于deque容器,如果删除时不包含第一个元素或最后一个元素,那么该deque容器相关的所有迭代器都会失效。
赋值与swap
c1 = c2 类型必须相同。
c1 =swap(c2) 交换的速度比赋值快。
c.assign(b,e) 重新设置c的元素:迭代器b,e必须不是指向c中元素的迭代器。可用于不同(或相同)类型的容器,但元素类型不同但相互兼容(或相同)中
c.assign(n,t) 将容器c重新设置为存储n个值为t的元素。
与复制相关的操作都作用于整个容器。除swap操作外,其他操作都可以用erase和insert操作实现。复制后,左右两边的容器相等:尽管复制前后两个容器的长度可能不相等。
通常来说,除非找到选择使用其他容器的更好理由,否则vector容器都是最佳选择。
选择容器类型法则(考虑插入/删除操作和访问操作哪个使用得多?):
(1)程序要求随机访问元素,则使用vector或deque容器。
(2)程序必须在容器的中间位置插入或删除元素,则采用list容器。
(3)程序在容器首部或尾部插入或删除元素,则采用deque容器。
(4)如果只需在读取输入是在容器的中间位置插入元素,然后需要随机访问元素,则可考虑在输入时将元素读入到一个list容器,接着对此容器重新排序,使其适合顺序访问,然后将排序后的list容器复制到一个vector容器。
关联容器(map,set,multimap,multiset)
pair类型:
multimap:一个键对应多个值,同一个键所关联的元素必然相邻存放,不可以通过下标访问。multimap访问元素方法:
(1)使用count和find操作。count计数,find返回指向第一个拥有正在查找的键:
string search_item("C++primer");
typedefmultimap<string,string>::size_type sz_type;
sz_type entries=authors.count(search_item);
multimap<string,string>::iteratoriter=authors.find(search_item);
for(sz_typecnt=0;cnt!=entries;++cnt,++iter)
{
cout<<iter->second<<endl;
}
(2)与众不同的迭代器的解决方案
m.lower_bound(k); 返回一个迭代器,指向键不小于k的第一个元素
m.upper_bound(k); 返回一个迭代器,指向键大于k的第一个元素
m.equal_range(k); 返回一个迭代器的pair对象,他的first成员等价于m.lower_bound(k)。second成员等价于m. upper_bound(k)。
泛型算法
算法不直接修改容器的大小。如果需要添加或删除元素,则必须使用容器操作。
类
在类内部定义的成员函数默认为inline,在类外部定义的成员函数需指明类的作用域。
设计类的接口时,设计者应该考虑的是如何方便类的使用;使用类的时候,设计者就不应该考虑类如何工作。
在声明和定义出指定inline都是合法的。在类外部定义inline使类容易阅读。
类的声明和定义:类声明是不完全类型,只能以有限方式使用,不能定义该类型的对象,只能用于定义指向该类型的指针及引用,或者声明使用该类型作为形参类型或返回类型的函数。类定义,一旦类被定义,我们就可以知道所有类的成员,以及存储该类的对象所需的存储空间。在创建类的对象之前或者使用引用或指针访问类的成员之前必须定义类。
类对象:定义一个类时,也就定义了一个类型。一旦定义了类,就可以定义该类型的对象。定义对象时,将为其分配足以容纳该对象的内存。(定义类类型时不分配内存,定义类对象时分配内存)每个对象具有自己的类型数据成员的副本。
定义类类型的对象:
(1)类名字 类对象名;如:Sales_item item1;
(2)class(struct)类名字 类对象名;class Sales_item item2;
为什么类的定义一分号结束?:分号是必需的,因为在类定义之后可以接一个对象定义列表(应避免这么做)。
在普通的const成员函数中,this的类型是一个指向类类型对象的const指针。可以改变this所指向的值,但不能改变this所保存的地址。在const成员函数中,this的类型是一个指向const类类型对象的const指针。既不能改变this所指向的对象,也不能改变this所保存的地址。不能从const成员函数返回指向类对象的普通引用。const成员函数只能返回*this作为一个const引用。
基于成员函数是否为const,可以重载一个成员函数;同样的,基于一个指针形参是否指向const,可以重载一个函数。const对象只能使用const成员。非cosnt对象可以使用任一成员,但最好也使用非cosnt成员。
可变数据成员:将数据成员声明为mutable来实现,可修改类的数据成员(甚至在const成员函数内)。可变数据成员永远都不能为const,甚至当它是const对象的成员时也如此。
构造函数不能声明为const。const构造函数是不必要的。创建类类型的const对象时,运行一个普通构造函数来初始化该const对象。构造函数的工作是初始化对象。不管对象是否为const,都用一个构造函数来初始化该对象。
构造函数初始化
有些成员必须在初始化列表中进行初始化。对这样的成员,在构造函数体中对它们赋值不起作用。没有默认构造函数的类类型的成员了,以及const或引用类型的成员,都必须在构造函数初始化列表中进行初始化。对非类类型的数据成员进行赋值或使用初始化在结果和性能上都是等价的。可以初始化const对象或引用类型的对象,但不能对他们赋值,在开始执行构造函数的函数体之前要完成初始化。当一个类中自己定义了构造函数时,编译器就不会自动合成一个默认构造函数了。
防止由构造函数定义的隐式转换:显示地声明构造函数—explicit关键字。
string类型的对象可以隐式地转换成临时的类对象。
stringnull_isbn=”9-999-9999-9”;
Sales_itemnull(null_isbn);//使用string的对象null_isbn为实参,调用Sales_item类的构造函数创建Sales_item 对象 null1。
Sales_itemnull(”9-999-9999-9”);// 使用接受一个 C 风格字符串形参的 string 类的构造函数,生成一个临时string 对象,然后用这个临时对象作为实参,调用Sales_item 类的构造函数来创建Sales_item 类的对象 null。
static类成员
sttic数据成员独立于该类的任意对象:每个static数据成员是与类关联的对象,而不与该类的对象相关联。
static成员函数没有this形参,它可以直接访问所属类的static成员,但不能直接使用非static成员。
使用static成员而不是全局对象的三个优点:
(1)static成员的名字是在作用域中,因此可以避免与其他类的成员或全局对象名字的冲突。
(2)可以实施封装。static成员可以是私有成员,而全局对象不可以。
(3)通过阅读程序容易看出static成员是与特定类关联的。
static成员定义
在类内部声明时加static关键字,在类外定义时不用static。static成员遵循正常的公有/私有访问规则。
static成员不是任何对象的组成部分,所以static成员函数不能声明为const。static成员函数也不能被声明为虚函数。static数据成员可以声明为任意类型,包括常量、引用、数组等。
static数据成员必须在类定义体的外部定义。static成员不是通过类构造函数进行初始化,而是应该在定义时进行初始化。
//base.h
classBase
{
public:
static double rate(){return interestRate;}
static void rate(double);
private:
std::stringowner;
static doubleinterestRate;
static doubleinitRate();
};
//base.cpp
doubleBase::interestRate=initRate();
voidBase::rate(double newRate)
{
interestRate= newRate;
}
static数据成员的类型可以是该成员所属的类类型。非static成员被限定声明为其自身类对象的指针或引用。static数据成员可用作默认实参传递,而非static数据成员不行,因为它的值不能脱离对象而使用。
classBase
{
public:
Base():val(ival){}
private:
static Base base1;//ok
Base*Base2;//ok
//Base base3;//error
static const int ival=0;
int val;
};
复制控制(复制构造函数、赋值操作符、析构函数)
两种情况下必须定义复制构造函数:类中有一个经常使用得数据成员指针;有成员表示在构造函数中分配的其他资源。而另一些类在创建新对象时必须做一些特定的工作。
禁止复制:显示声明其复制构造函数为private。然后类的友元和成员仍可以进行复制。如果想要禁止友元和成员的复制,可以声明一个private复制构造函数但不对其定义。
赋值操作符
在需要定义复制构造函数时,也需要定义赋值操作符,即如果一个类(1)类中包含指针型数据成员,(2)或者在进行赋值操作时需要做一些特定工作,则该类需要定义赋值操作符。
如果一个类需要析构函数,则它也需要赋值操作符和复制构造函数。--三法则
析构函数用于类的对象超出作用域时释放对象所获取的资源,或删除指向动态分配对象的指针。
合成析构函数的作用:(1)按对象创建时的逆序撤销每个非 static 成员,(2)对于类类型的成员,合成析构函数调用该成员的析构函数来撤销对象。
编译器总会为每个类合成一个析构函数。当(1)需要释放指针成员的资源时,(2)需要执行某些特定工作时,必须自己定义析构函数。
指针成员的管理:
(1)指针成员采取常规指针型行为。这样的类具有指针的所有缺陷但无需特殊的复制控制。
(2)类可以实现所谓的“智能指针”行为。指针所指向的对象是共享的,但类能够防止悬垂指针。
(3)类采取值型行为。指针所指向的对象是唯一的,有每个类对象独立管理。
与复制构造函数和赋值操作符不同,无论类是否定义了自己的析构函数,都会创建和运行合成析构函数。如果类定义了析构函数,则在类定义的析构函数结束之后运行合成析构函数。
重载操作符与转换
不能重载的操作符:
(1):: 作用域操作符。
(2). 成员访问运算符。因为‘.’在类中对任何成员都有意义,已经成为标准用法。
(3).* 成员函数调用运算符。
(4)?: 条件运算符。因为这个运算符对于类对象来说没有实际意义,相反还会引起歧义。
重载操作符必须具有一个类类型操作数。重载操作符不能重新定义用于内置类型对象的操作符的含义。
重载操作符不保证操作数的求值顺序,尤其是不会保证内置逻辑AND、逻辑OR和逗号表达式的操作数求值。因此,尽量避免重载&&、||和逗号运算符。
可以把非成员操作符声明为友元,允许访问类的私有成员。如operator>>、operator<<。
将操作符设置为类成员还是普通非成员函数的原则:
(1)赋值(=)、下标([])、调用(())和成员访问箭头(->)等操作符必须定义为成员。定义为非成员函数将在编译时出错。
(2)与赋值一样,复合赋值操作符通常应定义为类的成员。与赋值不同的是,它也可以定义为非成员函数(如果编译不会出错的话)。
(3)对改变对象状态或与给定类型紧密联系的其他操作符,如自增、自减和解引用,通常定义为类成员。
(4)对称的操作符,如算术操作符、相等操作符、关系操作符和位操作符,最好定义为普通非成员函数。
重载操作符尽量避免输出换行符。
IO操作符必须为非成员函数。否则,左操作数只能是该类类型的对象。如果想要支持正常用法,则左操作数必须为ostream类型。因此,类通常将IO操作符设为友元。
ostream& operator<<(ostream&out,const className& s) {out<<s.data;return out;}
istream& operator>>(istream&in,className& s) {in>>s.data;returnin;}
className operator+(const className& lhs,constclassName& rhs){className ret(lhs); ret += rhs;returnret;}
className operator+=(className&lhs,const className& rhs) {lhs=lhs+rhs;return lhs;}
className& operator+=(const className& rhs){return*this;}
inline booloperator==(constclassName& lhs,const className& rhs) {return lhs==rhs;}
inline booloperator!=(constclassName& lhs,const className& rhs) {return !(lhs==rhs);}
赋值必须返回对*this的引用。
类定义下标操作符时,一般需要定义两个版本:一个为const成员并返回普通引用,另一个为const成员并返回const引用。
int&operator[] (constsize_t index){return data[index];}
const int &operator[] (const size_t index)const{return data[index];}
自增操作符和自减操作符
classBase
{
public:
Base(int *b,int*e):beg(b),end(e),curr(b){}
Base&operator++() //前缀表达式
{
if(curr==end)
throw out_of_range("incrementpast the end of Base");
++curr;
return *this;
}
//为了区分前缀后缀,后缀表达式增加一个额外的int型参数,通常传递0值
Base&operator++(int) //后缀表达式
{
Baseret(*this);
++*this;
return ret;
}
Base&operator--();
Base&operator--(int);
private:
int *beg;
int* end;
int* curr;
};
//给Base类重载下标操作符
int& Base::operator[]( const size_t index )
{
if ( beg + index>= end || beg + index < beg )
throw out_ot_range( “invalid index“ );
return *( beg + index);
}
const int & Base::operator[](const size_tindex ) const
{
if ( beg + index>= end || beg + index < beg )
throw out_ot_range( “invalid index“ );
return *( beg + index);
}
转换操作符:对任何可作为函数返回类型的类型(void除外)都可以定义转换函数。一般来说,不允许转换为数组或函数类型,转换为指针类型以及引用类型是可以的。一种内置类型应该只有一种转换。
classSmallInt //定义从Small到int的转换
{
public:
SmallInt(int i=0):val(i){}
operator int() const{return val;}//operatortype();type表示内置类型名、类类型名或有类型别名所定义的名字
private:
std::size_tval;
};
int calc(int ); SmallIntsi; int i=calc(si); //将si 转换为 int 再调用 calc 函数
面向对象编程和泛型编程
在C++中,通过基类的引用(或指针)调用虚函数时,发生动态绑定。除构造函数外,任意非static成员函数都可以是虚函数。基类通常将派生类需要重定义的任意函数定义为虚函数。
一旦函数在基类声明为虚函数,他就一直为虚函数,派生类无法改变该函数为虚函数这一事实。
成员函数的重载、覆盖、隐藏:
成员函数被重载的特征:
(1)相同的范围(在同一个类中);
(2)函数名字相同;
(3)参数不同;
(4)virtual 关键字可有可无。
覆盖是指派生类函数覆盖基类函数,特征是:
(1)不同的范围(分别位于派生类与基类);
(2)函数名字相同;
(3)参数相同;
(4)基类函数必须有virtual 关键字。
隐藏是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
(1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。
(2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。
设计类时,派生类应反映与基类的“Is A”(是一种)的关系;类型之间另一种关系为“Has A”(有一个)
如果派生类protected/private继承基类,使得派生类对象不能够访问基类的public成员,可以通过在派生类中的public访问控制下使用using声明基类的成员,如:
public: using Base::size; //其中size为基类成员数据
默认继承:如果在继承是没有声明继承级别,则class声明的派生类默认私有继承基类,struct声明的派生类默认公有继承基类。
友元关系不能继承,因为友元不是类的成员函数。
派生类的构造函数只能初始化直接基类(派生列表中指定的类)。因为每个类都定义了自己的接口。
定义派生类复制构造函数:Derived(const Derived& d):Base(d) {/*………*/}先调用基类的赋值构造函数,在调用自己的构造函数。如果省略基类初始化函数Derived(const Derived& d) {/*………*/},效果是运行Base默认构造函数初始化对象的基类部分。假定Derived成员的初始化从d复制对应成员,则新构造函数将具有奇怪配置:它的Base部分将保存默认值,而Derived成员是另一对象的副本。
派生类赋值操作符(必须对基类部分显示赋值):
Derived &Derived::operator=(const Derived&rhs) {
if(this != &rhs) {Base:: operator=(rhs); /*对派生类成员赋值*/ }return *this;}
为什么构造函数不能定义为虚函数?因为构造函数是在对象完全构造之前运行的,在构造函数运行的时候,对象的动态类型还不完整。
如果派生类需要仅仅重定义一个重载集中某些版本的行为,并继承其他版本的含义。此时派生类可以用using声明其他重载版本,只需重定义确实必须定义的那些函数。
使用指针或引用会加重类用户的负担。C++中一个通用技术是定义包装类或句柄类。句柄类存储和管理基类指针。指针所指对象的类型可以变化,它既可以指向基类类型对象又可以指向派生类型对象。
模板与泛型编程
模板声明:像其他任意函数或类一样,对于模板可以只声明而不定义。声明必须指出函数或类是一个模板。
typename与class的区别
在函数形参表中,这两个关键字具有相同含义,可以互换使用,可同时出现在同一模板形参表中使用。使用typename更加直观。typename关键字是作为标准C++的组成部分加入到C++中的,因此旧的程序更有可能只用关键字class。
typename可以在模板定义内部指定类型。如,标准库的容器类定义了不同的类型,如size_type。如果要在函数模板内部使用这样的类型,必须告诉编译器我们正在使用的名字指的是一个类型成员的名字,而不是数据成员的名字。默认情况下编译器假定这样的名字指定数据成员,而不是类型。如:typename size_type *p;//声明p是指向size_type类型的指针。
智能指针auto_ptr
auto_ptr<int> p = new int(42);或auto_ptr<int> p1 =(new int(42));//p指向42这个int型对象。auto_ptr<int>p2;//默认情况下auto_ptr的内布置在置为0.对未绑定的auto_ptr对象解引用程序会出错。
if(p2) *p2=1024;//error
智能指针和普通指针的区别:auto_ptr和内置指针对待复制和赋值有非常关键的重要区别。
auto_ptr<string> p1 = new string (“abc”);
auto_ptr<string> p2 = new string (“abc”);
p2=p1;或(p2(p1))//
1删除p2指向的对象;
2将p2置为指向p1所指的对象;
3解除p1所绑定的对象,将p1置为未绑定状态。
而普通指针在复制或赋值之后,两个指针指向同一对象(即“相等”)。
auto_ptr类模板使用注意:1、不要使用auto_ptr对象保存指向动态分配数组的指针。当auto_ptr对象被删除的时候,它只释放一个对象—它使用普通delete操作符,而不是delete []操作符。2、不要将auto_ptr对象存储在容器中。容器要求所保存的类型定义复制和赋值操作符,复制和赋值前后的对象应相等。3、永远不要使用两个auto_ptr对象指向同一对象。4、不要使用auto_ptr对象保存指向静态分配对象的指针,当auto_ptr对象撤销的时候,他将试图删除指向非动态分配对象的指针,导致未定义的行为。
异常
class explicit out_of_stock : public runtime_time{
public:
explicit explicit out_of_stock(const string&s) : runtime_time(s){}
virtual ~ out_of_stock() throw(){}
};
void no_problem() throw();//异常说明是函数接口的一部分,函数定义以及该函数的任意声明必须具有相同的异常说明。如果一个函数声明没有指定异常说明,则该函数可以抛出任意类型的异常。
命名空间
命名空间的名字在定义该命名空间的作用域中必须是唯一的。命名空间可以在全局作用域或其他作用域内部定义,但不能在函数或类内部定义。命名空间作用域不能以分号结束。
虚继承
使用虚基类的多重继承比没有虚基类可以减少二义性问题。可以无二异性的直接访问共享虚基类中的成员。
class istream : public virtual ios {};//虚继承ios类
class ostream : virtual public ios {};//虚继承ios类
class istream : public istream, public ostream{};//只继承一个共享基类(ios称为虚基类)。
虚基类的初始化
通常,每个类只初始化自己的直接基类。但在用于虚基类的时候,这个初始化策略会失败。如果使用常规规则,就可能会多次初始化虚基类。为解决重复初始化问题,在虚派生中,由最低派生类的构造函数初始化虚基类。当创建istream对象的时候,首先使用构造函数初始化列表中指定的初始式是构造ios 部分;然后构造istream 部分,忽略istream 的用于ios构造函数初始化列表的初始化式;接下来构造ostream 部分,再次忽略ios 初始化式;最后构造istream 部分。如果istream 构造函数不显示初始化ios 基类,就是用ios 默认构造函数;如果ios没有构造函数,则代码出错。
虚基类的构造函数与析构函数
无论虚基类出现在继承层次中任何地方,总是在构造非虚基类之前构造虚基类,然后按照声明调用非虚基类的构造函数。如:
虚继承TeddyBear 层次(左): 调用构造函数次序(右):