您的位置:首页 > 编程语言 > C语言/C++

读书笔记-Thinking in C++-第11章 引用和拷贝构造函数

2012-10-31 10:12 316 查看
From: http://blog.csdn.net/sailor_8318/article/details/2218284

11、引用和拷贝构造函数

11、引用和拷贝构造函数

C++中的指针

C++中的引用

函数中的引用

参数传递的规则

拷贝构造函数

值传递的参数和返回值

何谓拷贝构造?

默认拷贝构造函数

禁止值传递

指向成员的指针

引用是由编译器自动解析的常量指针。

在C++中,引用是支持操作符重载的重要依据,同时方便的控制函数参数的输入输出。

C++中的指针

C++是一种很强的类型安全语言,在C中不允许随意将一种指针转换为另外一种指针,但其运行void
*类型随意转换,这就使得静悄悄的指针类型转换给系统带来了安全隐患。C++中必须显式的强制类型转换。

C++中的引用

引用通常作为函数的参数列表和返回值,但也可以单独使用。

编译器自动分配一块内存,将其初始化为12,然后将q指向之;关键之处在于任何引用必须指向一块内存,当你访问引用时就是访问那片内存。因此引用类似于指针,但其好处在于不要担心指针未初始化,因为编译器会强制执行。

使用引用的规则如下:

引用在创建的时候必须初始化,而指针可以任何时候初始化;

一旦引用指向一个对象后,其不能再指向另一个对象,但指针可以随意更改指向的对象;

不能有空引用,但可以有Null指针。

函数中的引用

引用的语法更清晰简单,内部做的任何更改都将影响函数体外面的参数。如果返回一个引用,必须确保函数返回时那个对象仍然存在,否则将引用未知的内存。

F的调用好处是方便清晰,明确传递的是指针,g更简单,传递的是地址,但你看不见。

Const引用

上述代码正常运行的前提是参数是非const对象;如果确定函数内部不会更改对象,那么将引用指定为const的可以确保其使用在任何场合。这意味着,对于内建类型,函数不会改变其值,对于用户定义的类型,函数只会调用其const成员函数。另外一个好处是const引用可以接收临时对象作为参数。临时对象可以是其他函数的返回值或者用户显式指定的对象。临时对象总是const的。

F(1)编译出错,因为编译器必须建立引用,其首先分配内存并赋值1,但是这块内存必须是const的,因为你不可再访问他们,这对于任何类型都是这样的。编译器告诉你出错非常有用,因为你做的任何修改将会丢失。

参数传递的规则

通常的规则是传递const引用,这不仅仅是效率的问题,后面还会涉及到的拷贝构造函数

效率问题是因为值传递需要构造和析构的过程,而如果你不修改其值时,传递引用只需要在栈上传递一个地址即可。

拷贝构造函数

通常形如X(X&) (“X of X ref”),其在函数调用时,对于控制值传递的用户类型的对象的参数和返回值非常有用。

值传递的参数和返回值

int f(int x, char c);

int g = f(a, b);

上述代码经过汇编即为

Push b

Push a

Call f()

Add sp,4

Mov g, register a

首先参数从右至左入栈,然后是参数调用,最后调用代码负责清除栈上的参数。但要注意,通过值传递,编译器必须指定参数的大小,这样才能拷贝一份放在栈上,并且其知道返回值的类型,这样才能将其放在寄存器中返回。对于原始数据类型,值按位拷贝就相等于拷贝对象。

大对象的值传递和返回

当通过值传递的方式传递某个类的一个对象时,编译器怎么处理呢?

//: C11:PassingBigStructures.cpp

struct Big {

char buf[100];

int i;

long d;

} B, B2;

Big bigfun(Big b) {

b.i = 100; // Do something to the argument

return b;

}

int main() {

B2 = bigfun(B);

} ///:~

通常编译器会调用一个helper的函数,其将对象的地址和大小给helper,其负责将对象拷贝到栈上。B2的地址也放在栈上了,尽管其不是参数,这需要了解函数调用时编译器的限制。

函数调用的堆栈模型

首先是所有参数入栈,然后调用函数,在函数内部,其在栈上留出一块空间存放函数的局部变量,并保存了函数的返回地址,具体模型如下:

只有这样才能有选择性的获取参数和本地变量而不至于损坏返回地址。返回的时候再清除栈上的参数,即sp减去特定的值。

你可能想也能把返回值放在栈上,返回一个偏移量告诉返回值在栈上的起始位置。

可重入

问题在于C和C++都支持中断和递归调用,中断函数使用堆栈应该是安全的。当试图在栈上保存返回值时,不能访问返回地址以上的栈空间,因此将返回值放在返回地址下面,返回时堆栈指向返回地址,若此时来了中断,那么将保存对应的返回地址和中断的局部变量,这样就覆盖了原来存在栈山的返回值。

可重入意味着任何时候,任何代码都可能成为其他函数的中断者,包括被中断的函数本身。唯一安全的地方是将返回值保存在寄存器中,但是问题在于寄存器可能无法保存大量信息,这样就将返回值的目的地址作为参数放在栈上,由其他函数将返回值拷贝倒目的地址上。

位拷贝和初始化

至此一切正常,对于传递和返回大的数据结构有一套可行的流程。但这仅仅是从一个地方按照位拷贝到另一个地方。对于C++的初始化,不仅仅是拷贝数据的问题。

一个例子,该类通过const的静态变量保存该类对象的个数。

//: C11:HowMany.cpp

// A class that counts its objects

#include <fstream>

#include <string>

using namespace std;

ofstream out("HowMany.out");

class HowMany {

static int objectCount;

public:

HowMany() { objectCount++; }

static void print(const string& msg = "") {

if(msg.size() != 0) out << msg << ": ";

out << "objectCount = "

<< objectCount << endl;

}

~HowMany() {

objectCount--;

print("~HowMany()");

}

};

int HowMany::objectCount = 0;

// Pass and return BY VALUE:

HowMany f(HowMany x) {

x.print("x argument inside f()");

return x;

}

int main() {

HowMany h;

HowMany::print("after construction of h");

HowMany h2 = f(h);

HowMany::print("after call to f()");

} ///:~

after construction of h: objectCount = 1

x argument inside f(): objectCount = 1

~HowMany(): objectCount = 0

after call to f(): objectCount = 0

~HowMany(): objectCount = -1

~HowMany(): objectCount = -2

F函数返回时,count为0,那么肯定出错了;在h传递给x的时候已经返回值传给h2的时候都是按位进行值拷贝的,但是C++的对象,单纯的位拷贝不能完整的初始化一个对象。在x和h2的创建过程中没有调用其构造函数,因此析构三次,构造一次,最后的情况是-2

何谓拷贝构造?

问题出在编译器如何看待从一个现有对象构造新的对象?当用值传递参数和返回对象时,编译器默认只执行了位拷贝而没有进行真正意义上的初始化。另外一个问题在于,当对象中存在指针时,是采用原有值还是指向新的内存?

任何时候编译器需要利用现有对象构建新对象时,定义自己的函数防止进行单纯的位拷贝。这个函数是创建新的对象,因此为构造函数,其唯一的参数是要拷贝的对象;当然不能用值传递,指针传递也不符号从一个对象构造新对象的意义,这时候引用派上用场了,函数称为copy-constructor,形式X(X&)。

//: C11:HowMany2.cpp

// The copy-constructor

#include <fstream>

#include <string>

using namespace std;

ofstream out("HowMany2.out");

class HowMany2 {

string name; // Object identifier

static int objectCount;

public:

HowMany2(const string& id = "") : name(id) {

++objectCount;

print("HowMany2()");

}

~HowMany2() {

--objectCount;

print("~HowMany2()");

}

// The copy-constructor:

HowMany2(const HowMany2& h) : name(h.name) {

name += " copy";

++objectCount;

print("HowMany2(const HowMany2&)");

}

void print(const string& msg = "") const {

if(msg.size() != 0)

out << msg << endl;

out << '/t' << name << ": "

<< "objectCount = "

<< objectCount << endl;

}

};

int HowMany2::objectCount = 0;

// Pass and return BY VALUE:

HowMany2 f(HowMany2 x) {

x.print("x argument inside f()");

out << "Returning from f()" << endl;

return x;

}

int main() {

HowMany2 h("h");

out << "Entering f()" << endl;

HowMany2 h2 = f(h);

//h值传递,先调用拷贝构造函数

// 在退出函数前,需先将x拷贝到外面的返回值处,因此先调用拷贝构造函数,从h copy拷贝,最后才调用x的析构函数

h2.print("h2 after call to f()");

out << "Call f(), no
return value" << endl;

f(h);

// 首先拷贝构造,尽管忽略了返回值,但在调用之前就将返回值的地址入栈了,因此编译器需要创建一个临时对象,在退出之前,将h copy复制到临时对象中,最后释放局部对象h
copy,函数返回后,语句执行完毕,临时对象无用,析构释放

out << "After call to f()" << endl;

按照构造的相反顺序析构

} ///:~

1) HowMany2()

2)h: objectCount = 1

3) Entering f()

4) HowMany2(const HowMany2&)

5)h copy: objectCount = 2

6) x argument inside f()

7)h copy: objectCount = 2

8) Returning from f()

9) HowMany2(const HowMany2&)

10)h copy copy: objectCount = 3

11) ~HowMany2()

12)h copy: objectCount = 2

13) h2 after call to f()

14)h copy copy: objectCount = 2

15) Call f(), no return value

16) HowMany2(const HowMany2&)

17)h copy: objectCount = 3

18) x argument inside f()

19)h copy: objectCount = 3

20) Returning from f()

21) HowMany2(const HowMany2&)

22)h copy copy: objectCount = 4

23) ~HowMany2()

24)h copy: objectCount = 3

25) ~HowMany2()

26)h copy copy: objectCount = 2

27) After call to f()

28) ~HowMany2()

29)h copy copy: objectCount = 1

30) ~HowMany2()

31)h: objectCount = 0

默认拷贝构造函数

在进行值传递参数和返回值时需要拷贝构造函数,因此当你未定义时,编译器将为你生成一个按位拷贝的默认拷贝构造函数。

对于合成的类,下面将演示编译器如何生成默认拷贝构造函数。

//: C11:DefaultCopyConstructor.cpp

// Automatic creation of the copy-constructor

#include <iostream>

#include <string>

using namespace std;

class WithCC { // With copy-constructor

public:

// Explicit default constructor required:

WithCC() {}

WithCC(const WithCC&) {

cout << "WithCC(WithCC&)" << endl;

}

};

class WoCC { //
Without copy-constructor

string id;

public:

WoCC(const string& ident = "") : id(ident) {}

void print(const string& msg = "") const {

if(msg.size() != 0) cout << msg << ": ";

cout << id << endl;

}

};

class Composite {

WithCC withcc; // Embedded objects

WoCC wocc;

public:

Composite() :
wocc("Composite()") {}

void print(const string& msg = "") const {

wocc.print(msg);

}

};

int main() {

Composite c;

c.print("Contents of c");

cout << "Calling Composite copy-constructor"

<< endl;

Composite c2 = c;

// Calls copy-constructor

c2.print("Contents of c2");

} ///:~

Contents of c: Composite()

Calling Composite copy-constructor

WithCC(WithCC&)

Contents of c2: Composite()

因为WithCC使用了自定义的拷贝构造函数,通知编译器要控制构造过程,因此编译器不会为之生成任何构造函数,必须显式指定无参构造函数。WoCC没有无参构造函数,因此其初始化需在Composite的初始化列表中进行。

Composite没有定义拷贝构造函数,编译器为之生成,其中会自动调用WithCC的拷贝构造函数,WithCC(WithCC&)为证;WoCC无拷贝构造函数,编译器生成值拷贝构造函数,并且在Composite中调用,Contents
of c2: Composite()为证,含有相同的内容。

禁止值传递

只有在需要值传递时才需要拷贝构造函数。为了防止编译器为你自动生成拷贝构造函数导致问题,可以声明一个私有的拷贝构造函数,若成员函数或者friend函数不进行值传递的话,甚至不需要定义拷贝构造函数。这样编译器不会为你自动生成,但同时你自己定义的为私有函数,编译器不能调用,因此会告警。

//: C11:NoCopyConstruction.cpp

// Preventing copy-construction

class NoCC {

int i;

NoCC(const NoCC&);
// No definition

public:

NoCC(int ii = 0) : i(ii) {}

};

void f(NoCC);

int main() {

NoCC n;

//! f(n); // Error: copy-constructor called

//! NoCC n2 = n; // Error: c-c called

//! NoCC n3(n); // Error: c-c called

} ///:~

修改外部对象的函数

引用的语法很简单明了,但其可能误导调用者。

char c;

cin.get(c)
看似值传递,但其实c的值被改变了。因此从代码维护的角度看,当你要改变对象的值时,传递对新的指针比较好。否则应该使用const引用,防止在程序内部修改外部变量。

指向成员的指针

指针指向一个地址,而指向成员变量的指针指向类中的某个位置。但问题在于指针需要地址,而类中的成员没有地址,选择某个成员只是意味着偏移量。偏移量结合某个具体的对象时才能产生一个地址。因此在解析指向成员变量的指针时需要指定特定对象,同时需要符号“*”,如:

objectPointer->*pointerToMember = 47;

object.*pointerToMember = 47;

定义一个指针,*表示pointerToMember为一个指针,其指向ObjectClass类中的任意int成员。

int ObjectClass::*pointerToMember;

定义时初始化,取类ObjectClass中成员a的偏移量。具体解析时需要具体对象和此偏移量。

int ObjectClass::*pointerToMember = &ObjectClass::a;

//: C11:PointerToMemberData.cpp

#include <iostream>

using namespace std;

class Data {

public:

int a, b, c;

void print() const {

cout << "a = " << a << ", b = " << b

<< ", c = " << c << endl;

}

};

int main() {

Data d, *dp = &d;

int Data::*pmInt =
&Data::a;

dp->*pmInt = 47;

pmInt = &Data::b;

d.*pmInt = 48;

pmInt = &Data::c;

dp->*pmInt = 49;

dp->print();

} ///:~

指向成员函数的指针

以通常的函数指针为基础,类比指向成员变量的指针,则指向成员函数的指针只需要在fp前加上类名和范围解析符::即可。

int (*fp)(float);

//: C11:PmemFunDefinition.cpp

class Simple2 {

public:

int f(float) const { return 1; }

};

int (Simple2::*fp)(float) const;

int (Simple2::*fp2)(float) const = &Simple2::f;

int main() {

fp = &Simple2::f;

} ///:~

初始化时,&Simple2::f前面的&是必须的,这与普通的函数指针不同。函数参数列表可以省略,重载的机制可以自动解析。

函数指针的好处在于可以改变运行时的行为。通常成员函数是public,而成员变量是私有的。

//: C11:PointerToMemberFunction.cpp

#include <iostream>

using namespace std;

class Widget {

public:

void f(int) const { cout << "Widget::f()/n"; }

void g(int) const { cout << "Widget::g()/n"; }

void h(int) const { cout << "Widget::h()/n"; }

void i(int) const { cout << "Widget::i()/n"; }

};

int main() {

Widget w;

Widget* wp = &w;

void (Widget::*pmem)(int) const = &Widget::h;

(w.*pmem)(1);

(wp->*pmem)(2);

} ///:~

对于普通用户上述方式太复杂,最好将指向成员函数的指针封装起来。只有select是public的,其他函数指针都是私有的,提高安全性,如下:

//: C11:PointerToMemberFunction2.cpp

#include <iostream>

using namespace std;

class Widget {

void f(int) const { cout << "Widget::f()/n"; }

void g(int) const { cout << "Widget::g()/n"; }

void h(int) const { cout << "Widget::h()/n"; }

void i(int) const { cout << "Widget::i()/n"; }

enum { cnt = 4 };

void (Widget::*fptr[cnt])(int) const;

public:

Widget() {

fptr[0] = &Widget::f; //
Full spec required

fptr[1] = &Widget::g;

fptr[2] = &Widget::h;

fptr[3] = &Widget::i;

}

void select(int i, int j)
{

if(i < 0 || i >= cnt) return;

(this->*fptr[i])(j);

}

int count() { return cnt; }

};

int main() {

Widget w;

for(int i = 0; i < w.count(); i++)

w.select(i, 47);

} ///:~

函数指针的个数都是类提供的,这样可以改变类的实现而丝毫不会影响用户的程序,因为个数是通过w.count()自适应的。

fptr[0] = &Widget::f; //
Full spec required

(this->*fptr[i])(j);

尽管在类中,可以不用Widget及this,但指向成员变量或函数的指针格式必须要求带上类名。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: