More Effective C++:基础议题
Item M1:指针与引用的区别
指针
定义:指针是一个变量,它存储的是另一个变量的内存地址。
语法:通过*操作符来定义,并且可以通过->操作符来访问成员。
灵活性:指针可以在任何时候被重新赋值,以指向不同的对象。
空值:指针可以被设置为nullptr(或NULL/0),表示它不指向任何有效的对象。
检查:在使用指针之前通常需要检查它是否为nullptr,以避免潜在的运行时错误。
初始化:指针可以不初始化就声明,但这通常是不安全的做法。
用途:当程序逻辑允许一个变量在某些情况下不指向任何东西,或者需要改变指向的对象时,使用指针是合适的。
引用
定义:引用是已存在的变量的别名。一旦初始化之后,引用就不能再指向其他的对象。
语法:通过&操作符来定义,并且通过.操作符来访问成员。
不可更改:一旦引用被初始化指向一个对象,它就不能再指向别的对象。
非空性:引用必须总是引用一个有效的对象,不能指向空值。
初始化:引用必须在声明时就进行初始化,否则会导致编译错误。
效率:由于引用总是有效的,所以在使用引用前不需要做空检查,这使得引用通常比指针更高效。
用途:当确保变量始终指向一个有效对象,并且不需要改变所指向的对象时,使用引用更为合适。此外,在重载某些操作符如[]时,通常返回引用以保持直观的语义。
总结
指针适合于那些需要改变指向对象或者可能不指向任何对象的情况。引用适用于那些始终需要引用到一个特定对象,并且不会改变引用对象的情况。此外,引用在函数参数传递中也常用来实现类似传址的效果,同时提供了更好的可读性和安全性。
Item M2:尽量使用 C++风格的类型转换
static_cast
用途:用于基础类型的转换,比如整数到浮点数的转换,或者指针之间的转换,这些转换在编译时是明确的。
限制:不能用于转换掉const、volatile属性,也不能用于不相关的类型转换,如结构体到整数的转换。
int firstNumber = 7;
double result = static_cast<double>(firstNumber) / 6; // 结果是 1.16667
const_cast
用途:用于添加或移除const或volatile属性。
重要性:确保只改变变量的const或volatile状态,而不改变其他属性。
const int x = 10;
int *nonConstX = const_cast<int*>(&x); // 移除 const 属性
不可以通过 nonConstX 修改 x 的值,因为 x 本身还是 const 的。根据C++标准,尝试通过const_cast得到的非const指针修改一个原本是const的对象是未定义行为。也就是说,标准没有定义这种行为的具体结果,它可能工作,也可能不工作,甚至可能导致程序崩溃。
移除const属性的主要用途是在某些特定情况下,当需要修改原本被声明为const的对象时。通常,const关键字用于表明一个对象不应该被修改,以增强程序的安全性和可读性。
然而有时候会遇到需要绕过const保护的情况
(1)库函数要求非const参数
当调用一个库函数,该函数需要一个非const参数,但只有一个const对象时,可以使用const_cast来临时移除const属性,以便将这个对象传递给函数。这种情况下的前提是,你确定这个库函数不会实际修改这个对象。有时,为了兼容旧代码或特定的API,你可能需要使用const_cast来适应那些不接受const参数的接口。
void someFunction(int* ptr); // 假设这是一个第三方库函数,要求非const指针
const int x = 1;
someFunction(const_cast<int*>(&x)); // 临时移除const属性
const函数修改非const成员变量,加mutable。
class MyClass {
private:mutable int counter; // 使用mutable关键字,允许const成员函数修改它
public:void incrementCounter() const {++counter; // 允许修改}
};
class MyClass {
private:int data; // 非 mutable 数据成员
public:MyClass(int d) : data(d) {}void modifyData(int newValue) const {// 使用 const_cast 移除 const 属性int* nonConstData = const_cast<int*>(&data);*nonConstData = newValue; // 修改 data}int getData() const {return data;}
};int main() {const MyClass obj(10); // 创建一个 const 对象obj.modifyData(20); // 尝试修改 datastd::cout << "Data: " << obj.getData() << std::endl; // 输出 Data: 20return 0;
}
在C++中,当声明一个对象为const类型时,这个对象的所有非mutable数据成员都会被视为const。不能通过任何非const成员函数或非const引用修改这些数据成员。
当一个成员函数被声明为const时,this指针实际上是const的。this指针指向的对象是const的,不能通过this指针修改对象的任何非mutable成员。在modifyData函数中,this指针是const MyClass* const this。这意味着不能直接通过this指针修改data成员,因为data是非mutable的。在modifyData函数中,const_cast<int*>(&data)的作用是将&data从const int*转换为int*。const_cast去除了&data的const属性,使得可以通过这个指针修改data的值。
dynamic_cast
用途:主要用于多态类型的安全转换,特别是从基类指针或引用来转换到派生类指针或引用。
(详细见对象模型)
特点:如果转换是不可能的(比如基类指针实际上并不指向派生类对象),dynamic_cast会返回nullptr(对于指针)或抛出std::bad_cast异常(对于引用)。
reinterpret_cast
用途:用于非常底层的类型转换,
指针类型之间的转换:将一种指针类型转换为另一种完全不同的指针类型。整数和指针之间的转换:将整数转换为指针,或将指针转换为整数。
和static_cast对比:主要用于基本类型之间的转换,如 int 到 double。
用于类层次结构中的安全向上转换(从派生类指针/引用到基类指针/引用)。
用于 void* 指针到具体类型的指针的转换。提供了一定程度的类型检查,确保转换是合理的。
风险:这种转换的结果依赖于具体的实现,并且可能导致不可移植的代码。
int i = 8;
char* c = reinterpret_cast<char*>(&i); // 将 int* 转换为 char*
// 这里 c 指向 i 的第一个字节
为什么要使用C++风格的类型转换?
精确性:C++风格的类型转换更加精确,可以更好地表达转换意图。
安全性:dynamic_cast可以安全地处理多态类型转换,避免了C风格转换中可能出现的错误。
可读性:使用特定的类型转换关键字,代码的意图更加清晰。
虽然C++风格的类型转换可能显得冗长,但它们提供了更好的类型安全性和代码清晰度。因此,推荐在C++代码中使用这些现代的类型转换操作符,以提高代码质量和可靠性。
Item M3:不要对数组使用多态
class BST {
public:virtual ~BST() {}virtual void print(ostream& out) const = 0; // 纯虚函数
};
class BalancedBST : public BST {
public:void print(ostream& out) const override {out << "BalancedBST object\n";}
};
打印数组的函数
void printBSTArray(ostream& s, const BST array[], int numElements) {for (int i = 0; i < numElements; ++i) {array[i].print(s); // 通过基类指针调用派生类的函数}
}
int main() {BST* bstArray = new BST[10]; // 假设 BST 有默认构造函数printBSTArray(cout, bstArray, 10); // 正常运行delete[] bstArray;return 0;
}
问题情况
int main() {BalancedBST* bBSTArray = new BalancedBST[10];printBSTArray(cout, bBSTArray, 10); // 可能会出错delete[] bBSTArray; // 可能会导致未定义行为return 0;
}
1. 数组元素大小的差异
基类指针访问派生类数组:在 printBSTArray 函数中,array 是一个指向 BST 类型的指针。编译器假设每个元素的大小是 BST 的大小。然而,bBSTArray 实际上存储的是 BalancedBST 对象,其大小通常大于 BST 对象。因此,编译器生成的指针算法将是错误的,可能会导致访问越界或未定义行为。
2. 析构函数的调用
删除派生类数组:在 delete[] bBSTArray 时,编译器会调用 BST 的析构函数,而不是 BalancedBST 的析构函数。这会导致 BalancedBST 对象的资源没有被正确释放,从而引发资源泄漏或未定义行为。
解决:
(1)使用容器:使用 std::vector 或其他标准库容器,它们可以正确处理多态性。
(2)使用智能指针:使用 std::unique_ptr 或 std::shared_ptr 来管理动态分配的对象。
(3)避免从具体类派生具体类(M33)
使用 std::vector
#include <vector>
#include <memory>
void printBSTVector(ostream& s, const vector<BST*>& vec) {for (const auto& elem : vec) {elem->print(s);}
}
int main() {vector<BST*> bstVec;for (int i = 0; i < 10; ++i) {bstVec.push_back(new BalancedBST());}printBSTVector(cout, bstVec);for (auto& elem : bstVec) {delete elem;}return 0;
}
使用 std::unique_ptr
#include <memory>
#include <vector>
void printBSTVector(ostream& s, const vector<unique_ptr<BST>>& vec) {for (const auto& elem : vec) {elem->print(s);}
}
int main() {vector<unique_ptr<BST>> bstVec;for (int i = 0; i < 10; ++i) {bstVec.emplace_back(make_unique<BalancedBST>());}printBSTVector(cout, bstVec);return 0; // unique_ptr 会自动释放资源
}
通过使用这些方法,可以避免由于多态数组导致的未定义行为,并确保资源的正确管理和释放。
Item M4:避免无用的缺省构造函数
C++默认构造函数是指不接受任何参数的构造函数,它允许在不提供任何外部数据的情况下创建对象。这种构造函数对于某些类是有意义的,比如那些可以被初始化为“空”或“未指定”状态的类,或者像链表、哈希表这样的容器类。
然而,对于需要特定数据才能正确初始化的对象,比如一个地址簿对象或带有公司ID的设备对象,缺省构造函数就不合适了。因为这样的对象如果没有正确的初始化数据,它们就没有意义。
如果一个类没有缺省构造函数,那么在使用这个类时会遇到一些限制:
- 数组问题:C++中不能在创建对象数组时传递构造函数参数。如果一个类没有缺省构造函数,不能直接创建该类的数组。
EquipmentPiece
类没有缺省构造函数,它需要一个IDNumber
参数。
文章提供了几种解决方案:
- 使用初始化列表来创建非堆数组。
// 没有缺省构造函数的类
class EquipmentPiece {
public:EquipmentPiece(int IDNumber);// ...
};
// 使用初始化列表创建数组
int ID1, ID2, ID3, ..., ID10; // 存储设备ID号的变量
EquipmentPiece bestPieces[] = {EquipmentPiece(ID1),EquipmentPiece(ID2),EquipmentPiece(ID3),// ...
};
使用指针数组来代替对象数组。
//使用指针数组
typedef EquipmentPiece* PEP; // PEP指针指向一个EquipmentPiece对象
PEP bestPieces[10]; // 正确,没有调用构造函数
for (int i = 0; i < 10; ++i) {bestPieces[i] = new EquipmentPiece(IDNumber);
}
使用placement new来在原始内存中构造对象。
// 使用placement new在原始内存中构造对象
void *rawMemory = operator new[](10 * sizeof(EquipmentPiece)); // 分配足够的内存
EquipmentPiece *bestPieces = static_cast<EquipmentPiece*>(rawMemory);
for (int i = 0; i < 10; ++i) {new (&bestPieces[i]) EquipmentPiece(IDNumber); // 使用"placement new"
}
虚基类:在设计虚基类时,如果没有缺省构造函数,那么所有派生类在实例化时都必须提供虚基类的构造函数参数,这可能会导致代码复杂和难以维护。
不要提供无意义的缺省构造函数:有些人认为所有类都应该有缺省构造函数,即使它们不能正确初始化对象。这可能会导致代码中的错误处理变得复杂,并且影响性能如果一个类的对象在没有接收到必要的初始化数据时没有意义,那么提供缺省构造函数(即无参数的构造函数)是不合理的。这样的缺省构造函数可能会导致对象处于未正确初始化的状态。提供无意义的缺省构造函数可能会导致类的成员函数需要额外检查对象是否已经正确初始化,这会增加运行时间的开销。因为每次调用成员函数时,都需要进行初始化状态的检查。如果成员函数需要检查对象的初始化状态,那么就需要更多的代码来处理这些检查和可能出现的错误情况。这会导致可执行文件或库的大小增加。