11. 类和动态内存分配

类和动态内存分配

动态内存和类

在类中使用 new 和 delete 来动态管理内存,可以让程序在运行时(而非编译时)决定内存分配。

在类构造函数中使用 new,需要额外执行一系列步骤:扩展析构函数、编写额外的类方法来帮助正确完成初始化和赋值。否则会导致严重问题。

复制代码
 #include <cstring>
 #include "strngbad.h"
 using std::cout;
 ​
 // 初始化静态成员
 int StringBad::num_strings = 0;
 ​
 // 从 C 风格字符串构造 StringBad
 StringBad::StringBad(const char * s) {
     len = std::strlen(s);
     str = new char[len + 1];          // 分配内存
     std::strcpy(str, s);              // 复制字符串
     num_strings++;                    // 对象计数 +1
     cout << num_strings << ": \"" << str << "\" object created\n";
 }
 ​
 // 默认构造函数
 StringBad::StringBad() {
     len = 4;
     str = new char[4];
     std::strcpy(str, "C++");
     num_strings++;
     cout << num_strings << ": \"" << str << "\" default object created\n";
 }
 ​
 // 析构函数
 StringBad::~StringBad() {
     cout << "\"" << str << "\" object deleted, ";
     --num_strings;
     cout << num_strings << " left\n";
     delete [] str;   // 释放动态分配的内存
 }
 ​
 std::ostream & operator<<(std::ostream & os, const StringBad & st) {
     os << st.str;
     return os;
 }

使用赋值运算符的时候,编译器会使用默认复制构造函数,会产生浅拷贝,导致程序调用两次析构函数,产生未定义行为。

特殊成员函数 何时自动生成 默认行为
默认构造函数 没有定义任何构造函数 不执行任何操作
默认析构函数 没有定义析构函数 不执行任何操作
复制构造函数 程序使用对象的方式需要时 逐成员复制(浅拷贝)
赋值运算符 程序使用对象的方式需要时 逐成员复制(浅拷贝)
地址运算符 程序使用对象的方式需要时 返回 this 指针

C++11 还新增了移动构造函数和移动赋值运算符(第18章),用于支持移动语义。

默认构造函数

如果没有提供任何构造函数,编译器会生成一个隐式默认构造函数:

复制代码
 Klunk::Klunk() { }   // 什么都不做

但是如果定义了任意一个构造函数,编辑器就不会生成默认构造函数了。

默认构造函数的定义方式

复制代码
 // 方式一:无参构造函数
 Klunk() { klunk_ct = 0; }
 ​
 // 方式二:所有参数都有默认值
 Klunk(int n = 0) { klunk_ct = n; }
复制构造函数
复制代码
 Class_name(const Class_name &);

复制构造函数的调用时机。显式初始化新对象为现有对象,函数按值传递对象,函数按值返回对象,编译器生成临时对象。

按值传递会调用复制构造函数,因此对于大型对象,应优先使用按引用传递以节省时间和内存。

默认构造函数默认浅拷贝。

在构造函数中使用new

规则

  1. 析构函数必须使用delete;

  2. new和delete匹配。new---delete,new\[\]---delete\[\];

  3. 必须自定义复制构造函数(深拷贝)。

  4. 必须自定义赋值运算符。必须检测自我赋值

对象的返回方式

返回指向对象的引用,返回指向对象本身,返回const对象;

返回指向const对象引用。可以提高效率(避免调用赋值构造函数)

复制代码
 // 版本1:按值返回(调用复制构造函数,效率较低)
 Vector Max(const Vector & v1, const Vector & v2) {
     if (v1.magval() > v2.magval())
         return v1;
     else
         return v2;
 }
 ​
 // 版本2:返回 const 引用(效率更高)
 const Vector & Max(const Vector & v1, const Vector & v2) {
     if (v1.magval() > v2.magval())
         return v1;      // 返回引用,指向调用者已存在的对象
     else
         return v2;
 }
返回非const对象的引用(两种场景)

场景一:赋值运算符

考虑到调用者可能对其进行修改(如 (s2 = s1).some_method())。选择*this。

复制代码
 String & String::operator=(const String & st) {
     if (this == &st)
         return *this;
     delete [] str;
     // ... 复制操作 ...
     return *this;   // 返回非 const 引用
 }

场景二:operator<< 与 cout

目的:支持链式输出(如 cout << s1 << "is coming!";)。

复制代码
 ostream & operator<<(ostream & os, const String & st) {
     os << st.str;
     return os;   // 必须返回 ostream &
 }

由于ostream没有公有的复制构造函数,返回引用支持链式调用。

为什么不能使用const引用:ostream内部对象都是非const对象,一个 const 对象无法绑定到非 const 引用参数。

输出流对象在输出过程中会改变内部状态(比如设置错误位、刷新缓冲区等)

使用对象的指针
使用new初始化对象的语法
复制代码
 // 使用复制构造函数
 String * favorite = new String(sayings[choice]);
 ​
 // 使用默认构造函数
 String * gleep = new String;
 ​
 // 使用带参构造函数
 String * glop = new String("my my my");

对定位 new 创建的对象使用 delete 是错误的(因为 delete 只应用于常规 new 分配的地址),必须显式调用析构函数。

常规new运算符和定位new运算符

常规 new:"分配内存 + 调用构造函数"。如果内存不够,它会自己去堆上找。

定位 new(Placement new):"只调用构造函数"。它不管内存从哪来(你已经提供好了地址),它就在那个地址上构造对象

复制代码
 // 常规 new
 String* p1 = new String("Hello");
 ​
 // 定位 new(多了一个括号参数,传入地址)
 #include <new> // 必须包含头文件!
 char buffer[100];               // 你准备好的“空地”(栈上)
 String* p2 = new (buffer) String("World"); // 在 buffer 这块内存上构造 String
复习

重载<< : 可以使用友元函数,或者通过公有函数暴露接口,写一个全局函数(推荐),返回值使用ostream &(非const引用)

转换函数:一个参数的构造函数,推荐使用explicit禁止隐式类型转换。

赋值运算符必须检查自我复制。

队列模拟
复制代码
 #ifndef QUEUE_H_
 #define QUEUE_H_
 ​
 // 客户类(模拟队列中存储的数据类型)
 class Customer {
 private:
     long arrive;        // 到达时间(分钟)
     int processtime;    // 服务所需时间(1~3分钟)
 public:
     Customer() : arrive(0), processtime(0) {}
     void set(long when);
     long when() const { return arrive; }
     int ptime() const { return processtime; }
 };
 ​
 typedef Customer Item;  // 队列中的项目类型
 ​
 // 队列类
 class Queue {
 private:
     // 嵌套结构:链表节点(作用域限定在类内部)
     struct Node {
         Item item;
         struct Node * next;
     };
 ​
     enum { Q_SIZE = 10 };    // 默认队列容量
 ​
     // 数据成员
     Node * front;            // 指向队首
     Node * rear;             // 指向队尾
     int items;               // 当前元素个数
     const int qsize;         // 最大容量(const成员,必须用初始化列表)
 ​
     // 禁止复制(私有化,防止浅拷贝导致的链表共享问题)
     Queue(const Queue & q) : qsize(0) { }
     Queue & operator=(const Queue & q) { return *this; }
 ​
 public:
     // 构造函数(指定容量,默认10)
     Queue(int qs = Q_SIZE);
     // 析构函数(释放所有节点)
     ~Queue();
     // 状态检查
     bool isempty() const;
     bool isfull() const;
     int queuecount() const;
 ​
     // 核心操作
     bool enqueue(const Item & item);  // 入队(队尾添加)
     bool dequeue(Item & item);        // 出队(队首移除)
 };
 ​
 #endif
复制代码
 #include "queue.h"
 #include <cstdlib>   // 用于 rand()
 ​
 // ---------- Customer 类方法 ----------
 void Customer::set(long when) {
     processtime = std::rand() % 3 + 1;  // 随机生成 1~3 分钟的服务时间
     arrive = when;
 }
 ​
 // ---------- Queue 类方法 ----------
 ​
 // 构造函数:初始化空队列
 Queue::Queue(int qs) : qsize(qs), front(nullptr), rear(nullptr), items(0) {
     // 函数体为空,所有初始化已在初始化列表中完成
 }
 ​
 // 析构函数:删除所有剩余节点
 Queue::~Queue() {
     Node * temp;
     while (front != nullptr) {
         temp = front;          // 保存当前队首
         front = front->next;   // 移动队首指针
         delete temp;           // 删除旧节点
     }
 }
 ​
 // 检查队列是否为空
 bool Queue::isempty() const {
     return items == 0;
 }
 ​
 // 检查队列是否已满
 bool Queue::isfull() const {
     return items == qsize;
 }
 ​
 // 返回当前队列中的元素个数
 int Queue::queuecount() const {
     return items;
 }
 ​
 // 入队:在队尾添加元素
 bool Queue::enqueue(const Item & item) {
     if (isfull())
         return false;
 ​
     // 创建新节点
     Node * add = new Node;
     add->item = item;      // 复制数据
     add->next = nullptr;   // 新节点将成为队尾,next 指向空
 ​
     items++;               // 计数加1
 ​
     // 链接到队列中
     if (front == nullptr)  // 如果队列为空
         front = add;
     else
         rear->next = add;  // 否则接到当前队尾后面
 ​
     rear = add;            // 更新队尾指针
     return true;
 }
 ​
 // 出队:从队首移除元素,数据存入引用参数
 bool Queue::dequeue(Item & item) {
     if (front == nullptr)
         return false;
 ​
     item = front->item;     // 取出队首数据
     items--;                // 计数减1
 ​
     Node * temp = front;    // 保存旧队首地址
     front = front->next;    // 移动队首指针
     delete temp;            // 释放旧节点
 ​
     if (items == 0)         // 如果队列已空
         rear = nullptr;
 ​
     return true;
 }
相关推荐
似璟如你2 小时前
Java 开发者的 Go 语法基础:从 0 开始快速上手 Go
java·开发语言·后端·golang·go·编程语言
鱼子星_2 小时前
【C++】stack和queue的应用及其模拟实现:适配器与容器适配器
数据结构·c++·笔记·stl
LccKyI3 小时前
C#学习day05(开发福彩双色球系统附思维导图)
开发语言·学习·c#
zzz_23683 小时前
TencentDB-Agent-Memory 深度解析:让多个 Agent 共享项目经验的记忆中枢
java·开发语言·jvm·人工智能·agent·memory·tencent db
jufeng13073 小时前
【系列:MiniKV 原理剖析 · 第 2 篇】
linux·c++·软件工程
charlie1145141913 小时前
IMX6ULL WM8960 的移植——前置介绍
开发语言·c++·开源项目·嵌入式linux
迷迭香yy4 小时前
Python实战:涨停板“假封板”识别系统的工程实现
开发语言·人工智能·python
charlie1145141914 小时前
Cinux是如何管理进程的 —— 上下文与调度
开发语言·c++·操作系统·开源项目
hold?fish:palm4 小时前
redis中AOF 重写机制解析
数据库·c++·redis