【c++篇】:从基础到实践--c++内存管理技巧与模版编程基础

news2024/12/23 20:26:21

✨感谢您阅读本篇文章,文章内容是个人学习笔记的整理,如果哪里有误的话还请您指正噢✨
✨个人主页:余辉zmh–CSDN博客
✨ 文章所属专栏:c++篇–CSDN博客

在这里插入图片描述

文章目录

  • 前言
  • 一.c/c++内存分布
  • 二.c/c++的动态内存管理方式
    • 2.1.c语言的动态内存管理方式
    • 2.2.c++的动态内存管理方式
    • 2.3.c/c++动态内存管理方式的区别
  • 三.new和delete的底层实现原理
    • 3.1`operator new`和`operator delete`函数
    • 3.2`new `和`delete`的实现原理
  • 四.定位new表达式
  • 五.内存泄漏
  • 六.模版初阶
    • 6.1泛型编程
    • 6.2函数模版
    • 6.3类模版

前言

内存管理和模板是C++编程中两个重要的主题,它们共同构成了高效、灵活和可复用的代码基础。内存管理涉及到如何有效利用计算机内存资源,确保程序的性能和稳定性;而模板则提供了一种参数化类型和函数的机制,使得代码能够以类型无关的方式编写,从而实现高度的抽象和复用。理解这些概念对于编写高质量和可维护的C++代码至关重要。本文将探讨C++中的内存管理和模板的基础知识,帮助读者掌握这两个核心主题,为构建复杂系统打下坚实的基础。

一.c/c++内存分布

在一个程序中,对于各种不同的数据需要存储在不同的地方,比如:局部数据存储在栈区,静态数据和全局数据存储在静态区或者数据段,常量数据存储在常量区或代码段,动态申请的数据则是存储在堆上。

在c/c++中,内存分为以下几个区域:

  • 栈区:

    • 编译器自动分配和释放
    • 存放函数的局部变量,函数参数,返回地址等。
    • 具有先进后出的特性,栈是向下增长的。
    • 内存分配率高,但空间有限
  • 堆区:

    • 由程序员自己手动分配空间和释放,在C语言中由malloc,calloc,realloc等函数分配空间,而c++中使用new操作符。
    • 如果没有释放,程序结束时操作系统会回收,释放空间时,C语言使用free,而c++使用delete
    • 内存分配效率相对较低,但空间较大。
    • 堆是可以向上增长的。
  • 全局或静态区:

    • 存放全局变量和静态变量
    • 程序加载时分配内存空间,程序结束时释放。
  • 区或代码段:

    • 常量区存放常量等只读数据,防止修改数据。
    • 代码段存放程序的机器指令,只能读取,不能修改。

这里提供一段代码来分析一下数据的存储区域:

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);
    free(ptr1);
}

globalVar 全局变量 在数据段(静态区) staticGlobalVar 静态全局变量 在数据段(静态区)

staticVar 静态局部变量 在数据段(静态区) localVar 局部变量 在栈

num1局部数组 在栈

char2 字符数组 在栈 *char2 数组元素存储位置 在栈

pchar3 指针变量 在栈 *pchar3常量字符 在代码段(常量区)

ptr1指针变量 在栈 *ptr1 动态内存 在堆

二.c/c++的动态内存管理方式

2.1.c语言的动态内存管理方式

在C语言中申请动态内存主要通过malloc,calloc,realloc等函数,这些函数都是堆上申请空间,且都是void*类型,需要进行数据类型转换,在申请完之后还要进行判断是否为空(因为申请失败会返回空)。

  • malloc函数:在动态存储区申请申请一块指定大小的连续空间,返回空间的地址(数组返回的是首元素的地址),类型为void*类型。释放时使用free函数。

    int* p1=(int*) malloc(sizeof(int));
    free(p1);
    
  • calloc函数:在内存的动态存储区申请指定数量相同大小的内存空间,返回空间的地址(数组返回的是首元素的地址),类型为void*类型,和malloc不同的是,calloc会将申请成功的空间初始化为0。

    int *p2=(int*)calloc(3,sizeof(int));
    
  • realloc函数是对原来申请空间的扩充,也就是增加原来空间的大小,如果原先空间后面大小充足允许扩充,就会原地扩充,大小不足时,就会重新在其他地方申请空间,再将原本空间的数据拷贝过来,类型依然是void*类型。

    int* p3=(int*)realloc(p2,sizeof(int)*10);
    free(p3);
    

2.2.c++的动态内存管理方式

在c++中,动态内存分配主要通过new,delete两个关键字实现,相比于C语言的动态内存分配,c++的更为直观和方便。

  • new关键字用于动态申请内存空间,不需要显示内存空间大小(编译器会根据类型自己计算),返回的是申请空间的地址(对于数组来说,返回的是首元素的地址),也可以设置初始值来初始化申请的内存空间。

  • delete关键字1用于释放动态分配的内存空间,对于单个对象,使用delete释放,对于数组空间,需要在delete后加上[],表示释放整个数组空间。

    对于内置类型:

    • 动态申请一个int类型的未初始化空间:

      int* ptr1=new int;
      delete ptr1;
      
    • 动态申请一个int类型的空间并初始化为10:

      //初始化使用的是()
      int* ptr2=new int(10);
      delete ptr2;
      
    • 动态申请10个int类型的空间:

      //申请连续的空间时,使用的是[];
      int* ptr3=new int[10];
      delete[] ptr3;
      

    对于自定义类型:

    new会调用构造函数,delete会调用析构函数;而mallocfree不会

    class A {
    public:
    	A(int a=0) 
            :int _a(a)
        {
    		cout << "A()" << endl;
    	}
    	~A() {
    		cout << "~A()" << endl;
    	}
    private:
        int _a;
    };
    
    int main() {
        //new申请空间后调用构造函数完成初始化
    	A* ptr1 = new A;
        //delete在释放空间前会调用析构函数完成对空间资源的清理
    	delete ptr1;
    
    	A* ptr2 = (A*)malloc(sizeof(A));
    	if (ptr2 == NULL) {
    		perror("malloc fail");
    	}
    	free(ptr2);
    
    	return 0;
    }
    

    在这里插入图片描述

2.3.c/c++动态内存管理方式的区别

c语言使用的是mallocfree,而c++使用的是newdelete,他们的共同点是:都是从堆上申请内存空间,并且需要用户手动释放空间。而不同点是:

  • mallocfree是函数,而newdelete是操作符。
  • malloc申请的空间不会初始化,而new申请的空间可以初始化,初始化时使用new 类型(初始值)
  • malloc申请空间时需要手动计算空间大小并传递,而new只需在new后面加上类型就可以,如果申请多个对象时,使用new 类型[个数]
  • malloc的返回值为void*类型,并且需要强制转换类型,而new不需要,直接在new后面加上类型既可
  • malloc申请空间失败时返回NULL,所以需要判断空间是否申请成功,而new不需要判断,因为new是抛异常
  • 对于自定义类型对象,mallocfree只能开辟空间和释放,不会调用构造函数和析构函数,而new在申请空间后会调用构造函数完成初始化,delete在释放空间前会调用析构函数完成对空间中资源的清理

三.new和delete的底层实现原理

3.1operator newoperator delete函数

  • operator new是系统提供的全局函数,new在底层调用operator new全局函数来申请空间,而operator new实际上也是通过malloc来申请空间,如果malloc申请空间成功就会直接返回,否则就会抛异常

  • operator new一样,operator delete也是系统提供的全局函数,delete在底层通过operator delete全局函数来释放空间,而operator delete最终也是通过调用free来释放空间

    下面代码通过模拟实现来演示new和delete如何完成空间的申请和释放:

    void* operator new(size_t size) {
    	void* ptr = malloc(size);
    	if (!ptr) {
    		throw bad_alloc();
    	}
    	cout << "申请空间调用operator new函数:" << endl;
    	cout << "void* operator new(size_t size)" <<" "<< ptr << endl;
    	return ptr;
    }
    
    void operator delete(void* ptr) {
    	cout << "释放空间调用operator delete函数:" << endl;
    	cout << "void operator delete(void* ptr)" <<" "<< ptr << endl;
    	free(ptr);
    }
    
    class Myclass {
    public:
    	Myclass() {
    		cout << "申请空间后调用构造函数完成初始化:" << endl;
    		cout << "Myclass()" << endl;
    	}
    	~Myclass() {
    		cout << "释放空间前调用析构函数完成对象资源的清理:" << endl;
    		cout << "~Myclass()" << endl;
    	}
    };
    
    int main() {
    	Myclass* ptr = new Myclass;
    	cout << " " << endl;
    	delete ptr;
    	return 0;
    }
    

    在这里插入图片描述

3.2new delete的实现原理

  • 对于内置类型:

    如果申请的是内置类型的空间,newmallocdeletefree基本类似,不同点可以看上面的2.3内容。

  • 对于自定义类型:

    • new的原理:

      1.调用operator new申请空间

      2.申请空间成功后调用构造函数完成初始化

    • new T[N]的原理:

      1.调用operator new[]函数,实际调用operator new函数完成对N个对象空间的申请

      2.申请N个空间,就要调用N次构造函数完成初始化

    • delete的原理:

      1.在释放空间前调用析构函数,完成对对象空间中资源的清理

      2.调用operator delete函数释放空间

    • delete []的原理:

      1.在释放空间前调用N次析构函数完成对N个对象的资源的清理

      2.调用operator delete[]函数释放空间,实际上是调用operator delete函数来释放空间

四.定位new表达式

在c++中,定位new表达式是一种特殊的语法结构,它允许程序员在自定义的内存位置上构造对象。定位new不会分配内存,他只会在指定的内存地址上调用对象的构造函数。使用定位new需要确保提供的内存位置足够大,否则可能会导致未定义行为。同样的,如果不再需要该对象时,需要手动调用析构函数来释放空间,因为定位new不会管理内存的生命周期。

以下面这段代码为例:

class Myclass {
public:
	Myclass(int a)
		:_a(a)
	{
		cout << "Myclass(int a)" << endl;
	}

	~Myclass() {
		cout << "Myclass()" << endl;
	}

private:
	int _a;
};

int main() {
	//分配一块足够大的内存
	char ptr[sizeof(Myclass)];
	//在ptr指向的内存上构造Myclass对象
    //使用定位new格式,new(内存空间)类类型(初始化值)
	Myclass* p = new(ptr)Myclass(12);

	//不再需要该对象时,手动调用析构函数
	p->~Myclass();

	return 0;
}

在这里插入图片描述

在上面这个例子中,ptr是一个字符数组,其大小足够容纳一个Myclass对象,然后使用定位newptr的内存空间上构造一个Myclass对象,最后在手动调用析构函数来销毁。

五.内存泄漏

内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

  • 内存泄漏的常见原因包括:

    • 程序员的错误:程序员未正确地释放动态内存,或者使用了不恰当的数据结构,导致内存无法释放。
    • 循环引用:在使用面向对象的编程语言时,两个或多个对象彼此引用,导致它们之间形成了循环引用,使得这些对象无法被垃圾回收器及时释放。
  • 内存泄漏的解决办法主要包括:

    • 正确使用动态内存分配:在使用完动态内存之后,及时将其释放。
    • 使用内存泄漏检测工具:可以自动检测出内存泄漏问题,并给出错误信息和定位。常见的内存泄漏检测工具包括Valgrind、GDB、DMalloc、Purify、Electric Fence等。

六.模版初阶

6.1泛型编程

我们首先来看一下下面这段代码:

void Swap(int& a,int&b){
    int tmp=a;
    a=b;
    b=tmp;
}
void Swap(double& a,double&b){
    double tmp=a;
    a=b;
    b=tmp;
}
void Swap(char& a,char&b){
    char tmp=a;
    a=b;
    b=tmp;
}

如果我们要写一个交换函数,对于不同类型的数据需要不同的类型函数,虽然可以使用函数重载来实现,但是复用率较低,而且有新的类型时,还需要自己增加对应类型的函数,并且可维护性较低,如果一个出错可能所有的重载函数均会出错。

那么能不能给编译器一个模版,让编译器自己根据类型来生成对应的代码呢?答案是当然可以,这就是我们接下来需要了解的泛型编程。

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

模版分为函数模版和类模版

6.2函数模版

函数模版与类型无关,在使用时被参数化,可以根据使用类型产生对应类型的函数。

  • 函数模版格式:
//可以使用typename或者class来定义模版参数,但不能使用struct
template<typename/class T1,typename/class T2....>
返回值类型 函数名(参数列表){
    ...
}

以上面的交换函数为例:

template<typename T>
void Swap(T& a, T& b) {
	T tmp = a;
	a = b;
	b = tmp;
}
  • 函数模版的实例化:

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

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

      template<typename T>
      void Swap(T& a, T& b) {
      	T tmp = a;
      	a = b;
      	b = tmp;
      }
      int main() {
      	int a = 1, b = 2;
      	Swap(a, b);
      	double c = 3, d = 4;
      	Swap(c, d);
      	cout << a <<" "<< b << endl;
      	cout << c << " " << d << endl;
      	return 0;
      }
      

      在这里插入图片描述

    • 显示实例化:在函数名前面指定模版参数的实际类型

      如果对于a,b两个不同类型的数据使用函数模版时,就会产生歧义

      template<typename T>
      T Add(const T& a, const T& b) {
      	return a + b;
      }
      int main()
      {
      	int a = 10;
      	double b = 20.0;
      	cout <<(Add(a, b))<< endl;
      	return 0;
      }
      

      在这里插入图片描述

      此时有两种处理方式:

      1.用户自己强制转化

      Add(a,(int)b);
      

      2.使用显示实例化

      Add<int>(a,b);
      

      以上两种方式结果都为30:

      在这里插入图片描述

  • 函数模版的原理:

    在编译阶段,对于模版函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。

    在这里插入图片描述

6.3类模版

  • 类模版的定义格式:

    //使用的是class关键字
    template<class T1,class T2....>
    class 类模版名
    {
        //类中成员定义
        ....
    }

    以栈Stack为例:

    template<class T>
    class Stack {
    public:
    	Stack(int capacity=4)
    		:_a(new T[capacity])
    		, _top(0)
    		, _capacity(capacity)
    	{
    		cout << "Stack()" << endl;
    	}
        
        //其他成员函数....
        
        //析构函数在类中声明,在类外定义
    	~Stack();
    
    private:
    	T* _a;
    	int _top;
    	int _capacity;
    };
    //在类外定义时需要加模版参数列表
    //函数名前加:(类名<T>::)
    template<class T>
    Stack<T>::~Stack() {
    	cout << "Stack<T>::~Stack()" << endl;
    	delete _a;
    	_top = 0;
    	_capacity = 0;
    }
    
  • 类模版的实例化:

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

    以上面的栈模版为例:

    int main() {
        //Stack为类名,Stack<数据类型>才是类型
    	Stack<int> s1(5);
    	Stack<double> s2(10);
    	Stack<char> s3(2);
    	return 0;
    }
    

以上就是关于c++内存管理方式和模版初阶的讲解,如果哪里有错的话,可以在评论区指正,也欢迎大家一起讨论学习,如果对你的学习有帮助的话,点点赞关注支持一下吧!!!
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2224451.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

JavaEE初阶---多线程(五)---定时器/线程池介绍

文章目录 1.定时器的介绍2.线程池2.1为什么需要使用线程池2.2如何进行线程池的创建2.3普通的构造方法的局限性2.4该种对象创建的方法的特点2.5线程池的模拟实现的逻辑 3.ThreadPoolExecutor类的介绍3.1构造方法3.2四种拒绝的策略 1.定时器的介绍 下面的这个就是我们的这个定时…

基于JSP的高校食堂食材选购管理系统【附源码】

基于JSP的高校食堂食材选购管理系统 效果如下&#xff1a; 系统首页界面 用户登录页面 食材信息页面 论坛交流界面 管理员登录界面 管理员功能主界面 食材信息管理界面 订单配送管理界面 用户功能主界面 商家功能主界面 司机功能主界面 研究背景 近年来互联网技术的发展使得…

【C++篇】手撕string类:从初级到高级入门

1.为什么手撕string类 在面试或者一些学习场景中&#xff0c;手撕 string 类不仅仅是对字符串操作的考察&#xff0c;更多的是考察程序员对 C 内存管理的理解。例如&#xff0c;深拷贝与浅拷贝的实现&#xff0c;如何正确重载赋值运算符&#xff0c;如何避免内存泄漏&#xff…

线上环境的 JAVA 程序占用太多 CPU 资源,定位原因

线上环境的 JAVA 程序占用太多 CPU 资源&#xff0c;定位原因 top 命令执行显示一下结果 我们可以看到有一个 PID 是 4054 的应用程占用了超过一半的 CPU 资源&#xff0c;这是十分糟糕的事情&#xff0c;这个时候我们首先定位一下他是哪个线程在这里搞事情&#xff0c;这个时…

【JavaSE】认识String类,了解,进阶到熟练掌握

#1024程序员节 | 征文# 下面就让博主带领大家一起解决心中关于String类的疑问吧~~~ 1.字符串构造&#xff1a; 第一种和第二种&#xff08;有一定的区别&#xff0c;在常量池上&#xff09; public static void main(String[] args) { // 使用常量串构造 String s1 "h…

【机器学习】——numpy教程

文章目录 1.numpy简介2.初始化numpy3.ndarry的使用3.1numpy的属性3.2numpy的形状3.3ndarray的类型 4numpy生成数组的方法4.1生成0和1数组4.2从现有的数组生成4.3生成固定范围的数组4.4生成随机数组 5.数组的索引、切片6.数组的形状修改7.数组的类型修改8.数组的去重9.ndarray的…

【Visual Studio】下载安装 Visual Studio Community 并配置 C++ 桌面开发环境的图文教程

引言 Visual Studio 是一个面向 .NET 和 C 开发人员的综合性 Windows 版 IDE&#xff0c;可用于构建 Web、云、桌面、移动应用、服务和游戏。 安装步骤 访问 Visual Studio 的官方下载页面&#xff1a; https://visualstudio.microsoft.com/zh-hans/downloads/运行已下载的 V…

java疫苗发布和接种预约系统源码(springboot)

项目简介 疫苗发布和接种预约系统实现了以下功能&#xff1a; 疫苗发布和接种预约系统的主要使用者分为&#xff1a; 管理员对公告信息&#xff0c;医院信息&#xff0c;疫苗信息&#xff0c;医生信息&#xff0c;用户信息&#xff0c;论坛帖子信息以及预约接种信息等信息进行…

ThinkPad T480拆机屏幕改装:便携式显示器DIY指南

ThinkPad T480拆机屏幕改装&#xff1a;便携式显示器DIY指南 本文记录了将旧笔记本电脑 T480 拆机屏幕改装为便携式显示器的全过程。作者在决定升级设备后&#xff0c;选择通过 DIY 方式利用原有的屏幕资源。文章详细介绍了屏幕驱动板的安装、螺丝孔的剪裁、排线连接及固定的步…

系统性能优化——绑核

简要 绑核正如其名&#xff0c;将线程/进程绑定在一个或多个CPU核心。该技术可以使进程或线程在特定的处理器上运行&#xff0c;而不会被操作系统调度到其他处理器上。这里有两层含义。 如果线程被绑定在指定核心上&#xff0c;则只会在该核心上运行&#xff0c;即使其他核心…

Django自定义过滤器

一、介绍 Django过滤器是一种用于在Django模板中处理数据的技术。它们的主要作用是对模板中的变量进行加工、过滤或格式化&#xff0c;然后返回一个新的值供模板使用。这些过滤器可以在变量输出时&#xff0c;对输出的变量值做进一步的处理&#xff0c;以满足特定的显示需求。…

C# 串口通信教程

串口通信&#xff08;Serial Communication&#xff09;是一种用于设备之间数据传输的常见方法&#xff0c;通常用于与外部硬件设备&#xff08;如传感器、机器人、微控制器&#xff09;进行通信。在 C# 中&#xff0c;System.IO.Ports 命名空间提供了与串口设备交互的功能&…

Golang | Leetcode Golang题解之第508题出现次数最多的子树元素和

题目&#xff1a; 题解&#xff1a; func findFrequentTreeSum(root *TreeNode) (ans []int) {cnt : map[int]int{}maxCnt : 0var dfs func(*TreeNode) intdfs func(node *TreeNode) int {if node nil {return 0}sum : node.Val dfs(node.Left) dfs(node.Right)cnt[sum]if…

数字后端零基础入门系列 | Innovus零基础LAB学习Day6

今天没有具体的数字IC后端lab实验。今天的重点是熟悉掌握静态时序分析STA中的几类timing path以及setup和hold检查机制&#xff08;包含setup和hold计算公式&#xff09;。 芯片流片失败的那些故事 数字后端零基础入门系列 | Innovus零基础LAB学习Day5 等大家把今天内容学习…

设计模式(二)工厂模式详解

设计模式&#xff08;二&#xff09;工厂模式详解 简单工厂模式指由一个工厂对象来创建实例,适用于工厂类负责创建对象较少的情况。例子&#xff1a;Spring 中的 BeanFactory 使用简单工厂模式&#xff0c;产生 Bean 对象。 工厂模式简介 定义&#xff1a;工厂模式是一种创建…

Spring Boot框架下中小企业设备管理系统开发

1系统概述 1.1 研究背景 随着计算机技术的发展以及计算机网络的逐渐普及&#xff0c;互联网成为人们查找信息的重要场所&#xff0c;二十一世纪是信息的时代&#xff0c;所以信息的管理显得特别重要。因此&#xff0c;使用计算机来管理中小企业设备管理系统的相关信息成为必然。…

Kafka-代码示例

一、构建开发环境 File > New > Project 选择一个最简单的模板 项目和坐标命名 配置maven路径 添加maven依赖 <dependencies><!-- https://mvnrepository.com/artifact/org.apache.kafka/kafka-clients --><dependency><groupId>org.apache.kaf…

最长子序列模型二(二分优化版)

文章目录 提高课题解一、拦截导弹二、导弹防御系统三、最长公共上升子序列四、二分函数速写 基础课题解五、最长上升子序列 II 提高课题解 一、拦截导弹 题目链接 第一问非常简单&#xff0c;直接用之前最长上身子序列模板就行 第二问就有难度了&#xff0c;我们要用最少的递…

基于SSM“毛毛宠物店”宠物信息交流平台的设计与实现

开发说明 开发语言&#xff1a;Java 框架&#xff1a;ssm 技术&#xff1a;JSP JDK版本&#xff1a;JDK1.8 服务器&#xff1a;tomcat7 数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09; 数据库工具&#xff1a;Navicat11 开发软件&#xff1a;eclipse/myec…

DEV-C++如何调试

1、先编译&#xff0c;再点击“调试”按钮 2、使用调试按钮&#xff0c;可以输入输出数据 第21次发博客 以后会慢慢更新