Effective C++ Note

基础部分

1. c++四个次语言

c,objective-orinted c++,Template C++,STL

2. 尽量用const,enum, inline 替换 #define

例如

1
2
#define ASPECT_RATIO 1.652 /* 预编译器 */
const double Aspect_Ration 1.652 /* 使用常量替换宏,以免error时看不到变量名 */

而当对于一个字符串常量

1
2
const char* const str = "stockdean";/* 常量字符串*/
const std::string str2("stockdean"); /* 比上一个更好*/

而对于类内变量

1
2
3
4
5
6
class GamePlayer{
private: //封装性
static const int NumTurns = 5; //常量声明式,static 表示只存在一个
int scores [NumTurns]; //使用该常量

};

此时NumTurns为声明式而非定义式,如果必须提供定义式,则需要在.cc文件内定义。const int GamePlayer::NumTurns ;
无法使用#define 来创建一个class 专属常量,因为#define并不重视作用域,却不提供封装性。如private。但是使用const 可以提供封装性。

下面提供一个例子,如果老式编译器不支持在声明式赋予初值,则如下

1
2
3
4
5
6
7
8
9
10
11
12
/**
xx.h
*/
class CostEstimate{
private:
static const double FudgeFactor; /* static class 常量声明 */
...
}
/**
xx.cpp
*/
const double CostEstimate::FudgeFactor = 1.35; /* 定义*/

但是,如果后面的数组必须在编译期间指导数组大小怎么办?

1
2
3
4
5
6
class GamePlayer{
private: //封装性
enum { NumTurns = 5 }; //使用enum可以充当int
int scores [NumTurns]; //使用该常量

};

一.enum hack 行为更像#define(比如可以取const的地址,但是不能取enum的地址,也不能取#define的地址)
二. 实用主义

macros的实现引发的错误,如下面的例子

1
2
3
4
5
6
7
#define CALL_WITH_MAX(a,b) f((a) > (b)?(a):(b))  
取a b 中的较大值调用f

一个有趣的例子
int a = 5,b = 0;
CALL_WITH_MAX(++a, b); //a累加两次
CALL_WITH_MAX(++a, b+10);//a累加一次

为了避免macro引发的错误,template inline函数可以避免

1
2
3
4
5
template<typename T>
inline void callWithMax(const T& a,const T& b)
{
f(a>b?a:b);
}

小节:常量避免#define,macro实用template inline func 来替代。

3. 尽可能使用const

例子

1
2
3
4
char* p ="stockdean";/* 字符串*/
const char* p ="stockdean";/*内容为const*/
char* const p ="stockdean" /*指针为const*/
const char* const p ="stockdean" /*内容和指针都是const*/

两种形式

1
2
void f1(const Widget* w);
void f2(Widget const* w); //都是指向不变的Widget类对象

对于STL迭代器有同等适用的规则[2]

1
2
3
4
5
6
7
8
std::vector<int> vec;
const std::vector<int>::iterator iter = vec.begin(); //类似T* const
*iter = 10;/*没问题*/
iter++;/* 报错 */

std::vector<int>::const_iterator iter = vec.begin(); //类似const T*
*iter = 10;/*报错*/
iter++;/*报错*/

对于重载的operator有同样的妙用

1
2
3
4
class Rational { .. };
const Rational operator* (const Rational& lhs, const Rational& rhs);

返回const 避免在a*b后再调用operator=

const 对于成员函数

  • 使class容易理解,标明什么是可以改的
  • 使操作const 对象成为可能(见[20])
1
2
3
4
5
6
7
8
9
10
11
12
13
class TextBook{
public: const char& operator[](std::size_t position)const//operator for const 对象
{return text[position];}
char& operator[](std::size_t position)//operator for non-const 对象
{return text[position];}


private: std::string text;
};
const TextBook ctb("hello");
std::cout<<ctb[0]; //调用const operator[]
TextBook tb("world");
std::cout<<tb[0];//调用non-const operator[]

Alt text
Alt text

bitwise const 和logical const
假如

1
2
3
4
5
6
7
8
9
10
11
12
13
class TextBook{
public: const char& operator[](std::size_t position)const//operator for const 对象
{return text[position];}
char& operator[](std::size_t position)//operator for non-const 对象
{return text[position];}


private: char* text;
};
const TextBook ctb("hello");
char* pc = &ctb[0];
*pc = 'J';//这是允许的
因为只改变了内部值,这是允许的

假如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class TextBook{
public: std::size_t length() const;
private: const char* pText;
std::size_t textLength;
bool lengthIsValid;
};
std::size_t TextBook::length() const
{
if(!lengthIsValid)
{
textLength = std::strlen(pText);//错误const成员函数内不能修改属性
lengthIsValid = true; //错误
}
return textLenght;
}
想要解决这个问题使用mutable类型(可变的)来修改non-static成员变量bitwise const限定

Alt text

在const和non-const成员函数间避免重复
Alt text

1
2
3
4
5
6
7
8
9
10
11
class TextBook{
public: const char& operator[](std::size_t position)const//operator for const 对象
{
...
return text[position];}
char& operator[](std::size_t position)//operator for non-const 对象
{
return const_cast<char &>(static_cast<const TextBook&>(*this)[position]);
}
private: std::string text;
};

Alt text

4. 确定对象使用前被初始化

  1. 要为内置对象手动初始化,因为C++不一定初始化(c part可能不会初始化,其他可能会)
  2. 构造函数使用初始值列表,而不要使用构造函数的赋值操作,排列顺序最好与声明顺序同
  3. 未免受”跨编译单元初始化次序”干扰,用local static 替换non-local static 对象

对于第一条举例:STL的vector会保证初始化,而c的array需要手动初始化。
对于非内置类型,类的初始化和member的初始化一般交由构造函数

举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class PhoneNumber{...};
class ABEntry{

public:
ABEntry(const std::string& name,std::string& address,const std::list<PhoneNumber> phones);

private: std::string theName;
std::string theAddress;
std::list<PhoneNumber> thePhones;
int numTimesConsulted;
};

ABEntry(const std::string& name,std::string& address,const std::list<PhoneNumber> phones)
{
theName = name; //这些都是赋值(assignment 操作)
theAddress = address;//不是初始化
thePhones = phones;
numTimesConsulted = 0;
}
//对于string等类型先执行了=default构造操作,然后才赋值。

更好的写法
ABEntry():theName(),theAddress,thePhones(),numTimesConsulted(0)
{}

有些情况下对于内置类型也一定要初始化,例如constreferencemember field
C++的初始化顺序一般与声明的顺序相同,但是有些操作一定要按顺序来(如数组大小要在数组前初始化)。

不同编译单元内定义之non-local static 对象次序
编译单元:
Alt text
local 对象:函数内的对象。
non-local static对象:global或者namespace内的或者class和file作用域内被声明为static的对象

例:

1
2
3
4
5
class FileSystem{
public:
std::size_t numDisks() const;
};
extern FileSystem tfs;//预备给客户使用的对象

此时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Directory{
public:
Directroy(params);
...
};
Directory::Directroy(params)
{
std::size_t disks = tfs.numDisks();
}

Directory tmpDir(params);
/**
除非tfs在tmpDir前被初始化,否则会用到未初始化的tfs,这个不同文件的是难以确定的。
/

所以需要用另外一种方式去实现。
这里使用的是单例模式Singleton
即用local static 替换non-local static。使用return T&类型的func来实现。

理由:C++保证,函数内的non-local对象会在该函数被调用期间和首次遇到该对向定义式时初始化。所以使用函数调用替换”直接访问non-local 对象”

修改后

1
2
3
4
5
6
7
8
9
10
11
12
13
class FileSystem{
public:
std::size_t numDisks() const;
FileSystem& tfs();
};
/**
local static 替换non-local static
*/
FileSystem& tfs()
{
static FileSystem fs;
return fs;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Directory{
public:
Directroy(params);
...
};
Directory::Directroy(params)
{
std::size_t disks = tfs().numDisks();
}

Directory& tempDir()
{
static Directory td;
return td;
}

构造/析构/赋值运算

5. 了解C++编译器默默做了什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Empty{//实质上cpp编译器生成的是下面的
};

class Empty{
public:
Empty(){}//默认构造函数
~Empty(){}
Empty(const Empty& empty){}//拷贝构造函数
Empty& operator= (const Empty& rhs){return rhs}//

};

默认生成四个:两个构造函数,一个析构函数,一个拷贝赋值运算符
只有当这些函数被调用,才会被编译器创造出来。



Empty e1();
Empty e2(e1);
e1 = e2;

编译器产出的析构函数是non-virtual类型的,除非base class自身声明
而对于拷贝构造函数和拷贝赋值运算符将non-static 对象拷贝到目标对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
template<typename T>
class NameObject{
public: NameObject(const char* name, const T& value);
NameObject(const std::string & name,const T& value);

private: std::string nameValue;
T objectValue;


};


由于已经声明了构造函数,所以编译器不再生产default。但是会生成拷贝构造函数和拷贝赋值运算符

但是生成copy assignment operator的条件必须满足:1.代码合法2.有适当机会证明有意义
下面一种情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template<class T>
class NameObject{
public: NameObject(std::string& name,const T& value);

private: std::string nameValue;
const T objectValue;


};

std::string newDog("P");
std::string oldDog("S");
NameObject<int> p(newDog,2);
NameObject<int> s(oldDog,32);
p = s//错误这里编译器会拒绝编译

因为这里string& name 不能变更,因为c++不允许”让reference改变向不同对象”

如果某个base classes将copy assignment operator声明为private,那么编译器会拒绝为其derived classes 生成CAO。因为无权调用。

6. 阻止编译器生成拷贝构造函数和拷贝构造运算符

两种方法:

  • 声明但是不定义
  • 继承一个uncopyable class
1
2
3
4
5
6
7
只声明不定义
class HomeForSale{
private: HomeForSale(const HomeForSale& hfs);//编译器生成都是public,这里为private
HomeForSale& operator=(const HomeForSale& rhs);


};

或者定义一个base class

1
2
3
4
5
6
7
8
9
10
11
12
class Uncopyable{
protected:
Uncopyable();
~Uncopyable();
private: Uncopyable(const Uncopyable& );
Uncopyable operator=(const Uncopyable& );
};

然后继承
class HomeForSale:private Uncopyable{//class 不再声明copy 构造或者CAO
...
};

7. 为多态基类声明virtual析构函数

  1. 如果一个(多态性质)作为base class的类有member函数为virtual,那么它的析构函数应为virtual
  2. 如果一个类不打算作为base class不要使用virtual修饰析构函数
    如果存在一个Factory类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Animal{
public: Animal();
~Animal();

....
};

class Dog:public Animal{
...
};

Animal* getAnimal();
Animal* p = getAnimal();
delete p;//会导致局部销毁,只销毁base class的部分。所以

改为

1
2
3
4
5
6
7
8
9
10
11
12
13
class Animal{
public: Animal();
// ~Animal();
virtual ~Animal();
....
};

class Dog:public Animal{
...
};

Animal* p = new Dog();
delete p;//会导致局部销毁,只销毁部分。所以

如果一个class不作为base class类但是声明了virtual member func会怎么样?

由于对象会保存一个vptr指向虚函数表(virtual function table)(指针数组),所以导致对象臃肿。
Alt text
如果继承一个没有virtual函数的class也会出现问题。如STL的string等(析构函数non-virtual)
生成一个抽象类

1
2
3
4
5
6
7
8
class AWOV{
public: virtual ~AWOV() = 0;//纯虚函数,不会被实例化,抽象类

}


需要定义析构函数
AWOV::~AWOV(){} //定义

8. 别让异常逃离析构函数

  • 析构函数绝对不要出现异常
  • 如果对异常要处理,使用普通函数包装
    Alt text
    这里假设vector销毁时,析构出现问题(大于一个出现异常,导致不明确行为)。
    可以抛出异常处理

    Alt text
1
2
3
4
5
6
7
8
9
10
11
12
13
class DBConn{

public: ~DBConn();
private: DBConnnection db;

};

DBConn::~DBConn()
{

db.close();

}

这时如果写下面的代码
Alt text

如果调用close()失败

1
2
3
4
5
6
7
8
9
10
11
12
DBConn::~DBConn()
{

//db.close(); 处理异常
try{
db.close();
}catch(...){

Log失败的问题
}

}

无法对抛出异常做出反应,可以使用普通函数处理

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
29
class DBConn{

public: ~DBConn();
void close()
{

db.close();
closed = true;
}

private: DBConnnection db;
bool closed;
};

DBConn::~DBConn()
{

//db.close();
if(!closed)
{
try{
db.close();
}
catch(...)
{
}
}

}

9. 绝不在构造和析构函数中调用virtual函数

  • 绝不在构造和析构函数中调用virtual函数,因为不会下降到derived层
    假如
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Transaction{
public: Transaction();

virtual void LogTransacition()const =0;

};

Transaction::Transaction()
{
LogTransaction();
}

class BuyTransaction:public Transaction{
public:
virtual void LogTransacition() const;
};

class SellTransaction:public Transaction{
public:
virtual void LogTransacition() const;
};

现在

1
2
BuyTransaction b;
这时候生成的对象仿佛隶属于Transaction(c++阻止下降到derived)

使用新一层包装问题可能更难发现

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
class Transaction{
public: Transaction(){
init(); //non virtual
}

virtual void LogTransacition()const =0;
private: void init()
{
LogTransaciton(); //virtual
}
};

Transaction::Transaction()
{
LogTransaction();
}

class BuyTransaction:public Transaction{
public:
virtual void LogTransacition() const;
};

class SellTransaction:public Transaction{
public:
virtual void LogTransacition() const;
};

所以更好地是避免使用virtual

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Transaction{
public: explicit Transaction(const std::string& logInfo)

void LogTransacition(const std::string& logInfo) const;
};

Transaction::Transaction(const std::string& logInfo)
{
LogTransaction(logInfo);
}

class BuyTransaction:public Transaction{
public:
BuyTransaction(Params):Transaction(createLogString(params))
{
...
}//log传给base构造函数(向上传递弥补不能向下传递)
private:
static std::string createLogString(params);
};

10. 令operator=返回一个referfence to *this

  • 这是个习惯
1
2
3
4
5
6
7
8
class Widget
{
public: Widget();
Widget& operator=(const Widget& rhs)
{
return *this;
}
};

11. 在operator=中处理自我赋值

Alt text
例子
如果

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
class BitMap{
...
};
class Weight{
public:
private: Bitmap* pb;
}


Weight& operator=(const Weight& rhs)
{
delete pb;
pb = new Bitmap(*(rhs.pb));
return *this;
}
如果pb指向的和rhs的pb是一样的,这里报错。

最简单的方法是进行“证同测试”
Weight& operator=(const Weight& rhs)
{
if(rhs == this)
return *this;

delete pb;
pb = new Bitmap(*(rhs.pb)); //不具有异常安全性
return *this;
}

Alt text
这种做法保证了异常安全

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class BitMap{
...
};
class Weight{
public: void swap(const Weight& rhs)
private: Bitmap* pb;
}


Weight& operator=(const Weight& rhs)
{
Weight temp(rhs);
swap(temp);
return *this;
}

12. 复制对象勿忘每一个成分

Alt text
如果在自我实现copy 构造函数和COA后,添加了成员变量,注意要修改这两个函数,否则会引起局部copy(采用default构造函数初始化)。
Alt text
Alt text

1
2
3
4
5
6
7
8
9
10
PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs):Customer(rhs),priority(rhs.priority)
{

}
PriorityCustomer& PriorityCustomer::PriorityCustomer(const PrioriCustomer& rhs)
{
Customer::operator=(rhs);//base 的CAO
Priority = rhs.Priority;
return *this;
}

Alt text

资源管理

13. 以Object管理资源

Alt text

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Investment{

};
Investment* createInvestment();//Factory模式

void f()
{
Investment* iv = createInvestment();
... //如果...内有return,会提前结束
delete iv;
}
为确保正常释放,把资源放入对象,利用析构函数自动释放
或者
void f()
{
std::auto_ptr<Investment> apiv(createInvestment());//auto_ptr的析构函数会自动释放
}

Alt text
Alt text

1
2
3
std::auto_ptr<Investment> priv1(createInvestment());
std::auto_ptr<Investment> priv2(priv1)//现在priv2 指向原来的priv1,priv1为空
priv1 = priv2;

Alt text

使用shared_ptr比auto_ptr更好

Alt text
Alt text

14. 在资源管理类中小心copying行为

Alt text

Alt text

1
2
3
4
5
6
7
8
9
10
11
class Lock{
public: explicit Lock(Mutex* pm):mutexPtr(pm)
{
lock(mutexPtr);
}
~Lock()
{unlock(mutexPtr);
}
private: Mutex* mutexPtr;

};

Alt text

1
2
Lock l1(&m);
Lock l2(l1)//禁止这种行为,因为不合理。

Alt text
重新实现

1
2
3
4
5
6
7
8
9
10
11
class Lock{
public: explicit Lock(Mutex* pm):mutexPtr(pm,unlock)//这里以unlock函数作为删除器
{
lock(mutexPtr.get());
}
/* ~Lock()
{unlock(mutexPtr);
}*/析构函数不再需要声明
private: std::tr1::shared_ptr<Mutex> mutexPtr;

};

15.在资源管理类提供对原始资源的访问

Alt text

1
std::tr1::shared_ptr<Investment> pInv(createInvestment());

Alt text
Alt text
这时候有两种方法可以解决这个问题。
显式转换

1
int days = daysHeld(pInv.get());

隐式转换
Alt text
Alt text

16. 成对使用new和delete使用相同形式

Alt text

1
2
3
4
5
std::string* stringPtr1 = new std::string;
std::string* stringPtr2 = new std::string[100];
...
delete stringPtr1;
delete[] stringPtr2

Alt text
Alt text

17. 以独立语句将newed对象置入只能指针

Alt text

1
2
int priority();
void processWidget(std::shard_ptr<Widget>pw,int priority);

如果

1
2
processWidget(new Widget,priority());  //不能通过编译
processWidget(std::shared_ptr<Widget>(new Widget),priority());//能通过编译但是会出现资源泄漏

Alt text
如果以下面的顺序
Alt text
一旦priority()调用出现问题,则会引发异常。
所以

1
2
std::shard_ptr<Widget>pw (new Widget) ;
processWidget(pw,priority());

设计与声明

18. 让接口容易被正确使用

Alt text

19. 设计class犹如设计type

Alt text
Alt text
Alt text

20. 宁以pass-by-reference-to-const替代pass-by-value

继承与OO-Design

32. 确定public继承是is-a的关系

Alt text
Alt text
考虑下面的例子

1
2
3
4
5
6
7
8
void eat(const Person& person);
void study(const Student& stu);
Person person;
Student stu;
eat(person);
eat(stu);
study(person); //错误person 不是Student
study(stu);

Alt text
下面更符合事实

1
2
3
4
5
6
7
8
9
10
class Bird{

};
class FlyingBird:public Bird{

virtual void fly();
};
class Penguin:public Bird{
和会飞的鸟区分
};

Alt text
讨论下面这个例子

1
2
3
4
5
6
class Rectangle{
public: virtual void setHeight(int newHeight);
virtual void setWidth(int newWidth);
virtual int Height()const;
virtual int Width()const;
};

1
2
3
4
5
6
void makeBigger(Rectangle& r)
{
int height = r.height();
r.setWidth(r.width() + 10);
assert(r.heigth()==height)//判断高度是否发生变化
}

Alt text
Alt text

33. 避免遮掩继承来的名称

Alt text
Alt text

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Base{
private: int x;
public:
virtual void mf1()=0;
virtual void mf2();
void mf3();
};
class Derived:public Base{
public:
virtual void mf1(); //子类会遮掩父类的mf1()
void mf4();
};
void Derived::mf4()
{
mf2();
}
编译器先找Derived-->Base-->namespace-->global,在Base找到便停止查找。

Alt text
下面这个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Base{
private: int x;
public:
virtual void mf1()=0;
virtual void mf1(int);
virtual void mf2();
void mf3();
void mf3(double);
};
class Derived:public Base{
public:
virtual void mf1(); //子类会遮掩父类的mf1()
void mf3();
void mf4();
};
如果下面调用会发生什么
Derived d;
int x;

d.mf1();//ok
d.mf1(x);//error!遮掩了
d.mf2();//ok
d.mf3();//ok
d.mf3(x);//error遮掩了

为了解决这个问题,一方面可以使用using声明来解决。

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
class Base{
private: int x;
public:
virtual void mf1()=0;
virtual void mf1(int);
virtual void mf2();
void mf3();
void mf3(double);
};
class Derived:public Base{
public:
using Base::mf1; //让base class 内mf1和mf3的东西全部可见
using Base::mf3;//在Derived作用域内都可见(并且public )
virtual void mf1(); //子类会遮掩父类的mf1()
void mf3();
void mf4();
};
Derived d;
int x;

d.mf1();//ok
d.mf1(x);//ok
d.mf2();//ok
d.mf3();//ok
d.mf3(x);//ok

Alt text
利用转交函数forward function可以选择性继承Base的virtual函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Base{
private: int x;
public:
virtual void mf1()=0;
virtual void mf1(int);
virtual void mf2();
void mf3();
void mf3(double);
};
class Derived:public Base{
public:

virtual void mf1(){ //转交函数
Base::mf1();
}
void mf3();
void mf4();
};

Derived d;
int x;
d.mf1();//ok
d.mf1(x);//error

34. 区分接口继承和实现继承

Alt text

模板与泛型编程

41. 隐式接口和编译器多态

  • classes和template都支持接口和多态
  • OOP支持的是显式接口,可以找到对应代码。virtual函数实现了运行期多态,根据对象的动态类型决定调用哪一个函数。
  • template 支持的是隐式接口,例如w无论是什么类型,都需要支持doProcessing内的函数操作。而且是编译器多态,在编译的时候决定实际运行的函数。
    Alt text
    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
    29
    30
    31
    32
    class Widget{
    public:
    Widget();
    virtual ~Widget();
    virtual std::size_t size() const;
    virtual void normalize();
    void swap(Widget& other);
    };

    void doProcessing(Widget& w)
    {
    if(w.size()>10&&w!=someNastyWidget)
    {
    Widget temp(w);
    temp.normalize();
    temp.swap(w);
    }

    }

    //改写成template 接口
    template<typename T>
    void doProcessing(T& w)
    {
    if(w.size()>10&&w!=someNastyWidget)
    {
    Widget temp(w);
    temp.normalize();
    temp.swap(w);
    }

    }

42. 了解typename的双重意义

1
2
3
4
5
6
7
8
9
10
11
template<typename C> 
void print2nd(const C& container)
{
if(container.size()>2)
{
C::const_iterator iter(container.begin()); //这里的const_iterator是一个嵌套从属类型名称,local variable
++iter;
int value = *iter; //这里的int 是非从属类型名
std::cout<<value<<std::endl;
}
}