C++ Primer(第五版)第七章内容——类。
注意:定义类之后,需要一个分号!!
-
定义类的目的
数据抽象与封装。
对外指提供接口,隐藏实现。或者说接口与实现分离。
-
类成员函数声明与定义
类的成员函数都要声明在类内部,但定义可以外部。
定义在内部的成员函数默认是内联(inline)的(隐式inline)。
-
类成员函数的隐式this指针
在成员函数内部访问类成员或函数,都隐式的通过
this
指针访问。```cpp string Info::get_info() { return info ; } Info inf ; cout << inf.get_info() ; ==> string Info::get_info(Info * const this ) { return this->info } Info inf ; cout << get_info(&inf) ; ```
如上,就大概是编译器实际实现类成员函数调用、访问的过程。这个通过Python中类成员函数定义就比较清晰。
Class Info(object) : def get_info(self) : return self.info
需要注意的是其中的
const
,根据书上的描述,this
的值是不能被改变的。所以我想它传递的应该是一个底层const的指针。因为this变量被隐式定义,所以我们不能再定义名为this的变量。也不能修改this变量的值。
-
定义const成员函数
如果成员函数不修改成员变量值,那么通过在函数形参列表后加入const关键字来实现该逻辑。
string Info::get_info() const {...}
实际上是通过给隐式的
this
指针加上顶层const
。string Info::get_info(const Info *const this) {...}
如果类要支持常量定义,那么定义const的函数是必须的。因为如果定义类的const实例,而函数都是非const的,由于非const实例不能绑定到顶层const指针上,所以成员函数不可用。
-
类作用域
类本身就是一个作用域。
在外部定义类的成员函数时,需要使用作用域运算符
::
。 string Info::get_info() { return info . } -
类成员变量、成员函数的定义顺序与调用顺序无关
如果函数A定义在函数B的后面,但是在函数B也可以直接调用函数A。
因为编译器分两步处理类:
首先编译成员的声明。
然后再编译函数体(如果有的话)。
-
返回调用对象本身的引用
使用
*this
来返回调用的对象本身。Info& Info::add(Info & b) { info += b.info ; return *this ; }
注意函数返回类型的引用符号——返回的是调用对象的引用,即左值。
-
类的拷贝
IO类不能拷贝。只能传引用。
默认情况下,拷贝类的对象其实拷贝的是对象的数据成员。
-
默认构造函数
当类中没有定义任何构造函数时,编译器尝试创建一个默认构造函数,也叫 合成的默认构造函数(synthesized default constructor)。
有3点:
-
如果自定义了构造函数,则默认构造函数不会被创建。
不过可以快捷的定义一个默认的构造函数。
Info::Info() = default ;
对,就是这么定义。
-
默认构造函数,编译器对成员变量执行初始化的流程:
-
如果指定了默认值,用默认值初始化。
-
没有,则尝试用默认值初始化该成员变量。
-
-
由上,引出了默认构造函数可能导致的问题:
-
对内置类型的成员变量,因为成员变量属于块内变量(局部变量),故执行随机初始化。
-
如果成员变量类型是另外一个类类型,且该类型没有默认构造函数,那就报错了。
-
-
-
构造函数的初始值列表
在参数列表后、函数体前(左大括号前)使用
:member_var(param_var),...
来定义初始值列表。Sales_data(string &s , unsigned n , double p): bookNo(s) , revenue(p*n)} {}
必须为const成员、引用、不能默认初始化的类提供初始值列表的初始方式(或者使用类内初始值)。
-
总结成员变量赋值顺序[已更新]
-
初始化过程
按照成员变量的定义顺序,依次初始化各成员变量,初始化规则为:
a. 如果成员变量在初始化列表中有定义,那么用初始化列表中的定义去初始化。
b. 否则,查看成员变量的定义形式:
1. 如果是类内初始值形式,用该值初始化 2. 如果无类内初始值,那么用默认方式初始化。 > 默认初始化,如前面所言:对内置类型,随机赋值,或者说直接分配空间,不赋初值;对类类型,调用默认构造函数,没有则报错。 > 用类内初始值定义时不能使用小括号!因为编译器在语法解释时其形式会跟函数声明的形式冲突!!!可以使用`=`或者`{}`来定义。使用`=`并不代表就是赋值!
由上,可知两点:
-
成员变量间的初始化顺序,是由其定义顺序决定的!(与初始化列表中顺序无关)
-
在进入构造函数体之前,各成员变量都已完成了初始化操作。 -> 所以const变量、引用,都必须在这个阶段给予初始值!而不能在构造函数体中赋值。
这样看来,每个构造函数都对应一段不同的初始化代码。
-
-
赋值过程
在构造函数体中做赋值操作。
之前对 类内初始值与初始化列表、默认值 三者的初始化选择优先级顺序有疑惑,后来无意间初始化一个const变量得到了答案!要知道const变量是只能做初始化而不能做赋值的,然而当同时在初始值列表和类内初始值同时给于const变量不同的初始值,得到的结果是初始值列表中指定的值。且不报错。说明整个过程只做了初始化操作,且初始值列表的优先级高于类内初始值。猜测类内初始值与默认值是同等级别,一个东西。
-
-
类的拷贝、赋值和析构
这里讲的应该仍然是基础。
类对象间的
=
操作会发生赋值(拷贝),对象生命结束会发生析构。如果我们未定义自己的赋值(拷贝)、析构操作,那么编译器将自动合成这种操作!
编译器的行为是:对类对象的每个成员,做赋值(拷贝)、析构操作。
所以,如果类中的成员,除了类对象本身的空间外,没有额外的空间占用(我的理解,就是指针!如果有new操作,那么显然类本身只有指针本身的大小,但是在类空间之外,还有由指针指向的堆空间),那么编译器默认的操作就是没有危险的。赋值(拷贝)不会有遗漏,析构不会漏掉内存泄露。
如果有动态内存操作,那么必须要手动管理这些操作!(应该在后续介绍,目前所知,对于析构就是定义析构函数了)
使用vector完全没有问题! 因为vector本身有完善的赋值(拷贝)、析构操作,不会有内存问题。vector在赋值时(
=
, push_back`等操作),会拷贝元素值!在析构时,会清空vevtor装的所有元素值!使用vector时就可以直接用默认的操作,这告诉我们封装的好处。即,自己的事情自己做,外部只需要告诉你命令。
-
访问控制与封装
主要说一下
public
和private
的依据:public
下的成员应该是要提供给外部的接口。private
下的内容应该是被隐藏的细节。以上就实现了封装!
-
关键字
class
与struct
的区别在C++中,
class
和struct
都是定义的类类型!他们唯一的区别就是——默认的访问权限。class
是private
,struct
是public
。不要受C的影响。
-
友元
友元函数定义的方法:
-
在类内部声明友元函数,使用
friend
关键字 [作为类的友元的声明] -
在类声明所在的头文件中声明友元函数。[作为函数的声明] (经过测试,在GCC、MSVC2010下可省略。)
建议:
友元声明在类的开始或者结束 [无所谓public 或 private ,无访问控制符影响,因为友元不是类的成员!]
性质:
友元函数不属于类的成员,不受所在位置的访问控制级别约束。
友元函数可以访问类的非公有成员。
写的一个例子:
------ test.h class People { // friend function declaration friend void teach_knowledge(std::string , People &) ; public : void make_self_introduction() ; private : std::string knowledge ; } ; // function declaration void teach_knowledge(std::string , People &) ; -------- test.cpp(调用部分) int main(int argc , char *argv[] ) { People p ; teach_knowledge("my name is xuwei" , p ) ; teach_knowledge("I am 5 years old " , p ) ; p.make_self_introduction() ; return 0 ; }
友元类:
与友元函数类似,还可以在类内部定义某个类为该类的友元,称为友元类。 友元类可以访问类的所有成员。
友元类成员函数:
不仅可以指定某个类为友元,还可以特指某个类的特定成员函数为该类的友元。 这时需要一个确定依赖顺序! 试想,有类People,和类Ball 。 类Ball中想要把类People中的play函数作为自己的友元, 1. 那么在Ball之前People中的Ball函数必须被声明 2. People中的play要访问Ball中的私有成员(友元函数的意义),则要求Ball必须先有定义!(因为只有完整的定义完一个类,才能访问该类的成员,否则报错—— 错误:对不完全的类型‘class Ball’的非法使用) 这似乎形成了一个环——其实不是,这里需要用到声明的作用。 第2条,明确要Ball有定义,但是注意到我们这个函数本身可以先声明再定义,所以这个友元函数放在所有类定义完后定义。 第一条,决定类People必须先定义。如果类People中需要用到Ball这个名称,那么还需要在定义People之前声明一下Ball。 如下顺序: 1. class Ball ; // just declaration 2. class People { void play(Ball& ball) ; } ; 3. class Ball { friend void People::play(Ball& ball) ; // 友元函数,无所谓public or private。 private : std::string name ; } ; 4. void People::play(Ball &ball) { std::cout << "Playing " + ball.name << std::endl ; }
-
-
在类中定义一个类型成员
即是,在类作用域下,定义一个类型成员。
如书上的例子:
class Screen { public : typdedef std::string::size_type pos ; //using pos = std::string::size_type }
在类中定义的类型,作用域在类中,且受到访问控制的约束!即上述定义的类型pos是public的,可以在外部被访问。
与其他类成员不同,类型必须先定义,才能使用。所以应该定义在类的开始。
-
令成员成为内联函数
前面说到,如果在类内部声明并定义,那么这个函数是隐式内联的。
也可以使用
inline
显式内联。此时,可以在声明位置或定义位置使用inline
表示,也可以在两个位置都写上inline
.书上说声明为内联的函数应该声明和定义在同一个(头)文件中;不过在GCC中,不在一个文件中也没有问题。
-
可变数据成员
使用
mutable
关键字来定义可变数据成员。看到的应用场景是:如果一个成员函数定义为
const
的,那么理论上它不能修改任何成员变量。但是如果一个变量使用mutable
修饰,那么这个成员就可以被const函数修改。可以用来统计类的操作被调用次数。例: mutable size_t access_ctr ;
-
类数据成员的初始化
在C++11的新标准中,建议使用
类内初始值
的方式。即,直接使用=
或{}
赋初值。 -
返回一个
*this
对象引用如果想要支持链式处理,那么就有必要返回一个
*this
的对象引用!注意的一点是,一个返回*this引用的成员函数,如果对象被定义const的。那么这个函数就不能够被调用了!因为对象为const时,传入的this指针是const的,然而返回时却要求返回非const的对象,我们知道可以将非const隐式转为const的,但是const的转为非const的,需要使用
const_cast
。 -
不完全的类型
类同样可以只声明,不定义。这时称其为
不完全类型(incomplete type)
。此种类型只可用于定义指针或者引用(引用可能在定义函数形参时使用,指针还可以用来定义指向该类型的指针对象)。
但,
-
不能定义对象
报错: 错误:类型不完全,无法被定义
-
不能访问成员
加入Ball类型是不完全类型。那么以下声明合法:
void play(Ball& ball) ;
但是定义就不合法了:
void play(Ball &ball) { cout « ball.name ; }
因为构造对象需要知道内存空间大小,没有定义编译器没法计算!访问成员编译器需要知道该成员地址,没有定义也没法知道。只有指针或引用是可以的,因为编译时仅仅生成一个符号。
-
-
类的作用域
[推翻之前观点]在类外部定义成员函数时,从参数列表开始就是在类作用域下了!
举例,设成员函数
move
和成员类型Position
都是在对象People
下的,则该函数的定义如下:People::Position People::move( Position moving_pos ) { pos += moving_pos ; return pos ; }
如上可以看出: 返回类型的作用域需要使用
People::
来指定在类下,成员函数也需要其指定,但是成员函数的参数列表中就可以不再需要类作用域的指定了。 -
类内部名字查找顺序
我们知道,类声明或定义时一般都是先函数再变量,且不会考虑变量、函数间的顺序的。
这与类外的名字查找顺序有些不一样,因为在外部我们都是从上到下的,后面的才能看到前面的。
为什么类内似乎就忽略这这种先后关系呢?
这其实是错觉!
事实是,编译器在处理类内成员时,是先整体从上到下扫描一遍名字(声明),不会去查看其内部的定义!只有在扫描完类内所有的名字后,才会再去查看定义,而此时,类内部所有的名字(成员变量、函数)都已经被扫描存储到了编译器的表中了,即都是可见的!
所以本质仍然是从前往后,只不过不先对定义做解释罢了。
因此,在使用
typedef
、using
定义的名字去定义参数前,这些名字必须出现在这些参数的前面。 -
成员变量的初始化顺序
是按照成员变量定义的先后顺序,而非初始值列表中的顺序。
这可以认为,编译器在扫描成员变量时,便开始做初始化了。 [猜测] -> 应该是针对每个构造函数做初始化。
-
使用默认构造函数定义对象[易错!]
不能加括号!!
即:
T t ;
而不能是
T t() ; // 错误!这是在声明函数,而非定义一个类对象!
-
隐式类类型转换及
explicit
含义
如果只接受一个参数的构造函数,实际上定义了一种隐式转换机制,这种机制通过隐式调用该构造函数,将构造函数需要的参数类型转为类类型。
这样的构造函数称为
转换构造函数
常见的隐式转换:
string s = "implict cast" ; // 将 const char* 隐式转为 string类型,这个很常见,但从未想过为何可以。 // 其实是隐式调用了 string(const char*)这一构造函数。
隐式转换只能跨一步,即不能在一条语句里,先隐式转为一个类型,再从转换后的类型转为新的类型,即使这种转换能够连起来。如书上的例子,在一个函数调用的,想要完成
const char* -> string -> Sales_data
, 结果不能通过编译。首先明确,这种隐式转换,本质是说是容易带来歧义的,不应该广泛使用。除非像 const char* -> string这样看起来可以理所当然的转换。
套用Python的话,
Explicit is better than implicit.
explicit关键字
在构造函数前加上explicit 关键字,就可以禁止隐式转换!
关于此关键字,两点注意:
-
只能出现在类内部的声明处,不能出现在类外部的定义位置。
-
只能禁止隐式转换,不能禁止强制转换——
static_cast
就是说,只要有只接受一个参数的构造函数,这个类就具有这种转换能力。explicit只是禁止的自动隐式转换的能力。
例子:
1. 多次转换出错 构造函数:People(std::string name) : name(name){} ; 使用: People p = "LiMing" ; 编译报错: 错误:请求从‘const char [7]’转换到非标量类型‘People’ 如上,因为想要将 const char* -> string -> People ,出现连续两次隐式转换而出错。 不过,因为const char* -> string 实在是太常用了,以致于常常忽略了其中存在隐式转换的过程。 因此,一定注意此点! 改: string name = "LiMing" ; People p = name ; // 编译通过 2. 使用explicit,禁止隐式转换 上面 “People p = name ;” 的语句看起来实在别扭!对应的,像vector中一些单参数的构造函数, 也是用explicit禁止隐式转换的。 因为它带来了不明确、不直观的含义! 改构造函数: explicit People(std::string name) : name(name){} ; 调用不变,编译结果:错误:请求从‘std::string {aka std::basic_string<char>}’转换到非标量类型‘People’ 试试强制转换: People p = static_cast<People>(name) // 成功编译 3. 在定义出使用explicit关键字 将之前inline的定义变为声明,同时在外部定义,且加上explicit关键字: explicit People(std::string name) : name(name){} ; 编译结果:错误:只有构造函数才能被声明为‘explicit’ 错误说明有些奇怪,可能这样之后构造函数的定义就不能被正确识别的。
最后,explicit对只有一个参数的构造函数才有意义,但是对任意构造函数加上explicit,似乎并不会影响编译。
最后,所谓只有一个参数的构造函数,是指在调用时只需一个参数的构造函数——即,包含默认参数的构造函数也同样有此能力。
explicit People(std::string name , std::string knowledge="haha") // 同样是转换构造函数。
-
-
聚合类
满足:
-
所有成员都是public
-
没有定义任何构造函数
-
没有类内初始值
-
没有基类,没有virtual函数
如:
struct ClassKnowledge { std::string class_name ; std::string knowledge ; } ;
可以如下初始化:
ClassKnowledge yuwen = {“语文” , “兰亭集序”} ;
如其名,一堆变量的聚合。不利于维护,不建议使用。
-
-
字面值常量
数据成员都是字面值类型的聚合类是字面值常量。
或者满足:
-
所有成员都是字面值类型
-
类必须至少有一个constexpr构造函数
-
如果数据成员有类内初始值,那么该值必须是字面值;如果数据成员是类,那么类必须也有constexpr构造函数
-
类必须使用析构函数的默认定义。
这个感觉意义不大。不过有些疑惑的是什么叫 “字面值类型”?
我想应该是初始化时,使用的是字面值,而不是变量。这样在编译阶段,变量的值就定了。这么说,其实这个字面值常量不过就是一堆常量(字面值)的聚合。
-
-
类的静态成员[重点]
我想这个是比较重要的、有用的一个点。
类的静态成员,是与类本身相关的变量。所有该类的对象都可以访问类的静态成员。
用
static
关键字来声明类的静态成员。 用类作用域或者对象来访问类的静态成员。关键是静态成员的定义(初始化)。
静态变量:
-
尝试类内初始化(类内初始值)
static double average_height = 1.68 ;
报错:错误:‘constexpr’ needed for in-class initialization of static data member ‘double People::average_height’ of non-integral type
就是说,如果要在类内初始化一个静态变量,那么这个静态变量必须是const的,用const或者constexpr修饰。
改为:
static constexpr double average_height = 1.68 ;
这样,要想类内初始化,那么这个值必须是常量的。或者该反过来说,只有静态常量表达式才可以在类内初始化。
-
尝试类内初始化一个非字面类型
已经按照第一条,使用constexpr , 但是初始化一个string出错:
static constexpr string attribute = "lovely" ;
报错一大串:
错误:广义常变量‘People::attribute’的类型‘const string {aka const std::basic_string<char>}’不是字面常量 ‘std::basic_string<char>’ is not literal because: (不是字面的) ‘std::basic_string<char>’ has a non-trivial destructor(有非平凡析构函数) 错误:in-class initialization of static data member ‘const string People::attribute’ of non-literal type (需要一个类外的初始化) ‘People::attribute’不能由一个声明时非常量的表达式初始化
观察上面的错误输出,猜测编译器对静态变量做类内初始化,应该是在编译阶段就做的!所以一个确定的内存开销、确定的字面值是必须的。
由上,要初始化一个静态变量,最好是在类外初始化。类内初始受到很大的限制——只能是字面值类型,且必须常量(有确定内存开销,有确定值,这样才能在编译阶段就分配空间)。
那么又如何类外初始化呢?
-
直接赋值(定义、初始化)
-
在全局状态下(非main函数中)
在private控制下静态变量,
直接赋值(这个说法不准确,这里指用“=”的方式初始化;以后一定谨慎使用“赋值”二字)使用“=”好做初始化,成功!!
string People::attribute = "666" ; // 成功编译
当尝试再次用“=”号访问时,报错:
错误:‘std::string People::attribute’ 重定义
string People::attribute = "666" ; string People::attribute = "777" ;
-
在main函数中
用“=”初始化失败,报错:
错误:对限定名‘People::attribute’的使用无效
故应该在全局环境下,在类外使用“=”做定义(初始化),且定义只能一次。
-
-
使用类接口(静态成员函数)
在不做1的条件下,直接定义一个函数来尝试设置该变量的值:
void People::set_attribute(string attr) { attribute = attr ; } ;
结果报错:
/tmp/ccGt9VJ7.o:在函数‘People::set_attribute(std::string)’中: test.cpp:(.text+0x16c):对‘People::attribute’未定义的引用 collect2: 错误:ld 返回 1
可知是链接时出错!
也就是说,在类中声明一个静态变量,如果不在类外部初始化,那么其实际上根本没有被分配内存空间!链接时根本找不到这个变量!而由之前的部分知,如果在类内部初始化,那么该值又必须是常量!外部不能更改。
在类外定义静态变量似乎是一件很神奇的事——因为看起来在类的外部访问了类的私有变量。但是我们可以这样认为,成员的定义是由类的作者完成的,是在类的使用者使用之前必须完成的。由于其只能被定义一次,所以类的使用者就再也不能去访问它了。
-