CPP学习笔记
第一章 绪论
- C++支持的程序设计方法
- 面向过程的程序设计方法
- 面向对象的程序设计方法
- 范型程序设计方法
- 软件 = 程序 + 文档
- 面向对象的语言
- 问题域:一个软件所要解决的问题,这些问题所涉及的业务范围称为该软件的问题域
- 面向对象的编程语言将客观事物看作具有属性和行为(或服务)的对象,通过抽象找出同一类对象的共同属性(静态特征)和行为(动态特征),形成类。
- 面向对象方法的由来
- 结构化程序设计思想:自顶向下、逐步求精;按功能划分为若干基本模块,这些模块形成一个树状结构;每个模块内部均是由顺序、选择和循环 3 种基本机构组成;其模块化实现的具体方法是子程序。
- 面向对象方法:首先,它将数据及对数据的操作方法放在一起,作为一个相互依存、不可分割的整体——对象。对同类型对象抽象出其共性,形成类。类中的大多数数据,只能用本类的方法进行处理,类通过一个简单的外部接口与外界发生关系,对象与对象之间通过消息进行通信。
- 面向对象的基本概念
- 对象——是系统中用来描述客观事物的一个实体,它是用来构成系统的一个基本单位。对象由一组属性和一组行为构成;
- 类——分类的依据是抽象;它是具有相同属性和服务的一组对象的集合;
- 封装——是面向对象中的一个重要原则,就是把对象的属性和服务结合成一个独立的系统单位,并尽可能隐蔽对象的内部细节;
- 继承——特殊类的对象拥有其一般类的全部属性与服务,称做特殊类对一般类的继承;
- 多态性——是指在一般类中定义的属性或行为,被特殊类继承之后,可以具有不同的数据类型或表现出不同的行为。
- 面向对象开发过程
算法与数据结构设计-源程序编辑-编译-连接-测试-调试
第二章 CPP 简单程序设计
- 什么是常量?什么是变量?
常量是指在程序运行的整个过程中其值始终不可改变的量,除了用文字表示常量外,也可以为常量命名,这就是符号常量;
在程序的执行过程中其值可以变化的量称为变量,变量是需要用名字来标识的。 - 符号常量
符号常量在声明时一定要赋值,而在程序中间不能改变其值。
第三章 函数
- 函数的调用关系
调用其他函数的函数称为主调函数,被其他函数调用的函数称为被调函数。 - 形参的作用
实现主调函数与被调函数之间的联系。函数在没被调用时形参只是一个符号,它标志着形参出现的位置应该有一个怎样的类型。只有在函数被调用时主调函数才将实参赋予形参。 return
的作用- 指定函数返回值
- 结束当前函数的执行
- 函数的参数传递
指的就是形参与实参结合的过程,形实结合的方式有值传递和引用传递。- 值传递(为什么值传递不会造成值得改变?)
是指当发生函数调用时,给形参分配内存空间,并用实参来初始化形参(直接将实参的值传递给形参)。
但这一过程是单向的,一旦形参获得值之后便与实参脱离关系,形参值的改变不会造成实参的改变。 - 引用传递
用引用作为形参,在函数调用时发生的参数传递,称为引用传递。
- 值传递(为什么值传递不会造成值得改变?)
- 函数重载
两个以上的函数,具有相同的函数名,但是形参的个数或者类型不同,编译器根据实参和形参的类型和个数的最佳匹配,自动确定调用哪一个函数,这就是函数的重载。
⚠️ 注意:1⃣️ 重载的形参必须不同(个数或类型);2⃣️ 如果要声明一个参数为空的函数,括号内必须写 void - 函数声明与类型安全
不同类型的数据,在内存中都以二进制序列表示,在运行时并没有保存它的信息,有关类型的特性全部蕴含在了数据所执行的操作之中。正因如此,在使用变量前必须声明。 - 深度探索
3-16
第四章 类与对象
- 面向对象程序设计的主要特点
抽象、封装、继承和多态。 - UML
- UML 规定数据成员表示的语法为:
1 | [访问控制属性] 名称 [重数] [:类型] [=默认值] [{约束特征}] |
- UML 规定函数成员表示的语法为:
1 | [访问控制属性] 名称 [(参数表)] [:返回类型] [{约束特征}] |
抽象
对具体问题(对象)进行概括,抽出一类对象的公共性质并加以描述的过程。对一个问题的抽象分为两个方面:- 数据抽象:描述的是此类对象区别于彼类对象的属性或者状态;
- 行为抽象:描述的是某类对象的共同行为或功能特征。
封装
就是将抽象得到的数据和行为(或功能)相结合,形成一个有机整体,也就是将操作数据和函数代码进行有机的结合,形成“类”。其中,数据和函数都是类的成员。- public:为类提供外部接口;
- protected:与私有类似,差别在于继承和派生时
派生类的成员函数可以访问基类的保护成员
。 - private:隐蔽部分成员,达到对成员访问权限的合理控制,使不同的类之间的相互影响减少到最低限度,进而增强数据的安全性和简化程序编写工作。
继承
允许程序员在原有类特性的基础上,进行更具体、更详细的说明。多态
多态性是指一段程序能够处理多种类型对象的能力。在CPP
中,这种多态性可以通过强制多态、重载多态、类型参数化多态、包含多态 4 种形式来实现。结构体和联合体
控制访问权限 数据成员和函数成员 默认访问控制属性 全部数据成员共享一组内存 继承/多态 类 有 有 私有 否 是 结构体 有 有 公有 否 是 联合体 有 有 公有 是 否 复制构造函数的调用场景
- 当用类的一个对象去初始化一个新的同类的对象;
- 如果函数的形参是类对象,调用函数进行形参和实参结合时;
- 如果函数的返回值是类对象,函数调用完成返回时。
复制构造函数和赋值运算符有何不同?
前置创建了一个新的对象,后者作用于已有对象。
⚠️ 注意:为什么要有类?用基本数据类型不行吗?每一种数据类型都包括了数据本身的属性以及对数据的操作。但基本数据类型的操作都是有限的。拥有内嵌对象的构造函数调用顺序
- 调用内嵌对象的构造函数,调用顺序按照内嵌对象在组合类的定义中出现的次序。
- 执行本类构造函数的函数体。
⚠️ 注意:内嵌对象在构造函数的初始化列表中出现的顺序与内嵌对象构造函数的调用顺序无关。
第五章 数据的共享与保护
- 标识符的作用域和可见性
- 作用域:指的是标识符的有效范围
- 可见性:标识符是否可以被引用
- 静态变量
不会随着每次函数调用产生副本,也不会随着函数返回而失效。也就是说,当一个函数返回时,下一次的调用,该变量还是会保持上一回的值(定义时未赋值会被初始化为0
)。 - 对象生存期
- 静态生存期
如果对象的生存期与程序的运行期相同,则称它具有静态生存期。 - 动态生存期
在局部作用域中声明的具有动态生存期的对象,习惯上也称为局部生存期对象。局部生存期对象诞生于声明点,结束于声明所在的块执行完毕之时。
- 静态生存期
- 静态数据成员
静态数据成员具有静态生存周期。是描述类的所有对象共同特征的一个数据项,对于任何对象实例,它的属性值是相同的。
它是为了解决同一个类的不同对象之间数据和函数共享的问题的。 - 静态函数成员
静态成员函数可以直接访问该类的静态数据和函数成员。而访问非静态成员,必须通过对象名。 - 友元函数
在它的函数体中可以通过对象名访问类的私有和保护成员。
一般在public
中声明。 - 友元类
若A
类是B
类的友元类,则A
类的所有成员函数都是B
类的友元函数,都可以访问B
类的私有和保护成员。
⚠️ 注意:① 友元关系不可传递;② 友元关系是单向的;③ 友元关系是不被继承的。 - 共享数据的保护
对于既要共享又需要防止改变的数据应该声明为常量。对于无需改变对象状态的成员函数,都应当使用const
。- 常对象在整个生存期内不能被改变;
- 常对象必须进行初始化,且不能被更新。
⚠️ 注意:类成员中的静态变量和常量都应当在类定义之外加以定义,但有个例外:类的静态常量如果有整数型或枚举类型可以直接在类定义中为它指定常量值。 - 常成员函数
const
是函数类型的一个组成部分。- 如果将一个对象说明为常对象,则通过该常对象
只能
调用它的常成员函数,而不能调用其他成员函数。这也是常对象唯一的对外接口方式。 - 无论是否通过常对象调用常成员函数,调用期间目的对象都被视为常对象,因此常成员函数不能更新目的对象的数据成员,也不能针对目的对象调用该类中没有
const
修饰的成员函数。 const
关键字可以用于对重载函数的区分。例如,如果在类中这样声明void print(); void print() const;
这是对print
的有效重载。- 如果仅以
const
关键字为区分对成员函数重载,那么通过非const
的对象调用该函数,两个重载的函数都可以与之匹配,这是编译器将选择最近的重载函数——不带const
关键字的函数。
- 常数据成员
- 常数据成员只能通过初始化列表来获得初值。
- 静态常数据成员在类外说明和初始化。
- 类成员中的静态变量和常量都应当在类定义之外加以定义,整型和枚举型可以直接指定常量值。
eg. static const int b = 10;
- 常引用
- 常引用所引用的对象不能被更新。
- 一个常引用,无论绑定的是普通还是常对象,都只能把它当作常对象。
- 编译预处理
#include
指令#define
和#undef
指令- 条件编译指令
#if#elif#else#endif#ifdef#ifndef
defined
操作符
是一个预处理操作符,而不是指令,不用以#
开头。使用方法:defined(操作符)
第六章 数组、指针和字符串
- 指针变量是干什么的?
指针变量是用于存储内存单元地址的。 - 与地址相关的运算
*
和&
*
:指针运算符,标识获取指针所指向变量的值。&
:取缔值运算符,用来得到一个对象的地址。
- 指向常量的指针、指针类型的常量和 void 型指针
- 指向常量的指针
const int * p = &a
:p 本身值可以改变,但不能改变所指的对象。 - 指针类型的常量
int * const p = &a
:p 本身的值不能改变。 viod
型指针可以访问任何类型的数据。
- 指向常量的指针
- 用指针作为函数参数的作用
- 使实参和形参指针指向共同的内存空间,以达到双向传递的作用;
- 减少函数调用时数据传递的开销;
- 通过指针向函数的指针传递函数代码的首地址。
- 指针型函数
- 指向函数的指针
函数指针是专门用来存放函数代码首地址的变量。P212 - 对象指针
- this 指针
它是一个隐含于每一个类的非静态成员函数中的特殊指针(包括构造函数和析构函数),它用于指向正在被成员函数操作的对象。 - 内存泄漏的原因
new
分配空间后,未用delete
回收,导致程序占用内存越来越大。 - 用 vector 创建数组对象
vector<int>name(length, initValue);
- 深复制与浅复制
隐含的复制构造函数只能完成浅复制,因为两个指针指向的是同一内存地址。对于类的浅复制,当程序结束时,原对象和浅复制对象先后会调用两次析构函数,该空间会两次释放,程序出错。对象的深复制可以是循环赋值。P229
第七章 继承和派生
- 类的继承与派生
- 继承:新的类从已有类那里得到已有的特性。
- 派生:从已有类产生新类的过程就是类的派生。
- 多继承、单继承和直接基类、间接基类
- 单继承:一个派生类只有一个基类。
- 多继承:一个派生类有多个基类。
- 直接基类:直接参与派生出某类的基类。
- 间接基类:基类的基类甚至更高层的基类。
- 继承方式
继承方式规定了如何访问从基类继承的成员。继承方式的关键字为:public
,protected
和private
(默认)。- 公有继承(除构造函数和析构函数):基类中的公有和保护成员在派生类中的访问属性不变,基类的私有成员不可直接访问。
- 保护继承:基类的公有成员和保护成员都以保护成员的身份出现在派生类汇总,而基类的私有成员不可直接访问。
- 私有继承:基类中的共有成员和保护成员都以私有成员身份出现在派生类中,而基类的私有成员在派生类中不可直接访问。如果在此被继承的话,基类的全部成员在新的派生类中就无法被直接访问。
- 派生类成员
是指除了从基类继承来的所有成员(除了默认的构造函数和析构函数)之外,新增加的数据和函数成员。 - 派生类生成过程
- 吸收基类成员
- 改造基类成员:着重学习不同继承方式下的基类成员的访问控制问题。① 基类成员的访问控制;② 对基类数据或函数成员的隐藏。
- 添加新的成员:构造与析构函数。
- 类型兼容规则
类型兼容规则是指在需要基类对象的任何地方,都可以使用公有派生类的对象来替代。替代之后,派生类对象就可以作为基类的对象使用,但只能使用从基类继承的成员。 - 派生类的构造函数
派生类构造函数的执行情况:先调用基类的构造函数,然后调用内嵌对象的构造函数。基类构造函数的调用顺序是按照派生类定义时的顺序;内嵌构造函数调用顺序是按照成员在类中声明的顺序。
1 | //派生类构造函数的一般语法形式 |
- 派生类的复制构造函数
如果程序员没有编写复制构造函数,编译系统会在必要时自动生成一个隐含的复制构造函数,这个隐含的复制构造函数会自动调用基类的复制构造函数,然后对派生类新增的成员对象一一执行复制。 - 派生类的析构函数
调用次序与构造函数相反。 - 虚基类
在派生类的对象中,同名的数据成员在内存中会有多个副本,同一函数名会有多个映射。可以使用作用域分辨符来唯一标识分别访问它们,也可以将共同基类设置为虚基类,这时从不同的路径继承过来的同名数据成员在内存中就只有一个副本,同一函数名也只有一个映射。 - 虚基类及其派生类构造函数
如果虚基类声明有非默认形式的(即带参数的)构造函数,并且没有声明
默认形式的构造函数。这时,在整个继承关系中,直接或间接继承虚基类的所有派生类,都必须在构造函数的成员初始化表中
列出对虚基类的初始化。
1 |
|
- 构造一个类的对象的一般顺序
- 如果该类有直接或间接的虚基类,则先执行虚基类的构造函数。
- 如果该类有其他基类,则按照他们在继承声明列表中出现的次序,分别执行他们的构造函数,但在构造过程中,不再执行他们的虚基类的构造函数。
- 按照在类定义中出现的顺序,对派生类中新增的成员对象进行初始化。对于类类型的成员对象,如未出现,则执行默认构造函数;对于基本数据类型的成员对象,如果出现在构造函数的初始化列表中,则使用其中指定的值为其赋值,否则什么也不做。
- 执行构造函数的函数体。
- 小结
派生类及其对象成员标识和访问问题:① 唯一标识问题;② 成员本身属性,即可见性问题。解决唯一标识问题介绍了同名隐藏规则、作用域分辨符和虚基类。
第八章 多态性
- 什么是多态
多态是指同样的消息被不同类型的对象
接收时导致不同的行为。 消息是指对类的成员函数的调用,不同的行为是指不同的实现,调用了不同的函数。 - 多态的类型
- 重载多态*(专用多态):普通函数和类的成员函数重载都叫做重载多态。
- 强制多态(专用多态):是指将一个变元的类型加以变化,以符合一个函数或者操作的要求。加法运算符进行整型和浮点型运算时,首先进行类型强制转换,这就是强制多态的实例。
- 包含多态*(通用多态):是类族中定义于不同类的同名成员函数的多态行为,主要通过虚函数实现。
- 参数多态(通用多态):与类模板相关联,在使用时必须赋予实际的类型才可以实例化。
- 多态的实现
- 编译时多态
- 运行时多态(绑定:计算机程序自身彼此关联)
- 静态绑定:绑定工作在编译链接阶段完成。
- 动态绑定:绑定工作在程序运行阶段完成。
- 运算符重载为成员函数
- 双目运算符的左操作数是对象本身,由
this
指针指出,右操作数则需要通过运算符重载函数的参数表传递。 - 单目运算符操作数由对象的
this
指针给出,就不需要参数了。 - 后置运算符,重载为 A 类的成员函数,这时函数要带有一个整型(int)形参(用于和前置运算符相区别)。
- 前置运算符不用加形参。
- 仅需要访问参数对象的私有成员才将该函数声明为类的友元函数。
- 双目运算符的左操作数是对象本身,由
- 运算符重载为非成员函数
- 运算所需的操作数都要通过参数表传递。
- 参数表左右顺序就是运算符操作数顺序。
- 仅需要访问参数对象的私有成员才将该函数声明为类的友元函数。
- 使用非成员函数重载的原因
- 要重载的操作符的第一个操作数不是可以更改的类型,如
ostream
,是标准库类型,无法添加成员函数。 - 以非成员函数形式重载,支持更灵活的类型转换。
- 要重载的操作符的第一个操作数不是可以更改的类型,如
- 虚函数
虚函数是动态绑定的基础。虚函数必须是非静态的成员函数。虚函数经过派生之后,在类族中就可以实现运行过程中的多态。
问题:如果用基类类型的指针指向派生类的对象,就可以通过这个指针来访问该对象,问题是当访问到的只是从基类继承来的同名成员。
解决办法:如果通过基类的指针指向派生类的对象,并通过这个指针访问某个与基类同名的成员,那么首先在基类
中将该同名函数说明为虚函数。
这样就可以通过基类类型指针,使属于不同派生类的不同对象产生不同的行为,从而实现运行过程中的多态。 - 一般虚函数成员
虚函数声明只能出现在类定义中的函数原型声明中,不能出现在成员函数实现的时候。
运行过程中的多态需要满足 3 个条件:- 类之间满足赋值兼容规则。
- 要声明虚函数。
- 要由成员函数来调用或者是通过指针、引用来访问虚函数。*
⚠️ 注意:如果是使用对象名来访问虚函数,则绑定在编译过程中就可以进行(静态绑定),而无需再运行过程中进行。(只能用引用或指针来通过->访问,不然就是静态绑定输出的都是基类的成员函数。原因:基类的指针可以是派生类对象的别名,但是基类对象却不能表示派生类对象。)
- 虚函数注意事项
- 只有虚函数是动态绑定的。
- 如果派生类需要修改基类的行为(即重写与基类函数同名的函数),就应该在积累中将相应的函数声明为虚函数。而基类中的非虚函数,通常代表那些不希望被派生类改变的功能,也是不能实现多态的。
- 虚析构函数
在CPP
中不能声明虚构造函数,但是可以声明虚析构函数。析构函数无类型和参数,较为简单。虚析构函数设置之后,在使用指针就能够调用适当的析构函数针对不同的对象的清理工作。 - 纯虚函数
对于在基类中无法实现的函数,在基类中只说明函数原型用来规定整个类族的统一接口形式,而在派生类中再给出函数具体的实现。
声明格式:virtual 函数类型 函数名(参数表)=0;
- 抽象类
带有纯虚函数的类是抽象类。抽象类的主要作用是通过它为一个类族建立一个公共的接口,使它们能够更有效发挥出多态特性。抽象类声明了一个类族派生类的公共接口,而接口的完整实现,要有派生类自己定义。
抽象类不能实例化。但是可以定义一个抽象类的指针和引用。通过指针或引用就可以指向并访问派生类的对象,进而访问派生类的成员,这种访问是具有多态特征的。