从零开始的C++学习生活 5:内存管理和模板初阶

个人主页:Yupureki-CSDN博客

C++专栏:C++_Yupureki的博客-CSDN博客

目录

前言

1. C语言的内存管理

1.1 内存区域分布

1.2 malloc/calloc/realloc对比

1.3 常见错误

2. C++的新内存管理方式

2.1 new和delete

2.2 new和delete操作自定义类型

3. operator new与operator delete

4. malloc/free和new/delete的区别

 对比总结表

5. 泛型编程:为什么需要模板?

6. 函数模板

6.1 函数模板的概念

6.2 函数模板的格式

6.3 函数模板的原理

6.4 函数模板的实例化

隐式实例化

显式实例化

6.5 模板参数的匹配原则

7. 类模板

7.1 类模板的定义

3.2 类模板的实例化


前言

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初始化为0calloc(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 newoperator 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/freenew/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> 才是具体的类型名。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值