第二周讲解的是仍然是object-based programming,以String类为例说明包含指针成员的类的写法。
包含指针成员的类需要自己实现三个特殊函数(称为Big Three,在维基百科上被称为Rule of Three ):
1)拷贝构造函数(copy constructor) 2)拷贝赋值操作符函数(copy assignment operator) 3)析构函数(destructor)
本周中给出的函数原型如下:
class String
{
public:
String(const char* cstr=0); //构造函数
String(const String& str); //拷贝构造函数
String& operator=(const String& str); //拷贝赋值操作符
~String(); //析构函数
char* get_c_str() const { return m_data; }
private:
char* m_data;
};
对于不包含指针成员的类,通常不需要编写Big Three,编译器会自动生成这三个函数,这些自动生成的函数会将源对象成员一一拷贝(浅拷贝,对于指针仅拷贝指针的值,不会拷贝所指向内容)到目标对象。
如果使用默认的拷贝构造函数和拷贝赋值操作操作符函数,那么执行拷贝后,两个String对象的指针可能指向同样的内容,另外一个对象指向的内存可能泄露。
String的构造函数实现代码如下:
inline
String::String(const char* cstr)
{
if (cstr) {
m_data = new char[strlen(cstr)+1];
strcpy(m_data, cstr);
}
else {
m_data = new char[1];
*m_data = '\0';
}
}
构造函数中会判断传入字符串是否为空,不空则分配新的空间(大小为给定参数长度+1)给m_data,然后将传入字符串考拷贝给m_data.否则,只需要分配一个字节长度给m_data并初始化成'\0'.
而拷贝构造函数更加特殊,它的用法如下:
String s1("hello");
String s2(s1);
拷贝构造函数语法给构造函数类似,只是其参数是类类型的对象,本例中实现如下:
inline
String::String(const String& str)
{
m_data = new char[ strlen(str.m_data) + 1 ];
strcpy(m_data, str.m_data);
}
这个函数就是分配合适大小空间并将实参str的m_data赋给它的m_data。
由于同一个类的多个对象之间互为友元,所以可以直接访问str实参中的m_data.
拷贝赋值操作符函数用法如下:
String s1("hello");
String s2 = s1;
实现如下:
inline
String& String::operator=(const String& str)
{
if (this == &str)
return *this;
delete[] m_data;
m_data = new char[ strlen(str.m_data) + 1 ];
strcpy(m_data, str.m_data);
return *this;
}
拷贝复制操作符函数中,最前面两句代码判断是否对自己赋值,如果对自己赋值,那么可以直接返回自己,否则,将自己的m_data释放,然后重新分配新空间给自己的m_data并将参数中存储的字符串信息拷贝给自己的m_data.
这个函数中必须考虑自我赋值,最前面两行代码,那么释放m_data后之后这个对象的m_data中就没有有效数据,后面执行再执行strlen就没法得到正确的结果。
在c++,对象可能在分配于不同的区域,例如栈(stack)或者堆(heap)上.堆是操作系统中提供的一块空间,程序可动态分配(通过malloc或者new)从中获取若干空间。普通函数内定义的局部变量通常是stack object(通常称为auto object),在作用域结束后会被自动清理。而栈上可以分配static 对象,栈上的对象在调用该函数时才会被创建,在程序结束时才会被清理掉。在所有函数之外定义的没有static声明的变量被称为全局变量,全局变量在程序执行前会被创建出来,在程序退出前被释放掉。
以上一周Complex类为例说明使用new创建新对象时编译器所做的事情,例如我们使用下面的代码:
Complex *pc = new Complex(1,2);
编译器转化成下面三条语句:
void *mem = operator new(sizeof(Complex));
pc = static_cast<Complex*>(mem);
pc->Complex::Complex(1,2);
其中operator函数内部调用了malloc函数。
在释放pc指针的时候使用下面代码:
delete pc;
编译器会转化成下面两条语句:
Complex::~Complex(pc);
operator delete(pc);
其中operator delete函数内部调用了free(pc)。
使用new动态分配内存时,在VC下编译器会多分配一些空间(下图左边是Complex在debug和release模式下分配堆空间,右边是String对象在debug或release模式下分配的堆空间)
debug模式下会多处32个byte的debug header和4个字节的debug footer。在其前后还有2个描述其结构体大小的字段,注意结构体大小需要是4个字节的倍数,所以可能还需要适当的padding.
并且大小51h的最后一位用于区分是创建或者释放对象,最后一位为1时表示分配对象,最后一位为0是表示释放对象。
下图给出Complex和String使用VC进行栈分配的的结构。
从上面的图可以知道,array new(即分配数组对象)一定要搭配array delete,如下图所示:
对于如果用new分配多个String对象,但是在释放时使用delete p,那么只会调用一次String的析构函数,另外两个String对象的m_data成员指向的内存就被泄露。而对于没有包含指针成员的对象,如果使用new分配多个对象,但是不用array delete来清理指针,那么空间也不会泄露,但是这样不是好的做法,使用array new时一定要搭配array delete.
从同一个类创建出不同对象有不同副本的数据成员成员,而所有函数都只有一个副本。事实上,数据成员和成员函数也可以定义成static,这是所有该类型的对象都只有一个副本。static数据成员需要在类外定义成相应的初始值才能起效。static函数跟普通成员函数的区别在于static成员函数没有this指针。调用static函数可以用直接用对象或者类名来调用。
上面以及第一周所讲解内容都是关于object-based programming(即单个类的设计),而OOP(object-oriented programming)主要包含三个概念:
继承(Inheritance)、复合(Composition)、委托(Delegation).
复合(composition)表示两个类有has-a的关系,其中一个类是另一个类的一部分,比如我们可以说手是身体的一部分。
复合下的构造和析构函数执行顺序如下:
1)构造函数执行从内到外,即先调用作为部分(component)的构造函数,然后调用自身的构造函数
2)析构函数执行从外到内,即先执行自身的西沟函数,然后调用部分(component)的析构函数
委托(Delegation)类似于复合,只是包含指向component的指针(不像复合中包含的是component对象)。
继承(Inheritance)表示两个类是is-a的关系,其构造函数和析构函数的执行顺序如下:
1)构造函数从内到外,即先执行基类的构造函数,后执行自身的构造函数
2)析构函数从外到内,即先执行自身的析构函数,然后执行子类的析构函数。
继承关系下函数可以根据virtual函数的类型分成三类:
1)non virtual function:不希望派生类(derived class)重新定义(override)这个函数
2)virtual function 希望派生类重新定义(override)它,并且已有默认定义
3)希望派生类一定要重新定义(override)它,并且没有默认定义。
virtual函数特别适用于c++应用框架中,开发者根据自己需要重写virual function来完成定制功能。
另外还有继承和复合结合,又分成两种情况:
1)复合类位于基类中,然后基类产生派生类,其构造和析构函数的执行顺序如下;
a.构造函数执行顺序是先component,后基类,最后是派生类
b.析构函数执行顺序是先派生类,后基类,最后是component.
写了个小程序验证,代码如下:
#includeusing namespace std;
class Component{
public:
Component()
{
cout << "component construction" << endl;
}
~Component()
{
cout << "component destruction" << endl;
}
};
class Base{
public:
Base()
{
cout << "base constructor" << endl;
}
~Base()
{
cout << "base destructor" << endl;
}
private:
Component d;
};
class Derived:public Base{
public:
Derived()
{
cout << "derived constructor" << endl;
}
~Derived()
{
cout << "derived destructor" << endl;
}
};
int main(void)
{
Derived d;
return 0;
}
编译链接后输出结果如下:
component construction
base constructor
derived constructor
derived destructor
base destructor
component destruction
2)基类产生派生类,然后复合类位于派生类中,其构造和析构函数的执行顺序如下;
a)构造函数执行顺序是先基类,后compnent,最后是派生类
b)析构函数执行顺序是先派生类,后component,最后是基类。
写了段小程序验证:
#includeusing namespace std;
class Component{
public:
Component()
{
cout << "component construction" << endl;
}
~Component()
{
cout << "component destruction" << endl;
}
};
class Base{
public:
Base()
{
cout << "base constructor" << endl;
}
~Base()
{
cout << "base destructor" << endl;
}
};
class Derived:public Base{
public:
Derived()
{
cout << "derived constructor" << endl;
}
~Derived()
{
cout << "derived destructor" << endl;
}
private:
Component d;
};
int main(void)
{
Derived d;
return 0;
}
编译链接后输出信息如下:
base constructor
component construction
derived constructor
derived destructor
component destruction
base destructor
委托和继承结合可以用观察者(observer)模式来说明。
在review其它同学的学习笔记时,有同学给出跟下面类似的函数返回对象时调用拷贝构造函数实例:
#includeusing namespace std;
class A{
public:
A() { cout << "constructor" << endl; }
A(const A& a)
{
cout << "copy constructor" << endl;
}
A& operator= (const A& a)
{
cout << "copy assignment operator" << endl;
return *this;
}
};
A f()
{
#if 0
A *p = new A;
cout << "before return" << endl;
return *p;
#else
A a;
cout << "before return" << endl;
return a;
#endif
}
int main(void)
{
f();
//A d = f();
//A d(f());
return 0;
}
在这部分代码中,函数f内可以定义局部变量或者定义指针并用new来进行初始化,这两种方式在执行时会有完全不同的效果。
如果像上面的代码直接定义局部变量,那么上面程序执行结果如下:
constructor
before return
这时候,局部变量定义的对象其实相当于被返回值给替代了来进行操作,只需要在创建新对象时调用一次构造函数即可。
但是如果f函数中定义指针使用new来初始化(将代码中的if 0改成if 1即可,注意这样子代码有bug,因为可能产生内存泄露),那么编译执行后结果如下:
constructor
before return
copy constructor
这时候在函数返回前会调用拷贝构造函数,这时候编译器会添加一个隐含的引用类似的参数,并且在return语句执行拷贝构造将局部指针指向对象拷贝给返回的对象。
这两种不同的处理方式在<深度探索C++对象模型>第二章有详细的说明。
另外,在习题中也遇到两个有趣的问题。
第一个是在派生类拷贝构造函数(如果是自己编写)如果没有显式调用基类的拷贝构造函数,那么此时调用的是基类的默认构造函数(可能是自己编写或者系统生成)。但是如果是派生类构造函数是系统生成的,那么会自动调用基类的拷贝构造函数。这个讲解来自于stackoverflow .
写了个小程序来进行验证:
#include <iostream>
using namespace std;
class Base{
public:
Base()
{
cout << "base contructor" << endl;
}
Base(const Base& other)
{
cout << "base copy constructor" << endl;
}
Base& operator=(const Base& other)
{
cout << "copy assignment operator" << endl;
return *this;
}
};
class Derived:public Base{
public:
Derived():Base()
{
cout << "derived contructor" << endl;
}
Derived(const Derived& other)
{
cout << "derived copy constructor" << endl;
}
Derived& operator=(const Derived& other)
{
cout << "copy assignment operator" << endl;
return *this;
}
};
int main(void)
{
cout << "test part 1" << endl;
cout << "create object a" << endl;
Derived a;
cout << "use copy constructor to copy a to another object" << endl;
Derived b(a);
return 0;
}
得到的执行结果是:
test part 1
create object a
base contructor
derived contructor
use copy constructor to copy a to another object
base contructor
赋值操作函数与拷贝构造函数有差别,如果没有在派生类赋值操作函数中调用基类的赋值操作函数,那么最终只会调用派生类的赋值操作函数。看下面例子:
#include <iostream>
using namespace std;
class Base{
public:
Base()
{
cout << "base contructor" << endl;
}
Base(const Base& other)
{
cout << "base copy constructor" << endl;
}
Base& operator=(const Base& other)
{
cout << "base copy assignment operator" << endl;
return *this;
}
};
class Derived:public Base{
public:
Derived():Base()
{
cout << "derived contructor" << endl;
}
Derived(const Derived& other)
{
cout << "derived copy constructor" << endl;
}
Derived& operator=(const Derived& other)
{
//Base::operator=(other);
cout << "derived copy assignment operator" << endl;
return *this;
}
};
int main(void)
{
cout << "test part 1" << endl;
cout << "create object a" << endl;
Derived a,b;
cout << "use copy assignment to another object" << endl;
b = a;
return 0;
}
得到执行结果如下:
test part 1
create object a
base contructor
derived contructor
base contructor
derived contructor
use copy assignment to another object
derived copy assignment operator
总而言之,为了安全起见,应当尽量显式的调用在派生类的构造函数、拷贝构造函数、拷贝复制函数中调用基类的相应函数,避免用c++隐含规则来做事情。