第七章 类(重点)

本文介绍了C++中类的概念,包括类的定义、抽象数据类型、访问控制与封装等内容。探讨了成员函数、构造函数、静态成员等特性,并详细解释了如何通过构造函数初始化成员变量。

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

7. 类(重点)

在C++语言中,我们使用类定义自己的数据类型。数据抽象能帮助我们将对象的具体实现与对象所能执行的操作分离开来。

数据抽象是一种依赖于接口和实现分离的编程(以及设计)技术。类的接口包括用户所能执行的操作;类的实现则包括类的数据成员、负责接口实现的函数体以及定义类所需的各种私有函数。

封装实现了类的接口和实现的分离。在抽象类型中,由类的设计者负责考虑类的实现过程;使用该类的程序员则只需要抽象地思考类型做了什么,而无须了解类型的工作细节。

7.1 定义抽象数据类型

  • 当我们设计类的接口时,应该考虑如何才能使得类易于使用;而当我们使用类时,不应该顾及类的实现机理。作为一个设计良好的类,既要有直观易于使用的接口,也必须具备高效的实现过程

  • 成员函数的声明必须在类的内部,它的定义则可以在类的内部也可以在类的外部。作为接口组成部分的非成员函数,它们的定义和声明都在类的外部。定义在类内部的函数是隐式的 inline 函数。成员函数通过一个名为 this 的额外的隐式参数来访问调用它的那个对象。

  • 默认情况下, this 的类型是指向类类型非常量版本常量指针。紧跟在参数列表后面的 const 表示 this 是一个指向常量的指针。常量对象,以及常量对象的引用或指针都只能调用常量成员函数。

  • 类的成员函数的定义嵌套在类的作用域之内。编译器分两步处理类:首先编译成员的声明,然后才轮到成员函数体。

  • 如果函数在概念上属于类但是不定义在类中,则它一般应与类声明(而非定义)在同一个头文件内。一般来说,如果非成员函数是类接口的组成部分,则这些函数的声明应该与类在同一个头文件内。IO类属于不能被拷贝的类型,因此我们只能通过引用来传递它们。默认情况下,拷贝类的对象其实拷贝的是对象的数据成员。

  • 每个类都分别定义了它的对象被初始化的方式,类通过一个或几个特殊的成员函数来控制其对象的初始化过程,这些函数叫做构造函数。构造函数的任务是初始化类对象的成员,无论何时只要类的对象被创建,就会执行构造函数。

  • 构造函数在 const 对象的构造过程中可以向其写值。类通过一个特殊的构造函数来控制默认初始化过程,这个函数叫做默认构造函数。默认构造函数无须任何实参。

  • 编译器创建的构造函数被称为合成的默认构造函数,某些类不能依赖于合成的默认构造函数。只有当类没有声明任何构造函数时,编译器才会自动地生成默认构造函数。

  • 构造函数初始值列表负责为新创建的对象的一个或几个数据成员赋初值。构造函数初始值是成员名字的一个列表,每个名字后面紧跟括号括起来(或者在花括号内的)成员初始值。构造函数不应该轻易覆盖掉类内的初始值,除非新赋的值与原值不同。构造函数的唯一目的就是为数据成员赋初值。

  • 一般来说,编译器生成的版本将对对象的每个成员执行拷贝、赋值和销毁操作。某些类不能依赖于合成的版本。使用 vector 或者 string 的类能避免分配和释放内存带来的复杂性。

  • 类内的成员变量:如果是在类中定义的类成员,则初始化顺序为:1.构造函数初始化;2.如果没有构造函数,则通过类内的初始值进行初始化;3.默认初始化(值将是未定义的,是任意的)。

7.2 访问控制与封装

  • 在 C++ 语言中,我们使用访问说明符加强类的封装性。定义在 public 说明符之后的成员在整个程序内可被访问, public 成员定义类的接口。定义在 private 说明符之后的成员可以被类的成员函数访问,但是不能被使用该类的代码访问, private 部分封装了(即隐藏了)类的实现细节。

  • 一个类可以包含0个或多个访问说明符,而且对于某个访问说明符能出现多少次也没有严格限定。每个访问说明符指定了接下来的成员的访问级别,其有效范围直到出现下一个访问说明符或者到达类的结尾处为止。一般来说,作为接口的一部分,构造函数和一部分成员函数应该定义在 public 说明符之后,而数据成员和作为实现部分的函数则应该跟在 private 说明符之后。

  • 使用 class 和 struct 定义类唯一的区别就是默认的访问权限。如果我们使用 struct 关键字,则定义在第一个访问说明符之前的成员是 public 的;相反,如果我们使用 class 关键字,则这些成员是 private 的。

  • 类可以允许其他类或者函数访问它的非公有成员,方法是令其他类或者函数成为它的友元( friend )。友元声明只能出现在类定义的内部,但是在类内出现的具体位置不限。友元不是类的成员也不受它所在区域访问控制级别的约束。

  • 一般来说,最好在类定义开始或结束前的位置集中声明友元。友元的声明仅仅指定了访问的权限,而非一个通常意义上的函数声明。如果我们希望类的用户能够调用某个友元函数,那么我们就必须在友元声明之外再专门对函数进行一次声明。

  • 封装有两个重要的优点:确保用户代码不会无意间破坏封装对象的状态;被封装的类的具体实现细节可以随时改变,而无须调整用户级别的代码。封装是指保护类的成员不被随意访问的能力。

  • 友元为类的非成员接口函数提供了访问其私有成员的能力,也可能就此破坏类的封装性,这种能力的提升利弊共存。

7.3 类的其他特性

  • 在类中,常有一些规模较小的函数适合于被声明成内联函数。最好只在类外部定义的地方说明 inline,这样可以使类更容易理解。 inline 成员函数应该与相应的类定义在同一个头文件中。

  • 一个可变数据成员永远不会是 const,一个 const 成员函数可以改变一个可变成员的值,通过在变量的声明中加入 mutable 关键字实现。类内初始值必须使用=的初始化形式或者花括号起来的直接初始化形式。

  • 含有指针数据成员的类一般不宜使用默认的拷贝和赋值操作,如果类的数据成员都是内置类型的,则不受干扰。

  • 返回引用的函数是左值的,意味着这些函数返回的是对象本身而非对象的副本。一个 const 成员函数如果以引用的形式返回 *this,那么它的返回类型将是常量引用。非常量版本的函数对于常量对象是不可以用的,所以我们只能在一个常量对象上调用 const 成员函数。

  • 函数的返回值如果是引用,则表明函数返回的是对象本身;函数的返回值如果不是引用,则表明函数返回的是对象的副本。

  • 通过 this 指针访问成员的优点是,可以非常明确地指出访问的是对象的成员,并且可以在成员函数中使用与数据成员同名的形参;缺点是显得多余,代码不够简洁。

  • 对一个类来说,在我们创建它的对象之前该类必须被定义过,而不能仅仅被声明。因为只有当类全部完成后类才算被定义,所以一个类的成员类型不能是该类自己,但是此类允许包含指向它自身类型的引用或指针。

  • 声明的作用是告知程序类的名字合法可用;定义的作用是规定类的细节。

  • class X; class Y { X* x; }; class X { Y y; }; 类X的声明称为前向声明,它向程序中引入了名字X并且指明X是一种类类型。对于类型X来说,此时我们已知它是一个类类型,但是不清楚它到底包含哪些成员,所以它是一个不完全类型。我们可以定义指向不完全类型的指针,但是无法创建不完全类型的对象。

  • 类可以把其他的类定义成友元,也可以把其他类(之前已定义过)的成员函数定义成友元。当把成员函数定义成友元时,要特别注意程序的组织结构。友元关系不存在传递性,友元并不能理所当然地具有访问 Screen 的特权。每个类负责控制自己的友元类或友元函数。

  • 我们仅仅是用声明友元的类的成员调用该友元函数,它必须是被声明过的。 重点是理解友元声明的作用是影响访问权限,它本身并非普通意义上的声明。

7.4 类的作用域

  • 每个类都会定义它自己的作用域(一个类就是一个作用域)。一旦遇到了类名,定义的剩余部分就在类的作用域之内了,这里的剩余部分包括参数列表和函数体。函数的返回类型通常出现在函数名之前,因此当成员函数定义在类的外部时,返回类型中使用的名字都位于类的作用域之外。

  • 用于类成员声明的名字查找。声明中使用的名字,包括返回类型或者参数列表中使用的名字,都必须在使用前确保可见。

  • 类型名要特殊处理。类型名的定义通常出现在类的开始处,这样就能确保所有使用该类型的成员都出现在类名的定义之后。

7.5 构造函数再探

  • 初始化 const 或者引用类型的数据成员唯一机会就是通过构造函数初始值,如果成员是 const、引用,或者属于某种未提供默认构造函数的类类型,我们必须通过构造函数初始值列表为这些成员提供初值。

  • 成员的初始化顺序与它们在类定义中的出现顺序一致:第一成员先被初始化,然后第二个,以此类推。最好令构造函数初始值的顺序与成员声明的顺序保持一致,而且如果可能的话,尽量避免使用某些成员初始化其他成员。如果一个构造函数为所有参数都提供了默认实参,则它实际上也定义了默认构造函数。

  • 一个委托构造函数使用它所属类的其他构造函数执行它自己的初始化过程。在委托构造函数内,成员初始值列表只有一个唯一的入口,就是类名本身。当一个构造函数委托给另一个构造函数时,受委托的构造函数的初始值列表和函数体被依次执行。

  • 当对象被默认初始化或值初始化时自动执行默认构造函数。在实际中,如果定义了其他构造函数,那么最好也提供一个默认构造函数。只有当类没有声明任何构造函数时,编译器才会自动地生成默认构造函数。

  • class NoDefault { public: NoDefault ( int i ) { val = i; } int val; };

    class C { public: NoDefault nd; C( int i = 0) : nd( i ) { } };

  • 能通过一个实参调用的构造函数定义了一条从构造函数的参数类型向类类型隐式转换的规则。只允许一步类类型转换,类类型转换不是总有效。

  • 聚合类使得用户可以直接访问其成员,并且具有特殊的初始化语法形式。聚合类其所有成员都是 public 的;没有定义任何构造函数;没有类内初始值;没有基类,也没有 virtual 函数。

  • 数据成员都是字面值类型的聚合类是字面值常量类。一个字面值常量类必须至少提供一个 constexpr 构造函数, constexpr 构造函数体一般来说应该是空的,我们通过前置关键字 constexpr 就可以声明一个 constexpr 构造函数。 constexpr 构造函数必须初始化所有数据成员。

7.6 类的静态成员

  • 有些时候类需要它的一些成员与类本身直接相关。

  • 静态成员是指声明语句之前带有关键字 static 的类成员,静态成员不是任意单独对象的组成部分,而是由该类的全体对象所共享。类的静态成员存在于任何对象之外,对象中不包含任何与静态数据成员有关的数据。

  • 和类的所有成员一样,当我们指向类外部的静态成员时,必须指明成员所属的类名。 static 关键字则只出现在类内部的声明语句中。当在类的外部定义静态成员时,不能重复 static 关键字,该关键字只出现在类内部的声明语句。

  • 要确保对象只定义一次,最好的办法是把静态数据成员的定义与其他非内联函数的定义放在同一个文件中。即使一个常量静态数据成员在类内部被初始化了,通常情况下也应该在类的外部定义一下该成员。

待完善…

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值