对象的初始化和清理
构造函数
没有返回值,没有void,函数名称 : 类名相同,可以发生重载,可以有参数
析构函数
没有返回值,没有void,函数名称 : ~类名,不可以发生重载,不可以有参数
系统会默认调用构造函数和析构函数,而且只会调用一次
如果程序员没有提供构造和析构,系统会默认提供,空实现
--------------------------------------------------------------------------------------------------------------------------
构造函数 和 析构函数,必须定义在public里面,才可以调用
---------------------
构造函数的分类及调用
1.1 按照参数分类
1.1.1 无参构造(默认构造) , 有参构造
1.2 按照类型分类
1.2.1 昔通构造函数(无参 或 有参) , 拷贝构造函数
1.3 无参构造写法和调用
1.3.1 Person p1 ; 注意不能写 Person p1() 。 定义p1的后面如果加了一个括号,不会调用默认构造函数,因为编译器认为这个是函数声明
1.4 有参构造写法和调用
1.4.1 Person p2(10)或者 Person p2= Person(10), 前者为括号法,后者为显式调用方法
将匿名对象起了p4 (调用一次有参构造)和 p5(调用一次拷贝构造函数)的名字
1.4.2 Person(10)匿名对象,执行当前行后就会释放这个对象
1.5 拷贝构造函数
1.5.1 Person(const Person &p),const引用避免修改p的属性
1.5.2 Perons p1(p2)或者 Person p1= Person(p2), 分别对应括号法和显式两种方法
1.5.3 不能用拷贝构造函数初始化匿名对象
1.5.3.1如果写成 Person(p1),这种写法等价于 Person p1,为对象的声明
当左值:
1.5.3.2写到右值可以做拷贝构造函数
当右值:
1.6 Person p = 100隐式类型转换,相当于调用 Person p = Person(100)
--------------------------------------------------------------------------------------------------------------------------
拷贝构造函数,Person(const Person &p)
如果去掉引用“&” ,会进入死循环(原因:去掉“&”后,变成了值传递(开辟新的空间),还会调用拷贝构造函数,所以会死循环无限调用拷贝函数)
--------------------------------------------------------------------------------------------------------------------------
拷贝构造函数调用时机
1、用已经创建好的对象来初始化新的对象
2、以值传递的方式给函数参数传值
3、以值方式返回局部对象
release模式下,编译器会做优化
1.
2.
3.
Debug 和 Release模式会有区别
(Release模式编译器会帮我们做优化,构造函数的运行结果看起来可能不是我们希望的):
--------------------------------------------------------------------------------------------------------------------------
构造函数的调用规则
1. 如果提供了有参的构造,那么系统就不会提供默认的构造了,但是会提供拷贝构造函数
2. 如果提供了拷贝构造函数,那么系统就不会提供其他的构造函数了(默认构造函数也不再提供了,如果需要自己写了)
-----------------
构造函数调用规则
默认情况下,c++编译器至少为我们写的类增加3个函数
1.默认构造函数无参,函数体为空
2.默认析构函数无参,函数体为空
3.默认拷贝构造函数,对类中非静态成员属性简单值拷贝
如果用户定义拷贝构造函数,c++不会再提供任何默认构造函数
如果用户定义了普通构造(非拷贝),c++不再提供默认无参构造,但是会提供默认拷贝构造
--------------------------------------------------------------------------------------------------------------------------
深拷贝与浅拷贝
- 系统默认提供的拷贝构造 会进行简单的值拷贝
- 如果属性里有指向堆区空间的数据,那么简单的浅拷贝会导致重复释放内存的异常
- 解决上述问题,需要我们自己提供拷贝构造函数,进行深拷贝
--------------------------------------------------------------------------------------------------------------------------
构造函数和其他函数不同,除了有名字,参数列表,函数体之外还有初始化列表。
初始化列表语法
- 在构造函数后面 + : 属性(值、参数), 属性(值、参数)…
如下代码,实现了3种方法初始化,其中第二种传值(写死)(自定义默认构造),第三种方法传参(自定义有参构造)
--------------------------------------------------------------------------------------------------------------------------
类对象作为成员的案例
- 当B类对象作为A类内部成员属性的时候,构造顺序是先构造A类内部的对象B,然后再构造A类自己
- 析构顺序与构造相反
class Person
{
public:
private:
study study_something;
paly play_something;
}
voide test01()
{
Person p1;
/*
构造函数调用顺序: study 类的默认构造函数 -> paly 类的默认构造函数->Person 类的默认构造函数
析构函数调用顺序:Person 类的默认构造函数 -> paly 类的默认构造函数->study 类的默认构造函数
*/
}
--------------------------------------------------------------------------------------------------------------------------
explicit关键字
- 作用:防止构造函数中的隐式类型转换
c++提供了关键字explicit,禁止通过构造函数进行的隐式转换。声明为explicit的构造函数不能在隐式转换中使用。
[explicit注意]
|
class MyString{ public: explicit MyString(int n){ cout << "MyString(int n)!" << endl; } MyString(const char* str){ cout << "MyString(const char* str)" << endl; } };
int main(){
//给字符串赋值?还是初始化? //MyString str1 = 1; MyString str2(10);
//寓意非常明确,给字符串赋值 MyString str3 = "abcd"; MyString str4("abcd");
return EXIT_SUCCESS; } |
--------------------------------------------------------------------------------------------------------------------------
new 运算符 和 delete运算符
为了在运行时动态分配内存,c在他的标准库中提供了一些函数,malloc以及它的变种calloc和realloc,释放内存的free,这些函数是有效的、但是原始的,需要程序员理解和小心使用。为了使用c的动态内存分配函数在堆上创建一个类的实例,我们必须这样做:
class Person{ public: Person(){ mAge = 20; pName = (char*)malloc(strlen("john")+1); strcpy(pName, "john"); } void Init(){ mAge = 20; pName = (char*)malloc(strlen("john")+1); strcpy(pName, "john"); } void Clean(){ if (pName != NULL){ free(pName); } } public: int mAge; char* pName; }; int main(){
//分配内存 Person* person = (Person*)malloc(sizeof(Person)); if(person == NULL){ return 0; } //调用初始化函数 person->Init(); //清理对象 person->Clean(); //释放person对象 free(person);
return EXIT_SUCCESS; } |
问题(C语言动态分配内存的缺点):
|
c的动态内存分配函数太复杂,容易令人混淆,是不可接受的,c++中我们推荐使用运算符new 和 delete.
new operator
当我们创建数组的时候,总是需要提前预定数组的长度,然后编译器分配预定长度的数组空间,在使用数组的时,会有这样的问题,数组也许空间太大了,浪费空间,也许空间不足,所以对于数组来讲,如果能根据需要来分配空间大小再好不过。
所以动态的意思意味着不确定性。
为了解决这个普遍的编程问题,在运行中可以创建和销毁对象是最基本的要求。当然c早就提供了动态内存分配(dynamic memory allocation),函数malloc和free可以在运行时从堆中分配存储单元。
然而这些函数在c++中不能很好的运行,因为它不能帮我们完成对象的初始化工作。
C++中解决动态内存分配的方案是把创建一个对象所需要的操作都结合在一个称为new的运算符里。当用new创建一个对象时,它就在堆里为对象分配内存并调用构造函数完成初始化。
Person* person = new Person;
相当于:
Person* person = (Person*)malloc(sizeof(Person)); if(person == NULL){ return 0; } person->Init(); |
New操作符能确定在调用构造函数初始化之前内存分配是成功的,所有不用显式确定调用是否成功。
现在我们发现在堆里创建对象的过程变得简单了,只需要一个简单的表达式,它带有内置的长度计算、类型转换和安全检查。这样在堆创建一个对象和在栈里创建对象一样简单。
delete operator
new表达式的反面是delete表达式。delete表达式先调用析构函数,然后释放内存。正如new表达式返回一个指向对象的指针一样,delete需要一个对象的地址。
delete只适用于由new创建的对象。
如果使用一个由malloc或者calloc或者realloc创建的对象使用delete,这个行为是未定义的。因为大多数new和delete的实现机制都使用了malloc和free,所以很可能没有调用析构函数就释放了内存。
如果正在删除的对象的指针是NULL,将不发生任何事,因此建议在删除指针后,立即把指针赋值为NULL,以免对它删除两次,对一些对象删除两次可能会产生某些问题。
class Person{ public: Person(){ cout << "无参构造函数!" << endl; pName = (char*)malloc(strlen("undefined") + 1); strcpy(pName, "undefined"); mAge = 0; } Person(char* name, int age){ cout << "有参构造函数!" << endl; pName = (char*)malloc(strlen(name) + 1); strcpy(pName, name); mAge = age; } void ShowPerson(){ cout << "Name:" << pName << " Age:" << mAge << endl; } ~Person(){ cout << "析构函数!" << endl; if (pName != NULL){ delete pName; pName = NULL; } } public: char* pName; int mAge; };
void test(){ Person* person1 = new Person; Person* person2 = new Person("John",33);
person1->ShowPerson(); person2->ShowPerson();
delete person1; delete person2; } |
用于数组的new和delete
使用new和delete在堆上创建数组非常容易。
//创建字符数组 char* pStr = new char[100]; //创建整型数组 int* pArr1 = new int[100]; //创建整型数组并初始化 int* pArr2 = new int[10]{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
//释放数组内存 delete[] pStr; delete[] pArr1; delete[] pArr2; |
当创建一个对象数组的时候,必须对数组中的每一个对象调用构造函数,除了在栈上可以聚合初始化,必须提供一个默认的构造函数。
/*eg:
以上代码
(new char[100];)会调用100次默认构造函数
(delete[] pStr; ) 会调用100次析构函数
*/
class Person{ public: Person(){ pName = (char*)malloc(strlen("undefined") + 1); strcpy(pName, "undefined"); mAge = 0; } Person(char* name, int age){ pName = (char*)malloc(sizeof(name)); strcpy(pName, name); mAge = age; } ~Person(){ if (pName != NULL){ delete pName; } } public: char* pName; int mAge; };
void test(){ //栈聚合初始化 Person person[] = { Person("john", 20), Person("Smith", 22) }; cout << person[1].pName << endl; //创建堆上对象数组必须提供构造函数 Person* workers = new Person[20]; } |
delete void*可能会出错
如果对一个void*指针执行delete操作,这将可能成为一个程序错误,除非指针指向的内容是非常简单的,因为它将不执行析构函数.以下代码未调用析构函数,导致可用内存减少。
class Person{ public: Person(char* name, int age){ pName = (char*)malloc(sizeof(name)); strcpy(pName,name); mAge = age; } ~Person(){ if (pName != NULL){ delete pName; } } public: char* pName; int mAge; };
void test(){ void* person = new Person("john",20); delete person; } |
问题:malloc、free和new、delete可以混搭使用吗?也就是说malloc分配的内存,可以调用delete吗?通过new创建的对象,可以调用free来释放吗? |
使用new和delete采用相同形式
Person* person = new Person[10]; delete person; |
以上代码有什么问题吗?(vs下直接中断、qt下析构函数调用一次)
使用了new也搭配使用了delete,问题在于Person有10个对象,那么其他9个对象可能没有调用析构函数,也就是说其他9个对象可能删除不完全,因为它们的析构函数没有被调用。
我们现在清楚使用new的时候发生了两件事: 一、分配内存;二、调用构造函数,那么调用delete的时候也有两件事:一、析构函数;二、释放内存。
那么刚才我们那段代码最大的问题在于:person指针指向的内存中到底有多少个对象,因为这个决定应该有多少个析构函数应该被调用。换句话说,person指针指向的是一个单一的对象还是一个数组对象,由于单一对象和数组对象的内存布局是不同的。更明确的说,数组所用的内存通常还包括“数组大小记录”,使得delete的时候知道应该调用几次析构函数。单一对象的话就没有这个记录。单一对象和数组对象的内存布局可理解为下图:
本图只是为了说明,编译器不一定如此实现,但是很多编译器是这样做的。
当我们使用一个delete的时候,我们必须让delete知道指针指向的内存空间中是否存在一个“数组大小记录”的办法就是我们告诉它。当我们使用delete[],那么delete就知道是一个对象数组,从而清楚应该调用几次析构函数。
结论:
如果在new表达式中使用[],必须在相应的delete表达式中也使用[].如果在new表达式中不使用[], 一定不要在相应的delete表达式中使用[]. |
new 运算符 和 delete运算符
- Person * p = new Person 会返回一个Person指针
- 默认调用构造函数,开辟空间,返回不是void* ,不需要强制转换
- delete释放
- new 对象 用void* 去接收,释放不了对象,不会调用析构函数
- new出来的是数组 ,如何释放? delete [] …
- new出来的是数组,肯定会调用默认构造(数组里面有多少个对象,就调用多少次默认构造函数;
其中调用了10次构造函数,delete [] pArray调用10次析构函数)
--------------------------------------------------------------------------------------------------------------------------