C++复习

1.C++基础

1.1.C/C++的比较

以面向过程和面向对象为切入点

面向过程侧重高效解决问题 (代码越精简越好)面向对象 侧重 复用维护 扩展

  • C:更好更快更直接

  • C++:更利于扩展维护

    • 三大特性:

      • 封装:属性和方法封装到一个类里

      • 继承:父类的属性和方法复制类一份给子类去用。

      • 多态:多态是同一个行为具有多个不同表现形式或形态的能力。

1.2.new和malloc的区别:
  1. malloc-free是c语言中的函数,需要头文件支持,new-delete是C++中的关键字,需要C++编译器的支持。

  2. malloc 传申请空间的大小,需要手动计算,返回的是泛型指针 void*一般强转为所需要的申请类型,new后边接的是申请类型,根据类型自动转换。

  3. new申请空间可以指定初始化的值,malloc不可以指定

  4. 在申请结构体,类空间时,new申请空间会自动调用析构函数,malloc-free则不会。

  5. new申请空间的同时可以手动初始化的值

    image-20240301102854903

    image-20240301103106739

  • 关于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.动态申请和静态申请的区别

image-20240301102225592

1.5.增强的范围for

通常我们遍历数组的时候,常用的写法是 for(;;),但是在C++新标准下允许了线面简化的写法:

type arr[n]
for(type val:arr) //type val = arr[i]

但是new出来的数组,是无法用增强范围for进行遍历的

image-20240302084251137

这是因为增强for循环可以遍历支持迭代器的容器类型数组类型,例如vectorlistarray,new出来的指针数组,既不是容器类型,又不是数组类型,所以无法进行遍历

1.6.函数重载

函数重载是一种特殊情况,C++允许在同一作用域中声明几个类似的同名函数这些同名函数的形参列表(参数个数,类型(值传递,地址传递,引用传递包含在内),顺序)必须不同。常用来处理实现功能类似数据类型不同的问题。

*注意:重载函数的参数个数,参数类型或参数顺序三者中必须有一个不同*

image-20240302091025960

但是仅凭返回值是无法区分,函数之间是否存在重载关系

image-20240302091126648

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)

  • 一般来说,定义了就要初始化引用一块真实存在的空间(不存在空引用)

  • 并不是重新引用其他变量,而是一个单纯的赋值操作,一旦引用某个变量(空间)就不能在去引用其他空间了。

image-20240302093652554

image-20240302094513981

image-20240302094917299

引用和指针的区别:

  • 引用定义了就要初始化,不存在空的引用,指针可以不用初始化(但不推荐),存在空的指针

  • 一旦引用了某个变量(空间)就不能去引用其他的空间了(从一而终),对于一般的指针来说,指向可以修改

  • 指针存储的是地址变量,占用指针大小的空间,引用不占额外的空间。


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 进行对象的初始化,竟然也没有报错:

image-20240302210819468

从这里我们可以看到,第一种有固定的模式,像是真正的初始化,而第二种更像是一个赋值操作,但是针对于C++这种强类型语言我们是知道,整型数字和自定义类型之间是不能做转换的。

所以下一个断点,来观察一下是否与构造函数有关。

image-20240303082929100

通过断点可以看到20直接匹配到了,构造函数中的 m_a,这类函数我们也称之为转换构造函数。

对于转换构造函数他的定义是这样的:可以发生隐式类型转换的构造函数:一个参数| 多个参数,只有第一个没有默认值|多个参数,全都没有默认值的

所以以下几种是不能称之为转换构造函数:

image-20240303083304521

注意:如果是多个参数且无默认值时,则不能自动隐式类型转换,如果想要避免隐式类型转换,在构造函数前要加上关键字 :explicit

image-20240303084025457


*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(堆)这就说明内存释放出现了问题

image-20240303092206019

这是因为默认的拷贝构造函数是一个浅拷贝,当我们将 m_p初始化一个具体空间的时候,拷贝构造只是将成员变量的地址拷贝到,新的对象中去,并不会处理指针指向的空间。

用一个简单的例子就可以证明这件事

image-20240303093309201

这样就导致了多个对象里的指针指向了同一块空间,这是非常危险的,那么也就会导致两个问题:

  1. 当众多对象中的其中一个对象想要通过指针去修改其空间的值,那么其他对象使用的就是他修改之后的值了。

  2. 如果是我们自己申请的空间,那么导致多个对象回收同一块内存空间,引起上边的堆报错

  • 深拷贝

那么解决这种问题的办法就是深拷贝深拷贝并不是一个固定的写法,而是一个解决问题的方法:也就是在拷贝构造是,如果参数对象的指针成员指向了一块内存空间,那么在重构拷贝构造是,需要为当前this对象额外开辟一块新的内存空间并初始化他的值

image-20240303094435145

  1. 为什么必须是当前类的引用呢?

    image-20240303094902568

    如果拷贝构造的参数不是当前类的引用,而是当前类的对象,那么在调用拷贝构造函数的时候,会将另一个对象传递给形参,这本身就是一次拷贝,那么就会再一次调用拷贝构造函数,循环往复

  2. 为什么使用const引用呢?

    拷贝构造函数的作用就是用一个对象来初始化另一个对象,目的是初始化而不是修改它本身的数据,这样写的话,可以让人看的更加明确,

2.3.3.operator=

类中默认存在 operator=,函数名 operator=,参数 当前类对象的引用(同拷贝构造),返回类型:当前类对象的引用。

默认的 operator=函数体代码:形参对象中的成员依次给this指针对象成员进行赋值

与拷贝构造一样,默认也是一个浅拷贝,也会有浅拷贝的问题

image-20240303100836464

由以上四条可以总结出来,在空类中有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作为一个修饰符,一般用于修饰变量,会影响局部变量的生命周期,本质上改变了局部变量的存储位置,生命周期边长,会随着程序的诞生而诞生,随着程序的消亡而消亡。静态局部变量存储于进程中的全局数据区

  1. static修饰局部变量

    image-20240303185450072

  2. static修饰全局变量:全局变量具有外部链接属性,即在跨cpp文件的时候,只要在一个工程目录下,即使不使用头文件包含,在使用 extern关键字也可以使用,但是,再将全局变量设置为静态属性的时候,就会使全局变量的作用范围变小,将不再具有跨文件性

    image-20240303185907874

    image-20240303190350905

  • 静态成员属性:属于类的,编译期存在,在类外以 <font color='red'> = nValue的方式进行初始化

image-20240303182815407

  • 静态成员方法与普通成员方法的区别:

  1. 本质上的区别:静态成员函数没有this指针,但是普通成员函数有隐藏的this指针,这也就意味着无法直接调用类成员属性:

    image-20240303183337190

  2. 静态成员函数只能使用类中的静态成员(静态成员属性和其他的静态成员函数)

    image-20240303183707428

  3. 静态成员函数可以不通过对象去调用,类成员函数必须通过对象去调用

    image-20240303184107070

  4. 静态成员变量既可以通过对象名访问,也可以通过类名访问,但要遵循 private,protected 和 public 关键字的访问权限限制。当通过对象名访问时,对于不同的对象,访问的是同一份内存。

🐴总结:

注!!!:static修饰的变量,只会改变其的存在周期,并不会改变他的作用域

image-20240304201204537

static:

  1. 函数中的局部变量:生命周期延长 编译期至程序结束

    声明周期延长:该变量不会随函数的结束而结束

    初始化:只在第一次调用该函数的时候进行初始化

    记忆性:后续调用的时候,该变量使用前一次函数调用完成之后保存的值

  2. 全局变量:改变变量的链接性,导致被修饰的变量只能在当前文件中使用

  3. 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 {

};

继承的基本性质:

  • 在继承中父类也称为基类,子类也称为派生类

  • 在设定完继承关系之后,子类可以直接使用父类的类成员属性

  • 当子类继承父类的成员属性之后,默认优先使用子类的成员属性

    image-20240303193107595

  • 当我们制定父类函数调用的时候,需要明确父类的作用域

6.21.继承中父类子类的内存空间排布

当子类继承父类的时候,子类会包含父类,所以CSon这个类型所定义的对象所占空间为16,那么他在内存空间中的排布是怎么样的呢?

image-20240303193918512

由此图可见,父类和子类成员属性在内存中的排布大致是,上一半是父亲,下一半是儿子,而二者空间分配顺序,就是各自类成员属性定义的顺序:

6.3.继承方式
继承方式/基类成员public成员protected成员private成员
public继承publicprotected不可访问
protected继承protectedprotected不可访问
private继承privateprivate不可访问

7.多态

  • 多态:相同的行为方式导致了不同的行为结果,即多态性,同一行语句展现了不同的表现形态(多态的本质)

  • C++中的多态:父类的指针可以指向任何继承于该父类的子类对象,父类指针具有子类的形态,多种子类具有多种形态,由父类的指针统一管理,父类指针具有多态性

7.1.静态多态(编译时多态)
  • 静态多态:也称为编译期间的多态,编译器在编译期间完成的,编译器根据函数实参的类型(可能会进行隐式类型转换),可推断出要调用哪个函数,如果有对应的函数就调用该函数,否则就出现编译错误

7.1.1.静态多态的两种实现方式:

函数重载:

函数重载上边说过,不懂去看

函数模版:

使用泛型来定义函数,其中泛型是可用具体的类型(int,double等)替换。通过将类型作为参数,传递给模版,可使编译器生成该类型的函数

image-20240305124450078

7.2.动态多态(虚函数多态:运行时多态)

C++中的多态:父类的指针可以指向任何继承于该父类的子类对象,父类指针具有子类的形态,多种子类具有多种形态,由父类的指针统一管理,父类指针具有多态性

多态形成的条件:

  1. 继承条件:存在子类继承父类的继承关系

  2. 父类指针指向子类对象(或者是引用)

  3. 虚函数关键字(virtual):在父类中存在虚函数,子类重写父类的虚函数

多态的实现原理:

image-20240305125229682

既然研究多态的实现原理,可供研究的就是这虚函数,将普通的类成员函数转变为虚函数,观察他的内存空间大小是否会发生变化:

image-20240305125429296

我们发现,空间大小发生了改变,那么他会不会是和类成员属性一样,是属于对象的呢,类成员属性添加一个,类对象空间大小就会增加相应的大小,所以为了求证,我们添加一个虚函数:

image-20240305125935970

可以看到,内存空间的大小并没有增加,他就不是属于对象的,但是刚开始增加到4个字节大小,这是为什么呢?

我们启用断点调试,就会发现一个叫__vfptr的一个东西:

image-20240305130031310

7.2.1.__vfptr

如果在类中定义了虚函数,在定义对象时,会多分配出指针大小的空间,这块空间描述的是 __vfptr虚函数指针

  • __vfptr:虚函数指针,是一个二级指针,多个对象都会有这个指针大小的空间,多份的 __vfptr指向的是同一个函数指针数组

既然是指针就会有指向的问题:

再定义一个对象,观察其指向的内容:

image-20240305130155175

我们发现两个对象的虚函数指针其指向的是一块内存空间,细致观察可以发现,展开之后,是 void*类型,而且 __vfptr本质上是一个数组

那么他是什么时候指向的呢?

  • 定义对象执行构造函数的初始化参数列表中进行:__vfptr初始化,指向函数指针数组,由编译器自动添加

7.2.2.__vfptr在内存中的排布

我们可以看到,m_a并没有在对象内存的首地址,由此可以解释:

  • 虚函数指针列表,在定义对象是会在对象内存空间前面多分配出指针大小空间,这块空间描述的 __vfptr

7.2.3. __vftable 虚函数列表:

函数指针数组,每个元素存储的是类中虚函数的地址(普通函数的地址不会在这),他是属于类的,一个类只有一份,编译期存在

7.2.4.虚函数的调用流程

image-20240305130551073

很明显,当我们使用CTest类型的指针去分别调用普通成员函数和虚函数的时候,我们发现虚函数是无法调用,当我们定义一个对象的时候,就是可以调用的。

image-20240305130729617

首先找到对象的内存空间前面的 __vfptr,对 __vfptr间接引用找到虚函数列表 __vftable,通过数组下表定位数组中的元素,找到虚函数的地址,通过地址跳转到函数入口执行函数。

虚函数和普通成员函数的区别:

  • 调用流程不同

  • 效率不同,普通函数效率高,执行速度快,虚函数效率低,执行速度慢

  • 使用场景:虚函数主要作用是为了实现多态,普通函数并不是用于实现多态

7.2.5.继承条件下多态的实现:
  • 前提:虚函数列表是属于类的,父类和子类都有各自的虚函数列表, __vfptr属于对象的,每个对象都有各自的 __vfptr

  • 原理:

  1. 由于子类继承父类,不但继承了父类的成员,也会继承父类的虚函数列表

  2. 编译期会检查子类是否重写父类的虚函数,如果有,在子类的虚函数列表中会替换掉父类的虚函数,一般称之为 覆盖,覆盖后便指向了子类的虚函数

  3. 如果子类没有重写的父类虚函数,父类虚函数会保留在子类的虚函数列表中

  4. 如果子类定义了独有的虚函数,按照顺序依次添加到虚函数列表的结尾

  • 流程:父类指针指向子类对象, __vfptr在子类的初始化参数列表中被初始化,指向子类的虚函数列表,申请哪个子类对象 __vfptr就指向了那个子类的虚函数列表,下图便是虚函数的调用流程,则实现了多态

手写实现多态:

待完成

7.2.重载,重定义,重写
  • 函数重载:相同的作用域下,函数名相同,参数列表不同(个数,类型,顺序,个数和类型不相同)这些函数的关系我们都说是重载的,对返回值没有限制。

    image-20240305110024293

  • 函数重定义:函数同名,且不在同一个作用域,参数不同时,无论有没有virtual关键字,基类函数将被隐藏,当参数相同时,如果基类函数没有virtual,此时基类函数将会被隐藏。

  • 重写(覆盖):在继承的条件下,父类和子类之间重名的虚函数(函数名 参数列表( 个数和类型 )返回值 都要相同),父类会在虚函数列表中重写子类的虚函数。

    image-20240305112119109

    可以看到,子类好像没有继承父类中的fun函数,但事实并非如此,而是父类的fun函数由于和子类的fun函数重名,而被隐藏了一样,但是当我们注释掉子类的fun函数。

    image-20240305112236027

    父类的fun函数又可以被子类所使用了,这种情况我们就叫做,重写(覆盖)。

    但是也不是不可以使用同名函数,只需要显式指定一下就可以了:

    image-20240305112520997

  • 24
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值