1. 问题说明
今天在写泛型代码的时候遇到了C/C++最恐怖的事情——非法内存访问,编译器提示没有报错信息,但是在一运行程序就会崩溃或卡死,上网找了很多博客和资料也没有类似的情况,反复排查也找不到可以修改的地方,最后通过调用堆栈信息和查看断点局部变量,最终将错误原因锁定在了析构函数的delete操作符里,如下图所示:
然后经过对析构函数的一顿拷打,终于发现了异常原因——竟然是由于实体类对象和模板类对象之间进行了浅拷贝,导致程序运行结束时分配给这些对象(们)的空间被硬生生析构两次,直接被编译器判定非法内存操作给紧急拦停了,我真的吐。。。
2. 异常代码
模板类文件Vector.h代码:
#pragma once
#include <iostream>
using namespace std;
template <typename T>
class Vector {
public:
// 构造函数
Vector(int size = 128) {
if (size > 0) {
this->len = size;
this->base = new T[this->len];
}
}
// 析构函数
~Vector() {
if (base != NULL) {
delete[] base;
base = NULL;
len = 0;
}
}
// 重载[]
T& operator[](int index) {
return *(base + index);
}
// 打印数据
void printVector() const {
for(int i = 0; i < len; i++) {
cout << *(base + i) << " ";
}
}
private:
T* base;
int len;
};
主函数文件main.cpp代码:
#include <iostream>
#include "Vector.h"
using namespace std;
class Student {
public:
Student() {
this->age = 0;
this->name = "";
this->dept = NULL;
}
Student(int age, string name, const char dept[]) {
int length = sizeof(dept) + 1;
this->dept = new char[length];
this->age = age;
this->name = name;
strcpy_s(this->dept, length, dept);
}
~Student() {
if (dept != NULL) {
delete[] dept;
dept = NULL;
cout << "Student::~Student" << endl;
}
}
// 重载<<符号
friend ostream& operator<<(ostream& os,
const Student& student);
private:
int age;
string name;
char* dept;
};
ostream& operator<<(ostream& os, const Student& student) {
os << student.age << student.name << ",专业:"
<< student.dept << " ";
return os;
}
int main(void) {
Student s1(18, "张鹏", "软件工程");
Student s2(19, "周飞", "土木工程");
Vector<Student> stuVector(2);
stuVector[0] = s1;
stuVector[1] = s2;
stuVector.printVector();
return 0;
}
执行程序后就可以看到异常中断的地方在main.cpp文件里的析构函数~Student()中,当然析构函数这样写本身并没有错误,语法和逻辑都说得通,关键罪魁祸首是stuVector[0] = s1 这一步用的是浅拷贝,析构函数没有想象的那么智能,所以它并不知道这件事,然受直接就对着两个同样的空间连续delete,不得不说C/C++这bug藏的是真够深,如果不能及时发现错误源,估计很多人要和析构函数死磕到底,浪费大量的时间。
3. 解决方法
解决方案有两种,第一种方法就是简单粗暴,对析构函数做个横向升级,不让析构函数对delete过的空间再delete一次,也就是直接在析构函数~Student()里添加个判断机制,代码如下:
~Student() {
if (dept != NULL) {
if (*dept != NULL) {
delete[] dept;
*dept = NULL;
}
dept = NULL;
}
}
第二种方法就是重载operator=,不让Student对象之间相互进行浅拷贝,重载operator=也有种叫法是赋值构造函数,两者是一个概念,重载operator=代码如下:
// 重载=符号
Student operator=(const Student& student) {
this->age = student.age;
this->name = student.name;
this->dept = new char[sizeof(student.dept) + 1];
strcpy_s(this->dept, sizeof(student.dept) + 1, student.dept);
return Student(this->age, this->name, this->dept);
}
记得重载函数写在Student类里。
4. 测试总结
运行上述第二种方式修改后的代码如下图所示:
可以明显看到程序只创建了两个对象,但最后却被析构了四次,当然如果有读者采用第一种方法,只会析构两次,这里就不做单一的展示了。 有时候不得不感慨,C/C++藏bug是真的深。