1.C++基础
1.1.C/C++的比较
以面向过程和面向对象为切入点
面向过程侧重高效解决问题 (代码越精简越好)面向对象 侧重 复用维护 扩展
-
C:更好更快更直接
-
C++:更利于扩展维护
-
三大特性:
-
封装:属性和方法封装到一个类里
-
继承:父类的属性和方法复制类一份给子类去用。
-
多态:多态是同一个行为具有多个不同表现形式或形态的能力。
-
-
1.2.new和malloc的区别:
-
malloc-free是c语言中的函数,需要头文件支持,new-delete是C++中的关键字,需要C++编译器的支持。
-
malloc 传申请空间的大小,需要手动计算,返回的是泛型指针
void*
一般强转为所需要的申请类型,new后边接的是申请类型,根据类型自动转换。 -
new申请空间可以指定初始化的值,malloc不可以指定
-
在申请结构体,类空间时,new申请空间会自动调用析构函数,malloc-free则不会。
-
new申请空间的同时可以手动初始化的值
- 关于malloc new delete free 的一些问题
关于malloc new delete free
大体上的区别在上边都已经概括了,但是仍有许多细节需要去考量,这就需要从他们的原始形态和在两种语言的定义上进行区别,我们都知道:
malloc-free是c语言中的函数,需要头文件支持,new-delete是C++中的关键字,需要C++编译器的支持。既然是函数那么就一定有返回值,void *malloc(unsigned int size)|void free (void* ptr);
到此,就可以解释上边的第二条,为什么malloc在申请空间的时候,需要手动传入空间大小
1.3.指针和引用的区别:
-
引用定义了就要初始化,不存在空的引用,指针可以不用初始化(但不推荐),存在空的指针
-
一旦引用了某个变量(空间)就不能去引用其他的空间了(从一而终),对于一般的指针来说,指向可以修改
-
指针存储的是地址变量,占用指针大小的空间,引用不占额外的空间。
1.4.动态申请和静态申请的区别
1.5.增强的范围for
通常我们遍历数组的时候,常用的写法是 for(;;)
,但是在C++新标准下允许了线面简化的写法:
type arr[n]
for(type val:arr) //type val = arr[i]
但是new出来的数组,是无法用增强范围for进行遍历的
这是因为增强for循环可以遍历支持迭代器的容器类型或数组类型,例如vector
、list
、array,
new出来的指针数组,既不是容器类型,又不是数组类型,所以无法进行遍历
1.6.函数重载
函数重载是一种特殊情况,C++允许在同一作用域中声明几个类似的同名函数,这些同名函数的形参列表(参数个数,类型(值传递,地址传递,引用传递包含在内),顺序)必须不同。常用来处理实现功能类似数据类型不同的问题。
*注意:重载函数的参数个数,参数类型或参数顺序三者中必须有一个不同*
但是仅凭返回值是无法区分,函数之间是否存在重载关系
1.7.nullptr
nullptr
和 NULL的区别:
-
NULL 本质上是宏,替换的是0,nullptr是关键字
-
NULL 含义为整形数字,nullptr含义为空指针
C++为什么要引入nullptr
?
避免了了C语言中,指针和整型0之间的混用问题
1.8.&引用
-
引用:对一块已存在的变量(空间)起的别名
另外在c语言中还有一个关键字 typedef
,但是他是对某个类型起别名
int a = 10;
int& b = a
定义一个引用,引用了变量a,a 和 b 描述的都是同一块空间
地址传递
fun(&a)
值传递:
fun(b)
-
一般来说,定义了就要初始化引用一块真实存在的空间(不存在空引用)
-
并不是重新引用其他变量,而是一个单纯的赋值操作,一旦引用某个变量(空间)就不能在去引用其他空间了。
引用和指针的区别:
-
引用定义了就要初始化,不存在空的引用,指针可以不用初始化(但不推荐),存在空的指针
-
一旦引用了某个变量(空间)就不能去引用其他的空间了(从一而终),对于一般的指针来说,指向可以修改
-
指针存储的是地址变量,占用指针大小的空间,引用不占额外的空间。
2024年3月3日10:13:
类的一些性质:
-
空类的对象的大小为1:占位作用,用来表示这个对象,区别于其他的对象
-
类成员属性:属于对象的,在定义对象时才会存在(为其开辟真实的空间),多个对象便会存在多份成员属性,这里的成员属性,指的是一般的成员属性,各个对象中的属性,彼此独立,互不干扰
-
类成员函数并不是属于对象的,是属于类的,编译期存在,存在与否和是否定义对象无关的,一个类只有一份,被多个对象所共享,但是不能直接调用,必须通过对象(可以是空指针对象,但是不建议这么做),存在与否和是否定定义对象无关的,一个类只有一份,被多个对象所共享。
2.类
2.1.什么是类?
具有相同行为(函数)和属性(成员属性--变量)的个体(对象)的抽象(类)
2.2.类的访问属性---权限:
关键字 | 描述 | 可访问 | 不可访问 |
---|---|---|---|
public | 公有属性 | 类内,子类以外的类外 | |
protected | 保护属性 | 类内,子类 | 子类体外的类外 |
private | 私有属性 | 类内 | 子类,子类以外的类外 |
*2.3.构造函数详解:
首先介绍一下有关于拷贝构造所需要知道的一些知识点:
-
默认构造:当没有写任何构造函数(包含拷贝构造),系统会生成默认的无参构造,并且访问属性是公有的
-
默认的拷贝构造:当没有写任何的拷贝构造,系统会生成默认的拷贝构造(浅拷贝)
-
浅拷贝造成的结果:会指向同一个空间,可能会多次回收,造成崩溃
-
-
深拷贝:指针成员并不是简单的赋值,而是会指向独立的空间,自己用自己的,避免多次回收
2.3.1.转换构造
首先来看一段代码:
class CTest {
public:
int m_a;
CTest(int a):m_a(a){}
};
int main() {
CTest tst(10);
cout << tst.m_a << endl;
CTest tst2 = 30;
cout << tst.m_a << endl;
}
我们在对类内成员属性进行初始化的时候,我们通常会以 Type 对象名(初始化的值)
这种方式去进行初始化,但是当我们以基本数据类型初始化的方式 CTest tst2 = 30
进行对象的初始化,竟然也没有报错:
从这里我们可以看到,第一种有固定的模式,像是真正的初始化,而第二种更像是一个赋值操作,但是针对于C++这种强类型语言我们是知道,整型数字和自定义类型之间是不能做转换的。
所以下一个断点,来观察一下是否与构造函数有关。
通过断点可以看到20直接匹配到了,构造函数中的 m_a
,这类函数我们也称之为转换构造函数。
对于转换构造函数他的定义是这样的:可以发生隐式类型转换的构造函数:一个参数| 多个参数,只有第一个没有默认值|多个参数,全都没有默认值的
所以以下几种是不能称之为转换构造函数:
注意:如果是多个参数且无默认值时,则不能自动隐式类型转换,如果想要避免隐式类型转换,在构造函数前要加上关键字 :explicit
*2.3.2.拷贝构造函数
严格意义上来讲,对象的创建包括两个阶段,分配内存然后初始化:
-
所谓的分配内存就是在,栈区,堆区,全局区给分配足够多的内存,但是这些类内成员所包含的一些数据都是随机且没有意义的
-
初始化就是第一次也就是首次对数据赋值。
拷贝构造函数是编译器默认提供给的一个特殊的构造函数,在空类中它与默认的无参构造并存,拷贝构造函数是众多构造函数中的一种。
但是与默认的无参构造不同的是,默认的拷贝构造函数体代码一般不为空:
操作为:参数中的对象成员依次给this对象成员进行初始化
我们来看一下拷贝构造函数的代码原型:
class CTest{
int m_a;
CTest(const CTest& tst):m_a(tst.m_a){
//this->m_a = tst.m_a;
}
}
拷贝构造函数是编译器默认提供给的一个特殊的构造函数,在空类中它与默认的无参构造并存,拷贝构造函数是众多构造函数中的一种。
所以拷贝构造函数的功能也就显而易见了:
当一个类对象给类的另一个对象进行初始化的时候,会调用拷贝构造函数
class CTest {
public:
int m_a;
int* m_p;
public:
CTest():m_a(10),m_p(new int(20)){}
~CTest(){
if (m_p) {
delete m_p;
m_p = nullptr;
}
}
};
int main() {
CTest tst;
CTest tst2(tst);
return 0;
}
报错的结果显示一个Heap(堆)
这就说明内存释放出现了问题
这是因为默认的拷贝构造函数是一个浅拷贝,当我们将 m_p
初始化一个具体空间的时候,拷贝构造只是将成员变量的地址拷贝到,新的对象中去,并不会处理指针指向的空间。
用一个简单的例子就可以证明这件事
这样就导致了多个对象里的指针指向了同一块空间,这是非常危险的,那么也就会导致两个问题:
-
当众多对象中的其中一个对象想要通过指针去修改其空间的值,那么其他对象使用的就是他修改之后的值了。
-
如果是我们自己申请的空间,那么导致多个对象回收同一块内存空间,引起上边的堆报错
-
深拷贝
那么解决这种问题的办法就是深拷贝,深拷贝并不是一个固定的写法,而是一个解决问题的方法:也就是在拷贝构造是,如果参数对象的指针成员指向了一块内存空间,那么在重构拷贝构造是,需要为当前this对象额外开辟一块新的内存空间并初始化他的值
-
为什么必须是当前类的引用呢?
如果拷贝构造的参数不是当前类的引用,而是当前类的对象,那么在调用拷贝构造函数的时候,会将另一个对象传递给形参,这本身就是一次拷贝,那么就会再一次调用拷贝构造函数,循环往复
-
为什么使用
const
引用呢?拷贝构造函数的作用就是用一个对象来初始化另一个对象,目的是初始化而不是修改它本身的数据,这样写的话,可以让人看的更加明确,
2.3.3.operator=
类中默认存在 operator=
,函数名 operator=
,参数 当前类对象的引用(同拷贝构造),返回类型:当前类对象的引用。
默认的 operator=
函数体代码:形参对象中的成员依次给this指针对象成员进行赋值
与拷贝构造一样,默认也是一个浅拷贝,也会有浅拷贝的问题
由以上四条可以总结出来,在空类中有4个默认的函数:默认的无参构造函数,析构函数,默认的拷贝构造函数,默认的operator=
2024年3月5日:19:30分
2.4.初始化参数列表:
初始化参数列表内容:父类的构造,成员(对象成员,const
成员,成员初值)
初始化顺序:父类的构造 先于成员构造或给初值(成员按声明的先后顺序);
2.5.拷贝构造什么时候调用
-
利用对象来初始化另一个对象
-
一个函数的参数是一个对象,传参的时候
-
一个函数的返回值是一个对象,函数返回的时候
vector在使用的时候,如果类型是一个类,涉及到空间问题,涉及到深拷贝。
2.6.析构
~类名没有参数 没有返回值
虚析构:析构函数 virtual 修饰。
父类是虚析构,就只调用父类析构。
3.关键字
3.1.const关键字
3.2.static关键字
static:静态关键字
既然学到了类相关的知识,我们也经常听说:在类中非静态成员函数,都包含this指针,那么其中这个静态到底是什么,到底有什么作用?
static作为一个修饰符,一般用于修饰变量,会影响局部变量的生命周期,本质上改变了局部变量的存储位置,生命周期边长,会随着程序的诞生而诞生,随着程序的消亡而消亡。静态局部变量存储于进程中的全局数据区
-
static修饰局部变量
-
static修饰全局变量:全局变量具有外部链接属性,即在跨cpp文件的时候,只要在一个工程目录下,即使不使用头文件包含,在使用 extern关键字也可以使用,但是,再将全局变量设置为静态属性的时候,就会使全局变量的作用范围变小,将不再具有跨文件性
-
静态成员属性:属于类的,编译期存在,在类外以
<font color='red'> = nValue
的方式进行初始化
-
静态成员方法与普通成员方法的区别:
-
本质上的区别:静态成员函数没有this指针,但是普通成员函数有隐藏的this指针,这也就意味着无法直接调用类成员属性:
-
静态成员函数只能使用类中的静态成员(静态成员属性和其他的静态成员函数)
-
静态成员函数可以不通过对象去调用,类成员函数必须通过对象去调用
-
静态成员变量既可以通过对象名访问,也可以通过类名访问,但要遵循 private,protected 和 public 关键字的访问权限限制。当通过对象名访问时,对于不同的对象,访问的是同一份内存。
🐴总结:
注!!!:static修饰的变量,只会改变其的存在周期,并不会改变他的作用域
static:
-
函数中的局部变量:生命周期延长 编译期至程序结束
声明周期延长:该变量不会随函数的结束而结束
初始化:只在第一次调用该函数的时候进行初始化
记忆性:后续调用的时候,该变量使用前一次函数调用完成之后保存的值
-
全局变量:改变变量的链接性,导致被修饰的变量只能在当前文件中使用
-
static修饰函数:和全局变量相同,导致被修饰的函数只能在当前文件中使用
3.4.virtual关键字
修饰虚函数,实现动态多态
构造函数,内联函数
3.5.inline 关键字,内联函数(建议性关键字)
适用于代码少,调用频繁的函数,编译时,在执行的位置直接展开函数,是空间来换时间的操作。析构可以是内联的
-
问:virtual inline可以修饰同一个函数吗?
不可以,编译时,inline是在执行的位置上直接展开函数,不能再变了
virtual 用来定义虚函数,是为了实现多态,动态联编,使代码不变,功能可变
inline是一个空间换时间的做法,在程序中多次使用内联函数,代码替换多次,导致内存增加,代码短小精悍是和作为内联函数,代码比较多,逻辑比较复杂(for,while ,switch)不适合作为内联函数(递归函数,虚函数不能为内联函数)
3.6.friend 关键字 友元
给朋友开放权限,友元函数不是类内的函数,指针传参,定义对象
友元类和友元函数,并不是类内的函数和类所以不能直接使用类的成员,可以通过对象来使用成员优缺点:访问使用保护和私有,使用更方便,破坏封装 安全性差平时可以使用 公有的Get方法,来替换友元是不可以被继承的 鉴于 破坏封装 的特点,很多面向对象的语言,已经不再使用友元
4.纯虚函数
4.1.纯虚函数
虚函数 = 0;
4.2.抽象类和接口类
-
含有纯虚函数的类就是抽象类
-
所有函数都是纯虚函数的类就是接口类特性
含有纯虚函数的类,无法实例化对象的,只有子类继承,并完成父类所有纯虚函数的实现之后,才能实例化对象.(子类无法实例化对象,没有意义)--》抽象类,强制子类完成所有抽象类的实现。
5.操作符 重载操作符
6.类之间的关系
6.1.横向
-
组合(不可分割的组成部分)
组合是一个类中包含另一个类的对象:相比于聚合,组合是一种强所属关系,具有相同的生命周期
举例:人与手,人与头之间的关系,人需要包含头和手,头,手也是人的一部分且不能脱离人独立而存在。
class CHand { }; class CPeople { public: CHand m_hand; };
-
依赖(完成功能借助的工具)
它是一种
use a
的关系。一个对象的某种行为依赖于另一个类对象,依赖之间是没有生命周期的约束class CComputer{ }; class CPeople{ void Code(CComputer* pc){} };
-
关联(指针的成员,类似于朋友)
它是一种
has a
的关系。关联不是从属关系,而是平等关系.class CFriend{ } class CPeople{ CFriend* m_pFriend }
-
聚合(班级和班级的学生)
它是一种
owns a
的关系,多个被聚合的对象聚集起来形成了一个巨大的整体,聚合的目的是为了统一进行管理同类型的对象,相当于强版本的关联class CPeople{}; class CFamily{ CPeople* m_pFamily[10]; }
6.2.纵向(继承)
-
继承(父类的成员方法和属性一份给子类用(各用各的))
//父类(派生类)
class CFather {
};
//子类(基类)
class CSon :public CFather {
};
继承的基本性质:
-
在继承中父类也称为基类,子类也称为派生类
-
在设定完继承关系之后,子类可以直接使用父类的类成员属性
-
当子类继承父类的成员属性之后,默认优先使用子类的成员属性
-
当我们制定父类函数调用的时候,需要明确父类的作用域
6.21.继承中父类子类的内存空间排布
当子类继承父类的时候,子类会包含父类,所以CSon
这个类型所定义的对象所占空间为16
,那么他在内存空间中的排布是怎么样的呢?
由此图可见,父类和子类成员属性在内存中的排布大致是,上一半是父亲,下一半是儿子,而二者空间分配顺序,就是各自类成员属性定义的顺序:
6.3.继承方式
继承方式/基类成员 | public成员 | protected成员 | private成员 |
---|---|---|---|
public继承 | public | protected | 不可访问 |
protected继承 | protected | protected | 不可访问 |
private继承 | private | private | 不可访问 |
7.多态
-
多态:相同的行为方式导致了不同的行为结果,即多态性,同一行语句展现了不同的表现形态(多态的本质)
-
C++中的多态:父类的指针可以指向任何继承于该父类的子类对象,父类指针具有子类的形态,多种子类具有多种形态,由父类的指针统一管理,父类指针具有多态性
7.1.静态多态(编译时多态)
-
静态多态:也称为编译期间的多态,编译器在编译期间完成的,编译器根据函数实参的类型(可能会进行隐式类型转换),可推断出要调用哪个函数,如果有对应的函数就调用该函数,否则就出现编译错误
7.1.1.静态多态的两种实现方式:
函数重载:
函数重载上边说过,不懂去看
函数模版:
使用泛型来定义函数,其中泛型是可用具体的类型(int,double等)替换。通过将类型作为参数,传递给模版,可使编译器生成该类型的函数
7.2.动态多态(虚函数多态:运行时多态)
C++中的多态:父类的指针可以指向任何继承于该父类的子类对象,父类指针具有子类的形态,多种子类具有多种形态,由父类的指针统一管理,父类指针具有多态性
多态形成的条件:
-
继承条件:存在子类继承父类的继承关系
-
父类指针指向子类对象(或者是引用)
-
虚函数关键字(virtual):在父类中存在虚函数,子类重写父类的虚函数
多态的实现原理:
既然研究多态的实现原理,可供研究的就是这虚函数,将普通的类成员函数转变为虚函数,观察他的内存空间大小是否会发生变化:
我们发现,空间大小发生了改变,那么他会不会是和类成员属性一样,是属于对象的呢,类成员属性添加一个,类对象空间大小就会增加相应的大小,所以为了求证,我们添加一个虚函数:
可以看到,内存空间的大小并没有增加,他就不是属于对象的,但是刚开始增加到4个字节大小,这是为什么呢?
我们启用断点调试,就会发现一个叫__vfptr
的一个东西:
7.2.1.__vfptr
如果在类中定义了虚函数,在定义对象时,会多分配出指针大小的空间,这块空间描述的是 __vfptr
虚函数指针
-
__vfptr
:虚函数指针,是一个二级指针,多个对象都会有这个指针大小的空间,多份的__vfptr
指向的是同一个函数指针数组
既然是指针就会有指向的问题:
再定义一个对象,观察其指向的内容:
我们发现两个对象的虚函数指针其指向的是一块内存空间,细致观察可以发现,展开之后,是 void*
类型,而且 __vfptr
本质上是一个数组
那么他是什么时候指向的呢?
-
定义对象执行构造函数的初始化参数列表中进行:
__vfptr初始化
,指向函数指针数组,由编译器自动添加
7.2.2.__vfptr
在内存中的排布
我们可以看到,m_a并没有在对象内存的首地址,由此可以解释:
-
虚函数指针列表,在定义对象是会在对象内存空间前面多分配出指针大小空间,这块空间描述的
__vfptr
7.2.3. __vftable
虚函数列表:
函数指针数组,每个元素存储的是类中虚函数的地址(普通函数的地址不会在这),他是属于类的,一个类只有一份,编译期存在
7.2.4.虚函数的调用流程
很明显,当我们使用CTest类型的指针去分别调用普通成员函数和虚函数的时候,我们发现虚函数是无法调用,当我们定义一个对象的时候,就是可以调用的。
首先找到对象的内存空间前面的 __vfptr
,对 __vfptr
间接引用找到虚函数列表 __vftable
,通过数组下表定位数组中的元素,找到虚函数的地址,通过地址跳转到函数入口执行函数。
虚函数和普通成员函数的区别:
-
调用流程不同
-
效率不同,普通函数效率高,执行速度快,虚函数效率低,执行速度慢
-
使用场景:虚函数主要作用是为了实现多态,普通函数并不是用于实现多态
7.2.5.继承条件下多态的实现:
-
前提:虚函数列表是属于类的,父类和子类都有各自的虚函数列表,
__vfptr
属于对象的,每个对象都有各自的__vfptr
-
原理:
-
由于子类继承父类,不但继承了父类的成员,也会继承父类的虚函数列表
-
编译期会检查子类是否重写父类的虚函数,如果有,在子类的虚函数列表中会替换掉父类的虚函数,一般称之为 覆盖,覆盖后便指向了子类的虚函数
-
如果子类没有重写的父类虚函数,父类虚函数会保留在子类的虚函数列表中
-
如果子类定义了独有的虚函数,按照顺序依次添加到虚函数列表的结尾
-
流程:父类指针指向子类对象,
__vfptr
在子类的初始化参数列表中被初始化,指向子类的虚函数列表,申请哪个子类对象__vfptr
就指向了那个子类的虚函数列表,下图便是虚函数的调用流程,则实现了多态
手写实现多态:
待完成
7.2.重载,重定义,重写
-
函数重载:相同的作用域下,函数名相同,参数列表不同(个数,类型,顺序,个数和类型不相同)这些函数的关系我们都说是重载的,对返回值没有限制。
-
函数重定义:函数同名,且不在同一个作用域,参数不同时,无论有没有virtual关键字,基类函数将被隐藏,当参数相同时,如果基类函数没有virtual,此时基类函数将会被隐藏。
-
重写(覆盖):在继承的条件下,父类和子类之间重名的虚函数(函数名 参数列表( 个数和类型 )返回值 都要相同),父类会在虚函数列表中重写子类的虚函数。
可以看到,子类好像没有继承父类中的fun函数,但事实并非如此,而是父类的fun函数由于和子类的fun函数重名,而被隐藏了一样,但是当我们注释掉子类的fun函数。
父类的fun函数又可以被子类所使用了,这种情况我们就叫做,重写(覆盖)。
但是也不是不可以使用同名函数,只需要显式指定一下就可以了: