全局对象在程序启动时分配,在程序结束时销毁;
局部自动对象在定义所在的程序块时被创建,在离开块时被销毁;
局部static对象在第一次使用前分配,在程序结束时销毁
动态分配的对象的生存期与它们在哪里创建是无关的,只有当显式地释放时,这些对象才会销毁
静态内存用来保存局部static对象、类static数据成员以及定义在任何函数之外的变量。——在使用前分配,在程序结束时销毁
栈内存用来保存定义在函数之内的非static对象。——仅在其定义的程序块运行时才存在
堆(或自由空间)用来存储动态分配的对象——即那些在程序运行时分配的对象
一、动态内存与智能指针
通过new在动态内存中为对象分配空间并返回一个指向该对象的指针,我们可以选择对对象进行初始化。
delete接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。
动态内存的使用很容器出问题,因此新标准库提供了两种智能指针类型来管理动态对象。智能指针的行为类似于常规指针,重要的区别是它负责自动释放所指向的对象。
shared_ptr允许多个指针指向同一对象;unique_ptr则独占多指向的对象;另外还有一个名为weak_ptr的伴随类,它是一种弱引用,指向shared_ptr所管理的对象。这三种类型都定义在memory头文件中
类似于vector,智能指针也是模板,当我们创建一个智能指针时,必须提供额外的信息——指针可以指向的类型。
shared_ptr<T> sp | 空智能指针,可以指向类型为T的对象 |
unique_ptr<T> up | |
p | 将p用作一个条件判断 |
*p | 解引用p,获得它指向的对象 |
p->mem | 等价于(*p).mem |
p.get() | 返回p中保存的指针 |
swap(p,q) | 交换p和q中的指针 |
p.swap(q) |
make_shared<T>(args) | 返回一个shared_ptr,指向一个动态分配的类型为T的对象。使用args初始化此对象 |
shared_ptr<T>p(q) | p是shared_ptr q的拷贝;此操作会递增q中的计数器。q中的指针必须能转换为T* |
p=q | 此操作会递减p的引用计数,递增q的引用计数;若p的引用计数变为0,则将其管理的原内存释放 |
p.unique() | 若p.use_count为1,返回true;否则返回false |
p.use_count() | 返回与p共享对象的智能指针数量;可能很慢,主要用于调试 |
make_shared函数
此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr:
//指向一个值为42的int的shared_ptr
shared_ptr<int> p3=make_shared<int>(42);
//p4指向一个值为“9999999999”的string
shared_ptr<string> p4=make_shared<string>(10,'9');
//p5指向一个值初始化的int,即,值为0
shared_ptr<int> p5=make_shared<int>();
使用auto定义一个对象来保存make_shared的结果比较简单:
//p6指向一个动态分配的空vector<vector<string>>
auto p6=make_shared<vector<string>>();
shared_ptr的拷贝和赋值
当进行拷贝或赋值操作时,每个shared_ptr都会记录有多少个其他shared_ptr指向相同的对象:
auto p=make_shared<int>(42);
auto q(p); //p和q指向相同对象,此对象有两个引用者
每个shared_ptr都有一个关联的计数器,称为引用计数。无论何时我们拷贝一个shared_ptr,计数器都会递增(初始化、函数从餐、函数返回值)。当我们给shared_ptr赋予一个新值或是shared_ptr被销毁时,计数器就会递减。
一旦一个shared_ptr的计数器变为0,它就会自动释放自己所管理的对象:
auto r=make_shared<int>(42);
r=q; //给r赋值,令它指向另一个地址
//递增q指向的对象的引用计数
//递减r原来指向的对象的引用计数
//r原来指向的对象已没有引用者,会自动释放
shared_ptr自动销毁所管理的对象
析构函数负责完成销毁工作,一般用来释放对象所分配的资源。类似于构造函数,每一个类都有一个析构函数
shared_ptr的析构函数会递减它所指向的对象的计数引用,如果引用计数变为0,shared_ptr的析构函数就会销毁对象,并释放它占用的内存。
shared_ptr还会自动释放相关联的内存
当动态对象不再使用时,shared_ptr类会自动释放动态内存,这一特性使得动态内存的使用变得非常容易。(根据计数引用是否为0)
对于一块内存,shared_ptr类保证只要有任何shared_ptr对象引用它,它就不会被释放掉。
如果将shared_ptr存放于一个容器中,而后不再需要全部元素,而只能使用其中一部分,要记得用erase删除不再需要的那些元素
使用了动态生存期的资源的类
程序使用动态内存出于以下三种原因之一:
1、程序不知道自己需要使用多少对象(典型例子:容器)
2、程序不知道所需对象的准确类型
3、程序需要在多个对象间共享数据
到目前为止,我们使用的类中,分配的资源都与对应对象生存期一致。当我们拷贝一个vector时,原vector和副本vector中的元素是相互分离的:
vector<string> v1;
{//新作用域
vector<string> v2={"a","an","the"};
v1=v2; //从v2拷贝元素到v1
} //v2被销毁,其中的元素也被销毁
//v1依然有三个元素
一般而言,如果两个对象共享底层的数据,当某个对象被销毁时,我们不能单方面的销毁底层数据:
Blob<string> b1;
{//新作用域
Blob<string> b2={"a","an","the"};
b1=b2; //从b2拷贝元素到b1
} //b2被销毁,但b2中的元素不能销毁
//b1指向最初由b2创建的元素
使用动态内存的一个常见原因就是允许多个对象共享相同的状态
定义StrBlob类
为了实现数据共享,我们为每个strBlob设置一个shared_ptr来管理动态分配的vector。
class StrBlob{
public:
typedef std::vector<std::string>::size_type size_type;
StrBlob();
StrBlob(std::initializer_list<std::string> il);
size_type size() const { return data->size(); }
bool empty() const { return data->empty(); }
//添加和删除元素
void push_back(const std::string &t) { data->push_back; }
void pop_back();
//元素访问
std::string &front();
std::string &back();
private:
std::shared_ptr<std::vector<std::string>> data;
//如果data[i]不合法,抛出一个异常
void check(size_type i, const std::string &msg) const;
};
void StrBlob::check(size_type i, const std::string &msg) const
{
if (i >= data->size())
throw out_of_range(msg);
}
string &StrBlob::front() const
{
check(0,"front on empty StrBlob");
return data->front();
}
string& StrBlob::back() const
{
check(0,"back on empty StrBlob");
return data->back();
}
void StrBlob::pop_back()
{
check(0, "pop_back on empty StrBlob");
data->pop_back();
}
StrBlob构造函数
默认构造函数分配一个空vector;接受一个initializer_list的构造函数将其参数传递给对应的vector构造函数,此构造函数通过拷贝列表中的值来初始化vector的元素:
StrBlob::StrBlob():data(make_shared<vector<string>>()) {}
StrBlob::StrBlob(initializer_list<string> il):
data(make_shared<vector<string>>(il)) {}
StrBlob的拷贝、赋值和销毁
StrBlob使用默认版本的拷贝、赋值和销毁成员函数来对此类型的对象进行这些操作。默认情况下,这些操作拷贝、赋值和销毁类的数据成员。
2、直接管理内存
我们使用运算符new分配内存,delete释放new分配的内存。相对于智能指针,使用这两个运算符管理内存非常容易出错。而且,自己直接管理内存的类与使用智能指针的类不同,他们不能依赖类拷贝、赋值和销毁操作的任何默认定义。
使用new动态分配和初始化对象
在自由空间分配的内存是无名的,因此new无法为其分配的对象命名,而是返回一个指向该对象的指针;
new表达式在自由空间构造一个对象,并返回指向该对象的指针
int *p=new int; //pi指向一个动态分配的、未初始化的无名对象
默认情况下,动态分配的对象是默认初始化的,这意味着内置类型或组合类型的对象的值是未定义的,而类类型对象将默认构造函数进行初始化:
string *ps=new string; //初始化为空的string
int *pi=new int; //pi指向一个未初始化的int
我们可以使用直接初始化来初始化一个动态分配的对象:
int *pi=new int(1024);
string *ps=new string(10,'9');
vector<int> *pv=new vector<int> {0,1,2,3,4,5,6,7,8,9};
也可以对动态分配的对象进行值初始化,只需要在类型名之后跟一对空括号即可:
string *ps1=new string; //默认初始化为空string
string *ps=new string(); //值初始化为空string
int *pi1=new int; //默认初始化;*pi1的值未定义
int *pi2=new int(); //值初始化为0,*pi2为0
对于内置类型,注意值初始化和默认初始化的区别:
1.对于定义了自己的构造函数的类类型(如string),要求值初始化是没有意义的:不管采用什么形式,对象都会通过默认构造函数来初始化;
2.对于内置类型,值初始化的内置类型对象有着良好定义的值,而默认初始化的对象的值则是未定义的。
3.对于类中那些依赖于编译器合成的默认构造函数的内置类型对象,如果它们未在类内初始化,它们的值也是未定义的
如果我们提供了一个括号包围的初始化器,就可以使用auto,此时初始化器可以推断我们想要分配的对象的类型,只有当括号中仅有单一初始化器才能使用auto:
auto p1=new auto(obj);
auto p2=new auto{a,b,c}; //错误:括号中只能有单个初始化器
动态分配的const对象
动态分配的const对象必须进行初始化。对于定义了默认构造函数的类类型,其const动态对象可以隐式初始化,而其他类型的对象就必须显式初始化:
const int *pci=new const int(1024); //显式初始化
const string *pcs=new const string; //默认初始化
内存耗尽
默认情况下,如果new不能分配所要求的内存空间,会抛出一个类型为bad_alloc的异常。我们也可以改变使用new的方式来阻止它抛出异常。bad_alloc和nothrow都定义在头文件new中:
//如果分配失败,new返回一个空指针
int *p1=new int; //如果分配失败,new抛出std::bad_alloc
int *p2=new (nothrow) int; //如果分配失败,new返回一个空指针
释放动态内存
delete表达式接受一个指针,指向我们想要释放的对象:
delete p;
指针值和delete
传递给delete的指针必须指向动态分配的内存,或者是一个空指针。释放一块并非new分配的内存,或者将相同的指针值释放多次,其行为是未定义的
虽然一个const对象的值不能被改变,但它本身是可以被销毁的:
动态对象的生存期直到被释放时为止
当使用动态内存时,必须记得释放内存:
void use_factory(T arg)
{
Foo *p=factory(arg);
//使用p
delete p; //现在记得释放内存,我们已经不需要它了
}
Foo *use_factory(T arg)
{
Foo *p=factory(arg);
//使用p
return p; //调用者必须释放内存
}
使用new和delete管理动态内存存在的三个常见问题:
1.忘记delete内存——内存泄露问题
2. 使用已经释放掉的对象。
3.同一块内存释放两次。
坚持只使用智能指针,就可以避免所有这些问题
3、shared_ptr和new结合使用
我们可以用new返回的指针来初始化智能指针。因为接受指针参数的智能指针构造函数是explicit的,因此我们不能将一个内置指针隐式转换成一个智能指针,必须使用直接初始化形式:
shared_ptr<int> p1=new int(1024); //错误:必须使用直接初始化形式
shared_ptr<int> p2(new int(1024)); //正确:使用了直接初始化形式
同理,一个返回shared_ptr的函数不能在其返回语句中隐式转换成一个普通指针:
shared_ptr<int> clone(int p){
return new int(p); //错误:隐式转换为shared_ptr<int>
}
shared_ptr<int> clone(int p){
return shared_ptr<int>(new int(p)); //正确:显式地用int*创建shared_ptr<int>
}
默认情况下,一个用来初始化智能指针的普通指针必须指向动态内存,因为智能指针默认使用delete释放它所关联的对象。
shared_ptr<T> p(q) | p管理内置指针q所指向的对象;q必须指向new分配的内存,且能够转换成T*类型 |
shared_ptr<T> p(u) | p从unique_ptr u那里接管了对象的所有权;将u置为空 |
shared_ptr<T> p(q,d) | p接管了内置指针q所指向的对象的所有权。p将使用可调用对象d来代替delete |
shared_ptr<T> p(p2,d) | p是shared_ptr p2的拷贝,唯一的区别是p将调用可调用对象d来代替delete |
p.reset() | 若p是唯一指向其对象的shared_ptr,reset会释放此对象;若传递了可选的参数内置指针q,会令p指向q,否则会将p置空。若还传递了参数d,将会调用d而不是delete来释放q。 |
p.reset(q) | |
p.reset(q,d) |
不要混合使用普通指针和智能指针
当将一个shared_ptr绑定到一个普通指针时,我们就将内存的管理责任交给了这个shared_ptr。一旦这么做了,我们就不应该再使用内置指针来访问shared_ptr所指向的内存了。
int *x(new int(24)); //危险:x是一个普通指针,而不是一个智能指针
process(x); //错误:不能将int*转换成一个shared_ptr<int>
process(shared_ptr<int>(x)); //临时shared_ptr,合法的,但内存会被释放,引用计数变为0
int j=*x; //未定义的:x是一个空悬指针
使用一个内置指针来访问一个智能指针所负责的对象是很危险的,因为我们无法知道对象何时会被销毁。内置指针很可能成为空悬指针
也不要使用get初始化另一个智能指针或为智能指针赋值
get返回一个内置指针,指向智能指针所管理的对象。get用来将指针的访问权限传递给代码,你只有在确定代码不会delete指针的情况下,才能使用get。特别是,永远不要用get初始化另一个智能指针或者为另一个智能指针赋值。
4、智能指针和异常
使用智能指针,即使程序块过早结束,智能指针类也能确保在内存不再需要时将其释放:
void f()
{
shared_ptr<int> sp(new int(42)); //分配一个新对象
//抛出一个异常,却未被捕获
}//在函数结束时shared_ptr自动释放内存
而在下面的例子中,内存不会被释放:
void f(){
int *ip=new int(42);
//抛出异常,且未被捕获
delete ip;
} //内存永远不会被释放了
智能指针与哑类,使用自己的释放操作
某些类没有定义析构函数,此时我们可以使用shared_ptr来保证该类生成的对象的内存被正确释放,首先定义一个函数(删除器)来代替得delete:
void end_connection(connection *p) {disconnect(*p);} //删除器
void f(destination &d/*其他参数*/){
connection c=connect(&d);
shared_ptr<connection> p(&c,end_connection);
//使用链接
//当f退出时(即使是由于异常而退出),connection会被正确关闭
}
智能指针陷阱:
为了正确使用智能指针,我们必须坚持一些基本规范:
- 不能使用相同的内置指针初始化(或reset)多个智能指针
- 不delete get()返回的指针
- 不使用get()初始化或reset另一个智能指针
- 如果你get()返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变为无效了
- 如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器
5、unique_ptr
在某个时刻,只能有一个unique_ptr指向一个给定对象,unique_ptr被销毁时,它所指向的对象也被销毁。
与shared_ptr不同,没有类似make_shared的标准函数返回一个unique_ptr。因此,当我们定义一个unique_ptr时,需要将其绑定到一个new返回的指针上。类似于shared_ptr,初始化unique_ptr必须采用直接初始化方式:
unique_ptr<double> p1; //可以指向一个double的unique_ptr
unique_ptr<int> p2(new int(42)); //p2指向一个值为42的int
由于一个unique_ptr拥有它指向的对象,因此unique_ptr不支持普通的拷贝或赋值操作。不过有一个列外:我们可以拷贝或赋值一个将要被销毁的unique_ptr。最常见的例子是从函数返回一个unique_ptr。
unique_ptr<T> u1 | |
unique_ptr<T,D> u2 | |
unique_ptr<T,D> u(d) | 空unique_ptr,指向类型为T的对象,用类型为D的对象d代替delete |
u=nullptr | |
u.release() | 返回u当前保存的指针并将其置为空 |
u.reset() | 释放u指向的指针 |
u.reset(q) | 如果提供了内置指针q,令u指向这个对象,原来的内存被释放 |
u.reset(nullptr) | 将u置空 |
虽然我们不能拷贝或赋值unique_ptr,但可以通过调用release或reset将指针的所有权从一个(非const)unique_ptr转移给另一个unique_ptr:
//将所有权从p1转移给p2
unique_ptr<string> p2(p1.release()); //release将p1置为空
unique_ptr<string> p3(new string("Trex"));
//将所有权从p3转移给p2
p2.reset(p3.release()); //reset释放了p2原来指向的内存
如果我们不用另一个智能指针来保存release返回的指针,我们的程序就要负责资源的释放:
p2.release(); //错误:p2不会释放内存,而且我们丢失了指针
auto p=p2.release(); //正确:但我们必须记得delete(p)
向unique_ptr传递删除器
重载一个unique_ptr中的删除器会影响到unique_ptr类型以及如何构造(或reset)该类型的对象。我们必须在尖括号中unique_ptr指向类型之后提供删除器类型。在创建或reset一个这种unique_ptr类型的对象时,必须提供一个指定类型的可调用对象(删除器):
void f(destination &d/*其他参数*/)
{
connection c=connect(&d); //打开连接
unique_ptr<connection,decltype(end_connection)*>
p(&c,end_connection);
//使用连接
//当f退出时(即使是由于异常而退出),connection会被正确关闭
}
6、weak_ptr
weak_ptr是一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的对象。讲一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放。
weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少.
weak_ptr<T> w | |
weak_ptr<T> w(sp) | |
w=p | |
w.reset() | |
w.use_count | |
w.expired() | |
w.lock() |
当创建一个weak_ptr时,要用一个shared_ptr来初始化它:
auto p=make_shared<int>(42);
weak_ptr<int> wp(p);
由于对象可能不存在,我们不能使用weak_ptr直接访问对象,而必须调用lock。此函数检查weak_ptr指向的对象是否存在。
if(shared_ptr<int> np=wp.lock()){//如果np不为空则条件成立
//在if中,np与p共享对象
}
二、动态数组
大多数应用应该使用标准库容器而不是动态分配的数组。使用容器更为简单、更不容易出现内存管理错误并且可能有更好的性能。
使用容器的类可以使用默认版本的拷贝、赋值和析构操作。分配动态数组的类则必须定义自己版本的操作,在拷贝、复制以及销毁对象时管理所关联的内存。
3、new和数组
使用new分配一个动态数组,必须在类型名之后跟一对方括号,在其中指明要分配的对象的数目。方括号中的大小必须是整数,但不必是常量。分配一个数组会返回指向第一个对象的指针:
int *p=new int[42]; //p指向第一个int
初始化动态分配对象的数组
不加括号——默认初始化
大小之后加一对空括号——值初始化
大小之后跟一个花括号列表——初始化器初始化
动态分配一个空数组是合法的
虽然我们不能创建一个大小为0的静态数组对象,但当n等于0时,调用new[n]是合法的:
char arr[0]; //错误:不能定义长度为0的数组
char *cp=new char[0]; //正确:但cp不能解引用
当我们用new分配一个大小为0的数组时,new返回一个合法的非空指针。但此指针不能解引用——毕竟它不指向任何元素。
释放动态数组
使用特殊的delete来释放动态数组——在指针前加上一个空方括号对(方括号必须加上):
delete []pa;
智能指针和动态数组
标准库提供了一个可以管理new分配的数组的unique_ptr版本。使用unique_ptr管理动态数组时,我们必须在对象类型后面跟一对方括号:
unique_ptr<int[]> up(new int[10]);
up.release(); //自动用delete[]销毁其指针
另外一方面,当一个unique_tr指向一个数组时,我们可以使用下标运算符来访问数组中的元素:
for(size_t i=0;i!=10;++i)
up[i]=i; //为每个元素赋予一个新值
指向数组的unique_ptr不支持成员访问运算符(点和箭头运算符) 其他unique_ptr操作不变 | |
unique_ptr<T[]> u | |
unique_ptr<T[]> u(p) | p为内置指针 |
u[i] |
与unique_ptr不同,shared_ptr不直接支持管理动态数组。如果我们希望使用shared_ptr管理一个动态数组,必须提供自己定义的删除器:
shared_ptr<int> sp(new int[10],[](int *p){delete [] p;});
sp.reset(); //使用我们提供的lambda释放数组,它使用delete[]
shared_ptr未定义下标运算符,而且智能指针类型不支持指针算数运算。因此,为了访问数组中的元素,必须用get获取一个内置指针,然后用它来访问数组元素:
for(size_t i=0;i!=10;++i)
*(sp.get()+i)=i; //使用get获取一个内置指针
2、allocator类
标准库allocator类定义在头文件memory中,它帮助我们将内存分配和对象构造分离开来。它提供一种类型感知的内存分配方法,它分配的内存是原始的、未构造的。
allocator<T> a | |
a.allocate(n) | |
a.deallocate(p,n) | |
a.construct(p,args) | |
a.destroy(p) |
allocator<string> alloc; //可以分配string的allocator对象
auto const p=alloc.allocate(n); //分配n个未初始化的string
auto q=p;
alloc.construct(q++); //*q为空字符串
alloc.construct(q++,10,'c'); //*q为cccccccccc
alloc.construct(q++,"hi"); //*q为hi
cout<<*p<<endl; //正确:使用string的输出运算符
cout<<*q<<endl; //灾难:q指向未构造的内存
当我们用完对象后,必须对每个构造的元素调用destory来摧毁它们。我们只能对真正构造了的元素进行destory操作
一旦元素被销毁后,我们就可以重新使用这部分内存保存其他string,也可以将其归还给系统。释放内存通过调用deallocate来完成。
拷贝和填充未初始化内存的算法
allocator还有两个伴随算法,可以在未初始化内存中创建对象。它们都定义在头文件memory中。
这些函数在给定目的位置创建元素,而不是由系统分配内存给它们 | |
uninitialized_copy(b,e,b2) | |
uninitialized_copy_n(b,n,b2) | |
uninitialized_fill(b,e,t) | |
uninitialized_fill_n(b,n,t) |
uninitialized_copy会返回(递增后的)目的位置迭代器:
vector<int> vi(10); //空间为10的vector
//分配比vi空间大一倍的动态内存
auto p=alloc.allocate(vi.size()*2);
//通过拷贝vi中的元素来构造从p开始的元素
auto q=uninitialized_copy(vi.begin(),vi.end(),p);
//将剩余元素初始化为42
uninitialized_fiil_n(q,vi.size(),42);
三、使用标准库:文本查询程序
#include<iostream>
#include<map>
#include<vector>
#include<set>
#include<new>
#include<fstream>
#include<sstream>
#include<memory>
using namespace std;
class QueryResult;
class TextQuery{
public:
using line_no = vector<string>::size_type;
TextQuery(ifstream &);
QueryResult query(const string&) const;
private:
shared_ptr<vector<std::string>> file;
map<string, shared_ptr<set<line_no>>> wm;
};
TextQuery::TextQuery(ifstream &is) :file(new vector<string>)
{
string text;
while (getline(is, text))
{
file->push_back(text);
int n = file->size() - 1;
istringstream line(text);
string word;
while (line >> word)
{
auto &lines = wm[word];
if (!lines)
lines.reset(new set<line_no>);
lines->insert(n);
}
}
}
class QueryResult
{
using line_no = vector<string>::size_type;
friend ostream &print(ostream&,const QueryResult &);
public:
QueryResult(string s, shared_ptr<set<line_no>>p, shared_ptr<vector<string>> f) :sought(s), lines(p), file(f){ }
private:
string sought;
shared_ptr<set<line_no>> lines;
shared_ptr<vector<string>> file;
};
string make_plural(size_t ctr, const string &word,const string& ending)
{
return (ctr > 1) ? word + ending : word;
}
ostream &print(ostream &os, const QueryResult &qr)
{
os << qr.sought << " occurs " << qr.lines->size() << " " << make_plural(qr.lines->size(), "time", "s") << endl;
for (auto num : *qr.lines)
os << "\t(line " << num + 1 << ")" << *(qr.file->begin() + num) << endl;
return os;
}
QueryResult TextQuery::query(const string &sought) const
{
static shared_ptr<set<line_no>> nodata(new set<line_no>);
auto loc = wm.find(sought);
if (loc == wm.end())
return QueryResult(sought, nodata, file);
else
return QueryResult(sought,loc->second,file);
}
void runQueries(ifstream &infile)
{
TextQuery tq(infile);
while (true)
{
cout << "enter word to look for,or q to quit:";
string s;
if (!(cin >> s) || s == "q") break;
print(cout, tq.query(s)) << endl;
}
}
int main()
{
ifstream in("text.txt");
if (!in)
{
cout << "文件不存在!" << endl;
return -1;
}
runQueries(in);
return 0;
}