第十二章 类和动态内存分配
动态内存和类
C++在分配内存时的策略:在运行时决定内存分配,而不是在编译时决定。这样,可以根据程序的需要,而不是根据一系列严格的存储类型规则来使用内存。C++使用new和delete运算符来动态控制内存。
🚢C++空指针
新关键字nullptr,用于表示空指针。
str = nullptr; // C++11 null pointer notation
在构造函数中使用new时应注意的事项
使用new初始化对象的指针成员时必须特别小心。具体来说:
- 如果在构造函数中使用new来初始化指针成员,则应在析构函数中使用delete。
- new和delete必须相互兼容。new对应于delete,new[ ] 对应于delete[ ]。
- 如果有多个构造函数,则必须以相同的方式使用new,要么都带中括号,要么都不带。因为只有一个析构函数,所有的构造函数都必须与它兼容。然而,可以在一个构造函数中使用new初始化指针,而在另一个构造函数中将指针初始化为空(0或nullptr),这是因为delete(无论是带中括号还是不带中括号)可以用于空指针。
NULL、0 还是 nullptr:以前,空指针可以用0或NULL(在很多头文件中,NULL是一个被定义为0的符号常量)来表示。C程序员通常使用NULL而不是0,以指出这是一个指针,就像使用’\0’而不是0来表示空字符,以指出这是一个字符一样。然而,C++传统上更喜欢用简单的0,而不是等价的NULL。C++11提供了新的关键字nullptr。
第十三章 类继承
C++提供了更高层次的重用性。目前,很多厂商提供了类库,类库由类声明和实现构成。因为类组合了数据表示和类方法,因此提供了比函数库更加完整的程序包。例如:单个类就可以提供用于管理对话框的全部资源。通常,类库是以源代码的方式提供的,这就意味着可以对其进行修改,以满足需求。然而,C++提供了比修改代码更好的方法来扩展和修改类。这种方法叫作类继承,它能够从已有的类派生出新的类,而派生类继承了原有类(基类)的特征,包括方法。通过继承派生出的类通常比设计新类要容易得多。
- 可以在已有类的基础上添加功能。例如:对于数组类,可以添加数学运算。
- 可以给类添加数据。例如:对于字符串类,可以派生出一个类,并添加指定字符串显示颜色的数据成员。
- 可以修改类方法的行为。例如:对于代表提供给飞机乘客的服务的Passenger类,可以派生出提供更高级别服务的FirstClassPassenger类。
一个简单的基类
从一个类派生出另一个类时,原始类称为基类, 继承类称为派生类。
// RatedPlayer derives from the TableTennisPlayer base class
class RatedPlayer : public TableTennisPlayer
{
...
};
冒号指出RatedPlayer类的基类是TableTennisPlayer类。上述特殊的声明头表明TableTennisPlayer是一个公有基类,这被称为公有派生。派生类对象包含基类对象。使用公有派生,基类的公有成员将成为派生类的公有成员;基类的私有部分也将成为派生类的一部分,但只能通过基类的公有和保护方法访问。
-
派生类对象存储了基类的数据成员(派生类继承了基类的实现)
-
派生类对象可以使用基类的方法(派生类继续了基类的接口)
需要在继承特性中添加什么?
- 派生类需要自己的构造函数
- 派生类可以根据需要添加额外的数据成员和成员函数。
构造函数:访问权限的考虑
派生类不能直接访问基类的私有成员,而必须通过基类方法进行访问。
具体地说,派生类构造函数必须使用基类构造函数。
创建派生类对象时,程序首先创建基类对象。从概念上说,这意味着基类对象应当在程序进入派生类构造函数之前被创建。C++使用成员初始化列表语法来完成这种工作。例如:
// 将参数从派生类构造函数传递给基类构造函数
RatedPlayer::RatedPlayer(unsigned int r, const string & fn,
const string & ln, bool ht) : TableTennisPlayer(fn, ln, ht)
// 基类构造函数
{
rating = r;
}
其中: TableTennisPlayer(fn, ln, ht)
是成员初始化列表。它是可执行的代码,调用TableTennisPlayer 构造函数。
例如:假设程序包含如下声明:
RatedPlayer rplayer1(1140, "Mallory", "Duck", true);
则RatedPlayer 构造函数将把实参 “Mallory”、“Duck”、 true 赋给形参fn、ln 和 ht,然后将这些参数作为实参传递给TableTennisPlayer 构造函数,后者将创建一个嵌套 TableTennisPlayer 对象,并将数据 “Mallory”、"Duck"和 true 存储在该对象中。然后,程序进入RatedPlayer 构造函数体,完成RatedPlayer 对象的创建,并将参数r的值赋给rating 成员。
有关派生类构造函数的要点如下:
- 首先创建基类对象
- 派生类构造函数应通过成员初始化列表将基类信息传递给基类构造函数
- 派生类构造函数应初始化派生类新增的数据成员
这个例子没有提供显示构造函数,因此将使用隐式构造函数。释放对象的顺序与创建对象的顺序相反,即首先执行派生类的析构函数,然后自动调用基类的析构函数。
注意:创建派生类对象时,程序首先调用基类构造函数,然后再调用派生类构造函数。基类构造函数负责初始化继承的数据成员;派生类构造函数主要用于初始化新增的数据成员。派生类的构造函数总是调用一个基类构造函数。可以使用初始化器列表语法指明要使用的基类构造函数,否则将使用默认的基类构造函数。
派生类对象过期时,程序首先调用派生类析构函数,然后再调用基类析构函数。
🚀成员初始化列表
派生类构造函数可以使用初始化器列表机制将值传递给基类构造函数。例如:
derived::derived(type1 x, type2 y) : base(x, y) // initializer list
{
...
}
其中derived 是派生类, base是基类, x和y 是基类构造函数使用的变量。例如:如果派生类构造函数接收到参数 10 和 12,则这种机制将把 10 和 12 传递给被定义为接受这些类型的参数的基类构造函数。除了虚基类外,类只能将值传递回相邻的基类,但后者可以使用相同的机制将信息传递给相邻的基类,依次类推。如果没有在成员初始化列表中提供基类构造函数,程序将使用默认的基类构造函数。成员初始化列表只能用于构造函数。
虚函数:
- 在基类方法的声明中使用关键字virtual可使该方法在基类以及所有的派生类(包括从派生类派生出来的类)中是虚的。
- 如果使用指向对象的引用或指针来调用虚方法,程序将使用为对象类型定义的方法,而不使用为引用或指针类型定义的方法。
- 如果定义的类将被用作基类,则应将那些要在派生类中重新定义的类方法声明为虚的。
访问控制:protected
关键字 protected 与 private 相似, 在类外只能用公有类成员来访问protected部分的类成员。private和protected之间的区别只有在基类派生的类中才会表现出来,派生类的成员可以直接访问基类的保护成员,但不能直接访问基类的私有成员。因此,对于外部世界来说,保护成员的行为与私有成员相似;但对于派生类来说,保护成员的行为与公有成员相似。
警告:最好对类数据成员采用私有访问控制,不要使用保护访问控制;同时通过基类方法使派生类能够访问基类数据。
总结
继承通过使用已有的类(基类)定义新的类(派生类),使得能够根据需要修改编程代码。公有继承建立is-a 关系,这意味着派生类对象也应该是某种基类对象。作为is-a模型的一部分,派生类继承基类的数据成员和大部分方法,但不继承基类的构造函数、析构函数和赋值运算符。派生类可以直接访问基类的公有成员和保护成员,并能够通过基类的公有方法和保护方法访问基类和私有成员。可以在派生类中新增数据成员和方法,还可以将派生类用作基类,来做进一步的开发。每个派生类都必须有自己的构造函数。程序创建派生类对象时,将首先调用基类的构造函数,然后调用派生类的构造函数;程序删除对象时,将调用派生类的析构函数,然后调用基类的析构函数。
如果希望派生类可以重新定义基类的方法,则可以使用关键字virtual将它声明为虚的。这样对于通过指针或引用访问的对象,能够根据对象类型来处理,而不是根据引用或指针的类型来处理。具体来说,基类的析构函数通常应当是虚的。
第十四章 C++中的代码重用
类模板
typedef unsigned long Item;
class Stack
{
private:
enum {MAX = 10}; // constant specific to class
Item item[MAX]; // holds stack items
int top; // index for top stack item
public:
Stack();
bool isempty() const;
bool isfull() const;
bool push(const Item & item); // add item to stack
bool pop(Item & item); // pop top into item
};
采用模板时,将使用模板定义替换Stack声明,使用模板成员函数替换Stack的成员函数。和模板函数一样,模板类以下面这样的代码开头:
template <class Type>
关键字template告诉编译器,将要定义一个模板。尖括号中的内容相当于函数的参数列表。可以把关键字class看作是变量的类型名,该变量接受类型作为其值,把Type看作是该变量的名称。
这里使用class并不意味着Type必须是一个类:而只是表明Type是一个通用的类型说明符,在使用模板时,将使用实际的类型替换它。较新的C++实现允许在这种情况下使用不太容易混淆的关键字typename代替class:
template <typename Type> // newer choice
可以使用自己的泛型代替Type,其命名规则与其他标识符相同。当前流行的选项包括T和Type,我们将使用后者。当模板被调用时,Type将被具体的类型值(如int或string)取代。在模板定义中,可以使用泛型名来标识要存储在栈中的类型。对于Stack来说,这意味着应将声明中的所有的typedef标识符Item替换为Type。例如:
Item items[MAX];
应改为:
Type items[MAX];
同样,可以使用模板成员函数替换原有类的类方法。每个函数头都将以相同的模板声明打头:
template <class Type>
同样应使用泛型名Type替换typedef标识符Item。另外,还需要将类限定符从Stack::
改为Stack<Type>::
。例如:
bool Stack::push(const Item & item)
{
...
}
// 应改为
template <class Type> // or template <typename Type>
bool Stack<Type>::push(const Type & item)
{
...
}
仅在程序包含模板并不能生成模板类,而必须请求实例化。为此,需要声明一个类型为模板类的对象,方法是使用所需的具体类型替换泛型名。例如,下面的代码创建两个栈,一个用于存储int,另一个用于存储string对象:
Stack<int> Kernels; // create a stack of ints
Stack<string> colonels; // create a stack of string objects
编译器将按Stack<Type>
模板来生成两个独立的类声明和两组独立的类方法。类声明Stack将使用int替换模板中所有的Type,而类声明Stack将使用string替换Type。当然,使用的算法必须与类型一致。
泛型标识符:例如Type称为类型参数(type parameter),这意味着它们类似于变量,但赋给它们的不能是数字,而只能是类型。
编译器可以根据函数的参数类型来确定生成哪种函数:
template <class T>
void simple(T t) { cout << t << '\n'}
simple(2); // generate void simple(int)
simple("two"); // generate void simple(const char *)
第十五章 友元、异常和其他
友元声明可以位于公有、私有或包含部分,其所在的位置无关紧要。
class Tv
{
friend class Remote; // Tv类决定谁是它的友元
...
};
Class Remote
{
...
};
// Remote 的所有方法都可能影响到Tv的私有成员
class Tv;
class Remote
{
...
};
class TV
{
friend void Remote::set_chan(Tv & t, int c); // Tv类决定谁是他的友元
};
// 只有Remote::set_chan() 能够影响Tv的私有成员
嵌套类
bool Queue::enqueue(const Item & item)
{
if(isfull())
return false;
Node * add = new Node; // create node
// on failure, new throws std::bad_alloc exception
add->item = item; // set node pointers
add->next = NULL;
...
}
模板中的嵌套
程序清单:
#ifndef QUEUETP_H_
#define QUEUETP_H_
template <class Item>
class QueueTP
{
private:
enum {Q_SIZE = 10 };
class Node
{
public:
Item item;
Node * next;
Node(const Item & i):item(1), next(0){ }
};
Node * front;
Node * rear;
int items;
const int qsize;
QueueTP(const QueueTP & q) : qsize(0){ return *this; }
public:
QueueTP{int qs = Q_SIZE};
~QueueTP();
bool isempty() const
{
return items == 0;
}
bool isfull() const
{
return items == qsize;
}
int queuecount() const
{
return items;
}
bool enqueue(const Item &item);
bool dequeue(Item &item);
};
// QueueTP methods
template <class Item>
QueueTP<Item>::QueueTP(int qs) : qsize(qs)
{
front = rear = 0;
items = 0;
}
template <class Item>
QueueTP<Item>::~QueueTP()
{
Node * temp;
while (front != 0)
{
temp = front;
front = front->next;
delete temp;
}
}
template <class Item>
bool QueueTP<Item>::enqueue(const Item &item)
{
if (isfull())
return false;
Node * add = new Node(item);
items++;
if(front == 0)
front = add;
else
rear->next = add;
return true;
}
template <class Item>
bool QueueTP<Item>::dequeue(Item & item)
{
if (front == 0)
return false;
item = front->item;
item--;
Node * temp = front;
front = front->next;
delete temp;
if(items == 0)
rear = 0;
return true;
}
#endif
异常
1、调用abort()
如果其中一个参数是另一个参数的负值,则调用abort()函数。Abort()函数的原型位于头文件cstdlib(或stdlib.h)中,其典型实现是向标准错误流(即cerr使用的错误流)发送消息abnormal program termination(程序异常终止),然后终止程序。
2、异常机制
C++异常是对程序运行过程中发生的异常情况(例如被0除)的一种响应。异常提供了将控制权从程序的一个部分传递到另一部分的途径。对异常的处理有3个组成部分:
- 引发异常
- 使用处理程序捕获异常
- 使用try块
程序在出现问题时将引发异常。
程序使用异常处理程序(exception handler)来捕获异常,异常处理程序位于要处理问题的程序中。catch关键字表示捕获异常。处理程序以关键字catch开头,随后是位于括号中的类型声明,它指出了异常处理程序要响应的异常类型;然后是一个用花括号括起的代码块,指出要采取的措施。catch关键字和异常类型用作标签,指出当异常被引发时,程序应跳到这个位置执行。异常处理程序也被称为catch块。
try块标识其中特定的异常可能被激活的代码块,它后面跟一个或多个catch块。try块是由关键字try指示的,关键字try的后面是一个由花括号括起的代码块,表明需要注意这些代码引发的异常。
#include <iostream>
double hmean(double a, double b);
int main()
{
double x, y ,z;
std::cout << "Enter two numbers: ";
while(std::cin >> x >> y)
{
try { // start of try block
z = hmean(x,y);
}
catch (const char * s) // start of exception handler
{
std::cout << s << std::endl;
std::cout << "Enter a new pair of numbers: ";
continue;
}
std::cout << "Harmonic mean of " << x << " and " << y
<< " is " << z << std::endl;
std::cout << "Enter next set of numbers <q to quit>: ";
}
std::cout << "Bye!\n";
return 0;
}
double hmean(double a, double b)
{
if(a == -b)
throw "bad hmean() arguments: a = -b not allowed";
return 2.0 * a * b / (a + b);
}
程序说明:
try { // start of try block
z = hmean(x,y);
} // end of try block
如果其中的某条语句导致异常被引发,则后面的catch块将对异常进行处理。如果程序在try块的外面调用hmean(),将无法处理异常。
引发异常的代码与下面类似:
if(a == -b)
throw "bad hmean() arguments: a = -b not allowed";
其中被引发的异常是字符串。异常类型可以是字符串或其他C++类型;通常为类类型。
执行throw语句类似于执行返回语句,因为它也将终止函数的执行;但throw不是将控制权返回给调用程序,而是导致程序沿函数调用序列后退,直到找到包含try块的函数。
在这个例子中,throw将程序控制权返回给main()。程序将在main()中寻找与引发的异常类型匹配的异常处理程序(位于try块的后面)。
处理程序(或catch块)与下面类似:
catch (char * s) // start of exception handler
{
std::cout << s << std::endl;
std::cout << "Enter a new pair of numbers: ";
continue;
}
catch块点类似于函数定义,但并不是函数定义。关键字catch表明这是一个处理程序,而char*
s 则表明该处理程序与字符串异常匹配。s与函数参数定义极其类似,因为匹配的引发将被赋给s。另外,当异常与该处理程序匹配时,程序将执行括号中的代码。
3、RTTI
RTTI是运行阶段类型的识别(Runtime Type Identification)的简称。
RTTI的工作原理:C++有3个支持RTTI的元素。
- 如果可能的话,dynamic_cast 运算符将使用一个指向基类的指针来生成一个指向派生类的指针;否则,该运算符返回0——空指针。
- typeid运算符返回一个指出对象的类型的值。
- type_info 结构存储了有关特定类型的信息。
只能将RTTI用于包含虚函数的类层次结构,,原因在于只有对于这种类层次结构,才应该将派生类对象的地址赋给基类指针。
警告:RTTI只适用于包含虚函数的类。
4、类型转换运算符
对于C语言松散的类型转换,C++提供更加严格地限制允许的类型转换,并添加4个类型转换运算符,使转换过程更规范:
- dynamic_cast
- const_cast
- static_cast
- reinterpret_cast
可以根据目的选择一个合适的运算符,而不是使用通用的类型转换。
1)dynamic_cast运算符的语法:
dynamic_cast < type-name > (expression)
- const_cast运算符的语法:
const_cast < type-name > (expression)
- static_cast运算符的语法:
static_cast < type-name > (expression)
4)reinterpret_cast运算符的语法:
reinterpret_cast < type-name > (expression)
总结
友元使得能够为类开发更灵活的接口。类可以将其他函数、其他类和其他类的成员函数作为友元。在某些情况下,可能需要使用前向声明,需要特别注意类和方法声明的顺序,以正确地组合友元。
嵌套类是在其他类中声明的类,它有助于设计这样的助手类,即实现其他类,但不必是公有接口的组成部分。
C++异常机制为处理拙劣的编程事件,如不适当的值、I/O失败等,提供了一种灵活的方法。引发异常将终止当前执行的函数,将控制权传给匹配的catch块。catch块紧跟在try块的后面,为捕获异常,直接或间接导致异常的函数调用必须位于try块中。这样程序将执行catch块中的代码。
RTTI(运行阶段类型信息)特性让程序能够检测对象的类型。dynamic_cast运算符用于将派生类指针转换为基类指针,其主要用途是确保可以安全地调用虚函数。Typeid运算符返回一个type_info对象。可以对两个typeid的返回值进行比较,以确保对象是否为特定的类型,而返回的type_info对象可用于获得关于对象的信息。
第十六章 string类和标准模板库
1、string类
string类包含了大量的方法,其中包括了若干构造函数,用于将字符串赋给变量、合并字符串、比较字符串和访问各个元素的重载运算符以及用于在字符串中查找字符和子字符串的工具等。简而言之,string类包含的内容很多。
string 类的构造函数
构造函数 | 描 述 |
---|---|
string(const char * s) | 将string对象初始化为s指向的NBTS |
string(size_type n, char c) | 创建一个包含n个元素的string对象,其中每个元素都被初始化为字符c |
string(const string & str) | 将一个string对象初始化为string对象str(复制构造函数) |
string( ) | 创建一个默认的string对象,长度为0(默认构造函数) |
string(const char * s, size_type n) | 将string对象初始化为s指向的NBST的前n个字符,即使超过了NBTS结尾 |
template<class Iter> string(Iter begin, Iter end) | 将string对象初始化为区间[begin, end)内的字符,其中begin和end的行为就像指针,用于指定位置,范围包括begin在内,但不包括end |
string (const string & str, string size_type pos = 0, size_type n = npos) | 将一个string对象初始化为对象str中从位置pos开始到结尾的字符,或从位置pos开始的n个字符 |
string(string && str) noexcept | 这是C++11 新增的,它将一个string对象初始化为string对象str,并可能修改str(移动构造函数) |
string(initializer_list<char> il) | C++11新增的,它将一个string对象初始化为初始化列表il中的字符 |
程序清单:
#include <iostream>
#include <string>
using namespace std;
int main()
{
// 将string对象初始化为常规的C-风格字符串
string one("Lottery Winner!");
cout << one << endl;
// 初始化为由20个$字符组成的字符串
string two(20, '$');
cout << two << endl;
// 复制构造函数将string对象three初始化为string对象one
string three(one);
cout << three << endl;
// 重载的+=运算符将字符串附加到字符串one的后面
one += " Oops!";
cout << one << endl;
// 重载[]运算符使得可以使用数组表示法来访问string对象中的各个字符
two = "Sorry! That was ";
three[0] = 'P';
string four;
four = two + three;
cout << four << endl;
// 将一个C-风格字符串和一个整数作为参数,其中的整数参数表示要复制多少个字符
char alls[] = "All's well that ends well";
string five(alls,20);
cout << five << "!\n";
// 数组名相当于指针,所以都是char * 类型
string six(alls+6, alls + 10);
cout << six << ", ";
// five[6]是一个char值,&five[6]是一个地址,可被用作该构造函数的一个参数
string seven(&five[6], &five[10]);
cout << seven << "...\n";
// 将一个string对象的部分内容复制到构造的对象中
string eight(four, 7, 16);
cout << eight << " in motion!" << endl;
return 0;
}
输出结果:
Lottery Winner!
$$$$$$$$$$$$$$$$$$$$
Lottery Winner!
Lottery Winner! Oops!
Sorry! That was Pottery Winner!
All's well that ends!
well, well...
That was Pottery in motion!
string 类输入
对于类,很有帮助的另一个点是,知道有哪些输入方式可用。对于C-风格字符串,有三种方式:
char info[100];
cin >> info;
cin.getline(info, 100); // read a line, discard \n
cin.get(info, 100); // read a line, leave \n in queue
对于string对象,有两种方式:
string stuff;
cin >> stuff; // read a word
getline(cin, stuff); // read a line, discard \n
两个版本的getline()都有一个可选参数,用于指定使用哪个字符来确定输入的边界:
cin.getline(info, 100,':'); // read up to :, discard :
getline(stuff, ':'); // read up to :, discard :
在功能上,它们之间的主要区别在于,string版本的getline()将自动调整目标string对象的大小,使之刚好能够存储输入的字符:
char fname[10];
string lname;
cin >> fname; // could be a problem if input size > 9 characters
cin >> lname; // can read a very, very long word
cin.getline(fname, 10); // may truncate input
getline(cin, lname); // no truncation
自动调整大小的功能让string版本的getline()不需要指定读取多少个字符的数值参数。
cin.operator>>(fname); // ostream class method
operator>>(cin,lname); // regular function
string版本的getline()函数从输入中读取字符,并将其存储到目标string中,直到发生下列三种情况之一:
- 到达文件尾,在这种情况下,输入流的eofbit将被设置,这意味着方法fail()和eof()都将返回true;
- 遇到分界字符(默认为\n),在这种情况下,将把分界字符从输入流中删除,但不存储它;
- 读取的字符数达到最大允许值(string::npos 和可供分配的内存字节数中较小的一个),在这种情况下,将设置输入流的failbit,这意味着方法fail()将返回true。
从文件中读取字符串示例:
程序清单:
#include <iostream>
#include <fstream>
#include <string>
#include <cstdlib>
using namespace std;
int main()
{
ifstream fin;
fin.open("tobuy.txt");
if(fin.is_open() == false)
{
cerr << "Can't open file. Bye.\n";
exit(EXIT_FAILURE);
}
string item;
int count = 0;
getline(fin, item, ':');
while(fin)
{
++count;
cout << count << ": " << item << endl;
getline(fin, item, ':');
}
cout << "Done\n";
fin.close();
return 0;
}
string 还提供了哪些功能
string库提供了很多其他的工具,包括完成下述功能的函数:
- 删除字符串的部分或全部内容
- 用一个字符串的部分或全部内容替换另一个字符串的部分或全部内容
- 将数据插入到字符串中删除字符串中的数据
- 将一个字符串的部分或全部内容与另一个字符串的部分或全部内容进行比较
- 从字符串中提取子字符串
- 将一个字符串中的内容复制到另一个字符串中
- 交换两个字符串的内容。
这些函数大多数都被重载,以便能够同时处理C-风格字符串和string对象。
内存块分配限制:
为了 避免字符串不断增大,超过了内存块的大小,或者不断地分配新的内存块,方法capacity()返回当前分配给字符串的内存块的大小,而reserve()方法能够请求内存块的最小长度。
2、智能指针模板类
智能指针是行为类似于指针的类对象,但这种对象还有其他功能。
void remodel(std::string & str)
{
std::string * ps = new std::string(str);
str = ps;
return;
}
您可能发现了其中的缺陷,每当调用时,该函数部分都分配堆中的内存,但从不回收,从而导致内存泄漏。
只要别忘了在return语句前添加下面的语句,以释放分配的内存即可:
delete ps;
但凡涉及”别忘了“的解决办法,很少是最佳的。例如:下面的变体
void remodel(std::string & str)
{
std::string * ps = new std::string(str);
if(weird_thing())
throw exception();
str = *ps;
delete ps;
return;
}
当出现异常时,delete将不被执行,因此也将导致内存泄露。
1)使用智能指针
这三个智能指针模板(auto_ptr、unique_ptr和shared_ptr)都定义了类似指针的对象,可以将new获得(直接或间接)的地址赋给这种对象。当智能指针过期时,其析构函数将使用delete来释放内存。因此,如果将new返回的地址赋给这些对象,将无需记住稍后释放这些内存:在智能指针过期时,这些内存将自动被释放。
void demo1()
{
double * pd = new double; // 为pd和一个double值分配存储空间,保存地址
*pd = 25.5; // 将值复制到动态内存中
return; // 删除pd,值被保留在动态内存中
}
void demo2()
{
auto_ptr<double> ap(new double); // 为ap和一个double值分配存储空间,保存地址
*ap = 25.5; // 将值复制到动态内存中
return; // 删除ap,ap的析构函数释放动态内存
}
要创建智能指针对象,必须包含头文件 memory,该文件模板定义。然后使用通常的模板语法来实例化所需类型的指针。例如:模板auto_ptr包含如下构造函数:
template<class X> class auto_ptr
{
public:
explicit auto_ptr(X * p = 0) throw();
};
throw()意味着构造函数不会引发异常;与aoto_ptr一样,throw()也被掘弃。因此,请求X类型的auto_ptr将获得一个指向X类型的auto_ptr:
auto_ptr<double> pd(new double); // pd an auto_ptr to double
// (use in place of double * pd)
auto_ptr<string> ps(new string); // ps an auto_ptr to string
// (use in place of string * ps)
new double是new返回的指针,指向新分配的内存块。它是构造函数auto_ptr<double>
的参数,即对应于原型中形参p的实参,同样,new string也是构造函数的实参。
其他两种智能指针使用同样的语法:
unique_ptr<double> pdu(new double);
shared_ptr<string> pss(new string);
因此,要转换remodel()函数,应按下面3个步骤进行:
- 包含头文件memory;
- 将指向string的指针替换为指向string的智能指针对象;
- 删除delete语句
#include <memory>
void remodel(std::string & str)
{
std::auto_ptr<std::string> ps (new std::string(str));
if(weird_thing())
throw exception();
str = *ps;
return;
}
注意到智能指针模板位于名称空间std中。
3、标准模板库
STL提供了一组表示容器、迭代器、函数对象和算法的模板。容器是一个与数组类似的单元,可以存储若干个值。STL容器是同质的,即存储的值的类型相同;算法是完成特定任务(如堆数组进行排序或在链表中查找特定值)的处方;迭代器能够用来遍历容器的对象,与能够遍历数组的指针类似,是广义指针;函数对象是类似于函数的对象,可以是类对象或函数指针(包括函数名,因为函数名被用作指针)。STL使得能够构造各种容器(包括数组、队列和链表)和执行各种操作(包括搜素、排序和随机排列)。
1)模板类vector
在计算中,vector对应数组。vector类可以创建vector对象,将一个vector对象赋给另一个对象,使用[ ]运算符来访问vector元素。要使类成为通用的,将它设计为模板类,STL正是这样做的,在头文件vector中定义一个vector模板。
要创建vector模板对象,可使用通常的表示法来指出要使用的类型。另外,vector模板使用动态内存分配,因此可以用初始化参数来指出需要多少矢量:
#include <vector>
using namespace std;
vector<int> rating(5); // a vector of 5 ints
int n;
cin >> n;
vector<double> scores(n); // a vector of n doubles
由于运算符[ ] 被重载,因此创建vector对象后,可以使用通常的数组表示法来访问各个元素:
ratings[0] = 9;
for (int i = 0; i < n; i++)
cout << scores[i] << endl;
程序清单:
#include <iostream>
#include <string>
#include <vector>
const int NUM = 5;
int main()
{
using std::vector;
using std::string;
using std::cin;
using std::cout;
vector<int> ratings(NUM);
vector<string> titles(NUM);
cout << "You will do exactly as told. You will enter\n"
<< NUM << "book titles and your ratings (0.10).\n";
int i;
for(i = 0; i < NUM; i++)
{
cout << "Enter title #" << i+1 << ":";
getline(cin,title[i]);
cout << "Enter your rating (0 - 10)";
cin >> ratings[i];
cin.get();
}
cout << "Thank you. You entered the following:\n"
<< "Rating\t Boook\n";
for(i = 0; i < NUM; i++)
{
cout << ratings[i] << "\t" << titles[i] << endl;
}
return 0;
}
2) 可对矢量执行的操作
除分配存储空间外,vector模板还可以完成哪些任务呢?所有的STL容器提供了一些基本方法,其中包括:
- size( ) : 返回容器中元素数目
- swap( ) :交换两个容器的内容
- begin( ) :返回一个指向容器中第一个元素的迭代器
- end( ):返回一个表示超过容器尾的迭代器
什么是迭代器?它是一个广义指针。事实上,它可以是指针,也可以是一个对其执行类似指针的操作:如解除引用(如operator*( )
)和递增(如operator++( )
)的对象。
通过将指针广义化为迭代器,让STL能够为各种不同的容器类(包括那些简单指针无法处理的类)提供统一的接口。每个容器类都定义了一个合适的迭代器,该迭代器的类型是一个名为iterator
的typedef,其作用域为整个类。
例如:要为vector的double类型规范声明一个迭代器,可以这样做:
vector<double>::iterator pd; // pd an iterator
假设scores是一个vector<double>
对象:
vector<double> scores;
则可以使用迭代器pd执行这样的操作:
pd = scores.begin(); // have pd pointer to the first element
*pd = 22.3; // dereference pd and assign value to first element
++pd; // make pd point to the next element
迭代器的行为就像指针。
顺便说一句,还有一个C++11 自动类型推断很有用的地方。例如:
vector<double>::iterator pd = scores.begin();
//转为
auto pd = scores.begin();
什么是超过结尾(past-the-end)呢?它是一种迭代器,指向容器最后一个元素后面的那个元素。这与C-风格字符串最后一个字符后面的空字符类似,只是空字符是一个值,而"超过结尾"是一个指向元素的指针(迭代器)。end( )成员函数标识超过结尾的位置。如果将迭代器设置为容器的第一个元素,并不断地递增,则最终它将到达容器结尾,从而遍历整个容器的内容。
vector模板类也包含一些只有某些STL容器才有的方法。push_back( )
是一个方便的方法,它将元素添加到矢量末尾。这样做时,它将负责内存管理,增加矢量的长度,使之能够容纳新的成员。这意味着可以编写这样的代码:
vector<double> scores; // create an empty vector
double temp;
while(cin >> temp && temp >= 0)
scores.push_back(temp);
cout << "You entered " << scores.size() << ".scores.\n;"
每次循环都给scores对象增加一个元素。在编写或运行程序时,无需了解元素的数目。只要能够取得足够的内存,程序就可以根据需要增加scores的长度。
erase( )
方法删除矢量中给定区间的元素。它接受两个迭代器参数,这些参数定义了要删除的区间。了解STL如何使用两个迭代器来定义区间至关重要。第一个迭代器指向区间的起始处,第二个迭代器位于区间终止处的后一个位置。
例如:下述代码删除第一个和第二个元素,即删除begin( ) 和begin( )+1指向的元素。
scores.erase(scores.begin(), scores.begin() + 2);
注意:区间[it1, it2) 由迭代器it1 和 it2 指定,其范围为it1 到it2(不包括it2)。
insert( )方法的功能与erase( )相反。它接受3个迭代器参数,第一个参数指定了新元素的插入位置,第二个和第三个迭代器参数定义了被插入 区间,该区间通常是另一个容器对象的一部分。
例如:下面的代码将矢量new_v中除第一个元素外的所有元素插入到old_v矢量的第一个元素的前面:
vector<int> old_v;
vector<int> new_v;
old_v.insert(old_v.begin(),new_v.begin()+1, new_v.end());
4、泛型编程
STL是一种泛型编程(generic programming)。面向对象编程关注的是编程的数据方面,而泛型编程关注的是算法。它们之间的共同点是抽象和创建可重用代码,但它们的理念绝然不同。
泛型编程旨在编写独立于数据类型的代码。在C++中,完成通用程序的工具是模板。当然,模板使得能够被泛型定义函数或类,而STL通过通用算法更进了一步。模板让这一切成为可能,但必须对元素进行仔细地设计。
不同的算法对迭代器的要求也不同。例如:查找算法需要定义++运算符,以便迭代器能够遍历整个容器;它要求能够读取数据,但不要求能够写数据。而排序算法要求能够随机访问,以便能够交换两个不相邻的元素。如果iter是一个迭代器,则可以通过定义+运算符来实现 随机访问,这样就可以使用像iter + 10这样的表达式了。
STL定义了5种迭代器,并根据所需的迭代器类型对算法进行了描述。这5种迭代器分别是输入迭代器、输出迭代器、正向迭代器、双向迭代器和随机访问迭代器。
1)输入迭代器
输入迭代器必须能够访问容器中的所有的值,这是通过支持++运算符来实现的。如果将输入迭代器设置为指向容器中的第一个元素,并不断将其递增,直到到达超尾位置,则它将依次指向容器中的每一个元素,顺便说一句,并不能保证输入迭代器第二次遍历容器时,顺序不变。另外,输入迭代器被递增后,也不能保证其先前的值仍然可以被解除引用。基于输入迭代器的任何算法都应当是单通行的,不依赖于前一次遍历时的迭代器值,也不依赖于本次遍历中前面的迭代器值。
注意:输入迭代器是单向迭代器,可以递增,但不能倒退。
2)输出迭代器
输出迭代器与输入迭代器相似,只是解除引用让程序能修改容器值,而不能读取。
简而言之,对于单通行、只读算法,可以使用输入迭代器;对于单通行、只写算法,则可以使用输出迭代器。
3)正向迭代器
与输入迭代器和输出迭代器相似,正向迭代器只使用++运算符来遍历容器,所以它每次沿容器向前移动一个元素;然而,与输入和输出迭代器不同的是,它总是按相同的顺序遍历一些列值另外,将正向迭代器递增后,仍然可以对前面的迭代器值解除引用,并可以得到相同的值。这些特征使得多次通行算法成为可能。
正向迭代器既可以使得能够读取和修改数据,也可以使得只能读取数据:
int * pirw; // read-write iterator
const int * pir; // read-only iterator
4)双向迭代器
例如:reverse函数可以交换第一个元素和最后一个元素、将指向第一个元素的指针加1、将指向第二个元素的指针减1,并重复这种处理过程。双向迭代器具有正向迭代器的所有特性,同时支持两种(前缀和后缀)递减运算符。
5)随机访问迭代器
有些算法(如标准排序和二分检索)要求能够直接跳到容器中的任何一个元素,这叫做随机访问,需要随机访问迭代器。随机访问迭代器具有双向迭代器的所有特性,同时添加了支持随机访问的操作(如指针增加运算)和用于对元素进行排序的关系运算符。
为何需要这么多迭代器呢?目的是为了在编写算法尽可能使用要求最低的迭代器,并让它适用于容器的最大区间。这样,通过使用级别最低的输入迭代器,find( ) 函数便可用于任何包含可读取值的容器。而sort( ) 函数由于需要随机访问迭代器,所有只能用于支持这种迭代器的容器。
注意:各种迭代器的类型并不是确定的,而只是一种概念性的 描述。每个容器类都定义了一个类级typedef名称——iterator,因此vector<int>
类的迭代器类型为vector<int>::iterator
。然而,该类的文档指出,矢量迭代器是随机访问迭代器,它允许使用基于任何迭代器类型的算法。因为随机访问迭代器具有所有迭代器的功能。同样,list<int>
类的迭代器类型为list<int>::iterator
。STL实现了一个双向链表,它使用双向迭代器,因此不能使用基于随机访问迭代器的算法,但可以使用基于要求较低的迭代器的算法。
5、容器种类
STL具有容器概念和容器类型。概念是具有名称(如容器、序列容器、关联容器等)的通用类别;容器类型是可用于创建具体容器对象的模板。
以前的11个容器类型分别是deque、list、queue、priority_queue、stack、vector、map、multimap、set、multiset和bitset;C++11 新增了forward_list、unordered_map、unordered_multimap、unordered_set和unordered_multiset,且不将bitset视为容器,而将其视为一种独立的类别。
容器是存储其他对象的对象。被存储的对象必须是同一类型的,它们可以是OOP意义上的对象,也可以是内置类型值。存储在容器中的数据为容器所有,这意味着当容器过期时,存储在容器中的数据也将过期。
不能将任何类型的对象存储在容器中,具体地说,类型必须是可复制构造的和可赋值的。基本类型满足这些要求;只要类定义没有将复制构造函数和赋值运算符声明为私有或保护的,则也满足这种要求。
基本容器不能保证其元素都按特定的顺序存储,也不能保证元素的顺序不变,但对概念进行改进后,则可以增加这样的保证。所有的容器都提供某些特征和操作。
#include <iostream>
#include <list>
#include <iterator>
#include <algorithm>
using namespace std;
void outint(int n) {std::cout << n << " ";}
int main()
{
list<int> one(5,2);
int stuff[5] = {1,2,4,8,6};
list<int> two;
two.insert(two.begin(), stuff, stuff + 5);
int more[6] = {6, 4, 2, 6, 5};
list<int> three(two);
three.insert(three.end(), more, more + 6);
cout << "List one: ";
for_each(one.begin(), one.end(), outint);
cout << endl << "List two: ";
for_each(three.begin(), three.end(), outint);
three.remove(2);
cout << endl << "List three minus 2s: ";
for_each(three.begin(), three.end(), outint);
three.splice(three.begin(), one);
cout << endl << "List three after splice: ";
for_each(three.begin(), three.end(), outint);
cout << endl << "List one: ";
for_each(one.begin(), one.end(), outint);
three.unique();
cout << endl << "List three after unique: ";
for_each(three.begin(), three.end(), outint);
three.sort();
three.unique();
cout << endl << "List three after sort & unique: ";
for_each(three.begin(), three.end(), outint);
two.sort();
three.merge(two);
cout << endl << "Sorted two merged into three: ";
for_each(three.begin(), three.end(), outint);
cout << endl;
return 0;
}
输出结果:
List one: 2 2 2 2 2
List two: 1 2 4 8 6 6 4 2 6 5 0
List three minus 2s: 1 4 8 6 6 4 6 5 0
List three after splice: 2 2 2 2 2 1 4 8 6 6 4 6 5 0
List one:
List three after unique: 2 1 4 8 6 4 6 5 0
List three after sort & unique: 0 1 2 4 5 6 8
Sorted two merged into three: 0 1 1 2 2 4 4 5 6 6 8 8
6、算法
STL包含很多处理容器的非成员函数。前面所说的:sort( )、copy( )、find( )、random_shuffle( )、set_union( )、set_intersection( )、set_difference( ) 和 transform( )。它们的总体设计是相同的,都是使用迭代器来标识要处理的数据区间和结果的放置位置。
对于算法函数设计,有两个主要的通用部分。首先,它们都使用模板来提供泛型;其次,它们都使用迭代器来访问容器中的数据的通用表示。
7、其他库
C++提供了三个数组模板:vector、valarray 和 array 。这些类是由不同的小组开发的,用于不同的目的。vector模板类是一个容器类和算法系统的一部分,它支持面向容器的操作,如排序、插入、重新排列、搜素、将数据转移到其他容器中等。而valarry类模板是面向数值计算的,不是STL的一部分。例如:它没有push_back( )和 insert( )方法,但为很多数学运算提供了一个简单、直观的接口。最后,array是为了替代内置数组而设计的,它通过提供更好、更安全的接口,让数组更紧凑,效率更高。Array表示长度固定的数组,因此不支持push_back( )和insert( ),但提供了多个STL方法,包括begin( )、end( )、rbegin( )和rend( ),这使得很容易将STL算法用于array对象。
例如:有如下声明:
vector<double> ved1(10), ved2(10), ved3(10);
arrat<double,10> vod1, vod2, vod3;
valarray<double> vad1(10), vad2(10), vad3(10);