类的重点在于两点:

  1. 数据抽象。
    1. 接口。用户所能使用的方法。
    2. 实现。实现接口的背后的原理。
  2. 封装。隐藏实现的部分。

成员函数

成员函数就是属于类对象的函数。隶属于成员本身。通过点运算符来调用。

声明时必须在类内部。而定义可以在类之外。

非成员函数,并不是类的一部分,不能被类对象调用。但希望可以对类对象进行操作,或者对多个类对象进行操作。

可以被重载。

this

所有的成员函数,都有一个隐式参数this,一个常量指针指向调用这个函数的类对象。通过这种方式告诉成员函数是哪一个类对象在调用它。

a.b()=class_name::b(&a)a.b()\\=class\_name::b(\&a)

const成员函数

由于this是一个指向非常量的常量指针,因此常量对象不能调用普通成员函数,因为底层const不同。

通过在函数的形参表后增加const关键字使得将this转为指向常量的常量指针,通过这种方式使得常量对象可以调用该函数,也就是const成员函数。

const成员函数常用来获取类的数据成员。并且在此过程中不能对类成员产生更改。

成员与成员函数

在编译时,会先编译成员的声明,后编译成员函数,因此成员函数可以任意调用成员。

在外部定义成员函数

在外部定义成员函数需要在函数名前增加类作用域,通过这种方式将这个函数的这个块,也就是作用域声明在类的作用域内,使其能够调用类的成员函数与成员。

成员函数的返回值

当成员函数的作用类似于某个内置运算符时,应该尽力去模仿这个运算符。如形参与返回值类型。

当我们希望返回的是一个左值时,就应该返回一个引用类型。

当我们希望通过引用返回一个右值来避免拷贝的时候,应该返回一个引用常量类型。

非成员函数

当非成员函数是类接口组成部分,应该将其的声明与类放在同一个头文件,但在类的外面。

IO类型可以作为函数的参数,但是只能使用引用的方式传入。因为IO类型不可拷贝且在读写过程中会改变IO的内容。

构造函数

构造函数用来初始化一个类对象。当初始化一个常量对象的时候,构造函数需要对常量对象的成员进行初始化,因此其只能在构造函数执行完成之后才能获得常量属性。

但是常量,引用,与未提供默认初始化的类型必须通过构造函数初始化列表的方式进行初始化而不能使用=赋值,没有默认初始化的类型不能完成赋值的第一步,即先默认初始化。

因此最好在一般情况下构造函数使用构造函数初始化列表的方式进行初始化。

若是没有在构造函数中显式初始化,则将在构造函数执行前默认初始化。

默认构造函数(?)

当一个构造函数不接受任何实参的时候,它就是默认构造函数。

当一个构造函数的形参都有默认参数时,其也将隐式地被用作默认构造函数,但同时也可以被用作非默认构造函数。

当定义一个对象时

typename  name;typename\ \ name;

此时就是默认初始化,无需在name后加括号。

当在类中并没有进行显式的对构造函数的声明时,编译器会自动生成一个合成的默认构造函数来作为构造函数。

其初始化的方式是:

  • 如果成员在类内有初始值,这用该值来初始化。
  • 如果不存在,这默认初始化。

最好只在简单的类中使用合成的默认构造函数。

  1. 当且仅当一个类中不存在任何的构造函数的声明,即便声明的构造函数不是默认构造函数也算是声明了,此时,编译器将不会自动生成合成的默认构造函数,此时,这个类的默认构造函数就缺失了。
  2. 当类的成员中有内置类型或复合类型,其默认初始化之后将会是未定义的。
  3. 当类中有其他类成员,若这个类也没有默认构造函数,当前类的默认构造函数是无法自动初始化的。

在第一种的情况下,我们希望能写一个和合成的默认构造函数相同的默认构造函数时,可以使用

class_name() = default;class\_name()\ =\ default;

来显式告诉编译器要生成一个合成的默认构造函数。

当default在类内部声明时使用时,函数是inline的。

当default在类外部定义时使用时,函数是非inline的。

构造函数初始值列表

name(param_list):variable1(initnum),variable2(initnum)...{ }name(param\_list):variable1(initnum),variable2(initnum)...\{\ \}

当某个数据成员在初始化时被列表忽略时,其将会被以像合成的默认构造函数中一样进行隐式初始化。

构造函数最好不要覆盖类内原值。

列表中对象的顺序并不是真实的初始化顺序,其真实的顺序是与对象在类中声明的顺序相同。

委托构造函数

将构造函数初始值列表改为另一个构造函数的执行语句即可。

classname(param_list):classname(another_param_list){}classname(param\_list):classname(another\_param\_list)\{\}

在执行委托构造函数的初始化列表时也会执行其函数体,之后才会执行该构造函数的函数体。

拷贝、赋值和折构

如构造函数一样,在这三种操作的函数没有被定义时,编译器会自动生成一个合成的版本。

但当其中存在对类成员以外的(如内存)进行操作时,合成的版本就是失效的。但可以使用vector或string来避免。

封装的好处

  1. 保证用户不会破坏封装对象的情况。
  2. 可以随时改变实现的细节。

访问说明符

public与private,private部分就是被封装的部分。

在一个类中访问说明符可以多次被使用,即便该说明符前面已经被使用过了。

每一个访问说明符的范围是其从冒号开始到下一个访问说明符或类结束。

class与struct的区别

class默认成员是private的,struct默认成员是public的。

友元

由于产生了封装,因此用户,其他类或者非成员函数无法访问其非公有成员。

可以通过在类中声明这些函数或类为友元来使其能够访问。

格式:与声明相同并在前加上friend关键字。

建议集中在一起,在开头或结尾。

友元声明并不是对函数或类的声明,只是对已有的已声明的函数或类进行权限的声明。最好在友元声明前先对其进行声明。

类型别名

可以在类内定义类型别名,但必须先定义后使用。

别名也存在公与非公,当为公的时候其可以被用户使用。

inline

定义在类内部的函数都是隐式的inline函数。

但是也可以在类外进行显式的定义。

可变数据成员

在将一个类对象定义为const时,其所有的成员都将是const类型。或者在调用const函数时。在这两种情况下都不能对类的数据成员的值进行更改。

可以通过使用mutable关键字来使这个变量不可以是const,在上述的两种情况中都可以被改值。

初始化

初始化与赋值是不同的,赋值是先默认初始化再赋予它一个值。

当对于类对象进行初始化时。

  1. 使用构造函数,直接初始化。
  2. 使用=,进行赋值。

返回类对象本身

当返回一个非引用类型的时候,不能将其作为左值继续调用类对象的其他成员,因为它是一个临时的类对象。

当返回一个引用类型的时候,可以将其作为左值继续调用类的对象的其他成员。

但是,若是使用const类型的成员函数,即便返回的是引用类型的类对象本身,也是底层const类型的引用,此时,其作为引用不能被当做左值调用其非常量成员函数(指会改变类对象值的成员函数)。

可以通过在其外部定义一个调用其的函数来解决。

这个函数有两个重载版本。

  1. 函数是const类型且返回值是const&。
  2. 函数是普通类型且返回值为普通引用。

在这种情况下,当类对象是常量时,会调用第一个,而为非常量时,会调用第二个。以此解决即便是非常量类型调用时返回值为常量的问题。

此处声明一个两个函数既能保证代码的模块化与简便性,同时在类内定义使其内联保证其不会产生额外的开销。

不完全类型

当一个类仅仅完成了声明而没有完成定义(被称为前向声明)时,这个类就属于不完全类型。

在完成声明后,这个类类型由于不知道自身的成员,也就并不知道其占有多大的存储空间。因此类的成员不能使自己。

虽然不能创建它的对象,或者定义以他为参数或返回值的函数。但是可以声明以他为参数或返回值的函数并且可以创建指向它的指针或引用。

因此一个类的成员可以是指向自身类型的指针或引用。

友元

除了可将非成员函数定义为友元,还可以将别的类或其成员函数定义为友元。

friend  class  classname;friend  type  classname::functionname(param_list);friend\ \ class\ \ classname;\\friend\ \ type\ \ classname::functionname(param\_list);

友元不具备传递性,每个类控制自己的友元。

当一个类要使用另一个类的成员时,另一个类的定义必须先于自身。

当要声明重载函数为友元时,必须全部一个一个声明。

当友元函数被定义在类内部时,是隐式inline的,此处的定义是指在提供被访问权限的类中定义内容时。但即便是在类中定义了,其也必须要在类的外部先进行声明。

作用域

对于定义在外的成员函数,返回值类型是声明在函数之前的,也就是说作用域并不没有包含返回值类型,因此若是返回值类型是在类内定义的,就必须在其前通过::::作用域运算符来声明其作用域。

实际上,如果在作用域外调用成员函数就是违规的。

但是通过点运算符和箭头运算符与调用成员函数的类变量,使这个函数处于了作用域之内。

名字查找

对于类来说,名字查找在遵守作用域从内到外与内层同名会隐藏外层同名的两条规则以外,其并不只对处于它之前的进行查找,而是整个作用域(仅作用于类这一层作用域)。

为了实现这一点,类的所有定义都是在类的所有成员声明完之后才进行的。

虽然被同名被隐藏了,但仍然可以通过强制指向与作用域来指定对象。

这一点对于定义于类外的函数也是成立的,因为其也属于类作用域。

但对于类型的定义有些许不同。其不遵守类的规则,仍然是按顺序的,且无视作用域。因此最好将类型定义写在类的开头。并且若是在类外类内都对同名进行了类型定义,就会报错。

初始化!!!!!

类类型转换

当构造函数的参数为一个时,就会自动作为类型转换的函数。

如定义一个参数为string的构造函数,此时可以将string类型作为自定义类的类型来使用,也就是在使用时如果有需要会自动通过这个构造函数转换为类类型(通过制造一个临时变量)。

但是只允许一步的类型转换。如果使用一个char数组,那么就不能了。

可以使用explicit关键字来声明一个构造函数不能作为类型转换函数,只应该出现在类内声明时。

而对于用了explicit关键字的构造函数,在拷贝初始化中也不能进行隐式的类型转换。

但是explicit并不会阻止显式的类型转换,包括直接初始化。

聚合类

  1. 所有的成员都是public的,使得所有成员都能被用户访问操作
  2. 没有构造函数与初始值
  3. 没有基类与虚函数

可以通过列表初始化的方式初始化,其列表的顺序对应声明的顺序,故没有初始化的将列初始化。

在这种情况下,类的初始化的重任完全交给了用户。

字面值常量类

  1. 数据成员必须为字面值类型,初始化时要么使用constexpr构造函数或字面初始值。
  2. 使用折构函数的默认定义。

一般来说constexpr折构函数的函数体为空。

作用列举:

静态成员

对于类中的成员或成员函数,都可以将其声明为static。

此时其将不属于类的任何一个实例化的一个对象,而是属于类本身,并且被其实例化的所有对象共享。不能被声明为const类型。

可以使用作用域符访问。

并且虽然不属于对象,但是可以被对象通过点运算符或箭头运算符访问。

static和explicit一样,只能在类内使用,而不能用在类外。

由于不属于对象,因此不在构造函数中定义。

一般在类外进行初始化,通过作用域符。

当静态成员是整型constexpr时,可以在类内初始化,但其他都不行。

静态成员不但可以是不完全类型,如类本身,也可以作为默认实参。


https://lhish.github.io/project/hide/类/
作者
lhy
发布于
2024年6月30日
许可协议