移植Java代码到C++的技巧二
二、字符串对象(string vs String):
在Java的代码中,我们经常会看到这样一种写法,相信几乎每一个Java程序员都有过这样的代码,因此他看上去非常熟悉,甚至还带有一点儿亲切感。
1 public String getName() {
2 return name;
3 }
这样的代码在Java中确实司空见惯,也无可厚非,因此对于我们来说没有太多可以讨论的空间,除非你非常希望了解JVM中对象常量池的概念,然而它并不是我们这个条目中将要讨论的主题。
那么现在让我们来看一下在C++中又是如何处理此类问题的,下面将列举出三种最常用的实现方式。
方法一: 直接返回内部name成员的指针。
1 const char* getName() {
2 return _name; //_name变量的类型为char*
3 }
方法二: 基于成员变量name的数据,重新分配相同长度的内存空间,之后再将name中的数据copy过来,最后返回函数中新分配的地址。
1 char* getName() {
2 size_t length = strlen(_name);
3 char* result = malloc(length + 1);
4 assert(result);
5 memcpy(result,_name,length);
6 result[length] = '\0';
7 return result;
8 }
方法三: 基于成员变量name的数据,返回C++标准库中的string对象。
1 string getName() {
2 return _name; //这里_name成员变量的类型不是char*,而是string。
3 }
首先需要肯定的是以上三种方法在一定程度上均能满足我们的需求,但也都存在各自的不足。现在我们需要针对以上三种实现逐一给出我们的剖析。
1. 直接返回内部数据的指针,这本身就是一个疯狂而又极度危险的实现方式。因为对于函数调用者而言,可以随时通过返回的指针修改其所指向的数据,从而破坏了该函数所在对象的数据封装性。事实上,我们在自行编写C++代码时,也是很少这样设计和实现此类函数的。
2. 和第一种方法相比,在同样实现功能的基础上,确实避免了内部数据会被调用者修改的风险,然而这样的做法却带来了效率开销,而且还从另外一个方面破坏了该函数所在类的封装性。
先说效率问题,很明显该方法和方法一相比多了一次内存分配和内存拷贝的操作,而此操作对性能的影响程度也需要视情况而定。至于封装性被破坏的问题其实也是非常明显的,因为返回值中的数据指针是在该函数内部被临时分配的,是需要被调用者自行释放的,因此对于调用者来说就需要关注该函数的内存分配方式,如果是malloc,调用者就需要使用对应的free函数来释放该内存空间,如果是new,则需要用delete []的方式来释放。一旦分配和释放内存的方式不匹配,将会导致极为严重而又难以察觉的堆内存混乱问题。直接引发的后果就是程序在运行时极不稳定,随时都有崩溃的可能,而开发人员在定位此类问题时也是非常非常的困难,因为通常他们报错的方式比较随机。有经验的开发者可以通过内存检测工具来帮助他们实现问题定位,然而在他们决定使用工具之前,则需要通过大量的其他手段来分析和判断该问题可能是内存混乱所致。 可以想象,这样一个微小的失误,给程序后期的调试和维护所带来的压力是难以估量的。这里还需要额外指出的是,如果该函数(getName)和调用函数分属不同的动态库,那么对于调用者而言,即便内存释放的方式和分配时保持一致,仍然有可能导致内存混乱的问题发生。至于具体原因,我们会在后面的条目中给出更为清晰的示例和解释。
3. 和第一种方法相比,该方式也存在着同样的效率问题。
由于该函数返回的是string对象本身,而不是对象的指针或引用,因此在函数返回时,将产生一次拷贝构造,用来生成并初始化返回的string对象。在该拷贝构造中,内存重新分配和内存数据拷贝等指令均将会被执行。然而需要指出的是,和前两种方法相比,该方法避免了对象封装性被破坏和可能造成的堆内存混乱等问题的发生,从而更好的保证了程序在运行时的稳定性。
需要另行指出的是,在实际开发中的绝大多数情况下,调用者针对此类函数返回的字符串都仅仅是执行读操作,如数据比较、数据打印或直接插入数据库等。只有在极个别的时候才会去主动修改它,因此额外的内存分配和拷贝操作所带来的开销往往是另人沮丧的。
在经过对上述三种方法的深入分析之后我们得出结论,为了更好和更彻底的解决字符串对象相关代码的移植问题,我们将不得不另辟蹊径,以找出运行效率较高、代码移植成本最低的方式。下面将给出我在实际移植Java代码到C++代码的过程中所采用的方法,见如下代码片段:
1 String getName() {
2 return _name; //这里_name成员变量的类型不是char*,而是String。
3 }
怎么会和Java的代码一样呢?是的,你没有看错,就是和Java的代码一样,只是这里的String被我偷梁换柱了。
由于在移植过程中会经常遇到类似这样的代码,如果采用上述方法之一,其结果就是后面的代码移植工作将会变得异常艰难。因为这些方法有的折损了C++语言本身在性能上的优势,有的则使本来就不够安全的C++代码(相比于Java)变得更加不安全,简直就是雪上加霜。然而这仅仅是开始,也是整个移植过程中针对字符串对象所遇到的问题中的一小部分,更多的麻烦则源于对字符串对象的操作和使用上。
众所周知,Java中的String类型提供了大量的公有方法用于操作字符串对象,其功能无论是在丰富程度还是易用性方面均远远好于C++标准模板库中提供的string类型。在性能方面,由于JVM为String等类型的对象提供了常量池机制,因此在以上Java代码中,将不会产生任何额外的内存数据拷贝指令。
为了解决上述诸多问题,我决定参照Java中String类型提供的常用功能,为我的C++程序重新实现一个与之对应String类型,这样不仅在性能上可以得到有效的保证,更重要的是在整个移植过程中,所有和字符串操作相关的代码移植将变得更加容易。我认为这应该是一个一劳永逸的选择了,因为今后不管是移植其他Java代码,还是直接用C++实现新的功能,该String类型都将会有更多的用武之地。见如下代码声明:
1 class String
2 {
3 public:
4 String();
5 String(const String& other);
6 String(const char* otherText);
7 String(const char otherChar);
8 String(const char* otherText, const int count);
9 String(const char* otherText, const int offset, const int count);
10 explicit String(const int value);
11 explicit String(const int64 value);
12 explicit String(const double value);
13 ~String();
14
15 public:
16 const String& operator= (const String& other);
17 const String& operator= (const char* otherText);
18 const String& operator= (const char otherChar);
19 const String& operator+= (const String& other);
20 const String& operator+= (const char* otherText);
21 const String& operator+= (const char otherChar);
22 const String operator+ (const String& other) const;
23 const String operator+ (const char* otherText) const;
24 const String operator+ (const char otherChar) const;
25 String& operator<< (const String& other);
26 String& operator<< (const char* otherText);
27 String& operator<< (const char otherChar);
28 String& operator<< (const int otherDecimal);
29 String& operator<< (const int64 otherDecimal);
30 String& operator<< (const float otherDecimal);
31 String& operator<< (const double otherDecimal);
32 bool operator== (const String& other) const;
33 bool operator== (const char* otherText) const;
34 bool operator== (const char otherChar) const;
35 bool operator!= (const String& other) const;
36 bool operator!= (const char* otherText) const;
37 bool operator!= (const char otherChar) const;
38 bool operator> (const String& other) const;
39 bool operator> (const char* otherText) const;
40 bool operator> (const char otherChar) const;
41 bool operator>= (const String& other) const;
42 bool operator>= (const char* otherText) const;
43 bool operator>= (const char otherChar) const;
44 bool operator< (const String& other) const;
45 bool operator< (const char* otherText) const;
46 bool operator< (const char otherChar) const;
47 bool operator<= (const String& other) const;
48 bool operator<= (const char* otherText) const;
49 bool operator<= (const char otherChar) const;
50 const char operator[] (const int index) const;
51 operator const char*() const;
52
53 public:
54 void append(const char* otherText,int count = -1);
55 bool isEmpty() const;
56 size_t length() const;
57 size_t capacity() const;
58 bool equals(const char* otherText,const size_t count,bool ignoreCase) const;
59 bool equalsIgnoreCase(const String& other) const;
60 bool equalsIgnoreCase(const char* otherText) const;
61 bool equalsIgnoreCase(const char otherChar) const;
62 int compareIgnoreCase(const String& other) const;
63 int compareIgnoreCase(const char* otherText) const;
64 int compareIgnoreCase(const char otherChar) const;
65 bool startsWith(const char* otherText, bool ignoreCase) const;
66 bool startsWith(const char otherChar, bool ignoreCase) const;
67 bool endsWith(const char* otherText, bool ignoreCase) const;
68 bool endsWith(const char otherChar, bool ignoreCase) const;
69 String toUpperCase() const;
70 String toLowerCase() const;
71 const String substring(const int startPos, const int count) const;
72 const String substring(const int startPos) const;
73 bool contains(const char* childText, bool ignoreCase) const;
74 bool contains(const char childChar, bool ignoreCase) const;
75 bool containsAnyOf(const char* childText) const;
76 int indexOf(const char* childText, bool ignoreCase) const;
77 int indexOf(const char childChar, bool ignoreCase) const;
78 int indexOf(const int startPos,const char* childText, bool ignoreCase) const;
79 int indexOf(const int startPos,const char childChar, bool ignoreCase) const;
80 int indexAnyOf(const char* childText, bool ignoreCase) const;
81 int indexAnyOf(const int startPos, const char* childText, bool ignoreCase) const;
82 int lastIndexOf(const char* childText, bool ignoreCase) const;
83 int lastIndexOf(const char childChar, bool ignoreCase) const;
84 int lastIndexAnyOf(const char* childText, bool ignoreCase) const;
85 String trim() const;
86 String ltrim() const;
87 String rtrim() const;
88 String insert(const int pos,const char newChar) const;
89 String insert(const int pos,const char* newText) const;
90 String replace(int startPos,int count,const char* newText,const int textCount);
91 int toIntValue(bool* ok = 0) const;
92 int64 toInt64Value(bool* ok = 0) const;
93 double toDoubleValue(bool* ok = 0) const;
94 bool toBoolValue() const;
95 const char* text() const;
96
97 private:
98 const char* _buffer;
99 size_t _dataLength;
100 }
从以上声明中可以看出,我们为String类型提供了大量且实用的接口方法,其中有相当一部分借鉴于Java中的String类型。相比于C++标准模板库中提供的string类型,新实现的String类型有以下几点优势:
1. 提供了更完整的构造函数重载方法,以使该类型的对象与其他类型,特别是和原始数据类型之间的交互更为便利。
2. 充分利用了C++中提供的操作符重载机制,使该类型的对象在使用上和原始数据类型更为贴近。
3. Java中String类型的常用方法在该类中均能找到与之对应的方法。
仅从上述第三点看,在整个代码移植过程中,由于几乎所有和字符串操作相关的功能在我们新实现的String类型中均能找到匹配的方法,信不信由你,这一点至关重要,因为由此而提升的代码复用程度可以大大缩短我们的移植周期,同时也降低了代码出错的几率。
现在让我们重新回到getName()函数的效率问题上,就目前而言,新的String类型在效率方面和STL中的string类型几乎是一样的,都同样会有对象拷贝动作的发生,同时也同样避免了getName()函数所在类的封装性被破坏的问题。简而言之,目前的实现方式和方法三相比存在着同样的优势和劣势,所以我们现在需要做的就是如何消除额外的内存重新分配和内存数据拷贝等操作。
为了解决这一棘手问题,我们将不得不在新的String类型中应用C++中比较常用的对象资源管理机制----引用计数,下面我们还是先看一下修订后的String类型的代码声明,之后再给出详细解释。
1 class String
2 {
3 public:
4 String();
5 String(const String& other);
6 String(const char* otherText);
7 ... ... //省略的构造函数重载方法和上面的声明相同
8 ~String();
9
10 public:
11 const String& operator= (const String& other);
12 const String& operator= (const char* otherText);
13 const String& operator= (const char otherChar);
14 ... ... //省略的操作符重载方法和上面的声明相同
15
16
17 public:
18 void append(const char* otherText,int count = -1);
19 bool isEmpty() const;
20 size_t length() const;
21 ... ... //省略的共有方法和上面的声明相同
22
23
24 private:
25 class InnerRefCountedString : public RefCountedBase
26 {
27 public:
28 InnerRefCountedString(const size_t count)
29 //(count+2)是为了保证count为1的时候,不会致使_containSize为0
30 :_containSize(calculateContainSize(count)),_count(count) {
31 _text = new char[_containSize + 1];
32 assert(_text);
33 }
34
35 virtual ~InnerRefCountedString() {
36 delete [] _text;
37 }
38
39 void copyData(const char* otherText, const size_t count) {
40 assert(0 != otherText && count <= _containSize);
41 memcpy(_text,otherText,count);
42 _count = count;
43 _text[_count] = 0;
44 }
45
46 void appendData(const char* appendText, const size_t appendCount) {
47 assert(0 != appendText && appendCount + _count <= _containSize);
48 memcpy(_text + _count,appendText,appendCount);
49 _count += appendCount;
50 _text[_count] = 0;
51 }
52
53 char* getText () const {
54 return _text;
55 }
56
57 static size_t calculateContainSize(const size_t s) {
58 return ((s + 2) >> 1) * 3;
59 }
60 ... ... //省略了该类的部分接口方法。
61 private:
62 size_t _containSize;
63 size_t _count;
64 char* _text;
65 };
66
67 private:
68 typedef RefCountedPtr<InnerRefCountedString> SmartString;
69 SmartString _smartText; //托管内部字符串的对象
70 };
从上面的代码片段我们可以看出,之前的const char* _buffer内部成员变量已经被一个带有引用计数功能的对象取代了。由于为该String类型提供了引用计数机制,因此在getName()函数返回时将仅仅执行对象本身的copy,而其实际包含的缓冲区中的数据将不会发生任何copy的操作,因此这两个对象将在底层共用同一个内部缓冲区地址,直到有任何一个对象发生了数据修改操作,在那时,String类型的数据修改方法将会为要修改的对象重新分配内存,并将原有数据拷贝到该新分配的内存地址中,之后再在新地址上完成的数据修改操作,从而避免了对共享底层数据的其它String对象的数据污染。
前面已经提及过,在绝大多数的应用中,调用者对返回的String对象仅仅是执行读取操作,在此场景下,引用计数机制的引进确实使额外的内存分配和数据拷贝等操作得以避免或延迟发生。事实上,在Java中,如果也遇到同样的修改操作,类似的数据拷贝等动作也将无法避免。最后我们给出引用计数类型的代码声明,至于其具体说明,我们将会在之后的专门条目中予以更多的介绍。
1 class RefCountedBase
2 {
3 public:
4 void addRef() {
5 //为了保证引用计数器变量在多线程情况下仍然正常工作,
6 //这里需要添加原子自增锁。
7 ATOMIC_INC(_refCount);
8 }
9 void release() {
10 //为了保证引用计数器变量在多线程情况下仍然正常工作,
11 //这里需要添加原子自减锁。
12 if (0 == ATOMIC_DEC_AND_RETURN(_refCount))
13 delete this;
14 }
15 uint64 getRefCount() const {
16 return _refCount;
17 }
18
19 protected:
20 RefCountedBase():_refCount(0) {
21 }
22 virtual ~RefCountedBase() {
23 }
24
25 private:
26 uint64 _refCount; //当前对象的引用计数值。
27 };
28
29 template<typename _TRefCountedObject>
30 class RefCountedPtr
31 {
32 public:
33 RefCountedPtr():_refObject(0) {
34 }
35 RefCountedPtr(const RefCountedPtr<_TRefCountedObject>& otherPtr)
36 :_refObject(otherPtr._refObject) {
37 if (NULL != _refObject)
38 _refObject->addRef();
39 }
40 RefCountedPtr(_TRefCountedObject* const otherObjectPtr)
41 :_refObject(otherObjectPtr) {
42 if (NULL != _refObject)
43 _refObject->addRef();
44 }
45 ~RefCountedPtr() {
46 if (NULL != _refObject)
47 _refObject->release();
48 }
49 const RefCountedPtr<_TRefCountedObject>& operator=
50 (const RefCountedPtr<_TRefCountedObject>& otherPtr) {
51 if (_refObject != otherPtr._refObject) {
52 _TRefCountedObject* oldPtr = _refObject;
53 _refObject = otherPtr._refObject;
54 //在新的对象上面增一
55 if (NULL != _refObject)
56 _refObject->addRef();
57 //在原有的对象上减一
58 if (NULL != oldPtr)
59 oldPtr->release();
60 }
61 return *this;
62
63 }
64 const RefCountedPtr<_TRefCountedObject>& operator=
65 (_TRefCountedObject* const otherObjectPtr) {
66 if (_refObject != otherObjectPtr) {
67 _TRefCountedObject* oldPtr = _refObject;
68 _refObject = otherObjectPtr;
69 //在新的对象上面增一
70 if (NULL != _refObject)
71 _refObject->addRef();
72 //在原有的对象上减一
73 if (NULL != oldPtr)
74 oldPtr->release();
75 }
76 return *this;
77 }
78 operator _TRefCountedObject* () const {
79 return _refObject;
80 }
81 bool operator== (_TRefCountedObject* const otherObjectPtr) const {
82 return _refObject == otherObjectPtr;
83 }
84 bool operator!= (_TRefCountedObject* const otherObjectPtr) const {
85 return !operator==(otherObjectPtr);
86 }
87 _TRefCountedObject* operator-> () const {
88 return _refObject;
89 }
90
91 private:
92 _TRefCountedObject* _refObject; //内部托管对象。
93 };