【对象和类】——C++ Prime Plus CH10

①过程性编程和面向对象的编程

以下纯纯为个人理解

过程性编程——将整体的数据单独考虑,通过单独的数据来设计函数达到自己的目的(考虑对象对单独的数据)

面向对象的编程——将整体的数据做成一个整体,对于这个整体进行考虑(把整体当成一个新的单独个体来考虑我要对它做什么 初始化,计算,显示和更新等),通过实现对整体的操作来达到目的(考虑对象为整体的东西)

②抽象和类

1.类的介绍

①什么是类型?

对于数据,我们想要对它分类,可以根据数据的外观对其进行分类(在内存中如何存储,char占1个字节,int占4个字节等),但是,我们也可以根据对它执行的操作来进行分类。例如,对于指针和int数据,它们在内存中可能都占4个字节,但是int数据我们可以对其进行加减乘除取余操作,但是对于指针数据,便不能进行乘运算,因为没有意义,所以我们根据对数据的不同操作将int数据和指针数据进行了分类。

我们在声明变量时,不仅分配了内存,还规定了可对变量执行的操作。也就是说,指定基本类型完成了三项工作:

1)决定数据对象需要的内存数量;

2)决定了如何解释内存中的位(转换数值的方法不同,比如int和float)

3)决定了对数据对象执行的操作和方法

2.C++中的类

①介绍

简单地说,类就是将数据表示和操作数据的方法组合成一个整洁的包

例如,我们现在要定义一个表示股票的类。

我们首先考虑数据表示,也就是我们要如何表示股票。具体地说,我们将存储以下信息来表示股票:

1)公司名称;

2)所持股票的数量;

3)每股的价格;

4)股票总值

接下来我们考虑对股票所执行的操作

1)获得股票;

2)增持;

3)卖出股票;

4)更新股票的价格;

5)显示关于所持股票的信息。

接下来定义类,一般来说,类规范由两部分组成。

1)类声明:数据成员的方式描述数据部分,以成员函数的方式描述公有接口。

2)类方法定义:描述如何实现成员函数

补充:什么是接口?

接口是一个框架,供两个系统交互时使用。对于类,我们说公共接口,它沟通了编写类的人和使用类的人(程序员,具体说是程序员设计的函数),接口具体来说可以当作对类的操作(成员函数)。

通常,C++程序员将接口(类定义)放在头文件中,并将实现(类方法的代码)放在源代码中。

②类的定义

#ifndef STOCK00_H_
#define STOCK00_H_

#include

class stock     //声明类
{
private:
	std::string company;
	long shares;
	double share_val;
	double total_val;
	void ste_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();
};
#endif

在这个例子中,我们将stock类的声明放在头文件中,使用了#ifndef等来访问多次包含同一个文件。我们来看一下类的通用特性。

1.C++关键字class指出这些代码定义了一个类设计,stock是这个新类的类型名,我们可以通过stock来声明stock类型的变量——称为对象或实例。

stock sally;
stock solly;

2.要存储的数据以类数据成员形式出现,要执行操作以类函数成员形式出现。

3.private和public描述了对类成员的控制访问。使用类对象的程序  可以 直接访问公有部分,但只能通过友元函数(11章介绍)来访问对象的私有成员,这被称为数据隐藏(防止程序直接访问数据)。

4.无论是数据成员还是成员函数,均可以在私有和公有声明,但是由于隐藏数据是OOP主要目标之一,因此数据通常放在私有部分,组成类成员接口的成员函数放在公有部分。不需要在类声明中使用关键字private,因为默认就是private。这也是类和结构体最大区别,结构体默认是public。

③实现类成员函数

#include
#include"stock00.h"

void stock::acquire(const std::string& co, long n, double pr)    //函数头使用解析运算符
{
    company = co;
    if (n < 0)
    {
        std::cout << "Number of shares can't be negative;"
            << company << " shares set to 0.\n";
        shares = 0;
    }
    else
        shares = n;
    share_val = pr;
    set_tot();
}
void stock::buy(long num, double price)
{
    if (num < 0)
    {
        std::cout << "Number of shares purchased can't be negative;"
            << " Transaction is aborted.\n";
    }
    else
    {
        shares += num;
        share_val = price;
        set_tot();
    }
}
void stock::sell(long num, double price)
{
    using std::cout;
    if (num < 0)
    {
        std::cout << "Number of shares sold can't be negative;"
            << " Transaction is aborted.\n";
    }
    else if (num > shares)
    {
        std::cout << "You can't sell more than you have!"
            << " Transaction is aborted.\n";
    }
    else
    {
        shares -= num;
        share_val = price;
        set_tot();
    }
}
void stock::update(double price)
{
    share_val = price;
    set_tot();
}
void stock::show()
{
    std::cout << "Company: " << company
        << "Shares: " << shares << '\n'
        << "Share Price:$ " << share_val
        << "Total Worth:$ " << total_val << '\n';
}

实现类成员函数需要注意的点:

1.定义成员函数时,使用作用域解析运算符来标识函数所属的类;——意味着我们可以在其他类中声明名称相同的函数;

2.类方法可以访问类的private组件(show函数)。

3.set_tot()只是实现代码的一种方式,而不是公有接口的组成部分,因此类将其声明为私有成员函数(定义在类声明的函数自动成为内联函数,在类声明中定义方法等同于用声明替换定义,然后在类声明的后面将定义改写为内联函数)。内联函数要求在每个使用它们的文件中都要对其定义,最简单的做法是将内联定义放在头文件中。

4.方法怎么使用对象?

和使用结构体成员一样,通过成员运算符:

stock kate, joe;
kate.show();
joe.show();

调用成员函数被称为发送消息,因此将同样的消息发送给两个不同的对象将调用同一个方法,方法被用于两个不同的对象。

④使用类

#include
#include"stock00.h"

int main()
{
	stock fluffy_the_cat;
	fluffy_the_cat.acquire("NanoSmart", 20, 12.50);
	fluffy_the_cat.show();
	fluffy_the_cat.buy(15, 18.125);
	fluffy_the_cat.show();
	fluffy_the_cat.sell(400, 20.00);
	fluffy_the_cat.show();
	fluffy_the_cat.buy(300000, 40.125);
	fluffy_the_cat.show();
	fluffy_the_cat.sell(300000, 0.125);
	fluffy_the_cat.show();
	return 0;
}

使用类就和使用基本类型一样。要创建类,可以直接声明类变量,也可以使用new为类对象分配存储空间,可以将对象作为函数的参数和返回值。也可以将对象赋给另一个。

⑤修改实现

数字格式不一致,采用定点表示法来修改实现。修改实现仅需要在类成员函数实现里修改即可,保留头文件和客户文件不变(不能影响使用类的用户)。

③类的构造函数和析构函数

1.构造函数

①什么是构造函数?

对于上边的stock类,我们还有其他工作要做。上边的代码还不能狗让您像初始化int那样来初始化stock类,这是因为数据部分的访问状态是私有的,这意味着程序不能直接访问数据成员,程序只能通过成员函数来访问。一般来说,最好再创建对象时对它进行初始化,为此,C++提供了一个特殊的成员函数——类构造函数,专门用于构造新对象、将值赋给它们的数据成员。构造函数名称和类的名称相同,虽然没有返回值,但是原型和函数头没有void关键字。

②声明和定义构造函数

现在我们为stock类创建构造函数。

在头文件中的公有部分声明构造函数原型:

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();
	stock(const std::string& co, long n = 0, double pr = 0);

由于需要为stock对象提供三个值,因此应为构造函数提供三个参数。程序员可能只想设置company成员,而将其他值设为0,这可以使用默认参数来完成。

下面是构造函数的定义:

stock::stock(const std::string& co, long n, double pr)
{
    company = co;
    if (n < 0)
    {
        std::cout << "Number of shares can't be negative;"
            << company << " shares set to 0.\n";
        shares = 0;
    }
    else
        shares = n;
    share_val = pr;
    set_tot();
}

构造函数定义和acquire()相同,区别在于,程序声明对象时,将自动调用构造函数。 

注意:

构造函数的参数不是类成员,而是要给赋给类成员的值,所以参数名不能与类成员相同,为了避免这种混乱,一种常见的做法是在数据成员名中使用m_前缀;或者在数据成员名后使用_后缀。

std::string m_company;
	long shares_;

③使用构造函数?

C++提供了两种使用构造函数来初始化对象的方式。

1.显示调用构造函数:

stock fluffy_the_cat = stock("world cabbage",250,1.25);  //圆括号

2.另一种方式是隐式调用构造函数:

stock fluffy_the_cat("world cabbage",250,1.25);       //圆括号

每次创建对象,(包括使用new动态分配内存),C++都使用类构造函数。

stock* pstock = new stock("Furry Mason", 50, 2.5);

 构造函数的使用方式不同于其他类方法。一般来说,使用对象来调用方法,但无法使用对象来调用构造函数,因为对象还没建立。

注:接受一个参数的构造函数允许使用赋值语句将对象初始化为一个值:

Bozo(int age);

Bozo tubby = 32;

(这种方法不是很好,11章将介绍如何关闭这项特性)。

④默认构造函数

默认构造函数是在未提供显式初始值时,用来创建对象的构造函数。

stock fluffy_the_cat;

如果没有提供任何构造函数,则C++将自动提供默认构造函数,默认构造函数没有参数,相当于创建变量未赋值。如果为类定义了构造函数,程序员就必须为他提供默认构造函数,如果提供了非默认构造函数,上边的声明将出错。

定义默认构造函数的方法有两种

1.给已有构造函数的所有参数提供默认值:

stock(const std::string& co = "Error", long n = 0, double pr = 0);

 2.通过函数重载来重新定义另一个构造函数——没有参数的构造函数;

stock::stock()
{
    company = "no name";
        shares = 0;
    share_val = 0;
    total_val = 0;
}

由于只能有一个默认构造函数,因此上边方式二选一,通常应初始化所有对象,以确保所有成员一开始有一个合理的值 。

注:隐式调用默认构造函数,不要使用圆括号。

2.析构函数

①什么是析构函数?

对象过期时,程序将自动调用一个特殊的成员函数——析构函数。析构函数完成清理工作。

如果构造函数使用new来分配内存,则析构函数使用delete来释放这些内存;

如果构造函数没有使用new,析构函数没有需要完成的任务,只需要编译器生成一个什么都不要做的隐式析构函数即可。

析构函数的名称为 :在类的前边加上~。和构造函数一样,析构函数也可以没有返回值和声明类型,但是析构函数没有参数。

析构函数都是自动被调用的,如果程序员没有提供析构函数,编译器将隐式地声明一个默认析构函数,并在发现导致对象被删除地代码后,提供默认析构函数地定义。

②改进stock类

在类中加入构造函数和析构函数:

#ifndef STOCK00_H_
#define STOCK00_H_

#include

class stock     //声明类
{
private:
	std::string company;
	long shares;
	double share_val;
	double total_val;
	void set_tot() { total_val = shares * share_val; }
public:
	stock(const std::string& co , long n = 0, double pr = 0);
	stock();
	~stock();
	void buy(long num, double price);
	void sell(long num, double price);
	void update(double price);
	void show();
	
	//
};
#endif

类实现文件:

#include
#include"stock00.h"

void stock::buy(long num, double price)
{
    if (num < 0)
    {
        std::cout << "Number of shares purchased can't be negative;"
            << " Transaction is aborted.\n";
    }
    else
    {
        shares += num;
        share_val = price;
        set_tot();
    }
}
void stock::sell(long num, double price)
{
    using std::cout;
    if (num < 0)
    {
        std::cout << "Number of shares sold can't be negative;"
            << " Transaction is aborted.\n";
    }
    else if (num > shares)
    {
        std::cout << "You can't sell more than you have!"
            << " Transaction is aborted.\n";
    }
    else
    {
        shares -= num;
        share_val = price;
        set_tot();
    }
}
void stock::update(double price)
{
    share_val = price;
    set_tot();
}
void stock::show()
{
    std::cout << "Company: " << company
        << "Shares: " << shares << '\n'
        << "Share Price:$ " << share_val
        << "Total Worth:$ " << total_val << '\n';
}
stock::stock(const std::string& co, long n, double pr)
{
    company = co;
    if (n < 0)
    {
        std::cout << "Number of shares can't be negative;"
            << company << " shares set to 0.\n";
        shares = 0;
    }
    else
        shares = n;
    share_val = pr;
    set_tot();
}
stock::stock()
{
    company = "no name";
        shares = 0;
    share_val = 0;
    total_val = 0;
}

stock::~stock()
{
   
}

 客户文件

#include
#include"stock00.h"

int main()
{
	{
		using std::cout;
		cout << "Using constructors to create new objects\n";
		stock stock1("NanoSmart", 12, 20.0);
		stock1.show();
		stock stock2 = stock("Boffo Objects", 2, 2.0);
		stock2.show();

		cout << "Assigning stock1 to stock2\n";
		stock2 = stock1;
		cout << "Listing stock1 and stock2\n";
		stock1.show();
		stock2.show();

		cout << "Using a constructor to reset an object\n";
		stock1 = stock("Nifty Food", 10, 50.0);
		cout << "Revised stock1\n";
		stock1.show();
		cout << "Done!";
		return 0;
	}
}

解释:

1.创建并初始化新对象:

①创建新对象,并将数据成员初始化为指定的值;

②构造函数创建一个临时对象,然后将临时对象复制到要创建的新对象中,将临时对象丢弃(调用析构函数)。

2.对存在的对象赋新值:

 构造函数创建一个临时对象,然后将临时对象复制到要创建的新对象中,将临时对象丢弃(调用析构函数)。

3.C++11列表初始化:

可以用大括号括起来对其进行初始化,注意,没有初始化的值参数默认设置为0;

4.const成员函数

const stock land = stock{ "Kludghsai Pdhsid" };
		land.show();

 第二行会报错,这是因为show()的代码无法确保调用对象不被修改,我们以前通过将函数参数声明为const引用或者指向const的指针来解决这个问题,但是这个函数没有参数,C++提供了新语法,将const关键字放在函数的括号后边。

void show() const;
void stock::show() const
{
    std::cout << "Company: " << company
        << "Shares: " << shares << '\n'
        << "Share Price:$ " << share_val
        << "Total Worth:$ " << total_val << '\n';
}

这样来声明和定义函数,从现在开始,只要类方法不修改调用对象,就应该将其声明为const。 

④this指针

1.this指针的提出

对于stock类,到目前为止,每个类成员都只涉及一个对象,即调用它的对象,但有时候方法可能涉及两个对象,这种情况下需要使用this指针。

假设我们现在需要设计一个函数,让程序查看股票,然后返回价值最高的那只股票。

由于程序无法直接访问数据,因此要让程序知道数据,最直接的方式就是让方法返回一个值,为此,通常使用内联代码来完成。

public:
	double total() const { return total_val; }

我们还可以采用this指针的方法来找到价格最高的股票。

我们定义一个成员函数,它查看两个stock对象,并返回股价较高的那个对象的引用。将成员函数名命名为topval(),函数调用stock1.topval()将访问stock1对象的数据,如果要实现比较,则必须把第二个对象作为参数传递给函数。处于效率考虑,我们按引用传递;如何将方法的答案传递给程序呢?最简单的方法就是返回一个引用。问题在于,我们可以返回参数的引用,如果要返回该对象的引用,我们怎么返回呢?(stock1没有别名)

解决办法是:使用被称为this的特殊指针。this 指针指向用来调用成员函数的对象(作为隐藏参数传递给方法),这样当函数调用stock1.topval(stock2)时,this指针将设置为stock1的地址。

一般来说,所有的类方法都将this指针设置为调用它的对象的地址。

每个成员函数(包括构造函数和析构函数)都有一个this指针,如果要引用整个调用对象,返回*this(将*this作为调用对象的别名来完成前边方法的定义)。 

2.使用this指针

头文件
public:
	stock(const std::string& co , long n = 0, double pr = 0);
	stock();
	~stock();
	void buy(long num, double price);
	void sell(long num, double price);
	void update(double price);
	void show() const;
   const stock & topval(const stock& s) const;
const stock& stock::topval(const stock& s) const
{
    if (s.total_val > total_val)
        return s;
    else
        return *this;
}

⑤对象数组

我们可以单独创建多个对象,也可以创建一个对象数组。

初始化对象数组的方案是,首先使用默认构造函数创建数组元素,然后花括号中的构造函数将创建临时对象,然后将临时对象的内容复制到相应的元素中。

stock mystuff[4] =
		{
			stock(),
			stock("Ndhas",12.5,20),
			stock{"Dfef",130,6.5}
		}

声明是初始化了数组的部分元素,余下的元素将使用默认构造函数进行初始化。

#include
#include"stock00.h"

int main()
{
	{
		using std::cout;
		stock mystuff[4] =
		{
			stock(),
			stock("Ndhas",12.5,20),
			stock{"Dfef",130,6.5},
			stock("Fldei dhaio",60,6.5)
		};

		cout << "Stock holding:\n";
		int st;
		for (st = 0; st < 4; st++)
			mystuff[st].show();

		const stock* top = &mystuff[0];
		for (st = 1; st < 4; st++)
			top = &top->topval(mystuff[st]);

		cout << "\nMost valuable holding:\n";
		top->show();
		return 0;

	}
}

⑥类作用域

第九章,我们介绍了全局(文件)作用域和局部(代码块)作用域,函数名称的作用域只能是全局的(不然函数将无法被调用。)C++类引入了新的作用域:类作用域。

在类中定义的名称(类数据成员和类函数成员)的作用域都为整个类,在类外是不可见的,因此,可以在不同类中使用相同的类成员名而不会引起冲突。

类作用域意味着不能从外部直接访问类的成员,公有函数也是如此,也就是说,要调用共有函数,必须通过对象。在类声明或成员函数定义中,才可以使用未修饰的成员名称,构造函数名称在被调用时,才能被识别,因为它的名称与类名相同。在其他情况下,使用类成员名,必须根据上下文使用直接成员运算符、间接成员运算符或作用域解析运算符。

1.作用域为类的常量

有时候,使符号常量的作用域为类很有用。该常量对于所有对象来说都是相同的,因此创建一个由所有对象共享的常量是一个很正常的想法。

但是如果直接在类private中定义const常量是行不通的,因为声明类只是描述了对象的形式,并没有创建对象,因此在创建对象前,将没有用于存储值的空间。有两种方式可以实现这个目标:

1.类中声明枚举

private:
	enum {Months = 12};
	double cost[Months];

注:这种方式声明枚举并不会创建类数据成员,只是编译器在遇到Months时,会用12来替换。由于这里使用枚举量只是为了创建符号常量,并不打算创建没哭类型变量,因此不需要提供枚举名。

2.另一种方式时使用关键字——static

	static const int Months = 12;
	double cost[Months];

这将创建一个名为Months的常量,该常量与其他静态变量存储在一起,而不是存储在对象里,只有一个Months常量,被所有对象共享。

2.作用域内枚举

传统的枚举,两个枚举定义中枚举量可能发生冲突,为了避免这种问题,C++11提供了一种新枚举,其枚举量的作用域为类。枚举的声明类如下:

        enum class egg {small,medium,large,jumbo};
		enum struct t_shirt {small,medium,large,jumbo};

		egg choice = egg::large;
		t_shirt floyd = t_shirt::large;

也可使用关键字 struct代替class,无论哪种方式,都需要使用枚举名来限定枚举量(不然没法访问)。

C++11还提高了作用域内枚举的类型安全,在有些情况下,常规枚举将自动转换为整型,如将其赋给int变量或用于表达式时,但作用域内枚举不能隐式地转换为整型,在必要时可进行显式类型转换:

enum  egg_old {small,medium,large,jumbo};
		enum struct t_shirt {small,medium,large,jumbo};
		egg_old one = small;
		t_shirt rolf = t_shirt::small;
		int king = one;    //允许
		int ring = rolf;   //不允许
		int ring = (int)rolf;   //允许

枚举用某种底层整型类型表示,C++11作用域内枚举的底层类型为int.

⑦抽象数据类型

stock类非常具体。然而,程序员常常通过定义类来标识更通用的概念,对于实现抽象数据类型(ADT),使用类是一种非常好的方式。ADT以通用的方式描述数据类型,而没有引入语言或实现细节。例如通过栈来储存或删除数据。

栈的操作有  创建空栈 压入 弹出 看栈是否填满 栈是否为空。

可以将上述描述转换为一个类声明,其公有成员函数提供了表示栈操作的接口,私有数据成员负责存储栈数据。类概念非常适合于ADT方法。私有部分必须标明数据存储的方式,可以使用常规数组、动态分配数组或更高级的数据结构。然而,公有接口应隐藏数据表示,而以通用的术语来表达,如创建栈,压入等。

示例:

头文件:

#ifndef STOCK00_H_
#define STOCK00_H_

typedef unsigned long Item;
class Stack
{
private:
	enum{MAX=10};
	Item items[MAX];
	int top;
public:
	Stack();
	bool isempty() const;
	bool isfull() const;
	bool push(const Item& item);
	bool pop(Item& item);
};
#endif

私有部分表明,栈是使用数组实现的;公有部分隐藏了这一点 。因此,可以使用动态数组来代替数组,而不会改变类的接口。

接口是冗余的,因为pop()和push()返回有关栈状态的信息,而不是void类型。

这个类不是根据特定的类型来定义栈,而是根据通用的Item类型来描述。如果需要double栈或者结构栈,只需要修改typedef语句。类模板提供更强大的办法(ch14).

成员函数定义:

#include
#include"stock00.h"

Stack::Stack()          //创建空栈
{
	top = 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 return false;
}

默认构造函数确保所有栈被创建时都为空。

源文件:

#include
#include"stock00.h"

int main()
{
	using namespace std;
	Stack st;
	char ch;
	unsigned long po;

	cout << "Please enter A to add a purchase order,\n"
		<< "p to process a PO,or Q to quit.\n";
	while (cin >> ch && toupper(ch) != 'Q')
	{
		while (cin.get() != '\n')
			continue;
		if (!isalpha(ch))
		{
			cout << '\a';
			continue;
		}
		switch (ch)
		{
		case 'A':
		case 'a':cout << "Enter a PO number to add:";
			cin >> po;
			if (st.isfull())
				cout << "stack  already full\n";
			else
				st.push(po);
			break;
		case 'p':
		case 'P':if (st.isempty())
			cout << "stack already empty\n";
				else
		        {
			         st.pop(po);
			         cout << "PO #" << po << " popped\n";
		        }
				break;
	
		}
		cout << "Please enter A to add a purchase order,\n"
			<< "p to process a PO,or Q to quit.\n";

	}
	cout << "Bye!\n";
	return 0;
}

你可能感兴趣的:(C++学习记录,c++,开发语言)