C++ customed type class

概念

  1. 数据抽象和封装。类的基本思想是数据抽象封装数据抽象是一种依赖于接口和实现分离的编程技术。类的接口包括用户所能执行的操作,类的实现则包括类的数据成员,负责接口实现的函数以及定义类所需要的各种私有函数。封装实现了类的接口和实现的分离,封装后的类隐藏了它的实现细节,类的用户只能使用接口而无法访问实现部分。
  2. **成员函数是定义为类的一部分的函数,有时候也被称为方法。使用.运算符后跟要使用的成员函数,同时使用调用运算符()来访问一个函数。**成员函数的声明必须在类的内部,成员函数的定义既可以在类的内部也可以在类的外部。定义在类内部的的函数是隐式的内联函数。而作为接口部分的非成员函数,如add,print,read等都必须在类的外部。
  3. 友元的声明只是指定了访问权限(可以访问类的私有成员,和第四条不冲突),而并非一个普通的函数声明,如果希望类的用户能够调用某个友元函数,必须在友元声明之外再次对函数进行一次声明.
  4. 封装的好处
    • 确保用户代码不会无意间破坏对象的状态,防止因为引入的原因造成数据被破坏,如果有程序缺陷破坏了对象的数据成员的状态,那么只有实现部分的代码可能产生这样的错误.降低了代码维护和错误修正的难度
    • 被封装的类的具体实现细节可以随时改变,无序调整用于级别的代码.类的作者可以比较自由的修改数据.当实现部分改变时,只要类的接口不变,用户代码就不需要改变.如果数据是public的,所有使用了原来数据成员的代码都可能失效,需要先定位并重写这部分代码.注意当类的实现发生改变时无序更改用户代码,但是使用了该类的源文件必须重新编译.
  5. 构造函数。类通过一个或几个特殊的成员函数控制其对象的初始化过程,这些函数叫做构造函数。
  6. 构造函数不能声明为const类型。在创建一个const对象时,直到构造函数完成初始化,对象才算真正取得了const属性。

类的基本思想是数据抽象和封装。数据抽象是一种依赖于接口和实现分离的编程技术。类的接口包括用户所能执行的操作,类的实现则包括类的数据成员,负责接口实现的函数以及定义类所需要的各种私有函数。
封装实现了类的接口和实现的分离,封装后的类隐藏了它的实现细节,类的用户只能使用接口而无法访问实现部分。

类想要实现数据抽象和封装,需要首先定义一个抽象数据类型,在抽象数据类型中,类的设计者负责考虑类的实现过程;使用该类的程序员只需要抽象的思考类型做了什么,不需要了解细节.

**类是基于对象的,类之间的关系(继承,组合,委托)是面向对象的。**C++ 是由C语言和C 标准库组成。

类的定义

每个类都定义了一个唯一的类型。即使两个类的成员列表完全一致,它们也是不同的类型。对一个类来说,它的成员和其他任何类的对象都不是一回事。
仅仅声明类而暂时不定义它,这种声明有时候也叫前向声明。在类声明之后定义之前,它属于不完全类型。我们知道它是一个类,但是不清楚它到底包含哪些类型。

类的组成包括成员函数,就是定义在类内部的函数;数据成员变量,定义在类内的数据变量;类的类型成员,就是typedef重命名的类型;访问控制等。

访问控制和封装

访问说明符

访问说明符用于加强类的封装性,让用户不能直接访问对象的内部。

  • public, 定义在public说明符后的成员在整个程序内可以被访问,public定义类的接口,向类的用户提供访问数据成员的功能。
  • private,定义在private说明符后的成员可以被类的成员函数访问,但是不能被该类的独享访问,它封装了类的实现细节。

C++中的structclass很像,只不过struct默认访问权限是public,而class默认访问权限是private的。

封装

封装的好处:

  1. 确保用户代码不会无意间破坏对象的状态,防止因为引入的原因造成数据被破坏,如果有程序缺陷破坏了对象的数据成员的状态,那么只有实现部分的代码可能产生这样的错误.降低了代码维护和错误修正的难度
  2. 被封装的类的具体实现细节可以随时改变,无序调整用于级别的代码。类的作者可以比较自由的修改数据。当实现部分改变时,只要类的接口不变,用户代码就不需要改变。如果数据是public的,所有使用了原来数据成员的代码都可能失效,需要先定位并重写这部分代码。注意当类的实现发生改变时无序更改用户代码,但是使用了该类的源文件必须重新编译。

类的作用域

类本身是一个作用域,类的成员函数的定义在类的作用域之内。编译器在处理类的时候,先编译成员的声明,然后编译成员函数体。
在类的外部定义成员函数时,成员函数的定义必须和它的声明匹配。

类的定义

成员函数

成员函数是定义为类的一部分的函数,有时候也被称为方法。使用.运算符后跟要使用的成员函数,同时使用调用运算符()来访问一个函数。
成员函数的声明必须在类的内部,成员函数的定义既可以在类的内部也可以在类的外部,定义在类内部的的函数是隐式的内联函数。而作为接口部分的非成员函数,如add,print,read等都必须在类的外部。

构造函数

构造函数完成类对象的初始化过程。详细介绍可以查看

类数据成员的初值

使用=或者列表初始化的方式为类的数据成员变量。

this指针

当一个对象调用类的成员函数时,到底发生了什么?比如:

1
2
Sales_data book;
book.isbn();

上面第二行代码其实相当于:

1
Sales_data::isbn(&book);

成员函数通过一个名字为this的额外的隐式参数来访问调用它的那个对象,编译器负责把book的地址传递给isbn的隐式形参this。在函数内部可以直接使用调用该函数的对象的成员,不需要通过成员访问运算符来实现,因为this所指的就是这个对象,***任何对类成员的直接访问都被当成this的隐式使用。**当isbn使用bookNo时,隐式的使用this指向的成员,就像我们写了this->bookNo一样.
虽然this形参是隐式定义的,但是定义任何名字为this的变量或者函数都是非法的.我们可以在成员函数体内部调用this,this的目的是总是指向当前这个对象,所以this是一个常量指针(顶层const),不允许更改它的指向。

const成员函数

默认情况下,this的类型是指向类类型的非常量版本的常量指针(顶层const).比如在Sales_data的成员函数中,this的类型是Sales_data *const,尽管this是隐式的,它仍然需要遵循初始化规则,即不能把它绑定到常量对象上,也就使得常量对象无法调用普通的成员函数(因为不能把常量对象绑定到普通指针上)。
如果isbn是一个普通函数而且this是一个普通的指针参数,应该把this生命成const Sales_data *const。顶层const是它自己带的,底层const是为了能够使得常量对象也能够调用普通的成员函数。但是因为this是隐式参数,C++ 选择将const关键字放在函数的参数列表之后,这个const表示this是一个指向常量的指针,这样的函数称为常量成员函数。
常量对象,常量的引用和指针只能调用常量成员函数.并且只能读取它的对象的数据成员,无法修改

返回this对象

当我们定义的函数类似于某个内置运算符时,应该尽量让函数的行为和内置运算符类似.比如内置运算符把它的左侧运算对象当做左值返回.如果我们在写一个复合赋值运算的时候,就需要返回一个引用类型,具体的返回值应该是this指针指向的整个对象,即*this。如下所示:

1
2
3
4
5
6
7
8
Sales_data &combine(const Sales_data &rhs)
{

units_sold += rhs.units_sold;
revenut += ths.revenue;

return *this;
}

函数返回指向当前类对象的this指针时,返回值是否为引用类型,结果完全不一样。不加引用的话,是this指针的一个副本,所有后续操作都是在这个副本上进行的,而不是在this指针指向的对象上。

从const成员函数返回*this。一个const成员函数如果以引用的形式返回*this,那么它的返回类型是常量引用。

内联成员函数

定义在类内部的函数隐式的被定义为内联的(即使不加inline关键字),定义在类内的构造函数也是内联函数。可以在类内把inline作为声明的一部分显式的声明成员函数,也能在类的外部用inline关键字修饰函数的定义。
内联成员函数最好和相应的类定义在同一个头文件中。
通常将内联函数和constexpr函数定义在头文件中。
内联函数允许多次定义,但是每次的定义必须一致。为什么?什么是内联函数,在每个调用点上将函数内联的展开。如果有两个文件foo.cpp和bar.cpp都需要调用一个相同的内联函数myinline,在编译这两个头文件的时候,需要将函数myinline进行展开,所以它们都需要定义myinline,但是在生成目标文件的时候,因为函数是强类型,就会出错,所以内联函数允许多次一致的定义。而最简单的方法就是将内联函数定义在头文件中,然后使用它的源文件包含它即可。

重载(const)成员函数

成员函数的重载和普通函数的重载很像,只不过成员函数是在类内。
同样的,对于const成员函数的重载,也和const函数的重载很像,底层const可以重载,顶层const不能重载。

可变数据成员

使用mutable关键字声明一个可变数据成员,即使它是常量对象的成员。因此,一个const成员函数(this指针是指向常量的指针)可以改变一个可变成员的值。可以使用mutable声明一个变量做特别的用途,比如统计类的某个(常量)成员函数被调用了多少次。

友元

  1. 友元提供了其他类或者函数(非类的成员函数)访问类的私有对象的功能。
  2. 友元的声明只需要在其他函数或者类前加上friend关键字即可。
    • 类之间的友元。一个类可以把其他类定义成友元,友元类的成员函数可以访问这个类包括非公有成员在内的所有成员。
    • 成员函数作为友元。可以把其他类中的某个函数设置成友元。但是这几个类之间的声明和定义需要满足一定的依赖关系。
    • 函数重载和友元。如果一个类想要一组重载函数声明为它的友元,需要对重载的每一个函数都声明为友元。
  3. 友元声明只能出现在类定义的内部,并且不会受区域访问控制级别(public,private, protected)的约束。
  4. 友元的声明只是指定了访问权限(可以访问类的私有成员,和第三条不冲突),而并非一个普通的函数声明,如果希望类的用户能够调用某个友元函数,必须在友元声明之外再次对函数进行一次声明。
    即使在类的内部定义函数,也必须在类的外部提供相应的声明从而使得函数变得可见。即使我们仅仅使用声明友元类的成员调用该友元函数,它也必须是被声明过的。友元声明的作用仅仅是影响访问权限,而非普通意义的声明。
  5. tips,一般来说,最好在类定义开始或者结束前的位置集中声明友元。
  6. 友元不具有传递性。
  7. 友元声明和作用域。类和非成员函数的声明不是必须在它们的友元声明之前。而友元类的成员函数的声明必须在它们的友元声明之前。

类的作用域

每个类都有自己的作用域,在类的作用域之外,普通的数据和函数成员只能通过对象,引用或者指针使用成员访问运算符进行访问,对于类类型成员则使用作用域运算符访问。
一个类就是一个作用域,在类的外部,成员的名字被隐藏起来了,在类的外部定义成员函数时必须同时提供类名和函数名。一旦遇到了类名,定义的剩余部分就在类的作用域之内了,剩余部分包括参数列表和函数体,接下来我们就可以直接使用类的其他成员而无需再次授权。如果函数的返回类型不是在当前类的作用域内定义的,还需要指定返回类型是哪个类的成员。

名字查找的过程:

  1. 在名字所在的块中寻找其声明语句,只考虑在名字的使用前出现的声明。
  2. 如果没找到,继续查找外层作用域。
  3. 如果没找到匹配的声明,则程序报错。

类的定义

类的定义分为两步处理,编译器处理完类中的全部声明后才会处理成员函数的定义:

  1. 首先,编译成员(包括函数和数据成员)的声明。
  2. 直到类的声明全部完成后才编译函数体。

成员函数声明的名字查找

在处理成员函数的声明时,函数声明中的返回值类型和参数列表中出现的名字,都必须在使用前确保可见。按照名字查找的过程进行查找这些名字。

类型名字

类型名字不能被重新定义。

成员函数定义的名字查找

  1. 首先在成员函数,该名字出现之前,查找该名字的使用。
  2. 如果在成员函数内部没有找到,在类的所有成员内查找。
  3. 如果类内没有找到,在成员函数定义之前的作用域内继续查找。

类的静态成员

声明

  1. 静态成员属于类本身,而不属于某个对象,可以通过在成员的声明前面加上static关键字表示这是一个静态成员。
  2. 类的静态成员存储在任何对象之外,对象中不包含任何与静态数据成员有关的数据。
  3. 类的静态成员函数也不和任意对象绑定在一起,因此它们也不包含this指针。因此,静态成员函数不能声明为const的,也不能在static对象内使用this指针。包含this指针的显示调用和其他非静态成员的调用。

静态成员的定义

静态成员函数既可以定义在类的内部,也可以定义在类的外部。定义在外部的时候,不需要重复static关键字。
而静态数据成员不属于类的任何一个对象,所以静态成员不能在构造函数中初始化。
一般来说,不在类内初始化静态数据成员,而是在类的外部定义和初始化每个静态成员,一个静态数据成员只能定义一次(最好把静态成员的定义和其他函数的定义放在同一个文件中)。下面会说到不一般的情况。

静态成员的类内初始化

不一般的情况是,可以为静态成员提供const类型的的类内初始值,在这种情况下,要求静态成员必须是字面值常量类型的常量表达式(constexpr)。

使用

  1. 静态成员不属于任何类的对象,但是可以通过类的对象,指针和引用访问静态成员(成员变量和成员函数)。成员函数也可以直接使用静态成员,不需要加上作用域运算符。

其他

头文件一旦改变,相关的源文件必须重新编译获取更新过的声明。
预处理器变量无视C++中关于作用域的规则。加上头文件保护符,防止重复包含。头文件保护符必须唯一。

1
2
3
#ifndef SALES_DATA_H
#define SALES_DATA_H
#endif

参考文献

1.《C++ Primer第五版》