对于想要入门C++的同学来说,《C++ Primer 》是一本不能错过的入门书籍,它用平易近人的 实例化教学激发学生的学习兴趣,帮助学生一步步走进C++的大门。在本文中,作者Jacen用两万多字总结了 《C++ Primer 中文版(第五版)》1-16章的阅读要点,可以作为该书的阅读参考。注:原书更为详细,本文仅作学习交流使用。
第一章 开始 1.1 编写一个简单的C++程序 int main() { return 0; }
每个C++程序都包含一个或多个函数,其中一个必须命名为main. 1.2 初识输入输出 1.3 注释简介 1.4 控制流 数据类型是程序的基础。C++语言支持广泛的数据类型。 基本内置类型 1.算术类型 2.类型转换 变量 1.变量定义 类型说明符,随后紧跟着一个或者多个变量名组成的列表,其中变量名以逗号分隔,最后以分号结束。 在C++中,初始化和赋值是2个完全不同的操作。初始化的含义是创建变量的时候赋予一个初始值,而赋值的含义是把对象的当前值擦除,用一个新值来替代。两者区别很小。 定义在函数体内部的内置类型变量将不被初始化,其值未定义。 2.变量声明和定义的关系 如果想声明一个变量,而非定义它,需要使用extern关键词。 extern int i; // 声明i而非定义i int j; // 声明并定义j ::: tip变量只能被定义一次,但可以被多次声明。::: 3.名字的作用域 作用域中一旦声明了某个名字,它所嵌套的所有作用域都能访问该名字。同时,允许在内层作用域中重新定义外层作用域中有的名字。 ::: warning如果函数有可能用到某全局变量,则不宜再定义一个同名的局部变量。::: 复合类型 1.引用 ::: warning引用必须被初始化。引用本身不是对象,所以不能定义引用的引用。引用要和绑定的对象严格匹配。引用类型的初始值,必须是一个对象。::: 2.指针 指针:本身就是一个对象。允许对指针赋值和拷贝。指针无须在定义的时候赋值。 如果指针指向了一个对象,则允许使用解引用符(*)来访问该对象。 3.理解复合类型的声明 不能定义指向引用的指针。但指针是对象,所以存在对指针的引用。 const限定符 定义:const用于定义一个变量,它的值不能被改变。const对象必须初始化。 默认状态下,const对象仅在文件内有效。当多个文件出现了同名的const变量时,等同于在不同文件中分别定义了独立的变量。 如果想让const变量在文件间共享,则使用extern修饰。 允许为一个常量引用绑定非常量的对象、字面值,甚至是个一般表达式。 一般,引用的类型必须与其所引用对象的类型一致,特殊情况是表达式。 top-level const 表示指针本身是个常量 low-level const表示指针所指的对象是一个常量。 C++新标准规定,允许将变量声明为constexpr类型以便由编译器来验证变量的值是否是一个常量表达式。 处理类型 类型别名 typedef double wages; //wages是double的同义词 typedef wages *p; // p是double*的同义词 using SI = Sales_item; // SI是Sales_item的同义词 auto类型说明符:让编译器通过初始值来推算变量的类型。 decltype类型指示符:选择并返回操作符的数据类型。只得到类型,不实际计算表达式的值。 自定义数据结构 数据结构是把一组相关的数据元素组织起来,然后使用它们的策略和方法。 类一般不定义在函数体内,为了确保各个文件中类的定义一致,类通常被定义在头文件中,而且类所在头文件的名字应该与类的名字一样。 #ifndef SALES_DATA_H #define SALES_DATA_H #endif 术语 空指针 :值为0的指针,空指针合法但是不指向任何对象。nullPtr是表示空指针的字面值常量。void* :可以指向任意非常量的指针类型,不能执行解引用操作。string表示可变长的字符序列,vector存放的是某种给定类型对象的可变长序列。 命名空间的 using 声明 标准库类型 string #include <string> using namespace std; string s1; sting s2(s1); string s3("value"); string s3 = "value"; string s4(n, 'c'); s.empty(); // 判空 s.size(); // 字符个数 s[n]; // s中第n个字符的引用 s1+s2; // s1和s2连接 <,<=,>,>= // 比较 标准局允许把字面值和字符串字面值转换成string对象。字面值和string是不同的类型。 ::: tipC++程序的头文件应该使用cname,而不应该使用name.h的形式::: for (declaration : expression) statement 标准库类型 vector 标准库vector表示对象的集合,其中所有对象的类型都相同。 vector<T> v1; vector<T> v2(v1); vector<T> v2 = v1; vector<T> v3(n, val); vector<T> v4(n); vector<T> v5{a,b,c...} vecrot<T> v5={a,b,c...} 如果用圆括号,那么提供的值是用来构造vector对象的。 如果用花括号,则是使用列表初始化该vector对象。 先定义一个空的vector对象,在运行的时候使用push_back向其中添加具体指。 v.empty(); v.size(); v.push_back(t); v[n]; 迭代器介绍 *iter // 解引用,返回引用 iter->mem // 等价于 (*iter).mem ++iter --iter iter1 == iter2 iter1 != iter2 iter + n iter - n iter += n iter -= n iter1 - iter2 // 两个迭代器相减的结果是它们之间的距离 >, >=, <, <= // 位置比较 凡是使用了迭代器的循环体,都不能向迭代器所属的容器添加元素。 数组 使用数组下标的时候,通常将其定义为size_t类型。 定义数组必须指定数组的类型,不允许用auto推断。 如果两个指针分别指向不相关的对象,则不能进行对这2个指针进行比较。 多维数组 size_t cnt = 0; for(auto &row : a) for (auto &col : row){ col = cnt; ++cnt; } int *ip[4]; // 整型指针的数组 int (*ip)[4]; // 指向含有4个整数的数组 术语 begin string和vector的成员,返回指向第一个元素的迭代器。也是一个标准库函数,输入一个数组,返回指向该数组首元素的指针。end string和vector的成员,返回一个尾后迭代器。也是一个标准库函数,输入一个数组,返回指向该数组尾元素的下一个位置的指针。4.1 基础 重载运算符 :为已经存在的运算符赋予了另外一层含义。当一个对象用作右值得时候,用的是对象的值(内容)。 当对象被用作左值得时候,用的是对象的身份(在内存中的位置)。 4.2 算术运算符 4.3 逻辑和关系运算符
&& 运算符和 || 运算符都是先求左侧运算对象的值再求右侧运算对象的值。 进行比较运算的时候,除非比较的对象是bool类型,否则不要使用布尔字面值true,false作为运算对象。 4.4 赋值运算符 4.5 递增和递减运算符 4.6 成员访问运算符 n = (*p).size(); n = p->size(); 4.7 条件运算符 condition ? expression1 : expression2; 4.8 位运算符
4.9 sizeof运算符 sizeof运算符返回一条表达式或一个类型名字所占的字节数,其所得值是一个size_t类型,是一个常量表达式。 4.10 逗号运算符 逗号运算符含有两个运算对象,按照从左向右的顺序依次求值。 4.11 类型转换 cast-name<type>(expression) // cast-name是static_cast,dynamic_cast,const_cast,reinterpret_cast 由于强制类型转换干扰了正常的类型检查,因此建议避免强制类型转换。 4.12 运算符优先级表
复合语句是指用花括号括起来的(可能为空的)语句和声明的序列,复合语句也被称作块(block)。 5.2 语句作用域 定义在控制结构当中的变量只在相应语句的内部可见,一旦语句结束,变量就超出其作用范围。 5.3 条件语句 case关键字和它对应的值一起被称为case标签。 如果某个case标签匹配成功,将从该标签开始往后顺序执行所有case分支,除非程序显示的中断了这一过程。 dedault 标签:如果没有任何一个case标签能匹配上switch表达式的值,程序将执行紧跟在default标签后面的语句。 5.4 迭代语句 while (condition) statement for (initializar; condition; expression) statement for (declaration : expression) statement do statement while (condition) 5.5 跳转语句 break只能出现在迭代语句或者switch语句内部。仅限于终止离它最近的语句,然后从这些语句之后的第一条语句开始执行。 continue语句终止最近的循环中的当前迭代并立即开始下一次迭代。 goto的作用是从goto语句无条件跳转到同一函数内的另一条语句。 5.6 try语句块和异常处理 C++中异常处理包括:throw表达式、try语句块。 try和catch,将一段可能抛出异常的语句序列括在花括号里构成try语句块。catch子句负责处理代码抛出的异常。 throw表达式语句,终止函数的执行。抛出一个异常,并把控制权转移到能处理该异常的最近的catch字句。 6.1 函数基础 形参和参数体内部定义的变量统称为局部变量 ,它们对函数而言是"局部"的,仅在函数的作用域内可见,同时局部变量还会隐藏外层作用域中同名的其他变量。 局部静态对象:在程序的执行路径第一次经过对象定义语句时候进行初始化,并且直到程序终止才会被销毁。 size_t count_calls() { static size_t ctr = 0; return ++ctr; } 分离式编译允许把程序分割到几个文件中去,每个文件独立编译。 6.2 参数传递 当形参是引用类型,这时它对应的实参被引用传递或者函数被传引用调用。 当实参被拷贝给形参,这样的实参被值传递或者函数被传值调用。 为函数传递一个数组时,实际上传递的是指向数组首元素的指针。 void print(const int*); void pring(const int[]); void print(const int[10]); // 以上三个函数等价 int *matrix[10]; // 10个指针构成的数组 int (*matrix)[10]; // 指向含有10个整数的数组的指针 for err_msg(initializer_list<string> li) 6.3 返回类型和return语句 return; return expression; 返回局部对象的引用是错误的;返回局部对象的指针也是错误的。 6.4 函数重载 重载函数:同一作用域内的几个函数名字相同但形参列表不通,我们称之为重载函数。(overloaded)。 不允许2个函数除了返回类型外其他所有的要素都相同。 如果在内存作用域中声明名字,它将隐藏外层作用域中声明的同名实体。 6.5 特殊用途语言特性 函数调用时,实参按其位置解析,默认实参负责填补函数调用缺少的尾部实参。 typedef string::size_type sz; string screen(sz ht = 24, sz wid = 80, char background = ' '); 当设计含有默认实参的函数时,需要合理设置形参的顺序。一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值。 constexpr函数是指能用于常量表达式的函数。 6.6 函数匹配 6.7 函数指针 void useBigger (const string &s1, const string &s2, bool pf(const string &, const string &)); 等价于 void useBigger (const string &s1, const string &s2, bool (*pf)(const string &, const string &)); 7.1 定义抽象数据类型 任何对类成员的直接访问都被看作this的隐式引用。 std::string isbn() const {return bookNo;} std::string isbn() const {return this->bookNo;} double Sales_data::avg_price() const { if (units_sol) return revenue/units_sols; else return 0; } 定义:类通过一个或几个特殊的成员函数来控制其对象的初始化过程,这些函数叫做构造函数。 类通过一个特殊的构造函数来控制默认初始化过程,这个函数叫做默认构造函数 。 只有当类没有声明任何构造函数的时,编译器才会自动的生成默认构造函数。 一旦我们定义了一些其他的构造函数,除非我们再定义一个默认的构造函数,否则类将没有默认构造函数 7.2 访问控制与封装 说明符 用途 使用public定义的成员,在整个程序内可被访问,public成员定义类的接口。 使用private定义的成员可以被类的成员函数访问,但是不能被使用该类的代码访问,private部分封装了类的实现细节。
类可以允许其他类或者函数访问它的非公有成员,方法是令其他类或者函数成为它的友元。 友元的声明仅仅制定了访问的权限,而非通常意义的函数声明。必须在友元之外再专门对函数进行一次声明。 // Sales_data.h class Sales_data { friend Sales_data add(const Sales_data&, const Sales_data&); friend std::ostream &print(std::ostream&, const Sales_data&); friend std::istream &read(std::istream&, Sales_data&); } // nonmember Sales_data interface functions Sales_data add(const Sales_data&, const Sales_data&); std::ostream &print(std::ostream&, const Sales_data&); std::istream &read(std::istream&, Sales_data&); //Sales_data.cpp Sales_data add(const Sales_data &lhs, const Sales_data &rhs) { Sales_data sum = lhs; // copy data members from lhs into sum sum.combine(rhs); // add data members from rhs into sum return sum; } // transactions contain ISBN, number of copies sold, and sales price istream& read(istream &is, Sales_data &item) { double price = 0; is >> item.bookNo >> item.units_sold >> price; item.revenue = price * item.units_sold; return is; } ostream& print(ostream &os, const Sales_data &item) { os << item.isbn() << " " << item.units_sold << " " << item.revenue << " " << item.avg_price(); return os; } 7.3 类的其他特性 Screen myScrren; char ch = myScreen.get(); ch = myScreen.get(0,0); class Window_mgr{ private: std::vector<Screen> screens{Screen(24, 80, ' ')}; } class Screen { public: // display overloaded on whether the object is const or not Screen &display(std::ostream &os) { do_display(os); return *this; } const Screen &display(std::ostream &os) const { do_display(os); return *this; } } 当某个对象调用display的时候,该对象是否是const决定了应该调用display的哪个版本。 对于一个类来说,在我们创建他的对象之前该类必须被定义过,而不能仅被声明。 如果一个类指定了友元类,则友元类的成员函数可以访问此类包括非公有成员在内的所有成员。 class Screen { // Window_mgr的成员可以访问Screen类的私有部分 friend class Window_mgr; } class Screen { // Window_mgr::clear必须在Screen类之前被声明 friend void Window_mgr::clear(ScreenIndex); } 7.4 类的作用域 7.5 构造函数再探 如果成员是const、引用,或者属于某种未提供默认构造函数的类类型化。我们必须通过构造函数初始值列表为这些成员提供初值。 class ConstRef{ public: ConstRef (int i); private: int i; const int ci; int &ri; }; ConstRef:ConstRef(int ii) : i(ii), ci(ii), ri(i){ } 成员初始化的顺序与它们在类定义中出现 的顺序一致。P259 使用它所述类的其他构造函数执行它自己的初始化过程。 在类内声明构造函数的时候使用explicit关键字。 7.6 类的静态成员 类的静态成员存在于任何对象之外,对象中不包含任何与静态成员有关的数据。 double r; r = Account::rate(); 小结 一是数据数据抽象,即定义数据成员和函数成员的能力; C++语言不直接处理输入输出,而是通过一组定义在标准库中的类型来处理IO。 stringstream完成内存string的IO ifstream和istringstream继承自istream ofstream和ostringstream继承自ostream 8.1 IO类 8.2 文件输入输出 8.3 string流 类 作用 既可从string读数据也可以向string写数据
// will hold a line and word from input, respectively string line, word; // will hold all the records from the input vector<PersonInfo> people; // read the input a line at a time until end-of-file (or other error) while (getline(is, line)) { PersonInfo info; // object to hold this record's data istringstream record(line); // bind record to the line we just read record >> info.name; // read the name while (record >> word) // read the phone numbers info.phones.push_back(word); // and store them people.push_back(info); // append this record to people } // for each entry in people for (vector<PersonInfo>::const_iterator entry = people.begin(); entry != people.end(); ++entry) { ostringstream formatted, badNums; // objects created on each loop // for each number for (vector<string>::const_iterator nums = entry->phones.begin(); nums != entry->phones.end(); ++nums) { if (!valid(*nums)) { badNums << " " << *nums; // string in badNums } else // ``writes'' to formatted's string formatted << " " << format(*nums); } if (badNums.str().empty()) // there were no bad numbers os << entry->name << " " // print the name << formatted.str() << endl; // and reformatted numbers else // otherwise, print the name and bad numbers cerr << "input error: " << entry->name << " invalid number(s) " << badNums.str() << endl; } 顺序容器为程序员提供了控制元素存储和访问顺序的能力。 9.1 顺序容器概述 类型 作 用 可变数组大小。支持快速随机访问。在尾部之外的位置插入或删除元素可能很慢。 双端队列。支持快速随机访问。在头尾位置插入/删除速度很快。 双向链表。只支持双向顺序访问。在list中任何位置进行插入/删除操作速度都很快。 单向链表。只支持单向顺序访问。在链表任何位置进行插入/删除操作速度都很快。 固定大小数组。支持快速随机访问。不能添加或删除元素。 与vector相似的容器,但专门用于保存字符、随机访问快。在尾部插入/删除速度快。
9.2 容器库概述 类型别名 无符号整数类型,足够保存此种容器类型最大可能容器的大小 元素的const左值类型(即,const value_type&)
构造函数 构造c,将迭代器b和e指定的范围内的元素拷贝到c(array不支持)
赋值与swap 将c1中的元素替换为列表中元素(不适用于array)
大小 若c中存储了元素,返回false,否则返回true
反向容器的额外成员(不支持forward_list)
标准库的迭代器允许我们访问容器中的元素,所有迭代器都是通过解引用运算符来实现这个操作。 一个迭代器返回由一对迭代器表示,两个迭代器分别指向同一个容器中的元素或者是尾元素之后的位置。它们标记了容器中元素的一个范围。 while (begin !=end){ *begin = val; ++begin; } 通过别名,可以在不了解容器中元素类型的情况下使用它。 C c; // 默认构造函数 C c1(c2) C c1=c2 C c{a,b,c...} // 列表初始化 C c={a,b,c...} C c(b,e) // c初始化为迭代器b和e指定范围中的元素的拷贝 // 只有顺序容器(不包括array)的构造函数才能接受大小参数 C seq(n) C seq(n,t) 当将一个容器初始化为另一个容器的拷贝时,两个容器的容器类型和元素类型都必须相同。 不过,当传递迭代器参数来拷贝一个范围时,就不要求容器类型相同,只要能将要拷贝的元素转换为要初始化的容器的元素类型即可。 不能对内置数组类型进行拷贝或对象赋值操作,但array并无此限制。P301 arrray类型不允许用花括号包围的值列表进行赋值。 array<int, 10> a2={0}; //所有元素均为0 s2={0}; // 错误! seq.assign(b,e) // 将seq中的元素替换为迭代器b和e所表示的范围中的元素。迭代器b和e不能指向seq中的元素。 swap用于交换2个相同类型容器的内容。调用swap之后,两个容器中的元素将交换。 empty 当size为0返回布尔值true,否则返回false max_size 返回一个大于或等于该类型容器所能容纳的最大元素数的值 关系运算符左右两边的元素符对象必须是相同类型的容器。 只有当元素类型也定义了相应的比较运算符,才可以使用关系元素安抚来比较两个容器 9.3 顺序容器操作 vector <string> svec; svec.insert(svec.begin(), "Hello!"); emplace_front、emplace和emplace_back分别对应push_front、insert和push_back。 emplace函数直接在容器中构造函数,不是拷贝。 在顺序容器中访问元 素的操作 返回c中下标为n的元素的引用,n是一个无符号整数。若n>=size(),则函数行为未定义 返回下标为n的元素的引用。如果下标越界,则抛出out_of_range异常
顺序容器的删除操作 删除c中尾元素。若c为空,则函数行为未定义。返回返回void 删除c中首元素。若c为空,则函数行为未定义。返回void 删除迭代器p所指定的元素,返回一个指向被删除元素之后元素的迭代器,如p指向尾元素,则返回尾后(off-the-end)迭代器。若p是尾后迭代器,则函数行为未定义 删除迭代器b和e所指定范围内的元素。返回一个指向最后一个被删除元素之后元素的迭代器。若e本身就是尾后迭代器,则函数也返回尾后迭代器
befor_begin();cbefore_begin();insert_after;emplace_after;erase_after; resize操作接受一个可选的元素值参数,用来初始化添加到容器内的元素。 如果容器保存的是类类型元素,且resize向容器中添加新元素,则必须提供初始值,或者元素类型必须提供一个默认构造函数。 9.4 vector对象是如何增长的 为了避免影性能,标准库采用了可以减少容器空间重新分配次数的策略。当不得不获取新的内存空间时,vector和string通常会分配比新的新的空间需求更大的内存空间。容器预留这些空间作为备用,可以用来保存更多的新元素。 容器大小管理操作 请将capacity()减少为与size()相同大小 分配至少能容纳n个元素的内存空间。reverse并不改变容器中元素的数量,它仅影响vector预先分配多大的内存空间。调用reverse永远不减少容器占用的内存空间。
capcacity则是在不分配新的内存空间的前提下它最多可以保存多少元素。 9.5 额外的string操作 构造string的其他方法 s是string s2从下标pos2开始的字符的拷贝。 string s (s2, pos2, len2) s是string s2从下标pos2开始len2个字符的拷贝
substr操作返回一个string,它是原始string的一部分或全部的拷贝。 s.substr(pos, n) 返回一个string,包含s中从pos开始的n个字符的拷贝。pos的默认值为0。n的默认值为s.size() - pos, 即拷贝从pos开始的所有字符 assign 替换赋值,总是替换string中的所有内容 append 末尾插入,总是将新字符追加到string末尾 string搜索操作 在s中查找args中任何一个字符最后一次出现的位置 s.find_first_not_of(args)
9.6 容器适配器 stack; queue; priority_queue; 适配器是一种机制,能使某种事物看起来像另外一种事物。 栈的操作 创建一个新元素压入栈顶,该元素通过拷贝或移动item而来,或者由args构造
queue和priority_queue操作 返回queue的首元素或priority_queue的最高优先级的元素,但不删除此元素 返回首元素或尾元素,但不删除此元素。只适用于queue 返回最高优先级元素,但不删除该元素。只适用于priority_queue q.push(item) q.empalce(args) 在queue末尾或priority_queue中恰当的位置创建一个元素,其值为item,或者由args构造
术语 begin容器操作:返回一个指向容器首元素的迭代器,如果容器为空,则返回尾后迭代器。是否返回const迭代器依赖于容器的类型。 cbegin容器操作:返回一个指向容器尾元素之后的const_iterator。 标准库并未给每个容器添加大量功能,而是提供了一组算法。这些算法是通用的,可以用于不同类型的容器和不同类型的元素。 10.1 概述 10.2 初识泛型算法 标准库函数对迭代器而不是容器进行操作。因此,算法不能直接添加或删除元素 10.3 定制操作 标准库允许我们提供自己定义的操作来代替默认运算符。 谓词是一个可调用的表达式,其返回结果是一个能用作条件的值。 bool isShorter(const string &s1, const string &s2) { retrun s1.size() < s2.size(); } sort(words.begin(), words.end(), isShorter); stable_sort算法维持相等元素的原有顺序。 lambda表达式表示一个可调用的代码单元。一个lambda具有一个返回类型、一个参数列表和一个函数体。 [capture list](parameter list) -> return type {function body} // capture list 捕获列表,lambda所在函数中定义的局部变量 // 捕获列表只用于局部非static变量,lambda可以直接使用局部static变量和在它所在函数之外声明的名字 // lambda必须使用尾置返回来指定返回类型 当以引用方式捕获一个变量时,必须保证在lambda执行时变量是存在的。 一般的,应该尽量减少捕获的数据量,来避免潜在的问题。 当混合使用隐式捕获和显式捕获时,捕获列表中的第一个元素必须是一个&或=。显式捕获的变量必须使用与隐式捕获不同的方式。 若希望改变一个被捕获的变量的值,必须在参数列表首加上关键字mutable。 当需要为lambda定义返回类型时,必须使用尾置返回类型。 auto newCallable = bind(callable, arg_list); // 调用newCallable时,newCallable会调用callable,并传递给它arg_list中的参数 10.4 再探迭代器 back_inserter:创建一个使用push_back的迭代器 front_inserter:创建一个使用push_front的迭代器 inserter:创建一个使用inserter的迭代器 ostream_iterator 向一个输出流写数据 istream-iterator操作 istream_iterator<T> in(is); 读取类型为T的值得istream_iterator迭代器,表示尾后位置 in1和in2必须读取相同类型。如果它们都是尾后迭代器,或绑定到相同的输入,则两者相等
ostream_iterator操作 ostream_iterator<T> out(os); ostream_iterator<T> out(os, d); out将类型为T的值写到输出流os中,每个值后面都输出一个d。d指向一个空字符串结尾的字符数组 用<<将val写入到out所绑定的ostream中
反向迭代器就是在容器中从尾元素向首元素反向移动的迭代器。 10.5 泛型算法结构 10.6 特定容器算法 对于list、forward_list,应该优先使用成员函数的算法而不是通用算法。 术语 cref标准库函数:返回一个可拷贝的对象,其中保存了一个指向不可拷贝类型的const对象的引用 11.1 使用关联容器 为了定义一个map,我们必须指定关键字和值的类型。 // 统计每个单词在输入中出现的次数 map<string, size_t> word_count; string word; while (cin >> word) ++word_count[word]; for (const auto &w : word_count) count << w.first << " cccurs " < w.second << ((w.second > 1) ? " times" : "time") << endl; // 统计输入中每个单词出现的次数,并忽略常见单词 map<string, size_t> word_count; set<string> exclude = {"the", "But"}; string word; while (cin >> word) // 只统计不在exclude中的单词 if (exclude.find(word) == exclude.end()) ++word_count[word]; //获取并递增word的计数器 11.2 关联容器概述 一个pair保存两个数据成员。当创建一个pair时,必须提供两个类型名。 pair<string, string> anon; // 保存两个string pair<string, string> author{"James", "Joyce"}; // 也可为每个成员提供初始化器 pair的数据类型是public的,两个成员分别命名为first和second。 11.3 关联容器操作 关联容器额外的类型别名 对于set,与key_type相同;对于map,为pair<const key_type, mapped_type>
map和set都支持begin和end操作。使用beigin、end获取迭代器,然后用迭代器来遍历容器。 从关联容器删除元素 从c中删除每个关键字为k的元素。返回一个size_type值,指出删除的元素的数量 从c中删除迭代器p指定的元素。p必须指向c中一个真实元素,不能等于c.end()。返回一个指向p之后元素的迭代器,若p指向c中的尾元素,则返回.end()
map和unorder_map的下标操作 返回关键字为k的元素;如果k不在c中,添加一个关键字为k的元素,对其进行值初始化 访问关键字为k的元素,带参数检查;若k不在c中,抛出一个out_of_range异常
map进行下标操作,会获得mapped_type对象;当解引用时,会得到value_type对象。 c.find(k) // 返回一个迭代器,指向第一个关键字k的元素,如k不在容器中,则返回尾后迭代器 c.count(k) // 返回关键字等于k的元素的数量。对于不允许重复关键字的容器,返回值永远是0或1 c.lower_bound(k) // 返回一个迭代器,指向第一个关键字不小于k的元素;不适用于无序容器 c.upper_bound(k) // 返回一个迭代器,指向第一个关键字大于k的元素;不适用于无序容器 c.equal_bound(k) // 返回一个迭代器pair,表示关键字等于k的元素的范围。如k不存在,pair的两个成员均等于c.end() 11.4 无序容器 无序容器使用关键字类型的==运算符和一个hash<key_type>类型的对象来组织元素。 无序容器在存储上组织为一组桶,适用一个哈希函数将元素映射到桶。 using SD_multiset = unordered_multiset<Sales_data, decltype(hasher)*, decltype(eqOp)*>; SD_multiset bookStore(42, haser, eqOp); 12.1 动态指针与智能指针 智能指针 用途 提供所有权共享的智能指针:对共享对象来说,当最后一个指向它的shared_ptr被销毁时会被释放。 提供独享所有权的智能指针:当unique_ptr被销毁的时,它指向的独享被释放。unique_ptr不能直接拷贝或赋值。 一种智能指针,指向由shared_ptr管理的对象。在确定是否应释放对象视,shared_ptr并不把weak_ptr统计在内。
make_shared在动态内存中分配一个对象并初始化它,返回此对象的shared_ptr。 share_ptr<int> p3 = make_shared<int>(42); 每个shared_ptr都有一个关联的计数器,称为引用计数。一旦一个shared_ptr的引用计数变为0,就会自动释放自己所管理的对象。 运算符new分配分配内存,delete释放new分配的内存。 // 默认情况下,动态分配的对象是默认初始化的 int *pi = new int; // pi指向一个动态分配的、未初始化的无名对象 // 直接初始化方式 int *pi = new int(1024); // pi指向的对象的值为1024 // 对动态分配的对象进行值初始化,只需在类型名之后加上一对空括号 int *pi1 = new int; // 默认值初始化;*pi1的值未定义 int *pi2 = new int(); // 值初始化为0;*pi2为0 const int *pci = new const int(1024); delete表达式执行两个动作:销毁给定的指针指向的对象;释放对应的内存。 某个时刻,只能有一个unique_ptr指向一个给定对象。 当unique_ptr销毁时,它所指向的对象也被销毁。 weak+ptr是一种不受控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的对象,而且不会改变shared_ptr的引用计数。 使用weak_ptr之前,需要调用lock,检查weak_ptr指向的对象是否存在。 12.2 动态数组 在类型名之后跟一对方括号,在其中指明要分配的对象的数目。 delete p; // p必须指向一个动态分配的对象或为空 delete [] pa; // pa必须指向一个动态分配的数组或为空 unique_ptr<T []> u; unique_ptr<T []> u(p); u[i]; 标准库allocator类定义在头文件memory中,帮助将内存和对象构造分离开来。 allocator<string> alloc; auto const p = alloc.allocate(n); 表达式 作用 定义了一个名为a的allocator对象,它可以为类型为T的对象分配内存 分配一段原始的、未构造的内存,保存n个类型为T的对象 为了使用allocate返回的内存,我们必须使用construct构造对象。使用未构造的内存,其行为是未定义的。 p为T*类型的指针,此算法对p指向的对象执行析构函数
术语 new : 从自由空间分配内存。new T 分配并构造一个类型为T的指针。如果T是一个数组类型,new 返回一个指向数组首元素的指针。类似的,new [n] T 分配 n 个类型为T的对象,并返回指向数组首元素的指针。空悬指针 :一个指针,指向曾经保存一个对象但现在已释放的内存。拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符、析构函数。 拷贝构造函数、移动构造函数定义了当用同类型的另一个对象初始化本对象时做什么。 拷贝赋值运算符、移动赋值运算符定义了将一个对象赋予同类型的另一个对象时做什么。 13.1 拷贝、赋值与销毁 class Foo { public : Foo(); // 默认构造函数 Foo(const Foo&); // 拷贝构造函数 } 拷贝初始化,要求编译器将右运算对象拷贝到正在创建的对象中。拷贝初始化通常使用拷贝构造函数来完成。 合成拷贝赋值运算符:若一个类未定义自己的拷贝赋值运算符,编译器会为它生成一个合成拷贝赋值运算符。 析构函数:用于释放对象使用的资源,销毁对象的非static数据成员。 class Foo { public: ~Foo(); // 析构函数,一个类只会有唯一一个析构函数。 } 在一个析构函数中,不存在类似构造函数中初始化列表的东西来控制成员如何销毁,析构部分是隐式的。销毁类类型的成员需要执行成员自己的析构函数。 合成析构函数 :当一个类未定义自己的析构函数时,编译器会为它定义一个合成析构函数。将拷贝控制成员定义为=dafault来显式地要求编译器生活才能合成的版本。 class Sales_data { public: Sales_data(const Sales_data&) = default; } 如果一个类有数据成员不能默认构造、拷贝、复制或销毁,则对应的成员函数将被定义为删除的。 13.2 拷贝控制和资源管理 为了提供类值的行为,对于类管理的对象,每个对象都应该拥有一份自己的拷贝。 类值拷贝赋值运算符 :通常组合了析构函数和构造函数的操作。HasPtr& HasPtr::operator=(const HasPtr &rhs) { auto newp = new string(*rhs.ps); delete ps; ps = newp; i = rhs.i; return *this; } 13.3 交换操作 13.4 拷贝控制示例 13.5 动态内存管理类 13.6 对象移动 与任何赋值运算符一样,移动赋值运算符必须销毁左侧运算对象的旧状态。 int && rr3 = std::move(rr1); 移动构造函数的第一个参数是该类类型的一个右值引用。 StrVec &StrVec::operator=(StrVec &&rhs) noexcept { } 若一个类定义了自己的拷贝构造函数、拷贝赋值运算符或者析构函数,编译器就不会为它合成移动构造函数和移动赋值运算符。 如果一个类没有移动操作,类会使用对应的拷贝操作来代替移动操作。 区分移动和拷贝的重载函数通常有一个版本接受一个const T&,而另一个版本接受一个T&&。 指出this的左值/右值属性的方式与定义const成员函数相同,在参数列表后放置一个引用限定符。P483 如果一个成员函数有引用限定符,则具有相同参数列表的所有版本都必须有引用限定符。P485 术语 引用限定符 :被&限定的函数只能用于坐值;被&&限定的函数只能用于右值。14.1 基本概念 定义 :重载运算符是具有特殊名字的函数。名字由operator和符号组成。重载运算符包含返回类型、参数列表和函数体。当一个重载的运算符是成员函数时,this绑定到左侧运算对象。成员运算符函数的显式参数数量比运算对象的数量少一个。 对于一个运算符来说,它或者是类的成员,或者至少含有一个类类型的参数。 data1 + data2; operator+(data1, data2); // 以上2个调用等价 14.2 输入和输出运算符 ostream &operator<<(ostream &os, const Sales_data &item) { os << item.isbn() << " " << item.unites_sold << " " << item.revenue << " " << item.avg_price(); return os; } istream &operator>>(istream &is, Sales_data &item) { double price; is >> item.bookNo >> item.units_sold >> price; if (is) item.revenue = items.units_sold * price; else item = Sales_data(); return is; } 14.3 算术和关系运算符 bool operator==(const Sales_data &lhs, const Sales_data &rhs) { return lhs.isbn() == rhs.isbn() && lhs.unites_sold == rhs.units_sold && lhs.revenue == rhs.revenue; } 14.4 赋值运算符 14.5 下标运算符 class StrVec{ public: std::string& operator[](std::size_t n){ return elements[n]; } const std::string& operator[](std::size_t n) const{ return elements[n]; } private: std::string *elements; } 14.6 递减和递增运算符 class StrBlobPtr{ public: StrBlobPtr& operator++(); // 前置运算符 StrBlobPtr& operator--(); } class StrBlobPtr{ public: StrBlobPtr operator++(int); // 后置运算符 StrBlobPtr operator--(int); } 14.7 成员访问运算符 14.8 函数调用运算符 如果类重载了函数调用运算符,则我们可以像使用函数一样使用该类的对象。 struct absInt{ int operator()(int val) const { return val < 0 ? -val : val; } }; absInt absObj; int ui = absObj(i); 14.9 重载、类型转换与运算符 类型转换运算符是类的一种特殊成员函数,将一个类类型的值转换成其他类型。形式: 如果对同一个类既提供了转换目标是算术类型的类型转换,也提供了重载的运算符,将会遇到重载运算符与内置运算符的二义性问题。 术语 类类型转换 :由构造函数定义的从其他类型到类类型的转换以及由类型转换运算符定义的从类类型到其他类型的转换。15.1 OOP:概述 (1)面对对象程序设计(object-oriented programming)的核心思想 : 继承是一种类联系在一起的一种层次关系。这种关系中,根部是基类 ,从基类继承而来的类成为派生类 。 基类负责定义在层次关系中所有类共同拥有的成员,而每个派生类定义各自特有的成员。 虚函数:virtual function。基类希望派生类各自定义自身版本的函数。 class Quote { public: std::string isbn() const; virtual double net_price(std::size_t n) const; } 在C++语言中,当我们使用基类的引用(或者指针)调用一个虚函数时将发生动态绑定(也称运行时绑定 )。P527 15.2 定义基类和派生类 基类通过在其成员函数的声明语句之前加上关键词virtual使得该函数执行动态绑定。 关键词virtual只能出现在类内部的声明语句之前而不能用于类外部的函数定义。 如果基类把一个函数声明成虚函数,则该函数在派生类中隐式的也是虚函数。 派生类必须通过派生类列表明确指出它是从哪个基类继承而来的。 class Bulk_quote : public Quote { ... // 省略 } 若派生类未覆盖基类中的虚函数,则该虚函数的行为类似其他普通成员。 C++允许派生类显式注明覆盖了基类的虚函数,可通过添加override关键字。 一个派生类对象包含多个部分:自己定义的成员的子对象,以及基类的子对象。 由于派生类对象中含有与其基类对象的组成部分,因此可以进行隐式的执行派生类到基类的转换。 Quote item; // 基类 Bulk_quote bulk; // 派生类 Quote *p = &item; // p指向Quote对象 p = &bulk; // p指向bulk的Quote部分 Quote &r = bulk; // r绑定到bulk的Quote部分。 每个类控制自己的成员的初始化过程。派生类首先初始化基类的部分,然后按照声明的顺序依次初始化派生类的成员。 派生类对象不能直接初始化基类的成员。派生类应该遵循基类的借口,通过调用基类的构造函数来初始化从基类继承来的成员。 若使用某个类作为基类,则该类必须已被定义而非仅仅声明。 派生类包含它的直接基类的子对象以及每个间接基类的子对象。 class NoDerived final {}; // NoDerived不能作为基类 静态类型:在编译时已知,是变量声明时的类型或表达式生成的类型。 动态类型:运行时才可知,是变量或表达式表示的内存中的对象的类型。 如果表达式既不是引用也不是指针,则动态类型与静态类型永远一致。 Quote base; Bulk_quote *bulkP = &base; // 错误! Bulk_quote *bulkRef = base; // 错误! 当我么用一个派生类对象为一个基类对象初始化或赋值时,只有该派生类对象中的基类部分会被拷贝、移动或赋值,它的派生类部分会被忽略掉。 15.3 虚函数 C++的多态性 :使用这些类型的多种形式,而无须在意它们的差异。一个派生类如果覆盖了某个继承而来的虚函数,则它的形参类型必须与被它覆盖的基类函数完全一致。 如果用override标记了某个函数,但是该函数并没有覆盖已存在的虚函数,此时编译器将报错。 如果用final标记了某个函数, 则之后任何尝试覆盖该函数的操作都将错误。 如果虚函数某次被调用使用了默认实参,则该实参值由本次调用的静态类型决定。 15.4 抽象基类 书写=0可以将一个虚函数说明为纯虚函数(pure virtual),纯虚函数无须定义。 class Disc_quote : public Quote { public: double net_price(std::size_t) const = 0; } 15.5 访问控制与继承 派生类的成员和友元只能访问派生类对象中的基类部分的受保护成员;对于普通的基类对象中的成员不具有特殊的访问权限。P543 派生访问说明符对于派生类的成员(及友元)能否访问其直接基类的成员无影响; 派生访问说明符的目的是控制派生类用户对于基类成员的访问权限。 通过在类的内部使用using声明语句,我们可以将该类的直接或间接基类中的任何可访问成员标记出来。 class Derived : private Base { public: using Base::size; } class Base {}; struct D1 : Base {}; // 默认public继承 class D2 : Base {}; // 默认private继承 15.6 继承中的类作用域 一个对象、引用或指针的静态类型决定了该对象的哪些成员是可见的。 出了覆盖继承而来的虚函数外,派生类最好不雅重用其他定义在基类中的名字。 如果派生类的成员函数与基类的某个成员函数同名,则派生类将在其作用域内隐藏掉该基类成员函数。 15.7 构造函数与拷贝控制 在基类中将析构函数定义成虚函数以确保执行正确的析构函数版本。 Quote *itemP = new Quote; delete itemP; // 调用Quote的析构函数 itemP = new Bulk_quote; delete itemP; // 调用Bulk_quote的析构函数 基类缺少移动操作会阻止派生类拥有自己的合成移动操作,所以当确实要执行移动操作的时候就要首先在基类中进行显式定义。P554 默认情况下,基类默认构造函数初始化派生类对象的基类部分。如果我们想拷贝(或移动)基类部分,则必须在派生类的构造函数初始值列表中显式的使用基类的拷贝(或移动)构造函数。 15.8 容器与继承 当使用容器存放继承体系中的对象时,必须采用间接存储的方式。因为不允许在容器中保存不同类型的元素。 术语 覆盖:override,派生类中定义的虚函数如果与基类中定义的同名虚函数与相同的形参列表,则派生类版本将覆盖基类的版本。 多态:程序能够通引用或指针的动态类型获取类型特定行为的能力。 当编译器遇到extern模板声明时,它不会在本文件中生成实例化代码。将一个实例化声明为extern就表示承诺在程序其他位置有该实例化的一个非extern声明(定义)。对于一个给定的实例化版本,可能有多个extern声明,但必须只有一个定义。 类模板:模板定义,可从它实例化出特定的类。类模板的定义以关键词template开始,后面跟尖括号对<和>,其内为一个用逗号分隔的一个或多个模板参数的列表,随后是类的定义。 函数模板:模板定义,可从它实例化出特定函数。函数模板的定义以关键词template开始,后跟尖括号<和>,其内以一个用逗号分隔的一个或多个模板参数的列表,随后是函数的定义。
▊《 C++ Primer中文版(第5版) 》
[美] Stanley B. Lippman,Josee Lajoie,Barbara E. Moo 著
王刚 杨巨峰 译
如果只读一本C++书,本书将是你永不局悔的选 择
征服全球数千万读者的大师之 作
C++学习头牌,技术影响力图书冠军
如果只读一本C++书籍,相信所有读过的人都会毫不犹豫选择《C++ Primer(中文版)(第5版)》,无论初学或老手既可用来理解C++11规则背后的原理,也可用其新语言特性与标准库来快速构建健壮程序,采用中文版页码全面呈现原书大量交叉引用及详尽索引,新式辅学设置与课后操练用于避开陷阱、巩固良法!
(扫码了解本书详情)
与广大群友共同学习成长