文章目录
- 1、对象的浅复制
- 2、构造函数中的操作符重载
- 3、拷贝构造函数不能模板化
- 4、析构函数未捕获异常导致coredump
- 5、构造函数抛出异常
- 6、基类析构函数非虚导致内存泄漏
- 7、删除void*指针引发内存泄露
- 8、成员函数尾部缺失const
- 9、使用memset初始化class
- 10、对象向下转换失败
1、对象的浅复制
class IntList {
public:static const int SIZE = 10;int *items;int numItems;int arraySize;
public:IntList() {cout << "new[]" << endl;items = new int[SIZE];items[0] = 10;numItems = 0;arraySize = SIZE;}~IntList() {cout << "delete[]" << endl;items[0] = 0;items = nullptr;delete[] items;}
// IntList& operator=(const IntList& old) {
// cout << "operator=" << endl;
// this->numItems = old.numItems;
// this->arraySize = old.arraySize;
// memcpy(this->items, old.items, sizeof(int)* this->numItems);
// return (*this);
// }
};int main() {IntList* list1 = new IntList();IntList list2; // 不能写成IntList list2 = *list1; 否则不会调用赋值重载而是会调用拷贝构造list2 = *list1;cout << "before" << endl;cout << list1->items << endl;cout << list1->items[0] << endl;cout << list2.items << endl;cout << list2.items[0] << endl;delete list1;list1 = nullptr;cout << "after" << endl;cout << list2.items << endl;cout << list2.items[0] << endl;return 0;
}
打印结果:
new[]
new[]
before
0xeb5e30
10
0xeb5e30
10
delete[]
after
0xeb5e30
0
delete[]
如果类中没有对“=”操作符重载或没有提供赋值构造函数,那么对象间的复制只是浅复制(shallow copy),浅复制的意思就是C++只会对对象中的每个成员使用赋值运算符。当类很简单的时候(如没有动态分配内存的情况),浅复制不会出问题。但是如果其中一个类成员变量为指针变量,并且指向动态分配的空间,那么在赋值之后,被赋值的对象的指针变量将会指向原对象动态分配的空间,因此原对象被析构之后,被赋值对象的指针就变成悬挂指针。如果我们在一个对象被析构的时候做出指针所指对象清零操作,那被赋值的对象指针所指的值也会被清零。
正确做法应该是增加赋值重载:将浅拷贝变成深拷贝
IntList& operator=(const IntList& old) {cout << "operator=" << endl;this->numItems = old.numItems;this->arraySize = old.arraySize;memcpy(this->items, old.items, sizeof(int)* this->numItems);return (*this);}
然后打印结果:
new[]
new[]
operator=
before
0xf15e30
10
0xf15e60
10
delete[]
after
0xf15e60
10
delete[]
2、构造函数中的操作符重载
这个问题和上面的问题有点相关,问题一的操作符重载的时候我们返回的是一个对象的引用,有没有想过为什么?
假设我们新构造一个类,然后operator=不返回引用,然后再增加一个拷贝构造函数:
class myClass {
private:int data;
public:myClass() {data = 0;}myClass(const myClass& i) {cout << "copy construct" << endl;*this = i; // 这里调用的operator=}myClass operator=(const myClass& i) {cout << "operator=" << endl;data = i.data;return *this;}int get() {return data;}void put(int d) {data = d;}
};int main() {myClass first;first.put(10);myClass second(first);cout << second.get() << endl;return 0;
}
此时打印结果会进入死循环:
copy construct
operator=
copy construct
operator=
....
在 C++中,如果一个函数返回值(非引用)时,会生成一个匿名的临时变量并将函数返回值赋值给匿名的临时变量;如果函数返回引用,则不会生成临时变量。
在拷贝构造函数MyClass(const MyClass &i_class)中,两个对象赋值时会调用操作符重载函数。调用操作符重载函数后会生成临时变量,并把操作符重载函数的返回值赋值给临时变量,这个过程会再次调用构造函数MyClass(const MyClass&i_class)和操作符重载函数……,从而导致反复调用构造函数及操作符重载函数,程序陷入死循环。
正确做法:
只需把操作符重载函数的返回值类型改成返回引用类型即可。
class myClass {
private:int data;
public:myClass() {data = 0;}myClass(const myClass& i) {cout << "copy construct" << endl;*this = i;}myClass& operator=(const myClass& i) {cout << "operator=" << endl;data = i.data;return *this;}int get() {return data;}void put(int d) {data = d;}
};int main() {myClass first;first.put(10);myClass second(first);cout << second.get() << endl;return 0;
}
打印结果:
copy construct
operator=
10
3、拷贝构造函数不能模板化
模板化的构造函数永远不会被编译器当做拷贝构造函数来使用,只会以转换构造函数的形式存在。因此,要想自定义的拷贝构造函数生效,就不能对其模板化。
template<unsigned int size>class myvector
{
private:int* _data;
public:myvector() {cout << "new[]" << endl;_data = new int[size];}~myvector() {cout << "delete[]" << endl;delete[] _data;}// 转换构造函数// 将template<unsigned int size1>类型转换为 template<unsigned int size>template<unsigned int n_size> myvector(const myvector<n_size>& other) {cout << "trans construct" << endl;_data = new int[size];int i = 0;for (; i < size && i < n_size; i++) {_data[i] = other[i];}for(; i < size; i++) {_data[i] = 0;}}// 模板化的拷贝构造 显然不调用这个 template<unsigned int nsize> myvector(const myvector<size>& other) {cout << "copy construct" << endl;_data = new int[size];for (int i = 0; i < size ; i++) {_data[i] = other[i];}}
// myvector(const myvector& other) {
// cout << "copy construct" << endl;
// _data = new int[size];
// for (int i = 0; i < size ; i++) {
// _data[i] = other[i];
// }
// }int& operator[](int i) {return _data[i];}const int& operator[](int i) const { // 表示成员函数隐含传入的this指针为const指针,决定了在该成员函数中,任意修改它所在的类的成员的操作都是不允许的return _data[i];}
};int main()
{myvector<2> vector2;vector2[0] = 1;vector2[1] = 2;// 调用转换构造函数myvector<3> vector3(vector2);for (int i = 0; i < 3; i++) {cout << vector3[i] << " ";}cout << endl; // 1 2 0// 调用了默认的拷贝构造函数myvector<3> other3(vector3);for (int i = 0; i < 3; i++) {cout << other3[i] << " ";}cout << endl; // 1 2 0return 0;
}
打印结果:很显然由于默认拷贝构造只是单纯浅拷贝,导致vector3和other3的成员指针变量_data指向同一片内存。模板类的析构函数先后对vector3和other3对象的成员data_进行delete,导致 double free的内存错误。
new[]
trans construct
1 2 0
1 2 0
delete[]
delete[]
正确的做法:在模板类中加上拷贝构造函数
#include <iostream>
#include <stdio.h>
#include <vector>using namespace std;template<unsigned int size>class myvector
{
private:int* _data;
public:myvector() {cout << "new[]" << endl;_data = new int[size];}~myvector() {cout << "delete[]" << endl;delete[] _data;}// 转换构造函数// 将template<unsigned int size1>类型转换为 template<unsigned int size>template<unsigned int n_size> myvector(const myvector<n_size>& other) {cout << "trans construct" << endl;_data = new int[size];int i = 0;for (; i < size && i < n_size; i++) {_data[i] = other[i];}for(; i < size; i++) {_data[i] = 0;}}myvector(const myvector& other) {cout << "copy construct" << endl;_data = new int[size];for (int i = 0; i < size ; i++) {_data[i] = other[i];}}int& operator[](int i) {return _data[i];}const int& operator[](int i) const { // 表示成员函数隐含传入的this指针为const指针,决定了在该成员函数中,任意修改它所在的类的成员的操作都是不允许的return _data[i];}
};int main()
{myvector<2> vector2;vector2[0] = 1;vector2[1] = 2;// 调用转换构造函数myvector<3> vector3(vector2);for (int i = 0; i < 3; i++) {cout << vector3[i] << " ";}cout << endl; // 1 2 0// 调用了拷贝构造函数myvector<3> other3(vector3);for (int i = 0; i < 3; i++) {cout << other3[i] << " ";}cout << endl; // 1 2 0return 0;
}
打印结果:不会造成内存泄漏
new[]
trans construct
1 2 0
copy construct
1 2 0
delete[]
delete[]
delete[]
TODO:对于模板化的一些函数有点忘了怎么写了,所以这里的copy construct可能存在错误,但是就算没错也不会去调用它的。
4、析构函数未捕获异常导致coredump
class Foo {
private:bool flag;
public:Foo() : flag(false) {}~Foo() {if (flag == true) {cout << "throw Exception1" << endl;throw "Exception1";}}void set_flag(bool value) {flag = value;}
};int main()
{try {bool unexpected_condition = false;Foo foo;foo.set_flag(true);unexpected_condition = true;if (unexpected_condition) {cout << "throw Exception2" << endl;throw "Exception2";}} catch(...) {cout << "Exception caught." << endl;}return 0;
}
打印结果:
terminate called after throwing an instance of 'char const*'
throw Exception2
throw Exception1
对于c++的异常处理机制:
当一个函数发现自己无法处理某个错误时,可以抛出一个异常,由它的调用者捕获和处理,调用者可以决定立即处理还是将问题再次抛出,或者终止程序。
如果抛出的异常未被捕获,则会一直向上传递直到C++自动调用标准库中的terminate函数,默认情况下terminate会再次调用abort函数结束程序,同时生成coredump。
这就牵扯到了栈展开的概念:
当一个异常抛出时,程序控制权从try代码块转移到catch异常处理代码块,C++运行时会调用所有从try语句开始到throw语句之间构造起来的本地自动对象的析构函数,销毁这些对象,回收它们所占用的空间。这个过程被称为“栈展开”(stackunwinding)。栈展开时销毁对象的顺序与构造这些对象的顺序相反。如果在栈展开的过程中,某个对象的析构函数又抛出了异常并且这个异常未被捕获,C++会调用terminate()函数,该函数默认调用abort()函数以非正常方式结束程序。此时程序被异常结束。
上面代码中,main函数中的try代码块抛出异常时,在栈展开的过程中调用foo的析构函数尝试销毁foo,但在foo的析构函数中再次抛出了异常并且未捕获,导致C++调用terminate函数进而调用abort函数异常终止程序,并生成coredump。
C++标准中没有禁止在析构函数中抛出异常,但从设计原则上来讲,析构函数应该杜绝抛出异常。析构函数中抛出异常往往是预示着这可能是一个Bad Design。如果析构函数非要抛出异常,或者调用了其他可能会抛出异常的函数方法,则析构函数应自己捕获这些异常。
5、构造函数抛出异常
class Foo {
private:char* array;
public:Foo(int flag) {array = new char[1024];if (flag) {throw runtime_error("Exception thrown.");}}~Foo() {delete[] array;}
};int main()
{try {Foo foo(10);} catch(const exception& e) {cout << e.what() << endl;}return 0;
}
打印:
Exception thrown.
上面代码中,在Foo的构造函数中动态分配了一个字符数组,随后抛出了一个异常。异常抛出时该动态数组并没有被释放,Foo的析构函数也不会被调用,因而发生内存泄露。
因为在类的构造函数中抛出异常,系统是不会调用它的析构函数的,可能会造成资源泄露,所以,在构造函数中抛出异常前要记得释放已经申请的资源。
class Foo {
private:char* array;
public:Foo(int flag) {array = new char[1024];if (flag) {delete[] array;throw runtime_error("Exception thrown.");}}~Foo() {delete[] array;}
};int main()
{try {Foo foo(10);} catch(const exception& e) {cout << e.what() << endl;}return 0;
}
不光在类的构造函数中,在其他地方,如一个函数中,也可能出现类似的情况。在抛出异常前,必须先释放前面已申请的资源,否则会引起资源泄露。这个资源包括已申请的内存、打开的文件描述符、打开的网络套接字等。
6、基类析构函数非虚导致内存泄漏
定义基类Base和子类Child,声明了一个基类指针base指向一个Child子类对象,这样能方便地利用面向对象编程的多态性。最后,通过该基类指针base删除Child对象。
C++标准规定,当一个派生类对象通过使用一个基类指针删除,如果这个基类的析构函数是非虚的,则删除结果是未知的。现实中大多情况是,子类的析构函数不会被调用,因此,对象的派生部分不会被销毁,引起内存泄露。
将基类的析构函数定义为虚函数。这样,通过删除基类指针释放派生类对象时,派生类的析构函数就会被调用,释放全部内存。
7、删除void*指针引发内存泄露
如果我们delete对象的时候传入的是void*指针
void GeneralDelete(void* ptr){ delete ptr;}
删除void* 类型的对象会导致了内存泄露。
当使用 delete 操作符进行释放对象时,delete 需要根据类型信息正确地释放指针所指向的内存块。
delete的工作可以概括成:
首先调用对象的析构函数,然后释放该对象指针。
在调用对象的析构函数前,首先需要知道该对象的类型。如果不知道该对象的类型,则无法知道该调用谁的析构函数。
由于对象为void*空类型,delete不会调用任何析构函数,所以,构造函数中动态分配的内存并没有被释放,导致内存泄露。
8、成员函数尾部缺失const
一个类的const对象只能使用该类的const方法,建议将所有不会修改对象数据成员的成员函数都声明为const类型。
9、使用memset初始化class
C++中虚函数赖以生存的底层机制:vptr + vtable;
- 编译器为每个包含虚函数的类生成了一个虚函数表,表中放着静态函数指针。在这个类或者其基类中定义的每一个虚函数都有一个相应的函数指针
- 每个包含虚函数的类的对象都包含一个不可见的数据成员vptr:虚函数指针,该指针由构造函数自动初始化,指向类的vtable
- 当调用虚函数时,代码通过vptr索引到vtbl中,然后在指定位置找到函数指针
下面看一段代码:
class Base {
protected:char* m_pcName;
public:virtual void Draw() {}char* Name(char* name) {m_pcName = name;cout << name << endl;return m_pcName;}
};class child : public Base {
public:void Draw() {cout << "draw" << endl;}
};int main()
{Base *obj = new child;memset((void*)obj, 0, sizeof(child));char* name = "1";obj->Name(name);obj->Draw();return 0;
}
Base *obj = new child;时虚函数指针已经自动初始化了,再执行memset会将初始化好的指针清0,因此导致程序调用虚函数draw时出现段错误。
建议:
在C++中对结构或变量进行初始化也可以memset。但不建议使用 memset 对一个类的对象进行初始化,在某些情况下会导致程序crash。
10、对象向下转换失败
父类的对象是不可以向下转换成子类对象的,只有父类指针指向某个子类对象才能进行dynamic_cast转换。不同对象之间的赋值,只允许从下往上赋值,传递被继承的信息。
这涉及到一个准则:
1、可将派生类对象截断,只使用继承来的信息
2、但不能将基类对象加长,无中生有变出派生类对象
class Father{
public:int data;virtual ~Father() {}Father() {data = 0;}virtual int get() {cout << "Father.data:" << data << endl;return data;}void put(int val) {data = val;}
};class Child : public Father {
public:Child() {data = 5;}int get(){cout << "Child.data:" << data << endl;return data;}void putchild(int value){data = value;}
};int main()
{Father* fa = new Father();auto ch = dynamic_cast<Child*>(fa);if (ch == nullptr) {cout << "null" << endl;} else {cout << ch->get() << endl;}return 0;
}
打印结果:
null
在上段代码的 main 函数中,利用 dynamic_cast 操作符把一个父类的指针强制转换成子类指针,然后调用子类的函数。在父类指针向子类指针转换时,由于父类的内存空间小于子类的,因此,父类向子类转换失败。
具体可以参考:
【C++grammar】动态类型转换、typeid与RTTI
(1)type-id和exdivssion必须保持类型一致。即,如果type-id是类指针类型,那么exdivssion也必须是一个指针;如果type-id是一个引用,那么exdivssion也必须是一个引用。(2)父类中必须有虚函数。因为dynamic_cast在转换时会进行类型检查,此时需要运行时类型信息,这些信息存储在类的虚函数表中,而且只有定义了虚函数的类才有虚函数表,因此,要求父类中必须有虚函数。
(3)dynamic_cast 转换指针失败会返回 NULL,如果是引用转换失败,则抛出bad_cast异常。因此,在dynamic_cast后要判断转换是否成功,如果成功才进行类函数调用,否则会出现段错误。