概念
- 数据抽象和封装。类的基本思想是数据抽象和封装。数据抽象是一种依赖于接口和实现分离的编程技术。类的接口包括用户所能执行的操作,类的实现则包括类的数据成员,负责接口实现的函数以及定义类所需要的各种私有函数。封装实现了类的接口和实现的分离,封装后的类隐藏了它的实现细节,类的用户只能使用接口而无法访问实现部分。
- **成员函数是定义为类的一部分的函数,有时候也被称为方法。使用.运算符后跟要使用的成员函数,同时使用调用运算符
()
来访问一个函数。**成员函数的声明必须在类的内部,成员函数的定义既可以在类的内部也可以在类的外部。定义在类内部的的函数是隐式的内联函数。而作为接口部分的非成员函数,如add
,print
,read
等都必须在类的外部。 - 友元的声明只是指定了访问权限(可以访问类的私有成员,和第四条不冲突),而并非一个普通的函数声明,如果希望类的用户能够调用某个友元函数,必须在友元声明之外再次对函数进行一次声明.
- 封装的好处
- 确保用户代码不会无意间破坏对象的状态,防止因为引入的原因造成数据被破坏,如果有程序缺陷破坏了对象的数据成员的状态,那么只有实现部分的代码可能产生这样的错误.降低了代码维护和错误修正的难度
- 被封装的类的具体实现细节可以随时改变,无序调整用于级别的代码.类的作者可以比较自由的修改数据.当实现部分改变时,只要类的接口不变,用户代码就不需要改变.如果数据是
public
的,所有使用了原来数据成员的代码都可能失效,需要先定位并重写这部分代码.注意当类的实现发生改变时无序更改用户代码,但是使用了该类的源文件必须重新编译.
- 构造函数。类通过一个或几个特殊的成员函数控制其对象的初始化过程,这些函数叫做构造函数。
- 构造函数不能声明为const类型。在创建一个
const
对象时,直到构造函数完成初始化,对象才算真正取得了const
属性。
类
类的基本思想是数据抽象和封装。数据抽象是一种依赖于接口和实现分离的编程技术。类的接口包括用户所能执行的操作,类的实现则包括类的数据成员,负责接口实现的函数以及定义类所需要的各种私有函数。
封装实现了类的接口和实现的分离,封装后的类隐藏了它的实现细节,类的用户只能使用接口而无法访问实现部分。
类想要实现数据抽象和封装,需要首先定义一个抽象数据类型,在抽象数据类型中,类的设计者负责考虑类的实现过程;使用该类的程序员只需要抽象的思考类型做了什么,不需要了解细节.
**类是基于对象的,类之间的关系(继承,组合,委托)是面向对象的。**C++ 是由C语言和C 标准库组成。
类的定义
每个类都定义了一个唯一的类型。即使两个类的成员列表完全一致,它们也是不同的类型。对一个类来说,它的成员和其他任何类的对象都不是一回事。
仅仅声明类而暂时不定义它,这种声明有时候也叫前向声明。在类声明之后定义之前,它属于不完全类型。我们知道它是一个类,但是不清楚它到底包含哪些类型。
类的组成包括成员函数,就是定义在类内部的函数;数据成员变量,定义在类内的数据变量;类的类型成员,就是typedef
重命名的类型;访问控制等。
访问控制和封装
访问说明符
访问说明符用于加强类的封装性,让用户不能直接访问对象的内部。
public
, 定义在public说明符后的成员在整个程序内可以被访问,public定义类的接口,向类的用户提供访问数据成员的功能。private
,定义在private说明符后的成员可以被类的成员函数访问,但是不能被该类的独享访问,它封装了类的实现细节。
C++中的struct
和class
很像,只不过struct
默认访问权限是public
,而class
默认访问权限是private
的。
封装
封装的好处:
- 确保用户代码不会无意间破坏对象的状态,防止因为引入的原因造成数据被破坏,如果有程序缺陷破坏了对象的数据成员的状态,那么只有实现部分的代码可能产生这样的错误.降低了代码维护和错误修正的难度
- 被封装的类的具体实现细节可以随时改变,无序调整用于级别的代码。类的作者可以比较自由的修改数据。当实现部分改变时,只要类的接口不变,用户代码就不需要改变。如果数据是
public
的,所有使用了原来数据成员的代码都可能失效,需要先定位并重写这部分代码。注意当类的实现发生改变时无序更改用户代码,但是使用了该类的源文件必须重新编译。
类的作用域
类本身是一个作用域,类的成员函数的定义在类的作用域之内。编译器在处理类的时候,先编译成员的声明,然后编译成员函数体。
在类的外部定义成员函数时,成员函数的定义必须和它的声明匹配。
类的定义
成员函数
成员函数是定义为类的一部分的函数,有时候也被称为方法。使用.运算符后跟要使用的成员函数,同时使用调用运算符()
来访问一个函数。
成员函数的声明必须在类的内部,成员函数的定义既可以在类的内部也可以在类的外部,定义在类内部的的函数是隐式的内联函数。而作为接口部分的非成员函数,如add
,print
,read
等都必须在类的外部。
构造函数
类数据成员的初值
使用=
或者列表初始化的方式为类的数据成员变量。
this
指针
当一个对象调用类的成员函数时,到底发生了什么?比如:1
2Sales_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
8Sales_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声明一个变量做特别的用途,比如统计类的某个(常量)成员函数被调用了多少次。
友元
- 友元提供了其他类或者函数(非类的成员函数)访问类的私有对象的功能。
- 友元的声明只需要在其他函数或者类前加上
friend
关键字即可。- 类之间的友元。一个类可以把其他类定义成友元,友元类的成员函数可以访问这个类包括非公有成员在内的所有成员。
- 成员函数作为友元。可以把其他类中的某个函数设置成友元。但是这几个类之间的声明和定义需要满足一定的依赖关系。
- 函数重载和友元。如果一个类想要一组重载函数声明为它的友元,需要对重载的每一个函数都声明为友元。
- 友元声明只能出现在类定义的内部,并且不会受区域访问控制级别(public,private, protected)的约束。
- 友元的声明只是指定了访问权限(可以访问类的私有成员,和第三条不冲突),而并非一个普通的函数声明,如果希望类的用户能够调用某个友元函数,必须在友元声明之外再次对函数进行一次声明。
即使在类的内部定义函数,也必须在类的外部提供相应的声明从而使得函数变得可见。即使我们仅仅使用声明友元类的成员调用该友元函数,它也必须是被声明过的。友元声明的作用仅仅是影响访问权限,而非普通意义的声明。 - tips,一般来说,最好在类定义开始或者结束前的位置集中声明友元。
- 友元不具有传递性。
- 友元声明和作用域。类和非成员函数的声明不是必须在它们的友元声明之前。而友元类的成员函数的声明必须在它们的友元声明之前。
类的作用域
每个类都有自己的作用域,在类的作用域之外,普通的数据和函数成员只能通过对象,引用或者指针使用成员访问运算符进行访问,对于类类型成员则使用作用域运算符访问。
一个类就是一个作用域,在类的外部,成员的名字被隐藏起来了,在类的外部定义成员函数时必须同时提供类名和函数名。一旦遇到了类名,定义的剩余部分就在类的作用域之内了,剩余部分包括参数列表和函数体,接下来我们就可以直接使用类的其他成员而无需再次授权。如果函数的返回类型不是在当前类的作用域内定义的,还需要指定返回类型是哪个类的成员。
名字查找的过程:
- 在名字所在的块中寻找其声明语句,只考虑在名字的使用前出现的声明。
- 如果没找到,继续查找外层作用域。
- 如果没找到匹配的声明,则程序报错。
类的定义
类的定义分为两步处理,编译器处理完类中的全部声明后才会处理成员函数的定义:
- 首先,编译成员(包括函数和数据成员)的声明。
- 直到类的声明全部完成后才编译函数体。
成员函数声明的名字查找
在处理成员函数的声明时,函数声明中的返回值类型和参数列表中出现的名字,都必须在使用前确保可见。按照名字查找的过程进行查找这些名字。
类型名字
类型名字不能被重新定义。
成员函数定义的名字查找
- 首先在成员函数,该名字出现之前,查找该名字的使用。
- 如果在成员函数内部没有找到,在类的所有成员内查找。
- 如果类内没有找到,在成员函数定义之前的作用域内继续查找。
类的静态成员
声明
- 静态成员属于类本身,而不属于某个对象,可以通过在成员的声明前面加上
static
关键字表示这是一个静态成员。 - 类的静态成员存储在任何对象之外,对象中不包含任何与静态数据成员有关的数据。
- 类的静态成员函数也不和任意对象绑定在一起,因此它们也不包含
this
指针。因此,静态成员函数不能声明为const
的,也不能在static
对象内使用this
指针。包含this
指针的显示调用和其他非静态成员的调用。
静态成员的定义
静态成员函数既可以定义在类的内部,也可以定义在类的外部。定义在外部的时候,不需要重复static
关键字。
而静态数据成员不属于类的任何一个对象,所以静态成员不能在构造函数中初始化。
一般来说,不在类内初始化静态数据成员,而是在类的外部定义和初始化每个静态成员,一个静态数据成员只能定义一次(最好把静态成员的定义和其他函数的定义放在同一个文件中)。下面会说到不一般的情况。
静态成员的类内初始化
不一般的情况是,可以为静态成员提供const类型的的类内初始值,在这种情况下,要求静态成员必须是字面值常量类型的常量表达式(constexpr
)。
使用
- 静态成员不属于任何类的对象,但是可以通过类的对象,指针和引用访问静态成员(成员变量和成员函数)。成员函数也可以直接使用静态成员,不需要加上作用域运算符。
其他
头文件一旦改变,相关的源文件必须重新编译获取更新过的声明。
预处理器变量无视C++中关于作用域的规则。加上头文件保护符,防止重复包含。头文件保护符必须唯一。
1 |
参考文献
1.《C++ Primer第五版》