C++学习之类的this指针、对象数组、作用域、ADT

this指针

对于Stock类,还有很多工作要做。到目前为止,每种类成员函数都最多只涉及一个对象,即调用它的对象。但显然有时候方法可能不只涉及到一个对象,在这种情况下需要用到C++的this指针。

例如,Stock类虽然能显示各种数据,但缺乏分析能力,如果我们要设计一个输出哪一只股票的价格最高,由于程序无法直接访问total_val,因此无法做出判断。我们设计两个函数,一个函数查看total_val的值,另一个函数比较两个对象的total_val的值。

1
double total()const { return total_val; }

另一个函数,我们定义一个成员函数,它查看两个Stock类对象,并返回股价较高的那个对象的引用。

由此我们可以引出以下两个问题来讨论:

  1. 如何将两个对象提供给成员函数呢?假设这个方法命名为topval(),则stock1.topval()将访问stock1的数据,则必须将第二个对象作为参数传递给它。出于效率方面的考虑,我们就可以按引用来传递参数。
  2. 如何返回答案呢?最直接的方案就是返回那个引用。因此可以这么写原型:
    1
    const Stock & topval(const Stock &s)const;

好的,假设实现完了topval细节,如何调用呢?很明显下面两个语句都是可行的:

1
2
top = stock1.topval(stock2);//隐式访问stock1,显示访问stock2
top = stock2.topval(stock1);//隐式访问stock2,显示访问stock1

这表示法也挺反人类的,但不要紧,我们这里就要着重介绍this指针的用法。

1
2
3
4
5
6
7
const Stock & topval(const Stock &s)const
{
if(s.total_val>total_val)
return s;
else
return ???????;
}

s.total_va是参数的成员,total_val是调用这个方法的对象的成员。问题在于在else分支内,stock1是没有别名的,如何称呼这个调用这个方法的对象自己呢?

C++解决这种问题的方法就是使用名称为this的特殊指针。this指针是指向用来调用成员函数的对象。这样,函数调用stock1.topval(stock2);this指针就是stock1对象的地址。一般来说,所以的类方法都将this指针设置为调用它的对象的地址。

1
2
3
4
5
6
7
const Stock & topval(const Stock &s)const
{
if(s.total_val>total_val)
return s;
else
return *this;
}

因为是返回的是整个引用,this只是地址,记得加上*运算符。

对象数组

类似于结构体数组,可以创建同一个类的多个对象,就是使用创建对象数组。

1
Stock a[3];

数组里的每一个元素都是对象。都可以使用类方法。

1
2
3
a[0].update();
a[1].show();
const Stock * tops = a[2].topval(a[1]);

使用构造函数初始化数组元素。如果这样做,必须为每个元素调用构造函数

1
2
3
4
5
Stock a[3] = {
Stock("a",1,2);
Stock("b",3,4);
Stock("c",5,6);
}

如果有多种构造函数,可以对不同的元素使用不同的构造函数,没有调用构造函数的将使用默认构造函数。
1
2
3
4
5
Stock b[10] = {
Stock("d",7,8);
Stock();
Stock("e",9,10);
}

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

作用域

在类定义的名称(成员名、成员函数名)的作用域都为整个类。

作用域在整个类的名称,只在该类中是可知的,在类外是不可知的。因此可以在不同类中使用相同类成员名而不会引起冲突。另外,类作用于以为着不能从外部直接访问类成员,公有成员函数也是如此。也就是说,要调用公有成员函数,必须通过对象。

作用域为类的常量

有时候,使符号常量的作用域为类很有用。例如,类声明用字面值30来指定数组的长度,由于该常量对所有对象来说都是相同的,因此创建一个所有对象共享的常量是个不错的主意。

错误的代码:

1
2
3
4
5
6
7
class Bakery
{
private:
const int Months = 12;//wrong
double costs[Months];
...
};

这是行不通的,因为声明类值描述对象形式,并没有创建对象。因此,将没有储存这个值的空间。然而,有两种方法可以实现这个目标,而且效果相同。

方法1

类中声明枚举。

1
2
3
4
5
6
class Bakery
{
private:
enum {Months = 12};
double costs[Months];
};

在类声明中的枚举的作用域为整个类,因此可以用枚举为整型常量提供类作用域的符号名称。而且这种方式声明枚举并不会创建类数据成员,也就是说,所以对象其实都不包含枚举,Months也只是一个符号名称,在类的代码里遇到Months编译器都是会用12来代替。

方法2

C++提供了另一种在类中定义常量的方式——使用关键字static。

1
2
3
4
5
6
7
8
class Bakery
{
private:
static const int Months;
double costs[Months];
};

const int Bakery::Months = 12;

这将创建一个静态变量Months,该常量将于其他静态变量存储在一起,不存储在对象中。因此,这只有一个Months常量,被所有的类对象共享。

ADT —— 抽象数据类型

Stock类非常具体。然而,程序员常常通过定义类来表示很多更通用的概念。例如,就实现抽象数据类型(abstract date type, ADT)而言,使用类就是非常好的方式。类概念非常适合ADT方法,可以用公有成员函数表示ADT操作的接口,公有接口应隐藏数据,而且使用通用的术语来表达,例如栈的压入,弹出等。私有数据成员负责存储ADT数据,需要表明数据的存储方式,可以使用常规数组、动态分配或者更高级的数据结构等。

下面是一个栈的class实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
//stack.h -- class defination for the Stack ADT

typedef unsigned long Item;

class Stack
{
private:
enum { MAX = 100 };
Item items[MAX];
int top;
public:
Stack();
bool isempty() const;
bool isfull() const;
bool push(const Item & item);
bool pop(Item & item);
};

//stack.cpp -- Stack member functions
//#include "stack.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;
}

Stock 设计改进

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
#include<string>

class Stock //class declaration
{
private:
std::string company;
long shares;
double share_val;
double total_val;
void set_tot() { total_val = shares*share_val; }
public:
//two constructors
Stock(); //default constructor
Stock(const std::string & co, long n = 0, double pr = 0.0);
~Stock(); //noisy destructor

//delete function acquire

void buy(long num, double price);
void sell(long num, double price);
void update(double price);
void show();
};

#include<iostream>

Stock::Stock() //default constructor
{
std::cout << "Default constructor called\n";
company = "no name";
shares = 0;
share_val = 0.0;
total_val = 0.0;
}

Stock::Stock(const std::string & co , long n, double pr)
{
std::cout << "Constructor using " << co << " called\n";

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()
{
std::cout << "Bye, " << company << "!\n";
}

//other methods
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)
{
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()
{
using std::cout;
using std::ios_base;
//set format to #.###
ios_base::fmtflags orig =
cout.setf(ios_base::fixed, ios_base::floatfield);
std::streamsize prec = cout.precision(3);

cout << "Company: " << company
<< " Shares: " << shares << '\n';
cout << " Shares Price : $" << share_val;
//set format to #.##
cout.precision(2);
cout << " Total Worth: $" << total_val << '\n';

//restore original format
cout.setf(orig, ios_base::floatfield);
cout.precision(prec);
}

int main()
{
{
using std::cout;
cout << "Using constructors to create new objects\n";
Stock stock1("Future Technology", 12, 20.0); //syntax 1
stock1.show();
Stock stock2 = Stock("Sun Prison", 2, 2.0); //syntax 2
stock2.show();
cout << '\n';

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

cout << "Using a constructor to create an object\n";
stock1 = Stock("Sun Empire", 10, 50.0);
cout << "Revised stock1:\n";
stock1.show();
cout << "\nDone\n\n";
}
return 0;
//to see how destructor works
}

小结

  1. 面对对象编程强调的是程序如何表示数据。使用OOP方法解决问题的第一步是根据它与程序间的接口来描述数据。
  2. 公有成员函数,又称为方法,提供访问数据的途径。
  3. 类将数据和方法组合成一个单元,其私有性实现数据隐藏。
  4. 通常将类声明放在头文件中、定义成员函数的源代码放在方法文件中,便将接口描述与实现细节分开了。从理论上说,只需知道公有接口就可以使用类。
  5. 类是用户定义的类型,对象是类的实例。如果提供了构造函数,则在创建对象时可以初始化对象。如果提供了析构函数,则对象消亡时将执行析构函数。
  6. 如果希望成员函数对多个对象进行操作,则可以使用this指针。*this是这个对象的别名。
  7. 类很适合用来描述ADT。