C++ Primer Plus 学习笔记(六)——类继承和类模板

1 公有继承——is-a

继承通过使用已有的类(基类)定义新的类(派生类),使得能够根据需要修改编程代码。

C++提供了3种继承方式——公有继承(public)保护继承(protect)私有继承(private)

  • 公有继承:允许派生类和外部访问。
  • 保护继承:只允许派生类访问。
  • 私有继承:只允许基类自己访问。

派生类继承基类的数据成员和大部分方法,但不继承基类的构造函数、析构函数和赋值操作符。

派生类可以直接访问基类的公有成员和保护成员,并能够通过基类的公有方法和保护方法访问基类的私有成员。

创建派生类对象时,程序首先调用基类构造函数,然后再调用派生类构造函数。可以使用初始化器列表句法指明要使用的基类构造函数,否则将使用默认的基类构造函数。在派生类对象过期时,程序将首先调用派生类析构函数,然后再调用基类析构函数。

class Father {
public:
    Father(int a) : m_a(a) { cout << "Father constructor(int)" << endl; }
    Father() : m_a(0) { cout << "Father constructor()" << endl; }
    // 如果基类的析构函数不是虚函数,则在多态调用中将只调用对应于指针类型(通常为基类)的析构函数。
    virtual ~Father() {}

private:
    int m_a;
};

class Son : public Father {
public:
    Son(int a, int b) : Father(a), m_b(b) { cout << "Son constructor(int, int)" << endl; }
    Son(int b) : m_b(b) { cout << "Son constructor(int)" << endl; } // 调用基类的默认构造函数
    ~Son() {}

private:
    int m_b;
};


int main()
{
    Son s1(1, 2);
    Son s2(1);
    return 0;
}

/*
输出:
Father constructor(int)
Son constructor(int, int)
Father constructor()
Son constructor(int)
*/

2 多态公有继承

当需要同一个方法在派生类和基类中行为不同时,即方法的行为将取决于调用该方法的对象,此时就需要使用到多态,其实现依赖于两个重要机制:

隐式向上强制转换使得指向基类引用或指针可以指向基类对象或派生类对象。

  • 在派生类中重新定义基类的方法。
  • 使用虚方法。

如果在派生类中重新定义函数,将不是使用相同的函数特征标覆盖基类声明,而是隐藏同名的基类方法,不管参数特征标如何。

但如果返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针,这种特性被称为返回类型协变。

#include <iostream>

using namespace std;

class Father {
public:
    virtual void say() { cout << "I'm father" << endl; }
    virtual void say2(int num) { cout << "I'm father2" << endl; }
};

class Son : public Father {
public:
    virtual void say() { cout << "I'm son" << endl; }
    virtual void say2() { cout << "I'm son2" << endl; }
};


int main()
{
    Father* person1 = new Son();
    person1->say();
    delete person1;

    Son person2;
    person2.say2();
    // person2.say2(1); // 无效调用,编译报错
}

/*
输出:
I'm son
I'm son2
*/

3 抽象基类

C++通过使用纯虚函数提供未实现的函数,包含纯虚函数的类就是抽象基类,不能创建该类的对象。

class AbstactTest {
public:
    virtual void write() = 0;
};

4 私有继承

使用私有继承,基类的公共成员和保护成员都将成为派生类的私有成员。

包含将对象作为一个命名的成员对象添加到类中,而私有继承将对象作为一个未被命名的继承对象添加到类中。因此,私有继承提供的特性与包含相同:获得实现但不获得接口,所以私有继承也可以用来实现has-a关系。

通常,应使用包含来建立has-a关系;如果新类需要访问原有类的保护成员,或需要重新定义虚函数,则应使用私有继承。

#include <iostream>
#include <valarray>

using namespace std;

// 包含
class HasClass {
private:
    string name;
    valarray<int> nums;

public:
    HasClass(const string& str, int num, int value) : name(str), nums(num, value) {}
    const string& Name() { return name; }
    int MaxNum() { return nums.max(); }
};


// 私有继承
class PrivateClass : private string, private valarray<int> {
public:
    PrivateClass(const string& str, int num, int value) : string(str), valarray<int>(num, value) {}
    const string& Name() const
    {
        return (const string&)*this;
    }

    int MaxNum() { return valarray<int>::max(); }

    // 使用using重新定义访问权限
    using valarray<int>::min;
};


int main()
{
    HasClass hc("test1", 3, 2);
    cout << hc.Name() << " " << hc.MaxNum() << endl;

    PrivateClass pc("test2", 4, 2);
    cout << pc.Name() << " " << pc.MaxNum() << " " << pc.min() << endl;

}

5 保护继承

使用保护继承时,基类的公有成员和保护成员都将成为派生类的保护成员。

请添加图片描述

6 多重继承

多重继承可能会造成以下问题:

  • 从两个或更多相关基类那里继承同一个类的多个实例。
  • 从两个不同的基类继承同名方法(需要使用类名限定符或重定义方法)。

6.1 虚基类

虚基类使得从多个类(它们的基类相同)派生出的对象只继承一个基类对象。

如果类有间接虚基类,则除非只需使用该虚基类的默认构造函数,否则必须显式地调用该虚基类的某个构造函数。

#include <iostream>

using namespace std;

class Worker {
    string name;

public:
    Worker(const string& str) : name(str) {}
};

class Waiter : virtual public Worker {
    int monkey;

public:
    Waiter(int m) : monkey(m), Worker(0) {}
};

class Singer : virtual public Worker {
    int vol;

public:
    Singer(int v) : vol(v), Worker(0) {}
};

class SingWaiter : public Waiter, public Singer {
    int num;

public:
    SingWaiter(string str, int m, int v, int n) : Worker(str), Waiter(m), Singer(v), num(n) {}
};


int main()
{
    // 虚基类使得可以直接用基类指针指向派生类对象,而不会因为多重继承产生的多基类实例导致产生二义性
    SingWaiter sw("sw", 1, 2, 3);
    Worker* wk = &sw;
}

7 类模板

类模板用于生成通用的类声明。

#include <iostream>

using namespace std;

template <typename T>
class Stack {
private:
    T item;

public:
    void Set(T i) { this->item = i; }
    T Get();
};

template <typename T>
T Stack<T>::Get()
{
    return item;
}

int main()
{
    Stack<int> s1;
    s1.Set(1);
    cout << s1.Get() << endl;

    Stack<string> s2;
    s2.Set("hello");
    cout << s2.Get() << endl;
}

7.1 模板参数

指定特殊的类型而不是用作通用类型名,称为非类型或表达式参数。

  • 表达式参数可以是整型、枚举、引用或指针。

  • 在实例化模板时,用作表达式参数的值必须是常量表达式。

#include <iostream>

using namespace std;

template <typename T, int n>
class Array {
private:
    T items[n];

public:
    virtual T& operator[] (int i)
    {
        if (i < 0 || i >= n) {
            cout << "error" << endl;
            exit(-1);
        }
        return items[i];
    }
};

int main()
{
    Array<int, 3> a1;
    a1[0] = 1;
    cout << a1[0] << endl;
}

以表达式参数作为数组大小用法为例,优点是直接使用为自动变量维护的内存栈,避免堆内存使用;缺点是每种数组大小都将生成自己的模板。

7.2 默认类型模板参数

可以为类型参数或非类型参数提供默认值。

template <class T1, class T2 = int>
class Test1 {};

template <class T1, int num = 3>
class Test2 {};

7.3 成员模板

模板可用作结构、类或模板类的成员。

template <typename T>
class Outer {
private:
    template <typename V>
    class Inner{
        V value;
    };

    Inner<T> i1;
    Inner<int> i2;
};

7.3 模板用作参数

模板还可以包含本身就是模板的参数。

template <typename T>
class Stack {
private:
    T value;

public:
    T Get() { return value; }
    void Set(T v) { this->value = v; }
};

template <template <typename T> class Thing>
class Crab {
private:
    // 需要显式指定类型
    Thing<int> t;

public:
    void set(int v) { this->t.Set(v); }

    void print()
    {
        cout << t.Get() << endl;
    }
};

int main()
{
    // 接收模板类作为类型参数
    Crab<Stack> ca;
    ca.set(1);
    ca.print();
}

7.4 模板类和友元

模板的友元分为3类:

  • 非模板友元。
  • 约束模板友元,即友元的类型取决于类被实例化时的类型。
  • 非约束模板友元,即友元的所有具体化都是类的每一个具体化的友元。
template <typename T> void Count3(T& t);

template <typename T>
class HasFriend {
    // 1、非模板友元
    friend void Count1();

    // 2、约束模板友元
    friend void Count2(HasFriend<T> hf);
    friend void Count3<>(HasFriend<T> hf);

    // 3、非约束模板友元
    template <typename TT> friend void Count4(TT& t);
};

8 友元类

友元类的所有方法都可以访问原始类的私有成员和保护成员。

#include <iostream>

using namespace std;

class Tv {
public:
    Tv(int n) : num(n) {}

private:
    friend class Remote;
    int num;
};

class Remote {
public:
    int GetTvNum(const Tv& tv) { return tv.num; }
};

int main()
{
    Tv tv(1);
    cout << Remote().GetTvNum(tv) << endl;
}

友元成员函数可以选择仅让特定的类成员称为另一个类的友元。

#include <iostream>

using namespace std;

// 1、前向声明,解决Tv类和Remote类循环依赖的问题
// 在Remote中使用Tv类型时,编译器需要知道Tv是一个类
class Tv;

class Remote {
public:
    // 2、由于当前编译器还不知道Tv类具体实现,因此此处只能使用函数原型
    int GetTvNum(const Tv& tv);
};

// 3、由于Tv类要使用Remote类的成员函数,因此Tv类声明应置于Remote之后
class Tv {
public:
    Tv(int n) : num(n) {}
    friend int Remote::GetTvNum(const Tv& tv);

private:
    int num;
};

inline int Remote::GetTvNum(const Tv& tv)
{
    return tv.num;
}

int main()
{
    Tv tv(1);
    cout << Remote().GetTvNum(tv) << endl;
}

9 嵌套类

在另一个类中声明的类被称为嵌套类,它通过提供新的类型类作用域来避免名称混乱。

请添加图片描述

class Queue {
private:
    class Node {
        private:
            int value;

        public:
            Node(int v) : value(v) {}
    };

    Node* head;
};

10 异常机制

程序有时会遇到运行阶段错误,导致程序无法正常地运行下去,此时程序默认会调用abort()函数来终止程序。

C++异常是对程序运行过程中发生的异常情况的一种响应,异常提供了将控制权从程序的一个部分传递到另一个部分的途径,有3个组成部分:

  • 引发异常。
  • 捕获有处理程序的异常。
  • 使用try块。
#include <iostream>

using namespace std;

// 1、程序在发生异常时,直接调用abort终止
void Test1(int a, int b)
{
    cout << (a / b) << endl;
}

// 2、在程序中对参数进行检查,在参数异常时手动向外抛出异常
void Test2(int a, int b)
{
    if (b == 0) {
        throw "The divisor cannot be 0";
    }
    cout << (a / b) << endl;
}

class MyException {
private:
    string msg;

public:
    MyException(const string& m) : msg(m) {}
    void print() { cout << msg << endl; }
};

// 3、对象也可以作为异常类型抛出
// 异常规范由关键字throw和异常类型列表组成,用于对函数定义进行限定,指出其引发的异常类型
// throw(optional_type_list)句法从C++11已废弃,建议使用noexcept(等价于throw())来替代
void Test3(int a, int b) throw(MyException)
{
    if (b == 0) {
        throw MyException("The divisor cannot be 0");
    }
    cout << (a / b) << endl;
}

int main()
{
    cout << "start" << endl;

    // Test1(1, 0);

    try {
        Test2(1, 0);
    } catch (const char* s) {
        cout << "catch a exception1: " << s << endl;
    }

    try {
        Test3(1, 0);
    } catch (MyException e) {
        cout << "catch a exception2: ";
        e.print();
    }

    cout << "end" << endl;
}

11 RTTI

RTTI是运行阶段类型识别(Runtime Type Identification)的简称,C++有3个支持RTTI的元素:

  • 如果可能的话,dynamic_cast操作符将使用一个指向基类的指针来生成一个指向派生类的指针;否则,该操作符返回0——空指针。
  • typeid 操作符返回一个指出对象类型的值。
  • type_info 结构存储了有关特定类型的信息。

RTTI只适用于包含虚函数的类(原因在于只有对于这种类层次结构,才应该将派生对象的地址赋给基类指针)。

#include <iostream>

using namespace std;

class GrandFarther {
    int n;
public:
    virtual void Say() { cout << "I'm GrandFarther" << endl; }
};

class Farther : public GrandFarther {
public:
    virtual void Say() { cout << "I'm Farther" << endl; }
    void Say2() { cout << "Farther haha" << endl; }
};

class Son : public Farther {
public:
    virtual void Say() { cout << "I'm Son" << endl; }
    void Say3() { cout << "Son xixi" << endl; }
};

void TransToFarther(GrandFarther* g)
{
    Farther* f1 = dynamic_cast<Farther*>(g);
    if (f1 == nullptr) {
        cout << "Farther is null" << endl;
    } else {
        f1->Say2();
    }
}

void TransToSon(GrandFarther* g)
{
    if (typeid(*g) == typeid(Son)) {
        Son* s = (Son*) g;
        s->Say3();
    } else {
        cout << "isn't Son type" << endl;
    }
}

int main()
{
    GrandFarther* g1 = new GrandFarther;
    GrandFarther* g2 = new Farther;
    GrandFarther* g3 = new Son;
    g1->Say();
    g2->Say();
    g3->Say();

    Farther* f1 = dynamic_cast<Farther*>(g1); // null,指向基类对象的指针不能转为派生类指针
    Farther* f2 = dynamic_cast<Farther*>(g2); // ok
    Farther* f3 = dynamic_cast<Farther*>(g3); // ok
    TransToFarther(f1);
    TransToFarther(f2);
    TransToFarther(f3);

    TransToSon(g1); // 不是Son类型
    TransToSon(g2); // 不是Son类型
    TransToSon(g3); // 是Son类型
}

/*
I'm GrandFarther
I'm Farther
I'm Son
Farther is null
Farther haha
Farther haha
isn't Son type
isn't Son type
Son xixi
*/

12 类型转换操作符

  • dynamic_cast:动态转换,能够在类层次结构中进行上下转换。
  • static_cast:静态转换,提供编译器认为安全的类型转换。
  • const_cast:去常转换,去掉指针或引用的常量属性。
  • reinterpret_cast: 类似于C风格的强制类型转换,不安全。

附录1

请添加图片描述

猜你喜欢

转载自blog.csdn.net/lyj_010/article/details/129786931