C++学习笔记----8、掌握类与对象(一)---- 对象中的动态内存分配(1)
1、FRIENDS
c++允许类声明为其它类,其它类的成员函数,或者非成员函数为friend。可以访问protected与private数据成员与成员函数。例如,假设你有两个类Foo与Bar。你可以指定Bar类是Foo类的一个friend:
class Foo
{
friend class Bar;
// ...
};
这样Bar类的所有成员函数可以访问Foo类的private与protected数据成员与成员函数。
如果你只是想让Bar类的一个特定成员函数做friend,也可以这样做。假设Bar类有一个成员函数processFoo(const Foo&)。下面的语法用于让这个成员函数成为Foo的一个friend:
class Foo
{
friend void Bar::processFoo(const Foo&);
// ...
};
独立的函数也可以成为类的friend。举例来说,你可能想要写一个函数来打印Foo对象的所有数据到控制台。可能想要让该函数在Foo类之外,因为打印不是Foo的核心功能,但是该函数需要访问对象的内部数据成员以便进行打印。下面是Foo类的定义,以printFoo()作为friend:
class Foo
{
friend void printFoo(const Foo&);
// ...
};
在类中的friend声明作为函数的原型。没有必要在其它地方再写原型了(写了也没什么坏处)。
下面是函数的定义:
void printFoo(const Foo& foo)
{
// Print all data of foo to the console, including
// private and protected data members.
}
在类定义之外写这样的函数与其它任何函数一样,除了可以直接访问Foo的private与protected成员。在函数定义时不必重复friend关键字。
注意类需要知道其它哪些类,成员函数,或者函数想要成为它的friend;一个类,成员函数,或者函数不能声明自己为一些其它类的friend来获得那个类的非public成员的访问。
friend类与函数容易被滥用;它们允许你破坏通过暴露类内部给其他类或函数来进行包装的原则。这样的话,只能在有限的情况下使用它们。我们会在博文中进行使用场景的展示。
2、对象中的动态内存分配
有时候你不知道在程序真正运行之前需要多大内存。解决方案是在程序执行时动态分配需要的内存。类也不例外。有时候你不知道在写类时一个对象需要多大内存。在这种情况下,对象应该动态分配内存。给对象动态分配内存带来了一些挑战,包括释放内存,处理对象拷贝,以及处理对象赋值。
2.1、Spreadsheet类
我们前面介绍过SpreadsheetCell类。现在我们继续写Spreadsheet类。与SpreadsheetCell类一样,Spreadsheet类也在我们的文章中不断演化。这样的话,多种尝试并不总是去演示类书写的每个方面的最好的方式。
我们先开始,Spreadsheet只是一个简单的SpreadsheetCell的二维数组,在Spreadsheet中带有成员函数来设置与访问其特定位置的cell。虽然大部分spreadsheet应用在一个方向上使用字母,在另一个方向上使用数字来指向cell,我们的Spreadsheet在两个方向上都使用数字。
Spreadsheet.cppm模块接口文件的第一行定义了模块的名字:
export module spreadsheet;
Spreadsheet类需要访问SpreadsheetCell类,所以它需要import spreadsheet_cell模块。另外,为了使SpreadsheetCell类对spreadsheet模块中的用户可见,spreadsheet_cell模块需要用下面的看起来很可笑的语法来进行导入导出:
export import spreadsheet_cell;
Spreadsheet类使用std::size_t类型,它定义在C头文件中<cstddef>。可以用下面的导入来获得访问权限:
import std;
最终,下面是Spreadsheet类的定义的第一次尝试:
export class Spreadsheet
{
public:
Spreadsheet(std::size_t width, std::size_t height);
void setCellAt(std::size_t x, std::size_t y, const SpreadsheetCell& cell);
SpreadsheetCell& getCellAt(std::size_t x, std::size_t y);
private:
bool inRange(std::size_t value, std::size_t upper) const;
std::size_t m_width{ 0 };
std::size_t m_height{ 0 };
SpreadsheetCell** m_cells{ nullptr };
};
注意:Spreadsheet类使用指向m_cells数组的正常指针。这样做的目的是为了展示结果以及解释怎么处理资源,比如类中的动态内存。在生产环境的代码中,应该使用标准c++容器,像std::vector,会大幅简化Spreadsheet的实现,但那样的话,你就无法学习如何使用原始指针正确处理动态内存。在现代c++中,永远不要使用带有属主语法的原始指针,但是对于既有代码你可能会碰到,这种情况下需要知道如何处理。
注意Spreadsheet类并不包含标准的SpreadsheetCell二维数组。实际上,它包含了一个SpreadsheetCell**的数据成员,是一个指向代表了一个数组的数组的指针。这是因为每一个Spreadsheet对象可能有不同的维度,所以类的构造函数需要动态分配二维数组,基于客户指定的高度与宽度。
为了动态分配一个二维数组,需要写下面的代码。记住在c++中,不像Java,不能只是简单地写new SpreadsheetCell[m_width][m_height]。
Spreadsheet::Spreadsheet(size_t width, size_t height)
: m_width { width }
, m_height { height }
{
m_cells = new SpreadsheetCell*[m_width];
for (size_t i{ 0 }; i < m_width; ++i) {
m_cells[i] = new SpreadsheetCell[m_height];
}
}
下图展示了叫做s1的Spreadsheet的结果内存结构,在栈上,宽度为4,高度为3。
inRange()的实现,设置与访问成员函数就很直接了:
bool Spreadsheet::inRange(size_t value, size_t upper) const
{
return value < upper;
}
void Spreadsheet::setCellAt(size_t x, size_t y, const SpreadsheetCell& cell)
{
if (!inRange(x, m_width)) {
throw out_of_range { format("x ({}) must be less than width ({}).", x, m_width) };
}
if (!inRange(y, m_height)) {
throw out_of_range { format("y ({}) must be less than height ({}).", y, m_height) };
}
m_cells[x][y] = cell;
}
SpreadsheetCell& Spreadsheet::getCellAt(size_t x, size_t y)
{
if (!inRange(x, m_width)) {
throw out_of_range { format("x ({}) must be less than width ({}).", x, m_width) };
}
if (!inRange(y, m_height)) {
throw out_of_range { format("y ({}) must be less than height ({}).", y, m_height) };
}
return m_cells[x][y];
}
setCellAt()与getCellAt()两者都使用了类的辅助函数inRange()来检查x与y,它们代表了spreadsheet中的有效坐标。尝试访问越界索引的数组元素会导致程序运行不正确。本例使用了例外,以后会详细介绍。
如果你仔细看setCellAt()与getCellAt()的实现,可以看到有很清晰的代码重复。我们以前说过要尽量避免代码重复。所以,让我们遵从指导,不使用inRange()的辅助函数,定义一个verifyCoordinate()的成员函数:
void verifyCoordinate(std::size_t x, std::size_t y) const;
其实现检查给定的坐标,如果坐标无效的话抛出例外:
void Spreadsheet::verifyCoordinate(size_t x, size_t y) const
{
if (x >= m_width) {
throw out_of_range { format("x ({}) must be less than width ({}).", x, m_width) };
}
if (y >= m_height) {
throw out_of_range { format("y ({}) must be less than height ({}).", y, m_height) };
}
}
这样的话,setCellAt()与getCellAt()就可以简化如下:
void Spreadsheet::setCellAt(size_t x, size_t y, const SpreadsheetCell& cell)
{
verifyCoordinate(x, y);
m_cells[x][y] = cell;
}
SpreadsheetCell& Spreadsheet::getCellAt(size_t x, size_t y)
{
verifyCoordinate(x, y);
return m_cells[x][y];
}