Dynarray
@heyx2025@shanghaitech.edu.cn @lichf2025@shanghaitech.edu.cn
heyx2025@shanghaitech.edu.cn
lichf2025@shanghaitech.edu.cn
实现一个“动态数组”:
int n; std::cin >> n; Dynarray arr(n); // arr has n value-initialized ints. for (int i = 0; i != n; ++i) std::cin >> arr.at(i); // arr.at(i) is just like arr[i] // 如何做到 arr[i]?以后再说 for (std::size_t i = 0; i != arr.size(); ++i) arr.at(i) += arr.at(i - 1) * 2; // ...
最后 arr 应当自己释放自己所占用的内存。
arr
class Dynarray { std::size_t m_size; int *m_storage; public: bool empty() const { return m_size == 0u; } std::size_t size() const { return m_size; } };
构造函数定义的是初始化的方式。
Dynarray a; // 对应默认构造函数 Dynarray() Dynarray b(n); // 对应构造函数 Dynarray(std::size_t) Dynarray c(n, x); // 对应构造函数 Dynarray(std::size_t, int)
class Dynarray { public: Dynarray(); Dynarray(std::size_t); Dynarray(std::size_t, int); };
假如我们需要一个默认构造函数,它应当具有什么行为?
0
DYNARRAY_DEFAULT_SIZE
= default
Dynarray a, b{}; Dynarray *c = new Dynarray, *d = new Dynarray(), *e = new Dynarray{}; Dynarray f[1000]; std::array<Dynarray, 1000> g; // 这是啥?
如果没有其它自定义的构造函数,或者我们显式地以 = default 要求编译器合成一个默认构造函数:
Dynarray 必须在自己不再被使用的时候释放所拥有的内存:
void fun() { Dynarray a(n); if (condition) { Dynarray b(m); // ... } // Now b should release the memory it allocated. // ... } // Now a should release the memory it allocated.
析构函数是在这个对象被销毁的时候自动调用的函数。
class Student { std::string name; std::string id; int entranceYear; public: ~Student() { std::cout << "dtor of " << name << "called.\n"; } };
{ Student alice("Alice", "2020123123"); if (condition) { Student bob("Bob", "2020123124"); // ... } // Output "dtor of Bob called." } // Output "dtor of Alice called."
~ClassName
非常直接的实现
class Dynarray { std::size_t m_size; int *m_storage; public: Dynarray(std::size_t n) : m_size(n), m_storage(new int[n]{}) {} ~Dynarray() { delete[] m_storage; } };
考虑一个问题:Student 是否拥有什么资源?
Student
class Student { std::string name; std::string id; int entranceYear; // ... };
Student 本身不拥有什么资源,但是 std::string 有。
std::string
在一个 Student 被销毁的时候:
析构函数在执行完函数体之后,会自动销毁它的所有数据成员
对比一下构造函数:
对于成员的销毁是不需要我们写的,会自动完成。
class Student { std::string name; std::string id; int entranceYear; public: ~Student() {} // 编译器会在最后插入代码来调用 id 和 name 的析构函数。 };
Student 类的析构函数只需一个空函数体即可。
等价的写法:~Student() = default;
~Student() = default;
在任何一个成员函数的函数体中,这个对象的所有数据成员都处在已经初始化、未被析构的状态下。
初学者典型的错误:
class Student { std::string name; std::string id; int entranceYear; public: ~Student() { // 完全误解了析构函数的函数体的用途。 delete this; } };
可以,但没必要:
class Student { std::string name; std::string id; int entranceYear; public: ~Student() { // 毫无意义的行为。 name.clear(); id.clear(); entranceYear = 0; } };
class Dynarray { std::size_t m_size; int *m_storage; public: ~Student() { delete[] m_storage; delete this; // 错误! } };
class Dynarray { std::size_t m_size; int *m_storage; public: ~Student() { delete[] m_storage; // 下面的赋值毫无意义。 m_size = 0; m_storage = nullptr; } };
如果一个类的析构函数是不可调用的,就意味着它无法被销毁
C++ 不允许定义这样的类类型的对象!
什么情况下析构函数不可调用?
~ClassName() = delete;
private
friend
如果一个类的析构函数是不可用的,创建这个类型的对象的唯一方式是 new ,但你还没法 delete 它。
new
delete
void drawUI(/* ... */) { hide_cursor(); disable_input_echo(); // 画一些东西 ... // 要记得恢复光标和输入显示 show_cursor(); enable_input_echo(); }
void loadMap(const char *path) { FILE *file = fopen(path, "r"); // 从文件中读取一些内容 ... fclose(file); }
void criticalSection(std::mutex &m) { m.lock(); // ... m.unlock(); }
struct PrinterGuard { PrinterGuard() { hide_cursor(); disable_input_echo(); } ~PrinterGuard() { show_cursor(); enable_input_echo(); } }; void drawUI(/* ... */) { PrinterGuard guard; // 画一些东西 ... // guard 被销毁,光标和输入显示被恢复 }
void loadMap(const std::string &path) { std::ifstream file(path); // ... // std::ifstream 的析构函数会关闭文件 }
void criticalSection(std::mutex &m) { std::lock_guard guard(m); // ... // std::lock_guard 的析构函数会解锁 m }
std::string *p = new std::string("Hello"); std::cout << *p << std::endl; delete p; p = new std::string; // 调用默认构造函数,*p 为空串 "" std::cout << p->size() << std::endl; delete p;
new 表达式会先分配内存,然后构造对象。
malloc
calloc
std::string *p = new std::string("Hello"); std::cout << *p << std::endl; delete p; p = new std::string; // 调用了默认构造函数,*p 为空串 "" std::cout << p->size() << std::endl; delete p;
delete 表达式会先销毁对象,然后释放这片内存。
free
.at(i) 应该返回什么?
.at(i)
class Dynarray { std::size_t m_size; int *m_storage; public: ??? at(std::size_t n) { return m_storage[n]; } };
arr.at(0) = 42; std::cout << arr.at(1);
如果我们希望通过 arr.at(i) 来修改这个元素,那必然要返回引用。
arr.at(i)
class Dynarray { std::size_t m_size; int *m_storage; public: int &at(std::size_t n) { return m_storage[n]; } };
这样可以吗?
试一试:
void print(const Dynarray &arr) { for (std::size_t i = 0; i != arr.size(); ++i) std::cout << arr.at(i) << ' '; }
无法编译:at 不是 const 成员函数,无法在 const Dynarray & 上调用!
at
const
const Dynarray &
this
加个 const?
class Dynarray { std::size_t m_size; int *m_storage; public: int &at(std::size_t n) const { return m_storage[n]; } };
结果是在 const Dynarray 上,你可以得到其中元素的 non-const 引用,进而修改它!
const Dynarray
正确的解决方案:const 和 non-const 的重载
class Dynarray { std::size_t m_size; int *m_storage; public: const int &at(std::size_t n) const { return m_storage[n]; } int &at(std::size_t n) { return m_storage[n]; } };
class Dynarray { public: const int &at(std::size_t n) const { return m_storage[n]; } int &at(std::size_t n) { return m_storage[n]; } }; arr.at(i) = 42;
// 左边的代码就如同: const int &at(const Dynarray *this, std::size_t n) { return this->m_storage[n]; } int &at(Dynarray *this, std::size_t n){ return this->m_storage[n]; } at(&arr, i) = 42;
假如我们要模仿标准库的行为的话,at 函数应该提供边界检查...
class Dynarray { public: const int &at(std::size_t n) const { if (n >= m_length) // 为什么不需要判断 n < 0? throw std::out_of_range{"Dynarray subscript out of range."}; return m_storage[n]; } int &at(std::size_t n) { if (n >= m_length) throw std::out_of_range{"Dynarray subscript out of range."}; return m_storage[n]; } };
假如我们还想在 at 访问中做一些其它的记录和检查...
class Dynarray { public: const int &at(std::size_t n) const { if (n >= m_length) throw std::out_of_range{"Dynarray subscript out of range."}; log_access(); verify_integrity(); return m_storage[n]; } int &at(std::size_t n) { if (n >= m_length) throw std::out_of_range{"Dynarray subscript out of range."}; log_access(); verify_integrity(); return m_storage[n]; } };
将一模一样的代码编写两遍实在是太麻烦了... 有没有办法避免重复?
假如没有额外的语法特性... 能不能让一个函数调用另一个?
假如我们让 non-const 版本的函数调用 const 版本的函数:
const int &
static_cast<const Dynarray *>(this)
->at(n)
const_cast
class Dynarray { public: const int &at(std::size_t n) const { if (n >= m_length) throw std::out_of_range{"Dynarray subscript out of range."}; log_access(); verify_integrity(); return m_storage[n]; } int &at(std::size_t n) { return const_cast<int &>(static_cast<const Dynarray *>(this)->at(n)); } };
能不能反过来,让 const 版本调用 non-const 版本?
class Dynarray { public: int &at(std::size_t n) { if (n >= m_length) throw std::out_of_range{"Dynarray subscript out of range."}; log_access(); verify_integrity(); return m_storage[n]; } const int &at(std::size_t n) const { return const_cast<Dynarray *>(this)->at(n); } };
比较一下两种方法中对于“危险的” const_cast 的使用?