【C++基础】第11章:类

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

  1. 数据成员可以使用 decltype 来声明其类型:
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    decltype(3)中的3是一个表达式,当decltype()里面包含一个表达式时,会返回表达式所对应的类型,而3的类型是int,故相当于int x。
  2. 数据成员不能使用 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 定义方式的衍化

  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中也一样。

  1. 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)
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  1. 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 “.” 与“ ->” 操作符(一般数据成员的访问方式)

  1. “.”:
    可以按下图7行这样访问静态数据成员:
    在这里插入图片描述
  2. “ ->”
    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 可以在结构体中定义函数,作为其成员的一部分:对内操作数据成员,对外提供调用接口

  1. c语言:
    在这里插入图片描述
    打印出3。
  2. 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指针有什么用?

  1. 如下图7行的x指的是5行fun函数的形参x(fun函数内部也会有域)
    在这里插入图片描述
    如果确实要访问类的数据成员x(上图9行),我们可以可以使用Str::x
    在这里插入图片描述
    我们也可以使用this->
    在这里插入图片描述
    即,我们可以通过this->来显式访问类的外部数据成员。
  2. 下图,我们想用类中的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 限定符

  1. 如果只需要拷贝行为,那么引入拷贝构造即可
  2. 如果不需要拷贝行为,那么将拷贝构造声明为 delete 函数即可
  3. 注意 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 一定要有意义)

5.2.4 成员指针的使用

5.2.4.1 对象 .* 成员指针
5.2.4.2 对象指针 ->* 成员指针

5.3 bind 交互

5.3.1 使用 bind + 成员指针构造可调用对象

5.3.2 注意这种方法也可以基于数据成员指针构造可调用对象

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

cashapxxx

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值