既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
- **申请内存所在位置:**new/delete是操作符,malloc/free是函数;new操作符从自由存储区(free store)上位对象动态分配内存空间,而malloc函数从从堆上动态分配内存。而且new在申请对象时会调用对象的构造函数和析构函数
**自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请该内存 - **返回类型安全性:*new操作符内存分配成功时,返回的是对象类型的指针。类型严格与对象匹配,无须进行类型转换,而malloc内存分配成功则是返回void,需要通过强制类型转换转换成我们需要的类型。
- **内存分配失败时的返回值:**new内存分配失败时,会抛出bad_alloc异常,不会返回NULL;malloc内存分配失败时返回NULL。
- 使用new操作符申请内存时无需指定内存大小,编译器会根据类型信息自动进行计算;而malloc需要显式的指定所需内存的大小
C++中有了maoolc/free, 为什么还需要new/delete?
malloc与free是c++/C语言的标准库函数,new/delete是C++的运算符,他们都可以用于申请动态内存和释放内存
对于非内部数据类型的对象而言,只用malloc/delete无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限范围内,不能够把执行构造函数和析构函数的任务强加于malloc/delete。
因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理与释放内存工作的运算符delete。
malloc内存分配机制是,在哪里分配内存,最大可以申请多大的内存
- 它是将可用的内存块连接为一个长长的列表,即所谓的空闲链表,调用malloc函数时,它沿连接表寻找一个大到足以满足用户请求所需要的内存块,将该内存块一分为二(一块大小与用户请求的大小相等,另一块大小就是剩下的字节)
调用free函数时,它将用户释放的内存块连接到空闲链上,到最后,空闲链会被切成很多的小内存片段,如果这时用户申请一个大的内存片段,那么空闲链上可能没有可以满足用户要求的片段了,于是,malloc函数请求延时,并开始在空闲链上检查各内存片段,并进行整理,将相邻的小空闲块合并成较大的内存块。 - malloc函数是在堆上面分配内存
- Linux下虚拟地址空间分配给进程的是3GB,Windows下默认的是2GB,真正能够使用多少内存跟你的机器和编译器决定了,
在Linux系统能申请到的内存地址空间在2.8GB左右,而在windows下面(32位)得到的地址空间在1.2GB左右
malloc(0)的返回值问题
- malloc(0)在我的系统里是可以正常返回一个非NULL值的。返回了一个正常的地址。
- malloc(0)申请的空间到底有多大不是用strlen或者sizeof来看的,而是通过malloc_usable_size这个函数来看的。—当然这个函数并不能完全正确的反映出申请内存的范围。strlen和sizeof都是计算为0
- malloc(0)申请的空间长度不是0,在我的系统里它是12,也就是你使用malloc申请内存空间的话,正常情况下系统会返回给你一个至少12B的空间。这个可以从malloc(0)和malloc(5)的返回值都是12,而malloc(20)的返回值是20得到。—其实,如果你真的调用了这个程序的话,会发现,这个12确实是”至少12“的。
- malloc(0)申请的空间是可以被使用的。
C语言程序中为什么要从main函数开始
main只是开发工具所规定的一个特殊函数名称而已,它既不是程序的入口,也不是必须要有的函数
如:
- 一.在Linux C中,使用attribute关键字,声明constructor和destructor,可以自定义程序入口点,不一定是在main函数开始执行
- 二.在标准C中,编译器在编译的时候把你的程序开始执行的地址设为main函数的地址,汇编中可以自由通过end伪指令制定
在VS中给你可以通过这么设置:
项目->属性->配置属性->链接器->高级->入口点,改为你想入口点的函数名 - 三.在单片机C中,在C语言运行前都有汇编程序编写的启动程序,里面对单片机进行的初始化,同时设置了C程序的入口main函数
new运算符的原理(底层使用了operator new(),最终调用了malloc),new运算符重载,怎么写重载函数,重载的定义
//++++++++++++++++++++++++++++++++++++++++++++//
首先区分new operator和operator new这两个概念,前者new operator就是new操作符,一般,在new一个对象的时候,底层做了如下两步:
- 为对象分配内存
- 调用构造函数初始化这块内存
在第一步中分配内存时就使用了operator new,其实现机制
void *operator new(std::size_t size) throw(std::bac_alloc)
{
if(size==0)
size=1;
void *p;
while((p=::malloc(size))==0)
{
std::new_handler nh=std::get_new_handler();
if(nh)
nh();
else
throw std::bad_alloc();
}
return p;
}
void operator delete(void *ptr)
{
if(ptr)
::free(ptr);
}
在第二步中,调用构造函数初始化内存,如果不涉及继承,这一步非常简单,就是给内存赋值,重点是operator new分配内存上,其处理流程如下:
3. 分配内存如果成功则直接返回,否则;
4. 查看new_handler是否可用,new handler的作用用于再释放一部分内存
5. 如果可用则调用之并跳转到1,否则;
6. 抛出bad_alloc异常
//++++++++++++++++++++++++++++++++++++++//
new,delete在C++中被归为运算符,所以可以重载他们
new的行为:
- 先开辟内存空间
- 再调用类的构造函数
在开辟内存空间的部分,可以被重载
delete的行为: - 先调用类的析构函数
- 再释放内存空间
释放内存空间的部分,可以被重载
为什么要重载?
有时候需要实现内存池的时候需要重载它们,频繁的new和delete对象,会造成内存碎片,内存不足等问题,调用构造函数placement new。
memset函数的作用,有哪些参数
函数原型:
extern void *memset(void *buffer, int c, int count)
说明:把buffer所指内存区域的前count个字节设置成字符c,返回指向buffer的指针
void *memset(void *dest, int val, size_t count)
{
if(dest == NULL || count < 0)
return NULL;
char *pdest = (char *)dest;
while(count-- > 0)
{
*pdest++ = val;
}
return dest;
}
linux系统应用程序的内存空间分配(内核空间和进程空间),一般进程空间多大,内核空间多大,进程空间分布是什么样的,堆区最大空间是多少
- 在Linux系统中内核空间为1GB(0xC0000000到0xFFFFFFFF),用户进程空间为3GB(0x00000000到0xBFFFFFFF)
- 在Windows系统中默认的内存分配是内核空间2GB,用户进程空间为2GB
进程空间分布: - stack(栈):存储局部,临时变量,在函数调用时,存储函数的返回指针,用于控制函数的调用和返回,在程序开始时自动分配内存,结束时自动释放内存,(Linux系统通常在8MB左右)
- Heap(堆):存储动态内存分配,需要程序员手工分配,手工释放
- uninitialized data(BSS):在程序运行初未对变量进行初始化的数据
- initialized data(Data):在程序运行初已经对变量进行初始化的数据
- Text(程序段):程序代码在内存中的映射,存放函数体的二进制代码
堆区的大小受限于计算机系统中有效的虚拟内存
C++中的模板机制了,原理,类模板和函数模板在定义时的区别
函数模板机制结论:
- 编译器并不是把函数模板处理成能够处理任意类的函数
- 编译器从函数模板通过具体类型产生不同的函数
- 编译器会对函数模板进行两次编译
- 在声明的地方对模板代码本身进行编译;在调用的地方的地方对参数替换后的代码进行编译
模板就是泛型编程的基础,泛型编程即以一种独立于任何特定类型的方式编写代码,可以实现算法与数据结构的分离。
函数重载不是一种面向对象的编程,而只是一种语法规则,重载跟多态没有什么直接关系
二叉树的原理,平衡二叉树的原理,二叉树的遍历方式,二叉树的最大节点数,二叉树插入删除的时间复杂度,二叉树插入的时间复杂度与树的节点数和树深度的关系,二叉树的优点
数组和链表的区别,它们的应用场景
- 链表是链式的存储结构,数组是顺序的存储结构,在内存中,数组是一块连续的区域
- 链表的插入与删除元素相对数组较为简单,不需要移动元素,但查找某个元素较为复杂
- 链表查找某个元素较为简单,但插入与删除元素较为复杂,且最大长度在编程一开始时就指定,扩充长度不如链表方便
- 数组在内存中连续,在栈区,链表在内存中不连续,一般在堆区
**应用场景:**需要快速访问数据,很少或不插入或者删除元素,用数组
需要经常插入或者删除元素用链表数组结构
C++继承机制,虚继承原理
继承是使代码可以复用的重要手段,也是面向对象程序设计的核心思想之一。简单地说,继承是指一个对象直接使用另一个对象的属性和方法。继承呈现了面向对象程序设计的层次结构,原始类称为基类,继承类称为派生类,也分别叫做父类和子类,子类也可以为父类,被另外的类继承。继承的方式有三种分别为公有继承(public)、保护继承(protect)、私有继承(private)
- **公有继承:**基类的公有成员和保护成员作为派生类的成员时,它们都保持原有的状态,而基类的私有成员对派生类是不可见的,也不能被这个派生类的子类所访问
- **私有继承:**基类的公有成员和保护成员作为派生类的私有成员,并且不能被这个派生类的子类所访问
- **保护继承:**基类的所有公有成员和保护成员都成为派生类的保护成员,并且只能被它的派生类成员函数或友元访问,基类的私有成员仍然私有,且对派生类不可见
public继承是一个接口继承,保持is-a原则,每个父类可用的成员对子类也可用,因为每个子类对象也都是一个父类对象
private/protected继承是一个实现继承,基类的部分成员并非完全成为子类接口的一部分,是has-a的关系原则
一个类有多个基类,这样的继承关系称为多继承
=====class 派生名:访问控制符 基类名1,访问控制符 基类名2
虚继承的作用是减少了对基类的重复,代价是增加了虚表指针的负担(更多的虚表指针)
虚继承:
虚拟继承是多重继承中特有的概念。虚拟基类是为解决多重继承(菱形继承)而出现的。如:类D继承自类B1、B2,而类B1、B2都继承自类A,因此在类D中两次出现类A中的变量和函数。为了节省内存空间,可以将B1、B2对A的继承定义为虚拟继承,而A就成了虚拟基类。实现的代码如下:
class A
class B1:public virtual A;
class B2:public virtual A;
class D:public B1,public B2;
虚拟继承在一般的应用中很少用到,所以也往往被忽视,这也主要是因为在C++中,多重继承是不推荐的,也并不常用,而一旦离开了多重继承,虚拟继承就完全失去了存在的必要因为这样只会降低效率和占用更多的空间。
虚函数机制,一个类有虚函数,有成员变量,求所占的内存大小
**虚函数:**虚函数的调用是通过虚函数表指针和虚函数表来实现的
- 虚函数表和类绑定,只此一份。虚函数表指针每个类对象都有,指针值都是同一个虚函数表的地址
- 虚函数指针vfptr不属于数据成员,vfptr的值是在构造函数内容执行之前进行初始化的,构造函数是根据本类的类型进行初始化的。所以拷贝构造函数,或者赋值函数都不会影响到vfptr的值
- 对虚函数的调用,都是通过实体的vfptr所指的虚函数表来进行调用的
内存排布:子类继承父类的成员变量,内存上会先排布父类的成员变量,接着排布子类的成员变量,其中成员函数不占字节 - 每个类都有虚指针和虚表
- 如果不是虚继承,那么子类将父类的虚指针继承下来,并指向自身的虚表(发生在对象构造时),有多少个函数,虚表里面的项就会有多少,多重继承时,可能存在多个的基类虚表和虚指针
- 如果是虚继承,那么子类会有两份虚指针,一份指向自己的虚表,另一份指向虚基表,多重继承时虚基表与虚基表指针有且只有一份
总结如下:
- **无虚函数覆盖的继承:**虚函数按照其声明顺序放在虚函数表中,父类的虚函数在其子类的虚函数的前面
- **有虚函数覆盖的继承:**派生类中起覆盖作用的虚函数放在原基类虚函数的位置,没有被覆盖的虚函数依旧
- **无虚函数覆盖的多重继承:**每个父类都有自己的虚函数表,派生类的虚函数被放在了第一个父类的虚函数表中(按照声明顺序排序)
- **有虚函数覆盖的多重继承:**派生类中起覆盖作用的虚函数放在原基类虚函数的位置,没有被覆盖的虚函数依旧
- **无虚函数覆盖的虚继承:**派生类有单独的虚函数表,另外也单独保存一份父类的虚函数表
- **有虚函数覆盖的虚继承:**派生类单独的虚函数表,另外也单独保存一份父类的虚函数表,派生类中起覆盖作用的虚函数放在原基类虚函数的位置,没有被覆盖的虚函数依旧
C++中的多态
多态主要是有静态多态和动态多态
- **静态多态:**主要是函数重载和泛型编程
- **动态多态:**虚函数
静态多态:也称为静态绑定或早绑,编译器在编译期间完成的,编译器根据函数实参的类型(可能会进行隐式类型转换),可推断出要调用的那个函数,如果有对应的函数就调用该函数,否则出现编译错误。
动态多态:在程序执行期间判断所引用对象的实际类型,根据实际类型调用相应的方法,使用virtual关键字修饰类的成员函数时,指明该函数为虚函数,派生类需要重新实现,编译器实现动态绑定
总结:
- 1.在编译过程中的联编的被称为静态联编(static binding)
- 2.在运行时选择正确的虚方法,被称为动态联编
- 3.编译器对非虚方法使用静态联编,编译器对虚方法使用动态联编
说说引用,什么时候用引用好,什么时候用指针好?
指针和引用的区别:
- 非空区别:指针可以为空,而引用不能为空
- 可修改区别:如果指针不是常指针,那么就可以修改指向,而引用不能
- 初始化区别:指针在定义时可以不用初始化,而引用在定义的同时必须初始化
- 从内存上来讲,系统为指针分配内存空间,而引用与绑定的对象共享内存空间,系统不为引用变量分配内存空间
使用引用参数的主要原因有两个:
•程序员能修改调用函数中的数据对象
•通过传递引用而不是整个数据–对象,可以提高程序的运行速度
一般的原则:
对于使用引用的值而不做修改的函数:
•如果数据对象很小,如内置数据类型或者小型结构,则按照值传递
•如果数据对象是数组,则使用指针(唯一的选择),并且指针声明为指向const的指针
•如果数据对象是较大的结构,则使用const指针或者引用,已提高程序的效率。这样可以节省结构所需的时间和空间
•如果数据对象是类对象,则使用const引用(传递类对象参数的标准方式是按照引用传递)
对于修改函数中数据的函数:
•如果数据是内置数据类型,则使用指针
•如果数据对象是数组,则只能使用指针
•如果数据对象是结构,则使用引用或者指针
•如果数据是类对象,则使用引用
关键字const是什么含义
const是C++中常用的类型修饰符,常类型使指使用类型修饰符const说明的类型,意味着“只读”
const的作用:
- **可以定义const常量:**如const int max = 100;
- **便于进行类型检查:**const常量有数据类型,而#define宏常量没有数据类型,编译器可以对前者进行类型安全检查,而对后者只进行字符替换,没有类型安全检查,并且在字符替换时可能产生意想不到的错误
- **可以保护被修饰的东西:**防止意外的修改,增强程序的健壮性
- 为函数重载提供了一个参考:
- **可以节省空间,避免不必要的内存分配:**const定义常量从汇编的角度来看,只是给出了对应的内存地址,而不是像#define一样给出的是立即数,所以,const定义的常量在程序运行过程中只有一份拷贝,而#define定义的常量在内存中有若干个拷贝
#define PI 3.14159 //常量宏
const double Pi = 3.14159 //此时并未将Pi放入ROM中
double i=Pi; //此时为Pi分配内存,以后不在分配
double I=PI; //编译期间进行宏替换,分配内存
double j=Pi; //没有内存分配
double J=PI; //再进行宏替换,有一次分配内存
**提高了效率:**编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率很高
const的使用
const int a;
int const a; //前两个作用一样,a是一个常整型数
const int *a; //a是一个指向常整型数的指针(整型数是不可修改的,指针是可以修改的)---底层const
int *const a; //a是一个指向整型数的常指针(指针指向的整型数是可以修改的,但指针是不可修改的)---顶层const
int const *a const; //a是一个指向常整型的常指针(指针指向的整型数是不可以修改的,同时指针也是不可以修改的)
即指针使用const有:
- 指针本身是常量不可变: char * const pContent;
- **指针所指向的内容是常量不可变:**const char *pContent;
- 两者都不可变: const char * const pContent;
**区别方法:**沿着*号划一条线:
- **底层const:**如果const位于*的左侧,则const就是用来修饰指针所指向的变量,即指针指向为常量;常量指针:指向的对象是个常量,不能修改其内容,只能更改指针指向的地址
- **顶层const:**如果const位于*的右侧,const就是修饰指针本身,即指针本身是常量,指针常量,不能修改指针指向的地址,只能修改其内容
函数中使用const
(1) const修饰函数参数
a.传递过来的参数在函数内不可以改变(无意义,因为Var本身就是形参)
void function(const int Var);
b.参数指针所指向内容为常量不可变
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
不可以改变(无意义,因为Var本身就是形参)
void function(const int Var);
b.参数指针所指向内容为常量不可变
[外链图片转存中…(img-MqqF1MkJ-1715828056169)]
[外链图片转存中…(img-t1dZYWpK-1715828056169)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新