【极客班】《c++面向对象高级编程上第二周》学习笔记

字数 3618阅读 70

第二周讲解的是仍然是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++隐含规则来做事情。

推荐阅读更多精彩内容