容易令人迷惑的构造函数
1、用初始值初始化对象
在创建一个对象时,始终要注意到有两件事情要发生:(1)分配空间,(2)初始化。这两件事情的发生有一个先后顺序,很显然,为对象分配空间发生在前,空间分配好后才针对该对象所分配到的空间进行初始化。所以在考虑对象的创建时就要想到:一、对象的空间在什么地方分配,生命期到什么位置结束;二、如何进行初始化,参数是怎么传递的。如果有显式调用构造函数,则表明对象空间一定已经分配。
在对对象进行初始化时,就是通过构造函数来完成初始化工作的。
为构造函数指定实参时有4种形式。
1.1 初始化对象
#define _CRT_SECURE_NO_DEPRECATE 1
//design by ajk
#include <iostream>
using namespace::std;
class Student{
public:
Student(char const *ptrName = "", int id = 0):m_id(id){
ptrName = ptrName ? ptrName:"";
m_sPtrName = new char[strlen(ptrName)+1];
strcpy(m_sPtrName, ptrName);
cout << "Constructing NEW student "
<< m_sPtrName
<< ", id is "
<< m_id << endl;
}
Student(Student const &s){
char *prefix = "COPY OF ";
size_t length = strlen(s.m_sPtrName) + strlen(prefix) + 1;
m_sPtrName = new char[length];
strcpy(m_sPtrName, prefix);
strcat(m_sPtrName, s.m_sPtrName);
m_id = s.m_id;
cout << "Constructing student "
<< m_sPtrName
<< ", id is "
<< m_id << endl;
}
~Student(){
cout << "Destructing the student "
<< m_sPtrName
<< ", id is "
<< m_id << endl;
delete []m_sPtrName;
}
Student& operator=(Student const &s){
char *prefix = "ASSIGNED FROM ";
size_t length = strlen(s.m_sPtrName) + strlen(prefix) + 1;
if(length != strlen(m_sPtrName)+1){
delete []m_sPtrName;
m_sPtrName = new char[length];
}
strcpy(m_sPtrName, prefix);
strcat(m_sPtrName, s.m_sPtrName);
m_id = s.m_id;
cout << "Assigned the student "
<< m_sPtrName
<< ", id is "
<< m_id << endl;
return *this;
}
void GetInfo(){
cout << "Get Info: The Student is "
<< m_sPtrName
<< ", id is "
<< m_id << endl;
}
private:
char *m_sPtrName;
int m_id;
};
Student Func(Student s){
cout << "In the Func()" << endl;
s.GetInfo();
cout << "Leaving the Func()" << endl;
return s;
}
int main(){
Student s1("ajk", 7);
Student s2 = Student("Tony", 8);
Student s3 = "Jack";
Student s4(Student("Summer", 9));
cout << "Back in main()" << endl;
}
(1)
初始化就是要用同等类型的值来初始化新生成的对象。例如:
int i(5);
创建的对象i是int类型,用来初始化的值5也是int类型。
但在
Student s1("ajk", 7);
用来初始化对象s1的一个是字符串”ajk”,另一个是整型7。看起来并不是用同等类型的值来初始化新生成的对象。
实际上这个时候Student s1("ajk", 7)是等价于Student s1(Student("ajk", 7))的,通过构造函数来达到在逻辑上用同等类型的值来对对象进行初始化。
(2)
对于Student s2 = Student("Tony", 8);
这里的符号=表示初始化,而不是赋值。
另外需要注意的是在符号=的右边显示地出现了构造函数,在这里并不会额外生成临时对象s2的首地址会传递给构造函数,构造函数通过此地址直接去初始化s2所在的空间。。在调用构造函数时,对象
(3)
对于Student s3 = "Jack";
在这里符号=的右边是一个字符串,通过匹配会调用“转换构造函数”
Student(char const *ptrName = "", int id = 0);
也就是这里的Student s3 = "Jack";
等价于Student s3 = Student("Tony");
需要特别注意的是:
如果在初始化对象时要象这样Student s3 = "Jack";通过“转换构造函数”来完成,那么只能有一个参数,并且要有构造函数能匹配只有一个参数的情况。这个例子中,虽然此构造函数有两个参数,但是第二个参数id有默认值,所以可以匹配一个参数的情况。
如果不是只有一个参数将不能用此种形式。
例如
Student s3 = ("Jack", 6); //error
这里企图带两个参数,但是在符号=的右边却构成了一个“逗号表达式”。
("Jack", 6)这个逗号表达式的值为6。
下面通过反汇编的代码可以看到,以上四种形式在调用构造函数时,反汇编的代码都是一模一样的。
int main(){
00411A50 push ebp
00411A51 mov ebp,esp
00411A53 push 0FFFFFFFFh
00411A55 push offset __ehhandler$_main (4155F0h)
00411A5A mov eax,dword ptr fs:[00000000h]
00411A60 push eax
00411A61 sub esp,100h
00411A67 push ebx
00411A68 push esi
00411A69 push edi
00411A6A lea edi,[ebp-10Ch]
00411A70 mov ecx,40h
00411A75 mov eax,0CCCCCCCCh
00411A7A rep stos dword ptr es:[edi]
00411A7C mov eax,dword ptr [___security_cookie (419008h)]
00411A81 xor eax,ebp
00411A83 push eax
00411A84 lea eax,[ebp-0Ch]
00411A87 mov dword ptr fs:[00000000h],eax
Student s1("ajk", 7);
00411A8D push 7
00411A8F push offset string "ajk" (417758h)
00411A94 lea ecx,[ebp-18h]
00411A97 call Student::Student (41129Eh)
00411A9C mov dword ptr [ebp-4],0
Student s2 = Student("Tony", 8);
00411AA3 push 8
00411AA5 push offset string "Tony" (417B28h)
00411AAA lea ecx,[ebp-28h]
00411AAD call Student::Student (41129Eh)
00411AB2 mov byte ptr [ebp-4],1
Student s3 = Student("Jack");
00411AB6 push 0
00411AB8 push offset string "Jack" (417744h)
00411ABD lea ecx,[ebp-38h]
00411AC0 call Student::Student (41129Eh)
00411AC5 mov byte ptr [ebp-4],2
Student s4(Student("Summer", 9));
00411AC9 push 9
00411ACB push offset string "Summer" (41776Ch)
00411AD0 lea ecx,[ebp-48h]
00411AD3 call Student::Student (41129Eh)
00411AD8 mov byte ptr [ebp-4],3
cout << "Back in main()" << endl;
比如,对于
Student s1("ajk", 7);
00411A8D push 7
00411A8F push offset string "ajk" (417758h)
00411A94 lea ecx,[ebp-18h]
00411A97 call Student::Student (41129Eh)
首先通过堆栈将参数7,和字符串”ajk”的首地址传送给构造函数。C/C++参数的入栈顺序是从右往左。然后,通过
lea ecx,[ebp-18h]
将对象s1的首地址通过ecx传递给构造函数。
1.2 初始化引用
再看下面的例子,仍然是用初始值来进行初始化,所不同的是这里用了引用。
#define _CRT_SECURE_NO_DEPRECATE 1
//design by ajk
#include <iostream>
using namespace::std;
class Student{
public:
Student(char const *ptrName = "", int id = 0):m_id(id){
ptrName = ptrName ? ptrName:"";
m_sPtrName = new char[strlen(ptrName)+1];
strcpy(m_sPtrName, ptrName);
cout << "Constructing NEW student "
<< m_sPtrName
<< ", id is "
<< m_id << endl;
}
Student(Student const &s){
char *prefix = "COPY OF ";
size_t length = strlen(s.m_sPtrName) + strlen(prefix) + 1;
m_sPtrName = new char[length];
strcpy(m_sPtrName, prefix);
strcat(m_sPtrName, s.m_sPtrName);
m_id = s.m_id;
cout << "Constructing student "
<< m_sPtrName
<< ", id is "
<< m_id << endl;
}
~Student(){
cout << "Destructing the student "
<< m_sPtrName
<< ", id is "
<< m_id << endl;
delete []m_sPtrName;
}
Student& operator=(Student const &s){
char *prefix = "ASSIGNED FROM ";
size_t length = strlen(s.m_sPtrName) + strlen(prefix) + 1;
if(length != strlen(m_sPtrName)+1){
delete []m_sPtrName;
m_sPtrName = new char[length];
}
strcpy(m_sPtrName, prefix);
strcat(m_sPtrName, s.m_sPtrName);
m_id = s.m_id;
cout << "Assigned the student "
<< m_sPtrName
<< ", id is "
<< m_id << endl;
return *this;
}
void GetInfo(){
cout << "Get Info: The Student is "
<< m_sPtrName
<< ", id is "
<< m_id << endl;
}
private:
char *m_sPtrName;
int m_id;
};
Student Func(Student s){
cout << "In the Func()" << endl;
s.GetInfo();
cout << "Leaving the Func()" << endl;
return s;
}
int main(){
Student &s1 = Student("ajk", 1);
s1.GetInfo();
Student &s2(Student("Tony", 2));
s2.GetInfo();
//Student &s3("Jack", 3); ERROR
//Student &s4 = "Summer"; ERROR
cout << "Back in main()" << endl;
}
首先了解引用在编译器中如何处理的。
有如下代码:
int main(){
int i = 9;
int &ref_i = i;
int *ptr_i = &i;
int tmp = ref_i;
}
以下是反汇编后的代码:
int main(){
00411A50 push ebp
00411A51 mov ebp,esp
00411A53 sub esp,0F0h
00411A59 push ebx
00411A5A push esi
00411A5B push edi
00411A5C lea edi,[ebp-0F0h]
00411A62 mov ecx,3Ch
00411A67 mov eax,0CCCCCCCCh
00411A6C rep stos dword ptr es:[edi]
int i = 9;
00411A6E mov dword ptr [i],9
int &ref_i = i;
00411A75 lea eax,[i]
00411A78 mov dword ptr [ref_i],eax
int *ptr_i = &i;
00411A7B lea eax,[i]
00411A7E mov dword ptr [ptr_i],eax
int tmp = ref_i;
00411A81 mov eax,dword ptr [ref_i]
00411A84 mov ecx,dword ptr [eax]
00411A86 mov dword ptr [tmp],ecx
}
以下是内存情况:
0x0012FF34 cc cc cc cc cc cc cc cc
0x0012FF3C 09 00 00 00 cc cc cc cc
0x0012FF44 cc cc cc cc 60 ff 12 00
0x0012FF4C cc cc cc cc cc cc cc cc
0x0012FF54 60 ff 12 00 cc cc cc cc
0x0012FF5C cc cc cc cc 09 00 00 00
0x0012FF64 cc cc cc cc b8 ff 12 00
首先,
int i = 9;
00411A6E mov dword ptr [i],9
为变量i分配的内存空间为首地址0012ff60H开始一个双字,在WIN32中int类型占32位。并初始化为值9。
int &ref_i = i;
00411A75 lea eax,[i]
00411A78 mov dword ptr [ref_i],eax
在定义引用类型的变量ref_i时,会首先给它分配一个双字空间,此双字空间的首地址在0012ff54H。在此双字空间中会用来盛放i的地址。所以,先用lea指令将变量i的地址0012ff60H提取到寄存器eax中。然后通过eax作为中转,将变量i的地址0012ff60H送到ref_i所在的双字空间[0012ff54H]中。
int *ptr_i = &i;
00411A7B lea eax,[i]
00411A7E mov dword ptr [ptr_i],eax
从这里可以看出,引用类型和指针类型实质是一样的。
在定义指针类型的变量ptr_i时,会首先给它分配一个双字空间,此双字空间的首地址在0012ff48H。在此双字空间中会用来盛放i的地址。所以,先用lea指令将变量i的地址0012ff60H提取到寄存器eax中。然后通过eax作为中转,将变量i的地址0012ff60H送到ptr_i所在的双字空间[0012ff48H]中。
int tmp = ref_i;
00411A81 mov eax,dword ptr [ref_i]
00411A84 mov ecx,dword ptr [eax]
00411A86 mov dword ptr [tmp],ecx
首先通过直接寻址方式,将ref_i所在空间中的值(即双字空间[0012ff54H]中的值)0012ff60H送到寄存器eax中。
然后通过寄存器间接寻址方式,将双字空间[0012ff60H]中的值00000009H送到变量tmp所在的双字单元[0012ff3cH]中。
在使用引用的时候,需要注意:
(1)如果有
int i = 9;
int &ref_i = i;
则引用 ref_i 的值就是i的值为9。
(2)对引用做取地址操作,取到的不是它自己的地址,而是初始化它的变量的地址值。
如果取变量i的地址 &i
则 &ref_i 取到的值仍然是 &i 。取到的就是变量i的地址值。
即,引用对程序员是透明的。
取地址时的反汇编代码如下:
unsigned tmp1 = (unsigned)&ref_i;
00411A81 mov eax,dword ptr [ref_i]
00411A84 mov dword ptr [tmp1],eax
所以,引用在使用上跟指针是有很大区别的。
对于指针:
int *ptr_i = &i;
(1)对指针变量取地址,取到的是指针变量本身的地址
&ptr_i 取到的是指针变量 ptr_i 本身的地址值。
(2)对以上定义有
ptr_i 里面盛放的值是 &i ,即变量i的地址值。
(3)*ptr_i 表示取出ptr_i所指向的内存单元里的值。即变量i的值。
对于
int main(){
Student &s1 = Student("ajk", 1);
s1.GetInfo();
Student &s2(Student("Tony", 2));
s2.GetInfo();
//Student &s3("Jack", 3); ERROR
//Student &s4 = "Summer"; ERROR
cout << "Back in main()" << endl;
}
输出结果为:
Constructing NEW student ajk, id is 1
Get Info: The Student is ajk, id is 1
Constructing NEW student Summer, id is 2
Get Info: The Student is Summer, id is 2
Back in main()
Destructing the student Summer, id is 2
Destructing the student ajk, id is 1
反汇编代码如下:
Student &s1 = Student("ajk", 1);
00411A8D push 1
00411A8F push offset string "ajk" (41764Ch)
00411A94 lea ecx,[ebp-24h]
00411A97 call Student::Student (4112BCh)
00411A9C mov dword ptr [ebp-4],0
00411AA3 lea eax,[ebp-24h]
00411AA6 mov dword ptr [ebp-14h],eax
s1.GetInfo();
00411AA9 mov ecx,dword ptr [ebp-14h]
00411AAC call Student::GetInfo (411078h)
Student &s2(Student("Summer", 2));
00411AB1 push 2
00411AB3 push offset string "Summer" (417758h)
00411AB8 lea ecx,[ebp-40h]
00411ABB call Student::Student (4112BCh)
00411AC0 mov byte ptr [ebp-4],1
00411AC4 lea eax,[ebp-40h]
00411AC7 mov dword ptr [ebp-30h],eax
s2.GetInfo();
00411ACA mov ecx,dword ptr [ebp-30h]
00411ACD call Student::GetInfo (411078h)
以下是内存情况:
0x0012FF24 cc cc cc cc e0 5f 3a 00
0x0012FF2C 02 00 00 00 cc cc cc cc
0x0012FF34 cc cc cc cc 28 ff 12 00
0x0012FF3C cc cc cc cc cc cc cc cc
0x0012FF44 08 5f 3a 00 01 00 00 00
0x0012FF4C cc cc cc cc cc cc cc cc
0x0012FF54 44 ff 12 00 cc cc cc cc
分析:
Student &s1 = Student("ajk", 1);
00411A8D push 1
00411A8F push offset string "ajk" (41764Ch)
00411A94 lea ecx,[ebp-24h]
00411A97 call Student::Student (4112BCh)
00411A9C mov dword ptr [ebp-4],0
00411AA3 lea eax,[ebp-24h]
00411AA6 mov dword ptr [ebp-14h],eax
已知ebp=0012ff68H
在定义引用变量 s1 时,为其分配一个双字单元的空间。此空间首地址为ebp-14h = 0012ff54H。在初始化此引用时,是用Student类型的对象来进行初始化。这里有显式调用构造函数,事先在ebp-24h = 0012ff44H处为对象分配好空间。为对象分配空间时,是按其数据成员所占据的空间大小来分配的。Student类型的对象有两个数据成员:
char *m_sPtrName;
int m_id;
第一个是指针,用来存放姓名字符串的地址。在WIN32中指针是32位,占一个双字。
第二个是整型,在WIN32中整型是32位,占一个双字。
所以,Student类型的对象会占据两个双字。
00411A8D push 1
00411A8F push offset string "ajk" (41764Ch)
00411A94 lea ecx,[ebp-24h]
00411A97 call Student::Student (4112BCh)
通过堆栈传递构造函数的两个参数,参数入栈顺序为从右往左。
然后通过ecx传递此对象所在空间的首地址ebp-24H = 0012ff44H。
调用构造函数初始化此对象。
从0012ff44H处开始:第一个双字为char *m_sPtrName的值,为姓名字符串的首地址003a5f08H,第二个双字为int m_id的值,为ID的值1。
00411A9C mov dword ptr [ebp-4],0
此处为对象计数器。
00411AA3 lea eax,[ebp-24h]
00411AA6 mov dword ptr [ebp-14h],eax
将对象的首地址0012ff44H送到引用变量Student &s1所在的单元[0012ff54H]处。
可以看到,这里均不会产生临时对象main()函数结束。。这里产生的对象生命期一直到
1.3 初始化指针
再看下面的例子,仍然是用初始值来进行初始化,所不同的是这里用了指针。
#define _CRT_SECURE_NO_DEPRECATE 1
//design by ajk
#include <iostream>
using namespace::std;
class Student{
public:
Student(char const *ptrName = "", int id = 0):m_id(id){
ptrName = ptrName ? ptrName:"";
m_sPtrName = new char[strlen(ptrName)+1];
strcpy(m_sPtrName, ptrName);
cout << "Constructing NEW student "
<< m_sPtrName
<< ", id is "
<< m_id << endl;
}
Student(Student const &s){
char *prefix = "COPY OF ";
size_t length = strlen(s.m_sPtrName) + strlen(prefix) + 1;
m_sPtrName = new char[length];
strcpy(m_sPtrName, prefix);
strcat(m_sPtrName, s.m_sPtrName);
m_id = s.m_id;
cout << "Constructing student "
<< m_sPtrName
<< ", id is "
<< m_id << endl;
}
~Student(){
cout << "Destructing the student "
<< m_sPtrName
<< ", id is "
<< m_id << endl;
delete []m_sPtrName;
}
Student& operator=(Student const &s){
char *prefix = "ASSIGNED FROM ";
size_t length = strlen(s.m_sPtrName) + strlen(prefix) + 1;
if(length != strlen(m_sPtrName)+1){
delete []m_sPtrName;
m_sPtrName = new char[length];
}
strcpy(m_sPtrName, prefix);
strcat(m_sPtrName, s.m_sPtrName);
m_id = s.m_id;
cout << "Assigned the student "
<< m_sPtrName
<< ", id is "
<< m_id << endl;
return *this;
}
void GetInfo(){
cout << "Get Info: The Student is "
<< m_sPtrName
<< ", id is "
<< m_id << endl;
}
private:
char *m_sPtrName;
int m_id;
};
Student Func(Student s){
cout << "In the Func()" << endl;
s.GetInfo();
cout << "Leaving the Func()" << endl;
return s;
}
int main(){
Student *s1 = &Student("ajk", 1);
s1->GetInfo();
Student *s2(&Student("Tony", 2));
s2->GetInfo();
cout << "Back in main()" << endl;
}
输出结果为:
Constructing NEW student ajk, id is 1
Destructing the student ajk, id is 1
Get Info: The Student is 葺葺葺葺[1], id is 1
Constructing NEW student Tony, id is 2
Destructing the student Tony, id is 2
Get Info: The Student is 葺葺葺葺葺葺葺葺, id is 2
Back in main()
需要注意的是,这两个地方都产生了临时对象。对于:
Student *s1 = &Student("ajk", 1);
指针与引用最大的不同就在于指针不会成为初始化它的对象的“别名”,因此这个地方创建的对象就成为了“无名对象”。然后通过取地址操作将此“无名对象”的地址送给指针变量s1。
“无名对象”的生命期仅持续于创建它的这条语句上。
一但一离开这条语句,“无名对象”的生命也就到了尽头。所以,一离开这条语句后,马上就会对此“无名对象”进行析构。
因此指针s1所指向的空间就被释放掉。
对于s2也是类似的分析。