赞
踩
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[]
这个问题和上面的问题有点相关,问题一的操作符重载的时候我们返回的是一个对象的引用,有没有想过为什么?
假设我们新构造一个类,然后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
模板化的构造函数永远不会被编译器当做拷贝构造函数来使用,只会以转换构造函数的形式存在。因此,要想自定义的拷贝构造函数生效,就不能对其模板化。
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 0 return 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 0 return 0; }
打印结果:不会造成内存泄漏
new[]
trans construct
1 2 0
copy construct
1 2 0
delete[]
delete[]
delete[]
TODO:对于模板化的一些函数有点忘了怎么写了,所以这里的copy construct可能存在错误,但是就算没错也不会去调用它的。
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。如果析构函数非要抛出异常,或者调用了其他可能会抛出异常的函数方法,则析构函数应自己捕获这些异常。
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; }
不光在类的构造函数中,在其他地方,如一个函数中,也可能出现类似的情况。在抛出异常前,必须先释放前面已申请的资源,否则会引起资源泄露。这个资源包括已申请的内存、打开的文件描述符、打开的网络套接字等。
定义基类Base和子类Child,声明了一个基类指针base指向一个Child子类对象,这样能方便地利用面向对象编程的多态性。最后,通过该基类指针base删除Child对象。
C++标准规定,当一个派生类对象通过使用一个基类指针删除,如果这个基类的析构函数是非虚的,则删除结果是未知的。现实中大多情况是,子类的析构函数不会被调用,因此,对象的派生部分不会被销毁,引起内存泄露。
将基类的析构函数定义为虚函数。这样,通过删除基类指针释放派生类对象时,派生类的析构函数就会被调用,释放全部内存。
如果我们delete对象的时候传入的是void*指针
void GeneralDelete(void* ptr){ delete ptr;}
删除void* 类型的对象会导致了内存泄露。
当使用 delete 操作符进行释放对象时,delete 需要根据类型信息正确地释放指针所指向的内存块。
delete的工作可以概括成:
首先调用对象的析构函数,然后释放该对象指针。
在调用对象的析构函数前,首先需要知道该对象的类型。如果不知道该对象的类型,则无法知道该调用谁的析构函数。
由于对象为void*空类型,delete不会调用任何析构函数,所以,构造函数中动态分配的内存并没有被释放,导致内存泄露。
一个类的const对象只能使用该类的const方法,建议将所有不会修改对象数据成员的成员函数都声明为const类型。
C++中虚函数赖以生存的底层机制:vptr + vtable;
下面看一段代码:
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。
父类的对象是不可以向下转换成子类对象的,只有父类指针指向某个子类对象才能进行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后要判断转换是否成功,如果成功才进行类函数调用,否则会出现段错误。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。