类
- 1 结构体与对象聚合
- 1.2 数据成员(数据域)的声明与初始化
- 2 成员函数(方法)
- 3 访问限定符与友元
- 4 构造、析构与复制成员函数
- 5 字面值类,成员指针与 bind 交互
1 结构体与对象聚合
1.1 结构体
结构体:对基本数据结构进行扩展,将多个对象放置在一起视为一个整体,如:
关于结构体,我们需要注意以下几点:
1.1.1 结构体的定义(注意定义后面要跟分号来表示结束)
结构体的定义:结构体占用多少内存?结构体里面的内容如何排布?
struct Str; // 声明
struct Str{ // 定义
int x;
int y;
};
也可以如下定义结构体(3~7行):(Str是结构体,别名为MStr,即使用Str和使用MStr是等价的)。这是c语言的风格。
1.1.2 仅有声明的结构体是不完全类型( incomplete type )
仅有声明的结构体是不完整的类型(incomplete type),不完整类型可以用来定义对应的指针,但不能用来定义对应的对象(3和14行)。
但可以这样:
为什么上图14行多了*就能编译?因为编译器知道Str1是一个结构体,那么就可以声明结构体类型的指针,而指针的大小和指针所指向的对象的大小没有绝对关系(指针的大小在64位机里就是8位)。即有结构体声明,那么就能声明结构体类型的指针。
1.1.3 结构体(以及类)的一处定义原则:翻译单元级别
如下,违反一处定义原则:
如果再写一个.cpp文件,里面同样定义上图代码结构体:
此时main.cpp改为:
以上代码是OK的。结构体一处定义原则只要求一个翻译翻译里面不能重复定义结构体(main.cpp是一个翻译单元;source.cpp也是一个翻译单元)。
1.2 数据成员(数据域)的声明与初始化
接下来我们需要了解关于数据成员(数据域)的声明与初始化。
如下,5、6行的x和y是Str的数据成员(数据域)
全局变量x的定义:
全局变量x的声明:
结构体内:
5、6行是结构体数据成员的声明,整块3~7行是结构体的定义。
结构体数据成员的定义是隐式的:下图11行是结构体的对象(m_str)的定义,同时11行也隐式的定义了结构体Str内部的数据成员。
1.2.1 ( C++11 )数据成员可以使用 decltype 来声明其类型,但不能使用 auto
- 数据成员可以使用 decltype 来声明其类型:
decltype(3)中的3是一个表达式,当decltype()里面包含一个表达式时,会返回表达式所对应的类型,而3的类型是int,故相当于int x。 - 数据成员不能使用 auto 来声明其类型
下图这样可以:
这样不可以:
1.2.2 数据成员声明时可以引入 const 、引用等限定
1.2.3 数据成员会在构造类对象时定义
1.2有解释。
1.2.4 ( C++11 )类内成员初始化
struct Str
{
int x = 3; // 注意这里仍然是声明而不是定义,并不会在此时为x分配内存并进行关联
int y;
};
由于上图5行的x使用了类内成员初始化,故12行可打印出3。
1.2.5 聚合初始化:从初始化列表到指派初始化器
有点类似数组的初始化,下图代码11行即把x初始化为3;y初始化为4。
但聚合初始化的顺序与结构体内数据的定义顺序有关,这无疑会在一定程度上导致BUG的发生:
上图打印出3
但如果将上图5行和6行调换位置:
所以在C++20之后引入了指派初始化器:
建议使用指派初始化来初始化结构体的数据成员。
1.3 mutable 限定符
mutable关键字。
下图12行报错,因为m_str是const常量,不能修改。
但是如果5行加上mutable,则代码可以通过编译。(mutable标识x可以被修改,可以绕过const限制,在保证常量特性的情况下修改结构体中某些数据成员的值)
1.4 静态数据成员——多个对象之间共享的数据成员
在结构体中,有一类特殊的数据成员——静态数据成员,它是多个对象之间共享的数据成员。
如下图,
对m_str1的x进行修改,不会影响到m_str2的x。
但如果把上述的x声明成静态数据成员(5行);
9行:x的定义
这是因为静态数据成员x在不同的对象之间(m_str1、m_str2)共享,即上图代码,m_str1和m_str2共享相同的x。
关于静态数据成员,需要关注以下几点:
1.4.1 定义方式的衍化
- C++98 :类外定义,const 静态成员的类内初始化
5行:静态数据成员的声明
9行:静态数据成员的定义(不能写成int x;
,x是位于Str这个域中)
C++98要求类外定义:
如下:我们在header.h定义静态数据成员(3行),静态数据成员会被所有对象所共享。
header.h:
main.cpp:
m_str1:Str类型的对象;能够访问静态数据成员。
source.cpp:
共享的意思是在source.cpp中将100写入x中,和main.cpp中读取x,这两个x是指同一个x,处于同一块内存。
那么怎么实现读写同一块内存?
对x进行声明并不能确定x所在的内存,定义x才可以确定x所在的内存位置。如下:(在str.cpp中定义x)
以上代码、翻译单元编译器处理流程:编译器在处理main函数时,看到header的声明(2行),看到header.h里面有静态数据成员x,那么main函数里面写m_str1.x
时,不会直接完全生成代码,因为这个静态数据成员在其他地方引入定义(str.cpp),相应地,会把main函数7行的m_str1.x
的读取x操作看成需要读取另一个翻译单元里面的x的定义;然后source.cpp中也一样。
- const静态数据成员可以在类内初始化
如下图,我们希望定义一个数组的尺寸(array_Size),根据这个尺寸分配空间。那么这个数组的尺寸一定得是编译器常量,而c++98中没有引入constexpr关键字(constexpr表达式是指值不会改变并且在编译过程就能得到计算结果的表达式。声明为constexpr的变量一定是一个const变量,而且必须用常量表达式初始化),故需要引入const。而且需要给出array_Size的值。此时在mian函数和fun函数中调用Str。
如下图,系统看到3行的const static int array_Size = 100;
时,在使用array_Size这个数值时,会把100代入4行的array_Size。因为3行的const static int array_Size = 100;
是编译期常量,故4行的array_Size可以替换为100。故下图3行即为类(Str)内引入定义(初始化array_Size)
- C++17 :内联静态成员的初始化
把上述的str.cpp文件删掉,header.h文件改为如下即可:
或不用const:(定义一个可修改的数据成员array_Size)
1.4.2 可以使用 auto 推导类型
将上述代码中,header.h中3行int换为auto,也ok。auto根据array_Size的初始值,能自动推导出array_Size的类型:
一般数据成员不能使用auto来声明类型,但静态数据类型可以使用auto来声明类型。
1.5 静态数据成员的访问
1.5.1 “.” 与“ ->” 操作符(一般数据成员的访问方式)
- “.”:
可以按下图7行这样访问静态数据成员:
- “ ->”
6、8、11行这样访问:
1.5.2 “::” 操作符(静态数据成员特有的访问方式)
对于静态数据成员,所有对象都共享同一个静态数据成员。故一些情况下,上述代码我们可以将m_str1
(10行)改为std::
(::
:针对结构体本身)
1.6 在类的内部声明与该类相同类型的静态数据成员
以下代码错误:在解析4行的Str时,系统并没有看到整个Str的定义,此时会报错Str是一个不完全的类型。
如果上图代码4行Str改为int:系统解析到9行使用Str时,会分配内存(根据Str内部包含的数据成员(int型在64位机占4个字节)加上一些xx来计算出Str所包含的内存)。
但是像上上图,系统在解析Str结构体时(2~5行),需要知道Str结构体占用多少内存,而想知道Str结构体占用多少内存,那么就需要知道结构体内的数据成员Str占用多少内存。。。死循环了,故编译器直接报错。
故一般的数据成员,如果我们定义Str这样的结构体时,不能在结构体内包含Str这样的数据成员。
但是可以在类(结构体)的内部声明与该类相同类型的静态数据成员:
因为上图4行的x是静态数据成员,它被所有的Str对象所共享,则也意味它不属于某个对象。即编译器需要开辟内存,会单独开辟一块新内存。如下图,9行和10行的m_str1和m_str2分别属于不同的内存,故编译器执行到2行计算Str所占的内存大小时,不会考虑static Str x;
这个静态数据成员x:
但上图4行只是x的声明,还没有对x进行定义。我们需要引入额外的定义,如下图7行:Str Str::x;
。首先,Str::x是一个对象,对象类型是Str,对象所处的域也是Str。
但如果对上图4行使用c++17的内联静态成员的初始化:则会报错。
改为以下这样又可以:
因为编译器是从上到下依次解析,当解析到上上图4行,会认为4行会产生一个x的定义,既然会产生x的定义,那么会对x的类型有个全面了解,但是解析到第4行时,编译器无法获取x的类型(因为Str都还没有解析完),即编译器认为Str还是一个不完全类型。
我们可以这样定义:(下图7行是x的定义,因为指出了x所在的内存处于Str这个域中)
规范点:把inline Str Str::x;
写入header.h中,然后在main.cpp和source.cpp中声明header.h,即可调用inline Str Str::x;
:
2 成员函数(方法)
2.1 可以在结构体中定义函数,作为其成员的一部分:对内操作数据成员,对外提供调用接口
- c语言:
打印出3。 - c++
struct Str{
int x = 3;
void fun(){
std::cout << x << std::endl;
}
};
还是打印出3。
方法可以在结构体中定义函数,作为其成员的一部分:对内操作数据成员,对外提供调用接口。
如下图:22行相当于Str提供了对外的接口——fun(),我们可以通过调用.fun
来实现一系列功能(调用.fun
,在Str内部会访问Str的数据成员来实现相应功能)。
通过这样的方法,即把简单的结构体变成了抽象数据类型。
2.1.1 在结构体中将数据与相关的成员函数组合在一起将形成类,是 C++ 在 C 基础上引入的概念
2.1.2 关键字 class
2.1.3 类可视为一种抽象数据类型,通过相应的接口(成员函数)进行交互
2.1.4 类本身形成域,称为类域
类与结构体的最大区别在默认的成员访问权限上,这个在后续进行讨论。
2.2 成员函数的声明与定义
2.2.1 类内定义(隐式内联)
类内定义是隐式内联的。如下,加一个头文件:
在编译main.cpp翻译单元时,header.h的内容会被加载进去,而header.h里面包含fun函数的定义;接下来编译source.cpp时也要把header.h加载进去,此时法人header.h里面还是包含了fun函数的定义。这样的话,如果fun函数不设计成内联函数,那么就会出现重复定义。
为了防止重复定义,c++规定,如果我们在类的内部定义了fun函数,那么这个fun函数就是隐式内联的。
2.2.2 类内声明 + 类外定义
此时,fun函数就不是内联的了。
str.cpp包含fun函数的定义;main.cpp中的main函数会使用fun函数;source.cpp里面的fun2函数也会使用这个fun函数。
即如果在类内(如上图Str)定义函数(如fun函数),那么这个函数是内联的;如果在类外定义函数,那么这个函数就不是内联的。
类内定义函数和类外定义函数各有好处,类内定义函数可以使用函数隐式内联的一些特性;类外定义函数可以使得类尽量简洁一些。
但是有时我们希望把类外定义的函数写到header.h里面,但又不希望将它放到类的内部。此时需要将这个fun函数变成内联函数:(如下图9行)
2.2.3 类与编译期的两遍处理
把str.cpp中的fun函数定义放到header.h中:
上图这个代码ok。
然后我们把x放到fun函数的下面:
还是ok。
通常,编译器在处理一个翻译单元,或一段代码时,是从上到下依次处理。而上图代码,在处理fun函数时,7行会遇到x,但是此时编译器还不知道x的含义(只有处理到9行时才知道x的含义),理论上会报错,但是在一个类中这么写并不会报错。这是c++的类与编译期的两遍处理。
类与编译期的两遍处理:
下图,5行看到fun函数,先不处理fun函数内部,相当于看到fun函数的声明,把它的信息记录下来,然后跳过函数的定义,看后面的内容,从上到下处理完之后,相当于对Str里面包含哪些函数、数据成员有了整体了解;接下来会第二遍处理fun函数内部逻辑(6~8行)。
第二遍只会对类内的函数(方法)内部逻辑进行处理。
如下图代码ok:
但下图不Ok:
执行到5行时,不知道MyRes是啥,在第二遍处理时又只会处理函数内部逻辑,并不会处理MyRes,故报错。
2.2.4 成员函数与尾随返回类型( trail returning type )
把上述代码的fun函数的定义放到str.cpp里面:
这样会报错。因为MyRes是在header.h中的Str这个类里面定义的,MyRes属于Str这个域;如果str.cpp中简单地写MyRes,编译器无法识别。故str.cpp应该改为:
但写了两个Str,麻烦。我们可以使用trail returning type:(4行:fun函数接收一个0个参数,返回类型是MyRes,auto指具体的类型得看后面的->
确定)
为什么这样就能把MyRes前面的Str省掉?
4行从左到右编译,看到Str::fun(),系统认为接下来整个函数的定义是位于Str这个类所形成的域中的一个函数(如fun函数),后面在写-> MyRes时,编译器会从Str域中查找MyRes。
class Str{
public:
using MyRes = int;
MyRes fun();
int x = 3;
};
//MyRes Str::fun(){ // 不合法
//return x;
//}
//Str::MyRes Str::fun(){ // 合法单写起来比较冗余
// return x;
//}
auto Str::fun() -> MyRes // 清晰明了的写法(主要逻辑是从左到右解析到Str这个域名称后会扩大对MyRes的名称的搜索域)
{
return x;
}
2.3 成员函数与 this 指针
在调用header.h中的fun函数时,编译器怎么知道使用的是那个x?(上上图代码有两个对象x)
c++的会引入this指针,this指针是一个指向常量的指针,它是一个隐式传递的参数,用于指向当前的对象。
2.3.1 使用 this 指针引用当前对象
7、12行分别打印m_str和m_str2的地址:
在fun函数中,我们打印this关键字:
输出的第一行对应上上图的7行;输出的第二行对应上图的7行;输出的第3行对应上上图的第12行;输出的第4行对应上图7行。
编译器在处理fun函数时,会给fun函数一个隐藏的参数:Str* this
在main函数中调用m_str.fun时,编译器会调用fun函数,然后把&m_str
传进去:
将&m_str
传入类中的fun函数后,我们就可以通过this访问类(Str)中所有的元素,因此,如下图7行8行,写x和this->x的行为是一样的:
那么this指针有什么用?
- 如下图7行的x指的是5行fun函数的形参x(fun函数内部也会有域)
如果确实要访问类的数据成员x(上图9行),我们可以可以使用Str::x
我们也可以使用this->
即,我们可以通过this->来显式访问类的外部数据成员。 - 下图,我们想用类中的x(11行)的值修改fun函数的形参x(5行)
我们不能修改成下图7行这样:
因为7行的x还是处在fun函数这个域中。我们应该写为:
2.3.2 基于 const 的成员函数重载
上图this的类型是什么?是Str*,或Str * const。如何理解?首先this是一个指针,这个指针指向一个Str类型的对象,const表示this通常而言不能修改(const表明指针本身不能修改)。但是this指向的内容可以修改。
但是如果不想对fun函数内部的数据成员修改(如x),那么可以像下图5行这样加上const。此时再7行将x修改为100时系统会报错。
上图,如果5行不写const,那么this的类型是Str* const(this本身不能修改,到那时this指向的内容可以修改);如果5行写上const,那么this的类型是const Str * const(指针本身不能修改,指针指向的内容也不能修改)。
下图这样也会形成重载关系:OK的
2.4 成员函数的名称查找与隐藏关系
2.4.1 函数内部(包括形参名称)隐藏函数外部
2.4.2 类内部名称隐藏类外部
2.4.3 使用 this 或域操作符引入依赖型名称查找
2.5 静态成员函数
在静态成员函数中返回静态数据成员
2.6 成员函数基于引用限定符的重载( C++11 )
3 访问限定符与友元
3.1 使用 public/private/protected 限定类成员的访问权限
3.1.1 访问权限的引入使得可以对抽象数据类型进行封装
3.1.2 类与结构体缺省访问权限的区别
3.2 使用友元打破访问权限限制——关键字 friend
3.2.1 声明某个类或某个函数是当前类的友元——慎用!
3.2.2 在类内首次声明友元类或友元函数
注意使用限定名称引入友元并非友元类(友元函数)的声明
3.2.3 友元函数的类内外定义与类内定义
3.2.4 隐藏友元( hidden friend): 常规名称查找无法找到()
3.2.4.1 好处:减轻编译器负担,防止误用
3.2.4.2 改变隐藏友元的缺省行为: 在类外声明或定义函数
4 构造、析构与复制成员函数
4.1 构造函数:构造对象时调用的函数
4.1.1 名称与类名相同, 无返回值, 可以包含多个版本(重载)
4.1.2 (C++11 )代理构造函数
4.2 初始化列表:区分数据成员的初始化与赋值
4.2.1 通常情况下可以提升系统性能
4.2.2 一些情况下必须使用初始化列表(如类中包含引用成员)
4.2.3 注意元素的初始化顺序与其声明顺序相关,与初始化列表中的顺序无关
4.2.4 使用初始化列表覆盖类内成员初始化的行为
4.3 缺省构造函数: 不需要提供实际参数就可以调用的构造函数
4.3.1 如果类中没有提供任何构造函数, 那么在条件允许的情况下,编译器会合成一个缺省构造函数
4.3.2 合成的缺省构造函数会使用缺省初始化来初始化其数据成员
4.3.3 调用缺省构造函数时避免 most vexing parse
4.3.4 使用 default 关键字定义缺省构造函数
4.4 单一参数构造函数
4.4.1 可以视为一种类型转换函数
4.4.2 可以使用 explicit 关键字避免求值过程中的隐式转换
4.5 拷贝构造函数:接收一个当前类对象的构造函数
4.5.1 会在涉及到拷贝初始化的场景被调用, 比如: 参数传递。因此要注意拷贝构造函数的形参类型
4.5.2 如果未显式提供,那么编译器会自动合成一个,合成的版本会依次对每个数据成员调用拷贝构造
4.6 移动构造函数 (C++11) :接收一个当前类右值引用对象的构造函数
4.6.1 可以从输入对象中“偷窃”资源, 只要确保传入对象处于合法状态即可
4.6.2 当某些特殊成员函数(如拷贝构造) 未定义时,编译器可以合成一个
4.6.3 通常声明为不可抛出异常的函数
4.6.4 注意右值引用对象用做表达式时是左值!
4.7 拷贝赋值与移动赋值函数( operator =)
4.7.1 注意赋值函数不能使用初始化列表
4.7.2 通常来说返回当前类型的引用
4.7.3 注意处理给自身赋值的情况
4.7.4 在一些情况下编译器会自动合成
4.8 析构函数
4.8.1 函数名:“~”加当前类型,无参数, 无返回值
4.8.2 用于释放资源
4.8.3 注意内存回收是在调用完析构函数时才进行
4.8.4 除非显式声明, 否则编译器会自动合成一个, 其内部逻辑为平凡的
4.8.5 析构函数通常不能抛出异常
4.9 通常来说, 一个类:
4.9.1 如果需要定义析构函数,那么也需要定义拷贝构造与拷贝赋值函数
4.9.2 如果需要定义拷贝构造函数,那么也需要定义拷贝赋值函数
4.9.3 如果需要定义拷贝构造(赋值)函数, 那么也要考虑定义移动构造(赋值)函数
4.10 示例:包含指针的类
4.11 default 关键字
只对特殊成员函数有效
4.12 delete 关键字
4.12.1 对所有函数都有效
4.12.2 注意其与未声明的区别
4.12.3 注意不要为移动构造(移动赋值) 函数引入 delete 限定符
- 如果只需要拷贝行为,那么引入拷贝构造即可
- 如果不需要拷贝行为,那么将拷贝构造声明为 delete 函数即可
- 注意 delete 移动构造(移动赋值)对 C++17 的新影响
4.12.4 特殊成员的合成行为列表(红框表示支持但可能会废除的行为)
5 字面值类,成员指针与 bind 交互
5.1 字面值类: 可以构造编译期常量的类型
5.1.1 其数据成员需要是字面值类型
5.1.2 提供 constexpr / consteval 构造函数 (小心使用 consteval)
5.1.3 平凡的析构函数
5.1.4 提供 constexpr / consteval 成员函数 (小心使用 consteval)
5.1.5 注意:从 C++14 起 constexpr / consteval 成员函数非 const 成员函数
5.2 成员指针
5.2.1 数据成员指针类型示例: int A:😗
5.2.2 成员函数指针类型示例: int (A:😗)(double)
5.2.3 成员指针对象赋值: auto ptr = &A::x;
注意域操作符子表达式不能加小括号(否则 A::x 一定要有意义)