参加秋招前,C++的一些学习笔记。
前言
CPP没有系统学过。竞赛中接触到的C++无非就是C with STL,在对数据结构和算法有了一定掌握后,回过头来补补C++相关基础。由于已经对部分知识相对熟悉,笔记中就不再记录了,只记下接触不多的知识点。
面向对象的概念
一般意义的对象:现实世界中的一个客观存在的事物;
面向对象方法中的对象:系统中用来描述客观事物的一个实体;
抽象出同一类对象的共同属性和行为,形成类;
封装:
- 隐藏对象的内部细节;
- 对外形成一个边界;
- 保留有限的对外接口;
- 使用方便、安全。
继承:
- 意义在于软件复用;
- 改造、拓展已有的类,形成新的类。
多态:
- 同样的消息作用在不同对象上,可能引起不同的行为;
面向过程vs面向对象
面向过程
优点:
流程化使得编程任务明确,在开发之前基本考虑了实现方式和最终结果,具体步骤清楚,便于节点分析。
效率高,面向过程强调代码的短小精悍,善于结合数据结构来开发高效率的程序。
缺点:
需要深入的思考,耗费精力,代码重用性低,扩展能力差,后期维护难度比较大。
面向对象
优点:
结构清晰,程序是模块化和结构化,更加符合人类的思维方式;
易扩展,代码重用率高,可继承,可覆盖,可以设计出低耦合的系统;
易维护,系统低耦合的特点有利于减少程序的后期维护工作量。
缺点:
开销大,当要修改对象内部时,对象的属性不允许外部直接存取,所以要增加许多没有其他意义、只负责读或写的行为。这会为编程工作增加负担,增加运行开销,并且使程序显得臃肿。
性能低,由于面向更高的逻辑抽象层,使得面向对象在实现的时候,不得不做出性能上面的牺牲,计算时间和空间存储大小都开销很大。
lnline内联函数
编译过程时在调用处函数体进行调换,节省了参数传递等开销
只是一种建议,具体的还要由编译器的优化来决定
使用inline,既可以在写程序时提高代码复用,又能减少函数调用带来的不必要的开销
构造函数
委托构造函数
1 | struct node{ |
对象生存期
静态生存期
动态生存期
友元函数
在类声明中由关键字friend修饰说明的非成员函数
在函数体内能够通过对象名访问private和protected成员
增加灵活性,使程序员可以在封装和快速性上做合理选择
访问对象成员比如通过对象名
指针
用指针数组存放矩阵
1 | int a[]={1,2,3,4,5}; |
指针类型的函数
函数的返回类型是指针。
定义形式:
1 | 存储类型 数据类型 * 函数名(){} |
- 不要将非静态局部地址作为函数的返回值(该地址生存周期伴随函数体结束而结束,所以是非法地址)
正确示例:
1 | int main(){ |
指向函数的指针
提高封装、复用
1 | int compute(int a,int b,int(*func)(int,int)){ |
动态内存分配与释放
1 | 操作符 new T(初始化参数列表) |
对于数组:
1 | new T[] |
vector
封装任何类型的动态数组,自动创建和删除,数组下标越界检查。
(留坑)
深层复制和浅层复制
如果类的成员中有指针,类的构造时是通过动态分配的方式获得的指针,这时如果需要复制,就不能使用浅层复制。
浅层复制只是把指针里的内容原样的搬到目标位置,这样会导致双方的指针指向的是同一个地址。
如何解决?
1 | ArrayOfPoints(const ArrayOfPoints& v){ |
继承
继承与派生
同一过程,不同角度看:
保持已有类的特性而构造新类称为继承;在已有类的基础上新增自己的特性称为派生;
单继承
语法:
1 | class 派生类名 : 继承方式 基类名{ |
多继承
语法:
1 | class 派生类名 : 继承方式 基类名1,继承方式 基类名2{ |
不同继承方式的差别,主要体现在:
派生类成员对基类成员的访问权限;
通过派生类对象对基类成员的访问权限。
public
继承的访问控制:
基类的public和protected成员:访问属性在派生类中不变;
基类的private成员:不可直接访问。
访问权限:
派生类的成员函数:可直接访问基类的public和protected成员,但不能直接访问基类的private成员;
通过派生类对象,只能访问public成员。
private
继承的访问控制:
基类的public和protected成员:访问属性在派生类中变成private;
基类的private成员:不可直接访问。
访问权限:
派生类的成员函数:可直接访问基类的public和protected成员,但不能直接访问基类的private成员;
通过派生类对象,不能访问从基类继承的任何成员。
protected
继承的访问控制:
基类的public和protected成员:访问属性在派生类中变成protected;
基类的private成员:不可直接访问。
访问权限:
派生类的成员函数:可直接访问基类的public和protected成员,但不能直接访问基类的private成员;
通过派生类对象,不能访问从基类继承的任何成员。
1 | class A{ |
类型转换
共有派生类的对象可以被当作基类的对象使用,反之不能(因为派生类新增了成员);
派生类对象可以隐含转换成基类对象;
派生类对象可以初始化基类的引用;
派生类指针可以隐含转化为基类的指针。
通过基类对象名、指针,就只能访问基类里的成员,而不能访问派生类中新增的成员。
构造函数与复制函数
C++11可以用using语句继承基类构造函数(只能初始化基类成员):
using B::B;
否则,需要对新增成员进行初始化,还要对继承来的成员进行初始化(从构造函数中传递基类参数)
1 | class B{ |
构造函数执行顺序:
调用基类构造函数(根据继承顺序从左至右);
对初始化列表中的成员进行初始化(按照在类中定义的顺序);
执行派生类构造函数体中写的内容。
派生类定义复制构造函数
由于要给基类传参,又要给派生类传参,但是复制构造函数只能传一个参数,这时候只要把这个参数既给派生类初始化,又给基类对象就行了。
1 | C::C(const C& c):B(c){} |
析构函数
整个过程是和构造函数的构造过程恰好相反的,最先执行构造的成员,最后进行析构。
派生类访问基类成员
重名的时候:
1 | class A{ |
虚基类
如果基类A被B和C继承,D由同时继承B和C,那么A是不是会在D中出现多次呢?
如何解决?
使用虚基类声明,用来解决多继承时产生的二义性问题,为最远的派生类提供唯一基类成员,不重复复制;
1 | class B:virtual public A |
那么此时派生类的构造函数需要考虑基类A,且只有最远派生类的构造函数里会对A进行构造,其余的都会被忽略。
1 | class D:public B,public C{ |
运算符重载
双目运算符重载为成员函数
1 | 函数类型 operator 运算符 (形参){ |
前置单目运算符
1 | Int& Int::operator ++(){ |
后置单目运算符++和–重载规则
1 | Int Int::operator ++(int){ |
运算符重载为非成员函数
1 | friend A operator + (const A& a,const A& b){ |
虚函数
- 实现动态绑定
- 不要重定义继承而来的非虚函数
1 | class b1{ |
- 类中会隐含产生一个虚表(存放各个虚函数的入口地址),类的对象有一个虚指针指向虚表头部;
- 虚表是和类对应的,虚指针是和对象对应的;
- 因此在运行时可以根据这个指针,去调用对应的功能函数
抽象类
纯虚函数
在基类中申明的虚函数,在基类中没有定义,要求各派生类根据自己需求来定义自己的版本
virtual 函数类型 函数名(参数表) = 0;
抽象类不能定义实例对象,规范整个类家族的对外接口。
override
C++11引入显式函数覆盖,在编译期而非运行期捕获覆盖错误;
在虚函数的显式重载中运用,编译器会检查基类是否存在一虚函数,与派生类中带有声明override的虚函数有相同签名,若不存在,就报错。
在编译阶段就能找到bug,避免了在运行阶段的不稳定性
final
修饰类:不希望该类被继承;
修饰函数:确保函数不被覆盖;
模板
函数模板
语法
1 | template<模板参数表> |
一个函数模板并非自动可以处理所有类型的数据
只有能够进行运算的类型才能作为类型实参,
自定义的类需要重载模板中的运算符才能作为类型实参。
类模板
使用类模板可以声明一种模式,使得类中某些数据成员、函数参数、返回值等都能取任意类型
(包括基本类型和用户自定义类型)
1 | template<模板参数表> |
1 | template<class T> |
泛型程序设计
术语:概念
用来界定具有一定功能的数据类型,如:
可比较数据类型:Comparable
有复制构造函数并可以用’=’赋值的数据类型:Assignable
可赋值,可比较大小:Sortable
术语:模型
符合某个概念的数据类型称之为该概念的模型,如:
int是Comparable的模型
静态数据类型不是Assignable的模型(因为无法用’=’给整个静态数组赋值)
用概念做模板参数名
1 | template<class Sortable> |
STL
标准模板库
- Standard Template Library定义了一套概念体系,为泛型程序设计提供了逻辑基础
- STL中的模板都是用这个体系中的概念来规定的
- 使用STL时,类型参数可以是C++标准类型,也可以是自定义类型(要求自定义类型是所要求概念的模型)
STL基本组件
- 容器 container
- 迭代器 iterator
- 函数对象 function object
- 算法 algorithms
迭代器是算法和容器的桥梁,将容器的迭代器作为算法的参数,而不是直接将容器作为算法参数
将函数对象作为算法的参数,而不是将函数对象所执行的内容作为算法的一部分
容器
顺序容器,关联容器,无序关联容器
(容器适配器:栈、队列、优先队列)
1 | template<class T,class Sequence = deque<T> >class stack; |
迭代器
- 可以认为是泛型指针,提供了顺序访问容器中每个元素的方法
- “++”运算符获得指向下一个元素的迭代器
- “–”运算符获得指向下一个元素的迭代器
- 可以使用“->”直接访问元素的一个成员
- 指针也拥有同样的特性,指针本身就是一种迭代器
新增了算法,不影响容器的实现,新增了容器,也不用修改算法,算法不直接操作容器中的数据,通过迭代器间接操作
类型
输入迭代器,输出迭代器
前向迭代器
双向迭代器
随机访问迭代器
迭代器的区间
用两个迭代器可以表示一个左闭右开的区间
函数对象
- 一个行为类似于函数的对象,可以像调用函数一样使用
- 函数对象其实是泛化的函数,任何普通的函数和任何重载了()运算符的类的对象,都可以作为函数对象
1 | bool cmp(int a,int b){ |
算法
广泛用于不同对象,内置多种排序算法、交换算法、置换算法、容器管理等
IO流
流是一种抽象,负责在数据的生产者和消费者之间建立联系,并管理数据的流动
程序建立流对象,通过文件系统对连接的文件对象进行作用
读操作并成为从流中提取,写操作被称为向流中插入
预定义输出流对象
cout 标准输出
cerr 标准错误输出,没有缓冲,立即输出
clog 类似cerr,但是有缓冲,等到缓冲区满再输出
1 | ofstream fout("b.out"); |