C++ Primer 第四版阅读笔记
阅读笔记
- 初始化
变量定义指定了变量的类型和标识符,也可以为对象提供初始值。定义时指定了初始值的对象被称为是 已初始化的。C++ 支持两种初始化变量的形式:复制初始化和 直接初始化。复制初始化语法用等号(=),直接初始化则是把初始化式放在括号中:
int ival(1024); // direct-initialization
int ival = 1024; // copy-initialization
使用 = 来初始化变量使得许多 C++ 编程新手感到迷惑,他们很容易把初始化当成是赋值的一种形式。但是在 C++ 中初始化和赋值是两种不同的操作
- 前向声明
可以声明一个类而不定义它:
class Screen; // declaration of the Screen class
这个声明,有时称为 前向声明(forward declaraton),在程序中引入了类类型的 Screen。在声明之后、定义之前,类 Screen 是一个不完全类型(incompete type),即已知 Screen 是一个类型,但不知道包含哪些成员。类的前身声明一般用来编写相互依赖的类.
- 不完全类型
不完全类型(incomplete type)只能以有限方式使用。不能定义该类型的对象。不完全类型只能用于定义指向该类型的指针及引用,或者用于声明(而不是定义)使用该类型作为形参类型或返回类型的函数。 因为只有当类定义体完成后才能定义类,因此类不能具有自身类型的数据成员。然而,只要类名一出现就可以认为该类已声明。因此,类的数据成员可以是指向自身类型的指针或引用.
- 类定义以分号结束
类的定义分号结束。分号是必需的,因为在类定义之后可以接一个对象定义列表。定义必须以分号结束:
class Sales_item { /* ... */ };
class Sales_item { /* ... */ } accum, trans;
通常,将对象定义成类定义的一部分是个坏主意。这样做,会使所发生的操作难以理解。
- 可变数据成员
有时(但不是很经常),我们希望类的数据成员(甚至在 const 成员函数内)可以修改。这可以通过将它们声明为 mutable 来实现。
- 类作用域
每个类都定义了自己的新作用域和唯一的类型。在类的定义体内声明类成员,将成员名引入类的作用域。两个不同的类具有两个的类作用域。形参表和函数体处于类作用域中
函数返回类型不一定在类作用域中***
class Screen {
public:
typedef std::string::size_type index;
index get_cursor() const;
};
inline Screen::index Screen::get_cursor() const
{
return cursor;
}
该函数的返回类型是 index,这是在 Screen 类内部定义的一个类型名。如果在类定义体之外定义 get_cursor,则在函数名被处理之前,代码在不在类作用域内。当看到返回类型时,其名字是在类作用域之外使用。必须用完全限定的类型名 Screen::index 来指定所需要的 index 是在类 Screen 中定义的名字。
- 构造函数初始化列表
有些成员必须在构造函数初始化列表中进行初始化。对于这样的成员,在构造函数函数体中对它们赋值不起作用。没有默认构造函数的类类型的成员,以及 const 或引用类型的成员,不管是哪种类型,都必须在构造函数初始化列表中进行初始化。
可以初始化 const 对象或引用类型的对象,但不能对它们赋值。在开始执行构造函数的函数体之前,要完成初始化。初始化 const 或引用类型数据成员的唯一机会是构造函数初始化列表中必须对任何 const 或引用类型成员以及没有默认构造函数的类类型的任何成员使用初始化式。
按照与成员声明一致的次序编写构造函数初始化列表是个好主意。此外,尽可能避免使用成员来初始化其他成员。
初始化类类型的成员时,要指定实参并传递给成员类型的一个构造函数。可以使用该类型的任意构造函数。
- 当对象定义在局部作用域中时,内置或复合类型的成员不进行初始化。
-
使用类定义一个对象
Sales_item myobj();//错误 defines a function
Sales_item myobj;
Sales_item myobj = Sales_item(); - mutable 成员永远都不能为 const,它的值可以在 const 成员函数中修改
-
隐式类类型转换
可以用单个实参来调用的构造函数定义了从形参类型到该类类型的一个隐式转换
class Sales_item {
public:
// default argument for book is the empty string
Sales_item(const std::string &book = ""):
isbn(book), units_sold(0), revenue(0.0) { }
Sales_item(std::istream &is);// as before
};
string null_book = "9-999-99999-9";
// ok: builds a Sales_itemwith 0 units_soldand revenue from
// and isbn equal to null_book
item.same_isbn(null_book);
抑制由构造函数定义的隐式转换可以通过将构造函数声明为 explicit,来防止在需要隐式转换的上下文中使用构造函数:
class Sales_item {
public:
// default argument for book is the empty string
explicit Sales_item(const std::string &book = ""):
isbn(book), units_sold(0), revenue(0.0) { }
explicit Sales_item(std::istream &is);
// as before
};
两个构造函数都不能用于隐式地创建对象。前两个使用都不能编译:
item.same_isbn(null_book); // error: string constructor is explicit
item.same_isbn(cin); // error: istream constructor is explicit
当构造函数被声明 explicit 时,编译器将不使用它作为转换
操作符。 -
为转换而显式地使用构造函数
string null_book = "9-999-99999-9";
// ok: builds a Sales_itemwith 0 units_soldand revenue from
// and isbn equal to null_book
item.same_isbn(Sales_item(null_book));
显式使用构造函数只是中止了隐式地使用构造函数。
任何构造函数都可以用来显式地创建临时对象。 -
类成员的显式初始化
struct Data {
int ival;
char *ptr;
};
// val1.ival = 0; val1.ptr = 0
Data val1 = { 0, 0 };
// val2.ival = 1024;
// val2.ptr = "Anna Livia Plurabelle"
Data val2 = { 1024, "Anna Livia Plurabelle" };
不建议使用
1. 要求类的全体数据成员都是 public。
2. 将初始化每个对象的每个成员的负担放在程序员身上。这样的初始化是乏味且易于出错的,因为容易遗忘初始化式或提供不适当的初始化式。
3. 如果增加或删除一个成员,必须找到所有的初始化并正确更新。
使用构造函数是比较好的选择。 -
友元
的声明以关键字 friend 开始。它只能出现在类定义的内部。友元声明可以出现在类中的任何地方:友元不是授予友元关系的那个类的成员,所以它们不受声明出现部分的访问控制影响。
友元可以是普通的非成员函数,或前面定义的其他类的成员函数,或整个类。
将一个类设为友元,友元类的所有成员函数都可以访问授予友元关系的那个类的非公有成员
必须先定义包含成员函数的类,才能将成员函数设为友元。另一方面,不必预先声明类和非成员函数来将它们设为友元。 -
重载函数与友元关系
类必须将重载函数集中每一个希望设为友元的函数都声明为友元,未设置的不能访问 -
使用类的 static 成员的优点
1. static 成员的名字是在类的作用域中,因此可以避免与其他类的成员或全局对象名字冲突。
2. 可以实施封装。static 成员可以是私有成员,而全局对象不可以。
3. 通过阅读程序容易看出 static 成员是与特定类关联的。这种可见性可清晰地显示程序员的意图。 -
使用类的 static 成员
可以通过作用域操作符从类直接调用 static 成员,或者通过对象、引用或指向该类类型对象的指针间接调用。
Account ac1;
Account *ac2 = &ac1;
// equivalent ways to call the static member rate functiondouble rate;
rate = ac1.rate(); // through an Account object or reference
rate = ac2->rate(); // through a pointer to an Account object
rate = Account::rate(); // directly from the class using the scope operator -
static 成员函数
Account 类有两个名为 rate 的 static 成员函数,其中一个定义在类的内部。当我们在类的外部定义 static 成员时,无须重复指定 static 保留字,该
保留字只出现在类定义体内部的声明处
因为 static 成员不是任何对象的组成部分,所以 static 成员函数不能被声明为 const。毕竟,将成员函数声明为 const 就是承诺不会修改该函数所属
的对象。最后,static 成员函数也不能被声明为虚函数 -
static 数据成员
static 数据成员必须在类定义体的外部定义(正好一次)。不像普通数据成员,static 成员不是通过类构造函数进行初始化,而是应该在定义时进行初始化。
保证对象正好定义一次的最好办法,就是将 static 数据成员的定义放在包含类非内联成员函数定义的文件中double Account::interestRate = initRate(); -
特殊的整型 const static 成员
一般而言,类的 static 成员,像普通数据成员一样,不能在类的定义体中初始化。相反,static 数据成员通常在定义时才初始化。
这个规则的一个例外是,只要初始化式是一个常量表达式,整型 const static 数据成员就可以在类的定义体中进行初始化:
class Account {
public:
static double rate() { return interestRate; }
static void rate(double); // sets a new rate
private:
static const int period = 30; // interest posted every 30 days
double daily_tbl[period]; // ok: period is constant expression
};
const static 数据成员在类的定义体中初始化时,该数据成员仍必须在类的定义体之外进行定义。
在类内部提供初始化式时,成员的定义不必再指定初始值:
// definition of static member with no initializer;
// the initial value is specified inside the class definition
const int Account::period; -
static 成员不是类对象的组成部分
static 数据成员的类型可以是该成员所属的类类型。非 static 成员被限定声明为其自身类对象的指针或引用:
class Bar {
public:
// ...
private:
static Bar mem1; // ok
Bar *mem2; // ok
Bar mem3; // error
}; -
static 数据成员可用作默认实参:
class Screen {
public:
// bkground refers to the static member
// declared later in the class definition
Screen& clear(char = bkground);
private:
static const char bkground = '#';
}; -
复制控制
复制构造函数、赋值操作符和析构函数总称为 复制控制 -
复制构造函数
是一种特殊构造函数,具有单个形参,该形参(常用 const 修饰)是对该类类型的引用。当定义一个新对象并用一个同类型的对象对它进行初
始化时,将显式使用复制构造函数。当将该类型的对象传递给函数或函数返回该类型的对象时,将隐式使用复制构造函数。
有一种特别常见的情况需要类定义自己的复制控制成员的:
类具有指针成员
根据另一个同类型的对象显式或隐式初始化一个对象。
复制一个对象,将它作为实参传给一个函数。
从函数返回时复制一个对象。
初始化顺序容器中的元素。
根据元素初始化式列表初始化数组元素。
- 初始化容器元素
复制构造函数可用于初始化顺序容器中的元素。例如,可以用表示容量的单个形参来初始化容器(第 3.3.1 节)。容器的这种构造方式使用默认构造函数和复制构造函数:
// default string constructor and five string copy constructors invoked
vector<string> svec(5);
编译器首先使用 string 默认构造函数创建一个临时值来初始化 svec,然后使用复制构造函数将临时值复制到 svec 的每个元素。
- 构造函数与数组元素
如果没有为类类型数组提供元素初始化式,则将用默认构造函数初始化每个元素。然而,如果使用常规的花括号括住的数组初始化列表(第 4.1.1 节)来提供显式元素初始化式,则使用复制初始化来初始化每个元素。根据指定值创建适当类型的元素,然后用复制构造函数将该值复制到相应元素:
Sales_item primer_eds[] = { string("0-201-16487-6"),
string("0-201-54848-8"),
string("0-201-82470-1"),
Sales_item()
};
如前三个元素的初始化式中所示可以直接指定一个值,用于调用元素类型的单实参构造函数。如果希望不指定实参或指定多个实参,就需要使用完整的构造函数语法,正如最后一个元素的初始化那样。
- 合成的复制构造函数
如果我们没有定义复制构造函数,编译器就会为我们合成一个。与合成的默认构造函数(第 12.4.3 节)不同,即使我们定义了其他构造函数,也会合成复制构造函数。 合成复制构造函数的行为是,执行 逐个成员初始化,将新对象初始化为原对象的副本。
有些类必须对复制对象时发生的事情加以控制。这样的类经常有一个数据成员是指针,或者有成员表示在构造函数中分配的其他资源。而另一些类在创建新对象时必须做一些特定工作。这两种情况下,都必须定义复制构造函数。
- 禁止复制
为了防止复制,类必须显式声明其复制构造函数为 private。
如果复制构造函数是私有的,将不允许用户代码复制该类类型的对象,编译器将拒绝任何进行复制的尝试。
然而,类的友元和成员仍可以进行复制。如果想要连友元和成员中的复制也禁止,就可以声明一个(private)复制构造函数但不对其定义。
通过声明(但不定义)private 复制构造函数,可以禁止任何复制类类型对象的尝试:用户代码中复制尝试将在编译时标记为错误,而成员函数和友元中的复制尝试将在链接时导致错误。
- 重载赋值
重载操作符是一些函数,其名字为 operator 后跟着所定义的操作符的符号。因此,通过定义名为 operator= 的函数,我们可以对赋值进行定义。像任何其他函数一样,操作符函数有一个返回值和一个形参表。形参表必须具有与该操作符数目相同的形参(如果操作符是一个类成员,则包括隐式 this 形参)。
赋值是二元运算,所以该操作符函数有两个形参:第一个形参对应着左操作数,第二个形参对应右操作数。
大多数操作符可以定义为成员函数或非成员函数。当操作符为成员函数时,它的第一个操作数隐式绑定到 this 指针。有些操作符(包括赋值操作符)必须是定义自己的类的成员。因为赋值必须是类的成员,所以 this 绑定到指向左操作数的指针。因此,赋值操作符接受单个形参,且该形参是同一类类型的对象。右操作数一般作为 const 引用传递。
复制和赋值常一起使用
- 三法则
指的是如果需要析构函数,则需要所有这三个复制控制成员。
析构函数与复制构造函数或赋值操作符之间的一个重要区别是,即使我们编写了自己的析构函数,合成析构函数仍然运行
即使对象赋值给自己,赋值操作符的正确工作也非常重要。保证这个行为的通用方法是显式检查对自身的赋值
- 重载操作符的定义
除了函数调用操作符之外,重载操作符的形参数目(包括成员函数的隐式this 指针)与操作符的操作数数目相同。函数调用操作符可以接受任意数目的操作数用于内置类型的操作符,其含义不能改变大多数重载操作符可以定义为普通非成员函数或类的成员函数。作为类成员的重载函数,其形参看起来比操作数数目少 1。作为成员函数的操作符有一个隐含的 this 形参,限定为第一个操作数。
一般将算术和关系操作符定义非成员函数,而将赋值操作符定义为成员:
// member binary operator: left-hand operand bound to implicit thispointer
Sales_item& Sales_item::operator+=(const Sales_item&);
// nonmember binary operator: must declare a parameter for eachoperand
Sales_item operator+(const Sales_item&, const Sales_item&);
- 不再具备短路求值特性
载操作符并不保证操作数的求值顺序,尤其是,不会保证内置逻辑 AND、
逻辑 OR(第 5.2 节)和逗号操作符(第 5.9 节)的操作数求值。在 && 和 ||
的重载版本中,两个操作数都要进行求值,而且对操作数的求值顺序不做规定。
因此,重载 &&、|| 或逗号操作符不是一种好的做法。
- 操作符重载和友元关系
操作符定义为非成员函数时,通常必须将它们设置为所操作类的友元(12.5节)。在本章的后面部分,将给出操作符可以定义为非成员的两个原因。在这种情况下,操作符通常需要访问类的私有部分。
- 重载操作符的设计
不要重载具有内置含义的操作符,重载逗号、取地址、逻辑与、逻辑或等等操作符通常不是好做法。这些操作符具有有用的内置含义,如果我们定义了自己的版本,就不能再使用这些内置含义有时我们需要定义自己的赋值运算。这样做时,它应表现得类似于合成操作符:赋值之后,左右操作数的值应是相同的,并且操作符应返回对左操作数的引
用。重载的赋值运算应在赋值的内置含义基础上进行定制,而不是完全绕开。
相等测试操作应使用 operator==。
一般通过重载移位操作符进行输入和输出。
测试对象是否为空的操作可用逻辑非操作符 operator! 表示
- 相等和关系操作符
将要用作关联容器键类型的类应定义 < 操作符。关联容器默认使用键类型的 < 操作符。即使该类型将只存储在顺序容器中,类通常也应该定义相等(==)和小于(<)操作符,理由是许多算法假定这个操作符存在。例如 sort 算法使用 < 操作符,而 find 算法使用 == 操作符。
如果类定义了相等操作符,它也应该定义不等操作符 !=。类用户会假设如果可以进行相等比较,则也可以进行不等比较。同样的规则也应用于其他关系操作符。如果类定义了 <,则它可能应该定义全部的四个关系操作符(>,>=,<,<=)。
- 选择成员或非成员实现
赋值(=)、下标([])、调用(())和成员访问箭头(->)等操作符必须定义为成员,将这些操作符定义为非成员函数将在编译时标记为错误。
像赋值一样,复合赋值操作符通常应定义为类的成员,与赋值不同的是,不一定非得这样做,如果定义非成员复合赋值操作符,不会出现编译错误。
改变对象状态或与给定类型紧密联系的其他一些操作符,如自增、自减和解引用,通常就定义为类成员。
对称的操作符,如算术操作符、相等操作符、关系操作符和位操作符,最好定义为普通非成员函数。
- 输出操作符 << 的重载
ostream& operator <<(ostream& os, const ClassType &object)
{
// any special logic to prepare object
// actual output of members
os << // ...
// return ostream object
return os;
}
- IO 操作符必须为非成员函数
我们不能将该操作符定义为类的成员,否则,左操作数将只能是该类类型的对象:
// if operator<< is a member of Sales_item
Sales_item item;
item << cout;
这个用法与为其他类型定义的输出操作符的正常使用方式相反。
- 算术操作符和关系操作符
一般而言,将算术和关系操作符定义为非成员函数,像下面给出的Sales_item 加法操作符一样:
// assumes that both objects refer to the same isbn
Sales_item operator+(const Sales_item& lhs, const Sales_item& rhs)
{
Sales_item ret(lhs); // copy lhs into a local object that we'll return
ret += rhs; // add in the contents of rhs
return ret; // return ret by value
}
注意,为了与内置操作符保持一致,加法返回一个右值,而不是一个引用。
既定义了算术操作符又定义了相关复合赋值操作符的类,一般应使用复合赋值实现算术操作符
- 相等操作符
如果类定义了 == 操作符,该操作符的含义是两个对象包含同样的数据。
如果类具有一个操作,能确定该类型的两个对象是否相等,通常将该函数定义为 operator== 而不是创造命名函数。用户将习惯于用 == 来比较对象,而且这样做比记住新名字更容易。
如果类定义了 operator==,它也应该定义 operator!=。用户会期待如果可以用某个操作符,则另一个也存在相等和不操作符一般应该相互联系起来定义,让一个操作符完成比较对象的实际工作,而另一个操作符只是调用前者。
- 赋值操作符
赋值操作符可以重载。无论形参为何种类型,赋值操作符必须定义为成员函数,这一点与复合赋值操作符有所不同
赋值必须返回对 赋值必须返回对 *this 的引用
- 重载解引用操作符
像下标操作符一样,我们需要解引用操作符的 const 和非 const 版本。它们的区别在于返回类型:const 成员返回 const 引用以防止用户改变基础对象。
- 重载箭头操作符
箭头操作符与众不同。它可能表现得像二元操作符一样:接受一个对象和一个成员名。对对象解引用以获取成员。不管外表如何,箭头操作符不接受显式形参。
- 定义前自增/前自减操作符
为了与内置类型一致,前缀式操作符应返回被增量或减量对象的引用
- 调用操作符和函数对象
- 向基类构造函数传递实参
构造函数初始化列表为类的基类和成员提供初始值,它并不指
定初始化的执行次序。首先初始化基类,然后根据声明次序初
始化派生类的成员
只能初始化直接基类
- 基类析构函数是三法则(第 13.3 节)的一个重要例外。
三法则指出,如果类需要析构函数,则类几乎也确实需要其他复制控制成员。基类几乎总是需要构造函数,从而可以将析构函数设为虚函数。如果基类为了将析构函数设为虚函数则具有空析构函数,那么,类具有析构函数并不表示也需要赋值操作符或复制构造函数。
即使析构函数没有工作要做,继承层次的根类也应该定
- 构造函数和赋值操作符不是虚函数
在复制控制成员中,只有析构函数应定义为虚函数,构造函数不能定义为虚函数。构造函数是在对象完全构造之前运行的,在构造函数运行的时候,对象的动态类型还不完整义一个虚析构函数。
如果在构造函数或析构函数中调用虚函数,则运行的是为构造函数或析构函数自身类型定义的版本.
- 名字冲突与继承
与基类成员同名的派生类成员将屏蔽对基类成员的直接访问使用作用域操作符访问被屏蔽成员
- 作用域与成员函数
在基类和派生类中使用同一名字的成员函数,其行为与数据成员一样:在派生类作用域中派生类成员将屏蔽基类成员。即使函数原型不同,基类成员也会被屏蔽
- 重载函数
如果派生类重定义了重载成员,则通过派生类型只能访问派生类中重定义的那些成员
- 容器与继承
因为派生类对象在赋值给基类对象时会被“切掉”,所以容器与通过继承相关的类型不能很好地融合。使用指针,智能指针。
- inline 函数模板
函数模板可以用与非模板函数一样的方式声明为 inline。说明符放在模板形参表之后、返回类型之前,不能放在关键字 template 之前
// ok: inline specifier follows template parameter list
template <typename T> inline T min(const T&, const T&);
// error: incorrect placement of inline specifier
inline template <typename T> T min(const T&, const T&);
- 函数模板的特化
模板特化(template specialization)是这样的一个定义,该定义中一个或多个模板形参的实际类型或实际值是指定的。特化的形式如下:
• 关键字 template 后面接一对空的尖括号(<>);
• 再接模板名和一对尖括号,尖括号中指定这个特化定义的模板形参;
• 函数形参表;
• 函数体。
下面的程序定义了当模板形参类型绑定到 const char* 时,compare 函数的特化:
// special version of compare to handle C-style character strings
template <> int compare<const char*>(const char* const &v1, const char* const &v2)
{
return strcmp(v1, v2);
}