C++学习笔记07

一、运算符重载

为什么要有运算符重载?
如果类没有重载运算符,类的对象不能进行运算符的操作
运算符重载的本质是函数重载,函数名是以运算符的形式来命名,调用函数也是通过运算符来调用。

class Person {};

Person p;
Person p1;

p + p1;

1.定义

  1. 重载:给运算符重新赋予新的含义,在类的内部定义的运算符重载和成员函数是一样的
  2. 重载方法:定义一个重载运算符的函数 在需要执行被重载的运算符时 系统会自动调用该函数
  3. 重载运算符格式:
    函数类型 operator 运算符名称(形参列表)
  4. 运算符重载的参数个数由运算符本身决定,但是类型可以自定义
  5. 由运算符的左值调用运算符的重载
  6. 如果类没有重载运算符,类的对象不能进行运算符的操作

注意:可以将函数的重载定义成全局的,但是不方便

运算符重载虽然对返回值类型和参数类型没有要求,但是我们依然不能随便定义;返回值类型和参数的类型一定要符合习惯才能让代码变得更优雅。

示例1

#include<iostream>
using namespace std;
class Person
{
private:
    int age;
public:
    Person(int a)
    {
        age = a;
    }
    bool operator==(const Person &s);   //==运算符的重载 operator==是重载==运算符的函数名。
};
//this是运算符的左值,参数是运算符的右值
bool Person::operator==(const Person &s)
{
    if(this->age==s.age)
    {
        return true;
    }
    else 
    {
        return false;
    }
}
int main()
{
    Person p1(20);
    Person p2(20);
    if(p1==p2)//这里之所以能够使用==运算符,是因为Person重载了==运算符
    {
        cout<<"the age is equal!"<<endl;
    }
    else 
    {
        cout<<"not equal"<<endl;
    }
}

示例2

类型自定义

class Person
{
private:
    string name;
    int age;
public:
    Person(int age):age(age)
    {}

    bool operator==(const Person& p)
    {
        if(this->age == p.age)
        {
            return true;
        }
        else
        {
            return false;
        }
    }

    bool operator==(int a)
    {
        if(age == a)
        {
            return true;
        }
        else
        {
            return false;
        }
    }
};

int main()
{
    Person p1(19);
    Person p2(19);
    if(p1 == 20)//因为重载了运算符==的参数是int,所以这里可以直接很int做比较
    {
        cout<<"===="<<endl;
    }
    else
    {
        cout<<"!!!!!"<<endl;
    }
}

练习1

设计一个三角形类Triange 包含三角形三条边长的私有数据成员,另有一个重载运算符'+' 以实现求两个三角形周长之和 。
注意:两个三角形对象相加

查看练习代码

class Triange
{
private:
    int a;
    int b;
    int c;
public:
    Triange(int a, int b, int c):a(a),b(b),c(c)
    {}

    int operator+(const Triange& other)
    {
        return a+b+c+other.a+other.b+other.c;
    }
};
int main()
{
    Triange t1(4,5,6);
    Triange t2(7,8,9);
    cout<<t1+t2<<endl;
    return 0;
}

赋值运算符重载

C++每个类都会有默认的赋值运算符重载,浅拷贝的逻辑,成员变量逐个赋值。

示例3

重载赋值运算符

#include <iostream>
#include <string>
#include <cstdio>
using namespace std;
class Person
{
private:
    string name;
    int age;
public:
    Person(){}
    Person(string name):name(name)
    {}

    //赋值运算符习惯与返回左值,左值在重载时就是this
Person& operator=(string name)
    {
        this->name = name;
        return *this;
    }

    Person& operator=(int age)
    {
        this->age = age;
        return *this;
    }

    void show()
    {
        cout<<name<<" "<<age<<endl;
    }
};

int main()
{
    Person p1("小明");
    p1 = 19;//int参数的赋值运算符
    p1 = "小强";//string参数的赋值运算符
    p1.show();

Person p2;
p2 = p1;//虽然自己定义了赋值运算符的重载,但是因为自定义的重载和默认的赋值运算符重载参数列表不一样,所以默认的赋值运算符重载还在。
}

示例4

系统默认的赋值运算符

class Person
{
public:
    int age;
public:
    Person(int age):age(age)
    {}
};
int main()
{
    Person p1(19);
    Person p2(20);
    p1 = p2;//因为有默认的赋值运算符,所以可以直接赋值
    cout<<p1.age<<endl;
}

3.总结

  1. 必须重载和默认赋值运算符相同类型的参数,才能覆盖掉默认的赋值运算符
  2. 自己重载了系统的重载是否还在?
    答:系统的还在,因为我们重载的赋值运算符的参数和c++默认生成的不一样,所以不会覆盖掉默认赋值运算符的重载
  3. 要求会写,在写出来一个系统默认的例子,写个和系统一样的
    Person& operator = (const Person& other )
     {
         this->age = other.age;
         cout<<"赋值运算符的重载"<<endl;
         return *this;
     }
  4. 赋值运算符是算浅拷贝 没有特殊需求 是不用覆盖系统默认的

4.C++默认存在的函数

  1. 默认构造函数
  2. 默认拷贝构造函数
  3. 析构函数
  4. 默认=重载
  5. 默认&重载:这是一个有争议的运算符重载。说它存在因为每个对象都能取地址;说它不存在没人会去重载取地址运算符。知道就比不知道好。

5.++ -- 运算符重载

默认情况下为前缀形式,参数列表是空的 operator++() 前缀++p
后缀形式需要添加一个int类型参数,参数无意义,
仅仅用于区分前缀和后缀形式 operator++(int a) 后缀p++参数不能用,只是为了让编译器这是一个后缀的重载。

示例5

#include <iostream>
#include <string>
#include <cstdio>

using namespace std;

class Person
{
private:
    int age;
public:
    Person(int age):age(age)
    {}

    Person& operator++()//前缀重载
    {
        cout<<"前缀形式"<<endl;
        ++age;
        return *this;//返回的对象age已经被+1了
    }


    //以当前函数中的逻辑,这里不应该返回引用,引用p++.show();并没有使用返回值初始化一个新的对象,返回的局部变量直接死了。
//如果函数的返回值是引用,想直接使用函数的返回值,那么返回的对象生命周期应该大于函数。
Person operator++(int a)//后缀重载
    {
        cout<<"后缀形式"<<endl;
Person p = *this;//拷贝一个+1之前的对象,用来作为返回值
        age++;
        return p;
    }
    void show()
    {
        cout<<age<<endl;
    }
};

int main()
{
    Person p(19);
    p++.show();//19
    (++p).show();//21
}

练习2

在MyTimer类中设计如下重载运算符函数++
1.私有成员h m s
2.重载后缀++ 让时间编程下一秒
3.show函数显示时间

查看练习代码

class MyTimer
{
private:
    int h;
    int m;
    int s;
public:
    MyTimer(int h, int m, int s):h(h),m(m),s(s)
    {}

    MyTimer operator++(int a)
    {
        MyTimer mt = *this;

        s++;
        m+=s/60;
        h+=m/60;

        s%=60;
        m%=60;
        h%=24;

        return mt;
    }

    void show()
    {
        cout<<h<<" "<<m<<" "<<s<<endl;
    }
};

int main()
{
    MyTimer mt(1,2,3);
    mt++;
    mt.show();
    return 0;
}

6.不能重载的运算符

5个不能重载的运算符

1 .(点运算符)通常用于去对象的成员,但是->(箭头运算符),是可以重载的
2 :: (域运算符)即类名+域运算符,取成员,不可以重载
3 .* (点星运算符,)不可以重载,成员指针运算符,.*即成员是指针类型
4 ? : (条件运算符)不可以重载
5 sizeof() 不可以重载

class Person
{
public:
    int age;

    Person(int a):age(a){}

    void show()
    {
        cout<<age<<endl;
    }
};

int main()
{
    void(Person::*p)() = Person::show;

    Person per(1);
    Person per2(2);
    (per.*p)();
    (per2.*p)();
}

笔试题

下面程序的输出结果为:

class Person
{
public:
    Person(int i = 0)
    {
        cout<<i;
    }
    Person(const Person& x)
    {
        cout<<2;
    }
    Person &operator =(const Person&x)
    {
        cout<<3;
        return *this;
    }
    ~Person()
    {
        cout<<4;
}
};

int main(int argc ,char* argv[])
{
    Person obj1(1),obj2(2);
    Person obj3 = obj1;
    obj1 = obj2;  
}

查看答案


1 2 2 3 4 4 4
忠告:不要为了重载运算符而重载运算符,重载运算符只是为了代码看起来更加简洁

7.全局重载运算符

当运算符的操作数类型,没有办法在类中重载运算符时,使用全局重载。

示例6

class MyTimer
{
friend ostream& operator<<(ostream& o, MyTimer& mt);
private:
    int h;
    int m;
    int s;
public:
    MyTimer(int h, int m, int s):h(h),m(m),s(s)
    {}

    MyTimer operator++(int a)
    {
        MyTimer mt = *this;

        s++;
        m+=s/60;
        h+=m/60;

        s%=60;
        m%=60;
        h%=24;

        return mt;
    }
};

//运算符左值是参数1   右值是参数2
ostream& operator<<(ostream& o, MyTimer& mt)
{
o<<mt.h<<" "<<mt.m<<" "<<mt.s;
return o;
}

int main()
{
    MyTimer mt(1,2,3);
    mt++;
    cout<<mt<<endl;
    return 0;
}

二、类继承

1.继承的概念

继承允许依据一个类来定义另一个类,使创建和维护一个应用程序变得更容易。
这样做,也达到了重用代码功能和提高执行时间的效果。

当创建一个类时,不需要重新编写新的数据成员和成员函数,只需指定新建的类继承了一个已有的类的成员即可。
已有的类称为基类或父类,新建的类称为派生类或子类。
一个子类可以有多个父类,它继承了多个父类的特性。

面向对象三大特性:
封装、继承和多态
继承可以实现面向对象代码重用,继承也是实现多态的必要语法

当子类继承了父类,子类中将具备父类中的所有成员,和访问权限没关系。父类中的私有成员也会继承,只是不能访问。

2.继承语法

class 子类名:继承方式 父类名1,继承方式 父类名2,继承方式 父类名3....
{
    子类新添加的成员;   
}

示例7:

#include <iostream>
#include <string>
using namespace std;
class Food
{
private:
string name;
public:
void taste()
{
cout<<name<<” 真美味”<<endl;
}
};
//KFC是子类Food是父类   子类:派生类   父类:基类
class KFC : public Food
{
private:
string chips;
public:
    KFC(string chips)
    {
        this->chips = chips;
    }

    void show()
    {
        cout<<"KFC中的  "<<chips<<endl;
    }
};

int main()
{
    KFC k("薯条");
k.show();
k.taste();//子类会具备父类中所有的成员,所以这里可以调用taste函数,但是name现在还是没有值的。

3. 继承中构造函数的赋值方法

子类中包含父类
子类中包含父类,所以创建子类对象的时候会调用父类的构造函数对子类中的父类部分进行初始化。

子类通过参数列表赋值示例:参考 父类的无参构造是否会调用?

示例8:

class Food
{
private:
    string name;
public:
    Food()
    {
        cout<<" 这是父类构造函数"<<endl;
    }
    void taste()
    {
        cout<<name<<" 真美味"<<endl;
    }
};

class KFC : public Food
{
private:
    string chips;
public:
    KFC(string chips)
    {
        this->chips = chips;
    }

    void show()
    {
        cout<<"KFC中的  "<<chips<<endl;
    }
};

int main()
{
    KFC k("薯条");
    k.show();
    k.taste();
}

结论:如果没有显式的调用父类构造函数,会默认调用父类无参的构造函数

示例9:

子类的初始化列表中可以通过父类的构造函数来初始化父类的成员。
父类中的成员变量,无论子类有没有访问权限,都只能通过父类构造函数对其初始化。

class Food
{
private:
    string name;
public:
    Food(string name):name(name)
    {
        cout<<" 这是父类构造函数"<<endl;
    }
    void taste()
    {
        cout<<name<<" 真美味"<<endl;
    }
};

class KFC : public Food
{
private:
    string chips;
public:
    //子类默认调用父类中的无参构造,当父类中没有无参构造时,就得显式的指定执行父类的某个构造函数。
    //在子类构造函数的初始化列表中指定要执行的父类构造函数
    KFC(string chips):Food("超级巨无霸薯条"),chips(chips)
    {   
    }
    void show()
    {
        cout<<"KFC中的  "<<chips<<endl;
    }
};

int main()
{
    KFC k("薯条");
    k.show();
    k.taste();
}

注意:如果父类中没有无参构造函数,子类的初始化列表中必须显式的调用父类带参的构造函数

4. 父类子类的初始化顺序

先初始化父类,在初始化子类;在父类中先初始化父类的成员,再调用父类构造函数;子类中先初始化子类成员,再调用子类构造函数。

示例10:

class A
{
public:
    A()
    {
        cout<<"A"<<endl;
    }
};

class B
{
public:
    B()
    {
        cout<<"B"<<endl;
    }
};
class Food
{
protected:
    string name;
    A a;
public:
    Food(string name):name(name)
    {
        cout<<"这是父类构造函数"<<endl;
    }
    void taste()
    {
        cout<<name<<" 真美味"<<endl;
    }
};

class KFC : public Food
{
private:
    string chips;
    B b;
public:
    //子类默认调用父类中的无参构造,当父类中没有无参构造时,就得显式的指定执行父类的某个构造函数。
    //在子类构造函数的初始化列表中指定要执行的父类构造函数
    KFC(string chips):Food("这样也可以"),chips(chips)
    {
        cout<<"这是子类构造函数"<<endl;
    }

    void show()
    {
        cout<<"KFC中的  "<<chips<<endl;
    }
};

int main()
{
    KFC k("薯条");
}

练习3:

  • 父类 :Person类
    定义构造函数,通过参数列表形式初始化成员
    数据:
    名字 name
    年龄 age
  • 子类: Student类
    定义构造函数,通过参数列表形式 初始化父类及子类成员
    数据:
    工作 work
    成员函数:show()显示数据

查看练习代码

class Person
{
protected:
    string name;
    int age;
public:
    Person(string name, int age):name(name),age(age)
    {}
};
class Student : public Person
{
private:
    string work;
public:
    Student(string name, int age, string work):
        Person(name, age), work(work)
    {
    }
    void show()
    {
        cout<<name<<" "<<age<<" "<<work<<endl;
    } 
};
int main()
{
    Student xiaoming("小明", 18, "运营总监");
    xiaoming.show();
    return 0;
}

练习4:

先设计一个Point类 包含:x,y(坐标) ----基类

  1. 由Point类派生出一个Circle类
    1)增加数据成员r(半径) //3
    2)Circle类定义函数计算面积
  2. 由Circle类为直接基类派生出一个Cylinder(圆柱体)类 //4
    1)再增加成员h(高)
    2)Cylinder(圆柱体)类定义函数计算体积

查看练习代码

#define PI 3.14f
class Point
{
protected:
    int x;
    int y;
public:
    Point(int x, int y):x(x),y(y)
    {

    }
};

class Circle : public Point
{
protected:
    float r;
public:
    Circle(int x, int y, float r):
        Point(x,  y),r(r)
    {  
    }

    float area()
    {
        return r*r*PI;
    }
};

class Cylinder : public Circle
{
private:
    float h;
public:
    Cylinder(int x, int y, float r, float h):
        Circle(x, y, r),h(h)
    {
    }

    float volume()
    {
        return area()*h;
    }
};

int main()
{
    Circle c(0,0, 3);
    cout<<c.area()<<endl;

    Cylinder cy(0, 0, 3, 6);
    cout<<cy.volume()<<endl;
    return 0;
}

5.继承权限

继承访问方式的影响:
public继承: 子类继承后的访问权限不变
protected继承: 子类继承后,父类中的public变成protected,其它不变(protected:只有子类可以用),类外不可用
private继承: 子类继承后,父类中的public和protected变成private,其他不变

三、虚函数

1.虚函数的定义

c++中定义成员函数时前边加关键字virtual,这个函数就是虚函数:

virtual void eat(){
cout << “ eating ...” << endl; 
}

2.重写(override)

override 这不是关键字,只是重写的英文翻译。
子类中定义和父类中虚函数同名,同返回值类型,同参数列表的函数,叫重写。
虚函数是为了重写,重写是实现多态的必要语法。

示例9:

#include <iostream>
#include <string>

using namespace std;

class A{
public:
    virtual void show(){//父类中定义了虚函数
            cout << "A show()..." << endl;
    }
};
class B:public A{//有继承
public:
    void show(){//和父类中的虚函数show同名,同返回值类型,同参数列表,这是重写
            cout << "B show()..." << endl;
    }
};
int main(){
    A a;
    a.show();//A show()...
    B b;
    b.show();//B show()...
    return 0;
}

3. C语言的多态

什么是多态?
多态:把逻辑当成参数传递。

示例10:

写一组函数实现对任意整型数组的升序和降序排序。

int cmpUp(int a, int b)
{
    return a > b;
}

int cmpDown(int a, int b)
{
    return a < b;
}

void sort(int* arr, int len,int(*cmp)(int,int))
{
    for(int i = 0;i < len-1;i++)
    {
        for(int j = 0;j < len-1-i;j++)
        {
            if(cmp(arr[j], arr[j+1]))
            {
                int t = arr[j];
                arr[j] = arr[j+1];
                arr[j+1] = t;
            }
        }
    }
}

int main()
{
    int a[10] = {23,45,76,89,0,8,3,2,576,1};
    sort(a, 10, cmpUp);
    for(int i = 0;i < 10;i++)
    {
        printf("%d ", a[i]);
    }
    printf("\n");

    sort(a, 10, cmpDown);
    for(int i = 0;i < 10;i++)
    {
        printf("%d ", a[i]);
    }
    printf("\n");

    return 0;
}

4. C++的多态

C++的虚函数主要作用是“运行时多态”,父类中提供虚函数的实现,为子类提供默认的函数实现。
子类可以重写父类的虚函数实现子类的特殊化。

父类类型的指针可以指向子类类型的对象,调用子类中重写的函数。

父类* 指针 = new 子类();
Shape* shape = new Rectangle();
使用这种方式,父类的指针可以指向自己的所有派生类对象。

如果使用这种指针方式,若调用父类普通函数,则调用函数时依然是父类的方法;
若调用父类的虚函数,调用函数时调用的是子类重写之后的函数。

多态:
1.子类可以重写父类中virtual函数,
2.用父类指向子类的指针或引用可以访问子类中重写的方法:为了将逻辑(类)当做参数进行传递
3.父类可以调用子类重写的方法

父类类型的指针 可以指向子类类型的对象

示例11

#include <iostream>
#include <string>
using namespace std;
class Person
{
public:
    virtual void eat()
    {
        cout<<"Person eat"<<endl;
    }

    void sleep()
    {
        cout<<"Person sleep"<<endl;
    }
};

class Student : public Person
{
public:
    void eat()//重写父类中的虚函数
    {
        cout<<"student eat"<<endl;
    }

    void sleep()//这不是重写,因为父类中的sleep不是虚函数, 这叫 覆盖
    {
        cout<<"student sleep"<<endl;
    }

};
int main()
{
    Student xiaoming;
    Person& p1 = xiaoming;//父类引用指向子类对象
    p1.eat();//因为父类引用指向子类对象,在调用函数时,优先去调用子类中重写的函数
    p1.sleep();//调用person类型中的sleep函数

    Person* p2 = &xiaoming;//父类指针指向子类对象
    p2->eat();//因为父类指针指向子类对象,在调用函数时,优先去调用子类中重写的函数
    p2->sleep();//调用person类型中的sleep函数
}

笔试题:

如果函数在基类中被声明为virtual,则函数在其派生类中( )
A. 都是虚函数
B. 只有被重新声明时才是虚函数
C. 只有被重新声明为virtual时才是虚函数
D. 都不是虚函数

查看答案

A

练习6:

  1. 定义基类Developer,有虚函数develop();
  2. 定义大神类Manito和菜鸟类SmallBird继承Developer,重写develop()函数
  3. 定义公司类Company
    1. 定义成员函数招聘 recruit();
      功能:随机生成一个开发者对象,返回值为Developer* (父类指针)
    2. 定义Company类的成员函数 work(Developer* );
      功能:调用develop()函数;(重写函数)
      例:Developer* recruit();
      void work(Developer*);
  4. main中创建Company对象,调用recruit()获得一个Developer
    然后调用work()传入Developer参数
End

本文标题:C++学习笔记07

本文链接:https://www.chisato.cn/index.php/archives/156/

除非另有说明,本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议

声明:转载请注明文章来源。

最后修改:2021 年 11 月 18 日 01 : 43 PM
如果觉得我的文章对你有用,请随意赞赏