C++//类

本文详细介绍了面向对象编程中的抽象和封装概念,以及类的构造函数。抽象关注模型接口的定义,封装则规定成员数据的访问控制。构造函数在对象创建时执行,用于初始化成员。类的静态成员不与任何对象绑定,由所有对象共享。此外,文章还探讨了默认构造函数、拷贝构造函数和析构函数的作用,以及如何通过访问控制和友元函数来增强封装性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 设计抽象类

类的基本思想在于抽象和封装。
两个概念:接口定义用户所能执行的操作实现则包括类的数据成员、接口的函数体、定义类的私有函数
在这里插入图片描述

在这里插入图片描述

抽象:提供模型接口,隐藏实现细节。
封装:规定成员数据的访问控制,控制内部细节能被外界访问的程度,外部能访问的越少,封装性越好。封装良好的代码:1.类的作者可以自由修改类成员,而无需改变用户代码。(当然也不一定,如果涉及接口变化还是得改) 2.可以防止用户代码无意破坏对象的状态。

在这里插入图片描述

总的来说,抽象是封装实现的前提,比如对一台电脑,把他抽象成一个类,包含一些硬件名称组成的成员变量,以及一些对电脑操作比如开关机组成的成员函数。这个过程就是抽象,把实体变成一些访问接口和特征。封装就是控制用户对里面的成员变量/函数的了解程度,如果使得所有的硬件都对用户隐藏(即机箱焊死,用户不可访问),封装性很好。如果把机箱CPU那块开个小洞,用户可以看到CPU的品牌等信息,这封装性就相对没这么好了。

类的执行加法和IO的函数不作为成员函数,定义为普通函数。执行复合赋值运算的函数是成员函数。

成员函数的声明必须在类的内部,定义则可以在内部也可以在外部,如果定义在内部,则默认是inline函数。非成员函数的声明和定义都在类的外部
在这里插入图片描述

1.1 this

this的引入:
在这里插入图片描述
因为前面已经定义了隐式的this形参,可以在函数体内用this->,尽管没必要。在成员函数内不能再定义任何名为this的变量或者形参

1.2 引入const成员函数

在这里插入图片描述
上面的const的作用是将this指针设为const的。
默认情况下,this是一个类型为:type * const this ,也就是说他是一个指向对应类类型的顶层const指针。
它是顶层const,意味着无法把this绑定到常量对象上。
可以把this设置为底层const,这样可以将this绑定到常量对象上。但是由于this是隐式的,不会出现在参数列表中,C++就用上述例子的方式,在使用对象初始化this时,将this声明为指向常量的指针。上述类型的成员函数被称为常量成员函数
在这里插入图片描述

1.3 类作用域和成员函数
1.3.1 类外部定义成员函数

看如上例子:
在这里插入图片描述
isbn定义在bookNo之前,为什么能使用bookNo?

编译器在处理类时,先编译成员的声明,再轮到成员函数体。所以,函数体使用类中成员是不需要按照顺序的。

在类外部定义的成员的名字必须包含它所属的类名。以类名::成员函数名的形式。当编译器看到这个名字,就能理解后续的函数代码是位于类的作用域内的。

1.3.2 定义返回this对象的函数

在这里插入图片描述

在这里插入图片描述

1.4 定义类相关的非成员函数

在这里插入图片描述
在这里插入图片描述

1.5 构造函数

构造函数没有返回值名字和类相同,可以有多个构造函数,不同构造函数必须在参数类型和数量上有所区别。构造函数可能有一个参数列表和函数体,二者都可能为空。

构造函数不能声明为const的,如果类中有const对象,只有当构造函数完成时才能获得它的const属性,在这之前,可以对该对象任意写值。

如果没有显式的定义构造函数,编译器就会隐式构造一个默认构造函数。又称合成的默认构造函数
合成的默认构造函数干两件事:

  1. 如果存在类内初始值,用该初始值初始化成员
  2. 否则,默认初始化该成员

有些类不能依赖合成的默认构造函数,原因如下:

编译器只有在类没有定义任何构造函数的情况下才会构造合成的默认构造函数,而一旦类中定义了其他的构造函数,该类将没有默认构造函数
合成的默认构造函数可能导致错误。在合成的默认构造函数的第二点显示,在某些情况下会默认初始化该成员,事实上,很多类型如内置对象和复合类型(指针,数组)在类内的默认初始化会得到未定义的值
如果该类的成员包含了其他类类型且没有默认构造函数的成员,那么将无法初始化这样的成员
诸如上述,最好是自定义一个默认构造函数。也可以要求编译器生成默认构造函数(需要不是上述三种情况,否则不能直接这样,需要用列表初始化每个成员):
在这里插入图片描述
在这里插入图片描述

列表初始化的构造函数,冒号和花括号用作分隔符,表示中间的部分是初始化列表的部分:
在这里插入图片描述
如果有个参数被构造函数的初始值列表忽略,他会以合成默认构造函数相同的方式隐式初始化

类外定义构造函数:

在这里插入图片描述
在这里插入图片描述

1.6 拷贝、赋值和析构

拷贝:初始化变量,以值的方式传递或者返回对象。
赋值:对对象用赋值运算符时。
析构:对象不存在时执行销毁操作。
如果没有显式定义这些操作,编译器会自动合成
同样的,有些类不能依赖这些操作的合成的版本:当类需要分配类对象之外的资源时,合成版本通常会失效,比如某些管理动态内存的类。所以在需要管理动态内存时,使用vectorstring对象管理所需的空间能避免分配和释放内存带来的复杂性。

2. 访问控制和封装

访问控制符public和private:
在这里插入图片描述
在这里插入图片描述

classstruct的区别:默认访问权限不一样。其他的仅是形式上的不同。
类如果在它的第一个访问控制符前定义成员,对这个成员的访问控制依赖于类定义的方式。struct对应的是publicclass对应的是private的。

2.1 友元

如果被定义为非成员函数(加法/IO操作)的接口需要访问类中的private对象怎么办?
类允许其他类或者函数访问它的private成员,需要将他们声明为friend(友元)。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3. 类的其他特性

3.1 定义类型成员

在这里插入图片描述

3.2 定义内联函数

在这里插入图片描述
最好只在类定义处声明inline,虽然可以声明定义都声明inline,但是这样更易理解。

成员函数也可以重载,书写规则和匹配规则和普通函数一样。

3.3 可变数据成员

在变量声明中加入mutable来将变量声明为可变数据成员:
在这里插入图片描述
注意不仅仅是在成员函数中可变,只要能访问到这个成员变量的地方,都可以修改它,比如友元函数等等。

3.4 为类数据成员提供初始值

类内初始值要么使用=(拷贝初始化),要么使用{}(列表初始化)

在这里插入图片描述
在这里插入图片描述
注意这里是对类内数据成员

3.5 返回*this的成员函数

在这里插入图片描述
在这里插入图片描述
当返回者是一个const成员函数(this被声明为指向const的指针),返回*this也是const的。不能修改。
在这里插入图片描述
当一个成员调用另一个成员时,this指针在其中隐式传递。
在这里插入图片描述
一个小细节,do_display在函数内部定义,被隐式声明为内联函数,不会带来任何额外的允许开销。

3.6 类类型

在这里插入图片描述
只声明不定义称为前向声明。在声明后,定义前,该类型为不完全类型。
在这里插入图片描述
不完全类型可以定义指针和引用,可以作为函数参数或者返回值

3.7 友元再探

声明友元就是用声明这个类/函数的格式,前面加上friend。即使进行了友元声明,还是得完成类/函数自己的声明和定义
在这里插入图片描述
声明了类友元后,一个类的对象可以访问另一个类的对象的成员。
如A,B是两个对象,且A的类是B的友元类,B有一个private的成员b,A可以通过B.b访问到该成员。
在这里插入图片描述
友元声明的顺序(把window_mgr的成员函数clear声明为Screen的友元函数):
在这里插入图片描述
如果想把重载函数声明为友元,需要把该重载函数组的每一个分别声明。否则的话,只有声明过的函数能访问成员。

友元函数需要在外部声明后才可见,才能被使用。即使在声明友元的类里面定义了该友元函数,也需要在外部声明,否则不能使用。
在这里插入图片描述

4. 类作用域

在类作用域之外,普通数据和函数成员可通过 对象名+成员访问运算符(.->)访问类内定义的类型成员需要使用 类名+作用域运算符(::)访问
在类外部定义成员函数必须同时提供类名和函数名,一旦遇到了类名,定义的剩余部分就处于类作用域内,可直接使用类成员而无需授权
在这里插入图片描述
如下:
在这里插入图片描述

4.1 名字查找与类的作用域

名字查找:寻找与所用名字最匹配的声明。
一般方式:

  1. 在名字所在块内且名字使用前寻找声明。
  2. 没找到,就查找外层作用域(同样在名字定义之前)。
  3. 最终没有找到,程序报错。

类定义分成两步:

  1. 编译/查找成员的声明(这就是为什么类类型成员)。
  2. 直到类全部可见才编译函数体。

在这里插入图片描述
在这里插入图片描述
如上所示的类,编译器首在account的作用域寻找money的定义,没找到,就去外层进一步寻找。直到找到typedef语句。然后,将该类型用作balbalance返回值的类型。最后才是处理函数体。所以balance函数返回的结构是double类型而不是string

与普通成员名可以重新定义不同,类的类型成员所使用的名字不能被重新定义。
在这里插入图片描述
在这里插入图片描述
在成员函数中的参数如果与类中成员同名,会将类成员隐藏,如果试图访问类成员,可用this->以及::强制访问。

在这里插入图片描述

5. 构造函数再探

在这里插入图片描述
当构造函数的函数体开始执行时,初始化就完成了。这就是为什么之前的构造函数在冒号和构造体之间使用列表初始化。在一般情况下,赋值和初始化区别不是很大,但考虑如下情况:
在这里插入图片描述
当成员有常量或者引用时,就会引发错误。因为试图初始化一个没有绑定对象的引用。所以含有const或者引用成员的类在初始化时对这些成员必须使用列表初始化
在这里插入图片描述

5.1 初始化成员的顺序

在这里插入图片描述
所以,最好不要使用同一对象的其他成员初始化某个成员
在构造函数中,可将实参设置为默认实参(见函数章节),使得即使不提供实参也能调用该构造函数,那么该构造函数实际上定义了默认构造函数。

5.2 委托构造函数

委托构造函数实际是某个构造函数将初始化的任务交给了同类的另一个构造函数。代码上的特征为原本应出现初始化列表的地方变成了对另一构造函数的调用。在实现过程中,会先调用受委托构造函数的初始值列表和函数体,然后再将控制权交还给委托者的函数体
在这里插入图片描述

5.3 默认构造函数的作用

什么时候会调用默认构造函数:对象执行默认初始化和值初始化时。
只要使用了括号(圆括号或花括号)但没有给出具体初始值,就是值初始化。可以简单理解为括号告诉编译器你希望该对象初始化。
没有使用括号,就是默认初始化。可以简单理解成,你放任不管,允许编译器使用默认行为。通常这是糟糕的行为,除非你真的懂自己在干什么。

默认初始化值初始化
块作用域内未初始化静态变量数组初始化过程提供的初始值数量少于数组的大小
类对象未在构造函数初始值列表显式初始化,成员会默认初始化不使用初始值定义一个局部静态变量
类本身含有一些其他类的对象,并且本类使用合成默认构造函数当我们通过书写形如T()的表达式显示地请求值初始化时
补充:

直接初始化和拷贝初始化主要是相对于我们自定义的对象的初始化而言的,对于内置类型,这两者没有区别。对于自定义对象,直接初始化和拷贝初始化区别是直接调用构造函数还是用"="来进行初始化。
列表初始化一般格式为花括号括起来的一组初始值,实际使用的是类的构造函数。列表初始化有时候等价于直接初始化(非聚合类型),有时候等价于直接赋值成员(聚合类型)。

使用默认构造函数的方法是不加括号:
在这里插入图片描述

5.4 隐式类型转化

在这里插入图片描述
例:
在这里插入图片描述
但是这种隐式转换生成的对象是临时的,它被传递给combine函数。

隐式类型转换只支持一步类型转换(内置类型转换不受限制),如上所说的:通过构造函数的参数类型转换成类类型。如果需要额外隐式转换,则是错误的:
在这里插入图片描述
对如上的例子,可以把两种隐式转换的某一种或者两种定义为显式转换,即可:
在这里插入图片描述

5.4.1 抑制隐式类型转换

在这里插入图片描述
注意在上文一直用的是”一个实参“,而不是”一个形参“,可能有多个形参,但是其他的都是默认形参,只需一个实参,这种情况也是可以用于隐式转换的。

声明为explicit的构造函数不再能被用于拷贝初始化,只能使用直接初始化的方法:
在这里插入图片描述
尽管声明为explicit的构造函数不能用于隐式转换,但是显式转换还是可以用该构造函数的:
在这里插入图片描述
标准库的explicit相关:
const char* -> string 不是explicit的,所以可以用于隐式转换。
int -> vector (用直接初始化方式构造vector) 是explicit的。

5.5 聚合类

在这里插入图片描述
可使用列表初始化的方法初始化聚合类对象:
在这里插入图片描述
类似于用列表初始化数组,成员按声明的顺序与列表的值进行匹配,如果列表提供的值少于成员数,剩下的成员将会进行值初始化,列表提供的值数目不能多于成员数目
在这里插入图片描述

5.6 字面值常量类

字面值常量存在的意义就是利用编译时期编译器的计算能力优化程序性能。
字面值常量类:
在这里插入图片描述
满足条件1,就可以在编译阶段求值,这一点和聚合类一样。
满足条件2,就可以创建这个类的constexpr类型的对象。
满足条件3,就可以保证即使有类内初始化,也可以在编译阶段解决。
满足条件4,就可以保证析构函数没有不能预期的操作。

constexpr构造函数:
在这里插入图片描述
constexpr构造函数的形式:

  1. 用默认构造函数形式。即=default
  2. 构造函数没有返回值,而constexpr函数只能有一条可执行语句就是返回语句。为满足两点,唯一的办法就是函数体为空
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

6. 类的静态成员

为了使得某个对象和类相关,而不是和类的其他成员相关。
静态成员存在于任何对象之外,被所有对象所共享。静态成员函数不与任何对象绑定(无法声明为const),没有this指针(不能显式使用this,也不能调用其他成员时隐式传递this)。

6.1 使用静态成员

成员函数可直接使用静态成员,其他情况可以使用作用域运算符(类名::静态成员)访问静态成员,或者藉由对象访问静态成员(.,->)。
在这里插入图片描述
在这里插入图片描述

6.2 定义静态成员

使用static关键字定义静态成员,只需在类内声明时加上该关键字即可。
必须在类外部定义和初始化每个静态成员。有一种情况例外,当静态成员是一个constexpr类型时,可以在类内给它提供常量表达式作为初始值。
如果该静态成员在类内部被初始化了,在外部最好定义下该成员。因为如前面所说,只有当静态成员是constexpr时才能在类内部对他初始化,而constexpr是在编译期间计算结果并将结果替换的,如果对该成员的使用过程中遇到了编译器无法替换值的情况,就会报错
在这里插入图片描述
如上情况,就无需在类外定义,因为编译器可以直接替换period在编译时计算的值。
但是当要把period传给某个类外函数作为参数时,就会找不到period的定义,因此需要在类外定义一下,要注意,如果在类内提供了初始值,类外定义的时候就不能提供初始值了

每个静态数据成员只能定义一次
声明:
在这里插入图片描述
定义:
在这里插入图片描述

6.3 静态成员的优势

静态成员可以作为不完全类型(只声明未定义的类型),非静态成员则只有声明成只想他的指针或者引用才能作为不完全类型。
在这里插入图片描述
静态成员可以作为默认实参:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值