C++基础教程面向对象(学习笔记(26))

重载下标运算符

使用数组时,我们通常使用下标运算符([])来索引数组的特定元素:

myArray[0] = 7; // 将值7放在数组的第一个元素中

但是,请考虑以下IntList类,该类具有作为数组的成员变量:

class IntList
{
private:
    int m_list[10];
};
 
int main()
{
    IntList list;
    // 从m_list我们如何访问元素?
    return 0;
}

因为m_list成员变量是private,所以我们无法直接从变量列表中访问它。这意味着我们无法直接获取或设置m_list数组中的值。那么我们如何获取或将元素放入list中呢?

没有运算符重载,典型的方法是创建访问函数:

class IntList
{
private:
    int m_list[10];
 
public:
    void setItem(int index, int value) { m_list[index] = value; }
    int getItem(int index) { return m_list[index]; }
};

虽然这有效,但它并不是特别方便用户。请考虑以下示例:

int main()
{
    IntList list;
    list.setItem(2, 3);
 
    return 0;
}

我们是将元素2设置为值3,还是将元素3设置为值2呢?没有看到setItem()的定义,所以就不是很清楚了。

您也可以只返回整个列表并使用operator []访问该元素:

class IntList
{
private:
    int m_list[10];
 
public:
    int* getList() { return m_list; }
};

虽然这也有效,但它在语法上是很奇怪的:

int main()
{
    IntList list;
    list.getList()[2] = 3;
 
    return 0;
}

重载运算符[]

但是,在这种情况下,更好的解决方案是重载下标运算符([])以允许访问m_list的元素。下标运算符是必须作为成员函数重载的运算符之一。重载的operator []函数将始终采用一个参数:用户放置在[]之间的下标。在IntList情况下,我们希望用户传入一个整数索引,然后我们将返回一个整数值作为结果。

class IntList
{
private:
    int m_list[10];
 
public:
    int& operator[] (const int index);
};
 
int& IntList::operator[] (const int index)
{
    return m_list[index];
}

现在,每当我们在类的对象上使用下标运算符([])时,编译器将从m_list成员变量返回相应的元素!这允许我们直接获取和设置m_list的值:

 IntList list;
    list[2] = 3; // 设置一个value
    std::cout << list[2]; // 获得一个value


return 0;

从理解的角度来看,这既简单又符合语法。当list[2]求值,编译器首先检查是否有一个重载operator[]函数。如果是这样,它将[]内的值(在本例中为2)作为函数的参数传递。

注意,虽然您可以为函数参数提供默认值,但实际上使用不带内标的operator []不被视为有效语法,因此没有意义。

为什么operator []返回引用

让我们仔细看看如何list[2] = 3调用。由于下标运算符的优先级高于赋值运算符,因此list[2]首先求值。 list[2]调用operator [],我们已经定义了返回引用list.m_list[2]。因为operator []正在返回引用,所以它返回实际的list.m_list[2]数组元素。我们的部分求值表达式变为list.m_list[2] = 3,这是一个简单的整数赋值。

在本课程中首先看一下变量,您了解到赋值语句左侧的任何值都必须是地址值(这是一个具有实际内存地址的变量)。因为operator []的结果可以在赋值的左侧使用(例如list[2] = 3),所以operator []的返回值必须是地址值。事实证明,引用始终是地址值,因为您只能引用具有内存地址的变量。因此,通过返回引用,编译器明白我们返回一个地址值。

考虑如果operator []按值而不是按引用返回整数会发生什么。 list[2]会调用operator [],它将返回list.m_list [2] 的值。例如,如果m_list [2]的值为6,则operator []将返回值6. list[2] = 3将部分调用为6 = 3,这没有意义!如果您尝试这样做,C ++编译器会报错:

C:VCProjectsTest.cpp(386):错误C2106:’=’:左操作数必须是地址值
处理const对象

在上面的IntList示例中,operator []是非const的,我们可以将它用作地址值来更改非const对象的状态。但是,如果我们的IntList对象是const呢?在这种情况下,我们将无法调用operator []的非const版本,因为这将允许我们可能更改const对象的状态。

好消息是我们可以分别定义operator []的非const和const版本。非const版本将与非const对象一起使用,而const版本将与const-objects一起使用。

class IntList
{
private:
    int m_list[10] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; // 用一些简单的数据初始化这个数组
 
public:
    int& operator[] (const int index);
    const int& operator[] (const int index) const;
};
 
int& IntList::operator[] (const int index) // 对于非const对象:可用于赋值
{
    return m_list[index];
}
 
const int& IntList::operator[] (const int index) const //对于const对象: 仅仅能访问
{
    return m_list[index];
}
 
int main()
{
    IntList list;
    list[2] = 3; // 调用非const版本 operator[]
    std::cout << list[2];
 
    const IntList clist;
    clist[2] = 3; // 编译错误:调用operator []的const版本,它返回一个const引用。无法分配给此。
    std::cout << clist[2];
 
    return 0;
}

如果我们注释掉该行clist[2] = 3,则上述程序将按预期编译并执行。

错误检查

重载下标运算符的另一个好处是我们可以使它比直接访问数组更安全。通常,在访问数组时,下标运算符不会检查索引是否有效。例如,编译器不会报错以下代码:

int list[5];
list[7] = 3; //索引7超出了边界!

但是,如果我们知道数组的大小,我们可以使用重载的下标运算符检查以确保索引在边界内:

#include <cassert> // for assert()
 
class IntList
{
private:
    int m_list[10];
 
public:
    int& operator[] (const int index);
};
 
int& IntList::operator[] (const int index)
{
    assert(index >= 0 && index < 10);
 
    return m_list[index];
}

在上面的例子中,我们使用assert()函数(包含在cassert头中)来确保我们的索引有效。如果assert中的表达式求值为false(这意味着用户传入了无效索引),程序将以错误消息终止,这比替代(破坏内存)要好得多。这可能是进行此类错误检查的最常用方法。

指向对象和重载运算符[]的指针不混合

如果您尝试在指向对象的指针上调用operator [],C ++将假定您正在尝试索引该类型的对象数组。

请考虑以下示例:

#include <cassert> // assert()
 
class IntList
{
private:
    int m_list[10];
 
public:
    int& operator[] (const int index);
};
 
int& IntList::operator[] (const int index)
{
    assert(index >= 0 && index < 10);
 
    return m_list[index];
}
 
int main()
{
    IntList *list = new IntList;
    list [2] = 3; // 错误:这将假设我们正在访问下标为2的IntLists数组
    delete list;
 
    return 0;
}

因为我们不能将整数分配给IntList,所以不会编译。但是,如果赋值整数有效,则会编译并运行,但结果不确定。

规则:确保您没有尝试在指向对象的指针上调用重载的operator []。

正确的语法是首先取消引用指针(确保使用括号,因为operator []的优先级高于operator *),然后调用operator []:

int main()
{
    IntList *list = new IntList;
    (*list)[2] = 3; // 获取我们的IntList对象,然后调用重载的operator []
    delete list;
 
    return 0;
}

这很不好理解且容易出错。更好的是,如果不需要,请不要设置指向对象的指针。

函数参数不需要是整数

如上所述,C ++将用户在[]之间键入的内容作为参数传递给重载函数。在大多数情况下,这将是一个整数值。但是,这不是必需的 - 事实上,您可以定义重载的运算符[]获得你想要的任何类型的值。你可以定义你的重载operator []来获取double,std :: string或者你喜欢的任何其他类型。

作为一个有趣的例子,你可以看到它的工作原理:

#include <iostream>
#include <string>
 
class Stupid
{
private:
 
public:
	void operator[] (std::string index);
};
 
// 重载operator []来打印某些东西是没有意义的
// 但是这是最容易的显示非int函数参数的方式
void Stupid::operator[] (std::string index)
{
	std::cout << index;
}
 
int main()
{
	Stupid stupid;
	stupid["Hello, world!"];
 
	return 0;
}

正如您所料,这打印:
Hello, world!
在编写某些类(例如使用单词作为索引的类)时,重载operator []以获取std :: string参数非常有用。
Conclusion:
下标运算符通常被重载以提供对类中包含的数组(或其他类似结构)的单个元素的直接访问。因为字符串通常实现为字符数组,所以operator []通常在字符串类中实现,以允许用户访问字符串的单个字符。

Quiz Time:

1)map是一个将元素存储为key-value的类。key必须是唯一的,并用于访问关联的对。在这个测验中,我们将编写一个应用程序,让我们使用简单的map类按名称为学生分配成绩。学生的名字将成为关键,而成绩(作为重点点)将是value。

1a)首先,编写一个名为StudentGrade的结构,其中包含学生的名字(作为std :: string)和成绩(作为char)。

解决方案

#include <string>
struct StudentGrade
{
    std::string name;
    char grade;
};

1b)添加一个名为GradeMap的类,其中包含名为m_map的StudentGrade的std :: vector。添加一个什么都不做的默认构造函数。

解决方案

#include <string>
#include <vector>
 
struct StudentGrade
{
	std::string name;
	char grade;
};
 
class GradeMap
{
private:
	std::vector<StudentGrade> m_map;
 
public:
	GradeMap()
	{
	}
};

1c)为这个类写一个重载的operator []。此函数应采用std :: string参数,并返回对char的引用。在函数体中,首先查询vector,看看学生的名字是否已经存在(你可以使用for-each循环)。如果学生存在,请返回成绩的参考,然后您就完成了。否则,使用std :: vector :: push_back()函数为这个新学生添加StudentGrade。执行此操作时,std :: vector将向您自己添加StudentGrade的副本(如果需要,可以调整大小)。最后,我们需要返回对刚刚添加到std :: vector的学生的成绩的引用。我们可以使用std :: vector :: back()函数访问我们刚刚添加的学生。

应运行以下程序:

#include <iostream>
 
int main()
{
	GradeMap grades;
	grades["Joe"] = 'A';
	grades["Frank"] = 'B';
	std::cout << "Joe has a grade of " << grades["Joe"] << '\n';
	std::cout << "Frank has a grade of " << grades["Frank"] << '\n';
 
	return 0;
}

解决方案

#include <iostream>
#include <string>
#include <vector>
 
struct StudentGrade
{
	std::string name;
	char grade;
};
 
class GradeMap
{
private:
	std::vector<StudentGrade> m_map;
 
public:
	GradeMap()
	{
	}
 
	char& operator[](const std::string &name);
};
 
char& GradeMap::operator[](const std::string &name)
{
	// 看看我们是否可以在vector中找到name
	for (auto &ref : m_map)
	{
		//如果我们在vector中找到了name,直接返回grade
		if (ref.name == name)
			return ref.grade;
	}
 
	// 否则为这个学生创建一个新的StudentGrade
	StudentGrade temp { name, ' ' };
 
	//否则添加到vector的末尾
	m_map.push_back(temp);
 
	// 并且返回这个元素
	return m_map.back().grade;
}
 
int main()
{
	GradeMap grades;
	grades["Joe"] = 'A';
	grades["Frank"] = 'B';
	std::cout << "Joe has a grade of " << grades["Joe"] << '\n';
	std::cout << "Frank has a grade of " << grades["Frank"] << '\n';
 
	return 0;
}

2)优化#1:我们编写的GradeMap类和示例程序效率低下有很多原因。描述可以改进GradeMap类的一种方法。

解决方案

std :: vector本质上是未分类的。这意味着每次调用operator []时,我们都可能遍历整个std :: vector来查找我们的元素。有一些元素,这不是问题,但随着我们继续添加名称,这将变得越来越慢。我们可以通过保持m_map排序并使用二进制搜索来优化这一点,因此我们最小化了我们必须查看的元素数量,以找到我们感兴趣的元素。
3)优化#2:为什么这个类没有按预期工作?

#include <iostream>
 
int main()
{
	GradeMap grades;
 
	char& gradeJoe = grades["Joe"]; //调用 push_back
	gradeJoe = 'A';
 
	char& gradeFrank = grades["Frank"]; // 调用 push_back
	gradeFrank = 'B';
 
	std::cout << "Joe has a grade of " << gradeJoe << '\n';
	std::cout << "Frank has a grade of " << gradeFrank << '\n';
 
	return 0;
}

解决方案

当添加Frank时,std :: vector必须增长才能保持它。这需要动态分配新的内存块,将数组中的元素复制到该新块,并删除旧块。发生这种情况时,对std :: vector中现有元素的任何引用都会失效!换句话说,在我们push_back(“Frank”)之后,gradeJoe被留下作为对已删除内存的悬空引用。这将导致不确定的结果。

猜你喜欢

转载自blog.csdn.net/qq_41879485/article/details/83213511
今日推荐