Administrator
发布于 2023-10-31 / 8 阅读 / 0 评论 / 0 点赞

10月31日😍

引用链接

C++设计模式入门_哔哩哔哩_bilibili

《C++ 设计模式》_c++设计模式-CSDN博客

百度网盘 (baidu.com)

C++设计模式课件3_Template Method_模板方法.pdf

c++的堆内存和栈内存

哪一些变量是在堆中,哪一些是在栈中?

在C++中,变量的存储位置可以分为两种主要情况:堆(Heap)和栈(Stack)。

栈(Stack)是一种自动管理的内存区域,用于存储局部变量和函数调用的上下文信息。它的特点是分配和释放速度快,但是空间有限且受到限制。在栈中分配的变量的生命周期与其所在的函数或代码块的执行周期相对应。当函数或代码块执行结束时,栈上的变量会自动被释放。

堆(Heap)是一种动态分配的内存区域,用于存储程序运行时动态分配的数据。它的特点是分配和释放速度相对较慢,但是空间较大且灵活。在堆中分配的变量的生命周期由程序员显式控制,需要手动申请内存(new运算符)和释放内存(delete运算符)。

下面是一些常见的变量类型和它们在堆和栈中的存储情况:

在栈中存储的变量:

1. 基本数据类型(如int、float、char等)和枚举类型;

2. 指针变量,存储指向堆或栈中数据的地址;

3. 结构体和联合体(struct和union)的对象,如果结构体或联合体中的成员也属于栈中存储的类型,则整个结构体或联合体对象也存储在栈中;

4. 函数的参数和局部变量。

在堆中存储的变量:

1. 使用new运算符动态分配的对象,如动态创建的类对象,它们的生命周期由程序员显式管理,需要手动释放内存;

2. 使用malloc()、calloc()等函数动态分配的内存块,需要手动释放内存。

需要注意的是,指针变量本身存储在栈中,但是指针所指向的对象可以存储在栈或堆中,具体取决于对象的创建方式。

总结起来,栈上的变量具有自动分配和释放的特性,适用于存储局部变量和函数调用的上下文信息;而堆上的变量需要手动申请和释放内存,适用于动态分配的对象和较大的数据结构。

为什么要设计堆分配和栈分配

C++之所以设计成在堆上分配和在栈上分配,是为了提供更灵活的内存管理和满足不同的编程需求。这种设计带来了以下几个优势:

1. 灵活性:堆上分配内存可以在运行时动态地分配和释放对象,使得程序能够适应不确定的运行时需求。相比之下,栈上分配的对象的生命周期是由作用域决定的,无法动态改变。

2. 对象生存期控制:栈上分配的对象在离开作用域时会自动释放,这样可以避免资源泄漏。而在堆上分配的对象的生存期由程序员控制,可以在需要时手动释放。这种灵活性对于管理动态数据结构、对象的共享和持久化存储非常有用。

3. 对象大小灵活:栈上分配的对象大小是静态确定的,而堆上分配的对象大小可以是动态的。对于大型对象或者需要在运行时确定对象大小的情况,堆上分配更为适用。

4. 对象共享和传递:堆上分配的对象可以通过指针进行共享和传递,多个指针可以指向同一个堆上的对象。这种共享和传递机制在很多情况下非常有用,如在函数间传递复杂的数据结构或者在多个对象之间共享数据。

在以下情况下可以体现堆和栈的优势:

1. 动态数据结构:当需要创建动态大小的数据结构,如链表、树、图等,堆上分配内存可以提供灵活性和扩展性。

2. 对象生命周期需要跨越多个作用域:如果对象的生命周期超出了创建它的作用域,或者需要在不同的函数之间共享对象,堆上分配内存是必要的。

3. 大型对象:对于大型对象,如大型数组或复杂的数据结构,栈上可能无法提供足够的空间,需要使用堆上分配内存。

需要注意的是,在使用堆上分配内存时,需要手动管理内存的分配和释放,避免内存泄漏和悬垂指针等问题。这是使用堆上内存的代价之一,需要程序员有一定的内存管理能力和责任心。

堆上分配的方式

在C++中,有以下几种方式可以在堆上分配内存:

1. 使用`new`关键字:使用`new`关键字可以在堆上动态地分配单个对象的内存。例如:

````cpp

// 分配一个int对象在堆上

int* ptr = new int;

```

2. 使用`new[]`关键字:使用`new[]`关键字可以在堆上动态地分配数组的内存。例如:

````cpp

// 分配一个包含5个int元素的数组在堆上

int* arr = new int[5];

```

3. 使用`malloc`函数:尽管`malloc`函数是C语言的标准函数,但在C++中也可以使用它来在堆上分配内存。需要注意的是,使用`malloc`函数分配的内存需要通过`free`函数手动释放,而不是使用`delete`。

````cpp

// 分配一个包含10个char元素的数组在堆上

char* str = (char*)malloc(10 * sizeof(char));

```

需要强调的是,在使用堆上分配内存后,应该及时释放这些内存,以避免内存泄漏。对于使用`new`和`new[]`关键字分配的内存,应使用`delete`和`delete[]`操作符进行释放;对于使用`malloc`分配的内存,应使用`free`函数进行释放。

同时,当在堆上分配的对象或数组不再需要时,应记得手动释放内存,以免造成内存泄漏。

设计模式

底层思维和抽象思维

软件设计复杂的根本原因->变化

在建筑行业,很少人会让一个已经设计完毕的100层楼再加一个地下室。但是在软件行业这种情况比比皆是。

人类在解决问题的2种思想

软件设计的目标->复用

面向对象设计原则

复杂的原因是:变化

设计的目的是:复用

面向对象的优势:抵御变化

重新认识面向对象
  • 隔离变化

  • 各司其职

多态调用,各司其职。

大家都是图形(共同的父类),都做画这件事(虚函数),但每个图形画的方式不一样

  • 对象是?

8大设计原则

依赖倒置原则

高层模块(稳定)不应该依赖低层模块(经常变化),他们之间应该加一层抽象(接口,就像网络之间的协议,高层调用协议,底层必须符合协议)

抽象(稳定)不应该依赖实现细节(经常变化),细节应该依赖抽象。

就像网络5层架构,上层不管下一次如何实现,只要底层完成协议的要求,上层根据协议调用即可。

shape这个抽象层,把底层的变化隔离起来,如果MainForm直接调用底层会导致自己也变的不稳定,而现在由shape隔离

开放封闭原则
  • 对扩展开发,对修改关闭

大型项目中一个模块要修改的代价很大,即使是小修改,也要重新各种测试。

因此,如果低层的变化导致高层模块的代码要修改,会带来大量工作量。

我们应该尽量让容易变化的低层模块与高层模块隔离,我们需要中间层(接口,像协议)。高层代码中调用的是提前定义好的协议。而底层产生变化时,新的实现依然要满足协议。

单一职责原则
  • 一个类应该仅有一个引起它变化的原因。

而这个变化的方向隐含着类的责任

里氏替换原则(Liskov替换原则)
  • 子类必须能够替换他们的基类。(白马 一定是 马,他必须能满足 马的共同特征 )IS - A

接口隔离原则

  • 客户端不应该依赖它不需要的接口

  • 一个类对另一个类的依赖应该建立在最小的接口上

问题由来:类A通过接口I依赖类B,类C通过接口I依赖类D,如果接口I对于类A和类B来说不是最小接口,则类B和类D必须去实现他们不需要的方法。

解决方案:将臃肿的接口I拆分为独立的几个接口,类A和类C分别与他们需要的接口建立依赖关系。也就是采用接口隔离原则。

优先组合而不是继承

继承:is a

组合:has a

继承子类和父类耦合性很高。一些新的变动很容易延申到子类或父类

封装变化点

面向接口编程
  • 不声明某个具体类而是声明某个接口(这是声明时,不是使用时)

依赖倒置原则和面向接口原则是相辅相成,经常同时违背

接口标准化是产业强生的标志

紧耦合和松耦合

设计模式的分类

按目的划分
按范围划分

类模式:更倾向于父类和子类的关系

对象模式:更倾向于不同对象之间的关系

按照

如何使用设计模式
  • 好的设计是为了“应对变化,提高复用”

  • 现在软件的特征是 “频繁变化”

  • 解决的关键是找到我们软件要变化的点(变化点)

  • 熟练后我们在写项目是,所有的类不是同等的,是有不同层次的,理解他们的依赖关系

重构的关键技巧

组件协作类 模式

把框架和应用区分

流程区别

把框架把主流程写好,用户只管调用,避免用户瞎搞
早绑定和晚绑定

模式定义

就像有些框架,同一类型的软件都使用一个框架实现。

本质上,这有一类事情是有着相同的步骤,只不过不同事件同一步骤实现有不同。

框架架就是把一类事情相同的步骤(不变的点)封装起来,把具体用户自定义的地方开发给用户重写(变化点)。

该模式做到了把(稳定点)和(变化点)分离。所谓的框架就是把(变化点)尽可能的限制到最小。

Template 模式的使用条件

要有一个稳定的主流程

Template 模式总结
  • 本质继承+虚函数 (多态)

  • 不要程序调用库,而是库写好流程调用我写的新实现。

  • 一般设为protect方法

C++注意点:有虚函数的类,他的析构函数也必须是虚函数

策略模式

当用多个ifelse时,考虑是否支持可拓展

观察者模式

定义对象的一对多的依赖关系时,一个对象的状态改变,所有依赖与他的对象得到通知并自动更新

单一职责类 模式

在软件组件中,如果责任划分不清楚,使用继承得到,往往子类会急剧膨胀,大量重复代码

Decorator装饰模式

动态和静态

  • 组合是动态的,继承是静态的,组合更加灵活

同时继承(完善接口的规范) 同时组合(将来支持多个stream的实现类)!

主要是解决了主题类的多方向延申问题,如果单单使用继承,考虑到有要用有的不要用,很容易出现阶乘。

文件流、网络流、内存流是主体操作,要 继承基类

加密流、缓存流是扩展操作,不应该继承具体类

桥模式

继承的是稳定的,组合的是变化的,从而实现编译时稳定,实现时动态。

例如:

Message类,

要分PC和移动2个实现(2个平台)

又要出个精简版和完全版。(2个功能)

如果单纯用继承实现,需要1+n*m的类,大量重复代码。

应该把平台和功能分离出来。

平台基类,Message功能基类,Message功能基类包含一个平台的指针。

Message根据两个功能继承出2个功能实现类,

平台继承出2个平台实现类

这样仅仅写 1+1+n+m

一般用在程序出现多个非常强的变化维度,并且多个维度很可能会复合,比如 n*m。

此时,应该先把每一个方向抽象成应该基类,多个方向符合时用组合实现m*n的多态。 f(g(x));


评论