C++内存管理与模板初阶
内存管理
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与*pc。pc是个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申请空间失败会返回NULL,new失败会抛异常。具体会在之后讲到
// 不要将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;
}
运行结果

匹配规则:
- 如果函数模板和普通函数都可以调用,会优先调用普通函数
- 若函数模板可以产生更好的匹配,会调用函数模板
一句话:有现成的就调用现成的,没有就套用模板
类模板
用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会慢慢深入
更多推荐
所有评论(0)