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)(按照子查询对外部查询的依赖性 ...
随机推荐
- 从web.xml入手分析jeecms配置文件
web.xml文件是web系统的核心配置文件,里面的所有配置都会加载的运行时的web容器,从她可以了解到整个web项目的配置情况.jeecms的所有配置文件都在config文件夹下面,通过web. ...
- HBase入门实例: Table中Family和Qualifier的关系与区别
Table中Family和Qualifier的关系与区别 就像用MySQL一样,我们要做的是表设计,MySQL中的表,行,列的在HBase已经有所区别了,在HBase中主要是Table和Family和 ...
- 2019-8-31-C#-程序集数量对软件启动性能的影响
title author date CreateTime categories C# 程序集数量对软件启动性能的影响 lindexi 2019-08-31 16:55:58 +0800 2018-10 ...
- id 工具: 查询用户所对应的UID 和GID 及GID所对应的用户组
id 工具是用来查询用户信息,比如用户所归属的用户组,UID 和GID等:id 用法极为简单:我们举个例子说明一下: 语法格式: id [参数] [用户名] 至于有哪些参数,自己查一下 id -- ...
- vue 如何发起网络请求 之 axios
1 1 2 3 4 5 6 7 8 9 10 // axios 请求 在main.js里边写入 import Axios from 'axios' // 配置请求信息 var $http = ...
- ASP.NET Core修改IOC为Autofac
如下是我为了了解如何更换ASP.NET Core中的IOC而查找的文章,如果大家英文OK的,可以直接前往阅读,同时也已经有简单的github例子供大家参考. 参考文章: ASP.NET Core文档: ...
- IDEA javax.servlet.http.HttpServletRequest; 不存在 解决方案
使用idea创建一个web项目,在项目中报HttpServletRequest和HttpServletResponse不存在. 问题原因:idea不会默认引用tomcat的jar包. 解决方法: [注 ...
- Java中字符串为什么不以\0结尾
Java中字符串为什么不以\0结尾 其实这个问题没有什么好说的,Java里面一切都是对象,是对象的话,字符串肯定就有长度,即然有长度,编译器就可以确定要输出的字符个数,当然也就没有必要去浪费那1字节的 ...
- request.setCharacterEncoding("utf-8");
request.setCharacterEncoding("utf-8");不加这句话时,从input控件得到值要用new String(request.getParameter( ...
- java-多线程的练习----妖,等待唤醒,代码重构,lock到condition
1 需求 资源有姓名和性别. 两个线程, 一个负责给姓名和性别赋值, 一个负责获取姓名和性别的值. 要求1,运行一下,解决程序的 "妖"的问题. 要求2,实现正确数据的 ...