目录
前言
1.C/C++内存分布
2. C++的内存管理方式
2.1 new/delete操作内置类型
2. new和delete操作自定义类型
3. operator new和operator delete函数
4. new和delete的实现原理
4.1 内置类型
4.2 自定义类型
5. malloc/free和new/delete的区别
6. 初识模版
6.1 泛型编程
6.2 函数模板概念和格式
6.3 函数模板原理
6.4 函数模板实例化
6.5 类模板定义格式与实例化
总结
前言
本文今天要浅浅的讲解C++内存管理和模板,关于C++是如何进行动态管理内存,C++中的模板的作用是什么,类型有哪些。虽然比较粗浅,但这是每个小伙伴学C++的必经之路,一起学起来吧!
1.C/C++内存分布
我们来看看下面的代码和相关问题:
int globalVar = 1;
static int staticGlobalVar = 1;
void Test()
{
int size = sizeof(int);
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(size*4);
int* ptr2 = (int*)calloc(4, size);
int* ptr3 = (int*)realloc(ptr2, size*4);
free(ptr1);
free(ptr3);
}
1. 选择题:
选项: A.栈 B.堆 C.数据段(静态区) D.代码段(常量区)
globalVar在哪里?____ staticGlobalVar在哪里?____
staticVar在哪里?____ localVar在哪里?____
num1 在哪里?____
char2在哪里?____ *char2在哪里?___
pChar3在哪里?____ *pChar3在哪里?____
ptr1在哪里?____ *ptr1在哪里?____
2. 填空题:
sizeof(num1) = ____;sizeof(char2) = ____; strlen(char2) = ____;
sizeof(pChar3) = ____; strlen(pChar3) = ____;
sizeof(ptr1) = ____;
3. sizeof 和 strlen 区别?
答案:
1. globalVar在C staticGlobalVar在C
staticVar在C localVar在A
num1 在Achar2在A *char2在A
pChar3在A *pChar3在D
ptr1在A *ptr1在B2. sizeof(num1) = 40;
sizeof(char2) = 5 strlen(char2) = 4
sizeof(pChar3) = 4 strlen(pChar3) = 4
sizeof(ptr1) = 4/8解析:
1.全局变量和静态变量都是存放在数据段(静态区)。
2.栈上存储的是函数内开辟的变量,在函数调用结束后,即时销毁。
- num是一个数组,本质上是数组首元素的地址,也是存放在栈上。
- char2本质是字符串首字符的地址,pChar也是类似的。不过当他们解引用的时候,char2是在栈上开辟的空间,pChar3是指向只读区域的代码段。
- ptr1是指针变量,也是局部变量。存放的地址是指向堆,这是动态开辟的内存区域。
- sizeof是计算该变量所占内存空间的大小,而char2这个字符串在字符结束后,会加上一个斜杠0,表示终止符。
- strlen是一个计算字符个数的函数。
内存区域划分图:
【说明】
1. 栈又叫堆栈--非静态局部变量/函数参数/返回值等等,栈是向下增长的。
2. 内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享共享内存,做进程间通信。
3. 堆用于程序运行时动态内存分配,堆是可以上增长的。
4. 数据段--存储全局数据和静态数据。
5. 代码段--可执行的代码/只读常量。
2. C++的内存管理方式
2.1 new/delete操作内置类型
void Test()
{
// 动态申请一个int类型的空间
int* ptr4 = new int;
// 动态申请一个int类型的空间并初始化为10
int* ptr5 = new int(10);
// 动态申请10个int类型的空间
int* ptr6 = new int[3];
delete ptr4;
delete ptr5;
delete[] ptr6;
}
- new + 内置类型 + (初始化内容)
- new + 内置类型 + [元素个数]
- delete + 申请空间的变量
- delete[] + 申请多个元素空间的变量
注意:申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间,使用new[]和delete[],注意:匹配起来使用。
2. new和delete操作自定义类型
class A
{
public:
A(int a = 0)
: _a(a)
{
cout << "A():" << this << endl;
}
~A()
{
cout << "~A():" << this << endl;
}
private:
int _a;
};
int main()
{
// new/delete 和 malloc/free最大区别是
// new/delete对于【自定义类型】除了开空间还会调用构造函数和析构函数
A* p1 = (A*)malloc(sizeof(A));
A* p2 = new A(1);
free(p1);
delete p2;
// 内置类型是几乎是一样的
int* p3 = (int*)malloc(sizeof(int)); // C
int* p4 = new int;
free(p3);
delete p4;
A* p5 = (A*)malloc(sizeof(A) * 10);
A* p6 = new A[10];
free(p5);
delete[] p6;
return 0;
}
运行结果如下:
注意:在申请自定义类型的空间时,new会调用构造函数,delete会调用析构函数,而malloc与
free不会。
3. operator new和operator delete函数
new和delete是用户进行动态内存申请和释放的操作符,operator new 和operator delete是系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通operator delete全局函数来释放空间。
下面是operator new和operator的底层代码:
void* __CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
// try to allocate size bytes
void* p;
while ((p = malloc(size)) == 0)
if (_callnewh(size) == 0)
{
// report no memory
// 如果申请内存失败了,这里会抛出bad_alloc 类型异常
static const std::bad_alloc nomem;
_RAISE(nomem);
}
return (p);
}
void operator delete(void* pUserData)
{
_CrtMemBlockHeader* pHead;
RTCCALLBACK(_RTC_Free_hook, (pUserData, 0));
if (pUserData == NULL)
return;
_mlock(_HEAP_LOCK); /* block other threads */
__TRY
/* get a pointer to memory block header */
pHead = pHdr(pUserData);
/* verify block type */
_ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse));
_free_dbg(pUserData, pHead->nBlockUse);
__FINALLY
_munlock(_HEAP_LOCK); /* release other threads */
__END_TRY_FINALLY
return;
}
4. new和delete的实现原理
4.1 内置类型
如果申请的是内置类型的空间,new和malloc,delete和free基本类似,不同的地方是:new/delete申请和释放的是单个元素的空间,new[]和delete[]申请的是连续空间,而且new在申请空间失败时会抛异常,malloc会返回NULL。
4.2 自定义类型
- new的原理
- 调用operator new函数申请空间。
- 在申请的空间上执行构造函数,完成对象的构造。
- delete的原理
- 在空间上执行析构函数,完成对象中资源的清理工作。
- 调用operator delete函数释放对象的空间。
- new A[N]的原理
- 调用operator new[]函数,在operator new[]中实际调用operator new函数完成N个对象空间的申请。
- 在申请的空间上指向N次构造函数。
- delete[]的原理
- 在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理。
- 调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释放空间。
5. malloc/free和new/delete的区别
共同点:都是从堆上申请空间,并且需要用户手动释放。
不同的地方:
- malloc和free势函数,new和delete是操作符。
- malloc申请的空间不会初始化,new可以初始化。
- malloc申请空间是,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可,如果是多个对象,[]中指定对象个数即可。
- malloc的返回值void*,在使用时必须强转类型,new不需要,因后面跟的是空间的类型。
- malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需要捕获异常。
- 申请自定义类型对象时,malloc/free只会对空间进行操作,不会调用构造函数与析构函数,而new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前调用析构函数完成空间中资源的清理。
6. 初识模版
6.1 泛型编程
Swap函数是经常使用的函数,内核逻辑就是开辟一个临时变量进行交换。当我们要交换的变量类型是int,double或者char时,需要写出三个Swap函数重载。如果还有其他类型变量需要交换,还要再写一个Swap函数的重载,而且函数内部实现是相同的。
重载函数有以下缺点:
- 重载的函数仅仅是类型不同,代码复用率比较低,只要有新类型出现时,就需要用户自己增加对应的函数。
- 代码的可维护性比较低,一个出错可能所有的重载均出错
那我能不能只告诉编译器一个模子,让编译器根据不同的类型利用该模子来生成代码呢?
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;
}
//需要重载三个Swap函数来实现交换
int main()
{
int a1 = 10, a2 = 20;
double d1 = 10.1, d2 = 20.2;
char ch1 = 'a', ch2 = 'b';
Swap(a1, a2);
Swap(d1,d2);
Swap(ch1, ch2);
return 0;
}
C++就存在这样的模具,通过给这个模具不同的参数类型,获得一份代码,这就是模板。
泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础。
6.2 函数模板概念和格式
函数模板代表一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数特定类型版本。
格式:
template<typename T1, typename T2,......,typename Tn>
返回值类型 函数名(参数列表){ }
//只需要实现一个函数即可
template<typename T>
void Swap(T& left, T& right)
{
T temp = left;
left = right;
right = temp;
}
int main()
{
int a1 = 10, a2 = 20;
double d1 = 10.1, d2 = 20.2;
char ch1 = 'a', ch2 = 'b';
Swap(a1, a2);
Swap(d1, d2);
Swap(ch1, ch2);
return 0;
}
6.3 函数模板原理
函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。所以其实模板就是将本来应该我们做的重复的事情交给了编译器。
在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。当用int类型使用函数模板时,编译器通过对实参类型的推演,将T确定为int类型,然后产生一份专门处理int类型的代码,对于浮点数和字符类型也是如此。
6.4 函数模板实例化
用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:隐式实例化和显式实例化。
1.隐式实例化:让编译器根据实参推演模板参数的实际类型
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
int main()
{
int a1 = 10, a2 = 20;
double d1 = 10.0, d2 = 20.0;
//隐式实例化
Add(a1, a2);
Add(d1, d2);
//下面这个语句可以通过编译器吗?
Add(a1, d1);
/*
该语句不能通过编译,因为在编译期间,当编译器看到该实例化时,需要推演其实参类型
通过实参a1将T推演为int,通过实参d1将T推演为double类型,但模板参数列表中只有一个T,
编译器无法确定此处到底该将T确定为int 或者 double类型而报错
注意:在模板中,编译器一般不会进行类型转换操作,
因为一旦转化出问题,编译器就需要背黑锅
*/
// 此时有两种处理方式:
//1. 用户自己来强制转化
//2. 使用显式实例化
//这就是强转
Add(a1, (int)d1);
return 0;
}
2.显式实例化:在函数名后的<>中指定模板参数的实际类型
int main(void)
{
int a = 10;
double b = 20.0;
// 显式实例化
Add<int>(a, b);
return 0;
}
6.5 类模板定义格式与实例化
template<class T1, class T2, ..., class Tn>
class 类模板名
{
// 类内成员定义
};
下面是栈使用类模板。
template <class T>
class Stack
{
public:
Stack (size_t capacity = 4)
{
_array = (T*)malloc(sizeof(T) * capacity);
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
_capacity = capacity;
_size = 0;
}
void Push(const T& data);
private:
T* _array;
size_t _capacity;
size_t _size;
};
//模版不建议声明和定义分离到.h和.cpp会出现链接错误
//模板在类外进行定义需要加参数列表
template<class T>
void Stack<T>::Push(const T& data)
{
// 扩容
_array[_size] = data;
++_size;
}
int main()
{
//类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<>中即可
//类模板名字不是真正的类,而实例化的结果才是真正的类。
Stack<int> st1; // int
Stack<double> st1; // double
return 0;
}
总结
看到这里的小伙伴肯定堆内存管理和模板有了一定的了解,也熟悉了语法的使用。本文有众多代码示例,可以尝试敲敲,运行查看结果,加深理解。
创作不易,希望这篇文章能给你带来启发和帮助,如果喜欢这篇文章,请留下你的三连,你的支持的我最大的动力!!!