构造函数和析构函数的概念
- 构造函数定义及调用
- a) C++中的类可以定义与类名相同的特殊成员函数,这种与类名相同的成员函数叫做构造函数;
- b) 构造函数在定义时可以有参数;
- c) 没有任何返回类型的声明。
- 构造函数的调用
- 自动调用:一般情况下C++编译器会自动调用构造函数,因为编译器可以自动调用构造函数,因此当我们在创建对象为元素的数组时,就不用一个一个地手动初始化,而是编译器在底层为我们把每个元素都初始化好了,这是非常高效便捷的方式;
- 手动调用:在一些情况下则需要手工调用构造函数,这里有个概念就行暂时不表。
2.1 简单构造函数的三种形式
#include "iostream"
using namespace std;
class Test2
{
public:
// 形式1
// 无参数的构造函数
Test2()
{
m_a = 100;
m_b = 101;
cout << "无参数的构造函数" << endl;
}
// 形式2
// 有参数的构造函数
Test2(int a, int b)
{
m_a = a;
m_b = b;
cout << "有参数的构造函数,参数个数为2个" << endl;
cout << "a:" << m_a << "\tb:" << m_b << endl;
}
Test2(int a)
{
cout << "有参数的构造函数,参数个数为1个" << endl;
m_a = a;
m_b = 0;
cout << "a:" << m_a << "\tb:" << m_b << endl;
}
// 形式3
// 用一个对象来初始化新的对象:赋值构造函数/拷贝构造函数
Test2(const Test2& obj)
{
cout << "我也是构造函数" << endl;
}
void printT()
{
cout << "普通成员函数" << endl;
}
protected:
private:
int m_a, m_b;
};
void objPlay()
{
Test2 t1; // 什么都不写,将会调用无参构造函数 C++编译器自动调用构造函数
// 调用有参数的构造函数,有三种方法
// 方法1:括号法
Test2 t2(1, 2);
t2.printT();
// 方法2:
Test2 t3 = 4; //C++编译器自动调用构造函数
Test2 t4 = (3, 4, 6, 8, 90, 100, 12, 99999); // c++对=运算符进行增强,C++编译器自动调用构造函数
// 上面的逗号表达式的意思是,有多个逗号隔开的一串值,这个表达式的值就是最后一个数值
// 方法3:手工调用构造函数
// 这样调用构造函数,将会产生一个匿名对象(匿名对象的去和留老师先作为一个话题抛砖引出来)
Test2 t5 = Test2(98, 99);
}
int main()
{
int nRet = 0;
objPlay();
cout << "hello world.." << endl;
return nRet;
}
2.2 拷贝构造函数
拷贝构造函数使用的四种时机
- 1). 用一个对象区初始化另外一个对象 Test t2 = t1;
- 2). 用一个对象区初始化另外一个对象 Test t2(t1);
#include "iostream"
using namespace std;
class Test
{
public:
Test()
{
m_a = 0;
m_b = 0;
cout << "无参数的构造函数" << endl;
}
Test(int a, int b)
{
m_a = a;
m_b = b;
cout << "输入两个参数的构造函数" << endl;
cout << "m_a: " << m_a << "\tm_b: " << m_b << endl;
}
Test(int a)
{
m_a = a;
m_b = 0;
cout << "输入一个参数的构造函数" << endl;
cout << "m_a: " << m_a << "\tm_b: " << m_b << endl;
}
Test(const Test & obj)
{
m_a = obj.m_a;
m_b = obj.m_b;
cout << "我也是构造函数:拷贝构造函数." << endl;
cout << "m_a: " << m_a << "\tm_b: " << m_b << endl;
}
void printTest()
{
cout << "printTest::普通成员函数" << endl;
cout << "m_a: " << m_a << "\tm_b: " << m_b << endl;
}
protected:
private:
int m_a;
int m_b;
};
int main()
{
int nRet = 0;
Test t0(1, 2); // 用两个输入参数为对象初始化
// 1. 用一个对象区初始化另外一个对象
Test t1 = t0;
// 但是,下面的语句是表示使用对象对另一个对象赋值操作,这是两个不同的概念!!!
// 这里使用的是C++重载的等号运算符
Test t2;
t2 = t0; // 使用t1初始化t2
t2.printTest();
// 2. 用一个对象区初始化另外一个对象 Test t2(t1);
// 这里调用的构造函数和第一种方法调用的是同一个构造函数
Test t3(t0);
cout << "hello world... " << endl;
return nRet;
}
- 3).第三种使用情况就是当函数的形参是一个对象,在调用函数的时候,参数传递的过程也会自动调用拷贝构造函数。
#include "iostream"
using namespace std;
class Location
{
public:
Location(int xx = 0, int yy = 0)
{
// 两个参数的构造函数
cout << "Normal-Constructor function." << endl;
m_x = xx;
m_y = yy;
printObj();
}
Location(const Location & obj)
{
// 拷贝构造函数
cout << "Copy-Contructor function." << endl;
m_x = obj.m_x;
m_y = obj.m_y;
printObj();
}
~Location()
{
cout << "m_x:" << m_x << "\tm_y:" << m_y << "\tObjcet destroyed." << endl;
}
int getX()
{
return m_x;
}
int getY()
{
return m_y;
}
void printObj()
{
cout << "m_x:" << m_x << "\t m_y:" << m_y << endl;
}
protected:
private:
int m_x;
int m_y;
};
void f(Location p)
{
// 这是一个业务函数,形参是一个对象
cout << p.getX() << endl;
}
void playObj()
{
cout << "func playObj()" << endl;
Location a(1, 2);
Location b = a;
cout << "对象b已经初始化完毕" << endl;
// 这里调用业务函数,在传入参数的时候,即把b传给形参p
// 因此会调用拷贝构造函数
f(b);
}
int main()
{
int nRet = 0;
playObj();
cout << "hello world..." << endl;
return nRet;
}
-
4).第四种使用情况:函数的返回值是一个对象,这个对象没有名字所以叫做“匿名对象”,匿名对象怎么处理,就看调用函数的地方怎么接这个对象。
- 有两种处理情况:
-- a. 若返回的匿名对象,赋值给另外一个同类型的对象(即该对象已经声明),那么匿名对象就会被析构;
Location g()
{
Location A(1, 2); // 调用Location的构造函数
return A;
}
void mainObjPlay()
{
Location B;
B = g(); // 用匿名对象赋值给已经声明的B,然后匿名对象被析构
}
PS: 通过在Ubuntu和VS2015中运行同样的代码,我发现了一些小的区别:在VS2015中,程序进入mainObjPlay()后,
> 1) 调用两个参数的构造函数,初始化对象A;
> 2) 在return A;的时候,程序自动调用了拷贝构造函数,使用对象A创建并初始化了一个匿名变量;
> 3) 然后函数结束了,所以A的声明周期结束,调用了A的析构函数;
> 4) 在调用g()的地方没有变量来接返回值,所以这个匿名变量也被析构了。
> 程序运行情况如下:
Normal-Constructor function.
m_x: 1 m_y:3
Copy-Construector function.
m_x:1 m_y:3
m_x:1 m_y:3 Object destroyed.
m_x:1 m_y:3 Object destroyed.
hello world.
请按任意键继续...
但是在Ubuntu中,程序进入mainObjPlay()后,
> 1) 与VS2015相同:调用两个参数的构造函数,初始化对象A;
> 2) return A;的时候,直接调用了析构函数
> 3) mainObjPlay()函数结束
> 4) 程序运行结果如下:
Normal-Constructor function.
m_x:1 m_y:3
m_x:1 m_y:3 Objcet destroyed.
hello world.
这里Ubuntu的执行结果好像有点“预先判断”的意思,调用函数的地方没有接收返回值的对象,那么函数g()也不创建匿名对象了,直接析构了临时变量,就是那个对象A,然后说了句:懒得理你......
-- b. 若返回的匿名对象被使用来初始化另外一个同类型的对象,那么这个匿名对象会直接转换为新的对象
Location g()
{
Location A(1, 2); // 调用Location的构造函数
return A;
}
void mianObjPlay()
{
cout << "00000000000" << endl;
Location B = g(); // 匿名对象会直接转换为新的对象
cout << "11111111111" << endl;
}
VS2015运行结果如下:能够看出Location B = g();这一句,并没有再次调用一个拷贝构造函数来初始化新的对象B,所以是匿名对象直接被转为B。这里做的就是相当于提前消耗了内存空间来获取执行的速率。
设计C++编译器的大牛们就是认为用户想要把返回的对象直接用起来,所以就不再调用拷贝构造函数了。
00000000000
Normal-Constructor function.
m_x: 1 m_y:3 >>>>这里是Location A(1, 2);的结果
Copy-Construector function.
m_x:1 m_y:3 >>>>创建匿名对象,调用了拷贝构造函数
m_x:1 m_y:3 Object destroyed. >>>>临时变量A被析构
11111111111
m_x:1 m_y:3 Object destroyed. >>>>临时变量B被析构
hello world.
请按任意键继续...
还可以继续探索,若接受返回值的对象已经被创建,代码如下:
Location g()
{
Location A(1, 2); // 调用Location的构造函数
return A;
}
void mianObjPlay()
{
Location B;
cout << "00000000000" << endl;
B = g(); // 匿名对象的值赋值给对象B,然后匿名对象就被析构,它的声明周期到此为止
cout << "11111111111" << endl;
}
运行结果:
Normal-Constructor function.
m_x: 0 m_y:0 >>>>这里是Location B;的结果
00000000000
Normal-Constructor function.
m_x: 1 m_y:3 >>>>这里是Location A(1, 2);的结果
Copy-Construector function.
m_x:1 m_y:3 >>>>创建匿名对象
m_x:1 m_y:3 Object destroyed.>>>> 局部变量A被析构
m_x:1 m_y:3 Object destroyed.>>>> 匿名对象在赋值结束后,被析构
11111111111
m_x:1 m_y:3 Object destroyed.>>>> 对象B的生命周期结束,被析构
hello world.
请按任意键继续...
结论1:函数的返回值是一个元素(复杂类型的),返回的是一个新的匿名对象(所以会调用匿名对象类的拷贝构造函数);
结论2:有关匿名对象的去和留:
如果用匿名对象 初始化 另外一个同类型的对象,匿名对象被转正为有名对象;
如果用匿名对象 赋值给 另外一个同类型的对象,匿名对象赋值之后立马被析构。
如果你写了构造函数,那你必须要用,原来编译器提供的默认无参构造函数就被代替了;或者说,你在试图初始化对象的时候,要三思一下,定义类的时候,里面有没有同样参数列表的构造函数?如果没有这样的构造函数,那就需要反思是初始化对象的方式需要调整,还是说需要编写一个具有对应参数列表的构造函数。
- 析构函数定义及调用
- C++中的类可以定义一个特殊的成员函数清理对象,这个特殊的成员函数叫做析构函数;
语法: ~ClassName()
- C++中的类可以定义一个特殊的成员函数清理对象,这个特殊的成员函数叫做析构函数;
- 析构函数没有参数也没有任何返回类型的声明;
- 析构函数在对象销毁时自动被调用, 同一个类的不同对象如果生命周期相同,那么先被创建的对象排在后面释放,因为对象也是存在栈上的。
析构函数调用机制:由C++编译器自动调用
如果当对象的成员变量中存在有malloc的情况时,C++编译器默认的拷贝构造函数会存在风险
#include "iostream"
#include <string.h>
#include <stdio.h>
using namespace std;
class Name
{
public:
Name(const char *myp)
{
int len = strlen(myp);
mLen = len; // mLen保存的是对象的字符串里面有效的字符个数,不包含‘\0’
mP = (char *)malloc(len + 1); // 给\0留一个位置
strcpy(mP, myp);
}
~Name()
{
if (mP != NULL) {
cout << "Destroy Name obj." << endl;
free(mP);
mP = NULL;
mLen = 0;
}
}
void printName()
{
printf("address: %ld\n", mP);
}
protected:
private:
char *mP;
int mLen;
};
// 在下面的函数中,使用默认的拷贝构造函数对obj2进行了初始化,这里的操作是:
// 1)把obj1.mP赋值给obj2.mP;
// 2)把obj1.mLen 赋值给obj2.mLen;
// 但是,因为mP是使用malloc分配的堆空间,随着objPlay()函数运行结束,程序要释放局部变量时,就会启用~Name()
// 来先释放obj2,再释放obj1;
// 可以看到obj1.mP和obj2.mP的值都是一样的,都指向了同一片堆区;
// 那么在释放obj2后释放obj1的时候,free(mP)就会把同一片堆空间释放两次,程序运行就会报错。
// 这就是所谓“浅拷贝”带来的风险。
// 因此,在这种必要的情况下:我们就要手工编写拷贝构造函数
void objPlay()
{
Name obj1("Hello Seiko");
obj1.printName();
Name obj2 = obj1;
obj2.printName();
}
int main()
{
int nRet = 0;
objPlay();
cout << "hello world !!!" << endl;
return nRet;
}
自行编写的拷贝构造函数如下
Name(const Name & obj)
{
mLen = obj.mLen;
mP = (char *)malloc(mLen+1);
strcpy(mP, obj.mP);
}
- 构造函数的初始化列表
使用构造函数的初始化列表可以面对这样的情况, 以保证对象能够被初始化:a)在类class B中包含了一个类class A,b)而且class A中已经设计了有参数的构造函数。那么根据构造函数的调用规则,既然已经有了class A的构造函数,则必须按照构造函数的参数进行调用,如果我们在class B中仍然很简单地使用C++默认的无参数构造函数,则C++编译器在构造class B的对象时,无法构造其中的class A对象(因为没有提供参数啊),那么就会报错。基于这种类中嵌套类的初始化需求,C++提供了构造函数的对象初始化列表。
语法规则为:
Constructor::Constructor() : m1(paramater1), m2(parameter2), m3(parameter3, parameter 4)...
{
// some some assignment operation.
}
对象的初始化列表也可以使用构造函数参数列表里面的形参变量。
PS: 初始化的顺序:
- 1)先执行被组合对象的构造函数;
- 2)如果被组合的对象有多个,则按照定义类时的变量的定义顺序,而不是初始化列表中对象的前后顺序,
- 3) 被组合对象的析构函数调用顺序和构造函数的调用顺序相反。
using namespace std;
class TestA
{
public:
TestA(int a)
{
mA = a;
cout << "TestA 构造函数, mA:" << mA << endl;
}
~TestA()
{
cout << "TestA 析构函数" << endl;
}
protected:
private:
int mA;
};
class TestB
{
public:
// 这里我故意把mTest2写在mTest1的前面,在后面的程序运行结果中可以看到,mTest1比mTest2先初始化
TestB(int p1, int p2, int p3, int p4) : mTestA2(p4), mTestA1(p3)
{
mB1 = p1;
mB2 = p2;
cout << "TestB构造函数:mB1:" << mB1 << "\t mB2: " << mB2 << endl;
}
~TestB()
{
cout << "TestB 析构函数" << endl;
}
protected:
private:
int mB1, mB2;
TestA mTestA1, mTestA2;
};
void objPlay()
{
TestA a1(10) ;
TestB b1(1,2,3,4);
}
int main()
{
int nRet = 0;
objPlay();
cout << "hello world... I am so tired..." << endl;
return nRet;
}
函数的运行结果
TestA 构造函数, mA:10 >>>>TestA a1(10) ;
TestA 构造函数, mA:3 >>>>虽然对象初始化列表里面TestA2在TestA1的前面,但是还是先把TestA1初始化,对应的参数为3
TestA 构造函数, mA:4 >>>>然后按照TestB中定义成员变量的顺序,为TestA2初始化
TestB构造函数:mB1:1 mB2: 2 >>>>初始化TestB自己的变量
TestB 析构函数 >>>>TestB最后被初始化,所以最先调用TestB的析构函数
TestA 析构函数 >>>>析构Test2
TestA 析构函数 >>>>析构Test1
TestA 析构函数 >>>>析构a1
hello world... I am so tired...
最后,还有一个范例示范了使用构造函数要避开的误区:
// 这段代码的目的是展示一个“构造中调用构造”
#include "iostream"
using namespace std;
class myTest
{
public:
myTest(int a, int b, int c)
{
this->a = a;
this->b = b;
this->c = c;
}
myTest(int a, int b)
{
this->a = a;
this->b = b;
// 在构造函数中调用构造函数是一种危险的行为:
// 你调用了,现有的对象没法接;
// 被调用的构造函数也不能帮你做初始化
// 所以每一个构造函数都要做活做全套
// 下面这句不正常的语句会产生两个动作:
// 1)调用ABCD构造函数;
// 2)匿名对象没有目标接收,然后立马被析构函数释放掉,所以下面这句话产生的匿名对象的生命周期只有这一句话
myTest(a, b, 100);
}
~myTest()
{
printf("~myTest: a: %d, b: %d, c: %d\n", a, b, c);
}
protected:
private:
int a, b, c;
public:
int getC() const {return c;}
void setC(int val) {c = val;}
};
int main()
{
int nRet = 0;
myTest t1(1, 2);
printf("c: %d\n", t1.getC());
return nRet;
}
// 运行结果:
#if (0)
~myTest: a: 1, b: 2, c: 100
c: 21945
~myTest: a: 1, b: 2, c: 21945
#endif