二、字符串对象(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过来,后返回函数中新分配的地址。

char* getName() {
         size_t length = strlen(_name);
         char* result = malloc(length + 1);
         assert(result);
         memcpy(result,_name,length);
         result[length] = '';
         return result;
     }

  方法三:基于成员变量name的数据,返回C++标准库中的string对象。

1     string getName() {
2         return _name;  //这里_name成员变量的类型不是char*,而是string。
3     }

  首先需要肯定的是以上三种方法在一定程度上均能满足我们的需求,但也都存在各自的不足。现在我们需要针对以上三种实现逐一给出我们的剖析。

  1、直接返回内部数据的指针,这本身是一个疯狂而又极度危险的实现方式。因为对于函数调用者而言,可以随时通过返回的指针修改其所指向的数据,从而破坏了该函数所在对象的数据封装性。事实上,我们在自行编写C++代码时,也是很少这样设计和实现此类函数的。

  2、和第一种方法相比,在同样实现功能的基础上,确实避免了内部数据会被调用者修改的风险,然而这样的做法却带来了效率开销,而且还从另外一个方面破坏了该函数所在类的封装性。

  先说效率问题,很明显该方法和方法一相比多了一次内存分配和内存拷贝的操作,而此操作对性能的影响程度也需要视情况而定。至于封装性被破坏的问题其实也是非常明显的,因为返回值中的数据指针是在该函数内部被临时分配的,是需要被调用者自行释放的,因此对于调用者来说需要关注该函数的内存分配方式,如果是malloc,调用者需要使用对应的free函数来释放该内存空间,如果是new,则需要用delete []的方式来释放。一旦分配和释放内存的方式不匹配,将会导致极为严重而又难以察觉的堆内存混乱问题。直接引发的后果是程序在运行时极不稳定,随时都有崩溃的可能,而开发人员在定位此类问题时也是非常非常的困难,因为通常他们报错的方式比较随机。有经验的开发者可以通过内存检测工具来帮助他们实现问题定位,然而在他们决定使用工具之前,则需要通过大量的其他手段来分析和判断该问题可能是内存混乱所致。 可以想象,这样一个微小的失误,给程序后期的调试和维护所带来的压力是难以估量的。这里还需要额外指出的是,如果该函数(getName)和调用函数分属不同的动态库,那么对于调用者而言,即便内存释放的方式和分配时保持一致,仍然有可能导致内存混乱的问题发生。至于具体原因,我们会在后面的条目中给出更为清晰的示例和解释。