C++ Primer读书笔记

文中标明【11】的为C++11新增标准。

第零部分

第一章 开始

  • C++是静态(编译时检查变量类型)、弱类型(会自动做隐式类型转换);
  • cin >>cout << 运算顺序均为从左至右,运算结果为一个istream/ostream对象;
  • while (cin >> a) 在读到EOF 时跳出循环;
  • 由于/* */ 注释的判定为遇到第一个*/ 结束,因此该注释不能嵌套。一般只用它来写注释,需要注释掉代码时使用多行//
  • cerr不可重定向,不通过缓冲区;endl会刷新缓冲区;
  • Windows下文件结束符为Ctrl+ZEnter,Linux下为Ctrl+D
  • 用户自定义的标识符不能连续出现两个下划线,也不能以下划线紧连大写字母开头,定义在函数体外的标识符不能以下划线开头;

第一部分 C++基础

第二章 变量和基本类型

  • 不同编译器字长和实现方式均不同,不要混用有符号和无符号变量;
  • 赋给带符号类型一个超出其表示范围的值是ub(未定义行为);
  • 字面值常量:编译期可以直接得到结果的常量,如整数1、字符’a’、字符串”a”、布尔true、指针nullptr等;
  • 【11】初始化和赋值不一样;C++11支持列表初始化,可以初始化类或数组等,在可能造成数据丢失时会警告;
  • 全局变量会被默认初始化,而函数体内置类型变量不会;
  • 作为静态类型语言,且为了支持分离式编译,C++区分了声明和定义;允许多次声明(当然声明必须一样),但定义只能有一次;
  • 复合类型(引用、指针)由基本数据类型(如int)和声明符(*和&)列表组成;
  • int *a,b此时b为整型,因此声明时建议将声明符列表紧挨变量名;
  • 【11】C++11用nullptr(字面值)带出原先的预处理变量NULL,以防止函数重载后无法分清参数是0还是NULL;
  • void *指针没有对象类型信息,不能解引用;
  • 常量引用const &必须在声明时初始化;
  • 顶层const表示指针本身是常量(const pointer):int * const;底层const表示指针所指对象为常量(pointer to const):const int *;此处国内叫法(常量指针)似乎不一致,建议直接使用英文;
  • 读声明时可以从右向左读,左边是右边的定语;
  • 【11】constexpr表示编译时常量,即可以直接由字面值简单运算得到;相应地有运行时常量,如用一个变量初始化常量;
  • 【11】C++11规定了新方法:别名声明using new_type_name = old_type_name;
  • 不应将typedef后的类型简单带入到新声明中,如typedef char * pstring; const pstring cstr;cstr的基本数据类型为指针,此为const pointer,而简单带入成const char * cstr后基本数据类型变为const char*成为声明符的一部分,变为pointer to const;
  • 【11】定义auto类型时必须初始化,否则编译期无法推导类型;auto一般会忽略顶层const,若需要应写明const auto
  • 【11】decltype(f()) sum = x;f()的返回值类型作为sum的推导类型,但不实际计算f()sumx初始化;
  • int i = 42, *p = &i, &r = i;decltype(*p)的结果是int&而非intdecltype(r)结果为int&decltype(r + 0)的结果为intdecltype的表达式若是加上括号的变量,结果将是引用,如decltype((i))会得到引用;其余情况均会将引用理解为变量别名,除了decltype的时候;
  • 头文件保护符:在代码首尾分别加上#ifndef A_H #define A_H#endif
  • typedef int i1, *i2;定义了一个int类型和一个int*类型;

第三章 字符串、向量和数组

  • 头文件不应包含using声明;
  • 若用=号初始化则为拷贝初始化,否则为直接初始化;
  • getline(cin, s)会读到一个'\n'为止(包括此'\n's中不存);
  • 【11】范围(range)for语句:for (declaration: expression)for (auto &c: str)
  • 有些老式编译器要求vector<vector<int> >此处必须有空格;
  • const_iterator 是pointer to const;auto it = v.cbegin(); 会得到一个const_iterator
  • 但凡使用了迭代器的循环体都不要向所属容器添加元素;
  • 迭代器可以和整数相加减;两个同容器的迭代器也可以相减,得到二者的距离,结果为有符号整型difference_type;两个指向同一数组中元素的指针相减的结果为有符号整型ptrdiff_t
  • 引用不是对象,故不存在引用的数组;
  • 读数组的声明:由内而外,优先右结合,其次左结合;
  • 数组下标为无符号整型size_t;数组定义的维度必须是编译期常量;
  • 当使用数组名时往往会被编译器转换为数组首地址(decltype时不会),如string *p = arr;等价于string *p = &arr[0];
  • 多维数组使用范围for语句时需要所有外层都使用引用for (auto &i: arr),否则会被编译器理解为数组首地址;
  • 指针也是迭代器;尾后迭代器end() 没有实际含义,不能被递增或解引用;
  • 【11】C++11支持数组的begin(arr)end(arr)函数;
  • 指向数组元素的指针也可以当数组用:int *p = &arr[2]; p[-1];
  • strlen(str)会一直找到空字符为止,所以可能产生缓冲区错误;
  • string::c_str()返回一个const char *,不保证一直有效;
  • 用数组初始化vector:vector<int> ivec(begin(arr), end(arr));
  • 优先使用string和vector而减少使用C风格的字符串和数组;

第四章 表达式

  • 【11】C++11允许直接使用初始化列表赋值、传参、作函数返回值;
  • 新版C++正负均向零舍入;
  • 赋值运算符具有右结合律;非必要不使用后置递增递减算符,因为会对迭代器产生较大不必要运算;
  • , 运算符从左至右运算,并只返回最后一项;
  • 无符号类型与带符号类型的运算会进行依赖于编译器的算数转换,故不要使用;
  • static_cast为不报警告的强制类型转换,如void* p = &d; double *dp = static_cast<double*>(p);
  • const_cast改变运算对象的底层const,转换本身是常量的对象是ub;示例用法:const char *pc; char *p = const_cast<char*>(pc);
  • reinterpret_cast重新解释位模式,示例:int *ip; char *pc = reinterpret_cast<char*>(ip); 尽量不要使用此强制转换;
  • 尽量使用C++风格的类型转换而非C风格的(type)var;
  • 提倡使用*p++,递增运算符优先级高于解引用;

第五章 语句

  • else默认匹配最近的没有elseif
  • case的值必须是整型常量表达式(字符算整型);case语句会一直执行直至遇到break
  • 异常类只有一个成员函数what(),返回const char *,提供异常文本信息;

第六章 函数

  • 函数调用的这对括号叫调用运算符;局部变量、形参等离开作用域自动销毁的变量称为自动对象;
  • 函数最外层作用域中的局部变量不能与形参重名;
  • 最佳实践:定义函数的源文件应包含声明函数的头文件;尽量使用常量引用作为形参;
  • C++允许用字面值初始化常量引用;
  • 【11】实参数量未知但类型都相同,C++11支持initializer_list类型,内存常量值,用法类似vectorinitializer_list<T> lst{a, b, c}; 如:void msg(initializer_list<string> il){ … },调用:msg({"a", "b"}
  • 省略符形参...只能用于形参列表的最后一个,且仅用于C和C++通用的类型;
  • 不使用typedef让函数返回数组指针:Type (*function(parameter_list))[dimension] ,或使用decltype(arr) *arrPtr(int i)*表示返回数组指针(这点函数指针同理)。
  • 【11】尾置返回类型: auto func(int i) -> int(*)[10];
  • 默认实参必须是全局定义,其值取决于调用时对应实参的值;
  • constexpr函数不一定返回常量表达式,只要编译期能得到值即可;
  • assert预处理宏依赖于NDEBUG预处理变量,编译参数加入NDEBUG等价于#define NDEBUG
  • 预处理器定义的5个程序调试用的名字:__func____FILE____LINE____TIME____DATE__
  • 函数指针:用指针替换函数名即可,如bool (*pf)(const int &);,函数指针可以作为形参;
  • 当把函数名作为值使用时,自动转换成函数指针(即pf = func等价于pf = &func),调用时也会自动解引用(即bool b = pf(1);等价于bool b = (*pf)(1);
  • 函数类型的形参会被自动转换为指针:void func(bool pf());等价于void func(bool (*pf)()); ;使用decltype(func) *定义函数指针;
  • 相反,函数返回值不会做自动转换,必须指明返回一个函数指针;
  • 局部静态变量一般拥有和全局变量同等地位和处理方式;
  • 内联函数、函数重载部分略;

第七章 类

  • constexpr函数和定义在类内的函数都是隐式inline函数;
  • this指针是一个Type * const,若需要对常量对象执行成员函数,可以在函数参数列表的最后加上const Type * const this;仅在使用整体而非访问部分成员的时候使用this
  • 类内函数定义顺序不影响,编译期先处理成员声明,再处理函数体(类外有影响);但是类内声明之间有顺序,如当一个函数使用类型Type时,必须之前已经定义此Type类型。
  • 只有当类没有声明任何构造函数时才会自动生成默认构造函数(且若有其它成员类且该类没有默认构造函数或其它特殊情况,则无法自动生成);
  • 【11】可以= default使用默认构造函数,若此定义在内部则为内联,否则不是;
  • structclass的唯一区别是默认访问控制,前者是private后者是public
  • 一个可变数据成员mutable即使是const对象成员也不是const
  • 友元声明:friend后接声明即可(并非真正的声明);外类的友元函数要写对应类classtype::;若有函数重载则每种均应分别声明;
  • 若类中已使用外层作用域定义的类型,则类内不可再定义此类型覆盖外层定义;
  • 构造函数初始化列表为初始化,但函数体内为赋值;若一个成员变量同时在定义时被初始化和在初始化列表中,则以初始化列表为准(不推荐);const或引用类型必须初始化;
  • 委托构造函数,即直接在初始化列表调用其它构造函数:Type():Type(...){...},此处Type()委托了Type(...)
  • 函数传参遵循最佳匹配原则,不匹配时会做一次(且仅一次)类类型转换;
  • 用初始化列表初始化类时需要所有成员均为public
  • 字面值常量类必须定义至少一个constexpr的构造函数,而普通类不能定义const的构造函数;
  • 应该在类外部定义静态成员,但需要在类内声明static;类外定义可以访问类的私有成员;
  • static constexpr int period = 30; 进行了声明和初始化,但没有进行定义,最好在类外再定义一下;
  • 前向声明暂时不进行定义,可以用来定义指针或引用,或声明以它为参数或返回类型的函数;声明之后定义之前的类型叫作不完全类型;
  • 静态成员类型可以是不完全类型,也可以就是它所属的类类型,而非静态成员只能声明成它所属类的指针或引用;

第二部分 C++标准库

第八章 IO库

  • ifstream(头文件fstream)和istringstream(头文件sstream)继承自istream(头文件iostream),输出同理;故使用cin的地方均可以使用自定义的ifstreamistringstream对象代替;
  • IO对象无拷贝和赋值;使用<< flush可以刷新缓冲区;当fstream对象被销毁时会自动调用close
  • 高级IO操作略;

第九章 顺序容器

  • 【11】array<type, size>可以灵活指定大小,支持赋值和复制(因此可以直接作为函数参数或返回值),也支持迭代器、内置方法,提供更好的类型安全检查(std::out_of_range异常);
  • 顺序容器提供arr.assign(begin, end) 进行赋值,但传入的迭代器不能指向调用者本身;
  • array外的swap()函数都是只交换指针;array交换整体,但可以用std::swap()实现交换指针;建议统一使用非成员版本的std::swap()
  • 【11】C++11中接受元素个数或范围的insert返回指向第一个新加入元素的迭代器(旧版本返回void),erase()返回被删元素之后元素的迭代器;同样insert的参数不能指向调用者容器;insert()不能使用初始化列表;
  • emplace_front()emplace()emplace_back()分别是push_front()insert()push_back()的构造函数而非拷贝构造函数版本;
  • 访问成员函数front()back()、下标[]at()返回的都是引用;下标不做安全检查,超出范围为ub,at(n)越界返回std::out_of_range()
  • 【11】C++11实现了高效简单的forward_list单向链表,仅支持before_begin()insert_after()emplace_after()erase_after()
  • 不要保存end(),因为在添加删除元素时原先end()会失效,end()操作很快;
  • resize()reserve()不会减少容器占用的内存空间,而C++11的shrink_to_fit()可以(但不保证退回内存);
  • 容器适配器stackqueue默认基于deque实现,可以指明使用除array外任何容器构造stack,以及用listdeque(不能用vector)构造queue ,如stack<string, vector<string>>

第十章 泛型算法

  • 迭代器令算法不依赖于容器而是依赖于元素类型的操作,泛型算法永远不会执行容器的操作,只会运行与迭代器之上;
  • 【11】lambda表达式:[capture list](parameter list) -> return type {...} 参数列表和返回类型可省略(代表指定空参数列表和自动推断返回类型);捕获值在lambda创建时而非调用时拷贝,引用捕获需要保证对应变量存在;
  • []不捕获变量;[names]规定捕获列表,默认为值拷贝,加&表示引用捕获;[&][=]表示自动隐式捕获;[&, identifier_list][=, identifier_list],后者变量前必须加&;
  • 若函数体包含return外任何语句,编译器假定此lambda返回void,可使用尾置->指定返回类型;
  • 【11】参数绑定:auto newCallable = bind(callable, arg_list); 其中使用_n表示newCallable的第n个参数(需要using namespace std::placeholders);使用ref函数和cref函数(#include <functional>)返回的对象实现引用参数绑定,如:auto g = bind(f, a, ref(b), _2, c, _1);(注:此特性新版本已被弃用,建议直接使用lambda);
  • 插入迭代器back_inserter it = vecit = t时会push_back(t)front_inserterpush_front(t)(插入多个时会倒序插入),inserter*it = val时等价于it = c.insert(it, val); ++ it;
  • 流迭代器:注意istream_iterator允许懒惰求值,知道使用迭代器时才真正读取;
1
2
3
4
5
istream_iterator<int> in_iter(cin), eof; // 默认被定义为空对象,故可以用做尾后迭代器
vector<int> vec(in_iter, eof);
// 或者:
while (in_iter != eof)
vec.push_back(*in_iter++);
1
2
3
ostream_iterator<int> out_iter(cout, " "); // 每输出一次后跟一个空格
for (auto e: vec)
*out_iter++ = e; // 事实上*和++不对out_iter做任何事,可以省略
  • 反向迭代器rptr.base()实际为rptr的后一个,以统一左闭右开区间;
  • 泛型算法可能要求的五类迭代器:输入、输出、前向、双向、随机访问;能力更强的迭代器可以传给能力更弱的形参,反之报错;标准库提供的泛型算法见附录A;
  • 对于listforward_list,应优先使用成员函数版本算法而非通用泛型算法;一般成员函数版本会改变容器及其迭代器,而通用函数不会;

第十一章 关联容器

  • 【11】可以使用比较函数定义关联容器:multiset<T, decltype(compareT)*>,第二个为函数指针;
1
2
3
4
5
set<string>::value_type;
set<string> key_type; // 与value_type相同
map<string, int>::key_type;
map<string, int>::mapped_type;
map<string, int>::value_type; // 即pair<const key_type, mapped_type>
  • 一般不对关联容器使用泛型算法(键值是const也意味着不能修改);
  • 面向迭代器的查找遍历:lower_bound()、upper_bound()equal_range()
  • 【11】无序关联容器:unordered_mapunordered_setunordered_multimapunordered_multiset;支持一系列桶接口、桶迭代和哈希策略函数;

第十二章 动态内存

  • 【11】C++11新特性支持智能指针shared_ptrunique_ptr和前者的伴随类weak_ptr
  • make_shared<T>(args)shared_ptr<T>p(q) 定义,编译期使用引用计数智能判定是否销毁指针指向的值并返还内存;当前指针设为nullptr将递减原对象引用计数,可以使用reset()销毁对象(注意别的指向此对象的指针);
  • 空悬指针是delete之后仍然指向原对象地址的指针,相当于野指针;不要混用智能指针和普通指针;
  • make_unique<T>(args) (11不支持)或unique_ptr<T> p(new int(42)); 定义unique_ptr,不能拷贝和赋值(但可以作为函数参数和返回值);用unique_ptr<int> p2(p1.release())p2 = move(p1)p2.reset(p1.release())转移对象所有权(p1、p2均交出当前所有权,并将p1所有权交给p2);
  • weak_ptr<T> p(sp)定义weak_ptr,不增加对象的引用计数,不阻止管理对象的销毁,用p.use_count()返回共享对象数量,p.expired()返回use_count()是否为0,用lock()expired时返回空shared_ptr,否则返回指向p的对象的shared_ptr
  • 不应使用旧规范的动态数组,而应使用vector;使用vector<int>().swap(vec);释放vec空间;
  • allocator类分离了内存分配和对象构造:
1
2
3
4
5
allocator<T> a;
p = a.allocate(n);
a.deallocate(p, n) // 要求p和n必须都是allocate时的
a.construct(p, args);
a.destroy(p);
  • 【11】construct在旧标准中args必须传入一个元素类型值,C++11中可以使用多个构造函数参数初始化,如a.construct(q++, 3, 'c')*q"ccc"
  • 对为构造部分进行初始化,copy函数返回初始化范围的后一个尾置指针;
1
2
3
4
uninitialized_copy(b, e, b2)
uninitialized_copy_n(b, n, b2)
uninitialized_fill(b, e, t)
uninitialized_fill_n(b, n, t)

第三部分 类设计者的工具

第十三章 拷贝控制

  • 三/五法则:五种拷贝控制操作特殊成员函数:拷贝构造、拷贝赋值、析构、移动构造、移动赋值,前三个可以控制类的拷贝操作;常常是否需要自定义拷贝构造和拷贝赋值就看是否需要析构函数;
  • 拷贝构造函数T(const T&);默认合成拷贝构造函数将参数成员逐个拷贝到当前对象中;
  • 直接初始化选取最符合的构造函数,可能调用拷贝构造函数;拷贝初始化可能进行类型转换;拷贝构造函数可以布置一个参数,但必须带默认参数;
  • 拷贝初始化发生:使用=定义变量时;函数传递值参、返回 值类型时;使用C++11的花括号列表时的部分类类型;emplace都进行直接初始化;
  • 重载拷贝赋值运算符:T& operator =(const T &);合成析构函数不会delete它的指针成员,重载析构函数:T::~T()
  • 给函数传递类类型对象时,除了常规作用域查找外还会查找实参类所属的命名空间;当自定义和std::有命名冲突时,默认使用自定义函数;不提倡使用using而应该在每个使用标准库函数时均添加std::
  • 标准库容器、string和shared_ptr既支持移动又支持拷贝,IO类和unique_ptr类可以移动但不能拷贝;
  • 【11】右值引用可以被绑定到要求转换的表达式、字面常量或返回右值的表达式;头文件utilitymove()返回给定对象的右值引用,即承诺除了赋值和销毁外不会再使用原左值,定义移动构造函数:T (T&& other);定义移动赋值函数:T& operator=(T&& other)
  • forwardmove不可以using,必须带std::

第十四章 重载运算与类型转换

  • 使用含有状态的函数对象类:可以被作为参数传入泛型算法,如for_each(vs.begin(), vs.end(), PrintString(cerr,'\n')) ;lambda是函数对象;
1
2
3
4
5
6
7
8
class PrintString{
public
PrintString(ostream &o = cout, char c = ' '):os(o), sep(c){} // 定义了构造函数
void operator()(const string &s)const{ os << s << sep; } // 定义了函数调用运算符
private:
ostream &os;
char sep;
};
  • 【11】C++11支持标准库function类型;
  • 类型转换运算符:[explicit] operator int() const;
  • 表示运算符的模板对象类:greater<int>()等;

第十五章 面向对象程序设计

  • 【11】C++11允许在参数列表后使用override关键字显式注明覆盖了继承的虚函数;
  • 静态成员即使被继承也只存在唯一实例;
  • 【11】在类名后使用final关键字防止继承;
  • 不存在从基类向派生类的隐式类型转换;派生类向基类的转换只对指针和引用有效;
  • 可以使用作用域运算符指定使用的虚函数;
  • 名字查找先于类型检查;在构造函数和析构函数中使用的虚函数就是此函数所在的类的虚函数,而非动态类型的虚函数;
  • 如果一个类会被派生,应该将其析构函数定义为虚函数;

第十六章 模板与泛型编程

  • 有关模板、实例化、包扩展、转发、特例化、std::move 等内容;
  • 推荐阅读 Effective Modern C++缩略版
  • 关于移动语义

第四部分 高级主题

第十七章 标准库特殊设施

  • 【11】tuple类似pair但成员数量任意(固定),定义为tuple<T1, T2, ..., Tn> t(v1, v2, ..., vn)make_tuple(v1, v2, ..., vn)get<i>(t)返回t的第i个成员的引用(t为左值则返回左值引用,右值则右值引用);拆包:std::tie(gpa, grade, name) = make_tuple(3.8, 'A', "张三");
  • 两个辅助类模板:
1
2
3
typedef decltype(item) T;
size_t sz = tuple_size<T>::value;
tuple_element<1, T>::type cnt = get<1>(item);
  • bitset<n> b(u) 定义bitset
  • 【11】regex类定义在regex头文件中,表示一个正则表达式;使用的是ECMAScript正则表达式语言,具体使用略;
  • 【11】随机数引擎类和随机数分布类用法:default_random_engine e; uniform_int_distribution<unsigned> u(0, 9); e是引擎类,u是分布类,用u(e) 返回一个随机数;具体使用略;
  • 【11】C++风格IO格式控制略;C++11新增了十六进制浮点数等格式操作;

第十八章 用于大型程序的工具

  • 异常处理之栈展开:沿函数嵌套调用链查找对应catch子句,若为找到调用标准库函数terminate ,沿着调用链创建的对象将被销毁;
  • 【11】紧跟函数参数列表之后的noexcept标识该函数不会抛出异常,与同样位置写throw()等价;catch(...)捕获所有异常,常常做部分处理后重新抛出throw空语句(会沿调用链向上传递);
  • 命名空间可以不连续;旧C++使用static表示文件级变量,文件外不可访问,新C++应使用未命名名字空间;
  • 多重继承,使用虚继承解决菱形继承问题;

第十九章 特殊工具与技术

  • 重载newdelete控制内存分配:
1
2
3
4
void *operator new(size_t);
void *operator new[](size_t)
void *operator delete(void*) noexcept;
void *operator delete[](void*) noexcept;
  • 运行时类型识别(Run-Time Type Identification,RTTI):使用基类对象指针或引用执行派生类非虚函数时使用;dynamic_cast<type*/type&/type&&>(e) 在转换失败时返回空指针或抛出bad_cast异常;typeid(e)返回运行时类型判断;
  • 析构函数销毁对象但不释放内存;
  • 枚举成员是const,可用enum classenum struct限定作用域;限定作用域的枚举必须加上作用域限定符访问,且不会进行隐式转换;
  • 【11】C++11中可以指定enum的大小:enum big: unsigned long long,且允许前置声明;
  • 成员指针:
1
2
3
4
auto pdata = &Screen::contents;
Screen myScreen, *pScreen = &myScreen;
auto s = myScreen.*pdata;
s = pScreen->*pdata;
  • union:节省空间的类,一次只有一个成员有效;匿名union的成员在union定义所在作用域可以被直接访问;

个人注记

现代C++教程:快速上手C++ 11/14/17/20

  • 现代C++不再允许将字符串字面值常量赋值给char *,应该使用const char *
  • unexpected_handlerset_unexpected()被弃用,应使用noexcept
  • auto_ptr被弃用,应使用unique_ptr
  • register 被弃用,若一个类有析构函数,不再自动生成拷贝构造函数和拷贝赋值运算符;
  • C++17弃用了<ccomplex>
  • 使用extern "C" 分离代码中的C代码和C++代码,再用clang++链接.o文件;(见此
  • C++14之后实现了泛型函数版本的begin()end()等,建议使用;