Item M32:在未来时态下开发程序
(1)适应变化:软件开发人员应认识到软件需求和技术环境会随时间变化,因此在设计和编码时应具备前瞻性,使软件能够适应未来的需求和平台变化。
(2)预防性设计:在设计类和接口时,应该考虑未来可能出现的功能扩展和代码重用。例如,如果一个类有可能被继承,则应提前声明虚析构函数,即便当前没有派生类。
(3)使用语言特性强制设计约束:利用C++的语言特性来实现设计意图,比如通过将某些成员函数声明为私有来防止不希望的操作,或者使用模板和泛型编程来提高代码的灵活性和复用性。
(4)易于理解和维护:编写清晰、易于理解的代码,考虑到未来的维护者可能不是原始开发者。这包括提供良好的文档、遵循良好的编程实践等。
(5)健壮性和错误处理:预见用户可能会犯的错误,并设计代码以防止、检测或修复这些错误。这有助于提高软件的健壮性和用户体验。
(6)可移植性:尽可能编写可移植的代码,除非性能是绝对关键的因素。可移植的代码更容易适应不同的平台和环境,也有助于扩大用户基础。
(7)局部化变化的影响:设计时应尽量减小更改一处代码对其他部分的影响,通过封装和模块化等方式实现。
(8)考虑长期生存能力:虽然软件需要满足当前的需求,但是过度关注眼前可能会牺牲软件的长期生存能力和价值。优秀的软件不仅能满足当前的需求,还能在未来的变化中保持相关性和竞争力。
Item M33:将非尾端类设计为抽象类
非尾端类(Non-Leaf Class)指那些被设计为基类,且预期会有派生类的类。这些类通常包含一些通用的行为和属性,但它们本身可能不适合直接实例化。它们的设计目的是为了让派生类继承并扩展其功能。在继承层次中,如果基类 Animal 有赋值操作符 operator=,而派生类 Lizard 和 Chicken 也有各自的赋值操作符,通过基类指针进行赋值时,只会调用基类的赋值操作符,导致部分赋值问题
首先,我们来看一下作者给出的初始代码示例,以及如何通过引入抽象类来解决问题。
class Animal {
public:Animal& operator=(const Animal& rhs);// 其他成员...
};
class Lizard : public Animal {
public:Lizard& operator=(const Lizard& rhs);// 其他成员...
};
class Chicken : public Animal {
public:Chicken& operator=(const Chicken& rhs);// 其他成员...
};
Lizard liz1;
Lizard liz2;
Animal *pAnimal1 = &liz1;
Animal *pAnimal2 = &liz2;
*pAnimal1 = *pAnimal2; // 只赋值了 Animal 部分,Lizard 部分未被赋值
解决方案:引入抽象类
class AbstractAnimal {
protected:AbstractAnimal& operator=(const AbstractAnimal& rhs);
public:virtual ~AbstractAnimal() = 0; // 纯虚析构函数// 其他成员...
};
// 纯虚析构函数的实现
AbstractAnimal::~AbstractAnimal() {}
class Animal : public AbstractAnimal {
public:Animal& operator=(const Animal& rhs);// 其他成员...
};
class Lizard : public AbstractAnimal {
public:Lizard& operator=(const Lizard& rhs);// 其他成员...
};
class Chicken : public AbstractAnimal {
public:Chicken& operator=(const Chicken& rhs);// 其他成员...
};
解决方案:
通过引入一个抽象基类 AbstractAnimal,将 Animal、Lizard 和 Chicken 都从 AbstractAnimal 继承。
纯虚析构函数:在 AbstractAnimal 中声明一个纯虚析构函数 virtual ~AbstractAnimal() = 0;,并在类外实现它。这使得 AbstractAnimal 成为一个抽象类,不能被实例化。
保护赋值操作符:将 AbstractAnimal 的赋值操作符声明为 protected,以防止通过基类指针进行赋值。
优点:
通过抽象类和保护赋值操作符,避免了通过基类指针进行部分赋值的问题。
确保赋值操作的正确性,避免运行时错误。如果未来需要添加新的动物类,只需从 AbstractAnimal 继承即可,不会影响现有代码。
缺点:
增加复杂度:引入抽象类会增加代码的复杂度,需要额外的类和成员函数。
编译开销:需要重新编译涉及 Animal、Lizard 和 Chicken 的代码。
设计原则:
非尾端类应该是抽象类:如果一个类被设计为基类,且有派生类,那么它应该是抽象类。这有助于确保派生类的行为正确性,避免部分赋值等问题。
明确抽象行为:通过引入抽象类,强迫设计者明确识别和定义有用的抽象行为,提高代码的可读性和可维护性。
Item M34:如何在同一程序中混合使用 C++和 C
1. 确保编译器兼容性
在混合使用 C++ 和 C 时,首先需要确保 C++ 编译器和 C 编译器生成的 .obj 文件是兼容的。因为不同的编译器在实现相关的特性(如 int 和 double 的字节大小、传参方式等)上可能有所不同。
2. 名变换
名变换:C++ 编译器会给函数名进行变换,以支持函数重载。C 语言中没有函数重载,因此不需要名变换。如果 C++ 代码调用 C 函数,C++ 编译器会对函数名进行变换,而 C 代码中函数名保持不变,导致链接错误。
解决方法:使用 extern "C" 声明 C 函数,禁止名变换。
extern "C" void drawLine(int x1, int y1, int x2, int y2);
批量声明:可以使用 extern "C" 包裹一组函数,以简化代码。
extern "C" {void drawLine(int x1, int y1, int x2, int y2);void twiddleBits(unsigned char bits);void simulate(int iterations);
}
条件编译:使用 #ifdef __cplusplus 宏来确保在 C++ 编译时添加 extern "C",而在 C 编译时不添加。
#ifdef __cplusplus
extern "C" {
#endif
void drawLine(int x1, int y1, int x2, int y2);
void twiddleBits(unsigned char bits);
void simulate(int iterations);
#ifdef __cplusplus
}
#endif
3. 静态初始化
静态初始化:C++ 在 main 执行前会自动调用静态对象的构造函数,执行静态初始化。同样,在 main 结束后会调用静态对象的析构函数。
如果 main 是用 C 编写的,静态对象的构造和析构可能不会被正确调用。
解决方法:用 C++ 写 main 函数,即使程序的大部分是用 C 编写的。可以通过将 C 的 main 改名为 realMain,然后在 C++ 的 main 中调用 realMain。
extern "C" int realMain(int argc, char *argv[]);
int main(int argc, char *argv[]) {return realMain(argc, argv);
}
4. 动态内存分配
规则:C++ 部分使用 new 和 delete,C 部分使用 malloc 和 free。
用 free 释放 new 分配的内存或用 delete 释放 malloc 分配的内存,其行为未定义。
解决方法:严格隔离 new 和 delete 与 malloc 和 free 的使用。
char *strdup(const char *ps); // C 函数库中的函数
char *str = strdup("Hello, World");
free(str); // 用 free 释放 C 函数库分配的内存
尽量避免调用那些既不在标准库中也没有固定形式的函数,以减少可移植性问题。
5. 数据结构的兼容性
C 函数不了解 C++ 的特性,因此在 C++ 和 C 之间传递数据时,只能使用 C 可表示的概念。:可以安全传递普通指针、指向非成员函数或静态成员函数的指针、结构和内建类型(如 int、char 等)。
结构兼容性:C++ 中的 struct 规则兼容 C 中的规则,因此相同的结构可以在 C++ 和 C 之间安全传递。增加非虚成员函数不会影响兼容性,但增加虚函数或进行继承会影响内存结构,从而影响兼容性。
struct Point {int x;int y;
};
// C++ 版本
struct PointWithMethods {int x;int y;void move(int dx, int dy) {x += dx;y += dy;}
};
// C 版本
struct PointWithMethods {int x;int y;
};