C++重要议题

C++重要议题

本文讨论c++中的pointer、reference、cast、array、constructor。他们虽然简单,但却有非常重要作用,而且很容易被误用。本文给出使用他们的一些重要意见。

1. 指针和引用的区别

指针:

  • 使用(*和->)
  • 可以指向空值。
  • 不一定要被初始化
  • 可以改变指向对象。

引用:

  • 使用(.)
  • 不能指向空值。
  • 一定要被初始化。
  • 不能改变指向对象。

因为他们的特性,可能会出现这样的代码:

char *pc = 0;
char& rc = *pc;

这是非常危险的代码。不知道会导致不可预计的情况。应当避免!

因为引用一定会指向对象,所以我们就可以省去测试合法性。

void printDouble(const double& rd) {
    cout << rd << endl;
}
// 但指针总是要被测试的。
void printDouble(const double *pd) {
    if (pd) {
        cout << *pd << endl;
    }
}

所以在以下情况下应该使用指针,

  • 考虑到存在不指向任何对象的可能性。(指针为空)
  • 需要在不同时间指向不同的对象。

而在这些情况下应该使用引用:

  • 指向一个对象并且不再改变。
  • 重载某个操作符时。(如下标操作符)

2. 尽量使用C++风格的类型转换

虽然C语言的转型操作已经非常方便,但却有非常大的局限。原因在于C的转型本来就是为C准备的,在C++中不那么高效也是理所当然的。

  • static_cast(expression)
    静态类型转换,和C转型差不多。
  • const_cast(expression)
    改变const属性,在C中没有。
  • dynamic_cast(expression)
    可以安全地沿着类的继承关系向下进行类型转换。如果转换失败会变成空指针或者抛出异常(当对引用进行类型转换时)。
  • reinterpret_cast(expression)
    最普通的用法就是在函数指针之间进行转换。
    例如:
typedef void (*FuncPtr)();
FuncPtr funcPtrArray[10];
int doSomething();
funcPtrArray[0] = &doSomething; // error! 类型不匹配。
funcPtrArray[0] = reinterpret_cast<FuncPtr>(&doSomething); // right!

但需要注意的是,转换函数指针的代码是==不可移植==的!(C++不保证所有的函数指针都被用一样的方法表示),在一些情况下这样的转换会产生不正确的结果,所以你应该避免转换函数指针类型。

3. 不要对数组使用多态

C++允许你通过基类指针和引用来操作派生类数组,但这不会有很好的结果。

class BST { ... };
class BalancedBST: public BST { ... }; 
void printBSTArray(ostream& s, 
                       const BST array[],
                       int numElements)
    {
      for (int i = 0; i < numElements; ) {
s << array[i]; //假设 BST 类 
    } //重载了操作符<< 
} 

BST BSTArray[10];
...
printBSTArray(cout, BSTArray, 10); // 运行正常 

BalancedBST bBSTArray[10];
...
printBSTArray(cout, bBSTArray, 10);  //  error!!

问题就出在了循环代码中。

for (int i = 0; i < numElements; ) { 
      s << array[i];
}

array[i]是一个指针算法的缩写。array是一个指向数组起始地址的指针,而下标操作是根据元素的大小来计算出对应元素的地址。编译器为了建立正确遍历数组的执行代码,它必须计算对象大小。毫无疑问是(sizeof(BST))!!那么对于BalancedBST而言,它的大小肯定不等于sizeof(BST),于是编译器在计算元素地址时就会出错,或者产生不可预计的后果。

而如果你试图删除一个含有派生类对象的数组,也会产生各种各样的问题。

void deleteArray(ostream& logStream, BST array[]) { 
      logStream << "Deleting array at address "
                << static_cast<void*>(array) << '\n';
delete [] array;
}
BalancedBST *balTreeArray = 
  new BalancedBST[50];
...
deleteArray(cout, balTreeArray); //  记录删除操作。

编译器遇到delete [] array时会产生以下代码:

for ( int i = 数组元素的个数 1; i >= 0;--i) { 
array[i].BST::~BST();
        // 调用 array[i]的 
        // 析构函数 
}

语言规范说:==通过一个基类指针来删除一个函数派生类对象的数组,结果将是不确定的!==所以多态和指针算法不能混合使用,数组和多态也不能混合使用!

4. 避免误用的缺省构造函数

缺省构造函数是编译器会自动生成的(除非显示给出有参数的构造函数)。但实际上,很多时候,某些特定的类是不允许提供缺省构造函数的。(因为他们没有特定的含义。)但由此也会产生一些在操作上的问题。

  1. 在数组的使用时。
class EquipmentPiece {
public: 
  EquipmentPiece(int IDNumber);
... }; 
EquipmentPiece bestPieces[10]; // 错误!没有正确调用 
                                // EquipmentPiece 构造函数 
EquipmentPiece *bestPieces =
new EquipmentPiece[10]; // 错误!与上面的问题一样 

当然也有方法可以解决。

1)在数组定义时提供必要的参数。

int ID1, ID2, ID3, ..., ID10;
...
EquipmentPiece bestPieces[] = {
  EquipmentPiece(ID1),
  EquipmentPiece(ID2),
  EquipmentPiece(ID3),
  ...,
  EquipmentPiece(ID10)
// 存储设备 ID 号的变量 
// 正确, 提供了构造函数的参数 
};

但这种方法能用在堆数组(heap arrays)的定义上。

2)更通用的解法是使用指针数组来替代对象数组。

typedef EquipmentPiece* PEP; // PEP 指针指向//一个 EquipmentPiece 对象 
PEP bestPieces[10]; // 正确, 没有调用构造函数 
PEP *bestPieces = new PEP[10]; // 指向指针的指针数组
// 也正确 

在指针数组里的每一个指针被重新赋值,以指向一个不同的 EquipmentPiece 对象:

for (int i = 0; i < 10; ++i) 
bestPieces[i] = new EquipmentPiece( ID Number ); 

不过这中方法有两个缺点,第一你必须删除数组里每个指针所指向的对象。如果你忘了, 就会发生内存泄漏。第二增加了内存分配量,因为正如你需要空间来容纳 EquipmentPiece 对象一样,你也需要空间来容纳指针。

3)如果你为数组分配 raw memory,你就可以避免浪费内存。使用 placement new 方法(参 见条款 M8)在内存中构造 EquipmentPiece 对象:


// 为大小为 10 的数组 分配足够的内存// EquipmentPiece 对象; 详细情况请参见条款 M8// operator new[] 函数void *rawMemory = 
  operator new[](10*sizeof(EquipmentPiece));
// make bestPieces point to it so it can be treated as an
// EquipmentPiece array
EquipmentPiece *bestPieces =
static_cast<EquipmentPiece*>(rawMemory);
// construct the EquipmentPiece objects in the memory 
// 使用"placement new"for (int i = 0; i < 10; ++i) 
    new (&bestPieces[i]) EquipmentPiece( ID Number );

注意你仍旧得为每一个 EquipmentPiece 对象提供构造函数参数。这个技术(和指针数组的主意一样)允许你在没有缺省构造函数的情况下建立一个对象数组。它没有绕过对构造 函数参数的需求,实际上也做不到。如果能做到的话,就不能保证对象被正确初始化。

使用 placement new 的缺点除了是大多数程序员对它不熟悉外(能使用它就更难了), 还有就是当你不想让它继续存在使用时,必须手动调用数组对象的析构函数,然后调用操作 符 delete[]来释放 raw memory。

// 以与构造 bestPieces 对象相反的顺序// 解构它。for (int i = 9; i >= 0; --i) 
  bestPieces[i].~EquipmentPiece();
// deallocate the raw memory
operator delete[](rawMemory);

如果你忘记了这个要求而使用了普通的数组删除方法,那么你程序的运行将是不可预测 的。这是因为:直接删除一个不是用 new 操作符来分配的内存指针,其结果没有被定义。

delete [] bestPieces; // 没有定义! bestPieces 
//不是用 new 操作符分配的。
  1. 无法在都铎基于模板的容器中使用。
    因为实例化一个模板时,模板的类型参数应该提供一个缺省构造函数,这是一个常见的要求。
template<class T> 
class Array {
public:
  Array(int size);
... 
private: 
T *data; 
}; 
template<class T>
Array<T>::Array(int size) {
  data = new T[size];
  ...
// 为每个数组元素 //依次调用 T::T() 
}

在多数情况下,通过仔细设计模板可以杜绝对缺省构造函数的需求。例如标准的 vector模板(生成一个类似于可扩展数组的类)对它的类型参数没有必须有缺省构造函数的要求。

  1. 在设计虚基类时提供缺省构造函数

不提供缺省构造函数的虚基类,很难与其进行合作。因为几乎所有的派生类在实例化时都必须给虚基类构造函数提供参数。这就要求所有由没有缺省构造函数的虚基类继承 下来的派生类(无论有多远)都必须知道并理解提供给虚基类构造函数的参数的含义。派生类 的作者是不会企盼和喜欢这种规定的。

总结:

很多人可能会提供无意义构造函数。(给出缺省值,但没有意义。)例如:

class EquipmentPiece { 
public:
  EquipmentPiece(  int IDNumber = UNSPECIFIED);
  ...
private:
  static const int   UNSPECIFIED;
};
这允许这样建立 EquipmentPiece 对象 EquipmentPiece e; 
// 其值代表 ID 值不确定。 
//这样合法 

这样的修改使得其他成员函数变得复杂,因为不再能确保 EquipmentPiece 对象进行了
有意义的初始化。假设它建立一个因没有ID而没有意义的EquipmentPiece对象,那么大多 数成员函数必须检测 ID 是否存在。如果不存在 ID,它们将必须指出怎么犯的错误。不过通 常不明确应该怎么去做,很多代码的实现什么也没有提供:只是抛出一个异常或调用一个函 数终止程序。当这种情形发生时,很难说提供缺省构造函数而放弃了一种保证机制的做法是 否能提高软件的总体质量。

提供无意义的缺省构造函数会影响类的工作效率。如果成员函数必须测试所有的部分 是否都被正确地初始化,那么这些函数的调用者就得为此付出更多的时间。而且还得付出更 多的代码,因为这使得可执行文件或库变得更大。它们也得在测试失败的地方放置代码来处 理错误。如果一个类的构造函数能够确保所有的部分被正确初始化,所有这些弊病都能够避 免。缺省构造函数一般不会提供这种保证,所以在它们可能使类变得没有意义时,尽量去避 免使用它们。使用这种(没有缺省构造函数的)类的确有一些限制,但是当你使用它时,它 也给你提供了一种保证:你能相信这个类被正确地建立和高效地实现。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值