目录
基础
- 进制
- 注释:单行注释、多行注释
数据类型
- 标识符
- 数据类型:整型、浮点型、布尔型、字符型、字符串型
- 变量、常量:
- 变量定义后必须赋值才能使用,可以同时定义多个变量
- 常量必须定义时进行初始化,使用const
- 转义字符
- 强制类型转换最后加上()
- 宏定义
#define
- 命名空间
- 可追加:不同文件使用相同命名空间即可追加
- 当前命名空间优先级最高
- 导入的命名空间冲突会报错
运算符
- 算术运算符
- 赋值运算符
- 关系运算符
- 逻辑运算符
- 位运算符
- 三目运算符
流程控制
- 分支结构:if/else、switch
- 循环结构:while/do-while/for
- 流程控制关键字:break/continue/goto
函数
- 函数定义、参数、默认值参数、返回值
- 函数重载:函数名相同,参数列表不同
指针与引用
内存区域:栈、堆、全局区、代码区
- 全局区存放:全局变量、常量(外部可访问),以及静态变量、静态常量(文件内部访问)
内存残留:在栈上分配的内存不会进行初始化,在堆上new出来的空间会被初始化为0的
空指针、野指针:
- 空指针即没有指向任何地址的指针,一般使用nullptr
- 野指针,存储的地址指向的空间已经不存在了
常量指针域指针常量
- 常量指针,const在
*
之前,表示指针指向的是常量,即不允许通过指针修改该值 - 指针常量,const放在
*
之后,表示该指针变量是一个常量,不允许修改改指针变量的值,即不允许将别的地址赋值给该变量
指针与引用在函数中的使用
- 函数参数通过指针可以修改参数的值,而非指针形参不可以
- 引用创建时必须初始化,且初始化后不能改变,不能有nullptr引用,必须确保引用与一块合法的存储单元关联,类型标识符必须与目标变量相同
引用的本质:类似于指针常量,但不是指针常量
- 引用本质上就是一个指针常量,如
int n=10; int &rn=n
,相当于int * const rn=&n
,只不过在使用的时候会自动将rn
转换为*rn
,这种说法正确吗?引用是不占用额外的存储空间的,只是变量的一个别名,而指针常量是需要占用存储空间来保存对象的地址的。
常量引用:对应用使用const修饰,防止修改引用代表的值,同时又避免了拷贝
数组
存储相同数据类型的容器,数组初始化完成后长度不可改变
- 数组的定义、初始化、访问、计算数组的长度
- 数组的访问:下标,基于range的for循环(只能在数组定义作用域内使用,在别的地方会退化为指针),指针计算
- 数组名称实际是首元素的地址,下标访问实际上通过首地址增加元素大小计算出来的地址进行访问的
- 当数组作为参数传递时,会退化为数组元素的首地址,丢失了长度信息
- 数组的排序、查找
- 深拷贝与浅拷贝
- 二维数组定制、初始化、访问
面相对象
类的设计和对象的创建
类的定义:
- 类定义:class关键字,定义属性和方法
- 访问权限:private(默认,当前类内访问)、protect(当前类及子类)、public(所有位置都可以访问)
- 对象占用空间大小:为类内所有属性占用空间大小之和,如果没有属性,也占用1字节,表示创建对象成功
对象的创建:
- 声明即可使用,使用了默认构造函数,或使用其他构造函数进行赋值,
- 如果没有定义构造函数,会有默认的,它会调用每个成员的默认构造器进行初始化,基本类型初始化为0,指针初始化为nulllptr,如果定义了构造器,但是有些属性没有初始化,其值是不确定的
成员访问:使用.
,如果是指针可以简化为使用->
静态属性、静态方法:
- 静态属性:存储在全局区,与对象无关,在程序编译的时候已经完成了空间的开辟和初始化操作,并在整个程序运行期间保存。
- 在类内定义的static属性,必须类内定义,类外初始化。对于静态常量,除整型、bool允许类内初始化,其他的也必须类外初始化
- 访问:可以使用对象访问,但建议使用类访问,因为是全局变量
- 在类声明时定义的方法都是内联方法,但编译器可以根据多种因素决定是否内联
构造函数和析构函数
构造函数:
- 在对象创建的时候触发,来初始化对象属性
- 构造函数的名称与类名相同,没有返回值,可以进行重载
- 可以显式调用和隐式调用,显式调用则是使用(),隐式调用直接赋值或者使用{}
- 使用
explicit
修改构造函数可以禁止隐式调用 - 如果没有写构造函数,系统会提供默认的public的无参构造,如果写了,则系统不在提供任何默认的构造函数
- 构造函数初始化可以使用初始化列表,对属性进行初始化,系统会自动区分属性和形参名称
复制构造函数:
- 参数是
const 类名& p
,也即是对象的常量引用,如果不写,系统会提供默认的 - 在一个对象赋值给另外一个对象的时候会被调用
析构函数:
- 对象生命周期的重点,在对象被销毁之前调用,一般进行资源的释放,如堆内存的释放
~
开头,不能有参数
浅拷贝与深拷贝:
- 浅拷贝,只是值拷贝,对于地址,只拷贝地址值,不拷贝地址指向的数据
- 深拷贝,对于地址,重写开辟空间,将原来地址指向的内容拷贝的新的空间
- 默认的复制构造函数就是浅拷贝
this指针:
- 指向当前对象的指针,当前对象指的是调用对象
- 在出现局部变量与属性名或方法名相同时,为了区分可以使用
this->属性名/方法名
表示对象的属性或方法 - 可以使用
*this
返回对象本身,如果要避免调用复制构造函数,可以是返回值设置为类&
- 可以使用空指针调用成员函数,如果成员函数中对空指针进行了保护,并且没有this访问空间的代码,则可以正常执行,但应该避免空指针访问
常函数与常对象
- 常函数的函数后面使用
const
修饰,在函数中不允许修改属性值,不允许调用普通函数,只能调用常函数,void display() const;
- 常对象:创建对象时使用const修饰,则该对象为常对象,可以读取属性值,不能修改属性值,不能调用普通函数,只能调用常函数,
const Person p = 10;
mutable
,用来修饰属性,表示可变,可以被常函数、常对象修改
友元
有时需要在类的外部往外私有成员,可以使用友元函数,是一种特权函数,可以使用这个特权函数访问私有成员。可以将全局函数、某个类中的成员函数、甚至整个类都声明为友元。
全局函数作为友元:
- 在类内部声明
friend+全局函数声明
,则该函数内部可以访问所有该类的私有属性和方法
成员函数作为友元:
- 在类内部声明
friend+成员函数成名
,则该成员函数内部可以访问所有该类的私有属性和方法
类作为友元:
- 在类A内部声明
friend 类B
,则类B的所有成员函数都可以访问所有类A的私有属性和方法
运算符重载
对已有的运算符进行重新定义,赋予其另一种功能,只是一种语法上的方便,只是另一种调用函数的方式。
语法:
- 定义重载运算符就像定义函数一样,只是函数的名字为
operatorXXX
,其中XXX为运算符,如+、-等 - 如果运算符是一元,只需要一个参数,如果是二元,则需要两个参数
- 运算符被定义为全局函数时,一元运算符需要一个参数,二元运算符需要两个参数。
- 运算符被定义为成员函数时,一元函数不需要参数(
*this
就是这个参数),二元函数只需要一个参数(*this
是第一个参数) - 注意:它只是语法上的方便,只有在能使涉及类的代码更易写、更易读的时候才有理由重载运算符,如果不是这样,就改用其他更易用易读的方式。
+/-/++/--
都可以使用成员函数或者友元全局函数实现重载,<<
如果需要类的private属性也需要使用友元全局函数重载,=
重载必须是非静态成员函数,不能是全局函数
封装(encapsulation)
- 对于属性,不希望外部直接访问,可以将其私有化,提供相应的public方法进行访问
继承(inheritance)
- 语法:
class 子类: [继承方式] 父类
- 父类中的所有非静态成员都可以继承给子类(不包括构造函数和析构函数),静态成员隶属于类,不属于对象
- 一个类可以有多个父类(多继承)
访问权限:
- public,可以在任意位置访问
- protected,可以在当前类和子类中访问
- private(默认),只能够在当前类中访问
继承方式:
- public,保留原有的访问权限;
- protected,超过protected权限部分,降为protected权限
- private(默认),访问权限都设置为private权限
继承中的构造函数和析构函数:
- 子类对象在创建的时候,需要先调用父类的构造函数,来初始化从父类继承的部分,再调用子类的构造函数,来完成对象的初始化。
- 如果父类没有无参构造,则需要子类显示调用父类的有参构造函数,父类的private构造函数也可以调用,这是c++继承机制的一部分,保证子类可以正确的初始化化父类
- 子类对象在销毁的时候,先调用自己的析构函数,再调用父类的析构函数
子类与父类同名成员:
- 子类会隐藏父类的同名成员,子类对象要访问父类的同名成员,则需要显式指定父类
子类对象::父类:同名成员
- 如果此时使用向上转型,则父类调用的还是父类,丢失了子类的信息,并创建了新的对象。如果是指针,子类指针调用子类方法,父类指针调用父类方法,虽然地址相同,但是父类指针的类型中丢失了子类的信息。
多继承:
- 一个类可以继承多个父类
- 二义性:如果多个父类中存在相同名字的成员,子类继承后无法确定到底访问哪一个,需要显式指定是哪个父类的成员
子类对象::父类:同名成员
- 菱形继承:两个类继承自同一个父类,又有了相同的子类。父类中的属性被两个类都继承了一份,子类在访问间接父类的属性时会有两份,有歧义,需要显式指定是哪个父类的成员
子类对象::父类:同名成员
,也可以使用虚继承,使得派生类中只保留一份相同的间接父类的成员。(菱形继承要尽量避免)
多态(polymorphism)
c++支持编译时多态和运行时多态,运算符重载和函数重载是编译时多态,派生类和虚函数实现运行时多态。区别就是函数地址是早绑定(静态联编)还是晚绑定(动态联编)。如果在编译阶段就确定了函数调用的地址并产生代码,那么就是编译时多态,地址早绑定。而如果函数地址不能在编译期间确定,而需要等待运行时才确定,就是运行时多态,地址晚绑定。
向上转型:
- 父类的引用、指针指向子类的对象(是同一个对象,但是类型此时是父类)
- 此时父类引用、指针只能调用父类的方法和属性,对于同名方法,调用的也是父类方法,之所以调用父类方法是由于编译期就进行了函数绑定,确定了调用的函数地址。如果需要调用子类的方法,需要运行期才能确定子类对象是哪个子类,c++通过虚函数实现。
虚函数:
- c++多态是通过虚函数实现的,虚函数允许子类重新定义父类方法,而子类重新定义父类虚函数的做法成为重写(override),并将函数声明增加
virtual
关键字,实现时不需要。 - 如果一个函数被声明为
virtual
,那么在所有派生类中它都是virtual
的。 - 构造函数不能为虚函数,析构函数可以
纯虚函数和抽象类:
- 基类仅仅作为派生类的接口,不希望用户创建基类的对象,那么可以设计这个基类包含纯虚函数
- 纯虚函数:虚函数的实现部分设置为0,即
virtual 函数声明=0;
- 抽象类:如果一个类包含纯虚函数,那么这个类就是一个抽象类,抽象类无法创建对象
- 如果一个类继承一个抽象类,这个类必须重写实现所有父类中的纯虚函数,否则它也是一个抽象类
纯虚函数与多继承:
- 多继承带来了一些争议,但是接口继承可以说是一种毫无争议的运用,接口类中只有函数原型定义,没有任何数据定义
- 多重继承不会带来二义性和复杂性的问题,接口类只是一个功能声明,并不是功能实现,子类需要根据功能说明定义功能实现。
- 构造函数不能声明为纯虚函数,析构函数可以
- 如果两个抽象父类中有相同的成员函数,则子类可以只实现一个,但是如果返回值不同,则无法同时继承这两个抽象类。
虚析构函数:
- 如果是父类对象的引用或指针,要通过虚析构函数析构子类对象的资源,否则只能调用父类的析构函数,而导致子类的资源泄漏
- 如果一个类的目的是为了实现多态,那么这类的析构函数就有必要设置为虚析构函数
结构体
- 使用
struct
声明,与使用class
声明类基本没有区别 - 结构体默认是public权限,类默认是private权限。结构体继承默认也是public
模板
函数体相同的函数都可以使用模板代替,不用定义多个函数,只需在模板中定义一次即可,在调用函数时系统会根据实参的类型来取代模板中的类型参数,从而实现不同的功能。c++提供函数模板和类模板。
- 模板把函数或类要处理的数据类型参数化,表现为参数的多态性,成为类属
- 模板用于表达逻辑结构相同,但具体数据元素类型不同的数据对象的通用行为
模板定义与使用
- 使用
template<typename T,...> void add(...)
,指定函数模板类型参数 - 调用函数模板可以显式指定类型参数
void add<T1, T2>(...)
,可以指定部分类型参数,可以为参数类型指定默认值,也可以让其自动推导,注意,如果自动推导,要保证相同类型参数的实参自动推导出的类型是一致的,否则无法编译。 - 在函数模板内部,使用模板类型参数,外部传入的实参需要保证函数内部使用时支持相应的功能,如
+
,实参类型需要有+
的运算符重载,否则会运行时错误 - 如果类型参数用在返回值,必须手动指定类型,无法自动推导,所以为了不为了写返回值类型还要写其他的类型参数的类型,一般将返回值的类型参数放在模板参数的第一个位置,其他的类型参数通过实参进行推导
普通函数调用与函数模板调用:
- 普通函数调用会发生自动类型转换,函数模板不会
- 如果在调用函数时,既可以匹配普通函数,也可以匹配函数模板,则优先调用普通函数
函数模板的局限性:
- 使用函数模板需要保证实参支持模板中使用实参的方式,如果不支持,编译可以通过,运行时会报错,如模板函数中实现
+
,对象确没有实现+
运算符重载 - 函数模板也可以重载,此时也是用
template
,但是具体指定类型参数的实际类型
类模板:
- 类模板与函数模板的定义和使用基本是一样的,但还是有区别的
- 类模板不能自动类型推导,函数模板可以通过实参推断,类模板无法推断(如果构造器中的参数包含了所有类型参数的形参,也可以推断)
- 普通函数使用类模板作为参数,需要明确类型
- 函数模板中使用类模板作为参数,类模板可以明确类型,也可以使用函数模板中的类型参数
类模板继承:
- 类模板中的类型参数时不能够继承的,继承类模板时,需要指定父类中的类型参数
类模板成员函数创建时机
- 在调用函数时才会创建,如果不调用,则该类模板成员函数不会创建,因为在编译的时候,只知道一个类型参数,不知道这个类型参数具体会有哪些属性或者方法,只有调用了,在编译时才会生成对应的成员函数,如果该类型参数没有调用的属性或者方法,则编译报错。
类模板中函数的类外实现:
- 对于普通类,在类的声明之外实现成员函数,可以使用
类::
限定,但如果是一个模板类,在类外实现时,也需要为每一个函数都加上类型参数模板声明template<类型参数> xxx 返回值 类<类型参数>::方法名(...){...}
类头文件与源文件分离:
- 普通类与模板类声明放在头文件中,实现放在源文件中,只要在编译时包含了头文件,并将实现添加进了源文件,编译时会正确找到实现的
类模板和全局友元函数:
- 可以直接在类声明内部直接实现友元函数,这个友元函数是全局的,可以直接在外部使用
- 可以在类内声明,类外实现,声明时在友元函数声明中函数名后增加
<>
,使其查找友元函数实现时查找一个模板函数,如果在同一个文件中,需要将友元函数的实现放在类声明的前面,否则查找不到友元模板函数的实现,如果友元函数中使用了当前类,则又需要将类的前置声明放在友元函数外部实现的前面。 - 如何是在头文件和源文件中分别定义类模板的友元函数的声明和实现,需要在类的声明前先声明友元函数模板的声明(如果下面没有声明该模板函数为友元,则其就是一个普通的模板函数,而不是友元),如果友元函数中使用到当前类,还需要在友元函数模板前对类进行前置声明。
- 为什么要提前声明友元函数模板呢?因为类模板、函数模板都只有在调用时才会生成对于类型的代码,类模板的友元函数在被调用时需要知道该友元函数的声明,
friend xxx
只是定义了友元关系。
STL标准模板库
容器
- string
- vector,底层是数组
- deque,双向队列,底层是中控器实现
- stack,栈,默认实现是deque,不提供迭代器,也无法遍历
- queue,队列,默认实现是deque,不提供迭代器,也无法遍历
- list,双向链表
- set,所有元素自动排序,去重,底层是红黑树,迭代器是只读的
- multiset,所有元素自动排序,允许重复,底层是红黑树,迭代器是只读的
- pair,键值对,可以使用有参构造,和make_pair构造
- map,存储的元素是pair,按照key排序,会对key去重,底层是红黑树,迭代器中的key是只读的
- multimap,存储的元素是pair,按照key排序,允许重复,底层是红黑树,迭代器中的key是只读的
算法
- 函数对象:重载函数调用操作符的类,其对象为函数对象。它的行为类似于函数,也叫做仿函数,本质上是由于
()
运算符重载,使得类对象可以像函数那样调用。函数对象超出了普通函数的概念,可以保存函数的状态,类似闭包 - 谓语:函数对象的返回值是bool类型。可结合
algorithm
中的find_if/copy_if/remove_if/all_of/sort
等函数使用,类似达到java中stream的用法 - 内置函数对象:算数类、关系运算类、逻辑运算类
- 常用算法,算法主要由头文件组成,是所有stl头文件中最大的一个,常用的功能涉及到比较、交换、查找、遍历、复制、修改、反转、排序、合并等,
for_each
,遍历元素,提供一元函数,无返回值(Consumer)transform
,遍历元素,提供一元函数,有返回值(Function)find/find_if
,按==
查找指定元素、按条件查找元素(Predict),返回迭代器,没找到返回end()adjacent_find
,查找相邻元素重复的,按==
,也可以提供二元函数(BiPredict)binary_search
,二分查找,按==
,或提供二元函数(BiPredict)count/count_if
,查找相等元素的数量,按==
,也可以提供一元函数(Predict)sort
,排序,按==
,或提供二元函数(BiPredict)merge
,合并两个数据源,数据源有序,按值比较合并,也可以提供二元函数(BiPredict)random_shuffle/shuffle
,随机打乱reverse
,反转排列copy/copy_if
,拷贝指定范围的元素,也可以提供一元函数(Predict)replace/replace_if
,替换指定范围的元素,也可以提供一元函数(Predict)swap
,交换两个容器中的元素accumulate
,对元素执行reduce操作,可以指定初始值,也可以提供二元函数(BiFunction)fill
,填充容器为指定元素set_intersection/set_union/set_difference
,计算有序集合的交集、并集、差集,也可以提供二元函数(BiPredict)