C++—类的总结

类和对象

类的声明、类的定义

类的声明:

image-20230311145832425

可以声明一个类而不定义它,但一般不会怎么做。

类的定义:

image-20230311145912736

定义一个类时,也就是定义了一个类型。定义类时并不会进行存储分配。

是由成员成员函数(包括构造函数)组成。成员函数可以在类的定义内或类的定义外去定义。
成员函数由返回类型函数名形参表函数体组成

在形参表后面可以加上const,来定义一个常量成员函数,其作用为:该const成员函数不能修改调用该函数的对象,即:该函数的功能为只读的

image-20230311162649728

对象的定义

一旦定义了类,就可以定义该类的对象。有两种方式定义对象,它们是等价的。

对象的定义:

image-20230311150330697

定义对象时会进行存储分配。

定义类的同时定义对象:

image-20230311150510775

image-20230311150623952

设计类和对象的目的

目的是进行数据抽象和封装,其好处是:

image-20230311150722323

构造函数,析构函数

构造函数

构造函数是特殊的成员函数。构造函数不能为const成员函数。构造函数在定义时可以比其他成员函数多一个初始化列表(放在形参表后,以:开头后面接上初始化列表)

image-20230311182338994

image-20230311182627447

默认构造函数

只要定义一个对象时没有初始化,就会调用默认构造函数。默认构造函数即为:为所有形参提供默认实参的构造函数。
合成默认构造函数(只有在没有定义任何一个构造函数时,编译器才会生成一个默认构造函数)会使用默认的初始化规则进行初始化。明白的说就是,该类中int会初始化为0、指针不会初始化、类类型的成员会调用它自己的默认构造函数进行初始化等等。(因此存在指针这类需要分配资源的成员时,要自己定义构造函数)

通过默认构造函数初始化对象时要注意,对象后不要加括号,要不然会被认为是函数声明。

image-20230311182941678

image-20230311183000599

image-20230311183011679

最好使用默认实参的形式定义构造函数。即:在形参列表里面给形参赋一个默认的值,然后后面的初始化列表里面将该形参赋给对应的数据成员。

image-20230312113415539

这样可以减少代码重复。因为,这样可以不用特地去写一个默认构造函数,直接在可以在实现接收参数的构造函数时同时实现默认构造函数。

构造函数会对传入的实参自动进行类型转换。

合成复制构造函数

其为C++编译器为用户合成的,其行为是逐个成员初始化,即:复制传进去的对象的所有非static成员。
如果自己定义了一个复制构造函数,则不会产生合成复制构造函数。通常在类里有指针数据成员的时候(分配了内存资源,或是要分配其他资源)需要自己定义复制构造函数。

image-20230311184048705

只有不存在任何其他的构造函数时,才会生成合成默认构造函数,所以:如果定义了复制构造函数,就必须定义默认构造函数!

合成赋值操作

类似合成复制构造函数,都是逐个成员初始化(赋值),将新的对象初始化为原对象的副本。
和合成复制构造函数一样,如果类里有指针数据成员(需要分配资源),需要自己重载一个赋值操作符。(同样自己重载了,编译器就不会生成合成赋值操作)

image-20230311184128753

析构函数

其作用是资源回收、释放资源
一般类对象会在超出其作用域(即花括号)时自动调用析构函数,释放资源。(删除每一个非static成员)
动态内存分配的对象(即用指针以及new出来的对象)只会在指针被删除(delete该指针)时才会调用析构函数。否则即使超出作用域,其对象以及其对象内部使用的任何资源都不会被释放。
与合成复制构造函数、合成赋值操作不同,即使自己定义了析构函数,编译器总是会生成合成析构函数,而且在运行完了自己定义的析构函数后,总是会运行合成析构函数。

image-20230311184332689

隐含的this指针,static成员,友元

隐含的this指针

每个成员函数(static成员函数除外)都有一个隐含的形参this。在调用函数时,形参this初始化为调用函数的对象的地址。在成员函数中,对这类的成员的任何没有前缀的使用,都被编译为通过指针this实现的引用。(当然在函数体中也可以显式地使用this指针,虽然没有必要)

一般this指针是不需要用到的,但在需要返回对象本身的时候就可以用到*this

image-20230311184624295

对于非const成员函数,this是一个指向该类的对象的const指针,this指向的值可以改变,但不能改变this保存的地址。
const成员函数里,this是一个指向const类对象的const指针,既不能改变所指向对象的值,也不能改变this保存的地址。

static成员

访问static成员可以通过类的域操作符,也可以通过对象引用。

image-20230311183804214

static数据成员

static数据成员是与类关联的(并不和该类的对象关联)其只能在类定义的外面定义、且只能定义一次。(不是也不能通过构造函数初始化的)

image-20230311185805064

特殊的是const static成员,其为常量成员。需要在类定义的内部定义,但还是需要在类定义外部声明一次。

image-20230311183941338

image-20230311183950249

static函数成员

和static数据成员一样,其是与类关联的(并不和该类的对象关联)也因此没有this指针。

static就是起控制、限制作用

image-20230311183626656

友元

目的是允许特定的其它的类及其成员函数访问本类的私有成员(protected、private)。

通过在本类里面添加friend类或函数。

image-20230311183401662

image-20230311183411046

因此添加的类或函数需要在本类前定义好。如果想把每一个重载函数设为友元的话,需要对每一个都声明为friend。

继承

C++ Primer里有段话概括的挺好的:
image-20230520173421449

基类、派生类

protected成员、继承的方式

protected成员有以下性质:
image-20230520174325384

注意,这里有个比较绕的地方(我感觉),子类其实并不能访问父类的protected成员,它的作用可以这么想:子类继承了父类的protected成员,因此子类可以用自己继承到的那个protected成员,但不能用父类的protected成员(虽然private成员其实也继承了,只是不可见而已,但这样记忆更有条理点,不会那么绕。)。而public成员则是,无论是自己、子类、还是别的类、甚至是任何一个地方(作用域内)都可以直接用(访问)。

关于private,这个文章私有成员是否会被继承感觉挺好的,写的是Java的,但感觉C++应该也差不多。

继承的格式image-20230520180257649

其中access-label(继承的方式)则有如下三种:
image-20230520180400266

这里要注意:实际上,无论继承的方式如何,其子类对父类成员的访问权限都是一样的不会变,因为该访问权限看的是父类成员自己到底是什么类型的成员,而不是看继承方式。而这里的继承方式影响的则只是使用该子类的用户对子类从父类继承而来的成员的访问权限

virtual函数、纯虚函数、派生类到基类的(自动)转换

上面说的三大概念之一—动态绑定就是通过虚函数来实现的。(动态绑定是指在程序运行时才确定函数调用的对象,而不是在程序编译时确定。)多态也是通过虚函数来实现的。(多态是指同一种操作作用于不同的对象,会产生不同的结果。)
C++中,函数调用默认不使用动态绑定。要触发动态绑定,必须满足:1.只有指定为虚函数的成员函数才能进行动态绑定。2.必须通过基类类型的引用或指针进行调用。
成员函数默认为非虚函数(非虚函数则不能进行动态绑定)
虚函数的目的就是为了允许用基类的指针或引用来调用子类的这个函数。而纯虚函数目的则是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数(否则编译会报错)。

纯虚函数格式(在函数原型后面加上=0):
image-20230521191301393

每个派生类对象都包含基类部分,所以1.可以将基类类型的引用绑定到派生类对象的基类部分,2.也可以用指向基类的指针指向派生类对象(实际上是指向其中的基类部分)。
概括来说就是派生类可以当作基类来使用。但要注意的是,只有派生类到基类的转换,而基类到派生类的转换是根本没有的。因为很显然,派生类有基类的一切(无论是其成员还是各种操作),因此基类类型的引用和指针可以安全地引用派生类对象;而基类却只有派生类的一部分,如果基类转换过去那就会存在很多缺失,因此是不被允许的(但代码上却可以通过static_cast强制类型转换)。

友元关系、静态成员、作用域

友元关系不能继承。也就是说,父类的友元对子类的私有成员没有访问权限。如果需要访问子类的私有成员,则子类需要将访问权限授予父类的友元(就是将它也写为自己的友元)。

父类的static成员在整个继承层次中只有一个这样的成员。无论父类派送出多少个子类,每个static成员只有一个实例。至于static成员的访问控制,则是和其它成员一样的规则,如果是private的话,子类不能访问它。
因为每个static成员在整个继承层次中只有一个实例,所以通过父类访问的static成员、通过子类访问的static成员;通过作用域操作符访问的、通过点或箭头访问的都是同一个东西——该static成员。

每个类都保持自己的作用域。在继承情况下,子类的作用域嵌套在父类作用域中。如果在子类中找不到名字,那就会在外围父类作用域里找该名字的定义。(也正是这种类作用域的嵌套,使得子类可以直接访问父类的成员)

构造函数、复制控制、析构函数

构造函数

构造顺序:先构造父类部分,再构造子类自己这部分(就是先运行父类的构造函数,再运行子类的构造函数)
子类的合成默认构造函数有特殊之处:除了初始化自己的数据成员外,还需要初始化父类部分,其父类部分是由父类自己的默认构造函数初始化的。当然,和所有类一样,可以自己定义默认构造函数,也可以定义含参数的构造函数。
下面是一个子类构造函数通过向父类构造函数传递实参来初始化的例子(因为子类不能通过初始化列表直接初始化继承而来的成员,所以需要用到父类的构造函数):
image-20230521225222335

这里要注意:子类只能初始化直接父类。例如,子类是不能初始化父类的父类的!

复制、赋值、撤销(析构)

和所有类一样,不含指针的类可以用合成复制、赋值、撤销操作。(如果子类显示定义自己的复制构造函数或赋值构造函数,则该定义将完全覆盖默认的合成构造、赋值函数)

复制构造函数和一般构造函数一样,先是通过调用父类的复制构造函数,将父类部分复制过来,然后运行自己的复制构造部分。
自己实现赋值构造函数时要注意的是,赋值操作符必须防止自身赋值,写代码时加上类似如下的判断就行:
image-20230521232118253

上面代码还需要注意的是:和一般构造函数一样,这里也是先将子类的父类部分通过父类的赋值运算符赋值给新子类的父类部分(这里运用到了子类向父类的转换),然后再是为子类自己的成员赋值。

析构函数和复制构造函数、赋值操作符不一样:子类不负责撤销(析构)父类对象的成员(每个析构函数只负责清除自己的成员),当然了合成默认析构都是自动完成的,需要注意的是析构的顺序正好和构造时顺序相反,先运行子类的析构函数、再运行父类的析构函数。

虚析构函数非虚的构造和赋值函数
1.因为存在子类到父类的转换,所以调用析构函数的时候,如果析构函数不是虚函数的话,作为一个指向子类的父类指针析构的时候只会调用父类的析构函数,而导致子类的成员没有被释放(析构)掉。因此析构函数需要是虚的。
2.构造函数则不需要为虚。因为在构造函数(除赋值构造)运行时,对象的动态类型状态还不完整;而对赋值操作符来讲,因为每个类型都有各自对应的、且参数不同(各自的类型对象)的赋值构造函数,所以虚函数并没有什么作用。所以他们都没必要是虚的。

还有个需要注意的地方:在构造过程中以及在析构过程中,类的对象是不完整的,而且其类型是在发生变化的(例如:析构的时候子类对象的子类部分被析构掉后就变成了父类对象)。因此在构造、析构函数里调用虚函数的话,其会绑定对应继承层级、类型的对象。即:还是刚才那个例子,子类析构变成父类后,其如果调用了虚函数,则运行的就是绑定了父类版本的虚函数。