C++重要议题

2024-02-02 10:38
文章标签 c++ 重要 议题

本文主要是介绍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,它们将必须指出怎么犯的错误。不过通 常不明确应该怎么去做,很多代码的实现什么也没有提供:只是抛出一个异常或调用一个函 数终止程序。当这种情形发生时,很难说提供缺省构造函数而放弃了一种保证机制的做法是 否能提高软件的总体质量。

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

这篇关于C++重要议题的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/670443

相关文章

使用C++实现链表元素的反转

《使用C++实现链表元素的反转》反转链表是链表操作中一个经典的问题,也是面试中常见的考题,本文将从思路到实现一步步地讲解如何实现链表的反转,帮助初学者理解这一操作,我们将使用C++代码演示具体实现,同... 目录问题定义思路分析代码实现带头节点的链表代码讲解其他实现方式时间和空间复杂度分析总结问题定义给定

C++初始化数组的几种常见方法(简单易懂)

《C++初始化数组的几种常见方法(简单易懂)》本文介绍了C++中数组的初始化方法,包括一维数组和二维数组的初始化,以及用new动态初始化数组,在C++11及以上版本中,还提供了使用std::array... 目录1、初始化一维数组1.1、使用列表初始化(推荐方式)1.2、初始化部分列表1.3、使用std::

C++ Primer 多维数组的使用

《C++Primer多维数组的使用》本文主要介绍了多维数组在C++语言中的定义、初始化、下标引用以及使用范围for语句处理多维数组的方法,具有一定的参考价值,感兴趣的可以了解一下... 目录多维数组多维数组的初始化多维数组的下标引用使用范围for语句处理多维数组指针和多维数组多维数组严格来说,C++语言没

c++中std::placeholders的使用方法

《c++中std::placeholders的使用方法》std::placeholders是C++标准库中的一个工具,用于在函数对象绑定时创建占位符,本文就来详细的介绍一下,具有一定的参考价值,感兴... 目录1. 基本概念2. 使用场景3. 示例示例 1:部分参数绑定示例 2:参数重排序4. 注意事项5.

使用C++将处理后的信号保存为PNG和TIFF格式

《使用C++将处理后的信号保存为PNG和TIFF格式》在信号处理领域,我们常常需要将处理结果以图像的形式保存下来,方便后续分析和展示,C++提供了多种库来处理图像数据,本文将介绍如何使用stb_ima... 目录1. PNG格式保存使用stb_imagephp_write库1.1 安装和包含库1.2 代码解

C++实现封装的顺序表的操作与实践

《C++实现封装的顺序表的操作与实践》在程序设计中,顺序表是一种常见的线性数据结构,通常用于存储具有固定顺序的元素,与链表不同,顺序表中的元素是连续存储的,因此访问速度较快,但插入和删除操作的效率可能... 目录一、顺序表的基本概念二、顺序表类的设计1. 顺序表类的成员变量2. 构造函数和析构函数三、顺序表

使用C++实现单链表的操作与实践

《使用C++实现单链表的操作与实践》在程序设计中,链表是一种常见的数据结构,特别是在动态数据管理、频繁插入和删除元素的场景中,链表相比于数组,具有更高的灵活性和高效性,尤其是在需要频繁修改数据结构的应... 目录一、单链表的基本概念二、单链表类的设计1. 节点的定义2. 链表的类定义三、单链表的操作实现四、

使用C/C++调用libcurl调试消息的方式

《使用C/C++调用libcurl调试消息的方式》在使用C/C++调用libcurl进行HTTP请求时,有时我们需要查看请求的/应答消息的内容(包括请求头和请求体)以方便调试,libcurl提供了多种... 目录1. libcurl 调试工具简介2. 输出请求消息使用 CURLOPT_VERBOSE使用 C

C++实现获取本机MAC地址与IP地址

《C++实现获取本机MAC地址与IP地址》这篇文章主要为大家详细介绍了C++实现获取本机MAC地址与IP地址的两种方式,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 实际工作中,项目上常常需要获取本机的IP地址和MAC地址,在此使用两种方案获取1.MFC中获取IP和MAC地址获取

C/C++通过IP获取局域网网卡MAC地址

《C/C++通过IP获取局域网网卡MAC地址》这篇文章主要为大家详细介绍了C++如何通过Win32API函数SendARP从IP地址获取局域网内网卡的MAC地址,感兴趣的小伙伴可以跟随小编一起学习一下... C/C++通过IP获取局域网网卡MAC地址通过win32 SendARP获取MAC地址代码#i