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;
}
};