第11章:构造函数
文章目录
本书要强调的一个主旨在于,面向对象编程(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 & 来源) - 不写拷贝构造函数,编译器会自动提供一个来执行简单的逐成员拷贝。