本章节包括过程性编程和面向对象编程;类概念;如何定义和实现类;公有类访问和私有类访问;类的数据成员;类方法(类函数成员);创建和使用类对象;类的构造函数和析构函数;const成员函数;this指针;创建对象数组;类作用域;抽象数据类型。
OOP特性:抽象;封装和数据隐藏;多态;继承;代码的可重用性。
这个例子很有意思,说明了c与c++的区别。
C与C++的区别;面向过程与面向对象的区别;_c++是面向对象吗-CSDN博客
10.1 过程性编程和面对对象编程
过程性编程方法,首先考虑要遵循的步骤,然后考虑如何表示这些数据;
OOP则是首先考虑数据(如何表示和使用数据);
用户与数据交互的方式有三种:初始化、更新和报告----这就是用户接口。
10.2 抽象和类
抽象是通往用户类型的捷径,在c++中,用户定义类型指的是实现抽象接口的类设计。
10.2.1 类型是什么
指定基本类型完成了三项工作:决定数据对象需要的内存数量;决定如何解释内存中的位;决定可使用数据对象执行的操作或方法。
10.2.2 C++中的类
类是一种将抽象转换为用户定义类型的C++工具,它将数据表示和操作数据的方法合成一个整洁的包。
类规范由两个部分组成:类声明:以数据成员的方式描述数据部分,以成员函数(方法)的方式描述公有接口;类方法定义:描述如何实现类成员函数。
接口:一个共享框架,供两个系统交互时使用。
class关键字表明定义了一个类,Stock是这个新类的类型名。该声明让我们能够声明Stock类型的变量----称为对象或实例。
创建类;
#ifndef STOCKOO_H_
#define STOCKOO_H_
#include <string>
class Stock
{
private:
std::string company;
long shares;
double share_val;
double total_val;
void set_tot() { total_val = shares * share_val; }
public:
void cquire(const std::string &co, long n, double pr);
void buy(long num, double price);
void sell(long num, double price);
void update(double price);
void show();
};
#endif
存储的数据以类数据成员(例如company)的形式出现;
要执行的操作以类函数成员(方法,例如sell())的形式出现。
成员函数可以就地定义(例如set_tot),也可以用原型表示。
1.访问控制
关键字private和public描述了对类成员的访问控制。使用类对象的程序都可以直接访问公有部分,但只能通过公有成员函数来访问对象的私有成员。因此公有成员函数是程序和对象的私有成员之间的桥梁,提供了对象和程序之间的接口。防止程序直接访问数据被称为数据隐藏。
c++还有第三个访问控制关键字protected。
将实现细节放在一起并将它们与抽象分开被称为封装。数据隐藏是一种封装。
数据隐藏不仅可以防止程序直接访问数据,还让开发者无需了解数据是如何被表示的。
2.控制对成员的访问:公有还是私有
对于类成员的数据成员和成员函数都可以在类的公有部分或私有部分声明;
OOP的主要目标之一是数据隐藏,所以数据项通常放在私有部分,组成类接口的成员函数放在公有部分;
private关键字可以不用显式的使用,因为这是类对象的默认访问控制;
class world
{
float mass;
public:
void tell(void);
……
};
类和结构:
类描述看上去像是包含成员函数以及public和private可见性标签的结构声明。实际上,c++对结构进行了扩展,使之具有与类相同的特性。它们之间的唯一区别是,结构的默认访问类型是public,而类是private。c++程序员通常使用类来实现类描述,而把结构限制为指标是纯粹的数据对象(常被称为普通老式数据(POD)结构)。
10.2.3实现类成员函数
定义成员函数时,使用作用域解析运算符::来标识函数所属的类;
类方法可以访问类的private组件。
如stock和buffoon是两个类,它们都有一个函数名为update()的函数,不同类可以有同名的函数。
void stock ::update(double price)
void buffoon ::update()
就可以说,update()具有类作用域。
类方法的完整名称中包含类名。stock::update()是函数的限定名;只是update()是非限定名,它只能在类作用域中使用。
方法可以访问类的私有成员。我的理解方法就是类里面的函数。
程序清单10.1;这是一个头文件,定义了一个类; · 196b405 · Kite/C和C++ - Gitee.com
程序清单10.2;类方法的应用; · 5855eee · Kite/C和C++ - Gitee.com
1.内联函数
定义在类声明中的函数都将自动成为内联函数,类声明常将短小的成员函数作为内联函数,如果想在类外声明内联函数,可以使用inline关键字。
内联函数要求每个使用它们的文件中都对其进行定义,为了方便使用内联函数,我们将内联函数放在类的头文件中。
2.方法使用哪个对象
创建对象: 类名 对象名(类名相当于一种数据类型 对象名是变量)
使用成员函数方法 对象名.函数名(参数),这个和结构成员有点类似也是应用句点运算符,指针成员是应用箭头运算符。
创建的每个新对象都有自己的存储空间,用于存储其内部变量和类成员;但同一个类的所有对象共享同一组类方法,即每个方法只有一个副本。
举个例子,还是Stock类,此时创建了kate和joe两个对象,那么kate.shares和kate.shares都将占用一个内存块,但是kate.show()和joe.show()都调用同一个方法。
在OOP中,调用成员函数叫发送消息。
10.2.4 使用类
创建类对象,可以声明类变量,也可以使用new为类对象分配存储空间。类对象可以作为函数的参数和返回值,可以将一个对象赋值给另一个对象。
code_c++/usestock0.cpp · Kite/C和C++ - 码云 - 开源中国 (gitee.com)
客户/服务器类型:OOP程序员常依照客户/服务器模型来讨论程序设计。
10.2.5 修改实现
在原有的基础上修改;定点表示法输出显式三位小数,而不是以前的还有科学计数法; · affb5de · Kite/C和C++ - Gitee.com
10.2.6 小结
指定类设计的第一步是提供类声明。类声明类似结构声明,包括数据成员和函数成员。在私有部分声明的成员只能通过成员函数进行访问。一般数据成员放在私有部分,成员函数放在公有部分。
公有部分的内容构成了设计的抽象部分----公有接口。将数据封装到私有部分中可以保护数据的完整性,这被称为数据隐藏。
第二步是实现类成员函数。在类声明中提供完整的函数定义,而不是函数原型,但是通常的做法是单独提供函数定义(除非函数很小)。这种情况下,就需要使用作用域解析符来指出成员函数属于哪个类。
10.3类的构造函数和析构函数
应为类提供被称为构造函数和析构函数的标准函数。
对象的变量不能像初始化int变量 那样来进行初始化,因为对象的存在私有成员,在类外无法进行访问。而类的成员函数可以访问这些成员变量,因此我们可以使用类的成员函数对对象进行初始化,而类的构造函数就是C++用来初始化对象的一个函数。
在创建对象的时候自动对它们进行初始化。为此,c++提供了一个特殊的成员函数,专门用于构造新对象、将值赋给它们的数据成员。
构造函数的原型和函数头有一个特征----没有返回值,但也没有被声明为void类型,构造函数没有声明类型。
名字必须与类名一致,且没有返回类型,可以有参数,可以重载(也就是可以有多个构造函数)。如fun类的构造函数为 fun()。
10.3.1 声明和定义构造函数
还是以Stock类举例,四个数据成员,其中第四个是通过计算所得,所以只需要给构造函数提供三个参数即可。
Stock函数原型:
Stock(const string & co,long n = 0, double pr = 0.0);
第一个参数是指向字符串的指针,该字符串用于初始化成员company。 注意,没有返回类型,原型位于类声明的公有部分。
有没有发现这个构造函数和上图里的公有成员函数acquire()是一样的,二者区别是,程序声明对象时,将自动调用构造函数。
成员名和参数名:
试图将类成员名称用作构造函数的参数名,如下,这是错误的。构造函数的参数表示的不是类成员,而是赋给类成员的值。因此,参数名不能与类成员相同,否则最终的代码将是
shares = shares;
Stock::Stock(const string & company,long shares, double share_val);
为了避免上述这种混乱,有两种常见的做法,第一是在定义数据成员名的时候使用m_前缀;第二是在成员名中使用后缀_。
10.3.2 使用构造函数
C++提供了两种使用构造函数来初始化对象的方式。
第一种是显式地调用构造函数:
Stock food = Stock("World Cabbage", 250, 1.25);
第二种是隐式的调用构造函数:
Stock garment("Furry Mason", 50, 2.5);
不管是显式地还是隐式地,二者是等价的。
每次创建类对象(new也一样)时,c++都使用类构造函数。下面是将构造函数和new一起使用的方法;
Stock *pstock = new Stock ("Electroshock Games", 18, 19.0);
这里创建了一个Stock对象,并将该对象的地址赋值给pstock指针,但是该对象没有名字,只能通过指针管理该对象。
构造函数的使用方式不同于其他类方法。构造函数无法通过对象来调用,因为在构造函数构造出对象之前,对象是不存在的。因此构造函数被用来创建对象,而不能通过对象来调用。
10.3.3 默认构造函数
默认构造函数是在未提供显式初始值时,用来创建对象的构造函数。比如下面这种声明;
Stock fluffy_the_cat;
若不定义构造函数,程序将提供一个默认的隐藏的构造函数,而这个构造函数并不进行任何操作。
对于Stock类的构造函数可能是这样的;
Stock::Stock(){ }
默认构造函数没有参数,因为声明中不包含值。
当且仅当没有定义任何构造函数时,编译器才会提供默认构造函数。当为类定义了构造函数后,程序员就必须编写默认构造函数。如果提供了非默认构造函数,没有提供默认构造函数,那么如下这种定义就会报错。
Stock fluffy_the_cat;
定义默认构造函数的方式有两种。一种是给已有构造函数的所有参数提供默认值;
Stock(const string & co = "Error", int n = 0, double pr = 0.0);
另一种是通过函数重载来定义另一个构造函数----一个没有参数的构造函数;
若手动定义了多个构造函数,程序则会根据定义对象时候括号里的参数,来确定使用哪一个构造函数。
Stock();
在设计类时,通常应提供对所有类成员做隐式初始化的默认构造函数。
Stock first;//隐式调用默认构造函数
Stock first = Stcok();//显式调用
Stock *prelief = new Stock;//隐式调用
Stock first("Concrete Conglomerate");//调用构造函数
Stock second();//声明一个函数
Stock third;//
10.3.4 析构函数
对象过期后,程序将自动调用一个特殊的成员函数,叫做---析构函数。析构函数完成清理工作。例如构造函数使用new分配内存,则析构函数就是用delete来释放这些内存。例如构造函数没有使用new,那么析构函数就什么都不用做,这种情况,只需要生成一个什么都不做的隐式析构函数即可。
析构函数的名称:在类名前加上~。例如Stock类的析构函数就是~Stock()。
和构造函数一样,析构函数也可以没有返回值和声明类型,但是析构函数没有参数,例如Stock析构函数的原型必须是;
~Stock();
Stock::~Stock()
{
}
什么时候调用析构函数?
由编译器决定,一般不在代码中显式的调用;
如果创建的是静态存储类对象,则析构函数在程序结束时自动调用;
如果是自动存储类对象,在执行完代码块时自动调用;
如果是通过new创建的,则它将驻留在栈内存或自由存储区中,当使用delete来释放内存时,自动调用析构函数;
如果是临时变量,程序将在结束对该对象的使用时自动调用其析构函数。
由于在类对象过期时析构函数将自动被调用,因此必须有一个析构函数。如果程序员没有提供析构函数,编译器将隐式的声明一个默认析构函数,并在发现导致对象被删除的代码后,提供默认析构函数的定义。
析构函数
析构函数的特点:
- 一个类只有一个析构函数,不能重载
- 名字与类名相同,定义时候要加上~
- 没有参数
- 不能有返回值,没有返回值类型
对应于构造函数初始化对象,析构函数是用来释放对象的内存的,可以将delete等操作放在析构函数里,析构函数在对象离开其作用域的时候自动执行。
对象析构的顺序和创建的顺序相反,即先构造的后析构,后构造的先析构,但这并非绝对成立!!!!
在不同的作用域中的对象,或者具有不同存储类别的对象,调用构造函数和析构函数的顺序也会有所不同
- 一个程序中有多个文件。在多个文件中定义了全局对象,那么这些对象的执行顺序是不确定的;
- 在函数中定义局部自动对象,如果函数被多次调用;多次调用构造函数和析构函数;
- 如果函数中定义静态局部对象;那么函数调用结束时对象并不释放,只有main结束或调用exit时才调用析构函数。
什么时候调用析构函数?
- 如果在函数中定义了一个对象,当函数调用结束时,释放对象前自动执行析构函数
- static 局部对象在函数调用结束时,包含的对象不会被释放,只在main函数结束或调用exit函数时,才调用static局部对象的析构函数。
- 如果定义了一个全局对象,则在程序的流程离开其作用域时(如main函数结束,或exit语句),调用该全局对象的析构函数
- 如果用new运算符动态地建立了一个对象,当用delete 运算符释放对象时,先调用该全局对象的析构函数。
10.3.5 改进Stock类
在stock00.h的基础上加入了构造函数和析构函数,命名为stock10.h;acquire()函数删除,不再需要,前面也说过它和构造函数的作用差不多;
stock00.cpp到stock10.cpp,添加了构造函数和析构函数的方法定义;
usestock0.cpp到usestock1.cpp,用于测试新方法;
code_c++/stock10.h · Kite/C和C++ - 码云 - 开源中国 (gitee.com)
三个文件都已经上传到gitee上,可以从上述链接去查看。
usestock1.cpp文件中,可以观察到多了一个大括号,这个大括号的作用是什么呢?诸如stock1和stock2等自动变量将在程序退出其定义所属代码块时小时。如果没有这个大括号,代码块为整个main(),因此仅当main()执行完毕后,才会调用析构函数。在窗口模式下,这意味着将在两个析构函数调用前关闭,导致无法看到最后两条消息。添加大括号后,最后两个析构函数调用将在到达返回语句前执行,从而能够显示。
Stock stock1("NanoSmart", 12, 20.0);
stock1 = Stock("Nifty Foods", 10, 50.0);
第一条语句是初始化,它创建有指定值的对象,可能会创建临时对象(也可能不会);第二条语句是赋值,赋值使用构造函数就会导致在赋值前创建一个临时对象。
既可以通过初始化,又可以通过赋值设置对象的值,初始化方式的效率更高。
类也可以使用列表初始化。
const Stock land = Stock("The Kite");
land.show();
上面代码,编译器将拒绝第二行,因为show()代码无法保证调用对象不被修改。这里c++提供了一种解决方法,将const关键字放在函数的括号后面。
void show()const;//声明
void stock::show()const//定义
以此种方式声明和定义的类函数叫const成员函数。
10.4this指针
众所周知,同一个类的多个对象公用一套方法,那么在调用同一套方法的时候,程序是如何知道要访问哪个对象里的成员的呢,答案是通过this指针,类的成员函数里有一个隐藏的指向对象的参数就是this指针。
this指针指向用来调用成员函数的对象(this被作为隐藏参数传递给方法),像下面这行代码,this指针就指向stock1对象的地址。
stock1.topval(stock2);
topval()函数用于比较两个类中的某个数据成员的值大小;
stock2对象作为函数参数传递,所以显式访问;
stock1对象调用类成员函数,所以隐式访问。
每个成员函数(包括构造函数和析构函数)都有一个this指针。
返回类型为引用意味着返回的是调用对象本身,而不是其副本。
10.5对象数组
对象数组跟结构体差不多,这里就讲讲一些不同点。
声明对象数组的方法和声明标准类型数组相同;
Stock mystuff[4];
首先是初始化,由于对象有构造函数,所以我们在初始化对象数组的时候要用大括号括起来,将构造函数列出来,来进行初始化;对于各个元素也可以使用不同的构造函数;
Stock stock[4] =
{
Stock("NanoSmart", 12, 20.0),
Stock("Boffo Objects", 200, 20.0),
Stock("Monolithic Obelisks", 130, 3.25),
Stock("Fleep Enterprises", 60, 6.5)
};
初始化对象数组的方案是:首先使用默认构造函数创建数组元素,然后花括号中的构造函数将创建临时对象,然后将临时对象的内容复制到相应的元素中。因此,创建类对象数组,则这个类必须要有默认构造函数。
10.6类作用域
在类中定义的名称(包括数据成员和成员函数名)的作用域都是整个类;
可以在不同类中使用相同的类成员名;
要调用公有成员函数必须通过对象,不能从外部直接访问;
在定义成员函数时,必须使用作用域解析运算符;
void Stock::update(double price)
10.6.1 作用域为类的常量
如何创建一个由所有对象共享的常量呢?
class Bakery
{
private:
const int Months = 12;
double costs[Months];
……
};
//这里是行不通的,因为声明类只是描述了对象的形式,并没有创建对象。
//因此,在创建对象前,将没有用于存储值的空间。
有两种方式可以实现上面整个目标,效果相同。
第一种方式是在类中声明一个枚举 。在类声明中声明的枚举的作用域是整个类,因此可以用枚举为整形常量提供作用域为整个类的符号名称。
class Bakery
{
private:
enum {Months = 12};
double costs[Months];
……
};
用这种方式声明枚举并不会创建类数据成员,也就是说,所有对象都不包含枚举。
这里使用枚举只是为了创建符号常量,并不打算创建枚举类型的常量,因此不需要提供枚举名。
另外一种方式是----使用static关键字;
class Bakery
{
private:
static const int Months = 12;
double costs[Months];
……
};
这里创建的常量与其它静态变量存储在一起,而不是存储在对象中。
10.6.2 作用域内枚举
传统的枚举在使用过程中存在一些问题,两个枚举定义中的枚举量可能发生冲突,这个冲突是因为它们在同一个作用域内;
enum egg {small, medium, large, jumbo};
enum t_shirt {small, medium, large, xlarge};
为了解决上述冲突,c++11提供了一种新枚举,其枚举量的作用域为类;
enum class egg {small, medium, large, jumbo};
enum class t_shirt {small, medium, large, xlarge};
这里也可以用struct替代class;
egg choice = egg::large;
t_shirt floyd = t_shirt::large;
枚举量的作用域为类后,不同枚举定义中的枚举量就不会发生名称冲突了。
c++还提高了作用域内枚举的类型安全。作用域内枚举不能隐式的转换为整形,但必要时可以显式的类型转换。
默认c++11作用域内枚举的底层类型为int。
看下述语法,指定了数据类型。但是注意底层必须为整形。
enum class : short pizza {small, medium, large, xlarge};
10.7 抽象数据类型
抽象数据类型(ADT)。这里主要说了抽象数据类型,使用类来表示是一种很好的方式。然后举了一个例子,栈是如何工作的。
栈由操作系统自动分配释放 ,用于存放函数的参数值、局部变量等,其操作方式类似于数据结构中的栈。后入先出,入栈和出栈,或者叫压栈和出栈都是从栈顶操作的。
类比一下堆,堆由开发人员分配和释放, 若开发人员不释放,程序结束时由 OS 回收,分配方式类似于链表。堆没有后入先出,先入先出这个东西,而是一种动态的。