04-c++类和对象(下)
一、友元
前面学习的类中,只能通过该类的公共方法访问私有数据。而如果将某个函数设置为类的友元,那么这个函数就可以直接访问该类的私有数据,破坏了类的封装性,只在某些特定的情况下使用。
友元的分类:普通全局函数作为友元、类的某个成员函数作为友元、整个类作为友元。关键字:friend。
1.全局函数作为类的友元
- 代码演示
class Object
{
friend void useObject(Object &ob);
private:
char cup[32];
public:
char pen[32];
Object(){}
Object(char *cup, char *pen)
{
strcpy(this->cup, cup);
strcpy(this->pen, pen);
}
};
// 定义一个全局函数
void useObject(Object &ob)
{
cout << "小明正在使用" << ob.pen << endl;
cout << "小明正在使用" << ob.cup << endl;
}
void test27()
{
Object xh_obj("小红的水杯", "小红的钢笔");
useObject(xh_obj);
}
- 运行结果
小明正在使用小红的钢笔
小明正在使用小红的水杯
- 说明:未设置友元的时候,全局函数直接访问对象的私有数据会报错,但是设置了友元就可以访问了。将全局函数设置友元,只需要在类的首行加上函数的声明,并在前面加上 friend 关键字即可。
2.类的成员函数作为另外一个类的友元
- 代码演示
// 向前声明Object类
class Object;
// 要将Person类中的成员函数设置为Object的友元,先定义Person类
class Person
{
private:
char name[32];
public:
Person(){}
Person(char *name)
{
strcpy(this->name, name);
}
void useCup(Object &obj);
void usePen(Object &obj);
};
class Object
{
friend void Person::useCup(Object &obj);
private:
char cup[32];
public:
char pen[32];
Object(){}
Object(char *cup, char *pen)
{
strcpy(this->cup, cup);
strcpy(this->pen, pen);
}
};
// 将Person的函数在两个类的下方首先,否则没法识别到Object的数据
void Person::useCup(Object &obj) {
cout << name << "正在使用" << obj.cup << endl;
}
void Person::usePen(Object &obj) {
cout << name << "正在使用" << obj.pen << endl;
}
void test27()
{
Object xh_obj("小红的水杯", "小红的钢笔");
Person xm("小明");
xm.usePen(xh_obj);
xm.useCup(xh_obj);
}
- 运行结果
小明正在使用小红的钢笔
小明正在使用小红的水杯
- 说明:假设将 A 类中的成员函数设置为 B 类的友元,其步骤如下:
- 先定义 A 类,并且 A 类的成员函数在类外(A 和 B 类的下方)实现;
- 向前声明 B 类,在 A 类前面声明 B 类;
- 在 B 类中使用 friend 声明 A 的成员函数为友元。
3.一个类作为另外一个类的友元
- 代码演示
// 其它代码和上面类的成员函数作为另外一个类的友元一样
// 只是不是将单个函数作为友元,而是整个类,只修改一句代码
// 将class Object中的
friend void Person::useCup(Object &obj); // 改为
friend Person;
// 这样,如果Person类中有多个成员函数需要设置为Object的友元
// 不需要一个个函数去 friend ,只需要把整个类 friend 就行
- 总结
- 友元关系不能被继承,继承后面学。即:类A 是父类,类B 是 类A 的朋友,类C 是 类A 的子类,但 类B 不一定是 类C 的朋友;
- 友元关系是单向的。即:类 A 是类 B 的朋 友,但类 B 不一定是类 A 的朋友;
- 友元关系不具有传递性。即:类 B 是类 A 的朋 友,类 C 是类 B 的朋友,但类 C 不一定是类 A 的朋友。
二、数组类
数组类,本质上是一个类,可以通过这个类创建一个个数组对象,实现数组的部分功能。
- 代码演示
class Array
{
private:
// 数组的实际元素个数(数组大小)
int size;
// 数组可容纳的元素个数(数组容量)
int capacity;
// 数组首元素地址
int *array;
public:
// 构造函数
Array();
Array(int capacity);
Array(const Array &obj);
// 析构函数
~Array();
// 数组的功能函数
// 返回数组大小和数组容量
int getSize();
int getCapacity();
// 尾部插入数据
void pushBack(int num);
// 删除尾部数据
void popBack();
// 遍历数组
void printArray();
};
Array::Array() {
size = 0;
// 将容量默认初始化为5
capacity = 5;
// 为数组申请堆区空间
array = new int[capacity];
}
Array::Array(int capacity) {
size = 0;
this->capacity = capacity;
// 为数组申请堆区空间
array = new int[capacity];
}
Array::Array(const Array &obj) {
size = obj.size;
capacity = obj.capacity;
// 为数组申请堆区空间
array = new int[capacity];
// 将旧对象的数据通过内存拷贝拷贝给新对象
memcpy(array, obj.array, sizeof(int) * capacity);
}
Array::~Array() {
// 释放前先判断数组是否为空
if(array != NULL)
{
// 注意释放的是数组,加 []
delete [] array;
array = NULL;
}
}
int Array::getSize() {
return size;
}
int Array::getCapacity() {
return capacity;
}
void Array::pushBack(int num) {
// 插入前先判断数组是否已满
if (size == capacity)
{
// 如果满了就为数组增加一倍的空间
int *tempArray = new int[capacity * 2];
// 将以前的数组数据拷贝给新开辟的数组空间
memcpy(tempArray, array, sizeof(int) * capacity);
capacity = capacity * 2;
// 释放以前的数组
delete [] array;
// 指向新开辟的空间
array = tempArray;
}
// 末尾插入数据
array[size] = num;
size++;
}
void Array::popBack() {
// 先判断数组是否为空
if (NULL == array)
{
cout << "数组为空" << endl;
return;
}
size--;
}
void Array::printArray() {
int i = 0;
for (i = 0; i < size; i++)
{
cout << array[i] << " ";
}
cout << endl;
}
void test28()
{
// 创建一个数组对象
Array array1;
// 给数组添加5个元素
array1.pushBack(11);
array1.pushBack(22);
array1.pushBack(33);
array1.pushBack(44);
array1.pushBack(55);
// 打印数组大小容量
cout << "size = " << array1.getSize() << " capacity = " << array1.getCapacity() << endl;
// 遍历数组
array1.printArray();
// 添加一个元素,打印大小和容量
array1.pushBack(66);
cout << "size = " << array1.getSize() << " capacity = " << array1.getCapacity() << endl;
array1.printArray();
// 删除一个,打印信息
array1.popBack();
cout << "size = " << array1.getSize() << " capacity = " << array1.getCapacity() << endl;
array1.printArray();
}
- 运行结果
size = 5 capacity = 5
11 22 33 44 55
size = 6 capacity = 10
11 22 33 44 55 66
size = 5 capacity = 10
11 22 33 44 55
- 注意:
- 数组类中包含数组的必要数据:数组的的大小,即实际的元素个数,数组的容量,数组首元素的地址;
- 数组初始化,除了初始化容量以外,还得申请相应大小的堆区空间;
- 插入数据的时候,如果数组的大小和容量相等,代表数组已满,要插入需要先追加空间。但追加空间不是在原空间后面追加,而是新申请内存(为原来的内存加新申请的空间的大小总和),然后再将原来空间的数据,拷贝到新空间。不在后面直接追加的原因是,不确定原空间后面的空间是否存储了其它数据,后面添加可能占用非法内存。
三、运算符重载
运算符重载:就是对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型的运算需求。
关键字:operator 比如重载 + 运算符,其函数名 operator+
。
一元运算符:运算符需要的运算对象是一个,如i++ --i
等;二元运算符:运算符需要的运算对象是两个,如+ - *
等。
1.重载运算符的步骤
- 首先必须明确:运算符左边的运算符对象是自定义对象还是其他;
- 如果左边是其他,必须使用全局函数完成运算符重载,且全局函数必须是友元;
- 如果左边是自定义对象,可以使用成员函数或全局函数完成运算符重载,推荐用成员函数实现。如果使用成员函数,可以少一个参数,如果使用全局函数,不可以少参数,且必须把全局函数设置友元。
先大致了解重载运算符的步骤及注意事项,结合下面的案例具体学习。
2.运算符重载应用
2.1全局函数重载 << 运算符
- 代码演示
// 定义一个 Person 类
class Person
{
friend void operator<<(ostream &out, Person &obj);
private:
int num;
// string 需要包含 <string> 头文件
string name;
float score;
public:
Person()
{
num = 000;
name = "admin";
score = 0.0f;
}
Person(int num, string name, float score)
{
this->num = num;
// string 中重载了 = 运算符,因此这里可以直接赋值
this->name = name;
this->score = score;
}
void showPerson()
{
cout << "num = " << num << " name = " << name << " score = " << score << endl;
}
};
// 重载 << 运算符
void operator<<(ostream &out, Person &obj)
{
out << "num = " << obj.num << " name = " << obj.name << " score = " << obj.score << endl;
}
void test29()
{
// 创建一个对象并初始化
Person xm(101, "小明", 88.5);
xm.showPerson();
// 之前我们想要查看对象信息,需要通过公共方法来访问
// 现在可以通过 cout << 对象 直接访问
cout << xm;
Person xh(102, "小红", 85.0);
cout << xh;
}
- 运行结果
num = 101 name = 小明 score = 88.5
num = 101 name = 小明 score = 88.5
num = 102 name = 小红 score = 85
-
说明:
- 上面重载了 << 运算符,首先 << 左边是一个非自定义对象,因此需要通过全局函数完成运算符重载;
- 因为这里是需要通过重载函数操作对象数据的,因此需要将重载函数设置为类的友元;
- 这里重载 << 运算符只是针对当前类创建的对象有效;
operator<<
为重载函数名,<< 左边的作为函数的第一个参数,右边的作为函数的第二个参数;cout << xm
是编译器优化后的函数调用方式,其本质是operator<<(cout, xm)
。
-
还可以完成链式操作,连续输出多个对象:
// 还是上面案例的代码
// 修改函数
ostream& operator<<(ostream &out, Person &obj)
{
out << "num = " << obj.num << " name = " << obj.name << " score = " << obj.score << endl;
return out;
}
// 函数修改了,记得修改类的友元
friend ostream& operator<<(ostream &out, Person &obj);
// 在 test29 函数中添加
cout << xh << xm;
- 运行结果
num = 102 name = 小红 score = 85
num = 101 name = 小明 score = 88.5
- 说明:
- 上面的链式操作可以一次输出多个对象的信息;
- 原理是将 out 返回,即 cout 的引用,那么上一次函数运算的结果 cout 继续作为 << 的左值,完成连续输出,实现链式操作。
2.2全局函数重载 >> 运算符
这里要实现通过cin >> 对象
给对象成员赋值
- 代码演示
// 还是上面的类
// 重载 >> 运算符
istream &operator>>(istream &in, Person &obj) {
in >> obj.num >> obj.name >> obj.score;
return in;
}
// 将全局函数设置为类的友元
friend istream &operator>>(istream &in, Person &obj);
// 调用函数运行
void test30() {
Person jack;
cin >> jack;
cout << jack;
}
- 运行结果
101 jack 88
num = 101 name = jack score = 88
- 和上面重载 >> 运算符原理类似,不做过多解释。
2.3全局函数重载 + 运算符
这里要实现两个对象的加法,即:对象1 + 对象2
。
- 代码演示
// 还是使用上面的 Person 类
// 重载 + 运算符
Person operator+(Person obj1, Person obj2)
{
// 定义一个临时对象用于存放运算结果
Person temp;
temp.num = obj1.num + obj2.num;
temp.name = obj1.name + obj2.name;
temp.score = obj1.score + obj2.score;
return temp;
}
// 添加友元
friend Person operator+(Person obj1, Person obj2);
// 将之前的重载 << 函数的第二个参数引用传递改为值传递
ostream &operator<<(ostream &out, Person obj) // 同样友元也记得该
// 调用函数执行
void test31()
{
Person jack(101, "jack", 88.5);
Person rose(102, "rose", 77.5);
cout << jack + rose;
}
- 运行结果
num = 203 name = jackrose score = 166
- 说明:
Person operator+(Person obj1, Person obj2)
的返回值不能为引用,因为函数运行结束,临时变量 temp 就释放了,如果返回引用的话,后面 cout 输出会访问非法内存,因此,重载 << 运算符的第二个参数也改为值传递了,这样才能类型匹配;- 如果返回值为非引用,那么它返回的就是具体的值,那么重载 + 运算符的第一个参数也要非引用,因为如果进行链式操作,那么会将上一次 + 运算的结果作为下一次 + 运算的左值,但上一次运算结果是具体值,不能对值取引用,因此第一个参数不能直接引用传递;
- 如果第一个参数一定要引用传递的话,那么要加 const 修饰为常引用,才能对值取引用。
2.4成员函数重载 + 运算符
上面的案例, + 的左值为自定义对象,因此可以通过成员函数实现运算符重载。
- 代码演示
// 还是上面的 Person 类,只是在类中添加如下成员函数
// 成员函数重载 + 运算符
Person operator+(Person obj)
{
Person temp;
temp.num = num + obj.num;
temp.name = name +obj.name;
temp.score = score + obj.score;
return temp;
}
// 调用函数执行
void test32()
{
Person jack(101, "jack", 88.5);
Person rose(102, "rose", 77.5);
Person bob(102, "bob", 66.5);
cout << jack + rose + bob;
}
- 运行结果
num = 305 name = jackrosebob score = 232.5
- 说明:成员函数实现重载,函数调用本质上是:
jack.operator+(rose)
,只是被编译器简化为了jack + rose
,因此只需要传一个参数,因为对象调用函数,函数内部会自动生成保存调用该函数的对象的 this 指针,因此函数内部可以直接使用调用该函数的对象的成员数据,可以少传一个参数。
2.5成员函数重载 == 运算符
直接通过 == 比较两个对象是否相等,即实现 对象1 == 对象2
的判断。
- 代码演示
// 还是上面的 Person 类,只是在类中添加如下成员函数
// 成员函数重载 == 运算符
bool operator==(Person &obj)
{
if ((num == obj.num) && (name == obj.name) && (score == obj.score))
return true;
return false;
}
// 调用函数执行
void test33()
{
Person jack(101, "jack", 88.5);
Person rose(102, "rose", 77.5);
Person jack2(101, "jack", 88.5);
if (jack == jack2)
{
cout << "相等" << endl;
}
else
{
cout << "不相等" << endl;
}
}
- 运行结果
相等
2.6重载 ++ – 运算符
++ 和 – 原理相同,这里就以重载 ++ 运算符为例。
重载 ++ 运算符,有两种情况, ++ 在前和在后,但是其函数名都是 operator++ ,而且该运算符的操作数只有一个,该怎样区分两种不同的情况呢,这里就用到了函数的重载,为区分两个函数,还用到了占位参数。
- 因此分为两种情况, ++ 前置和 ++ 后置:
- 当编译器看到 ++a(前置++)时,它就调用 operator++(a), 先++ 后使用;
- 当编译器看到 a++(后置++)时,它就调用 operator++(a, int),先使用后++。
2.6.1重载前置 ++
- 代码演示
// 还是上面的 Person 类,只是在类中添加如下成员函数
// 成员函数重载前置 ++
Person& operator++() {
num++;
name = name + name;
score++;
return *this;
}
// 调用函数执行
void test34() {
Person jack(101, "jack", 88.5);
Person bob;
bob = ++jack;
cout << jack;
cout << bob;
}
- 运行结果
num = 102 name = jackjack score = 89.5
num = 102 name = jackjack score = 89.5
- 说明:重载前置 ++ 的时候,函数没有占位参数。
2.6.2重载后置 ++
- 代码演示
// 还是上面的 Person 类,只是在类中添加如下成员函数
// 成员函数重载后置 ++
Person operator++(int) {
Person old_obj = *this;
num++;
name = name + name;
score++;
return old_obj;
}
// 调用函数执行
void test35() {
Person jack(101, "jack", 88.5);
Person bob;
bob = jack++;
cout << jack;
cout << bob;
}
- 运行结果
num = 102 name = jackjack score = 89.5
num = 101 name = jack score = 88.5
- 说明:
- 重载后置 ++ 的时候,函数有占位参数;
- 函数的返回值不能是引用,因为返回的是一个临时局部变量,保存的对象自增前的值,如果返回引用,变量在函数调用结束后就释放了,操作释放的变量会访问非法内存。
2.7重载 () 运算符
我们在前面调用函数的时候,都会在函数名后面加 () 表示调用函数,因此 () 又叫做函数调用运算符。重载函数调用运算符,实现通过对象加()完成函数调用。
- 代码演示
class Print {
public:
Print &operator()(char *str)
{
cout << str;
return *this;
}
};
void test36()
{
Print obj1;
obj1("hello");
cout << endl;
Print print;
print("hello ")("world ")("hello ")("friend");
cout << endl;
Print()("hello ")("world ")("hello ")("friend");
cout << endl;
}
- 运行结果
hello
hello world hello friend
hello world hello friend
- 说明:上面定义的类又叫仿函数。