More Effective C++: 05技术(30-31)
30:Proxy classes 代理类
在C++中使用变量作为数组大小是违法的,也不允许在堆上分配多维数组:
int data[dim1][dim2];
int *data = new int[dim1][dim2]; // error!
为了弥补上述缺点,可以设计一个二维数组类:
template<class T>
class Array2D {
public:
Array2D(int dim1, int dim2);
...
}; Array2D<int> data(, );
Array2D<float> *data = new Array2D<float>(, );
void processInput(int dim1, int dim2)
{
Array2D<int> data(dim1, dim2);
...
}
因为没有operator[][]这样的操作符,为了能以data[3][6]的形式访问该二维数组,所以,这里需要使用proxy类:
template<class T>
class Array2D {
public:
Array2D(int dim1, int dim2){
m_dim1 = dim1;
m_dim2 = dim2;
data = new T[dim1*dim2];
}
~Array2D(){delete data;}
class Array1D {
public:
Array1D(T *data, int begin){
innerdata = data+begin;
}
T& operator[](int index);
const T& operator[](int index) const;
private:
T *innerdata;
};
Array1D operator[](int index);
const Array1D operator[](int index) const;
private:
T *data;
int m_dim1, m_dim2;
}; template<class T>
typename Array2D<T>::Array1D Array2D<T>::operator[](int index){
return Array2D::Array1D(data, index * m_dim2);
} template<class T>
const typename Array2D<T>::Array1D Array2D<T>::operator[](int index) const{
return Array2D::Array1D(data, index * m_dim2);
} template<class T>
T& Array2D<T>::Array1D::operator[](int index){
return innerdata[index];
} template<class T>
const T& Array2D<T>::Array1D::operator[](int index) const{
return innerdata[index];
} int dim1 = , dim2 = ;
Array2D<int> array(dim1, dim2);
array[][] = ;
array[][] = ;
array[][] = ;
array[][] = ;
每个Array1D对象表示一个一维数组,而 Array2D的用户不需要知道Array1D的存在。注意上述模板内定义嵌套类的函数定义写法。
利用proxy类,还可以实现区分operator[]读写操作。上一章讲述的引用计数的String类,其operator[]可以用来读字符,也可以用来写字符。读操作是所谓的右值引用;写操作是左值引用。虽然编译器无法告诉我们operator[]到底是用来读还是写,但是只要将所要处理的动作放缓,直到operator[]的返回结果被使用为止。可以修改operator[],使其返回字符串中字符的proxy,就可以看见该proxy如何被使用:
class String {
public:
String(const char *value = ""); class CharProxy {
public:
CharProxy(String& str, int index);
CharProxy& operator=(const CharProxy& rhs); // 左值引用
CharProxy& operator=(char c);
operator char() const; // 右值引用
private:
String& theString;
int charIndex;
};
const CharProxy operator[](int index) const;
CharProxy operator[](int index); friend class CharProxy; private:
struct StringValue: public RCObject {
char *data;
StringValue(const char *initValue);
StringValue(const StringValue& rhs);
void init(const char *initValue);
~StringValue();
}; RCPtr<StringValue> value;
};
有了上面的代码,考虑这条语句:cout<<s1[5],s1[5]产生一个CharProxy对象,但是该对象没有定义output操作符,所以编译器需要找一个合适的隐式类型转换:将CharProxy隐式转换为char,然后进行输出即可。
而对于s1[5]=’x’,s1[5]返回CharProxy,调用CharProxy::operator=函数即可,这时,被赋值的CharProxy对象被用来作为一个左值。同样的,s1[5]=s1[8]也是一样。
String的operator[]代码如下:
const String::CharProxy String::operator[](int index) const{
return CharProxy(const_cast<String&>(*this), index);
}
String::CharProxy String::operator[](int index){
return CharProxy(*this, index);
}
注意const operator[]返回一个const CharProxy,由于CharProxy::operator=不是一个 const成员函数,const CharProxy因此不能被用来做为赋值的目标物。因此不论是const operator[] 所传回的 proxy,或是该proxy所代表的字符,都不能被用来做为左值。这正是我们希望 const operator[]所具备的行为。
另外,当const operator[] 返回CharProxy对象时,需要对其进行const_cast转换,去除const属性,这是因为CharProxy的构造函数只接受non-const String。
operator[]返回的每一个CharProxy都会记住它所附属的字符串,以及它所在的索引位置,以便将来进行读取或赋值:
String::CharProxy::CharProxy(String& str, int index)
: theString(str), charIndex(index) {}
CharProxy的char转换函数如下:
String::CharProxy::operator char() const{
return theString.value->data[charIndex];
}
该函数以by value的方式返回一个字符,由于C++ 限制只能在右值情境下使用这样的返回值,所以这个转换函数只能用于右值合法之处。
接下来是CharProxy的operator=操作符:
String::CharProxy& String::CharProxy::operator=(const CharProxy& rhs){
if (theString.value->isShared()) {
theString.value = new StringValue(theString.value->data);
} theString.value->data[charIndex] = rhs.theString.value->data[rhs.charIndex];
return *this;
} String::CharProxy& String::CharProxy::operator=(char c){
if (theString.value->isShared()) {
theString.value = new StringValue(theString.value->data);
}
theString.value->data[charIndex] = c;
return *this;
}
与上一章中的non-const String::operator[]比较,会发现它们非常类似。在上一章中悲观地假设所有non-const operator[]的调用都是为了写操作,此处们把完成“写操作”的代码转移到CharProxy的operator=中,避免了non-const operator[]在右值情境下也需付出写操作的昂贵代价。
尽管我们希望proxy对象能够无间隙的代表它所表示的对象,但是这种想法很难达成,因为除了赋值之外,对象还有很多其他操作,比如:char *p = &s1[5],这条语句有两个报错,s1[5]返回一个临时CharProxy对象,既不能取临时对象的地址,也无法将CharProxy*赋值给char *,这种情况下,需要重载operator&:
const char * String::CharProxy::operator&() const{
return &(theString.value->data[charIndex]);
} char * String::CharProxy::operator&(){
if (theString.value->isShared()) {
theString.value = new StringValue(theString.value->data);
} theString.value->markUnshareable();
return &(theString.value->data[charIndex]);
}
non-const版本需要返回一个指针,指向一个可能被修改的字符,因此,需要将StringValue成为其专属副本。
这还仅仅是取地址,原始对象可能还支持+=,++等操作,这些需要左值的操作想要成功,必须一一为proxy类定义相应的操作符(CharProxy转换为char的操作符,返回的是右值,因此无法使用这些操作符)。
另外,如果proxy代表的对象具有成员函数,则为了能使proxy看起来像原始对象一样能调用相应的成员函数,也需要在proxy中定义相应的函数。
另外,proxy虽然能隐式转换为它所代表的对象,但是这种隐式转换只能发生一次,如果原始对象A能隐式转换为B,则在需要参数B的函数调用中可以使用A,但不能使用A的proxy。
31:让函数根据一个以上的对象类型来决定如何虚化
假设你在写一个游戏,游戏中需要处理宇宙飞船、太空站、小行星等的碰撞问题,不同物体之间的碰撞需要做不同的处理。于是有下面的代码:
class GameObject { ... };
class SpaceShip: public GameObject { ... };
class SpaceStation: public GameObject { ... };
class Asteroid: public GameObject { ... }; void checkForCollision(GameObject& object1, GameObject& object2)
{
if (theyJustCollided(object1, object2)) {
processCollision(object1, object2);
}
else {
...
}
}
processCollision需要根据object1和object2的具体类型做不同的处理。这就是所谓的double-dispatching问题。在面向对象程序设计社区,人们把一个“虚函数调用动作”称为一个"message dispatch"。因此某个函数调用如果根据两个参数而虚化,自然而然地就被称为 "double dispatch"。更广泛的情况则被称为multiple dispatch。
最一般化的double-dispatching实现,就是利用虚函数和RTTI:
class GameObject {
public:
virtual void collide(GameObject& otherObject) = ;
...
};
class SpaceShip: public GameObject {
public:
virtual void collide(GameObject& otherObject);
...
}; class CollisionWithUnknownObject {
public:
CollisionWithUnknownObject(GameObject& whatWeHit);
...
};
void SpaceShip::collide(GameObject& otherObject){
const type_info& objectType = typeid(otherObject);
if (objectType == typeid(SpaceShip)) {
SpaceShip& ss = static_cast<SpaceShip&>(otherObject);
process a SpaceShip-SpaceShip collision;
}
else if (objectType == typeid(SpaceStation)) {
SpaceStation& ss = static_cast<SpaceStation&>(otherObject);
process a SpaceShip-SpaceStation collision;
}
else if (objectType == typeid(Asteroid)) {
Asteroid& a = static_cast<Asteroid&>(otherObject);
process a SpaceShip-Asteroid collision;
}
else {
throw CollisionWithUnknownObject(otherObject);
}
}
上面这段代码的缺点,collide函数必须知道其每一个兄弟类(所有继承自GameObject的那些类)。如果有新的类型加入了这个游戏,就必须修改程序中每一个可能遭遇新对象的RTTI-based if-then-else链,这会造成程序难以维护。
还可以只用虚函数就解决这个问题:
class GameObject {
public:
virtual void collide(GameObject& otherObject) = ;
virtual void collide(SpaceShip& otherObject) = ;
virtual void collide(SpaceStation& otherObject) = ;
virtual void collide(Asteroid& otherobject) = ;
...
};
class SpaceShip: public GameObject {
public:
virtual void collide(GameObject& otherObject);
virtual void collide(SpaceShip& otherObject);
virtual void collide(SpaceStation& otherObject);
virtual void collide(Asteroid& otherobject);
...
};
void SpaceShip::collide(GameObject& otherObject){
otherObject.collide(*this);
}
void SpaceShip::collide(SpaceShip& otherObject){
process a SpaceShip-SpaceShip collision;
}
void SpaceShip::collide(SpaceStation& otherObject){
process a SpaceShip-SpaceStation collision;
}
void SpaceShip::collide(Asteroid& otherObject){
process a SpaceShip-Asteroid collision;
}
比较有意思的是接收GameObject&的那个collide函数,当对象的静态类型是GameObject时调用该函数,函数内部,根据参数的动态类型决定调用该类型的哪个虚函数,而函数参数为*this,也就是个SpaceShip类型。
这种做法的缺点跟上面使用RTTI的类似,每个类都必须知道其兄弟类,一旦有新的类加入,代码就必须修改。而这里的修改又与RTTI解法不同,RTTI解法中,只需要修改每个类的实现部分,也就是collide中的if-else-then,而此处需要修改类的定义,增加一个新的虚函数。然而你并不一定有机会或者权利去修改类的定义式。
简而言之,如果你需要在你的程序中实现double-dispatching,最好的方向就是修改设计,消除此项需求。如果不能,那么,虚拟函式法比RTTI 法安全一些,但是如果你对头文件的权力不够,这种作法会束缚你的系统扩充性。至于RTTI法,虽不需要重新编译,却往往导至软件难以维护。你总是得付出代价,才能获得机会。
之前说过,虚函数是通过vtbl实现的。在这个double- dispatching场景中,我们可以自己实现一个vtbl,这比RTTI-based会更有效率,而且可以将RTTI的使用集中在vtbl初始化的地方。
class GameObject {
public:
virtual void collide(GameObject& otherObject) = ;
...
}; class SpaceShip: public GameObject {
public:
virtual void collide(GameObject& otherObject);
virtual void hitSpaceShip(SpaceShip& otherObject);
virtual void hitSpaceStation(SpaceStation& otherObject);
virtual void hitAsteroid(Asteroid& otherobject);
...
private:
typedef void (SpaceShip::*HitFunctionPtr)(GameObject&);
static HitFunctionPtr lookup(const GameObject& whatWeHit);
}; void SpaceShip::collide(GameObject& otherObject){
HitFunctionPtr hfp = lookup(otherObject);
if (hfp) {
(this->*hfp)(otherObject);
}
else {
throw CollisionWithUnknownObject(otherObject);
}
} void SpaceShip::hitSpaceShip(SpaceShip& otherObject){
process a SpaceShip-SpaceShip collision;
}
void SpaceShip::hitSpaceStation(SpaceStation& otherObject){
process a SpaceShip-SpaceStation collision;
}
void SpaceShip::hitAsteroid(Asteroid& otherObject){
process a SpaceShip-Asteroid collision;
}
这里没有将collide重载,而是用函数名区分不同对象之间的碰撞,原因稍后就会解释。
在SpaceShip::collide函数内,调用lookup函数,根据GameObject参数查找合适的成员函数指针,一旦找到则调用即可,找不到的时候抛出异常。
现在考虑lookup函数的实现,在lookup中,需要一个关系型数组,lookup查找该数组,根据对象类型得到某个成员函数指针。这个关系型数组应该在使用之前就产生并初始化了,并在不再需要时进行销毁,可以使用new和delete来产生和销毁数组,但那样容易发生错误。为了保证数组会在使用之前进行初始化,比较好的解决办法就是让关系型数组成为 lookup内的static对象,只有在lookup第一次调用时它才会被产生,而在main结束之后它才会被摧毁:
class SpaceShip: public GameObject {
private:
typedef map<string, HitFunctionPtr> HitMap;
...
}; SpaceShip::HitFunctionPtr SpaceShip::lookup(const GameObject& whatWeHit){
static HitMap collisionMap; //稍后我们将看到如何初始化这玩意儿。 HitMap::iterator mapEntry = collisionMap.find(typeid(whatWeHit).name());
if (mapEntry == collisionMap.end()) return ;
return (*mapEntry).second;
}
这里根据typeid(whatWeHit).name()查找collisionMap,但是标准并未明确规定type_info::name的返回值,不同的编译器可能会有不同的行为,比如对于SpaceShip类,有的编译器可能就会返回"class SpaceShip"。一个更好的设计是,以type_info对象的地址来识别class,因为它绝对是独一无二的,此时HitMap类型应该是map<const type_info*, HitFunctionPtr>。
现在,唯一的问题就是collisionMap的初始化问题了,可以将其初始化放进一个名为initializeCollisionMap的private static成员函数中:
SpaceShip::HitFunctionPtr SpaceShip::lookup(const GameObject& whatWeHit)
{
static HitMap collisionMap = initializeCollisionMap();
...
}
但是这种方法会有map的复制成本,因此,这里最高改为指针,为了不操心delete指针的问题,可以使用智能指针:
SpaceShip::HitFunctionPtr SpaceShip::lookup(const GameObject& whatWeHit){
static unique_ptr<HitMap> collisionMap(initializeCollisionMap());
...
} SpaceShip::HitMap * SpaceShip::initializeCollisionMap(){
HitMap *phm = new HitMap;
(*phm)["SpaceShip"] = &hitSpaceShip;
(*phm)["SpaceStation"] = &hitSpaceStation;
(*phm)["Asteroid"] = &hitAsteroid;
return phm;
}
这个初始化代码还有最后一个问题,map中的value类型是:typedef void (SpaceShip::*HitFunctionPtr)(GameObject&),这种函数的参数为GameObject,但是hitSpaceShip、hitSpaceStation、hitAsteroid它们的参数分别为 SpaceShip、SpaceStation、Asteroid。虽然它们都可以隐式转换为GameObject,但是函数指针之前却不存在这种转换。
你可能觉得下面的方法可以解决这个问题:
// 一个坏主意…
SpaceShip::HitMap * SpaceShip::initializeCollisionMap()
{
HitMap *phm = new HitMap;
(*phm)["SpaceShip"] = reinterpret_cast<HitFunctionPtr>(&hitSpaceShip);
(*phm)["SpaceStation"] = reinterpret_cast<HitFunctionPtr>(&hitSpaceStation);
(*phm)["Asteroid"] = reinterpret_cast<HitFunctionPtr>(&hitAsteroid);
return phm;
}
这是个坏主意,因为它欺骗了编译器,一旦满足某种条件,编译器就会对这种欺骗进行报复:如果SpaceStation、SpaceShip或Asteroid有GameObject之外的其他基类,你可能会发现,你在collide中对碰撞处理函式的呼叫,会导至相当粗鲁的行为。比如下面是一个具有菱形继承的类D:
D对象内的四个“基类成份”,每一个都有不同地址。虽然指针和引用的行为不同,但编译器通常是以指针来实现引用的。因此,当对象拥有多个基类,并以引用的方式传递给函数时,编译器是否传递了正确的地址(此地址对应于被调用函数的参数类型),将是非常重要的关键。
如果你欺骗编译器,告诉它你的函数期望获得一个GameObject,而其实它真正期望获得的是个SpaceShip,当你调用那个函数,编译器就会传递错误的地址,导至执行时期可怕的大屠杀。这种问题很难找出原因。转型令人沮丧,原因有许多个,这是其中之一。
因此,只能改变hitSpaceShip这些成员函数的原型,使他们接受GameObject对象:
class SpaceShip: public GameObject {
public:
virtual void collide(GameObject& otherObject);
virtual void hitSpaceShip(GameObject& spaceShip);
virtual void hitSpaceStation(GameObject& spaceStation);
virtual void hitAsteroid(GameObject& asteroid);
...
};
这就是为什么没有重载collide函数的原因。因为参数都一样了。
下面是剩下的代码:
SpaceShip::HitMap * SpaceShip::initializeCollisionMap(){
HitMap *phm = new HitMap;
(*phm)["SpaceShip"] = &hitSpaceShip;
(*phm)["SpaceStation"] = &hitSpaceStation;
(*phm)["Asteroid"] = &hitAsteroid;
return phm;
} void SpaceShip::hitSpaceShip(GameObject& spaceShip){
SpaceShip& otherShip=dynamic_cast<SpaceShip&>(spaceShip);
process a SpaceShip-SpaceShip collision;
} void SpaceShip::hitSpaceStation(GameObject& spaceStation){
SpaceStation& station=dynamic_cast<SpaceStation&>(spaceStation);
process a SpaceShip-SpaceStation collision;
} void SpaceShip::hitAsteroid(GameObject& asteroid){
Asteroid& theAsteroid = dynamic_cast<Asteroid&>(asteroid);
process a SpaceShip-Asteroid collision;
}
在hitSpaceShip这样的函数中,需要将参数对象使用dynamic_cast强制转换为真正的类型。
实际上,上面这种自行实现vtbl的解法依然有类似的问题,因为针对每一个兄弟类,都需要有一个成员函数处理碰撞。一旦有新的GameObject类型加入到这个游戏中,还是需要修改类的定义。
如果关系型数组内含的指针指向的是non-member functions,重新编译的问题便可消除:
#include "SpaceShip.h"
#include "SpaceStation.h"
#include "Asteroid.h"
namespace {
void shipAsteroid(GameObject& spaceShip, GameObject& asteroid);
void shipStation(GameObject& spaceShip, GameObject& spaceStation);
void asteroidStation(GameObject& asteroid, GameObject& spaceStation); void asteroidShip(GameObject& asteroid, GameObject& spaceShip)
{ shipAsteroid(spaceShip, asteroid); }
void stationShip(GameObject& spaceStation, GameObject& spaceShip)
{ shipStation(spaceShip, spaceStation); }
void stationAsteroid(GameObject& spaceStation, GameObject& asteroid)
{ asteroidStation(asteroid, spaceStation); } typedef void (*HitFunctionPtr)(GameObject&, GameObject&);
typedef map< pair<string,string>, HitFunctionPtr > HitMap;
pair<string,string> makeStringPair(const char *s1, const char *s2);
HitMap * initializeCollisionMap();
HitFunctionPtr lookup(const string& class1, const string& class2);
} void processCollision(GameObject& object1, GameObject& object2){
HitFunctionPtr phf = lookup(typeid(object1).name(), typeid(object2).name());
if (phf) phf(object1, object2);
else throw UnknownCollision(object1, object2);
}
这里使用了匿名namespace,匿名namespace内的所有东西对其所在的编译单元而言都是私有的,也就是说,其效果好像是在文件中将函数声明为static一样,更推荐使用匿名namespace。
剩下的代码如下:
namespace {
pair<string,string> makeStringPair(const char *s1, const char *s2)
{ return pair<string,string>(s1, s2); }
} namespace {
HitMap * initializeCollisionMap() {
HitMap *phm = new HitMap;
(*phm)[makeStringPair("SpaceShip","Asteroid")] = &shipAsteroid;
(*phm)[makeStringPair("SpaceShip", "SpaceStation")] = &shipStation;
...
return phm;
}
} namespace {
HitFunctionPtr lookup(const string& class1, const string& class2) {
static unique_ptr<HitMap> collisionMap(initializeCollisionMap()); HitMap::iterator mapEntry = collisionMap->find(make_pair(class1, class2));
if (mapEntry == collisionMap->end()) return ;
return (*mapEntry).second;
}
}
现在,一旦有新的GameObject子类加进这个继承体系中,原有的类不再需要重新编译,也不需维护纠葛混乱的“以 RTTI 为基础的”switch 或 if-then-else。如果新的类加入到GameObject的继承体系中,只要其本身定义良好;我们的系统只需在initializeCollisionMap 内调整代码,并在“与processCollision相应的那个匿名namespace”内增加新碰撞处理函数。
还有一个问题,如果需要满足inheritance-based类型转换,比如现在宇宙飞船需要区分商业宇宙飞船和军事宇宙飞船:SpaceShip现在派生出了CommercialShip和MilitaryShip,而他们与原有对象的碰撞处理与SpaceShip完全相同,因此如果有一个MilitaryShip和一个Asteroid碰撞,我们希望调用的是void shipAsteroid(GameObject& spaceShip, GameObject& asteroid)。但是就目前的代码而言,这种情况下实际上会抛出一个UnknownCollision异常,因为lookup中会根据”MilitaryShip”和”Asteroid”寻找对应的函数,而map中没有这样的函数。
没有什么简单的办法解决这个问题,如果需要实现double-dispatching,而且需要支持 inheritance-based 参数转换,只能使用最早介绍的“双虚拟函数调用”机制。
以上的代码中,collisionMap是静态的,一旦初始化便不再改动,如果需要动态处理,也就是能增加、删除、修改collisionMap中内容,则需要重新设计一个CollisionMap类:
class CollisionMap {
public:
typedef void (*HitFunctionPtr)(GameObject&, GameObject&);
void addEntry(const string& type1, const string& type2,
HitFunctionPtr collisionFunction, bool symmetric = true); void removeEntry(const string& type1, const string& type2);
HitFunctionPtr lookup(const string& type1, const string& type2); static CollisionMap& theCollisionMap();
private:
CollisionMap();
CollisionMap(const CollisionMap&);
}; void shipAsteroid(GameObject& spaceShip, GameObject& asteroid);
CollisionMap::theCollisionMap().addEntry("SpaceShip", "Asteroid", &shipAsteroid); void shipStation(GameObject& spaceShip, GameObject& spaceStation);
CollisionMap::theCollisionMap().addEntry("SpaceShip", "SpaceStation", &shipStation); void asteroidStation(GameObject& asteroid, GameObject& spaceStation);
CollisionMap::theCollisionMap().addEntry("Asteroid", "SpaceStation", &asteroidStation);
...
addEntry的symmetric参数,主要是为了能对称处理条目而设,也就是增加<T1,T2>时也会增加<T2,T1>。
为了确保这些map条目在其对应的任何撞击发生之前就被加入map,可以使用RegisterCollisionFunction类:
class RegisterCollisionFunction {
public:
RegisterCollisionFunction(const string& type1, const string& type2,
CollisionMap::HitFunctionPtr collisionFunction, bool symmetric = true)
{
CollisionMap::theCollisionMap().addEntry(type1, type2,
collisionFunction, symmetric);
}
}; RegisterCollisionFunction cf1("SpaceShip", "Asteroid", &shipAsteroid);
RegisterCollisionFunction cf2("SpaceShip", "SpaceStation", &shipStation);
RegisterCollisionFunction cf3("Asteroid", "SpaceStation", &asteroidStation);
...
int main(int argc, char * argv[])
{
...
}
使用全局对象,保证在main函数之前,就已经将所需的条目加入到了map中。
稍后如果有新的派生类加入,无需改动原有代码,只需新增代码:
class Satellite: public GameObject { ... }; void satelliteShip(GameObject& satellite, GameObject& spaceShip);
void satelliteAsteroid(GameObject& satellite, GameObject& asteroid); RegisterCollisionFunction cf4("Satellite", "SpaceShip", &satelliteShip);
RegisterCollisionFunction cf5("Satellite", "Asteroid", &satelliteAsteroid);
More Effective C++: 05技术(30-31)的更多相关文章
- More Effective C++: 05技术(29)
29:引用计数 本章首先实现一个带引用计数String,然后逐步优化,介绍引用计数的常规实现. 实现引用计数的String,首先需要考虑:引用计数在哪存储.这个地方不能在String对象内部,因为需要 ...
- More Effective C++: 05技术(25-28)
25:将constructor 和 non-member functions 虚化 所谓 virtual constructor是某种函数,视其输入可产生不同类型的对象.比如下面的代码: class ...
- Effective Java 第三版——31.使用限定通配符来增加API的灵活性
Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...
- Effective C++: 05实现
26:尽可能延后变量定义式的出现时间 1:只要你定义了一个变量而其类型带有一个构造函数或析构函数,那么当程序的控制流到达这个变量定义式时,你便得承受构造成本:当这个变量离开其作用域时,你便得承受析构成 ...
- 1、关于Boolean(2015年05月30日)
背景:刚在看Effective Java,看到一段关于Boolean提供一个返回实例的静态方法的例子,便去看了下Boolean的源码,发现有些内容是之前没注意到的,于是便有了下面这些. 1. Bool ...
- none 和 host 网络的适用场景 - 每天5分钟玩转 Docker 容器技术(31)
本章开始讨论 Docker 网络. 我们会首先学习 Docker 提供的几种原生网络,以及如何创建自定义网络.然后探讨容器之间如何通信,以及容器与外界如何交互. Docker 网络从覆盖范围可分为单个 ...
- Effective C++ 笔记:条款 31 将编译关系降至最低
31 : Minimize compilation dependencies between files 1 这关乎C++的类(或说都是类惹的祸) 1.1 C++类定义式的问题 C++类定义式不只叙述 ...
- 2018年12月30&31日
小结:昨天由于做的题目比较少,所以就和今天写在一块了,昨天学习了差分约束和树上差分,当然树上差分是用线段树来维护的,今天重点整理了博客\(233\),然后做了几个题. 一. 完成的题目: 洛谷P327 ...
- 05 技术内幕 T-SQL 查询读书笔记(第四章)
第四章 子查询:在外部查询内嵌套的内部查询(按照期望值的数量分为,标量子查询 scalar subqueries,多值子查询multivalued subqueries)(按照子查询对外部查询的依赖性 ...
随机推荐
- Spring AOP(二)--注解方式
本文介绍通过注解@AspectJ实现Spring AOP,这里要重点说明一下这种方式实现时所需的包,因为Aspect是第三方提供的,不包含在spring中,所以不能只导入spring-aop的包,为了 ...
- Django项目: 6.新闻详情页
一.功能需求分析 1.功能 新闻详情 加载评论功能 添加评论功能 二.新闻详情页 1.业务流程分析 业务流程: 判断前端传递新闻id是否为空,是否为整数,是否存在 2.接口设计 接口说明: 类目 说明 ...
- Hibernate-HQL-Criteria-查询优化
1 查询总结 oid查询-get 对象属性导航查询 HQL Criteria 原生SQL 2 查询-HQL语法 2.1 基础语法 2.2 进阶语法 排序 条件 分页 聚合 投影 多表查询 SQL HQ ...
- Leetcode448.Find All Numbers Disappeared in an Array找到所有数组中消失的数字
给定一个范围在 1 ≤ a[i] ≤ n ( n = 数组大小 ) 的 整型数组,数组中的元素一些出现了两次,另一些只出现一次. 找到所有在 [1, n] 范围之间没有出现在数组中的数字. 您能在不 ...
- 设置mysql二进制日志过期时间
((none)) > show variables like 'expire_logs_days'; +------------------+-------+ | Variable_name | ...
- 一些js面试高频知识点的总结
第一部分:Object Prototypes (对象原型) (1)定义一个方法,要求传入一个string类型的参数,然后将string的每个字符间加个空格返回,例如: spacify('hello w ...
- Linux/UNIX之信号(1)
版权声明:本文为博主原创文章,未经博主同意不得转载. https://blog.csdn.net/walkerkalr/article/details/24462723 信号(1) 信号是软件中断.每 ...
- jenkins自动部署
最近在使用公司的jenkins进行自动部署项目,由于之前没有用过,一直半生不熟,因此特意写个随机记录. 1.登录jenkins服务 jenkins安装好后,我们通过浏览器访问它的主页(如下),输入用户 ...
- 模板与泛型编程 c++ primer ch16.1
在摸板定义中,模板参数列表不能为空, 编译器用推断出的参数来进行 实例化(instantiation) 一般来说 模板是有type parameter 但是也可以声明 nontype paramete ...
- ML面试1000题系列(21-30)
本文总结ML面试常见的问题集 转载来源:https://blog.csdn.net/v_july_v/article/details/78121924 21.请简要说说EM算法. @tornadome ...