C++笔记
- 基础语法
- 特别关键字
- const
- 程序运行期间不可以改变的值;
- const和*应该是从右往左读(反着读),把*号读成 pointer to
- 注
- 当且仅当只有一层间接关系(如指针指向基本类型)时,才可以将非const地址或者指针赋值给const指针;
- typedef
- 创建类型别名
- typedef double real;
- constexpr
- 编译时常量,告诉编译器这是一个常量,可以对它做优化(把这变量当成常量优化)
- 限制
- 常量表达式不能包含函数调用或者对象构造,因为这些是在运行时才能知道的;
- constexpr函数的返回值不能是void类型,不能用于声明变量或者新的类型;
- volatile
- 告诉编译器这是一个易变量,每次都需要重新从内存地址中读取这个数据;
- 例子:编译器不知道的方法,比如嵌入式汇编改变变量的值,或者有可能是硬件改变的值;
- 命名空间
- 命名空间的使用方法
- 一般用法
- 大型项目中,避免变量或者函数名的冲突
- 把需要限制的范围的代码用namespace name {}括起来,在命名空间以外使用命名空间的代码时,需要在代码面前添加name::(name是自己取的名字);
- 嵌套使用
- 命名空间别名
- 项目很大的时候,命名空间的name本身就可能出现名字冲突,为了避免冲突name就会变得很长,起个别名好用;
- namespce new_name = old_name
- using(编译指令)
- 文件中只会用到一个或者几个命名空间的变量,而且可以肯定不会有名称冲突,就可以使用using namespace xxx;
- 匿名命名空间
- 命名空间的内容只想在本文件中使用
- 编译器会给这个匿名命名空间一个唯一的名字,并且在本文件中使用using namespace,匿名命名空间和static的作用相似,但是看起来更简洁;
- 运算符
- 命名修饰
- C++编译器的一个技术,为了解决程序实体的名字必须唯一的问题;
- &
- ->
- .
- ::
- ""
- ''
- 类型转换
- 隐式类型转换
- 数值类型转换
- 提升型(不会丢信息,编译器不需要特殊处理)转换,编译器自动进行转换
- char short -> int
- float -> double
- 指针类型转换
- 空指针可以转换到任意指针类型;
- 任意指针类型都可以转换到void*指针;
- 派生类的指针可以转换到可访问的明确的基类指针,转换的同时不会改变const或者volatile属性;
- 注
- 负数转化为无符号类型会出错,(-1和无符号的2^32-1是一样的二进制);
- 浮点转化为整型会截断,如果转化时发生了溢出,可能会出现未定义的行为;(正浮点可能会四舍五入,负浮点可能直接截断;)
- 类型转换运算符(强制转换)
- dynamic_cast
- 运行时类型转换,动态转换
- 仅用于is-a类层级结构中进行向上转换,也就是把派生类指针类型转化为基类指针类型,否则会编译报错;
- p1 = dynamic_cast<Low *> ph;
- ph本来是派生类的指针,这里转化为基类Low的指针;
- const_cast
- 仅用于只改变值为const或者volatile时(增加或者解除const/volatile),否则会报错;
- 如果一个值是const,但是在有些情况下又要修改他,
- const_cast<type-name> (expression);
- static_cast
- 编译时类型转换,静态转换
- 仅当type_name可被隐式转换为experssion的所属类型,或者反过来成立时,才合法,否则报错;
- static_cast<type-name> (expression);
- int a = 1; static_cast<float> (a);
- reinterpret_cast
- dynamic_cast和static_const的区别
- 动态和静态
- 使用
- dynamic_cast
- static_const
- 安全性
- dynamic_cast更安全
- static_cast自己保证安全
- C++类型转换的方法
- 十进制转二进制
- std::bitset<64> binary(opcode)
- to_string()
- stoi()
- 数据类型
- size_t
- 是一种无符号整数类型,通常用于表示内存中对象的大小或数组的索引。优点是不同平台的兼容性。
- 编程规范
- 代码风格
- 命名
- C++文件以.cpp为扩展名,头文件以.h为扩展名
- C++文件名和类名保持一致
- 驼峰命名风格
- 大驼峰
- 命名空间,类类型,结构体类型,联合体类 型,枚举类型,typedef定义的类型,类型别 名;
- 函数(包括全局函数,作用域内函数,成员函 数)
- 带 'g_' 前缀的小驼峰
- 全局变量(包括全局、文件、namespace域 下的变量以及相应作用域下的静态变量)
- 小驼峰
- 类成员变量
- 局部变量,函数参数,宏参数,结构体和联合 体中的成员变量
- 全大写,下划线分割
- 全大写,下划线分割
- 格式
- 每个变量单独一行进行声明或赋值
- 语句
- 选择、循环语句使用大括号
- 单行选择语句需要加大括号
- 单行循环语句需要加大括号
- 空循环体需要加大括号
- 避免将if/else/else if写在同一行
- 输入输出
- 输入输出接口
- 缓冲区
- 刷新缓冲区
- 控制符
- flush
- endl
- 控制符也是函数,可以使用:flush(cout);
- 格式化输入输出
- cout
- 进制显示控制符
- dec:十进制
- hex:十六进制
- oct:八进制
- 使用
- cout << dec;
- 一旦使用,对后面执行的所有的输出都有效;(哪怕是别的函数内的cout输出,只要是在之后执行的都有效)
- 调整字段宽度
- cout.width()
- width()方法只影响接下来显示的一个项目,然后字段宽度就会恢复默认值;
- 注:
- C++永远不会截断数据,如果在字段宽度2内打印一个7位值,C++将增加宽字段;C++认为显示所有的数据比保持数据格式整洁更重要;
- 填充字符
- 默认情况下,使用空格填充字段中未被使用的部分,使用fill()改变;
- cout.fill('*');
- 一直有效,直到下次修改;
- 设置浮点数的显示精度
- 浮点数精度的含义在不同的模式下不一样,默认模式下指显示的总位数,定点模式和科学模式指小数点后面的位数;默认是6,小数末尾的0不显示;
- cout.precision();
- 四舍五入
- 一直有效,直到下次修改;
- 打印末尾的0和小数点
- cout.setf(ios_base::showpoint);
- 一直有效,直到下次修改;
- C++为setf()的使用,提供了很多方便的标准控制符;
- 头文件iomanip
- 提供了几个可以像控制符一样使用的函数;
- setprecision()
- setfill()
- setw()
- cin
- 使用
- cin >> value_holder;
- value_holder可以是变量,引用,被解除引用的指针,类,结构的成员;
- 可以和dec, hex, oct控制符一起使用;
- cin>>如何检查输入
- 几种:
- 跳过空白字符(空格,换行符,制表符),直到遇到非空白的字符
- 从非空白字符开始,到与目标类型不匹配的第一个字符之间的全部内容;
- 注
- 使用cin键入的时候注意类型匹配,不要给一个int型输入一个double的值,否则cin会把整数部分赋值给int,小数部分会留在输入流中,会对后面的输入带来影响;
- int a; cin >> a; 输入-123z。-123赋值给a,z会被留在输入流中;
- 流状态
- cin和cout对象有流状态,由三个标志给定流状态;
- 非格式化输入函数
- 只是读取字符输入,不会跳过空白,也不会进行数据类型转换;
- 单字符输入
- get()
- 读取下一个字符,即使该字符是空格,制表符,换行符;
- get(char &)
- cin.get(ch)
- 返回值是指向istream的引用
- 遇到文件末尾的返回值:转化为false
- get(void)
- ch = cin.get()
- 返回值的类型是int(或者更大的整型)
- 遇到文件末尾的返回值:EOF
- while ((ch = cin.get()) != EOF)
- 改变分界符
- 用哪个?
- 如果希望跳过空白字符
- 如果不希望跳过空白字符
- get(char &)
- get(void):只是替换C的getchar()方便;
- 字符串输入
- getline()
- 不段读取直到遇到指定符号,舍弃指定符号,默认是"\n";
- 改变分界符
- getline(char *, int , char)
- get()
- ignore()
- 舍弃一行;
- cin.ignore(读取的最大字符数目,分界符)
- 注:
- getline会舍弃"\n",但是这个舍弃的"\n"会被留在缓冲区,下一个getline()会读取这个"\n"直接结束;可以使用get()把这个留在缓存区的换行符读取掉;
- 把一个东东放回到输入流中
- 数据结构
- 数组
- &a(假设a是数组名)
- 代表整个数组的地址,如果&a++,直接会到整个数组后面的那个地址;
- &a[0](假设a是数组名)
- 代表数组a第一个元素的地址,如果a++,就是a[1]的地址;
- 指针
- 指针(包括数组名)加1
- 实际上是加了一个与指针指向的类型长度(以字节为单位)相等的值
- 两个恒等式
- arr[i] == *(arr + i)
- &arr[i] == arr + i
- 结构
- 赋值
- 结构变量的行为更接近于基本的单值变量,将其数据组合成单个实体活数据对象,该实体被视为一个整体;
- 结构名只是结构的名称,要获得地址,必须使用地址运算符;
- 函数
- 使用函数
- 函数定义
- 函数原型(声明)
- 描述了函数的接口:传递给函数的值的数目和类型以及函数的返回类型,也就是函数的输入输出;
- C++中函数原型是必不可少的;
- 调用函数
- 函数返回值
- 返回值类型的限制
- 不能是数组(字符串本质也是数组),但是可以其他任务类型,包括结构和对象
- 不能直接返回数组,但是可以将数组作为结构或者对象的组成本部分返回;
- 函数参数
- 函数参数的自动转化
- C++自动将传递的值转化为原型中指定的类型,条件是可以的话(比如都是算术类型),double转int编译可能会报警告,或者错误;
- C中,如果原型是int,返回一个double,那么C就只检查double的前16位(假设改架构下int占16位),不会有提示,会导致未知的错误;
- 形参(parameter)
- 用于接收传递过来值的变量
- 也就是接收的,也就是函数声明和定义里的
- 实参(argument)
- 传递给函数的值
- 也就是往过传的,也就是调用函数地方的。
- 二维数组做函数参数的情况
- C/C++里,二维数组的排布还是一维数组;
- 一定要指定第二维的维数,或者指针的指针
- 函数的传参
- 默认参数
- 因为C++实际不允许没有函数原型,所以其实默认参数只能再函数原型里设置
- 在函数定义中设置默认参数,则函数定义一定要在函数调用之前;
- 当函数声明中已经设置了默认参数时,不允许在定义中再设置;
- 函数原型中,默认参数的右边不允许出现非默认参数,就是说默认参数是从右往左写;
- 在调用时,如果其中一个参数不想采用默认参数,其左边的参数都必须显式给与一个值;否则不知道那个是默认;
- 可变参数
- 所有实参类型相同:使用initializer_list标准库
- void error_msg(initializer_list<string> msg);
- error_msg({"123", str1, str2})(花括号不能去掉)
- 所有实参类型不同:可变参数模板
- void foo(int dwFixedArg, ...) {//省略符 va_list pArgs = NULL; va_start(pArgs, dwFixedArg); int dwVarArg = va_arg(pArgs, int); int dwVarArg2 = va_arg(pArgs, char); va_end(pArgs); }
- 注
- 函数名后面()的内容
- 如果是空和void等效(C不是,C必须指明是void)
- 如果是“...”代表是不指出参数,通过,仅当与接受可变参数的C函数(如printf())交互是才使用;
- 函数和数组
- int arr[],int *arr
- 当且仅当用于函数头或函数原型中两者的含义才是相同的
- 函数和结构
- 递归
- C允许main()函数调用自己
- C++不允许main()函数调用自己
- 函数指针
- 函数的地址
- 使用
- 获取函数的地址
- 函数名(后面不跟()不跟参数)就是函数的地址,如think()是一个函数,该函数的地址就是think;
- 声明一个函数指针
- 复制要指向的函数的原型,然后把函数名字替换成(*pt)就OK;
- double (*pf) (int)
- (*pf)是一个函数,pf是一个函数指针,只能指向该类型函数的函数指针;
- *pf(int)
- (*pf)(int)
- 声明之后,就可以把pf当作一般的指针变量,进行赋值和修改;
- 使用函数指针来调用函数
- (*pf)就是函数名
- C++也允许直接使用pf做函数名,和上面等价;
- 引用变量
- 就是给变量起个别名,引用之后,你就是我,我就是你;功能上就像是一个const指针,本质上引用就是通过const指针实现的;
- 引用和指针的区别
- 指针是一个变量,需要分配内存空间,而引用是一个别名,它是原变量的一个别名,没有自己的内存地址(引用的内存地址就是他引用的变量的),不需要分配内存空间。
- 用处
- 原因
- 能够修改调用函数中的对象
- 传递引用,而不是整个数据对象,运行速度快;
- return
- 返回值
- 会先把值给到一个临时位置,然后在复制到接收的变量;
- 返回引用
- 注
- 应避免返回函数终止时不再存在的内存单元引用;
- 避免的方法
- 返回一个作为参数传递给函数的引用
- 用new分配新的存储空间
- 注:
- 必须在声明引用变量时进行初始化;
- 引用更接近const指针,一旦成为某个变量的引用,就不能变,意思是不能再是别的变量的引用;
- 不要返回指向局部变量或临时对象的引用,因为函数执行完毕后,局部变量和临时对象将消失,引用会指向不存在的数据;
- 函数重载
- 功能相同,处理不同类型对象的函数;
- 函数名字一样,但是函数特征标不一样;
- 函数特征标:参数列表的数目和类型一致,则函数特征标一致;
- 只看特征标,返回类型可以一致,也可以不一致;
- 类型引用和类型本身视为同一个特征标
- 使用
- 重载匹配
- 没有匹配的原型并不会停止,而是会自动的接着尝试使用标准类型强制转换匹配,前提是使用标准类型强制转换匹配时只有一个可以匹配的;
- 函数重载匹配的顺序
- 实参形参类型相同,数组类型和指针类型之间的转换;
- const转换实现的匹配
- 通过类型提升实现的匹配(如:float到double)
- 算数类型转换或者指针转换实现的匹配
- 类类型转换实现的匹配
- 函数模板
- 使用
- 模板:对多个不同类型的数据使用同一种算法
- 实例
- template <typename T> // or class T void Swap(T &a , T &b) { T temp; temp = a; a = b; b = temp; }
- 函数模板的参数不是必须都是模板参数类型,如Swap(T &a, T &b, int n)
- 模板重载
- 模板的局限性
- 编写的模板函数很可能无法处理某些类型,比如对数组,结构比大小,做乘积;
- 注:
- 函数模板只是一个用于生成函数定义的方案,不是函数定义,编译器使用模板为特定的类型生成函数定义时,得到的时模板的实例;
- 函数模板也可以看成一种特殊的函数重载,模板会自动的完成重载函数的过程;
- 面向对象特性
- 类&对象
- 面向对象的三大特征
- 封装性
- 把细节封装到一个对象中,对外部不可见,使用时不用在乎细节,方便简洁效率更高;
- 继承性
- 多态性
- 有些地方是四大特性,还有一个抽象;
- 类:
- 类是一种用户定义类型的工具,将数据表示和操纵数据的方法组合成一个整洁的包;
- 类是同一类对象的抽象,对象是类的某一个特定实体。类是抽象的橘子(比如就是橘子这俩字),对象是拿在你手上的这一个实际的橘子;
- 类成员的访问控制
- 权限类型
- private
- protected
- 作用
- 对成员函数进行访问控制,可以让派生类访问类外访问不到的成员函数;
- 对于基类的派生类才有意义
- 对于类外而言,protected和private一样;
- 对于派生类而言,protected和public一样;
- 注
- 对于成员数据最好采用私有控制,即便是派生类也用基类的方法访问;
- public
- 成员默认的访问控制权限是private
- 友元函数或者友元类可以访问类的保护成员和私有成员
- friend 类名或者函数名声明;
- 友元是单向的;
- 尽量不要使用友元,使用友元就意味着破坏了类的封装性,需要重新设计类;
- 类的实例化
- 栈实例化
- 堆实例化
- ClassName* 对象名 = new ClassName;
- 区别
- 对象的存储方式不一样,一种是自动存储,一种动态存储;
- 注
- 定义位于类声明的中的函数都将自动成为内联函数;(就是在类中声明函数的时候,顺便定义),类声明外的需要加inline(最终到底会不会内联是编译器决定的);
- C++试图让用户定义的类型尽可能与标准类型类似,因此可以声明对象,指向对象的指针,对象数组,可以按值传递,对象之间赋值;
- 类的构造与析构
- 构造函数
- 作用
- 为了实现使用类对象就像使用标准类型一样,所以默认加了一个初始化函数,也就是构造函数;
- 用于构造对象的特殊函数,在对象被创建时自动被调用以初始化对象;
- 名称
- 默认构造函数
- 未定义构造函数时,编译器会自动生成不带参数的默认版本,只有一个函数头,函数体是空的,也就是说啥也不初始化;
- 两种方式
- 定义一个所有参数都是默认参数的构造函数;
- 在设计类的时候,通常应该提供对所有类成员做隐式初始化的默认构造函数;
- 使用函数重载,定义一个默认构造函数(所有参数都默认的构造函数);
- 注:以上两个的区别就是,一个是使用默认参数,一个是在函数内部给所有参数赋值默认值;
- 注
- 使用构造函数给一个已经存在的对象赋新值
- 构造函数会构造一个临时的对象,然后把这个对象赋值给已经存在的对象,需要注意的是,赋值之后这个临时对象会被调用析构函数删除;
- 执行构造函数时先执行其初始化列表,再执行函数体;
- 一个类的对象的成员的初始化,是按照类中声明的顺序初始化的,和初始化列表的顺序无关;
- 如果使用初始化列表,就一定要注意变量声明的顺序,是不是有依赖;
- 想要调用无参数的构造函数的时候,在实例化时候,不要加(),不然会让编译器认为是声明,(声明一个返回值为该类的函数);
- 构造函数没有声明类型;
- 无法使用对象来调用构造函数,因为在构造函数构造出对象之前,对象还不存在;
- 构造函数和其他函数一样,可以被重载,可以被委托;
- 委托:利用别的函数的功能,实现我的功能;
- 允许一个构造函数的定义中使用另一个构造函数;
- 构造函数中使用new是需要注意的
- 构造使用new,析构一定要有对应的delete,并且new,new[]和delete,delete []对应;
- 如果有多个构造函数必须使用同一种new,因为只有一个析构函数;(delete,delete []都可以作用于空指针;)
- 对不是new创建的指针使用delete,结果是未知的,可能是有害的;
- 析构函数
- 作用
- 用于完成对象销毁前的清理工作,如释放内存,关闭文件等;
- 实际上,如果构造函数使用的new来分配内存,那么析构函数使用delete来释放这些内存,否则析构函数就啥也不干(有作用只是函数内部啥也不干);
- 名称
- 未定义析构函数,编译器会自动生成默认版本,函数体为空;
- 析构函数什么时候被调用
- 注:
- 析构函数也没有返回值和声明类型,析构函数没有参数;
- 关键字
- default
- 要求编译器生成默认构造函数
- 当我们自己写了一个构造函数之后,那么编译器就不会自动生成默认的无参构造函数,如果你还想生成,就使用这个关键字
- Student() = default;
- explicit
- 拒绝对象被隐士构造
- 只能修饰类的构造函数,不能被继承(构造函数就不能被继承);
- delete
- 删除部分构造函数避免对象不符合预期的被构造
- 避免传参是的某些隐式的类型转换;
- student(char id) = delete:当有人调动student的时候传入的参数是char类型,则不被允许,C++允许类型的向上提升,也就是说定义student(int id)可以被student(id) id是char类型的调用,delete可以避免这种情况的发生;
- 复制构造函数
- 用处
- 特殊的构造函数,在创建对象时,用已有的对象来初始化新对象的构造函数;注意那个新对象还不存在。
- 何时调用
- 新建一个对象并将初始化为同类现有对象时,复制构造函数都会被调用;
- 用一个已经存在的对象来初始化一个新的不存在的对象;
- 按值将对象传递给函数;
- 函数按值返回对象;
- 编译器生成临时对象;
- 定义复制构造函数的时候,函数参数应该是const的,如果未定义,编译器会自动生成默认的按比特位进行复制的复制构造函数,即浅拷贝;
- 如果不希望对象被复制构造,可使用delete关键字拒绝;
- 使用默认复制构造函数可能的问题
- 如果要计数对象创建的次数,默认的复制构造函数内部并没有,不会计数;
- 默认复制构造函数是浅复制,可能会遇到浅复制了一个数组,数组占用内存被释放,那这里的这个浅复制的数据是未知的;
- 赋值运算符有同样的问题;
- 类的其他特性
- this指针
- 指向自己的指针:指向当前对象本身,与对象的首地址相同;
- 隐含于类的每一个非静态成员函数参数列表中(包括构造和析构函数);静态成员函数没有对象的概念,和对象不绑定,相当于就是一个名称空间是所属类的静态函数;
- *this
- this指向的对象的地址,*this是指针的解引用,也就是对象的别名;
- 对象数组
- 声明
- 声明的时候直接初始化(调用构造函数)
- 不显示调用构造函数就都使用默认的;
- 可以对不同的元素使用不同的构造函数;
- 初始化对象数组的方案
- 首先使用默认构造函数创建数组元素,然后用花括号中的构造函数(你自己显示调用的构造函数)创建一个临时对象,然后赋值给数组元素;(赋值之后会调用析构函数删除临时对象)
- 因此,要创建类对象数组,那这个类必须有默认构造函数;(用户怎么使用类是不知道的,所有设计类时,一定要有默认构造函数)
- 运算符重载
- 和函数的重载一样,给一个已经存在的运算符重载一个新的含义;
- 实现
- 用一个函数实现重载运算符的新功能,函数名字是:operator运算符(),例如:operator+();
- 限制
- 重载后的运算符必须至少有操作数是用户定义的类型;(防止用户为标准类型重载运算符)
- 重载运算符不能违反原来的句法规则(如:操作数的个数,优先级)
- 不能创造新运算符;
- 不能重载规定的一些运算符;
- 必须使用成员函数进行重载的运算符
- 重载<<
- 一般,要重载<<来显示c_name的对象,使用友元函数
- 使用友元的原因:如果使用成员函数重载<<使用起来会非常迷惑,比如cout的使用: xxx << cout;
- ostream & operator<<(ostream & os, const c_name & obj) { os << ...; return os; }
- 重载函数运算符
- 类的继承
- 派生类的构造函数
- 默认情况下派生类需要定义自己的构造函数,不会继承基类的构造函数;
- 派生类构造函数的执行顺序
- 基类构造函数,派生类初始化列表中其余项,构造函数体;
- 析构函数相反
- 派生类构造函数应通过成员初始化列表将基类的信息传递给基类构造函数
- 初始化列表要显式的指明要使用的基类构造函数,否则将使用默认的基类构造函数;
- Classic::Classic(char* c1, char* s1, char* s2, int n, double x): Cd(s1, s2, n, x)
- 派生类的析构函数
- 派生类不会继承基类的析构函数,可以自己定义
- 派生类不需要显示的调用基类的析构函数,编译器会隐式调用;
- 派生类和基类的特殊关系
- 派生类对象都是基类对象,所以可以对基类对象进行的任何操作,都适用于派生类对象;
- 基类指针可以在不进行显示类型转换的情况下指向派生类对象,引用也是一样的;
- 可以将派生类对象赋值给基类对象,但是只会赋值基类描述的数据成员;(这种情况是实际是会默认重载赋值运算符,也需要使用强制转换)
- 基类的虚函数会被派生类继承;
- 注
- 以上描述的单向的
- 基类指针指向派生类指针是隐式转换,但是派生类指针指向基类指针是需要强制类型转换的。
- C++继承
-
-
-
-
-
-
-
- 如果继承的两个基类中有同名的成员,那么派生类中访问就需要加作用域限定符;
- C++继承类型
- is-a(子集关系)
- has-a(并集关系)
- 实现has-a的两种方式
- 两种方式
- 包含
- 正常用就行,就是一个类里包含了其他的类,比如你自己定义了一个类,类里声明了string的数据成员;
- 私有继承
- 通过私有继承,基类的所有成员都是派生类的私有成员。也就意味这个类的派生类用不了基类的东西,这个类的实例化对象也用不了基类的东西。
- 私有继承的派生类怎么访问基类的对象和友元
强制类型转换
-
-
-
-
-
-
-
- 保护继承
- 私有继承的变体
- 私有继承,第三代类中放不能使用基类的接口,但是保护继承的可以;
- 区别?
- 主要的一个区别就是,包含的成员是有名称的,私有继承的成员是没有名称的(直接用类名);
- 所以无论是声明(私有继承就没声明),初始化,调用都不一样,包含用的是成员名称,私有继承用的是作用域解析符;
- 用谁?
- 优先使用包含
- 包含使用更简单,并且会避免继承可能引起的问题;
- 包含可以使用多个同类的子对象,私有继承只能一个
- 使用私有继承
如果新类需要访问原有类的方法保护成员,或者需要重新定义虚函数
-
-
-
-
-
-
-
- 注
- 包含就是再一个类里使用另一个类,没有继承的关系,所以不能访问原有类的保护成员,也无法重新定义原有类的虚函数;
- using:重新定义访问权限
- 多重继承
- 主要的问题
- 从两个不同的基类继承同名的方法
- 从两个或更多的相关基类哪里继承同一个类的多个实例
- 虚基类
- 使得从多个类(条件是基类相同)派生出的对象只继承一个基类对象;
- 注
- 使用类限定符解决名称二义性的问题;
- 使用虚基类来避免继承多个基类对象的问题,但是使用虚基类之后,就需要为编写构造函数初始化列表已经解决二义性问题引入新的规则;
- 无必要不使用
- 类的多态
- 多态
- 同一个接口可以有多种形态;通过派生类重写基类虚函数实现。
- 多态和函数重载的区别
- 类的多态,是派生类重写了基类一个接口一摸一样的函数(名称和参数列表的个数类型都一样),函数重载是函数名和实现功能一致,但是函数列表不一样;
- 多态的实现:虚函数
- 虚函数的实现:在基类的成员函数最前面加 virtual关键字;
- 虚函数作用
- 虚函数是实现动态绑定的基础;虚函数经过派生类的重写之后就可以实现多态;
- 如果不是虚函数,程序将根据指针的类型或者引用的类型选择方法;也就是程序将根据引用和指针的类型选择方法;
- 如果是虚函数,程序将根据指针指向的对象类型或者引用本身来选择方法;也就是程序将根据对象的类型来选择方法;
- 惯例
- 如果在派生类中重新定义基类的方法,将基类的方法声明为虚的;
- 基类的析构函数应该是虚函数;
- 为了确保释放派生类是正确的顺序调用析构函数,想想虚函数的作用;
- 主要原因是为了实现多态性,即在基类指针指向派生类对象时,能够正确地调用派生类的析构函数,从而释放派生类对象的资源。(比如,把一个派生类赋值给一个基类的指针,delete这个基类指针,静态绑定下就只会调用基类的析构函数,不会调用派生类的,导致派生类的空间没有被释放,造成资源泄露;)
- 实际上只要是个类,就声明析构函数是虚函数,就算不发生上面的描述,效率也更高;
- 注
- 多态通过运行时动态绑定实现,在程序运行期间将一个函数标识符和函数地址绑定在一起;如果不是虚函数,那绑定是静态的;
- 如果在派生类中重新定义了基类的方法,那么会隐藏基类中所有同名(相同函数名,无论参数一样不一样)的方法;
- 如果基类中函数有重写,派生类要重新定义,就得重新定义所有的重写函数,因为一但重新定义一个,其他所有的都会被隐藏;(如果其余函数没有重写的需求,可以直接调用基类函数)
- 虚函数使用的注意
- 虚函数不允许是静态成员函数和构造函数
- 静态成员函数没有和对象绑定,无法确定对象的地址;
- 构造函数本身就是需要明确的知道构造的是什么,没必要多态;
- 友元不能是虚函数
- 因为友元都不是类成员,只有类成员才能是虚函数,如果有设计问题,可以用友元调用虚函数解决;
- override
- 派生类重写基类虚函数时,可以添加这个关键字,告诉编译器这个函数是对基类虚函数的重写,方便编译器检查,也可以不写;
- final
- 如果不希望基类的某个函数被重写,可使用这个关键字明确拒绝;
- 虚函数和动态绑定
- 虚表
- 如果某个类声明了虚函数,则该类及其所有直接,间接派生类,都各自有一张虚表;
- 虚表里会有当前类的各个虚函数(新增的,继承的都有)的入口地址(就是函数text段对应的地址)
- 编译器遇到虚函数调用时,产生虚表指针+偏移量的指令和确定函数地址,实现动态绑定;
- 注
- 是每个类型都有一个虚表,每个对象有一个指向虚表的指针;
- 每个类型的虚表里只有虚函数的地址(重写之后的情况见下方的内存结构布局)(本质是个偏移,地址=虚表指针+偏移);
- 对象虚表指针赋值在基类的构造函数执行之后, 也就是发生在当前类的构造函数初始化列表执行前,其基类构造函数执行之后;
- 禁止在父类的构造函数和析构函数内调用虚函数;
- 父类构造内,派生类虚表没有完全生成,父类析构内,派生类的虚表已经被释放了;也就说构造和析构内虚表要不没生成完毕,要不被释放了,不能知道准确的函数地址;实际的编码规范其余不允许在构造和析构函数内调用虚函数,无论是基类还是派生类。
- 虚表的内存结构布局
- 一般继承
- 虚函数按照其声明顺序放于表中,且基类在派生类前面。
- 一般继承且存在虚函数覆盖(就是派生类重写虚函数)
- 覆盖的虚函数将被放到虚表中原来父类虚函数的位置,没有被覆盖的函数按之前的顺序保存,最后是子类新加的虚函数;
- 多重继承(无虚函数覆盖)
- 每个父类都有自己的虚表,且子类的虚函数被放到了第一个父类的表的后面;
- 比如说,C继承了B1,B2,B3。那么C的虚表里存着B1,B2,B3的虚表,C的虚函数在B1虚表的后面;
- 多重继承(有虚函数覆盖)
- 父类虚表中对应的虚函数地址将被子类的虚函数地址覆盖,子类新加的虚函数地址将被添加到第一个父类的虚函数表之后。
- 注:
- 总的说,就是按照声明顺序排,子类在第一个父类的后面,有覆盖那就子类的那个重写的覆盖父类对应的位置。
- 虚表的第一项(或第二项)是什么?:父类或者子类的第一个或者第二个函数地址,到底是父类的还是子类的和子类重不重写父类虚函数决定;
- RTTI(运行时类型识别)
- 是一种在运行时确定对象类型的机制。它允许程序在运行时查询一个对象的类型信息,以便进行动态类型转换和类型安全的操作。
- 两个部分
- typeid
- typeid运算符用于获取一个对象的类型信息,它返回一个type_info对象,该对象包含了有关类型的信息,如类型名称、类型大小等
- typeid(expression)
- expression是一个对象或表达式,可以是任何类型的对象或指针。
- dynamic_cast
- 仅用于is-a类层级结构中进行向上转换,也就是把派生类指针类型转化为基类指针类型,否则会编译报错;
- dynamic_cast<type*>(expression)
- type是目标类型,expression是一个指向基类对象的指针或引用。
- p1 = dynamic_cast<Low *> ph;
- ph本来是派生类的指针,这里转化为基类Low的指针;
- 注:
- 如果继承树中没有虚函数存在,dynamic_cast无法通过编译,typeid运算符会给出静态绑定的结果;
- C++ RTTI机制提供了一种方便的方式来处理多态类型的对象,但是它也会带来一些性能上的开销,因此在实际应用中需要谨慎使用。
- 编译器会默认在虚表中生成一个type_info的指针,指向该类的信息,-fno-rtti可以关闭;
- 抽象类
- 纯虚函数
- 基类无需定义,只给一个声明的虚函数,具体的定义交给派生类;
- 析构函数不能是纯虚函数,因为析构函数必须要做事情;
- 抽象类
- 抽象类用于代码抽象和设计,约束不同的具体实现类对外出现相同的接口,可以理解为实现整个程序就给几个高度抽象的接口;抽象类的所有派生类必须至少支持其指定的功能;
- 抽象类至少有一个纯虚函数;
- 抽象类无法实例化,因为这个类里面的这个纯虚函数什么都没有实现,实例化无意义;
- 抽象类要求其派生类必须重写其纯虚函数;
- 类模板
- 类的模板和函数的模板类似;
- 数组类模板
- 类模板可以使用模板参数来提供常规数组的大小;
- 优点
- 执行速度快(没有new deleted的内存分配和释放)
- 缺点
- 每个数组都会生成自己的模板
- 使用不够灵活
- 不能将一个尺寸的数组赋值给另一个尺寸的数组;
- 不能创建允许数组大小可变的类;
- 模板的多功能
- 常规类的技术可以用到模板类
- 可以递归使用
- 就是嵌套的使用,比如ArrayTP<ArrayTp<int, 5>, 10> twodee;
- 可以使用多个类型参数
- template <class T1, class T2>
- 可以使用默认模板参数
- template <class T1, class T2 = int>
- 模板类的具体化
- C++允许很多类型的实例化,但是常用的就是隐式实例化,我们实例化容器,类模板,函数模板常用的那种方式就是隐式实例化;
- 成员模板
- 注
- 模板只是编译器指令,告诉编译器有这个东西,遇到的时候该怎么编译,并且模板不是函数,不能单独编译。(一般放到头文件里)
- 如果要使用指针栈,调用的时候应该提供一个指针数组;
- 类和动态内存分配
- 构造函数使用成员初始化列表
- 在给类似string,vector赋值的时候可以直接使用复制构造函数进行赋值,否则(以string举例)就是先调用string的构造函数,然后在调用string的赋值运算符进行赋值;
- 注
- 初始化和赋值的区别:初始化是创建新对象,赋值是修改已经存在的对象;
- 类的静态成员和常量成员
- 静态成员:static; 常量成员:const;
- 静态成员变量
- 类的静态成员变量被所有对象共享,具有静态生存期,且不占用某个对象的内存;可以看做就是一个在类里声明的静态全局变量;
- 不能在类声明中初始化静态成员变量,因为声明只是描述了如何分配内存,但并不分配内存;
- 如果静态成员是const整数类型或者枚举型,则可以在类内声明的时候初始化;
- 静态成员函数
- 静态成员函数只能访问静态成员变量,静态成员函数可以被直接调用,也可以实例化之后通过对象调用;静态成员函数没有对象的概念,和对象不绑定,相当于就是一个名称空间是所属类的静态函数;
- 就是一个名称空间是所属类的静态函数,就是一个声明定义在一个类里的静态函数;
- 常量成员只能通过类内初始值,或者初始化列表完成初始化;
- const成员函数
- 关键字const放到函数声明的最后面;
- 只要类方法不修改调用对象,就应该声明为const;
- 存储
- 创建的每个新对象都有自己的存储空间,同一个类的所有对象共用同一组类方法,类变量是各自分别存储;
- 也就是方法和静态变量所有对象共用,变量各自有各自的;
- 友元
- 友元函数
- 友元类
- 定义
- 不是is-a,也不是has-a,还有关系可以用友元类实现,比如:电视机和遥控器;
- 作用
- 友元类的所有方法都可以访问原始类的私有和保护成员;
- 使用
- 在本来的类中声明原始类的友元类,在私有还是共有还是保护里声明无所谓;
- 友元成员函数
- 作用
- 进一步约束访问限制,只有友元成员函数可以访问原始类的私有和保护成员;
- 使用
- 因为循环依赖,编译器无法正确编译,所以需要向前声明;
- 向前声明
- 就是一种规定的格式,为了解决友元成员函数你依赖我,我依赖你的问题;
- 其他友元
- 相互是友元类,比如:电视机和遥控器实现双线的交互;
- 一个函数同时是两个类的友元;
- 嵌套类
- 注
- 友元是单向的,如果想双向,就要双向声明为对方的友元;
- 类的设计
- 编译器生成的成员函数
- 默认构造函数
- 默认析构函数
- 复制构造函数/拷贝构造函数
- 赋值运算符
- 对于派生类的基类部分,C++使用基类的赋值运算符,如果派生类什么成员数据都没新增,可以不用重新定义赋值运算符,直接用基类的就行;
- 可以把派生类赋值给基类,只能赋值基类描述的成员数据,反过来不一定可以;
- 地址运算符
- 注:
- 只有当实际使用这些函数的时候,编译器才会去定义它们;
- 拷贝构造函数和赋值运算符的区别
- 类方法
- 转换
- 按值传递对象还是传递引用
- 返回对象还是返回引用
- 如果返回在函数中创建的临时对象,返回对象,否则都返回引用;
- 友元函数
- 派生类的友元函数使用基类的友元
- 通过强制类型转换将派生类引用或者指针转换为基类引用或者指针,然后使用转换后的引用或者指针调用基类的友元;(P432)
- 几个常见函数的编码
- 复制构造函数
- 实现和构造函数差不多,只不过传入的参数是类的引用;
- 赋值运算符的重载
- 判断传入的引用是不是自己,不是自己就清空自己,然后把传入的类赋值给自己,返回自己就OK;
- <<的重载
- 类声明这个函数是友元,传入两个参数,一个是ostream的引用,一个是类的引用,编写输出格式返回ostream引用就行;
- 派生类可以调用基类的<<的重载,把派生类指针或者引用强制转化为基类就行;
- 文件操作
- 文件读取
- 步骤
- 包含头文件fstream
- 创建一个ifstream对象
- 将该对象和一个文件关联
- 判断是不是打开成功
- if (!inFile.is_open()) { cout << "Could not open the file\n" << filename << endl; cout << "Program terminating.\n"; exit(0); }
- 使用该对象继续做处理
- 文件写入
- 步骤
- 包含头文件fstream
- 创建一个ofstream对象
- 将该对象和一个文件关联
- 像使用cout那样使用ofstream
- 写入和读取也可以使用
- 缓冲区
- 文件的输入和输出都会利用缓冲区,创建一个文件读取或者写入的对象时,也会创建一个由该对象管理的缓冲区;
- 流状态检查
- 打开多个文件
- 如果需要同时打开两个文件,就必须为每个文件创建一个流;
- 如果时依次打开多个文件,可以创建一个流,分别用于每个文件;创建的时候不要初始化,然后依次和文件关联;
- 文件模式
- C++
- ios_base::in
- ios_base::out
- ios_base::ate
- ios_base::app
- ios_base::trunc
- ios_base::binary
- 使用
- 多个模式用"|"分隔;
- r
- w
- ios_base::out
- ios_base::out | ios_base::trunc
- a
- ios_base::out | ios_base::app
- r+
- ios_base::in | ios_base::out
- w+
- ios_base::in | ios_base::out | ios_base::trunc
- 二进制读取
- ios_base::in | ios_base::binary
- c
- C++的命令行处理
- 使用main函数
- int main(int argc, char *argv[])
- argc:参数的个数,包括命令自己;
- argv[]:指针数组;
- getopt
- 命令行处理函数
- 函数原型
- #include <unistd.h>
- int getopt(int argc, char * const argv[], const char *optstring);
- argc和argv,就是main函数的参数
- optstring是一个字符串,每一个字符都是一个参数的名字,每一个字符用 : 隔开(不是必须的)
- 返回值
- 返回下一个选项的字符,如果没有更多的选项了,就返回-1
- 输入参数
- getopt函数,处理argv[]内存储的每一个参数,已字符串的形式存储在参数optarg中(每个参数都有一个optarg)
- 注
- 最终实现命令行参数解析:while循环 + getopt() + switch;
- strtoul()
- 字符串函数,用于把字符串转化为无符号长整型
- 函数原型
- unsigned long int strtoul(const char* str, char** endptr, int base);
- str:需要转换的字符串
- endptr:指向一个指针,用于存储转换后未被转换的字符串部分的指针。通常都是nullptr
- base:转换的进制,默认是十进制
- ELF文件相关
- open()
- 函数原型
- #include <fcntl.h>
- int open(const char *pathname, int flags);
- 参数解释
- pathname:文件路径
- flags文件的打开方式和权限
- O_RDONLY
- O_WRONLY
- O_RDWR
- O_CREAT
- O_EXCL
- 与O_CREAT一起使用,如果文件已经存在,则open()函数失败
- O_TRUNC
- O_APPEND
- 注:
- 权限:使用数字表示,不是必须
- 返回值
- 返回一个类型是int的文件描述符,如果出错,返回-1
- 可以使用文件描述符进行读写操作
- 注:
- Elf对象
- #include <libelf.h>
- elf_version()
- elf_begin()
- 打开ELF文件
- 函数原型
- Elf* elf_begin(int fildes, Elf_Cmd cmd, Elf *ref)
- 参数解释
- fides:int型的文件描述符,比如open()的返回值
- cmd:打开方式
- ref:elf的描述符,设置为nullptr就行
- 返回值
- elf_kind()
- 获取Elf对象的类型,如ELF_K_ELF表示可执行文件
- elf_getphdrnum()
- int elf_getphdrnum(Elf *elf, size_t *dst)
- 获取Elf对象中程序头表的数量
- gelf_getehdr()
- 获取Elf对象的头部信息
- 返回值
- GELF_Ehdr的结构体(包含Elf文件的基本信息,文件类型,入口地址,程序头表偏移量,节头表偏移量等)
- 入口地址:->e_entry
- elf_getscn()
- 获取Elf对象中指定节(section)的信息
- 函数原型
- Elf_Scn *elf_getscn(Elf *elf, size_t index);
- 参数解释
- elf:指向Elf对象的指针
- index:要获取的节的索引
- 返回值
- elf_getshdr()
- 获取Elf对象中指定节头表的信息
- elf32_getshdr()
- Elf32_Shdr *elf32_getshdr(Elf_Scn *scn)
- elf64_getshdr()
- Elf64_Shdr *elf64_getshdr(Elf_Scn *scn)
- elf_getdata()
- 用于从 ELF 文件中获取指定段或节的数据
- 函数原型
- Elf_Data *elf_getdata(Elf_Scn *scn, Elf_Data *data)
- 参数解释
- scn:指向Elf_Scn结构体的指针,也就是elf_getscn()的返回值;
- data:通常设置为nullptr
- 返回值
- 返回Elf_Data结构体的指针,错误返回空指针;
- elf_getshdrstrndx()
- 获取 ELF 文件中节头字符串表的索引
- 函数原型
- int elf_getshdrstrndx(Elf *elf, size_t *dst)
- 返回值
- elf_getphdr()
- elf_getshdrnum()
- elf_strptr()
- elf_end()
- elf_errno()
- 内存模型和名称空间
- 内存模型
- 头文件
- 头文件常包含的内容
- 函数原型
- 使用#define或const定义的符号常量
- C++会对包含该头文件的所有源代码文件里都copy一份常量定义;
- 结构声明
- 类声明
- 模板声明
- 内联函数
- 函数内不内联是编译器决定的,哪怕你告诉编译器这个函数内联,也有可能不内联,所以不确定的情况下,不要把函数定义写到头文件中;
- #include 头文件
- <>
- 编译器首先在存储标准头文件和主机系统的文件系统查找,找不到才在当前目录或源代码目录找;
- “”
- 编译器首先在当前的工作目录或源代码目录,找不到才会去其他地方找(见上面);
- 存储,作用域,链接性
- 存储类型
- 自动存储
- 函数内部定义的常规(非static和多线程)变量;
- 静态存储
- 动态存储
- 动态的进行内存的分配和释放,比如new和deletem,使用自由存储空间或堆进行存储;
- 为什么要动态分配内存
- 因为要合理的使用内存,避免浪费,比如用一个char类型的数组存人名,如果最长的人名有2000个字符?
- 线程存储(C++11)
- 变量声明使用thread_local,在整个线程内都存在;
- 作用域
- 链接性
- 描述了名称如何在不同单元之间共享;
- 自动变量的名称没有链接性,因为他们不能被共享;
- 静态持续变量(和静态存储一个概念)
- 链接性
- 外部链接性
- 可在其他文件中访问
- 在函数外部定义的变量
- 使用
- 在每个使用外部变量的文件中都必须声明,使用关键字extern
- 单定义:只能在一个文件中有该变量的定义;
- 内部链接性
- 无链接性
- 只能在函数内部或者代码段中访问
- 静态局部变量:只会在程序启动时初始化一次;
- 注
- 局部变量可能隐藏同名的全局变量,代码段内的局部变量也可能隐藏之外的同名变量,一个文件内的静态变量也会隐藏常规的外部变量;
- 静态变量的数目在程序运行期间不变,所以不需要特殊机制来存储,分配固定内存存储就行;
- 说明符
- auto(C++11)
- register
- static
- extern(引用声明)
- 声明引用在其他地方定义的变量,就是表明这变量是在别的地方定义好的;(函数也可以,不过一般不用)
- thread_local(C++11)
- 表明变量的持续性与其所属的线程持续性相同;
- 只有这个说明符可以和其他说明符组合用,其他只能单独使用;
- mutable
- 如果结构或者类变量为const,mutable标记的那个变量也是可以被修改的;
- 限定符
- const(不变的)
- 变量初始化之后,不能进行修改;
- C++中
- 对于全局变量的链接性而言,const的效果和static是一样的,即如果给一个外部链接的全局变量加const,那这个变量的链接性将变为内部链接;
- volatile(易变的)
- 表明即使程序代码没有对内存单元做修改,其值也可能发生改变(硬件改的), 告诉编译器每次读取这个变量的值,都去内存里去读;
- 函数和链接性
- 所有函数的存储连续性都自动是静态的,默认是链接性外部的,如果加static,那就变为内部的(static原型和定义都得加)
- c++在哪找函数
- 如果是内部的,就只在本文件中找;
- 如果是外部的,首先在所有的程序文件中查找,找不到再从库中查找;
- 如果定义了一个与库函数同名的函数,编译器将使用程序员定义的版本,但是C++保留了标准库函数名称,程序员不能用;
- 名称空间
- 功能
- 允许定义一个可在其中声明标识符的命名区域
- 就是可以自己定义一个区域,然后在这个区域内定义名称,区域内和区域外的名称没关系;默认会有一个全局名称空间;
- 作用
- 特性
- 可以把名称添加到已有的名称空间中;
- 名称空间可以嵌套;
- using编译指令是可传递的;
- using namespace name1::name2
- using
- using声明
- using编译指令
- 使所有的名称可用
- using namespace xx;
- 规范
- 不要在头文件使用using编译指令;
- 如果非要使用using编译指令,应该在所有的预处理编译指令#include之后;
- 导入名称时,首选使用作用域解析运算符或者using声明;
- 对于using声明,首选将其作用域设置为局部,而不是全局;
- 程序组织策略
- 异常
- throw try catch的使用
- try
- throw
- catch
- 可以将对象做异常处理;
- 标准异常类
- exception
- logic_error:理论上可以通过读取代码来检测异常
- domain_error:当使用了一个无效的数字域时
- invalid_argument:当使用了一个无效的参数时
- length_error:创建了太长的std::string
- out_of_range:std::vector等数据格式越界
- bad_alloc:通过new抛出
- bad_cast:通dynamic_cast抛出
- bad_typeid:通过typeid抛出
- bad_exception:无法预期的异常
- runtime_error:理论上不可以通过读取代码来检测异常
- range_error:尝试存储超出范围的值时
- overfloat_error:当发生数字上溢时
- underflow:当发生数字下溢时
- 也可以自己定义异常类,通过继承exception;
- 栈展开
- 执行throw类似执行返回语句,程序沿函数调用序列后退,直到找到try块,然后执行try块代码,然年执行对应的catch进行异常处理;
- 1、首先检查异常发生的位置是否在当前函数的某个try块内,如果在的话,执行第2步,如果不在,沿着函数调用栈重复做第1步;
- 2、找到对应的catch时,使用RTTI(运行时类型检查)查看这个异常类型是否和catch块中的捕获类型相同, 若相同,则执行3,若不相同,重复执行1和2;
- 3、处理异常,执行栈回退,释放try块中的局部变量;
- 注
- 使用异常时可能会导致,出发异常之后直接跳到catch执行,没有释放内存,导致内存泄露;
- RAII(资源获取即初始化)
- 一种思想,用对象管理资源,把构造和析构的过程封装在对象里,在栈展开的过程中,封装了资源的对象会被自动调用其析构函数以释放资源;
- 应该尽量避免使用异常;
- 库
- STL(standard template library)
- 一些知识点
- 泛型编程
- 关注的是算法,编写独立于数据类型的代码,STL就是一种泛型编程;
- 容器
- 算法
- 所有的算法都是半开区间
- 好处是对空空间不用做特殊处理,开始和结束直接就是相等。
- 迭代器
- 对象的集合,对象可以是一个容器,也可以是容器的一部分。
- 广义指针,可以是指针,也可以是一个可以对其进行类似指针的操作;有这个东西方便统一接口(指针能处理的类,和不能处理的类);借助迭代器,算法只需要写一遍,任意容器都可以使用(如果使用有意义的话)。
- 操作
- operator*
- operator++,operator--(大部分支持)
- ++pos, pos++:++pos效率更高,pos++内部需要构建一个临时对象。
- operator== !=
- operator=
- 分类
- 操作
- 都支持的操作
- 范围
- begin(),end()
- cbegin()和begin()的区别
- 返回的迭代器不同,begin()返回的是一个普通迭代器,cbegin()返回的是const迭代器,不能通过const迭代器修改容器中的元素,也就是说,如果不修改容器的值,就尽量使用cbegin();
- 大小
- 添加,删除(array不适用)
- c.insert(args)
- c.emplace(inits)
- 使用inits构造c中的一个元素
- 可以直接在容器中构造一个新的元素,而不需要先创建一个临时对象,然后再将其插入(复制或者移动)到容器中
- c.erase(args)
- c.erase(删除位置的迭代器,个数)
- 从指定位置删除指定个数元素,默认个数不同的结构不一样;
- c.erase(b迭代器,e迭代器)
- c.clear()
- 只支持顺序容器
- 添加元素
- push_back(t)/emplace_back(args)
- 尾部添加,返回void(forward_list不支持)
- push_front(t)/emplace_front(args)
- 头部添加,返回void(vector, string不支持)
- insert(p, t)/emplace(p, args)
- 位置p之前插入,返回指向新添加元素迭代器;(forward_list有自己版本)
- insert(p, il)
- 位置p之前插入,il(il是初始化列表),返回新插入的第一个元素的迭代器;
- 删除元素
- pop_back()
- 删除尾部,容器为空,函数行为未定义,删除之前一定要判断;
- pop_front()
- 删除头部,容器为空,函数行为未定义,删除之前一定要判断;
- erase(p)
- 删除p,返回指向p之后元素的迭代器;(forward_list有自己版本)
- erase(b, e)
- 访问元素
- 下标访问
- 只适用于string, vector, deque, array
- back()
- front()
- 注
- 引用就是变量本身,可以直接通过修改引用,修改容器内的元素;
- reverse()
- #include <algorithm>
- reverse(str.begin(), str.end())
- 顺序容器
- 容器适配器
- stack (栈)
- push():将元素压入栈顶。
- pop():弹出栈顶元素。
- top():返回栈顶元素。
- swap():交换两个栈的内容。
- operator=():将一个栈赋值给另一个栈。
- queue(队列)
- push():将元素插入队列的末尾。
- pop():将队列的第一个元素弹出。
- front()
- back()
- priority_queue(优先队列)
- std::string
- 输入
- C风格字符串的输入(char info[100])
- cin >> info;
- cin.getline(info, 100)
- cin.get(info, 100)
- string的输入(string stuff)
- cin >> stuff
- getline(cin, stuff)
- replace():替换指定位置的字符或一段字符。
- reserve()
- s.substr(pos, n)
- 返回string类型字符串,包含从pos(就是数字,不是迭代器)开始的n个字符
- find(args, index)
- 在s中查找args的第一次出现
- index:从下标几开始查。
- rfind(args)
- array
- 固定大小数组,不能添加,删除元素;
- 实例
- array<string, 5> coll;
- 元素个数也是array类型的一部分,也就是说也元素个数不同的两个array类型也不同。
- 赋值所有元素为0
- valarray(数值数组)
- sum():返回所有元素的总和
- max():返回最大的元素
- min():返回最小的元素
- vector(动态数组)
- 可变大小数组,在尾部之外的位置插入,删除元素可能会很慢;
- #include<vector>
- 查找
- 自己遍历
- find(begin, end, val),返回一个迭代器;
- 头
- 尾
- 添加
- push_bask(x)
- 建议使用
- emplace_back(x)
- 直接在容器的内存中原地构造一个对象,无需先构造临时对象。避免了拷贝或移动操作。
- 插入
- insert(rets.begin() + index,x)
- 删除
- 删除末尾
- 指定位置
- erase(rets.begin() + index)
- deque(双端队列)
- 支持在队列两端进行插入和删除操作
- push_front():在队列头部插入一个元素。
- push_back():在队列尾部插入一个元素。
- pop_front():删除队列头部的元素。
- pop_back():删除队列尾部的元素。
- front():返回队列头部的元素。
- back():返回队列尾部的元素。
- 注
- deque是一种动态数组,因此它支持随机访问,可以使用下标操作符[]来访问队列中的元素。
- deque支持迭代器,可以使用迭代器来遍历队列中的元素。
- list(双向链表)
- push_back():在链表尾部插入一个元素
- push_front():在链表头部插入一个元素
- pop_back():删除链表尾部的元素
- pop_front():删除链表头部的元素
- front():返回链表头部的元素
- back():返回链表尾部的元素
- reverse():将链表反转
- sort():对链表进行排序
- 注:
- list中的元素是动态分配的,因此在使用完毕后需要手动释放内存。
- 高效使用
- emplace版本比push版本高效,少了一次拷贝构造;
- push构建一个元素,然后拷贝到容器中;
- 直接在容器来构建一个元素;
- 使用vector时,如果明确知道需要多大的空间,可以提前使用reserve函数进行预留;
- vector<T> vec; vec.reserve(n); // 预留 n 个元素的空间
- 迭代器失效
- 如果循环的条件使用了迭代器,循环内部还删除了迭代器,就会导致迭代器不存在,出现未知的错误,因为erase()返回的是删除元素下一个元素的迭代器,可以使用这个规避;
- 关联容器
- 有序(按关键字)
- 关键字不重复
- map
- <key, value>
- 遍历
- 使用auto方便
- for (auto it = myMap.begin(); it != myMap.end(); ++it)
- set (有序唯一集合)
- 基于红黑树实现的关联容器,它可以自动对元素进行排序,并且保证元素的唯一性;
- find():查找set中是否存在某个元素。
- 关键字重复
- multimap (关键字可重复的map)
- multiset (关键字可重复的set)
- 无序
- unordered_map (键唯一的无序字典)
- unordered_set(元素唯一的无序哈希集合)
- unordered_multimap(键可重复的无序字典)
- unordered_multiset
- 操作
- 插入
- insert(value)
- insert(p)
- insert(b, e)
- 删除
- srase(k)
- srase(p)
- srase(b, e)
- 访问
- find(k)
- 返回一个迭代器,指向第一个关键字k,若k不在容器中,则返回尾后迭代器;
- count(k)
- 返回关键字等于k的元素的数量;是关键字的数量,不是键对应的值的数量;
- lower_bound(k)
- upper_bound(k)
- equal_range(k)
- 返回一个迭代器pair,表示关键字等于k的元素范围;
- 下标
- 仅支持unordered_map和map
- c[k]
- c.at(k)
- 注:
- 无序容器比有序容器的性能好,因为不维护有序;
- 无序容器,可以自己定义hash函数和==运算符;
- pair(模板类)
- #include <utility>
- 用于存储两个不同类型的对象
- others
- 字符数组
- #include <cstring>
- strcpy()
- strlen()
- strcmp()
- strcmp(first, second)
- 0:相同
- 大于0:first大
- 小于0:second大
- 一个字符,一个字符的比,只要有不一样的,就会给个结果
- strcat()
- 极值
- 无穷
- int的极值
- #include <climits>
- 最大值
- 最小值
- 常用函数
- 类型转换
- to_string()
- stoi()
- 字符转字符串
- string(1, c)
- string temp; temp += c;
- 数学
- abs()
- max()
- min()
- fmax()
- fmin()
- 搜索和排序
- #include <algorithm>
- sort()
- sort(begin, end, comp)
- begin,end:开始和结束的迭代器;
- comp:比较函数,默认是升序,降序得使用比较函数,自己定义,直接用库定义好的都可以(greater<int>());
- 可以对数组,二维数组,链表,向量排序;
- 对二维数组排序,默认是以数组的第一个元素为准排序,不想默认可以在比较函数里写;
- find()
- reverse()
- C++11
- 声明
- using=(模板别名)
- 和typedef的功能一样,只是typedef不能用于模板部分具体化
- typedef std::vector<std::string>::iterator itType
- using itType = std::vector<std::string>::iterator
- nullptr
- 类型推导
- auto(自动类型推断)
- 能够让编译器在编译阶段自动推导出变量的类型;
- 注
- 自动类型推断只能用于单值初始化,不能用于初始化列表;
- 使用auto类型的变量必须立即初始化,否则编译器无从推导;
- auto a = b; b本身是const int的话,这里auto会抛弃const;
- 常用
- decltype
- 将变量的类型声明为表达式指定的类型;
- decltype(x) y; :把x声明为y的类型;
- 注
- 基于范围的for循环
- 右值引用
- 左值和右值
- 左值
- 在作用域内,表达式运行结束后依然存在的持久对象,有确切的地址能够在程序中被访问;
- 右值
- 表达式结束后就不再存在的临时对象
- 纯右值
- 没有地址可供访问,比如字面量,求值结果相当于字面量,匿名临时变量,非引用返回的临时变量等;
- 将亡值
- 有地址但是仅编译器能够操作,程序不可访问,即将被销毁,能够被移动的值,也可以被复用(可以被传递,比如 return a);
- 注:
- 右值无法在等号的左边,因为没有程序可以访问的地址;
- 右值引用和左值引用
- 左值引用
- 右值引用
- std::move()
- 注:
- 右值引用不能引用左值;
- 常量的左值引用能够延长临时变量的生命周期,就是说可以把一个临时变量赋值给一个const的左值引用;(临时变量本来是右值,但是赋值给常量左值引用的时候就是左值了)
- 不只是临时变量,右值都可以赋值给常量的左值引用,也就是常量左值引用可以引用右值;
- 右值引用能够延长临时变量的生命周期,就是说可以把一个临时变量赋值给一个右值引用;(右值引用是左值)
- 右值引用是对右值的引用,但是引用本身是左值,也就是说右值的引用是左值;
- 移动语义
- 移动语义是通过右值引用实现的,也就是右值引用的作用;
- 移动构造和移动赋值
- 通过定义移动构造函数和移动赋值函数避免无意义的拷贝操作;也就是减少临时变量构建的过程;
- 移动构造函数
- 将某个临时对象的资源交给另一个正在创建的新对象;比如用返回值构造一个新的对象;
- 移动赋值函数
- 注
- 拷贝需要分配内存,然后拷贝内存,资源的移动说白了就是指针的赋值;
- 完美转发
- 右值无法转发(引用坍缩)
- 右值的引用是左值,传递的时候可能会出现,你传递的是右值,接收方接收的确实右值的引用;(比如模板函数,参数是个万能参数,使用的时候会自动进行推导;)
- 使用std::forward()解决
- lambda表达式
- 作用
- 定义匿名函数:不用考虑函数的名字,也不用考虑函数定义在什么地方;
- lambda表达式就是用表达式表示一个函数,不用定义额外的函数,代码更高效,在STL算法式使用很多;
- 基本语法
- [捕获列表](参数列表)mutable(可选)异常属性 -> 返回类型{//函数体}
- 捕获列表
- 值捕获
- 引用捕获
- 隐式捕获
- 表达式捕获
- 语法形式
- []
- [name1, &name2]
- [this]
- [&]
- [=]
- [=, &x]
- [&, x]
- [name1=expr1, name2=expr2, ...]
- mutable
- lambda表达式默认是const函数,表达式内无法修改捕获的任何变量;
- mutable:修改表达式为非const函数;
- 闭包和std::function
- 闭包
- lambda表达式捕获列表不为空时,本质上时一个与仿函数相似的闭包类型;
- 仿函数
- C++中的仿函数(Functor)是一种重载了函数调用运算符()的类,它可以像函数一样被调用。
- 就是一个可以像函数一样调用的类;
- 闭包类型
- lambda表达式捕获列表为空时,可以转换为函数指针进行传递;
- std::function
- std::function是一种类型安全的通用,多态的函数封装,可对任何调用对象进行打包;
- std::function对象可以存储,复制,调用,提供了闭包的统一封装格式;
- std::initializer_list
- 使用
- 函数的参数个数未知,但是类型相同;
- 初始化列表中的元素是const的,所有参数在{}里传入,不会修改;
- 实例
- 智能指针
- 头文件:memory
- 帮助自动完成,new和deleted的过程,也就是把动态内存管理的任务交给语言,交给系统;
- unique_ptr和shared_ptr都支持的操作
- 构建一个空智能指针;(啥都不指向)
- unique_ptr<T> up
- shared_ptr<T> sp
- shared_ptr<std::string> sp
- p
- 可以使用p进行条件判断,如果p指向一个对象,则为true;
- *p
- p.get()
- swap(p, q)
- three
- unique_ptr
- 独占所指向的对象
- 在某个时刻只能有一个unique_ptr指向给定的对象;
- unique_ptr销毁时,他所指向的对象也被销毁;
- 操作
- make_unique(args)
- 通过对象参数构建一个unique_ptr的智能指针;(C++ 14支持)
- unique_ptr<string> sp = make_unique<string>("123456789");
- 指定释放的方式(默认是delete)
- unique_ptr<T, D> u2
- unique_ptr<T, D> u2(d)
- u.release()
- u放弃指向对象的控制权(不会释放指向的对象),返回指向对象的普通指针(不是只能指针),方便赋值另一个指针(也就是移动构造),并将u置为空;
- 用于u管理的资源(对象)移交给其他普通指针,比如想手动管理;
- u.reset()
- 释放u指向的对象,并把u置为空;
- 用于释放u管理的资源,并把u指向新的对象;
- 注:
- 由于unique_ptr不支持拷贝,赋值。可以通过release,reset来转移控制权,先置空,在重新指向对象;
- 禁止复制语义也存在特例,即可以通过一个函数返回一个 std::unique_ptr:
- #include <memory> std::unique_ptr<int> func(int val) { std::unique_ptr<int> up(new int(val)); return up; } int main() { std::unique_ptr<int> sp1 = func(123); return 0; }
- shared_ptr
- 允许多个指针指向同一个对象;
- 内部使用引入计数的方式管理资源,如果资源的计数值变为0,则释放资源;
- 每多一个 std::shared_ptr 对资源的引用,资源引用计数将增加 1,每一个指向该资源的 std::shared_ptr 对象析构时,资源引用计数减 1,最后一个 std::shared_ptr 对象析构时,发现资源计数为 0,将释放其持有的资源。
- sp2 = sp1:sp1的资源引用计数加1(比如加1之后是2),但是sp2的是1(0+1),sp2的和sp1的不是一个值;
- 操作
- make_shared(args)
- 通过对象参数构造一个shared_ptr智能指针;(C++11)
- shared_ptr<T> p(q)
- p是shared_ptrq的拷贝,此操作会递增q中的计数器;
- p=q
- p.use_count()
- weak_ptr
- 指向shared_ptr所管理的对象;
- std::weak_ptr 可以从一个 std::shared_ptr 或另一个 std::weak_ptr 对象构造,std::shared_ptr 可以直接赋值给 std::weak_ptr ,也可以通过 std::weak_ptr 的 lock() 函数来获得 std::shared_ptr;
- 一种不控制所指向对象生存期的智能指针;
- 不会改变绑定的shared_ptr的引用计数;也就是weak_ptr的构造和析构不会改变绑定的shared_ptr的引用计数;
- 用途
- 防止shared_ptr出现环,引用计数会出现循环引用,导致资源无法释放的问题;
- 获得临时使用权
- 当一个对象随时可能被删除,并且只有当对象存在时才能被访问,可以使用weak_ptr获得临时使用权(转成shared_ptr)类型;
- 操作
- p.expired()
- true:管理的资源已经被释放,false:没有释放;
- 使用场景
- unique_ptr
- shared_ptr
- 资源需要在其他地方共享,并且需要管理对象的生命周期
- weak_ptr
- 智能指针和异常
- 即使程序发生异常,内存也能被回收,不会因为处理异常时会错过delete;
- 注:
- 智能指针也是模板
- 智能指针不支持自增自检操作,如果遍历智能指针指向的数组需要用到auto;
- 并发编程
- 多进程和多线程
- 占用资源
- 进程占用内存多,切换复杂;线程共享内存,占内存小,切换简单。
- 编码调试
- 进程相互独立,可以尽量减少线程的互斥加解锁,编码调试简单(实际线程才简单,因为有现成的库)。
- 运行方式
- 多进程是在CPU的多个核上运行多个,多线程是个单个核上运行多个。比如,一个核上,先运行这个,然后这个休眠,再运行那个。
- 运算密集型程序,CPU休息的时间少,就是没事干的时间少。IO密集型程序,CPU的休息时间多(因为CPU的处理速率比IO口的整个处理速率高的多)。
- 使用场景
- 注:以上的内容对不同的语言有不同的说法
- C++:多线程也可以实现真正的多核并行,还有现成的成熟的库,那没必要用多进程了。
- python:和上面讲的一致。
- 多线程编码
- c++11引入的多线程编程库
- 原子操作
- <atomic>
- 主要声明了两个类,std::atomic 和 std::atomic_flag
- 另外还声明了一套C风格的原子类型和与C兼容的原子操作的函数
- std::atomic<T>
- 与大多数赋值运算符不同,原子类型的赋值运算符不会返回对它们的左参数的引用,返回一个存储值的副本;(所以没有拷贝构造函数)
- is_lock free()
- 可以查询某原子类型的操作是直接使用的原子指令还是使用了锁
- 对于不耗时的存储,使用compare_exchange_weak(),而对于耗时的存储,使用 compare_exchange_strong(),可以避免发生“伪失败”;
- <thread>
- 主要声明了 std:thread 类
- 另外 std:this_thread命名空间也在该头文件中
- <mutex>
- 主要声明了与互斥量(mutex)相关的类,包括 std:mutex 系列类,std::lock_guard, std::uniq ue_lock,以及其他的类型和函数;
- <condition_variable>
- 主要声明了与条件变量相关的类,包括 std::condition_variable 和 std:conditi on_variable_any;
- <future>
- 主要声明了std::promise, std:package_task 两个 Provider 类,以及 std:future 和 std::sha red_future 两个 Future 类,另外还有一些与之相关的类型和函数,std::async()函数就声明在此头文件中;
- 这个头文件是对上面四个的进一步封装
- 注
- 线程使用
- <thread>
- std::thread
- 构造
- 默认构造函数
- 创建一个空的thread执行对象,无参数。
- std::thread first (funcname)
- 初始化构造函数
- 创建一个thread对象,改thread对象可被joinable(可连接的) ,有参数,函数参数通过args传递。
- std::thread first (funcname, func参数1,func参数2...)
- 注:如果函数是类的成员函数
- std::thread first (&类名::成员函数名, &类的实例化对象名,成员函数参数...)
- 禁用拷贝构造
- move构造函数
- thread (thread&& x) noexcept
- 调用成功之后 x 不代表任何 thread 执行对象。
- 实例
- std::thread t4(std::move(t3))
- t3不再线程,t4是,也就是把t3给t4。
- 注:
- 可被 joinable 的 thread 对象必须在他们销毁之前被主线程 join 或者将其设置为 detached(分离的)。
- 其他成员函数
- join
- get_id
- joinable
- detach
- swap
- 互斥量与锁
- std::mutex,std::lock_guard,std::unique_lock
- mutex
- std::mutex 对象提供了独占所有权的特性——即不支持递归地对 std::mutex 对象上锁。
- 成员函数
- 构造函数
- 不允许拷贝构造,也不允许 move 拷贝,最初产生的 mutex 对象是处于 unlocked 状态的。
- lock()
- 调用线程将锁住该互斥量
- 如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前,该线程一直拥有该锁。
- 如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住。
- 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。就是锁一次,在锁一次。
- unlock()
- try_lock()
- 尝试锁住互斥量
- 如果互斥量被其他线程占有,则当前线程也不会被阻塞,而是不锁,也就啥也不干,等下次再尝试能不能锁。其余情况和lock()一样。
- 注
- 容易忘记解锁,为了防止忘记,所以一般都用下面的那个。
- lock_guard
- 与 Mutex RAII 相关,方便线程对互斥量上锁。为在作用域块期间的mutex提供编译的RAII风格机制,也就是运行时自动上锁和解锁。
- lock_guard在构造时经进行mutex上锁,析构时对mutex进行释放。
- 使用:在需要并行的函数内部开始声明一个mutex的锁就行
- std::mutex mtx; ... (需要并行的函数内刚开始) std::lock_guard<std::mutex> lck (mtx);
- unique_lock
- 与 Mutex RAII 相关,方便线程对互斥量上锁,但提供了更好的上锁和解锁控制。
- 使用:在需要并行的函数内部开始声明一个mutex的锁就行
- std::mutex mtx; ... (需要并行的函数内刚开始) std::unique_lock<std::mutex> lck (mtx);
- lock_guard和unique_lock
- 如果仅有一次上解锁,就用lock_guard,多次就用unique_lock,我理解的是,除了条件变量的通知,其余无脑用unique_lock就完了。
- 其他
- std::recursive_mutex
- 允许同一个线程对互斥量多次上锁,也就是递归的上锁和解锁。
- std::time_mutex
- 条件变量
- std::condition_variable
- 功能
- 简单说,就是对线程进行阻塞和唤醒控制。
- 用于通知(唤醒阻塞的线程)
- 两步
- 获取互斥量std::mutex, 这个操作通常使用std::lock_guard来完成。
- 在持有锁的期间,在条件变量std::condition_variable上执行notify_one或者notify_all去唤醒阻塞线程。
- 用于阻塞(阻塞线程)
- 三步
- 使用std::unique_lock<std::mutex>来实现加锁操作。
- 执行wait,wait_for 或 wait_until。该操作能够原子性的释放互斥量mutex上的锁,并阻塞这个线程。
- 当条件变量condition_variable被通知,超时,或虚假唤醒时,该线程结束阻塞状态,并自动的获取到互斥量mutex上的锁。当然,这里应该检查是否为虚假唤醒。
- 唤醒丢失和虚假唤醒
- wait是用来阻塞的,notify_one和notify_all是用来唤醒阻塞的。
- 虚假唤醒
- 指当你对线程进行唤醒时,你不希望被唤醒的线程也被唤醒的现象。可能是操作系统或或者不正确的代码导致的。
- 唤醒丢失
- 发送方已经发生唤醒了,但是接受方没有收到,就会一直阻塞。
- 详细来说就是某个线程在调用notify时,另一个线程还没有进行wait,那么这个后面运行的wait就会永远阻塞,永远不会别notify。
- 解决办法
- 阻塞时使用谓词
- condVar.wait(lock, []{return flag;});
- 记住,wait是用来阻塞的,notify_one和notify_all是用来唤醒阻塞的,所以这里只有谓词是false的时候才会wait,true的时候就不wait。
- 循环检测
- while(!flag) { cv.wait(mtx); }
- 这里只有标志是false的时候才会wait,true的时候就不wait。
- wait的工作流程
- 注
- 使用谓词或者循环检测解决唤醒丢失的理解:如果notify在wait之前执行,那到执行wait的时候先判断一下条件,这个时候条件实际是true,压根就不执行wait,不阻塞。
- 实例
- 基于锁的安全队列实现
- 所有操作都上锁,pop的时候阻塞pop,push内部要通知唤醒。
- 生产者消费者模型
- 用安全队列实现,就是生产不停的往队列里放,消费者不停的阻塞时的从队列里取就行。
- 并发编程技巧
- 并发的相关错误
- 不必要的阻塞
- 死锁
- 活锁
- I/O阻塞
- 条件依赖外部输入或者内部输出,而这个输出输入永远不发生。
- 条件竞争
- 数据竞争
- 破坏不变量
- 生命周期问题
- 访问不存在的数据。访问一个生命周期已经结束的变量。
- 并发问题的定位技巧
- 消除非并发的错误,多线程转为单线程进行定位。
- 代码评审
- 多线程测试技术
- 数据库
- 概念
- 是什么?
- 数据库(Database)是按照数据结构来组织、存储和管理数据的仓库。
- 术语
- 数据库
- 数据表
- 表是数据的矩阵。在一个数据库中的表看起来像一个简单的电子表格。
- 列
- 相同类型的数据。可以理解为EXCEL表格的一列,例如姓名那一列。
- 行
- 键
- MySQL
- MySQL是一个流行的开源关系型数据库管理系统
- 关联数据库将数据保存在不同的表中,而不是将所有数据放在一个大仓库内,这样就增加了速度并提高了灵活性。
- 特点
- MySQL 支持大型的数据库。可以处理拥有上千万条记录的大型数据库。
- MySQL 使用标准的 SQL 数据语言形式。
- MySQL 可以运行于多个系统上,并且支持多种语言。
- MySQL 对 PHP 有很好的支持,PHP 是很适合用于 Web 程序开发。
- MySQL 是可以定制的,采用了 GPL 协议,你可以修改源码来开发自己的 MySQL 系统。
- 其余数据库
- SQLite
- SQLite是一个轻量级的嵌入式关系型数据库管理系统
- Redis
- 高性能编码
- 性能优化的4个层次
- 性能优化的关键
- 编码建议
- 控制语句
- 小范围紧凑的判断使用switch语句替代if-else
- 使用Switch/case语句,减少不必要的if语句的多次条件判断。
- 提取循环体内与循环无关的处理
- 和循环无关的重复计算,条件判断等,放到循环的外部;
- 多重循环,地址偏移小且次数多的循环放到内层
- 对于多维数组来说,其低维的地址偏移小,如果以此为循环内层,可以提升缓存的利用率。
- 对于for循环,内层循环更多,则展开的语句执行的次数更少。
- 字符串
- 避免对整个字符串数组清零
- 设置字符串数组为空字符串,只需要对第一个字节赋值'\0',避免对整个空间清零。
- C++语言
- 避免使用try catch 异常处理
- 避免启用RTTI机制
- RTTI机制会带来一定的性能损耗,项目中应该根据实际情况选择,编码中可以设计合理的多态来替代RTTI;(作用和性能损耗要权衡)
- 性能敏感流程中,避免临时对象申请
- 合理使用智能指针,采用unique_ptr 代替shared_ptr
- 在有明确的独占所有权的情况下,推荐使用unique_ptr代替shared_ptr;
- STL等容器使用时,需做规格预估与预留,避免容器内存自动扩张;
- STL等容器遍历时,采用迭代器的前置自增(自减)操作替代后置自增(自减)操作
- STL等容器结构选型时,针对预先确定长度的线性结构,采用array 代替vector
- 工具链辅助性能优化
- cmake
- makefile
- cmake
- 特点
- 工作过程
- 入口:根目录的CMakeLists.txt
- 入口指定的下一级目录的CMakeLists.txt
- 基础指令
- cmake_minimum_required
- cmake的最小版本号,一般IDE会自己帮助生成
- cmake_minimum_required(VERSION 3.0.0)
- project
- 添加工程名和版本,就是给你这个构建起个名字
- project(NAME VERSION 0.1)
- 设置完之后使用:${PROJECT_NAME}
- 指定include目录
- include_directories
- Topic1
-
-
-
- 注释
- 打印
- message,也没啥,就是把你想答应的内容填里面就行
- message("Hello" ${PARAS} "XXX")
- 定义变量
- set
- set(PARAS "-std=c++11 ${PARAS2}")
- option
- option(变量名 "解释说明" ON/OFF)
- 一般仅用于设置ON/OFF
- 指定子构建目录
- add_subdirectory
- add_subdirectory(子目录相对路径)
- 条件分支
- if()-else
- if()-else()-endif()
- 注:if()内部使用变量,不要有${}
- 默认变量
- PROJECT_NAME
- PROJECT_SOURCE_DIR
- EXECUTABLE_OUTPUT_PATH
- 构建过程
- 设置编译选项
- set(CMAKE_CXX_FLAGS "-std=c++11 ${CMAKE_CXX_FLAGS}")
- 指定构建配置
- 指定include目录
- 生成静态库
- add_libary(静态库名字 STATIC ${静态库源码目录})
- 生成动态库
- add_libary(动态库名字 SHARED ${静态库源码目录})
- 指定外部lib库
-
-
-
- 生成可执行目标文件
- add_executable(${PROJECT_NAME} main.cpp)
- Topic1
- 改善程序与设计的55个具体做法
- 习惯C++
- 02:尽量以const, enum, inline替换 #define
- 对于单纯常量,最好以const对象或者enums替换#define;
- 如果类内需要一个常量,还应该声明为static,声明这个常量是内部静态常量,无链接性,这样只会有一个实体;
- 对于类似函数的宏,最好改用inline函数代替#define
- 03: 尽量使用const
- 如果const加到函数后面,表示该函数是const(内部不会改变任何值),如果加到返回类型前,仅表示函数返回值是个const,并且可以用一个非const去接收;
- 04:确定对象被使用前已经初始化
- 最好对象定义的时候就进行初始化;
- 构造函数优先使用初始化列表的方式;(初始化列表是初始化,构造函数内部其实是先初始化,然后在赋值)
- 为了免除“跨编译单元之初始化次序”问题,请以local static对象替换non-local-static对象;
- 构造析构赋值
- 08:不要在析构函数中抛出异常,析构函数也绝不吐出异常(如果析构函数调用的函数抛出异常,析构函数也要吞下异常(不传播)或者结束程序)
- 09:决不在构造和析构中调用虚函数;
- 10:令operator= 返回一个reference to *this
- 11:在operator=中处理“自我赋值”;(判断需要赋值的对象是不是自己)
- others
- 除非有一个好理由允许构造函数被用于隐式类型转换,否则就声明为explicit;
- 常见问题及其解答
- sizeof()和strlen()的区别
- sizeof()是操作符,编译之后就知道结果了,strlen()是库函数,运行的时候才知道结果;
- sizeof()计算的是整个数据类型占内存的大小,strlen()计算的是字符串实际的长度,从开始到第一个'\0'的长度;
- C中的 malloc 和C++中的 new 的区别
- new和delete是操作符,是类中实现的操作符重载,会调用对象的构造和析构函数,本质也会调用malloc()和free()函数实现;
- malloc()和free()是函数,仅仅就是分配和释放内存;
- 返回值:new和delete返回的是某种数据类型的指针,free返回的是void指针;
- 指针可以是 volatile吗
- 可以,指针是一种普通的变量,从访问上没有什么不同于其他变量的特性。其保存的数值是个整型数据,和整型变量不同的是,这个整型数据指向的是一段内存地址。
- C、C++程序编译的内存分配情况
- 从静态存储区域分配
- 内存在程序编译时就已经分配好,这块内存在程序的整个运行期间都存在。速度快、不容易出错,因为有系统会善后。例如全局变量,static 变量等。
- 在栈上分配
- 自动内存分配
- 函数调用时的传参,函数内局部变量的存储单元都在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
- 从堆上分配
- 注:
- strcpy、sprintf 与 memcpy 的区别
- 功能不同
- strcpy 主要实现字符串变量间的拷贝
- sprintf 主要实现其他数据类型格式到字符串的转化
- sprintf()
- 格式化输出函数,和printf类似,只是输出到字符数组中;
- sprintf(buffer, "n=%d, f=%.2f", n, f);
- memcpy 主要是内存块间的拷贝
- 执行效率不同
- memcpy 最高,strcpy 次之,sprintf 的效率最低。
- 设置地址为 0x67a9 的整型变量的值为 0xaa66
- 定义一个int指针,然后把值强转为int指针赋值给,定义的int指针,然后让指针指向int值;
- int *ptr; ptr = (int *)0x67a9; *ptr = 0xaa66; //直接这么写代码肯定不行,因为编译器没有把0x67a9这个地址分配给你写的这个代码比编译之后的二进制;
- C++设计一个不能被继承的类
- 构造函数声明为私有
- 派生类的构造函数需要调用基类的构造函数,而如果基类的构造函数是私有的,那么派生类就无法调用它,从而无法继承基类。
- 提供一个静态方法来获取类的唯一实例
- 构造函数私有,只有类的成员函数可以访问,用静态成员函数实现实例,因为是静态的,所以可以在不创建类的实例的情况下调用;
- class FinalClass { private: FinalClass() {} // 将构造函数声明为私有的 public: static FinalClass& getInstance() // 提供一个静态方法来获取类的唯一实例 { static FinalClass instance; return instance; } };
- 虚函数可以被声明为inline吗
- 不可以,因为inline的替换静态的,在编译之后就完成了,而虚函数是动态的;