date: 2013-12-10 19:37
三年前就买了<a href="http://book.douban.com/subject/1767741/" class="douban_book" name="1767741" target="_blank">《C++ Primer·中文版·第四版》</a>,但只是把它当做偶尔翻翻的工具书,一直没认真读。接下来的几个月得做硕士论文的课题,又不想继续像以前那样对C++浑浑噩噩地使用,所以,计划用30天的时间,读一遍这本书。
目标:粗读,复习标准库类型、类、容器,学习泛型编程,尽可能细读高级主题。
<!--more-->
##Chapter 2. 变量和基本类型
C++是一门静态类型语言,在编译时会做类型检查。
###一些基本概念。
<dl>
<dt>左值</dt>
<dd>左值可以出现在赋值语句的左边或右边。例如变量(variable)。</dd>
<dt>右值</dt>
<dd>右值只能出现在赋值语句的右边,不能出现在赋值语句的左边。例如字面量常值(literal constant)。</dd>
<dt>对象</dt>
<dd>一般而言,对象(object)就是内存中具有类型的区域。具体一些,计算左值表达式就会产生对象。</dd>
<dt>定义</dt>
<dd>为了让多个文件访问相同的变量,C++区分了声明和定义。变量的定义(definition)用于为变量分配存储空间,还可以为变量指定初始值。在一个程序中,变量有且仅有一个定义。在C++中,变量必须且仅能定义一次,而且在使用变量之前必须定义或声明变量。</dd>
<dt>声明(declaration)</dt>
<dd>声明用于向程序表明变量的类型和名字。定义也是声明:当定义变量时我们声明了它的类型和名字。程序中变量可以声明多次,但只能定义一次。如果声明有初始化式,那么它可以被当做是定意思,即使声明标记为extern。</dd>
</dl>
###关于作用域
C++的作用域研究的是“名字”,而非“变量”。作用域:用来区分名字的不同意义的上下文称为作用域(scope)。
<dl>
<dt>全局作用域</dt>
<dd>global scope,定义在所有函数之外的名字,具有全局作用域。</dd>
<dt>局部作用域</dt>
<dd>local scope。出现在for(){}循环的条件语句中。</dd>
<dt>语句作用域</dt>
<dd>statement scope</dd>
<dt>类作用域</dt>
<dd>class scope</dd>
<dt>命名空间作用域</dt>
<dd>namespace scope</dd>
</dl>
###const限定符
`const`限定符把一个对象`o`转换成一个常量,但这个对象依然是一个左值。
const对象默认为文件的局部变量。要使const变量能够在其他的文件中访问,必须显式地指定它为`extern`(非const变量默认为extern)。
###引用
<strong>引用(reference)就是对象的另一个名字。</strong>
在实际程序中,引用主要用作函数的形式参数。引用是一种复合类型(compound type),通过在变量名前添加`&`符号来定义。JavaScript是词法作用域,非常简单。C++……好复杂啊。我得弄个abc的例子提醒一下自己了:
``` cpp
#include <iostream>
using namespace std;
void func(int &a, int b);
int main(){
int a=0, b=1, c=3;
for (int d=0; d<3; d++){
a += d;
}
cout<< "a = " <<a<<endl; // => a = 3
func(a,b);
cout<< "a = " <<a<<endl; // => a = 4
return 0;
}
void func(int &a, int b){
a = a+b;
}
```
##Chapter 3. 标准库类型
两种最重要的标准库类型是`string`和`vector`。`string`类型支持长度可变的字符串,`vector`可用于保存一组指定类型的对象。
###string类
字符串的下标操作的返回值是个左值。
``` cpp
string str("Ha");
str[1] = 'f'; // => str变成"Hf"
```
C++的一个不足是,字符串字面值之间无法连接:
``` cpp
// VS2010下会这样报错
// error C2110: “+”: 不能添加两个指针
string s3 = "i am a string." + "ss";
```
总的来说(以我目前掌握的知识水平来判断),与JavaScript相比,string不够强大,但相比于C确实是很不错了。希望以后C++标准库能够更强吧。
###vector类模板
vector是同一类型的对象的集合,每个对象都有一个对应的整数索引值。
vector是一个类模板(class template)。vector不是一种数据类型。
vector对象(以及其他标准库容器对象)的重要属性就在于可以在运行时高效地添加元素。
vector中的对象是没有命名的,只能通过下标进行访问。与string类型的下标操作符一样,vector下标操作的返回值也是一个左值,因此可以进行写入(通过复制操作)。
作者说:“像size()这样的小库函数几乎都定义为内联函数,所以每次循环过程中调用它的运行时代价是比较小的。”我对此比较疑惑,因为在JavaScript中,这种类似size()的运算与读取变量值相比,都是要消耗更多性能的。不过后来大牛说C++中这样做确实不太消耗性能,就释然了。只是不明白size()如何实现的。
###缓冲区溢出
vector容器的下标操作不能进行元素添加操作。
试图对不存在的元素进行下标操作是程序设计过程中经常会犯的严重错误。所谓的“缓冲区溢出”错误就是对不存在的元素进行下标操作的结果。这样的缺陷往往导致PC机和其他应用中最常见的安全问题。
> 总的来说,相对于C++内置数据类型的数组和指针而言,程序员应优先使用标准库类类型。设计良好的程序只有在强调速度时才在类实现的内部使用数组和指针。
##Chapter 4. 数组和指针
与vector类型相比,数组的显著缺陷在于:数组的长度是固定的,而且程序员无法知道一个给定数组的长度。数组没有获取其容量大小的size操作,也不提供push_back操作在其中自动添加元素。如果需要更改数组长度,程序员只能创建一个更大的数组,然后把原数组的所有元素复制到新数组空间中去。
###数组的定义和初始化
数组的维数必须用值大于1的常量表达式。此常量表达式只能包含整型字面值常量、枚举常量或者用常量表达式初始化的整型const对象。非const变量以及要到运行阶段才知道其值的const变量都不能用于定义数组的维数。
`&`符号是取地址(address-of)操作符,当次操作符用于一个对象上时,返回的是该对象的存储地址。取地址操作只能用于左值,因为只有当变量用作左值时,才能取其地址。
<strong>建议:尽量避免使用指针和数组。</strong>
###指针
C++不允许void*指针操作它指向的对象。
``` cpp string变量的可以随时动态修改的
void func(){
string s("this is some text.");
string *ps = &s;
cout <<"length of string ""<< (*ps) <<"" is "<<(*ps).size() <<endl;
*ps = "goodbye";
cout <<"length of string ""<< s <<"" is "<<s.size() <<endl;
}
// 运行结果:
// length of string "this is some text." is 18
// length of string "goodbye" is 7
```
####指针和引用的比较
使用引用和指针都可以间接访问另一个值,但二者存在重要区别:
+ 引用总是指向某个对象:定义引用时没有初始化是错误的。指针指向的对象不是固定的。
+ 赋值行为的差异:给引用赋值,修改的是该引用所关联的对象的值,而不是使引用于另一个对象关联;但是对指针赋值,修改的是指针关联的对象。引用一经初始化,就始终指向同一个特定对象。
####指向指针的指针
指针指向对象,那么也有指向指针的指针,以及指向指向指针的指针的指针,如此无限类推。所以说,指针最大优点也仅仅是提高运行效率,就没啥优点了——实在是太靠近硬件了。
``` cpp
int i =42,j=1024;
int *p1 = &i, *p2 = &j;
int **pp1 = &p1;
int ***ppp1 = &pp1;
int ****pppp1 = &ppp1;
cout<<"pppp1: "<< ****pppp1<<endl; // => pppp1: 42
```
####指针减法
数组size()运算返回值类型是标准库类型中的size_t类型,指针相减运算的返回值是标准库类型的ptrdiff_t类型数据。这两种类型都是与机器相关的类型。size_t是unsigned类型,而ptrdiff_t是signed整型。
这两种类型的差别体现了它们各自的用途:size_t类型用于指明数组长度,因而必须是整数;ptrdiff_t类型则应保证足以存放同一数组中两个指针之间的差距,有可能是负数。
####指针与const限定符
二者之间有两种交互类型:指向const对象的指针,const指针。
``` cpp 指向const对象的指针
const double pi = 3.14159;
double *ptr = π // => error
const double *cptr = π // => ok: cptr is a pointer to const
```
const指针是本身的值不能修改的指针。
###动态数组
堆(heap)或自由存储区(free store):每个程序在执行时都占用一块可用的内存空间,用于存放动态分配的对象,此内存空间称为程序的自由存储区或堆。
##Chapter 5. 表达式
+ 一元操作符,unary operator
+ 二元操作符,binary operator
+ 三元操作符,ternary operator
###逻辑操作符
逻辑或、逻辑与的求值策略也是“短路求值”(short-circuit evaluation)。即只有在左操作数的值无法确定整个逻辑表达式的值时,才会求解右操作数。
``` cpp 将一个句子的第一个单词的各字符全部变成大写
string s = "You can create art and beauty on a computer.";
string::iterator itr = s.begin();
while( itr != s.end() && !isspace(*itr)){
*itr = toupper(*itr);
++itr;
}
```
###赋值操作符
赋值操作具有右结合性。<strong>赋值表达式的值是其左操作数的值,其结果的类型是左操作数的类型。</strong>——这一点与JavaScript截然相反,对于后者而言,赋值表达式的值是右操作数的值。(可参考:<a href="http://book.douban.com/annotation/29593293/" target="_blank">《JavaScript DOM编程艺术》的笔记-第23页</a>)
``` cpp
int ival = 0, eval;
eval = ival = 3.14159;
cout <<"ival = "<<ival<<endl; // => ival = 3
cout <<"eval = "<<eval<<endl; // => eval = 3
```
####复合赋值操作符
记住5种复合算术操作符即可:
+= -= /= %= *=
###自增、自减操作符
作者提到二者的性能区别,并建议尽可能只使用前置操作符,原因如下:前置操作需要做的工作更少,只需要加1然后返回修改后的值即可;而后置操作符则必须先保存操作数原来的值,以便返回未加1之前的值作为表达式的结果。对于int型对象和指针,编译器可优化掉这项额外工作。但是对于更多的复杂迭代器类型,这种额外工作可能会花费更大的代价。因此,养成使用前置操作这个好习惯,就不必操心性能差异的问题。
后自增操作的优先级高于解引用操作,所以`*iter++`等效于`*(iter++)`。
###箭头操作符
箭头操作符是包含点操作符和解引用操作符表达式的同义词。
###条件操作符
即那个唯一的三元操作符。作者给出一个建议:避免条件操作符的深度嵌套。但<strong>没有给出原因</strong>,我想大概是因为太难懂。
###sizeof操作符
sizeof操作符的作用是返回一个对象或类型名的长度,返回值类型为size_t,长度的单位是字节。sizeof表达式的结果是编译时常量。可以用sizeof操作符计算数组元素的个数。
``` cpp sizeof操作符计算数组元素的个数
char *words[] = {"stately", "plump", "buck","list", ""};
size_t words_size = sizeof(words) / sizeof(char *);
cout<<"sizeof(words) => "<< sizeof(words)<<endl; // 整个数组在内存中的存储长度, 20
cout<<"sizeof(char *) => "<<sizeof(char *)<<endl; // 元素类型的长度, 4
cout<<"words_size = "<< words_size<<endl; // 数组元素个数,words_size = 5
```
###复合表达式的求值
####求值顺序
C++中,规定了操作数的计算顺序的操作符只有:
+ 逻辑与 `&&`
+ 逻辑或 `||`
+ 条件操作符 `?:`
+ 逗号操作符
除此之外,其他操作符并未制定其操作数的求值顺序。
> 其实,以什么方式求解操作数通常没有多大关系。只有当操作符的两个操作数设计到同一个对象,并改变其值时,操作数的计算顺序才会影响结果。
###类型转换
如果两个类型之间可以相互转换(conversion),则称这两个类型相关。
####隐式类型转换(implicit type conversion)
在赋值操作中,因为不可能更改左操作数对象的类型,因此左操作数的类型占主导地位。如果赋值操作的左右操作数类型不同,则右操作数会被转换为左边的类型。
算数转换(arithmetic conversion):转换规则最重要的一点是要确保计算值的精度。
算数类型与布尔类型的转换:将算数类型转换为bool类型时,零转换为false,而其他值则转换为true。将bool对象转换为算数类型时,true变成1,而false则为0.
``` cpp
bool b;
b = -1;
cout<<b; // => 1
```
####强制类型转换
作者建议避免使用强制类型转换,因为强制类型转换关闭或挂起了正常的类型检查。不依赖强制类型转换也能写出很好的C++程序。
##Chapter 6. 语句
###空语句
如果在程序的某个地方,语法上需要一个语句,但逻辑上并不需要,此时应该使用空语句。这种用法常见于在循环条件判断部分就能完成全部循环工作的情况。例如:
``` cpp
// read until we hit end-of-file or find an input equal to sought
while(cin>>s && s!=sought) ;
```
###复合语句
复合语句(compound statement),通常被称为块(block),是用一对花括号括起来的语句序列。块标识了一个作用域,在块中引入的名字只能在该块内部或嵌套在块中的子块里访问。通常,一个名字只从其定义处到该块的结尾这段范围内可见。
<strong>块并不是以分号结束的。</strong>于是`{}`就相当于空语句`;`了。
###switch语句
switch语句花括号、case标号都必须是整型常量表达式。包括整数、枚举类型。
如果需要为某个特殊的`case`定义变量,则可以引入块语句,在该块语句中定义变量,从而保证这个变量在使用前被定义和初始化。
###for语句
for语句头必须是声明语句、表达式语句或空语句(这些语句都以分号结束);而不能是复合语句、条件语句等。
for语句头中可以定义多个对象;但是不管怎样,该处只能出现一个语句,因此所有的对象必须具有相同的一般类型。
##Chapter 7. 函数
Q:求两个整数的最大公约数
A:经典解决方案是辗转相除法。
<blockquote><p>在数学中,辗转相除法,又称欧几里得算法,是求最大公约数的算法……辗转相除法最早出现在欧几里得的几何原本中(大约公元前300年),所以它是现在仍在使用的算法中最早出现的。</p>
<p><a href="http://zh.wikipedia.org/wiki/%E8%BC%BE%E8%BD%89%E7%9B%B8%E9%99%A4%E6%B3%95" title="维基百科-辗转相除法" target="_blank"> —— 维基百科-辗转相除法</a></p>
</blockquote>
``` cpp 辗转相除法求两个整数的最大公约数
// return the greatest common divisor
int gcd(int v1, int v2){
while(v2){
int tmp = v2;
v2 = v1%v2;
v1 = tmp;
}
return v1;
}
```
###函数的调用
函数调用做了两件事:用对应的实参初始化函数的形参,并将控制权转移给被调用的函数。主调函数(calling function)的执行被挂起,被调函数(called function)开始执行。
实参必须具有与形参相同或者能隐式转换为形参类型的数据类型。
<strong>在定义或声明函数时,没有显示指定返回类型是不合法的。</strong>
###参数传递
形参的初始化与变量的初始化一样:如果形参具有非引用类型,则复制实参的值;如果形参为引用类型,则它只是实参的别名。
参数传递的过程,本质是用实参对形参进行初始化的过程。
####复制实参的局限性
不适宜复制实参的情况包括:
+ 当需要在函数中修改实参的值时
+ 当需要以大型对象作为实参传递时。对实际的应用而言,复制对象所付出的时间和存储空间代价往往过大。
+ 当没有办法实现对象的复制时。
对于上述几种情况,有效的解决办法是将形参定义为引用类型或指针类型。
###引用形参
> 从C语言背景转到C++的程序员习惯通过传递指针来实现对实参的访问。在C++中,使用引用形参则更安全和自然。
####利用const引用避免复制
``` cpp 比较两个非常长的字符串的长度,但又不希望该函数修改实参
// compare the length of two strings
bool isShorter(const string &s1, const string &s2){
return s1.size() < s2.size();
}
```
> 如果使用引用形参的唯一目的是避免复制实参,则应将形参定义为const引用。否则,就毫无必要地限制了该函数的使用,如下。
####非const引用形参的问题
如果函数具有普通的非const引用形参,则显然不能通过const对象进行调用。毕竟此时函数可以修改传递进来的对象,这样就违背了实参的const特性。
——容易忽略的是:调用这样的函数时,传递一个右值或具有需要转换的类型的对象同样是不允许的。
``` cpp
int incr(int &val){
return ++val;
}
int main(){
short v1 = 0;
const int v2 =42;
int v3 = incr(v1); //=> Error: v1 is not and int. VS2010: 无法用"short"类型的值初始化"int &"类型的引用(非常量限定)
v3 = incr(1); //=> Error: literals are not lvalues. VS2010 非常量引用的初始值必须为左值
v3 = incr(v2); //=> Error: v2 is const. VS2010 将"Int &"类型的引用绑定到"const int"类型的初始值设定项时,限定符被丢弃
v3 = incr(v1+v2); //=> Error: addition doesn't yield an lvalue.
int v4 = incr(v3); //=> ok: v3 is a non const object type int.
}
```
所以,结论是:
> 应该将不需要修改的引用形参定义为const引用。普通的非const引用形参在使用时不太灵活。这样的形参既不能用const对象初始化,也不能用字面值或产生右值的表达式实参初始化。
####传递指向指针的引用
mark 一下。略过。
####vector和其他容器类型的形参
> 通常,函数不应该有vector或其他标准库类型的形参。调用含有普通的非引用vector形参的函数将会复制vector的每一个元素。
从避免复制vector的角度出发,应考虑将形参声明为引用类型。然而,C++程序员倾向于通过传递指向容器中需要处理的元素的迭代器来传递容器:
``` cpp
// pass iterations to the first and one past the last element to print
void print(vector<int>::const_iterator beg, vector<int>::const_iterator end){
while(beg != end){
cout<< *beg++ <<" ";
}
cout<<endl;
}
// 这样使用
vector<int> vec;
for (int i=0;i<20;i++){
vec.push_back(i*2);
}
print(vec.begin(), vec.end());
```
####main:处理命令行选项
即`int main(int argc, char *argv[]){/*...*/}`的用法。见<a href="http://blog.csdn.net/laixingjun/article/details/8937931" target="_blank">CSDN- main() 处理命令行选项</a>即可。
####默认实参
默认实参是通过给形参表中的形参提供明确的初始值来指定的。但是,如果有一个形参具有默认实参,那么,它后面的所有形参都必须有默认实参。函数调用的实参按位置解析,默认实参只能用来替换函数调用缺少的尾部实参。
###局部对象
C++中,每个名字都有作用域,每个对象都有生命期(lifetime)。名字的作用域指的是知道改名字的程序文本区。对象的生命期则是在程序执行过程中对象存在的时间。
这种作用域与JavaScript的词法作用域截然不同!
``` cpp 块作用域示例
// about scope
{
int some_int = 20; // somee_int 只能在这个花括号内部访问
}
cout<< some_int<<endl; // Error: 未定义标识符"some_int"
```
####自动对象
####静态局部对象
一个变量如果位于函数的作用域内,但生命期却跨越了这个函数的多次调用,这种变量往往很有用。此时应该将这一的对象定义为static(静态的)。
static局部对象(static local object)确保不迟于在程序执行流程第一次警告该对象的定义语句时进行初始化。这种对象一旦被创建,在程序结束前都不会被撤销。当定义静态局部对象的函数结束时,静态局部对象不会撤销。在该函数被多次调用的过程中,静态局部对象会持续存在并保持它的值。
###内联函数
在大多数机器上,调用函数都要做很多工作:调用前要先保存寄存器,并在返回时恢复;复制实参;程序还必须转向一个新位置执行。
使用内联函数可以避免函数调用的开销。内联函数应该在头文件中定义。
###类的成员函数
函数原型必须在类中定义,但是函数体则既可以在类中也可以在类外定义。编译器会隐式地将在类内定义的成员函数当做内联函数。类的所有成员都必须在类定义的花括号里面声明,此后,就不能在为类增加任何成员。
####const成员函数
this指针:每个成员函数都有一个额外的、隐含的形参this。在调用成员函数时,形参this初始化为调用函数的对象的地址;任意成员函数对这个类的成员的任何没有前缀的引用,都被假定为通过指针this实现的引用。
const member function。
####默认构造函数
由编译器创建的默认构造函数通常称为<strong>合成的默认构造函数(synthesized default constructor)</strong>。
> 合成的默认构造函数一般适用于仅包含类类型成员的类。而对于含有内置类型或复合类型成员的类,则通常应该定义它们自己的默认构造函数初始化这些成员。
###重载函数
出现在相同作用域中的两个函数,如果具有相同的名字而形参表不同,则成为重载函数(overloaded function)。
####重载与作用域
``` cpp 局部变量名覆盖全局函数名的例子
string init(); // the name init has global scope
void func(){
int init = 0; // init is local and hides global init
string s = init(); // error: globale init is hidden
}
```
##Chapter 9. 顺序容器
> 关键概念:容器元素都是副本
>
> 在向容器中添加元素时,系统是将元素值复制到容器里。类似地,使用一段元素初始化新容器时,新容器存放的是原始元素的副本。被复制的原始值与新容器中的元素各不相关,此后,容器内元素值发生变化时,被复制的原值不会受到影响,反之亦然。
###9.4 vector元素的自增长
vector容器是顺序存储的线性表,list是链式存储的线性表。
##Chapter 10. 关联容器
9、10两章其实就是讲数据结构在C++标准库里的实现,可以暂时略过。
##Chapter 11. 泛型算法
不知道在讲什么。决定先把后面的看完。。。
##Chapter 12. 类
###12.2 隐含的this指针
###12.4 构造函数
构造函数的工作是保证每个对象的数据成员具有合适的初始值。
构造函数可以包含一个构造函数初始化列表。
##书评
本书大而全,是优点,亦是缺点。
糟粕:
+ 数组与指针,完全属于C语言的东西,虽说C++对C完全兼容,但也不应该放在C++书里介绍。真正的C++程序员会尽量避免使用指针与数组;大量使用指针与数组的程序员是C程序员,如果他还偶尔使用类,那就是C-C++程序员。所以,这是糟粕一:这本书不但介绍了指针与数组,还建议读者尽量不要使用它们——这也许是写手册类书籍的惯例。
学C++的好处
+ 很多数据结构、算法的书籍是用C或C++写的,所以,学习C++可以提升你在看这些书籍时的自信,或者说减少读这些书籍的阻碍。
《C++ Primer》笔记
对“《C++ Primer》笔记”的回应
《C++ Primer中文版》热门书评
-
关于这本书的读法和适合的人群
292有用 18无用 [已注销] 2009-09-22
说实话,我学完C以后进阶到C++的第一本书就是这本CP4。CP4确实是经典,但是它却不适合初学者,我当时读了大概有一半了,但是对很多东西还是一知半解。因为这本书是站在编译器的角度来写的,很多东西非常细节,事无巨细的论述对初学者一点好处也没有。初学者需要快速能先吸引眼球的东西。我比较推崇C++新手能先...
-
关于C++的扯淡
131有用 12无用 LUCKYsama 2009-01-29
今天下午在看C++Primer的时候跟同学东扯西扯一些乱七八糟的东西就填在这里。这本书还真的是不错的。 C++ 的诞生大约是在1980-1983年。总之我的一直以来的基本观点是,C++当初的诞生一定不是为了现在面向对象方法论上教授的那些扯淡的东西……什么把一个类的数据封装起来,只提供Get()和Se...
-
我的语言学习黄金期
125有用 20无用 archer 2008-07-02
那个时候不知道大学期间应该找个女朋友,不知道应该出去跟同学喝酒唱歌,不知道还有很多更诱人的技术,我只知道我现在要学c++,而且我手头这本书,是个大砖头,有1000多页,英文版,不争分夺秒看的话,一学期肯定看不完。学期一开始的时候,我就握着这本书,拿出一张纸制定我的学习计划,这周看这章,下周学习这一章...
-
习题答案 及 StackOverflow上的讨论
47有用 0无用 第四只猴子 2014-01-09
更新:不少同学要代码。我折腾了一晚上,终于把习题的代码发到github上了:https://github.com/Mooophy/Cpp-Primer要讨论的同学要熟悉一下git的用法,需要以下几步:1、注册帐号;2、fork 我的repository;3、在你自己的repository里commi...
-
《C++ Primer》第5版问世:一本不折不扣的C++蓝本
27有用 2无用 白云鹏 2013-12-09
科学计算、分布式应用、嵌入式行业、智能控制、算法研究,乃至学术讨论和上机考试都会有一种语言的身影,那就是大名鼎鼎的C++语言。原汁原味的C++目前已经执到C++11标准。听到这门编程语言,多数人伴随而来的是晦涩、复杂、强大等关键词!这样的感觉实际上是真实的。Lippman(著名C++语言专家,《C+...
书名: C++ Primer中文版
作者:
出版社: 中国电力出版社
副标题: 第三版
译者: 潘爱民 | 张丽
出版年: 2002-4
页数: 1033
定价: 128.00元
装帧: 平装
ISBN: 9787508309897