C++ primer plus学习笔记-第十章:对象和类
前言: 本章重点阐述对象和类的具体特性和一些实现细节
OOP的特性:
- 抽象
- 封装和数据隐藏
- 多态
- 继承
- 代码的可重用性
使用OOP编程时,首先从用户的角度考虑对象——描述对象所需的数据以及描述用户与数据交互所需要的操作
定义一种数据类型时,不仅仅是分配了内存,还规定了可以对变量执行的操作,具体来说完成了三项工作:
- 决定数据对象需要的内存数量
- 决定如何解释内存中的位
- 决定可使用数据对象执行的操作或方法
接口:
接口是一个共享框架,供两个系统交互使用;
公众是使用类的程序,交互系统由对象组成,而接口由编写的人提供的方法组成
通常C++将类定义放在头文件中,而将类方法的实现放在源文件中;为了帮助识别类,还有这样一个约定俗成但不通用的事实:类的首字母使用大写
使用类对象的程序都可以直接访问公有部分,但只能题通过公有成员函数来访问对象的私有成员;
比如让我们来定义一个类:
class Stock
{
private:
std::string company;
long shares;
double share_val;
double total_val;
void set_tot(){total_val = shares * share_val}
public:
void acquire(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();
};
任何出现在类中的成员都默认为private的,而publid需要手动声明;然而为了强调,显式使用private也是完全可以的;
类和结构有相似之处,不同点是:类成员的默认属性是private,而结构体的默认类型是public;
- 定义类方法时使用作用域解析运算符::标识所属的类
- 类方法可以访问private组件
定义位于类定义中的函数会自动成为内联函数,实现的方法是在定义时使用inline限定符:
class Stock
{
private:
...
void set_tot();
...
public:
...
};
inline void Stock::set_tot()
{
total_val = shares * share_val;
}
内联函数比较特殊,要求在每个使用它的文件中都对它进行定义,因此最好是将它的定义放在头文件中;
调用成员函数的方法:
kate.show();
joe.show();
//调用成员函数时,它将使用被用来调用它的对象的数据成员
每一个类对象的数据是单独各有一个副本存储的,但是类方法是所有类共享的,内存中总共只有一个副本;
禁止cout使用科学计数法显示数据的方法:
std::cout.setf(std::ios_base::fixed,std::ios_base::floatfield);
//强制cout使用定点表示法
实际上在每次更改完cout的输出模式后,都应该重新把状态重置:
std::streamsize prec = std::cout.precision(3);
//这里定义了一个类型为std::streamsize的变量来保存原始信息
std::cout.precision(prec);
//这里使用之前保存的信息重置cout的状态
std::ios_base::fmtflags orig = std::cout.setf(std::ios_base::fixed);
//这里定义了一个类型为std::ios_base::fmtflags的变量来保存出初始状态
std::cout.setf(orig, std::ios_base::floatfield);
//这里重置cout的状态
应当为类对象提供构造函数和析构函数,前者的作用是在类对象被创建时对其中的变量进行初始化,后者的作用是在类对象被清除时做一些清理工作;
构造函数和析构函数都没有返回类型,前者和类本身同名,后者是类名称前加上符号~
程序声明类时会自动调用构造函数;但构造函数的参数表示的不是类成员,而是赋值给类成员的值,因此使用这样的方法:在数据成员名称后加上前缀m_或使用后缀_;
C++提供了两种使用构造函数来初始化对象的方式:
Stock food = Stock("World cabbage", 250,1.25);
//显式初始化
Stock garment("Furry Mason", 50,2.5);
//隐式初始化
注意:无法使用类对象调用构造函数!仅在创建对象时被使用!
如果没有提供构造函数,C++将自动提供默认的构造函数,它是默认构造函数的隐式版本,不做任何工作;
注意:当且仅当没有定义任何构造函数时,编译器才会提供默认构造函数;为类定义创造了构造函数后,就必须为它提供默认构造函数;如果提供了非默认构造函数但没有提供默认构造函数,则下面的声明会出错:
Stock stock1;
如果要创建对象而不显式初始化,就必须要定义一个不接受任何参数的默认构造函数;实现的方法有两种:
//第一种方案是给已有的构造函数的所有参数提供默认值:
Stock(const string & co = "Enter", int n = 0, double pr = 0.0);
//第二种方案是通过函数重载来定义另一个参数列表为空的构造函数
Stock();
析构函数完成清理工作;如果使用new为类分配内存,则析构函数将使用delete来释放这些内存;
析构函数的名称前有符号~,没有返回值和声明类型;调用析构函数的时机由编译器决定,通常不在代码中显式调用析构函数;
关于这三种情况:如果创建的是静态存储对象,析构函数在程序结束时自动调用;如果创建的是自动存储类对象,析构函数在程序执行完代码块时自动调用;如果对象是通过new创建的,在使用delete释放内存时析构函数会被自动调用;
可以把一个对象赋值给另一个对象那么,程序会逐个成员赋值直到两个对象的所有成员状态都相同;
当存在匹配的构造函数时,可以使用列表来初始化新创建的类对象;
要声明类方法不能修改类成员时:
void show() const;
//在类定义中声明函数时
void Stock::show() const;
//在源文件中定义类函数时
类对象中有一个名为this的指针,这个指针指向类本身;
下面是一个关于const的例子:
const stock & topval(const Stock & s) const;
//第一个const表示函数返回的对象是不可修改的
//第二个const表示传给函数的引用是不可修改的
//第三个const表示类方法本身是不能修改类成员的
返回类型是引用表示返回的是调用对象本身,而不是副本;
可以声明由对象构成的数组,和声明由基本类型构成的数组之间是没有区别的;像这样:
Stock mtstuff[4];
//C++会自动为每个类对象调用构造函数
可以使用列表对类列表进行初始化,并且如果定义了不同的构造函数,还可以对不同的类对象调用不同的构造函数:
const int STKS = 10;
Stock stocks[STKS] = {
Stock("Nanosmart", 12.5, 20),
Stock(),
Stock("Monolightic Obelisks", 130, 3.25),
};
//没有在列表中指定的显式初始化方法的类对象会使用默认构造函数进行初始化
类作用域: 在类中定义的名称的作用域都为整个类;
在类中的常量很有用,但是不能使用const来定义常量;下面是两种方案:
//第一种方案,使用枚举
class Bakery
{
private:
enum {Months = 12};
double costs[Months];
...
}
//第二种方案,使用关键字static
class Bakery
{
private:
static const int Months = 12;
double const[Months];
}
另外,C++提供了一种枚举,其作用域为类:
enum class egg {Small,Medium,Large,Jumbo};
enum class t_shirt {Small,Medium,Large,Jumbo};
也可以使用struct来代替关键字class,无论使用哪种方式,都需要使用枚举名来限定枚举变量:
egg choice = egg::Large;
t_shirt Floyd = t_shirt::Large;
拓展:实现栈
栈是一种很基础的数据结构,上文提到的操作足够实现这样一种简单且重要的数据结构;栈可执行的操作有:
- 可创建空栈
- 可将数据项添加到栈顶(压入)
- 可从栈顶删除数据项(弹出)
- 可查看栈是否填满
- 可查看栈是否为空
下面让我们来完成头文件:
#ifndef STACK_H_
#define STACK_H_
typedef unsigned long Item;
class Stack
{
private:
enum {MAX =10};//栈的长度
Item items[MAX];//创建栈空间
int top;//栈顶的位置
public:
Stack();//栈对象的构造函数
bool isempty();//检测栈是否为空
bool isfull();//检测栈是否填满
bool push(const Item & item);//将数据压入栈中
bool pop(Item & item);//将栈栈顶的数据弹出
};
#endif
上面的代码已经提供了类接口,下面在另一个源文件中向它提供具体实现:
#include"stack.h"
Stack::Stack()
{
top = 0;//将栈顶的位置初始化为0
}
bool Stack::isempty() const
{
return top == 0;
}
bool Stack::isfull() const
{
return top == MAX;
}
bool Stack::push(const Item & item)
{
if (top < MAX)
{
items[top++] = item;
return true;//压栈成功
}
else
return false;//压栈失败
}
bool Stack::pop(Item & item)
{
if (top > 0)
{
item = items[--top];
return true;//弹出成功
}
else
retuen false;//栈为空,弹出失败
}
至此我们已经成功实现了"栈"这种数据类型;