Skip to content

wanghan2789/CPP_learning

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

64 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

你可以通过下面的链接快速到访你想去的地方,当然你也可以直接浏览本页面,本页面现在主要包含C++的相关知识。

  
Sample
C++学习笔记
Sample
基础算法
Sample
Py和人工智能
Sample
数据库
Sample
操作系统
Sample
计算机网络

第一章 开启你的C++学习之旅

No.1 概述--起源与面向对象

  • 汇编语言直接操作硬件,访问寄存器和内存

  • C语言采用自顶向下结构化编程思路

  • 泛型编程

    • 重用代码,抽象通用概念
    • C++模板
  • 编译步骤

    源代码 - 编译器 - 目标代码 - 链接程序 - 可执行代码
    
                              |     |
    
                          启动代码  代码库
    
  • IDE和编译器

    • IDE可以使用户管理所有的编程过程
    • GUN等只能处理链接阶段和编译阶段
  • 编译和链接

    • .o文件可以用来表示目标程序,你可以通过最终的a.out控制

No.2 技巧复苏

  • 简单书店系统
  • 如果你的系统支持posix标准,那么0代表返回成功
  • iostream istream表示输入流 ostream表示输出流
  • std namespace 命名空间可以帮助我们避免不经意间重复命名导致的冲突,当然,当包含不同的命名空间且具有相同的名字,你需要显示说明
  • 作用运算符 :: 表示成员包含在那个空间集合中

第二章 变量与基本类型

No.1 基本变量类型

  • 位与字节
    • 1 byte 字节 是 8 bit 位
    • 1KB = 1024字节(byte)
    • 1M = 1024K
    • sizeof返回字节数
  • int 整型
    • short至少16位 系统内置 SHRT_MAX
    • int至少和short一样 系统内置 INT_MAX #4个字节
    • long至少32位 系统内置 LONG_MAX
    • long long至少min(64位,long) 系统内置 LLONG_MAX
    • 系统中的 0代表8进制 0x代表16进制
    • dec hex oct 三种格式显示 10 16 8 进制的数字
    • 当你给无符号的数赋值一个超出范围的数据,它会对容量极值取模
  • char 字符型
    • 字符一般不超过128个
    • A的ASCII是65 a为97

No.2 变量的声明与定义

  • 几种初始化方式
    • int a = 0;
    • int a = {0};
    • int a{0};
    • int a(0);
  • extern关键字,声明一个变量; 声明,使得name为程序所知,而定义是创建实体的过程。

No.3 const关键字

  • const常量 const int Varible = 100;

  • 便于进行类型检测

    • 相对于宏定义,const常量有数据类型,宏定义只是单纯替换文本,可能会出现意想不到的问题。
  • 防止意外修改

  • 函数重载提供了参考

  • const只为数据提供一次内存分配

  • const通常保留在符号表中,这样在编译的过程没有存储和读内存的操作。

  • C++中的const默认为内部连接,也就是说,const仅在const被定义过的文件里才是可见的,而在连接时不能被其他编译单元看到。当定义一个const时,必须赋一个值给它,除非用extern作出了清楚的说明。
    通常C++编译器并不为const创建存储空间,相反它把这个定义保存在它的符号表里。但是extern强制进行了存储空间分配(另外还有一些情况,如取一个const的地址,也要进行存储空间分配),由于extern意味着使用外部连接,因此必须分配存储空间,这也就是说有几个不同的编译单元应当能够引用它,所以它必须存储空间。
  • const的使用

    • char* const p 指针指向不变
    • const char *p 指针内容不变
    • const char * const p 两者都不变
    • 函数中的const
      • 参数在函数体内不可变 void f(const int i)
      • 参数指针指向内容不可变 void f(const int *p)
      • void f(const Class& var) 以及 void f(const Type& var) 这样的const引用传递和普通的函数按值传递的效果是相同的,它会禁止引用的对象的修改,不同的是按值传递会建立类对象的副本,然后传递过去。但是const直接传递地址。
      • const与返回值; 通常不建议对象或者引用为const属性, 因为这样做返回的实例只能访问类中的共有/保护数据以及const成员函数,并且不允许对其进行赋值操作。
    • 类中的const
      • const修饰成员变量,那么该成员只能在初始化列表中初始化赋值,后面不可修改。
      • const修饰成员函数,则该方法不能调用其他任何非const方法,也不能改变对象的成员变量。 void f() const;
      • const修饰对象表示该对象的任何数据不能被修改。const对象的任何非const成员都不能被调用。
  • const_cast

    • const_cast<type_id>(expression)

    • int &b = const_cast<int &>(a);
      p = const_cast<int*>(&a);
    • 注意,如果你直接在编译器里面用对已有数字的const_cast,原本的const a不会变化,但是如果你是悬而未决,例如const a = j j需要cin,那么编译器不能像define那样优化,此时a就会变化。

No.4 Static关键字

  • 作用范围为一个文件内,程序开始的时候分配空间,结束的时候释放,默认初始化为0,使用的时候可以变更数值。静态变量或者静态函数具有隐藏效果,只有本文件内才能找到他们。

  • 函数内部的static变量,可以作为对象之间的一种通信机制。这个对象将会仅在执行线程第一次到达它的定义对其初始化。

  • 局部静态变量,第一次的定义被控制线程到达的时候会调用构造函数,程序结束时会依次逆序调用析构函数。

    • 存储在静态数据区的变量会在程序刚开始运行时就完成初始化,也是唯一的一次初始化。共有两种变量存储在静态存储区:全局变量和static变量,只不过和全局变量比起来,static可以控制变量的可见范围,说到底static还是用来隐藏的。虽然这种用法不常见
      
  • 静态成员和静态函数成员,一个static成员只有唯一的一份副本,而不像常规的非static成员那样在每个对象里各有一份副本。同理,一个需要访问类成员,而不需要针对特定对象去调用的函数,也被称为一个static成员函数。类的静态成员函数只能访问类的静态成员(变量或函数)。

    • static数据属性有点像类属性,而不是对象属性
    • static类方法而不是对象方法
    • 静态方法不包含this指针,因此它不能访问对象的费静态数据成员以及函数。
    • static成员函数不能被声明为const
    • 非静态函数可以任意访问静态函数和属性

No.5 自定义数据类型

  • 类型别名
    • typedef double wages; wages就是double的别名可以当做double使用
    • using SI = Salary SI就是Salary的别名
    • auto说明符 auto a = 1 + 1.0; 但是一条语句只能包含一个声明类型。
    • decltype 你可能需要从表达式推测类型,但是你又不想这样初始化这个变量。
      • decltype() 就可以推算出一个类型
      • 于是decltype() a = 0; 就是 int a=0;
      • 注意 如果decltype(()),或者里面有更多层括号,都会被编译器认为是一个表达式。变量是一种可以作为左值的表达式,因此会的到引用类型。
  • 自定义数据结构
    • 类的定义结束要加 ;

第三章 复合类型

No.1 数组

1.0 数组基础

  • 创建一个数组,你需要 类型; 数组名; 数组的元素个数

  • 声明数组里面元素的个数的时候,你需要const或者整型常数, 也可以是常量表达式, 总之必须是编译的时候已知的。

  • 你可以在声明的时候初始化你的数组: int a[2] = {0,1};

  • sizeof数组name你将会得到整个数组的内存占有

  • 如果你初始化了一个元素,那么其他元素将会被初始化为0

  • C++11 数组初始化可以省略= int a[2]{0,1}; int a[2]{} int a[2]={} 都是将数组置0。

  • 列表的初始化禁止缩窄操作,double->int

  • 字符数组的特殊性:

    • char a[] = {'A'};  //初始化后没有空字符
      char a2[] = {'A', '\0'};  //显示说明空
      char a3[] = "ABC";  //自动包含 \0
      char a[0] = "Hentai"  //没有存放空,且越界
  • 复杂数组的声明

    • int *ptr[10];  //包含10个指针的数组
      int (*ptr)[10] = &arr;  //指向10个元素的数组
      int (&ptr)[10] = arr;  //10各元素的数组的别名
  • 指针与数组, 实际上编译器会把数组名的位置替换成数组首元素的指针

    • int firstconfirm()
      {
          int a[10]{};
          auto b(a);    //认为b是指向a的指针
          cout<<(b==&a[0])<<endl;
          return 0;
      }
      输出: True
  • begin(x) 与 end(x) 可以返回x的 首地址 和 尾地址+1 使用这两个libfuc你需要 import iterator头文件, 值得说明的是 end-begin就是元素的个数

  • 如果你要使用C风格的字符串, 你必须保证这个字符串是 \0 结尾的

  • strlen() strcmp() strcat() strcpy()

  • string允许直接用"" 初始化 string a("ABC");

  • 我们允许使用数组初始化vector ivec(begin(arr), end(arr));

2.0 多维数组

  • 列表嵌套形成的数组,也就是多维数组。这样 ([[A,B,C],[D,E,F]])

  • int a[3][4] 实际上是大小为3的数组,每个数组里面包含一个大小为4的数组
  • 第一个维度是行,第二个维度是列。也就是说(X,Y,Z)这样的列向量存储模式。

  • 内存中 二维数组是按行存储 类似于列向量的转置 (X^T)

  • 多维数组实际是数组的数组;

  • C++ 逗号运算符 所有表达式中优先级最低的, 只需要按照逻辑以及优先级计算。整个表达式是最后一个表达式的结果。

No.2 字符串

  • C风格的字符串包含很多安全隐患,建议使用string
  • "" 里面的内容叫做字符串常量,也叫作字符串面值
  • '' 字符常量不能与字符串混用
  • 里面包含strlen()函数, 他返回字符串长度而不是数组的长度, sizeof会返回整个数组的字节数
  • 输入流
    • cin输入流使用空格,tab,enter来确定字符结束,假设你输入 "AA BB" 它会把AA读入,再把BB读入,并置于输入队列。
    • cin.getline() 这里将输入一行,并将结尾的换行丢弃 读取到指定数量的字符或者遇到换行停止读取; cin.getline(name, 20), 读取的时候不会超过19个
    • cin.get(), 会保留enter在你的队列中
    • 在你混合输入数字和字符串的时候很可能会遇到问题, cin>>10 意味着会在输入队列缓存一个enter, 那么你的下一个getline会自动丢弃一个行。在输入数字后使用cin.get()可以丢弃上一个换行。当然你也可以做一个拼接,丢弃那个换行。例如: (cin>>10).get();

No.3 String

  • 要使用string你需要使用头文件,并且using namespace std;

  • 你可以

    • 使用C的字符串初始化string
    • cin到string
    • cout一个string
    • 数组法取第i个元素
  • 赋值,拼接,附加

    • 允许将一个string赋值给另一个string
    • 允许使用+连接string
    • 允许使用一个string拼合c字符串和一个string
    • 你可以通过.size()确定string中的字符数
    • string s(n, 'A'); s是n个A
    • s.empty() 是否为空
    • 注意你不能把两个字面值直接相加, "A"+"B"
  • string IO

    • getline(cin, string str);

    • while(cin >> word) cout<< word << endl;
      //反复读入直至遇到文件尾
  • 字符处理

    • 类似于python的迭代

      for(char i: string_s)    xxx;
      for(auto &i: string_s)   xxx;
    • 如果你要直接修改里面的字符,你需要使用引用的方式,例如使用toupper()函数,返回大写字幕,可以将小写转化为大写

No.4 指针基础

  • *用于解除引用 &用于引用或取地址

  • 一个有效的指针,应当保留对象的地址; 指向某个对象后面的另一个对象,或者是Null。可以是0值也就是空指针。

  • C++在创建指针的时候并不会为指针指向的开辟内存

  • 函数指针(P129-133)

    • 指向函数的指针

    • bool (*ptr)(const string&, const string&);
      typedef bool (*ptr_name)(const string&, const string&);  //使用typedef, 后面你就可以用ptr_name代表整个函数指针了。
    • 函数指针只能通过相同类型的函数或者函数指针或者0值进行初始化或者赋值。

    • 不同类型的函数指针之间不存在转换。

    • 通过函数指针你可以直接调用这个函数。

    • 返回指向函数的指针

    • int (*f(int))(int*, int);    // f有int参数, 返回一个int(*)(int*, int)的指针
      typedef int (*ptr)(int*, int);    //简化定义
      ptr f(int);    //这就和第一句的定义一模一样了
    • 你可以把函数指针的形参定义为函数类型,但是函数的返回类型必须是指向函数的指针,而不能是一个函数体。具有函数类型的形参所对应的实参将会被自动转换为指向相应函数类型的指针,但是当返回的是函数的时候,这样的转化将不会被实现。

    • 指针可以指向重载函数,但是如果这样,务必匹配每一个类型

  • void*指针

    • 它可以保存任何类型对象的地址,但是也因此它只能进行有限的几个操作
      • 与另一个指针比较
      • 向函数体传递或者从函数体返回void*
      • 给另一个void*赋值
      • 你不能通过void*操作指向的对象
      • 你返回的void*并不类似于void类型的函数
  • typedef与指针

    typedef string *ptr;
    const ptr cstr;        //ptr实质是string的指针
  • 你并不能将一个数字直接赋值给一个指针,但是你可以强制类型转化

    int *pt = (int*) 0xFFFF;
  • 内存分配

    • int *p = new int;

    • delete p; //释放内存

    • 你不能使用delete释放声明的内存

    • 创建/删除动态数组

      int *arr_ptr = new int [10];
      delete [] arr_ptr;
    • 不要使用delete重复释放,也不要释放非new块,空指针来说delete是安全的。

    • 如果使用new[]为数组分配内存,你需要delete[]; 如果你new[]分配了一个实体,请delete;

No.5 指针、数组、指针算数

  • 指针和数组基本等价的原因在于指针算数和C++内部的数组处理方式。指针变量+1之后,增量就是指向类型的字节数。同时C++将数组名解释为地址。
  • sizeof指针得到的是指针的长度,数组名得到的是数组的长度

No.6 引用

  • 引用的实质是对象的别名 &A, 在定义的时候,会执行初始值和引用的绑定操作, 一旦初始化完成就会一直绑定,所以你必须保证引用的初始化。
  • &处于左值为引用,并且不能让一个引用贴标签于0 1 2 这样的常亮,你需要贴到一个变量(对象)上去
  • sizeof引用,你会得到对象的大小,而sizeof指针,你会得到指针本身(4)的大小
  • 动态分配内存的时候,必须使用指针,引用可能引起内存泄漏
  • 引用没有const

No.7 struct union enum

Struct

  • 和C语言相比,你定义好结构体之后,声明的时候不需要在加struct关键字
  • 你可以对一个结构体直接置零初始化 struct_name name{};
  • 不允许缩窄转换
  • 允许结构数组的存在
  • 位字段:
    • C++允许占用特定位的结构成员,这样可以令创建于某个硬件设备的寄存器的数据结构对应的非常方便
    • 字段类型为整型或者枚举类型,然后是一个冒号,然后是数字,数字即为指定的使用位数。
  • 与class不同的是,class更多的是private属性,而struct是默认public属性
  • 结构体不能定义递归; 结构体可以包含指向本身类型的指针; 结构体可以嵌套其他结构体; 结构体可以再定义的时候初始化。
  • 注意结构体的位运算
  • C结构体不能包含函数, c++可以包含函数
  • 结构体与内存对齐
    • VC为了效率,会将各个变量存放的起始地址相对于结构体首地址的偏移量为该类型的倍数;

共同体union

  • 你只能同时存储相同的数据类型。共同体每次只能存储一个值。共同体的长度是最大成员的长度。你可以在如下场景使用,例如商品条码有的是string, 有的是数字。
  • 匿名共同体没有名称,只有一个成员是当前成员。
  • 节省空间
  • 共同体的内存
    • 所有成员会从低地址开始存放(这与结构体相同),于是这就会与小端存储格式与大端存储格式一起考察。
    • 大小端存储格式; 大端存储格式,低位保存在高地址,高位保存在低地址; 小端存储格式, 高位高地址,低位低地址。
    • X86小端模式, Sun的SPARC是大端模式。
    • printf函数是游策先入栈
    • longlong8字节 int4字节 char1 short2 尽管小于4字节,但也需要占用4字节

枚举enum

  • 类似struct变量, 但是它只能有有限个可能, 你不能非法赋值; 枚举只有赋值运算符,没有定义算数运算符。
  • 你可以让enum包含相同数值,如果你没有为enum全部显示赋值,默认初始位为0,后面与上一位增加1。
  • 枚举的取值范围,枚举定义的取值范围即可。大于max的最小的2的n次幂,-1; 下限,取反+1

No.8 new delete与动态数组

  • 如果你创建一个未命名的动态类型

    • yourtype *ptr = new yourtype;
  • 这个时候你可以使用 -> 来访问相关成员

  • 自动存储、静态存储、动态存储

    • 自动变量的实质是局部变量, 它通常存储在栈中, 你在执行代码块的时候,里面的变量会依次入栈, 离开的时候按照相反的顺序释放变量, 这被称为LIFO后进先出。程序在执行过程中栈会不断地放大和缩小。
    • 静态存储是整个程序运行过程中都会保存的;
    • 动态存储, 他们管理了一个内存池, 这叫做自由存储空间或者堆, 数据的周期不受程序或者函数的生存时间控制, 与常规的变量相比, 使用new和del会更灵活自由,但是管理也将更复杂。在栈中自动添加和删除机制确保占用的内存总是连续的, 但new和del相互影响导致自由区并不连续。
  • 智能指针有助于在一定程度上帮助自动规避内存泄漏。

  • 一个C++程序包含 栈 堆 静态存储区 文字常量区 代码段

  • 栈和堆的区别

    • 栈是编译器自动分配和释放, 存放函数参数,局部变量等。结构类似数据结构里面的stack,速度快。
    • heap,堆; 程序猿分配和释放, 如果你不释放,则在程序结束之后会由操作系统回收。它的分配方式类似于链表,速度较慢,而且容易产生碎片, 不过用起来方便。
  • int *ptr = new int[10]();    //默认初始化为0, 去掉()则不会初始化
  • malloc free是标准库函数, 而new和del是运算符; new自动计算分配空间,malloc手动计算; new是类型安全的,malloc不是, malloc只会关心总的字符数; new会调用构造函数; malloc需要库文件支持;

No.9 Vector

  • vector<your_type> name;
  • vector<your_type> name(name_c); 包含所有name_c的副本;
  • push_back() 方法类似于append v.size() 返回元素个数

第四章 表达式

No.1 基础概念与优先级

  • 作用于一个对象的是一元运算符, 作用于两个对象的是二元运算符。
  • 当运算符作用于类类型的运算对象的时候,用户可以自定义其含义。此过程的实质是对已存在的运算符赋予新的含义,于是被称之为重载。
  • 左值与右值:
    • 对于一个C++表达式,它不是右值就是左值。
    • 当一个对象作为右值,用的是该数值; 当一个对象作为左值,用的是该标签;

No.2 算术表达式与赋值运算符

  • % 求余, 也叫取模
  • 逻辑运算中包含短路求值策略, 只有当左侧不能确定整个逻辑才会继续计算右边;
  • 赋值运算符满足右结合律; 赋值运算符的级别较低;
  • 三目运算符 a>b? a:b a>b嘛?大于返回a, 否则返回b

No.3 位运算

  • ~取反 <<左移 >>右移 &按位与 ^位异或 |按位或
  • 左移 << int低位补0, unsign低位补0
  • 右移 >> int高位补符号, unsign高位补0
  • 原码补码与反码:
    • 正数三码一致
    • 一个数的二进制表示就是它的原码; 你将它依次取反就是反码; 你在将反码+1就是补码
    • 那么符号位置1 +数字本身的补码, 那么就是一个负数的表示

No.4 SizeOf()

  • char 1
  • 对引用类型执行sizeof你会得到对象的大小
  • 对指针sizeof你会得到指针的大小, 你对一个野指针sizeof是安全的, 因为你不需要解引用, 对解引用的指针sizeof你会得到对象的大小;
  • 对数组sizeof你会得到整个数组的大小;
  • 对string或者vector执行sizeof仅仅会返回该类型的固定不分大小, 不会计算对象中的元素的占用空间量。

No.5 逗号运算符

  • 依次运算,正常会丢弃左边的,直到最后一个;
  • =的优先级高于, a=3*4, a+1; a=12 然后整个表达式的值为13, 但是a仍然是12

No.6 类型转换

  • bool char signed char short等类型, 如果相对运算存在int, 他们就会被提升为int; 否则提升成 usigned int;

强制类型转换

  • static_cast:

    • a = int(a);
      a = static_cast<int>(a);
    • 最常用的类型转化

  • const_cast

    • 去除const属性, 把const指针转变为非const

    • int* Ptr = const_cast<int*>(*const_ptr)
  • dynamic_const

    • 该操作符用于运行时检查该类型转化是否安全, 但是仅仅在多态类型时合法,该类至少具有一个虚拟方法。dunamic_cast语法上与static相同, 但是其应用于类层次的转化。同时, 它具备类型检查功能。
  • reinterpret_cast

    • reinterpret为重新解释的意思,即为数据的二进制形式重新解释,但是不改变其值

    • char* ptr = "hello freind!";
      int i = reinterpret_cast<int>(ptr); 

第五章 语句

No.1 基础概念

  • ; 空语句, 也就是占位符pass
  • 多余的空语句并不总是无害的

No.2 条件语句

  • 基本结构

    if condition1{}
    else if condition2{}
    else{}
  • 悬垂else, 就近原则

  • 你可以使用花括号使得else与花括号前的if强制匹配;

  • switch语句方便的可以为我们提供对固定选项中做出选择

    switch (ch)
    {
        case 'a':
            statement;
            break;
        case 'b':
            statment2;
            break;
        default:
            ....
    }
    //一条语句与第一项匹配成功,它会一直执行到句子尾部或者与被退出
  • 字符库函数cctype

    • isalpha, isdigit, islower, isupper

No.3 循环语句

  • 定义在while条件部分或者循环体内部的变量,每次循环都要经历创建和销毁

  • 范围for语句

    vector<int> v(9);
    ...for(auto &lable : v){}  //遍历迭代器
  • do while是先执行循环体在检查循环条件;

No.4 跳转语句

  • break终端当前距离它最近的语句
  • continue停止当前最近的循环, 并立即进行下一次
  • goto无条件跳转到同一条函数的另一条语句

No.5 异常处理

  • throw raise(抛出)一个异常

    throw runtime_error("this is a test"); 
    int a = 0;
        try
        {
            if(!a) throw runtime_error("this is test");
        }
        catch(runtime_error err)
        {
            cout<<err.what()<<' '<<a<<endl;
        }
  • try 开始 catch子句结束。try抛出的异常会被某个catch处理掉, 因此这部分又叫做异常处理代码。

  • exception class, 用于throw和相关的catch之间传递异常的具体信息

    try
        {
            throw runtime_error("this is test");
        }
    catch(exception)
        {
            cout<<a<<endl;
        }

第六章 函数

No.1 函数基础

  • 重载函数, 同一个函数名可以对应不同的几个函数;

  • 函数不能返回一个数组或者一个函数,但是它可以返回一个数组指针或者函数指针。

  • 名字有作用域; 对象有生命周期。

  • 自动对象是只存在于块的执行期间的对象。例如形参。

  • 形参不含有初始值会被默认初始化。

  • 局部静态变量

    • 有的时候需要把局部变量的生命周期贯穿到整个程序的运行时间,这个时候你可以选择局部静态变量。局部静态对象在第一次经过该定义语句的时候初始化,直到程序终止才会被销毁。
  • 函数值能够定义一次,但是你可以多次声明函数。

  • 分离式编译允许程序分割成几个文件,每个文件独立编译

    g++ my_program parta.c++
    g++ my_program parta.c++ -o main
    #如果我们修改了某几个文件,我们只需要单独编译那几个文件就可以了
  • 在C++中,你定义一个f()的函数,表明没有参数, 而f(...)才表示函数的参数可选。C语言中f()认为你的参数列表可选。

  • C++原型

    • 正确处理函数返回值
    • 检查参数的数目是否正确
    • 检查类型是否正确

No.2 函数参数与参数传递

  • 形参为引用类型的时候,实参被引用传递。
  • 实参的值被拷贝给形参时, 形参和实参是两个相互独立的对象, 这样的传递方式是值传递。
  • 执行指针拷贝操作的时候,拷贝的是指针的值。
  • 有的时候某些类类型不支持拷贝操作,这个时候你可以通过引用形参访问这个类对象。
  • 如果无需改变引用值,最好声明为常量引用。

No.3 数组形参, 指针形参, const

Const形参与实参

  • 与其他初始化的过程一样,你使用实参初始化形参的时候,都会忽略顶层const。当形参有顶层const的时候,传给它常量对象或者非常量对象都是可以的。
  • 应尽量使用常量引用, 避免普通引用不接纳const, 字面值等问题。

数组形参

  • 数组的两个影响函数定义的性质: 不允许拷贝数组, 使用数组的时候通常会把数组转化为指针;

    void fun(const int*);
    void fun(const int[]);
    void fun(const int[10]);    //10并没有什么影响
  • 如果我们传递的是一个数组,那么它将会自动转化为指向数组首元素的指针, 数组的大小对函数的影响就没有什么影响了。

  • 使用标记指定数组长度 类似C风格的字符串, 但是这样的处理手段对int这样的对所有取值都合法的数据就不是很有效了。

  • 使用表转库规范 你可以传入数组的首地址和尾地址

  • 显示传递一个表示数组大小的形参

  • 数组形参和const 函数不需要对数组执行写的操作的时候,你应该把数组定义为指向const的指针。

  • 数组引用形参

    int &a[10]; //形参数组
    int (&a)[10]; //整个数组的引用

默认参数

  • 如果包含默认参数,调用的时候你可以忽略它。
  • 一个形参被赋予了默认值,从它开始,后面所有的必须被赋默认值。
  • 你只能省略尾部的形参。
  • 形参只能被赋予一次默认实参,你不能再声明的时候在改动它。

No.4 返回

  • void 函数没有返回值; 实际上结束阶段会隐式执行return
  • return的返回值要么与定义的函数类型相同,要么能隐式转化。
  • 不应当返回局部对象的引用或者指针。函数结束后局部变量会被释放, 哪里不在是有效的内存区。
  • 列表返回值 C++里面,函数可以返回花括号保卫的值的列表。

No.5 递归函数

  • 递归函数就是自调用函数。被调函数运行的代码虽是同一个函数的代码体,但由于调用点,调用时状态, 返回点的不同,可以看作是函数的一个副本,与调用函数的代码无关,所以函数的代码是独立的。被调函数运行的栈空间独立于调用函数的栈空间,所以与调用函数之间的数据也是无关的。函数之间靠参数传递和返回值来联系,函数看作为黑盒。
  • 递归增加了系统开销。 时间上, 执行调用与返回的额外工作要占用CPU时间。空间上,随着每递归一次,栈内存就多占用一截。

No.6 内联函数

  • 背景:
    • 一个短小的函数, 阅读起来比一个相同规格的表达式简单
    • 使用函数可以保证行为的统一
    • 修改一个函数比多次使用表达式方便简易
    • 函数也可以被重复利用
    • 但是调用函数一般来说比求解表达式需要消耗更多资源
    • inline内联函数可以避免函数调用的开销
  • 内联函数通常就是在内联点出展开函数, 展开的函数类似于表达式 (类比宏定义)
  • 在函数定义和声明加上关键字 inline 就能定义一个内联函数了
  • 内联说明是向编译器发出的请求,编译器可以选择忽略
  • 内联一般用于优化规模小,调用频繁流程直接的函数
  • constexpr函数 能用于常量表达式的函数; 其中定义方式类似于普通函数,但是return有且只有一条, 而形参的类型必须是字面值类型。为了能在编译过程中随时展开,constexpr函数被隐式调用为内联函数。constexpr可以包含其他不执行操作的语句。

No.7 函数重载

  • 同一作用域下的几个函数的函数名相同,但是参数列表不同,我们称之为函数重载。(main函数不能重载)
  • 编译器会很久实参类型确定调用哪一种函数。
  • 不允许两个函数除了返回类型之外其他所有要素都相同。例如 bool f() 和int f()
  • 顶层的const不影响传入函数的对象, 一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开来。
  • 形参是某个对象的指针,可以通过区分底层是const还是非const来做区分。
  • const_cast与重载函数
    • const_cast可以将普通类型转换成const类型
    • 转换后的const可以转换回去,而且很安全

No.8 函数模板

  • 基本定义

    • template <typename ScalarT>
      void Fuction(ScalarT a){
      ...
      }
  • 函数模板可以重载

  • 实现实例化,这个时候编译器不会再去寻找匹配的代码,而是直接找匹配的代码段。(不要运用模板,用我实例好的对象)

    • template void Fuction<int>(int a);
  • 显示具体化

    • template <> void Fuction<int>(int a);

No.9 函数指针

  • 定义一个函数指针后,你可以用一个函数名初始化它

    • bool (*ptr)(int a); 初始化 ptr=Function;
  • 我们可以通过函数指针直接调用函数,不需要解指针,当然你也可以解。

    bool T1 = ptr(1);
    bool T2 = (*ptr)(1);
  • 函数指针不存在规则转化

  • 重载函数的指针, 上下文必须清晰定义你选择了那个函数。

  • 形参可以是函数的指针。你把形参定义为函数,但是它会自动转化为指针。

  • 返回函数指针, 与数组类似,你不能返回一个函数,但是你可以返回它的指针。

第七章 内存模型和名称空间

No.1 单独编译

  • 注意不要将函数定义或者变量声明放在头文件中。
  • 通常头文件包括:
    • 函数原型
    • 宏定义或者const
    • 结构声明
    • 类声明
    • 模板声明
    • 内联函数
  • 多个库链接: C++允许不同的编译器进行自己的名城修饰, 因此不同编译器创建的二进制文件可能无法统一链接在一起, 如果拥有源码, 你应该在自己的系统上单独编译来消除链接错误。

No.2 存储连续性、作用于和链接性

  • 在C++11中有四种方案来存储数据
    • 自动存储持续性: 函数定义中声明的变量, 包括函数参数的存储持续性是自动的。它们在程序调用函数的时候被创建,在程序结束调用的时候被释放。
    • 静态存储持续性: static在整个程序运行过程中都存在。
    • 线性存储持续性: thread_local, 其生命周期与所属线程一样长。
    • 动态存储持续性: 使用new运算符分配的内存将一直存在, 知道你去delete或者程序结束。你也可以称其为自由存储或者堆。
  • LIFO简化了参数的传递。
  • 寄存器变量 register 建议编译器使用寄存器存储变量
  • 除了默认的初始化,你还可以对静态变量进行常量表达式的初始化。
  • volatile关键字: 即使程序代码没有对内存修改,但是这个数值仍旧可能发生变化。告诉编译器不要进行寄存器优化。
  • mutable: 及时某个结构体或者类是const, 但是某个成员可以被修改。
  • 默认的全局变量它的链接性是外部的,但是const的全局变量的连接性是内部的。
  • 内部链接性意味着每个文件都有自己的一组常量而不是所有文件共享一组常量。没个定义都是该文件私有的。如果处于某种原因,程序猿希望某个常量的链接性为外部的,那么你可以使用extern关键字覆盖默认内部链接性。
  • C++不允许函数内定义另一个函数
  • C++对重载函数应用名称矫正,为重载函数生成不同的符号名称。这种方法被称为C++语言链接。
  • extern "C" void function(); 使用C语言链接性。
  • 如果new不到内存,会抛出异常
  • namespace name{}
  • 名称空间可以是全局的,也可以位于另一个名称空间中。但是不能位于代码块中。
  • using声明使一个名称有效,而using namespace name的编译指令使得名称都可用

No.3 C++内存管理

  • C++包含堆, 栈, 自由存储区, 全局静态区, 常量存储区;
  • 栈的速度很快,但是内存容量有限。
  • 指针Ptr会分配到栈中, new出来的在堆中。它们的区别:
    • 管理方式不同,栈是编译器自动管理,堆需要手动操作;
    • 空间大小不同, 堆在32位OS下可以达到4GB空间,但是栈在VC6只有1M
    • 对于栈有着严格的入栈出栈顺序,不会产生碎片,但是堆会造成内存空间的不连续
  • 常见的内存错误:
    • 分配未成功,但是要使用它。请在使用之前检查Ptr是不是Null
    • 内存分配成功,没有初始化。记得对内存负责。
    • 越界。
    • 忘记释放。
    • 释放后继续使用。

No.4 Static和Extern总结

  • static修饰局部变量。其生命周期变为与程序相同,但是作用域没有发生改变。
  • static修饰全局变量。它只能在本文件中访问。
  • extern外链接,表示去其他文件查找这个定义。
  • static修饰类属性,该属性属于类而不再是对象,也不包含this指针。只能访问静态成员函数和静态成员变量。

第八章 类

No.1 类基础

  • 在C++语言中我们使用类定义自己的数据类型。类的基本思想是数据抽象和封装。数据抽象是一种依赖于接口和实现分离的编程技术。类的接口包含用户能够执行的操作。类的实现包括类的数据成员类的方法等。
  • 成员函数的函数体可以定义在类的内部也可以定义在类的外部。
  • this指针
    • 当我们在调用某个方法的时候,实际上是在替对象调用这种方法。
    • 成员函数通过this的额外隐式参数来访问调用它的那个对象,当我们调用一个成员函数的时候,用请求该函数的对象地址初始化this。
    • 编译器负责把对象的地址传递给函数。
    • 成员函数内部,我们可以直接调用对象属性。this.a
    • this是一个常量指针,总是指向现在的这个对象
  • const成员函数
    • 默认情况下this是指向非常量的类的常量指针。这也就意味着我们不能把this绑定到常量对象上去。这导致我们不能在一个常量对象上调用普通的成员函数。因此我们定义方法的时候可以。int f()const{};
    • C++允许把const关键字放在成员函数的参数列表之后,此时紧跟在参数列表后面的const就表示this是一个指向常量的指针。这样的成员函数我们称之为常量成员函数。
    • 你可以理解为 int f(const ScalarT *const this)
    • 常量对象以及常量对象的引用或者指针都只能调用常量成员函数。
  • 即使一个成员函数定义在某一个成员函数之后,你还是可以使用它。编译器分两步处理类,首先处理编译成员的声明,然后才轮到函数体。
  • 在外部定义成员函数必须包含它所属的类名。
  • 定义返回this对象的函数。 return *this;
  • 数据项通常放在私有部分,方法通常放在共有部分。

No.2 构造函数与析构函数

  • 构造函数的任务是初始化类对象的数据成员,无论何时只要类对象被创建就会执行构造函数。

  • 构造函数的名字和类名相同,和其他函数不一样,构造函数没有返回类型。构造函数也有一个参数列表和一个函数体。类可以包含多个构造函数和其他重载函数差不多,不同的构造函数之间必须在参数数量或者参数类型上有所区别。

  • 构造函数不能const。当我们创建类的一个const对象的时候,知道构造函数完成初始化过程,对象才能真正取得其常量属性。

  • 合成的默认构造函数

    • 默认构造函数无需任何实参。
    • 如果我们没有为一个类定义一个构造函数,那么会调用默认的构造函数。
    • 合成的默认构造函数,如果类内的初始值用它来初始化成员,否则默认初始化该成员。
  • 只有发现类中没有声明任何构造函数时,编译器才会自动地生成默认构造函数。

  • 如果类包含有内置类型或者复合类型,则只有当这些成员全部被赋予了类内的初始值的时候,这个类才适合使用合成的默认构造函数。

  • =default的含义 在C++新标准中,如果我们需要默认的行为,那么可以通过在参数列表后面增加这个来要求编译器生成构造函数。当然你也可以定义在类的外部。它如果在类的内部,他是内联的; 如果它在类的外部,则该成员默认的情况下不是内联的。

  • 构造函数初始值列表可以再你的编译器不支持类内初始值的情况下来帮助你初始化类的成员。

  • 构造函数初始值列表:

    • 构造函数初始值是成员名字的一个列表,每个名字后面紧跟着括号扩起来的(包括花括号内的)成员赋初值。不同长远的初始化通过逗号分隔开来。

    • My_data(const string s): 
          list_a(0), list_b(""){}
      //这里面,冒号后面的就是初始化参数列表,我们用冒号将函数名,参数和初始化列表函数体分割开
    • 如果你的编译器不支持类内初始值,则所有构造函数都应该显示的初始化每个内置类型的成员。

  • 尽管编译器能替我们合成拷贝赋值和销毁的操作,但是有的时候合成的版本无法正常工作。

No.3 访问控制与封装

访问说明符

  • public与private
  • 作为接口的一部分,构造函数与部分成员函数应该在public里面; 而数据成员和作为实现部分的函数应该在private说明符后面。一个类的访问说明符的有效期从它出现开始,到下一个出现或者类的end结束。

友元

  • 有些函数尽管是类接口的一部分,但是他们不是类的成员,这样它们不能访问private属性的数据成员。

  • 类可以允许其他类或者函数访问非共有属性,这样的途径被称之为友元。你可以在类中增加以 friend关键字开头的函数声明语句,这样这个函数就能访问类的私有属性了。

  • 一般来说,最好在类开始或者end的时候集中声明友元。

  • 如果一个类指定了友元类,则友元类的成员函数可以访问此类包括非公有成员在内的所有成员。

  • 你也可以让成员函数作为友元。

    friend my_class::function();
  • 重载函数本质不同,因此你需要对所有函数友元。

令成员作为内联函数

  • 在类中,有些小规模函数适合被生命为内联函数。定义在类内部的成员函数是自动inline的。
  • 你可以在类的内部或者外部使用inline关键字来显式声明内联函数。
  • 重载成员函数的方法和一般函数一样。

返回*this的成员函数

  • 一个const成员函数如果以引用的形式返回*this,那么它的返回类型将是常量引用。

this指针的总结

  • this的作用域是类的内部,当类的费静态成员函数中访问类的非静态成员的时候,编译器会自动将对象本身的地址作为隐含参数传递给函数。
  • this指针的使用, this->n = n;
  • this指针指向当面对象。

No.4 类的作用域

  • 每个类都会定义它自己的作用域,在类的作用域之外,普通的数据和函数成员只能通过对象引用或者指针访问运算符。

No.5 深入探索构造函数

构造函数初始值列表

  • 如果没有在构造函数的初始值列表中显式的初始化成员,则该成员在构造函数体之前执行默认初始化。

  • 构造函数的初始值有的时候必不可少。如果成员是const那么你必须将它进行初始化。

  • 随着构造函数体一开始执行,初始化就完成了。我们初始化const或者引用类型的数据成员的唯一机会就是通过构造函数初始值。

  • 如果成员是const或者属于某种没有提供默认构造函数的累类型,我们必须通过构造函数初始值列表为这些成员提供初值。

  • 成员初始化的顺序

    • 构造函数的初始值列表只说明用于初始化成员的值,而不限定初始化的具体执行顺序。而成员初始化的顺序与类内定义中出现的顺序一致。
    • 列表中谁先被初始化决定于类的定义顺序。
  • 构造函数与默认实参

    • 如果一个构造函数为所有参数都提供了默认实参,则他实际上也定义了默认构造函数。
  • 委托构造函数

    • 允许一个构造函数把自己的部分或者全部职责委托给其他构造函数。

    • class my_class{
          public:
          my_class(int a){}
          my_class():my_class(0){}  //委托给上一个函数
      };
  • 默认构造函数的作用

    • 默认初始化将会在下列情况下发生
      • 没有初始值的非静态变量
      • 使用合成的默认构造函数
      • 类类型的成员没有在构造函数初始列表中显式初始化
    • 值的初始化
      • 数组初始化过程如果提供的数据少于数组宽度
      • 我们不是用初始值定义一个局部静态变量
      • my_class(1);这样的初始化方式
    • 在实际情况下,如果定义了其他构造函数,那么最好提供一个默认构造函数。
    • 使用构造函数
      • 初始化对象 class_type class_name;
  • 隐式的类类型转换

    • 只能一次转换
    • 转化不是总有效的
    • 如果你使用explicit关键字,隐式转化将被抑制
  • 聚合类

    • 属性
      • 所有成员都是public
      • 没有定义任何构造函数
      • 没有类内初始值
      • 没有基类,没有virtual

No.6 类的静态成员

  • 如果你需要类属性,而不是对象属性的时候,你可以用static关键字完成这一请求。
  • 虽然静态成员不属于对象,但是我们依然可以使用类的对象引用或者指针访问这一成员。

第九章 拷贝构造与动态内存

No.1 拷贝构造函数

  • 我们通过拷贝构造函数,拷贝赋值运算符,移动构造函数,移动赋值运算符和析构函数实现对对象的拷贝,移动,赋值和销毁。
  • 如果你没有显式定义所有拷贝控制成员,那么编译器会自动补全。但是编译器自动补全的版本可能并不是我们所想要的。
  • 如果一个构造函数的第一个参数必须是自身类型的一个引用,且额外参数都有默认值,那么这个构造函数就是拷贝构造函数。虽然第一个参数可以不是const引用,但是这个参数几乎总是一个const的引用。拷贝构造函数经常会被隐式调用,因此你不应该在拷贝构造函数里面用explicit来修饰。
  • 即使我们定义了一个其他类型的构造函数,编译器也会为我们创建一个拷贝构造函数。
  • 每个成员的类型决定了它如何被拷贝。对于类对象,会调用拷贝构造函数,内置类型的成员则直接拷贝。虽然我们不能直接拷贝一个数组,但是合成拷贝构造函数会对数组的每个元素逐一拷贝。
  • 拷贝初始化: 当使用直接初始化,我们实际上是要求编译器使用普通的函数匹配。当我们使用拷贝初始化,我们要求编译器将右侧运算对象拷贝到正在创建的对象中。如果需要的话可能还要进行类型转换。
  • 拷贝初始化除了在赋值的时候发生,在其他情况下也会发生
    • 将一个对象作为实参传递给一个非引用类型的形参
    • 返回非引用类型的对象
    • 用花括号列表初始化一个数组中的元素或一个聚合类中的成员
  • 拷贝构造函数被用来初始化非引用类类型参数,这一特性解释了为什么拷贝构造函数自己的参数必须是引用类型。如果参数不是引用类型,则调用永远不会成功。为了调用拷贝构造函数,我们必须拷贝它的实参,但是为了拷贝实参,我们又必须调用拷贝构造函数。

No.2 拷贝复制运算符

  • 重载赋值运算符 operator= 的函数

    Classname operator=(const Classname&);

  • 赋值运算符通常应该返回一个指向其左侧运算对象的引用。

  • 合成拷贝赋值运算符。如果未定义,编译器自动提供。

No.3 析构函数

  • 析构函数释放对象使用的资源,并销毁对象的非static数据成员。

    ~del_fuction()      //开头有~ 不接受参数 不允许有返回值
    {}
  • 在一个构造函数中,成员的初始化是函数体执行之前完成的,并且按照它们在类中出现的顺序进行初始化。在一个析构函数中,受限制性函数体,然后才会销毁成员。按照初始化的顺序逆序销毁。

  • 销毁类类型需要用析构函数,但是销毁内置类型什么也不需要做。隐式销毁一个内置指针类型的成员不会delete它所指向的对象。

  • 智能指针是类类型,所有具有析构函数。与普通指针不同,只能指针在析构阶段会被自动销毁。

  • 调用析构函数的情况

    • 变量在离开其作用域时被销毁
    • 当一个对象被销毁时,其成员被销毁
    • 容器销毁,其中的元素销毁
    • 动态分配的对象,大概对指向它的指针应当delete
    • 临时对象会在表达式结束后销毁
  • 当指向一个对象的引用或者指针离开作用域的时候,析构函数不会执行。

No.4 三五法则

  • 需要析构函数的类也需要拷贝和赋值操作
    • 当我们决定一个类是否需要自己版本的拷贝构造函数的时候,一个基本的原则是首先确定这个类是否需要一个析构函数。对析构函数的需求要比对拷贝构造函数或者赋值运算符的需求更为明显,如果一个类需要一个析构函数,那么它大概率也需要一个拷贝构造函数和一个拷贝赋值运算符。
    • 自定义析构函数,那么必须自定义拷贝赋值运算符和拷贝构造函数。
  • 需要拷贝操作的类也需要赋值操作,反之亦然

No.5 default

  • 我们可以通过将靠被控制成员定义为 = default来显式要求编译器生成合成的版本。

  • class TestDefaultClass
    {
    public:
        TestDefaultClass() = default;
        TestDefaultClass(const TestDefaultClass&) = default;
        TestDefaultClass& operator =(const TestDefaultClass&);
        ~TestDefaultClass() = default;
    };
  • 阻止拷贝

    • 定义删除的函数。对于删除函数,我们虽然声明了它们,但是我们不能以任何方式使用它们。在参数列表后面追加 = delete; 来指出我们希望的函数是删除的。
    • =delete; 必须出现在函数第一次声明的时候。
    • 析构函数不能使删除的成员。如果析构函数是删除的函数,那么说明析构函数被阻止了,于是我们会发生无法销毁类对象的问题。
    • 合成的拷贝控制成员可能是删除的。
      • 类的析构函数是不可访问或者删除的,那么合成析构函数被定义为删除;
      • 如果类的某个成员的拷贝构造函数是删除或者不可访问的,那么累的合成拷贝构造函数是删除的。同样析构函数是删除的或者不可访问的,那么合成拷贝构造函数也会被定义为删除。
      • 如果类的某个成员的拷贝赋值运算符是删除的或不可访问,或者类有个const或引用,那么累的合成拷贝赋值运算符被定义为删除的。
      • 类有一个const成员,它没有类初始化且其类型未显示定义默认构造函数,那么该类的默认构造函数是删除的。
      • 本质上,如果一个类有数据成员不能默认构造拷贝赋值或者销毁,那么对应的函数成员将会被定义为delete。

No.6 拷贝控制和资源管理

  • 一般来说,管理类外资源的类必须要定义拷贝控制成员。

    • 可以定义拷贝操作,让类的行为看起来像一个值或者指针。

    • 如果一个类像一个值,那么意味着它应该有自己的状态,当我们拷贝一个像值的对象的时候,副本和原对象是完全独立的。改变副本不应该影响母体。

    • 行为像指针的类则共享状态。当我们拷贝一个这样的类的对象的时候,副本和原对象使用相同的底层数据,改变副本也会改变原对象。

  • 行为像值的类

    • 每个对象都有一份属于自己的拷贝。

    • 你需要定义一个拷贝构造函数,完成拷贝而不是拷贝指针。

    • 定义析构函数。

    • 定义拷贝运算符释放当前的类,并从右侧运算对象拷贝。

    • class TestClassValue
      {
      public:
          TestClassValue(const string &s=string()):
          Ptr(new string(s)){}
          TestClassValue(const TestClassValue&my_class):
              Ptr(new string(*my_class.Ptr)){}
          TestClassValue& operator=(const TestDefaultClass&);
      private:
          string *Ptr;
      };
  • 类值拷贝赋值运算符通常结合了析构函数和构造函数的操作。类似析构函数,赋值操作先销毁左值的资源,然后从右侧运算对象拷贝数据。

  • 拷贝底层 -> 释放旧内存 ->将右值拷贝到本对象 -> 返回本对象

        TestClassValue& operator=(const TestDefaultClass&deal)
        {
            auto new_Ptr = new string(*deal.Ptr);
            delete Ptr;
            this->Ptr = new_Ptr;
            return *this;
        }
  • 当你编写一个赋值运算符,如果他要赋值给自身必要能够正常工作; 大多数赋值运算符组合了析构函数和拷贝构造函数。

  • 定义行为像指针的类

    • 这个时候你需要定义拷贝构造函数和拷贝赋值运算符。
    • 只有当最后一个指针也被销毁了的时候才会释放。
    • 你可以使用引用计数来方便的处理这种情况。
  • 引用计数

    • 除了初始化对象之外,每个构造函数还要创建一个引用计数来记录有多少个对象和正在创建的对象共享状态。计数器初始化为1。

    • 拷贝构造函数不重新分配计数器,二是拷贝给定对象的成员包括计数器,拷贝构造函数递增共享的计数器。

    • 析构函数递减计数器,指出共享状态的用户少了一个,如果为0那么析构函数释放状态。

    • 拷贝赋值运算符递增右侧的运算对象的计数器,递减左侧的计数器。

    • class TestClassCount
      {
      public:
          TestClassCount(const string&s = string()):
          Ptr(new string(s), i(0), useing(new size_t(1))){}
          TestClassCount(const TestClassCount& my_class):
              Ptr(my_class.Ptr),i(my_class.i),useing(my_class.useing)
          {useing++;}
          TestClassCount& operator= (const TestClassCount&);
      private:
          int i;
          size_t* useing;
          string* Ptr;
      };

No.7 类的动态内存管理

模拟STL Vector的设计

  • vector每个添加元素的成员函数会检查是否有足够多的空间来容纳元素,如果有那么会通过构造函数在下一个可用位置构造对象。如果没有,那么需要执行扩容: 将已有元素移动到新的空间,释放旧的空间,添加新的元素。
  • 我们在STrVec中采用类似的策略,使用一个allocator来获得原始内存,由于这块内存没有被构造,我们将需要添加的新元素用allocator的construct成员在原始内存中创建对象。类似的,删除元素我们使用destroy成员销毁元素。
  • 成员中包含三个指针成员
    • *element is head
    • *first_free
    • *cap is capacity
  • 除此之外,StrVec还有一个名为alloc的静态成员,其类型为allocator。alloc成员会分配StrVec的内存。
  • alloc_n_copy会分配内存,并拷贝一个给定范围的元素
  • free会销毁构造的元素并释放内存
  • chk_n_alloc保证至少有一个新元素的空间。如果没有,那么会调用惹allocate来分配更多内存。
  • 使用construct: 函数push_back调用chk_n_alloc确保空间容纳新的元素。如果需要,那么会调用reallocate。
  • free成员,free成员首先负责destroy元素,然后释放分配的内存空间。for循环,从构造的微元素开始,到首元素为止,逆序销毁所有元素。
  • 再重新分配内存的过程中移动而不是拷贝元素。
    • 为一个新的更大的数组分配内存
    • 在内存空间的前一部分构造对象,保存现有元素
    • 销毁原内存里面的元素,并且你要释放它
  • 移动构造函数和std::move
    • 移动构造函数通常是将资源从给定对象移动而不是拷贝到正在创建的对象。
    • move标准库函数,我们使用的时候直接调用 std::move因为一般不会提供using声明
  • 每次重新分配你可以分配两倍大小的内存空间

对象移动

  • 为了支持移动操作,新标准引入了新的引用类型,右值引用。所谓右值引用就是必须绑定到右值的引用。我们需要通过&&来获取右值引用。右值引用只能绑定在一个即将销毁的对象上。我们可以自由的将一个右值引用的资源移动到另一个对象中。

  • 左值和右值是表达式的属性。一些表达式生成或者要求左值,一些要求右值。一般来说左值表达式表示的是对象的身份,右值表达式表达的是对象的值。

  • 右值引用是某个对象的另一个名字。为了和常规引用分开,我们把普通的引用叫做左值引用。普通引用不能绑定转换的表达式,字面常量,或者返回右值的表达式。而右值引用的绑定特性与左值引用的绑定特性相反。顺便,const引用可以绑定在右值。

  • 左值持久右值短暂。右值要么是字面常量,要么是表达式求值过程中创建的临时对象。右值引用的对象即将要被毁灭,该对象没有其他用户。

  • 虽然不能将一个右值引用直接绑定在一个左值上,但是通过move函数我们可以改变这一情况。这会告诉编译器,现在的这个左值,你要去像右值那样处理它。

    int &&r = std::move(left);
  • 移动构造函数和移动赋值运算符

    • 这个时候是窃取对象的资源而不是拷贝资源。

    • 移动构造函数的第一个参数是该类类型的一个引用,不同于拷贝构造函数的是,这个引用参数在移动函数中是一个右值引用。与拷贝构造函数一样,任何额外的参数必须都有默认实参。

    • 窃取s的资源然后释放s

    • clna::clna(clna &&inpara) noexcept 
          : ele1(inpara.ele1), ele2(inpara.ele2)
          {
            inpara.ele1=inpara.ele2=nullptr;
          }
  • noexcept的含义是不要抛出任何异常。

  • 移动操作运算符的定义: 不是自己, 释放, 窃取。

  • 移动之后原对象必须是可以析构的。

  • 只有当一个类没有定义任何自己版本的拷贝控制成员,并且它的所有的数据成员都能移动构造或者移动赋值,那么编译器才会为它合成移动构造函数或者移动赋值运算符。

  • 定义了一个移动构造函数或者移动赋值运算符的类必须也定义自己的拷贝操作,否则默认这个定义是删除的。

  • 移动右值,拷贝左值。但是如果没有移动构造函数,右值也会被拷贝。

  • 所有五个拷贝控制成员应该视为一体,定义了一个就应当全部定义。

  • 建议不要随意使用move。

  • 一个函数同时用const和&修饰的话,你需要const &的顺序

第十章 类继承

No.1 基类与派生类

  • OOP概述
    • OOP的核心思想是数据抽象、继承以及动态绑定。数据抽象能够帮助我们将类的接口与实现分离; 继承可以定义相似的类型并对相似关系进行建模; 动态绑定可以再一定程度上忽略相似类型的区别,用统一的方式使用它们的对象。
    • 基类和派生类。基类负责定义所有类共同拥有的成员,而派生类在定义各自特有的成员。
    • 对于某些函数,基类希望派生类个自定义适合自身的版本,那么此时这些函数应当被定义为虚函数。你可以使用virtual关键字来声明虚函数。
    • 派生类需要通过类派生列表,知名它从哪些类继承。class child: public parent{};
    • 派生类必须在内部对所有重新定义的虚函数进行声明。派生类可以再这样的函数前面加上virtual关键字。C++11允许派生类显式注明它将使用哪个成员函数改写基类的虚函数,具体措施实在该函数的形参列表之后加override关键字。
    • 函数的运行版本是由实参决定的,因此动态绑定又叫做运行时绑定。
    • 我们是用基类的引用或者指针调用一个虚函数的时候会发生动态绑定。
  • 基类
    • 定义及类注意在你需要子类自适应的方法前面加上virtual关键字。
    • 作为继承关系里面root节点的类通常会定义一个虚析构函数。机师这个函数不执行任何实际操作也应当如此。
    • 成员函数与继承:
      • 对于需要覆盖的函数通常定义为虚函数。当我们使用指针或者引用调用虚函数的时候,这个调用将被动态绑定。根据引用或者指针所指向的对象类型不同,改调用可能执行基类的版本也可能执行子类的版本。
      • 基类通过成员函数的声明语句之前加上virtual使得该函数进行动态绑定。任何构造函数之外的函数都可以是virtual的。virtual只能应用在类的内部。如果一个基类的函数是virtual的,那么在子类中也隐式的为virtual。
      • 非虚函数的解析是发生在编译的时候而不是运行的时候。
    • 访问控制与继承
      • 子类可以访问外部不能访问的protected
  • 派生类
    • 大多数的类都只继承自一个类,这样的情况通常被称之为单继承。
    • 派生类对象及派生类向基类的类型转换
      • 我们可以将基类的指针或者引用bind到派生类对象中的基类部分。编译器会隐式的执行派生类到基类的转换。
    • 每个类,控制自己的成员初始化过程。派生类需要调用父类的构造函数去初始化它的基类部分。
    • 除非我们特别写清楚,否则派生类对象的基类部分会像数据成员一样执行默认初始化。
    • 首先初始化基类的部分,然后按照声明的顺序依次初始化派生类的成员。
    • 派生类可以访问基类的public和protect
    • 遵循基类的接口,不要自定义基类的属性应当如何,而是调用基类的构造函数
  • 继承与静态成员
    • 每个静态成员只存在唯一实例
  • 派生类声明的时候不应该包含它的派生列表
  • 作为基类的类,它必须是已经定义了的,而不是仅仅声明。
  • 继承阻止,你可以通过final关键字修饰一个类,表示这个类到头了,不能被继承
  • 继承与类型转换
    • 我们可以将基类的指针绑定到派生类上。
  • 对象之间不存在类型转换
  • 存在继承关系的类型之间的转换规则:
    • 从派生类向基类的类型转换只对指针或者引用有效
    • 基类向派生类不存在隐式转换
    • 由于访问限制,派生类到基类的类型转换可能不可行

No.2 虚函数

  • 在我们使用基类的引用或者指针调用一个虚成员函数的时候回执行动态绑定。因为我们直到运行的时候才知道我们需要调用哪一个版本的虚函数。因此虚函数必须有定义。一般来说,如果我们不是用函数的话,可以只声明这个函数。但是虚函数必须要提供完整的定义。
  • 虚函数的调用可能在运行时才会被解析。
  • 动态绑定只有指针或者引用调用虚函数的时候才会发生。因为我们调用一个普通对象的虚函数的时候,我们完全可以直接确定你要调用的版本。
  • 通过指针或者引用调用虚函数才会在运行的时候解析调用,也只有这种情况下,动态类型才有可能与静态类型不同。
  • 基类中的虚函数在派生类隐式为虚函数,当派生类覆盖了某个虚函数的时候,该函数在基类中的形参必须与派生类中的形参严格匹配。
  • 当我们使用override说明符修饰一个派生类的虚函数的时候,如果我们的参数列表或其他失误导致父类的虚函数没有被覆盖,这个时候会报错。
    • override可以检测参数列表的错误,但是你只能把它应用于父类存在而且是虚函数的方法。
    • 如果你指定了一份final,那么后面的任何一个类都不允许去覆盖它。这个方法已经定死了。
  • 你可以通过域名运算符直接指定你要调用的函数的版本
  • 一般来说只有成员函数或者友元函数才需要来回避虚函数机制。
  • 如果一个派生类虚函数需要调用基类版本的虚函数,如果你没有使用作用域运算符,那么在运行的时候该调用会被解析成自身无限递归。
  • 表达式的静态值编译的时候已知,而动态值在运行时可知。由于我们允许将父类类型的指针绑定在子类上,于是这个引用的静态类型和动态类型可能不同。调用的时候已知,于是应当由动态类型决定你当前调用那个版本的虚函数。

No.3 抽象基类

  • 纯虚函数
    • 我们可以将一个函数设计成纯虚函数。这样相当于告诉用户这个函数是没有实际意义的。和普通的函数不一样,一个纯虚函数不需要定义。
    • 在类的内部,参数列表之后分号之前加上 =0就可以定义一个纯虚函数了。
  • 含有或者没有经过集成覆盖的纯虚函数的类就是一个抽象基类。抽象基类负责定义接口,而后续的其他类可以覆盖这个接口。我们不能直接创建一个抽象对象,但是我们可以定义覆盖了纯虚函数的派生类的类对象。
  • 派生类构造函数只初始化它的直接基类。
  • 重构
    • 重构负责重新设计类的体系。

No.4 多态(静态绑定和动态绑定)

  • 多态允许你将父类对象设置成和一个或者更多的子对象相等的技术。允许将子类指针赋值给父类指针。(一个接口多种方法)
  • 编译时多态(静态多态)是通过重载函数实现的; 运行时多态(动态多态)是通过虚函数实现的。
  • 多态的作用:
    • 封装可以使得代码模块化,继承可以扩展,他们的目的都是代码重用。但是多态的目的是接口重用。也就是说,无论传递来的是什么,函数都能通过同一个接口自适应到各自对象的实现方法。
  • 多态的用法
    • 声明基类指针,绑定子类,调用虚函数。于是就可以根据指向的子类实现不同的方法了。如果不使用虚函数,则总是会调用父类的方法。
    • 当然你也可以通过函数重载来实现静态多态。
  • 隐藏规则(重写)
    • 同名函数,参数不同,有virtual,基类函数隐藏。
    • 同名函数,参数相同,没有virtual,隐藏。

No.5 访问控制与继承

  • 对于project, 派生类的成员或者友元只能通过一个派生类对象去访问基类的protect, 派生类不能直接访问基类的protect。
  • protect并不会影响派生类的访问权限。
  • 派生类向基类转换的可访问性
    • 假定child继承自base;
    • 只有c公有继承b,用户代码才可以令派生类向基类转化。如果c继承b的方式是其他,那么你不能转换
    • 无论如何继承,c的成员函数和友元都能使用派生类向基类的转换; 派生类向其直接基类的类型转换对于派生类的成员和友元来说永远是可以访问的。
    • 如果公有或者保护继承,那么c的派生类成员和友元可以使用c向b的类型转换; 反之不行。
  • 友元与继承
    • 友元关系不能传递,友元关系也不能继承。基类的友元在访问派生类的成员的时候没有特殊性; 类似的,派生类的友元也不能随意访问基类的成员。
    • 每个类负责控制自己成员的访问权限。所以如果A是Base的友元类,那么A可以访问B的成员, 于是如果C是继承B的,那么A是可以访问C中的来自B的成员
    • 不能继承友元关系; 每个类负责各自的访问权限
  • 你可以通过 using 类名::属性; 到对应位置上,来改变个别成员的可访问性。
  • 派生类只能为他可以访问到的名字提供using声明。
  • class的继承默认私有,struct的继承默认公有。

No.6 继承中的类作用域

  • 如果一个名字不能解析,编译器会依次向外层寻找。
  • 派生类的作用域位于基类之内。
  • 定义在内存作用域(派生类)的名字将会隐藏定义在外层作用域的名字。当然你可以使用作用域运算符使用隐藏的成员。
  • 为什么基类与派生类的虚函数必须相同形参:
    • 如果实参不同,我们就无法通过父类引用或者指针调用派生类的虚函数。
  • 你可以使用 using 声明调入重载函数(不指定形参列表), 然后派生类里面可以覆盖你需要的函数。

No.7 构造函数与拷贝控制

  • 虚析构函数
    • 为了确保正确析构,例如一个Child继承了Base, 我们delete Base* 的Child对象,为了正确找到析构函数,我们需要能够进行动态绑定。运行时找到正确的析构函数。
  • 派生类中删除的拷贝控制与基类的关系
    • 如果默认构造函数,拷贝构造函数,拷贝复制运算符或者析构函数是被删除的或者不可访问的。那么派生类中对应的成员是被删除的。
    • 如果基类中包含不可访问或者删除的析构函数,那么派生类里面的河中的默认和构造拷贝构造函数将是被删除的,因为编译器无法销毁派生类对象的基类部分。
    • 编译器不会合成一个删除掉的移动操作。当我们default一个移动的时候,如果基类的对应操作是删除的,那么派生类也是删除的,原因是派生部分不允许移动。同样如果基类的析构函数是删除或者不能访问,那么派生类的移动构造函数也将被删除。
  • 基类不含有移动操作,那么也会阻止派生类的移动。
  • 派生类的拷贝或者移动构造函数可以先用父类的函数,然后在写自己的
  • 书写子类的拷贝操作符时,你必须显示的为基类赋值。
  • 派生类的析构函数只需要销毁自己负责的部分。
  • 如果构造函数或者析构函数调用了某个虚函数,则我们应该执行与构造函数或者析构函数所属类型相对应的虚函数版本。
    • 例如,当基类的构造函数调用派生类虚函数很可能导致coredump。执行基类构造函数的时候,派生类还没有初始化。
  • 你可以用using语句"继承"父类的构造函数。但是这样做不会改变构造函数的级别。在父类中的级别直接作用于子类,不受转变的影响。且这个时候不兼容explicit或者constexpr
  • 构造函数的默认实参不会被保留下来。它会保留下来包含全部形参的构造函数以及仅包含不含有实参部分的构造函数。

第十一章 IO库

  • istream 输入流
  • ostream 输出流
  • cin 标准输入
  • cout 标准输出
  • cerr 输出错误信息
  • getline 读取一行

No.1 IO类

  • iosteram定义了读写流的基本类型

  • fstream定义了读写命名文件的类型

  • sstream定义了读写内存string对象的类型

  • 为了兼容宽字符对象,操纵例如wchar_t类型 你可以使用wcin wcout wcerr

  • IO对象不存在拷贝和赋值,也因此我们不能将形参或者返回类型设置为流类型,读写一个IO会改变其状态,传递和返回的引用也不能是const

  • 格式输出:

        cout<<fixed<<setw(8)<<setprecision(3)<<10.0<<endl;

条件状态

  • 确定一个流的状态的最简单方法 while(cin >> word)

  • practice1

  • istream& TestPractice8(istream& in)
    {
        //输入一个流的引用,遇到文件终止符结束
        //返回之前将流重置,使之有效
        string my_list;
        while(in>>my_list)
        {
            cout<<my_list<<endl;
        }
        in.clear();//流的置位
        return in;
    }
  • 什么时候 while(cin>>i) 会终止?

    • badbit 崩溃的流
    • failbit IO操作失败
    • eofbit 文件终止符

管理输出缓冲

  • 导致缓冲刷新,即数据真正写到输出设备或者文件的原因有很多
    • main的return操作的一部分,缓冲刷新被执行
    • 缓冲区满
    • endl 显示刷新
    • unitbuf可以用来设置流的内部状态,用来清空缓冲区。默认cerr是设置unitbuff属性的,因此写到cerr的都是立即刷新的。
    • 一个输出流被关联到领一个流。默认cin和cerr都关联到cout,因此读cin或者写cree都会导致cout的缓冲区刷新。
  • 除了endl,flush和ends都可以对缓冲区刷新。 flush 输出后刷新,不附加任何额外字符 ends 输出后增加一个空字符 然后刷新缓冲区
  • 每次输出操作都刷新,那么你在某处指定ubitbuf 直到你的下一个 nounitbuf
  • 如果程序异常终止,那么输出缓冲区不会被刷新。因为在崩溃的时候数据可能正在等待打印;
  • 交互式系统的输入输出都应当相互关联。这样所有输出以及提示信息都会在读操作之前被打印出来。
  • tie方法
    • 其包含两个版本,一个是不带参数的。此时返回指向输出流的指针。如果本对象当前关联到了一个输出流,那么返回的就是指向这个流的指针,否则则返回空指针。
    • 也可以接受ostream的指针,此时将自己关联到ostream。即x.tie(&o) x关联到输出流o

No.2 文件的输入输出

  • fstream是继承自iostream 也就是说相同的函数你可以直接用fstream实现多态

  • ifstream in(iflie);    //构建一个ifstream
    ofstream out;          //输出文件流,但没有关联
    out.open(ifile+"myfile.text");    //打开指定文件
    if(out);    //打开检测
    in.close();    //关闭一个流
  • 你在新的打开之前应当记得关闭; 常常进行打开检测是一个良好的习惯

  • practice

    //打开一个文件按行读入
    string myfilename = "file.txt";
    ifstream in(myfilename);
    vector<string>context;
    if(in)
    {
        string a_element;
        while(getline(in, a_element))  //独立输入,只需要单词你可以in>>直接载入
        {
            context.push_back(a_element);
        }
    }
  • 文件模式

    • in 只读
    • out 只写
    • app每次操作前定位到结尾
    • ate打开文件后立刻去尾部
    • trunc截断文件
    • binary二进制的方法进行IO
  • 以out打开会丢弃已有数据,你可以定义app来阻止这样的请况

  • 为了保留文件内容,可以显示指定app模式、

    • ofstream app2("file", ofstream::out | ofstream::app);
    • 保留ofstream打开的文件中的已有数据,你可以显示app或者in

No.3 string流

  • 头文件sstream里面的类型定义也是继承与IOstream

  • 例如你从一个流读出了一行,你可以将这一行嵌套城string流

  • getline(cin, line);
    istringstream record(line);    //将记录绑定到行
    record>>info; //把record写进去
    //当然你也可以
    while(record >> word){...}

第十二章 顺序容器

No.1 顺序容器概述

  • 容器是特定类型的对象的集合,顺序容器提供控制元素存储和访问顺序的能力,这种顺序不依赖于元素的值,而是与元素存入容器的位置相对应。
  • 顺序容器提供了快速访问顺序元素的能力,但是相应的,在增、删、非顺序访问方面都有不同的性能损耗。
  • Type:
    • vector 可变大小数组,支持快速随机访问。尾部之外的位置插入或者删除会很慢。
    • deque 双端队列,支持快速随机访问。头部尾部的增删很快。
    • list 双向链表,仅支持双向顺序访问。任何位置插入和删除O(1)
    • forward_list 单向链表,只能单向顺序访问,增删速度很快
    • array 固定大小数组。能够快速随机访问,不能增删元素
    • string 与vector相似的容器,专门用来保存字符,随机访问很快,尾部的增删很快
  • 选择容器的基本原则:
    • 通常选用vector除非你有理由选择其它容器
    • 如果包含很多小元素,且额外开销很重,那么不应选择list或者forward_list
    • 要求随机访问,则应当使用vector或者deque
    • 如果只有读取输入的时候才需要在容器的中间位置插入元素,而后需要随机访问
      • 你可以通过append之后进行sort
      • 你也可以输入时使用list,然后拷贝到vector
    • 如果你无法确定使用什么,那么你可以使用vector和list的公共操作,使用迭代器避免随机访问。

No.2 容器库概览

  • 容器的定义均为模板类。
  • 顺序容器可以保存任何类型的容器。

迭代器

  • forward_list的迭代器不支持 -- 操作符。
  • begin标记了起始; end标记了结尾。 一般来说,取值类似range函数。[begin, end)
  • 对一个合法的迭代器,应当满足:
    • 如果begin和end相等,则迭代器为空
    • 如果不相等,那么至少包含一个元素,begin应该指向开头
    • 通过递增,可以使得 begin==end

容器类型成员

  • size_type 无符号整数型,足够保存这个容器类型最大可能容器的大小

  • 别名在泛型编程里面非常好用

  • Type:

    auto it1 = a.begin();
    auto it1 = a.rbegin();
    auto it1 = a.cbegin();
    auto it1 = a.rcbegin();
  • 如果你不需要更改,你应当使用const

容器定义和初始化

C c; //默认构造函数,如果是array,则c中元素按默认初始化方式初始化,否则为空。
C c1(c2); //c1初始化为c2的拷贝
C c1=c2; //c1初始化为c2的拷贝
C c(b, e); //c初始化为迭代器b和e指定范围中的元素的拷贝。
C seq(n); //seq包含n个元素,这些元素进行了初始化; 此构造函数是explicit的
  • 只有顺序容器的构造函数支持大小参数,关联容器并不支持
  • 顺序容器可以使用assign方法,实现元素替换。new.assign(old.cbeign(), old.cend())
  • 由于旧的元素被替换,因此传递给assign的迭代器不能只想调用assign的迭代器。
  • swap可以交换两个相同类型容器的内容。swap(a, b) 常数时间,交换的是引用?但是swap一个arry会真正进行元素交换
  • size方法查询数目; empty判断是否为空; max_size判断最大容纳数值;
  • 只有它的元素定义关系运算符,才能对容器进行整体关系运算

No.3 顺序容器的操作

  • 除了array,所有的标准库容器都提供灵活的内存管理。在运行的时候根据需要进行对容器大小的动态管理。

  • 常用操作表:

    • forward_list有自己专属的insert和emplace
      vector和string不支持push_front和emplace_front
      push_back() //返回空, 尾部插入
      emplace_back(args) //由args创建一个元素,并插入尾部,返回void
      push_front() //头插
      emplace_front(args) //头插, 由args创建元素
      element.insert(p, value) //在迭代器p的位置进行插入
      element.inset(p, args) //由args创建
      ele.insert(p, n, t) //插入n个元素t
      ele.insert(p, b, e) //将另一个容器b~e范围的元素插入到p之前(必须是另一个), 返回新的第一个元素, 对插入空集, 返回p
      ele.insert(p, {0,1,2}) //将花括号列表插入到p之前的元素,返回新元素第一个位置的迭代器, 对空返回p
  • 使用insert的返回值可以再某个位置反复的插入元素

  • 我们使用push_back会将元素对象传递给它们, 这些对象被拷贝到容器中。而当我们使用emplace的时候,则是将参数传给构造函数进行构造。即,在emplace_back的时候,会直接在内存里面创建元素; 而push_back则需要创建局部临时对象然后压入其中。但是使用emplace的时候,必须注意与构造函数的类型相匹配。

  • 某个迭代器iter.front()或者back()可以查找前或者后的元素;其中,函数的返回值都是引用

  • iter.at(n) 返回元素n所在位置的引用

删除

  • 常用操作表

    • pop_back() //弹出尾部, 返回void
      pop_front()  //弹出头, 返回void
      erase(p)  //删除p位置的元素, 返回删除之后的元素位置
      erase(b, e)  //删除范围元素, 返回删除后元素的位置
      clear()  //清空
  • 删除deque首尾之外的元素都会使得迭代器失效; 删除vector或者string的元素, 原本后面的迭代器都会失效。你应当删除一个存在的元素。

  • foward_list 单独定义了 inser_after emplace_after 和 erase_after

变动容器大小

  • c.resize(n) c.resize(n, val) reseize大小为n, 初值为val。如果缩小容器,多余的值会被丢弃,并且指针失效; 对vector string deque进行任何resize都可能导致指针失效

指针失效

  • 增加元素引起失效:
    • 对vector和string, 若存储空间重分配, 则引用失效。如果未重分配,则插入位置之后的迭代器失效。
    • 对deque,除非对首尾元素才做,否则指针失效。如果在首地址插入,则迭代器失效。但是此时指向存在元素的引用和指针不会失效。
    • 对于list forward_list 迭代器与指针均有效
  • 删除
    • 对list或者forward_list无影响
    • 对deque,对首尾之外操作,则失效; 删除尾元素,尾元素后面的迭代器失效。但是其他位置不受影响。删除首元素这些不会影响。
    • 对vector和string, 删除位置之后的失效;
  • end由于经常失效,因此注意不要保存end

No.4 Vector的增长

  • 容器大小管理

    • shrink_to_fit() //修剪空间,与令apcity与实际size()一样大
      capacity() //查询容纳体积
      reserve(n) //应当至少预留n个元素的空间
  • 只有迫不得已才会进行内存重调

No.5 String

构造一个string

  • 基本方法

    • string s(cp, n); //s是cp指向的数组的前n个字符的拷贝
      string s(s_1, pos); //s是 string s_1从pos位置开始直到结束的拷贝。
      string s(s_1, pos, len_1); //s是pos开始长度为len_1的拷贝, 至多拷贝到结尾。
  • substr返回一个string, 它是原始string的一部分拷贝。例如 s = c.substr(0,5), 或者 c.substr(6) 都表示拷贝前6个元素(从0开始的n个字符的拷贝)。 c.substr(0,)这是一个错误的,虽然py可以缺省[0:], 但是显然C++不允许这样做

  • s.assign(cp, 7) s是cp的前7个的复制, 其中cp可以使const char*

查找操作

  • find("test") 查找匹配到的, 返回第一个字符的下标, 否则返回npos
  • rfind() 查找最后一个位置

数值转换

  • to_string(i) //将i转化为string
  • stod(s) //将s转化成double

第十三章 关联容器

No.1 关联容器概述

  • 顺序容器按位置,关联容器按键访问

  • map 键值; set集合。

  • 标准库关联容器

    • //关键字保存
      map    //key-value
      set    //关键字即值
      multimap    //允许关键字重复
      multiset    //允许重复
      //无序集合
      unordered_map    //基于hash的map
      unordered_set    //基于hash的set
      unordered_multimap    //基于hash的map, 但关键字可重复
      unordered_multiset    //匀速重复
  • 简单的定义和应用一个map

    • map<string, size_t>test;
      test["My_Test"]=0;
      ...
      for(auto &Refrence_Of_Test:test)
      {
          Refrence_Of_Test.first //代表当前map的key
          Refrence_Of_Test.second //代表当前map的value
          //这是由于map包含pair, 而pair是以first和second两个公有成员存储key&value的
      }
  • set的使用

    • set<string>Exclude = {"The", "Exclude", "World"};
      if (Exclude.find("My_Test") == Exclude.end()){...}
      //在这里, find== end证明你排除了不该有的World, 即上述字符串不包含在Exclude

Introduction

  • 关联容器支持普通容器的操作, 但关联容器不支持与位置相关的操作, 例如push_back等
  • 关联容器也不支持构造函数或者插入操作这些接受元素值和数量的操作
  • 关联容器的迭代器是双向的

Define

  • 定义map需要制定key和value的类型
  • 定义set只需要key的类型
  • 每个关联容器都有默认的构造函数。我们可以将一个关联容器初始化为另一个同类型的拷贝, 也可以利用值范围初始化关联容器, 但要注意类型符合。
    • map<str, str>test={{"A","A_V"}, {"B", ""B_V}};
    • set<int> MySet(icp.cbegin(), icp.cend());

Type Limit

  • 有序容器: map set multu.., 必须定义元素的比较方法。

  • 自定义类型的multiset的初始化

    • multiset<ScalarT, decltype(compareIsbn)*>Test(Compare_Function);
      //你在初始化的时候调用自定义比较函数, 在这里, 如果你使用一个函数名作为参数, 那么它会在你需要的时候转化成函数指针

pair

  • 它包含在utility中, 数据成员是public的。其名字分别为first和second。
  • 特殊的 make_pair(V1,V2) 会返回一个pair (你可以把它用在return里面)

No.2 关联容器的操作

Introduction

  • key_type 容器的关键字类型
  • mapped_type 映射的类型
  • value_type 值的类型, 其中, 对map它是一个pair<const key_ytpe, mapped_type>
  • set<string>::value_type v1; v1是一个string

关联容器的迭代器

  • 对map的迭代器, 他是一个pair。但是注意key也就是first是const的, second是可变的

  • set的迭代器是const, 只能访问不能修改

  • 你依然可以用之前的迭代器方法遍历一个关联容器

    • auto iter = Test.begin();
      while(iter!=Test.end())
      {
          ...;
          ++iter;
      }

  • set_Test.insert({1,2,3})
  • set_Test.insert(another.cbeign(), another.cend())
  • 对一个map进行增加元素, 你必须增加一个pair, 你可以使用make_pair确保这一条件, 当然你也可以value_type
  • emplace, 只有关键字不存在才会插入
  • insert和emplace会返回一个pair, 第一个是指向插入元素的迭代器, 第二个是插入结果是否成功的bool
  • 对于multi, 插入就不会返回bool了

  • erase返回0代表删除的元素根本不在里面

  • 你可以通过[] 下标来找到一个 name[key] 对应的就是value
  • 但你不能对multi进行这样的查找操作
  • name.at(k) 关键字k在不在c里面, 如果不在, 会返回宜昌
  • map的下标操作会得到mapped_type 但如果你对map的迭代器解引用, 你会得到value_type
  • map的下标运算会得到一个左值
  • find(k) 返回指向k的迭代器, 不存在则为iter.end
  • count(k) 返回k出现的次数, 不存在未0
  • lower_bound(k) 返回第一个关键字不小于k的元素的迭代器
  • upper_bound(k) 返回第一个key大于k的迭代器
  • equal_range(k) 返回一个pair是等于的范围, 对不相等则自动为.end迭代器

No.3 无序容器

  • 无序容器的底层是hash

  • 容器管理操作

    • bucket_count();  //正在使用的桶的数目
      max_bucket_count();  //最大的桶的数目
      bucket.size(n);  //第n个桶里面有多少元素
      bucket(k);  //k在那个桶
      
      load_factor();  //每个桶平均元素数量
      max_load_factor(); //平均桶大小, 在必要的时候会增加新桶一遍load<max_load
      rehash(n);  //重组存储使得正在使用的桶数>=n
      reserve(n);  //保存n个元素时不需要rehash

第十四章 泛型算法

No.1 概述

  • 大多数算法定义在头文件algorithm里面,标准库还在头文件numeric中定义了一组数值泛型算法。通常情况下,这些算法并不直接操作容器,而是遍历由两个迭代器指定的一个元素范围来进行操作。例如你在查找的时候

    auto res = find(vec.cbegin(), vec.cend(), val); //查找val是不是在vec里面, 是的话返回对应位置的引用, 否则返回end
  • 算法不依赖于容器,但是算法依赖于元素类型的操作

  • 泛型算法本身不会基于容器,而是基于迭代器; 算法基于迭代器就造就了算法不会直接增删元素,而是可以修改或者移动。

No.2 泛型算法基础

  • 标准库算法均会对输入范围的元素进行操作, 其中,输入范围定义为[begin, end)。基于这两个迭代器将会对你选定的元素进行操作。

只读算法

  • 仅读取元素但不会修改的一类算法。例如 find count
  • accumulate也是如此, 它定义在numeric中。它接收三个参数,前两个为输入范围,第三个参数是和的初值,返回范围内所有元素和初值的和。和的初值决定了accumulate的返回值类型以及所用 + 的类别

algorithm and element type

  • 由于const char* 没有定义相关的+运算符, 你在使用accumulate的时候,初始值应采用string类型。
  • 对于读取不可改变元素的算法最好使用cbegin和cend,但是如果返回的迭代器用来修改元素,那么应当使用普通版本的begin和end。

operate two sequence

  • equal算法用来确定两个序列的值是否相等。相等返回True否则为false。
  • 它接受三个参数, 输入范围以及第二个序列的首迭代器。且需要序列2的元素数量大于等于序列1。但不要求元素类别相同,当然前提是有定义好的==符。

写算法

  • 一些算法会修改序列中的元素。这要求我们注意容器的数量至少与写元素的数量相同。

  • fill(begin, end, val); 会将输入范围内的所有元素替换为val

  • 算法不检查写操作

    • 例如对fill_n操作一个空容器, 算法本身不能对容器扩容, 这会导致语句执行结果未定义。
    • 向目的位置迭代器写入数据的算法都假定目标位置足够大,能够容纳要写入的元素。
  • back_inserter

    • 插入迭代器能够保证算法有足够的元素空间容纳输出数据。

    • 当我是使用插入迭代器赋值,一个与等号右值相等的元素会被添加到容器里面。

    • 它定义在头文件iterator中的一个函数。

    • 它接受一个指向容器的引用(别名, 本名都可以),返回一个插入迭代器。当我们需要通过这个迭代器赋值,会自动调用push_back

    • vector<int> vec;
      auto it = back_insert(vec);
      fill_n(it, 10, 0);  //ok, 编译正确。
    • 展开来看实质就是将对it的所有*it= 替换成了push_back

  • 拷贝算法

    • 接收三个参数,输入范围,也是目的迭代器的起始与终止;新容器的首迭代器。你需保证新的容器足够大。
    • replace_copy(begin, end, 0, 42); 把所有0换成42并且输出一个新序列

排序

  • 去重复的排序

    • sort(begin, end); //直接在迭代器begin, end内进行排序
      auto end_unique = unique(begin, end); //将不重复部分放置前列,返回所有不重复的后置位
      my.erase(end_unique, end); //删除冗余的后半部分, 及时没有冗余部分也是安全的, 因为明显此时的end == end_unique
      // nlogn + n + n

No.3 定制操作

向算法传递函数

predicate 谓词

  • 谓词是一个表达式,它返回一个能用做条件的值。标准库算法接受一元谓词和二元谓词。X元代表这个表达式含有X个参数。接受谓词参数的算法对输入序列中的元素调用谓词,因此元素类型必须能转换为谓词的参数类型。一个简单的例子,基于string长度排行字典序。

    • class TestAgorithm
      {
      public:
          //你的比较函数必须是static或者全局函数, 不然会出现奇怪的报错
          static bool my_judgefunc(const string &a, const string &b)
          {
              return a.size()>b.size();
          }
          void judge(vector<string>&a)
          {
              sort(a.begin(), a.end(), my_judgefunc);
              for(auto &in : a)
              {
                  cout<< in<< endl;
              }
          }
      };
  • stable_sort: 稳定的排序算法。例如你可以: 按字典序排序, 然后消除重复单词, 按长度重排序, 那么相同长度的自动按照字典序go on

lambda表达式

  • 我们可以向一个算法传递任何类别的可调用对象。对于一个对象或者一个表达式,如果可以对其使用调用运算符 (args), 那么它是可调用的。例如函数, 函数指针以及lambda表达式

  • 我们可以把lambda表达式想象成一个没有名字的内联函数,它的类型如下:

    • [capture list](parameter list) -> return {func body}
    • 其中, capture是一个lambda所在函数中定义的局部变量的列表(通常是空)
    • 其余部分与函数定义类似, 但是lambda表达式必须使用尾置返回
  • lambda表达式不能含有默认参数;

    • auto f_lambda = [](const string& a, const string& b){return a.size()>b.size();}
      //空列表[]表示不适用函数的任何局部变量
      stable_sort(begin, end, f_lambda);
      
      //当然你也可以使用捕获参数, s_len是函数体内的局部变量。如果你不捕获,那么是不能使用s_len的
      auto f_lambda = [s_len](const string& a){return a.size()>=s_len;}
  • find_if: 输入范围, 以及lambda表达式。返回一个迭代器, 指出第一个满足条件的迭代器,没有满足条件的返回end

  • for_each 你可以利用它, 输入范围和lambda表达式进行直接输出

    • for_each(a.begin(), a.end(), [](const string s){cout<<s<<' ';});
  • 捕获列表只用于局部非静态变量, 静态变量与全局变量是可以直接使用的

  • lambda是创建是拷贝捕获的参数, 因此可以保留值。但是如果是引用, 那么运行时就要注意是不是存在的初值了。

  • 你可以在捕获列表写一个& = 让编译器推断你需要什么捕获。你也可以混合[&,stra]这样,&是引用,且会自动推断, stra是值而不是引用。

  • 为了改变一个被捕获的值拷贝变量的值,需要在lambda表达式增加mutable修饰

    • [V]()mutable{return ++v;};
  • 如果你的lambda包含多条语句,编译器默认返回void, 否则会进行推断。你可以使用尾置返回类型强制指定类型

    • [](int a) -> int {if(a) return a; else return a+1;}
  • 参数绑定

    • 我们可以轻易的使用函数替代lambda表达式的功能,但是显然,在需要一元谓词的地方这样做是困难的,为解决参数问题,标准库提供了bind函数。

    • 头文件functional包含了bind。可以将bind视为通用函数适配器。它接受一个可调用对象,生成一个新的可调用对象来适应原来的参数列表。

    • bind的一般形式 auto n_call = bind(fun_name, arg_list); n_call是一个可调用对象,arg_list是一个参数列表。

      • auto check = bind(check_f_name, _1, 6); //_1代表接受一个参数,有一个占位符 如果存在_2 就是两个占位符有两个参数
        //于是你可以将新的函数代替lambda表达式
        //占位符用于find_if的反省算法操作
        find_if(begin, end, bind(check_f_name, _1, 6));
    • placeholders。名字_n是占位符,它们定义在placeholders的命名空间里面。这个命名空间包含在std。为了使用这些名字,你需要引入std和placeholders。为了使用所有的_n, 你可以应用通用的using语句。 using namespace namespace_name;

    • bind的参数。bind可以用于修正参数顺序。

      • auto g = bind(f_name, a, b, _2, c, _1);
        //新的可调用对象把自己的第三个和第五个参数传给f_name的第一个和第二个参数。
    • 如果我们希望传递给bind一个对象而且不去拷贝它,那么应当使用ref函数。例如bind(printf, ref(os),_1); ref会返回一个对象,包含给定的引用。cref返回const引用。他们定义在functional里面。

No.4 迭代器

  • 除了容器迭代器,在标准库iterator里面还定义了其他的迭代器。
    • 插入迭代器
    • 流迭代器
    • 反向迭代器
    • 移动迭代器

插入迭代器

  • 插入迭代器是一种迭代器适配器,它接受一个容器,生成一个迭代器,能够实现向给定容器添加元素。
    • it=t在it指定的地方插入t
    • *it ++it it++ 不会对it做任何事, 只会返回it
  • back_insert 创建一个push_back插入迭代器
  • front_insert push_front
  • inserter 创建一个基于insert的迭代器。这个函数接受第二个参数,这个参数必须是一个指向给定容器的迭代器。元素将被插入到给定迭代器所表示的元素之前。

iostream迭代器

  • istream_iterator 读取输入流, ostream_iterator向输出流写数据。通过使用流迭代器,我们可以用泛型算法从流对象读取数据以及向其写入数据。

istream_iterator操作

  • 标准输入读取数据存入vector

    • istream_iterator<int>in_iter(cin); //从cin读取int
      istream_iterator<int>eof; //istream的尾后迭代器, 这是一个空的迭代器
      while(in_iter != eof) vec.push_back(*in_iter++);
      
      //你可以简化程序
      istream_iterator<int>in_iter(cin), eof;
      vec.push_back(in_iter, eof);
  • 你可以对算法使用流迭代器 accumulate(in_iter, eof, 0);

  • 我们可以对任何具有输出运算符<<的类型定义ostream_iterator。

    • ostream_iterator<int>out_iter(cout, " "); //第二个参数可选。如果你选择,那么每次输出之后会增加space后缀
      for(auto e:out_iter) *out_iter++ = e; //用<<运算符将e写入out_iter绑定的流对象
      //这里面,实际上你可以忽略解引用和递增运算。 out_iter = e;在循环体内部也可完成这个需求
  • * ++ 实际上对ostream_iterator对象不做任何事情,因此忽略它们对我们的程序没有任何影响。但是推荐使用繁琐的写法,表达相对清晰。

  • 上述需求使用copy来打印元素更为方便

    • copy(vec.begin(), vec.edn(), out_iter);
  • 使用流迭代器还可以处理类类型

    • 我们可以为任何定义了输入运算符>>的类型创建流迭代器对象。

反向迭代器

  • ++it会向前移动; --it会向后移动。我们可以调用rbegin rend crbegin crend来获得反向迭代器。
  • 一般的反向迭代器是支持递减运算符的,但是流迭代器除外
  • sort(rbegin, rend); 会对容器逆序排序

No.5 泛型算法结构

  • 算法所要求的迭代器操作可以分为5个迭代器类别
    • 输入迭代器 读 不写 单次扫描 只能递增
    • 输出迭代器 写 不读 单次扫描 只能递增
    • 前向迭代器 可读写 多扫描 只能递增
    • 双向迭代器 可读写 多扫描 可增减
    • 随机访问迭代器 可读写 多扫描 支持所有迭代器运算
  • _if算法。find查找val出现的第一个位置; find_if查找令一个表达式为真的元素位置
  • _copy拷贝原宿版本, 会返回一个完全的元素。

第十五章 动态内存

No.1 动态内存与智能指针

  • 到目前为止,我们编写的对象都有严格的生命周期。全局对象在程序启动时分配空间,在程序结束时销毁。局部自动对象,当我们进入程序时创建,离开块被销毁。static在第一次使用之前被创建,程序结束时销毁。C++支持动态创建对象, 它的生命周期与创建地点无关,只能显示释放销毁。
  • 动态对象的正确释放是容易出错的点,为了安全使用动态对象,标准库定义了智能指针类型动态管理分配的对象。当一个对象应该被释放,指向它的智能指针可以确保自动地释放它。
  • 静态内存用于保存局部static、类static以及任何定义在函数之外的变量; 栈内存用于保存定义在函数内的非static对象。这两部分的内存管理由编译器自动掌控。对于栈对象只有程序运行的时候才存在, 对于static对象, 在使用之前分配在程序结束的时候销毁。
  • 除了静态内存和栈内存,每个程序还拥有内存池。这部分为自由空间/堆,程序用堆来存储动态分配的对象。存储在这里的对象需要我们去显示销毁。
  • shared_ptr允许多个指针指向同一个对象; unique_ptr独占所指向的对象。标准库还定义了一个名为weak_ptr的伴随类,它是一种弱引用,指向shared_ptr所管理的对象。这三个类型都定义在memory头文件中。

shared_ptr

  • 智能指针也是模板。

    • shared_ptr<string> p1;
      shared_ptr<list<int>>p2;  //指向int类型的list
  • 解引用一个智能指针会返回它引用的对象。

  • 智能指针的通用操作

    • p.get();  //返回p保存的指针。注意如果智能指针释放了其指向的对象,那么返回的指针所指向的对象也就消失了。
      
      swap(p, q);  //交换p 和 q中的指针
      p.swap(q); 
  • shared_ptr独有

    • make_shared<T>(args);  //返回一个shared_ptr, 指向一个动态分配的T类型对象, 使用args初始化对象
      shared_ptr<T>p(q);  //p是shared_ptr q的拷贝此操作会使得q的计数器递增。要求q中的指针必须能够转换为T*。
      p = q;  //p q都是shared_ptr, 所保存的指针需要能够相互转换。此操作会递减p的引用计数,增加q的引用计数。若p的引用计数变为0,则会释放p。
      p.unique();  //若p引用了一次则返回True 否则false即p.use_count()==1?
      p.use_count();  //查询可能会非常慢,用于调试。
      shared_ptr<T>p(u);  //p从unique_ptr处接管对象的所有权,并把u制空
      shared_ptr<T>p(q, d);  //p接管了内置指针q指向对象的所有权。这里要求q必须能转化为*T的类型。p将使用可调用参数d来代替delete
      p.reset();  //若p是唯一指向, 则释放内存
      p.reset(q);  //p指向q
      p.reset(q, d);  //这里将会调用d来释放p而不是用delete
  • make_shared函数是很安全的标准库函数。这个函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr。这个函数也存在于memory里面。使用auto类型接受返回值是方便的。

  • 一旦一个关联计数器,也就是引用计数,归0的时候,shared_ptr就会被释放。

直接管理内存

  • new (nothrow) int; //如果分配失败返回空指针
  • 坚持使用智能指针,因为悬空指针包含了野指针所有的缺点。

定义自己的删除器

  • 当我们创建一个shared_ptr的时候,可以穿第一个指向删除器函数的参数。这样p被销毁的时候不会对自己保存的指针执行delete, 而是调用我们自制的删除器。

    • {
      	shared_ptr<Type> p(c, delfuncname);
      } //这样当块结束的时候,即使是异常退出,这块内存也会被释放。这不同于new和del,对于new和del,如果中间发生了异常,那么这部分对于函数外部来说就是遗失的内存了。
  • 为了正确使用智能指针,你需要坚持一些基本规范。

    • 不适用相同的内置指针值初始化或者reset多个职能指针
    • 不要delete ge()返回的指针
    • 如果你使用get()返回的指针,记住最后一个对应的智能指针销毁后,你的指针就是无效的了
    • 如果你使用智能指针管理非new分配的资源,记得传递删除器。

unique_ptr

  • 独享指针在它本身被delete的时候,它指向的对象也会被销毁。

  • 我们使用它的时候需要把它绑定在new出来的对象上

    • unique_ptr<int> p(new int(32)); //p指向一个值为32的int
  • 由于unique_ptr拥有它指向的对象,因此它不支持一般的拷贝或者赋值

  • unique_ptr的操作

    • unique_ptr<T,D> u; //u指向类型T的对象, 它定义了一个删除器D
      unique_ptr<T,D> u(d); //空的u, 指向T类型对象, 使用D的对象d来进行析构操作
      u = nullptr; //释放底层u指向的对象
      u.release(); //u放弃对指针的控制, 返回指针, 将u空置
      
      u.reset(); //释放资源
      u.reset(q); //u指向新的对象q; 否则将u空置
      u.reset(nullptr); 
  • 不能拷贝存在一个例外,那就是我们可以拷贝或者赋值一个将要被销毁的unique_ptr。例如从函数里面返回一个unique_ptr

weak_ptr

  • 一种指向shared_ptr管理的对象的弱引用指针,不会对计数造成影响。如果所有智能指针已经消失,无论weak_ptr的形态如何,此时底层对象都会被析构。
  • 创建弱指针的时候我们需要shared_ptr来初始化这个指针weak_ptr<int> wp(shar_p);
  • 由于对象可能不存在,此时我们需要使用lock方法来验证这个东西是否存在,能否使用。

No.2 动态数组

  • int *p = new int [10];    //此处没有初始化
    int *p = new int [10]();    //此处初始化
  • 动态分配一个空数组是合法的,但是你不能对那个返回的指针解引用。

  • 释放

    • delete p; //p必须指向一个动态对象或者空
      delete []pa; //pa应当指向数组或者空, 数组元素逆序销毁
  • unique_ptr<int[]> u(new int[10]); //u指向10个未初始化int的数组

  • u[i] 返回u指向的数组第i处的值

  • 但shared_ptr不支持管理数组,如果你想使用它,那么你应该为他增加一个自定义的删除器

allocator类

  • 定义在memory里面,帮助我们将内存分配和对象构造分离开。它分配一块原始的内存。
  • 它会自动根据类型来确定恰当的内存大小和位置对齐。
  • allocator<string> alloc; //可以分配string的allocator对象
  • auto const p = alloc.allocate(n); //分配n个位初始化的string
  • 你可以用construct(p, args); 你可以用这个方法来对分配的原始内存进行初始化。p为当前内存的位置,args为适应构造函数参数。