C / C++ 的区别
1 C语言头文件名字风格为<XXX.h>,而C++中头文件名字中不带.h
2 C++标准库中的函数或者对象都是在命名空间std中定义
3 引入新类型 bool 类型
4 C语言中没有函数重载,C++中引入了函数重载机制
函数重载:是一种多态的表现形式,通常用于面向对象编程,允许在同一个作用域下定义多个同名函数,但这些函数的参数列表必须不同
函数重载作用:增强代码的可读性、灵活性和可维护性,可以让开发者使用相同的函数名执行不同的操作根据传递给函数的类型或数量不同来调用不同的函数实现
为什么C语言中不支持函数重载机制?
因为C语言的设计初衷是为了保持简单性和直接性,在C语言中函数调用是基于函数名匹配的,不会考虑参数的类型或者数量,这说明函数名将作为唯一标识符,同名函数会被视为重定义,编译器无法根据参数列表来区分(好处:减少了潜在的二义性)
面向对象的三大特性
封装
将属性和操作组合成为一个整体
继承
子类继承父类的行为,使子类获得与父类相同的行为
多态
静态多态(编译期间就能确定的多态)
函数重载(运算符重载) 模板等
动态多态(运行期间能确定的多态)
父类通过指针或引用指向子类重写父类的虚函数
启动多态的条件:有继承关系 子类重写父类的虚函数并且父类通过指针或引用调用子类重写父类的虚函数
函数重写
子类重新定义父类中具有相同名称、返回值和参数列表的虚函数
函数隐藏
在父类和子类中,函数名相同,参数不同,无论父类中的同名函数没有 virtual 关键字为隐藏
指针和引用的区别 (引用的特点)
引用使用时必须初始化而且不能初始化为NULL,指针可以初始化为NULL
引用自增仅是单纯+1,而指针自增是指针上的运算
没有多级引用,但是可以多级指针
引用不可改变引用关系,指针可以改变引用关系
引用的大小根据引用的对象类型大小决定,指针的大小在32位内存下是4字节,64 - 8字节
引用实体和引用类型必须为同一类型
引用和引用实体共享同一块内存(取别名)
函数参数默认值
在函数声明或者函数定义的时候直接给形参赋值,这样在函数调用的时候可以不需要再给形参传值,会使用其默认值
注意:参数默认值必须从右向左依次赋值
为什么参数默认值必须从右向左赋值?
因为参数入栈的顺序是从右向左的,这样能保证传进来的值能正确赋给所想赋予的函数,也不会覆盖掉预设好的默认值
声明和定义的区别
声明就是告诉编译器变量或函数的类型和名字,不会为其分配空间
定义就是对这个变量或函数进行内存分配和初始化,需要分配空间
同一个变量可以被声明多次,但只能被定义一次
this指针
用于区分形参和成员变量
本质:指针常量 const Type* const pointer
储存了调用它的对象的地址并且不能被修改这样成员函数才知道自己修改的成员变量是哪个对象的
this指针只能在成员函数中使用,在全局函数、静态成员函数中都不能使用this
(原因:this始终指向当前对象 静态成员函数属于类)
this指针不属于对象而属于类
构造函数
作用:实现了对象的属性(所占内存)的赋值(初始化),对象初始化时会强制执行构造函数
如果没有自己编写构造函数则编译器会提供默认的构造函数
特点:
函数名和类名相同
构造函数可以有参数,可以重载
编译器在创建对象的时候会自动调用构造函数
构造函数不可以是虚函数
为什么构造函数不可以是虚函数?
因为虚函数需要依靠虚表指针vptr进行选择调用,但虚表指针是在类对象创建时才初始化,构造函数如果是虚函数的话执行的时候需要找到虚表指针,此时虚表指针还未初始化,因此构造函数不可以是虚函数
析构函数
用于对象销毁工作,清空对象内部指针指向的堆区内存,会在释放对象的时候自动调用,栈区对象自动释放
特点:
函数名称与类名相同但是前面要有~
析构函数不可以有参数,因此不可以发生重载
编译器在对象销毁前会自动调用析构函数
析构函数可设置为虚函数
为什么析构函数应设置为虚函数?
当析构一个指向子类的父类指针时,编译器可以根据虚函数表找到子类的析构函数进行调用,从而正确释放子类对象的资源,否则可能会造成内存泄漏。
什么是内存泄漏,如何避免?
内存泄漏是指开发者使用 new / malloc 向系统申请分配内存后,系统在堆区为其分配了一块内存,但使用后没有释放掉该块内存,使得这块内存一直被占用不能重新分配的情况
避免内存泄漏:
在使用 new / malloc 申请堆区空间后需要 delete / free 掉申请的堆区空间
减少野指针,悬空指针情况的出现
分配内存后及时为其初始化
智能指针(还没细看)
什么是野指针,悬空指针?
野指针指的是没有初始化过的指针,没有指向任何内存
悬空指针指的是指向的内存已经被释放的指针
new / malloc有什么区别?
1 new / delete 是运算符 而 malloc / free 是函数
2 new 会调用构造函数,malloc 不会调用构造函数
3 delete 会调用析构函数,free 不会调用析构函数
4 new 会返回特定类型的指针自动计算大小,而 malloc 需要自己计算申请空间的大小
5 new 在出现错误时会抛出异常,malloc 出现错误时会返回 NULL
6 new 可以对申请到的内存初始化,malloc 不能对申请到的内存初始化
为什么要将函数设置为虚函数?
父类在定义函数时决定暂缓具体实现步骤,因为子类在设计时会有更具体,精细的想法,作为父类允许子类去拓展通过将自己的函数设置为虚函数,子类才有权限去重新定义
多态的实现 动态绑定技术的核心 -- 虚函数表
每个含有虚函数的类都包含一个虚函数表(编译阶段即被初始化),其中存放虚函数指针的数组,虚函数通过虚表指针指向虚函数表中函数的地址,通过地址指向函数
什么是动态绑定(动态联编)虚函数的作用:动态联编
动态联编是指程序在编译阶段不知道将要调用的函数,只有在程序执行时才能确定将要调用的函数,为此要确切地知道将要调用的函数,要求联编工作在程序运行时进行,这种在程序运行时进行的联编工作称为动态联编
纯虚函数与抽象类
纯虚函数没有函数体 在定义时函数名后需要加上 =0
拥有至少一个纯虚函数的类是抽象类,抽象类不能直接创建对象,只有子类重写了所有纯虚函数后才能创建子类对象
纯虚函数在虚函数表中其函数指针值为0
七大排序
拷贝构造函数
用一个对象初始化一个新对象
使用场景:
使用一个已经存在的对象创建一个新的对象
对象作为函数的返回值以值的方式从函数返回
对象作为函数参数,以值传递的方式传给函数
深拷贝 / 浅拷贝
深拷贝会创造出一个一模一样的对象,新对象和源对象不共享内存,不会指向同一地址
对复制后的文件的修改不会同步在源文件中
浅拷贝只复制指向某个对象的指针,而不复制对象本身,新对象和源对象共享同一块内存
对复制后的文件的修改也会同步在源文件中
如果对象中含有指针变量却使用了浅拷贝,那么会导致两个指针指向同一块内存,释放对象时一块内存会释放两次,报错
内联函数(inline)
如果一些函数体较短,功能简单的函数被频繁调用,不断有函数入栈,会造成栈空间或栈内存大量消耗,为了解决这个问题出现内联函数
内联函数和宏定义的区别?
内联函数在编译时期展开,可以做一些类型检测处理。宏在预编译时展开
内联函数直接嵌入到目标代码中,宏只是做简单的文本替换
C++中引入了类和类的访问控制,在涉及到类的保护成员和私有成员就不能用宏定义操作
内联函数相比于宏定义有什么优点?
内联函数代码是被放到符号表中,使用时像宏一样展开,没有调用的开销的,效率较高
内联函数会对一系列的数据类型进行检查
内联函数作为类的成员函数可以使用类的保护成员及私有成员
内联函数能否声明为虚函数?
不能,因为内联函数不能被取到地址,如果要成为虚函数必须要取到地址
内存区域
低地址 -------> 高地址
代码区:存放程序的代码
常量区
全局(静态)区
堆区(向上增长)
栈区(向下增长)
静态成员变量特点
1 静态成员变量属于整个类所有,所有对象共享类的静态成员变量
2 静态成员变量需要在类外初始化
3 可以通过类名直接访问公有静态成员变量
4 可以通过对象名访问公有静态成员变量
5 静态成员变量位于全局数据区(静态区)
6 静态成员变量的生命周期与程序的生命周期一致
7 静态成员变量不会被继承,父类和子类共享静态成员变量
8 静态成员不会影响类的大小
静态成员函数
静态成员函数属于整个类所有,没有this指针
静态成员函数只能访问静态成员变量和静态成员函数
可以通过类名 / 对象名访问类的公有静态成员函数
友元函数(friend)
类的友元定义在类外部,但有权访问类的所有私有成员
(可以是函数也可以是类 友元函数 / 友元类)
为什么使用友元函数?
使用友元函数可以允许外面的类或函数访问类的私有变量和保护变量,从而使两个类共享同一函数或变量
友元函数有哪些优缺点?
优点:能够提高效率,表达上简单清晰
缺点:破坏了封装机制,不得已情况下才使用
常成员函数
函数头部语句结尾后加const:常成员函数
函数头部之前加const:表示返回值为const,不能修改
常成员函数的特点:
可以读取成员变量的值,但是不能更新成员变量的值
只能调用常成员函数
常函数能修改传入自身的形参以及内部定义的局部变量
常对象只能调用常成员函数,而不能调用其他成员函数
运算符重载(实现String类)
暂未复习
单例模式
说一说你知道的单例模式?
单例模式也叫单件模式,是使用最广泛的设计模式之一,其是保证一个类有且仅有一个实例并提供一个访问它的接口,定义单例类需要私有化其构造函数防止外界创建单例类的对象,需要使用类的私有静态指针变量指向类的唯一实例,需要使用一个公有的静态方法获取实例,其中单例模式分为懒汉模式和饿汉模式,懒汉模式是实例在被使用时才会被初始化,这种情况下线程不安全,另一种是饿汉模式,实例在系统运行时就被初始化,这种情况下线程是安全的
单例模式有哪些特点?
其构造函数和析构函数为私有类型(目的:禁止外部实现构造和析构)
拷贝构造函数和赋值构造函数是私有类型(目的:禁止外部拷贝和赋值,确保实例的唯一性)
类中有一个获取实例的静态方法,可以全局访问
关于懒汉模式和饿汉模式的线程安全?
哪些情况会造成线程不安全?
线程的调度是抢占式执行
修改操作不是原子操作
多个线程同时修改同一个变量
内存可见性
指令重排序
懒汉模式和饿汉模式的线程安全?
饿汉模式:因为饿汉模式下系统运行时实例就被初始化,多线程同时调用getInstance(),由于getInstance只做了一件事就是读取instance实例的地址,也就是多个线程在同时读取一个变量而不是多个线程同时修改同一个变量的情况,所以说饿汉模式是线程安全的
懒汉模式:在调用懒汉模式的getInstance时,其函数做了四件事
1 读取instance内容
2 判断instance是否为NULL
3 如果instance为NULL则new实例(此时会修改instance)
4 返回实例的地址
这样懒汉模式在多个线程调用getInstance时就会造成多个线程修改同一个变量这一情况,所以说懒汉模式是不安全的
那么如何解决懒汉模式的线程不安全问题?
加锁
关于STL容器底层原理
顺序容器
vector :查询时间复杂度 O(1) 尾部插入和删除时间复杂度 O(1) 头部插入和删除代价很高
其底层实现:数组(内存可2倍增长的动态数组)
数据结构:线性连续空间
维护三个迭代器:start finish end_of_storage
双端队列 deque :头部和尾部进行插入和删除操作的时间复杂度是 O(1)
其底层实现:双向链表
deque 和 vector 的区别:
1 允许常数时间内首部进行元素的插入和删除
2 deque 没有容量概念,因为其是分段连续空间组合成随时可以增加一段新的空间连接起来
双向链表 list :对任意位置的插入和删除操作的时间复杂度都为 O(1) 查询元素的代价很高
其底层数据结构:环状双向链表
vector 和 list 的区别:
1 底层结构上:
vector 的底层结构是顺序表,在内存中是一段连续的空间
list 的底层结构是带头结点的双向循环链表,在内存中不是一段连续的空间
2 随机访问
vector 支持随机访问,利用下标定位至元素上,访问元素的时间复杂度是O(1)
list 不支持随机访问,必须从前往后遍历,时间复杂度O(n)
3 插入和删除的区别
vector 在任意位置插入和删除效率低,因为需要搬移数据,时间复杂度为O(N),而且插入可能还要扩容,需要开辟新空间
list 在任意位置插入或删除时只需改变插入或删除前后两个结点的指向即可,时间复杂度为O(1)
4 空间利用率
vector 底层是动态顺序表,在内存中是一段连续的空间,所以不容易造成内存碎片,空间利用率高,缓存利用率高
list 底层节点动态开辟空间,节点容易造成内存碎片,空间利用率低,缓存利用率低
5 使用场景
vector 适合需要高效率存储,需要随机访问而且不管插入和删除效率的场景
list 适合有大量插入和删除操作,并且不关心随机访问的场景
映射map :由{ 键,值 }对组成的集合,具有快速查找的能力
其底层实现:平衡二叉树
map 中 key 的值是唯一的
查找的时间复杂度为log2n
多重映射multimap :一个键可以对应多个值,具有快速查找的能力
其底层实现:二叉树