1. C到C++的升级
① C与C++的关系
(1)C++继承了所有的C特性
(2)C++在C的基础上提供了更多的语法和特性
(3)C++的设计目标是运行效率与开发效率的统一
注:C++顾名思义是对C语言的加强,加强主要体现在两个方面:面向对象的支持和类型的加强
② C++中更强调语言的实用性,所有的变量都可以在需要使用时再定义
③ regiter关键字的变化:鸡肋
④ C语言中,重复定义多个全局变量是合法的(不进行初始化);在C++中,不允许定义多个同名全局变量
⑤ struct类型的加强:C++中的struct是一个新类型的定义声明
struct Students { const char* name; int age; };
⑥ C++中的任何标识符都必须显示的指明类型(C++是一种强类型的语言)
⑦ C语言中的const
(1)const修饰的变量是只读的,本质还是变量(除去一种情况:const声明的全局变量并对它进行初始化,这种情况变量存贮在只读代码段)
(2)const修饰的局部变量在栈上分配空间(只读变量)
(3)const修饰的全局变量,如果初始化的,在只读代码段分配空间(用指针尝试修改其值会引发段错误);如果未初始化,则仍在全局数据段分配空间(只读变量)
(4)const只在编译期有用,在运行期无用
注:const不能定义真正意义上的常量,C语言用只有用enum可以定义真正意义上的常量
⑧ C++中的const声明:
(1)当碰见const声明时在符合表中放入常量(用常量初始化)
(2)编译过程中若发现使用常量则直接用符号表中的值替换
(3)编译过程中若发现对const使用了extern或者&操作符,则会给对应常量分配存储空间
注:C++编译器虽然可能为const常量分配空间,但不会使用其存储空间的值
(4)const声明的变量(无论是全局变量还是局部变量),未用常量初始化,仍然是只读变量
⑧ C++在C语言的基本类型系统之上增加了bool,可取的值只有true(非0)和false(0)
⑨ C++中的三目运算符可直接返回变量本身(引用),因此可以作为左值使用
注:三目运算符可能返回值中如果有常量值,则不能作为左值使用
2. 引用的深入理解
① 在C++中增加了引用的概念:
(1)引用可以看做一个已定义的变量的别名
(2)引用的语法:Type& name = var;
int a = 5; int& b = a;
注:引用的意义是实现多个变量指向同一段存储空间
② 引用在一些场合可以代替指针,相对于指针来说,引用具有更好的可读性和实用性
③ const引用让变量拥有只读属性
④ 当使用常量对const引用进行初始化时,C++编译器会为常量值分配空间,并将引用作为这段空间的别名(只读变量)
const int& b = 1;
⑤ 引用有自己的存储空间,和指针占用同样大小的空间
#include <stdio.h> struct Test { char& r; }; int main() { char c = 'c'; char& rc = c; Test t = {c}; printf("sizeof(char&) = %d ", sizeof(char&));//1 printf("sizeof(rc) = %d ", sizeof(rc));//1 printf("sizeof(Test) = %d ", sizeof(Test));//8 printf("sizeof(t.r) = %d ", sizeof(t.r));//1 return 0; }
⑥ 引用在C++中的内部实现是一个常指针
Type& name <==> Type* const name
注:C++编译器在编译过程中使用常指针作为引用的内部实现,因此引用所占用的空间大小与指针相同
⑦ 引用在定义的时候,在栈中储存了原变量的地址,在使用的时候,通过所储存的原变量的地址来访问原变量。
int c = 12; 1047c: e3a0300c mov r3, #12 10480: e50b300c str r3, [fp, #-12] int& rc = c; 10484: e24b300c sub r3, fp, #12 10488: e50b3008 str r3, [fp, #-8] rc = 20; 1048c: e51b3008 ldr r3, [fp, #-8] 10490: e3a02014 mov r2, #20 10494: e5832000 str r2, [r3]
⑧ 当函数返回值为引用时,
(1)若返回栈变量:不能成为其他引用的初始值;不能作为左值使用(只能把它的值赋给其他变量)
(2)若返回静态或全局变量:可以成为其他引用的初始值;既可以作为左值使用,又可以作为右值使用
注:不能返回局部变量的引用
3. C++中的函数升级-上(内联函数及默认参数)
① C++编译器直接把内联函数体插入函数调用的地方
② C++中可以在函数声明时为参数提供一个默认值,而且参数的默认值必须从右向左提供
4. C++中函数的升级-下(函数重载)
① 重载:同一个标识符在不同的上下文有不同的意义
② 函数重载(Function Overload)
(1)同一个函数名定义不同的函数
(2)当函数名和不同的参数搭配时函数的含义不同
③ 函数重载至少满足下面一个条件
(1)参数个数不同
(2)参数类型不同
(3)参数顺序不同
注:函数类型由参数和返回值决定,但重载仅和参数相关,与返回值无关
④ 函数重载是由函数名和参数列表决定的,返回值不嫩作为函数重载的依据
⑤ 编译器调用重载函数的准则:将所有同名函数作为候选者,尝试寻找可行的候选参数
(1)精确匹配实参
(2)通过默认参数能够匹配实参
(3)通过默认类型转换匹配实参
⑥ 函数重载注意事项
(1)重载函数本质上是相互独立的不同函数
(2)重载函数的函数类型是不同的
(3)函数返回值不能作为函数重载
注:函数重载是由函数名和参数列表决定的
⑦ 当使用重载函数名对函数指针进行赋值时:
(1)根据重载规则挑选与函数指针参数列表一致的候选者
(2)严格匹配候选者的函数类型与函数指针的函数类型(包括函数参数和函数返回值)
#include <stdio.h> int Test(int a) { printf("Test(int a) "); return a; } int Test(int a, int b) { printf("Test(int a, int b) "); return (a + b); } int main() { int (*pT1)(int a) = Test; int (*pT2)(int a, int b) = Test; printf("pT1 = %p ", pT1); printf("pT2 = %p ", pT2); pT1(10); pT2(10, 20); return 0; }
⑧ C++编译器编译C代码:extern "C"
#ifdef __cplusplus extern "C" { #endif ... #ifdef __cplusplus } #endif
5. C++中动态类型分配
① 变量申请与释放
Type* pointer = new Type;
delete pointer;
② 数组申请与释放
Type* pointer = new Type[N];
delete[] pointer;
注:对象(包括类对象)的释放用delete,对象数组的释放用delete[]
6. C++中的命名空间
① C++中提出了命名空间的概念
(1)命名空间将全局作用域分成不同的部分
(2)不同命名空间中的标识符可以同名而不会发生冲突
(3)命名空间可以相互嵌套
(4)全局作用域也叫默认命名空间
② C++中命名空间的定义:namespace name {/* ... */}
③ C++命名空间的使用
(1)使用整个命名空间:using namespace name
(2)使用命名空间中的变量:using name::variable
(3)使用默认命名空间中的变量:::variable
注:默认情况下可以直接使用默认命名空间中的所有标识符
#include <stdio.h> namespace First { int i = 0; } namespace Second { int i = 1; namespace Internal { struct P { int x; int y; }; } } int main() { using namespace Second; using Second::Internal::P; printf("First::i = %d ", i); printf("Second::i = %d ", Second::i); return 0; }
7. 强制类型转换
① C++将强制类型转换分为4中不同的类型:static_cast、const_cast、dynamic_cast、reinterpret_cast
② C++中强制类型转换用法:xxx_cast<type>(Exptession)
③ static_cast:
(1)用于基本类型间的转换,但不能用于基本类型指针间的转换
(2)用于有继承关系类对象之间的转换和类指针之间的转换
④ const_cast:
用于去除变量的const属性
⑤ reinterpret_cast:
(1)用于指针之间的强制转换
(2)用于整数和指针间的强制转换
⑥ dynamic_cast:
(1)主要用于类层次间的转换,还可用于类之间的交叉转换
(2)dynamic_cast具有类型检查的功能,比static_cast更安全
8. 经典问题解析一
① 符号表
(1)符号表是编译器在编译过程中产生的关于源程序中语法符号的数据结构,如常量表、变量名表、数组名表、函数名表
(2)符号表是编译器自用的内部数据结构,它不会进入最终产生的可执行程序中
② const和引用的问题
(1)只有用字面量初始化的const常量才会进入符号表
(2)使用其他变量初始化的const常量仍然是只读变量
(3)被volatile修饰的const常量不会进入符号表(仍然是只读变量)
注:在编译期间不能直接确定初始值的const量,都被作为只读变量处理
(4)const引用的类型与初始化变量的类型相同:变量成为只读变量,不同:生成一个新的只读变量,其初始值与初始化变量相同
9. 面向对象基本概念
① 基本概念
(1)类指的是一类事物,是一个抽象概念
(2)对象指的是属于某个类的一个实体,是一个具体存在的事物
② 类的封装:public、protect、private
③ 在用struct定义类时,所有成员的默认属性为public;在用class定义类时,所有成员的默认属性为private
10. 构造与析构
① C++中的类可以定义与类名相同的特殊成员函数,用于创建类时的初始化工作,这种函数叫做构造函数。
注:构造函数在定义时可以有参数,但是没有任何返回类型的声明,且在对象创建时自动调用
② 构造函数的调用
Test t1(4); //自动调用 Test t2 = 5; //自动调用 Test t3 = Test(6); //手动调用
注:对于第二种情况(Test t2 = 5;),如果用explicit声明构造函数,该调用将会失败
③ 两个特殊的构造函数
(1)无参构造函数:当类中没有定义构造函数时,编译器默认会提供一个无参构造函数,其函数体为空
(2)拷贝构造函数:当类中没有定义拷贝构造函数时,编译器默认会提供一个拷贝构造函数,简单的进行成员变量的值复制
class Test { public: Test() //无参构造函数 { } Test(const Test& v)//拷贝构造函数 { } };
注:如果类中定义任意构造函数,则编译器将不会提供默认的无参构造函数,但仍会提供拷贝构造参数
④ 拷贝构造函数的调用时机
(1)类对象作为函数参数(值传递)
(2)类对象作为返回值(值传递)
(3)用另一类对正在创建的类初始化
⑤ 浅拷贝与深拷贝
(1)浅拷贝:拷贝后对象的物理状态相同
(2)深拷贝:拷贝后对象的逻辑状态相同
注:编译器提供的拷贝构造函数只进行浅拷贝
(3)深拷贝使用时机:对象中有成员指代了系统中的资源,如动态内存空间、打开文件、使用网络端口
(4)一般性原则,自定义拷贝构造函数,必然需要实现深拷贝
⑥ C++中的类可以定义一个特殊的成员函数清理对象,这个特殊的成员函数叫做析构函数
定义:~ClassName();
注:析构函数没有参数也没有任何返回类型的声明,且在对象销毁时自动调用
⑦ C++提供了初始化列表对成员变量进行初始化
Constructor::Constructor() : m1(v1), m2(v1, v2), m3(v3)
{
...
}
(1)成员的初始化顺序与成员声明顺序相同,与初始胡列表中的位置无关
(2)初始化列表先于构造函数的函数体执行
⑧ 类中的const成员
(1)类中的const成员会被分配空间
(2)类中的const成员的本质是只读变量
(3)类中的const成员只能在初始化列表中指定初始值
注:编译器无法直接得到const成员的初始值,因此无法进入符号表成为真正意义上的常量
⑨ 初始化与赋值不同
(1)初始化:对正在创建的对象进行初值设置
(2)赋值:对已经存在的对象进行初值设置
⑧ 对象的定义和对象的声明不同
(1)对象的定义:申请对象的空间并调用构造函数
(2)对象的声明:告诉编译器存在一个这样的对象
⑨ 对象的构造顺序
(1)对于局部对象:当程序执行流到达对象定义语句时进行构造
(2)对于堆对象:当程序执行流到达new语句时创建,使用new创建对象将自动触发构造函数的调用
(3)对于全局对象:对象的构造顺序是不确定的,不同的编译器使用不同的规则确定构造顺序(不要定义相互依赖的全局对象)
注:全局对象的构建总是先于局部对象的构建
11. 经典问题解析二
① 单个对象创建时构造函数的调用顺序
(1)调用父类的构造函数
(2)调用成员变量的构造函数(调用顺序与声明顺序相同)
(3)调用类自身的构造函数
注:析构函数与对应构造函数的调用顺序相反
② 多个对象析构时:析构顺序与构造顺序相反
③ 对于栈对象和全局对象,类似于入栈与出栈的顺序,最后构造的对象最先析构;堆对象的析构发生在delete的时候,与delete的使用顺序相关
④ C++中的const对象
(1)const修饰的对象为只读对象
(2)只读对象的成员函数不允许被改变
(3)只读对象是编译阶段的概念,运行时无效
⑤ C++中const成员函数
(1)const对象只能调用const的成员函数
(2)const成员函数中只能调用const成员函数
(3)const成员函数中不能直接改写成员变量的值
⑥ const成员函数的定义:Type ClassName::function(Type p) const
注:类中的函数声明与实际函数定义都必须带const关键字
⑦ 成员变量和成员函数与具体对象的关系:每一个对象都有一套自己的成员变量,但所有对象共享一套成员函数
(1)从面相对象的角度:对象由属性(成员变量)和方法(成员函数)构成
(2)从程序运行的角度:对象由数据和函数构成,数据可以位于栈、堆和全局数据区,函数只能位于代码段(数据段是可读可写的、代码段是只读的)
⑧ 成员函数和成员变量与对象关系总结
(1)每一个对象拥有自己独立的属性(成员变量)
(2)所有对象共享类的方法(成员函数)
(3)方法能够直接访问对象的属性
(4)方法中的隐藏参数this用于指代当前对象
注:一套成员函数通过一个特殊的指针this指针来访问不同的对象
12. 类的静态成员
① 类的静态成员变量
(1)静态成员属于整个类所有,不需要依赖任何对象
(2)静态成员变量的生命期不依赖任何对象
(3)所有对象共享类的静态成员变量
(4)可以通过类名直接访问public静态成员(ClassName::StaticVar)
(5)可以通过对象名访问public静态成员(ObjName.StaticVar)
(6)静态成员函数只能访问静态成员变量,而不能访问普通成员变量
② 静态成员变量的定义
(1)在类内定义时直接通过static关键修饰
(2)类的静态成员变量不依赖于任何对象,需要在类外单独分配空间
(3)类的静态成员变量在程序内部位于全局数据区
(4)类的静态成员变量在类外单独分配空间语法规则:Type ClassName::StaticVar;
③ 静态成员函数的定义:直接通过static关键字修饰
class Test { private: static int i; public: static int GetI() { return i; } static void SetI(int v) { i = v; } }; int Test::i = 0;
④ C++中的静态成员函数
(1)静态成员函数是类中特殊的成员函数,它属于整个类所有
(2)可以通过类名直接访问公有静态成员函数
(3)可以通过对象名范文公有静态成员函数
⑤ 静态成员函数与普通成员函数的区别:静态成员函数不包含指向具体对象的指针,普通成员变量包含一个指向具体对象的指针
注:静态成员函数只能访问静态成员变量,而不能访问普通成员变量,因为其未包含this指针
⑥ C++对象模型初探
(1)成员变量
* 普通成员变量:存储于对象中,与struct变量有相同的内粗布局和字节对齐方式
* 静态成员变量:存储于全局数据区中
(2)成员函数
* 存储于代码段中
13. 二阶构造模式
① 构造函数的真相
(1)只提供自动初始化成员变量的机会
(2)不能保证初始化逻辑一定成功
(3)执行return语句后构造函数立即结束
注:构造函数能决定的只是对象的初始状态,而不是对象的诞生
② 半成品对象
(1)初始化顺序不能按照预期完成而得得到的对象
(2)半成品对象是合法的C++对象,也是Bug的重要来源
注:在工程里,野指针和半成品对象是两类最难调试的Bug
③ 工程开发中的构造函数可分为
(1)资源无关的初始化操作:不可能出现异常情况的操作
(2)需要使用系统资源的操作:可能出现异常情况,如内存申请、访问文件
④ 二阶构造流程图
⑤ 二阶构造示例代码
class TwoPhaseCons { private: TwoPhaseCons() { // Resource independent cod } bool Construct() { // Resource related code return true; } public: static TwoPhaseCons* NewInstance(); }; TwoPhaseCons* TwoPhaseCons::NewInstance() { TwoPhaseCons* ret = new TwoPhaseCons(); if((ret != NULL) && (ret->Construct() == NULL)) { delete ret; ret = NULL; } return ret; }
注:二阶构造能够确保创建的对象都是完整初始化的
14. 友元
① 友元的概念
(1)友元是C++中的一种关系
(2)友元关系发生在函数与类之间或者类与类之间
(3)友元关系是单向的,不能传递
② 友元的用法
(1)在类中一friend关键字声明友元
(2)类的友元可以是其他类或者函数
(3)友元不是类的一部分
(4)友元不受类中访问级别的限制
(5)友元可以直接访问具体类的所有成员
③ 在类中用friend关键字对函数或类进行友元声明
class Point { double x; double y; friend void func(Point& p); } void func(Point& p) { }
④ 友元的评价
(1)友元是为了兼顾C语言的高效而诞生的
(2)友元直接破坏了面向对象的封装性
(3)友元在实际产品中的高效是得不偿失的
(4)友元在现代软件工程中已经逐渐被遗弃
⑤友元注意事项
(1)友元关系不具备传递性
(2)类的友元可以是其它类的成员函数
(3)类的友元可以是某个完整的类:所有的成员函数都是友元
15. 类中的函数重载
① 函数重载回归
(1)函数重载的本质为相互独立的不同函数
(2)C++中通过函数名和函数参数确定函数调用
(3)无法直接通过函数名得到重载函数的入口地址
(4)函数重载必然发生在同一个作用域中
② 类中的成员函数可以进行重载
(1)构造函数的重载
(2)普通成员函数的重载
(3)静态成员函数的重载
③ 关于重载万变不离其宗的准则
(1)重载函数的本质为多个不同的函数
(2)函数名和参数列表是唯一的标识
(3)函数重载必须发生在同一个作用域中
注:普通成员函数和静态成员函数可以重载,全局函数与普通成员函数那会不能重载,全局函数与静态成员函数不能重载
④ 重载的意义
(1)通过函数名对函数功能进行提示
(2)通过参数列表对函数用法进行提示
(3)扩展系统中已经存在的函数功能
16. 操作符重载
① 操作符重载
(1)C++中的重载能够扩展操作符的功能
(2)操作符重载为操作符提供不同的语义
(3)操作符重载以函数的方式进行
(4)操作符重载的本质是用特殊形式的函数扩展操作符的功能
② C++中操作符重载的本质:(函数重载)
(1)C++中通过operator关键字可以利用函数扩展操作符
(2)operator的本质是通过函数重载实现操作符重载
(3)语法:
Type operator Sign(const Type p1, const Type p2) { Tpye ret; return ret; }
Sign为系统中预定义的操作符,如:+,-,*,/ 等
③ 用全局函数实现操作符重载:在类中需要用friend关键字把该函数声明为友元
class Complex { int a; int b; public: Complex(int a = 0, int b = 0) { this->a = a; this->b = b; } int GetA() { return a; } int GetB() { return b; } friend Complex operator +(const Complex& p1, const Complex& p2); }; Complex operator+(const Complex& p1, const Complex& p2) { Complex ret; ret.a = p1.a + p2.a; ret.b = p1.b + p2.b; return ret; }
④ 可以将操作符重载函数定义为类的成员函数
(1)比全局操作符重载函数少一个参数(左操作数,因为成员函数默认有一个this指针)
(2)不需要依赖友元就可以完成操作符重载
(3)编译器优先在成员函数中寻找操作符重载函数
class Complex { private: int a; int b; public: Complex(int a = 0, int b = 0) { this->a = a; this->b = b; } int GetA() { return a; } int GetB() { return b; } Complex operator+(const Complex& p1); }; Complex Complex::operator+(const Complex& p1) { Complex ret; ret.a = this->a + p1.a; ret.b = this->b + p1.b; return ret; }
⑤ 操作符重载注意事项
(1)C++规定赋值操作符(=)只能重载为成员函数
(2)操作符重载不能改变原操作数的优先级
(3)操作符重载不能改变操作数的个数
(4)操作符重载不应改变操作符的原有语义
注:在C++中,= [] () ->只能通过成员函数来重载
17. 初探C++标准库
① 重载左移操作符,将变量或常量左移到一个对象中
② C++标准库
(1)C++标准库并不是C++语言的一部分
(2)C++标准库是由类库和函数库组成的集合
(3)C++标准库中定义的类和对象都独立于std命名空间中(using namespace std;)
(4)C++标准库的头文件都不带.h后缀(#include <ostream>)
(5)C++标准库涵盖了C库的功能(#include<cstdlib>)
③ C++编译环境的组成:
注:#include<stdio.h>使用的是C语言兼容库函数;#include <cstdio>使用的是C++标准库的函数
④ C++标准库预定义了多数常用的数据结构
(1)<bitset>
(2)<bit>
(3)<deque>
(4)<stack>
(5)<list>
(6)<vector>
(7)<queue>
(8)<map>
(9)<cstdio>
(10)<cstring>
(11)<cstlib>
(12)<smath>
(13)<iostream>
18. C++中的字符串类
① C语言中的字符串
(1)C语言不支持真正意义上的字符串
(2)C语言用字符数组和一组函数实现字符串操作
(3)C语言不支持自定义类型,因此无法获得字符串类型
② C++中的字符串
(1)从C到C++的进化过程引入了自定义类型
(2)在C++中可以通过类完成字符串类型的定义
(3)C++语言直接支持C语言的所有概念
(4)C++语言中没有原生的字符串类型
③ C++标准库提供了string类型
(1)string直接支持字符串连接
(2)string直接支持字符串的大小比较
(3)string直接支持子串查找和提取
(4)string直接支持字符串的插入和替换
④ 字符串与数字的转换
(1)标准库中提供了相关的类对字符串和数字进行转换
(2)字符串流类(sstream)用于string的转换
* <sstream> -相关头文件
* istringstream - 字符串输入流
* ostringstream - 字符串输出流
(3)使用方法:string---->数字
istringstream iss("123.45"); double num; iss >> num;
(4)使用方法:数字---->string
ostringstream oss; oss << 543.21; string s = oss.str();
⑤ 字符串循环右移:
string operator>>(const string& s, unsigned int n) { string ret = ""; unsigned int pos = 0; n = n % s.length(); pos = s.length() - n; ret = s.substr(pos); ret += s.substr(0, pos); return ret; }
⑥ 字符串翻转:
19. 数组操作符[]重载
① string类最大限度的考虑了C字符串的兼容性;可以按照使用C字符串的方式使用string对象,str[2]
② 数组访问符是C/C++中的内置操作符,数组访问符的原生意义是数组访问和指针运算
a[n] <----> *(a + n) <----> *(n + a) <----> n[a]
③ 数组访问操作符重载([])
(1)只能通过类的成员函数重载
(2)重载函数能且仅能使用一个参数
(3)可以定义不同参数的多个重载函数
class Test { int a[5]; public: int& operator [] (int i) { return a[i]; } int length() { return 5; } };
20. 函数对象分析(函数调用操作符()重载)
① 函数对象(实现斐波那契数列)
(1)使用具体的类对象取代函数
(2)该类的对象具备函数调用的行为
(3)构造函数指定具体数列项的起始位置
(4)多个对象相互独立的求解数列项
② 函数调用操作符重载(())
(1)只能通过类的成员函数重载
(2)可以定义不同参数的多个重载函数
class Fib { int a0; int a1; public: Fib(): a0(0), a1(1) { } Fib(int n) { a0 = 0; a1 = 1; for(int i = 0; i < n; i++) { int temp = a1; a1 = a0 + a1; a0 = temp; } } int operator()() { int ret = a1; a1 = a0 + a1; a0 = ret; return ret; } };
21. 经典问题分析三
① 关于赋值操作符
(1)编译器为每个类默认重载了赋值操作符
(2)默认的赋值操作符仅完成浅拷贝
(3)当需要进行深拷贝时必须重载赋值操作符
(4)赋值操作符与拷贝构造函数有相同的存在意义
注:拷贝构造函数是对象在创建时、用另一个对象初始化该对象时调用;赋值操作符重载函数是用一个对象对另一个已经创建好的对象赋值时调用
② 编写赋值操作符重载函数注意事项
(1)赋值操作符重载函数返回值为对象的引用:a = b = c
(2)赋值操作符重载函数的参数为const对象的引用
(3)自拷贝判断:如果自己给自己赋值,则什么都不做
(4)注意判断是否需要释放之前申请的内存空间(深拷贝)
(5)返回自己:return *this;
Test& operator=(const Test& obj) { if(this != &obj) //To avoid self assign value { if(mPointer != NULL) { delete mPointer; } mPointer = new int(*obj.mPointer); } return *this; }
③ 编译器默认提供的函数
(1)无参构造函数
(2)拷贝构造函数
(3)赋值操作符重载函数
(4)析构函数
22. 智能指针(指针操作符*、->重载)
① 内存泄漏(臭名昭著的Bug)
(1)动态申请内存空间,用完后不归还
(2)C++语言中没有垃圾回收的机制
(3)指针无法控制所指堆空间的生命周期
② 需求
(1)需要一个特殊的指针
(2)指针声明周期结束时主动释放堆空间
(3)一片堆空间最多只能由一个指针标识
(4)杜绝指针运算和指针比较
③ 解决方案(智能指针)
(1)重载指针特征操作符(->和*)
(2)只能通过类的成员函数重载
(3)重载函数不能使用参数
(4)只能定义一个重载函数
④ 智能指针的使用军规:只能用来指向堆空间中的对象或者变量
23.逻辑操作符的陷阱
① 逻辑运算符的原生语义
(1)操作数只有两种值(true和false)
(2)逻辑表达式不用完全计算就能确定最终(短路规则)
(3)最终结果只能是true或者false
② 重载逻辑操作符,短路规则完全失效
(1)C++通过函数调用扩展操作符的功能
(2)进入函数体前必须完成所有参数的计算
(3)函数参数的计算次序是不定的
(4)短路法则完全失效
③ 逻辑操作符重载后无法完全实现原生语义
④ 一些有用的建议
(1)实际工程开发中避免重载逻辑操作符
(2)通过重载比较操作符代替逻辑操作符
(3)直接使用成员函数代替逻辑操作符
(4)使用全局函数对逻辑操作符进行重载
24. 逗号操作符分析
① 逗号操作符(,)可以构成逗号表达式
(1)逗号表达式用于将多个子表达式连接为一个表达式
(2)逗号表达式的值为最后一个子表达式的值
(3)逗号表达式中的前N-1个子表达式可以没有返回值
(4)逗号表达式按照从左向右的顺序计算每个子表达式的值
② 重载逗号操作符
(1)在C++中重载逗号操作符是合法的
(2)使用全局函数对逗号操作符进行重载
(3)重载函数的参数必须有一个是类类型
(4)重载函数的返回值类型必须是引用(可以作为左值使用)
Test& operator,(const Test& a, const Test& b) { return const_cast<Test&>(b); }
③ 重载逗号操作符问题本质分析
(1)C++通过函数调用扩展操作符的功能
(2)进入函数体前必须完成所有参数的计算
(3)函数参数的计算次序是不定的
(4)重载后无法严格从左向右计算表达式
④ 工程中不要重载逗号表达式
25. 前置操作符和后置操作符
① 下面代码的区别
i++; //i的值作为返回值,i自增1 ++i; //i自增1,i的值作为返回值
② 现代编译器对上述代码优化后,语义一样(会编译为同样的汇编代码)
(1)现代编译器产品会对代码进行优化
(2)优化后使得最终的二进制程序更加高效
(3)优化后的二级制程序丢失了C/C++的原生语音
(4)不可能从编译后的二进制程序还原C/C++程序
③ ++ 操作符重载
(1)全局函数和成员函数均可进行重载
(2)重载前置++操作符不需要额外的参数
(3)重载后置++操作符需要一个int类型的占位参数
④ 前置++操作符与后置++操作符的真正区别
(1)对于基础类型的变量:前置++的效率与后置++的效率基本相同,根据项目组编码规范进行选择
(2)对于类类型的变量:前置++的效率高于后置++,尽量使用前置++操作符提高程序效率
⑤ 示例
class Test { int mValue; public: Test(int i) { mValue = i; } int value() { return mValue; } Test& operator++()//++i { ++mValue; return *this; } Test operator++(int)//i++ { Test ret(mValue); mValue++; return ret; } };
26. 类型转换函数
① C语言中的隐式类型转换
(1)标准数据类型之间会进行隐式的类型安全转换(小类型默认转换为大类型)
(2)转换规则:(32位机器上)
② 转换构造函数(普通类型---->类类型)
(1)构造函数可以定义不同类型的参数
(2)参数满足下列条件时称为转换构造函数
* 有且仅有一个参数
* 参数是基本类型
* 参数是其他的类类型
explicit Test(int i) { mValue = i; }
③ 隐式类型转换(Bug重要来源)
Test t; t = 100;
(1)编译器会尽力尝试让源码通过编译,编译器尽力尝试的结果是隐式类型转换
(2)隐式类型转换会让程序以意想不到的方式进行工作,是工程中bug的重要来源
④ explicit关键字(explicit:)
(1)工程中通过explicit关键字杜绝编译器的转换尝试
(2)转换构造函数explicit修饰时只能进行显示类型转换
(3)显示类型转换方式
* static_cast<ClassName>(value) //C++推荐
* ClassName(value)
* (ClassName)value
⑤ 类型转换函数 (类类型---->普通类型)
(1)C++中可以定义类型转换函数
(2)类型转换函数用于将类对象转换为其他类型
(3)语法规则:
operator Type() { Type ret; ... return ret; }
⑥ 类型转换函数说明
(1)与转换构造函数具有同等的地位
(2)使得编译器有能力将对象转化为其它类型
(3)编译器能够隐式的使用类型转换函数
⑦ 编译器会尽力尝试让源码通过编译:隐式的尝试调用类型转换函数
Test t(1); int i = t;
⑧ 类类型之间的相互转换:类型转换函数和转换构造函数都可用于类类型之间的转换
(1)无法抑制隐式的类型转换函数调用
(2)类型转换函数可能与转换构造函数冲突(解决办法:用explicit抑制转换构造函数的隐式调用)
(3)工程中以Type toType()的共有成员函数代替类型转换函数(如QT中)
27. 继承
① 类之间存在的直接关联关系:组合关系和继承关系
② 组合关系:整体与部分的关系
③ 组合关系的特点
(1)将其他类的对象作为当前类的成员使用
(2)当前类的对象与成员对象的生命期相同
(3)成员对象在用法上与普通对象完全一致
④ 继承关系:父子关系
⑤ 面向对象中的继承指类之间的父子关系
(1)子类拥有父类的所有属性和行为
(2)子类就是一种特殊的父类
(3)子类对象可以当做父类对象使用
(4)子类中可以添加父类没有的方法和属性
⑥ C++中继承关系的描述
class Parent { int mv; public: void method(){}; }; class Child : public Parent { };
⑥ 重要规则
(1)子类就是一个特殊的父类
(2)子类对象可以直接初始化父类对象
(3)子类对象可以直接赋值给父类对象
(4)父类指针可以直接指向子类对象
(5)父类引用可以直接引用子类对象
⑦ 类在C++编译器内部可以理解为结构体,子类是由父类成员叠加子类新成员得到的
⑧ 继承的意义:
(1)继承是C++中代码复用的重要手段。
(2)通过继承,可以获得父类的所有功能,并且可以在子类中重写已有功能,或者添加新功能。
28. 继承中的访问级别
① 面向对象中的访问级别不只有public和private,也可以定义protected访问级别
② 关键字protected的意义:
(1)修饰的成员不能被外界直接访问
(2)修饰的成员可以被子类直接访问
③ C++中支持三种不同的继承方式
(1)public继承:父类成员在子类中保持原有访问级别
(2)private继承:父类成员在子类中变为私有成员
(3)protected继承:父类中的公有成员变为保护成员,其他成员保持不变
④ 继承成员的访问属性 = Max{继承方式,父类成员访问属性}
注:C++中的默认继承方式为private
⑤ 一般而言,C++工程项目中只使用public继承,C++的派生语言只支持一种继承方式(public),protected和private继承带来的复杂度远大于实用性
29. 继承中的构造和析构
① 子类对象的构造
(1)子类中可以定义构造函数
(2)子类构造函数必须对继承而来的成员进行初始化
* 直接通过初始化列表或者赋值的方式进行初始化
* 调用父类构造函数进行初始化
② 父类构造函数在子类中的调用方式
(1)默认调用:默认隐式调用无参构造函数或者使用默认参数的构造函数(无参构造函数与使用默认参数的构造函数可能会产生冲突)
(2)显式调用:通过初始化列表进行调用,适用于所有父类构造函数
③ 父类构造函数的调用:
class Parent { string mString; public: Parent(string s = "") { cout << "Parent(string s)" << endl; mString = s; } }; class Child : public Parent { public: Child() { cout << "Child()" << endl; } Child(string s) : Parent("Parent") { cout << "Child(string s = "")" << s << endl; } };
④ 构造规则
(1)子类对象在创建时会首先调用父类的构造函数
(2)先执行父类构造函数再执行子类的构造函数
(3)父类构造函数可以被隐式调用或者显式调用
⑤ 对象创建时构造函数的调用顺序
(1)调用父类的构造函数
(2)调用成员变量的构造函数
(3)调用类自身的构造函数
口诀心法:先父母,后客人,再自己。
⑥ 析构函数的调用顺序与构造函数相反
(1)执行自身的析构函数
(2)执行成员变量的析构函数
(3)执行父母的析构函数
注:在子类对象构造的时候需要调用父类构造函数对其继承得来的成员进行初始化;在子类对象析构的时候需要调用父类析构函数对其继承来的成员进行清理。
30. 父子间的冲突
① 父子类的同名成员变量
(1)子类可以定义父类中的同名成员
(2)子类中的成员将隐藏父类中的同名成员
(3)父类中的同名成员依然存在于子类中
(4)通过作用域分辨符(::)访问父类中的同名成员
② 访问父类中的同名成员
Child c; c.mi =100; c.Parent::mi = 1000;
③ 类中的成员函数可以进行重载
(1)重载函数的本质为多个不同的函数
(2)函数名和参数列表是唯一标识
(3)函数重载必须发生在同一个作用域中
④ 子类中的成员函数和父类中的成员函数(无法重载,作用域不同)
(1)子类中的函数将隐藏父类中的同名函数
(2)子类无法重载父类中的成员函数
(3)使用作用域分辨符访问父类中的同名函数
(4)子类可以定义父类中完全相同的成员函数
31. 同名覆盖引发的问题
① 子类对象可以当做父类对象使用(兼容性)
(1)子类对象可以直接赋值给父类对象
(2)子类对象可以直接初始化父类对象
(3)父类指针可以直接指向子类对象
(4)父类引用可以直接引用子类对象
② 当使用父类指针(引用)指向子类对象时
(1)子类对象退化为父类对象
(2)只能访问父类中定义的成员
(3)可以直接访问被子类覆盖的同名成员
③ 函数重写
(1)子类中可以重定义父类中已经存在的成员函数
(2)这种重定义发生在继承中,叫做函数重写
(3)函数重写是同名覆盖的一种情况(隐藏父类中的同名函数)
class Parent { public: int mI; virtual void add(int a, int b) { mI += (a + b); } }; class Child : public Parent { public: int mI; void add(int a, int b) { mI += (a + b); } };
④ 函数重写遇上赋值兼容性问题分析
(1)编译期间,编译器只能根据指针的类型判断所指向的对象
(2)根据赋值兼容,编译器认为父类指针指向的是父类对象
(3)因此,编译结果只可能是调用父类中定义的同名函数
32. 多态
① 函数重写
(1)在子类中定义与父类中原型相同的函数
(2)函数重写只发生在父类与子类之间
(3)父类中被重写的函数依然会继承给子类
(4)默认情况下子类中重写的函数将隐藏父类中的函数
(5)通过作用域分辨符::可以访问到父类中被隐藏的函数
② 面向对象中的多态:根据实际的对象类型决定函数调用语句的具体调用目标
(1)如果父类指针指向的是父类对象则调用父类中定义的函数
(2)如果父类指针指向的是子类对象则调用子类中定义的函数
③ C++中多态的支持
(1)C++中通过virtual关键字对多态进行支持
(2)使用virtual声明的函数被重写后即可展现多态特性(虚函数)
④ 多态的意义
(1)在程序运行过程中展现出动态的特性
(2)函数重写必须多态实现,否则没有意义
(3)多态是面向对象组件化程序设计的基础特性
⑤ 理论中的概念
(1)静态联编:在程序的编译期间就能确定具体的函数调用,如函数重载
(2)动态联编:在程序实际运行后才能确定具体的函数调用,如函数重写
33. C++对象模型分析
① class是一种特殊的struct
(1)在内存中class依旧可以看做变量的集合
(2)class与struct遵循相同的内存对齐规则
(3)class中的成员函数与成员变量是分开存放的
* 每个对象有独立的成员变量
* 所有对象共享类中的成员函数
② 运行时的对象退化为结构体的形式
(1)所有成员变量在内存中一次排布
(2)成员变量间可能存在内存空隙
(3)可以通过内存地址直接访问成员变量
(4)访问权限关键字在运行时失效(访问权限关键字仅在编译期间有效)
③ 类中的成员函数
(1)类中的成员函数位于代码段中
(2)调用成员函数时对象地址作为参数隐式传递(this指针)
(3)成员函数通过对象地址访问成员变量
(4)C++语法规则隐藏了对象地址的传递过程
④ 在C++编译器的内部类可以理解为结构体,子类是由父类成员叠加子类新成员得到的
⑤ C++中多态的实现原理
(1)当类中声明虚函数时,编译器会在类中生成一个虚函数表(存储于全局数据区,每个类只有一个虚函数表)
(2)虚函数表是一个存储类成员函数指针的数据结构
(3)虚函数表是由编译器自动生成与维护的
(4)virtual成员函数会被编译器放入虚函数表中
(5)存在虚函数时,每个对象中都有一个指向虚函数表的指针(VPTR)
注:父类对象和子类对象各有一个虚函数表
⑥ 编译器在用父类引用或指针调用函数时,会先判断该函数是否是虚函数
(1)如果是虚函数:编译器在对象VPTR所指向的虚函数表中查找函数
(2)如果不是虚函数:编译器直接可以确定被调用函数的地址
注:虚函数的调用效率要低于普通成员函数
⑦ VPTR指针存储于类对象的第一个位置
33. C++中的抽象类和接口
① 面向对象中的抽象类(不能创建对象,只能被继承)
(1)可用于表示现实世界中的抽象概念
(2)是一种只能定义类型,而不能产生对象的类
(3)只能被继承并重写相关函数
(4)直接特征是相关函数没有完整的实现
② Shape是现实世界中各种图形的抽象概念, 因此:
(1)程序中必须能够反映抽象的图形
(2)程序中通过抽象类表示图形的概念
(3)抽象类不能创建对象,只能用于继承
③ C++实现抽象类
(1)C++语言中没有抽象类的概念
(2)C++中通过纯虚函数实现抽象类
(3)纯虚函数是指只定义原型的成员函数
(4)一个C++类中存在纯虚函数就成为了抽象类
注:纯虚函数是指连函数体都不用定义的函数
④ 纯虚函数的语法规则:
class Shape { public: virtual double area() = 0; };
注:“=0”用于告诉编译器当前是声明纯虚函数,因此不需要定义函数体
⑤ 抽象类与纯虚函数
(1)抽象类只能用工作父类被继承
(2)子类必须实现纯虚函数的具体功能
(3)纯虚函数被实现后称为虚函数
(4)如果子类没有实现纯虚函数,则子类成为抽象类
⑥ 满足下面条件的C++类则称为接口(一种行为的规范,一组函数原型)
(1)类中没有定义任何的成员函数
(2)所有的成员函数都是公有的
(3)所有的成员函数都是纯虚函数
(4)接口是一种特殊的抽象类
注:C++中没有真正的接口,只有用纯虚函数模拟接口
34. 被遗弃的多重继承
① C++支持编写多重继承的代码
(1)一个子类可以拥有多个父类
(2)子类拥有所有父类的成员函数
(3)子类继承所有父类的成员函数
(4)子类对象可以当做任意父类对象使用
② 多重继承的语法规则
class Derived : public BaseA, public BaseB, public BaeeC { }
注:多重继承的本质与单继承相同
③ 多重继承的问题一:通过多重继承得到的对象可能拥有“不同的地址”
class Derived : public BaseA, public BaseB
Derived d(1, 2, 3); BaseA* pa = &d; BaseB* pb = &d; cout << "pa = " << pa << endl; cout << "pb = " << pb << endl;
注:以上代码pa和pb地址值不同
④ 多重继承的问题二:当多重继承关系出现闭合时将产生数据冗余问题
注:doctor对象将会拥有两份m_name和m_age
⑤ 数据冗余问题解决方案
(1)解决方案:虚继承
(2)续继承能够解决数据冗余问题
(3)中间层父类不再关心顶层父类的初始化(虚继承子类不会调用父类构造函数)
(4)最终子类必须直接调用顶层父类的构造函数
注:当架构设计中需要继承时,无法确定使用直接继承还是虚继承
⑥ 多重继承的问题三:多重继承可能产生多个虚函数表
Derived d; BaseA* pa = &d; BaseB* pb = &d; BaseB* pbb = (BaseB*)pa;
注:当需要对类进行强制类型转换、且类中有虚函数的时候,推荐使用dynamic_cast进行转换
⑦ 工程开发中多重继承方式:单继承某个类+实现(多个接口)
⑧ 一些有用的工程建议
(1)先继承自一个父类,然后实现多个接口
(2)父类中提供equal()成员函数
(3)equal()成员函数用于判断指针是否指向当前对象
(4)与多重继承相关的强制类型转换用dynamic_cast完成
35. 经典问题解析四
① new关键字和malloc函数的区别
(1)new关键字是C++的一部分;malloc是由C库提供的函数
(2)new以具体类型为单位进行内存分配;malloc以字节为单位进行内存分配
(3)new在申请内存空间是可进行初始化;malloc仅根据需要申请定量的内存空间
(4)new在所有C++编译器都被支持;malloc在某些系统开发中不能调用
(5)new能够触发构造函数的调用;malloc仅分配需要的内存空间
(6)对象的创建只能使用new;malloc不适合面向对象开发
② delete和free的区别
(1)delete在所有C++编译器中都被支持;free在某些系统开发中是不调用
(2)delete能够触发析构函数的调用;free仅归还之前分配的内存空间(free并不会调用析构函数)
(3)对象的销毁只能使用delete;free不适合面向对象开发
③ 关于虚函数
(1)构造函数不可能成为虚函数:在构造函数执行结束后,虚函数表指针才会被正确的初始化
(2)析构函数可以成为虚函数:建议在设计类时将析构函数声明为虚函数
④ 关于多态
(1)构造函数中不可能发生多态行为:在构造函数执行时,虚函数表指针未被正确初始化
(2)析构函数中不可能发生多态行为:在析构函数执行时,虚函数表指针已经被销毁
注:构造和析构函数中不能发生多态行为,只调用当前类中定义的版本。
④ 关于继承中的强制类型转换(dynamic_cast)
(1)dynamic_cast是与继承相关的类型转换关键字
(2)dynamic_cast要求相关的类中必须有虚函数
(3)用于有直接或者间接继承关系的指针(引用)之间
(4)编译器会检查dynamic_cast的使用是否正确
(5)类型转换的结果只可能在运行阶段才能得到
36. 函数模板
① 泛型编程:不考虑具体数据类型的编程模式
② C++中的泛型编程——函数模板
(1)语法规则
--template关键字用于声明开始进行泛型编程
--typename关键字用于声明泛指类型
template <typename T> void swap(T& a, T& b) { T t = a; a = b; b = t; }
(2)函数模板的应用
--自动类型推导调用:swap(a, b);
--具体类型显示调用:swap<int>(a, b);
③ 函数模板深入理解
(1)编译器并不是把函数模板处理成能够处理任意类型的函数
(2)编译器根据函数模板通过具体类型产生不同函数
(3)编译器会对函数模板进行两次编译
-- 在声明的地方对模板代码本身进行编译
-- 在调用的地方对参数替换后的代码进行编译
④ 函数模板注意事项:函数模板本身不允许隐式类型转换
(1)自动类型推导时,必须严格匹配
(2)显式类型推导时,可以进行默认类型转换
#include <iostream> #include <string> using namespace std; class Test { // Test(const Test&); public: Test() {} }; template <typename T> void Swap(T& a, T& b) { T t = a; a = b; b = t; } typedef void (*FuncI)(int&, int&); typedef void (*FuncD)(double&, double&); typedef void (*FuncT)(Test&, Test&); int main() { FuncI pi = Swap; FuncD pd = Swap<double>; // FuncT pt = Swap<Test>; cout << "pi = " << reinterpret_cast<void*>(pi) << endl; cout << "pd = " << reinterpret_cast<void*>(pd) << endl; // cout << "pt = " << reinterpret_cast<void*>(pt) << endl; return 0; }
⑤ 函数模板可以定义任意多个不同的类型参数
template <typename T1, typename T2, typename T3> T1 Add(T2 a, T3 b) { return static_cast<T1>(a + b); } int r = Add<int, float, double>(0.5, 0.7);
⑥ 多参数函数模板注意事项
(1)无法自动推导返回值类型
(2)可以从左向右部分指定类型参数
注:工程中将返回值参数作为第一个类型参数
int r1 = Add<int>(0.5, 0.7); int r2 = Add<int, float>(0.5, 0.7); int r3 = Add<int, float, double>(0.5, 0.7);
⑦ 函数模板可以像普通函数一样被重载
(1)C++编译器优先考虑普通函数
(2)如果函数模板可以产生一个更好的匹配,那么选择函数模板
(3)可以通过空模板实参列表的语法限定编译器只通过模板匹配
Max<>(a, b);
37. 类模板
① 类模板
(1)一些类主要用于存储和组织数据元素
(2)类中数据组织的方式和数据元素的具体类型无关
(3)如:数组类,链表类,Stack类,Queue类
② C++中的类模板:
(1)以相同的方式处理不同的类型
(2)在类声明前使用template进行标识
(3)<typename T>用于说明类中使用的泛指类型T
template<typename T>
class Operator
{
public:
T op(T a, T b);
};
③ 类模板的应用
(1)只能显式指定具体类型,无法自动推导
(2)使用具体类型<Type>定义对象
Operator<int> op1; Operator<double> op2;
int i = op1.op(1, 2);
Sting s = op2.op("Hello", "Kevin";)
(3)声明的泛指类型T可以用于声明成员变量和成员函数
(4)编译器对类模板的处理方式和函数模板相同(两次编译)
* 从类模板通过具体类型产生不同的类
* 在声明的地方对类模板代码本身进行编译
* 在使用的地方对参数替换后的代码进行编译
④ 类模板的工程应用
(1)类模板必须在头文件中定义
(2)类模板不能分实现在不同的文件中
(3)类模板外部定义的成员函数需要加上模板<>声明
⑤ 类模板可以定义任意多个不同的类型参数
template <typename T1, typename T2> class Test { public: void Add(T1 a, T2 b); }; Test<int, float> t;
⑥ 类模板可以被特化
(1)指定类模板的特定实现
(2)部分类型参数必须显示指定
(3)根据类型参数分开实现类模板
⑦ 类模板的特化类型
(1)部分特化:用特定规则约束类型参数
(2)完全特化:完全显式指定类型参数
template <typename T1, typename T2> class Test { public: void Add(T1 a, T2 b) { cout << "void Add(T1 a, T2 b)" << endl; cout << a + b << endl; } }; //部分特化 template <typename T> class Test <T, T> { public: void Add(T a, T b) { cout << a + b << endl; } }; //完全特化 template <> class Test <int, int> { public: void Add(int a, int b) { cout << a + b << endl; } }; // 关于指针的特化实现 template <typename T1, typename T2> class Test < T1*, T2* > { public: void add(T1* a, T2* b) { cout << *a + *b << endl; } };
⑧ 类模板特化注意事项
(1)特化只是模板的分开实现:本质上是同一个类模板
(2)特化类模板的使用方式是统一的:必须显示指定每一个类型参数
⑨ 重定义和特化不同
(1)重定义
* 一个类模板和一个新类(或者两个类模板)
* 使用时需要考虑如何选择的问题
(2)特化
* 以统一的方式使用类模板和特化类
* 编译器自动优先选择特化类
⑩ 函数模板只支持类型参数的完全特化
template <typename T> bool Equal(T a, T b) { return a == b; } template <> bool Equal(int a, int b) { return a == b; }
注:当需要重载函数模板是,优先考虑使用模板特化,当模板特化无法满足需求,再使用函数重载
38. 数组类模板
① 模板参数可以是数值型参数(非类型参数)
template<typename T, int N> void func() { T a[N]; //使用模板参数定义局部数组 }
② 数值型模板参数的限制
(1)变量不能作为模板参数
(2)浮点数不能作为模板参数
(3)类对象不能作为模板参数
本质:模板参数是在编译阶段被处理的单元,因此在编译阶段必须准确无误的唯一确定
③ 用高效的方法求1 + 2 + 3 ... + N
template <int N> class Sum { public: static const int VALUE = Sum<N-1>::VALUE + N;//递归定义 }; template <> class Sum < 1 > { public: static const int VALUE = 1; }; int main() { cout << Sum<100>::VALUE << endl; return 0; }
④ 数组类模板的实现:https://github.com/Kevin-l-wu/ArrayClass
39. 智能指针类模板
① 智能指针的意义
(1)现代C++开发库中最重要的类模板之一
(2)C++中自动内存管理的重要手段
(3)能够在很大程度上避开内存先关的问题
② STL中的只能指针auto_ptr
(1)生命周期结束时,销毁指向的内存空间
(2)不能指向堆数组,只能指向堆对象(变量)
(3)一片堆空间只能属于一个智能指针对象
(4)多个智能指针对象不能指向同一片堆空间(避免多次释放)
#include <iostream> #include <string> #include <memory> using namespace std; class Test { string mName; public: Test(const char* name) { cout << "Test(const char* name)" << endl; mName = name; } void Print() { cout << mName << endl; } ~Test() { cout << "~Test()" << endl; } }; int main() { auto_ptr<Test> pt(new Test("Kevin")); pt->Print(); cout << pt.get() << endl; auto_ptr<Test> pt1(pt); pt1->Print(); cout << pt.get() << endl; cout << pt1.get() << endl; return 0; }
注:当用pt初始化pt1的时候,pt的内部指针将会赋值为NULL
③ STL中的其他智能指针
(1)shared_ptr:带有引用计数机制,支持多个指针指向同一片内存
(2)weak_ptr:配合shared_ptr而引入的一种智能指针
(3)unique_ptr:一个指针对象指向一片内存空间,不能拷贝构造和赋值
④ Qt中的只能指针
(1)QPointer:
* 当其指向的对象被销毁时,他会被自动置空
* 析构时不会自动销毁所指向的对象
(2)QSharedPointer
* 引用计数型智能指针
* 可以被自由地拷贝和赋值
* 当引用计数为0时才删除指向的对象
(3)Qt中的其他智能指针
* QWeakPointer
* QScopedPointer
* QScopedArrayPointer
* QSharedDataPointer
* QExplicitlySharedDataPointer
⑤ 智能指针的实现:https://github.com/Kevin-l-wu/SmartPointer
40. 单例类模板
① 在架构设计时,某些类在整个系统生命期中最多只有一个对象存在(Single Instance)
② 要控制类的对象数目,必须对外隐藏构造函数
(1)将构造函数的访问属性设置为private
(2)定义instance并初始化为NULL
(3)当需要使用对象时,访问instance的值
* 空值:创建对象,并用instance标记
* 非空值:返回instance标记的对象
③ 单例类示例
class Singleton { static Singleton* mInstance; Singleton(const Singleton& obj); Singleton& operator=(const Singleton& obj); Singleton() { } public: static Singleton* GetInstance(); void Print() { cout << mInstance << endl; } }; Singleton* Singleton::mInstance = NULL; Singleton* Singleton::GetInstance() { if(mInstance == NULL) { mInstance = new Singleton(); } return mInstance; }
④ 单例类模板示例:https://github.com/Kevin-l-wu/Singleton
41. STL简介
① STL,即:Standard Template Library,是C++的一部分;STL是常用数据结构和算法的集合;STL的目标是标准化组件,提高开发效率和程序可靠性。
② STL库作为C++的一部分与编译器一同被发布,它主要由以下3个部分构成
(1)容器(Container):管理数据的集合
(2)算法(Algorithm):处理集合内部的元素
(3)迭代器(Iterator):遍历集合内的元素
③ Vector容器及迭代器使用
#include <iostream> #include <string> #include <cctype> #include <vector> #include <bitset> using namespace std; int main() { int* tP = nullptr; vector<int> vec1(4, 5); for(vector<int>::size_type index = 0; index < vec1.size(); index++) { cout << vec1[index] << endl; } vector<string> vec2(4, "c"); for(vector<string>::size_type index = 0; index < vec1.size(); index++) { cout << vec2[index] << endl; } vector<string> vec3{"Ni", "Hao"}; vec3.push_back("Hello"); vec3.push_back("World"); vec3.push_back("Kevin"); for(vector<string>::iterator iter = vec3.begin(); iter != vec3.end(); iter++) { cout << *iter << endl; } for(vector<string>::const_iterator iter = vec3.begin(); iter != vec3.end(); iter++) { cout << *iter << endl; } for(auto iter = vec3.begin(); iter != vec3.end(); iter++) { cout << *iter << endl; } vector<string>::iterator iter = vec3.begin() + (vec3.size() / 2); cout << *iter << endl; return 0; }
42. C语言中的异常处理
① 异常的概念
(1)程序在运行过程中可能产生异常
(2)异常(Exception)与Bug的区别
* 异常是程序运行过程中可能产生异常
* Bug是程序中的错误,是不被预期的运行方式
② 异常(Exception)与Bug的对比:
(1)异常
* 运行时产生除0操作
* 需要打开的文件不存在
* 数组访问时越界
(2)Bug
* 使用野指针
* 堆数组使用结束未释放
* 选择排序无法处理长度为0的数组
③ C语言中对异常的经典处理方式:if...else...
void func(...) { if(判断是否产生异常) { 正常情况代码逻辑; } else { 异常情况代码逻辑; } }
注:工程中一般if下面的为正常逻辑,else下面为异常逻辑
④ 通过setjump()和longjump()进行优化(这两个函数将破坏结构化程序设计的三大特性:顺序执行、分支执行、循环执行)
(1)int setjump(jmp_buf env):将当前上下文保存在jmp_buf结构体中(jmp_buf保存PC指针、栈指针、以及CPU所有工作寄存器)
(2)void longjmp(jmp_buf env, int val)
* 从jmp_buf结构体重恢复setjump()保存的上下文
* 最终从setjmp函数调用点返回,返回值为val
注:这种方式有点类似于进程切换
⑤ setjump()和longjump()使用示例
⑥ setjump()和longjump()引入的缺陷
(1)必然涉及到使用全局变量
(2)暴力跳转导致代码可读性降低
(3)本质还是if...else...异常处理方式
注:C语言中的经典异常处理方式会使得程序逻辑中混入大量的处理异常代码
43. C++中的异常处理
① C++内置了异常处理的语法元素try...catch...
(1)try语句处理正常代码逻辑
(2)catch语句处理异常情况
(3)try语句中的异常由对应的catch语句处理
try { double r = divide(1, 0); } catch(...) { cout << "Divided by zero..." << endl; }
② C++通过throw语句抛出异常信息
③ C++异常处理分析:throw抛出的异常必须被catch处理
(1)当前函数能够处理异常,程序继续往下执行
(2)当前函数无法处理异常,则函数停止执行,并返回
④ 未被处理的异常会顺着函数调用栈向上传播,知道被处理为止,否则程序将停止执行
⑤ 同一个try语句可以跟上多个catch语句
(1)catch语句可以定义具体处理的异常类型
(2)不同类型的异常由不同的catch语句负责处理
(3)try语句中可以抛出任何类型的异常
(4)catch(...)用于处理所有类型的异常
(5)任何异常都只能被捕获(catch)一次
⑥ 异常处理匹配规则
(1)异常抛出后,自上而下严格匹配每个catch语句处理的类型
(2)异常处理匹配时,不进行任何类型转换(类型必须严格匹配)
⑦ catch语句块中可以抛出异常:catch中抛出的异常需要外层的try...catch...捕获
(1)catch中捕获的异常可以被重新解释后抛出
(2)工程开发中使用这样的方式统一异常类型
⑧ 异常的类型可以是自定义类类型
(1)对于类类型异常的匹配依旧是自上而下严格匹配
(2)赋值兼容性原则在异常匹配中依然适用(父类对象可以catch到子类对象)
(3)一般而言
* 匹配子类异常的catch放在上部
* 匹配父类异常的catch放在下部
⑨ C++中的异常类
(1)在工程中会定义一系列的异常类
(2)每个类代表工程中可能出现的一种异常类型
(3)代码复用时可能需要重新解释不通的异常类
(4)在定义catch语句块时推荐使用引用作为参数(避免对象构造带来的开销)
⑩ C++标准库中提供了实用异常类族
(1)标准库中的异常都是从exception类派生的
(2)exception类有两个主要的分支
* logic_error:常用于程序中的可避免逻辑错误
* runtime_error:常用于程序中无法避免的恶性错误
⑪ C++标准库中的异常
//TODO
44. C++类型识别
① 在面向对象中可能出现下面的情况
(1)基类指针指向子类对象
(2)基类引用成为子类对象的别名
Base* p = new Derived(); Base& r = *p;
注:赋值兼容性原则
② 静态类型和动态类型的定义
(1)静态类型:变量(对象)自身的类型
(2)动态类型:指针(引用)所指向对象的实际类型
void Test(Base* b) { Derived* d = static_cast<Derived*> (b);/*危险的转换方式*/ }
注:基类指针是否可以强制类型转换为子类指针取决于动态类型
③ 解决方案--利用多态
(1)在基类中定义虚函数返回具体的类型信息
(2)所有的派生类都必须实现类型相关的虚函数
(3)每个类中的类型虚函数都需要不同的实现
④ 多态解决方案的缺陷
(1)必须从基类开始提供类型虚函数
(2)所有的派生类都必须重写类型虚函数
(3)每个派生类类型名必须唯一
⑤ C++提供了typeid关键字用于获取类型信息
(1)typeid关键字返回对应参数的类型信息
(2)typeid返回一个type_info类对象
(3)当typeid的参数为NULL时将抛出异常
int i = 0; const type_info& tiv = typeid(i); const type_info& tii = typeid(int); cout << (tiv == tii) << endl;
⑥ typeid的注意事项
(1)当参数为类型时:返回静态类型信息
(2)当参数为变量时:
* 不存在虚函数表--返回静态类型信息
* 存在虚函数表--返回动态类型信息
注:基类的析构函数声明为虚函数的意义,在释放父类指针时可以释放它所指向的子类对象
45. 经典问题解析五
① 编写程序判断变量是否为指针
(1)C++中仍然支持C语言中的可变参数
(2)C++编译器的匹配调用优先级
* 重载函数
* 函数模板
* 变参函数
(3)思路
* 将变量分为两类:指针和非指针
* 指针变量调用时返回true,非指针变量调用时返回false
template <typename T> bool IsPtr(T* ptr) //Match pointer { return true; } bool IsPtr(...) //Match non-pointer { return false; }
(4)存在缺陷:变参函数无法解析对象,可能造成程序崩溃
(5)优化:让编译器精确匹配函数但不进行实际调用
template <typename T> char IsPtr(T* ptr) //Match pointer { return 'p'; } int IsPtr(...) //Match non-pointer { return 0; } #define ISPTR(p) (sizeof(IsPtr(p)) == sizeof(char))
② 如果构造函数中抛出异常会发生什么情况
(1)构造函数中抛出异常
* 构造过程立即停止
* 当前对象无法生成
* 析构函数不会被调用
* 对象所占用的空间立即收回
注:由于在构造函数中抛异常仅仅是回收对象所占用空间,且不会调用析构函数,所以可能导致内存泄漏或者资源重复释放问题
(2)工程项目中的建议
* 不要在构造函数中抛出异常
* 当构造函数可能产生异常时,使用二阶构造模式
(3)析构函数中抛出异常,也可能导致对象所使用的资源无法完全释放
46. 令人迷惑的写法
① 下面的程序想要表达的意思
template <class T> class Test { public: Test(T t){} }; template <class T> void func(T a[], int len) { }
② 历史上的原因
(1)早起的C++直接复用class关键字来定义模板
(2)但是泛型编程针对的不只是类类型
(3)class关键字的复用使得代码出现二义性
③ typename诞生的直接诱因
(1)自定义类类型内部的嵌套类型
(2)不同类中的同一个标识符可能导致二义性
(3)编译器无法辨识标识符究竟是什么
④ typename的作用:
(1) 在模板定义中声明泛指类型
(2) 明确告诉编译器其后的标识符为类型
⑤ 下面程序想要表达什么意思
int func(int i) try { return i; } catch(...) { return -1; } int function(int i, int j) throw (int) { return (i + j) }
⑥ try...catch用法
(1)try...catch用于分割正常功能代码与异常处理代码
(2)try...catch可以直接将函数实现分隔为2部分
(3)函数声明和定义时可以直接指定可能抛出的异常类型
(4)异常声明称为函数的一部分可以提高代码可读性
⑦ 函数异常声明的注意事项
(1)函数异常声明是一种与编译器之间的契约
(2)函数声明异常后就只能抛出声明的异常类型
* 抛出其它异常将导致程序运行终止
* 可以直接通过异常声明定义无异常函数
int func(int i, int j) throw(int, char) { if( (0 < j) && (j < 10) ) { return (i + j); } else { throw '0'; } } void test(int i) try { cout << "func(i, i) = " << func(i, i) << endl; } catch(int i) { cout << "Exception: " << i << endl; } catch(...) { cout << "Exception..." << endl; }
47. 自定义内存管理
① 统计对象中某个成员变量的访问次数
class Test { mutable int mI; mutable int mCount; public: Test(int value = 0) { mI = value; mCount = 0; } int GetI() const { mCount++; return mI; } void SetI(int value) const { mCount++; mI = value; } int GetCount() const { return mCount; } };
② mutable关键字
(1)mutable是为了突破const函数的限制而设计的
(2)mutable成员变量将永远处于可改变的状态
(3)mutable在实际的项目开发中被严禁滥用
③ mutable的深入分析
(1)mutable成员变量破坏了只读对象的内部状态
(2)const成员函数保证只读对象的状态不变性
(3)mutable成员变量的出现无法保证状态不变性
④ new关键字创建出来的对象位于什么地方?
⑤ new/delete的本质是C++预定义的操作符
⑥ C++对这两个操作符做了严格的行为定义
(1)new
* 获取足够大的内存空间(默认为堆空间)
* 在获取的空间中调用构造函数创建对象
(2)delete:
* 调用析构函数销毁对象
* 归还对象所占用的空间(默认为堆空间)
⑦ 在C++中能够重载new/delete操作符
(1)全局重载(不推荐)
(2)局部重载(针对具体类进行重载)
注:重载new/delete的意义在于改变动态对象创建时的内存分配方式
⑧ 如何在指定的地址上创建C++对象
⑨ 解决方案
⑩ new[] / delete[] 与new / delete完全不同
48. 展望未来的学习之路
① 以上内容是经典C++语言
(1)经典是指
* C++98/03标准在实际工程中的常用特性
* 大多数企业的产品开发中需要使用的C++技能
② C++语言的学习需要重点在于以下几个方面
(1)C语言到C++的改进有哪些?
(2)面向对象的核心是什么?
(3)操作符重载的本质是什么?
(4)模板的核心意义是什么?
(5)异常处理的使用方式是什么?