本章内容包括:
- 过程性编程和面向对象编程
- 类概念
- 如何定义和实现类
- 共有类访问和私有类访问
- 类的数据成员
- 类方法(类函数成员)
- 创建和实用类对象
- 类的构造函数和析构函数
- const成员函数
- this指针
- 创建对象数组
- 类作用域
- 抽象数据类型
OOP(面向对象编程)最重要的特性:
指定基本类型完成了三项工作:
类的定义,一般来说,类规范由两个部分组成:
简单说就是类声明提供了类的蓝图,方法定义提供了细节。
接口是一个共享框架,供两个系统之间(比如任何计算机系统之间或者人和计算机之间)交互时使用。
C++关键字中class指出了这些代码定义了一个类设计。使用类定义接口时,将会自动指定使用对象的规则。
1.访问控制
关键字private和public也是新的,描述了对类成员的访问控制。使用类对象的程序都可以直接访问公有部分,但是只能通过公有成员函数或者友元函数来访问对象的私有成员。防止程序直接访问数据被称为数据隐藏。
类设计尽可能将公有接口和实现细节分开。公有接口表示小合集的抽象组件,将实现细节放在一起并将他们和抽象分开被称为封装。
2.控制对成员的访问:公有还是私有
由于OOP,数据项通常会放在私有部分,组成类接口的成员函数被放在公有部分。
使用私有成员函数来处理不属于公有接口的实现细节。
C++中结构和类都能够使用private和public,但是结构中默认访问类型为public,但是类中默认访问类型为private。C++中通常实用类实现类描述,而把结构限制为指标是纯粹的数据对象。
类成员函数相比于普通函数的两个特殊特征:
比如,update()成员函数:
void Stock::update(double price)
类成员函数的内联方法
函数定义为与类声明中的函数都将自动成为内联函数。类声明常将短小的成员函数作为内联函数。
如果想在声明之外定义内联成员函数,只需要加iniline限定符号。
根据改写规则,在类声明中定义的方法等同于使用原型替换方法定义,然后在类声明的后面将定义改写为内联函数。所以两者其实等价。
调用成员函数时,将会使用对象本身的数据成员。
每个对象都有字节的存储空间,用来存储内部变量和类成员。但是同一个类的所有对象共享一组类方法,即每种方法只有一个副本。
在OOP中,调用成员函数被称为发送消息,因此将同样的消息发送给两个不同的对象将会调用同一个方法。
由于类的成员变量存在私有,所以不能够使用之前常规的方法进行初始化。
所以类构造函数为了解决创建时自动初始化的问题,提供了特殊的类成员构造函数,构造函数的函数名和类名相同,且没有声明类型,也没有返回值。
构造函数中的参数名不能和类成员相同,不然会引起混乱。常见的解决方法是在成员变量之后或者之前加上特定的前缀。
C++可以显示或者隐式调用构造函数。
- // 显示方法
- Stock food = Stock("Woril", 220, 1.25);
-
- // 隐式方法
- Stock food("Woril", 220, 1.25);
-
- // 将构造函数和new一起使用的方法
- Stock *food = new Stock("Woril", 220, 1.25);
两种构造方法等价。
构造函数只能够用来创建对象,不能够被对象调用。
如果没有提供任何构造函数,C++将会自动提供默认构造函数。
Stock::Stock(){}
但只有当没有提供构造函数时,C++才会提供默认构造函数;
如果提供了构造函数,那么必须提供一个默认构造函数。
定义默认构造函数的方法有两种,一种是为构造函数的所有参数提供默认值;一种是提供一个没有参数的构造函数。
在创建的对象过期后,程序将会自动调用析构函数。析构函数用来完成清理工作。
析构函数的名称是~类名。比如:
~Stock(){}
与构造函数不同的是,析构函数没有参数。何时调用析构函数由编译器来决定,不应该显示的调用析构函数。
如果没有提供析构函数,那么编译器将会自动添加一个隐式析构函数。
比如,如果定义一个静态类型对象,那么将会在整个程序结束后调用析构函数;如果定义一个自动变量,那么在执行完代码块时调用析构函数;如果通过new来创建对象,那么在delete时将会调用析构函数。
在默认情况下,将一个对象付给同类型的另一个对象时,将会将每个数据成员的内容复制到目标对象的数据成员中。
- Stock s1 = Stock{"asd" , 100 . 45.0};
- s2 = Stock{"asd" , 100 . 45.0};
考虑上面两个语句。
第一条语句是初始化语句,创建有指定值的对象,可能会创建临时对象;第二条语句是赋值语句,在赋值语句中使用构造函数总会创建一个临时对象。
如果既可以通过初始化也可以通过赋值来设置对象的值,则应该采用初始化的方式,通常这种方法的效率更高。
const 成员函数
- const Stock land = Stock("dsadsa");
- land.show();
编译器将会拒绝第二行的操作,因为无法保证调用对象不被修改,
之前会通过函数参数声明为const引用或者指向const的指针来解决这个问题,但是这里这样做会出现语法问题,因此需要一种新的语法来保证函数不会修改调用对象。
C++将const关键字放在函数括号后面来解决这个问题。
- void show() const;
-
- void Stock::show() const;
使用这种方法声明定义的类函数被称为const成员函数。
只要类方法不修改调用对象,就应该将其声明为const。
如果类成员函数中涉及到了多个对象,那么为了区分,就需要用到this指针。
定义一个比较函数,使用const引用作为参数,函数不改变调用对象的值,返回一个满足函数条件的引用:
- const Stock & topval(const Stock & s) const
- {
- if (s.val > val)
- {
- return s;
- }
- else
- {
- return ???
- }
- }
此时有个问题,如何表示隐式访问的对象本身?
C++中使用this的特殊指针,指向用来调用成员函数的对象。
- const Stock & topval(const Stock & s) const
- {
- if (s.val > val)
- {
- return s;
- }
- else
- {
- return *this
- }
- }
用户通常会创建一个类的多个对象,此时就需要用到对象数组。
声明对象数组的方法和声明标准类型数组的方法是相同的。
Stock mystaff[4];
此时如果没有构造函数,那么就会调用隐式构造函数。
也可以显示调用构造函数:
- const int STKS = 4;
- Stock stocks[STKS] = {
- Stock{"s1", 1, 1.0},
- Stock{"s2", 2, 1.0},
- Stock{"s3", 3, 1.0}
- };
当然,也可以将其中的构造函数换成不同的构造函数。
显示构造函数之外的对象都将调用隐式默认构造函数。
C++类引入了一种新的作用域叫做类作用域。
在类中定义的名称,比如数据成员名和类成员函数名,作用域都为整个类,作用域为整个类的名称只在该类中是已知的,在类外是不可知的。
类作用域意味着不能从外部直接访问类的成员。使用公有也是一样,需要通过对象来访问。
不能够在声明类的时候赋值常量(直接使用const在类内不行),但是有两种方式能够达到相同的效果。
第一种方式是在类中声明一个枚举:
- class Bakery
- {
- private:
- enum {Months = 12};
- double costs{Months};
- ...
- }
使用这种方式声明枚举并不会创建类数据成员,也就是说,所有对象中都不会包含枚举。
第二种方式是使用关键字static:
- class Bakery
- {
- private:
- static const int Months= 12;
- double costs{Months};
- ...
- }
此时将会创建一个名为Months的常量,该常量和其他静态变量储存在一起,而不是存储在对象中。
此时只有一个Months常量,被所有Bakery对象共享。
- enum egg{Small,Medium,Large};
- enum t_shirt{Small,Medium,Large};
此时将无法通过编译,因为发生了冲突。
C++11提供了一种新的枚举方法,枚举量的作用域为类:
- enum class egg{Small,Medium,Large};
- enum class t_shirt{Small,Medium,Large};
也可以通过struct代替class。此时使用枚举名来限制枚举量:
- egg choice = egg::Large;
- t_shirt Floyed = t_shirt::Small;
使用这种方法提高了类型安全性,作用域内枚举不能隐式与int进行转换。
本章内容包括:
- 运算符重载
- 友元函数
- 重载<<运算符,用于输出
- 状态成员
- 使用rand()生成随机数
- 类的自动转换和强制类型转换
- 类转换函数
运算符重载是一种形式的C++多态。
之前的章节中介绍了函数多态的方式。而运算符重载将重载的概念扩展到运算符上,允许赋予C++运算符多重含义。
C++允许将运算符扩展到用户定义的类型,要重载运算符,需要使用被称为运算符函数的特殊函数形式,运算符函数的格式如下:
operator op (argument-list)
比如说,operator + ()重载+运算符,operator *()重载*运算符。
当然,op必须是有效的C++运算符,不能够虚构一个新的符号。
举例来说,如果重载了Saleperson类的+操作,那么就可以在代码中使用如下的形式:
ans = sid + sara;
编译器此时发现数据类型为Saleperson类,那么将会替换成如下形式:
ans = sid.operator+ (sara);
总之,使用operator+()或者使用运算符表示法都可以进行调用。
C++对用户定义的运算符重载有限制:
| sizeof | 内存量 |
| . | 成员运算符 |
| .* | 成员指针运算符 |
| :: | 作用域解析运算符 |
| ?: | 条件运算符 |
| typeid | 一个RTTI运算符 |
| const_cast | 强制类型转换运算符 |
| dynamic_cast | 强制类型转换运算符 |
| reinterpret_cast | 强制类型转换运算符 |
| static_cast | 强制类型转换运算符 |
只能通过公有方法对数据进行访问,有时候这种限制过于严格。C++提供了另外一种形式的访问权限:友元。
友元有三种:
考虑使用非成员函数,进行运算符重载:
A= 2.7 * B;
编译器将会与下面的非成员函数调用进行匹配:
A= operator * (2.7, B);
该函数的原型如下:
Time operator * (double m,const Time & t);
使用非成员函数可以按照所需要的顺序获取操作数(先是double ,然后是Time)但是问题是,非成员函数不能够直接访问类的私有数据。然而, 有一类特殊的非成员函数,可以访问类的私有成员,它们被称为友元函数。
创建友元函数的第一步是将函数的原型放在类声明中,并在原型声明前加上关键字friend。
friend Time operator* (double m , const Time & t);
该原型会说明:
第二步是编写函数定义,因为不是Time的成员函数,所以不用加Time ::的限定符。另外不要在定义中使用friend的关键字:
- Time operator* (double m , const Time & t)
- {
- Time result;
- long totalminutes = t.hours * 60 + t.minutes;
- result.hours = totalminutes / 60;
- result.minutes = totalminutes % 60;
- return result;
- }
友元函数是否违背了C++的OOP呢?其实没有,应该将友元函数视作类的扩展接口的组成部分。
如果要为类重载运算符,并将分类的项作为其第一个操作数,则可以用友元函数来反转操作数的顺序。
1. << 的第一种重载版本
如果要将类作为第一个操作数,意味着需要这样写:A << cout;
这样写会让人迷惑,所以可以通过友元函数来进行重载:
- void operator << (ostream & os, const Time & t)
- {
- os << t.hours << " hours, "<< t.minutes << " minutes";
- }
-
- // 之后可以直接使用cout进行输出
- cout << A;
由于只需要对于os整体进行使用,所以不需要成为os的友元函数。
2. << 的第二种重载版本
方法1实际上存在一个问题:
只能使用cout << A的形式,如果cout中有其他类型,那么就不允许了,比如cout << "Trip Time:" << A;
cout << A << B等同于(cout << A)<< B
<<运算符要求左边是一个ostream对象,所以可以对友元函数采用相同的方法,只要修改operator <()函数,让他返回ostream对象的引用即可:
- ostream & operator << (ostream & os, const Time & t)
- {
- os << t.hours << " hours, "<< t.minutes << " minutes";
- return os;
- }
在定义运算符时,必须选择其中的一种格式,而不能同时选择两种方式,否则会出现二义性错误。
(这里是使用的一个例子举例进行说明,不展开)
因为运算符重载是通过函数来实现的,所以只要运算符函数的特征值不同,使用的运算符数量与相应的内置C++运算符相同,就可以多次重载同一个运算符。
比如对 - 进行重载,有两种版本:
- // 两个操作数的版本
- Vector operator - (const Vector & b) const;
-
- // 一个操作数的版本
- Vector operator - () const;
标准ANSI库有一个rand()函数,会返回一个从0到某个值之间的随机整数,但直接使用一般由于种子数默认,返回的都是伪随机数。
可以使用srand(time(0))来覆盖默认的种子值,其中time(0)代表返回当前时间,这样每次运行都会设置不同的种子。
如果类的构造函数只有一个参数,那么可以直接进行赋值,比如:
Stonewt myCat;
myCat =15.6;
此时,程序会调用构造函数创建一个临时对象,并将临时对象赋值。这一过程为隐式转换。
但这种情况只有接收一个参数的构造函数才能作为转换函数。对于两个或多个参数的构造函数,如果之后的参数都有默认值,也可以进行转换。
但是这种转换其实并不安全。
C++新增了explicit关键字,用来关闭这种自动特性。
explicit Stonewt(double lbs);
这样将会关闭隐式转换,但是还是会允许显式进行转换。
可以将数字转换为类,那么能不能将类转为数字?
可以,但是不是使用构造函数,而是使用特殊的C++运算符函数——转换函数。
转换函数是用户定义的强制类型转换,可以像使用强制类型转换一样使用。
转换函数形式:
operator typeName();
其中:
例如,如果要将类转换为int和double 。那么类声明中应该包含如下的原型:
operator int();
operator double();
- // 定义如下:
- Stonewt::operator int() const
- {
- return int (pandas + 0.5);
- }
由于二义性的原因,所以如果不能显示的指出需要转换成什么类型,那么就不能进行转换。
如果指出了,那么可以进行隐式转换:
int w = stonewt;
如果不想进行隐式转换的话,可加上explicit关键字。
要将double类和自定义类相加,有两种选择。
第一种方法是,将函数定义为友元函数,调用构造函数,将double转换为自定义类。
operator +(const Stonewt &,const Stonewt &);
第二种方法是,将加法运算符重载为一个显示使用double参数的函数:
Stonewt operator+(double x);
friend Stonewt operator+(double x, Stonewt &s);
优缺点分析:
第一种方法(依赖于因式转换)能够让程序更加简短,工作量少,不易出错。但是缺点是每次需要转换时,都需要调用转换构造函数,增加内存和时间开销。
第二种方法(增加显示匹配类型函数)则相反,程序较长,工作量大,容易出错。但是运行速度较快。