文章目录
-
-
- 1.封装、继承、多态
- [2. 堆和栈的区别](#2. 堆和栈的区别)
- 3.指针和引用的区别
- [4. 托管与非托管](#4. 托管与非托管)
- [5. c++的垃圾回收机制](#5. c++的垃圾回收机制)
- [6. 实现一个单例模式 注意事项](#6. 实现一个单例模式 注意事项)
-
1.封装、继承、多态
- 封装就是将数据和内部的方法封装到一个类中,对外隐藏内部实现细节,但是留下了公共接口提供给外部使用 。
- 继承:子类继承父类的属性和方法,并对其进行延申,使用代码复用和功能扩展。
- 多态:不同的对象在使用同一个函数或者在同一个环境下有不同的行为。
关于多态
多态就是不同的类对象去调用同一个函数产生了不同的行为 1.必须通过基类的引用或者指针调用虚函数 2.被调用的函数必须是虚函数,且派生类必须对虚函数进行重写。
- 静态多态:体现在函数重载和运算符重载方面,编译阶段就已经确定要调用的函数了,静态多态的执行效率高
- 动态多态:父类声明虚函数,子类重写虚函数,通过父类的指针或者引用去调用虚函数,程序会在运行的时候根据指针或者引用所指向的实际对象的类型来决定调用哪一个版本的虚函数。
但在底层实际上是,创建子类的时候,子类重写的虚函数会覆盖虚函数表中的地址,父类指针指向子类对象的时候,访问虚函数表地址访问的就是子类覆盖后的地址。
2. 堆和栈的区别
栈区作用:
存储函数内部定义的局部变量和函数传递的参数,当函数被调用的时候,这些局部变量都会被分配内存,函数调用结束之后,又会将这些占用的内存自动释放掉。还会保存函数的一些调用信息,例如函数调用的返回指针,栈帧指针等等。这些信息用于函数调用结束后能正确的返回到调用的位置。
堆区作用:
主要用于动态的内存分配,提供给程序员在程序运行的时候根据需要动态的申请和释放内存的区域。C中提供malloc、calloc、realloc、free等进行操作,在C++当中使用new和delete运算符进行操作。
区别:
- 栈区的内存分配和释放是系统自动完成的,而堆区的内存分配和释放操作需要我们手动完成
- 栈区的内存分配效率会相对较高一些,栈区的内存分配和释放操作时通过移动指针完成的,所以会非常快,而堆区的话需要查找合适的空闲内存块、更新管理的内存分配表等等操作,所以销毁会低很多。
3.指针和引用的区别
- 指针可以不初始化(会指向一个随机的地址),引用必须初始化
- 指针需要通过解引用 * 操作符来获取指向的值,引用不需要解引用
- 指针本身占用内存(通常为4字节或8字节,取决于平台),引用本身不占有额外空间,只是一个别名,指向已有的对象
- 指针的const属性:const int* ptr = &a; // 指针所指向的值不能被修改,int* const ptr2 = &a; // 指针本身的地址不能被修改
关于使用场景 :
- 指针就是一个地址,内部存放了区域的起始地址,可以用于接受动态分配的内存,创建数据结构时会用到大量的指针;
- 引用的底层原理也是一个指针,只不过从上层来看是一个变量的别名。 两者在传参和函数返回值的场景下,可以减少数据的拷贝工作
4. 托管与非托管
托管和非托管是计算机编程领域中的两个术语,它们主要用来描述内存管理的方式。
-
托管代码(Managed Code):在托管环境中运行的代码,如.NET框架、Java虚拟机等。在这种环境下,内存管理和资源回收由运行环境(如CLR或JVM)自动处理。程序员不需要手动分配和释放内存,减少了因忘记释放内存而导致的内存泄漏等问题。同时,托管环境还会提供垃圾回收机制,自动回收不再使用的内存空间。
-
在托管环境中运行的代码,如.NET框架、Java虚拟机等。在这种环境下,内存管理和资源回收由运行环境(如CLR或JVM)自动处理。程序员不需要手动分配和释放内存,减少了因忘记释放内存而导致的内存泄漏等问题。同时,托管环境还会提供垃圾回收机制,自动回收不再使用的内存空间。
简而言之,托管代码的优势在于简化了内存管理,降低了开发难度和出错概率;而非托管代码则提供了更底层、更灵活的操作,但同时也要求开发者有更强的对系统资源管理的能力。
5. c++的垃圾回收机制
- c++基于引用计数的智能指针,已经很大程度上解决了内存泄漏的问题。
- 常用的stl容器已经集成了allocator(std::allocator是一个简单的分配器,它基于全局的new和delete操作符来分配和释放内存),在运行时建有默认的内存分配策略
6. 实现一个单例模式 注意事项
什么是单例模式?
-
单例模式是一种设计模式(Design Pattern),设计模式就是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式的目的就是为了可重用代码、让代码更容易被他人理解、保证代码可靠性程序的重用性。
-
单例模式指的就是一个类只能创建一个对象,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享
-
比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象同一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。
单例模式有两种实现方式,分别是饿汉模式和懒汉模式:
饿汉模式 :
单例模式的饿汉实现方式如下:
- 将构造函数设置为私有,并将拷贝构造函数和赋值运算符重载函数设置为私有或删除,防止外部创建或拷贝对象。
- 提供一个指向单例对象的static指针,并在程序入口之前完成单例对象的初始化。
- 提供一个全局访问点获取单例对象。
go
class Singleton
{
public:
//3、提供一个全局访问点获取单例对象
static Singleton* GetInstance()
{
return _inst;
}
private:
//1、将构造函数设置为私有,并防拷贝
Singleton()
{}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
//2、提供一个指向单例对象的static指针
static Singleton* _inst;
};
//在程序入口之前完成单例对象的初始化
Singleton* Singleton::_inst = new Singleton;
线程安全相关问题:
- 饿汉模式在程序运行主函数之前就完成了单例对象的创建,由于main函数之前是不存在多线程的,因此饿汉模式下单例对象的创建过程是线程安全的。
- 后续所有多线程要访问这个单例对象,都需要通过调用GetInstance函数来获取,这个获取过程是不需要加锁的,因为这是一个读操作。
- 当然,如果线程通过GetInstance获取到单例对象后,要用这个单例对象进行一些线程不安全的操作,那么这时就需要加锁了。
懒汉模式
单例模式的懒汉实现方式如下:
- 将构造函数设置为私有,并将拷贝构造函数和赋值运算符重载函数设置为私有或删除,防止外部创建或拷贝对象。
- 提供一个指向单例对象的static指针,并在程序入口之前先将其初始化为空。
- 提供一个全局访问点获取单例对象。
go
class Singleton
{
public:
//3、提供一个全局访问点获取单例对象
static Singleton* GetInstance()
{
//双检查
if (_inst == nullptr)
{
_mtx.lock();
if (_inst == nullptr)
{
_inst = new Singleton;
}
_mtx.unlock();
}
return _inst;
}
private:
//1、将构造函数设置为私有,并防拷贝
Singleton()
{}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
//2、提供一个指向单例对象的static指针
static Singleton* _inst;
static mutex _mtx; //互斥锁
};
//在程序入口之前先将static指针初始化为空
Singleton* Singleton::_inst = nullptr;
mutex Singleton::_mtx; //初始化互斥锁
为什么需要双重检查
- 减少锁的使用:通过第一次检查,如果实例已经存在,直接返回实例,避免加锁,从而减少锁的使用频率,提高性能。
- 线程安全:通过第二次检查,确保在加锁后再次检查实例是否已经创建,避免多个线程同时创建多个实例,确保线程安全。
- 优点 : 减少锁的使用频率,提高性能。 缺点 : 需要两次检查,实现稍显复杂。