C++的学习第二部分

一、程序的内存模型

程序的内存模型:一个将程序运行时使用的内存划分为静态存储区、栈区和堆区(以及常量区、代码区)的抽象布局,用于管理不同生命周期(全局/局部/动态)数据的分配、访问和释放。

1、内存四区

内存四区的意义:根据数据不同的生命周期与用途(持久的全局数据、临时的局部数据、动态分配的灵活数据、只读的指令数据),分别用全局/静态区、栈区、堆区、代码区进行管理,从而兼顾访问效率、动态灵活性与运行安全。

①程序运行前

程序编译后,生成的exe可执行程序,在程序运行前分两个区域:代码区、全局区。

(1)代码区

代码区:用于存储程序编译后的机器指令(即可执行二进制代码)的只读内存区域。

(2)全局区(静态区)

全局区:用于存放全局变量、静态变量以及常量字符串字面量,它在程序启动时分配、结束时释放,并通常划分为已初始化和未初始化两个区域(.data段和.bss段)。

②程序运行后

(1)栈区

栈区:由编译器自动管理,用于存储函数参数、局部变量等,分配释放速度极快但空间有限且生命周期与函数绑定;

⚠️注意:不要返回局部变量的地址,栈区分配的资源由编译器自动释放。

(2)堆区

堆区:由程序员手动控制(new/delete),用于动态分配任意大小的内存,灵活但速度较慢且需谨慎避免内存泄漏。

⚠️注意:

2、new运算符

new运算符:用于在堆区动态分配内存并可选地调用构造函数初始化对象,最终返回该对象的指针。

二、C++的引用

引用:一个已存在变量的别名,它在声明时必须初始化且绑定后不可更改,对引用的操作会直接映射到原变量上。

1、引用的基本语法

语法:数据类型 &别名 = 原名;

2、引用的注意事项

⚠️注意:①引用必须初始化 ②引用初始化后不可以改变

3、引用做函数参数

引用做函数参数:可以实现对实参的直接修改并避免拷贝开销,效果类似指针但语法更简洁安全(函数内对形参的修改即影响实参)。

4、引用做函数返回值

引用做函数返回值:可以避免返回值的拷贝并允许将函数调用用作左值(直接赋值给返回值),但必须确保返回的引用所绑定的变量在函数返回后依然有效(如返回静态变量、全局变量或类成员,不能返回局部变量的引用)。

⚠️注意:不要返回局部变量的引用。 用法:函数调用可作为左值。

5、引用的本质

引用的本质:底层通过指针实现的、自带自动解引用且不可重新绑定的指针常量(Type * const),在编译器层面提供"变量的别名"语义。

6、常量引用

常量引用:一个指向只读数据的别名,它既可以绑定到普通变量(禁止通过引用修改原值),也可以绑定到字面量或临时对象(延长其生命周期),常用于避免拷贝且防止函数内意外修改实参。

在引用中,可以使用const修饰形参,防止形参改变实参。(防止误操作)

三、函数高级

1、函数的默认参数

函数的默认参数:在函数声明时为形参指定默认值,调用时若省略该实参则自动使用默认值,但需遵循"从右向左连续提供"的规则。

⚠️注意:

2、函数的占位参数

函数的占位参数:只指定参数类型而不使用其变量名的形参(如 int),用于满足语法要求(如函数重载、运算符重载)或未来扩展,调用时必须传入对应类型的实参但函数体内部无法使用该值。

3、函数重载

函数重载:在同一作用域内定义多个同名函数,但它们的参数列表(参数个数、类型或顺序)必须不同,编译器根据调用时传入的实参自动匹配对应的函数版本。

函数重载的满足条件:

①同一个作用域下 ②函数名称相同 ③函数参数类型不同个数不同顺序不同

⚠️注意:函数的返回值不可以作为函数重载的条件

(1)基本语法

(2)注意事项

①引用作为重载条件

②函数重载碰到函数默认参数

四、类和对象

C++面向对象的三大特性:封装、继承、多态。

C++认为万事万物皆为对象,对象上有其属性和行为。具有相同性质的对象,我们可抽象称为类,比如人属于人类,车属于车类。

1、封装

封装:将数据和操作数据的函数捆绑在一个类中,并通过访问限定符(如private)隐藏内部实现细节,仅暴露必要的公共接口。

(1)属性和行为作为整体

(2)案例:设计学生类

(3)访问权限

①public公共权限 ②protected保护权限 ③private私有权限

(4)C++中class和struct的区别

唯一区别:默认的访问权限不同。

(5)成员属性设置为私有

(6)设计案例1:设计立方体类

(7)设计案例2:点和圆的关系

两点之间的公式:

分模块化的编写

main.cpp:

point.h: point.cpp:

circle.h: circle.cpp:

2、对象特性

(1)构造函数和析构函数

构造函数:在创建类的对象时自动调用,主要用于初始化对象的成员变量或分配资源。

析构函数:在销毁对象时自动调用,主要用于清理对象占用的资源(如释放内存、关闭文件)。

如果不提供构造和析构,编译器会自动提供构造和析构,但是一个空实现。

(2)构造函数的分类及调用

构造函数的分类:C++构造函数主要分为默认构造函数(无需参数)、有参构造函数(可包含转换构造函数)、拷贝构造函数(用同类型对象初始化新对象)和移动构造函数(C++11引入,转移资源所有权)。

构造函数的调用:构造函数在对象创建时自动调用(包括定义变量、new创建对象、传值返回/传参时调用拷贝构造),且调用次数等于对象实例化的总数(静态局部对象除外,仅调用一次)。

移动构造函数:C++11 引入的一种特殊构造函数,它通过转移资源所有权而不是复制资源来初始化新对象,从而避免不必要的深拷贝,提升性能。

核心特点:

  • 参数是右值引用(ClassName&&

  • 通常标记为 noexcept(不抛出异常)

  • 将源对象的资源指针"窃取"过来,并把源对象置于有效但未定义的空状态(如将指针置为 nullptr

(3)拷贝构造函数的调用时机

拷贝构造函数在对象以值传递方式传入函数、以值返回对象、使用已有对象初始化新对象(包括直接初始化和拷贝初始化)以及放入容器时被调用。

(4)构造函数的调用规则

调用规则:

①如果用户定义有参构造函数,C++不再提供默认无参构造,但会提供默认拷贝构造。

②如果用户定义拷贝构造函数,C++不再提供其他构造函数。

无参、有参、拷贝构造都定义:

结果:

无参、有参构造定义:

结果:

有参构造定义:

结果:

拷贝构造定义:

结果:

(5)深拷贝与浅拷贝

浅拷贝:简单的赋值拷贝操作。

深拷贝:在堆区重新申请空间,进行拷贝操作。

浅拷贝带来的问题:堆区内存重复释放。

浅拷贝带来的问题代码:

浅拷贝带来的问题图示:

解决方法:

总结:

(6)初始化列表

初始化列表:C++构造函数中在函数体执行前以冒号起始、用逗号分隔的成员变量初始化语法,用于直接初始化成员(尤其是const成员、引用成员和基类)。

(7)类对象作为类成员

类对象作为类成员:一个类的成员变量是另一个类的对象,此时构造函数的执行顺序是先构造成员对象(按照它们在类中声明的顺序),再构造自身,而析构顺序则完全相反。

构造函数和析构函数的调用顺序:

其他类的构造➔本类的构造➔本类的析构➔其他类的析构(有多个其他类,按照创建的顺序)

(8)静态成员

静态成员:在成员变量和成员函数前加上static关键字,称为静态成员。

静态成员分为静态成员变量和静态成员函数

静态成员变量:

静态成员函数:

(9)C++对象模型 - 成员变量和成员函数分开存储

test1()测试的Person类没有int A的成员变量,test2()测试的Person类有int A的成员变量。

(10)this指针

this指针:每个非静态成员函数都隐含拥有的、指向当前对象自身的指针常量,用于在成员函数内部区分不同对象的成员变量,并支持链式调用。

技巧:谁调用类成员函数,this指针就指向谁。

this指针的用途:

(11)空指针访问成员函数

C++中空指针也可以调用成员函数,但要注意在函数体内部有没有用到this指针,如果用到,要注意一些坑!

(12)const修饰成员函数

常函数:

常对象:

3、友元

友元:C++提供的一种机制,允许一个类将对其私有和保护成员的访问权授予外部函数或其他类,从而突破封装性。

友元的目的:让一个函数或者类访问另一个类中的私有成员。

友元函数的关键字:friend

(1)全局函数作友元

(2)友元类

(3)成员函数作友元

4、C++运算符重载

C++运算符重载:C++中允许为自定义类型(类)重新定义或扩展已有运算符(如+、-、*、/等)行为的特性,使其能够像内置类型一样支持直观的运算表达。

(1)加号运算符重载

加号运算符重载:C++中通过定义operator+函数,让自定义类的对象之间或对象与基本类型之间支持加法运算(如obj1 + obj2)的语法机制。

(2)左移运算符重载

左移运算符重载:(operator<<)在C++中通常用于自定义类型的输出流操作,通过重载该运算符实现将对象内容便捷地输出到cout等输出流(如cout << obj)。

(3)递增运算符重载

递增运算符重载:(operator++)是C++中为自定义类型实现前置(++obj)或后置(obj++)自增逻辑的机制,通过函数形参表中的伪参数int区分后置版本。

(4)赋值运算符重载

赋值运算符重载:(operator=)是C++中用于定义对象之间赋值(如 obj1 = obj2)行为的成员函数,负责处理深拷贝、自我赋值等逻辑以正确复制资源。

(5)关系运算符重载

关系运算符重载:(如operator==operator<等)是C++中为自定义类型定义比较逻辑(如判断两个对象是否相等或大小关系)的机制,使其能像内置类型一样支持if (obj1 == obj2)等表达式。

(6)函数调用运算符重载

函数调用运算符重载:(operator())是C++中允许对象像函数一样被调用(即"仿函数"或函数对象)的机制,通过重载该运算符使类实例支持对象(参数)的语法形式。

小括号的重载:

5、继承

继承:C++中允许一个类(子类)复用另一个类(基类)的成员(数据和函数)的机制,支持代码复用和建立类之间的层次关系。

(1)基本语法

继承的好处:减少重复的代码。

语法:

class 子类:继承方式 父类

子类也叫派生类,父类也叫基类。

(2)继承方式

继承方式:公共继承、保护继承、私有继承

①类的成员访问权限

公有成员:本类的类内、派生类的类内、类外都可以访问。

保护成员:本类的类内、派生类的类内可以访问,但类外不可以访问。

私有成员:只要本类的类内可以访问,派生类的类内、类外都不可以访问。

②类的继承访问权限

基类成员类型 公有继承 保护继承 私有继承
公有成员 在派生类中为公有 在派生类中为保护 在派生类中为私有
保护成员 在派生类中为保护 在派生类中为保护 在派生类中为私有
私有成员 派生类不可访问 派生类不可访问 派生类不可访问

(3)继承中的对象模型

对象模型:派生类对象在内存中会完整包含基类部分的数据成员,并按照继承顺序依次排列,从而通过基类指针或引用可以无缝访问派生类对象中的基类子对象。

①代码查看类的大小

私有成员不可访问,但也被继承下去了。

②命令行查看对象模型

命令:cl /d1 reportSingleClassLayout类名 文件名

(4)构造和析构的顺序

(5)同名成员处理

①访问子类的同名成员,直接访问。

②访问父类的同名成员,添加作用域。

总结:

(6)同名静态成员处理

处理方式与非静态成员同名处理一致。

①访问子类的同名成员,直接访问。

②访问父类的同名成员,添加作用域。

总结:

(7)继承语法

C++允许一个类继承多个类,但实际开发不建议使用多继承。

语法:class 子类:继承方式 父类1, 继承方式 父类2...

总结:

(8)菱形继承问题与解决方法

菱形继承:C++中一个派生类同时从两个或多个间接基类(它们共同继承自同一个顶级基类)继承,导致该顶级基类在派生类中存在多个副本,从而引发数据冗余和二义性问题。

菱形继承图例:

菱形继承的问题:

菱形继承的示例:

示例代码

查看对象模型

cl /d1 reportSingleClassLayoutTuoSheep "main.cpp"

菱形继承的解决 - 虚继承:

虚继承:解决菱形继承问题而设计的机制,通过在继承时使用 virtual 关键字,使得多个派生类共享同一个基类子对象,从而避免数据冗余和二义性。

对象模型的详解:

Sheep类中的成员vbptr指向vbtable,也就是虚基指针指向虚基表,然后虚基表中存的是指针的偏移量,Sheep的虚基表是8,相当于虚基指针0+8=8,最后指向Animal类的成员m_age,Tuo类同理。

总结:

6、多态

多态:通过基类指针或引用调用虚函数时,程序在运行时根据实际绑定的对象类型动态决定调用哪个函数版本(即"一个接口,多种实现")的机制。

多态的分类和区别:

(1)多态的基本语法

静态多态:

动态多态:

总结:

(2)多态的原理剖析

静态多态:

由于是空类(1字节),没有虚函数指针和虚函数表,在编译的时候,调用do_speak()时cat被隐式转换为Animal &引用,编译器看到animal.speak(),发现speak不是虚函数则编译时直接绑定到Animal::speak(),所以输出是:"动物在说话"。

动态多态:

没有重写的对象模型:

f - function,虚函数指针指向虚函数表。

Cat类存储的函数地址是Animal类的speak()。

有重写的对象模型:

f - function,虚函数指针指向虚函数表。

Cat类存储的函数地址是Cat类的speak()。

存在虚函数指针和虚函数表,在运行的时候,调用do_speak()时cat被隐式转换为Animal &引用,编译器看到animal.speak(),发现speak是虚函数,运行时直接绑定到Cat::speak(),所以输出是:"小猫在说话"。

(3)案例一:计算器类

案例描述:

多态的优点:

普通写法:

真实开发中提倡开闭原则,那什么是开闭原则呢,就是对扩展进行开发,对修改进行关闭,换句话说就是需要添加的新功能是原有代码的基础上添加新的代码,而不是去动原来的旧代码。

多态写法:

总结:C++开发提倡利用多态进行程序架构的设计。

(4)纯虚函数和抽象类

纯虚函数:在声明时使用=0语法、无需定义,但可提供实现体,旨在强制派生类必须重写该接口的成员函数。

抽象类:包含至少一个纯虚函数、无法实例化对象,只能作为接口规范供派生类继承使用的类。

(5)案例二:制作饮品

(6)虚析构和纯虚析构

虚析构:为了当用基类指针删除派生类对象时,能按正确顺序(先派生类后基类)调用析构函数,防止资源泄漏。

纯虚析构:为了让抽象类也能拥有虚析构的完整功能,同时强制所有派生类实现自己的析构逻辑(但纯虚析构必须在类外提供函数体,否则链接报错)。

总结:

(7)案例三:电脑组装

思路:

1、创建类

电脑的部件➔CPU抽象类、显卡抽象类、内存条抽象类

电脑类➔组装电脑的部件

厂商➔Intel厂商类(三个部件类)、Lenovo厂商类三个部件类)

2、开始电脑的组装

创建电脑的部件,厂商生产电脑部件(基类的抽象部件指针指向派生类的部件对象),开始制造完整的电脑(电脑类中组装三个部件类),调用电脑类的工作函数让电脑工作起来。

五、文件操作

文件操作:用 <fstream> 中的 ifstream(读)和 ofstream(写)类,像操作 cin/cout 一样,通过 open>><<close 等函数对硬盘文件进行读写。

文件类型分两种:

①文本文件:文件以文本的ASCII码形式存储在计算机中。

②二进制文件:文件以文本的二进制形式存储在计算机中,用户一般不能直接读懂它们。

操作文件的三大类:①ofstream:写操作 ②ifstream:读操作 ③fstream:读写操作

1、文本文件

(1)写文件

写文件的步骤:

文件的打开方式:

注意:文件打开方式可以配合使用,利用 | 操作符。

总结:

(2)读文件

读文件的步骤:

第一种读取方法:使用operator>>的流式逐词读取(格式化提取)

第二种读取方法:使用getline()的逐行读取(行缓冲提取)。

第三种读取方法:使用std::getline()的C++风格逐行读取(动态行提取)。

第四种读取方法:使用get()的逐字符读取(单字节提取)。

总结:

2、二进制文件

问题:使用string类型的成员变量访问出错。

Person类中包含std::string类型的成员m_namestd::string内部维护着指向堆内存的指针,直接二进制写入时保存的是指针地址,而不是字符串内容,读取时读回一个无效的指针地址,访问时触发内存访问冲突。

解决:使用C风格字符数组,char m_name10

(1)写文件

总结:

(2)读文件

总结:

六、职工管理系统

有待解锁。。。