C++之动态内存管理(new、delete、operator new、operator delete、new的定位表达式)

一、C/C++内存分布

  • 栈,又叫堆栈,非静态局部变量/函数参数/返回值等等,栈是向下增长的。
  • 内存映射段,是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享内存,做进程间通信。
  • 堆,用于程序运行时动态内存分配,堆是可以向上增长的。
  • 数据段,存储全局数据和静态数据。
  • 代码段,可以执行的代码/只读常量。

二、C++动态申请内存

  • C++中通过new和delete操作符进行动态内存管理。

1.对于内置类型的操作

  • 申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间,使用new[]和delete[]。
#include <iostream>

using namespace std;

void test()
{
	// 动态申请一个int类型的空间
	int* ptr = new int;
	//对应的delete
	delete ptr;

	// 动态申请一个int类型的空间并且初始化为5
	int* ptr2 = new int(5);
	//对应的delete
	delete ptr2;

	//动态申请5个int类型的空间(动态数组)
	int* ptr3 = new int[3];
	//对应的delete
	delete[] ptr3;

}
int main()
{
	test();

	return 0;
}

2.对于自定义类型的操作

  • 在申请自定义类型的空间时,new会调用构造函数,delete会调用析构函数,而malloc与free不会。
#include <iostream>

using namespace std;

namespace sheena
{
	class Test
	{
	public:
		Test()
			:_t(0)
		{
			cout << "Test()构造函数:" << this << endl;
		}

		~Test()
		{
			cout << "~Test()析构函数:" << this << endl;
		}
	private:
		int _t;
	};
}
void test1()
{
	// 申请单个Test类型的空间
	sheena::Test* ptr1 = (sheena::Test*)malloc(sizeof(sheena::Test));
	// 对应的释放空间
	free(ptr1);

	// 申请10个Test类型的空间
	sheena::Test* ptr2 = (sheena::Test*)malloc(sizeof(sheena::Test) * 10);
	// 对应的释放空间
	free(ptr2);
}
void test2()
{
	// 申请单个Test类型的空间
	sheena::Test* ptr1 = new sheena::Test;
	// 对应的释放空间
	delete ptr1;

	// 申请10个Test类型的空间
	sheena::Test* ptr2 = new sheena::Test[10];
	// 对应的释放空间
	delete[] ptr2;
}
int main()
{
	cout << "test1()malloc和free:" << endl;
	test1();
	cout << endl;

	cout << "test2()new和delete:" << endl;
	test2();
	cout << endl;

	return 0;
}

在这里插入图片描述

3.operator new 与 operator delete函数

  • operator new 和 operator delete都不是重载函数。
  • new和delete是用户进行动态内存申请和释放的操作符,operator new和operator delete是系统提供的全局函数,new在底层调用operator new全局函数来申请空间的,delete在底层通过operator delete全局函数来释放空间的。
  • operator new:该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回;申请空间失败,尝试执行空间不足应对措施,如果该应对措施用户设置了,则继续申请,否则抛出异常。
  • operator delete:该函数最终通过free来释放空间的。
using namespace sheena;
void test3()
{
	// 申请单个Test类型的空间 + 申请失败返回0
	Test* ptr1 = (Test*)malloc(sizeof(Test));

	// 申请单个Test类型的空间 + 申请失败抛出异常(用户若没设置)
	Test* ptr2 = (Test*)operator new(sizeof(Test));

	// 申请单个Test类型的空间 + 申请失败抛出异常 + 初始化(调用构造函数)
	// 也就是operator new + 初始化(调用构造函数)
	Test* ptr3 = new Test;
}

4.new和delete的实现原理

(1)对于内置类型

  • 如果申请的是内置类系的空间,new和malloc,delete和free基本类似。
  • new和malloc,delete和free不同的地方是:new/delete申请和释放的是单个元素的空间,new[]/delete[]申请和释放的是连续空间。
  • new在申请空间失败时会抛出异常,而malloc是直接返回NULL。

(2)对于自定义类型

  • new的原理:调用operator new函数申请空间,然后在申请的空间上执行构造函数,完成对象的初始化。
  • delete的原理:在要释放的空间上执行析构函数,完成对象中资源的清理工作。然后调用operator delete函数释放对象的空间。
  • new T[ N]的原理:调用operator new[]函数,在operator new[]函数中实际用operator new函数完成对N个对象空间的申请,然后在申请的空间上执行N次构造函数,完成N个对象的初始化。
  • delete[]的原理:在要释放的对象空间上执行N此析构函数,完成N个对象中资源的清理。然后调用operator delete[]函数,在operator delete[]函数中实际调用operator delete函数来释放空间。

5.new的定位表达式

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

using namespace std;

namespace sheena
{
	class Test
	{
	public:
		Test()
			:_t(0)
		{
			cout << "Test()构造函数:" << this << endl;
		}

		~Test()
		{
			cout << "~Test()析构函数:" << this << endl;
		}
	private:
		int _t;
	};
}
using namespace sheena;

void test()
{
	//在内存池中申请空间
	char* pool = (char*)malloc(1024 * 4);
	Test* ptr = (Test*)pool;
	pool += sizeof(Test);

	//new的定位表达式
	new(ptr)Test;

	ptr->~Test();
	ptr = nullptr;
	pool -= sizeof(Test);//将释放的空间还给内存池
}
int main()
{
	test();

	return 0;
}

在这里插入图片描述

5.malloc/free和new/delete的区别

  • malloc和free是函数,new和delete是操作符。
  • malloc申请的空间没有初始化,new申请的空间可以初始化。
  • malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可。(Test* ptr1 = (Test*)malloc(sizeof(Test)),Test* ptr3 = new Test;)
  • malloc的返回值是void*,在使用时必须强转,new不需要强转,因为new后跟的是空间的类型。
  • malloc申请空间失败时,返回的是NULL,因此使用时必须要判断是否为空。而new申请空间失败时会抛出异常,因此在使用时不需要判空,但是需要捕获异常。
  • 在对自定义类型对象空间的申请时,malloc/free只会申请和释放空间。而new在申请空间之后会调用构造函数完成对象的初始化,delete在释放空间之前会调用析构函数完成对象中资源的清理。
  • 当是在内存池中申请空间和不能抛异常的场景下使用malloc和free。

三、内存泄漏

1.内存泄漏的定义

  • 内存泄漏指的是因为疏忽或者错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为错误,失去了对该段内存的控制,因而造成了内存的浪费。

2.内存泄漏的危害

  • 长期运行的程序出现内存泄漏,影响会很大,如操作系统、后台服务等等。出现内存泄漏会导致设备响应越来越慢,最终卡死。

3.内存泄漏的分类

  • 堆内存泄漏:堆内存值的是程序执行中依据要分配通过malloc、calloc、realloc、new等从堆中分配的一块内存,用完后必须通过调用相应的free、delete释放掉。如果程序的设计错误导致部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。
  • 系统资源泄露:指程序使用系统分配的资源,如套接字、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。

4.如何避免

  • 申请的空间内存使用完毕之后一定要释放。
  • 尽量避免在堆上分配内存。尽可能的使用栈上的内存。
  • 采用RAII思想或者智能指针来管理资源。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
表达式语法分析器是一个程序,它可以解析并计算数学表达式。它的实现原理如下: 1. 词法分析:将输入的字符串分解成多个单词,如标识符、常量、运算符等。这个过程称为词法分析。 2. 语法分析:将分解后的单词按照规则组合成语法树。这个过程称为语法分析。在语法分析过程中,会检查表达式中是否有语法错误,例如括号不匹配或者操作符使用不正确等。如果有错误,则会提示用户。 3. 计算表达式:在语法分析完成后,将语法树上的节点进行计算,得出表达式的结果。 C++代码实现示例: ```cpp #include <iostream> #include <string> #include <map> #include <stack> #include <vector> #include <cmath> using namespace std; // 定义词法分析的单词类型 enum TokenType { TOK_IDENTIFIER, // 标识符 TOK_CONSTANT, // 常数 TOK_PLUS, // + TOK_MINUS, // - TOK_MULTIPLY, // * TOK_DIVIDE, // / TOK_LEFTPAREN, // ( TOK_RIGHTPAREN, // ) TOK_UNKNOWN // 未知类型 }; // 定义语法树节点类型 enum NodeType { NODE_CONSTANT, // 常数节点 NODE_IDENTIFIER, // 标识符节点 NODE_UNARYOP, // 一元运算符节点 NODE_BINARYOP, // 二元运算符节点 NODE_FUNCTION_CALL // 函数调用节点 }; // 定义运算符的优先级 map<TokenType, int> OperatorPrecedence = { {TOK_PLUS, 1}, {TOK_MINUS, 1}, {TOK_MULTIPLY, 2}, {TOK_DIVIDE, 2} }; // 定义词法分析器 class Lexer { public: Lexer(string input) : input(input) {} // 获取下一个单词 TokenType getNextToken(string& tokenValue) { tokenValue = ""; // 跳过空格 while (pos < input.length() && isspace(input[pos])) { pos++; } if (pos >= input.length()) { return TOK_UNKNOWN; } // 判断单词类型 if (isalpha(input[pos])) { // 标识符 while (pos < input.length() && (isalnum(input[pos]) || input[pos] == '_')) { tokenValue += input[pos]; pos++; } return TOK_IDENTIFIER; } else if (isdigit(input[pos])) { // 常数 while (pos < input.length() && (isdigit(input[pos]) || input[pos] == '.')) { tokenValue += input[pos]; pos++; } return TOK_CONSTANT; } else { // 运算符或括号 switch (input[pos]) { case '+': pos++; return TOK_PLUS; case '-': pos++; return TOK_MINUS; case '*': pos++; return TOK_MULTIPLY; case '/': pos++; return TOK_DIVIDE; case '(': pos++; return TOK_LEFTPAREN; case ')': pos++; return TOK_RIGHTPAREN; default: pos++; return TOK_UNKNOWN; } } } private: string input; int pos = 0; }; // 定义语法树节点 class ASTNode { public: ASTNode(NodeType type, string value = "") : type(type), value(value) {} NodeType getType() const { return type; } double evaluate() const { switch (type) { case NODE_CONSTANT: return stod(value); case NODE_IDENTIFIER: return variables[value]; case NODE_UNARYOP: if (value == "-") { return -child->evaluate(); } else { return child->evaluate(); } case NODE_BINARYOP: switch (value[0]) { case '+': return left->evaluate() + right->evaluate(); case '-': return left->evaluate() - right->evaluate(); case '*': return left->evaluate() * right->evaluate(); case '/': return left->evaluate() / right->evaluate(); default: throw "Unknown operator: " + value; } default: throw "Unknown node type"; } } // 释放语法树的内存 void free() { if (child) { child->free(); delete child; child = nullptr; } if (left) { left->free(); delete left; left = nullptr; } if (right) { right->free(); delete right; right = nullptr; } } string value; // 值 ASTNode* child = nullptr; // 子节点 ASTNode* left = nullptr; // 左子节点 ASTNode* right = nullptr; // 右子节点 private: NodeType type; static map<string, double> variables; // 变量表 }; map<string, double> ASTNode::variables; // 定义语法分析器 class Parser { public: Parser(string input) : lexer(input) {} ASTNode* parse() { auto rootNode = parseExpression(); if (currentToken != TOK_UNKNOWN) { throw "Unexpected token: " + currentToken; } return rootNode; } private: // 解析表达式 ASTNode* parseExpression(int precedence = 0) { auto leftNode = parsePrimary(); while (true) { auto token = lexer.getNextToken(currentTokenValue); auto tokenPrecedence = OperatorPrecedence[token]; if (tokenPrecedence == 0 || tokenPrecedence <= precedence) { break; } lexer.getNextToken(currentTokenValue); if (token == TOK_MINUS) { // 一元运算符 auto unaryNode = new ASTNode(NODE_UNARYOP, "-"); unaryNode->child = leftNode; leftNode = unaryNode; } else { // 二元运算符 auto binaryNode = new ASTNode(NODE_BINARYOP, currentTokenValue); binaryNode->left = leftNode; binaryNode->right = parseExpression(tokenPrecedence); leftNode = binaryNode; } } return leftNode; } // 解析基础表达式 ASTNode* parsePrimary() { auto token = lexer.getNextToken(currentTokenValue); ASTNode* node = nullptr; switch (token) { case TOK_IDENTIFIER: // 标识符 node = new ASTNode(NODE_IDENTIFIER, currentTokenValue); break; case TOK_CONSTANT: // 常数 node = new ASTNode(NODE_CONSTANT, currentTokenValue); break; case TOK_LEFTPAREN: // 括号 node = parseExpression(); if (lexer.getNextToken(currentTokenValue) != TOK_RIGHTPAREN) { throw "Expecting right parenthesis"; } break; default: throw "Unexpected token: " + token; } return node; } Lexer lexer; TokenType currentToken = TOK_UNKNOWN; string currentTokenValue; }; int main() { while (true) { cout << "> "; string input; getline(cin, input); if (input == "quit") { break; } try { Parser parser(input); auto rootNode = parser.parse(); cout << rootNode->evaluate() << endl; rootNode->free(); delete rootNode; } catch (const char* error) { cout << error << endl; } } return 0; } ``` 上述代码实现了一个简单的表达式语法分析器,支持基本的运算符和括号,并可以计算表达式的结果。其中,关键的步骤包括词法分析、语法分析、语法树构建和表达式求值。在词法分析中,程序首先会跳过空格,然后根据字符类型确定单词类型,例如标识符、常数、运算符或括号等。在语法分析中,程序将单词组合成语法树,判断表达式是否符合语法规则,并检查是否有语法错误。在语法树构建完成后,程序对语法树进行遍历,计算表达式的结果。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值