1.c++类主要知识点一

1.类的定义,类的种类

类的定义

在 C++ 中,类是一种封装数据和操作数据的函数的用户定义类型。类的定义和种类是面向对象编程的基础。

class 关键字:用于定义类。
成员函数:类中的函数,用于操作类的数据。
成员变量:类中的变量,用于存储数据。

类的种类

1) 普通类

普通类是最基本的类,用于封装数据和操作数据的函数。

2) 抽象类

抽象类是包含纯虚函数的类,不能实例化。它通常用于定义接口。
注意: 仅仅含有虚函数的不叫抽象类,必须是纯虚函数

class Shape {
public:
    virtual void draw() const = 0; // 纯虚函数
    virtual ~Shape() {} // 虚析构函数
};

3) 模板类

模板类使用模板参数定义类,可以处理不同类型的数据。

template <typename T>
class MyTemplateClass {
public:
    MyTemplateClass(T value) : value_(value) {}
    T getValue() const {
        return value_;
    }
private:
    T value_;
};

4) 内嵌类

内嵌类是在另一个类中定义的类,通常用于封装内部逻辑。

class OuterClass {
public:
    class InnerClass {
    public:
        void innerFunction() {
            std::cout << "Inner function called" << std::endl;
        }
    };
};

5) 单例类

单例类确保一个类只有一个实例,并提供一个全局访问点。

class Singleton {
public:
    static Singleton& getInstance() {
        static Singleton instance;
        return instance;
    }
    void doSomething() const {
        std::cout << "Doing something" << std::endl;
    }
private:
    Singleton() {}
    // 为什么需要删除拷贝构造函数和赋值运算符?
    // 防止拷贝构造: 如果允许拷贝构造,可以通过复制现有实例来创建新的实例,这会破坏单例的唯一性。
    // 防止赋值操作: 如果允许赋值操作,可以通过赋值语句将一个实例的内容复制到另一个实例,这同样会破坏单例的唯一性。
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};

6) 具体类

具体类是实现了所有纯虚函数的类,可以实例化。

// Shape是抽象类,Circle类中实现纯虚函数draw()
class Circle : public Shape {
public:
    void draw() const override {
        std::cout << "Drawing a circle" << std::endl;
    }
};

7) 混合类

混合类是继承了多个基类的类,通常用于组合多个接口或实现。

class Drawable {
public:
    virtual void draw() const = 0;
};

class Movable {
public:
    virtual void move(int dx, int dy) = 0;
};

class Ball : public Drawable, public Movable {
public:
    void draw() const override {
        std::cout << "Drawing a ball" << std::endl;
    }
    void move(int dx, int dy) override {
        std::cout << "Moving a ball" << std::endl;
    }
};

8) 特殊类

8.1 类模板特化
类模板特化为特定类型提供不同的实现。

template <typename T>
class MyTemplateClass {
public:
    T value_;
};

template <>
class MyTemplateClass<int> {
public:
    int value_;
    void specialFunction() {
        std::cout << "Special function for int" << std::endl;
    }
};

8.2 友元类
友元类可以访问另一个类的私有成员。

class MyClass {
    friend class MyFriendClass;
private:
    int privateVar;
};

class MyFriendClass {
public:
    void accessPrivate(MyClass& obj) {
        std::cout << obj.privateVar << std::endl;
    }
};

2.类的访问修饰符public、protected、private

在C++中,默认情况下,类的成员(包括属性和方法)的访问权限是私有(private)。如果你没有明确指定访问权限,那么类的成员将默认为私有。

1) 访问修饰符

public
    定义:公开成员,可以在类外部访问。
    访问权限:凡是在它下面声明的变量和函数,都可以在类的内部和外部访问。

protected
    定义:受保护成员,只能在类内部和派生类中访问。
    访问权限:凡是在它下面声明的变量和函数,只能在类的内部以及派生类(子类)中访问。

private
    定义:私有成员,只能在类内部访问。
    访问权限:凡是在它下面声明的变量和函数,只能在类的内部访问。可以使用公有成员函数,用于获取私有变量的值。

// 外部访问举例
class MyClass {
public:
    void publicFunction() {} // 公开成员
protected:
    void protectedFunction() {} // 受保护成员
private:
    void privateFunction() {} // 私有成员
};
// 外部代码
int main() {
    MyClass obj;

    obj.publicFunction(); // 允许:外部代码可以访问公开成员
    // obj.protectedFunction(); // 错误:外部代码不能访问受保护成员
    // obj.privateFunction(); // 错误:外部代码不能访问私有成员

    return 0;
}

2) 继承时的访问条件

访问规则

    public 成员:
        public 继承:在派生类中保持 public,外部代码可以访问。
        protected 继承:在派生类中变为 protected,外部代码不能访问。
        private 继承:在派生类中变为 private,外部代码不能访问。
    protected 成员:
        public 继承:在派生类中保持 protected,外部代码不能访问。
        protected 继承:在派生类中保持 protected,外部代码不能访问。
        private 继承:在派生类中变为 private,外部代码不能访问。
    private 成员:
        public 继承:在派生类中不可访问。
        protected 继承:在派生类中不可访问。
        private 继承:在派生类中不可访问。

// 1.public 继承举例
class BaseClass {
public:
    void publicFunction() {}
protected:
    void protectedFunction() {}
private:
    void privateFunction() {}
};

class DerivedClass : public BaseClass {
public:
    void test() {
        publicFunction(); // 可以访问
        protectedFunction(); // 可以访问
        // privateFunction(); // 不能访问
    }
};

3) 友元函数和友元类

友元函数可以访问类的私有和受保护成员。

class MyClass {
    friend void myFriendFunction(MyClass& obj);
private:
    int privateVar;
};

void myFriendFunction(MyClass& obj) {
    std::cout << obj.privateVar << std::endl; // 可以访问私有成员
}

// 访问权限:友元函数可以访问类的私有和受保护成员,但外部代码不能。

友元类的所有成员函数都可以访问类的私有和受保护成员。

class MyClass {
    friend class MyFriendClass;
private:
    int privateVar;
};

class MyFriendClass {
public:
    void accessPrivate(MyClass& obj) {
        std::cout << obj.privateVar << std::endl; // 可以访问私有成员
    }
};

// 访问权限:友元类的所有成员函数都可以访问类的私有和受保护成员,但外部代码不能。

3.类的默认成员函数

在 C++ 中,类可以有多个默认成员函数,这些函数在某些情况下会由编译器自动生成。
这些默认成员函数包括默认构造函数、析构函数、拷贝构造函数、拷贝赋值运算符、移动构造函数和移动赋值运算符。

3.1 默认构造函数

定义:无参数的构造函数,用于初始化对象。
生成条件:
    如果类中没有定义任何构造函数,编译器会自动生成一个默认构造函数。
    如果类中定义了其他构造函数,但没有定义无参数的构造函数,编译器不会自动生成默认构造函数。

示例:
class MyClass {
public:
    MyClass() {} // 显式定义的默认构造函数
};

// 如果类中没有定义任何构造函数:
class MyClass {
    // 没有定义任何构造函数
};
// 编译器会自动生成一个默认构造函数。

3.2 析构函数

定义:用于清理资源,当对象销毁时调用。
生成条件:
    如果类中没有定义析构函数,编译器会自动生成一个默认析构函数。
    默认析构函数会调用基类和成员变量的析构函数。

示例:
class MyClass {
public:
    ~MyClass() {} // 显式定义的析构函数
};

// 如果类中没有定义析构函数:
class MyClass {
    // 没有定义析构函数
};
// 编译器会自动生成一个默认析构函数。

3.3 拷贝构造函数

定义:用于创建对象的副本。
生成条件:
    如果类中没有定义拷贝构造函数,编译器会自动生成一个默认拷贝构造函数。
    默认拷贝构造函数会逐成员进行浅拷贝。

示例:
class MyClass {
public:
    MyClass(const MyClass& other) {} // 显式定义的拷贝构造函数
};

// 如果类中没有定义拷贝构造函数:
class MyClass {
    // 没有定义拷贝构造函数
};
// 编译器会自动生成一个默认拷贝构造函数。

3.4 拷贝赋值运算符

定义:用于将一个对象的内容赋值给另一个对象。
生成条件:
    如果类中没有定义拷贝赋值运算符,编译器会自动生成一个默认拷贝赋值运算符。
    默认拷贝赋值运算符会逐成员进行浅拷贝。

示例:
class MyClass {
public:
    MyClass& operator=(const MyClass& other) { return *this; } // 显式定义的拷贝赋值运算符
};

// 如果类中没有定义拷贝赋值运算符:
class MyClass {
    // 没有定义拷贝赋值运算符
};
// 编译器会自动生成一个默认拷贝赋值运算符。

3.5 移动构造函数(C++11)

定义:用于将资源从一个对象移动到另一个对象,通常用于优化性能。
生成条件:
    如果类中没有定义移动构造函数,且类中没有定义拷贝构造函数、拷贝赋值运算符或析构函数,编译器会自动生成一个默认移动构造函数。
    默认移动构造函数会逐成员进行移动。

示例:
class MyClass {
public:
    MyClass(MyClass&& other) noexcept {} // 显式定义的移动构造函数
};

// 如果类中没有定义移动构造函数:
class MyClass {
    // 没有定义移动构造函数
};
// 编译器会自动生成一个默认移动构造函数(如果满足条件)。

3.6 移动赋值运算符(C++11)

定义:用于将资源从一个对象移动到另一个对象,通常用于优化性能。
生成条件:
    如果类中没有定义移动赋值运算符,且类中没有定义拷贝构造函数、拷贝赋值运算符或析构函数,编译器会自动生成一个默认移动赋值运算符。
    默认移动赋值运算符会逐成员进行移动。

示例:
class MyClass {
public:
    MyClass& operator=(MyClass&& other) noexcept { return *this; } // 显式定义的移动赋值运算符
};

// 如果类中没有定义移动赋值运算符:
class MyClass {
    // 没有定义移动赋值运算符
};
// 编译器会自动生成一个默认移动赋值运算符(如果满足条件)。

4.类的对象和类的指针,以及他们的生命周期

4.1 类的对象

// 1) 创建对象

class MyClass {
public:
    MyClass(int value) : value_(value) {
        std::cout << "Constructor called" << std::endl;
    }
    ~MyClass() {
        std::cout << "Destructor called" << std::endl;
    }
    void display() const {
        std::cout << "Value: " << value_ << std::endl;
    }
private:
    int value_;
};

MyClass obj(10); // 创建对象

// 自动存储期对象:在栈上创建的对象,其生命周期与作用域相同。当作用域结束时,对象自动销毁。

// 2) 对象的生命周期

void myFunction() {
    MyClass obj(10); // 自动存储期对象
    obj.display();
} // obj 在函数结束时自动销毁

// 自动存储期对象:在函数内部创建的对象,其生命周期与函数的作用域相同。当函数结束时,对象自动销毁。

4.2 类的指针

// 1) 创建指针

MyClass* ptr = new MyClass(10); // 创建指针
ptr->display();

// 指针:指针是一个变量,存储了对象的地址。通过指针可以间接访问对象。

// 2) 指针的生命周期

void myFunction() {
    MyClass* ptr = new MyClass(10); // 动态存储期对象
    ptr->display();
    delete ptr; // 手动销毁对象
} // ptr 在函数结束时自动销毁,但对象需要手动销毁

// 指针的生命周期:指针本身在栈上,其生命周期与作用域相同。但指针指向的对象在堆上,需要手动销毁。
// 对象的生命周期:指针指向的对象在堆上,其生命周期由程序员控制。必须使用 delete 关键字手动销毁对象,以避免内存泄漏。

4.3 常量对象和常量指针

// 1) 常量对象

const MyClass obj(10); // 常量对象
obj.display(); // 只能调用常量成员函数
// 常量对象:常量对象的成员变量和成员函数不能被修改。只能调用常量成员函数。

// 2) 常量指针

const MyClass* ptr = new MyClass(10); // 常量指针
ptr->display(); // 只能调用常量成员函数
delete ptr; // 手动销毁对象
// 常量指针:常量指针指向的对象不能被修改。只能调用常量成员函数。

4.4 智能指针

#include <memory>
void myFunction() {
    std::unique_ptr<MyClass> ptr = std::make_unique<MyClass>(10); // 智能指针
    ptr->display();
} // ptr 在函数结束时自动销毁,对象也会自动销毁

// 智能指针:智能指针(如 std::unique_ptr 和 std::shared_ptr)自动管理对象的生命周期,避免手动管理内存。
// 生命周期:智能指针在栈上,其生命周期与作用域相同。智能指针指向的对象在堆上,但智能指针会自动销毁对象,避免内存泄漏。

总结

    对象的生命周期:
        自动存储期对象:在栈上,生命周期与作用域相同,自动销毁。
        动态存储期对象:在堆上,生命周期由程序员控制,需要手动销毁。
    指针的生命周期:
        指针本身:在栈上,生命周期与作用域相同,自动销毁。
        指针指向的对象:在堆上,生命周期由程序员控制,需要手动销毁。
    智能指针:自动管理对象的生命周期,避免手动管理内存,推荐使用。

5.普通类继承以及构造和析构顺序

5.1 没有虚函数的普通类继承以及构造和析构顺序-单继承

// 1) 没有虚函数的普通类继承
class BaseClass {
public:
    BaseClass() {
        std::cout << "BaseClass constructor called" << std::endl;
    }
    ~BaseClass() {
        std::cout << "BaseClass destructor called" << std::endl;
    }
    void baseFunction() {
        std::cout << "BaseClass function called" << std::endl;
    }
};

class DerivedClass : public BaseClass {
public:
    DerivedClass() {
        std::cout << "DerivedClass constructor called" << std::endl;
    }
    ~DerivedClass() {
        std::cout << "DerivedClass destructor called" << std::endl;
    }
    void derivedFunction() {
        std::cout << "DerivedClass function called" << std::endl;
    }
};

// 2) 构造函数的调用顺序
//    基类的构造函数:首先调用基类的构造函数。
//    派生类的构造函数:然后调用派生类的构造函数。
// 3) 析构函数的调用顺序
//    派生类的析构函数:首先调用派生类的析构函数。
//    基类的析构函数:然后调用基类的析构函数。
// 这种顺序确保了对象的基类部分在派生类部分之前被初始化,并且在销毁对象时,派生类部分先被销毁,然后是基类部分。
int main() {
    DerivedClass obj;
    return 0;
}
// BaseClass constructor called
// DerivedClass constructor called
// DerivedClass destructor called
// BaseClass destructor called

5.2 没有虚函数的普通类继承以及构造和析构顺序-多重继承

一个类可以继承多个基类,这称为多重继承。多重继承允许一个派生类继承多个基类的成员函数和成员变量。然而,多重继承也带来了一些复杂性,例如菱形继承问题。

class BaseClass1 {
public:
    BaseClass1() {
        std::cout << "BaseClass1 constructor called" << std::endl;
    }
    ~BaseClass1() {
        std::cout << "BaseClass1 destructor called" << std::endl;
    }
    void baseFunction1() {
        std::cout << "BaseClass1 function called" << std::endl;
    }
};

class BaseClass2 {
public:
    BaseClass2() {
        std::cout << "BaseClass2 constructor called" << std::endl;
    }
    ~BaseClass2() {
        std::cout << "BaseClass2 destructor called" << std::endl;
    }
    void baseFunction2() {
        std::cout << "BaseClass2 function called" << std::endl;
    }
};

class DerivedClass : public BaseClass1, public BaseClass2 {
public:
    DerivedClass() {
        std::cout << "DerivedClass constructor called" << std::endl;
    }
    ~DerivedClass() {
        std::cout << "DerivedClass destructor called" << std::endl;
    }
    void derivedFunction() {
        std::cout << "DerivedClass function called" << std::endl;
    }
};

int main() {
    DerivedClass obj;
    return 0;
}

// 输出结果
// BaseClass1 constructor called
// BaseClass2 constructor called
// DerivedClass constructor called
// DerivedClass destructor called
// BaseClass2 destructor called
// BaseClass1 destructor called

// 构造顺序
//     基类的构造函数:按照继承列表中的顺序调用基类的构造函数。在上面的例子中,BaseClass1 的构造函数先被调用,然后是 BaseClass2 的构造函数。
//     派生类的构造函数:在所有基类的构造函数调用完成后,派生类的构造函数被调用。

// 析构顺序
//     派生类的析构函数:首先调用派生类的析构函数。
//     基类的析构函数:按照继承列表中的逆序调用基类的析构函数。在上面的例子中,BaseClass2 的析构函数先被调用,然后是 BaseClass1 的析构函数。

5.3 没有虚函数的普通类继承以及构造和析构顺序-菱形继承问题

多重继承的一个常见问题是菱形继承。当两个基类继承自同一个祖先类时,派生类会继承两个基类的祖先类部分,这可能导致祖先类的成员被继承两次。

class BaseClass {
public:
    BaseClass() {
        std::cout << "BaseClass constructor called" << std::endl;
    }
    ~BaseClass() {
        std::cout << "BaseClass destructor called" << std::endl;
    }
    void baseFunction() {
        std::cout << "BaseClass function called" << std::endl;
    }
};
class DerivedClass1 : public BaseClass {
public:
    DerivedClass1() {
        std::cout << "DerivedClass1 constructor called" << std::endl;
    }
    ~DerivedClass1() {
        std::cout << "DerivedClass1 destructor called" << std::endl;
    }
};
class DerivedClass2 : public BaseClass {
public:
    DerivedClass2() {
        std::cout << "DerivedClass2 constructor called" << std::endl;
    }
    ~DerivedClass2() {
        std::cout << "DerivedClass2 destructor called" << std::endl;
    }
};
class FinalClass : public DerivedClass1, public DerivedClass2 {
public:
    FinalClass() {
        std::cout << "FinalClass constructor called" << std::endl;
    }
    ~FinalClass() {
        std::cout << "FinalClass destructor called" << std::endl;
    }
};
// 输出结果
// BaseClass constructor called
// DerivedClass1 constructor called
// BaseClass constructor called
// DerivedClass2 constructor called
// FinalClass constructor called
// FinalClass destructor called
// DerivedClass2 destructor called
// BaseClass destructor called
// DerivedClass1 destructor called
// BaseClass destructor called

5.4 没有虚函数的普通类继承以及构造和析构顺序-解决菱形继承问题: 虚继承

class BaseClass {
public:
    BaseClass() {
        std::cout << "BaseClass constructor called" << std::endl;
    }
    ~BaseClass() {
        std::cout << "BaseClass destructor called" << std::endl;
    }
    void baseFunction() {
        std::cout << "BaseClass function called" << std::endl;
    }
};

class DerivedClass1 : virtual public BaseClass {
public:
    DerivedClass1() {
        std::cout << "DerivedClass1 constructor called" << std::endl;
    }
    ~DerivedClass1() {
        std::cout << "DerivedClass1 destructor called" << std::endl;
    }
};

class DerivedClass2 : virtual public BaseClass {
public:
    DerivedClass2() {
        std::cout << "DerivedClass2 constructor called" << std::endl;
    }
    ~DerivedClass2() {
        std::cout << "DerivedClass2 destructor called" << std::endl;
    }
};

class FinalClass : public DerivedClass1, public DerivedClass2 {
public:
    FinalClass() {
        std::cout << "FinalClass constructor called" << std::endl;
    }
    ~FinalClass() {
        std::cout << "FinalClass destructor called" << std::endl;
    }
};
// 输出结果
// BaseClass constructor called
// DerivedClass1 constructor called
// DerivedClass2 constructor called
// FinalClass constructor called
// FinalClass destructor called
// DerivedClass2 destructor called
// DerivedClass1 destructor called
// BaseClass destructor called

6.虚函数、静态绑定、动态绑定

6.1 虚函数、方法隐藏(Method Hiding)、方法覆盖

C++ 中,虚函数(Virtual Function)是实现多态(Polymorphism)的关键机制。虚函数允许派生类重写基类的函数,从而在运行时根据对象的实际类型调用相应的函数。

1. 虚函数的作用

// 1.1 实现多态: 虚函数的主要作用是实现多态。多态允许通过基类的指针或引用调用派生类的函数,从而实现动态绑定(Dynamic Binding)。
class Base {
public:
    virtual void display() {
        std::cout << "Base display" << std::endl;
    }
};
class Derived : public Base {
public:
    void display() override {
        std::cout << "Derived display" << std::endl;
    }
};
void callDisplay(Base* obj) {
    obj->display(); // 调用虚函数
}

int main() {
    Derived obj;
    callDisplay(&obj); // 输出 "Derived display"
    return 0;
}
// 多态:通过基类的指针或引用调用虚函数时,会根据对象的实际类型调用相应的函数。
// 动态绑定:在运行时根据对象的实际类型调用相应的函数。
// 思考: 如果直接使用派生类的指针或引用,代码会变得硬编码,难以扩展
void callDisplay(Derived* obj) {
    obj->display(); // 调用派生类的函数
}
// 这种写法限制了函数只能处理 Derived 类型的对象,无法处理其他派生自 Base 的类的对象。这使得代码缺乏通用性和可扩展性。


// 1.2 提供默认实现: 虚函数可以提供默认实现,派生类可以选择性地重写这些函数。如果派生类没有重写虚函数,将使用基类的默认实现。
class Base {
public:
    virtual void display() {
        std::cout << "Base display" << std::endl;
    }
};
class Derived : public Base {
    // 没有重写 display 函数
};
int main() {
    Derived obj;
    obj.display(); // 输出 "Base display"
    return 0;
}
// 默认实现:基类的虚函数提供了默认实现,派生类可以选择性地重写。

2.虚函数的特性

// 2.1 动态绑定: 虚函数的调用是在运行时根据对象的实际类型进行的,这称为动态绑定。
Base* obj = new Derived();
obj->display(); // 输出 "Derived display"
// 动态绑定:在运行时根据对象的实际类型调用相应的函数。

// 2.2 虚函数表(V-Table): 每个包含虚函数的类都有一个虚函数表(V-Table),用于存储虚函数的地址。对象在内存中包含一个指向其类的 V-Table 的指针,通过这个指针调用虚函数。
// V-Table:每个类的 V-Table 存储了虚函数的地址。
// 对象的 V-Table 指针:对象在内存中包含一个指向其类的 V-Table 的指针。

// 2.3 虚函数的覆盖和隐藏: 派生类可以覆盖基类的虚函数,也可以隐藏基类的非虚函数。
class Base {
public:
    virtual void display() {
        std::cout << "Base display" << std::endl;
    }
    void show() {
        std::cout << "Base show" << std::endl;
    }
};
class Derived : public Base {
public:
    void display() override {
        std::cout << "Derived display" << std::endl;
    }
    void show() {
        std::cout << "Derived show" << std::endl;
    }
};
int main() {
    Derived obj;
    obj.display(); // 输出 "Derived display"
    obj.show(); // 输出 "Derived show"
    return 0;
}
// 覆盖:派生类重写基类的虚函数。
// 隐藏:派生类隐藏基类的非虚函数。

3.虚析构函数: 如果基类有虚析构函数,派生类的析构函数会自动调用基类的析构函数。这确保了在销毁对象时,基类和派生类的资源都能被正确释放。

// 问题点: 如果基类没有虚析构函数
class Base {
public:
    ~Base() {
        std::cout << "Base destructor" << std::endl;
    }
};
class Derived : public Base {
public:
    ~Derived() {
        std::cout << "Derived destructor" << std::endl;
    }
};
int main() {
    Base* obj = new Derived();
    delete obj; // 只调用 Base 的析构函数
    return 0;
}
// 输出结果
// Base destructor

// 问题分析
//     基类没有虚析构函数:当通过基类指针删除派生类对象时,只会调用基类的析构函数,而不会调用派生类的析构函数。
//     资源泄漏:如果派生类中分配了动态资源(如动态内存、文件句柄、网络连接等),这些资源不会被正确释放,导致资源泄漏。
//     未定义行为:在某些情况下,可能会导致程序崩溃或其他未定义行为。

// 解决方法
// 使用虚析构函数: 为基类提供一个虚析构函数,确保在删除派生类对象时,派生类的析构函数会被正确调用。

4.总结

    多态:通过基类的指针或引用调用派生类的函数。
    动态绑定:在运行时根据对象的实际类型调用相应的函数。
    默认实现:基类的虚函数可以提供默认实现。
    纯虚函数和抽象类:纯虚函数用于定义接口,包含纯虚函数的类称为抽象类。
    V-Table:每个类的 V-Table 存储了虚函数的地址。
    覆盖和隐藏:派生类可以覆盖基类的虚函数,也可以隐藏基类的非虚函数。
    虚析构函数:确保在销毁对象时,基类和派生类的资源都能被正确释放。

5.思考: 使用了虚函数,派生类才能重写父类的方法,如果没有使用虚函数,那么不能重写吗 ----方法隐藏、方法覆盖

// 在 C++ 中,即使没有使用虚函数,派生类仍然可以“重写”基类的方法,但这并不是真正的多态意义上的重写。
// 这种行为称为方法隐藏(Method Hiding),而不是方法覆盖(Method Overriding)。

1) 方法覆盖(Method Overriding)
// 方法覆盖是指派生类中的方法覆盖了基类中的同名方法。这要求基类的方法是虚函数(virtual)。当通过基类的指针或引用调用虚函数时,会根据对象的实际类型调用相应的函数,这称为动态绑定(Dynamic Binding)。
class Base {
public:
    virtual void display() {
        std::cout << "Base display" << std::endl;
    }
};
class Derived : public Base {
public:
    void display() override {
        std::cout << "Derived display" << std::endl;
    }
};
void callDisplay(Base* obj) {
    obj->display(); // 调用虚函数
}
int main() {
    Derived obj;
    callDisplay(&obj); // 输出 "Derived display"
    return 0;
}
// 输出结果 "Derived display"
// 动态绑定:通过基类的指针或引用调用虚函数时,会根据对象的实际类型调用相应的函数。
// override:显式表示派生类的方法覆盖了基类的虚函数。

2) 方法隐藏(Method Hiding)
// 方法隐藏是指派生类中的方法隐藏了基类中的同名方法。这不需要基类的方法是虚函数。当通过派生类的对象调用方法时,会调用派生类中的方法,而不是基类中的方法。这种行为称为静态绑定(Static Binding)。
class Base {
public:
    void display() {
        std::cout << "Base display" << std::endl;
    }
};
class Derived : public Base {
public:
    void display() {
        std::cout << "Derived display" << std::endl;
    }
};
int main() {
    Derived obj;
    obj.display();      // 输出结果: Derived display 静态绑定: Derived对象,所以调用Derived方法

    Base base;
    base.display();     // 输出结果: Base display    静态绑定: Base对象,所以调用Base方法

    Base* basePtr = new Derived(); // 指针类型:basePtr 是 Base 类型的指针。对象类型:basePtr 指向的是 Derived 类型的对象。
    basePtr->display(); // 输出结果: Base display    静态绑定: 通过基类指针调用方法时,会根据指针的静态类型(而不是对象的实际类型)来决定调用哪个方法。
    delete basePtr;

    return 0;
}

// 静态绑定:通过派生类的对象调用方法时,会调用派生类中的方法。
// 方法隐藏:派生类中的方法隐藏了基类中的同名方法。

3) 区别
// 3.1 虚函数 vs 非虚函数
//     虚函数:基类的方法是虚函数,派生类可以覆盖基类的方法,实现多态。
//     非虚函数:基类的方法不是虚函数,派生类可以隐藏基类的方法,但不会实现多态。

// 3.1 总结
//     方法覆盖(Method Overriding):
//         虚函数:基类的方法是虚函数。
//         动态绑定:通过基类的指针或引用调用虚函数时,会根据对象的实际类型调用相应的函数。
//         多态:实现多态,允许通过基类的指针或引用调用派生类的方法。
//     方法隐藏(Method Hiding):
//         非虚函数:基类的方法不是虚函数。
//         静态绑定:通过派生类的对象调用方法时,会调用派生类中的方法,而不是基类中的方法。
//         非多态:不会实现多态,通过基类的指针或引用调用方法时,只会调用基类中的方法。

6.2 静态绑定、动态绑定

静态绑定,是指成员函数的地址在编译期就可以确定,运行时则直接跳转到对应地址执行;

特点
    编译时确定:函数调用在编译时就已经确定,不会在运行时改变。
    类型信息:编译器根据变量的声明类型来决定调用哪个函数。
    效率:静态绑定通常比动态绑定更高效,因为编译器可以直接生成调用特定函数的代码,而不需要在运行时进行额外的查找。

Base* obj = new Derived();
obj->display(); // 调用 Base 的 display 函数

动态绑定则是指编译期不确定,只有到运行时才能找到函数地址,这需要两次额外的寻址指令:第1次找到虚表,第2次从虚表中找到函数地址。

特点
    运行时确定:函数调用在运行时根据对象的实际类型决定,而不是在编译时。
    虚函数表(V-Table):每个包含虚函数的类都有一个虚函数表(V-Table),用于存储虚函数的地址。对象在内存中包含一个指向其类的 V-Table 的指针,通过这个指针调用虚函数。
    灵活性:动态绑定允许通过基类的指针或引用调用派生类的函数,从而实现多态。

Base* obj = new Derived();
obj->display(); // 调用 Derived 的 display 函数

哪些情况会出现动态绑定?答案是只有使用指针或引用调用虚成员函数时才会出现。

动态绑定的实现

动态绑定的实现条件:
    类的定义中成员函数声明为虚函数
    通过引用或指针来访问对象的虚函数

virtual声明需要注意:
    一旦在基类中指定某成员函数为虚函数,那么,不管在派生类中是否给出virtual声明,派生类(以及派生类的派生类,…)中对其重定义的成员函数均为虚函数
    重定义: 对派生类中定义的成员函数, 其函数名, 参数个数和类型以及返回值类型与基类的某个虚成员函数相同(override)

使用场景

    静态绑定:
        非多态场景:当你不需要多态,且希望函数调用尽可能高效时。
        简单类型:适用于没有继承层次结构的简单类型。
    动态绑定:
        多态场景:当你需要通过基类的指针或引用调用派生类的函数时。
        继承层次结构:适用于有继承层次结构的类,特别是需要实现多态的场景。

7.含有虚函数的类/抽象类的构造和析构顺序

7.1 含有虚函数的类的构造和析构顺序

// 1. 构造函数的调用顺序
// 当创建一个派生类的对象时,构造函数的调用顺序如下:
//     基类的构造函数:首先调用基类的构造函数。
//     派生类的构造函数:然后调用派生类的构造函数。

// 2. 析构函数的调用顺序
// 当销毁一个派生类的对象时,析构函数的调用顺序如下:
//     派生类的析构函数:首先调用派生类的析构函数。
//     基类的析构函数:然后调用基类的析构函数。
class Base {
public:
    Base() {
        std::cout << "Base constructor called" << std::endl;
    }
    virtual ~Base() {
        std::cout << "Base destructor called" << std::endl;
    }
    virtual void display() {
        std::cout << "Base display" << std::endl;
    }
};

class Derived : public Base {
public:
    Derived() {
        std::cout << "Derived constructor called" << std::endl;
    }
    ~Derived() {
        std::cout << "Derived destructor called" << std::endl;
    }
    void display() override {
        std::cout << "Derived display" << std::endl;
    }
};

int main() {
    Derived obj;
    return 0;
}
// 输出结果
// Base constructor called
// Derived constructor called
// Derived destructor called
// Base destructor called


// 3. 虚函数的调用
// 在构造函数和析构函数中,虚函数的调用与普通函数的调用类似。然而,由于虚函数的多态性,通过基类的指针或引用调用虚函数时,会根据对象的实际类型调用相应的函数。
void callDisplay(Base* obj) {
    obj->display(); // 调用虚函数
}
int main() {
    Derived obj;
    callDisplay(&obj); // 输出 "Derived display"
    return 0;
}
// 输出结果
// Derived display

7.2 抽象类的构造和析构顺序

抽象类是包含纯虚函数的类,不能直接实例化。然而,派生类可以继承抽象类,并实现纯虚函数。抽象类的构造和析构顺序与普通类类似。

1. 为什么抽象类不能实例化?
抽象类不能实例化的原因是它包含纯虚函数,而纯虚函数没有实现。纯虚函数的作用是定义接口,要求派生类必须实现这些接口。
如果允许直接实例化抽象类,那么在调用纯虚函数时,程序将不知道调用哪个实现,因为抽象类本身没有提供实现。

2. 总结
    实例化:根据类的定义创建一个具体的对象。
    抽象类:包含一个或多个纯虚函数的类,不能直接实例化。
    派生类:继承自抽象类的类,必须实现抽象类中的纯虚函数,才能被实例化。

3.抽象类不能被直接实例化,但可以创建一个指向派生类对象的指针或引用。这是实现多态的关键机制。通过基类的指针或引用,可以调用派生类的实现,从而实现动态绑定和多态行为。
class AbstractBase {
public:
    AbstractBase() {
        std::cout << "AbstractBase constructor called" << std::endl;
    }
    virtual void display() = 0; // 纯虚函数
    virtual ~AbstractBase() {
        std::cout << "AbstractBase destructor called" << std::endl;
    }
};
class Derived : public AbstractBase {
public:
    Derived() {
        std::cout << "Derived constructor called" << std::endl;
    }
    void display() override {
        std::cout << "Derived display" << std::endl;
    }
    ~Derived() {
        std::cout << "Derived destructor called" << std::endl;
    }
};

int main() {
    Derived obj; // 实例化派生类
    AbstractBase* basePtr = &obj; // 创建指向派生类对象的基类指针
    basePtr->display(); // 调用派生类的实现

    // AbstractBase base; // 编译错误: 不能实例化
    // AbstractBase* base = new AbstractBase(); // 编译错误: 不能实例化
    AbstractBase* base = new Derived();

    return 0;
}
// 输出结果
// AbstractBase constructor called
// Derived constructor called
// Derived display
// Derived destructor called
// AbstractBase destructor called
// AbstractBase* base = new Derived();输出结果: 只有构造,因为指针没有删除,所有没有调用析构函数,需要手动删除
// AbstractBase constructor called
// Derived constructor called
posted on 2025-03-06 10:42  JJ_S  阅读(54)  评论(0)    收藏  举报