本 Wiki 开启了 HTTPS。但由于同 IP 的 Blog 也开启了 HTTPS,因此本站必须要支持 SNI 的浏览器才能浏览。为了兼容一部分浏览器,本站保留了 HTTP 作为兼容。如果您的浏览器支持 SNI,请尽量通过 HTTPS 访问本站,谢谢!
C++ Primer 笔记 第七章
在C++中,我们用类来创建自定义类型。
类的主要概念有两个:数据抽象(data abstraction)、封装(encapsulation)。
数据抽象又主要分为两个部分:
数据抽象和封装定义了抽象数据类型(abstract data type)。在这种类型中,设计者只用关心如何实现类,而使用者则只用关心如何操作类即可。
首先我们来看看以前我们定义的 Sales_data 自定义类型:
struct Sales_data {
std::string book_no;
unsigned units_sold = 0;
double revenue = 0.0;
}
这是一个自定义的类型;但不是一个抽象数据类型。这个类型里所有的数据成员大家都可以访问,并没有 interface 和 implementation 的区别。相比之下书中第一章使用的 Sale_items 类就是一个抽象数据类型。我们并不知道类里有什么数据类型,但我们仍然通过设计者提供的函数来完成了对类的使用。这样的数据类型,才叫抽象数据类型。
定义成员函数的写法与普通函数一致:
std::string isbn() const { return book_no;};
成员运算符是如何访问成员函数的?
默认情况下,任意对类成员的直接使用,会引用 this
指向的对象。以下面代码为例:
total.isbn();
实际上该语句完成了三步操作:
total
this
的 parameter,保存了 total
的地址。this
以被调用的形式,将当前对象传递给了成员函数。整个调用过程实际上可以写成:
Sales_data::isbn(&total);//&total is a argument that will pass to 'this' pointer
this 实际上指代什么?如何利用? this
是一个指针,指向当前调用成员函数的类对象。因此 this
可以像下例一样利用:
(*this).bookno;
this -> bookno;//equivalent to the first one
那么先前我们定义的函数 isbn()
实际上就可以写成:
Sales_data::isbn() const {return this->book_no};
this 是什么样的指针? this
的本意就是代表当前调用成员函数的对象,因此 this
是一个常量指针(top-const pointer)。
成员函数前加 const 的意义?
目的是不希望通过该成员函数修改调用的对象。
是怎么实现的?
Sales_data::isbn() const {return this->book_no};
直接在成员函数后加 const 即可。需要注意的是,为了实现上述的效果,这里的 this
指针隐式的完成了从 non-const
到 low-const
的转变。 this
指针来访问对象。this
指针是一个 const pointer(top const),在作为 initailizer 的时候,会转换成 non-const 的指针(值传递)。this
指针需要可以指向 const object,需要从 non-const 转换 low-const(pointer to const)this
指针的类型改变为 const Sales_data* const
this
不可见,因此 C++ 规定,只要在成员函数的 Parameter List 后加上 const 关键字的,都会为 this
加上 low-const 属性。
我们将此类的函数称为常量成员函数(Const member functions)。该类函数隐含的意思是:函数调用的对象不会发生变化,且函数也无法修改调用的对象。
谁可以调用常量成员函数?
编译器处理类按照以下顺序处理:
因此在类中,成员函数对成员的使用不受成员声明位置的影响。
在类外部定义的成员函数需要满足两个条件:
我们通过 类名 + scope operator 来注明来源类,比如:
double Sale_data::avg_price const {
if (units_sold) {
return revenue / units_sold;
} else {
return 0;
}
scope operator 意味着将之后的函数所在 scope 都转到了类中。
如果需要返回当前对象,则可以使用下列语句:
return *this;
实例中的逻辑:当前类的内容 += 新的内容则可以实现为下面的形式:
Sales_data& Sales_data::combine(const Sales_data &rhs) {
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this; //return the object on which the function was called.
}
再以
total.combine(trans);
的形式调用。注意这里的返回类型是 Sale_data&
什么函数可以归类到非成员函数(nomember functions?)
如果函数在概念上属于类,但不会在类中声明与定义,那这部分函数可以算作非成员函数。非成员函数属于 interface 的一部分,通常用于定义对类的操作。
非成员函数应该在哪里声明?
非成员的函数应该在其对应类所在 header 进行声明。
I/O类提供了 istream 和 ostream 两种类型来让我们自定 read
和 print
的函数:
istream &read(istream &is, Sales_data &item)
{
double price = 0;
is >> item.bookNo >> item.units_sold >> price;
item.revenue = price * item.units_sold;
return is;
}
ostream &print(ostream &os, const Sales_data &item)
{
os << item.isbn() << " " << item.units_sold << " "
<< item.revenue << " " << item.avg_price();
return os;
}
几个注意事项:
is
/ os
都是不可拷贝的,因此必须使用引用来初始化对象。print
函数没有使用 endl
打印 newline。通常打印类函数只会做最小化的格式输出,以免影响到用户的自定义格式。add 函数不修改两个对象,但需要得到两个对象的和,因此我们使用一个临时对象存储结果。由于存在临时对象,因此需要按值传递的方式返回:
Sales_data add(const Sales_data &lhs, const Sales_data &rhs) {
Sales_data sum = lhs; // copy data members from lhs into sum
sum.combine(rhs); // add data members from rhs into sum
return sum;
}
构造函数类用来控制对象初始化的一类特殊的函数。任何时候类对象被创建,都会用构造函数来初始化。构造函数有几个特点:
如果我们没有显式的给指定类定义构造函数,那么编译器自己会定义一个构造函数给类。我们称这个构造函数为:合成的默认构造函数(Synthesized Default Constructor),类似于重载函数,构造函数可以有多个。
默认构造函数初始化类对象的策略是?
默认构造函数按下面的策略初始化类成员:
下列定义了 4 个构造函数:
Sales_data() = default; //ask compiler to generate the constructor for us
Sales_data(string& s, unsigned n, double p) : book_no(s), units_sold(n), revenue(p*n) { }
Sales_data(string& s, unsigned n, double p) : book_no(s) { }
Sales_data(std::istream& is);
第一个带 default 的构造函数是什么意思?
Sales_data() = default;
这样的写法等同于手动为当前类提供一个合成默认函数。= default
是 C++ 11 新写法,用于告知编译器我们需要一个默认行为的构造函数。该语句可以在任意地方写,如果放置到类中书写,则该合成默认函数为 inline 函数。
使用默认构造函数的前提是所有类成员完成初始化。某些编译器可能不支持类成员的初始化,这种情况下必须使构造函数初始化列表来进行手动初始化。
自定义构造函数应该怎么写?
例子中的第二个,第三个构造函数均为正常的自定义构造函数。我们使用构造函数初始化列表来进行初始化:
Sales_data(const std::string s, unsigned n, double p) :
book_no &s, unit_sold(n), revenue(p*n) {}
构造函数的 Parameter 定义了一些用于初始化类成员的变量;而冒号后的列表则被称为初始化列表,用于初始化类成员。Sales_data(string& s, unsigned n, double p) : book_no(s) { }
这种情况下,其他未被构造函数初始化的类成员会以默认构造函数初始化成员的方式进行初始化。Sales_data::Sales_data(std::istream& is) {
//read will get a stream from is and give it to which object is calling function.
read(is, *this);
}
与外部定义的成员函数一样,外部定义的构造函数需要加上类的作用域:
Sales_data::Sales_data(std::istream& is) {
read(is, *this); //read will read a transaction from is into the oject
}
上面的函数是什么意思?is
,意味着该构造函数试图从输入的内容中获取对类成员的初始值。为了配合这个意图,函数体重调用了 read(is, *this)
。根据 read
函数的功能,该构造函数实现的功能是使用输入信息对当前对象进行初始化,实际上等同于:
Sales_data item1;
double price = 0;
std::cin >> item1.book_no >> item1.units_sold >> price;
为什么 this 要解引用?C++ 中,编译器不仅支持类的默认初始化,同时也控制类与对象的拷贝,赋值以及析构(Destory)。如果用户没有定义这些行为,编译器会为我们做默认的合成,比如:
total = trans;
实际上编译器帮你做的是:
total.bookNo = trans.bookNo;
total.unit_sold = trans.unit_sold;
total.revenue = trans.revenue;
某些情况下,默认合成的版本是无法正确的工作的:比如管理动态内存。当然,如果希望编译器进行有效的动态内存管理,可以使用 Vector 和 String。以上三种操作这两种标准库类型都可以以默认的形式正确运行。
如何实现封装(Encapsulation)?
为了实现封装,C++ 提供了关键 Public / Private 来对访问进行控制:
Specifier 有哪些特点?
Class 与 Struct 有什么不同?
两者的主要区别在于如何定义成员函数:
私有成员是不能被类外部的对象访问的。但我们知道有些非成员函数其实是属于 interface,其实现可能需要访问类成员。我们可以在类里面用 friend 关键字使这些函数获得访问类成员的权限,比如:
friend Sales_data add(const Sales_data&, const Sales_data&);
需要注意的是,友元的声明必须在类里。当然出现的位置是随意的,不过一般都把友元的声明集体的放到类的开头。
友元函数的声明不能代替正经的函数声明。它的作用只在于指定访问权限。如果用户需要调用友元函数,那么我们还需要对被友元的函数重新声明一次。
通常的做法是在 class 被定义的头文件 对友元函数分开提供一般性的声明。
本章范例:
class Screen {
public:
typedef std::string::size_type pos; //typedef
using pos2 = std::string::size_type; //using
private:
statements...;
}
为什么使用 type alias?
如何使用 type alias 进行封装?
将用户使用的类型在 public 中进行 alias 即可:
public:
typedef std::string::size_type pos;
using pos = std::string::size_type;
type alias 的注意事项?之前提到过,所有类中定义的成员函数都会自动转化为 inline 函数。因此,类中定义的函数之前的 inline 关键字是可以省略的。利用这个性质,可以只对外部定义成员函数添加 inline关键字,从而达到提高代码可读性的效果:
inline Screen &Screen::move(pos r, pos c) {
statements;
}
书上提出了两点建议:
成员函数也可以进行重载,重载规则与一般函数一致:
char get() const { return contents[cursor]; }
char get(pos ht, pos wd) const;
Mutable 关键字的作用?
使用 Mutable 关键字可以使目标获得被修改的权限。被 mutable 修饰以后,被修饰的对象将永远可以被修改,即使:
Mutable 的应用场景?
比如下面的例子:
class SomeClass {
public:
void some_func() const;
private:
mutable size_t ctr; // can change even if in a const object;
}
void SomeClass::some_func() {
ctr++;
}
书上希望使用 Window_mgr
类来管理多个屏幕,于是选择了 Screen 类型的 vector 作为管理容器。如果希望给这个 vector 一个初始值,在 C++ 标准中最好的办法是进行构造函数的列表初始化:
class Window_mgr {
private:
std::vector<screen> screens {Screen(24, 80, ' ') };
}
这里使用了构造函数为 vector 的第一个元素进行了初始化,等于创建了一个空白屏幕。因此可知,在为在类中对成员初始化时,可以由以下两种方式实现:
返回值为 *this
表达式的成员函数,代表了该函数打算返回当前成员函数所在的类对象。通常情况下,类对象的返回以引用的形式返回。*this
会得到当前类对象,如果返回类型是类的引用则返回引用,否则则以复制的形式返回对象。
返回引用有什么额外的好处?
除了提高程序的性能以外,由于引用返回值是一个左值,利用该性质我们可以对对象进行连续的调用:
Screen myScreen;
myScreen.move(4,0).set('*');
假设我们新建一个成员函数 display()
,该函数的作用是打印出 Screen 对象的类容。由于打印不涉及修改 Screen,因此该函数应该被设置为一个常量成员函数。但是,常量成员函数会自动的将 *this
,也就是这里的 Screen 类改变为 low-const, 因此 display 的返回值实际上是:
const Screen&;
由于 reference to const 是不能作为左值的,因此下面的调用是有问题的:
Screen myScreen;
myScreen.move(4,0).display(cout).set('*'); //error, display returns a const Screen&
为什么要考虑 const 的重载情况?
之前的例子实际上说明了一种应用,即同时存在 non-const 和 low-const 的对象需要传递;在很多情况下我们希望保证对象的 constness 的一致性,也就函数处理不会改变对象的 constness。
由于常量成员函数会隐性的将 *this
转换为 low-const 的版本(比如上例中的 display()
),导致 Non-const 的对象被其处理之后也会以 low-const 的形式返回(也就是所谓的 const chain)。为了保证常量成员函数不会将 non-const 的对象以 low-const 对象的形式返回,使用重载区分 const / non-const 的对象是非常必要的。
如何实现 const 对象的分离处理?
函数的重载有一个性质:non-const 的对象调用重载函数,会优先选择 non-const 的函数版本。根据这个思路来设计:
do_display()
class Screen {
public:
Screen &display(std::ostream &os)
{ do_display(os); return *this; }
const Screen &display(std::ostream &os) const
{ do_display(os); return *this; }
private:
void do_display(std::ostream &os) const { os << contents; }
这段代码是如何进行 const 对象的筛选的?
do_display()
完成具体的功能。执行的过程中,这两个函数的 this
指针都会传递给 do_display()
display()
传递给 do_dislay()
的 this
指针会由 pointer to non-const 转换为 pointer to const,也就是在 do_display()
中,该 this
指针无法对对应的对象进行修改。*this
对当前对象解引用,并且返回。由于 non-const 版本中,即便 this
指针不能修改对应对象,但该对象的 constness 并没有发生改变。因此以解引用形式 *this
返回的依然是 non-const 的对象。以 *this 解引用形式返回对象是使用重载保证 constness 的关键。
Screen myScreen(5,3);
const Screen blank(5, 3);
myScreen.set('#').display(cout); // calls non const version
blank.display(cout); // calls const version
这样的设计有什么好处?
总而言之,设计良好的 C++ 程序更家倾向于拥有多而小的功能实现函数。需要使用的时候,再通过一系列的其他函数对其调用。
对于类来说,每个类都定义了一个不同的类型。两个类即使成员完全一样,他们也是不同的类型。
什么是类的提前声明?
与函数相同,没有类定义的声明被称为类的提前声明(Forward declaration)。样的声明表明我们的类是一种不完全类型(Incomplete type)。
类的提前声明的应用场景?
class link_screen {
link_screen *prev;
link_screen *next;
}
什么时候类必须被定义?
由于上述的要求,类的对象是不能作为该类的成员的。
与函数相同,类也可以作为友元的对象。
什么情况下需要友元类?
当我们需要控制类,即使用另外一个类对目标类的私有成员进行操作的时候。比如书中的例子,使用 Windows_mgr
对 Screen
进行屏幕清空操作,就需要访问 Screen
类的私有成员 std::string contents
:
友元类在哪里声明?声明方式是?
与友元函数相同,友元类的声明在类定义的开头:
class Screen {
friend class Window_mgr; // define Window_mgr as a friend of Screen
}
将 Windows_mgr
声明为友元类之后,就可以使用其 clear
成员函数清空 Screen::contents
的内容了:
class Window_msr {
public:
//screen ID
using screen_index = std::vector<Screen>::size_type;
//reset screen at given position
void clear(screen_index i);
private:
std::vector<Screen> screens{Screen(24, 80, ' ')};
}
void
Window_msr::clear {screen_index i} {
Screen &s = screens[i];
s.contents = string(s.height * s.width, ' ');
}
友元关系并不能传递。因此如果目标类有友元类,其友元类的友元类无法访问目标类的私有成员(你朋友的朋友并不是是你的朋友)。
不仅类可以作为友元的对象,类的成员函数也可以单独作为友元的对象。比如上例中,单独对 clear
函数进行友元:
class Screen {
friend void Window_mgr::clear(ScreenIndex);
}
声明顺序对类成员函数进行友元有什么影响?Screen
类:
因为接下来 Window_mgr
的定义需要用到 Screen
类的类型。
Window_mgr
类:
Window_mgr
类中有两处使用到了 Screen
类的类型:
using screen_index = std::vector<Screen>::size_type;
std::vector<Screen> screens{Screen(24, 80, ' ')};
如果这里没有 Screen
类的提前声明, Window_mgr
无法确定 Screen
到底是什么。
Window_mgr::clear()
:
不定义 clear()
函数是因为该函数需要操作Screen
类的类成员 Screen::contents
。在 Screen
类没有定义之前,该成员是无效的。
Screen
,并完成 Window_mgr::clear()
对其的友元声明:
如果没有对 clear()
进行提前声明,那么该友元声明也是无法完成的。
Window_mgr::clear()
此时 clear()
定义所需的所有条件均满足。
重载过的函数尽管名字相同,但却是两个不同的函数。因此,每一个需要友元的重载版本都需要进行对应的友元声明。
友元函数声明的要求是什么?
友元函数的声明要求在类外必须要有该函数对应的一般声明。
为什么这么要求?
归根结底还是函数的作用域问题。由于友元函数的声明只能保证访问权而没有创建作用域的功能,因此这种声明只能作出一种假定:我假定该函数作用域已经存在了;也就是说,友元函数声明的时候,默认该函数已经声明过了。
由于友元函数声明在类中的位置,为了确保该函数的作用域可见,最好的办法就是在类外先声明该函数。
每个类都有自己的 socpe.如果要从外部访问类内部数据,通常有两种方法:
但是无论哪种方法,访问者都需要有访问权限,比如:
Screen::pos ht = 24, wd = 80; //using pos to define screen.
Screen scr(ht, wd, ' ');//pos type defined by the first line.
Screen *p = &scr; // p point to a object scr;
char c = scr.get(); //using object to access member function get();
p -> get(); // using pointer to access member function.
如果想在外部定义类成员,必须指定类的 scope。这点可以通过 类名 + scope 操作符实现:
void Window_mgr::clear(ScreenIndex i) {
statements....
}
外部定义的成员函数的返回类型是如何处理的?默认情况下,外部定义的函数返回类型并没有处于类的 scope下。如果返回类型属于类成员,那么必须给该返回类型也指定类的 scope。比如下面的函数需要返回一个类成员类型:
//declaration in the class
screen_index add_scrren(const Screen&);
//definition outside class
Window_msr::screen_index
Window_msr::add_screen(const Screen& scr) {
screens.pushback(scr);
return screens.size() - 1;
}
名字查找(Name lookup)指是匹配 name 与其对应声明的过程。常规的名字查找策略如下:
但对于类成员函数中使用的变量名,编译器有不同的策略:
为什么有成员函数的例外?
为了方便成员函数的定义。如果按照一般声明可见后才可以使用类型的规则,那么函数中的变量名只能用已经可见的哪些。
该处理策略应用的范围?
只能应用于成员函数内部中出现的 name。成员函数自身的返回值类型、parameter 类型 需要遵循一般的 name lookup 规则,也就是使用该类型之前必须声明该类型。
对于类层级的名字(类成员的名字,成员函数的 parameter 和 返回值名),遵循一般的规则:
比如下面的例子:
typedef double Money;
string bal;
class Account {
public:
Money balance() { return bal; }
private:
Money bal;
根据之前的规则:
Account
,没有找到 Money。bal
变量,不属于返回类型和参数列表类型,则遵循的是类成员的两步查找规则:bal
的声明blance()
函数中, bal
的类型是 Money。这里看出明显的区别。遵循一般名字查找规则的名字只会搜索当前 scope 中名字第一次可见之前的区域;而满足要求的类中名字,则会搜索整个类的 scope。
需要注意的是,不同于一般名字的特性,C++ 不允许经过 type alias 的名字在内部和外部 scope 同时使用。比如下例的 Money 类型,由于外部已经有定义了,因此不允许在类中再定义,即便是同类型也不可以:
typedef double Money;
string bal;
class Account {
public:
Money balance() { return bal; } //using outter type name Money
private:
Money bal;
typedef double Money; //error, redefined
一般来说,type alias 的声明应该放到类的起始部位,这样接下来的声明都可以使用。
注意:请不要为类成员和成员函数内的变量指定相同的名字。
下面这个问题在 习题7.30 中也出现过。该例子很好的说明了成员函数体内部的名字的查找方式:
int height; // defines a name subsequently used inside Screen
class Screen {
public:
typedef std::string::size_type pos;
void dummy_fcn(pos height) {
cursor = width * height; // which height? the parameter
}
private:
pos cursor = 0;
pos height = 0, width = 0;
};
需要注意的是,因为 dummy_fcn() 的 parameter height
的 scope 与其相同,因此参与函数定义的 height
并不是指的类成员,也不是指的外部定义的成员,而是 parameter height
。如果这里要使用类成员 height
,那么需要显式的使用 this
指针,或者加上类的作用域:
cursor = width * this->height;
cursor = width * Screen::height; //equivalent way to indicate the member 'height'
此时最外层的 height
变量被类成员 height
隐藏了。如需使用最外层的 height
,需要显式的加上全局作用域:
cursor = width * ::height; //global 'height'
之前在类中的 name look up 规则依然适用于外部定义的成员函数。成员函数在外部进行定义的 scope 会被当做函数体内部的 scope 处理。
int height; // defines a name subsequently used inside Screen
class Screen {
public:
typedef std::string::size_type pos;
void setHeight(pos);
pos height = 0; // hides the declaration of height in the outer scope
};
Screen::pos verify(Screen::pos);
void Screen::setHeight(pos var) {
// var: refers to the parameter
// height: refers to the class member
// verify: refers to the global function
height = verify(var);
}
除此之外,第三条中 “查找的范围为成员函数定义的位置之前” 可以解释这里 verify
名字的合法性问题:
verfiy
名字处于函数体内部,但第一轮查找中,verify
之前的区域并没有声明Screen
中查找,查找完类中所有名字,也没有发现声明verfiy
的声明处于 setHeight()
的定义之前,因此找到了 verify
。如果不使用构造函数的初始化列表,也能进行函数的初始化:
Sales_data::Sales_data(const string &s) { //default initialized here
bookNo = s;
}
为什么推荐使用初始化列表?bookNo
, 再给 bookNo
赋值。相对于真正的初始化,这样的方法有两个问题:
由此看来,使用初始化列表是非常有必要的。
类成员初始化的顺序与构造函数初始化列表中的顺序无关,只与类成员在类中出现的位置有关。
初始化顺序会带来什么问题吗?
如果某个初始化的过程与初始化的顺序有关,那么很可能会带来问题:
class X {
int i;
int j;
public:
X(int val): j(val), i(j) { } //undefined, i is initialized before j
上面的过程,本意是想让 j
先被初始化。但根据类中成员的顺序,i
需要先初始化,而此时 j
并没有初始化。因此用 j
去初始化 i
会导致 undefined.
X(int val): i(val), j(val) { };
如果一个构造函数使用其默认初始值初始化所有成员,那么该构造函数即为默认构造函数。
委托构造函数(Delegating Construtor)是 C++11 中新增的,提高代码复用性的写法。如果几个构造函数拥有重复性的功能,那么就可以以委托的方式调用某个函数的功能:
class Sales_data {
public:
// nondelegating constructor initializes members from corresponding arguments
Sales_data(std::string s, unsigned cnt, double price):
bookNo(s), units_sold(cnt), revenue(cnt*price) {}
// remaining constructors all delegate to another constructor
Sales_data(): Sales_data("", 0, 0) {}
Sales_data(std::string s): Sales_data(s, 0,0) {}
Sales_data(std::istream &is): Sales_data() { read(is, *this); }
// other members as before
};
上例中有总共 4 个构造函数。实际上,所有的构造函数都通过委托第一个构造函数实现了对应的初始化功能:
注意多重委托的例子:
委托函数提供的 arugment 列表必须与被委托函数的 parameter list 匹配。
为什么必须要有默认构造函数?
因为在默认初始化或者值初始化的时候,必须要使用默认构造函数来处理。
默认初始化的场合:
值初始化的场合:
缺少默认构造函数将导致以上场合中的变量 / 对象无法进行初始化。
class NoDefault {
public:
NoDefault(const std::string&);
// additional members follow, but no other constructors
};
struct A { // my_mem is public by default; see § 7.2 (p. 268)
NoDefault my_mem;
};
A a; // error: cannot synthesize a constructor for A
struct B {
B() {} // error: no initializer for b_member
NoDefault b_member;
};
上面的例子中,A
未为自己的类成员提供默认构造函数(违反默认初始化第二条),B
提供了,但没有显式的提供初始值(违反了默认初始化的第三条),因此都会初始化失败。
默认构造的调用不能写成函数的形式:
Sales_data obj(); //error, obj() is a function that returns an object of type 'Sales_data'
Sales_data obj; //ok, obj is a class object
类的隐式转换指的什么?
指在特定的条件下,会将某个 parameter 直接转换为对应的类类型。
类的隐式转换需要什么条件?
string null_book = "99-99"
item.combine("null_book"); //a tempeory Sale_data object (99-99, 0, 0) has been created
item.combine("99-99"); //error, "99-99" need to be convert to a string first
这种隐式转换的坏处? item.combine(std::cin);
该例中,输入的信息会保存到临时类对象中;但当 combine 的操作结束后,输入的信息就会随着临时类对象的消亡而丢失。
如果我们不想执行上述的隐式转换,我们可以用 explicit 关键字来申明构造函数:
explicit Sale_data(std::istream &is);
item.combine(cin); //error, istream constructor is explicit
几个注意点:
同时,通过手动指定转换类型的方式也能达到 explicit 的效果:
item.combine(Sales_data("99-99")); //using constructor as an argument
item.combine(static_cast<Sales_data>(std::cin)); //explicitly convert the parameter to the class type
聚合类是一种用户可以直接访问成员的形式。他的特点是:
下面就是一个聚合类的例子:
struct Data {
int ival;
string s;
};
对这样的类我们可以使用列表初始化:
Data vall = { 0, "Anna" };
几个要点:
什么是 Literal Class?
对于 Literal class 的构造函数,我们可以声明为 constexpr;并且,Literal class 至少需要一个 constexpr 构造函数。
对于 constexpr 构造函数:
class Debug {
public:
constexpr Debug(bool b = true): hw(b), io(b), other(b) {}
constexpr Debug(bool h, bool i, bool o): hw(h), io(i), other(o) {}
constexpr bool any() { return hw || io || other; }
void set_io(bool b) { io = b; }
void set_hw(bool b) { hw = b; }
void set_other(bool b) { hw = b; }
private:
bool hw; // hardware errors other than IO errors
bool io; // IO errors
bool other; // other errors
};
有几点要注意的是:
constexpr Debug io_sub(false, true, false); // debugging IO
if (io_sub.any()) // equivalent to if(true)
cerr << "print appropriate error messages" << endl;
constexpr Debug prod(false); // no debugging during production
if (prod.any()) // equivalent to if(false)
cerr << "print an error message" << endl;
类的静态成员(Static Class Member)与类挂钩,而不是与类的对象挂钩。其旨在提供一个全局的,可以被所有类对象共享的成员。
静态类成员的声明在之前添加关键字 static
即可:
static double interestRate;
static double initRate();
注意:
this
指针(包括其内部 name),该成员也不能被声明为 const
class Account {
public:
void calculate() { amount += amount * interestRate; }
static double rate() { return interestRate; }
static void rate(double);
private:
std::string owner;
double amount;
static double interestRate;
static double initRate();
};
一般情况下,我们用 scope 运算符访问静态类成员:
double r;
r = Account::rate();
不过即便静态类成员不属于具体的类的对象,我们也依然可以用类的对象,类的引用,或者类的指针来访问它们:
Account ac1;
Account *ac2 = &ac1;
r = ac1.rate(); //through an Account object or reference
r = ac2->rate(); //through a pointer to an Account object
而且,成员函数可以直接使用静态变量。
class Account
{
static double inserestRate;
};
//init
double Account::interestRate = constant expression;
static 关键字使用的位置是?
构造函数与静态类成员有关系吗?
没有。由于静态类成员不是类对象的一部分,因此构造函数不会对其进行初始化。
静态类成员的生命周期与 scope?
静态类成员会一直存在到程序结束。其 scope 为整个类的 scope。静态类成员函数享有一切类成员函数的权限。
比如使用初始化的静态类成员作为数组的维度:
static constexpr int period = 30; //period is a constant expression
double daily_tbl[period];
需要注意的是,即便我们在这里对 period
初始化了,但某种意义上,这种初始化并不是我们所理解的常规 “初始化”。实际上,该初始化只是在编译期做了“值的替换”。因此,如果要将该值使用到其他地方,必须再次重新定义。
比较保险的做法是,在外部重新定义一下在类内部初始化过的的 constexpr 静态类成员。
静态成员和普通成员的区别主要在于使用的场景:
class Bar {
public:
// ...
private:
static Bar mem1; // ok: static member can have incomplete type
Bar *mem2; // ok: pointer member can have incomplete type
Bar mem3; // error: data members must have complete type
};