cpp primer notes

迭代器

取中间值

mid = beg + (end-beg)/2;而不是mid = (beg + end)/2;

  1. 没有两个迭代器相加的operator
  2. 可能overflow

数组

复杂的数组声明

int (*Parray)[10] 首先是指针,然后是10个数组,数组里是整数。是一个指向一个包含十个int的数组的指针。
int *(&array)[10] 首先是个引用,10个数组,是整数指针,所以是一个有10个整数指针数组的引用。

begin函数

C++11中可以使用begin()end()来获得。在iterator头文件中。

指针也是迭代器

比较字符串

如果是数组,比较的是指针。string比较的是值。

函数

内联函数

inline

将它在每个调用点上“内联地”展开,可以消除函数运行时的开销。

函数指针

bool (*pf) (const string &)

注意,函数指针的括号不可以升略。

debug技巧

__func__存放当前调试的函数名

__FILE__存放文件名的字符串字面值

__LINE__存放当前行号的字面值

__TIME__存放文件编译时间

__DATE__存放编译的日期

类的基本思想是数据抽象(data abstraction)封装(encapsulation)

数据抽象是一种依赖于接口(interface)实现(implementation)分离的编程技术。

const成员函数

常量成员函数(const member function)

允许把const关键字放在成员函数的参数列表之后,紧跟在参数列表后面的const表示this是一个指向常量的指针。这样使用const的成员函数被称为常量成员函数。

编译器处理类

  1. 编译成员的声明
  2. 成员函数体

所以无需在意类中的其他成员的出现顺序。

构造函数

名字和类名一样,没有返回类型。

1
2
3
struct Sales_data{
Sales_data() = default; // C++11标准,可以写上default要求编译器生成构造器。
}

构造函数初始值列表(constructor initialize list)

1
2
Sales_data(const std::string &s) : bookNo(s) { }
Sales_data(const std::string &s, unsigned n, double p) : bookNo(s) , units_sold(n) , revenue(p*n){ }

访问控制与封装(隐藏)

classstruct的区别:class的默认权限是privatestruct的默认权限是public

友元(friend)

类允许其他类或函数访问它的非公有成员。

可变数据成员(mutable data member)

const成员函数可以改变一个可变成员的值,任何函数都可以改变它的值。

顺序容器

简介

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

stringvector将元素保存在连续的内存空间内。

容器库

迭代器

实际上,end是最后一个元素的后面,所以范围:[begin, end)

基于此的一些假定:

  • 如果beginend相等,范围为空。
  • 如果不等,则范围至少包括一个元素且为begin所指向的元素。
  • 可以对begin递增若干次,使得begin == end
赋值

相同但是不同容量的时候,用assign

seq.assign(b, e)可以将seq中的元素替换成b和e所表示的范围中的元素。

容器元素是拷贝

string

string的搜索操作:find(args), find_first_of(args), find_last_not_of(args)

一个写法:while( pos = name.find_first_of(numbers, pos) != string.npos)

find_first_of()解决的是查找与给定字符串中任何一个字符匹配的位置

rfind(args)find_last_of(args) 的区别:

rfind找的是匹配的全部字符,find_last_of()是匹配任意一个字符。

提取一句话中的数字

对于形如pi = 3.14的语句:

d = stod(s2.substr(s2.find_first_of("+-.0123456789")))

容器适配器

适配器(adaptor)是标准器中的一个通用概念。适配器有一些基本的要求,符合要求以后啥都可以。

泛型算法(generic algorithm)

多数在algorithm中,少数在numeric中。这些算法不直接操作容器,而是遍历由两个迭代器指定的一个元素范围来进行操作。

只读算法

findaccumulate(位于numeric库中)。

int sum = accumulate(vec.begin(), vec.end*(), 0);第三个参数的类型决定了函数中使用哪个加法运算符和返回值的类型。最好使用cbegin()cend()

equal用来确定两个序列是否保存相同的值。假设第二个至少和第一个一样长

写容器元素的算法

fill接受一对迭代器表示范围,一个值用来赋值。

fill_n从迭代器开始计数n个。这些都不能用来给空的写,除非使用插入迭代器(insert iterator)

back_inserter是定义在iterator的插入迭代器,接受一个指向容器的引用,返回一个与容器绑定的插入迭代器。通过给这个迭代器赋值的时候,赋值运算符会调用push_back将一个具有给定值的元素添加到容器中。

copy,replace,replace_copy

重排容器元素

sort函数。stable_sort函数可以维持相等元素的原有顺序。

利用unique标准库去重:

1
2
3
4
5
6
7
8
9
void elimDups(vector<string> &words)
{
sort(words.begin(), words.end());
// unique重排输入范围,每个单词只出现一次
// 排列在范围的前部,返回指向不重复区域之后一个位置的迭代器。
auto end_unique = unique(words.begin(), words.end());
// 使用erase来去掉重复的元素。
words.erase(end_unique, words.end());
}
向算法传递函数

谓词(predicate)是一个可调用的表达式,其返回结果是一个能用作条件的值。一元谓词,二元谓词是针对参数的个数。

lambda表达式

\[capture list\](parameter list) -> return type { function body }

lambda通过将局部变量包含在捕获列表中指出将会使用这些变量。只用局部非static变量

通常是不能改变值的,但是可以在参数列表首加上mutable就可以了。

如果一个lambda体包含return之外的任何语句,编译器假定此lambda返回void

[](int i) -> int { if( i<0 ) return -i; else return i; }

find_if, for_each算法

隐式捕获:[=]表示值捕获,[&]表示引用捕获。

bind函数

定义在functional中,可看作一个通用的函数适配器。它接受一个可调用对象,生成一个新的可调用对象来是一个原对象的参数列表。

auto newCallable = bind(callable, arg_list);

例如:bind(check_size, _1, sz);使用_1表示第一个参数,然后check_size的第二个参数绑定到sz的值上。不过可能是using std::placeholders::_1

引用的问题,可以用标准库的ref函数。

插入迭代器

接受一个容器,生成一个迭代器,能实现向给定容器插入元素。插入器有三种类型:back_inserter, front_inserter, inserter.

iostream迭代器

istream_iterator读取输入流,ostream_iterator向一个输出流写数据。

1
2
3
4
5
istream_iterator<int> in_iter(cin);
istream_iterator<int> eof;
while (in_iter != eof)
// 返回的是旧值,*取的也是旧值。
vec.push_back(*in_iter++);

可以使用算法操作流迭代器

1
2
3
istream_iterator<int> in(cin), eof;
// 输出从标准输入读取的值的和。
cout << accumulate(in, eof, 0) << endl;

ostream_iterator还有更好用的功能,可以加一个值,每个值后面都输出一个。

反向迭代器

反向迭代器会反着处理。需要调用reverse_iteratorbase成员函数来完成“掉头”的转换。

泛型算法结构

五个迭代器类别(iterator category)

类别 描述
输入迭代器 只读,不写;单遍扫描,只能递增
输出迭代器 只写,不读;单遍扫描,只能递增
前向迭代器 可读写;多遍扫描,只能递增
双向迭代器 可读写;多遍扫描,可递增递减
随机访问迭代器 可读写;多遍扫描,支持全部迭代器计算

关联容器

标准库一共有8个关联容器,两大种:setmap。有multi前缀表示关键字可重复出现。有unordered表示是用哈希组织的。

对于有序类型,关键字类型必须定义元素比较的方法。

pair类型

pair的数据类型是public的,两个成员分别命名为firstsecond

几个类型别名:key_type, mapped_type, value_type

解引用一个容器迭代器的时候,我们会得到一个类型为value_type的值的引用,对map而言,是pair类型。

setmap的关键字都是const的。

insert的返回值是一个pairpair.first是指向具有给定关键字的元素,second成员是一个bool值。

1
2
3
// map
c[k] // 返回关键字为k的元素,如果k不在c中,添加一个关键字为k的元素对其进行值初始化。
c.at(k) // 访问关键字为k的元素,带参数检查;若k不在c中,抛出一个out_of_range异常。
访问元素

c.find(k):返回一个迭代器,指向第一个关键字为k的元素。

c.count(k):返回关键字为k的元素的数量。

c.lower_bound(k):返回一个迭代器,指向第一个关键字不小于k的元素。

c.upper_bound(k):指向第一个关键字大于k的元素。

c.equal_range(k):返回一个迭代器pair,表示关键字等于k的元素的范围。若不存在,返回c.end()

如果我们想知道一个元素是否在map中,而不想插入这个元素,用find更好。

无序容器

无序容器在存储上组织为一个桶,每个桶保存零个或多个元素。无序容器还提供了一组管理桶的函数。

动态内存

静态内存:局部static对象、类static数据成员以及定义在任何函数之外的变量。

栈内存:定义在函数内的非static对象。

堆/自由空间:动态分配的对象。

动态内存的管理

通过一对newdelete操作。

很容易危险:1. 没有释放导致内存泄漏。2. 提前删除导致引用非法内存的指针。

智能指针

新的标准库提供了两种智能指针(smart pointer)

shared_ptr, unique_ptr, weak_ptr.

shared_ptr

智能指针也是模板,需要提供指针可以指向的类型。

shared_ptr独有的操作:

make_shared<T> (args)

<memory>头文件中。

返回一个shared_ptr,指向一个动态分配的类型为T,用args初始化的对象。

拷贝与赋值

shared_ptr会记录有多少个其他的shared_ptr指向相同的对象,引用计数(reference count)。

当一个对象的最后一个shared_ptr被销毁时,shared_ptr会自动销毁此对象。通过调用析构函数来完成。

newdelete

默认情况下,动态分配的对象是默认初始化的。

如果分配失败,new会抛出std::bad_alloc错误。可以new (nothrow) int,此时分配失败会返回空指针。

delete后需要重置指针值。

shared_ptr接受指针参数的智能指针构造函数是explicit的,不能把一个内置指针隐式转为一个智能指针,必须使用直接初始化形式。

1
2
shared_ptr<int> p1 = new int(1024); // 错误
shared_ptr<int> p1 (new int(1024)); // 正确

一个危险的情况:

1
2
3
int *x(new int(1024)); //危险:x是一个普通指针,不是智能指针
process(shared_ptr<int>(x)); //合法,但内存会被释放。
int j = *x; //未定义的,x是一个空指针。

一旦把指针交给了智能指针,就由智能指针来负责了。

另一个危险的情况:永远不要用get初始化另一个智能指针或为另一个智能指针赋值,两者的计数都是1,所在程序块结束后,会销毁的。

使用自己的释放操作

删除器(deleter)函数能够完成对shared_ptr中保存的指针进行释放的操作。在创建一个shared_ptr的时候,可以传递一个(可选的)指向删除器函数的参数。

1
2
void end_connection(connection *p) { disconncet(*p); }
shared_ptr<connection> p(&c, end_connection);
unique_ptr

某个时刻只能有一个unique_ptr指向一个给定对象,当它被销毁时,它所指向的对象也被销毁。没有make_shared类似的操作。unique_ptr不支持拷贝、赋值(p1赋值给p2)。

但是可以通过使用releasereset将指针的所有权从一个(非constunique_ptr转移给另一个unique

但是对于编译器知道返回的对象将要被销毁的时候,是可以拷贝的。

weak_ptr

一个弱指针,不控制所指向的对象的生存期。因为它指向的不一定可以用,所以需要调用lock来看一下。

动态数组

c++语言定义了new表达式语法,可以分配并初始化一个对象数组。标准库包含一个名为allocator的类,允许我们将分配和初始化分离。

new
1
2
3
4
5
6
7
8
9
int *pia = new int[get_size()];
typedef int arrT[42];
int *p = new arrT;
int *pia = new int[10];
int *pia2 = new int[10]();// 10个值初始化为0的int
string *psa = new string[10];
string *psa = new string[10]();

分配一个数组会得到一个与元素的类型的指针。

shared_ptr不支持直接管理动态数组,必须要提供自己定义的删除器。

allocator

在头文件memory中,它帮助我们将内存分配和对象构造分离开。它提供一种类型感知的内存分配方法,它分配的内存是原始的、未构造的。

allocator分配的内存是未构造的,我们按需要在此内存中构造对象。

1
2
3
4
5
6
7
auto q = p; // q指向最后构造元素之后的位置
alloc.construct(q++);
alloc.construct(q++, 10, 'c');
alloc.construct(q++, "hi");
while (q != p)
alloc.destory(--q);

还可以拷贝和填充未初始化内存的算法

1
2
uninitialized_copy(b, e, b2); // 把迭代器b到e指出的输入范围中拷贝元素到迭代器b2指定的未构造的原始内存中
uninitialized_fill(b, e, t);

拷贝控制

一个类通过定义五种特殊的成员函数来控制这些操作,包括:拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符和析构函数。

拷贝、赋值与销毁

拷贝构造函数(copy constructor)

如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则是拷贝构造函数。

直接初始化实际上是要求编译器使用普通的函数匹配来选择我们提供的参数最匹配的构造函数。当我们使用拷贝初始化时, 我们要求编译器将右侧运算对象拷贝到正在创建的对象中,如果需要的话还要进行类型转换。

拷贝初始化的发生:

  1. 使用=定义变量时
  2. 将一个对象作为实参传递给一个非引用类型的形参。
  3. 从一个返回类型为非引用类型的函数返回一个对象。
  4. 用花括号初始化一个数组中的元素或一个聚合类中的成员。

某些标准库容器在insertpush时也会对元素进行拷贝初始化。

拷贝构造函数自己的参数必须是引用类型

拷贝赋值运算符

重载运算符(overloaded operator)

本质是函数,其名字由operator关键字后接表示要定义的运算符的符号组成。

析构函数

一个波浪号接类名构成。

析构部分是隐式的,智能指针是类类型,所以也有析构函数。

调用析构的时机

  • 变量在离开作用域时
  • 当一个对象被销毁时,其成员被销毁
  • 容器被销毁时,其元素被销毁
  • 动态分配的对象,当对指向它的指针引用delete运算符时被销毁
  • 对于临时对象,当创建它的完整表达式结束时被销毁

面向对象程序设计

核心思想:数据抽象、继承和动态绑定。

继承(inheritance)

通过继承联系在一起的类构成一种层次关系。基类和派生类。

对于某些基类希望它的派生类各自定义适合自身的版本,就可以声明成虚函数(virtual function)

派生类必须通过使用类派生列表明确指出是从哪些基类继承而来。

动态绑定(dynamic binding)

基类通常应该定义一个虚析构函数

任何构造函数之外的非静态函数都可以是虚函数。

基类希望它的派生类有权访问该成员,同时禁止其他用户访问,用protected来说明。

派生类经常但不总是override它继承的虚函数。

可以把基类的指针或引用绑定到派生类对象中的基类的部分上,这种转换通常称为派生类到基类的类型转换。

静态成员在整个继承体系中只存在该成员的唯一定义。

存在继承关系的类型之间的转换规则
  • 从派生类到基类的类型转换只对指针或引用类型有效。
  • 基类向派生类不存在隐式类型转换。
  • 和任何其他成员一样,派生类到基类的类型转换也会由于权限受限而变得不可行。

虚函数

对虚函数的调用可能在运行时才被解析

动态绑定只有当我们通过指针或引用调用虚函数时才会发生。

多态性

多种形式。有继承关系的多个类型称为多态类型,我们可以使用这些类型而无须在意他们的差异。引用或指针的静态类型与动态类型不同这一事实正是C++语言支持多态性的根本所在。

override用来标记对虚函数的覆盖。

Tips

  1. C++中,名字查找发生在类型匹配前。
  2. 内联说明只是向编译器发送一个请求,编译器可以忽略。
  3. 在类内部的函数是隐式的inline函数。
  4. 没什么特别好的理由,就用vector
  5. 除了array之外,swap不对任何元素进行拷贝、删除或插入操作,可以保证在常数时间内完成。
  6. 泛型算法不会执行容器的操作,所以不会直接添加或者删除元素。
  7. 在新版本的C++中, 可以用花括号来创建pair类型。
  8. ->相当于*.
  9. 如果把shared_ptr存放于一个容器,而后不再需要全部元素,只使用其中一部分,要记得用erase删除不再需要的那些元素。