第七章(类)
1).类的问题。
- 自定义运算符。
- 如何控制对象的拷贝,移动,赋值,销毁等。
2).类的基本思想。
- 数据抽象,是一种依赖于接口和实现分离的编程以及设计技术。接口包括用户所能执行的操作,类的实现包括类的数据成员,负责接口实现的函数体以及定义类所需要的各种私有函数。
- 封装,实现类的接口和实现的分离,封装后的类隐藏了它的实现细节,用户只能使用接口,而无法访问实现部分。
3).类要想实现数据抽象和封装,
- 定义一个抽象数据类型
- 类的设计者负责设计考虑类(接口)的实现
- 使用者只需要思考类型做了什么(使用类的接口)。
/1.定义抽象数据类型
1).Sales_item
类是一个抽象数据类型。我们可以使用它的接口实现各种操作,但是不能访问它的数据成员。
2).而Sales_data
类不是一个抽象数据类型,我们是可以访问它的数据成员,并且要求用户自己编写操作。我们需要设计一些操作,使得类可以提供一些操作,从而隐藏数据成员,实现抽象数据类型。
//1.设计Sales_data类
1).步骤
- 我们暂时先将运算定义为普通的函数,
- 执行加法和IO的函数不做为成员函数**(是友元)**,
- 执行复合赋值的运算定义为成员函数,
Sales_data
无需专门定义赋值运算?
2).类的用户(还是程序员)和程序的用户(普通人)不一样。
3).注意打印的print
的返回值是cout
;
//2.定义改进的Sales_data类
1).注意。
- 类的成员函数的声明必须在类内,定义可以在类内部,也可以是外部,
avg_price
的目的不是通用的,它应该属于类实现的一部分,而不是接口,- 对于接口组成部分的非成员函数,它们的定义和声明都在类的外部,
- 定义在类内部的函数是隐式的
inline
函数。
2).注意。
this
,当我们调用成员函数时,实际上时替某个对象调用它,函数里面的类的数据成员,被隐式地指定为调用该成员函数的对象里面的数据成员;实际上,是对象调用成员函数时,向它传递自己的地址,并赋值给this
,成员函数通过this来寻找数据成员,所以我们在成员函数里面可以直接使用对象里面的数据成员。this
是一个隐式参数,我们不能定义名为this
的参数或者变量。- 定义成员函数函数体时,
this
可以显式指出this->data
,也可以不,通常不。 const
,目的在于是我们可以访问常量对象里面的数据成员。有const
的函数称为常量成员函数。此时常量成员函数是不能写(修改对象的内容)的,但可以读。为了提高函数的灵活性,我们可以设置,避免是const
对象时,无法使用。- 常量对象,常量对象的引用或指针,都只能调用常量成员函数。
3).类的作用域。
- 类本身就是一个作用域,类的成员函数使用的的类成数据成员,从作用域这个角度理解也自然是对象里面的数据成员,
- 编译器分两步处理类,
- 首次编译成员的声明,
- 然后才是成员函数体(可能没有)。
- 因此成员函数可以不用顾及次序地使用其他成员。
4).在类的外部定义成员函数,
- 定义和声明匹配,包括是否是常量成员函数也要一致
- 包括它所属的类名,作用域运算符,将函数名写成如下形式,
Sales_data::avg_price
,其他不变。
- 因为这样编译器就知道使用的类的数据成员就是哪个类里面的。
- 也知道了是定义哪一个类里面的成员函数。
5.定义一个返回this对象的函数,
- 我们设计函数类似于某一个运算符时,我们也会关注它的形式,返回的是什么。
- 因此我们在定义类似复合赋值语句时,我们设计调用的对象是复合赋值号的左值,而传入的参数是复合赋值号的右值,并且返回的是左值对象,因此我们在设计函数时,函数的返回类型是调用函数的对象的引用,
return
语句是*this
。
练习,7.5,注意使用后置函数类型返回,
- 前面加上auto,
- 后面是比较复杂的引用指针,函数指针。
- 类里面的常量成员函数的意义在于
- 不修改,
- 常量对象也可以使用,例如,读。
//3.定义类相关的非成员函数
1).注意事项。
- 和普通的函数是一样的,需要把声明和定义分开,
- 我们通常把这些函数的声明放在和类声明(不是定义)同一头文件中,然后再在一个源文件中定义函数和类,
- 这样做方便用户使用接口,只需要引入一个和类名称一样的头文件即可。
2).注意我们定义的read
和print
函数的iostream
和类都是使用引用类型。
- 之所以使用
iostream
的引用是需要返回iostream
的状态,进行判断。 iostream
类是不能拷贝的,- 读取和写入会改变
iostream
的状态所以不是对常量的引用, print
不负责换行,一般来说,格式的控制由用户自己决定。
3).定义add
函数,
- 默认情况下,类的拷贝,拷贝的是数据成员,所以不需要特别定义类的赋值。2. 不能返回的是局部变量,字面值的引用或者指针。
//4.构造函数
1).类通过构造函数控制对象中数据成员的初始化过程,对象一旦创建,就会执行构造函数。
2).关于构造函数。
- 构造函数没有返回值,2. 构造函数的名字和类的名字是一样的,其他和普通函数没有什么区别。形参和函数体都可以为空。
- 可以有多个构造函数,此时就像是重载函数一样,必须在形参上有差异,
- 不同于成员函数,构造函数不可以被声明为
const
的, - 当我们声明一个
const
的对象,其实时先构造,再有const
性质的,所以我们可以利用构造函数进行赋值。
3).合成默认构造函数。
- 如果我们没有定义构造函数,编译器会为我们隐式地定义一个默认构造函数,默认构造函数无需任何实参,类通过这个默认构造函数进行初始化。
- 规则:如果类内有初始值,按照初始值进行初始化;如果没有就默认初始化。
4).某些类不能依赖于默认构造函数,我们最好自己定义构造函数,原因:
- 默认构造函数只适合于简单的类,
- 编译器只在没有任何构造函数时,才会隐式地定义构造函数,一旦我们定义了构造函数,而没有定义默认的构造函数,那么我们将会没有默认的构造函数,
- 对于类内含有内置类型或者类类型时,如果只依靠默认构造函数,那么内置类型将会是未定义的,而且对于类中类,默认构造函数不起作用。除非内置类型和数组指针都有类内的初始值。
- 类中类没有默认构造函数,编译器无法为该类成员初始化。是无法对哪个类生成默认构造函数,外类还是内类?
5).构造函数的声明和定义。
=default
既可以和声明一起出现在内部,也可以和定义一起出现在外部。Sales_data() = default
;2. 构造函数一样地,声明在内部,定义可以外可以内。- 不接受任何实参,它是一个默认的构造函数。我们希望这个默认构造函数等同于合成的默认构造函数,
=default
可以要求编译器生成构造函数。 - 如果
=default
在内部,那么该默认构造函数是inline
,否则不是。 - 构造函数的初始值列表,与默认构造函数不一样,自定义的构造函数有初始值,第一部分用括括起来表示形参用来接受实参,然后以冒号分割,接下来用类内数据成员名和(这里可以是表达式)或者{}将拷贝的对象括起来,就是一种初始化,
Sales_data(const string &s,unsigned i,double p) : bookNo(s)/{s},units_sold(i),revenue(p){}
- 如果编译器不支持类内的初始值,对于每一个类中内置类型我们都应该显式地指出它的初始值。
- 当我们只对个别的数据成员指定初始化,那么其他的执行默认初始化。
- 函数体为空,那么表示不执行任何操作,只是进行赋值而已。
6).在类的外部定义构造函数,
- 注意带上
类名::
, - 注意构造函数的初始值列表可以为空,但是不妨碍它接受参数,它可以通过函数体来实现成员的初始化,调用
read
函数进行初始化(通过输入进行初始化)。 - 没有出现在构造函数初始值列表的成员将会通过类内初始值或者默认初始化方式进行初始化。
练习:7.15,
struct person;//类的声明,
- 注意函数应该先声明再使用。
- 类的声明在
read
之前是没有问题的。
//5.拷贝,赋值,和析构
1).除了初始化,还需要控制拷贝,赋值,和销毁。
2).对象被拷贝发生在,
- 初始化变量,
- 以值的方式传递或者返回一个对象时,
3).对象被赋值,使用赋值运算符,
4).对象被销毁。
5).如果我们不定义这些操作时,编译器会替我们去合成它们。一般来说编译器将对对象的每一个成员执行拷贝,赋值,销毁操作。
6).某些类不能依赖于合成的版本,
- 类需要分配类对象之外的资源时,合成的往往会失效,例如,管理动态内存的类通常不能依赖于上述操作合成的版本。
- 但是很多需要动态内存的类能且应该使用
vector
和string
对象管理必要的空间,它们可以避免分配和释放空间带来的复杂性。 - 也就是说,类包含的是
vector
或者是string
成员,那么拷贝,赋值,销毁的操作能够正常工作。 - 因为
vector,string
类支持拷贝和销毁。
/2.访问控制与封装
1).使用访问说明符加强类的封装性。使得用户不可以直达Sales_data
的内部并且控制它的实现细节。
2.关于操作说明符。
- 定义在
public
说明符之后的成员在整个程序内可以被访问,public
成员定义类的接口,包括构造函数以及部分成员函数 。 - 定义在
private
说明符之后的成员可以被类的成员函数访问,但是不能被使用该类的代码访问,即private
部分封装了类的实现细节。 (包括数据成员和实现部分的函数)。 - 类可以有0个或者多个访问说明符,可以重复使用同一个说明符。
- 每一个访问说明符表明了接下来成员的访问级别,其有效范围直到下一个说明符的出现或者直到结尾为止。
3).使用class
或者struct
关键字,
- 都可以定义类,
- 唯一区别,默认的访问权限不太一样,类可以在它的第一个访问说明符之前定义成员,对于这样的成员的访问权限依赖于关键字。
- 如果是
struct
那么它是public
,(由c语言就可以知道) - 如果是
class
那么它是private
。 - 同一风格建议使用同一种关键字。
练习:7.18,类封装的好处。
- 类的封装可以保证用户的代码不会修改类的状态(数据成员),
- 用户代码无需修改,只需要修改封装的代码。但是需要重新编译。
- 如果发现对象的状态改变了,那么只可能是在实现部分的代码出现错误。
//1.友元
1).让其他类或者函数成为某一个类的友元,可以使得它允许其他的类或者函数访问它们的非公有成员。
- 在类中,进行这些函数的声明,并在其前加上一个
friend
关键字就可以。 - 友元的声明只能在类内,位置没有限定。
- 友元不是类的成员,不受访问说明符的约束。
- 一般在类定义开始或者结束位置集中声明友元。
2).友元的声明仅仅指定了访问的权限,而不是一个通常意义的声明。如果我们希望类的用户可以调用某一个友元函数,那么我们必须在友元声明之外再专门对函数进行一次声明。(直接定义了,还需要声明吗?)
3).为了使得友元对类的用户可见,我们通常把友元的声明与类本身放在同一头文件中。也就是说我们再为这些友元提供独立的声明。
4).很多的编译器并没有强制限定友元函数在使用之前必须在类的外部声明。但是为了可移植性还是有独立的声明。
/3.类的其他特性
//1.类成员的再探
1).与c语言中的结构体一样,class
替换struct
即可。
2).除了定义数据和函数成员之外,我们也可以在类的内部使用类型别名,
- 使用
typedef
或者using
关键字, - 注意类型别名必须先定义后使用,所以我们一般把它放在类定义的一开始,
- 访问说明符对它起作用。
- 这样可以使得实现细节得到隐藏。
3).inline
函数。
- 在类内定义的成员函数是隐式的
inline
函数, - 我们也可以(在类内的声明指出或者在外部的定义)显式的地指出(只需要在一处就可以,两处也没有问题)但是最好在外定义时指出,便于理解。
- 内联函数应该放在头文件,便于展开。
- 注意类内时成员函数的声明。
4).成员函数也支持重载,不仅仅时构造函数,还有普通的成员函数。
5).可变数据成员,有时候我们希望修改类的某一个数据成员,即便是在const
成员函数里面我们也可以通过在变量声明中加入mutable
关键字做到这一点。
- 可变数据成员不会是
const
,它可以是const
对象的成员,因此一个const
成员函数可以改变一个可变成员的值(注意在变量声明之前加上mutable
关键字),实际上任何一个成员函数都可以修改可变成员的值。
6).类数据成员的初始化,
- 类内的初始值必须用=或者{}表示。
- 对类类型的成员我们如何初始化?用默认值声明成初始值。
std::vector<Screen> Screens{Screen(10,10,' ')};
初始化Screen
类类型时需要实参,初始化vector
用的是一个已经初始化的Screen
作为实参。
//2.返回*this的成员函数
1).返回的是否是引用。
- 返回引用,表示不是副本(拷贝),而是本身
- 如果返回的是副本那么之后再调用并不是改变它本身。
//这里的myScreen返回的是对象的拷贝。
Screen temp = myScreen.move();
temp.set('#');//改变的不是对象的引用,而是temp,也就是对原对象没有影响。
//以下方法返回类的引用,那么就可以缩写为一个序列。因为返回的是一个左值。并且是对于同一个对象做以下的操作。
myScreen.move().set('#');//注意这样的表达。
2).从const
成员函数返回*this
,一个const
成员函数以引用的形式返回*this
,那么结果将是const s&
,即是常量的类。const
的类,无法调用非常量成员函数。于是不能写在同一个序列中。即,myScreen.display(cout).set('#');
是错误的,因为display
返回的是一个const
对象,无法调用非const
成员函数。
3).基于const的重载,成员函数是否是const
的是可以实现重载的。就是底层的const
。我们可以基于返回类型和this类型是常量引用还是非常量引用来确定函数,因此它还是重载的。典型例子就是重载函数调用同一个实现函数,我们就会把这个实现函数放在private
中去。
- 避免多次书写同样的代码,
- 降低类的复杂度,
- 要调试时,我们只需要在一个地方修改代码就可以,
- 最重要的是,类里面定义的函数就是一个内联函数,并没有额外的开销,
- 实际开发中,有很多这样的例子。
练习:7.30,在代码里面显式地指出this,
- 优点,可以很明确知道是对象的数据成员,同时可以定义和数据成员一样的形参,
- 缺点,多余冗余。
//3.类类型
1).类类型的几点说明。
- 即使两个类的成员列表完全一致,它们(名字不一样),它们也是不一样的类,
- 对于一个类来说,它的成员和其他类的成员都不是一个作用域的,
- 我们可以直接将类名作为类型名,也可以加上关键字
struct
或者class
(这种方式是从C语言中继承而来的), - 类的声明,有时候也称为前向声明,
class Screen;
- 在类的声明之后和定义之前,Screen是一个不完全类型,因为我们不知道它的成员,
- 不完全类型的使用,
- 可以定义指向这种类型的指针或者引用,
- 可以声明(不能定义)以不完全类型为参数或者返回值的函数,
- 类的对象创建之前,使用引用或者指针访问类的成员前,类必须被定义。(只有知道成员后才知道要分配多少的空间,才知道有那些成员),
- 类声明的例外,直到类被定义之后,数据成员才可以被定义成这种类型,因此一个类的数据成员不能是该类自己。但是可以是指针或者引用,数据结构。
//4.友元再探
1).把其他类,其他类(已经定义过的)的成员函数定义成友元。友元函数能定义在类的内部,这样的函数是隐式内联的。
2).注意类的友元类,声明形式,friend class ...;
只有定义成友元,其内部的所有成员函数,都可以访问(使用原点运算符号,或者箭头符号,和C的结构体是一样的)这个类的私有数据。
3).注意友元关系没有传递性,一个类的友元类的友元不具有访问该类的private
的权利。每一个类控制自己的友元类或者函数。
4).将成员函数作为友元类,
- 需要加上类名,其他一致
friend void Window::clear();
- 注意要在该类定义之前声明这个成员函数,
- 注意设计方法
- 首先定义
window
类同时里面声明成员函数,但不能定义,因为此时Screen
的数据成员还没有定义, - 再定义类
Screen
,添加友元函数, - 定义成员函数。
5).函数重载和友元,如果将一组重载函数声明为友元,需要为这一组重载函数中的每一个单独声明。注意内部友元声明时,extern
关键字不需要,加不加都是一个声明。
6).友元声明和作用域。
- **类和非成员函数的声明不是必须要在它们的友元声明之前。**当一个名字第一次出现在友元声明中,我们隐式地认为该名字在当前的作用域是可见的,而且友元本身不一定真的声明在当前作用域中。
- 在类内部定义非成员函数函数,为了使得函数在外部可见,我们必须在外部声明。
- 我们如果要在成员函数中使用友元函数,该友元函数也必须要在外部先声明过。
- 先声明再使用,
- 友元声明不算声明,只是规定了访问权限。。
7).以上说明。即使我们在内部进行友元声明甚至定义,但是为外部可见或者内部成员函数可以使用该友元函数。都需要在外部进行正式地声明。
- 不论如何,记住在外部进行友元函数的声明。
- 声明的顺序是无所谓的。保证所有声明都在定义之前就可以。
/4.类的作用域
1).类都会定义自己的作用域,
- 对于
public
的数据或者函数成员只能由对象,引用或者指针使用成员访问运算符.或者->
来访问, - 对于类,类型(public?)成员,数据成员,函数成员,例如
typedef
,using
定义的别名,数据成员,函数成员等则使用类名加::(作用域运算符)
来访问。不管是哪一种,跟在运算符后面的名字只能是对应类的成员。(是否是只有public才可以访问呢?)
2).几点说明。
- 一个类就是一个作用域,所以我们定义成员函数时,必须要提供类名,避免在类的外部,类内的成员全部都被隐藏起来了。
- 一旦遇到类名(表明我们当前就在这个类的作用域里面),定义剩下的部分,参数列表,函数体使用类的成员无需再次指明是这个类。
- 但是函数的返回类型是在类名之前的(在类的作用域之外的),所以我们必须要要再一次指明是哪一个类内的数据成员。(定义在外部的类成员函数)
- 友元函数不需要。
//1.名字查找和类的作用域
1).名字查找,寻找和所用名字最匹配声明的过程。
- 首先在名字所在块寻找,注意在名字使用之前的声明,
- 如果没有找到,就继续在外层寻找,
- 最终没有找到就报错。
2).类的编译顺序
- 编译所有的成员声明,包括类型声明等。
- 直到类全部可见后,才编译函数体。因此声明顺序不要紧。
3).类型别名的声明必须在使用之前。,
- 例如,在声明变量中使用的类型名,或者函数的返回类型,参数列表中使用类型名,必须确保先声明再使用。
- 类型别名和成员函数体的查找名字的方式是不一样的。
4).同一个类型别名不要在类中重复声明。例如,在下面这个例子中,会导致内部声明的函数返回值为string
而外部定义的函数的返回值是int
。矛盾。
{
typedef int val;
val getName()
{
...
}
class people{
typedef string val;
val getName();
}
}
5).很多编译器,对上述的错误不负责,忽略这样的错误。
6).定义类型别名一般在类的一开头处。
7).成员函数体的名字查找,
- 首先考虑在函数里面的使用之前出现声明的,
- 考虑类中的所有成员,
- 外层继续寻找。
8).一般我们不会为成员函数使用成员名字作为形参。因为不好理解。容易混淆。解决办法
this->height;
Screen::height;
9).类作用域中使用全局变量,如果我们需要使用与类内同名的全局变量,(即此时类内的成员把全局的变量屏蔽了),方法是使用全局的作用域运算符,::height
,来指定使用的是全局的。但是我们一般不会这样做。
10).对函数体中的名字,是在其出现处对其进行解析,即,当我们的成员函数在外部定义时,它使用的名字的查找,当需要在类外层作用域中寻找时,不仅要在类定义之前寻找还要在函数定义之前去寻找。
练习:
- 7.35,在类内部和全局都有同名的类型说明符时,内部声明的函数和外部定义的函数的返回值,会不匹配,我们应该加上类和作用域运算符,根本办法是不要这样做。
/5.构造函数再探
//1.构造函数的初始值列表
1).类内初始化,在变量声明时就对其赋值。
2).对于对象的成员,也是有一样的区别的。对于构造函数,如果我们没有在初始值列表中,显式地指出它的初始化;那么它就会在构造函数体之前进行默认初始化,所以在构造函数体里面的,其实就是赋值语句,虽然最后的成员的数据是一样的,但是过程却不一样。
3.以下两个例子说明初始化的重要性。
- 对于
const
或者引用的话,初始化是必不可少的,此时赋值和初始化是完全不一样的概念。 - 对于类类型且这个类类型没有定义默认构造函数,我们也必须对它进行初始化。(也就是说一个类中的类成员如果没有显示地指出默认构造函数,是没办法进行默认初始化的。)
4).再一次强调,如果有引用或者const
,或者类我们一定要记住为它们进行初始化,这是唯一一次可以修改它们值得机会。
5).注意我们可以用数据成员作为赋值对象,即()
里的表达式的组成部分,~~初始化列表是逗号运算符,求值顺序是明确的,~~错误说法
6).(1)类的初始化还关乎底层效率的问题,它们的过程是不一样的,赋值是更加多的步骤的,(2)养成在构造函数中初始化的习惯。
7).成员初始化的顺序以及注意事项,
- 每一个数据成员只能出现一次,
- 构造函数的初始值列表仅仅说明成员的初始值,而不限定初始化的顺序,
- 真正的初始化的顺序与它们在类中定义的顺序是一样的。
- 尽量初始化的列表和定义的顺序一样;尽量不要初始化时一个成员为另一个成员赋值;
- 有的编译器会警告,当初始化的列表的顺序和定义顺序不一样时。
8).默认实参和构造函数,我们也可以在构造函数中设置默认实参。
- 如果一个构造函数为所有的参数都提供了默认实参,则它实际上也定义了默认构造函数,(即与我们在定义类时为每一个数据成员初始化是一样的效果)
- 但是我们应该注意到一些逻辑,例如有了销售数量,我们也同时需要销售价格的输入的。
练习:
- 7.38,注意我们如果使用了默认实参,效果等同于默认构造函数,此时我们需要把默认构造函数去掉,否则具有二义性。
- 7.39,我们一定不能为一个类,定义两个形参列表中默认实参都全的构造函数,一旦这样,当我们不输入值时,编译器将会有二义性的报错。
//2.委托构造函数
1).委托构造函数特点。
- 委托构造函数也有函数体,也有参数列表,也一样的和类同名,没有返回值,
- 它的初始化列表变成了其他的构造函数,其他细节没有改变(
:
不可以省略。) - 其实我们可以认为可以接受任何参数的构造函数就是默认构造函数。
- 委托构造函数的形参可以作为实参传递给被委托的构造函数,
- 注意,当我们进行委托时,需要把被委托函数的整个,包括初始化列表和函数体执行完毕后,才继续执行委托函数的函数体。
- 被委托函数一定在类内要有,也是一个匹配的过程
- 被委托函数可以是一个委托函数,此时是被委托函数的被委托函数的函数体执行完之后再执行被委托函数,最后才是一开始的委托函数的函数体。
- 这很像一个递归的过程。
//3.默认构造函数的作用
1).对象被默认初始化或值初始化时,会执行默认构造函数。
2).默认初始化的情况,
- 在块作用域内不使用任何初始值定义一个非静态变量,或者数组(如果数组是内置类型,那么元素将会是未定义的)时,
- 类本身含有类类型的成员且使用合成的默认构造函数时,(如果类成员没有默认构造函数将会导致编译器没办法生成正确的合成的默认构造函数),
- 类类型的内置数据成员没有在构造函数初始值列表显式地初始化。
3).值初始化情况,
- 数组初始化时我们提供的初始值的数量少于数组的大小时,不够的会进行值初始化,
- 不使用初始值定义一个局部静态变量时,
- 当我们书写
T();
T为类型名,显式地请求值初始化时。例如vector
就是用这样的形式进行的值初始化。
4).对于类中的某些类成员缺少默认构造函数,那么这个类创建对象时,是没有办法合成默认构造函数的。(这个类没有定义默认构造函数)。
5).类必须包含一个默认构造函数以便在以上情况下时不会未定义,尤其是当它作为别的类的类成员时。
6).使用默认构造函数,请注意
Sales_data obj();
这样是声明了一个返回类型为Sales_data
,形参为空的函数。- 而如果想要定义一个使用默认构造函数进行初始化的对象,正确的方法是去掉空括号。这很容易错。
练习:
- 7.43,
- 解决没有默认构造函数的类成员的办法就是对它显式地初始化。
- 类成员可以不设置任何默认构造函数,但是如果有设置一定要有默认构造函数。
- 对于类成员,编译器合成的默认构造函数会按照它的默认构造函数对它进行默认初始化。所以一定要有默认构造函数。
- 7.44,使用类的容器时,如果类没有默认构造函数,那么我们显式请求默认初始化会报错。
//4.隐式的类类型转换
1).定义类的类型的隐式转换规则,如果构造函数只接受一个实参,那么它实际上定义了转换为此类类型的隐式转换机制,有时候我们这种构造函数称为转换构造函数。
2).能通过一个实参的调用的的构造函数定义一条从实参类型向类类型隐式转换的机制,例如,
- 注意到
item.combine(null_book);
接受的实参是string
,并且Sales_data
有仅仅接受string
类型的构造函数;由于combine
的形参是const Sales_data &
,我们可以做到隐式的转换。 - 编译器通过
null_book
自动创建了一个临时的Sales_data
对象,这个临时量传递给combine
。
3).只允许一步类类型转换,例如
item.combine("ssssss");
那么将会是错的,因为此时将要隐式执行两种转换规则,
- 首先把字符串字面量转换为
string
, - 再把临时的
string
转换成Sales_data
对象。
- 改进,
item.combine(string("sssssssss"));
或者item.combine(Sales_data("sssssssss"));
创建没有名字的临时量。
4).类类型不总是好的,
- 是否需要这样做,依赖于需求,
- 我们也可以,
item.combine(cin);
这样做会调用构造函数,需要用户自己输入,然后创建一个临时的Sales_data
对象,随后进行combine
操作; - 缺点是:操作完成之后就会被丢弃,我们就不能再访问它了。
5).抑制构造函数定义的隐式转换,
- 用关键字
explicit
进行限制, - 我们只需要在只接受一个参数的构造函数前面进行限制就可以了。
- 并且
explicit
关键字只对一个实参的构造函数有效。 - 只能在类内声明构造函数时使用
explicit
,在类的外部定义时不应该重复。
6).使用explicit
构造函数只能用来直接初始化,
- 使用
()
形式的初始化时,可以使用explicit
声明的构造函数,(其实就是还是传参) - 而使用
=
(拷贝)方式,那就是要隐式转换了。即此时如果构造函数是explicit
限定的话那么是不可以使用的。因为编译器不会进行隐式转换。 - 总结起来就是,我们可以通过
()
创建一个类的实体;但是不可以通过隐式转换,让编译器为我们创建一个temp
量。
7).使用构造函数进行显式转换,
item.combine(Sales_data(null_book));
item.combine(static_cast<>Sales_cast(cin));
8).标准库中的显式构造函数的类,
- 接受一个容量参数的
vector
构造函数是explicit
的 - 接受单参数的
const char *
和string
构造函数不是explicit
的。
练习:
- 7.47,我们应该使用
explicit
来避免与我们的初衷相违背; - 7.48,使用
()
定义对象,是不是explicit
是没有差别的; - 7.49,???(b)修改成常量引用就可以??隐式类型转换,是得到一个临时对象,临时对象不可以修改??
- 7.51,是否设置成
explicit
应该看语义,string
的转换是合理的那么我们不应该设置explicit
;但是对于vector
明显的需要设置explicit
。
//5.聚合类
1).聚合类是用户可以直接访问其成员,并且具有特殊的初始化形式的类。满足以下条件的类,是聚合的,
- 所有的成员都是
public
, - 没有定义任何的构造函数,
- 没有类内的初始值,
- 没有基类,也没有
virtual
函数。
2).初始化,
- 使用
{}
,{}
里面的成员初始值列表用来初始化聚合类的数据成员,初始值的顺序一定要和声明的顺序是一样的。 - 与初始化数组的规则一样,如果初始值列表的元素个数少于成员的数量,那么不足的成员应该进行值初始化;初始值列表的个数一定不能超过类的成员数量。
3).显式地初始化类的对象的成员有两个明显缺点
- 正确初始化成员的重任交给了用户,并且这样的初始化是很乏味的,(需要记住顺序,不能漏,不能错位)
- 添加或者删除一个成员,所有的相关语句都需要更细新。
//6.字面值常量类
1).几点说明。
- 类也可以是字面值类型(除了指针,引用,内置数据类型),
constexpr
函数的参数和返回值必须是字面值类型;如果形参不是常量返回值也不一定是常量。- 隐式
const
例如给成员传递的this
就是隐式的const
。字面值类型的类中可能含有constexpr
函数成员,必须满足constexpr
函数的所有要求,这些constexpr
函数是隐式的const
。即是const
成员函数??
2).数据成员都是字面值类型的聚合类是字面值常量类。如果一个类不是聚合但是满足以下条件,那么它也是字面值常量类,
- 数据成员都是字面值类型(算术类型,指针,引用);(注意,类,IO库,string不是字面值,字面值类型,编译的时候就要得到,所以它简单,容易得到,和字面量没有什么复杂方面的区别。),
- 类必须至少有一个**
constexpr
构造函数**, - 如果一个数据成员有类内初始值,则内置类型成员的初始值必须是一条常量表达式,如果是类成员,则必须使用自己的
constexpr
构造函数, - 类必须使用析构函数的默认定义,该成员负责销毁类的对象。(就是使用编译器合成的销毁操作。)
3).constexpr构造函数,
- 构造函数不能是
const
的, - 但是字面值常量类的构造函数可以是
constexpr
函数,事实上,一个字面值常量类必须至少提供一个constexpr
构造函数, constexpr
构造函数可以是=default
,或者删除函数的形式,- 此外有需要满足构造函数和
constexpr
函数的要求,既没有返回类型,意味着不能有return
,而它只能有return
,所以函数体一般为空。 - 通过前置的关键字就可以声明一个
constexpr
构造函数,类中可以有constexpr
成员函数 , constexpr
构造函数必须初始化所有的数据成员,且初始值或者(使用constexpr
构造函数或者是一条常量表达式???示例中,并没有,不是常量表达式)。constexpr
构造函数用于生成constexpr
对象(实例化一个对象)以及constexpr
函数的参数或者返回类型。(即某constexpr
函数的返回类型/形参是该字面值类类型)
练习,
- 7.54,(1)再一次强调,
constexpr
函数函数体中只有return一条操作语句可以有其他的using,typedef
等。(2)而且它应该还是const
的不能修改值。????
/6.类的静态成员
1).有时候类需要它的一些成员只和类本身有关,而不是与类的各个对象相互关联。典例,银行账户的基准利率。
- 首先是没有必要再每一个类中存储利率的信息,
- 如果利率修改了,那么每一个对象都可以使用新值而不需要逐一修改。
2).声明静态成员,
- 方法,在成员声明之前加上关键字
static
,使其与类关联在一起。 - 静态成员也可以是
public
或者private
, - 静态成员可以是指针,引用,常量,类类型,函数等。
- 静态数据成员存在于所有的对象之外,任何对象不包含任何与静态成员有关的数据;但是静态成员被所有的类的对象所共享,就是一种 对象不包含,但是可以使用 的意思。
- 静态函数成员,不和任何对象绑定在一起,所以它们没有
this
指针;所以它们不能声明成const
的 ;所以我们也不能在静态函数里面使用this
指针。既不能显式地用this
,也不能,在静态成员函数调用**非静态(因为非静态是绑定对象的)**成员时隐式使用。静态函数成员只能使用静态成员,或者使用非静态成员时加上对象名。
3).使用类的静态成员,
- 我们可以使用类名加上
::
作用域运算符,访问静态成员, - 虽然类的静态成员不属于类的任何一个对象,但是我们可以使用类的对象,引用,指针来访问
public
静态成员。(包括public
数据成员吗?) - 成员函数不用通过作用域运算符就可以直接使用静态成员(数据和函数)。
4).定义静态成员,
- 与普通的成员函数一样,我们也可以在类的内部或者外部定义成员函数,
- 在类的外部定义静态成员函数时,关键字
static
是不能重复的,它只能出现在类的内部。 - 由于静态数据成员不属于任何一个类,它们并不是在创建类的对象的时候被定义的,这意味着它们不是由类的构造函数初始化的。
- 而且我们一般不能在类的内部初始化静态成员,相反地,必须在类的外部定义和初始化每一个静态数据成员
- 和其他对象是一样的,一个静态数据成员只能被定义一次,
- 类似于全局便变量,静态数据成员定义于所有的函数之外,因此它一旦被定义就将是整个程序的生命周期。
- 方式,与定义成员函数的方式差不多,
double Account::interestRate = initRate();
- 注意从类名开始,整条语句的剩余部分就位于类以内了,这一点和成员函数是一样的;还有一点就是,数据成员也可以访问类的
private
成员。 - 保证静态数据成员只定义一次,最好的办法就是,与其他函数的定义放在同一个文件当中。(会有重名问题吗?)
5).静态成员的类内初始化,
- 通常类的静态数据成员不应该在类内初始化,但是我们可以为静态数据成员提供**
const
整数类型的类内初始值**,要求
- 静态数据成员必须是字面值常量类型(
constexpr
) - 初始值必须是常量表达式。
static constexpr int period = 20;
- 因为这些成员本身就是常量表达式,所以我们可以用它在任何需要常量表达式的地方,例如数组的维度。
6).即使常量静态数据成员在类内已经初始化了,我们通常还是需要在类外定义它,
- 如果某一个静态成员的仅仅用于编译器可以替换它的值,那么一个初始化的
const
或者constexpr
不需要分别定义, - 如果我们不要将它用于值不能替换,例如拷贝,则该成员必须要有一条定义语句,?
- 外部的定义语句,
constexpr int Account::period;//注意我们不再指定一个初始值,(这一点与const很像。)
7).静态成员可以用于某一些场景,而普通的成员是不可以的,
- 静态数据成员可以是不完全类型,它的类型可以是它所属的类。
- 静态成员可以作为默认实参(成员函数的默认实参),而普通的数据成员是不可以的,因为没办法真正地提供一个对象以便从中获取成员的值。
练习:
- 类的作用域运算符是否只能访问
public
,是 - 对象获取类的成员只能是
public
,包括位于public
的static
函数和数据,是否包括类型别名。 - 友元,成员函数(包括
static
)都可以访问所有static
(函数,数据)。
- 7.56,静态数据成员是
- 全局的,而且不会和其他类以及普通的全局变量重名冲突,
- 它可以是私有的,
- 它是与特定的类关联的。
- 7.58,类外的静态成员初始化和函数很像,
double 类名::成员名字
。
/7.类
- 数据抽象就是定义数据成员和函数成员的能力,
- 封装就是不被任意访问。
- 可变成员可以用
const
- 抽象数据类型,封装了实现细节的数据结构。
- 显式构造函数,可以用一个实参调用但是不可以隐式转换的的构造函数。