总结一些C++问题
为什么empty class的大小不是0?
http://stackoverflow.com/questions/2362097/why-is-the-size-of-an-empty-class-in-c-not-zero
C++中的虚函数表: http://blog.csdn.net/haoel/article/details/1948051
C++对象内存布局: http://blog.csdn.net/haoel/article/details/3081328/
C++对象池: http://www.csdn.net/article/2015-11-27/2826344-C++
技术博客: http://www.cnblogs.com/qicosmos/ 祁宇
C++ Primer 知识点整理:
C++基础
声明与定义的关系(2.2)
分离式编译。理解声明和定义的区别。声明使得名字被程序所知。定义负责创建与名字关联的实体。变量的声明规定了变量的类型和名字。定义除此之外还申请存储空间,也可能会为变量赋一个初始值。
extern int i; // 声明
int j; // 定义
extern double pi = 3.14; // 定义。(extern包含初始值就不再是声明了,而是定义。但是在函数体内,如果试图初始化一个由extern标记的变量,将引起错误。)
(定义放在.cpp中,声明放在.h中。见P54)
char ch = 336; // ch = 80 , 336 % 256 = 80
unsigned char uch = -1; // uch = 255, -1 % 256 = 255
- 复制初始化,直接初始化
直接初始化是创建对象并给它赋初值,复制初始化是擦出对象当前值并用新值代替。
对于内置类型,二者没有区别。对类类型的对象来说,有些初始化仅能用直接初始化完成。
int ival(1024); // 直接初始化
int ival = 1024; // 复制初始化
非const对象默认为extern,要使const对象可以在其他文件中使用,必须显示的指定它为extern
引用是一种复合类型,复合类型是指用其他类型定义的类型。(数组和指针是低级的复合类型。)
- 非const引用必须用与该引用同类型的对象初始化(注意是对象,所以字面常量不行)
- const引用可以初始化为右值(字面常量)或不同但相关的类型。
这两点可以看出,const引用在作为函数形参时更加灵活。
int ival = 0x10;
int &refval = ival; // ok
int &refval; // error, must init
int &refval = 0x10; // error, initializer must be a object
const int ival = 0x20;
cont int &refval = ival; // ok
int &refval = ival; // error
int i = 42;
cont int &r = 42; // ok
cont int &r2 = r + i; // ok
string::size_type
是string定义的配套类型,是unsigned。string的size函数返回值类型就是size_type, 不要把它的返回值赋值为int,因为unsigned的范围比int大,所以有可能会溢出。所以要习惯使用类的配套类型来编程。
而数组下标的正确类型是size_tC++不允许定义长度为0的数组变量,但是调用new创建长度为0的数组是合法的,new会返回有效的非零指针,但是不能解引用,因为没有指向任何元素,但是允许进行比较运算。
0为false,非0为true。(那么负数也是true了)。
要理解由多个操作符组成的表达式,必须先理解操作符的 优先级、结合性、求值顺序
new/delete
string *s = new string("hello"); // 动态创建时进行初始化
string *s = new string; // 调用默认构造函数初始化,空字符串
string *s = new string(); // 值初始化,调用默认构造函数,和上面等价
int *n = new int; // uninit
int *n = new int(); // init to 0
在内存耗尽时,new无法获取空间,系统将抛出bad_alloc异常
delete一个0指针是安全合法的
delete p; // 指针p变为悬垂指针(dangling pointer),这有些危险,因为p指向的空间已经释放,所以一个好习惯是在delete之后,将指针置为空。
p = nullptr; // 好习惯
vector(动态增长)
不是一种数据类型,而只是一个类模板,可以定义任意多种的数据类型。vector<int>
是数据类型。
vector的下标操作只能用于获取已经存在的对象,不能用于添加元素,添加元素用push_back。下标操作返回的是引用,可以通过下标改变元素内容。
const_iterator(指向的对象不能更改) 与 const 的iterator是不同的(迭代器本身不能改变,因此它没什么用处)。
任何改变vector长度的操作(如push_back)都会使已存在的迭代器失效。隐式类型转换
在混合类型表达式中,操作数被转换为相同的类型。
用作条件的表达式被转换为bool类型。
用一个表达式初始化或赋值给一个变量,将表达式转换为该变量类型。
函数调用过程中也会发生隐式类型转换。
顶层const,底层const
顶层const表示指针本身是个常量
底层const表示指针指向的对象是个常量
auto 类型说明符
auto让编译器通过初始值来推算变量的类型,所以,auto定义的变量必须有初始值:
int val1 = 1, val2 = 2, &refval = val1;
auto item = val1 + val2; // item为int
auto item2 = refval; // 以引用对象的类型作为auto的类型,item2为int,而不是int&, 如果需要的话,需要显示的写出来'&'
/* auto 会忽略掉顶层const,同时底层const会保留 */
const int ci = i, &cr = ci;
auto b = ci; // b是int(ci的顶层const特性被忽略了)
auto c = cr; // c是int,因为cr是ci的别名
auto d = &i; // d是int*
auto e = &ci; // e是 const int * (对)常量对象取地址是一种底层const
/* 如果希望推断出的auto有顶层const,则需要明确指出 */
const auto f = ci; // f是const int
/* auto 忽略顶层const,保留底层const的例子 */
char c = 'a';
const char* const p1 = nullptr;
auto p2 = p1; // 忽略p1的顶层const,p2的类型是 const char*
cout << typeid(p1).name() << endl; // 使用typeid查看类型
cout << typeid(p2).name() << endl;
p1 = &c; // error
p2 = &c; // OK
*p2 = 'b'; // error
const p3 = p1; // 需要顶层const时,需要显示写出来,p3为 const char* const
decltype 类型提示符
作用是选择并返回操作数的数据类型。
int f()
{
return 0;
}
decltype(f()) ret = 10; // ret 的类型就是函数的返回值类型,即int。注意,编译器并没有调用函数f()
const int ci = 0, &cj = ci;
decltype(ci) x = 0; // x的类型是 const int
decltype(cj) y = x; // y的类型是 const int&,y绑定到x,即y是x的别名。这与auto不同
- 基于范围的for语句
/* 统计字符串中的标点符号个数 */
string str("Hello World!!!");
decltype(str.size()) punct_cnt = 0;
for(auto c : str)
if(ispunct(c))
punct_cnt++;
cout << punct_cnt << endl;
/* 改为大写,使用引用类型 */
for(auto &c : str)
c = toupper(c);
cout << str;
/* 把第一个单词改为大写 */
for(decltype(str.size()) index = 0;
index != str.size() && !isspace(str[index]);
index++)
str[index] = toupper(str[index]);
- 类型别名
/* 两种方式等价 */
typedef std::string::size_type pos;
using pos = std::string::size_type; /*c++11新增*/
- 指向函数指针
一个函数的原型是这样的:int (* f(int))(int*, int)
。能解释出该函数的返回值是什么吗?可以从声明的名字开始由里向外理解。首先f(int)
表示函数名为f,它带有一个int参数。那么该函数的返回值是一个指针。指针的类型是int (*) (int *, int)
.
可以使用typedef简化函数指针的定义:
typedef int (*pFun)(int *, int)
那么原函数可以写成这样:
pFun f(int)
可以看一下Linux 下signal函数的定义。返回值是函数指针,参数也有函数指针.
如果不用typedef的话,函数原型会比较复杂, 像是这样 :
void (*signal(int, void (*)(int)))(int)
使用typedef简化一下:
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
直接使用函数名和在函数名上取地址是等效的(C语言中也是这样):
pFun pf1 = StringCmp;
pFun pf2 = &StringCmp;
指向重载函数的指针 P279
指针的类型必须与重载函数的一个版本精确匹配,没有精确匹配会导致编译出错。
- 重载函数
在相同作用域中的函数,如果函数名字相同,而形参表不同(参数个数、参数类型、引用参数是否为const),称为重载函数(Overloaded function)
不能通过返回值来重载。
对非引用形参而言,const与非const是等价的。
对于引用形参而言,const与非const是不等价的。
同样的,对于指针也是如此。所以,仅当形参是引用或指针时,形参是否为const才有影响。像这样:
bool length(string& s);
bool length(const string& s);
void foo(int *a);
void foo(const int *a);
但是不能通过指针为const实现重载:
// error, redeclaration
void foo(int *a);
void foo( int *const a);
/*下面的两个函数声明被视为重复声明(redeclaration), 原因在于非引用实参的传递方式是按值传递,函数操作的只是一个‘副本’,所以这两种声明对调用者来说是一样的。而第二个函数的const只是为了约束该函数本身不能修改这个‘副本’,并没有对调用者进行约束*/
void fun(int arg);
void fun(const int arg);
/*对于引用形参就不同了,引用是某个变量的别名,操作引用相当于操作变量本身。所以这里的const对调用者进行了约束:调用者传递const int时,调用第二个函数; 传递 int时,调用第一个。所以这两个函数是重载函数。*/
void fun(int &arg);
void fun(const int &arg);
重载与作用域
局部作用域的名字会屏蔽全局作用域的名字。这个规则对变量名和函数名都有效。编译器子如果在局部作用域找到了要查找的名字,就不会在外层作用域中查找了,接下来只检查该名字的使用是否有效。(C++的名字查找发生在类型检查之前)重载确定:将函数调用与重载函数集合中的一个函数相关联的过程分为三个步骤:
- 候选函数:函数名相同的函数(注意,在调用点上,声明可见)
- 选择可行函数:参数个数匹配,每个实参类型与形参类型匹配或者可以被隐式的转换为形参。
- 寻找最佳匹配(如果有的话)
const_cast 和重载 P209
const string & shorterString(const string &s1, const string &s2)
{
return s1.size() <= s2.size() ? s1 : s2;
}
string & shorterString(string &s1, string &s2)
{
auto &r = shorterString(const_cast<const string&>(s1),
const_cast<const string&>(s2));
return const_cast<string &>(r);
}
- 标准I/O库
I/O对象不允许复制或赋值。(因为有不能共享的缓冲区吧。)
由于vector或其他容器要求元素可以复制,所以流对象不能存储在vector或其他容器中;
此外,形参和返回类型也不能为流类型,如果需要传递或返回I/O对象,则必须返回该I/O对象的指针或引用。
如果需要重用文件流读写多个文件,必须在读另一个文件之前调用clear清楚该流的状态! chapter8 P 254
- stringstream
把int添加到一个string中:
http://stackoverflow.com/questions/64782/how-do-you-append-an-int-to-a-string-in-c
与c语言中的sscanf sprintf一个意思。
// 习题8.16: 将一个文件的每一行放在vector<string>中,然后使用istringstream提取每一行中的word
int main()
{
vector<string> content;
string word, line;
ifstream infile;
istringstream iss;
infile.open("test");
while(getline(infile, line))
{
if(line.empty()) continue;
content.push_back(line);
cout << "Full line:"<<line << endl;
cout << "words:" << endl;
iss.str(line);
while(iss >> word)
{
cout << word << endl;
}
iss.clear(); // 将istringstream设置为有效状态,不然后续的while不会被执行
}
infile.close();
return 0;
}
- string
std::string 的 copy-on-write。意思与fork的copy-on-write相同,即:为了提高效率,不是马上进行复制,而是进行了“写”操作时,才进行真正的复制。
/* https://en.wikipedia.org/wiki/Copy-on-write */
std::string x("Hello");
std::string y = x; // x and y use the same buffer
y += ", World!"; // now y uses a different buffer
// x still uses the same old buffer
来验证一下
/* 环境: Ubuntu 12.04; g++ 5.2.1
在str2进行“写”之前,str1和str2指向同一块空间 */
string str1("123");
string str2 = str1;
cout << static_cast<const void *>(str1.c_str()) << endl;
cout << static_cast<const void *>(str2.c_str()) << endl;
str2 += "ABC";
cout << static_cast<const void *>(str1.c_str()) << endl;
cout << static_cast<const void *>(str2.c_str()) << endl;
(吐槽一下,C中打印地址值一个%p就搞定了,C++居然这么复杂。)
不过stl::string还有一种实现方式叫:eager-copy
, 每个string都是一个独立的内存空间,每次拷贝都是深拷贝
,因此c_str()返回的指针地址都是不一样的,优点是内存空间互不干扰,缺点是内存浪费。(摘自CPP开发者2016年5月13日的一篇文章,作者因为copy-on-write而遇到一个坑)
/* 坑在这里,只想通过c_str()返回的地址改变str2的内容,结果连str1的内容也该了! */
string str1("123");
string str2 = str1;
char *p = const_cast<char *>(str2.c_str());
/* 或者这样 char *p = (char *)str2.c_str(); */
p[0] = 'A';
cout << str1 << endl
<< str2 << endl;
/* str1 和 str2 还是指向同一内存空间,内容均为 “A23”。
所以c_str()返回const char*是有道理的,意思就是只能读取不能修改!肆意使用类型转换去掉const属性是危险的。 */
=default =delete
C++11新增加:详细解释见这里Sales_data
class Sales_data
{
public:
/* c++11 新增:如果需要默认行为,那么可以在参数列表后面写上 =default来要求编译器生成构造函数
注意:
1. 如果一个构造函数的所有参数都提供了默认实参,则它实际上也定义了一个默认构造函数
2. 使用默认构造函数时,不要加后面的'()'
Sales_data s();// 错误,这是定义了一个函数。
Sales_data s; // 正确调用默认构造函数
3. 在实际中,如果定义了其他构造函数,那么最好也提供一个默认构造函数
*/
Sales_data() = default;
/* 初始值列表的使用
注意:
1. 如果成员是const、引用,或者属于某种未提供默认构造函数的类类型,则必须使用"初始值列表"为这些成员提供初值! 因为const、引用类型的变量,必须在定义时初始化啊
2. 成员的初始化顺序与它们在类内定义的顺序一致,初始值列表中的顺序不会影响实际的初始化顺序*/
Sales_data(const string &s, unsigned n, double p) :
bookNo(s), units_sold(n), revenue(p) { }
/*
没有出现在构造函数初始值列表中的成员将通过相应的类内初始值(如果存在的话)初始化,或执行默认初始化。
该构造函数的初始化列表中没有显式初始化units_sold和revenue。如果有,类内初始值,则他们使用类内初始值初始化。
*/
Sales_data(const string &s):bookNo(s){}
Sales_data(istream&);
/* c++11: 委托构造函数 delegating constructor */
Sales_data() : Sales_data("", 0, 0) {}
string isbn() const { return bookNo; }
Sales_data &combine(const Sales_data &);
private:
double avg_price() const
{
return units_sold ? revenue/units_sold : 0;
}
string bookNo;
unsigned units_sold = 0; /*c++11新增: 类内初始值(in-class initializer),没有初始值的成员,将被默认初始化*/
double revenue = 0.0;
};
直接初始化 拷贝初始化 P76
使用=
初始化一个变量,实际上执行的是拷贝初始化
,编译器把右侧初始值拷贝到新创建的对象中去。与之相反,如果不使用等号,则执行的是直接初始化
。当初始值参数个数只有一个时,使用直接初始化或拷贝初始化都可以。如果初始化用到的值有多个,一般来说只能用直接初始化式。类类型的隐式转换
如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制,称为:转换构造函数(converting constructor)
。但是只允许一步类类型转换。想要阻止隐式类类型转换时,给该构造函数前面加上explicit
,表示必须显式地调用该构造函数。explicit构造函数只能用于直接初始化,所以不能用于拷贝形式的初始化过程。
class foo
{
public:
foo(const string &s) : name(s) {}
void show() const {
cout << name << endl;
}
private:
string name;
};
//void print(foo &f) // 参数为非const引用,所以不能传递一个临时量,也就是不能传递一个字面值常量,这样使用起来不方便
void print(const foo &f) { // 参数是const引用,所以可以给该参数传递一个临时量,即,可以传递一个字面值常量来使用隐式类类型转换
f.show();
}
int main()
{
string n("hello");
print(n);
/* 错误的方式: 是不是想这样: "hong" --> string("hong")-->foo 。 不好意思,只能允许一步类类型转换。 */
show("hong");
/* 正确:显式地转换为string , 隐式的转换为foo */
print(string("hello"));
/* 正确:隐式的转换为string , 显式地转换为foo */
print(foo("hello"));
return 0;
}
- 类的静态成员 P271
既可以在类内部也可以在类的外部定义静态成员函数。当在类外部定义静态成员时,不能重复static关键字,该关键字只能出现在类内部的声明语句。如果一个常量静态成员在类内部被初始化了,通常也应该在类的外部定义一下该成员。与普通成员相比,静态成员可以作为默认参数,可以是不完全类型。
顺序容器
- vector 可变大小数据组,支持快速随机访问。在尾部之外的位置插入或删除元素可能很慢(因为需要移动元素)
- deque(double-ended queue 双端队列),支持快速随机访问,在头尾插入/删除速度很快
- list 双向量链表,只支持双向顺序访问,在任何位置插入删除速度都很快
- forward_list 单向链表。只支持单向顺序访问。在任何位置插入删除速度都很快(**c++11**)
- array 固定大小数组。支持快速堆积访问。不能添加或删除元素。(**c++11**)
- string 与vector相似的容器,但专门用于保存字符。随机访问快。在尾部插入/删除速度快
P293 选择容器的基本原则,一般使用vector,除非有更好的选择。
所有容器类型都提供的操作
- 容器元素类型必须满足两个约束
- 元素类型必须支持赋值
- 必须支持复制
因此,除了引用类型外(引用不支持一般意义的赋值运算),所有内置类型和复合类型都可以用作元素类型。此外,除了输入输出标准库外(以及auto_ptr),所有其他标准库类型都是有效的容器元素类型。
/* 显式指定类型 */
list<string>::iterator it1 = a.begin();
list<string>::const_iterator it2 = a.begin();
auto it3 = a.begin(); /* 仅当a是const时,it3是const_iterator,还记得吗,auto会保留地层const */
auto it4 = a.cbegin(); /* const iterator , c++11 新增了以c开头的函数 ,当不需要进行写操作时,应使用 cbegin和cend */
/* c++11 可以对一个容器进行列表初始化 */
list<string> authors = {"hong", "jin", "cao"};
/* 提供 容器大小和一个(可选)元素初始值 的构造函数。如果元素类型时内置类型或者是具有默认构造函数的类类型,可以只为构造函数提供一个容器大小参数。如果元素类型没有默认构造函数,除了大小参数外,还必须指定一个显示的元素初始值。此外,只有顺序容器的构造函数才接收大小参数,关联容器并不支持 */
vector<int> ivec(10, -1); // 10个int元素,每个都初始化为-1
forward_list ivec(10); // 10个int元素,每个都初始化为0
/* array 具有固定大小,定义时需要指定元素类型和容器大小 */
array<int, 42>
array<string, 10>
/* 不能对内置数组类型进行拷贝或对象赋值操作,但是array没有此限制 */
int digs[5] = {1,2,3,4,5};
int cpy[5] = digs; // 错误:内置数组不支持拷贝或赋值
array<int, 5> digits = {1,2,3,4,5};
array<int, 5> copy = digits; // array支持
/* 向一个vector、string或deque插入元素会使所有指向容器的迭代器、引用和指针失效 */
/* push_bask 在尾部添加元素,除了array, forward_list之外,每个顺序容器都支持push_back*/
/* push_front list, forward_list,deque支持插入到容器头部 */
/* insert 函数将元素插入到迭代器所指定的位置之前 */
/* emplace : emplace_front emplace emplace_back, 这些操作构造而不是拷贝元素 ,在调用emplace时,会在容器管理的内存空间中直接创建对象,即:在容器中直接构造元素,注意传递给emplace的参数必须与元素类型的构造函数相匹配。 它们对应 push_front insert push_back , 这些函数是将元素拷贝到容器中。*/
c.emplace_back("9870-9110", 25, 15.9); // 使用三个参数的Sales_data构造函数
c.push_back("9870-9110", 25, 15.9); // 错误
c.push_back(Sales_data("9870-9110", 25, 15.9)); // 创建一个临时的Sales_data对象传递给push_back。
/* 容器中访问元素的成员函数: front back 下标 at ,返回的都是引用 ,如果容器是一个const对象,则返回的是const引用,否则返回的是非const应用,可以用来改变元素的值。 */
if(!c.empty()){
c.front() = 42; // 返回的是引用,可以改变它的值
auto &v = c.back(); // 注意auto的用法,需要给出&来定义引用类型
v = 1024;
auto v2 = c.back(); // 这里没有定义引用,因此v2是c.back()的一个拷贝,因此不会改变c中的元素。
v2 = 0;
}
vector<string> svec; // 空vector
cout << svec[0]; // 运行时错误
cout << svec.at(0); // 抛出一个out_of_range异常
/* resize 改变容器大小。array不支持resize,它是固定大小的。如果当前大小大于所要求的大小,容器后部的元素会被删除;如果当前大小小于新大小,会将新元素添加到容器后部 。如果resize缩小容器,则指向被删除元素的迭代器,引用和指针都会失效;对vector、string、deque 进行resize可能导致迭代器、指针和引用失效。P315
1. 不要保存end返回的迭代器 */
/* 管理容量的成员函数, capacity reserve : 只适用于vector和string
capacity() 不重新分配内存空间的话,c可以保存多少元素。
reserve(n) 分配至少能容纳n个元素的内存空间。不改变容器中元素的数量,仅影响vector预先分配多大的内存空间。只有当需要的内存空间超过当前容量时,reserve调用才会改变vector的容量。如果需求大小大于当前容量,reserve至少分配与需求一样大的内存空间(可能更大)。如果需求的大小小于当前容量,reserve什么也不做。
注意capacity 与 size的区别。size是已经保存的元素的数目。
*/
容器适配器
此外,标准库提供了三种容器适配器(adapter,设计模式中为adapter pattern),根据原始容器提供的操作,定义了新接口,包括
- stack
- queue
- priority_queue
默认情况下,stack和queue是基于deque实现的。priority_queue 是在vector之上实现的。可以在创建一个适配器时将一个命名的容器作为第二个类型参数,来重载默认的容器类型:
stack<string, vector<string>> str_stk; //在vector上实现的空栈
模板
模板参数表示在类或函数定义过程中用到的类型或值(非类型参数)。当使用模板时,我们(隐式地[编译器推断]或显示地)使用模板实参,将其绑定到模板参数上。
函数模板
非类型模板参数
一个非类型参数表示一个值而非一个类型。
template <unsigned N, unsigned M>
int compare(const char(&p)[N], const char(&q)[M])
{
return strcmmp(p, q);
}
调用compare(“hi”, “hello”)时编译器根据字面常量的大小来代替N和M,从而实例化模板。实例化出来的版本为:
int compare(const char(&p)[3], const char(&q)[6])
注意: 一个非类型参数可以是一个整形,或者是一个指向对象或函数类型的指针或(左值)引用。绑定到非类型整形参数的实参必须是一个常量表达式。绑定到指针或引用非类型参数的实参必须具有静态的生存期。不能使用非static的局部变量、动态对象作为指针或引用非类型模板参数的实参。指针参数可以使用nullptr或一个值为0的常量表达式实例化。
编写类型无关的代码
模板程序应尽量较少对实参类型的要求
模板编译
注意体会模板编译的特点
类模板
类模板是用来生成类的蓝图的。与函数模板不同的是,编译器不能为类模板推断模板参数类型,必须显式地给出。
泛型算法
back_inserter: 它接收一个指向容器的引用,返回一个与该容器绑定的插入迭代器。当通过此迭代器赋值时,赋值运算符会调用push_back将一个具有给定值的元素添加到容器中。
vector<int> vec; // 空的vector
auto it = back_inserter(vec); // 返回了一个插入迭代器,通过它进行赋值,都会在vec上调用push_back。
*it = 42;
vector<int> vec; // 空
fill_n(back_inserter(vec), 10, 0); // 10个0
可调用对象(callable object)
- 函数
- 函数指针
- 重载了函数调用运算符的类
- lambda表达式 P345
lambda 表达式
lambda表达式表示一个可调用的代码单元。可以理解为一个未命名的内联函数(c++11).
[capture list] (parameter list) -> return type { function body }
可以忽略参数列表和返回类型,但是必须永远包含捕获列表和函数体。
捕获列表是一个lambda所在函数中定义的局部变量的列表。(捕获列表只用于局部非static变量,lambda可以直接使用局部static变量和它所在函数之外声明的名字。)
auto f = [] { return 42; }; //定义了一个可调用对象f
cout << f() << endl; // 对f进行调用
vector<string> words;
stable_sort(words.begin(), words.end(),
[] (const string &a, const string &b)
{ return a.size() > b.size();});
// 使用捕获列表
int sz = 10;
auto wc = find_if(words.begin(), words.end(),
[sz](const string &a)
{ return a.size() >= sz;});
值捕获 & 引用捕获
10.3.3
当定义一个lambda时,编译器将生成一个与lambda对应的新的(未命名的)类类型。默认情况下,该类类型包含对应捕获变量的数据成员。
/**
* lambda
*/
int v1 = 0x10;
auto f = [&v1]{ return v1;}; // 引用捕获
auto result = f();
std::cout << result << std::endl;
return 0;
隐式捕获
让编译器推断捕获列表,方法是在捕获列表中写一个=或&。
bind
10.3.4 (非常重要)
可以将bind看成一个通用的函数适配器。它接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。
默认情况下,bind的那些不是占位符的参数被拷贝到bind返回的可调用对象中。当然也可以引用方式传递。(有可能是希望使用引用,或者对象不能拷贝)
这时需要使用std::ref函数。该函数返回一个对象,包含给定的应用,此对象是可以拷贝的。std::cref生成一个保存const应用的类。
总结如下:
此外,容器只定义了少量操作,大多数操作由算法库提供。所有容器都是类模板。
- 容器元素的初始化:
// 将一个容器初始化为另一个容器的副本
int arr[] = {1,2,3};
vector<int> ages(arr, arr+3); // 指针也是迭代器
vector<int> dump_ages(ages); // 相同类型的容器可以直接复制
// 初始化为一段元素的副本:通过一对儿迭代器
list<int> list_nums(ages); // error: 不同类型的容器不能直接复制
list<int> list_nums(ages.begin(), ages.end()); // 但是可以通过迭代器实现不同类型容器间传递一段范围的数据
list<int> list_nums2(arr, arr+3); // 指针也是迭代器
// 分配和初始化指定数目的元素
const list<int>::size_type list_size = 20;
list<int> list_nums3(0x10, list_size);
list<int> list_nums4(list_size); // 这样进行初始化,元素必须是内置类型或复合类型,或者提供了默认构造函数。如果没有默认构造函数,必须显示的提供元素的初始化值
// c++11 新增: 列表初始化,下面两种方式等价
list<int> list_nums5 = {1,2,3};
list<int> list_nums6 {1,2,3};
list<string> list_str1{10}; //编译器发现不能使用初始化表进行初始化,则会尝试用默认值初始化。即list有10个默认初始化的string元素。
list<string> list_str2{10, "hi"}; // 同理,list有10个值为“hi”的元素
vector不能通过下标形式添加元素,请使用push_bask。下标运算符只能用来访问已经存在的元素。不过下标操作返回的是一个左值,可以用来修改元素。
某些对vector的操作会使迭代器失效:所以如果循环中使用了迭代器,就不要向迭代器所属的容器中添加元素,否则引用失效的迭代器会导致严重错误。
容器内元素的类型约束 (最低要求) P310
- 元素类型必须支持赋值运算
- 元素类型的对象必须可以复制
(PS:可以字节写一个类,来验证一下)
那么除引用类型外(引用不支持一般意义的赋值运算),所有内置类型和复合类型都可以作为容器的元素。除了前面说到的I/O标准库类型(不支持复制或赋值,还有auto_ptr类型)之外,所有其他的标准库类型都是有效的容器元素类型。
上面仅仅是最低要求,因为一些容器操作对元素还有特殊要求,比如:上面提到的“指定容器大小并提供单个初始化式的构造函数”,这就要求元素提供默认构造函数,否则不能使用该容器操作。因此应该注意每个容器操作对元素类型的约束。
关联容器
- 关联容器是set或map。
- 允许键重复或不允许键重复。(multi)
- 排序或不排序 。(unordered_)
关联容器不支持顺序容器的位置相关的操作,例如push_front或push_back。原因是关联容器中元素是根据关键字存储的,这些操作对关联容器没有意义。
关联容器中,关键字类型要求
对于map,multimap,set,multiset: 关键字类型必须定义元素比较的方法(通常使用’<’)。关联容器中的类型别名
key_type 容器关键字类型
mapped_type 关键字关联的类型:只适用于map
value_type 对于set,与key_type相同;对于map,为pair<const key_type, mapped_type> . 注意其中的const,即不能修改关键字
// 一个map的value_type是一个pair,可以改变pair的值,但不能改变关键字成员key_type的值。
set的迭代器是const
尽管有iterator和const_iterator,但是它们都是只读的。用迭代器遍历一个map, multimap, set, multiset时,迭代器关键字升序遍历元素。
通常不对关联容器使用泛型算法。P383
关联容器中,添加元素
// 使用insert
map<string, int> word_count;
word_count.insert({"foo", 10}); //c++11, 用花括号初始化最简便
word_count.insert(make_pair("bar", 10));
word_count.insert(pair<string, int>("cor", 10));
word_count.insert(map<string, int>::value_type("dot", 10));
for(const auto & e : word_count) {
cout << e.first << ":" << e.second << endl;
}
// insert操作的返回值是一个pair<map<x,y>::iterator, bool>
// bool表示有没有插入成功
// iterator指向在map中的位置
// 注意关联容器map,multimap的下标操作也可用来插入元素,注意和insert的区别:
// 使用下标时,当键不存在时,会创建一个默认的键值对而。
关联容器删除
map的下标操作
先搜索关键字,如果没有找到,就将一对新的key-value插入(value是值初始化)
word_count["anna"] = 1;
// 首先根据关键字anna查找,没有找到,就将pair(“anna”,0)插入
// 然后取出新插入的元素,将1赋给它。
通常,解引用一个迭代器返回的类型与下标运算符返回类型是一致的。但对于map则不然。
map下标操作的返回值是mapped_type对象;而解引用map的迭代器时,返回的是value_type对象。
此外,与其他下标运算符一样,map的下标运算符也返回一个左值。
- 关联容器访问元素
如果在使用map时,不存在时不想插入,那么不要使用下标操作,因为下标操作会在不存在时执行插入操作。这时应使用find
动态内存
动态内存,使用动态内存的原因:
- 程序不知道自己需要使用多少对象 : 如容器类
- 程序不知道所需对象的准确类型 chapter15
- 程序需要在多个对象间共享数据
智能指针
智能指针 : 负责自动释放所指向的对象,头文件 memory。类似vector,智能指针也是模板。
- shared_ptr : 允许多个指针指向同一个对象
- unique_ptr : 独占所指向的对象
- weak_ptr : 是一种弱引用,指向shared_ptr所管理的对象
- shared_ptr
shared_ptr<int> p1 = make_shared<int>(42);
cout << *p1 << endl; // 42
shared_ptr<int> p2 = make_shared<int>();
cout << *p2 << endl; // 0
auto p3 = make_shared<string>("joy");
cout << *p3 << endl; // joy
{
auto p4(p1);
cout << p1.use_count() << endl; // 2
cout << p4.use_count() << endl; // 2
} // p4离开了作用域,则引用计数减一,如果引用计数变为0,则释放指向的空间
cout << p1.use_count() << endl; // 1
auto p5 = make_shared<int>(42);
auto p6(p5);
cout << p6.use_count() << endl; // 2
p5 = p1; // p5's count -1 , p1's count +1
cout << p1.use_count() << endl; // 2
cout << p6.use_count() << endl; // 1
/* 如果将shared_ptr存放在了容器中,而后不再需要某些元素,要记得用erase删除不再需要的那些元素,从而释放那些内存 */
StrBlobs设计的目的,对象之间共享vector,使用shared_ptr保证共享的同时,能够使得资源能够正确的释放。
class StrBlob
{
public:
using size_type = vector<string>::size_type;
/* 使用初始化列表来初始化data成员: 7.1.4节*/
StrBlob() :
data(make_shared<vector<string>>()){}
StrBlob(initializer_list<string> il) :
data(make_shared<vector<string>>(il)){}
size_type size() const { return data->size(); }
bool empty() const { return data->empty(); }
void push_back(const string &t) {data->push_back(t);}
void pop_back()
{
check(0, "pop_back on empty StrBlob");
data->pop_back();
}
string& front()
{
cout << "call non-const front()" << endl;
check(0, "front on empty StrBlob");
return data->front();
}
string& back()
{
check(0, "backon empty StrBlob");
return data->back();
}
/* overload with const */
const string& front() const
{
cout << "call const front()" << endl;
check(0, "front on empty StrBlob");
return data->front();
}
/* overload with const */
const string& back() const
{
check(0, "backon empty StrBlob");
return data->back();
}
private:
shared_ptr<vector<string>> data;
void check(size_type i, const string &msg) const
{
if(i >= data->size())
throw out_of_range(msg);
}
};
int main()
{
StrBlob strblob({"hong", "jin"});
cout << strblob.front() << endl; // call non-const front()
strblob.push_back("cao");
cout << strblob.back() << endl;
while(!strblob.empty()) { // make strbloc empty
strblob.pop_back();
}
try{
strblob.pop_back();
}catch(out_of_range e) {
cerr << e.what() << endl;
}
const StrBlob cstrblob({"hong", "jin"}); // 使用初始化列表 c++11
cout << cstrblob.front() << endl; // call const front()
//cstrblob.push_back("cao"); // error
return 0;
}
unique_ptr
独占它所指向的对象。没有类似make_shared的标准库函数返回一个unique_ptr。由于独占,因此unique_ptr不支持普通的拷贝或赋值操作。但是可以拷贝或赋值一个将要被销毁的unique_ptr,如从函数返回一个unique_ptr。早版本的标准库包含了auto_ptr,它具有unique_ptr的部分特性,但不是全部。不能在容器中保存auto_ptr,也不能从函数中返回。编写程序时应该使用unique_ptr。weak_ptr
是一种不控制所指向对象生存期的只能指针,它指向由一个shared_ptr管理的对象。将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。当最后一个指向对象额shared_ptr被销毁,对象就会被释放,即使weak_ptr指向对象,对象也会被释放,因此名字中有弱。动态数组 P425
使用new分配一个大小为0的数组时,new返回一个合法的非空指针,此指针保证与new返回的其他任何指针都不同,此指针不能解引用。
//接收指针参数的智能指针构造函数是explicit的,必须使用直接初始化的形式初始化一个智能指针
shared_ptr<int> p = new int(1); // 错误,因为不能将内置指针隐式转换为一个智能指针
shared_ptr<int> p(new int(1)); // 正确,直接初始化
// 使用智能指针管理动态数组
unique_ptr<int[]> up(new int[10]); // 其中的[]表示,up指向的是一个数组
up.release(); // auto delete [] array
/* shared_ptr不直接支持管理动态数组,如果想这么做,必须自己定义一个删除器 */
shared_ptr<int> sp(new int[10], [](int *p) { delete [] p; }); // lambda表达式
sp.reset(); // use lambda release the array, it call delete []
- allocator 类
它帮助我们将内存分配和对象构造分离开来。它提供一种类型感知的内存分配方法,它分配的内存是原始的、未构造的。
/* 分配空间 */
allocator<string> alloc;
auto const p = alloc.allocate(10);
/* 构造 : construct */
auto q = p; // auto 忽略顶层const,因此q是 string *
alloc.construct(q++); //空string
alloc.construct(q++, 10, 'c'); // cccccccccc
alloc.construct(q++, "hello"); // hello
for(auto r = p; r != p + 3; r++) {
cout << *r << endl;
}
cout << *(p+4) << endl; // error: access unconstruction location
//auto r = q;
//for_each(p, r, [](const string &s) { cout << s << endl; });
//while(r != p)
// cout << *--r << endl;
/* 释放真正构造的string */
while(q != p)
alloc.destroy(--q);
/* 释放内存 */
alloc.deallocate(p, 10);
文本查询程序 P432
// 使用shared_ptr共享数据
class QueryResult
{
friend ostream &print(ostream & os, const QueryResult &qr);
public:
using line_no = vector<string>::size_type; // 与typedef等价
QueryResult(string s,
shared_ptr<set<line_no>> p,
shared_ptr<vector<string>> f) :
sought(s), lines(p), file(f){}
private:
string sought;
shared_ptr<set<line_no>> lines;
shared_ptr<vector<string>> file;
};
class TextQuery
{
public:
using line_no = vector<string>::size_type;
TextQuery(ifstream &ifs) : file(make_shared<vector<string>>()) // 别忘了最后的括号
//: file_(new vector<string>())
{
string line;
while(getline(ifs, line))
{
file->push_back(line);
int n = file->size() - 1; // 行号
istringstream iss(line);
string word;
while(iss >> word)
{
auto &lines = wm[word]; // 要清楚map的下标操作的含义。如果key不存在,就新建一项,并初始化value。这里的value是一个shared_ptr,会初始为nullptr
if(!lines)
//r.reset(new shared_ptr<set<lineno>>()); // reset only use ordinary pointer
//r.reset(make_shared<set<lineno>>()); // reset only use ordinary pointer
lines.reset(new set<line_no>); // 由于上面返回的是引用,因此可以对lines进行改变,使用reset,让lines指向一个空的set
lines->insert(n);
// 使用 find和insert, 比较麻烦
//auto fr = map_.find(word);
//if(fr == map_.end()) {
// auto r = map_.insert(pair<string, shared_ptr<set<lineno>>>(word, make_shared<set<lineno>>()));
// if(!r.second) {
// std::abort();
// }
// r.first->second->insert(n);
//} else {
// fr->second->insert(n);
//}
}
}
}
QueryResult query(const string & sought) const
{
static shared_ptr<set<line_no>> nodata(make_shared<set<line_no>>());
//static shared_ptr<set<lineno>> nodata(new set<lineno>);
auto loc = wm.find(sought); // dont't use wm[sought], if sought not exist, sought will be added to map!!
if(loc == wm.end())
return QueryResult(sought, nodata, file);
else
return QueryResult(sought, loc->second, file);
}
private:
shared_ptr<vector<string>> file;
map<string, shared_ptr<set<line_no>>> wm;
};
ostream &print(ostream & os, const QueryResult &qr)
{
os << qr.sought << "occures" << qr.lines->size() << endl;
for(auto l : *(qr.lines))
{
os << "line:" << l + 1 << " "
<< (*(qr.file))[l] << endl;
}
return os;
}
int main()
{
ifstream ifs;
ifs.open("testfile");
TextQuery tq(ifs);
print(cout, tq.query("hong"));
return 0;
}
注意什么reset只能用普通指针, share_ptr在构造的时候可以使用new和make_shared
此外,学习了网友的实现,可以检测单词中是否有标点,如果有就删除, 如”again.” –> “again”
// g++ test_remove_copy_if -std=c++11 -Wall
#include <algorithm>
#include <iostream>
#include <string>
#include <vector>
#include <cctype> // ispunct
using namespace std;
bool IsOdd(int n) { return n % 2 == 0;}
void Print(int n) { cout << n << endl;}
bool IsPunct(int c) { return ispunct(c);}
int main()
{
/* remove punct in string */
string word("again."), word_nopunct;
//remove_copy_if(word.begin(), word.end(), back_inserter(word_nopunct), ispunct);
//remove_copy_if(word.begin(), word.end(), back_inserter(word_nopunct), IsPunct);
remove_copy_if(word.begin(), word.end(),
back_inserter(word_nopunct),
//[](string::value_type c)->bool { return ispunct(c); }); // ok
//[](string::value_type c){ return ispunct(c); }); // ok
IsPunct); // ok
//std::ispunct); // compile error
cout << word_nopunct << endl;
/* remove odd in nums */
vector<int> nums = {1, 2, 3, 4, 5, 6};
vector<int> odd_nums;
remove_copy_if(nums.begin(), nums.end(), back_inserter(odd_nums), IsOdd);
//remove_copy_if(nums.begin(), nums.end(), odd_nums.begin(), IsOdd); // wrong, odd_nums is empty, you should use back_inserter
for_each(odd_nums.begin(), odd_nums.end(), Print);
return 0;
}
- 容器的容器
容器中的元素是容器。注意下面的空格!(c++11已经可以不用空格了,-_-|||)
vector<vector<int> > something1; // 必须有空格
vector<vector<int>> something2; // 错误, 编译器会将 >> 认为是单个符号
一个类通过定义5种特殊的成员函数来控制对象的拷贝、移动,赋值和销毁:
- 拷贝构造函数 copy constructor
- 拷贝赋值运算符 copy-assignment operator
- 移动构造函数 move constructor c++11
- 移动赋值运算符 move-assignment operator c++11
- 析构函数 destructor
拷贝和移动构造函数定义了当同类的另一个对象初始化本对象时发生什么。
拷贝和移动赋值运算符定义了将一个对象赋予同类型的另一个对象时做什么。
析构函数定义了此类型对象销毁时做什么。这些操作称为 拷贝控制操作 (copy control)
- 拷贝、赋值、销毁 P442
拷贝初始化通常使用拷贝构造函数来完成,如果有移动构造函数,则拷贝初始化有时会使用移动构造函数来完成。
什么时候发生拷贝初始化?(与直接初始化相区别)
- 使用 = 定义变量
- 将一个对象作为实参传递给一个非引用类型的形参
- 从一个返回类型为非引用类型的函数返回一个对象
- 用花括号列表初始化一个数组中的元素或一个聚合类的成员。P266
- 标准库容器的insert、push成员对其元素进行拷贝初始化。与之相对,emplace进行直接初始化。
对于拷贝初始化,初始值不能通过隐式调用一个explicit的构造函数进行类型转换。
vector<string> v1(10); // 直接初始化,10个空的string
vector<string> v2 = 10 ; // error, 接收大小参数的构造函数是explicit
void f(vector<string>); // 参数进行拷贝初始化
f(10); // error,不能用一个explicit的构造函数拷贝一个实参
f(vector<string>(10)); // ok, 从一个int直接构造一个临时的vector
编译器可以绕过拷贝构造函数,直接创建对象。
string null_book = "9-99"; // 拷贝初始化,(还记得前面说的直接初始化吗? 使用=是拷贝初始化,使用()是直接初始化)
--->
string null_book("9-99"); // 编译器被允许将上面的改成下面这句。但是要保证拷贝/移动构造函数必须存在并可访问。(例如,不能是private)
编译器对return by value的优化,C++ FAQ值得一看
All(?) commercial-grade compilers optimize away the extra copy。编译器将代码优化,所以没有额外的开销了。
拷贝赋值运算符
- 析构函数
构造函数有一个初始化部分(初始化列表)和一个函数体。析构函数类似,包括一个函数体和一个析构部分。在构造函数中,成员的初始化是在函数体执行之前完成的,按照它们在类中出现的顺序初始化。析构函数中,先执行函数体,然后撤销成员。成员按初始化顺序逆序销毁。(注意,销毁一个内置指针成员时,不会delete它所指向的对象。所以需要在函数体中释放!而智能指针是类类型,具有析构函数,智能指针成员能在析构阶段被自动销毁。)
还有,当一个指针或引用离开作用域时,析构函数不会运行。
- 何时调用析构函数
- 变量离开其作用
- 一个对象被销毁时,其成员被销毁
- 容器(标准容器、数组)被销毁时,其元素被销毁
- 动态分配的对象,使用delete时被销毁
- 对于临时对象,当创建它的完整表达式结束时被销毁
析构函数体本身并不直接销毁成员,成员是在析构函数体之后隐含的析构阶段中被销毁的。在整个对象销毁过程中,析构函数体是作为成员销毁步骤之外的另一部分而进行的。
class Bar{
public:
~Bar() {
cout << "Bar's dtor" << endl;
}
};
class Foo{
public:
// copy assign operator
Foo & operator=(Foo & o) {
*ip = *o.ip;
b = o.b;
}
// copy ctor
Foo(Foo & o) {
if(ip == nullptr)
ip = new int(0);
*ip = *o.ip;
b = o.b;
}
Foo() {
ip = new int(0x10);
}
~Foo() {
cout << "in dtor" << endl;
if(ip != nullptr) {
cout << "delete dynamic variable" << endl;
delete ip;
}
cout << "out dtor" << endl;
}
private:
int * ip = nullptr; // in-class initializer
Bar b;
};
int main(){
Foo f;
return 0;
}
/*
输出:
in dtor
delete dynamic variable
out dtor
Bar's dtor
*/
三/五法则
三个控制类的拷贝操作:
- 拷贝构造函数
- 拷贝赋值运算符 (通常返回左值的引用,需要考虑自赋值, 拷贝构造函数不用考虑,呵呵,废话)
- 析构函数
新标准下,还可以定义一个移动构造函数和一个移动赋值运算符。(c++11)
需要析构函数的类,也需要拷贝和赋值操作。
例如上面的类Foo,它包含一个动态分配的数据成员,合成的析构函数不会delete一个指针成员,因此,该类需要一个析构函数来释放构造函数分配的内存。 那么,这样的话,拷贝构造函数和赋值操作符也是需要的。如果使用合成的拷贝和赋值操作(浅拷贝,只复制指针的值,并不复制指向的内存),那么会导致多个对象指向同一块内存,将会导致同一块内存被delete多次或者使用不存在的内存(一个释放了,当是另一个指针并不知道),显然是一个错误。
需要拷贝操作的类也需要赋值操作,反之亦然。但是不必然意味需要析构函数。
使用=default
可以通过将拷贝构造和赋值操作符定义为=default,来显示地要求编译器生成合成的版本。阻止拷贝 =delete
iostream类阻止了拷贝,以避免多个对象写入或读取相同的IO缓冲。可以通过将拷贝构造函数和拷贝赋值操作符定义为删除函数来阻止拷贝。 =delete告诉编译器和代码的读者,我们不希望定义这些成员。
struct NoCopy{ // 默认全是public的类,可以使用struct
NoCopy() = default; // 使用合成的默认构造函数
NoCopy(const NoCopy &) = delete; // 阻止拷贝
NoCopy & operator=(const NoCopy &) = delete; // 阻止赋值
~NoCopy() = default; // 使用合成的析构函数
};
与=default不同,=delete必须出现在函数第一次声明的时候。而且可以对任何函数指定=delete(只能对编译器可以合成的默认构造函数或拷贝控制成员:拷贝构造和拷贝赋值,使用=default)。例如,希望引导函数匹配过程时,删除函数有时也是有用的。
析构函数不能是删除的。
合成的拷贝控制成员可能时删除的。P450
在旧标准中,是通过将拷贝构造函数和赋值操作符声明为private来阻止拷贝的。但是,友元和成员函数仍旧可以拷贝对象。为了阻止友元和成员函数进行拷贝,可以将这些拷贝控制成员声明为private,并但并不定义它。这样,试图 的代码将在编译阶段标记为错误;成员函数或友元函数中的拷贝将导致链接错误。
不过,在新标准中应该使用=delete来定义,而不是使用private。
// 类值版本的HasPtr, 需要析构函数(delete 指针),拷贝控制成员(深拷贝,创建新内存空间)
class HasPtr {
public:
// default ctor
HasPtr(const string &s = string()):
ps(new string(s)), i(0) {}
// copy ctor
HasPtr(const HasPtr &p) :
ps(new string(*p.ps)), i(p.i) { }
// copy assign
HasPtr & operator= (const HasPtr &rhs) {
// 可以防止自拷贝时出现错误
auto newp = new string(*rhs.ps);
delete ps;
ps = newp;
i = rhs.i;
return *this;
}
~HasPtr() { delete ps; }
private:
string *ps;
int i;
};
// 使用shared_ptr的类, 可以看到使用shared_ptr会很方便
class HasPtrSP
{
public:
// default ctor
HasPtrSP(const string &s = string()) :
sps(make_shared<string>(s)) , i(0){ }
// copy ctor
HasPtrSP(const HasPtrSP & p) :
sps(p.sps), i(p.i) {}
// copy assign
HasPtrSP & operator= (const HasPtrSP & rhs){
sps = rhs.sps; // shared_ptr能正确处理自赋值的情况。左操作数引用计数减一,右边引用计数加一。如果是同一对象,自赋值后,引用计数还是一。
i = rhs.i;
}
private:
shared_ptr<string> sps;
int i;
};
// 不使用shared_ptr, 自己进行计数统计
class HasPtrSC {
public:
// default ctor, 开始时,计数是1
HasPtrSC(const string &s = string()):
ps(new string(s)), i(0), count(new size_t(1)) {}
// copy ctor
HasPtrSC(const HasPtrSC &p) :
ps(p.ps), i(p.i), count(p.count){
++*count; // 增加计数
}
// copy assign
HasPtrSC & operator= (const HasPtrSC &rhs) {
++*rhs.count; // 增加右操作数的计数
if(--*count == 0) { // 减少左操作数,即自己的计数
delete ps; // 记得释放资源
delete count; // 记得释放资源
}
ps = rhs.ps;
i = rhs.i;
count = rhs.count;
return *this; // 返回左操作数的引用(赋值操作符这么规定的)
}
// dtor
~HasPtrSC() {
if(--*count == 0) { // 减少计数,如果计数变为0,则释放资源。
delete ps;
delete count;
}
}
int use() const { return *count;}
private:
string *ps;
int i;
size_t *count; // 由于引用计数是多个对象共享的,所以使用了指针
};
// 问题: shared_ptr是怎么实现的呢?
// 参考这里: http://my.oschina.net/costaxu/blog/103119
/* shared_ptr:
1. shared_ptr是一个非常实用的智能指针。
2. shared_ptr的实现机制是在拷贝构造时使用同一份引用计数。
3. 对同一个shared_ptr的写操作不是线程安全的。 对使用同一份引用计数的不同shared_ptr是线程安全的。
*/
- 交换操作 P459
对于与重排元素顺序的算法一起使用的类,定义swap是非常重要的。这类算法在需要交换两个元素时会调用swap。如果一个类定义了自己的swap,那么算法会使用这个自定义版本;否则将使用标准库定义的swap。
// 为了提高效率,有时需要自定义的swap。这里swap是HasPtr类的友元函数。
// 为了优化代码,将swap声明为inline
inline void swap(HasPtr &lhs, HasPtr &rhs)
{
using std::swap;
swap(lhs.ps, rhs.ps);
swap(lhs.i, rhs.i);
}
习题13.31值得一看 13.31, 定义了swap,供std::sort排序时使用。
还有stackoverflow上关于swap不总是被调用的解答
- 动态内存管理类
书上对于这个类的设计很值得学习,课后习题也很有价值(13.39 ~ 13.44)!
https://github.com/pezy/CppPrimer/tree/master/ch13
13.44 是设计string类
https://github.com/pezy/CppPrimer/blob/master/ch13/ex13_40.cpp
https://github.com/chenshuo/recipes/blob/fcf9486f5155117fb8c36b6b0944c5486c71c421/string/StringTrivial.h (面试中写string类)
// 实现类似Vector的容器,但是只可以用string作为元素
class StrVec{
public:
StrVec() : elements(nullptr), first_free(nullptr), cap(nullptr) { }
StrVec(const StrVec&);
StrVec &operator=(const StrVec &);
~StrVec();
void push_back(const string &);
size_t size() const { return first_free - elements; }
size_t capacity() const { return cap - elements; }
string *begin() const { return elements; }
string *end() const { return first_free; }
private:
static allocator<string> alloc;
void chk_n_alloc() {
if(size() == capacity())
reallocate();
}
pair<string*, string*> alloc_n_copy(const string*, const string *);
void free();
void reallocate();
string *elements;
string *first_free;
string *cap;
};
allocator<string> StrVec::alloc;
void StrVec::push_back(const string &s)
{
chk_n_alloc();
alloc.construct(first_free++, s);
}
pair<string*, string*>
StrVec::alloc_n_copy(const string *b, const string *e)
{
auto data = alloc.allocate(e - b);
return {data, uninitialized_copy(b, e, data)}; // 初始化列表,pair->first 是内存的起始地址,pair->second是最后一个内存的后面位置
}
void StrVec::free()
{
if(elements) {
for(auto p = first_free; p != elements; )
alloc.destroy(--p); // 调用string的析构函数
alloc.deallocate(elements, cap - elements); // 释放空间
}
}
StrVec::StrVec(const StrVec &s)
{
auto newdata = alloc_n_copy(s.begin(), s.end());
elements = newdata.first;
cap = first_free = newdata.second;
}
StrVec::~StrVec() { free(); }
StrVec & StrVec::operator=(const StrVec &s)
{
auto newdata = alloc_n_copy(s.begin(), s.end()); // 先创建新内存,可以正确处理自赋值的情况
free();
elements = newdata.first;
cap = first_free = newdata.second;
return *this;
}
void StrVec::reallocate()
{
auto newcapacity = size() ? 2 * size() : 1; // 容量扩大一倍
auto newdata = alloc.allocate(newcapacity);
auto dest = newdata;
auto elem = elements;
for(size_t i = 0; i != size(); ++i)
alloc.construct(dest++, std::move(*elem++)); // 调用std::move的返回的结果(一个右值引用)会令construct使用string的移动构造函数(参数为右值引用)!
free();
elements = newdata;
first_free = dest;
cap = elements + newcapacity;
}
int main()
{
StrVec sv;
sv.push_back("hong");
sv.push_back("jin");
cout << *sv.begin() << endl; // hong
cout << *(sv.begin() + 1) << endl; //jin
cout << sv.size() << " " << sv.capacity() << endl; // 2 2
sv.push_back("jin");
cout << sv.size() << " " << sv.capacity(); // 3 4
return 0;
}
StrVec类中在自己内部空间需要重新分配时,使用了move ctor
在拷贝构造、拷贝赋值过程中,使用的是uninitialized_copy
想想为什么? 个人理解:对于自己内部空间不够用时,可以使用move接管。但是拷贝时,如果使用move接管‘别人’的内存空间会显得不合适,因为你接管了,别人就没办法使用了。
对象移动
新标准一个最主要的特性是可以移动而非拷贝对象的能力。比如有些情况下,对象拷贝后就立即被销毁了,所以移动而非拷贝对象会大幅提升性能。
旧版本的标准库中,容器中所保存的类必须是可拷贝的。但在新标准中,我们可以用容器保存不可拷贝的类型,只要它们能被移动即可。
标准库容器,string、shared_ptr既支持移动也支持拷贝。IO类和unique_ptr可以移动但不能拷贝,因为它们包含不能被共享的资源,如指针或IO缓冲。
右值引用
为了支持移动操作,新标准引入了一种新的引用类型,即 右值引用 rvalue reference,就是必须绑定到右值的引用,也就是它只能绑定到一个将要销毁的对象。因此,我们可以自由地将一个右值引用的资源“移动”到另一个对象中。
关于左值和右值,见4.1.1节, P121
int i = 42;
int &r = i; // ok
int &&rr = i; // error, 不能将右值引用绑定到一个左值上
int &r2 = i * 42; // error i * 42 是一个右值,
const int &r3 = i * 42; // ok, we can bind a const ref to a rvalue
int &&rr2 = i * 42; // ok, we can bind a rvalue ref to a rvalue;
返回左值引用的函数,还有赋值、下标、解引用和前置递增/递减运算符,都是返回左值的表达式的例子。可以将一个左值引用绑定到这类表达式的结果上。
返回非引用类型的含糊,还有算数、关系、位以及后置递增/递减运算符,都生成右值。不能将一个左值引用绑定到这类表达式上,但我们可以将一个const的左值引用或者一个右值引用绑定到这类表达式上。
左值持久;右值短暂
左值有持久的状态,而右值要么是字面常量,要么是在表达式求值过程中创建的临时对象。由于左值只能绑定到临时对象,所以:- 所引用的对象将要被销毁
- 该对象没有其他用户
这就意味着:使用右值引用的代码可以自由地接管所引用的对象的资源。
可以通过move来获得绑定到左值上的右值引用。
在移动操作之后,移动源对象必须保持有效的、可析构的状态,但是用户不能对其值进行任何假设(不要使用它的值了)。
- 移动构造函数和移动赋值运算符
// 移动构造函数的参数是一个右值引用,且不分配新内存,它接管给定的StrVec中的内存。接管之后,将被接管对象的指针置为nullptr。这样就完成了从给定对象的移动操作。最终,被接管对象被销毁,意味着调用它的析构函数。
StrVec::StrVec(StrVec &&s) noexcept // 不抛出任何异常时,一定要写上noexcept
:elements(s.elements), first_free(s.first_free), cap(s.cap)
{
s.elements = s.first_free = s.cap = nullptr;//将rhs置于可析构状态
}
StrVec & operator=(StrVec &&rhs) noexcept
{
if(this != &rhs) { // 防止自赋值
// 释放已有资源
free();
// 接管已有资源
elements = rhs.elements;
first_free = rhs.first_free;
cap = rhs.cap;
//将rhs置于可析构状态
rhs.elements = rhs.first_free = rhs.cap = nullptr;
}
}
- 移动右值,拷贝左值。但是如果没有移动构造函数,右值也拷贝。
//根据=右边的操作数类型(左值,还是右值),决定调用拷贝赋值操作符还是移动赋值操作符。
StrVec v1, v2;
v1 = v2; // v2是左值,使用拷贝赋值
StrVec getVec(istream &); // getVec返回一个右值
v2 = getVec(cin); // getVec返回的是右值,调用移动赋值
注意学习P478的“拷贝并交换赋值运算符和移动操作”,单一的赋值运算符就实现了拷贝赋值运算符和移动赋值运算符两种功能。总结在这里:
移动和拷贝的重载函数通常有一个版本接收一个const T&, 而另一个版本接收一个T&&。原因是,拷贝不会修改源对象,而移动则是‘接管’源对象。所以一个有const,一个没有const。
引用折叠
16.2.5 P608 涉及到模板参数推断和引用。比较复杂,很重要。
先记住下面的知识点。
重载
哪些运算符可以重载,哪些不可以?
总结:带点的都不能被重载。重载作为成员还是普通友元函数?
// 可调用对象,重载了函数调用运算符()
class PrintString{
public:
PrintString(ostream &o = cout, char s = ' ')
: os(o), sep(s) {}
void operator()(const string &s){
os << s << sep;
}
private:
ostream &os;
char sep;
};
int main()
{
PrintString ps;
ps("hello");
ps("world");
vector<string> vs = {"hong", "jin"};
// 可调用对象可以用在标准库中的函数。
for_each(vs.begin(), vs.end(), PrintString(cerr, '\n'));
int i = -42;
absInt abs;
cout << abs(i) << endl;
return 0;
}
面向对象程序设计
// 基类
class Quote{
public:
Quote() = default; // 如果用户定义了一个构造函数,那么编译器不会自动生成默认构造函数。如果仍需要编译器生成默认构造,可以使用=default告诉编译器继续生成默认构造函数
Quote(const std::string &book, double sales_price) :
bookNo(book), price(sales_price){}
std::string isbn() const { return bookNo; }
virtual double net_price(std::size_t n) const { // 虚函数
return n * price;
}
virtual ~Quote() = default; // 虚析构函数!
private:
std::string bookNo; // private成员只能在基类中使用。
protected:
double price = 0.0; // 类内初始化。protected成员能被派生类访问。不能在类外访问
};
class Bulk_quote : public Quote{
public:
Bulk_quote() = default;
Bulk_quote(const std::string &book, double sales_price, std::size_t min, double dsc) :
Quote(book, sales_price), min_qty(min), discount(dsc) {} // 调用基类构造函数初始化基类成员。否则这些成员将进行默认初始化。通常,每个类控制它自己的成员初始化过程。派生类构造函数只初始化它的直接基类。
double net_price(std::size_t n) const override; // override指出覆盖了基类的虚函数。
private:
std::size_t min_qty = 0;
double discount = 0.0;
};
double Bulk_quote::net_price(std::size_t n) const{
if(n >= min_qty) return n * price *(1 - discount);
else return n * price;
}
- 防止继承-final关键字 (c++11)
class NoDerived final { }; // NoDerived 不能作为基类。
也可以将某个函数指定为final,那么任何尝试覆盖该函数的操作都将引发错误。
- 静态类型和动态类型 (重要)
使用存在继承关系的类型时,必须将一个变量或表达式的静态类型 static type与该表达式表示对象的动态类型 dynamic type 。 静态类型在编译时总是已知的,它是变量声明时的类型或表达式生成的类型:动态类型则是变量或表达式表示的内存中的对象的类型。动态类型直到运行时才可知。
基类的指针或引用的静态类型可能与其动态类型不一致。(由此可以实现运行时多态)
存在派生类向基类类型的转换,因为派生类对象包含一个基类的部分,基类的引用或指针可以绑定到该基类部分上。
不存在基类向派生类的隐式转换。
派生类向基类的自动类型转换只对指针或引用类型有效,在派生类类型和基类类型之间不存在这样的转换。
存在继承关系的类型之间的转换规则:
- 从派生类向基类的类型转换只对引用或指针有效
- 不存在由基类向派生类的隐式转换
派生类向基类类型的转换可能由于访问受限而变得不可行。将一个派生类对象拷贝、移动或赋值给一个基类对象,这种操作只能处理派生类对象的基类部分,称为被切掉 sliced down
虚函数
使用基类的引用或指针调用一个虚成员函数时会执行动态绑定,因为直到运行时才能知道到底调用了哪个版本的虚函数。所以所有虚函数都必须有定义。公有 私有 受保护继承
// P543
struct Base {
}
struct Pub_Derv : public Base {
}
struct Priv_Derv : private Base { // private 不影响派生类的访问权限,它只限制了派生类用户对于基类成员的访问权限。
}
- 友元与继承
就像友元关系不能传递一样,友元关系也不能继承。
名字查找与继承
除了覆盖继承而来的虚函数之外,派生类最好不要重用其他定义在基类中的名字。否则将发生隐藏。
隐藏 P549
虚函数与作用域 P550 一定要理解这个例子。关键是理解上面的名字查找过程。
模板与泛型编程
模板参数:类型参数(使用typename或class),非类型参数(使用特定的类型名,如unsigned)
函数模板和类模板成员函数的定义通常放在头文件中。
类模板(class template)是用来生成类的蓝图。对于函数模板,编译器可以推断出模板参数的类型,但是对于类模板必须提供类型信息,即显式模板实参列表,它们被绑定到模板参数,编译器使用这些模板实参来实例化特定的类。
默认情况下,一个类模板的成员函数只有当程序用到它时才进行实例化。如果一个成员函数没有被使用,则不会被实例化。
默认模板实参 (c++11)
template <typename T, typename F = less<T>>
int compare(const T &v1, const T &v2, F f = F())
{
if(f(v1, v2)) return -1;
if(f(v2, v1)) return 1;
return 0;
}
一个类(普通类、模板类)可以包含本身是模板的成员函数,这种成员为成员模板。成员模板不能是虚函数。
std::move 是如何定义的
template <typename T>
typename remove_reference<T>::type && move(T&& t)
{
return static_cast<typename remove_reference<T>::type&&>(t);
}
// move的参数T&& 是一个指向模板参数的右值引用。通过引用折叠,此参数可以与任何类型的实参匹配,可以传递给move一个左值,也可以传递一个右值。
// remove_reference P606 去掉引用,T& 和 T&& 被转换为T保存在type中,如果传递给move的不是引用,则直接返回T。
变长模板 (c++11) P618
可变参数函数通常是递归的,第一步调用处理包中的第一个实参,然后用剩余实参调用自身。为了终止递归,还需要定义一个非可变参数的函数。
template<typename T>
ostream &print(ostream &os, const T &t)
{
return os << t;
}
template<typename T, typename... Args>
ostream &print(ostream &os, const T &t, const Args&... reset)
{
os << t << ",";
return print(os, reset...);
}
- 运行时类型识别 (RTTI run-time identification),通过下面两个运算符实现:
- typeid 返回表达式的类型
- dynamic_cast 将基类的指针或引用安全的转换成派生类的指针或引用
当将这两个运算符作用于某种类型的指针或引用,并且该类型含有虚函数时,运算符将使用指针或引用所绑定对象的动态类型。
这两个运算符特别适用于一下情况:
想用基类对象的指针或引用执行某个派生类操作并且该操作不是虚函数。一般来说,只要有可能我们应该尽量使用虚函数。当操作被定义成虚函数时,编译器将根据对象的动态类型自动地选择正确的函数版本。
- dynamic_cast
// 对于指针类型
void f(Base *bp)
{
if(Derived *dp = dynamic_cast<Derived *>(bp)) {
// 使用dp指向的Derived对象
} else { // dynamic_cast 对于指针类型失败时返回0
// 使用bp指向的Base对象
}
}
// 对于引用类型, 转换失败时会抛出一个名为std::bad_cast的异常。
void f(const Base &b)
{
try {
const Derived &d = dynamic_cast<const Derived&>(b);
} catch (bad_cast) {
}
}
- typeid
通常使用typeid比较两条表达式的类型是否相同,或者比较一条表达式的类型是否与指定类型相同。
Derived *dp = new Derived;
Base *bp = dp;
if(typeid(*bp) == typeid(*dp)){
// bp 和 dp 指向同一类型的对象
}
if(typeid(*bp) == typeid(Derived)){
// bp实际指向Derived对象
}
- 使用RTTI
比如想要为具有继承关系的类实现相等运算符时。不能用虚函数实现,因为基类的引用或指针不能访问派生类对象。可以让每个类都实现一个equal函数,让友元运算符==先用typeid判断比较对象是否是同一类型,然后调用响应的equal。equal是虚函数,在派生类中的覆盖版本需要使用dynamic_cast将其转换为派生类的引用,此时dynamic_cast不会抛出异常,因为首先进行了类型是否相同的检查。
class Base{
friend bool operator==(const Base&, const Base&);
public:
Base() = default;
Base(int num) : n(num) {}
virtual bool equal(const Base &r) const{
return n == r.n;
}
virtual ~Base(){}
private:
int n = 0;
};
bool operator==(const Base& l, const Base& r){
return typeid(l) == typeid(r) && l.equal(r);
}
class Derived : public Base{
public:
Derived() = default;
Derived(int im) : m(im) {}
bool equal(const Base &r) const override {
return m == dynamic_cast<const Derived&>(r).m;
}
private:
int m = 0;
};
int main()
{
Base b1, b2;
cout << static_cast<int>(b1 == b2) << endl; // 1
Derived d1, d2;
cout << static_cast<int>(d1 == d2) << endl; // 1
Derived d3(0x10), d4(0x20);
cout << static_cast<int>(d3 == d4) << endl; // 0
cout << static_cast<int>(b1 == d1) << endl; // 0
return 0;
}
收藏夹:
三十分钟掌握STL : http://net.pku.edu.cn/~yhf/UsingSTL.htm
怎么选择合适的容器:http://homepages.e3.net.nz/~djm/containerchoice.png