目录

C++ Primer Plus 笔记 (9-13章)

内存模型和名称空间

单独编译

组件函数独立放置在文件中,将程序分为头文件、源代码文件、源代码文件…

头文件常包含

  • 函数原型
  • 使用 #defineconst 定义的符号常量
  • 结构声明
  • 类声明
  • 模板声明
  • 内联函数

同一个文件中,同一个头文件只能包含一次。c/c++用预处理器编译指令 #ifndef 来避免多次包含。

存储持续性、作用域和链接性

c++ 使用三种(c++11 中是4种)方案来存储数据:自动存储持续性,静态存储持续性,线程存储持续性(c++11),动态存储持续性。

自动存储持续性

在默认情况下,在函数中声明的函数参数和变量的存储持续性为自动,作用域为局部,没有链接性。

由于自动变量的数目随函数的开始和结束而增减,因此程序必须在运行时对自动变量进行管理。常用的方法是留出一段内存,并将其视为,以管理变量的增减。

静态持续

和C语言一样,C++也为静态存储持续性变量提供了3 种链接性:外部链接性(可在其他文件中访问)、内部链接性(只能在当前文件中访问)和无链接性(只能在当前函数或代码块中问)。这3 种链接性都在整个程序执行期间存在,与自动变量相比,它们的寿命更长。

编译器将分配固定的内存块来存储所有的静态变量,这些变量在整个程序执行期间一直存在。另外,如果没有显式地初始化静态变量,编译器将把它设置为0。在默认情况下,静态数组和结构将每个元素或成员的所有位都设置为0

在 C++ 代码中,空指针用0表示,但内部可能采用非零表示,因此指针变量将被初始化相应的内部表示。结构成员被零初始化,日填充位都被设置为零。

在函数的外面使用关键字 static 定义的变量的作用域为整个文件,但是不能用于其他文件(内部链接性)

C++提供了两种变量声明。一种是定义声明(defining declaration)或简称为定义 (definition);另一种是引用声明(referencing declaration)或简称为声明(declaration)。引用声明使用关键字extern,且不进行初始化;否则,声明为定义,导致分配存储空间

外部存储尤其适于表示常量数据,因为这样可以使用关键字 const 来防止数据被修改。

#### 动态分配 使用C++运算符new(或C函数 malloc())分配的内存,这种内存被称为动态内存。

new 可能会失败,引发异常 std::bad_alloc

new 负责在 (heap) 中找到一个足以能够满足要求的内存块。

名称空间

名称空间可以嵌套

可以通过省略名称空间的名称来创建未命名的名称空间:

1
2
3
4
namespace
{
    int ice;
}

不能在未命名名称空间所属文件之外的其他文件中,使用该名称空间中的名称。这提供了链接性为内部的静态变量的替代品。

对象和类

过程性编程和面向对象编程

采用过程性编程方法时,首先考虑要遵循的步骤,然后考虑如何表示这些数据

采用OOP 方法时,首先从用户的角度考虑对象一描述对象所需的数据以及描述用户与数据交互所需的操作。完成对接口的描述后,需要确定如何实现接口和数据存储。

抽象和类

一般来说,类规范由两个部分组成。

  • 类声明:以数据成员的方式描述数据部分,以成员函数(被称为方法)的方式描述公有接口。
  • 类方法定义: 描述如何实现类成员函数。

将实现细节放在一起并将他们与抽象分开被称为封装

数据隐藏不仅可以防止直接访问数据,还让开发者(类的用户) 无需了解数据是如何被表示的。

类与结构之间唯一的区别是,结构的默认访问类型是 public,而类为private。

其定义位于类声明(一般来说就是hpp)中的函数都将自动成为内联函数

类的构造函数和析构函数

为区分构造函数参数名和类成员变量名,常见做法有数据成员名前加m_,或者成员名后加_

1
2
3
private:
    string m_company;
    string company_; // or

c++ 提供两种使用构造函数初始化对象的方式。

1
2
Stock garment("Furry Mason",50,2.5); //这种格式更紧凑,它与下面的显式调用等价
Stock garment = Stock("Furry Mason",50,2.5);

使用new

1
Stock *pstock = new Stock("Electroshock Games",18,19.0);

默认构造函数是在未提供显式初始值时,用来创建对象的构造函数,如

1
Stock stock;

当且仅当没有定义任何构造函数时,编译器才会提供默认构造函数。为类定义了构造函数后,程序员就必须为它提供默认构造函数。通常应提供对所有类成员做隐式初始化的默认构造函数。

构造函数和析构函数都没有返回值和声明类型。析构函数没有参数。

在 C++11 中,可将列表初始化语法用于类,只要提供与某个构造函数的参数列表匹配的内容。

1
2
Stock hot tip ={"Derivatives Plus Plus",100,45.0};
Stock::Stock(const std::string & co, long n = 0,double pr = 0.0); // 与之匹配的构造函数

类函数中,把const放到函数括号后,就称为const成员函数。

this指针

this 指针指向用来调用成员函数的对象

对象数组

类作用域

C++11 提供了一种新枚举,其枚举量的作用域为类。这种枚举的声明类似于下面这样:

1
2
enum class egg {Small, Medium, Large, Jumbo};
enum class t_shirt {Small,Medium, Large, Xlarge};

也可使用关键字 struct 代替 class。无论使用哪种方式,都需要使用枚举名来限定枚举量:

1
2
egg choice= egg::Large;// the Large enumerator of the egg enumt 
shirt Floyd = t_shirt::Large; // the Large enumerator of the t shirt enum

抽象数据类型

使用类

运算符重载

运算符重载是一种形式的 C++多态。

隐藏了内部机理,并强调了实质,这是 OOP 的另一个目标。 要重载运算符,需使用被称为运算符函数的特殊函数形式。运算符函数的格式如下

operatorop(argument-list)

计算时间:一个运算符重载示例

对于一个Time类,将其sum函数转换为运算符+

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
Time Time::Sum(const Time & t) const{
    Time sum;
    sum.minutes = minutes + t.minutes;
    sum.hours = hours + t.hours + sum.minutes / 60;
    sum.minutes= 60;
    return sum;
}
// 转换为
Time Time::operator+(const Time & t) const{
    Time sum;
    sum.minutes = minutes + t.minutes;
    sum.hours = hours + t.hours + sum.minutes / 60;
    sum.minutes= 60;
    return sum;
}

重载限制

  1. 重载后的运算符必须至少有一个操作数是用户定义的类型,这将防止用户为标准类型重载运算符。
  2. 使用运算符时不能违反运算符原来的句法规则。不能修改运算符的优先级。
  3. 不能创建新运算符。 如不能用**表示幂
  4. 不能重载下面的运算符
    • sizeof: sizeof运算符。
    • .: 成员运算符。
    • .*: 成员指针运算符
    • ::: 作用域解析运算符
    • ?:: 条件运算符。
    • typeid: 一个RTTI运算符
    • const_cast: 强制类型转换运算符
    • dynamic_cast: 强制类型转换运算符
    • reinterpret_cast: 强制类型转换运算符
    • static_cast: 强制类型转换运算符。
  5. 下面的运算符只能通过成员函数进行重载
    • =: 赋值运算符。
    • (): 函数调用运算符。
    • []: 下标运算符。
    • ->: 通过指针访问类成员的运算符。

友元

友元有3种:友元函数;友元类;友元成员函数。

通过让函数成为类的友元,可以赋予该函数与类的成员函数相同的访问权限

非成员函数,显示声明对象参数,创建友元函数的第一步是将其原型放在类声明中,并在原型声明前加上关键字 friend: friend Time operator*(double m, const Time & t); // goes in class declaration

该原型意味着下面两点:

  • 虽然operator*()函数是在类声明中声明的,但它不是成员函数,因此不能使用成员运算符来调用
  • 虽然operator*()函数不是成员函数,但它与成员函数的访问权限相同

提示:一般来说,要重载<<运算符来显示 c_name 的对象,可使用一个友元函数,其定义如下:

1
2
3
4
ostream & operator<<(ostream & os, const c_name & obj){
    os << ... ; // display object contents
    return os;
}

重载运算符:作为成员函数还是非成员函数

非成员版本的重载运算符函数所需的形参数目与运算符使用的操作数数目相同;而成员版本所需的参数数目少一个,因为其中的一个操作数是被隐式地传递的调用对象。

再谈重载:一个矢量类

因为运算符重载是通过函数来实现的,所以只要运算符函数的特征标不同,使用的运算符数量与相应的内置 C++运算符相同,就可以多次重载同一个运算符。

对于只有二元形式的运算符(如除法运算符),只能将其重载为二元运算符。

rand()函数将一种算法用于一个初始种子值来获得随机数,该随机值将用作下一次函数调用的种子) 依此类推。这些数实际上是伪随机数,因为 10 次连续的调用通常将生成10个同样的随机数(具体值取决于实现)。

然而,srand( )函数允许覆盖默认的种子值,重新启动另一个随机数序列。该程序使用 time (0) 的返回值来设置种子。time(0)函数返回当前时间,通常为从某一个日期开始的秒数。

类的自动转换和强制类型转换

C++语言不自动转换不兼容的类型。

只接受一个参数的构造函数定义了从参数类型到类类型的转换。如果使用关键字 explicit 限定了这种构造函数,则它只能用于显示转换,否则也可以用于隐式转换。

要转换为 typeName类型,需要使用这种形式的转换函数:

operator typeName();

  • 转换函数必须是类方法;
  • 转换函数不能指定返回类型;
  • 转换函数不能有参数;

这样,下面的语句将与成员函数operator +(double x)完全匹配:

total = jennySt + kennyD; // Stonewt + double

而下面的语句将与友元函数operator +(doublex, Stonewt &s)完全匹配:

total = pennyD + jennySt; // double + Stonewt

类和动态内存分配

动态内存和类

静态数据成员在类声明中声明,在包含类方法的文件中初始化。初始化时使用作用域运算符来指出静态成员所属的类。但如果静态成员是整型或枚举型 const,则可以在类声明中初始化。

复制构造函数用于将一个对象复制到新创建的对象中。也就是说,它用于初始化过程中(包括按值传递参数),而不是常规的赋值过程中。类的复制构造函数原型通常如下:

1
Class name(const Class name &);
  • 新建一个对象并将其初始化为同类现有对象时,复制构造函数都将被调用
  • 每当程序生成了对象副本时,编译器都将使用复制构造函数。具体地说,当函数按值传递对象或函数返回对象时,都将使用复制构造函数。
  • 默认的复制构造函数逐个复制非静态成员(成员复制也称为浅复制),复制的是成员的值。
  • 隐式复制构造函数是按值进行复制的,对于sailor.str = sport.str;这里复制的并不是字符串,而是一个指向字符串的指针。

如果类中包含了使用new 初始化的指针成员,应当定义一个复制构造函数,以复制指向的数据而不是指针,这被称为深度复制。复制的另一种形式(成员复制或浅复制)只是复制指针值。浅复制仅浅浅地复制指针信息,而不会深入“挖掘”以复制指针引用的结构。

对于一个StringBad类,其显示复制构造函数形如

1
2
3
4
5
6
7
StringBad::StringBad(const StringBad & st){
    num strings++; // handle static member update
    len = st.len; // same length
    str = new char [len + 1]; // allot space
    std::strcpy(str,st.str); // copy string to new location
    cout << num strings <<": \"" << str << "\" object created\n"; // For Your Information
}

浅复制的问题也可以通过重写赋值运算符的方式解决

对于由于默认赋值运算符不合适而导致的问题,解决办法是提供赋值运算符(进行深度复制)定义。 其实现与复制构造函数相似。

  • 由于目标对象可能引用了以前分配的数据,所以函数应使用 delete[]来释放这些数据。
  • 函数应当避免将对象赋给自身:否则,给对象重新赋值前,释放内存操作可能删除对象的内容
  • 函数返回一个指向调用对象的引用。

对于一个StringBad类,重写赋值运算符函数形如

1
2
3
4
5
6
7
8
9
StringBad& StringBad::operator=(const StringBad & st){
    if (this == &st) // object assigned to itself
        return *this; // all done
    delete[] str; // free old string
    len = st.len;
    str = new char [len + 1]; // get space for new string
    std::strcpy(str,st.str); // copy the string
    return *this; // return reference to invoking object
}

改进后的String类

在 C++98 中,字面值 0 有两个含义: 可以表示数字值零,也可以表示空指针。C++11,引入了 nullptr。

不能通过对象调用静态成员函数;静态成员函数不能使用 this 指针,只能使用静态数据成员。

在构造函数中使用new时应注意的事项

有关返回对象的说明

当成员函数或独立的函数返回对象时,有几种返回方式可供选择。可以返回指向对象的引用、指向对象的 const引用或 const 对象。

返回对象将调用复制构造函数,而返回引用不会

使用指向对象的指针

通常,如果 Class_name 是类,value 的类型为 Type_name,则下面的语句: Class name * pclass = new Class name(value) ; 将调用如下构造函数: Class name(Type name) ;

这里可能还有一些琐碎的转换,例如: Class name(const Type name &) ;

另外,如果不存在二义性,则将发生由原型匹配导致的转换( 如从 int到 double)。下面的初始化方式将调用默认构造函数: Class name * ptr = new Class name;

复习各种技术

队列模拟

成员初始化列表的语法

如果 Classy 是一个类,而 meml、mem2 和 mem3 都是这个类的数据成员,则类构造函数可以使用如下的语法来初始化数据成员:

1
2
3
Classy::Classy(int n, int m) :mem1(n), mem2(0), mem3(n*m +2){

}
  • 这种格式只能用于构造函数:
  • 必须用这种格式来初始化非静态 const 数据成员(至少在 C++11 之前是这样的 );
  • 必须用这种格式来初始化引用数据成员。
  • 数据成员被初始化的顺序与它们出现在类声明中的顺序相同,与初始化器中的排列顺序无关。

C++11的内始化

C++11 允许您以更直观的方式进行初始化:

1
2
3
4
class Classy {
    int mem1 = 10;// in-class initialization
    const int mem2 = 20; // in-class initialization
}

这与在构造函数中使用成员初始化列表等价

类继承

一个简单的基类

关于派生类构造函数

  • 首先创建基类对象;
  • 派生类构造函数应通过成员初始化列表将基类信息传递给基类构造函数:
  • 派生类构造函数应初始化派生类新增的数据成员。
  • 若没有提供显式构造函数,因此将使用隐式构造函数。释放对象的顺序与创建对象的顺序相反即首先执行派生类的析构函数,然后自动调用基类的析构函数。

派生类对象可以使用基类的方法,条件是方法不是私有的

基类指针可以在不进行显式类型转换的情况下指向派生类对象;基类引用可以在不进行显式类型转换的情况下引用派生类对象。然而,基类指针或引用只能用于调用基类方法

不可以将基类对象和地址赋给派生类引用和指针

继承:is-a关系

C++有 3 种继承方式:公有继承、保护继承和私有继承。

公有继承是最常用的方式,它建立一种 is-a 关系,即派生类对象也是一个基类对象,可以对基类对象执行的任何操作,也可以对派生类对象执行。

多态公有继承

2种重要的机制可用于实现多态公有继承:

  • 在派生类中重新定义基类的方法。
  • 使用虚方法。

如果方法是通过引用或指针而不是对象调用的,它将确定使用哪一种方法。如果没有使用关键字 virtual,程序将根据引用类型或指针类型选择方法; 如果使用了 virtual,程序将根据引用或指针指向的对象的类型来选择方法。也就是说没有virtual就看左边的类型,有就看右边的类型。

为什么需要虚析构函数:如果析构函数不是虚的,则将只调用对应于指针类型的析构函数。也就是说基类引用或者指针指向派生类对象的时候,如果基类析构函数不是虚的,就只会调用基类析构,如果是虚的,就会先调用派生类析构,再自动调用基类析构。

静态联编和动态联编

将源代码中的函数调用解释为执行特定的函数代码块被称为函数名联编(binding)。 在编译过程中进行联编被称为静态联编(static binding),又称为早期联编(early binding)。编译器必须生成能够在程序运行时选择正确的虚方法的代码,这被称为动态联编 (dynamic binding),又称为晚期联编 (late binding)。

将派生类引用或指针转换为基类引用或指针被称为向上强制转换 (upcasting),这使公有继承不需要进行显式类型转换。

相反的过程–将基类指针或引用转换为派生类指针或引用——称为向下强制转换 (dwncasting)。如果不使用显式类型转换,则向下强制转换是不允许的。

通常,编译器处理虚函数的方法是: 给每个对象添加一个隐藏成员。隐藏成员中保存了一个指向函数地址数组的指针。这种数组称为虚函数表 (vitual function table, vtbl)。虚函数表中存储了为类对象进行声明的虚函数的地址。例如,基类对象包含一个指针,该指针指向基类中所有虚函数的地址表。派生类对象将包含一个指向独立地址表的指针。如果派生类提供了虚函数的新定义,该虚函数表将保存新函数的地址:如果派生类没有重新定义虚函数,该 vtbl 将保存函数原始版本的地址。如果派生类定义了新的虚函数,则该函数的地址也将被添加到 vtbl中。

使用虚函数时,在内存和执行速度方面有一定的成本,包括:

  • 每个对象都将增大,增大量为存储地址的空间;
  • 对于每个类,编译器都创建一个虚函数地址表 (数组);
  • 对于每个函数调用,都需要执行一项额外的操作,即到表中查找地址。 虽然非虚函数的效率比虚函数稍高,但不具备动态联编功能。

构造函数不能是虚函数,析构函数应当是虚函数,除非类不用做基类

友元不能是虚函数,因为友元不是类成员,而只有成员才能是虚函数。

重新定义继承的方法并不是重载。如果在派生类中重新定义函数,将不是使用相同的函数特征标覆盖基类声明,而是隐藏同名的基类方法,不管参数特征标如何。这引出了两条经验规则: 第一,如果重新定义继承的方法,应确保与原来的原型完全相同,但如果返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针 (这种例外是新出现的)。这种特性被称为返回类型协变(covariance ofreturn type),因为允许返回类型随类类型的变化而变化:

访问控制:protected

关键字 protected 与 private 相似,在类外只能用公有类成员来访问 protected 部分中的类成员。private 和 protected之间的区别只有在基类派生的类中才会表现出来。派生类的成员可以直接访问基类的保护成员,但不能直接访问基类的私有成员。

也就是对派生类不私有,对外私有。

抽象基类

C++通过使用纯虚函数(pure virtual function) 提供未实现的函数。当类声明中包含纯虚函数时,则不能创建该类的对象。

继承和动态内存分配

假设派生类使用了new,在这种情况下,必须为派生类定义显式析构函数、复制构造函数和赋值运算符。

类设计回顾

由于友元函数并非类成员,因此不能继承。然而,您可能希望派生类的友元函数能够使用基类的友元函数。为此,可以通过强制类型转换将,派生类引用或指针转换为基类引用或指针,然后使用转换后的指针或引用来调用基类的友元函数