构造函数和析构函数
在用类实例化出来一个对象的时候,会默认调用构造函数;在使用完一个对象的时候,会默认调用析构函数。所谓构造函数,顾名思义就是在实例化一个对象的时候,为对象做一些比如赋初值这样的初始化操作;析构函数,就是在对象执行完毕后对象进行销毁,执行一些清理工作。
- 构造函数:
- 没有返回值,连void也不写
- 函数名和类名相同
- 构造函数可以有参数,因此可以发生重载
- 程序在调用对象的时候会自动调用构造函数,无须手动动调用而且只会调用一次;
语法:类名(){}
- 析构函数
- 没有返回值,也不写void
- 函数名称与类名相同,但要在前面加上符号~
- 析构函数不可以有参数,因此不可以发生重载
- 程序在对象销毁前调用析构函数,无须手动调用而且只会调用一次
语法: ~类名(){}
来看一个例子:
class Person
{
public:
//构造函数
Person()
{
cout << "Person 构造函数调用" << endl;
}
~Person()
{
cout << "Person 析构函数调用" << endl;
}
};
int main(void)
{
Person p; //在创建对象的时候会调用构造函数 销毁对象的时候调用析构函数
return 0;
}
注意事项:
- 构造函数和析构要在类外进行访问,所以要写在public作用域上才可以;
- 如果我们没有手写构造和析构函数,编译器会为我们创建两个空的析构和构造函数。
构造函数的分类与调用
构造函数按参数分类有两种,有参构造和无参构造两种。本质上是函数的重载。
有参构造与无参构造
class Person
{
public:
//构造函数
Person() //无参构造(默认构造)
{
cout << "Person 无参构造函数调用" << endl;
}
Person(int a) //有参构造
{
cout << "Person 有参构造函数调用" << endl;
}
};
从上面的例子中很容易看出,有参构造的函数参数列表有参数,无参构造的函数参数列表没有参数。
拷贝构造
所谓拷贝,就是复制的意思,顾名思义就是要复制一个类的属性,创建一个类。参数是另一个类。我们再来创建一个Person类,这次给一个属性Age,来看看如何使用拷贝构造:
class Person
{
public:
//拷贝构造函数
Person(const Person &p)
{
m_Age = p.m_Age; //将传入的类的所有属性,拷贝到自己身上
}
int m_Age;
};
留意一下拷贝构造传入的参数,是一个类的引用,并用const修饰。
调用
了解了拷贝构造的分类以及使用方法,我们来看一段完整的代码,看看如何对它们进行调用
class Person
{
public:
//构造函数
Person(int age)
{
cout << "Person有参构造函数调用:" << endl;
m_Age = age;
}
Person()
{
cout << "Person无参构造函数调用:" << endl;
}
Person(const Person &p)
{
m_Age = p.m_Age;
cout << "Person拷贝构造函数调用:" << endl;
}
//析构函数
~Person()
{
cout << "Person析构函数调用" << endl;
}
int m_Age;
};
int main(void)
{
//1、括号法
Person p1; //调用默认构造函数
Person p2(10); //调用有参构造函数
Person p3(p2); //拷贝构造函数调用 将p2的属性拷贝给p3;
cout << "p2的年龄为:" << p2.m_Age << "岁。" << endl; //10岁
cout << "p3的年龄为:" << p3.m_Age << "岁。" << endl; //10岁
//2、显示法
Person p4 = Person(10); //等号右边如果单成一行代码,就是一个匿名对象
Person p5 = Person(p2);
//3、隐式转换法
Person p6 = 10; //相当于 Person p6 = Person(10); 有参构造
Person p7 = p2; //相当于 Person p7 = Person(p2); 拷贝构造
}
构造函数的初始化调用一共有三种,括号法,显示法,隐式转换法。使用最多的就是括号法了。
注意事项:
- 默认构造的时候不要加()
也就是说Person p1; 会默认调用默认构造,而不能写成Person p1();因为后者会被误认为是函数的声明; - 不要利用拷贝构造函数初始化匿名对象
上述代码中,显示法调用的构造函数例子里面,等号右边就是一个匿名对象。如果等号右边单独写一行,在此行代码执行完毕后,就会销毁掉所实例化的对象。如果利用了拷贝构造初始化匿名对象,编译器会将Person(p3)当成Person p3。
拷贝构造函数的调用时机
- 用已经创建的对象来初始化一个新的对象,这在之前的例子中有所体现。
void test01()
{
Person p1(10); //有参构造
Person p2(p1); //用p1来创建一个与之一摸一样的对象p2
}
- 值传递的方式给函数传值
由于值传递的本质是拷贝出来一个临时的副本,所以传递值的时候会调用拷贝构造函数
void work1( Person p)
{
}
void test02()
{
Person p; //有参构造
work1(p); //拷贝构造
}
- 值方式返回局部对象
值得方式返回对象的时候类似于值传递对象,也会创建临时的副本返回(老师上课讲的意思)。
Person work2() //值得方式返回第一个局部对象
{
Person p1;
cout << (int *)&p1 << endl;
return p1;
}
void test03()
{
Person p1= work2();
cout << (int *)&p1 << endl;
}
这里是有些问题的,因为我的编译环境运行出来的结果和老师的不一样。我运行上段代码发现根本没有调用拷贝构造函数,并且两者的输出的地址结果完全一样,并且是在第二个地址输出后才调用的析构函数。这说明返回一个对象的时候,并没有发生赋值,而是直接把原来的对象返回了,类似于返回引用值。于是我抱着试一试的态度再次写了一段代码来测试:
int func()
{
int a = 10;
cout << (int *)&a << endl;//0x61fddc
return a;
}
void test04()
{
int a = func();
cout << (int *)&a << endl;//0x61fe1c
}
这次的两次的地址不一样。但我觉得这不足以说明返回的时候会拷贝一个值回来,有可能是在接收的时候才发生了拷贝。总结以上两个例子,在返回对象和返回一个局部变量的情形是不一样的。
构造函数的调用规则
一般情况下,编译器至少会给以个类添加三个构造函数:
- 默认构造函数(无参,函数体为空);
- 默认析构函数(无参,函数体为空);
- 默认拷贝构造函数,对属性进行拷贝。
构造函数的调用规则如下:
- 如果用户定义有参数构造函数,编译器不再提供默认无参构造,但是会提供拷贝构造;
- 如果用户定义拷贝构造函数,编译器不再提供其他构造函数。
这里就不再新的举例子了,利用之前的例子就可以做一些验证,如果把Person类中的无参构造函数注释掉,发现无法使用无参构造的方法创建一个对象;如果把拷贝构造函数注释掉,发现依然可以使用拷贝构造的方法来创建一个对象;如果只留下一个拷贝构造函数,把有参构造,无参构造全部注释掉, 发现无法采用有参构造,无参构造的方法创建一个对象。
深拷贝与浅拷贝
- 浅拷贝:简单大赋值拷贝操作,到目前为止的拷贝构造函数都是浅拷贝。
- 深拷贝:在堆区重新申请空间,进行拷贝操作。
我们对之前的例子做些修改,定义一个新的指针类型的"身高"属性。
class Person
{
public:
//有参构造函数
Person(int age, int height)
{
cout << "Person有参构造函数调用:" << endl;
m_Height = new int(height);
m_Age = age;
}
Person()
{
cout << "Person无参构造函数调用:" << endl;
}
~Person()
{
//析构代码,将堆区的数据也做释放操作
if(m_Height!= NULL)
{
delete m_Height;
m_Height = NULL;
}
cout << "Person析构函数调用" << endl;
}
int m_Age; //年龄
int *m_Height; //身高
};
int main(void)
{
Person p1(18, 170);
cout << "p1的年龄为:" << p1.m_Age << " 身高为:" << *p1.m_Height << endl;
Person p2(p1); //浅拷贝带来的问题,堆区内存重复释放。
cout << "p1的年龄为:" << p1.m_Age << " 身高为:" << *p1.m_Height << endl;
return 0;
}
此段代码在执行的过程中会出现错误。由于编译器为我们提供的拷贝构造函数是浅拷贝的,在拷贝的过程中会只是简单的一个值的复制,导致p2的m_Height和p1的m_Height指向同一块堆区内存。而栈区是先进后出的,所以当p2相关代码执行完毕后,会先执行析构代码。然后,p1也会执行一次,由于堆区内存已经被释放了,所以这里会报错,重复释放堆区数据。
我们可以手写拷贝构造函数代码:
Person(const Person &p) //拷贝构造函数
{
m_Age = p.m_Age;
//m_Height = p.m_Height; 编译器默认实现这行代码
//深拷贝
m_Height = new int(*p.m_Height);
cout << "Person拷贝构造函数调用" << endl;
}
在拷贝构造函数中,我们也在堆区来申请另一块内存空间,来存放相同的内容,这样就避免了重复释放,各自释放各自的堆区内容。
- 总结,如果属性有堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的问题。
初始化列表
与构造函数类似,初始化列表也可以用来除初始化属性。
- 语法 构造函数(): 属性1(值1),属性2(值2)...{}
class Person
{
public:
Person() : m_A(10), m_B(20), m_C(30)
{
}
int m_A;
int m_B;
int m_C;
};
int main(void)
{
Person p;
cout << "m_A = " << p.m_A << endl;
cout << "m_B = " << p.m_B << endl;
cout << "m_C = " << p.m_C << endl;
return 0;
}
注意到这中写法有一些局限性,能付的初始值是固定的,所以,我们还可以这样写:
class Person
{
public:
Person(int a, int b, int c) : m_A(a), m_B(b), m_C(c)
{
}
int m_A;
int m_B;
int m_C;
};
int main(void)
{
Person p(30, 20, 9);
cout << "m_A = " << p.m_A << endl;
cout << "m_B = " << p.m_B << endl;
cout << "m_C = " << p.m_C << endl;
return 0;
}
这样就比较灵活了,我们可以随意赋初值,并且可以在函数体内写上其他的代码
类作为对象类成员
之前在点圆的关系那里提到过类的嵌套使用,这里我们来深入了解一下:
//手机类
class Phone
{
public:
Phone(string pName)
{
cout << "Phone的构造函数调用" << endl;
m_PName = pName;
}
~Phone()
{
cout << "Phone 的析构函数调用" << endl;
}
string m_PName; //手机品牌
};
class Person
{
public:
//Phone m_Phone = Pname; 隐式转换法 --> Phone m_Phone = Phone(Pname)
Person(string name, string Pname) : m_Name(name), m_Phone(Pname)
{
cout << "Person的构造函数调用" << endl;
}
string m_Name; //姓名
Phone m_Phone; //手机
~Person()
{
cout << "Person的析构函数调用" << endl;
}
};
int main(void)
{
Person p("张三", "苹果MAX");
cout << p.m_Name << "拿着" << p.m_Phone.m_PName << endl;
return 0;
}
在Person类里面有一个Phone类的成员,那么在创建Person的时候会先创建一个Phone对象,再创建一个Person对象;而析构的时候恰恰相反,先析构Person对象,再析构Phone对象。
静态成员
静态成员就是在成员变量和成员函数前面加上static关键字。
静态成员变量
- 所有对象共享同一份数据;
- 在编译阶段分配内存;
- 类内声明,类外初始化。
class Person
{
public:
static int m_A; //类内声明
private:
static int m_B;
};
int Person::m_A = 100; //类外初始化
int Person::m_B = 200; //类外初始化
int main(void)
{
Person p;
cout << p.m_A << endl; //100
Person p2;
p2.m_A = 200;//创建另一个类,将其修改为200
cout << p.m_A << endl; //200
//静态成员变量不属于任何一个对象,因此有两种访问方式
//1、通过对象进行访问
cout << p.m_A << endl; //100
//2、通过类名进行访问
cout << Person::m_A << endl;
//私有的静态成员变量是不可以在类外进行访问的
// cout<< Person::m_B << endl; //错误写法,类外不可以访问
return 0;
}
静态成员函数
- 所有对象共享同一个函数
- 静态成员函数只能访问静态成员变量
class Person
{
public:
//静态成员函数
static void func()
{
cout << "func的调用:" << endl;
m_A = 100; //静态成员函数可以访问静态成员变量
//m_B = 200; //非静态成员变量,不可以访问非静态成员变量,无法区分到底要访问哪一个对象的m_B成员。
}
static int m_A; //静态成员变量 类内声明
int m_B = 200; //非静态成员变量
private:
static void func2()
{
cout << "func2()的调用" << endl;
}
};
int Person::m_A = 100; //类外初始化
int main(void)
{
//静态成员函数也有两个访问方式
//1、通过对象访问
Person p;
p.func();
//2、通过类名访问
Person::func();
//Person::func2(); //类外不可以访问私有静态成员变量
return 0;
}
静态成员变量无法访问非静态成员函数,原因是静态成员变量是属于某一个被实例化出来的对象的。静态成员函数无法确定是哪一个对象在调用,也就无法确定要修改哪一个对象的成员。