类和动态内存分配
动态内存和类
在类中使用 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
规则
-
析构函数必须使用delete;
-
new和delete匹配。new---delete,new\[\]---delete\[\];
-
必须自定义复制构造函数(深拷贝)。
-
必须自定义赋值运算符。必须检测自我赋值
对象的返回方式
返回指向对象的引用,返回指向对象本身,返回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;
}