C++专栏:C++_Yupureki的博客-CSDN博客
目录
3. operator new与operator delete
前言
C语言中,malloc开辟空间大小需要我们自己去计算,也不能初始化每一块空间的值,有没有更加方便快捷的方法?
再之,我们常常会遇到需要处理多种数据类型的情况。例如,实现一个交换两个变量的函数,如果对每种类型都写一个重载函数,代码会显得冗余且难以维护。有没有一种方式,可以让我们只写一次代码,就能适用于多种类型呢?
在C++里,这些问题都得到了解决,new/delete和模板都是C++不可或缺,十分重要的部分。
本文将带你初步了解C++中的新的空间开辟机制和模板机制,包括函数模板和类模板的基本概念、使用方法及其原理。
1. C语言的内存管理
1.1 内存区域分布
在C/C++程序中,内存被划分为几个关键区域,每个区域都有其特定的用途和管理方式
【说明】
1. 栈又叫堆栈--非静态局部变量/函数参数/返回值等等,栈是向下增长的。
2. 内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口 创建共享共享内存,做进程间通信。
3. 堆用于程序运行时动态内存分配,堆是可以上增长的。
4. 数据段--存储全局数据和静态数据。
5. 代码段--可执行的代码/只读常量。
#include <iostream>
#include <cstdlib>
using namespace std;
int globalVar = 1; // 全局变量
static int staticGlobalVar = 1; // 静态全局变量
void Test() {
static int staticVar = 1; // 静态局部变量
int localVar = 1; // 局部变量
int num1[10] = {1, 2, 3, 4}; // 局部数组
char char2[] = "abcd"; // 局部字符数组
const char* pChar3 = "abcd"; // 指向常量字符串的指针
int* ptr1 = (int*)malloc(sizeof(int) * 4); // 动态分配
int* ptr2 = (int*)calloc(4, sizeof(int)); // 动态分配并初始化为0
int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4); // 重新分配
free(ptr1);
free(ptr3);
}
1.2 malloc/calloc/realloc对比
#include <iostream>
#include <cstdlib>
using namespace std;
void TestCMemory() {
// malloc - 分配指定字节数的内存,不初始化
int* p1 = (int*)malloc(sizeof(int) * 4);
// calloc - 分配并初始化为0
int* p2 = (int*)calloc(4, sizeof(int)); // 分配4个int,初始化为0
// realloc - 重新分配内存大小
int* p3 = (int*)realloc(p2, sizeof(int) * 10);
// 注意:p2已被realloc处理,不需要再free
free(p1);
free(p3); // 释放重新分配后的内存
}
三种函数的区别:
函数 | 初始化 | 参数形式 | 使用场景 |
---|---|---|---|
malloc | 不初始化 | malloc(size) | 普通内存分配 |
calloc | 初始化为0 | calloc(count, size) | 需要零初始化的数组 |
realloc | 保持原数据 | realloc(ptr, new_size) | 调整已分配内存大小 |
1.3 常见错误
内存泄漏(未使用free),重复释放(多次使用free),解引用野指针(读取已释放空间的数据)和越界访问都是常见的错误
void MemoryErrors() {
// 1. 内存泄漏
int* leak = (int*)malloc(sizeof(int) * 10);
// 忘记free(leak)
// 2. 重复释放
int* p = (int*)malloc(sizeof(int));
free(p);
// free(p); // 错误:重复释放
// 3. 使用已释放内存
int* q = (int*)malloc(sizeof(int));
free(q);
// *q = 10; // 错误:野指针
// 4. 越界访问
int* arr = (int*)malloc(sizeof(int) * 5);
// arr[5] = 10; // 错误:越界访问
free(arr);
}
2. C++的新内存管理方式
C语言内存管理方式在C++中可以继续使用,但有些地方就无能为力,而且使用起来比较麻烦,因 此C++又提出了自己的内存管理方式:通过new和delete操作符进行动态内存管理。
2.1 new和delete
#include <iostream>
using namespace std;
void TestCPPMemory() {
// 申请单个int空间
int* ptr1 = new int;
// 申请单个int空间并初始化为10
int* ptr2 = new int(10);
// 申请10个int类型的空间(数组)
int* ptr3 = new int[10];
// 申请数组并初始化(C++11)
int* ptr4 = new int[10]{1, 2, 3}; // 前3个初始化,其余为0
cout << "*ptr2 = " << *ptr2 << endl; // 输出:10
// 释放内存
delete ptr1;
delete ptr2;
delete[] ptr3; // 数组要用delete[]
delete[] ptr4;
}
new为操作符,和malloc或者calloc相似,后面跟要开辟空间数据的类型和大小。如果要以数组的形式开辟则加[],里面加大小,后面的{}可以初始化前n个数据的大小。
delete为操作符,和free相似。如果是单个数据则直接用delete释放空间;如果是数组,那么最好加上[](这里不加也不会报错,但如果是类类型,那么极有可能出错)
2.2 new和delete操作自定义类型
当new和delete为自定义类型开辟空间时,则会自动调用对应的构造函数和析构函数
而内置类型不会,毕竟都没有构造函数和析构函数
class MyClass {
public:
MyClass(int val = 0) : data(val) {
cout << "构造函数: " << this << ", data = " << data << endl;
}
~MyClass() {
cout << "析构函数: " << this << ", data = " << data << endl;
}
private:
int data;
};
void TestDifference() {
// C++方式:分配内存并调用构造函数
MyClass* p2 = new MyClass(42);
delete p2; // 调用析构函数并释放内存
cout << "=== 数组操作 ===" << endl;
// 对象数组
MyClass* p3 = new MyClass[3]; // 调用3次构造函数
delete[] p3; // 调用3次析构函数
}
如果以数组的形式为自定义类型开辟空间,那么开辟了几个元素的空间,就会调用几次的构造函数和析构函数
3. operator new与operator delete
在C++中,new和delete只是用户进行动态内存申请和释放的操作符
在运用当中会分别调用对应的operator new与operator delete函数
void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
while ((p = malloc(size)) == 0)//new的底层其实是调用malloc
//.......
}
void operator delete(void *pUserData)
{
_free_dbg( pUserData, pHead->nBlockUse );//delete的底层是调用free
//.......
}
#define free(p) _free_dbg(p, _NORMAL_BLOCK)
如果观察这两个函数的内容,则会发现其实就是malloc和free套了个马甲,本质还是使用malloc和free。如果 malloc申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施 就继续申请,否则就抛异常。
4. malloc/free和new/delete的区别
malloc/free和new/delete的共同点是:都是从堆上申请空间,并且需要用户手动释放。
不同的地 方是:
1. malloc和free是函数,new和delete是操作符
2. malloc申请的空间不会初始化,new可以初始化
3. malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可, 如果是多个对象,[]中指定对象个数即可
4. malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型
5. malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需要捕获异常
6. 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new 在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成 空间中资源的清理释放
对比总结表
特性 | malloc/free | new/delete |
---|---|---|
语言 | C函数 | C++操作符 |
初始化 | 不初始化 | 可初始化 |
大小计算 | 手动计算 | 自动计算 |
类型安全 | 需要强转 | 类型安全 |
错误处理 | 返回NULL | 抛出异常 |
构造/析构 | 不调用 | 自动调用 |
重载 | 不可重载 | 可重载 |
数组 | 需要手动处理 | 专用语法 |
5. 泛型编程:为什么需要模板?
在没有模板的情况下,如果我们想实现一个通用的交换函数,可能会写出如下代码:
void Swap(int& left, int& right) {
int temp = left;
left = right;
right = temp;
}
void Swap(double& left, double& right) {
double temp = left;
left = right;
right = temp;
}
void Swap(char& left, char& right) {
char temp = left;
left = right;
right = temp;
}
这种方式虽然可行,但存在明显的问题:
-
代码冗余:每个函数体几乎相同,只是类型不同。
-
不易维护:新增类型时需要手动添加函数,容易出错。
我们希望能有一个“模具”,让编译器根据我们传入的类型自动生成对应的函数代码。这就是模板的由来。
6. 函数模板
6.1 函数模板的概念
函数模板是一个通用的函数蓝图,它不具体指定类型,而是在使用时根据传入的实参类型自动实例化出具体的函数版本。
6.2 函数模板的格式
template<typename T>
void Swap(T& left, T& right) {
T temp = left;
left = right;
right = temp;
}
-
template<typename T>
表示定义一个模板,T
是一个占位符类型。 -
你也可以使用
class
代替typename
,但不能用struct
。
6.3 函数模板的原理
函数模板本身不是函数,而是编译器生成具体函数的“模具”。当我们调用 Swap(a, b)
时,编译器会根据 a
和 b
的类型推导出 T
,然后生成对应的函数代码。
int main() {
int i1 = 10, i2 = 20;
Swap(i1, i2); // 生成 void Swap(int&, int&)
double d1 = 1.1, d2 = 2.2;
Swap(d1, d2); // 生成 void Swap(double&, double&)
}
6.4 函数模板的实例化
使用函数模板时,编译器会根据传入的实参类型生成具体的函数,这个过程称为实例化。实例化分为两种:
隐式实例化
编译器自动推导类型:
Add(1, 2); // T 被推导为 int
Add(1.1, 2.2); // T 被推导为 double
显式实例化
手动指定类型:
Add<int>(1, 2.2); // 将 2.2 强制转换为 int
6.5 模板参数的匹配原则
-
如果存在非模板函数与模板函数同名,且参数匹配,优先调用非模板函数。
-
如果模板能生成更匹配的版本,则选择模板。
-
模板不支持隐式类型转换,而普通函数支持。
7. 类模板
7.1 类模板的定义
类模板允许我们定义一种通用的类,其成员类型可以是参数化的。
template<typename T>
class Stack {
public:
Stack(size_t capacity = 4) {
_array = new T[capacity];
_capacity = capacity;
_size = 0;
}
void Push(const T& data);
private:
T* _array;
size_t _capacity;
size_t _size;
};
// 类外定义成员函数
template<class T>
void Stack<T>::Push(const T& data) {
// 扩容逻辑
_array[_size] = data;
++_size;
}
7.2 类模板的实例化
类模板的实例化必须在类名后加上 <类型>
:
Stack<int> st1; // 存储 int 类型的栈
Stack<double> st2; // 存储 double 类型的栈
注意:Stack
是模板名,Stack<int>
才是具体的类型名。