前面对SpreadsheetCell类的定义足以让你生成类的对象。然而,如果想调用setValue()或者getValue()成员函数,连接器就会抱怨这些函数没有定义。这是因为到目前为止,这些成员函数只有原型,而还没有实现。通常,类的定义会在模块接口文件。对于成员函数的定义,你有一个选择:可以在模块定义文件或者在模块实现文件。
下面是SpreadsheetCell类,在类内对成员函数进行了实现:
export module spreadsheet_cell;
export class SpreadsheetCell
{
public:
void setValue(double value) { m_value = value; }
double getValue() const { return m_value; }
private:
double m_value{ 0 };
};
与头文件不同,c++模块中把成员函数定义放在模块接口文件中并没有什么不好。这个我们以后再讨论。然而,我们还是通常把成员函数定义放在模块实现文件中,是为了保证模块接口干净,不需要实现细节。
模块实现的第一行指出该实现是哪个模块。下面是spreadsheet_cell模块中SpreadsheetCell类的两个成员函数的定义:
module spreadsheet_cell;
void SpreadsheetCell::setValue(double value)
{
m_value = value;
}
double SpreadsheetCell::getValue() const
{
return m_value;
}
注意类名之后两个冒号后跟着的是每个成员函数的名字:
void SpreadsheetCell::setValue(double value)
::被叫做范围解析操作符。具体内容就是,语法告诉编译器接下来对setValue()成员函数的定义是SpreadsheetCell类的一部分。也要注意,当你定义成员函数时,不需要重复访问说明。
对于像setValue()与getValue()这样类的非静态成员函数总是哇规定类的特定对象执行。在成员函数体内,拥有所有对象的类的数据成员的访问权限。对于前面对setValue()的定义,下面这行代码不管什么样的对象调用了成员函数都会改变m_value变量的值:
m_value = value;
如果setValue()被两个不同的对象调用,同样的代码行(每个对象执行一次)在两个不同的对象中改变了变量值。
可以在另外一个成员函数中调用一个类的成员函数。例如,考虑对SpreadsheetCell类进行扩展,允许使用字符串或者数字来设置和访问格子的值。当你尝试将格子的值用字符串来设置时,格子尝试将字符串转化为数字。如果字符串无法转化为一个合法的数字,格子值会忽略。在程序中,无法转化为数字的字符串会为格子生成一个0值。下面是对SpreadsheetCell的类定义:
export module spreadsheet_cell;
import std;
export class SpreadsheetCell
{
public:
void setValue(double value);
double getValue() const;
void setString(std::string_view value);
std::string getString() const;
private:
std::string doubleToString(double value) const;
double stringToDouble(std::string_view value) const;
double m_value{ 0 };
};
这一版的类只保存了一个double数据。如果客户将数据设置为字符串,会将其转化为double。如果字符串不是合法的数字,double值会被设置为0。类定义显示了两个新的成员函数来设置与访问格子中的文本表达,两个新的内部的辅助成员函数将double转化为字符串以及反向转化。下面是对所有这些成员函数的实现:
module spreadsheet_cell;
import std;
using namespace std;
void SpreadsheetCell::setValue(double value)
{
m_value = value;
}
double SpreadsheetCell::getValue() const
{
return m_value;
}
void SpreadsheetCell::setString(string_view value)
{
m_value = stringToDouble(value);
}
string SpreadsheetCell::getString() const
{
return doubleToString(m_value);
}
string SpreadsheetCell::doubleToString(double value) const
{
return to_string(value);
}
double SpreadsheetCell::stringToDouble(string_view value) const
{
double number{ 0 };
from_chars(value.data(), value.data() + value.size(), number);
return number;
}
std::to_string()与from_chars()函数我们以前解释过,这里就不过多阐述了。
注意这里对doubleToString()成员函数的实现,例如6.1会被转化成6.100000。然而,因为它是一个内部辅助成员函数,可以不必修改任何客户端代码的情况下自由修改其实现。
前面对类SpreadsheetCell的定义,包含了一个数据成员,四个公共的成员函数,两个内部成员函数。然而,类定义没有产生任何实际的SpreadsheetCell;它只是指出其形状与行为。在这个层面,一个类就像一个架构蓝图。蓝图指出房子应该盖成什么样,但是画出蓝图并没有建造任何房子。房子一定要基于蓝图之后建造。
同样的,在C++中,可以通过声明一个SpreadsheetCell类型的变量从SpreadsheetCell类定义中构造一个SpreadsheetCell对象。正像建筑工人可以基于给定的蓝图建造多个房子,程序员也可以从SpreadsheetCell类生成多个SpreadsheetCell对象。有两种生成和使用对象的方式:在栈上和在自由分配空间上。
下面是一些栈上生成和使用SpreadsheetCell对象的例子:
import spreadsheet_cell;
import std;
using namespace std;
int main()
{
SpreadsheetCell myCell, anotherCell;
myCell.setValue(6);
anotherCell.setString("3.2");
println("cell 1: {}", myCell.getValue());
println("cell 2: {}", anotherCell.getValue());
println("cell 1: {}", myCell.getString());
println("cell 2: {}", anotherCell.getString());
}
就像声明简单变量一样生成对象,除了变量类型就是类名字之外。myCell.setValue(6)代码行中的.被叫做“点”操作符,也叫做成员访问操作符;允许调用对象的公共成员函数。任何类中的公共的数据成员,都可以使用点操作符来访问。记住我们并不推荐将成员变量设置为可以公共访问。
程序的输出如下:
cell 1: 6
cell 2: 3.2
cell 1: 6.000000
cell 2: 3.200000
也可以使用new来动态分配对象:
import spreadsheet_cell;
import std;
using namespace std;
int main()
{
SpreadsheetCell* myCellp{ new SpreadsheetCell{ } };
myCellp->setValue(3.7);
println("cell 1: {} {}", myCellp->getValue(), myCellp->getString());
delete myCellp;
myCellp = nullptr;
}
当在自由分配空间上生成一个对象时,可以通过“箭头”操作符->来访问成员。箭头包含了间接引用(*)和成员访问(.)。也我可能要用这两个操作符,但是这样做的话从风格上来看就有点尴尬:
import spreadsheet_cell;
import std;
using namespace std;
int main()
{
SpreadsheetCell* myCellp{ new SpreadsheetCell{ } };
(*myCellp).setValue(3.7);
println("cell 1: {} {}", (*myCellp).getValue(), (*myCellp).getString());
delete myCellp;
myCellp = nullptr;
}
就像你要释放分配在自由分配空间上其他内存一样,也必须通过调用delete来释放分配在自由分配空间上的对象的内存,就像上面的代码那样!为了保证安全,避免问题,你真的应该使用智能指针,举例如下:
import spreadsheet_cell;
import std;
using namespace std;
int main()
{
auto myCellp{ make_unique() };
// Equivalent to:
// unique_ptr myCellp{ new SpreadsheetCell{ } };
myCellp->setValue(3.7);
println("cell 1: {} {}", myCellp->getValue(), myCellp->getString());
}
使用智能指针就不需要手动释放内存了,它会自动释放。
当使用new分配对象空间时,使用完毕之后要用delete进行释放,或者,最好使用智能指针来自动管理内存!
如果不使用智能指针的话,将删除其指向的对象之后设置为nullptr不失为一个好主意。我们并不要求你这么做,如果在删除之后指针被意外使用到的话,也会很容易排错。