构造函数

构造函数的基本概念

构造函数是特殊的成员函数,只要创建类类型的新对象, 都要执行构造函数。

构造函数的工作是保证每个对象的数据成员具有合适的初始值。

class Sales_item {
public:
// operations on Sales_itemobjects
// default constructor needed to initialize members of built-in type
Sales_item(): units_sold(0), revenue(0.0) { }
private:
std::string isbn;
unsigned units_sold;
double revenue;
};

这个构造函数使用构造函数初始化列表来初始化 units_sold 和 revenue成员。isbn 成员由 string 的默认构造函数隐式初始化为空串。

构造函数的名字与类的名字相同,并且不能指定返回类型。像其他任何函数一样,它们可以没有形参,也可以定义多个形参。

构造函数可以被重载

可以为一个类声明的构造函数的数量没有限制,只要每个构造函数的形参表是唯一的。一般而言,不同的构造函数允许用户指定不同的方式来初始化数据成员。

例如,逻辑上可以通过提供两个额外的构造函数来扩展 Sales_item 类:一个允许用户提供 isbn 的初始值,另一个允许用户通过读取 istream 对象来初始化对象:

class Sales_item;
// other members as before
public:
// added constructors to initialize from a string or an istream
Sales_item(const std::string&);
Sales_item(std::istream&);
Sales_item();
};

实参决定使用哪个构造函数

我们的类现在定义了三个构造函数。在定义新对象时,可以使用这些构造函数中的任意一个:

// uses the default constructor:
// isbn is the empty string; units_soldand revenue are 0
Sales_item empty;
// specifies an explicit isbn; units_soldand revenue are 0
Sales_item Primer_3rd_Ed("0-201-82470-1");
// reads values from the standard input into isbn, units_sold, and revenue
Sales_item Primer_4th_ed(cin);

用于初始化一个对象的实参类型决定使用哪个构造函数。在 empty 的定义中,没有初始化式,所以运行默认构造函数。接受一个 string 实参的构造函数用于初始化 Primer_3rd_ed;接受一个 istream 引用的构造函数初始化Primer_4th_ed。

构造函数自动执行

只要创建该类型的一个对象,编译器就运行一个构造函数:

// constructor that takes a string used to create and initialize
variable
Sales_item Primer_2nd_ed("0-201-54848-8");
// default constructor used to initialize unnamed object on the heap
Sales_item *p = new Sales_item();

第一种情况下,运行接受一个 string 实参的构造函数,来初始化变量Primer_2nd_ed。第二种情况下,动态分配一个新的 Sales_item 对象。假定分配成功,则通过运行默认构造函数初始化该对象。

用于 const 对象的构造函数

构造函数不能声明为 const:

class Sales_item {
public:
Sales_item() const; // error
};

构造函数初始化式

与任何其他函数一样,构造函数具有名字、形参表和函数体。与其他函数不同的是,构造函数也可以包含一个构造函数初始化列表:

// recommended way to write constructors using a constructor initializer
Sales_item::Sales_item(const string &book):
isbn(book), units_sold(0), revenue(0.0) { }

构造函数初始化列表以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个数据成员后面跟一个放在圆括号中的初始化式。这个构造函数将 isbn成员初始化为 book 形参的值,将 units_sold 和 revenue 初始化为 0。与任意的成员函数一样,构造函数可以定义在类的内部或外部。构造函数初始化只在构造函数的定义中而不是声明中指定。

省略初始化列表在构造函数的函数体内对数据成员赋值是合法的。例如,可以将接受一个 string 的Sales_item 构造函数编写为:

// legal but sloppier way to write the constructor:
// no constructor initializer
Sales_item::Sales_item(const string &book)
{
isbn = book;
units_sold = 0;
revenue = 0.0;
}

这个构造函数给类 Sales_item 的成员赋值,但没有进行显式初始化。不管是否有显式的初始化式,在执行构造函数之前,要初始化 isbn 成员。这个构造函数隐式使用默认的 string 构造函数来初始化 isbn。执行构造函数的函数体时,isbn 成员已经有值了。该值被构造函数函数体中的赋值所覆盖。

从概念上讲,可以认为构造函数分两个阶段执行:(1)初始化阶段;(2)普通的计算阶段。计算阶段由构造函数函数体中的所有语句组成。

不管成员是否在构造函数初始化列表中显式初始化,类类型的数据成员总是在初始化阶段初始化。初始化发生在计算阶段开始之前。

在构造函数初始化列表中没有显式提及的每个成员,使用与初始化变量相同的规则来进行初始化。运行该类型的默认构造函数, 来初始化类类型的数据成员。内置或复合类型的成员的初始值依赖于对象的作用域: 在局部作用域中这些成员不被初始化,而在全局作用域中它们被初始化为 0。

有时需要构造函数初始化列表

如果没有为类成员提供初始化式,则编译器会隐式地使用成员类型的默认构造函数。如果那个类没有默认构造函数,则编译器尝试使用默认构造函数将会失。在这种情况下,为了初始化数据成员,必须提供初始化式。

有些成员必须在构造函数初始化列表中进行初始化。对于这样的成员,在构造函数函数体中对它们赋值不起作用。没有默认造函数的类类型的成员,以及 const 或引用类型的成员,不管是哪种类型,都必须在构造函数初始化列表中进行初始化。

class ConstRef {
public:
ConstRef(int ii);
private:
int i;
const int ci;
int &ri;
};

// no explicit constructor initializer: error ri is uninitialized
ConstRef::ConstRef(int ii)
{ // assignments:
i = ii; // ok
ci = ii; // error: cannot assign to a const
ri = i; // assigns to ri which was not bound to an object
}

记住,可以初始化 const 对象或引用类型的对象,但不能对它们赋值。在开始执行构造函数的函数体之前,要完成初始化。初始化 const 或引用类型数据成员的唯一机会是构造函数初始化列表中。编写该构造函数的正确方式为

// ok: explicitly initialize reference and const members
ConstRef::ConstRef(int ii): i(ii), ci(i), ri(ii) { }

成员初始化的次序

构造函数初始化列表仅指定用于初始化成员的值,并不指定这些初始化执行的次序。成员被初始化的次序就是定义成员的次序。 第一个成员首先被初始化, 然后是第二个, 依次类推。

初始化的次序常常无关紧要。然而,如果一个成员是根据其他成员而初始化,则成员初始化的次序是至关重要的。

class X {
int i;
int j;
public:
// run-time error: i is initialized before j
X(int val): j(val), i(j) { }
};

按照与成员声明一致的次序编写构造函数初始化列表是个好主意。此外,尽可能避免使用成员来初始化其他成员。

一般情况下,通过(重复)使用构造函数的形参而不是使用对象的数据成员,可以避免由初始化式的执行次序而引起的任何问题。例如,下面这样为 X 编写构造函数可能更好:

X(int val): i(val), j(val) { }

在这个版本中,i 和 j 初始化的次序就是无关紧要的。

初始化式可以是任意表达式

一个初始化式可以是任意复杂的表达式。例如,可以给 Sales_item 类一个新的构造函数,该构造函数接受一个 string 表示 isbn,一个 usigned 表示售出书的数目,一个 double 表示每本书的售出价格:

Sales_item(const std::string &book, int cnt, double price):
isbn(book), units_sold(cnt), revenue(cnt * price) { }

类类型的数据成员的初始化式

初始化类类型的成员时,要指定实参并传递给成员类型的一个构造函数。可以使用该类型的任意构造函数。例如,Sales_item 类可以使用任意一个 string构造函数来初始化 isbn。也可以用 ISBN 取值的极限值来表示isbn 的默认值,而不是用空字符串。可以将 isbn 初始化为由 10 个 9 构成的串:

// alternative definition for Sales_item default constructor
Sales_item(): isbn(10, '9'), units_sold(0), revenue(0.0) {}

这个初始化式使用 string 构造函数,接受一个计数值和一个字符,并生成一个 string,来保存重复指定次数的字符。

默认实参与构造函数

再来看看默认构造函数和接受一个 string 的构造函数的定义:

Sales_item(const std::string &book):
isbn(book), units_sold(0), revenue(0.0) { }
Sales_item(): units_sold(0), revenue(0.0) { }

这两个构造函数几乎是相同的:唯一的区别在于,接受一个 string 形参的构造函数使用该形参来初始化 isbn。默认构造函数(隐式地)使用 string 的默认构造函数来初始化 isbn。

可以通过为 string 初始化式提供一个默认实参将这些构造函数组合起来:

class Sales_item {
public:
// default argument for book is the empty string
Sales_item(const std::string &book = ""):
isbn(book), units_sold(0), revenue(0.0) { }
Sales_item(std::istream &is);
// as before
};

在这里, 我们只定义了两个构造函数,其中一个为其形参提供一个默认实参。

对于下面的任一定义,将执行为其 string 形参接受默认实参的那个构造函数:

Sales_item empty;
Sales_item Primer_3rd_Ed("0-201-82470-1");

在 empty 的情况下,使用默认实参,而 Primer_3rd_ed 提供了一个显式实参。类的两个版本提供同一接口:给定一个 string 或不给定初始化式,它们都将一个 Sales_item 初始化为相同的值。

posted @ 2018-05-05 23:06  刘-皇叔  阅读(362)  评论(0)    收藏  举报