目录
1.什么是防御性编程
防御性编程是一种编程实践,旨在提高软件的健壮性和可靠性,减少运行时错误。在C++中,这尤其重要,因为C++是一种允许进行低级内存操作和指针操作的语言,这使得它容易出错。
顾名思义,防御性编程是一种细致、谨慎的编程方法。为了开发可靠的软件,我们要设计系统中的每个组件,以使其尽可能的"保护"自己。我们通过明确地在代码中对设想进行检查,这是一种努力,防止我们的代码以将会展现错误行为的方式被调用。
2.防御性编程技巧
2.1.采用良好的编码风格
1) const关键字
关键字const可以给读你代码的人传达非常有用的信息。例如,在函数的形参前添加const关键字意味着这个参数在函数体内不会被修改,属于输入参数。
同时,合理地使用关键字const可以使编译器很自然的保护那些不希望被修改的参数,防止其被无意的代码修改,减少bug的出现。
2) volatile关键字
在一些并行设备的硬件寄存器(如状态寄存器),中断服务子程序中会访问到的全局变量以及多线程应用中被几个任务共享的变量前使用volatile关键字来防止编译优化。
3) static关键字
函数体内static变量的作用范围为该函数体,不同于auto变量,该变量的内存只被分配一次,因此其值在下次调用时仍维持上次的值。
在模块内的static全局变量可以被模块内的所有函数访问,但不能被模块外其它函数访问。
在模块内的static函数只可能被这一模块内的其它函数调用,这个函数的使用范围被限制在声明它的模块内。
-
位操作运算中,尽可能使用<<、 >>、 &、|等运算符,尽可能少使用/、%、*运算符。
-
变量和函数的命名要有意义,并且尽可能做到一个函数只做一件事情。
-
多采用面向对象的思想来编写代码。
-
在投入到编码工作之前,先考虑大体的设计方案,这也非常关键。
2.2.合理使用assert
断言(assert)是一种调试辅助工具,用于在代码中设置检查点。如果条件为真,程序可以继续执行;如果条件为假,程序将显示错误消息并终止。这有助于在开发阶段捕获错误。
在日常编程过程中,可以在无法预知的逻辑当中增加assert判断,很容易发现程序中的逻辑错误。比如下面的代码:
cpp
//示例1,检查输入参数的合法性
void func(int value) {
assert(value >= 0 && value <= 10); // 假设value应该在0到10之间
// 处理value
}
//示例2
using dealWithFunc = std::function<void(const void*, int)>;
std::map<int, dealWithFunc> taskCmds;
bool func1(int type){
auto it = taskCmds.find(type);
if (it == taskCmds.end()){
assert(false); //没有处理这个type的逻辑, 逻辑一旦走到这里,就知道某个type的事件没有处理
return false;
}
。。。
return true;
}
2.3.检查函数参数
函数应检查其参数的有效性,并在接收到无效参数时采取适当的行动,如返回错误代码或抛出异常。
cpp
void process(const char* data, size_t length) {
if (data == nullptr) {
throw std::invalid_argument("data pointer is null");
}
if (length == 0) {
throw std::invalid_argument("length cannot be zero");
}
// 处理数据
}
2.4.使用异常处理
在C++中,异常处理是一个强大的工具,可以用于捕获和处理运行时错误。通过使用try-catch块,可以优雅地处理异常情况,避免程序崩溃。
cpp
try {
// 可能抛出异常的代码
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << std::endl;
}
2.5.避免裸指针
尽可能使用智能指针(如std::unique_ptr
和std::shared_ptr
)代替裸指针,以减少内存泄漏和悬挂指针的风险。
cpp
std::unique_ptr<MyClass> ptr(new MyClass());
ptr->doSomething();
2.6.资源管理
在C++中,资源管理(如内存、文件句柄等)是一个重要的问题。RAII(Resource Acquisition Is Initialization)是一种常用的资源管理策略,通过构造函数获取资源,析构函数释放资源,确保资源的正确释放。
cpp
class FileWrapper {
public:
FileWrapper(const std::string& filename) {
file = std::fopen(filename.c_str(), "r");
if (!file) {
throw std::runtime_error("Unable to open file");
}
}
~FileWrapper() {
if (file) {
std::fclose(file);
}
}
private:
FILE* file;
};
2.7.最小化使用全局变量
全局变量在程序中随时可以被修改,容易引发难以调试的错误。尽量使用局部变量和参数传递,保持代码的模块化和可维护性。
cpp
class MyClass {
public:
void doSomething() {
int localVar = 0;
helperFunction(localVar);
}
private:
void helperFunction(int value) {
// 使用局部变量进行处理
}
};
2.8.封装和模块化
封装是面向对象编程的基本原则之一。通过封装数据和方法,可以减少模块之间的耦合,提高代码的可维护性和可扩展性。
1) 封装数据和方法:通过封装数据和方法,可以减少模块之间的耦合,提高代码的可维护性和可扩展性。
2) 使用局部变量和参数传递:尽量使用局部变量和参数传递,避免使用全局变量,因为全局变量在程序中随时可以被修改,容易引发难以调试的错误。
cpp
class MyClass {
public:
void setValue(int value) {
if (value >= 0) {
this->value = value;
} else {
throw std::invalid_argument("value cannot be negative");
}
}
int getValue() const {
return value;
}
private:
int value;
};
2.9.避免使用宏
宏可能导致代码难以理解和维护。尽量使用常量、内联函数或模板来代替宏。
2.10.初始化所有变量
确保所有变量在使用前都已初始化,以避免未定义行为。
cpp
class MyClass {
public:
MyClass() : value(0) {}
private:
int value;
};
2.11.使用范围枚举
使用范围枚举(enum class
)代替传统的枚举,以避免枚举值的隐式转换和名称冲突。
cpp
enum class Color { Red, Green, Blue };
Color color = Color::Red;
2.12.防止数组越界
在访问数组元素时,确保索引在有效范围内,避免数组越界访问。非常容易搞错的有以下几点:
1)字符串和字符数组的区别,字符串必须是以'\0'结束,字符数组必须带长度。比如:
cpp
char name[7] = "1234455"; // "1234455"
char* name = "1234455"; // "1234455\0"
2)数组在函数传递的过程中会退变成指针,所以必须带长度,不然不知道长度
cpp
void accessArray(int* arr, size_t size, size_t index) {
if (index >= size) {
throw std::out_of_range("index out of range");
}
// 访问arr[index]
}
int a[] = {1,44,56,7,8,9,9};
accessArray(a, sizeof(a)/sizeof(a[0]), 5);
2.13.使用标准库和智能算法
C++标准库提供了多种容器(如std::vector、std::map等),它们封装了复杂的数据结构和操作,能有效避免内存泄漏和指针错误。
cpp
std::vector<int> vec = {1, 2, 3};
vec.push_back(4);
std::sort(vec.begin(), vec.end());
2.14.线程安全
在多线程环境中,确保代码的线程安全是至关重要的。使用互斥锁(mutex)、条件变量(condition variable)等同步机制,确保多个线程访问共享资源时不会产生冲突。
cpp
std::mutex mtx;
void threadSafeFunction() {
std::lock_guard<std::mutex> lock(mtx);
// 访问共享资源
}
2.15.代码审查和测试
代码审查
代码审查(Code Review)是一种通过检查源代码来找出并修正错误的系统性方法。它可以帮助开发团队提高代码质量,促进团队成员之间的知识共享,以及增强团队对软件项目的整体理解。代码审查通常关注以下几个方面:
- 代码质量:检查代码是否遵循了编码规范,是否存在潜在的错误或不合理的设计。
- 可读性:评估代码是否易于理解,变量、函数和类的命名是否清晰。
- 性能:分析代码的执行效率,是否存在性能瓶颈。
- 安全性:检查代码是否存在安全漏洞,如SQL注入、跨站脚本(XSS)等。
测试
测试是验证软件功能、性能和安全性是否符合预期要求的过程。它包括多个层次和类型,如单元测试、集成测试、系统测试和验收测试等。
- 单元测试:针对软件中的最小可测试单元(如函数或方法)进行的测试。
- 集成测试:测试软件模块之间的交互,确保它们能够正确地协同工作。
- 系统测试:将整个软件系统作为一个整体进行测试,验证其是否满足规定的需求。
- 验收测试:由用户或客户进行的测试,以确认软件是否满足他们的需求和期望。
代码审查与测试的关系
代码审查和测试是相互补充的。代码审查可以在代码提交之前发现并修正错误,而测试则可以在代码运行期间验证其功能和性能。结合使用这两种方法可以显著提高软件的质量和开发效率。
- 提高代码质量:通过代码审查,可以及早发现并修正潜在的错误和不合理的设计。而测试则可以进一步验证代码的正确性和稳定性。
- 促进知识共享:代码审查是一个团队成员之间相互学习和交流的过程,有助于提升整个团队的技术水平。测试则可以让团队成员更深入地了解软件的功能和性能。
- 增强软件安全性:代码审查和测试都可以帮助发现软件中的安全漏洞,并及时进行修复,从而增强软件的安全性。
综上所述,代码审查和测试是软件开发过程中不可或缺的两个环节。它们相互补充,共同确保软件的质量、稳定性和安全性。在实际开发过程中,应该充分重视并合理运用这两种方法,以提高软件开发效率和软件产品质量。
3.实践中 的防御性编程
在实际开发中,防御性编程不仅仅是一个技术问题,更是一种编码思维和习惯的养成。以下是一些具体的实践建议:
代码审查定期进行代码审查,发现潜在的错误和问题。通过集体智慧,可以提高代码的质量和健壮性。
编写单元测试单元测试可以帮助验证代码的正确性,捕获边界条件和异常情况。编写全面的单元测试是防御性编程的重要组成部分。
持续学习和改进防御性编程是一门需要不断学习和实践的艺术。通过阅读相关书籍、博客和参加技术讨论,可以不断提高自己的防御性编程水平。
4.总结
通过实施这些防御性编程策略,你可以提高C++代码的质量,减少错误和漏洞,从而创建更可靠、更易于维护的软件,更重要的是减少软件交付的时间,节约开发成本。