C++ | 多态:派生&虚函数&模板

继承与派生

C ++ 是面向对象编程,那么只要面向对象,都会有多态、继承的特性。C++是如何实现继承的呢?来看下面篇幅。 继承(Inheritan**ce)**可以理解为一个类从另一个类获取成员变量和成员函数的过程。例如类 B 继承于类 A,那么 B 就拥有 A 的成员变量和成员函数。

在C++中,**派生(Derive)**和继承是一个概念,只是站的角度不同。继承是儿子接收父亲的产业,派生是父亲把产业传承给儿子。

被继承的类称为父类或基类,继承的类称为子类或派生类。“子类”和“父类”通常放在一起称呼,“基类”和“派生类”通常放在一起称呼。

在C++中继承称为派生类,基类孵化除了派生类,使用来表示子类继承父类,C++中支持多继承,使用逗号分隔

class Parent {
public:
    int name;
protected:
    int code;
private:
    int num;
};

class Parent1 {

};

//C++中,:表示继承,可以多继承逗号分隔
//public/protected/private继承,对于基类起到一些保护机制 默认是private继承
class Child : public Parent, Parent1 {
    void test() {
        //派生类可以访问到public属性和protected属性
        this->name;
        this->code;
    }
};
复制代码

C++中派生类中添加了public 派生、protected派生、private派生,默认是private派生

class 派生类名:[继承方式] 基类名{

派生类新增加的成员
复制代码

};

//private私有继承
class Child1 : private Parent {
    void test() {
        this->name;
        this->code;
    }
};

//protected继承
class Child2 : protected Parent {
    void test() {
        this->name;
        this->code;
    }
};

复制代码

public 派生、protected派生、private派生对于,创建的对象调用父类的属性和方法起到了限制和保护的作用

    Child child;
    child.name;//public继承。调用者可以访问到父类公有属性,私有属性访问不到的

    Child1 child1;
//    child1.name;//private继承.调用者访问不到父类公有属性和私有属性

    Child2 child2;
//    child2.name;//protected继承,调用者访问不到父类公有属性和私有属性
复制代码

虚函数

**重点!!!**C++的继承和java中的继承存在的不同点: 基类成员函数和派生类成员函数不构成重载

基类成员和派生类成员的名字一样时会造成遮蔽,这句话对于成员变量很好理解,对于成员函数要引起注意,不管函数的参数如何,只要名字一样就会造成遮蔽。换句话说,基类成员函数和派生类成员函数不会构成重载,如果派生类有同名函数,那么就会遮蔽基类中的所有同名函数,不管它们的参数是否一样。​

父类代码如下:

class Person {
protected:
    char *str;
public:
    Person(char *str) {
        if (str != NULL) {
            this->str = new char[strlen(str) + 1];
            strcpy(this->str, str);
        } else {
            this->str = NULL;
        }
        cout << "parent" << endl;
    }

    Person(const Person &p) {
        cout << "copy parent" << endl;
    }

    void printC() {
        cout << "parent printC" << endl;
    }

    ~Person() {
//        if (str != NULL) {
//            delete[] str;//如果调用了这个方法只会调用一次析构函数
//        }
//        cout << "parent destroy" << endl;
    }
};
复制代码

子类继承父类,并且调用父类的构造函数, 通过来调用父类的构造函数

//子类
class CTest : public Person {
public:
    //调用父类的构造方法
    CTest(char *str) : Person(str) {
        cout << "child" << endl;
    }

    void printC() {
        cout << "child printC" << endl;
    }

    ~CTest() {
        cout << "child destroy " << endl;
    }
};
复制代码

在C++中和Java的不同在于如下代码:只要是父类的指针都是调用的父类的方法,哪怕子类对象直接赋值给父类,也会调用父类的方法,而不会调用子类的方法。

    Person person = CTest("jake");

    person.printC();//parent printC

    cout << "-----------" << endl;
    Person *p = NULL;
    CTest c1("123");
    p = &c1;
    c1.printC();//child printC
    p->printC();//parent printC 为什么会调用的是parent的方法呢?
复制代码

哪怕通过指针传递和引用传递,只要使用的父类都会调用父类的方法

//通过指针传递只会调用父类的方法,不会调用子类的方法
void howToPaint(Person *p) {
    p->printC();
}

//通过引用类型,只会调用父类的方法,不会调用子类的方法
void howToPaint1(Person &p) {
    p.printC();
}



	cout << "---------" << endl;
    howToPaint(p);//parent printC
    howToPaint(&c1);//parent printC

    cout << "-------" << endl;
    Person p1("123");
    //都是父类的方法
    howToPaint1(p1);//parent printC
    howToPaint1(c1);//parent printC

    cout << "--------" << endl;
    CTest c2("123");
    Person p2 = c2;//会不会调用父类的拷贝函数呢? copy parent 会进行调用
复制代码

这是为什么呢?如下图内存模型图所示:

C ++中会按照函数表的顺序进行调用,很显然父类的函数是在子类函数的前面的

image.png 那么如何调用到子类的方法呢?C ++提供了虚函数的方式,虚函数也是实现多态的关键。 ​

虚函数与纯虚函数,纯虚函数在java中abstract == 纯虚函数

实际开发中,一旦我们自己定义了析构函数,就是希望在对象销毁时用它来进行清理工作,比如释放内存、关闭文件等,如果这个类又是一个基类,那么我们就必须将该析构函数声明为虚函数,否则就有内存泄露的风险。也就是说,大部分情况下都应该将基类的析构函数声明为虚函数。

包含纯虚函数的类称为抽象类(Abstract Class)。之所以说它抽象,是因为它无法实例化,也就是无法创建对象。原因很明显,纯虚函数没有函数体,不是完整的函数,无法调用,也无法为其分配内存空间。

抽象类通常是作为基类,让派生类去实现纯虚函数。派生类必须实现纯虚函数才能被实例化。

  1. 一个纯虚函数就可以使类成为抽象基类,但是抽象基类中除了包含纯虚函数外,还可以包含其它的成员函数(虚函数或普通函数)和成员变量。
  1. 只有类中的虚函数才能被声明为纯虚函数,普通成员函数和顶层函数均不能声明为纯虚函数。
  1. 基类的析构函数必须声明为虚函数。

#include <iostream>

using namespace std;

class Person {
public:
    //增加了一个虚函数表的指针
    virtual void look() {//虚函数 子类可以覆写的函数
        cout << "virtual look" << endl;
    }

    virtual void speak() {

    };//纯虚函数 必须要让子类实现的
    //基类的析构函数必须声明为虚函数
    virtual ~Person() {
        cout << "~Person" << endl;
    }
};

class Child : public Person {
public:
    void speak() override {//子类实现纯虚函数
        cout << "child speak" << endl;
    }

    void look() override {
        cout << "child look" << endl;
        Person::look();//访问父类的方法
    }
    ~Child() {
        cout << "~Child" << endl;
    }
};

int main() {
    Person *person = new Child();//必须通过指针的方式,不同通过栈的方式去派生抽象
    person->speak();//child speak
    person->look();//child look

    Person p;
    cout << sizeof(p) << endl;//8 这就表明了虚函数是有一个虚函数表,增加一个指针*vtable,指向了虚函数表
    //下面代码来证明
    typedef void (*func)(void);
    func fun = NULL;
    cout << (int*)&p << endl;//指向函数的首地址 0x61fdf8
    cout << (int*)*(int*)&p << endl;//函数的地址 0x404560
    fun  = (func)*((int*)*(int*)&p);
    fun();//virtual look
    return 0;
}

    /**
     * child speak
        child look
        virtual look
        ~Child
        ~Person
     */
复制代码

模板

模板和java的泛型类似。 模板类不支持声明(.h)和实现(.cpp)分开写,「不能将模板的声明和定义分散到多个文件中」的根本原因是:模板的实例化是由编译器完成的,而不是由链接器完成的,这可能会导致在链接期间找不到对应的实例。

  • 函数模板
#include <iostream>
#include <string>
#include <cstring>

using namespace std;
/**
 * 函数模板和java中的泛型类似
 */

//方法泛型 这里只能声明在方法上
template<typename T, typename R=int>
//R的默认类型是int
//typename == class 两个等价的
void swap2(T t, R r) {
}


template<typename T>
void swapT(T &a, T &b) {
    cout << "swap: T a T b" << endl;
    T temp = a;
    a = b;
    b = temp;
}

//普通函数优先级比泛型函数高,只有类型重合的状态下
void swapT(int &a, int &b) {
    cout << "swap : int a int b" << endl;
    int temp = a;
    a = b;
    b = temp;
}


int main() {
    //函数模板
    int a = 10;
    int b = 20;
    char c = 'a';
    swapT<int>(a, b);//显示调度
    swapT(a, b);//自动推导
//    swap(a,c);//报错 无法推导出具体的类型

//    swap2();//报错 无法推导出具体的类型
    return 0;
}


复制代码
  • 类模板
//模板修饰在类上
template<typename T, typename R>
class Person {
public:
    T a;
    R b;

    Person(T t) {

    }

    T &getA() {
        T t1;
//        return t1;//这里不可以返回,因为方法执行完毕后会销毁掉
        return a;//返回值是引用
    }
};

/**
 * 和java不同的部分,比java更加灵活
 */
class Pp {
public:
    void show() {
        cout << "Pp show" << endl;
    }
};

template<typename T>
class ObjTemp {
private:
    T obj;
public:
    void showPp() {
        //自动检查 但是会出现不可预期的错误
        obj.show();//假设模板是Pp,可以调用Pp的变量和方法,在java中需要<T extend Pp> T才能调用方法
    }
};


template<typename T, typename R>
class CTest {
public:
    T m_name;
    R m_age;

    CTest(T name, R age) {
        this->m_name = name;
        this->m_age = age;
    }

    void show() {
        cout << "show T:" << m_name << " R:" << m_age << endl;
    }
};

template<typename T, typename R>
void doWork(CTest<T, R> &cTest) {
    cTest.show();
}

template<typename T>
void doWork2(T &t) {
    t.show();//在java中必须是<T extend xxxx>
}


//继承模板问题和java是一样的
template<typename T>
class Base {
public:
    T t;
};

//确定的类型或者模板
template<typename T, typename R>
class Son : Base<R> {
public:
    T t1;
};

int main(){
  	CTest<string, int> test("jake", 28);//show T:jake R:28
    doWork(test);
    doWork2<CTest<string, int>>(test);//显示调用
    doWork2(test);//自动推导

    ObjTemp<Pp> temp;
    temp.showPp();//Pp show 可以调用传递过来的模板的方法   
    
        //自动类型推导,在类模板上不可以使用,无法推导出具体的类型
    Person<int, string> p(100);
    cout << p.getA() << endl;
}

复制代码

如下实现一个模板类ArrayList类似Java的列表实现:

注意在之前学习的.h和.cpp分开的方式,不支持模板,一般模板的部分都会合并到.h文件中。

#include <iostream>

using namespace std;

#ifndef CPPDEMO_ARRAYLIST_H
#define CPPDEMO_ARRAYLIST_H

template<typename T>
class ArrayList {
public:
    int d = 11;

    ArrayList() {
        this->size = 16;
        this->realSize = 0;
        this->arr = new T[this->size];
    }

    //explicit 不能通过隐式调用
    explicit ArrayList(int capacity) {
        this->size = capacity;
        this->realSize = 0;
        //在堆区申请数组
        this->arr = new T[this->size];//在堆中开辟的一块空间 存储的是一个int[size] 数组,arr指向数组的首地址
    }

    //拷贝函数
    ArrayList(const ArrayList &arrayList) {
        this->size = arrayList.size;
        this->realSize = arrayList.realSize;
        this->arr = new T[arrayList.size];
        //将数组的值赋值到arr中
        for (int i = 0; i < this->size; ++i) {
            this->arr[i] = arrayList.arr[i];//arrayList.arr[i]他也是指针  this->arr[i] 是指针
        }
    }

    //析构函数
    ~ArrayList() {
        if (this->arr != nullptr) {
            delete[] this->arr;
            this->arr = nullptr;
        }
    }

    void add(T val) {
        add(val, this->realSize);
    }

    void add(T val, int index) {
        if (index < 0 || index > size) {
            return;
        }
        //判断容量是否够大 不够进行扩容
        if (this->realSize >= size * 0.75) {
            resize();
        }
        this->arr[index] = val;// 等价于   *((this->arr)+index) = val
        this->realSize++;//数据量大小+1
    }

    T get(int index) {
        if (index < 0 || index >= realSize) {
            return -1;
        }
        return this->arr[index];
    }

    T remove(int index) {
        if (index < 0 || index >= realSize) {
            return -1;
        }
        //如何移除呢?循环往前移动
        int result = this->arr[index];
        for (int i = index; i < size - 1; ++i) {
            this->arr[i] = this->arr[i + 1];
        }
        this->realSize--;
        //判断缩减容量
        return result;
    }

    //const 定义为常函数
    int getLength() const {
        //    realSize = realSize - 1; 这样会报错 不能修改函数内部的所有变量
        c = 11;//mutable 修饰的变量可以在常函数中修改
        return realSize;
    }

    bool isEmpty() const {
        return realSize == 0;
    }

    void resize() {
        int netLength = size * 2;
        T *p = new T[netLength];
        //拷贝数据
        for (int i = 0; i < size; ++i) {
            *(p + i) = this->arr[i];
        }
        //释放之前的数组
        delete[] this->arr;
        //重新赋值
        this->arr = p;
        this->size = netLength;
    }

    void toString() {
        cout << "[ ";
        for (int i = 0; i < realSize; ++i) {
            cout << arr[i] << ", ";
        }
        cout << " ] " << endl;
    }

private:
    int size{};//容器的大小
    int realSize{};//真实的数组长度
    T *arr;//这里不能使用数组,因为数组名是arr指针常量,不能对arr重新赋值, 指针是指针变量,而数组名只是一个指针常量
    mutable int c = 10;//可以在常函数中修改的变量 需要使用mutable进行修饰
};


#endif //CPPDEMO_ARRAYLIST_H

复制代码

字符串

    //字符串 string 是C++独有的string是一个对象,内部封装了和C一样的字符串的表现形式
    string s1();
    string s2("123");
    string s3 = "wew";//string字符串是声明在堆区的
    string s4(4, 'k');//4个K组成 kkkk
    string s5("123456", 1, 4);//从1开始,输出四个字符串:2345
    cout << s4 << " " << s5 << endl;
    s2.append(s3);//追加123wew
    s2.append(s3,1,2);//ew
    cout << s2 << endl;//123wewew
    string sub = s2.substr(2,3);//字符串裁剪
    cout << sub << endl;//3we

    s4.swap(s5);//字符串交换,只有引用和地址才会改变外部的值

    //c_str 支持C,转换为char *
    string s = "jakeprim";//存储在堆区 方法执行完毕 执行析构函数 从堆区移除
    //一般不会这样使用
    const char *s_c = s.c_str();//将C++ string转换为支持C的字符串,返回常量指针 指针指向了常量,不能通过指针来修改常量
    printf("%s\n", s_c);

    //一般开发会使用strcpy拷贝,防止被销毁掉等问题 在FFmpeg是使用的C,所以在使用C++开发时必须要对C的转换
    char ss[20];
    strcpy(ss, s.c_str());//拷贝到一个新的变量中
复制代码

猜你喜欢

转载自juejin.im/post/7018096224293093384

相关文章