《C++Primer》第十二章-类-学习笔记(2)-作用域&构造函数
日志:
1,2020-03-03 笔者提交文章的初版V1.0
作者按:
最近在学习C++ primer,初步打算把所学的记录下来。
传送门/推广
《C++Primer》第二章-变量和基本类型-学习笔记(1)
《C++Primer》第三章-标准库类型-学习笔记(1)
《C++Primer》第八章-标准 IO 库-学习笔记(1)
上一篇
《C++Primer》第十二章-类-学习笔记(1)
类作用域
类作用域(class scope)
:每个类定义一个作用域。类作用域比其他作用域复杂得多——在类的定义
体内定义的成员函数可以使用出现在该定义之后的名字。
每个类都定义了自己的新作用域和唯一的类型。在类的定义体内声明类成员,将成员名引入类的作用域。两个不同的类具有两个的类作用域。即使两个类具有完全相同的成员列表,它们也是不同的类型。每个类的成员不同于任何其他类(或任何其他作用域)的成员。
class First {
public:
int memi;
double memd;
};
class Second {
public:
int memi;
double memd;
};
First obj1;
Second obj2 = obj1; // error: obj1 and obj2 have different types
使用类的成员
在类作用域之外,成员只能通过对象或指针分别使用成员访问操作符 .
或->
来访问。这些操作符左边的操作数分别是一个类对象或指向类对象的指针。
跟在操作符后面的成员名字必须在相关联的类的作用域中声明:
Class obj; // Class is some class type
Class *ptr = &obj; //指向类对象的指针
// member is a data member of that class
ptr->member; // fetches member from the object to which ptr points 指针通过-> 来访问成员
obj.member; // fetches member from the object named obj
// memfcn is a function member of that class
ptr->memfcn(); // runs memfcn on the object to which ptr points
obj.memfcn(); // runs memfcn on the object named obj
一些成员使用成员访问操作符
来访问,另一些直接通过类使用作用域操作符::
来访问。
一般的数据或函数成员必须通过对象来访问。定义类型的成员,如 Screen::index,使用作用域操作符来访问
。
作用域与成员定义
尽管成员是在类的定义体之外定义的,但成员定义就好像它们是在类的作用域中一样。回忆一下,出现在类的定义体之外的成员定义必须指明成员出现在哪个类中:
double Sales_item::avg_price() const
{
if (units_sold)
return revenue/units_sold;
else
return 0;
}
我们用完全限定名 Sales_item::avg_price 来指出这是类Sales_item 作用域中的 avg_price 成员的定义。一旦看到成员的完全限定名
,就知道该定义是在类作用域中。因为该定义是在类作用域中,所以我们可以引用revenue 或 units_sold,而不必写 this->revenue 或 this->units_sold。
形参表和函数体处于类作用域中
在定义于类外部的成员函数中,形参表和成员函数体都出现在成员名之后。这些都是在类作用域中定义,所以可以不用限定而引用其他成员。例如,类Screen 中 get 的二形参版本的定义:
char Screen::get(index r, index c) const
{
index row = r * width; // compute the row location
return contents[row + c]; // offset by c to fetch specified character
}
该函数用 Screen 内定义的 index 类型来指定其形参类型。因为形参表是在 Screen 类的作用域内,所以不必指明我们想要的是 Screen::index。我们想要的是定义在当前类作用域中的,这是隐含的。同样,使用 index、width 和contents 时指的都是 Screen 类中声明的名字。
函数返回类型不一定在类作用域中
与形参类型相比,返回类型出现在成员名字前面。如果函数在类定义体之外定义,则用于返回类型的名字在类作用域之外。如果返回类型使用由类定义的类型,则必须使用完全限定名
。例如,考虑 get_cursor 函数:
class Screen {
public:
typedef std::string::size_type index;
index get_cursor() const;
};
inline Screen::index Screen::get_cursor() const //比如这个Screen::index
{
return cursor;
}
该函数的返回类型是 index,这是在 Screen 类内部定义的一个类型名。如果在类定义体之外定义 get_cursor,则在函数名被处理之前,代码在不在类作用域内。当看到返回类型时,其名字是在类作用域之外使用。必须用完全限定的类型名 Screen::index 来指定所需要的 index 是在类 Screen 中定义的名字。
类作用域中的名字查找
名字查找
指:寻找与给定的名字使用相匹配的声明
的过程
- 首先,在使用该名字的块中查找名字的声明。只考虑在该项使用之前声明的名字。
- 如果找不到该名字,则在包围的作用域中查找。
- 如果找不到任何声明,则程序出错。在 C++ 程序中,所有名字必须在使用之前声明。
类作用域也许表现得有点不同,但实际上遵循同一规则。可能引起混淆的是函数中名字确定的方式,而该函数是在类定义体内定义的。
类定义
实际上是在两个阶段中处理:
- 首先,编译成员声明;
- 只有在类的所有成员出现之后,才编译它们的定义本身。
当然,类作用域中使用的名字并非必须是类成员名。类作用域中的名字查找也会发生在其他作用域中声明的名字。在名字查找期间,如果类作用域中使用的名字不能确定为类成员名,则在包含该类或成员定义的作用域中查找,以便找到该名字的声明。
类成员声明的名字查找
按以下方式确定在类成员的声明中用到的名字。
- 检查出现在名字使用之前的类成员的声明。
- 如果第 1 步查找不成功,则检查包含类定义的作用域中出现的声明以及出现在类定义之前的声明。
例如:
typedef double Money; //出现在类定义之前的声明
class Account {
public:
Money balance() { return bal; }
private:
Money bal;
// ...
};
在处理 balance 函数的声明时,编译器首先在类 Account 的作用域中查找Money 的声明。编译器只考虑出现在 Money 使用之前的声明。因为找不到任何成员声明,编译器随后在全局作用域中查找 Money 的声明。只考虑出现在类Account 的定义之前的声明。找到全局的类型别名 Money 的声明,并将它用作函数 balance 的返回类型和数据成员 bal 的类型。
必须在类中先定义类型名字,才能将它们用作数据成员的类型,或者成员函数的返回类型或形参类型。
编译器按照成员声明在类中出现的次序来处理它们。通常,名字必须在使用之前进行定义。而且,一旦一个名字被用作类型名
,该名字就不能被重复定义:
typedef double Money;
class Account {
public:
Money balance() { return bal; } // uses global definition of Money
private:
// error: cannot change meaning of Money //这里重复定义了类型名字,出错。
typedef long double Money;
Money bal;
// ...
};
类成员定义中的名字查找
按以下方式确定在成员函数的函数体中用到的名字
。
- 首先检查成员函数局部作用域中的声明。
- 如果在成员函数中找不到该名字的声明,则检查对所有类成员的声明。
- 如果在类中找不到该名字的声明,则检查在此成员函数定义之前的作用域
中出现的声明。
类成员遵循常规的块作用域名字查找
下面的函数使用了相同的名字来表示形参和成员,这是通常应该避免的。这样做的目的是展示如何确定名字:
// Note: This code is for illustration purposes only and reflects bad practice
// It is a bad idea to use the same name for a parameter and a member
int height;
class Screen {
public:
void dummy_fcn(index height) {
cursor = width * height; // which height? The parameter 指的是函数形参的height
}
private:
index cursor;
index height, width;
};
查找 dummy_fcn 的定义中使用的名字 height 的声明时,编译器首先在该函数的局部作用域中查找。函数的局部作用域中声明了一个函数形参。dummy_fcn的函数体中使用的名字 height 指的就是这个形参声明。
在本例中,height 形参屏蔽名为 height 的成员。尽管类的成员被屏蔽了,但仍然可以通过用类名来限定成员名或显式使用 this 指针来使用它。
如果我们想覆盖常规的查找规则,应该这样做:
// bad practice: Names local to member functions shouldn't hide member names
void dummy_fcn(index height) {
cursor = width * this->height; // member height
// alternative way to indicate the member 这两句效果一样的
cursor = width * Screen::height; // member height
}
函数作用域之后,在类作用域中查找
如果想要使用 height 成员,更好的方式也许是为形参取一个不同的名字:
// good practice: Don't use member name for a parameter or other local
variable
void dummy_fcn(index ht) {
cursor = width * height; // member height
}
现在当编译器查找名字 height 时,它将不会在函数内查找该名字。编译器接着会在 Screen 类中查找。因为 height 是在成员函数内部使用,所以编译器在所有成员声明中查找。尽管 height 是先在 dummy_fcn 中使用,然后再声明,编译器还是确定这里用的是名为 height 的数据成员。
类作用域之后,在外围作用域中查找
如果编译器不能在函数或类作用域中找到,就在外围作用域中查找。在本例子中,出现在 Screen 定义之前的全局作用域中声明了一个名为 height 的全局声明。然而,该对象被屏蔽了。
尽管全局对象
被屏蔽了(被形参屏蔽了),但通过用全局作用域确定操作符
来限定名字,仍然可以使用它。
// bad practice: Don't hide names that are needed from surrounding scopes
void dummy_fcn(index height) {
cursor = width * ::height;// which height? The global one 全局作用域确定操作符
}
在文件中名字的出现处确定名字
当成员定义在类定义的外部时,名字查找的第 3 步不仅要考虑在 Screen类定义之前的全局作用域中的声明,而且要考虑在成员函数定义之前出现的全局作用域声明
。例如:
class Screen {
public:
// ...
void setHeight(index);
private:
index height;
};
Screen::index verify(Screen::index); //全局函数 verify
void Screen::setHeight(index var) {
// var: refers to the parameter
// height: refers to the class member
// verify: refers to the global function
height = verify(var);
}
注意,全局函数 verify 的声明在 Screen 类定义之前是不可见的。然而,名字查找的第 3 步要考虑那些出现在成员定义之前的外围作用域声明,并找到全局函数 verify 的声明
。
构造函数
构造函数是特殊的成员函数,只要创建类类型的新对象,都要执行构造函数。
构造函数的工作
是保证每个对象的数据成员具有合适的初始值。展示了如何定义构造函数:
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; //isbn 成员由 string 的`默认构造函数` `隐式初始化`为空串。
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 出错
};
const 构造函数是不必要的。创建类类型的 const 对象时,运行一个普通构造函数来初始化该 const 对象。构造函数的工作是初始化对象。不管对象是否为 const,都用一个构造函数来初始化化该对象。
构造函数初始化式
与任何其他函数一样,构造函数具有名字、形参表和函数体。与其他函数不同的是,构造函数也可以包含一个构造函数初始化列表(constructor initializer list)
:
// recommended way to write constructors using a constructor initializer
Sales_item::Sales_item(const string &book):
isbn(book), units_sold(0), revenue(0.0) { }
构造函数初始化列表(constructor initializer list)
以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个数据成员后面跟一个放在圆括号中的初始化式。构造函数初始化列表
负责构造函数中显式初始化的工作。
这个构造函数将 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。
在本节中编写的两个 Sales_item 构造函数版本具有同样的效果:无论是在构造函数初始化列表中初始化成员,还是在构造函数函数体中对它们赋值,最终结果是相同的。构造函数执行结束后,三个数据成员保存同样的值。
不同之外在于,使用构造函数初始化列表的版本初始化数据成员,没有定义初始化列表的构造函数版本在构造函数函数体中对数据成员赋值。这个区别的重要性取决于数据成员的类型。
有时需要构造函数初始化列表
如果没有为类成员提供初始化式,则编译器会隐式地使用成员类型的默认构造函数
。如果那个类没有默认构造函数,则编译器尝试使用默认构造函数将会失败。在这种情况下,为了初始化数据成员,必须提供初始化式。
有些成员必须在构造函数初始化列表中进行初始化
。对于这样的成员,在构造函数函数体中对它们赋值不起作用。没有默认构造函数的类类型的成员,以及 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
//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) { }
必须对任何 const 或引用类型成员以及没有默认构造函数的类类型的任何成员使用初始化式。
成员初始化的次序
每个成员在构造函数初始化列表中只能指定一次,这不会令人惊讶。毕竟,给一个成员两个初始值意味着什么?也许更令人惊讶的是,构造函数初始化列表仅指定用于初始化成员的值,并不指定这些初始化执行的次序。成员被初始化的次序
就是定义成员的次序。第一个成员首先被初始化,然后是第二个,依次类推。
初始化的次序常常无关紧要。然而,如果一个成员是根据其他成员而初始化,则成员初始化的次序是至关重要的。
考虑下面的类:
class X {
int i;
int j;
public:
// run-time error: i is initialized before j
X(int val): j(val), i(j) { } //i根据j初始化的
};
在这种情况下,构造函数初始化列表看起来似乎是用val 初始化 j,然后再用 j 来初始化 i。然而,i 首先被初始化。这个初始化列表的效果是用尚未初始化的 j 值来初始化 i!
如果数据成员在构造函数初始化列表中的列出次序与成员被声明的次序不同,那么有的编译器非常友好,会给出一个警告。
如何解决
按照与成员声明一致的次序编写构造函数初始化列表是个好主意。此外,尽可能避免使用成员来初始化其他成员。
一般情况下,通过(重复)使用构造函数的形参而不是使用对象的数据成员,可以避免由初始化式的执行次序而引起的任何问题。
例如,下面这样为 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) { } //revenue(cnt * price)使用了初始化式
revenue 的初始化式使用表示价格和售出数目的形参来计算对象的 revenue 成员
类类型的数据成员的初始化式
初始化类类型的成员时,要指定实参并传递给成员类型的一个构造函数。可以使用该类型的任意构造函数。例如,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 初始化为相同的值。我们更喜欢使用默认实参,因为它减少代码重复。
默认构造函数
只要定义一个对象时没有提供初始化式,就使用默认构造函数
。为所有形参
提供默认实参的构造函数也定义了默认构造函数。
合成的默认构造函数
一个类哪怕只定义了一个构造函数,编译器也不会再生成默认构造函数。这条规则的根据是,如果一个类在某种情况下需要控制对象初始化,则该类很可能在所有情况下都需要控制。
只有当一个类没有定义构造函数时,编译器才会自动生成一个默认构造函数。
合成的默认构造函数(synthesized default constructor)
使用与变量初始化相同的规则来初始化成员。具有类类型的成员通过运行各自的默认构造函数来进行初始化。内置和复合类型的成员,如指针和数组,只对定义在全局作用域中的对象才初始化。当对象定义在局部作用域中时,内置或复合类型的成员不进行初始化。
如果类包含内置或复合类型的成员,则该类不应该依赖于合成的默认构造函数。它应该定义自己的构造函数来初始化这些成员。
此外,每个构造函数应该为每个内置或复合类型的成员提供初始化式。没有初始化内置或复合类型成员的构造函数,将使那些成员处于未定义的状态。除了作为赋值的目标之外,以任何方式使用一个未定义的成员都是错误的。如果每个构造函数将每个成员设置为明确的已知状态,则成员函数可以区分空对象
和具有实际值的对象
。
//内置类型,类类型,符合类型的构造函数与初始化的关系是啥?
使用默认构造函数
初级 C++ 程序员常犯的一个错误是,采用以下方式声明一个用默认构造函数初始化的对象:
// oops! declares a function, not an object
Sales_item myobj();
编译 myobj 的声明没有问题。然而,当我们试图使用 myobj 时
Sales_item myobj(); // ok: but defines a function, not an object
if (myobj.same_isbn(Primer_3rd_ed)) // error: myobj is a function
编译器会指出不能将成员访问符号用于一个函数!问题在于 myobj 的定义被编译器解释为一个函数的声明,该函数不接受参数并返回一个 Sales_item 类型的对象——与我们的意图大相径庭!使用默认构造函数定义一个对象的正确方式是去掉最后的空括号:
// ok: defines a class object ...
Sales_item myobj;
另一方面,下面这段代码也是正确的:
// ok: create an unnamed, empty Sales_itemand use to initialize myobj
Sales_item myobj = Sales_item();
在这里,我们创建并初始化一个 Sales_item 对象,然后用它来按值初始化myobj。编译器通过运行 Sales_item 的默认构造函数来按值初始化一个Sales_item。
隐式类类型转换
C++ 语言定义了内置类型之间的几个自动转换。也可以定义如何将其他类型的对象隐式转换为我们的类类型,或将我们的类类型的对象隐式转换为其他类型。为了定义到类类型的隐式转换,需要定义合适的构造函数。
可以用单个实参来调用的构造函数定义了从形参类型到该类类型的一个隐式转换。
conversion constructor(转换构造函数)
:可用单个实参调用的非 explicit 构造函数。隐式使用转换构造函数将实参的类型转换为类类型。
让我们再看看定义了两个构造函数的 Sales_item 版本:
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
};
这里的每个构造函数都定义了一个隐式转换。因此,在期待一个 Sales_item类型对象的地方,可以使用一个 string 或一个 istream:
string null_book = "9-999-99999-9";
// ok: builds a Sales_itemwith 0 units_soldand revenue from
// and isbn equal to null_book
item.same_isbn(null_book); //该函数期待一个 Sales_item 对象作为实参
//这段程序使用一个 string 类型对象作为实参传给 Sales_item 的same_isbn 函数。
编译器使用接受一个 string 的 Sales_item 构造函数从 null_book 生成一个新的Sales_item 对象。新生成的(临时的)Sales_item 被传递给 same_isbn。
这个行为是否我们想要的,依赖于我们认为用户将如何使用这个转换。在这种情况下,它可能是一个好主意。book 中的 string 可能代表一个不存在的ISBN,对 same_isbn 的调用可以检测 item 中的 Sales_item 是否表示一个空的 Sales_item。另一方面,用户也许在 null_book 上错误地调用了 same_isbn。
更成问题的是从 istream 到 Sales_item 的转换:
// ok: uses the Sales_item istream constructor to build an object
// to pass to same_isbn
item.same_isbn(cin);
这段代码将 cin 隐式转换为 Sales_item。这个转换执行接受一个 istream的 Sales_item 构造函数。该构造函数通过读标准输入来创建一个(临时的)Sales_item 对象。然后该对象被传递给 same_isbn。
这个 Sales_item 对象是一个临时对象。一旦 same_isbn 结束,就不能再访问它。实际上,我们构造了一个在测试完成后被丢弃的对象。这个行为几乎肯定是一个错误。
抑制由构造函数定义的隐式转换
可以通过将构造函数声明为 explicit
,来防止在需要隐式转换的上下文中使用构造函数:
class Sales_item {
public:
// default argument for book is the empty string
explicit Sales_item(const std::string &book = ""):isbn(book), units_sold(0), revenue(0.0) { }
explicit Sales_item(std::istream &is);
// as before
};
explicit
关键字只能用于类内部的构造函数声明上。在类的定义体外部所做的定义上不再重复它:
// error: explicit allowed only on constructor declaration in class header
explicit Sales_item::Sales_item(istream& is)
{
is >> *this; // uses Sales_iteminput operator to read the members
}
现在,两个构造函数都不能用于隐式地创建对象。前两个使用都不能编译:
item.same_isbn(null_book); // error: string constructor is explicit
item.same_isbn(cin); // error: istream constructor is explicit
当构造函数被声明 explicit 时,编译器将不使用它作为转换操作符
。
为转换而显式地使用构造函数
只要显式地按下面这样做,就可以用显式的构造函数来生成转换:
string null_book = "9-999-99999-9";
// ok: builds a Sales_itemwith 0 units_soldand revenue from
// and isbn equal to null_book
item.same_isbn(Sales_item(null_book)); //显式的构造函数来生成转换
在这段代码中,从 null_book 创建一个 Sales_item。尽管构造函数为显式的,但这个用法是允许的。显式使用构造函数只是中止了隐式地使用构造函数。
任何构造函数都可以用来显式地创建临时对象
。
通常,除非有明显的理由想要定义隐式转换,否则,单形参构造函数
应该为 explicit。将构造函数设置为explicit 可以避免错误,并且当转换有用时,用户可以显式地构造对象。
类成员的显式初始化
尽管大多数对象可以通过运行适当的构造函数进行初始化,但是直接初始化简单的非抽象类的数据成员仍是可能的。对于没有定义构造函数并且其全体数据成员均为 public 的类,可以采用与初始化数组元素相同的方式初始化其成员:
struct Data {
int ival;
char *ptr;
};
// val1.ival = 0; val1.ptr = 0
Data val1 = { 0, 0 };
// val2.ival = 1024;
// val2.ptr = "Anna Livia Plurabelle"
Data val2 = { 1024, "Anna Livia Plurabelle" };
根据数据成员的声明次序来使用初始化式。例如,因为 ival 在 ptr 之前声明,所以下面的用法是错误的:
// error: can't use "Anna Livia Plurabelle" to initialize the int ival
Data val2 = { "Anna Livia Plurabelle" , 1024 };
这种形式的初始化从 C 继承而来,支持与 C 程序兼容。显式初始化类类型对象的成员有三个重大的缺点:
- 要求类的全体数据成员都是 public。
- 将初始化每个对象的每个成员的负担放在程序员身上。这样的初始化是乏味且易于出错的,因为容易遗忘初始化式或提供不适当的初始化式。
- 如果增加或删除一个成员,必须找到所有的初始化并正确更新。
定义和使用构造函数几乎总是较好的。当我们为自己定义的类型提供一个默认构造函数时,允许编译器自动运行那个构造函数,以保证每个类对象在初次使用之前正确地初始化。
参考资料
【1】C++ Primer 中文版(第四版·特别版)
注解
本文许可证
本文遵循 CC BY-NC-SA 4.0(署名 - 非商业性使用 - 相同方式共享) 协议,转载请注明出处,不得用于商业目的。