这段时间又刷了一遍C++ Primer (第五版),把之前尚未完全理解以及重要内容记录在本文中。

一些概要

  • C++之所以脱离C而存在,还是因为其强大的抽象能力。C++同时支持4中不同的编程风格:C风格、基于对象、面向对象和泛型。

  • 利用无符号数来写循环,很可能不经意间就会出现死循环。

C++基础

  • 区分初始化赋值,尤其是用“=”号来进行初始化的情况。

    初始化:

    C++新标准中,用花括号的形式进行“列表初始化”得到了全面应用。但当用于内置变量时,这种初始化有一个重要特点:如果使用列表初始化,且初始值存在丢失信息的风险,则编译器将报错:

    1
    2
    3
    long double ld = 3.1415926536;
    int a{ld}, b={ld}; // 错误:存在信息丢失的风险,double->int
    int c(ld), d=ld; // 正确:因为用非列表初始化的形式进行初始化

    若定义变量时没有指定初值,将被默认初始化,变量被赋予“默认值”:定义于任何函数体之外的变量被初始化为0,而定义在函数体内部的内置数据变量将不被初始化,如果试图拷贝或以其他形式访问此类值将引发错误。

    在新标准下,现在C++程序最好使用nullptr为指针类型初始化空指针,同时尽量避免使用NULL。

  • 分离式编译机制:允许将程序分割为若干个文件,每个文件可以被独立编译。

    C++将声明(declaration)定义(definition)区分开,声明使名字为程序所知,一个文件如果想使用别处定义的名字则必须包含对那个名字的声明;定义则负责创建与名字关联的实体。

    要声明一个变量而非定义,就在变量名前添加关键字extern,而且不要显式地初始化变量。

    不过注意,在函数体内部初始化一个由extern关键字标记的变量,将引发错误。函数体内可以声明并使用extern变量,但是去初始化/定义它,那么在离开函数体后改变量的作用域就失效了,所以会引发错误。

  • const关键字:

    const引用:引用的类型必须与其所引用对象的类型一致,且不能为非常量引用绑定字面值,但有两个例外:(1)在初始化常量引用时允许用任意表达式作为初始值,只要该表达式结果能够转换成引用的类型即可;(2)对const的引用可能引用一个非const的对象。

    注意,const引用仅限制了对引用的操作(不能修改所引用的值,注意,引用本身就是从一而终的,只能在定义时初始化一次,后面不可改变而引用其他变量),但并未限制所引用的对象是否为常量。

    const指针:分为(1)指向常量的指针:不能改变所指向对象的值;(2)常量指针:不能改变指针的指向。指向常量的指针并没有规定所指对象是否一定为常量。

    1
    2
    3
    4
    const double pi = 3.14;
    const double *cptr = π // 指向常量的指针
    double *const cp = π // 常量指针
    const double *const pip = π // 指向常量的常量指针

    这里其实可以抽象出两个独立的问题,即顶层const:表示指针/基本类型本身是一个常量,和底层const:表示指针所指的对象是一个常量。C++11中的constexpr会将所定义的对象置为顶层const。

    区分 顶层const 和 底层const 很重要。

  • 处理类型

    类型别名:有两种方法定义类型别名,(1)传统的typedef,(2)新标准中的别名声明

    1
    2
    typedef double base, *p;           // typedef定义类型别名,base是double同义词,p是double*同义词
    using LL = long long; // using别名声明

    auto类型说明符:C++11新标准引入auto,能让编译器替我们分析表达式所属的类型,显然auto定义的变量必须有初始值。其次,auto一般会忽略顶层const,而保留底层const,如果希望推断出auto类型为顶层const需要显示指出“const auto ...

    1
    2
    3
    4
    5
    const int ci = i, &cr = ci;
    auto b = ci; // b是个整数
    auto c = cr; // c是个整数
    auto d = &i; // d是个整型指针
    auto e = &ci; // e是一个指向整数常量的指针

    decltype类型指示符:C++11新标准引入decltype,能够不调用操作而选择操作返回的数据类型。

    decltype与auto的不同:(1)如果decltype使用的表达式是一个变量,那么返回该变量的类型(包括顶层const和引用);(2)decltype的结果类型与表达式形式密切相关,如果给变量加上了一层或多层括号,这样就会得到引用类型。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    decltype(f()) sum = x;       // sum的数据类型就是函数f返回的类型

    const int ci = 0, &cj = ci;
    decltype(ci) x = 0; // x类型为const int
    decltype(cj) y = x; // y类型为const int&
    decltype(cj) z; // 错误,z是const int& 类型,必须初始化!

    decltype((i)) d; // 错误,d是int& 类型,必须初始化
    decltype(i) e; // e是int类型
  • 头文件

    预处理器(preprocessor)常用来确保头文件多次包含仍能安全工作。包括:

    1
    2
    3
    4
    5
    #ifndef xxx_H             // 当且仅当变量未定义时为真
    #define xxx_H // 把一个名字设定为预定义变量
    #include<string> // 头文件替代
    ...
    #endif // 结束预处理命令

    头文件中一般不包含using 声明命名空间。

  • 标准库类型:string

    string表示可变长的字符序列,包含在头文件string中。初始化有如下几种方式:

    1
    2
    3
    4
    string s1;              // 默认初始化
    string s2(s1); // s2是s1的副本
    string s3("value"); // s3是字面值“value”的副本
    string s4(n, 'c'); // s4初始化为连续n个字符c组成的串

    string.size()返回size_type类型,略微复杂,但可以理解为一个unsigned类型的整数,可以用autodecltype来推其类型。

    可以用字面值常量与string相加,但是两个字面值不能相加。

    若要单独处理string中的字符,可以利用cctype头文件中定义的标准库函数。例如:

    1
    2
    3
    4
    5
    6
    isalnum(c)                 c是字母或数字时为真
    isalpha(c) c是字母时为真
    isidigit(c) c是数字时为真
    tolower(c) 转换为小写
    toupper(c) 转换为大写
    ...

    可以使用范围for语句(C++新标准)来处理string中的每个字符,例如:

    1
    2
    3
    string str("some string");
    for(auto c : str)
    cout << c << endl;
  • 标准库类型:vector

    vector模板定义和初始化的方法:

    1
    2
    3
    4
    5
    vector<T> v1;             // v1是一个空的vector,元素类型为T,默认初始化
    vector<T> v2(v1); // v2中包含v1所有元素的副本
    vector<T> v3(n); // v3包含了n个元素,每个元素的值默认初始化
    vector<T> v4(n, val); // v4包含了n个元素,每个元素的值为val
    vector<T> v5{a,b,c..}; // v5初始化为a,b,c.. 注意,列表初始化要用大括号而不是圆括号

    注意,圆括号和大括号,圆括号用来构建vector对象,而大括号用来进行列表初始化

    运行时向vector添加成员:push_back,向尾端(back)添加元素。

    思考:vector能够高效增长,那么vector是如何提升动态添加元素的性能的呢?(暂时先留着这个问题)

    vector使用时的一些编程假定:

    • 如果循环内包含有向vector对象添加元素的语句,那么就不能使用范围for循环。
    • 只能对已存在的元素执行下标操作,若vector为空是不能执行下标操作的。
  • 迭代器

    容器类型一般都有迭代器,能够提供对于对象的间接访问。迭代器由名为beginend的成员函数获得。

    1
    auto b = vec.begin(), e = vec.end();

    b表示vec的第一个元素,而e表示vec尾元素的下一个位置,这是容器的一个本不存在的“尾后”元素,没有实际意义,仅是一个标记。注意,如果容器为空,那么begin和end返回的是同一个迭代器,都是尾后迭代器。

    迭代器使用 == 和 != 来判断两个迭代器是否相等,支持++,– , +/- 整数,以及迭代器之间的减法操作;使用解引用 * 可以获得迭代器所指对象的引用。const_iterator类型是只读迭代器。

    某些对vector对象的操作会使迭代器失效!

    vector的动态增长会带来一些副作用:

    1,不能在范围for循环中向vector对象添加元素;

    2,任何一种可能改变vector对象容量的操作(如push_back),都会使vector对象的迭代器失效。【315页详细说明迭代器如何失效的】

  • 数组

    定义数组时,必须指定数组的类型(不能用auto来根据初始值列表判断类型)和长度(数组大小必须是常量表达式)。且数组元素必须是对象,故不会存在引用的数组。

    数组与vector存在一定的差异,体现在如下方面:

    • 字符数组:用字符串字面值来为字符数组初始化时,字符串的末端会有一个空字符,字符数组的长度应该为字符串长度+1。
    • 不允许利用一个数组为另一个数组赋值,以及初始化。

    数组与指针有紧密联系,在使用数组的时候,编译器一般会把它转化成指针。这一结论有很多隐含意义:

    1. 用到数组名字的地方,编译器都会自动将其替换为一个指向数组首元素的指针。

      1
      2
      string nums[] = {"one", "two", "three"};
      string *p2 = nums; // 等价于 string *p2 = &nums[0];
    2. 使用数组作为一个auto变量的初始值时,推断得到的类型是指针而不是数组。

      1
      2
      int ia[] = {0,1,2,3,4,5};
      auto ia2 = ia; // ia2 是一个整型指针,指向ia的第一个元素
    3. 指针也可以作为迭代器,但是要知道头指针和尾后指针的位置,可以用标准库函数begin和end来获得。

      1
      2
      3
      4
      5
      int *beg = begin(ia);
      int *last = end(ia);
      for(int* curr = begin(ia); curr!=last; curr++){
      cout<<*curr<<endl;
      }

    多维数组:当使用多维数组的名字时,也会自动将其转换成指向数组首元素的指针。

    1
    2
    3
    int ia[3][4];
    int (*p)[4] = ia; // p是一个指针,指向含4个int的数组,圆括号不能省略
    int *p[4]; // p时一个含4个int*类型的数组

    写不写圆括号经常会引发问题,C++11新标准中,可以通过使用auto或者decltype来避免这样复杂的写法。

    1
    2
    3
    4
    5
    for(auto p = ia; p != ia+3; ++p){
    for(auto q = *p; q != *p+4; ++q)
    cout << *q << ' ';
    cout << endl;
    }

    进一步地,结合标准库的begin和end,能够使代码更加简洁。

  • 运算符与类型

    对于自增、自减运算符,除非必要,尽量避免使用后置版本。因为后置版本需要将原始值存储下来,如果我们不需要修改前的值,那么这就是一种浪费。

    注意运算符的优先顺序。

    成员运算符 p->item 等价于 (*p).item。

    显示的强制类型转换cast-name<type>(expression);

    • type是转换的目标类型

    • cast-name为 static_cast、dynami_cast、const_cast 和 reinterpret_cast 中的一种;dynamic_cast 支持运行时类型识别,将在730页做更详细的介绍。

      static_cast: 任何具有明确定义的类型转换,只要不包含底层const,都可以使用static_cast。

      可用于,执行浮点数除法;将较大类型赋值给较小类型;找回void*指针中的值 等等。

      1
      2
      void *p = &d;
      double *dp = static_cast<double*>(p);

      const_cast只能改变运算对象的底层const,不能改变表达式的类型,将常量对象转化为非常量对象。

      常常用于有函数重载的上下文中。

      1
      2
      3
      4
      5
      const char *pc;
      char *p = const_cast<char*>(pc);
      // 这是可以的,但是通过p写值是未定义的行为:如果pc指向的常量值,那么将引发错误;如果pc指向的不是常量,那获得写权限是合法的。
      static_cast<string>(pc); // 可以,将字符串字面值转化为string
      const_cast<string>(pc); // 错误,const_cast不能改变表达式的类型

      reinterpret_cast通常为运算对象的位模式提供较低层次上的重新解释。

      比如将 int指针 强制转换成 char指针,但是这样是很危险的,改变了类型但是编译器没有警告或者错误,就很可能在运行时发生错误。

      reinterpret_cast本质上依赖于机器,要安全使用就必须对涉及的类型和编译器实现转换的过程都非常了解。

      建议还是避免强制类型转换!

  • 函数

    将只存在于块执行期间的对象称为自动对象,当块的执行结束后,块中创建的自动对象的值就变成未定义的了。形参和局部变量都是自动对象。

    局部静态对象:将局部变量定义为static,它在第一次定义语句时初始化,作用域在块中,生命周期一直到程序终止。例如,在函数中定义局部静态对象来统计函数被调用的总次数。

    参数传递:形参初始化的机理与变量初始化一样。

    • 形参是引用类型时,它对应的实参被引用传递。引用形参其实跟引用一样,在其上的操作实际是作用在所引用的对象上。使用引用形参:

      (1)能够避免实参的拷贝(若不允许实参改变,可以加const),尤其是某种类型不支持拷贝操作时,函数就只能通过引用形参来访问。

      关于形参const:与其他初始化一样,用实参初始化形参时会忽略顶层const(实参是const引用或者底层const,那么形参肯定也要加const),也就是说,形参为const类型,但是实参既可以传const也可以传非const。所以,

      1
      2
      void fcn(const int i){}
      void fcn(int i){}

      这两个函数不能一起定义,因为他们形参实际上没有什么不同,接受的实参类型一样。

      总的来说,若实参不能改变,那么形参也不能在函数中改变实参。即:(1)若形参是对象类型,那么初始化形参相当于创建了实参的副本,肯定不会改变实参;(2)若形参是引用或者指针类型,那么若形参不加const,就有可能改变实参,这样就不对了。

      建议:尽量使用常量引用,可以避免实参被改变,也扩展了函数所能接受的实参类型

      (2)能够返回额外信息,一个函数只能返回一个值,引用形参为一次返回多个结果提供了途径。

    • 实参的值拷贝给形参时,形参和实参是两个独立的对象(形参不会改变实参),此时实参被值传递

      注意,指针形参的行为与其他非引用类型一样,执行指针拷贝操作,拷贝的是指针的值,但是形参和实参是不同的指针,只是指向的地址相同,所以可以间接地访问所指对象。

数组形参

数组具备两个特性:(1)不允许拷贝数组;(2)使用数组时(通常)会将其转换成指针。

数组以指针的形式传递给函数,所以一开始函数并不知道数组的确切尺寸。有时候往往需要传递额外的信息表示数组的大小。

传递多维数组:将多维数组 arr[m][n] 传递给函数时,真正传递的是指向数组首元素的指针 ( int (*p)[n] ),所以第二维在形参中是不可省略的。p 两端的括号也不可省!

返回值

返回一个值的方式和初始化一个变量或形参的方式完全一样:返回的值用于初始化调用点的一个临时量,这个临时量就是函数调用。

(1)不要返回局部对象的引用或者指针:函数完成,它所占用的存储空间也随之释放,引用局部遍历的引用将指向不再有效的内存区域。

(2)返回类型为引用,将会返回左值。

1
2
3
4
5
6
7
8
9
char& get_val(string &str, string::size_type ix){
return str[ix];
}
int main()
{
string s("a value");
get_val(s, 0) = 'A';
cout<< s << endl; // 输出 A value
}

函数重载

在同一作用域内的几个函数名字相同但形参列表不同,就称之为重载(overloaded)函数。当调用这些函数时,编译器会根据传递的实参类型判断想要的是哪个函数。

对于重载的函数,它们应该在 形参数量 或者 可接受的实参类型或者顺序 上有所不同。

需要注意的是,一个拥有顶层const的形参无法与另一个没有顶层const的形参区分开。

1
2
3
4
5
6
7
8
9
10
11
Record lookup(Phone);
Record lookup(const Phone); // 重复声明,对象形参不会改变实参,都可以接受const或非const实参

Record lookup(Phone*);
Record lookup(Phone* const); // 重复声明,这是指针本身const,顶层const形参,无效

Record lookup(Account&);
Record lookup(const Account*); // 正确,作用于const引用

Record lookup(Account*);
Record lookup(const Account*); // 正确,这是指向const的指针,底层const

对于引用形参而言,若不需要在函数中修改它,建议使用const引用形参,这样可以扩展实参的范围也可以更安全。但如果返回值还是原来对象的引用的话,就只能返回const引用了;如果实参是非const的引用,但是返回了const引用,那么就无法对返回结果进行修改了。这种情况可以通过const_cast来解决:

1
2
3
4
5
6
7
8
9
10
11
12
// 很好,唯一缺点就是若实参是非const引用,但是返回了const引用,无法对结果修改
const string& shorterStr(const string &s1, const string &s2)
{
return s1.size() <= s2.size() ? s1 : s2;
}

// 基于以上问题,利用const_cast重载这个函数
string& shorterStr(string &s1, string &s2)
{
auto &r = shorterStr(const_cast<const string&>(s1), const_cast<const string&>(s2));
return const_cast<string&>(r);
}

想法:如果这个函数返回bool值,那么两个函数返回类型一致【1】,第一个函数形参支持const和非const,第二个仅支持非const,这样传非const会调用哪个函数?是不是会发生问题?

==> 这样重载不会发生问题,优先调用形参为非const的函数。

【1 后期思考】:其实这里也有思维bug,返回值类型不影响函数重载!

重载确定:编译器将调用的实参与重载集合中每一个函数的形参进行比较,然后根据比较的结果决定到底用哪个函数。这可能导致三种结果:

  • 编译器找到一个与实参最佳匹配的函数,并调用该函数的代码。

  • 找不到任何一个函数与调用的实参匹配,编译器发出无匹配的错误信息。

  • 有多于一个函数可以匹配,但每一个都不是明显的最佳选择,将发生二义性调用

    重载函数拥有共同的名字,这个名字也遵循作用域的相关原则(如,隐藏作用域)

    1
    2
    3
    4
    5
    6
    7
    8
    void print(const string&);
    void print(double);
    void fooBar(int ival)
    {
    void print(int); // print名字进入了新的作用域,隐藏前面出现的print
    print("Value "); // 错误,fooBar之前的print名字被隐藏
    print(3.14); // 正确
    }

内联函数

调用函数一般比求等价表达式的值要慢一些,因为一次函数调用包含着一系列的工作:调用前先保存寄存器,在返回时恢复,可能拷贝实参等。

将函数指定为内联函数,就是会将函数在每个调用点上“内联地”展开。例如把之前的 shorterStr 定义为内联(inline),那么将产生这样的效果:

1
2
3
4
// 调用
cout << shorterStr(s1,s2) << endl;
// 在编译过程中展开类似于下面的形式
cout << (s1.size() < s2.size() ? s1 : s2) << endl;

一般来说,内联机制用于优化规模较小、流程直接、频繁调用的函数,很多编译器不支持内联递归函数。通常定义在头文件中。

  • 定义在类内部的函数是隐式inline函数。(??)

    类的成员函数可以使用 this 隐式参数来访问调用它的那个对象,当调用成员函数时,会用请求该函数对象地址初始化this。因为this总是指向“这个”对象,所以它是一个常量指针。

    可以在函数圆括号后加上const来构成 常量成员函数:常量对象,以及常量对象的引用或指针都只能调用常量成员函数。

    类中成员与函数的顺序无需在意,编译器会首先编译成员的声明,然后才轮到成员函数体。

    构造函数

    C++11新标准中,如果需要默认的构造行为,可以在参数列表后面写上 = default 来告诉编译器。

    1
    2
    3
    4
    struct Sales_data{
    Sales_data() = default; // 默认构造
    Sales_data(const string &s):bookNo(s){} // 初始值列表
    };

    注意:构造函数初始值列表只说明用于初始化的值,而不限定初始化的具体执行顺序!成员的初始化顺序与它们在类定义中的出现顺序一致。

    class 与 struct 的默认访问权限不同,class默认为private,而struct默认为public。

    类可以允许其他类或者函数访问它的非公有成员,方法是让其他类或者函数成为它的友元,在类中用friend关键字来声明函数。

    静态成员

    类的静态成员存在于任何对象之外,对象中不包含任何于静态数据成员有关的数据。静态数据成员被所有该类对象共享,静态成员函数也不与任何对象绑定在一起,它们不含this指针。

    静态成员函数可以在类内和类外定义,当在类外定义时,不能重复static关键字。)。

C++标准库

  • IO库

    标准库定义了一些IO类型,分别在三个独立的头文件中:iostream 定义了用于读写流的基本类型, fstream 定义了读写命名文件的类型, sstream 定义了读写内存string对象的类型。同时也支持宽字符的语言(如 wcin 对应 cin 宽字符版对象)。

    下面介绍标准库流的特性,这些流特性可以无差别地应用于普通流、文件流和string流,以及char或宽字符流版本:

    • IO对象无拷贝或赋值

      不能拷贝IO对象,故不能将形参或返回类型设置为流类型。读写一个IO对象会改变其状态,因此传递和返回的引用不能为const。

    • 流存在条件状态

      IO操作可能发生错误,标准库定义了一些函数和标志,来访问和操纵流的条件状态

      1
      2
      3
      4
      5
      6
      7
      8
      strm::iostate           strm是一种IO类型,这是与机器无关的类型
      strm::badbit 指出流已崩溃
      strm::failbit 指出一个IO操作失败了
      strm::eofbit 指出流到达了文件结束
      strm::goodbit 指出流未处于错误状态,此值为0,表示未发生错误
      s.clear() 将流s中所有条件状态位复位,流状态设置为有效
      s.rdstate() 返回流s的当前条件状态,返回类型为strm::iostate
      s.setstate(flags) 根据给定flags标志位,将流s中对应的条件状态位置位,flags类型为strm::iostate

      一个流一旦发生错误,其上后续的IO操作都会失败。确定一个流对象最简单的方法就是将它作为一个条件来使用:

      1
      2
      while(cin >> word)
      // ok: 读操作成功
    • 输出缓冲

      每个输出流都管理一个缓冲区,用来保存程序读写的数据。缓冲机制可以让操作系统将程序的多个输出操作组合成单一的系统级写操作。

      能够刷新缓冲(数据真正写道输出设备或文件)的原因有:

      • 程序正常结束
      • 缓冲区满时
      • 使用操纵符如 endl 来显式刷新缓冲区(flush也可以,且不附加额外字符)
      • 每个输出操作后,可以用操纵符unitbuf设置流内部状态,来清空缓冲区
      • 一个输出流被关联到另一个流,则关联到的流的缓冲区会被刷新(可以将一个istream对象关联到另一个ostream对象,也可以将一个ostream关联到另一个ostream,用tie函数)
  • 顺序容器

    顺序容器提供了控制元素存储和访问顺序的能力。顺序不依赖元素的值,而与元素加入容器时的位置相对应。

    容器类型 说明
    vector 可变大小数组。支持快速随机访问,在尾部之外的地方插入或删除元素较慢
    deque 双端队列。支持快速随机访问,在头尾位置插入/删除速度很快
    list 双向链表。只支持双向顺序访问,在链表任何位置进行插入/删除都很快
    forward_list 单向链表。只支持单项顺序访问,在链表任何位置进行插入/删除都很快
    array 固定大小数组。支持快速随机访问,不能添加或删除元素,只能进行覆盖
    string 与vector类型,但专用于保存字符,随机访问快,在尾部插入/删除快

    swap操作

    容器提供swap操作,交换两个相同类型容器中的内容,速度比拷贝快得多。(亿数量级别的整形vector,用拷贝的方式交换要399ms,而swap耗时0ms)

    除array外,交换两个容器内容的操作保证会很快,因为元素本省并没交换,swap只是交换了两个容器的内部数据结构,能在常数时间内完成。swap有两个特例:

    • 对一个string调用swap会导致迭代器、引用和指针失效;
    • 对array进行swap操作,指针、引用和迭代器所绑定的元素不变,但是会真正交换元素,所需时间与array中的元素数目成正比。

    在新标准库中,容器既提供成员函数版本的swap,也提供非成员函数版本的swap,统一使用非成员函数版本的swap是一个好习惯。

    向顺序容器添加元素

    除array外,所有标准库容器都提供灵活的内存管理,可以在运行时动态添加或删除元素来改变容器大小。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 这些操作会改变容器大小(array不支持)
    c.push_back(t);
    c.emplace_back(args); // 在c尾部创建一个值为t或由args创建的元素,返回void

    c.push_front(t);
    c.emplace_front(args); // 在c头部创建一个值为t或由args创建的元素,返回void

    c.insert(p, t);
    c.emplace(p, args); // 在迭代器p指向元素前创建一个值为t或由args创建的元素,返回新添元素迭代器

    注意:

    • forward_list有自己专有版本的 insert 和 emplace
    • forward_list不支持 push_back 和 emplace_back
    • vector和string不支持 push_front 和 emplace_front,但支持insert来实现头部插入

    关键概念

    当用一个对象来初始化容器时,或者将一个对象插入到容器中,实际上放入到容器中的对象值是一个拷贝,而不是对象本身,这类似于将对象传入一个非引用参数。

    可以使用insert来向指定位置插入元素,同时,利用insert的返回值,能够在容器特定位置反复插入元素。

    新标准引入三个新成员:emplace_front、emplace 和 emplace_back,这些操作构造,而不是拷贝元素。

    push_* 与 emplace_* 的区别在于,push系列函数会构造临时对象,再将这个对象拷贝到容器末尾,而emplace系列函数则直接在容器末尾构造对象,省去了拷贝的过程,会更高效一些。

    调用 push 或 insert 时,要求传递对应类型的对象,这些对象被拷贝到容器中;而当调用 emplace 时,则将参数传递给对象的构造函数,emplace成员使用这些参数在容器管理的内存空间中直接构造元素:

    1
    2
    3
    4
    5
    // 在c末尾添加一个Sales_data对象
    // 使用三个参数的Sales_data构造函数
    c.emplace_back("978-0001", 25, 15.99);
    // 传递Sales_data给push函数
    c.push_back(Sales_data("978-0001", 25, 15.99));

    emplace函数在容器中直接构造元素对象,所以传递给emplace函数的参数必须与元素类型的构造函数相匹配。

    访问元素

    1
    2
    3
    4
    5
    6
    7
    8
    9
    at和下标操作只适用于string、vector、deque和array
    back不适用于forward_list

    c.back() 返回c中尾元素的引用。若c为空,函数行为未定义
    c.front() 返回c中首元素的引用。若c为空,函数行为未定义
    c[n] 返回c中下标为n的元素的引用。若n>=c.size()则函数行为未定义
    c.at(n) 返回下标为n的元素的引用。若下标越界,抛出 out_of_range 异常(比下标操作更安全)

    c.front() 等价于 *(c.begin())

    注意,容器中访问元素的成员函数(front, back, 下标和 at)返回的都是引用。如果容器是const对象,那么返回const引用。非const容器返回的引用可以修改,但如果用 auto变量来保存返回值,得到的是一个拷贝(比如返回 int引用,auto就推导出int),并不能修改容器的值,所以auto变量也要注意加引用。

    1
    2
    auto v = c.front();        // 仅是一个拷贝
    auto &v1 = c.front(); // 是一个引用

    删除元素

    1
    2
    3
    4
    5
    6
    7
    8
    9
    删除操作会改变容器大小,故不适用于array
    forward_list有特殊版本的erase,且不支持pop_back
    vector和string不支持pop_front

    c.pop_back() 删除c中尾元素。若c为空,函数行为未定义
    c.pop_front() 删除c中首元素。若c为空,函数行为未定义
    c.erase(p) 删除迭代器p所指的元素,返回被删元素之后元素的迭代器
    c.erase(b, e) 删除迭代器b和e所指范围内的元素,返回最后指向最后一个被删元素后元素的迭代器
    c.clear() 删除c中所有元素

    注意,删除deque中除首尾位置之外的任何元素都会是所有迭代器、引用和指针失效;指向vector或string中删除点之后位置的迭代器、引用和指针都会失效。