目录
- 一、C/C++内存分布
- 二、C/C++动态内存管理方式
- 2.1 new和delete的用法
- 2.2 new与malloc、delete与free比较
- 2.3 较复杂场景分析
- 三、operator new与operator delete函数
- 四、 new和delete的实现原理
- 五、初识模板
- 5.1 泛型编程
- 5.2 函数模板
- 5.2.1 概念
- 5.2.2 写法
- 5.2.3 不同类型时使用函数模板
- 5.2.4 函数模板实例化
- 5.2.5 函数模板匹配调用原则
- 5.3 类模板
一、C/C++内存分布
C/C++的内存分布主要分为栈区、堆区、数据段和代码段,还有内存映射段。
栈又叫堆栈–非静态局部变量/函数参数/返回值等等,栈是向下增长的。
内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享共享内存,做进程间通信。
堆用于程序运行时动态内存分配,堆是可以上增长的。
数据段–存储全局数据和静态数据
代码段–可执行的代码/只读常量。
看以下代码:
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));
int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4);
free(ptr1);
free(ptr3);
}
有几个问题:
1.char2在哪里 2.*char2在哪里
3.pChar3在哪里 4.*pChar3在哪里
5.ptr1在哪里 6.*ptr1在哪里
7.sizeof(num1) =
8.sizeof(char2) = ? 9.strlen(char2) = ?
10.sizeof(pChar3) = ? 11.strlen(pChar3) = ?
12.sizeof(ptr1) = ?
1.2 char2是定义在Test函数里的数组名,是局部变量,在栈区;* char2对数组解引用得到数组的地址是字符串,但是是把字符串的内容拷贝到数组的地址去,所以 * char2 在栈区。
3.4 pChar3是定义在Test函数里的指针,是局部变量,在栈区;* pChar3对指针进行解引用,得到字符串,字符串在代码段。
5.6 ptr1是定义在Test函数里的整型指针,是局部变量,在栈区;* ptr1解引用得到的地址是malloc函数开辟的空间,在堆区。
7. sizeof计算的是整个数组的大小,以字节为单位,数组里有10个元素,一个元素4个字节,所以答案是40
8. sizeof计算的是字符串的大小,注意包括后面的斜杠0,所以答案是5
9. strlen计算的是字符串的大小,不包括后面的斜杠0,所以答案是4
10. sizeof只关注类型,pChar3是指针,所以答案是4/8
11. strlen计算的是字符串的大小,不包括后面的斜杠0,所以答案是4
12. ptr1是整型指针,所以答案是4/8
一张图表示:
二、C/C++动态内存管理方式
C语言中动态开辟的方式有malloc、calloc和realloc,常用的是malloc函数,释放空间的方式是free。但是,这两种方式有一些局限性,所以C++有新的动态开辟管理方式,通过new和delete操作符进行动态内存管理。
2.1 new和delete的用法
先来作下对比:
int* a = (int*)malloc(sizeof(int));
///
int* a = new int;
new不需要我们自己写sizeof计算字节大小,是不是看起来更简洁了。
delete a;
///
free(a);
delete和free差不多,注意释放空间完要置空。
1️⃣new初始化元素
在后面加个括号,括号里就是要初始化的值
int* a = new int(4);//初始化为4
2️⃣new创建元素个数
在后面加个方括号,方括号里就是元素的个数
int* a = new int[5];
3️⃣new创建元素个数并初始化
在2️⃣的基础上后面加个花括号,花括号里面分别为初始化的值。如果初始化的数量小于元素个数,补0
int* a = new int[5]{ 1,2,3 };
4️⃣delete单个元素时
delete a;
5️⃣delete多个元素时
delete[] a;
注意:
申请单个元素空间与释放单个对象的new和delete要搭配使用;申请多个元素空间与释放多个对象的new[]和delete[]要搭配使用
2.2 new与malloc、delete与free比较
共同点:
都是从堆上申请空间,并且需要用户手动释放。
不同点:
1️⃣malloc只开辟空间大小,不能初始化;new就既可以开辟空间的大小,也可以对空间进行初始化。 前面代码就用演示。
2️⃣自定义类型初始化问题
当对象是自定义类型时,malloc不方便自定义类型初始化,因为malloc只会开辟空间,free只会释放空间。new会先开空间,再调用构造函数;delete会先调用析构函数,再释放空间。
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a = 0)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
int main()
{
A* aa = new A[3]{ 11,22,33 };
delete aa;
return 0;
}
同时new可以在开空间时完成初始化
补充:
malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型;
malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需要捕获异常;
malloc和free是函数,new和delete是操作符
2.3 较复杂场景分析
看以下代码:
class Stack
{
public:
Stack(int capacity = 4)
{
cout << "Stack(int capacity = 4)" << endl;
_a = new int[capacity];
_top = 0;
_capacity = capacity;
}
~Stack()
{
cout << "~Stack()" << endl;
delete[] _a;
_a = nullptr;
_top = 0;
_capacity = 0;
}
private:
int* _a;
int _top;
int _capacity;
};
int main()
{
Stack* st = new Stack;
delete st;
return 0;
}
这段代码main函数里有一个new和delete,构造函数里也有一个new,析构函数也有一个delete,那么它们之间的关系是怎样的
图示分析:
自定义类型先给对象开辟空间(对象个数问题后面分析),再调用构造函数,构造函数里给_a数组开辟空间,因为_a是内置类型,所以这里的处理方式与malloc相同。delete对象,先调用析构函数,清理_a,再释放空间,此时清理的是对象。
注意,有的人乱着用把delete st写成free st,free只会释放空间不会调用析构函数,它释放了对象的空间,但是对象的空间里的_a指向的空间没有被释放掉,是不是就变成野指针了,所以不可free和delete混着用。
三、operator new与operator delete函数
new和delete是用户进行动态内存申请和释放的操作符,operator new 和operator delete是系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间。
先来看一段汇编代码:
operator new 实际是通过malloc来申请空间,如果malloc申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施就继续申请,否则就抛异常。operator delete 最终是通过free来释放空间的。 所以,operator new 和operator delete可以说是malloc和free的封装
四、 new和delete的实现原理
1️⃣对于内置类型,new和malloc,delete和free基本类似。有一点不同,只有单个对象时,匹配的两者是new/delete;多个对象时,匹配的两者是new[]和delete[],跟数组一样,也是连续的空间。要注意的是new在申请空间失败时会抛异常,malloc会返回NULL。
2️⃣对于自定义类型,只有单个对象时,就是前面一段代码的例子,这个就不讨论了。下面来看看多个对象的情况:
class Stack
{
public:
Stack(int capacity = 4)
{
cout << "Stack(int capacity = 4)" << endl;
_a = new int[capacity];
_top = 0;
_capacity = capacity;
}
~Stack()
{
cout << "~Stack()" << endl;
delete[] _a;
_a = nullptr;
_top = 0;
_capacity = 0;
}
private:
int* _a;
int _top;
int _capacity;
};
int main()
{
Stack* st = new Stack[10];
delete[] st;
return 0;
}
new []的原理:
1.调用operator new[]函数,在operator new[]中实际调用operator new函数完成10个对象空间的申请
2.在申请的空间上执行10次构造函数
delete[]的原理:
1.在释放的对象空间上执行10次析构函数,完成10个对象中资源的清理
2.调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释放空间
一个栈对象有3个内置类型(int* int int),共12个字节,那么10个栈对象总共有120个字节,但是事实是这样的吗?
打开内存查看:
在st指向的地址处前面有4个字节存储的是对象的个数
如果把delete 后面的方括号去掉会怎样:
Stack* st = new Stack[10];
delete st;
程序运行崩溃了,那原因是什么呢?先来看以下代码:
以下是一个A类,私有成员变量只有一个整型,先把析构函数注释了,看会发生什么:
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a = 0)" << endl;
}
/*~A()
{
cout << "~A()" << endl;
}*/
private:
int _a;
};
int main()
{
A* aa = new A[10];
delete aa;/// 把方括号去掉
return 0;
}
程序正常运行。如果不注释析构函数会怎样:
程序运行崩溃了。
为什么这个A类注释不注释析构函数有区别?
没有注释析构函数,会去调用这个析构函数,调用析构函数,aa指向下图的位置
前面红色的区域是存储对象个数的。我们知道delete[]对应匹配的是new[],而delete对应匹配的是new,因为delete后面没有方括号,所以对应使用的new开空间时就不需要前面的4个字节来存储对象的个数(1个对象没啥好记录个数的),因此aa指针指向如下图所示。
这样的话释放空间如下图:
释放的空间是蓝色区域,但是前面的红色的区域也必须要释放,不可以只释放部分,所以释放空间位置不对导致运行崩溃。
有注释析构函数,虽然编译器可以自动调用默认生成的析构函数,但是这取决于编译器。因为A类里面只有内置类型(一个整型),没有申请空间,所以可以不做处理。不做处理就不调用析构函数,没有析构函数也就没有前面的位置存储对象个数,释放的位置就不会只释放部分。
总结:new[]与delete[]和new与delete要匹配使用,不能混。
五、初识模板
5.1 泛型编程
概念:泛型编程是一种无具体类型的通用代码,可以实现不同类型时的复用。模板是泛型编程的基础,模板分为函数模板和类模板。
5.2 函数模板
5.2.1 概念
函数模板是一种通用的函数,通过实参的类型推出相应类型的函数,实现函数调用。
5.2.2 写法
关键字templete
格式:templete < typename T1,typename T2… >,T是类型
typename 也可以是class
一个交换函数的模板:
template <class T>
void Swap(T& left, T& right)
{
T tmp = left;
left = right;
right = tmp;
}
实现两个数的交换:
int main()
{
int a = 1;
int b = 3;
cout << a << endl << b << endl;
Swap(a, b);//a和b传进去,编译器推出类型,T就变成了int类型
cout << a << endl << b << endl;
return 0;
}
运行结果:
5.2.3 不同类型时使用函数模板
1️⃣调用两次函数,函数实参的类型不同,一个是int,另一个是double,这两次调用都使用了函数模板,但是这两次调用的不是同一个函数。
通过汇编查看:
2️⃣多模板参数打印
想同时打印不同的类型可以使用多个模板参数
template <class T1, class T2>
void Print(T1& a, T2& b)
{
cout << a << endl << b << endl;
}
int main()
{
int a = 33;
double b = 1.22;
Print(a, b);
return 0;
}
a和b的类型由直接定,反正传过去什么,它就打印什么
3️⃣加法函数
代码:
template <class T>
T Add(const T& a, const T& b)
{
return a + b;
}
int main()
{
int a = 2;
int b = 6;
cout << Add(a, b) << endl;
return 0;
}
运行结果:
如果把b的类型换成double:
编译器报错了,说明不可以两个类型不同,不然的话就不确定应该使用哪种类型了。
5.2.4 函数模板实例化
1️⃣隐式实例化
通过实参的类型编译器自动推演出相应的函数模板参数类型叫做隐式实例化。
加法函数:
template <class T>
T Add(const T& a, const T& b)
{
return a + b;
}
int main()
{
int a = 9;
int b = 2;
int c = Add(a, b);
cout << c << endl;
return 0;
}
T推出为int类型。
如果其中一个参数的类型不一样,编译器会报错,这里有两种办法解决。
强制类型转换:
template <class T>
T Add(const T& a, const T& b)
{
return a + b;
}
int main()
{
int a = 9;
double b = 2.1;
cout << Add(a, (int)b) << endl;
return 0;
}
另一种是显示实例化
2️⃣显示实例化
template <class T>
T Add(const T& a, const T& b)
{
return a + b;
}
int main()
{
int a = 9;
double b = 2.1;
cout << Add<int>(a, b) << endl;
return 0;
}
在函数名后的<>中指定模板参数的实际类型
一般情况下使用函数模板隐式实例化就够了,但是有些情况必须使用显示实例化
template <class T>
T* func()
{
return new T[3];
}
int main()
{
func<int>();
return 0;
}
没有传参数,T就不知道应该变成什么类型,所以要显示实例化来确定T的类型。
5.2.5 函数模板匹配调用原则
当代码中既有普通函数,又有函数模板,那么调用函数时会使用哪个呢?
//函数模板
template <class T>
T Add(const T& a, const T& b)
{
return a + b;
}
//普通函数
int Add(int a, int b)
{
return a + b;
}
int main()
{
int a = 1;
int b = 3;
cout << Add(a, b) << endl;
return 0;
}
打开调试:
变量a和b定义的时候都是int类型,普通函数的参数类型也是int,所以有现成的就用现成的。
现在把a和b的类型改变一下。
double a = 1.1;
double b = 3.3;
调试:
如果没有现成的普通函数,就使用合适的,函数模板参数类型为double
5.3 类模板
类模板与函数模板差不多,但是要注意的点:必须显示实例化
template <class T>
class Stack
{
public:
Stack(int capacity = 4)
{
cout << "Stack(int capacity = 4)" << endl;
_a = new T[capacity];
_top = 0;
_capacity = capacity;
}
~Stack()
{
cout << "~Stack()" << endl;
delete[] _a;
_top = 0;
_capacity = 0;
}
private:
T* _a;
int _top;
int _capacity;
};
int main()
{
Stack<int> st1;
Stack<double> st2;
return 0;
}
这样才能确定类中成员变量的类型。