Skip to content

C++ 第 13 节课:纯虚函数、抽象类、深浅拷贝及智能指针

一、纯虚函数与抽象类 9

1. 纯虚函数:

  • 可以没有函数体的虚函数。
  • 格式:virtual 返回类型 函数名(参数列表) = 0;
  • 作用:将派生类强制实现该函数。

代码示例 1 (无纯虚函数):

c++
class Animal {
public:
    virtual void speak() { std::cout << "Animal speaking..." << std::endl; }
};

class Dog : public Animal {
public:
    // 可以选择不实现 speak() 函数,将继承基类的实现
};

int main() {
    Dog dog;
    dog.speak(); // 输出: Animal speaking...
    return 0;
}

代码示例 2 (有纯虚函数):

c++
class Animal {
public:
    virtual void speak() = 0; // 纯虚函数
};

class Dog : public Animal {
public:
    void speak() override { std::cout << "Woof!" << std::endl; } // 必须实现 speak()
};

int main() {
    Dog dog;
    dog.speak(); // 输出: Woof!
    return 0;
}

2. 抽象类:

  • 包含至少一个纯虚函数的类称为抽象类,也叫做纯虚类。
  • 抽象类不能实例化,即不能创建该类的对象。
  • 抽象类主要用于定义接口,规范派生类的行为。
  • 很多编程语言中的抽象类也叫接口类。

关于“虚”的概念:

  • 虚继承: 解决菱形继承问题,避免数据冗余和二义性。
  • 虚基类: 在虚继承中,被继承的公共基类称为虚基类。
  • 虚函数: 使用 virtual 关键字声明的成员函数,允许在运行时动态绑定(多态)。
  • 纯虚函数: 没有函数体的虚函数,使用 = 0 声明。
  • 纯虚类/抽象类: 包含至少一个纯虚函数的类。

代码示例:

c++
class Shape {
public:
    virtual double getArea() = 0; // 纯虚函数
};

class Circle : public Shape {
private:
    double radius;
public:
    Circle(double r) : radius(r) {}
    double getArea() override { return 3.14159 * radius * radius; }
};

int main() {
    //Shape shape; // 错误: 不能实例化抽象类
    Circle circle(5);
    std::cout << "Circle area: " << circle.getArea() << std::endl;
    return 0;
}

二、浅拷贝和深拷贝 9

1. 浅拷贝:

  • 只拷贝指针成员变量的值(即地址),不拷贝指针指向的内存内容。
  • 两个对象共享相同的内存空间,容易导致内存泄漏或数据不一致。

2. 深拷贝:

  • 不直接拷贝指针成员变量,而是重新申请内存空间,并将原指针指向的内容拷贝到新申请的内存中。
  • 每个对象拥有独立的内存空间,避免了数据共享带来的问题。

3. 默认拷贝构造函数:

  • 编译器默认生成的拷贝构造函数进行浅拷贝。
  • 如果类中有指针成员变量,需要自定义拷贝构造函数来实现深拷贝。

4. 成员指针变量的处理:

  • 构造函数: 要么申请新的内存地址并将内容拷贝,要么将其置为 nullptr
  • 析构函数: 判断指针是否为 nullptr,如果不为空则使用 delete 释放内存。

代码示例 1 (浅拷贝):

c++
class MyClass {
public:
    int* data;
    MyClass(int value) {
        data = new int;
        *data = value;
    }
    // 默认拷贝构造函数 (浅拷贝)
};

int main() {
    MyClass obj1(10);
    MyClass obj2 = obj1; // 浅拷贝

    std::cout << *obj1.data << std::endl; // 输出: 10
    std::cout << *obj2.data << std::endl; // 输出: 10

    delete obj1.data; // 释放内存

    // obj2.data 指向的内存已被释放,再次访问会导致错误
    // std::cout << *obj2.data << std::endl; 

    return 0;
}

代码示例 2 (深拷贝):

c++
class MyClass {
public:
    int* data;
    MyClass(int value) {
        data = new int;
        *data = value;
    }
    // 自定义拷贝构造函数 (深拷贝)
    MyClass(const MyClass& other) {
        data = new int;
        *data = *other.data;
    }
    ~MyClass() {
        delete data;
    }
};

int main() {
    MyClass obj1(10);
    MyClass obj2 = obj1; // 深拷贝

    std::cout << *obj1.data << std::endl; // 输出: 10
    std::cout << *obj2.data << std::endl; // 输出: 10

    delete obj1.data; // 释放 obj1 的内存

    // obj2.data 指向独立的内存空间,访问安全
    std::cout << *obj2.data << std::endl; // 输出: 10

    return 0;
}

三、智能指针和裸指针 8

1. 智能指针:

  • std 标准库提供的类模板,用于管理动态分配的内存。
  • 比裸指针更安全,可以自动释放内存,避免内存泄漏。
  • 头文件: #include <memory>

2. 裸指针:

  • 直接指向内存地址的指针。
  • 需要手动管理内存,容易出现内存泄漏或悬空指针等问题。

3. 常见的智能指针:

  • shared_ptr: 具有引用计数功能,多个 shared_ptr 可以共享同一块内存,当引用计数为 0 时自动释放内存。
  • weak_ptr: "弱" 指针,不拥有对象的生命周期控制权,用于解决 shared_ptr 循环引用的问题。
  • unique_ptr: 独占式拥有对象,不允许复制,保证只有一个 unique_ptr 指向该对象。

代码示例 1 (裸指针):

c++
int main() {
    int* ptr = new int(10);

    // ... 使用 ptr ...

    delete ptr; // 需要手动释放内存

    return 0;
}

代码示例 2 (shared_ptr):

c++
#include <memory>

int main() {
    std::shared_ptr<int> ptr1 = std::make_shared<int>(10); // 推荐使用 make_shared

    std::shared_ptr<int> ptr2 = ptr1; // 引用计数加 1

    std::cout << *ptr1 << std::endl; // 输出: 10
    std::cout << *ptr2 << std::endl; // 输出: 10

    // ptr1 和 ptr2 出作用域时,引用计数减为 0,自动释放内存
    return 0;
}

代码示例 3 (weak_ptr):

c++
#include <memory>

int main() {
    std::shared_ptr<int> sharedPtr = std::make_shared<int>(10);
    std::weak_ptr<int> weakPtr(sharedPtr);

    if (auto spt = weakPtr.lock()) { // 检查 shared_ptr 是否有效
        std::cout << *spt << std::endl; // 如果有效,则输出: 10
    } else {
        std::cout << "The shared_ptr is no longer valid." << std::endl;
    }

    return 0;
}

代码示例 4 (unique_ptr):

c++
#include <memory>

int main() {
    std::unique_ptr<int> ptr1 = std::make_unique<int>(10); 

    // std::unique_ptr<int> ptr2 = ptr1; // 错误: unique_ptr 不允许复制

    std::unique_ptr<int> ptr2 = std::move(ptr1); // 所有权转移到 ptr2

    // ptr1 不再拥有该对象
    if (ptr1) {
        std::cout << *ptr1 << std::endl; 
    } else {
        std::cout << "ptr1 is empty." << std::endl; // 输出: ptr1 is empty.
    }

    std::cout << *ptr2 << std::endl; // 输出: 10

    // ptr2 出作用域时,自动释放内存
    return 0;
}

4. 智能指针的常用操作:

  • get(): 获取内部裸指针。
  • reset(): 释放当前管理的对象,并可选地指向新的对象。
  • operator*operator->: 像普通指针一样访问对象成员。

作业:

以下是一个名为 MyData 的类,完善下面的这个类,实现它的深拷贝构造函数

c++
#include <iostream>

class MyData {
public:
    int* data;
    int size;

    MyData(int s) : size(s) {
        data = new int[size];
        for (int i = 0; i < size; ++i) {
            data[i] = i;
        }
    }

    ~MyData() {
        delete[] data;
    }
};