《Accelerated C++中文版》--- 读书笔记

1.

C++中同名函数嵌套,不一定都是递归,有可能是函数重载 [2010-07-24 22:14:25]
2.

避免单个语句产生多个副作用。因为当此句发生异常[异常本身是一种副作用,因为他产生了异常对象]时,会对运行环境造成不可预料的影响。 典型的单个不良语句, 输出语句中调用产生异常的函数。 [2010-07-25 09:57:02]
3.

函数参数(parameter)为“非常量引用”(如:vector<double>& hw)时,要求传入的实参(argument)为“左值”(lvalue). 常见左值, 变量,返回非常量引用类型的函数。 典型非左值, 常量,常量引用。 [2010-07-25 10:03:24]
4.

捕获到抛出的异常后, 若在处理此异常的catch块中没有return x;或exit(x);语句, 异常处理完后(catch语句块执行完后),接着执行try-catch块下面在语句。 [2010-07-25 14:49:02]
5.

自定义头文件(.h)中,良好的习惯是  类型名要经过完全限定,如:std::string name;和 std::vector<double> homework;。  而在源文件(.cpp)中可以 依照方便 用using声明类型名,如,using std::string;和using std::cout; etc.。 这是应为,头文件是要被包含到相应源文件中的,若用using声明类型名会对包含此头文件的源文件产生隐含(implict)影响,可能引起类型冲突(type conflict)而是我们摸不着头脑。而在源文件中我们用using声明语句所产生的影响是看的见(explict)的,这种影响完全在我们的掌控之下。 [2010-07-25 17:29:52]

6.

先让我们来看两个函数:

double Student_info::grade() const {...}  //member-function version

double grade(const Student_info&) {...}   //全局函数

这两个函数一个是成员函数封装在Student_info类中, 一个是全局函数来处理Student_info数据结构。 后者C语言常见, 前者C++中常见。但是本质上两个函数是等价的, 下面就来分析一下原因:

后者参数是const引用, 直接读取传入(实参)中的数据,而不能够更改其中的数据。 但在C++中,我们可以把Student_info结构和其有关的方法封装到一个类中得到Student_info类, 在调用成员方法时,必须通过实例对象调用(static方法可以直接用类名调用),也正因为我们通过实例对象调用grade方法,我们可以在声明时省略(行参), 实例对象隐式说明了那个以引用形式传入的(实参)。但是问题随之而来了,没用了行参,我们就没法限定行参为const类型,那么方法体内应该怎么限定对数据的写操作呢,此时我们就可以用double Student_info::grade() const {...}来声明了。

再让我们看一个类:

template <class T> class Vec {
public:

        // new operations: index
        T& operator[](size_type i) { return data[i]; }
        const T& operator[](size_type i) const { return data[i]; }

}

我们能够重载索引操作符,这看上去令人惊讶, 因为这两个版本的索引操作符函数的参数列表都相同; 它们都带有一个单独的size_type类型的参数。 然而, 每个成员函数, 包括每个成员函数包括这里的操作符, 都带有一个隐式的参数, 那就是它们操作的对象。 由于不管这个对象是否是const, 都需要这些操作, 所以我们可以重载这些操作。

It may be surprising that we can overload the index operator, because it appears that both argument lists are the
same; each appears to take a single parameter of type size_type. However, every member function, including each of
these operators, takes an implicit parameter, which is the object on which it operates. Because the operations differ
regarding whether that object is const, we can overload the operation.

7.

什么时候应该自己写一个析构函数呢?

当类中存在动态分配资源时,我们就应该写一个析构函数了,以来释放动态分配的资源,避免内存泄漏。

8.

什么时候应该自己写“复制构造函数”和“赋值操作符”呢?

当我们应该自己写“析构函数”也就应该自己写以上两者了。原因是: 我们往往用指针来操纵动态分配的资源, 而在复制和赋值时, 我们需要此资源的副本,当然我们也希望操作资源的指针指向副本资源,为了达到此目的我们必须自己写“复制构造函数”和“赋值操作符”。

以上的7和8说明,析构函数、复制构造函数、赋值操作符 三者缺一不可!(系统合成的三个函数已经符合我们需求时,可以都不出现)

9.

C++标准并没有说明系统如何管理模板实例化, 所以每个系统都按照它自己的特殊方式来解决实例化。 虽然我们无法准确的说明你的编译器如何解决实例化, 但是依然有两点需要明确:

第一点是, C++系统遵循的是传统的 编辑--编译--链接 模型, 实例化往往不是发生在编译时期, 就是在链接时期。 直到模板实例化, 系统才会检测模板的代码是否可以用于指定的类型。 因此, 在链接时期, 我们可能会得到看起来像编译时期的错误

第二点只与你编写自己的模板时有关: 当前的大多数系统都要求, 为了可以实例化一个模板, 模板的定义, 而不仅仅是声明, 都必须是系统可以访问的。 一般来说, 这个要求意为着, 定义模板的源文件和头文件都必须是可访问的。 每个系统定位源文件的方式不同。 很多系统都希望模板的头文件可以包含源文件, 直接包含或者通过一个#include来包含。 要想知道你的系统的要求, 最好的方式是查看系统的文档。

###以上是 靳志伟 老师翻译的, 一下是 Koenig & Moo 的原话:

The C++ standard says nothing about how implementations should manage template i nstantiation, so every
implementation handles instantiation in its own particular way. While we cannot say exactly how your compiler will
handle instantiation, there are two important points to keep in mind:

The first is that for C++ implementations that
follow the traditional edit-compile-link model, instantiation often happens not at compile time, but at link time. It is not
until the templates are instantiated that the implementation can verify that the template code can be used with the types
that were specified. Hence, it is possible to get what seem like compile-time errors at link time.

The second point matters if you write your own templates: Most current implementations require that in order to
instantiate a template, the definition of the template, not just the declaration, has to be accessible to the
implementation. Generally, this requirement implies access to the source files that define the template, as well as the
header file. How the implementation locates the source file differs from one implementation to another. Many
implementations expect the header file for the template to include the source file, either directly or via a #include. The
most certain way to know what your implementation expects is to check its documentation.

###PS###

对于这句 “很多系统都希望模板的头文件可以包含源文件, 直接包含或者通过一个#include来包含。” 要注意:

第一,若模板头文件直接包含源文件, 则原来的源文件 不能在编译器的文件参数列表中(可以把模板源文件删除), 否则会出现重复定义得到错误, 这是因为“包含源文件的模板头文件”和“模板源文件”中定义重复导致的。

第二,若模板头文件通过一个#include来包含源文件, 则原来的源文件 不能在编译器的文件参数列表中(不能删除模板源文件)否则会出现重复定义得到错误, 原因同上也是因为“包含源文件的模板头文件”和“模板源文件”中定义重复导致的。

总得,也就是说在全部头文件/包含#include命令的头文件,和编译命令行文件列表的全部cpp文件中不能有重复的定义

10.

应该把什么样大函数定义为成员?

通常有一个通用大规则可以帮我们做出决定:如果这个函数会改变对象大状态,那么这个函数就应该成为这个对象大成员。

11.

为什么有的构造函数要定义explicit? 什么类型的构造函数应该定义成explicit型?

第一个问题的答案是:为类避免危险的系统隐式的自动类型转换。使用explicit定义会告诉编译器,只能在显示的构造对象的时候,才能使用这个构造函数,而编译器无法使用explicit构造函数,来说把表达式或者调用函数中的操作函数隐式的转换为类的对象

第二个问题的答案是:一般来说,如果一个构造函数(对赋值构造函数来说)是用来定义对象的结构的构造方式, 而不是定义对象内容的构造方式的话,这个构造函数就应该被定义为explicit。如果构造函数的参数是对象的一部分的话,这种构造函数就不应该定义为explicit。

Now that we know thatconstructors that take a single argument define conversions, we can understand what happens when we make a
constructor explicit: Doing so tells the compiler to use that constructor only to construct objects explicitly. The
compiler will not use an explicit constructor to create objects implicitly by converting operands in expressions or
function calls.

In general, it is useful to make explicit the constructors that define the structure of the object being constructed, rather
than its contents. Those constructors whose arguments become part of the object usually should not be explicit.
As an example, the string and Str classes have constructors that take a single const char* and are not explicit. Each
constructor uses its const char* argument to initialize the value of its object. Because the argument determines the
value of the resulting object, it is sensible to allow automatic conversions from a const char* in expressions or function
calls.
On the other hand, the vector and Vec constructors that take a single argument of type Vec::size_type are explicit.
These constructors use their argument value to determine how many elements to allocate. The constructor argument
determines the structure of the object, but not its value.

12.

一个类可以通过两种方式来定义类型转换(无论是与其他类类型还是与内置类型): 类 a可以把其他类型的对象转换为自己的类型的对象,或者 b把自己的类型的对象转换为其他类型。

对于a方向转换(其他类型->本类):我们可以通过一个单独的参数的构造函数来定义这种转换。例如:

class Str {

public:

     // create a Str from a null-terminated array of char
        Str(const char* cp) {
                std::copy(cp, cp + std::strlen(cp), std::back_inserter(data));
        }

private:
        Vec<char> data;

}

定义了从char*(字符串常量值)到Str类类型的转换。

对于b方向的转换(本类->其他类型):我们可以定义显示的类型转换操作符(conversion operation),这种操作符可以说明如何把这个类的对象转换为目标类型。 例如:

class Student_info {
public:
  operator double();
  // ...
};

上面的这个类就可以根据一个Student_info对象创建一个double类型的值。 这种转换的具体含义取决与这个操作符的定义, 比如这个操作符会把对象转换为与它队形的最终成绩。编译器会在需要一个double类型,而我们提供了一个Student_info类型对象时,使用这个类型转换操作符。

我们可以利用类型转换操作符,方便的计算所有学生的平均成绩:

vector<Student_info> vs;
// fill up vs
double d = 0;
for (int i = 0; i != vs.size(); ++i)
d += vs[i];  // vs[i] is automatically converted to double
cout << "Average grade: " << d / vs.size() << endl;

A class can define conversions in two ways: It can convert from other types to its type, or from its type to other
types.

for a direction of conversions:

The more common conversion defines how to
convert other types to the type that we are defining. We do so by defining a constructor with a single argument.


Our Str class already has such a constructor, namely the one that takes a const char*. Therefore, the compiler will
use this constructor when an object of type Str is needed and an object of type const char* is available. The
assignment of a const char* to a Str is exactly such a situation. When we write s = "hello"; what really happens is that
the compiler uses the Str(const char*) constructor to create an unnamed local temporary of type Str from the string
literal. It then calls the (synthesized) assignment operator of class Str to assign this temporary to s.

for b direction of conversions:

Class authors can also define explicit
conversion operators, which say how to convert an object from its type to a target type. Conversion operators must
be defined as members of a class. The name of a conversion operator is operator followed by the target type name.
Thus, if a class has a member called operator double, that member says how to create a value of type double from a
value of the class type.

(拓展)Conversion operators are most often useful when converting from a class type to a built-in type, but they can also be
useful when converting to another class type for which we do not own the code. In either case, we cannot add a
constructor to the target type, so we can define the conversion operator only as part of the class that we own.

In fact, we use this kind of conversion operator every time we write a loop that implicitly tests the value of an istream.
As we discussed in §3.1.1/39, we can use an istream object where a condition is expected:
if (cin >> x) { /*...*/ }
which we saw was equivalent to
cin >> x;
if (cin) { /*...*/ }
We can now understand what happens in this expression.
As we know, the if tests a condition, which is an expression that yields a truth value. The precise type of such a truth
value is bool. Using a value of any arithmetic or pointer type automatically converts the value to type bool, so we can
use values of these types as the expression in a condition. Of course, iostream is neither a pointer nor an arithmetic
type. However, the standard library defines a conversion from type istream to void*, which is a pointer to void. It
does so by defining istream::operator void*, which tests various status flags to determine whether the istream is valid,
and returns either 0 or an implementation-defined nonzero void* value to indicate the state of the stream.
We have not previously used the void* type. We said in §6.2.2/114 that the void type could be used only in a few

ways—the basis for a pointer being one of them. A pointer to void is sometimes called a universal pointer, because it
is a pointer that can point to any type of object. Of course, you cannot dereference the pointer, because the type of
the object to yield isn't known. But one thing that can be done with a void* is to convert it to bool, which is exactly
how it is used in this context.
The reason that class istream defines operator void* rather than operator bool is to allow the compiler to detect the
following erroneous usage:
int x;
cin << x;
// we should have written cin >> x;
If class istream were to define operator bool, this expression would use istream::operator bool to convert cin to bool,
and then convert the resulting bool value to int, shift that value left by a number of bits equal to the value of x, and
throw the result away! By defining a conversion to void*, rather than to an arithmetic type, the standard library still
allows an istream to be used as a condition, but prevents it from being used as an arithmetic value.

13.

一个空析构函数是怎么释放空间的? 什么时候只需要在基类显式的提供一个空的虚析构函数?

第一个问题的解是: 当类的对象作为局部成员要被释放时或用delete操作符释放时, 系统会先执行析构函数体, 当然析构函数为空时, 系统什么也不做, 然后系统把对象占据的空间返回给系统(这时系统会递归的调用对象成员的析构函数来达到释放空间的目的)。

第二个问题的解是:在我们要用基类的指针来销毁派生类的对象时,就需要在基类提供一个虚构函数了(实现了析构时的多态)。而没有别的理由(释放动态分配的空间)要求定义一个析构函数的话,这个析构函数就没有工作要做了,所以它的函数也就是空的了。

(增订修改中...)

你可能感兴趣的:(读书笔记)