C++ 是在C语言基础上开发的,一种面向对象编程的语言。C++在C语言基础上封装了类实现了面向对象的功能,封装了函数实现了泛型编程的功能。与C相比具有安全性更高、功能更强的特点。本文以wiki知识库的形式,介绍C++的基本概念和语法规则,并在每个术语旁边配有对应的英文,为初学C++的人提供一份快速查阅的手册。旨在用最简短的文字,介绍清概念以及用法。
基本概念
面向对象编程 Object Oriented Programming(OOP)
编译过程:编译+连接
计算机语言的分类
- 机器语言:二进制代码
- 汇编语言:机器语言的简化,直接对地址、内存、寄存器进行操作,汇编代码与机器语言是一一对应的
- 高级语言:抽象程度更高,需要编译成机器语言
- 脚本语言:不需要编译
分类
- 关键字
- 标识符
基本数据类型与运算
数据类型
- 基本数据类型:整型int,浮点型float,双精度浮点double,字符char,布尔bool
- 自定义数据类型:枚举类型,结构体类型,联合体类型,类类型
流程控制
- 条件判断:if,while,switch
C语言和C++中,所有非零的值都被判定为逻辑真,为零的值才会被判定为逻辑假。
if、while语句中的逻辑判断是分步进行的,比如与运算中,只要前一项为非,不会对后一项进行判断,而是直接跳出判断。
数据的存储与操作
每个数据都会单独分配内存空间来处理
CPU只是对内存数据进行操作,而不会区分不同的数据类型。区分数据类型并选择不同的操作,是编译器需要完成的工作。
函数 Function
函数由参数表和函数体组成,函数在使用前必须先声明,可以之后再定义
参数传递
函数在调用时才分配存储单元
值传递
语法规定:传递给函数的实参,与函数形参表中的数据类型一致
1 | //函数声明,此处省略函数定义 |
引用传递
语法规定:使用“&”符号定义一个引用,引用必须指向已经存在的对象。函数的形参是一个引用,传递给函数的实参是被引用对象
1 | int i; |
函数实例:
1 | //函数声明,此处省略函数定义 |
指针传递
语法定义:传递实参的地址给子函数,子函数的形参表中定义形参为指针类型
1 | //函数声明,此处省略函数定义 |
函数的调用
运行栈
最后调用的函数总是最先返回,因此可以用栈这种数据结构来保存。运行栈就是专为函数调用设计的数据结构,
指针
寄存器中通常会保存两个指针,分别指向栈顶和函数调用发生的位置,来进行函数的调用和返回。
- 栈顶指针:指向栈顶的地址
- 帧指针:指向函数调用时的地址
函数的声明与安全性
C++ 要求函数在使用时先声明返回值类型和参数表,这样可以在编译过程中发现错误,是设计更为合理的编程语言。如果不做声明,在参数传递和参数返回两个过程中,都可能发生错误。
- 参数传递中,有可能把参数类型传错,将整型变量传递给浮点变量,而不做类型转换。
- 参数返回中,不做声明,有可能用整型方法去获取void类型的函数值,就会读取垃圾数据。
类与对象 Class & Object
构造函数与析构函数 Constructor and Destructor
构造函数
语法定义:函数名没有返回值类型,函数名和类名一致,不允许有返回值列席,不允许有return语句。当没有声明构造函数时,编译器会自动生成默认的构造函数,用于分配类成员的空间。
1 | //类定义 |
初始化列表
除参数表和函数体以外,构造还可以拥有初始化列表,效率比在函数体中赋值要高一些。因为当传递参数是类时,在初始化列表中进行初始化,只需要执行一次复制构造函数。若在函数体中进行初始化,首先需要调用构造函数构造对象,再调用构造函数的赋值运算符进行赋值运算。
语法定义:初始化列表位于参数表和函数体之间,用冒号跟参数表隔开,初始化对象之间用逗号分隔。
1 | //类定义 |
委托构造函数
使用委托构造函数,可以保证代码的一致性,提高代码的复用率,降低修改成本
语法定义:定义构造函数时,可以在初始化列表中使用其他已经声明的构造函数
1 | //类定义 |
复制构造函数
语法定义:函数名与类名一致,形参表为const定义的类对象的引用。可以用”=delete”指令隐藏复制构造函数。有两个要点:复制和构造。
1 | //类定义 |
析构函数
语法定义:函数名在类名前加”~”,不允许有参数表,不允许有返回值列席,不允许有return语句。不定义时,编译器会自动生成析构函数。在对象消亡时,析构函数会被自动调用。
1 | //类定义 |
组合类
组合类的构造函数:初始化的顺序与类中声名的顺序相同,与初始化列表中的顺序不同,组合类初始化时,会调用两次初始化构造函数,第一次是将实参赋值给形参,第二次是把形参值赋值给组合类中的对象。
前向引用声名:只声名类的名字而不包含任何细节。
UML
- 事物:
- 关系:依赖,重数,聚集(组合),泛化
- 图:
特殊类
- 结构体:在C++中是特殊的类,成员默认是public
- 联合体:成员共用存储空间,只有一个成员有效,可以使用无名的联合体。
- 枚举类:类型控制更严格,无法比较不同类的枚举类型;作用域限制在类中,可以使用同样的名字。
数据的共享与保护
作用域
作用域由小到大可以分为以下几种:
- 函数原型作用域:只在形参表中存在
- 局部作用域:又称为块作用域,变量定义所在的最小的一对大括号内
- 类作用域:
- 文件作用域:又称为静态作用域
- 命名空间作用域:
可见性
作用域和可见性可能不一致:当内层作用域与外层作用域出现变量重名
生存期
- 静态生存期:文件作用域中定义的变量,或用static声明的变量;如果在函数体内声明静态变量,那么该变量只会初始化一次,在之后对函数的调用过程中,访问的是同一个变量。
- 动态生存期:与作用域一致,同时消亡;在函数中调用的会随着函数运行栈的消亡而消亡
类的静态对象
- 为所有类的对象共有,可以使用类名直接调用
- 静态数据:声明在类体重,初始化和定义必须在类体之外
- 静态函数:无法确定调用对象,不可直接访问对象的非静态变量
友元: freind
- 友元函数:在类中声明,在类外定义,需要把对象当做实参传递给函数
- 友元类:友元关系式单向的
常类型:const
常对象:必须初始化,不能被更新
常成员:
a. 常数据成员
b. 常函数成员(一种函数重载的判断条件):承诺不改变数据状态,并由编译器进行检验(bitwise检验)
注一:可使用“mutable” 摆动场,释放非静态成员的bitwise constness 约束,从而使得常函数可以修改这些非静态成员的值。
注二:可使用转型“casting”方法,用non-const函数调用const函数,从而实现代码复用。常引用:提高参数传递效率的同时,满足安全性的条件
常指针
1
2
3const int* p1; //指向整型常量的指针
int const * p2; //指向整型常量的指针
int* const p3; //指向整型变量的常量指针
多文件结构
C++工程结构:声明文件、定义文件、使用文件
- 外部变量:文件作用域中定义的变量,默认都是外部变量。使用时需要用extern关键字声明之后,才可以使用
- 外部函数:
- 编译预处理:#include, #define, #if #endif #else #elif, #ifdef
标准C++库
- 输入输出类iostream:
- 容器类与抽象数据类型stl:
- 存储管理类
- 算法algorithms:
- 错误处理
- 运行环境支持
指针和数组 Pointers & Array
&: 取数据对应的地址,*: 取地址对应的数据
指针的应用场景
- 动态内存分配,返回值只能是指针
- 深拷贝:自定义构造函数,为类内的指针属性申请内存空间。
指针 Pointers
- 定义:用于存放地址类型的变量
- 初始化:指针变量的赋值必须是合法获得的地址,不可用非静态变量去初始化静态指针,空指针:nullptr
- 指向常量的指针,指针类型的常量
- 指针的算术运算:+n指向第n个数据的起始位置
- 指针的关系运算:指向相同类型的指针可以进行关系运算,也可以跟0进行关系运算
数组 Array
普通数组
语法定义:类型说明符 + 数组名 + [常量表达式][常量表达式]
1 | int array_1[3]; //一维数组 |
注意:数组名后接的一定是常量表达式,不可以是变量
- 初始化:可以只给一部分元素初始值,数组名保存首元素的内存地址,为常量
1 | int array_1[10] = {0,1,2,3,4,5,6,7,8,9}; //全部初始化 |
- 传递:数组作为函数参数传递时,直接传递指针
指针数组
语法定义:类型说明符 + 指针运算符 + 数组名 + [常量表达式][常量表达式]
代码实例
1 | int *pa[3]; |
指针数组的用法跟多维数组类似,但是多维数组可以按照一维数组来使用,而指针数组不行。
函数与指针
指针类型的函数
语法定义:存储类型 + 数据类型 + *函数名(形参表){函数体}
代码实例
1 | int* func_1(){} |
注意:返回值必须是在主调函数中合法有效的地址,合法的地址包括
- 数组作为参数传递给函数
- 子函数中动态分配的地址
指向函数的指针
语法定义:存储类型 + 数据类型 + (*函数名)(形参表){函数体}
典型用途:函数回调
代码实例
1 | //定义主调函数 |
对象与指针
- 使用语法:对象指针 + “->” + 成员
1 | Point p; |
- this指针:类对象隐含的指针
动态内存分配
- 使用语法:new 类型名T(初始化参数列表);delete 指针标识符;delet[] 数组标识符
1 | Point *ptr = new Point(0.0,0.0); |
- 注意事项:分配和释放一定要配合使用
智能指针
- unique_ptr :不允许多个指针共享资源,可以用标准库中的move函数转移指针
- shared_ptr :多个指针共享资源
- weak_ptr :可复制shared_ptr,但其构造或者释放对资源不产生影响
对象的复制与移动
复制
- 浅层复制:数据成员的一一对应
- 深层复制:在复制对象数据成员的基础上,复制指针指向的动态内存空间
移动构造
语法定义:函数名与类名一致,参数表为类对象的右值引用
代码实例
1 | class IntNum{ |
字符串
- C风格字符串:字符串数组,最后一位用”\0”结尾
- C++风格:string 类
继承 Inherit
基本概念和语法
语法定义: class + 派生类名 + : + 继承方式 + 基类名
1 | class DerivedPublic : public Base {} //基类Base的派生类(公有继承) |
继承方式
类成员的三种访问权限与三种继承方式是对应的,分别限定了类成员对于类对象、子类成员和子类对象的可见性。如果需要基类的某个成员对其派生类可见,但是对其对象不可见,那么就采用保护的可见性。
公有继承
公有继承不改变基类成员的可见性
成员类型 | 基类对象 | 派生类成员 | 派生类对象 |
---|---|---|---|
public | 可见 | 可见 | 可见 |
protected | 不可见 | 可见 | 不可见 |
private | 不可见 | 不可见 | 不可见 |
私有继承
私有继承将基类成员全部转变为派生类的私有成员
成员类型 | 基类对象 | 派生类成员 | 派生类对象 |
---|---|---|---|
public | 可见 | 可见 | 不可见 |
protected | 不可见 | 可见 | 不可见 |
private | 不可见 | 不可见 | 不可见 |
保护继承
保护继承将基类的共有成员转变为保护成员,其他成员的可见性不变
成员类型 | 基类对象 | 派生类成员 | 派生类对象 |
---|---|---|---|
public | 可见 | 可见 | 不可见 |
protected | 不可见 | 可见 | 不可见 |
private | 不可见 | 不可见 | 不可见 |
类型转换
派生类对象可以转换为基类对象。
注意:不要定义继承来的非虚函数
派生类的构造和析构函数
构造函数
语法定义:派生类名::派生类名(基类所需的形参,本类成员所需的形参):
基类名(参数表), 本类成员初始化列表
可以用using直接使用基类的构造函数。
代码示例
1 | class B; //基类B |
复制构造函数
语法定义:派生类名::派生类名(const 派生类名 &派生对象):
基类名(派生对象)
原理在于派生对象可以直接转换为基类对象
程序实例
1 | C::C(const &c1):B(c1){} |
析构函数
语法定义:派生类名::~派生类名()
并不需要显式地调用基类的析构函数
程序实例
1 | C::~C(){} |
访问基类的成员
二义性
当同名成员被多次定义的时候,出现了二义性,可以使用类名来限定。
当派生类从多个基类派生,而这些基类又有共同基类时,二义性会导致程序的冗余,甚至引起混淆或错误。
虚基类
慎用虚基类
语法定义:使用virtual调用
代码实例
1 | class C: virtual public B{} |
构造函数:最远派生类(建立对象时使用的类)给虚基类传递函数。
多态性 Polymorphism
- 编译多态性:重载运算符, 重载函数
- 运行多态性:虚函数
运算符重载
重载规则
运算符的重载不改变运算符的优先级,并且只能重载C++中已经包含的运算符
不能够重载的运算符: “.”, “.*”, “::”, “?:”
双目运算符重载为成员函数
语法定义:函数类型 operator 运算符(形参)
参数个数 = 原操作数 - 1
代码实例
1 | class Complex{ |
单目运算符重载为成员函数
语法定义:
前置:函数类型 &operator 运算符()
后置:函数类型 operator 运算符(int)
代码实例
1 | Clock& operator ++ (); //前置运算符 |
重载操作符为非成员函数
语法定义:需要列出所有的操作数,前置、后置单目运算符需要添加一个int
代码实例
1 | Complex operator+(const Complex &c1, const Complex &c2){ |
虚函数
虚函数
语法定义:virtual 函数类型 函数名(形参表)
虚函数不可以是静态,构造函数不可以是虚函数
代码实例
1 | vitual void display() const; |
虚析构函数
语法定义:与虚函数的定义一样
使用虚析构函数,可以保证动态申请的内存空间能够被释放。否则,基类指针指向的派生类对象消亡时,只会调用基类的析构函数。
虚表与动态绑定
每个多态类有一个虚表,虚表中有当前各个虚函数的入口地址
每个对象有一个指向当前虚表的指针
抽象类
纯虚函数: virtual 函数类型 函数名(参数表) = 0
包含纯虚函数的类为抽象类,不能够构造对象
显示函数覆盖 overriide
不允许被继承 final
语法定义: class 对象名 final;
1 | clase Base1 final {} |
模板 Template
模板
函数模板
语法定义:template<模板参数表> 函数定义
模板参数表:typename 或者 class
1 | template<typename T> |
类模板
语法定义:template<模板参数表> 类定义
1 | template<class T> |
线性群体
直接访问
数组
数组类模板特点:
- 存储空间在内存上连续,可以直接访问数组成员,访问开销为O(1)
数组基本功能:
- 构造函数:构造函数、复制构造函数、析构函数
- 重载操作符:[], *, =
- 数组操作:修改数组大小、取数组大小
代码实现
1 | template <class T> //数组类模板定义 |
顺序访问
节点 Node
节点是顺序访问的基本元素
节点特点:
节点基本功能:
代码实现
1 | template <class T> |
链表 List
链表特点:
- 访问必须从第一个对象开始
链表基本功能:
- 构造函数:构造函数、复制构造函数、析构函数
- 操作符重载:
代码实现
1 | template <class T> |
栈 Stack
栈特点:
- 只能从一端存储和读取数据
栈基本功能:
- 构造函数:
- 栈操作函数
代码实现
1 | template <class T, int SIZE = 50> |
队列 Quene
队列特点:
- 只能从有一段存储数据,从另一端删除数据
队列基本功能:
- 构造函数:
- 队列操作(有修改的):入队、出队、清空
- 队列操作(无修改的):访问队首元素、求队列长度、判断队列是否为空、判断队列是否满
代码实例
1 | template <class T, int SIZE = 50> |
索引访问
排序
排序分为内排序、外排序。内循环全部在内存中进行循环,外排序需要分批次从硬盘中读取数据。
插入排序
每次将待排序元素与已排序数组进行比较,并插入到合适位置上去。
选择排序
在未排序队列中选择最小的数据,放在未排序队列的末尾
交换排序
相邻元素两两比较,如果不合适,则进行交换
堆排序
什么是堆排序?
查找
顺序查找
将整个数组遍历一遍
二分查找
折半查找
泛型程序设计与STL标准模板库
泛型程序设计
泛型程序设计,是为了提高代码复用率的方法。在解决同一类问题的过程中,抽象出一种概念,根据这种概念设计通用的算法。。
术语
- 概念:具备一定功能的数据模型
- 模型:符合一个概念的数据类型,称为该概念的模型
使用概念作为模板参数名,设计对应的类模板和函数模板,从而可以解决同一类问题。
STL标准模板库
基本组件
迭代器是算法和容器的桥梁,将函数对象作为算法的参数。
- 迭代器:泛型指针,提供顺序访问容器对象的方法
- 容器:容纳、包含一组元素的对象,并且通过适配器实现基本容器的特殊化
- 算法:需要包含头文件,提供了70多种算法
- 函数对象:行为类似函数的对象,任何普通函数以及重载”()”运算符的类对象都可以作为函数对象使用
迭代器
输入流迭代器
用来从序列中读取数据
用法:
- 以输入流为参数构造
- 可以使用*(p++)获取下一个输入的元素
代码实例1
istream_iterator<double>(cin)
输出流迭代器
用来向序列中写入数据
用法:
- 以输出流为参数构造
- 可以使用*(p++) = x,将x输出到输出流
代码实例1
ostream_iterator<double>(cout,"\t")
前向迭代器
既是输入流迭代器也是输出流迭代器,可以对序列进行单向遍历
双向迭代器
与前向迭代器类似,不过可以在两个方向遍历数据
随机访问迭代器
与双向迭代器类似,但是可以在任意位置进行跳转。vector容器的迭代器就是随机访问迭代器。
容器
根据组织方式,可分为:顺序容器、关联容器
根据访问方式,可分为:可逆容器(又可衍生出随机访问容器的概念)、不可逆容器
通用接口:
- 默认构造函数
- 关系运算符
- begin(),end()
- clear()
- empty()
- size()
- swap()
顺序容器
举例:向量,双端队列,列表,单向链表,数组
通用接口:
- asign()
- insert()
- resize()
向量(vector):
- 可扩展的数组
- 尾部插入比较快,中间插入比较慢
双端队列(deque):
- 尾部和头部插入较快,中间插入较慢
- 随机访问比向量慢
列表(List):
- 任意位置插入和删除很快
- 不支持随机访问
数组(array):
- 对内部数组的封装
关联容器
特点:每个容器都有key,可根据key进行高效查找
分类:
- 按照键对应的元素多少,可分为单重关联容器和多重关联容器
- 按照类型参数的多少,可分为简单关联容器和二元关联容器
举例:set,multiset,map,multimap
通用接口: - insert()
- erase()
- find()
- lower_bound(),upper_bound(),equal_range()
- count()
集合(set):
- key即是元素本身
映射(map):
- 元素是由键和附加数据组成的二元组pair
- 可以使用下标运算
1 |
|
多重集合(multiset):
- 允许有重复元素
多重映射(multimap):
- 一个键有多个附加数据
函数对象
算法
流类库与输入输出
异常处理
异常:可以预测但是无法避免的错误。
使用独立异常处理模块的原因:1.使得程序整体逻辑连贯;2.小的功能模块没有权限处理错误;
异常处理语法
throw 块1
throw: 抛出异常
try 块1
2try: 所有可能抛出异常的语句,在try块中运行,一旦抛出异常,就中断运行
catch: 捕获异常
注:可在函数声明同时,声明异常类型,方便处理
程序抛掷A,B,C,D类型的异常1
void fun() throw(A,B,C,D);
函数抛掷任意类型的异常1
void fun();
函数不抛掷异常1
void fun() throw();
异常处理机制会自动析构try块中构造的对象
标准异常类
1 | exception |
异常安全性问题
异常安全性:异常发生时,既不泄露资源,也不能使任何对象陷入非法的状态。
原则:不抛掷异常是异常安全的基础
技巧:
- swap()函数一定不会抛出异常;
- 析构函数尽量不要抛出异常,否则在捕获异常时,析构栈上的对象,如果抛出异常,就会调用terminate函数;
- 在抛出异常前,将函数中动态申请的内存释放(过于繁琐);
- 尽量使用对象;
- 使用智能指针auto_ptr指向动态申请的地址,那么当异常被捕获时,会自动析构智能指针,也就释放了动态申请的内存;