【C++ Primer Plus】类、运算符重载、虚函数、友元函数模板

发布时间 2023-11-24 22:32:47作者: imXuan

1.运算符重载

1.1 普通运算符重载

  在类内重写operator+函数,实现加号运算符的重载,下面给出了两种调用方式,注意加号前为调用者,加号后为参数,第三行代码的完整写法实际上是第四行

Time Time::operator+(int minutes)const;
Time time;
Time time2 = time+50;
Time time3 = time.operator+(50);

1.2 运用友元实现运算符重载

  上述运算符重载存在一个问题,50 + time 是无效的,因为50没有对应的加法运算符重载,我们可以使用友元解决

  • 虽然 operator+() 函数在类内声明,但它并不是成员函数
  • 虽然 operator+() 不是成员函数,但它与成员函数访问权限相同
// 类内声明友元函数
friend Time operator+(int minutes, const Time& t);
// 类外实现函数
Time operator+(int minutes, const Time& t);
// 调用
Time time2 = 50 + time;
Time time3 = operator+(50, time);

1.3 其他运算符重载

  同样使用友元实现左移运算符的重载

// 类内声明友元函数
friend std::ostream& operator<<(std::ostream& os, const Time& time);
// 类外实现函数
std::ostream& operator<<(std::ostream& os, const Time& time){
    cout << time.hours << " hours, " << time.minutes << " minutes" << endl;
    return os;
}
// 调用
Time time;
cout<<time;

1.4 示例代码

// main.cpp
#include "time.h"
#include <iostream>
int main()
{
    Time time1, time2;
    time1.setHours(1); time1.setMinutes(30);
    time2.setHours(5); time2.setMinutes(55);
    // ----------- 普通的运算符重载 ------------
    Time time3 = time1 + time2;             // 这里 + 号前的是函数调用者
    Time time4 = time1.operator+(time2);    // 上面的函数等同于下面
    // ----------- 这类运算符重载会有调用顺序的问题, 可以用友元解决 --------------
    Time time5 = time1 + 50;                // 这里就会出现一个问题, 只能把time1放在前面, 50放在后面
    Time time6 = 50 + time1;                // 这个实际上也是运算符重载, 但利用了友元, 具体调用是下面的方式
    Time time7 = operator+(50, time1);      // 这里是上面等效的样式
    std::cout << time1 << time2 << time3 << time4 << time5 << time6 << time7;
}

// time.h
#pragma once
#include <ostream>
class Time
{
private:
    int hours;
    int minutes;
public:
    void setHours(int hours) { this->hours = hours; }
    void setMinutes(int minutes) { this->minutes = minutes; }
    Time operator+(const Time& t) const;
    Time operator+(int minutes) const;
    friend Time operator+(int minutes, const Time& t);
    friend std::ostream& operator<<(std::ostream& os, const Time& time);
};

// time.cpp
#include "time.h"
#include <iostream>
using namespace std;

Time Time::operator+(const Time& t)const
{
    Time sum;
    sum.minutes = minutes + t.minutes;
    sum.hours = hours + t.hours + sum.minutes / 60;
    sum.minutes %= 60;
    return sum;
}

Time Time::operator+(int minutes)const
{
    Time sum;
    sum.minutes = this->minutes + minutes;
    sum.hours = hours + sum.minutes / 60;
    sum.minutes %= 60;
    return sum;
}

std::ostream& operator<<(std::ostream& os, const Time& time)
{
    cout << time.hours << " hours, " << time.minutes << " minutes" << endl;
    return os;
}


Time operator+(int minutes, const Time& t)
{
    Time sum;
    sum.minutes = t.minutes + minutes;
    sum.hours = t.hours + sum.minutes / 60;
    sum.minutes %= 60;
    return sum;
}

2. 类和动态内存分配

2.1 静态成员变量初始化

  • 不能在类声明中初始化静态成员变量,因为声明只描述如何分配内存,但不分配内存,我们通过这种格式创建对象,从而分配和初始化内存
  • 静态变量先于对象创建,而对象创建时才去设置这个变量是不合适的
  • 使用const修饰的整数或枚举可以在类的声明中初始化
// ----- student.h -----
class student{
public:
    char* name;
    static int numofstu;
    const static int maxname = 5;    // 如果静态类型是整形或枚举型 const 可以在类内初始化
};
// ----- student.cpp -----
#include "student.h"
int student::numofstu = 0;    // 静态变量初始化必须在类外

2.2 类的初始化

  • 初始化顺序:构造函数中多个项目被初始化的顺序是他们在类中声明的顺序,而不是在初始化列表中的顺序
class Student{
private:
    char* name;
    double scores;
public:
    Student(double scores, const char* str)
                : scores(score), name(str){}    // 仍然是name首先被初始化,因为它首先被声明
}
  • 使用 explicit 防止构造函数的隐式转换,下图给出了如果显示给出 explicit 关键字编译器会报出的错误
#include <iostream>
class Square {
public:
    int x;
    int y;
    Square(int x = 0, int y = 0) :x(x), y(y) {}
};

void printSquare(const Square& s) {
    std::cout << "Square x: " << s.x << " ," << s.y << std::endl;
}
int main(){
    Square s = 1;      // 隐式调用构造函数
    printSquare(2);    // 这里会隐式调用构造函数, 创建一个Square对象传入, 可能引起一些错误
}

2.3 特殊成员函数

  设计类的过程中如果不正确处理c++自动提供的这些函数可能造成很多问题,必须正确处理

  • 默认构造函数
    • 默认构造函数不接受任何参数,也不执行任何操作,没有任何数据的初始化等行为
  • 默认析构函数
    • 默认不做任何处理
    • 注意:构造函数可以存在多个,但析构函数只有一个。所以构造函数中 new 或 new[] 必须统一,析构函数只能是new 或 new[]
  • 复制构造函数
    • 复制构造函数是新建一个对象并将其初始化为同类现有对象时调用的(实际上只要是按值传递都会调用复制构造函数,包括函数传参,返回值)
    • 默认复制构造函数会逐个复制非静态成员(浅复制),对于指针可能造成重复回收,或者另一个对象销毁后本对象指向一个空值等各种问题(只要包括new,就必须重写)
    • 以下声明都会调用复制构造函数
StringBad ditto(mitto);              // StringBad(const StringBad &);
StringBad metoo = motto;             // StringBad(Const StringBad &);
StringBad also = StringBad(motoo)    // StringBad(Const StringBad &);
StringBad * pStringBad = new StringBad(motto);    // StringBad(Const StringBad &);
  • 赋值运算符
    • 默认赋值运算符也会存在浅复制的问题,同样应该重写
    • 以下声明会调用复制运算符
StringBad ditto;    // 首先声明一个变量(调用无参构造函数)
ditto = mitto;      // 调用赋值运算符
  • 地址运算符
    • 一般情况下都没啥问题,特殊情况下可以给取地址运算符返回nullptr避免其他人获取地址

2.4 虚函数

2.4.1 virtual 函数调用

  • 如果不使用 virtual 标记,函数不是虚函数,程序只根据引用类型或指针类型调用方法(根据引用者调用方法)
  • 如果使用 virtual 标记,函数是虚函数,程序将根据实际指向的对象类型来选择方法(根据实际对象调用方法)
    • 基类用virtual标记的函数,派生类不论是否标记都是虚的,但应该标记

// (Banana继承自Fruit,假设他们都有输出方法 View();)
// 假设没有虚函数关键字, 程序将简单的根据指针类型去选择方法
Fruit fruit;
Banana banana;
Fruit & f1 = fruit;    // 根据 Fruit 类型直接调用 Fruit::View()
Fruit & f2 = banana;    // 根据 Fruit 类型直接调用 Fruit::View()
f1.view();   f2.view();   
// 假设有虚函数关键字,程序将根据实际引用的对象选择调用的方法
Fruit fruit;
Banana banana;
Fruit & f1 = fruit;    // 根据 fruit 类型选择调用 Fruit::View()
Fruit & f2 = banana;    // 根据 banana 类型选择调用 Banana::View()
f1.view();   f2.view();   
  • 注意:如果重新定义一个同名的虚函数,不会生成函数的重载版本,子类会隐藏基类的函数版本

    • 所以如果基类的声明被重载了,子类想重新定义一个重载版本,必须重新定义所有重载版本,否则其他的重载将被隐藏!
    • 特殊情况:如果返回值是基类的引用或指针,则子类可以修改为子类的引用和指针,这不会导致隐藏(这种特性是返回类型协变)
// 如果子类重载了方法,父类方法将被隐藏,所以如果子类重载,必须完整实现父类所有的重载方法
class Fruit{
public:
    virtual void show();
}
class Banana{
public:
    virtual void show(int a);
}
Banana banana;
banana.show(5);    // 合法调用
banana.show();     // 非法调用(隐藏同名的基类方法)

Fruit b2 = banana;
b2.show();         // 合法调用,这样才可以调用隐藏的方法
// 对于返回值为基类指针,子类重写返回值指针的情况
// 不论引用者是父类还是子类,都调用对象自己的方法,这种情况就是普通的 virtual 标记的函数
class Fruit{
public:
    virtual Fruit* build();
}
class Banana{
public:
    virtual Banana* build();
}
Banana banana;
Fruit b2= banana;
banana.show();    // 调用 Banana::build()
b2.show();        // 调用Banana::build()

2.4.2 函数传值

  函数传参过程中,引用传递和指针传递都会将对象完整传递过去,而值传递可能只将部分对象传递到函数内

void fr(Fruit & rf);    // rf.View();
void fp(Fruit * pf);    // pf->View();
void fv(Fruit f);       // f.View();
int main(){
    Banana banana;
    fr(banana);    // Banana::View();    隐式进行向上转换
    fp(banana);    // Banana::View();    隐式进行向上转换
    fv(banana);    // Fruit:View();      值传递,只将Fruit部分传递给函数fv
}

2.4.3 虚析构函数

  其实这里的原理与普通的 virtual 关键字声明的函数相同,单独哪出来是为了强调和提醒只要做基类的析构函数都应该是虚函数。

  如果不用 virtual 关键字声明析构函数,则只会调用指针类型指向的析构函数。例如Fruit* 指向一个Banana,但它只会调用Fruit的析构函数,导致内存泄漏。但如果析构函数是虚函数,将调用相应对象的析构函数,然后自动调用基类的析构函数,这样才可以完整释放该类占用的内存。

2.4.4 复制构造函数

  子类的赋值构造函数,在子类有new分配内存的时候也需要重写,但他必须调用基类的复制构造函数来处理基类的数据

hasDMA::hasDMA(const hasDMA & hs) : baseDMA(hs);

2.4.5 赋值运算符

  子类存在 new 动态分配内存的时候,需要重写赋值运算符,但它作为子类的方法,只能访问子类的数据,但派生类必须处理父类的数据来对父类进行赋值

hasDMA & hadDMA::operator=(const hasDMA & hs){
    if(this == &hs)
        return *this
    baseDMA::operator=(hs);    // 显示调用父类的赋值运算符来给父类赋值
    /* ... */
}

2.4.6 静态联编和动态联编

  将源代码中的函数调用解释为指定特定的代码块被称为函数名联编

  • 静态联编:普通的函数以及函数重载都可以在编译过程中完成这种联编(static binding、early binding)
  • 动态联编:编译器必须生成能够在运行时选择正确的虚函数的代码(dynamic binding、late binding)
    • 动态联编通过虚函数表实现,每个类中都有一个隐藏的指针成员vptr,它指向自己这个类的虚函数表
    • 在调用函数的时候,不管指针对象是什么,都通过该对象对应的vptr指针来调用它对应的函数

2.5 继承

  • 使用私有继承时,只能在派生类的方法中使用基类的方法(第三代基类将不能再直接调用)
  • 使用保护继承时,基类的公有成员和保护乘员都将成为派生类的保护乘员(后代仍然可以调用)
  • 使用公有继承时,还是public

 2.5.1 多重继承

  • 多重继承从不同的基类中继承同名方法
  • 多重继承从不同的基类中继承同一个类的多个实例

class Worker{};
class Waiter : public Worker{};
class Singer : public Worker{};
class SingingWaiter : public Waiter, public Singer{};
SingingWaiter ed;
Worker * pw = &ed;    //存在错误

  通常情况下,这种赋值把基类指针设置为派生对象中的基类对象的地址。但是 ed 中包含两个 Worker 对象,有两个地址可以选择,这会产生问题

Worker * pw1 = (Waiter *) &ed;    // Waiter 中的 Worker 对象
Worker * pw2 = (Singer *) &ed;    // Singer 中的 Worker 对象

  同样如果想调用基类中同名的函数也应该显示的声明

ed.Waiter::View();
ed.Singer::View();
// 更好的做法是重新定义View方法或者指明使用哪个版本的View
void SingingWaiter::View(){
    Singer::View();
}

  •  虚基类:虚基类可以让多个基类相同的类,派生出的对象只继承一个基类对象(实际上引入了一种新的规则)
class Singer : virtual public Worker {};
class Waiter : virtual public Worker {};
class SingingWaiter : public Singer, public Waiter {};
// 注意, Waiter(wk, p)和Singer(wk, v) 这两条路径是不会将 wk 传给基类的, 因为两条传输路径存在冲突, 所以会调用基类的默认构造函数
SingingWaiter(const Worker & wk, int p=0, int v = Singer::other)
              : Waiter(wk, p), Singer(wk, v) {}

// 只有显示调用基类的构造函数才可以, 这里是调用复制构造函数的例子
SingingWaiter(const Worker & wk, int p=0, int v = Singer::other)
              : Worker(wk), Waiter(wk, p), Singer(wk, v) {}

2.6 模板

2.6.1 模板示例和非类型参数

  class 指出 T 为类型参数,而后面的 int 指出 n 的类型为 int,这种参数是非类型参数或表达式参数

template<class T, int n>
class Array {
private:
    T data[n];
};

int main(){
    Array<int, 100> m;    // 表达式参数可以是整形、枚举、引用或指针
}

2.6.2 将模板用作参数

  鄙人有些菜,实际使用中一般都是直接传入一个 queue<int> 而不是写下面这种我感觉很复杂的方式

// T2 的模板参数类型是 template<class T> class
template <template <class T> class T2>
class A{
    T2<int> t;
};
template <class T>
class queue{
    T data;
};
int main(){
    A<queue> a;    // 相当于把用queue<int> 替换了T2<int>
}

2.6.3 友元函数

  如果有一个友元函数 View,它的参数是类本身,会存在一个问题,类具体化和友元函数之间没有对应的关系,没有办法直接调用友元函数

friend void View(HasFriend<T> &);    // 不可以这样声明,因为不存在HasFriend这样的对象,只有特定的具体化对象HasFriend<int>等
  • 非模板友元:这种友元函数最简单,直接在声明时写清楚类型
template<class T>
class HasFriend {
public:
    T data;
    friend void View(HasFriend<T>& hf);
};
void View(HasFriend<int>& hf) {
    cout << "模板类的非模板的友元函数,int 类型" << endl;
}
void View(HasFriend<char>& hf) {
    cout << "模板类的非模板的友元函数,char 类型" << endl;
}
int main(){
    HasFriend<int> hf;
    hf.data = 10;    // 因为HasFriend具体化为FasFriend<int>类型,所以它会调用对应的函数
    View(hf);
}
  • 约束(bound)模板友元
    • 声明模板原型 -> 类内声明友元函数 -> 类外实现友元函数
    • 通过给定模板类的 T 类型变量,编译器推断对应的友元函数形式
    • 模板原型只是一个形式,友元函数实现中”长得接近即可“,参见 View() 的第三个重载
// main.cpp
#include "HasFriendT.hpp"

int main()
{
    HasFriendT<char> hf;
    hf.data = 'x';
    View<char>();       // 这里没有办法自动类型推断,需要显示的写
    View(hf);           // 这里存在自动类型推断, 与后面的写法效果相同
    View('c', hf);
    View<char>(hf);
}

// HasFriendT.hpp
#pragma once
#include<iostream>
using namespace std;
// *第一步* 声明模板原型
template<class T> void View();
template<class T> void View(const T& hf);
template<class T1, class T2> void View(T1 t1, T2& t2);

template<class T>
class HasFriendT {
public:
    T data;
    // *第二步* 在模板类中声明友元
    friend void View<T>();
    friend void View<HasFriendT<T>>(const HasFriendT<T>& hf);
    friend void View<T, HasFriendT<T>>(T data, HasFriendT<T>& hf);
};

// *第三步* 根据原型实现模板函数
template<class T>
void View() {
    cout << "sizeof(T): " << sizeof(T) << endl;
}

template<class T>
void View(const HasFriendT<T>& hf) {
    cout << hf.data << endl;
}

template<class T>
void View(T data, HasFriendT<T>& hf) {
    hf.data = data;
}
  • 非约束(unbound)模板友元 
    • 只需要在类内声明一个友元函数,类外去实现,自由的过分
// main.cpp
#include "HasFriendT.hpp"
int main(){
    HasFriendT<char> hf1;
    HasFriendT<int> hf2;
    View(hf1, hf2);
    int a = 1;
    int b = 2;
    View(a, b);
}

// HasFriendT.cpp
#pragma once
#include<iostream>
using namespace std;
template<class T>
class HasFriendT {
public:
    T data;
    template<class T1, class T2> friend void View(T1& t1, T2& t2);
};

template<class T1, class T2>
void View(T1& t1, T2& t2) {
    cout << "友元非约束方式, 这个真是太自由了" << endl;
}