C++_12_构造函数和析构函数 - 重写版
构造函数
创建一个对象时,常常需要作某些初始化的工作,例如对数据成员赋初值。注意,类的数据成员是不能在声明类时初始化的,因为类不占用内存,只能在定义对象后使用构造函数初始化。
什么是构造函数
构造函数是特殊的public型成员函数,其特征如下:
1、函数名与类名相同。
2、构造函数无函数返回类型说明。注意是没有而不是void,即什么也不写,也不可写void!构造函数不写返回值,实际上构造函数有返回值,返回的就是构造函数所创建的对象。构造函数可以带有参数,也可以不带参数(不带参数的一般就是默认构造函数,手动创建的一般都会带有参数)
3、在程序运行时,当新的对象被建立,该对象所属的类的构造函数自动被调用,在该对象生存期中也只调用这一次。
4、构造函数可以重载。严格地讲,说明中可以有多个构造函数,它们由不同的参数表区分,系统在自动调用时按一般函数重载的规则选一个执行。
5、构造函数可以在类中定义,也可以在类外定义。(类内声明,类外或者类内定义)
6、如果类说明中没有给出构造函数,则C++编译器自动给出一个默认缺省的构造函数:类名(void) { }; ,当类中显示地声明了任意一个构造函数,系统均不会再自动生成构造函数
构造函数除了自身独有的三个特性以外,其它与普通成员函数类似,可以完成普通函数能完成的所有功能,如函数调用、条件判断、循环、赋值等。
参数初始化
对对象进行初始化方法:
1、构造函数初始化 eg: book(){};
2、参数初始化表 eg: book::book(char *a, double p):title(a),price(p){};
参数初始化表还有一个很重要的作用,那就是为 const 成员变量初始化, const 变量不能在函数体内部初始化:
参数初始化顺序与初始化表列出表量的顺序无关,参数初始化顺序只与成员变量在类中声明的顺序有关。
默认构造函数:是指编译器自动加入并调用的构造函数,不需要直接写,一般没有参数,不要记成了默认参数构造函数,这是两个概念
默认构造函数初始化,会赋值给成员变量随机值
----(针对次有成员变量,公有成员函数默认构造不管,需要另外赋值)
切记:一旦自己定义了构造函数,就不存在默认构造函数了,必须要给变量进行初始化赋值;
默认参数的构造函数 (带有默认参数)
设置成为一个带默认参数的构造函数,如果在创建对象的时候,没有传递实参给该参数 ,则该参数会被默认设置为默认参数值。
带默认参数的构造函数,其默认参数必须置于参数列表的结尾
默认参数的构造函数最好不要重载,容易出现多个构造函数均可使用的冲突问题,导致报错
构造函数对创建对象的限制策略:可以通过手动创建构造带参函数(抑制无参默认构造函数的产生)来使创建的对象正确初始化,当创建的对象的参数和构造函数的参数不匹配的时候,对象是无法被成功创建的
手动调用构造函数方法:
//无参构造函数调用 Test2 t1; //创建对象t1,ti后面没接参数,默认自动调用无参数构造函数 //带参构造函数 //1括号法 Test2 t1(1, 2); //调用参数构造函数 c++编译器自动的调用构造函数 // 2 =号法 Test2 t2 = (3, 4, 5, 6, 7); // = c++对等号符进行功能增强 c++编译器自动的调用构造函数, //参数取最右边的7,逗号表达式取最右边的值,因此是一个参数的构造函数 //3 直接调用构造函数 手动的调用构造函数 Test2 t4 = Test2(1, 2); //匿名对象 (匿名对象的去和留) 抛砖 ....//t4对象的初始化 /*第3种方法要小心,因为这种直接调用构造函数是发生在创建t4对象之前的, 我们知道调用构造函数前一定是先创建一个对象的,显而易见, 在第3种方法下创建了一个匿名对象作为过度, 那么匿名对象在发挥了应有的作用后就留下了一个匿名对象的去留问题*/
转型构造函数
构造函数分为两类:1、不带参数构造函数 (默认构造函数)
2、带参数构造函数 (拷贝构造函数、转型构造函数)
转型构造函数用于类型间的转换,将其他数据类型转变为类的对象类型,转型构造函数只有一个参数
拷贝构造函数
拷贝构造函数就是通过拷贝对象的方式创建一个新对象。
//赋值构造函数 (copy构造函数)
//拷贝构造函数给对象一个修改或运算数据的操作机会
Test4(const Test4& obj ) //当写出“Test4 t2 = t1; 类名 对象1 = 对象2”自动调用
{
cout<<"我也是构造函数————拷贝构造函数 " <<endl;
m_b = obj.m_b + 100; //通过拷贝构造
m_a = obj.m_a + 100;
}
// t0 = t1; //这叫赋值,将对象t1的数据都赋值给对象t2的数据
// Test4 t2 = t1; //这叫初始化,用对象t1初始化对象t2,将对象t1的数据拷贝给t2的数据
//上面的赋值是强制给与,赋值多少就是多少,两个对象数据一样;
//但是拷贝不同,拷贝完了,不强制你接受,你还可以更改,将运算方法写在拷贝构造函数内
拷贝构造函数,两种调用方法:
1、Test4 t2 = t1; //用t1对象来初始化 t2对象
2、Test4 t2(t1); //用t1对象 初始化 t2对象
浅拷贝:浅拷贝只是对指针的拷贝,浅拷贝后两个指针指向同一个内存空间;又称值拷贝,但实际上拷贝是个假象,浅拷贝类似于引用。
深拷贝:深拷贝不仅对指针进行拷贝,而且对指针指向的内容进行拷贝,会重新开辟一块内存空间,经深拷贝后的指针是指向两个不同地址的指针。
当对一个已知对象进行拷贝时,编译系统会自动调用一种构造函数——拷贝构造函数,如果用户未定义拷贝构造函数,则会调用默认拷贝构造函数。当拷贝一个基类指针到派生类时,如果调用系统默认的拷贝构造函数,这时只是对指针进行拷贝,两个指针指向同一个地址,这就会导致指针被分配了一次内存,但内存被释放了两次(两次调用析构函数),造成程序崩溃。所以在对含有指针成员的对象进行拷贝时,必须要自己定义拷贝构造函数,使拷贝后的对象指针成员有自己的内存空间,即进行深拷贝,这样就避免了内存泄漏发生。
//浅拷贝和深拷贝的区别:
// 主要体现在指针上!
//浅拷贝是,直接拷贝出两个指针变量指向同一块内存;(注意内存释放的问题)
//深拷贝是,拷贝出两个指针变量,且会重新申请内存,数据独立;
//浅拷贝
#pragma warning(disable:4996)
#include<iostream>
#include<stdlib.h>
#include<string>
using namespace std;
class Person
{
public:
//无参构造
Person() {
m_name = NULL;
m_num = 0;
cout << "无参构造" << endl;
}
//有参构造
Person(const char *name,int num) {
//为m_name 申请一块内存
cout << "有参构造函数" << endl;
m_name = (char*)calloc(1,strlen(name)+1);
if (m_name == NULL)
{
cout << "构造失败!内存分配失败!" << endl;
return;
}
cout << "有参构造成功申请到内存空间" << endl;
strcpy(m_name,name);
m_num = num;
cout << "有参构造!" << endl;
}
//拷贝构造函数
Person(const Person &ob) { //ob ===> lucy
cout << "拷贝构造函数" << endl;
m_name = (char*)calloc(1,strlen(ob.m_name)+1);
cout << "拷贝构造,空间已经申请!" << endl;
strcpy(m_name,ob.m_name);
m_num = ob.m_num;
}
//通过自定义拷贝构造函数,实现深拷贝(进行空闲的申请)
//析构函数
~Person()
{
cout << "析构函数!" << endl;
if (m_name != NULL)
{
cout << "析构函数u,空间已被释放!" << endl;
free(m_name);
m_name = NULL;
}
}
int asd()
{
cout << "n_name = " << m_name << "\tm_num = " << m_num << endl;
cout << "&n_name = " << &m_name << endl;
return 0;
}
private:
char* m_name;
int m_num;
};
void test1()
{
Person lucy("lucy", 12);
lucy.asd();
Person bob = lucy; //拷贝构造,通过自定义构造函数,实现深拷贝(自己申请内存)
bob.asd();
}
int main()
{
test1();
return 0;
}
//现在存放字符串都不用 char数组了,固定长度,太low
//现在都使用指针来存放字符串,不过就是需要申请内存
#include<iostream>
using namespace std;
class Base
{
public:
Base(); //构造函数声明
~Base();//析构函数声明
private:
};
//构造函数,类外定义
//作用:用户成员数据初始化操作
//注意,类的数据成员是不能在声明类时初始化的,因为类不占用内存,只能在定义对象后使用构造函数初始化。
Base::Base()
{
}
//析构函数类外定义
Base::~Base()
{
}
int main()
{
return 0;
}
初始化参数列表
//构造函数初始化参数列表使用; //1、只能在构造函数中使用; //2、初始化顺序是按照类中声明决定的, //-------》即便初始化参数列表是按照[a,c,b]顺序定义; //-------》但是实际是按照类中声明[a,b,c]顺序进行初始化 //3、const修饰成员变量初始化,不能在构造函数内进行初始化,只能使用初始化参数列表进行初始化 Data::Data(int a ,int b ,int c):m_a(a),dack(100),m_c(c), m_b(b),m_d(c)
/*这里可以替代下面的初始化赋值*/ { cout << "构造函数" << endl; //m_a = a; //m_b = b; //m_c = c; /*dack = 100;*/ //m_d = c; //err m_d被const修饰作为左值,不可修改; }
析构函数
什么是析构函数
当一个对象定义时,C++自动调用构造函数建立该对象并进行初始化,那么当一个对象的生命周期结束时,C++也会自动调用一个函数注销该对象并进行善后工作,这个特殊的成员函数即析构函数(destructor):
1、析构函数名也与类名相同,但必须在前面加上字符‘~’,符号“~”与类名之间可以有空格,如~CGoods()。
2、析构函数无函数返回类型说明,与构造函数在这方面是一样的。但析构函数不带任何参数,因此也不能被重载
3、一个类有一个也只有一个析构函数,这与构造函数不同。析构函数也可以缺省。
4、对象注销时,系统自动调用析构函数。
5、析构函数是先创建(构造)的后释放(析构)
析构函数的使用
对象用完后,系统会自动调用的,不用烦了。但是,动态对象要记着自己主动释放!
对于不同作用域的对象类型,构造函数和析构函数的调用如下:
对全局定义的对象,当程序进入入口函数main之前对象就已经定义,这时要调用构造函数。整个程序结束时调用析构函数。
对于局部定义的对象,每当程序控制流到达该对象定义处时,调用构造函数。当程序控制走出该局部域时,则调用析构函数。
对于静态局部定义的对象,在程序控制首次到达该对象定义处时,调用构造函数。当整个程序结束时调用析构函数。
对象成员函数与初始化参数列表
对象成员,必须借助构造初始化参数列表,来调用有参构造,不然调用不了有参构造。
#include <iostream>
using namespace std;
class A
{
private:
int m_a;
public:
A()
{
cout << "A无参构造函数" << endl;
}
A(int a)
{
m_a = a;
cout << "A有参构造函数" << endl;
}
~A()
{
cout << "A析构函数" << endl;
}
};
class B
{
private:
int m_b;
public:
B()
{
cout << "B无参构造函数" << endl;
}
B(int b)
{
m_b = b;
cout << "B有参构造函数" << endl;
}
~B()
{
cout << "B析构函数" << endl;
}
};
class My_Data
{
private:
A oba;//对象成员
B obb;//对象成员
int data;//基本类型成员
public:
My_Data()
{
cout << "Data无参构造" << endl;
}
//初始化列表:对象名+() 显示调用 调用对象成员的构造函数
My_Data(int a, int b, int c) :oba(a), obb(b), data(c)
{
//data =c;
cout << "My_Data有参构造" << endl;
}
//A有参构造函数
//B有参构造函数
//My_Data有参构造
//My_Data析构函数
//B析构函数
//A析构函数
~My_Data()
{
cout << "My_Data析构函数" << endl;
}
};
void test12_4()
{
//先调用 对象成员的构造-->自己的构造函数-->析构自己--->析构对象成员
//Data ob1;
//系统默认调用的是 对象成员的无参构造
//必须在Data的构造函数中 使用初始化列表 使其对象成员 调用有参构造
My_Data ob2(10, 20, 30);
}
int main(int argc, char* argv[])
{
test12_4();
return 0;
}
explict 关键字
//explicit 该有参构造函数 不允许 隐式转换
void test12_5()
{
//隐私转换 这种赋值 Data12_5 = 10 的最终会被隐式转换成 Data12_5(10)
//Data12_5 Data12_5 = 10;//explicit 该有参构造函数 不允许 隐式转换
Data12_5 Data12_5(10);//ok
Data12_5.showNum();
}