当前位置: 首页 > article >正文

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 类的友元,其步骤如下:
  1. 先定义 A 类,并且 A 类的成员函数在类外(A 和 B 类的下方)实现;
  2. 向前声明 B 类,在 A 类前面声明 B 类;
  3. 在 B 类中使用 friend 声明 A 的成员函数为友元。

3.一个类作为另外一个类的友元

  • 代码演示
// 其它代码和上面类的成员函数作为另外一个类的友元一样
// 只是不是将单个函数作为友元,而是整个类,只修改一句代码
// 将class Object中的
friend void Person::useCup(Object &obj); // 改为
friend Person;
// 这样,如果Person类中有多个成员函数需要设置为Object的友元
// 不需要一个个函数去 friend ,只需要把整个类 friend 就行
  • 总结
    1. 友元关系不能被继承,继承后面学。即:类A 是父类,类B 是 类A 的朋友,类C 是 类A 的子类,但 类B 不一定是 类C 的朋友;
    2. 友元关系是单向的。即:类 A 是类 B 的朋 友,但类 B 不一定是类 A 的朋友;
    3. 友元关系不具有传递性。即:类 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 
  • 注意:
    1. 数组类中包含数组的必要数据:数组的的大小,即实际的元素个数,数组的容量,数组首元素的地址;
    2. 数组初始化,除了初始化容量以外,还得申请相应大小的堆区空间;
    3. 插入数据的时候,如果数组的大小和容量相等,代表数组已满,要插入需要先追加空间。但追加空间不是在原空间后面追加,而是新申请内存(为原来的内存加新申请的空间的大小总和),然后再将原来空间的数据,拷贝到新空间。不在后面直接追加的原因是,不确定原空间后面的空间是否存储了其它数据,后面添加可能占用非法内存。

三、运算符重载

运算符重载:就是对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型的运算需求。

关键字:operator 比如重载 + 运算符,其函数名 operator+

一元运算符:运算符需要的运算对象是一个,如i++ --i等;二元运算符:运算符需要的运算对象是两个,如+ - *等。

1.重载运算符的步骤

  1. 首先必须明确:运算符左边的运算符对象是自定义对象还是其他;
  2. 如果左边是其他,必须使用全局函数完成运算符重载,且全局函数必须是友元;
  3. 如果左边是自定义对象,可以使用成员函数或全局函数完成运算符重载,推荐用成员函数实现。如果使用成员函数,可以少一个参数,如果使用全局函数,不可以少参数,且必须把全局函数设置友元。

先大致了解重载运算符的步骤及注意事项,结合下面的案例具体学习。

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
  • 说明:

    1. 上面重载了 << 运算符,首先 << 左边是一个非自定义对象,因此需要通过全局函数完成运算符重载;
    2. 因为这里是需要通过重载函数操作对象数据的,因此需要将重载函数设置为类的友元;
    3. 这里重载 << 运算符只是针对当前类创建的对象有效;
    4. operator<<为重载函数名,<< 左边的作为函数的第一个参数,右边的作为函数的第二个参数;
    5. 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
  • 说明:
    1. 上面的链式操作可以一次输出多个对象的信息;
    2. 原理是将 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
  • 说明:
    1. Person operator+(Person obj1, Person obj2)的返回值不能为引用,因为函数运行结束,临时变量 temp 就释放了,如果返回引用的话,后面 cout 输出会访问非法内存,因此,重载 << 运算符的第二个参数也改为值传递了,这样才能类型匹配;
    2. 如果返回值为非引用,那么它返回的就是具体的值,那么重载 + 运算符的第一个参数也要非引用,因为如果进行链式操作,那么会将上一次 + 运算的结果作为下一次 + 运算的左值,但上一次运算结果是具体值,不能对值取引用,因此第一个参数不能直接引用传递;
    3. 如果第一个参数一定要引用传递的话,那么要加 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++ ,而且该运算符的操作数只有一个,该怎样区分两种不同的情况呢,这里就用到了函数的重载,为区分两个函数,还用到了占位参数。

  • 因此分为两种情况, ++ 前置和 ++ 后置:
    1. 当编译器看到 ++a(前置++)时,它就调用 operator++(a), 先++ 后使用;
    2. 当编译器看到 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
  • 说明:
    1. 重载后置 ++ 的时候,函数有占位参数;
    2. 函数的返回值不能是引用,因为返回的是一个临时局部变量,保存的对象自增前的值,如果返回引用,变量在函数调用结束后就释放了,操作释放的变量会访问非法内存。

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
  • 说明:上面定义的类又叫仿函数。

http://www.kler.cn/a/465920.html

相关文章:

  • JAVA:Spring Boot 集成 Quartz 实现分布式任务的技术指南
  • 【UE5 C++课程系列笔记】21——弱指针的简单使用
  • 数据挖掘入门介绍及代码实战
  • Linux下部署ElasticSearch集群
  • xdoj isbn号码
  • 46. Three.js案例-创建颜色不断变化的立方体模型
  • FreshTomato 路由器固件常见配置以及踩坑记录
  • PyQt5:自定义QListView显示
  • 高质量C++小白教程:2.10-预处理器简介
  • GIT 企业级开发学习 1
  • Emacs折腾日记(七)——布尔变量、逻辑运算符与位运算
  • token、cookie和session
  • 【AIGC】 ChatGPT实战教程:如何高效撰写学术论文引言
  • MR30分布式IO模块助力PLC,打造高效智能仓储系统
  • [redux] useDispatch的两种用法
  • OCR图片中文字识别(Tess4j)
  • Lua开发环境如何安装?保姆级教程
  • 大数据-268 实时数仓 - ODS层 将 Kafka 中的维度表写入 DIM
  • Java编程规约:集合处理
  • 线性变换在机器学习中的应用实例
  • 深入AIGC领域:ChatGPT开发者获取OpenAI API Key的实用指南
  • 公司一个bug引出的对象判空、空指针异常话题之NUll、isEmpty()和“”区别
  • 【华为OD-E卷 - Linux发行版的数量 100分(python、java、c++、js、c)】
  • 【开源免费】基于SpringBoot+Vue.JS保密信息学科平台(JAVA毕业设计)
  • 电脑文件msvcp110.d丢失的解决方法
  • Transformer算法实现IMDB文本分类任务和WMT14机器翻译任务