《C++编程思想》 第十章 引用和拷贝构造函数(知识点+习题+解答)

一.相关知识点

使用引用时有一定的规则:

1) 当引用被创建时,它必须被初始化。(指针则可以在任何时候被初始化。)

2) 一旦一个引用被初始化为指向一个对象,它就不能被改变为对另一个对象的引用。(指针则可以在任何时候指向另一个对象。)

3) 不可能有NULL引用。必须确保引用是和一块合法的存储单元关连。

仅当准备用传值的方式传递类对象时,才需要拷贝构造函数。如果不需要这么做,就不要拷贝构造函数。

对于指向一个对象的指针新的语法变为 ->*,对于一个对象或引用则为.*。

二.相关代码

1.

/*REFRNCE.cpp*/
#include <iostream>
using namespace std;
//对函数f()的调用缺乏使用引用的方便性和清晰性,但很清楚这是传递一个地址。
//在函数 g()的调用中,地址通过引用被传递,但表面上看不出来。
int* f(int* x)
{
	(*x)++;
	return x;
}

int& g(int& x)
{
	x++;
	return x;//safe;outside this scope
}

int& h()
{
	int q;
	//!return q;
	static int x;
	return x;//safe;x lives outside scope
}

int main()
{
	int A = 0;
	f(&A);
	g(A);

	return 0;
}

2.

/*在函数参数中使用常量引用特别重要。这是因为我们的函数也许会接受临时的对象,这个
临时对象是由另一个函数的返回值创立或由函数使用者显式地创立的。临时对象总是不变的,
因此如果不使用常量引用,参数将不会被编译器接受。*/
/*PASCONST.cpp*/
#include <iostream>
using namespace std;
//调用f(1)会产生一个编译错误,这是因为编译器必须首先建立一个引用。即编译器为一个
//int类型分派存储单元,同时将其初始化为 1并为其产生一个地址和引用捆绑在一起。存储的内
//容必须是常量,因为改变它将使之变得没有意义。对于所有的临时对象,必须同样假设它们是
//不可存取的。当改变这种数据的时候,编译器会指出错误,这是非常有用的提示,因为这个改
//变会导致信息丢失。
void f(int&)
{}

void g(const int&)
{}

int main()
{
	//!f(1);//ERROR
	g(1);

	return 0;
}

3.

/*REFPTR.cpp*/
//指针本身增加,而不是指向的内容增加
#include <iostream>
using namespace std;

void increment(int*& i)
{
	i++;
}

int main()
{
	int* i = 0;
	cout << "i = " << i << endl;
	increment(i);
	cout << "i = " << i << endl;

	return 0;
}

4.

/*PASSTRUC.cpp*/
#include <iostream>
using namespace std;
//无法完成预期结果
struct big
{
	char buf[100];
	int i;
	long d;
}B, B2;

big bigfun(big b)
{
	b.i = 100;
	return b;
}

int main()
{
	B2 = bigfun(B);

	return 0;
}

5.

/*一个类在任何时候知道它存在多少个对象*/
/*HOWMANY.cpp*/
#include <fstream.h>
ofstream out("howmany.out");

class howmany
{
	static int object_count;
public:
	howmany()
	{
		object_count++;
	}
	static void print(const char* msg = "")
	{
		if(msg)
		{
			out << msg << ": ";
		}
		out << "object_count = "
			<< object_count << endl;
	}
	~howmany()
	{
		object_count--;
		print("~howmany()");
	}
};

int howmany::object_count = 0;

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()");

	return 0;
}

6.

/*HOWMANY2.cpp*/
#include <fstream.h>
#include <string.h>
ofstream out("howmany2.out");

class howmany2
{
	enum
	{
		bufsize = 30
	};
	char id[bufsize];//字符缓冲器id起着对象识别作用,所以可以
                     //判断被打印的信息是哪一个对象的
	static int object_count;
public:
	howmany2(const char* ID = 0)
	{
		if(ID)
		{
			strncpy(id, ID, bufsize);
			//strncpy()只拷贝一定数目的字符,这是为防止
            //超出缓冲器的限度。
		}
		else
		{
			*id = 0;
		}
		++object_count;
		print("howmany2()");
	}
	howmany2(const howmany2& h)
	//其次是拷贝构造函数howmany2(howmany2&)。拷贝构造函数可以仅从现有的对象创立新
    //对象,所以,现有对象的名字被拷贝给id,id后面跟着单词“copy”,这样我们就能了
	//解它是从哪里拷贝来的。
	{
		strncpy(id, h.id, bufsize);
		strncat(id," copy", bufsize - strlen(id));
		++object_count;
		print("howmany2(howmany2&)");
	}
	void print(const char* msg = 0) const
	//print()函数必须存取具体对象的id数据,所以不再是static成员函数
	{
		if(msg)
		{
			out << msg << endl;
		}
		out << '\t' << id << ": "
			<< "object_count = "
			<< object_count << endl;
	}
	~howmany2()
	{
		--object_count;
		print("~howmany2()");
	}
};

int howmany2::object_count = 0;

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);
	h2.print("h2 after call to f()");
	out << "call f(), no return value" << endl;
	f(h);
	out << "after call to f()" << endl;

	return 0;
}

7.

/*这是预处理器仍然有用的另一个例子,因为 _ FILE_和_LINE_指示仅和预处理器一
起起作用并用在assert( )宏里。假如assert( )宏在一个错误函数里被调用,它仅打
印出错函数的行号和文件名字而不是调用错误函数。这儿显示了使用宏联接(许多是
assert()方法)函数的方法,紧接着调用assert()(程序调试成功后这由一个#define
 NDEBUG消除)。*/
/*ALLEGE.h*/
#ifndef ALLEGE_H_
#define ALLEGE_H_
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

inline void allege_error(int val, const char* msg)
//函数allege_error()有两个参数:一个是整型表达式的值,另一个是这个值为
//false时需打印的消息
{
	if(!val)
	{
		fprintf(stderr, "error: %s\n", msg);
		//函数fprintf()代替iostreams是因为在只有少量错误的情况下,它工作得
		//更好。假如这不是为调试建立的,exit(1)被调用以终止程序。
#ifdef NDEBUG
		exit(1);
#endif
	}
}

//allege()宏使用三重if-then-else强迫计算表达式expr求值。在宏里调用了
//allege_error(),接着是assert(),所以我们能在调试时获得 assert()的好处
//——因为有些环境紧密地把调试器和assert()结合在一起。
#define allege(expr, msg){	allege_error((expr)?1:0, msg);		assert(expr);}

#define allegemem(expr)	allege(expr, "out of memory")
//allegefile( )宏是allege( )宏用于检查文件的专用版本

#define allegefile(expr)    allege(expr, "could not open file")
//allegemen()宏是allege( )宏用于检查内存的专用版本

#endif

/*LINENUM.cpp*/
#include <fstream.h>
#include <strstrea.h>
#include <stdlib.h>
#include "allege.h"

int main(int argc, char* argv[])
{
	if(argc < 2)
	{
		cerr << "usge: linenum file\n"
			 << "adds line numbers to file"
			 << endl;
		exit(1);
	}
	strstream text;
	{
		ifstream in(argv[1]);
		allegefile(in);
		text << in.rdbuf();
	}
	ofstream out(argv[1]);
	const bsz = 100;
	char buf[bsz];
	int line = 0;
	while(text.getline(buf, bsz))
	{
		out.setf(ios::right, ios::adjustfield);
		out.width(2);//行号以右对齐方式按2个字段宽打印,所以输出仍然按原来的方式排列
		out << ++line << ")" << buf << endl;
	}

	return 0;
}

8.

/*当编译器为我们的新类创建缺省拷贝构造函数
时编译器干了那些事*/
/*AYTOCC.cpp*/
#include <iostream.h>
#include <string.h>

class withCC
{
public:
	withCC()
	{}
	withCC(const withCC&)
	{
		cout << "withCC(withCC&)" << endl;
	}
};

class woCC
{
	enum
	{
		bsz = 30
	};
	char buf[bsz];
public:
	woCC(const char* msg = 0)
	{
		memset(buf, 0, bsz);
		if(msg)
		{
			strncpy(buf, msg, bsz);
		}
	}
	void print(const char* msg = 0) const
	{
		if(msg)
		{
			cout << msg << ": ";
		}
		cout << buf << endl;
	}
};

class composite
{
	withCC WITHCC;
	woCC WOCC;
public:
	composite():WOCC("composite()"){}
	void print(const char* msg = 0)
	{
		WOCC.print(msg);
	}
};

int main()
{
	composite c;
	c.print("contents of c");
	cout << "calling composite copy-constructor"
		 << endl;
	composite c2 = c;
	c2.print("contents of c2");

	return 0;
}

9.

/*有一个简单的技术防止通过传值方式传递:声明一个私有(private)拷贝构造函数。我们
甚至不必去定义它,除非我们的成员函数或友元(friend)函数需要执行传值方式的传递。如
果用户试图用传值方式传递或返回对象,编译器将会发出一个出错信息。这是因为拷贝构造函
数是私有的。因为我们已显式地声明我们接管了这项工作,所以编译器不再创建缺省的拷贝构
造函数。*/
/*STOPCC.cpp*/
#include <iostream>
using namespace std;

class noCC
{
	int i;
	noCC(const noCC&);
public:
	noCC(int I = 0):i(I){}
};

void f(noCC);

int main()
{
	noCC n;
	//!f(n);
	//!noCC n2 = n;
	//!noCC n3(n);

	return 0;
}

10.

/*PMEM.cpp*/
#include <iostream>
using namespace std;

class widget
{
public:
	void f(int);
	void g(int);
	void h(int);
	void i(int);
};

void widget::h(int){}

int main()
{
	widget w;
	widget* wp = &w;
	void (widget::*pmem)(int) = &widget::h;
	(w.*pmem)(1);
	(wp->*pmem)(2);

	return 0;
}

11.

/*PMEM2.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
	{
		count = 4
	};
	void (widget::*fptr[count])(int) const;
public:
	widget()
	{
		fptr[0] = &widget::f;
		fptr[1] = &widget::g;
		fptr[2] = &widget::h;
		fptr[3] = &widget::i;
	}
	void select(int I, int J)
	{
		if(I < 0 || I >= count)
		{
			return;
		}
		(this->*fptr[I])(J);
	}
	int Count()
	{
		return count;
	}
};

int main()
{
	widget w;
	for(int i = 0; i < w.Count(); ++i)
	{
		w.select(i, 47);
	}

	return 0;
}

三.习题+题解

1. 写一个函数,这个函数用一个 char&作参数并且修改该参数。在 main( )函数里,打印一个char变量,使用这个变量做参数,调用我们设计的函数。然后,再次打印此变量以证明它已被改变。这样做是如何影响程序的可读性的?

#include <iostream>
using namespace std;

void fun(char& c)
{
	c = 'b';
}

int main()
{
	char a = 'a';
	cout << a << endl;
    fun(a);
	cout << a << endl;

	return 0;
}

无法得知是传引用来改变,会误以为是传值改变。

2. 写一个有拷贝构造函数的类,在拷贝构造函数里用cout自我声明。现在,写一个函数,这个函数通过传值方式传入我们新类的对象。写另一个函数,在这个函数内创建这个新类的局部对象,通过传值方式返回这个象。调用这些函数以证明通过传值方式传递和返回对象时,拷贝构造函数确实悄悄地被调用了。

#include <iostream>
using namespace std;

class A
{
public:
	A()
	{}
	A(const A& a)
	{
		cout << "A::A(const A& a)" << endl;
	}
	~A()
	{}
};

void fun(A a)
{}

A func()
{
	A b;
	return b;
}

int main()
{
	A a;
	fun(a);
    func();

	return 0;
}

测试结果:

拷贝构造函数确实被调用了。

3. 努力发现如何使得我们的编译器产生汇编语言,并请为PASSTRUC.CPP产生汇编代码。跟踪和揭示我们的编译器为传递和返回大结构产生代码的方法。

产生的汇编代码如下:

版权声明:本文为博主原创文章,未经博主允许不得转载。

时间: 2024-10-11 21:44:17

《C++编程思想》 第十章 引用和拷贝构造函数(知识点+习题+解答)的相关文章

《C++编程思想》 第十一章 运算符重载 (习题+解答)

一.相关代码 1. /*运算符重载语法*/ /*OPOVER.cpp*/ /*这两个重载的运算符被定义为内联成员函数.对于二元运算符,单个参数是出现在运算符 右侧的那个.当一元运算符被定义为成员函数时,没有参数.成员函数被运算符左侧的对象调 用. 对于非条件运算符(条件运算符通常返回一个布尔值),如果两个参数是相同的类型,希 望返回和运算相同类型的对象或引用.如果它们不是相同类型,它作什么样的解释就取决于程 序设计者.用这种方法可以组合复杂的表达式: K += I + J ; 运算符+号产生一个

《C++编程思想》 第七章 常 量 (习题+解答)

一.相关知识点        在 C语言中可以选择这样书写: const bufsize:        这样写在C++中是不对的,而 C编译器则把它作为一个声明,这个声明指明在别的地方有存储分配.因为C默认const是外部连接的, C++默认const是内部连接的,这样,如果在 C++中想完成与C中同样的事情,必须用extern把连接改成外部连接: extern const bufsize;//declaration only 这种方法也可用在C语言中. 指向const的指针 使用指针定义的技

《C++编程思想》第三章 隐藏实现 (习题+解答)

一.相关知识点 1.  在C++中,存取控制并不是面向对象的特征,但它为类的创建者提供了很有价值的访问控制.类的用户可以清楚地看到,什么可以用,什么应该忽略.更重要的是,它保证了类的用户不会依赖任何类的实现细节.有了这些,我们就能更改类的实现部分,没有人会因此而受到影响,因为他们并不能访问类的这一部分.一旦我们有了更改实现部分的自由,就可以在以后的时间里改进我们的设计,而且允许犯错误.要知道,无论我们如何小心地计划和设计,都可能犯错误.知道犯些错误也是相对安全的,这意味着我们会变得更有经验,会学

Java编程思想(前十章)

Java编程思想 有C++编程基础的条件下, 前10章可以快速过一下,都是基本语法,不需要花太多时间. 着重中后段的一些章节,类型信息.泛型.容器.IO.并发等. 中文翻译版 阅读地址 对于一个架构师而言,掌握各种语言的优势并可以运用到系统中,由此简化系统的开发,是其架构生涯的第一步. 每一个程序员都不能固步自封,要多接触新的行业,新的技术领域,突破自我. 对象入门 类的继承一般使用'统一标记法'(UML图)来画继承的图. 在面向对象的程序中, 通常要用到上溯造型(向上转型)的技术, 需要动态绑

《C++编程思想》第八章 内 联 函 数 (知识点+习题+解答)

一.相关知识点 任何在类中定义的函数自动地成为内联函数,但也可以使用inline关键字放在类外定义的函数前面使之成为内联函数.但为了使之有效,必须使函数体和声明结合在一起,否则,编译器将它作为普通函数对待.因此 inline int PlusOne(int x); 没有任何效果,仅仅只是声明函数(这不一定能够在稍后某个时候得到一个内联定义).成功的方法如下: inline int PlusOne(int x) { return ++x ;} 在头文件里,内联函数默认为内部连接--即它是 stat

《C++编程思想》 第九章 命 名 控 制 (知识点+习题+解答)

一.相关知识点         那些通常放在头文件里的名字,像常量.内联函数(inline function),在缺省情况下都是内部连接的(当然常量只有在C + +中缺省情况下是内部连接的,在 C中它缺省为外部连接).注意连接只引用那些在连接/装载期间有地址的成员,因此类声明和局部变量并没有连接. 名字空间的产生与一个类的产生非常相似: namespace MyLib{ //Declarations } 这就产生了一个新的名字空间,其中包含了各种声明.namespace与class.struct

Java编程思想---第十章 内部类(上)

第十章  内部类(上) 可以将一个类的定义放在另一个类的定义内部,这就是内部类. 内部类是一种非常有用的特性,它允许你把一些逻辑相关的类组织在一起,并控制位于内部的类的可视性,但内部类与组合是完全不同的概念. 10.1 创建内部类 创建内部类的方法就是,把类的定义置于外围类的里面: public class Parcel1 { class Contents { private int i = 11; public int value() { return i; } } class Destina

Java编程思想---第十章 内部类(下)

第十章 内部类(下) 10.9 内部类的继承 因为内部类的构造器必须连接到指向其外围类对象的引用,所以在继承内部类的时候事情会变得有些复杂,问题在于那个指向外围类对象的引用必须被初始化,而在导出类中不再存在可连接的默认对象,要解决这个问题,必须使用特殊的语法来明确说清他们之间的关联: class WithInner { class Inner {} } public class InheritInner extends WithInner.Inner { InheritInner(WithInn

c++类的拷贝、赋值与销毁(拷贝构造函数、拷贝赋值运算符析构函数)

拷贝构造函数     如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数. 拷贝构造函数第一个参数必须是一个引用类型.此参数几乎总是一个const的引用.拷贝构造函数在几种情况下都会被隐式地使用.因此,拷贝构造函数通常不应该是explicit的. 合成拷贝构造函数 与合成默认构造函数不同,即使我们定义了其他构造函数,编译器也会为我们合成一个拷贝构造函数. 对某些类来说,合成拷贝构造函数用来阻止我们拷贝该类类型的对象.而一般情况,合成的拷贝构造函数