欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

第11章:构造函数

程序员文章站 2022-07-12 22:48:07
...


本书要强调的一个主旨在于,面向对象编程(OOP)是创建基本新数据类型的一种方式。这种类型如足够有用,可在多个程序中重复使用。
类型的一个重要特点是能够初始化。声明时就能初始化更佳。这使面向对象语法更好用,对程序员更友好。
构造函数本质上就是一个初始化函数。欢迎学习C++的构造艺术!

11.1 构造函数入门

构造函数(constructor)告诉编译器如何解释下面这样的声明:

Fraction a(1, 2);	// a = 1/2

基于迄今为止学到的 Fraction 类的知识,你或许已猜到上述声明的目的就是获得以下语句的效果:

Fraction a;
a.set(1, 2);

本章目的就是让类准确实现上述效果,这要依赖于构造函数,其语法如下:

类名(参数列表)

一个看起来很奇怪的函数。没有返回类型(连void都没有),某种意义上,类名就是返回类型。例如:

Fraction(int n, int d);

将该声明放到类的上下文中:

class Fraction
{
public:
// ...
	Fraction(int n, int d);
// ...
};

这只是声明。和其他函数一样,构造函数必须在某处定义,定义可以放到类声明外面,但必须澄清作用域

Fraction::Fraction(int n, int d)
{
    set(n, d);
}

在类声明外面定义的构造函数具有以下语法形式:

类名::类名(参数列表){
语句
}

第一个类名是名称前缀(类名::),表明这是该类的成员函数(换言之,具有类作用域)。第二个类名表明该函数是构造函数。

构造函数可以内联。大多数构造函数都很短,所以特别适合内联。

class Fraction
{
public:
// ...
	Fraction(int n, int d) {set(n, d);}
// ...
};

有了构造函数,就可在声明 Fraction 对象的同时初始化。

Fraction frOne(1, 0), frTwo(2, 0), frHalf(1, 2);

多个构造函数(重载)

C++允许重用名称创建不同函数,用参数列表加以区分。构造函数也不例外。

例如,可为 Fraction 类声明多个构造函数,一个无参,另一个有两个,第三个只有一个。

class Fraction
{
public:
// ...
	Fraction();
	Fraction(int n, int d);
	Fraction(int n);
// ...
};

C++/C++14:成员初始化

C++ 14

本节内容限定适用于 C++11 和更新的编译器。

从C++11起,语言提供了一种新方式来指定数据成员的默认值。表面上和构造函数冲突,似乎不用写构造函数?实情并非如此。两种技术可配合无间。

Point 类的一个合理设计是创建默认零值的对象。C++11允许在类声明中初始化成员。

class Point
{
public:
    int x = 0;
    int y = 0;
};

现在,即使是局部变量,未初始化的 Point 对象也会获得零值。

int main()
{
    Point silly_point;
    cout << silly_point.x;	// 打印0
}

Fraction 类则希望为分母赋值1而不是0,因为 0/0 非法。

class Fraction
{
private:
    int num = 0;
    int den = 1;
};

以这种方式初始化,每个构造函数都为指定数据成员分配指定的值(本例是 0 和 1 ),除非构造函数用自己的值覆盖。

如果一个构造函数都不写,这种方式可将对象初始化为合理的默认值。但还是应该坚持写默认构造函数,除非想禁止用户在不初始化的前提下创建对象(如下节所述)。

默认构造函数

每个类都应当有一个默认构造函数(即无参构造函数),除非要求用户在创建对象时必须初始化。这是由于如果不写构造函数,编译器会自动生成一个默认的,它什么事情都不做。
但只要写了任意构造函数,编译器就不会提供默认版本。

假定声明一个无构造函数的类:

class Point
{
private:
    int x, y;
public:
    set(int new_x, int new_y);
    int get_x();
    int get_y();
};

由于没有构造函数,编译器会自动提供一个默认的,即无参构造函数。正是因为有这个函数,所以才能用类声明对象。

Point a, b, c;

再来看看自己写一个构造函数会发生什么:

class Point
{
private:
    int x, y;
public:
    Point(int new_x, int new_y) { set(new_x, new_y);}
    set(int new_x, int new_y);
    int get_x();
    int get_y();
};

该构造函数支持在声明对象的同时初始化:

Point a(1, 2), b(10, -20);

但现在声明对象而不提供参数就会出错:

Point c;	//错误!无默认构造函数!

自动生成的、你以前不知不觉依赖的默认构造函数,就这样悄无声息地溜走了!刚开始写类的代码时,编译器的这个行为会让你不知不觉“中招”。以前习惯不写构造函数,允许类的用户像下面这样声明对象:

Point a, b, c;

而一旦写构造函数,同时又不是默认构造函数,上述似乎“无辜”的代码就会出错。
有时不想要任何默认构造函数,而是强迫用户将对象初始化为特定值,这时上述行为完全能够接受。

C++ 故意用默认构造函数来坑你吗?

C++的行为看起来有点儿古怪:提供默认构造函数(也就是无参构造函数)来营造一种虚假的安全氛围,一旦开始自己写构造函数,又悄悄取消这一“感赐”。

确实古怪,但并非毫无理由,之所以成为C+的一个“特点”,完全是因为C+既是一种面向对象的语言,又是一种需要向后兼容C的语言(并非百分百,但也差不多).
该行为使 struct 关键字成了“受害者",C++将 struc t类型视为类(以前说过),但为了向后兼容,以下C代码肯定能在C++中成功编译:

struct Point
{
    int x, y;
};

struct Point a;
a.x = 1;

C语言没有 public 或 private 关键字,所以只有成员默认公共,上述代码才能编译,另一个问题是c没有构造函数的概念,所以上述代码要在C++中编译,编译器必须提供默认构造函数,否则以下语句无法编译:

struct Point a;

顺便说一句,C++允许将上述语句的struct拿掉:

point a;

总之,为了向后兼容,C++必须提供一个自动的默认构造函数,但只要自己写了一个,就认为你是在用C++原生代码编程,不再涉及兼容问题,必须由你自己负责所有成员函数和构造函数.
这时就没借口了(不知道构造函数的事),C++认为你希望写你需要的任何东西,包括默认构造函数。

还要记住,C++允许你选择不要任何默认构造函数,强迫类的用户显式初始化对象,这在某些时候很有用,例如第12章就故意不要默认构造函数,确保创建了节点就必须初始化。

C++11/C++14:委托构造函数

C++ 14

本节内容限定使用C++11和更新的编辑器。

写好的构造函数最好能在其他构造函数中重用。C+14支持该功能。事实上,C++11 就已将其纳入其中,只是某些编译器最近才实现。例如以下 point 类:

Class Point
{
private:
    int x, y;
public:
    Point(int new_x, int new_y) { x = new_x; y = new_y;}
};

默认构造函数如果能重用这个现成的构造函数就好了。C++11 和之后的编译器支持像下面这样写:

Class Point
{
private:
    int x, y;
public:
    Point(int new_x, int new_y) {x = new_x; y = new_y;}
    Point():Point(0, 0){}
};

新的一行代码如下所示

Point():Point(0, 0) {}

这声明的是默认构造函数,但将工作委托给另一个构造函数。换言之,调用两个参数的构造函数并传递实参0,0。C+11 和更高版本允许在类中初始化单独的数据成员来获得一样的结果。

Class Point
{
private:
    int x = 0;
    int y = 0;
public:
    Point(int new_x, int new_y) {x = new_x; y = new_y;}
    Point() { }
};

例 11.1:Point 类的构造函数

本例修订上一章的 Point 类, 添加两个简单的构造函数:一个默认,另一个获取两个参数。然后用一个简单的程序来测试。

# include <iostream>
using namespace std;

class Point
{
private:	// 私有数据成员
	int x, y;
public:
	// 构造函数
	Point()
	{
		x = 0;
		y = 0;
	}
	Point(int new_x, int new_y)
	{
		set(new_x, new_y);
	}

	// 其他成员函数
	void set(int new_x, int new_y);
	int get_x();
	int get_y();
};

int main()
{
	Point pt1, pt2;
	Point pt3(5, 10);
	cout << "pt1 是 ";
	cout << pt1.get_x() << ", ";
	cout << pt1.get_y() << endl;
	cout << "pt3 是 ";
	cout << pt3.get_x() << ", ";
	cout << pt3.get_y() << endl;
	return 0;
}

void Point::set(int new_x, int new_y)
{
	if (new_x < 0)
	{
		new_x *= -1;
	}
	if (new_y < 0)
	{
		new_y *= -1;
	}
	x = new_x;
	y = new_y;
}

int Point::get_x()
{
	return x;
}

int Point::get_y()
{
	return y;
}

工作原理

声明两个构造函数:

public:	// 构造函数
	Point() { x = 0; y = 0;}
	Point(int new_x, int new_y) {set(new_x, new_y);}

注意,构造函数在类的公共区域声明。如声明为私有,Point类的用户就无法访问,失去了构造函数的意义。
默认构造函数将数据成员设为零。如果类的用户忘了显式初始化,该行为可以起到救场的作用。

Point() {x = 0; y = 0;}

main的代码使用了两次默认构造函数(创建 pt1 和 pt2 对象);创建 pt3 对象使用另一个构造函数:

Point pt1, pt2;
Point pt3(5, 10);

练习

练习11.1.1

为 Point 类的两个构造函数添加代码来报告它们的用途。默认构造函数打印
“正在使用默认构造函数”,另一个构造函数打印 “正在使用 (int, int) 构造函数”。

答案:

# include <iostream>
using namespace std;

class Point
{
private:
	int x, y;
public:
	Point()
	{
		x = 0;
		y = 0;
		cout << "In default constructor." << endl;
	}

	Point(int new_x, int new_y)
	{
		set(new_x, new_y);
		cout << "In Point(int, int) c'tor." << endl;
	}

	// other member functions
	void set(int new_x, int new_y);
	int get_x();
	int get_y();
};

int main()
{
	Point pt1, pt2;
	Point pt3(5, 10);

	cout << "The value of pt1 is ";
	cout << pt1.get_x() << ", ";
	cout << pt1.get_y() << endl;

	cout << "The value of pt3 is ";
	cout << pt3.get_x() << ", ";
	cout << pt3.get_y() << endl;
	return 0;
}

void Point::set(int new_x, int new_y)
{
	if (new_x < 0)
	{
		new_x *= -1;
	}
	if (new_y < 0)
	{
		new_y *= -1;
	}
	x = new_x;
	y = new_y;
}

int Point::get_x()
{
	return x;
}

int Point::get_y()
{
	return y;
}

练习11.1.2

添加第三个构造函数,只获取一个整数,x设为该值,y设为0。

答案:

# include <iostream>
using namespace std;

class Point
{
private:
	int x, y;
public:
	Point()
	{
		x = 0;
		y = 0;
	}
	Point(int new_x, int new_y)
	{
		set(new_x, new_y);
	}

	// here is the new constructor:

	Point(int new_x)
	{
		set(new_x, 0);
	}

	// other member functions
	void set(int new_x, int new_y);
	int get_x();
	int get_y();
};

int main()
{
	Point pt1, pt2;
	Point pt3(5);

	cout << "The value of pt1 is ";
	cout << pt1.get_x() << ", ";
	cout << pt1.get_x() << endl;

	cout << "The value of pt3 is ";
	cout << pt3.get_x() << ", ";
	cout << pt3.get_y() << endl;
	return 0;
}

void Point::set(int new_x, int new_y)
{
	if (new_x < 0)
	{
		new_x *= -1;
	}
	if (new_y < 0)
	{
		new_y *= -1;
	}
	x = new_x;
	y = new_y;
}

int Point::get_x()
{
	return x;
}

int Point::get_y()
{
	return y;
}

练习11.1.3

如前所述,使用 C++11 或更新的编译器,可通过单独的成员初始化为 × 和 y 创建默认值0。写一个什么事情都不做的默认构造函数,再写一个构造函数向 × 赋值,但不向 y 赋值。最后测试这一套构造函数。采用这种方式,× 和 y 无论如何都能获得默认值0,但构造函数可以随意覆盖这些值。

答案:

# include <iostream>
using namespace std;

class Point
{
private:
	int x = 0;
	int y = 0;
public:
	Point() { }
	Point(int new_x, int new_y)
	{
		set(new_x, new_y);
	}

	Point(int new_x)
	{
		set(new_x, 0);
	}

	// other member functions
	void set(int new_x, int new_y);
	int get_x();
	int get_y();
};

int main()
{
	Point pt1, pt2;
	Point pt3(5, 10);

	cout << "The value of pt1 is ";
	cout << pt1.get_x() << ", ";
	cout << pt1.get_y() << endl;

	cout << "The value of pt3 is ";
	cout << pt3.get_x() << ", ";
	cout << pt3.get_y() << endl;
	return 0;
}

void Point::set(int new_x, int new_y)
{
	if (new_x < 0)
	{
		new_x *= -1;
	}
	if (new_y < 0)
	{
		new_y *= -1;
	}
	x = new_x;
	y = new_y;
}

int Point::get_x()
{
	return x;
}

int Point::get_y()
{
	return y;
}

例11.2:Fraction类的构造函数

Fraction类的默认构造函数将分数设为 0/1,按照惯例,增改代码加粗。其余代码全部来自第10章。

# include <iostream>
# include <string>
# include <cstdlib>
using namespace std;

class Fraction
{
private:
	int num, den;
public:
	Fraction()
	{
		set(0, 1);
	}
	Fraction(int n, int d)
	{
		set(n, d);
	}

	void set(int n, int d)
	{
		num = n;
		den = d;
		normalize();
	}
	int get_num()
	{
		return num;
	}
	int get_den()
	{
		return den;
	}
	Fraction add(Fraction other);
	Fraction mult(Fraction other);
private:
	void normalize();
	int gcf(int a, int b);
	int lcm(int a, int b);
};

int main()
{
	Fraction f1, f2;
	Fraction f3(1, 2);

	cout << "f1 的值是 ";
	cout << f1.get_num() << "/";
	cout << f1.get_den() << endl;

	cout << "f3 的值是 ";
	cout << f3.get_num() << "/";
	cout << f3.get_den() << endl;
	system("PAUSE");
	return 0;
}

// Fraction 类的成员函数
// Normalize(标准化):分数化简,
// 数学意义上每个不同的值都唯一
void Fraction::normalize()
{
	// 处理涉及0的情况
	if (den == 0 || num == 0)
	{
		num = 0;
		den = 1;
	}
	// 仅分子有负号
	if (den < 0)
	{
		num *= -1;
		den *= -1;
	}
	// 从分子和分母中分解出GCF
	int n = gcf(num, den);
	num = den / n;
}

// 最大公因数
int Fraction::gcf(int a, int b)
{
	if (b == 0)
	{
		return abs(a);
	}
	else
	{
		return gcf(b, a%b);
	}
}

// 最小公倍数
int Fraction::lcm(int a, int b)
{
	int n = gcf(a, b);
	return a / n * b;
}

Fraction Fraction::add(Fraction other)
{
	Fraction fract;
	int lcd = lcm(den, other.den);
	int quot1 = lcd / den;
	int quot2 = lcd / other.den;
	fract.set(num * quot1 + other.num * quot2, lcd);
	return fract;
}

Fraction Fraction::mult(Fraction other)
{
	Fraction fract;
	fract.set(num * other.num, den * other.den);
	return fract;
}

工作原理
只要理解了例11.1,本例就很简单,只需要注意一点:即默认构造函数要将分母初始化为1(而不是6).

Fraction() {set(0, 1);}

main 调用了三次构造函数,声明 f1 和 f2 时调用默认构造函数,声明 f3 时调用了另一个.

练习

练习11.2.1

重写默认构造函数,不是调用 set(0, 1),而是直接设置数据成员 num 和 den.这样效率更好还是更差?有必要调用 normalize 函数吗?
// 调用 normalize 并非必须,但却是一个良好的编程习惯
// 它能有效预防问题,尤其是在 normalize 函数可能会被子类改写的前提下
//(详情在以后的章节里讲述).
// 直接设置 num 和 den 理论上更有效,因为它绕过了函数调用.
// 但是,不要指望它会带来明显的效率提升。

答案:

# include <iostream>
using namespace std;

class Fraction
{
private:
	int num, den;
public:
	Fraction()
	{
		num = 0;
		den = 1;
	}
	Fraction(int n, int d)
	{
		set(n, d);
	}

	void set(int n, int d)
	{
		num = n;
		den = d;
		normalize();
	}
	int get_num() { return num; }
	int get_den() { return den; }
	Fraction add(Fraction other);
	Fraction mult(Fraction other);
private:
	void normalize();
	int gcf(int a, int b);
	int lcm(int a, int b);
};

int main()
{
	Fraction f1, f2;
	Fraction f3(1, 2);

	cout << "The value of f1 is ";
	cout << f1.get_num() << "/";
	cout << f1.get_den() << endl;

	cout << "The value of f3 is ";
	cout << f3.get_num() << "/";
	cout << f3.get_den() << endl;
	return 0;
}

void Fraction::normalize()
{
	if (den == 0 || num == 0)
	{
		num = 0;
		den = 1;
	}

	if (den < 0)
	{
		num *= -1;
		den *= -1;
	}

	int n = gcf(num, den);
	num = num / n;
	den = den / n;
}

int Fraction::gcf(int a, int b)
{
	if (b == 0)
	{
		return abs(a);
	}
	else
	{
		return gcf(b, a%b);
	}
}

int Fraction::lcm(int a, int b)
{
	int n = gcf(a, b);
	return a / n * b;
}

Fraction Fraction::add(Fraction other)
{
	Fraction fract;
	int lcd = lcm(den, other.den);
	int quot1 = lcd / den;
	int quot2 = lcd / other.den;
	fract.set(num * quot1 + other.num * quot2, lcd);
	return fract;
}

Fraction Fraction::mult(Fraction other)
{
	Fraction fract;
	fract.set(num * other.num, den * other.den);
	return fract;
}

练习11.2.2

写第三个构造函数,只获取一个 int。num 设为该值,den 设为1.

答案:

# include <iostream>
using namespace std;

class Fraction
{
private:
	int num, den;
public:
	Fraction() { set(0, 1); }
	Fraction(int n, int d) { set(n, d); }
	Fraction(int n) { set(n, 1); }

	void set(int n, int d)
	{
		num = n;
		den = d;
		normalize();
	}
	int get_num() { return num; }
	int get_den() { return den; }
	Fraction add(Fraction other);
	Fraction mult(Fraction other);
private:
	void normalize();
	int gcf(int a, int b);
	int lcm(int a, int b);
};

int main()
{
	Fraction f1, f2;
	Fraction f3(2);

	cout << "The value of f1 is ";
	cout << f1.get_num() << "/";
	cout << f1.get_den() << endl;

	cout << "The value of f3 is ";
	cout << f3.get_num() << "/";
	cout << f3.get_den() << endl;
	return 0;
}

void Fraction::normalize()
{
	if (den == 0 || num == 0)
	{
		num = 0;
		den = 1;
	}

	if (den < 0)
	{
		num *= -1;
		den *= -1;
	}

	int n = gcf(num, den);
	num = num / n;
	den = den / n;
}

int Fraction::gcf(int a, int b)
{
	if (b == 0)
	{
		return abs(a);
	}
	else
	{
		return gcf(b, a%b);
	}
}

int Fraction::lcm(int a, int b)
{
	int n = gcf(a, b);
	return a / n * b;
}

Fraction Fraction::add(Fraction other)
{
	Fraction fract;
	int lcd = lcm(den, other.den);
	int quot1 = lcd / den;
	int quot2 = lcd / other.den;
	fract.set(num * quot1 + other.num * quot2, lcd);
	return fract;
}

Fraction Fraction::mult(Fraction other)
{
	Fraction fract;
	fract.set(num * other.num, den * other.den);
	return fract;
}

11.2 引用变量和引用参数(&)

为了理解另一种特殊的构造函数(称为“拷贝构造函数”),首先必须理解 C++ 的“引用”.第6章曾做过简单介绍。

操纵变量最简单的方式就是直接操纵:

int n;
n = 5;

操纵变量的另一种方式(第6章讲过)是通过指针

int n, *p;
p = &n;	// 让 p 指向 n
*p = 5;	// 将 p 指向的东西设为 5

p指向n,所以将 *p 设为5等价于将 n 设为5。
重点在于,n 只有一个,但可以有任意数量的指针指向它。获得指向n的指针并不会创建新整数。这只是操纵 n 的另一种方式。

引用做的是相似的事情,只是避免了使用指针语法。

int n;
int &r = n;

但 & 不是取址操作符吗?区别在于,由于 & 是在一个数据声明中使用,所以创建的是一个引用变量,该变量引用变量n,结果是改动 r 相当于改动 n:

r = 5;	//相当于将 n 设为5

引用和指针的共同点在于,它们只是建立了一种方式来引用现有数据项,而不是为新数据分配空间。例如,可创建对n的多个引用:

int n;
int &r1 = n;
int &r2 = n;
int &r3 = n;
r1 = 5;	// n 现为 5.
r2 = 25;	// n 现为 25.
cout << "n 的新值是 " << r3;	// 打印 n.

修改任何引用变量(r1,r2和r3)都相当于修改n.

C++很少像这样使用引用变量,更有用的是“引用参数”。还记得第7章的 swap 函数吗?当时用的是指针。使用引用参数可获得一样的结果。

void swqp_ref(int &a, int &b)
{
    int temp = a;
    a = b;
    b = temp;
}

本例的 swap 函数不获取 a 和 b 的拷贝,而是获取对它们的引用。这使函数能永久性地更改实参。

传引用效果和传指针一样,只是避免了指针语法。调用函数时只需传递整数,不需要传递指向整数的指针。

int big = 100;
int little = 1;
swap_ref(big, little);	// 交换 big 和 little

11.3拷贝构造函数

除了默认构造函数,另一个特殊构造函数是拷贝构造函数。特殊性体现在两个方面.首先,该构造函数会在许多常规情况下调用,有时你根本意识不到它的存在.

其次,如果不自己写一个,编译器会自动提供一个,虽然它做的并不一定是你想做的。编译器提供的只是执行一次简单的逐成员拷贝(大多数时候是足够的)。

下面列出会自动调用拷贝构造函数的情况。

  • 函数返回类类型的值.

  • 参数是类类型,会创建实参的拷贝并传给函数。

  • 使用一个对象初始化另一个对象。例如:
    Fraction a(1, 2);
    Fraction b(a);

以前说过,还可用等号(=)执行一个对象对另一个对象的初始化。
Fraction b = a;

最后,在 C++11 和更高版本中,可以(而且应该)用大括号指定多个初始化实参。
Fraction a {1, 2};

那么,怎样写自己的拷贝构造函数?什么时候需要写?记住,不自己写编译器会自动提供一个。用以下语法声明拷贝构造函数:
类名(类名 const & 来源)
const 关键字确保实参不会被函数更改。这很有道理,创建拷贝当然不该破坏原始的东西。

注意,上述语法使用了引用参数。函数获取对 来源对象 的引用,不是获取一个新的拷贝。
下面是 Point 类的一个例子。必须先声明好拷贝构造函数。

class Point
{
    //...
 public:		// 构造函数
    Point(Point const &src);
    // ...
};

函数定义没有内联,必须单独定义。定义时,Point 会出现三次,第一次是声明作用域,第二次是返回值类型,第三次是来源类型。

Point::Point(Point const &src)
{
    x = src.x;
    y = src.y;
}

既然编译器会提供现成的,为什么还要自己写一个?本例(和 Fraction 类)确实用不着。
编译器提供的拷贝构造函数执行简单的逐成员拷贝,这已足够。

只有在每个对象都分配了资源(比如内存)时才需自己写拷贝构造函数。这时不能逐个成员地拷贝,而是需要深拷贝:拷贝出来的每个类的实例都需要分配自己的资源。不过,本书所有例子都不需要深拷贝。

拷贝构造函数和引用
C++ 支持“引用”的一个主要目的就是让你能写拷贝构造函数。没有引用语法,写拷贝构造函数就是一个不可能完成的任务。例如,像下面这样声明拷贝构造函数会发生什么?

Point(Point const src)

编译器根本不允许这样写,仔细想想就知道原因,向函数传递实参时,必须将那个对象的拷贝放入栈(用于存储函数实参和地址的内存区域),但这意味着拷贝构造函数要想工作必须先创建同种对象(都是 Point 类型)的一个拷贝,所以它必须调用自身!这就没完了。

那么,能不能像下面这样声明:

Point(Point const *src)

语法没问题,也是一个构造函数,但不是拷贝构造函数,语法要求实参是指针而非对象。所以,只能向该函数传递指向对象的指针,而不能传递 Point 对象本身。

幸好,困难不难克服,毕竟只是语法问题,换成引用后,函数就可以作为拷贝构造函数使用,语法上实参是对象而不是指针,但由于函数调用幕后用指针来实现,所以不会产生无限循环.

Point(Point const &src)

11.4 将字符串转换为分数的构造函数

用字符串来初始化 Fraction 对象是不是很酷?例如:

Fraction a = "1/2", b = "1/3";

写一个构造函数获取 char* 字符串作为参数即可实现。可用该构造函数轻松初始化 Fraction 对象的数组。

Fraction arr_of_fract[4] = {"1/2", "1/3". "3/4"};

不用引号是不是更好?但那样行不通。不用 char* 字符串(带引号的那种),C++ 会对 1/3 执行整数除法并向下取整为0.

 Fraction a = 1/3; //这达不了目的

引号还是有必要的。由于需要访问 C 字符串函数,所以必须添加以下 include 指令;

#include <cstring>

接着要在 Fraction 类的声明中声明构造函数:

Fraction(char *s);

到目前为止都很容易。函数定义要麻烦一些,但不用担心。后面会详细分析代码。

Fraction::Fraction(char *s)
{
    int n = 0;
    int d = 1;
    char *p1 = strtok(s, "/, ");
    char *p2 = strtok(NULL, "/, ");
    if (p1)
    {
        n = atoi(p1);
    }
    if (p2)
    {
        d = atoi(p2);
    }
    set(n, d);
}

函数首先声明两个整数变量并赋予合理默认值:

int n = 0;
int d = 1;

d(分母)默认1。这使用户能像下面这样初始化Fraction对象:

Fraction a = "5";	// a 初始化为 5/1

接着两个语句提取由除号(/)或逗号(,)分隔的两个子串:

char *p1 = strtok(s, "/, ");
char *p2 = strtok(NULL, "/, ");

strtok 函数(第7章讲过)在输入字符串中找不到更多子串就返回空指针。所以代码必须先测试 p1 和/或 p2 是否为空指针。不是空指针才能传给 atoi 函数。无论如何,最终都会调用 set。

if (p1)
{
    n = atoi(p1);
}
if (p2)
{
    d = atoi(p2);
}
set(n, d);

请读者自行练习将上述代码放到 Fraction 类中并测试。

小结

  • 构造函数是类的初始化函数,形式如下:
    类名(参数列表)
  • 未内联的构造函数像这样定义:
    类名::类名(参数列表)
    { 语句 }
  • 可以提供任意数量的、但各不相同的构造函数,所有构造函数具有相同的函数名(也就是类名)。为了进行区分,每个构造函数必须具有不同的参数数量或类型。
  • 默认构造函数是无参构造函数,像这样声明:
    类名( )
  • 声明对象时不提供参数会调用默认构造函数,例如:
    Point a;
  • 不提供任何构造函数,编评器会自动提供一个默认的,它什么事情都不做(也就是一个no-op),但只要自己写了构造函数,编译器就不会自动提供默认版本.
  • 所以,为了实现防御性编程,最好养成总是写默认构造函数的习惯,如果愿意,可以不包含任何语句,例如:
    Point a( ) { };
  • C++ 的引用是使用 & 来声明的变量或参数,幕后几乎总会传递指针,只是避免了使用指针语法,程序是在传值,尽管传递的是可能是指针.
  • 拷贝对象(包括将对象传给函数,或函数返回对象)时会调用类的拷贝构造函数。
  • 拷贝构造函数使用引用参数和const关键字,后者防止修改实参(来源对象),语法如下:
    类名(类名 const & 来源)
  • 不写拷贝构造函数,编译器会自动提供一个来执行简单的逐成员拷贝。
相关标签: 学习C++ c++