【C++】内存管理 + 模板 - 指南

1. 内存管理

1.1 C/C++内存分布

整个进程最多可以申请4GB的空间

答案:

1.2 C语言中动态内存管理方式:malloc/calloc/realloc/free

int main()
{
	// 1.malloc/calloc/realloc的区别是什么?
	int* p2 = (int*)calloc(4, sizeof(int));
	int* p3 = (int*)realloc(p2, sizeof(int) * 50);
	cout << p2 << endl;
	cout << p3 << endl;
	// 这里需要free(p2)吗?
	free(p3);
	int* p5 = new int;	    // 单个对象
	int* p6 = new int[10];  // 数组
	int* p7 = new int(5);	    // 单个对象
	int* p8 = new int[10]{1,2,3,10};  // 数组
	delete p5;
	delete[] p6;
	delete p7;
	delete[] p8;
}

malloc开辟空间,calloc开辟空间的同时还初始化,realloc扩容(原地扩容或者重新找一块地方)

1.3 C++内存管理方式

C语言内存管理方式在C++中可以继续使用,但有些地方就无能为力(自定义类型),而且使用起来比较麻烦,因此C++又提出了自己的内存管理方式:通过new和delete操作符进行动态内存管理。malloc是函数,new是操作符

①new/delete操作内置类型

②new/delete操作自定义类型

new和delete会调用构造函数和析构函数,而malloc不会

class A
{
public:
	A(int a = 0)
		: _a(a)
	{
		cout << "A():" << this << endl;
	}
	~A()
	{
		cout << "~A():" << this << endl;
	}
private:
	int _a;
};
struct ListNode
{
	ListNode* _next;
	int _val;
	ListNode(int val)
		:_next(nullptr)
		, _val(val)
	{}
};
int main()
{
	// 只开空间,不调用构造初始化
	A* p1 = (A*)malloc(sizeof(A));
	A* p2 = new A;
	A* p3 = new A(10);
	delete p2;
	delete p3;
	ListNode* n1 = new ListNode(1);
	ListNode* n2 = new ListNode(2);
	ListNode* n3 = new ListNode(3);
	return 0;
}

如果不断往堆里开辟新的空间存储ptr,最后运行到进程崩溃,如下所示。

void func()
{
	int i = 1;
	int* ptr = nullptr;
	do{
		ptr = new int[1024 * 1024];
		cout << i++ << ":" << ptr << endl;
	} while (ptr);
}

大概i到3656次,进程崩溃,所以我们可以添加调试信息,小技巧

我要调试那个位置在循环的第多少次,怎么到达那呢?

①打条件断点(个人不建议用这个,麻烦)

② 手动搞一个条件断点

在实际项目中,C++都会添加异常处理,避免程序崩溃,用 try catch 进行捕获异常,异常必须被捕获,如果异常没有被捕获,程序就会异常结束

void func()
{
	int i = 1;
	int* ptr = nullptr;
	do{
		if (i == 3656)
		{
			int x = 0; // 只是为了可以在这句加断点,因为vs不可以在空语句上加断点
		}
		ptr = new int[1024 * 1024];
		cout << i++ << ":" << ptr << endl;
	} while (ptr);
}
int main()
{
	try
	{
		func();
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
}

添加异常处理后,程序会报出bad allocation的异常提醒,不会像之前那样崩溃。

1.4 operator new与operator delete函数(重点)

new和delete是用户进行动态内存申请和释放的操作符,operator new 和operator delete是系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间。

注意,new delete和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()
{
	// A* p2 = (A*)operator new(sizeof(A));
	A* p1 = new A(1);
	delete p1;
	A* p2 = new A[10];
	delete[] p2;
	return 0;
}

new先调operator new,再调构造函数;delete先调析构函数,再调operator delete

1.5 new / delete实现原理

1.6 定位new表达式(placement-new) (了解)

定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象

使用格式:
new (place_address) type或者new (place_address) type(initializer-list)
place_address必须是一个指针,initializer-list是类型的初始化列表

使用场景:
定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如
果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化。

class A
{
public:
	A(int a)
		:_a(a)
	{
		cout << "A():" << this << endl;
	}
	~A()
	{
		cout << "~A():" << this << endl;
	}
private:
	int _a;
};
// 定位new/replacement new
int main()
{
	A* p = new A(1);
	// p现在指向的只不过是与A对象相同大小的一段空间,还不能算是一个对象,因为构造函数没有执行
	A* ptr = (A*)operator new(sizeof(A));
	// 显示调用构造函数
	//new(ptr)A,A有默认构造函数时
	new(ptr)A(1);  // 注意:如果A类的构造函数有参数时,此处需要传参
	// 调用析构函数并释放空间
	ptr->~A();
	operator delete(ptr);
	return 0;
}

在 C++ 中,我们日常开发时通常直接使用  new  关键字(如  A* p = new A(1); ),它会自动完成“内存分配 + 构造函数调用”这两个步骤。而  operator new  是更底层的内存分配函数,仅负责分配内存(不调用构造函数),一般只有在需要精细控制内存分配逻辑时才会直接使用它,日常开发中很少直接操作  operator new  或  operator delete 

1.7 malloc/free和new/delete的区别

2. 模板初阶

2.1 泛型编程

如何实现一个通用的交换函数呢?

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;
}

使用函数重载虽然可以实现,但是有一下几个不好的地方:

  1. 重载的函数仅仅是类型不同,代码复用率比较低,只要有新类型出现时,就需要用户自己增
    加对应的函数
  2. 代码的可维护性比较低,一个出错可能所有的重载均出错

那能否告诉编译器一个模子,让编译器根据不同的类型利用该模子来生成代码呢?

如果在C++中,也能够存在这样一个模具,通过给这个模具中填充不同材料(类型),来获得不同材料的铸件(即生成具体类型的代码),那将会节省许多头发。巧的是前人早已将树栽好,我们只
需在此乘凉。

泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础。

2.2 函数模板

1. 概念

函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。

2. 函数模板格式

template
返回值类型 函数名(参数列表){}

示例代码:

template
//template // 第二种写法,class代替typename
void Swap(T& x, T& y)
{
	T temp = x;
	x = y;
	y = temp;
};
int main()
{
	int i = 1, j = 2;
	Swap(i, j);
	char c1 = 'z', c2 = 'y';
	Swap(c1, c2);
	// Swap(i, c2)  // 类型不同,报错
	return 0;
}

注意:typename是用来定义模板参数关键字,也可以使用class(切记:不能使用struct代替
class)

3. 函数模板的原理:

函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。
所以其实模板就是将本来应该我们做的重复的事情交给了编译器

        在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用double类型使用函数模板时,编译器通过对实参类型的推演,
将T确定为double类型,然后产生一份专门处理double类型的代码,对于字符类型也是如此。

4. 函数模板的实例化

不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:隐式实例化
和显式实例化。

①隐式实例化:让编译器根据实参推演模板参数的实际类型

②显示实例化:在函数名后的<>中指定模板参数的实际类型

template
T Add(const T& left, const T& right)
{
	return left + right;
}
template
void Func(size_t n)
{
	T* ptr = new T[n];
	cout << ptr << endl;
}
int main()
{
	int a1 = 10, a2 = 20;
	double d1 = 10.1, d2 = 20.1;
	/*
    该语句不能通过编译,因为在编译期间,当编译器看到该实例化时,需要推演其实参类型
    通过实参a1将T推演为int,通过实参d1将T推演为double类型,但模板参数列表中只有
	一个T,编译器无法确定此处到底该将T确定为int 或者 double类型而报错
   	   注意:在模板中,编译器一般不会进行类型转换操作,因为一旦转化出问题,编译器就需要
	背黑锅
    Add(a1, d1);
   */
   // 此时有两种处理方式:1. 用户自己来强制转化 2. 使用显式实例化
	// 隐式实例化(实参类型,推导模板参数类型)
	cout << Add(a1, a2) << endl;
	cout << Add(d1, d2) << endl;
	cout << Add((double)a1, d1); //  用户自己来强制转化
	cout << Add(a1, (int)d1);
	// 显示实例化
	cout << Add(a1, d1) << endl;
	cout << Add(a1, d1) << endl;
    Func(10);
    Func(10);
	return 0;
}

注意:当不知道T是什么类型时,尽量加const引用

5. 模板参数的匹配原则:

  1. 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这
    个非模板函数
  2. 对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而
    不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模
  3. 模板函数不允许自动类型转换,但普通函数可以进行自动类型转换
// 专门处理int的加法函数
int Add(int left, int right)
{
	return left + right;
}
// 通用加法函数
template
T Add(const T& left, const T& right)
{
	return left + right;
}
int main()
{
	int a1 = 10, a2 = 20;
	double d1 = 10.1, d2 = 20.1;
	cout << Add(a1, a2) << endl;  // 有现成的匹配现成的
	cout << Add(a1, a2);     //这个强制调模板
	cout << Add(d1, d2) << endl;  //这个没有现成的,调模板
	return 0;
}
// 专门处理int的加法函数
int Add(int left, int right)
{
	return left + right;
}
// 通用加法函数
template
T2 Add(const T1& left, const T2& right)
{
	return left + right;
}
int main()
{
	cout << Add(1, 2) << endl;  //调用现成的第一个
	cout << Add(1, 1.21) << endl; // 调用通用的模板函数
	return 0;
}

2.3 类模板

1. 类模板的定义格式

template
class 类模板名
{
    // 类内成员定义
};
////类模板
//typedef int T;
//
template
class Stack
{
public:
	Stack(size_t n = 4)
		:_a(new T[n])
		, _top(0)
		, _capacity(0)
	{}
private:
	T* _a;
	size_t _top;
	size_t _capacity;
};
int main()
{
	//类模板都必须显示实例化
	//普通类:类名就是类型;类模板:类名不是类型,类名<模板参数> 这个才是类型 -> eg:Stack
	//Stack st1;  // 存int
	//Stack st2;  // 存double
	Stack st1; // 存int
	Stack st2; // 存double
	return 0;
}

上面存int的Stack和存double的Stack,两个不是一个类型

类的特性,模板也都遵循

eg:你不写构造,编译器会默认生成构造

示例代码:

// 泛型编程
// 模板不支持声明和定义分离定义.h 和 .cpp
template
class Stack
{
public:
	//Stack(size_t n = 4)
	//	:_a(new T[n])
	//	, top(0)
	//	, capacity(0)
	//{}
	//void Push(const T& x)
	//{
	//	//扩容
	//	//...
	//	_a[top++] = x;
	//}
	Stack(size_t n = 4);  // 函数的声明
	void Push(const T& x);
private:
	T* _a;
	size_t _top;
	size_t _capacity;
};
template
Stack::Stack(size_t n)
	:_a(new T[n])
	, _top(0)
	, _capacity(0)
{}
template
void Stack::Push(const T& x)
{
	// 扩容
	//...
	_a[_top++] = x;
}
template  //可以给缺省参数
class A
{
public:
	T x1;
	T x2;
};
template  //可以写全缺省,也可以写半缺省,跟以前一样的用法
class B
{
public:
	T1 x1;
	T2 x2;
};
int main()
{
	//类模板都必须显示实例化
	Stack st1;  //存int
	st1.Push(1);
	st1.Push(2);
	st1.Push(3);
	Stack st2;  //存double
	st2.Push(1.1);
	st2.Push(2.1);
	st2.Push(3.1);
	A<> aa1;  // 用缺省的int类型
	A aa2;
	B bb1;
	B bb2;
}

类模板的实例化

类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。

// Stack是类名,Stack才是类型
Stack st1;    // int
Stack st2; // double

posted on 2025-10-01 22:58  slgkaifa  阅读(11)  评论(0)    收藏  举报

导航