内存管理

C/C++内存分布

为了更方便管理数据,C++中程序内存划分如下:
在这里插入图片描述

  • 栈区:由编译器自动分配释放,存放非静态局部变量 / 函数参数 / 返回值等,栈是向下增长的
  • 堆区:由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收。堆是向上增长的
  • 静态区(数据段):存放全局变量,静态变量,程序结束后由操作系统释放
  • 常量区(代码段):存放可执行的代码 / 只读常量

下面代码每个变量都在哪个区?

#include<iostream>
using namespace std;

int global_v = 2;
static int s_global_v = 3;

int main()
{
    int v = 10;			// 局部变量
    const int c_v = 10;	// const修饰的局部变量
    static int s_v = 12;	//静态局部变量
    int arr[10] = { 1,2,3 };	
    printf("global_v  :%p\n", &global_v);
    printf("s_global_v:%p\n", &s_global_v);
    printf("v         :%p\n", &v);
    printf("c_v       :%p\n", &c_v);
    printf("s_v       :%p\n", &s_v);
    printf("arr       :%p\n", &arr);
    return 0;
}

global_v, s_global_v , s_v 在静态区;v, c_v, arr在栈区
运行结果:
在这里插入图片描述

再加点难度:

#include<iostream>
#include<cstdlib>
using namespace std;

int main()
{
    char s[] = "abcd";
    const char* pc = "abcd";
    int* pi = (int*)malloc(sizeof(int) * 10);

	// 问题1:s在哪个区?*s呢?
	// 问题2:pc在哪个区?*pc呢?
	// 问题2:pi在哪个区?*pi呢?
    printf("s  :%p\n", &s);
    printf("*s :%p\n", &(*s));
    printf("pc :%p\n", &pc);
    printf("*pc:%p\n", &(*pc));
    printf("pi :%p\n", &pi);
    printf("*pi:%p\n", &(*pi));
    return 0;
}

答:s, *s, pc, pi都在栈区,*pc在常量区,*pi在堆区。*s其实就是s[0]
具备争议的可能是*s*pcpc是个const char*类型的局部变量,存放的是字符串"abcd"的地址
在这里插入图片描述

C/C++内存管理方式new/delete

内置类型

#include<iostream>
#include<cstdlib>
#include<cstring>
using namespace std;

int main()
{
    int* p = new int;  // 动态申请一个int变量。等价于(int*)malloc(sizeof(int));
    delete p;          // 释放资源

    int* pa = new int[4];  // 动态申请一个int数组,长度为4。等价于(int*)malloc(sizeof(int) * 4);
    delete[] pa;            // 释放资源,数组要在前面加上[]。(直接写成delete pa在有些编译器可能会出问题)

	// 申请n个空间
    int n = 20;
    int* a = new int[n];
    delete[] a;
    
    int* p1 = new int(10); // 申请一个int变量, 并将其初始化为10,注意这里是(),[]才是申请10个int
    int* p2 = new int[4] { 1, 2, 3, 4 }; // 申请一个长度为4的int数组, 并将其初始化为{1,2,3,4}
    cout << *p1 << endl;
    for (int i = 0; i < 4; i++) cout << p2[i] << ' ';
    delete p1;
    delete[] p2;
    return 0;
}

运行结果
在这里插入图片描述
与C语言malloc处理方式差不多

自定义类型

class A
{
    int _a;
public:
    A(int a = 10) 
        :_a(a)
    { cout << "A()" << endl; }
    ~A() { cout << "~A()" << endl; }

    void Print() { cout << _a << endl; }
};
int main()
{
    A* pa = new A;  // 申请好空间后,会调用默认构造
    pa->Print();
    delete pa;      // 先析构,再释放空间

    cout << endl;

    A* pa1 = new A(222);    // 申请好空间后,会调用默认构造
    pa1->Print();
    delete pa1;
    return 0;
}

运行结果
在这里插入图片描述

// class A与上面的代码一样
int main()
{
    A* pa2 = new A[4]{ 1,2,3,4 };   // 这里其实是隐式类型转换,对于第一个元素,相当于pa2[0] = 1; 不信的话你可以用explicit关闭隐式类型转换
    for(int i = 0; i < 4; i++)
        pa2[i].Print();
    delete[] pa2;
    return 0;
}

再把前面的知识结合起来

class A
{
    int _a1;
    int _a2;
public:
    A(int a = 10) 
        :_a1(a)
        ,_a2(a)
    { cout << "A()" << endl; }

    A(int a1, int a2) 
        :_a1(a1)
        ,_a2(a2)
    { cout << "A(int a1, int a2)" << endl; }

    ~A() { cout << "~A()" << endl; }

    void Print() { cout << _a1 << ' ' << _a2 << endl; }
};

int main()
{
    A* pa = new A[4] { 1, {21,22}, A(2), A(33,44) };   
    // 第一个:单参数隐式类型转换;第二个:多参数隐式类型转换;后面两个:匿名对象
    for(int i = 0; i < 4; i++)
        pa[i].Print();
    return 0;
}

运行结果
在这里插入图片描述
与C语言的realloc函数不同,C++并没有扩容

#include<iostream>
#include<cstdlib>
using namespace std;

int main()
{
	// C 扩容
    int* a1 = (int*)malloc(sizeof(int) * 5);
    a1 = (int*)realloc(a1, sizeof(int) * 10); // 扩成10个
    
	// C++ 扩容
    int* a2 = new int[5];
    delete[] a2;
    a2 = new int[10];
    return 0;
}

补充一点:malloc申请空间失败会返回NULLnew失败会抛异常。具体会在之后讲到

// 不要将malloc/free 与 new/delete交错着用,有时候能正常运行,有时候会出bug
// C++建议统一使用new/delete
class A
{
    int _a;
public:
    A(int a = 10) 
        :_a(a)
    { cout << "A()" << endl; }
    ~A() { cout << "~A()" << endl; }

    void Print() { cout << _a << endl; }
};
int main()
{
    int* p = new int[10];
    free(p);
    
    A* pa = new A[10];
    free(pa);
    return 0;
}

内存泄漏

下面代码就会内存泄漏

class A
{
    int _a;
public:
    A(int a = 10) 
        :_a(a)
    { cout << "A()" << endl; }
    ~A() { cout << "~A()" << endl; }

    void Print() { cout << _a << endl; }
};

int main()
{
    int* pa = new int[10];
    A* pA = new A[10];

    // ... 满足一定条件
    if (1)  goto tar;

    delete[] pa;
    delete[] pA;

tar:
    // ...
    return 0;
}

在C++异常章节还会谈到内存泄漏

模板初阶

本篇模板这块只是初步介绍

函数模板引入

先看一些问题,关于Swap函数

void Swap(int& a, int& b) { int t = a; a = b, b = t; }
void Swap(double& a, double& b) { double t = a; a = b, b = t; }
void Swap(char& a, char& b) { char t = a; a = b, b = t; }
// ... 一个类型就要写一个Swap,很是繁琐

int main()
{
    int a1 = 1, a2 = 2;
    Swap(a1, a2);

    double d1 = 3.1, d2 = 2.1;
    Swap(d1, d2);

    char c1 = 'a', c2 = 'z';
    Swap(c1, c2);
    return 0;
}

Swap函数主体都是一样的,只不过数据类型不同。C++提供函数模板解决:

#include<iostream>
using namespace std;

template<typename T>  // 即可以用typename,也可以用class。template<class T>,现阶段可以认为是等价的
void Swap(T& a, T&b)
{
    T tmp = a;
    a = b;
    b = tmp;
}

int main()
{
    int a1 = 1, a2 = 2;
    Swap(a1, a2);

    double d1 = 3.1, d2 = 2.1;
    Swap(d1, d2);

    char c1 = 'a', c2 = 'z';
    Swap(c1, c2);
    
    // 写成这样会报错,a1跟c1类型不同
    // Swap(a1, c1);
    return 0;
}

函数模板本质是让编译器根据模板生成指定函数,编译器会根据实参的类型推演模板参数类型。比如调用Swap(a1, a2);时,a1、a2都是int类型,编译器会将模板函数里的所有T替换成int,生成一个新的函数:

void Swap(int& a, int&b)
{
    int tmp = a;
    a = b;
    b = tmp;
}

函数模板显式实例化

template<class T>
T Add(const T& t1, const T& t2)
{
    return t1 + t2;
}

int main()
{
    double d1 = 3.14, d2 = 2.53;
    int c = Add<int>(d1, d2); // 显式实例化,明确表明将函数里的T替换成int
    cout << c << endl;      // 输出 5
    return 0;
}

函数模板显式实例化就是在函数名参数之间加上尖括号,在尖括号里面指定模板参数的类型。

不能推演出模板参数类型时,必须显式实例化

template<class T>
T* func(int n)
{
    T* p = new T[n];
    return p;
}

int main()
{
    // int* a = func(5); // err 无法通过返回值推演模板参数类型
    int* a = func<int>(5);
    return 0;
}

函数模板的匹配

一个函数模板可以和一个同名的普通函数同时存在

template<class T>
T Add(const T& t1, const T& t2)	// 函数模板Add
{
    cout << "T Add(const T& t1, const T& t2)" << endl;
    return t1 + t2;
}

int Add(int t1, int t2) // 普通函数Add,也可以称为非模板函数
{
    cout << "int Add(int t1, int t2)" << endl;
    return t1 + t2;
}
int main()
{
    int a = 1, b = 2;
    int c = Add(a, b);  // 调用非模板函数Add,有现成的就调用现成的,没有就套用模板
    int d = Add<int>(a, b); // 调用编译器特化的Add版本
    return 0;
}

运行结果
在这里插入图片描述

匹配规则:

  1. 如果函数模板和普通函数都可以调用,会优先调用普通函数
  2. 若函数模板可以产生更好的匹配,会调用函数模板

一句话:有现成的就调用现成的,没有就套用模板


类模板

用C++实现一个计算器会用到两个栈,一个用于存储整数,一个存储运算符。没有模板就只能写两个类型的栈。。。模板就能很好解决

简单实现一个栈

template<class T>
class Stack
{
    T* _arr;
    int _top;
    int _capacity;
public:
    Stack(int n = 1000)
        :_arr(new T[n])    // 就相当于T* _arr = new T[n]
        ,_top(0)
        ,_capacity(n)
    {}
    ~Stack()
    {
        delete[] _arr;
        _arr = nullptr;
        _top = _capacity = 0;
    }
    // 下面的只实现函数的核心,扩容、判空等等先不考虑
    void push(const T& x) { _arr[_top++] = x; }
    void pop() { _top--; }
    bool empty() { return _top == 0; }
    T top() { return _arr[_top - 1]; }
};

int main()
{
    Stack<int> st;      // 类模板必须显式实例化!在类名与对象名之间加上尖括号,里面指定模板参数的类型
    Stack<char> st2;
    return 0;
}

类模板的声明和定义分离

以栈的push函数为例:

template<class T>
class Stack
{
    T* _arr;
    int _top;
    int _capacity;
public:
// 。。。省略其他成员函数
// 函数声明
    void push(const T& x);
};

// 函数定义
template<class T>
void Stack<T>::push(const T& x) 
{
     _arr[_top++] = x; 
}

一般不建议将模板的声明跟定义分离到两个文件。(比如声明放在.h文件,定义放在.cpp文件。这样会出现链接错误,具体原因后面细讲)

好了模板初步内容已经介绍完毕,在接下来的STL会慢慢深入

更多推荐