C++(三)

今天学习新的内容总结,也是面向对象的核心。

面向对象核心

一.继承

初步理解

定义:
在C++语言中,一个派生类可以从一个基类派生,也可以从多个基类派生。从一个基类派生的继承称为单继承;从多个基类派生的继承称为多继承。
理解:
继承就是在一个已经存在的类的基础上建立一个新的类,并拥有其特性,体现了代码复用的思想。
已经存在类被称为“基类”或“父类”;
新建立的类被称为“派生类”或“子类”。
例子:

class Father{
public:
    void work(){
        cout<<"程序员"<<endl;
    }
};
class Son:public Father{
};

int main()
{
    Son s;
    s.work();
}

格式: 子类:权限 父类名

注意:派生类(子类)继承了基类(父类)的属性和方法后可以对其进行修改。

需要注意的是,基类和派生类是相对的,某个类可能既是类A的基类,又是类B的派生类;派生类往往比基类更具体,基类往往比派生类更抽象。

进步理解

什么不能够继承呢?
类的构造函数和析构函数不能被继承。
为什么概念:
构造函数和析构函数是用来处理对象的创建和析构的,它们只知道对在它们的特殊层次的对象做什么。 所以,在整个层次中的所有的构造函数和析构函数都必须被调用,也就是说,构造函数和析构函数不能被继承。 子类的构造函数会显示的调用父类的构造函数或隐式的调用父类的默认的构造函数进行父类部分的初始化。 析构函数也一样。它们都是每个类都有的东西,如果能被继承,那就没有办法初始化了
理解:
在创建一个类的时候,一般会自动生成构造函数和析构函数(在自己没有自定义的基础上),构造函数专门用来初始化,而析构函数专门用来销毁对象时调用。每个类的都有自定的构造和析构。
例子:

#include <iostream>
using namespace std;

class Father
{
private:
    string name;
public:
    Father(string name):name(name){}

    string get_name() const
    {
        return name;
    }
    void set_name(string name)
    {
        this->name = name;
    }
};
class Son:public Father
{
};
int main()
{
//    Son s("张三"); 错误
    return 0;
}

上面的例子可以看出,派生类并没有继承基类的带参数的构造函数。这是因为当程序员不手写构造函数时,编译器会增加以下代码:
●给基类Father增加一个无参构造函数
●给派生类Son增加一个无参构造函数
●在派生类Son的无参构造函数中,调用基类的无参构造函数

派生类调用基类构造函数

原因:
因为派生类中省略了基类的代码,因此在创建派生类对象时,需要调用基类的代码完成派生类中基类代码部分内存的开辟,即派生类的任意一个构造函数都必须直接或间接调用基类的任意一个的构造函数
方法:
●透传构造
●委托构造
●继承构造(只是叫这个名字,并没有继承构造函数)

透传构造

概念:透传构造指的是在派生类的构造函数中直接调用基类的构造函数。
使用方法
在这里插入图片描述
例子:

#include <iostream>
using namespace std;
class Father{
private:
    string first_name;
    int age;
public:
    Father(string name){
        this->first_name=name;
    }
    Father(string name,int age){
        this->first_name=name;
        this->age=age;
    }
    void show(){
        cout<<first_name<<" "<<age;
    }
};
class Son:public Father{
public:
    Son(string name):Father(name){}
    Son(string name,int age):Father(name,age){}
};
int main()
{
    Son s("张三",18);
    s.show();
}
委托构造

概念:委托构造指的是,某个类的构造函数可以调用这个类的另一个重载的构造函数。
使用方法
在这里插入图片描述例子:

#include <iostream>
using namespace std;
class Father{
private:
    string first_name;
    int age;
public:
    Father(string name){
        cout<<"Father一个参数构造函数"<<endl;
        this->first_name=name;
    }
    Father(string name,int age){
        cout<<"Father两个参数构造函数"<<endl;
        this->first_name=name;
        this->age=age;
    }
    void show(){
        cout<<first_name<<" "<<age<<endl;
    }
};
class Son:public Father{
public:
    Son(string name):Son(name,18){}
    Son(string name,int age):Father(name,age){}

};
int main()
{
    Son s("张",20);
    s.show();
    Son s2("李");
    s2.show();
}

委托构造需要注意以下几点:
●如果是派生类,最终委托的构造函数要透传调用基类的构造函数。
●不要形成委托闭环。

继承构造

概念:是C++11的新写法,在派生类中使用继承构造后,编译器会按照基类的构造函数格式,创建出派生类的构造函数,并且分别使用透传构造调用基类的同参数构造函数。

使用方法
在这里插入图片描述例子:

#include <iostream>
using namespace std;

class Father
{
private:
    string name = "无名氏";
    int age = 1;
public:
    Father(string name):name(name)
    {
        cout << "Father的一参构造函数" << endl;
    }
    Father(string name,int age):name(name),age(age)
    {
        cout << "Father的二参构造函数" << endl;
    }
    void show()
    {
        cout << name << " " << age << endl;
    }
};
class Son:public Father
{
public:
    using Father::Father; // 继承构造
};
int main()
{
    Son s("张三",12);
    s.show();
    Son s2("李四");
    s2.show();
    return 0;
}

和委托类似,比委托简便多了。

二.多重继承

基础使用

概念:
之前的代码都是单继承,即一个派生类只有一个基类,实际上C++是支持多继承的,即一个派生类可以有多个基类,派生类在多重继承中与每一个基类的关系,仍然可以看做是一个单继承。
例子:

#include <iostream>
using namespace std;
class Sofa{
public:
    void sit(){
        cout<<"可以坐着";
    }
};
class Bed{
public:
    void lay(){
        cout<<"可以躺着"<<endl;
    }
};
class SofaBed:public Sofa,public Bed{
};
int main()
{
    SofaBed s;
    s.sit();
    s.lay();
}
//如图sofabed就是继承了sofa和bed两个基类,有了他们的方法
二义性问题

继承中的二义性:
1.当多个基类中有同名的成员,此时就会产生二义性,也就是一个子类继承了多个父类,父类当中的方法重名。
2.菱形继承:如果一个基类有两个派生类,这两个派生类又作为基类拥有一个派生类,此时在最终的派生类中访问间接基类的成员会出现二义性。
解决办法:
1.多继承二义性

#include <iostream>
using namespace std;
class Sofa{
public:
    void sit(){
        cout<<"可以坐着"<<endl;
    }
    void position(){
        cout<<"放在客厅";
    }
};
class Bed{
public:
    void lay(){
        cout<<"可以躺着"<<endl;
    }
    void position(){
        cout<<"放在卧室";
    }
};
class SofaBed:public Sofa,public Bed{

};
int main()
{
    SofaBed s;
    s.sit();
    s.lay();
    s.Sofa::position();
    s.Bed::position();
}

2.菱形继承二义性
方法一:

#include <iostream>
using namespace std;
class Furniture // 家具
{
public:
    void func()
    {
        cout << "家里要有家具" << endl;
    }
};
class Sofa:public Furniture{};
class Bed:public Furniture{};
class SofaBed:public Sofa,public Bed{};
int main()
{
    SofaBed sb;
//    sb.func();            错误
//    sb.Furniture::func(); 错误
    sb.Bed::func();
    sb.Sofa::func();
    return 0;
}

方法二:
虚继承:
在这里插入图片描述

虚继承的实现通过基类指针与虚基类表完成,当Sofa和Bed使用虚继承继承Furniture类时,会在会在Sofa和Bed类中增加一个虚基类表,虚基类表中记录的是Furniture的成员,Sofa类(Bed类)的所有对象共用这一张虚基类表,通过每个对象创建时新增一个隐藏成员虚基类表指针调用虚基类表。SofaBed继承了Sofa和Bed类,SofaBed对象也会有继承来的虚基类指针,虚基类指针可以查询Sofa和Bed的虚基类表,在调用时通过查表来区分二义性问题。
例子:

#include <iostream>
using namespace std;
class Furniture // 家具
{
public:
    void func()
    {
        cout << "家里要有家具" << endl;
    }
};
class Sofa:virtual public Furniture{};
class Bed:virtual public Furniture{};
class SofaBed:public Sofa,public Bed{};
int main()
{
    SofaBed sb;
    sb.func(); 错误
    return 0;
}

虚继承在代码形式上完美解决了菱形继承的二义性问题,但是调用这些原本二义性的代码时,增加了一些开销(虚基类表与虚基类指针内存占用和查询等),因此虚继承代码执行效率比普通继承要低。

三.权限

权限修饰符

三种权限修饰符:
在C++中有三种权限修饰符:private(私有)、protected(保护)和public(公有),这三种权限的访问呢能力如下表所示。
在这里插入图片描述例子:

#include <iostream>
using namespace std;
class Father
{
private:
    string str1 = "private成员";
protected:
    string str2 = "protected成员";
public:
    string str3 = "public成员";

    void test()
    {
        cout << str1 << endl;
        cout << str2 << endl;
        cout << str3 << endl;
    }
};
class Son:public Father
{
public:
    void test()
    {
//        cout << str1 << endl; 错误
        cout << str2 << endl;
        cout << str3 << endl;
    }
};
int main()
{
    Father f;
    f.test();
//    cout << f.str1 << endl; 错误
//    cout << f.str2 << endl; 错误
    cout << f.str3 << endl;
    Son s;
    s.test();
    return 0;
}
不同权限的继承

在C++有三种权限的继承:
●公有继承
●保护继承
●私有继承
之前使用的继承都是公有继承,实际上公有继承也是使用最多的继承。

公有继承

公有继承的特点如下:
在这里插入图片描述例子:

#include <iostream>
using namespace std;
class Father{
public:
    string str1="public";
protected:
    string str2="protected";
private:
    string str3="private";
};
class Son:public Father{
public:
    void show(){
        cout<<str1<<endl;
        cout<<str2<<endl;
        //cout<<str3<<endl;父类私有不能访问
    }
};
int main()
{
    Son s;
    s.show();
}

public继承之后 成员在原来基类是什么权限,在派生类中还是什么权限

保护继承

保护继承的特点如下:
在这里插入图片描述例子:

#include <iostream>
using namespace std;
class Father{
public:
    string str1="public";
protected:
    string str2="protected";
private:
    string str3="private";
};
class Son:protected Father{
public:
    void show(){
        cout<<str1<<endl;
        cout<<str2<<endl;
        //cout<<str3<<endl;父类私有不能访问
    }
};
class GrandSon:public Son{
public:
    void show(){
        cout<<str1<<endl;
        cout<<str2<<endl;
        //cout<<str3<<endl;父类私有不能访问
    }
};
int main()
{
   Father f;
   cout<<f.str1<<endl;
   Son s;
   cout<<s.str1<<endl;  //在子类中变成了protected类型,在全局中不能访问
}

protect继承之后。所有继承过来成员都会在Son中变成protected

私有继承

私有继承的特点如下:
在这里插入图片描述例子:

#include <iostream>
using namespace std;
class Father{
public:
    string str1="public";
protected:
    string str2="protected";
private:
    string str3="private";
};
class Son:private Father{
public:
    void show(){
        cout<<str1<<endl;
        cout<<str2<<endl;
        //cout<<str3<<endl;父类私有不能访问
    }
};
class GrandSon:public Son{
public:
    void show(){
//        cout<<str1<<endl; //因为子类中变成私有 孙子类全访问不到
//        cout<<str2<<endl;
    }
};
int main()
{
   Son s;
   s.show();
   GrandSon gs;
   gs.show();
}

派生类私有继承之后 属性在派生中都变成私有.也就是下方的Son类中的属性都变成私有

补充内容

public继承
派生类对象可以给基类对象赋值
派生类对象地址赋给基类指针
派生类对象赋给基类引用
例子:

#include <iostream>
using namespace std;
class Father{
public:
    int age;
    string name;
};
class Son:public Father{
public:
    string school;

};
int main()
{
  Son s;
  s.age=18;
  s.name="李四";
  s.school="历城中学";

  Father f=s;
  cout<<f.age<<" "<<f.name<<endl;

  Father f2;
  f2.name="李";
  f2.age=38;
  //Son s2=f2; 不可以
  //父类指针可以指向子类对象。但只能访问父类中有的成员,不能访问子类中的新增的成员
  Father * f2=&s; 
  f2->age;
  f2->name; 
}

四.多态*

概念

多态按照字面的意思可以认为是“多种状态”,可以简单概括为“一个接口,多种状态”,即程序在运行时动态决定调用的代码。

多态与模板的区别在于,模板针对不同的数据类型采用同样的策略,而多态针对不同的数据类型采用不同的策略。
多态的实现需要以下3个条件:
1. 要有公有继承
2. 要有函数覆盖:派生类中覆盖基类的成员函数
3. 基类引用/指针指向派生类对象

函数覆盖

函数覆盖与函数隐藏类似,但是函数覆盖可以通过虚函数来支持多态,一个成员函数使用virtual关键字修饰,这个函数就是虚函数
在派生类中,使用之前函数隐藏的方式重新实现一个基类中的虚函数,此时就是函数覆盖,函数覆盖与虚函数具有以下特点:
●当函数覆盖成功时,虚函数具有传递性
●C++11中可以在派生类的新覆盖的函数后增加override关键字进行覆盖是否成功的验证
成员函数与析构函数可以定义为虚函数,静态成员函数与构造函数不可以定义为虚函数
●如果成员函数的声明与定义分离,virtual关键字只需要在声明时使用
例子:

#include <iostream>
using namespace std;
class Animal
{
public:
    virtual void eat()
    {
        cout << "吃东西" << endl;
    }
};
class Dog:public Animal
{
public:
    void eat() override
    {
        cout << "吃骨头" << endl;
    }
};
int main()
{
    Animal a;
    a.eat(); // 吃东西
    Dog d;
    d.eat(); // 吃骨头
    return 0;
}
使用方式

多态既可以用引用的方式,也可以使用指针的方式实现。
例子:

#include <iostream>
using namespace std;
class Animal
{
public:
    virtual void eat()
    {
        cout << "吃东西" << endl;
    }
};
class Dog:public Animal
{
public:
    void eat() override
    {
        cout << "吃骨头" << endl;
    }
};
class Cat:public Animal
{
public:
    void eat()
    {
        cout << "吃鱼" << endl;
    }
};
int main()
{
    Dog d;
    Cat c;
    // 基类引用派生类对象
    Animal& a1 = d;
    Animal& a2 = c;
    // 多态
    a1.eat(); // 吃骨头
    a2.eat(); // 吃鱼
    Dog* d2 = new Dog;
    Cat* c2 = new Cat;
    //  基类指针指向派生类对象
    Animal* a3 = d2;
    Animal* a4 = c2;
    // 多条
    a3->eat(); // 吃骨头
    a4->eat(); // 吃鱼
    // 先别管delete问题
    return 0;
}
应用

多态的主要应用是函数的参数传递,从而实现“一个接口,多种状态”的效果。
例子:

#include <iostream>
using namespace std;
class Animal
{
public:
    virtual void eat()
    {
        cout << "吃东西" << endl;
    }
};
class Dog:public Animal
{
public:
    void eat() override
    {
        cout << "吃骨头" << endl;
    }
};
class Cat:public Animal
{
public:
    void eat()
    {
        cout << "吃鱼" << endl;
    }
};
// 基于引用的多态参数传递
void test_eat1(Animal& a)
{
    a.eat();
}
// 基于指针的多态参数传递
void test_eat2(Animal* a)
{
    a->eat();
}
int main()
{
    Dog d1;
    Cat c1;
    Animal a1;
    // 测试基于引用的多态参数传递
    test_eat1(d1); // 吃骨头
    test_eat1(c1); // 吃鱼
    test_eat1(a1); // 吃东西
    Dog* d2 = new Dog;
    Cat* c2 = new Cat;
    Animal* a2 = new Animal;
    // 测试基于指针的多态参数传递
    test_eat2(d2); // 吃骨头
    test_eat2(c2); // 吃鱼
    test_eat2(a2); // 吃东西
    // 先别管delete问题
    return 0;
}
原理

当一个类中有虚函数时,编译器会为这个类创建一个虚函数表,这个类的对象拥有隐藏的成员变量虚函数表指针指向虚函数表。此类被继承时,虚函数表也会被继承,但是如果当前继承的派生类中出现函数覆盖,则会在派生类中更新这张表。
在实际多态的运行中是一个动态类型绑定的过程,当使用基类引用或指针指向派生类对象时,编译器会产生一段代码,用来检查当前内存中的对象的真正类型,在运行时通过对象的虚函数表指针找到真正的调用函数,因此多态也是一个查表的过程。
使用多态会产生一些额外的代码调用开销,不要滥用多态

缺陷

当形成多态时,有可能会出现内存泄露的问题。

#include <iostream>
using namespace std;
class Animal
{
public:
    virtual void eat()
    {
        cout << "吃东西" << endl;
    }
    ~Animal()
    {
        cout << "Animal析构函数" << endl;
    }
};
class Dog:public Animal
{
public:
    void eat() override
    {
        cout << "吃骨头" << endl;
    }
    ~Dog()
    {
        cout << "Dog析构函数" << endl;
    }
};
int main()
{
    Dog* d = new Dog;
    delete d; // 先调用Dog的析构,再调用Animal的析构
    Animal* a = new Dog;
    delete a; // 只调用Animal的析构函数
    return 0;
}

上面问题的出现是由于析构函数没有函数覆盖,实际上析构函数也无法覆盖,但是析构可以设置为虚函数,且无需覆盖就能解决上述问题。

如果一个类可能作为基类,建议都手写为虚析构函数,以防未来可能出现的内存泄露问题。

五.抽象类

概念

抽象类只表达一个抽象的概念,并不与具体的对象相联系,即无法创建对象,通常为其派生类提供一个算法框架。抽象类不光无法创建实体对象,也无法作为声明类型,因此不能作为参数类型、返回值类型等。

如果一个类有纯虚函数,那么这类就是抽象类;
如果一个类是抽象类,那么一定有纯虚函数。

纯虚函数是一种的虚函数,这种函数只有声明,没有定义。

#include <iostream>
using namespace std;
class Shape
{
public:
    // 纯虚函数
    virtual void area() = 0; // 面积
    virtual void perimeter() = 0; // 周长
};
int main()
{
//    Shape s; 错误
    return 0;
}
使用
抽象类作为基类,其派生类继承抽象类并实现其所有纯虚函数,实现指的是函数覆盖并定义函数,这样的派生类可以作为普通类使用。
#include <iostream>
using namespace std;
class Shape
{
public:
    // 纯虚函数
    virtual void area() = 0; // 面积
    virtual void perimeter() = 0; // 周长
};
class Circle:public Shape
{
public:
    // 实现基类的所有纯虚函数
    void area()
    {
        cout << "πR^2" << endl;
    }
    void perimeter()
    {
        cout << "2πR" << endl;
    }
};
int main()
{
    Circle c;
    c.area();
    c.perimeter();
    return 0;
}
抽象类的派生类没有实现抽象基类的所有纯虚函数,此时派生类也会变成抽象类,需要继续继承直到所有的纯虚函数都被实现。
#include <iostream>
using namespace std;
class Shape
{
public:
    // 纯虚函数
    virtual void area() = 0; // 面积
    virtual void perimeter() = 0; // 周长
};
class Polygon:public Shape
{
public:
    void perimeter()
    {
        cout << "∑边长" << endl;
    }
};
class Rectangle:public Polygon
{
public:
    void area()
    {
        cout << "h*w" << endl;
    }
};
int main()
{
//    Polygon p; 错误

    Rectangle r;
    r.area();
    r.perimeter();
    return 0;
}

抽象类也支持多态,因此抽象类的析构函数应该写为虚析构函数。

六.对象的创建与销毁

代码:

#include <iostream>
using namespace std;
class Value{
private:
    string name;
public:
    Value(string name):name(name){
        cout<<name<<"创建了"<<endl;
    }
    ~Value(){
        cout<<name<<"销毁了"<<endl;
    }
};
class Father{
public:
    static Value s_value;
    Value value=Value("Father类的成员变量");
    Father(){
        cout<<"Father的构造函数"<<endl;
    }
    ~Father(){
        cout<<"Father的析构函数"<<endl;
    }
};
Value Father::s_value=Value("父类的静态Value");
class Son:public Father{
public:
    static Value s_value;
    Value value=Value("Son类的成员变量");
    Son(){
        cout<<"Son的构造函数"<<endl;
    }
    ~Son(){
        cout<<"Son的析构函数"<<endl;
    }
};
Value Son::s_value=Value("子类的静态Value");
int main()
{
    cout<<"main方法开始"<<endl;
    {
        Son s;
        cout<<"Son类对象使用中。。。"<<endl;
    }
    cout<<"main方法结束"<<endl;
}

执行结果:
在这里插入图片描述上述运行结果可以发现遵循如下规律:
●创建流程与销毁流程对称。
●相同部分的创建,基类先派生类后;相同部分的销毁,派生类先基类后。
●静态成员先创建,后销毁。

一个派生类对象的创建与销毁需要逐层调用到最上层的基类,因此体现了面向对象编程的特点:编写效率高,运行效率低。
可以使用继承,但是不要滥用继承。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值