《C++ Primer Plus》学习笔记

258 阅读25分钟

P77 字符串输入

先举一个有BUG的程序

#include <iostream>
using namespace std;
int main()
{
       const int ArSize = 20;
       char name[ArSize];
       char dessert[ArSize];
       cout << "Enter you name:\n";
       cin >> name;
       cout << "Enter your favorite dessert" << endl;
       cin >> dessert;
       cout << "I have some delicious " << dessert << "for you, " << name << ".\n";
       return 0;

}

输出结果如下:

Enter you name:
A B
Enter your favorite dessert
I have some delicious B for you, A.

上述出现的问题是他将nameA输入到了name数组中,再将B输入到dessert数组中。原因是,系统将A、B中间的空格作为读取输入的终止符,所以第一次读取到空格后就终止读取此时B还存在输入的缓冲队列中,当下次读取时发现队列中已经有内容了,然后就直接从队列中读取内容而跳过用户输入的阶段。解决该问题的方法是每次读取一行字符串输入。

运用getline()

getline()函数读取整行,他通过回车键输入的换行符来确定输入结尾。 该函数有两个参数,第一个参数是用来放数据的数组名称,第二个参数是要读取得字符数。

getline()函数优化程序。

#include <iostream>
using namespace std;
int main()
{
       const int ArSize = 20;
       char name[ArSize];
       char dessert[ArSize];
       cout << "Enter you name:\n";
       cin.getline(name, ArSize);
       cout << "Enter your favorite dessert " << endl;
       cin.getline(dessert, ArSize);
       cout << "I have some delicious " << dessert << " for you, " << name << ".\n";
       return 0;
}

输出结果:

Enter you name:
A B
Enter your favorite dessert 
SHIT
I have some delicious SHIT for you, A B.

输出正常,getline()函数每次读取一行,它通过换行符来确定行尾,但不保存换行符,相反,在存储字符串时,它用空字符来替换换行符。

运用get()

get()函数同样是读取一行,参数也和getline()函数的参数一样,但不同点是get()函数读取也不丢弃换行符,而是将其保留在输入队列中。 用get()函数优化程序。

#include <iostream>
using namespace std;
int main()
{
       const int ArSize = 20;
       char name[ArSize];
       char dessert[ArSize];
       cout << "Enter you name:\n";
       //由于get()函数不读取换行符,所以cin.get(name, ArSize)
       //后需要添加.get()在进行读取一个字符,才能保证下次读取的正常
       cin.get(name, ArSize).get();
       cout << "Enter your favorite dessert " << endl;
       cin.get(dessert, ArSize).get();
       cout << "I have some delicious " << dessert << " for you, " << name << ".\n";
       return 0;
}

输出结果如下:

Enter you name:
A b  
Enter your favorite dessert 
shit
I have some delicious shit for you, A b.

注:调用cin >> year后,会在输入队列后留下换行符,所以应该调用(cin >> year).get()

P82 字符串类简介

string对象与字符数组之间的相同:

  • 使用C风格字符串初始化string对象。
  • 可以使用cin来将键盘输入储存到string对象中。
  • 可以使用cout来显示string对象。
  • 可以使用数组表示法来访问string对象中的字符

主要区别在于:

  • 可以将string对象声明为简单变量,而不是数组。类设计让程序能够自动处理string的大小。
  • 可以将一个string对象赋值给另一个string对象。
  • 可以使用str3 = str1 + str2;来合并两个string对象。
  • 通过使用str1.size()来获取字符串的大小。

P144 字符串的比较

  • C语言风格用strcmp(str1, str2)函数,相同返回0,不同返回1。
  • C++运用string类,可直接用对象与字符串进行比较,例:
  string word = "?ate";
  word != "mate";

P179 cctype中的字符函数

P88 结构简介

CC++的异同:

  • C++中定义结构变量不需要在前面加struct

P105&P320 运算符new

使用new运算符初始化

int *pi = new int (6); //*pi set to 6

struct where {souble x; double y; double z;}
where * one = new where {2.5, 5.3, 7.2};

int * ar = new int [4] {2, 4, 6, 7};

使用newdelete时应该遵守以下规则:

  • 不要使用delete来释放不是new分配的内存。
  • 不要使用delete释放同一内存块两次。
  • 如果使用new []为数组分配内存,呢应使用delete []来释放。
  • 对空指针应用delete是安全的。
  • 不能使用sizeof运算符来确定动态数组包含的字节数。

定位new运算符

char buffer[BUF];
double *p = new (buffer) double[N];

将在指定的内存中分配空间,不能用delete释放。

P120 数组的替代品

模板类vector
例:vector<int> vi; vi是一个vector<int>对象,在插入或添加值时自动调整长度。同样可以运用vector<double> vb(n);声明一个名为vb,可储存n个元素。
模板类array

vector类功能强大,但是效率低。 所以新增了模板类array,长度固定,效率与数组相同,但更方便和安全。

定义如下:

array<int, 5> ai = {0, 1, 2, 3, 4};

注:arrayvector可以使用成员函数at()来捕获非法引用。

P224 函数和二维数组

二维数组作为函数参数时的函数定义:

  • int sum(int (*arr)[4], int size);
  • int sum(int arr[][4], int size);

指针类型指定了列数,但是行数是随意的。

P253 内联函数

内联函数与常规函数

  • 常规函数的调用需要将函数参数压入堆栈中,然后程序跳到函数代码存储的位置,执行完后返回主程序。
  • 内联函数把所有函数调用的位置换成函数的代码段,这样会使运行速度边快,但代价是需要占用更多的内存。
  • 在函数声明和定义前都要加上inline
  • C语言中的宏时内联代码的原始实现,但是宏不能按值传递。

内联方法
其定义位于类声明中的函数都将自动成为内联函数。内联函数的特殊规则要求在每个使用它们的文件中都对其进行定义。确保内联定义对多文件程序中的所有文件都可用、最简单的方法是:将内联定义放在定义了类的头文件中。

P255 引用变量

定义

int rat; int & rodents = rats; rodentsrats的引用,他们在内存中的地址和内容一样,可以理解为rodentsrats的别名。

  • 必须在声明引用变量时进行初始化。例:int & rodents = rats;
  • 不要返回指向局部变量或临时对象的引用。函数执行完完毕后,局部变量和临时对象将消失,引用将指向不存在的数据。

将引用用作函数参数

double refcube(const double & ra);
当引用参数是const时,编译器会在下面两种情况下生成临时变量:

  • 实参的类型正确,但不是左值(可通过地址访问的是左值)。
  • 实参类型不正确,但可以转换为正确类型。
    例:
    long edge = 5L;
    double side = 3.0;
    double C1 = refcube(edge); //类型不匹配,所以ra是一个临时变量
    double C2 = refcube(7.0); //7.0不是一个左值,所以ra是一个临时变量
    double C3 = refcube(side + 10.0); //同上

将引用参数声明为常量数据的理由:

  • 使用const可以避免无转移中修改数据的变成错误;
  • 使用const势函数能够处理const和非const实参,否则将只能接受非const数据;
  • 使用const引用使函数能够正确生成并使用临时变量。

何时使用引用参数:

  • 程序员能够修改调用函数中的数据对象。
  • 通过传递引用而不是整个数据对象,可以提高程序的运行速度。
  • 如果数据对象是数组,只能使用指针,若数据不能修改,则使用const指针。
  • 若数据对象是结构,则使用引用或指针,若数据不能被修改则使用const结构和const指针。
  • 若数据对象是类,则只能使用引用,若数据不能被修改则使用const引用。

P274 默认参数

    int chico(int n, int m = 6, int j =5);
    beeps = chico(2);       //same as chico(2,6,5)
    beeps = chico(1,8);     //same as chico(1,8,5)
    beeps = chico(8,7,6);  //no default arguments used

P276 函数重载

当函数基本上执行相同的任务,但使用不同形式的数据时,才应采用函数重载。 另外,应该考虑是否可以通过使用默认参数来实现同样的目的。

  • 编译器在检查函数特征标时,将把类型引用和类型本身视为同一特征标。
  • 重载引用参数时,将调用最匹配的版本。

P281 函数模板

交换两数的函数模板:

//function template prototype
template <typename T> void swap(T &a, T &b);

// function template definition
template <typename T>
void swap(T &a, T &b)
{
   T temp;
   temp = a;
   a = b;
   b = temp;
}
  • 如果需要多个将同一种算法用于不同类型的函数,请使用模板。
  • 函数模板不能缩短可执行程序。

重载的模板

//重载一个交换数组元素的函数模板
void swap(T *a, T *b, int n); 

显式具体化

// explicit specialization for the job type
template <> void swap<job>(job &, job &);

实例化

// explicit instantiation for the job type
template void swap<job>(job &, job &);
  • 具体化并非函数定义,编译器根据函数调用中实际使用的参数,生成相应的版本。
  • 编译器看到job实例化后,将使用模板生成swap()job版本。

编译器选择使用哪个函数版本?

  • 第一步:创建候选参数列表。 包含于被调函数的名称相同的函数和模板函数。
  • 第二步:创建可行函数列表。 这些都是参数数目正确的函数。
  • 第三步: 确定是否有最佳的可行函数。

最佳到最差的选择顺序:

  • 完全匹配,常规函数优先于模板函数。
  • 提升转换(例如:charshort自动转换为int。)
  • 标准转换(例如:int转换为char
  • 用户定义的转换。

创建自定义选择

swap<int>(m, n);其中的<>指出,编译器应选择模板函数,而不是非模板函数。

关键字decltype

为确定类型,编译器必须遍历一个核对表。假如有如下声明: decltype(expression) var;
核对表的简化版如下:

  • 第一步:如果expression是一个没有用括号括起的标识符,则var的类型与标识符的类型相同,包括const等限定符。
  • 第二步:如果expression是一个函数调用,则var的类型与函数的返回类型相同。(注:并不会实际调用函数。
  • 第三步:如果expression是一个左值,则var为指向其类型的引用。(expression是用括号括起的标识符。) -第四步:前面都不满足,则var的类型与expression类型相同。例:decltype(100L) i; (i type long)

后置返回类型

template<typename T1, typename T2>
auto gt(T1 X, T2 Y) -> decltype(X + Y)
{
    ...
    return X + Y;
}

P302 头文件管理

在同一个文件中只能将同一个头文件包含一次。有一种标准的C/C++技术可以避免多次包含同一个头文件。

    #ifndef COORDIN_H_
    #define COORDIN_H_
    #endif

P317 关键字mutable

关键字volatile

关键字volatile表明,即使程序代码没有对内存单元进行修改,其值也可能发生变化。例如,假设编译器发现,程序在几条语句中两次使用了某个变量的值,则编译器可能不是让程序查找这个值两次,而是将这个值缓存到寄存器中。将变量声明为volatile,相当于告诉编译器,不要进行这种优化。

关键字mutable
可以用它来指出,即使结构或类变量为const,其某个成员也可以被修改。例如:

struct data
{
    char name[30];
    mutable int accesses;
}

const data temp = {"clodde", 0};
strcpy(temp.name, "joye joux");  //not allowed
temp.accesses++;    //allowed

P319 语言链接性

为满足内部需求,C语言编译器可能将`spiff`这样的函数名翻译为`_spiff`。但在C++中,同一个名称可能对应多个函数,可能将`spiff(int)`翻译为`spiff_i`,将`spiff(double, double)`翻译为`spiff_d_d`。但如果要在C++程序中使用C库中预编译的函数将为出错,为解决这种问题,可以用函数原型来制定要使用的约定:
extern "C" void spiff(int); //use C protocol for name look-up
extern void spiff(int);  //use C++ protocol for name look-up
extern "C++" void spaff(int); //use C+++ protocol for name look-up

P325 名称空间

namespace jack{
    double pail;
    int pal;
}

namespace jill{
    double fetch;
    int pal;
}

调用名称空间:

  • 使用using声明,using jill::fetch; ,如果空间中已有一个fetch变量,编译器则会报错。
  • 使用using编译指令,using namespace jack; 使用名称空间的名字全部可用,若空间中已有pail变量,则局部变量则会隐藏名称空间中的变量。
  • 名称空间可嵌套。
  • 可以给名称空间创建别名,namespace job = jill;

未命名的名称空间

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

P352 类的构造函数和析构函数

构造函数

  • 构造函数与类同名,且无返回值。
  • 在定义类对象时,自动调用构造函数,例:stock food = stock("World Cabbage", 250, 1.25); 或者stock food("World Cabbage", 250, 1.25); 在有些编译器中,第一种初始化方法会先创建一个临时变量,然后将其赋值给food,然后再删除临时变量,此时会调用析构函数。
  • 可通过列表初始化,
    stock jock{"sport age "};  //使用与其参数列表匹配的构造函数。
    stock temp{};   // 使用默认构造函数

默认构造函数

  • 默认构造函数是在未提供显式初始值时,用来创建对象的构造函数。它用于下面这种声明的构造函数:stock fluffy;( uses the default constructor)
  • 当且仅当没有定义任何构造函数时,编译器才会提供默认构造函数。未类定义了构造函数后,程序员就必须为它提供默认构造函数。
  • 定义默认构造函数的方式有两种:一种是给已有构造函数的搜有参数提供默认值:
    stock(const string & co = "Error", int n = 0, doule pr = 0.0);

  另一种方式是通过函数重载来定义另一个构造函数。

析构函数

  • 析构函数定义,例:

    stock::~stock()
    {
    }
  • 通常不在代码中显式地调用析构函数。如果创建的是静态存储类对象,则其析构函数在程序结束时自动调用。
  • 如果创建的是自动存储类对象,其析构函数将在程序执行完代码块时自动被调用。
  • 如果对象是通过new对象创建的,则它将驻留在栈内存或自动存储区中,当使用delete来释放内存时,其析构函数将自动被调用。

const成员函数
当需要保证成员函数不能修改对象时,引用一下的语法:

void show() const;  //在类中声明
void stock::show() const  //在类外定义,隐式地访问调用改方法的对象
{
}

P363 this指针

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

  • 如果方法需要引用整个调用对象,则可使用表达式*this

P370 类作用域

作用域为类的常量

  • 方法一:在类中定义enum {mouths = 12};。这种方式声明枚举并不会创建类数据成员,在作用域为整个类的代码中遇到它时,编译器将用12来替换它。由于这里使用枚举只是为了创建符号常量,并不打算创建枚举类型的变量,因此不需要提供枚举名。
  • 方法二:在类中使用关键字static,例:static const int Months = 12;该常量与其他静态变量存储在一起,而不是存储在对象中。

作用域内枚举

    enum class egg{small, medium, large, xlarge};
    enum class t_shirt{small, medium, large, xlarge};
    egg choice = egg::large;  //the large enumerator of the egg enum

P381 运算符重载

以时间的加法为例,首先展示不用运算符重载的方法实现。

    //成员函数定义
    Time Time::Sum(const Time & t) const
    {
        Time sum;
        sum.minutes = minutes + t.minutes;
        sum.hour = hours + t.hour + sum.minutes / 60;
        sum.minutes =%= 60;
        return sum;
    }
    //类定义
    Time total, fixing, coding;
    //调用方法
    total = coding.Sum(fixing);

用重载运算符实现上述功能,

    //成员函数定义
    Time Time::operator+(const Time & t) const
    {
        Time sum;
        sum.minutes = minutes + t.minutes;
        sum.hour = hours + t.hour + sum.minutes / 60;
        sum.minutes =%= 60;
        return sum;
    }
    //类定义
    Time total, fixing, coding;
    //调用方法,same as total = coding.operator+(fixing) 
    total = coding + fixing;     

重载限制

  • 重载后的运算符至少有一个操作数是用户定义的类型。
  • 使用运算符时不能违反运算符原来的句法规则。
  • 不能创建新运算符。

P390 友元

友元函数
如果重载运算符*,使其能让Time类型和double类型相乘。

    //成员函数定义
    Time Time::operator*(double mult) const
    {
        Time result;
        long totalminutes = hours * mult * 60 + minutes * mult;
        result.hours = totalminutes / 60;
        result.minutes = totalminutes % 60;
        return result;
    }
    //类定义
    Time A, B;
    A = B * 2.75; // same as A = B.operator*(2.75);
    A = 2.75 * B; // cannot correspond to a member function

为了能让上述的的*能满足交换律,将采用友元函数,使其与下面的非成员函数调用匹配:

    A = operator*(double m, const Time & t);
  • 创建友元函数的第一步是将其原型放在类生命中,并在原型声明前加上关键字friend
    friend Time operator*(oduble m, const Time & t);
  • 虽然opreator*()函数是在类声明中声明的,但它不是成员函数,因此不能使用成员运算符来调用;
  • 虽然operator*()函数不是成员函数,但它可与成员函数的访问权限相同。
  • 函数定义不要使用Time::限定符。另外不要在定义中使用关键字friend
    Time operator*(double mult, const Time & t) 
    {
        Time result;
        long totalminutes = hours * mult * 60 + minutes * mult;
        result.hours = totalminutes / 60;
        result.minutes = totalminutes % 60;
        return result;
    }
  • 如果要为类重载运算符,并将非类的项作为其第一个操作数,则可以用友元函数来翻转操作数的顺序。
  • 非成员版本的重载运算符函数所需的形参数目与运算符使用的操作数数目相同;而成员版本所需的参数数目少一个,因为其中的一个操作数是被隐式地传递的调用对象。

P490 多态公有继承

有两种重要的机制可实现多态公有继承:  
  • 在派生类中重新定义基类的方法
  • 使用虚方法。

虚方法
在函数前加上关键字virtual,例:virtual void ViewAcct() const;

  • 如果方法是通过引用或者指针而不是对象调用,如果没有使用关键字virtual,程序将根据引用类型或指针类型选择方法;如果使用了virtual,成功逆袭将根据引用或者指针指向的对象的类型来选择方法。
  • 经常在基类中将派生类会重新定义的方法声明为虚方法。方法在基类中被声明为虚后,它将在派生类中将自动成为虚方法。
  • 应该定义虚析构函数。 如果虚构函数不是虚的,则将只调用对应于指针类型的析构函数。
  • 构造函数不能是虚方法。
  • 友元不能是虚函数。因为友元不是类成员,而只有成员才能是虚函数。
  • 重新定义基类方法不是重载,这会隐藏基类方法。
  • 若果基类声明被重载了,则应在派生类中重新定义所有的基类版本。如果只重新定义一个版本,则另外的版本将被隐藏,派生类对象将无法使用它们。

静态联编和动态联编

  • 编译器在编译过程后者能够进行联编被称为静态联编
  • 由于虚函数的存在,使用哪一个函数是不能再编译时确定的。所以在程序运行时是选择正确的虚方法代码,这被称为动态联编
  • 编译器对非虚方法使用静态联编,对虚方法使用动态联编。这样效率会更高。

访问控制:protected

  • 在类外只能用公有类成员来访问protected部分中的类成员。派生类的成员可以直接访问基类的保护成员。

抽象基类

	virtual double Area() const = 0;// a pure virtual function

上式中定义的函数为纯虚函数。当类声明中包含纯虚函数时,则不能创建该类的对象。,包含纯虚函数的类只用作基类。抽象基类要求具体派生类覆盖其纯虚函数。

继承和动态内存分配

  • 第一种情况:派生类不使用new。派生类的默认复制构造函数使用显示基类的复制构造函数来复制派生类对象中的基类部分。赋值运算符也是如此。
  • 第二种情况:派生类使用new
	//复制构造函数
    hasDMA::hasDMA(const hasDMA & hs) : baseDMA(hs)
    {
    	//复制派生类中的独有的数据
    }
    //赋值运算符重载
    hasDMA & hasDMA::operator=(const baseDMA & rs)
    {
    	if (this == &hs)
        	return *this;
        baseDMA::operator=(hs); // same as *this = hs
        delete [] style;
        style = new char[std::strlen(hs.style) + 1];
        std::strcpy(style, hs.style);
        return *this;
    }

P536 包含对象成员的类

组合

  • 创建一个包含其他类对象的类。
 class Student
 {
 	private:
    	string name;
        valvarray<double> scores;
 };
  • 使用公有继承时,类可以继承接口,可能还有实现(基类的纯虚函数提供接口,但不提供实现)。而组合可以获得实现,但不能获得接口。
  • 当初始化列表包含多个项目时,这些项目被初始化的顺序为他们被声明的顺序,而不是他们再初始化列表中的顺序。

私有继承

	class Student : private std::string, private std::valarray<double>
    {
    }
  • 使用私有继承基类的公有方法将变成派生类的私有方法。
  • 私有继承将获得实现,但不获得接口。
  • 初始化基类组件: 它使用类名而不是成员名来标识构造函数。
	Student(const char * str, const double * pd, int n)
    : std::string(str){}
  • 访问基类的方法: 只能在派生类的方法中使用基类的方法。包含时将使用对象名来调用方法,而是用私有继承时将使用类名和作用域解析运算符来调用方法。
  • 访问基类对象: 使用强制类型转换。
	const string & Student::Name() const
    {
    	return (const string &) *this;
    }
  • 在私有继承中,未进行显式类型转换的派生类引用或指针,无法赋值给基类的引用或指针。
  • 通常,应使用包含来建立has-a关系;若果心累需要访问原有类的保护成员,或需要重新定义虚函数,则应使用私有继承。

保护继承

  • 基类的公有和保护成员变成派生类的保护成员,派生类只能通过基类接口访问私有成员。

P551 多重继承

  多重继承可能会给程序员带来很多新问题。主要的两个问题是:从两个不同的基类继承同名方法;从两个或者更多相关基类那里继承同一个类的多个实例。

继承多个实例解决方法

虚基类

  • 定义:
	class singer : virtual public worker{...};
    class waiter : virtual public worker{...};
    
    class singerWaiter : public singer, public waiter{...};
  • 这样,singerWaiter对象中只包含了一个worker对象的副本。从本质上说,继承的singerwaiter对象共享一个worker对象。
    新的构造函数规则
  • 例:
	singerWaiter(const worker & wk, int p = 0, int v = singer::other) 
    					: worker(wk), waiter(wk, p), singer(wk, v) {}
  • 上述代码将显示地调用构造函数worker(const worker &)。对于虚基类,这样做是必须的。而对于非虚基类,这样做是非法的。
  • 如果有间接虚基类,则除非只需要使用费虚基类的默认构造函数,否则必须显式的调用该虚基类的某个构造函数。

继承同名方法的解决方案

  • 多重继承可能导致函数调用的二义性。可以使用作用域解析运算符来澄清编程者的意图。
  • 另一种方法是在新的派生类中重新定义方法,在方法内部指出哟啊是用那个版本的方法。

类模板

P568 定义

	//类定义
	template <class Type>
    class Stack
    {
    	private:
        	enum {MAX = 10};
            Type items[MAX];
            int top;
        public:
        	Stack();
            bool isempty();
            bool isfull();
            bool push(const Type & item);
            bool pop(Type & item);
    }
    //成员函数定义
    template <class Type>
    Stack<Type>::Stack()
    {
    	top = 0;
    }
  • 如果在类声明中定义了方法(内联定义),则可以省略模板前缀和类限定符。
  • 模板必须与特定的模板实例化请求一起使用。

P570 使用模板类

  • 隐式实例化
	//implicit instantiation
	Stack<int> kernels;			//creat a stack of ints	
    Stack<string> colonels; 	//creat a stack of string objects
  • 显式实例化:声明必须位于模板定义所在的名称空间中。
	//explicit instantiation
    template class ArrayTP<string, 100>;

   在这种情况下,虽然没有创建或提及具体的类对象,编译器也将生成类声明。

  • 显式具体化:显式具体化是特定类型(用于替换模板中的泛型)定义。
	template <> class SortedArray<const char *>
    {
    	...//details omitted
    }
    
    SortedArray<int> scores;            //use general definition
    SortedArray<const char *> dates;	//use specialized definition
  • 部分具体化
	//general template
	template <class T1, class T2> class Pair{...};
    //specialization with T2 set to int
    template <class T1> class Pair<T1, int>{...};

  如果有多个模板可供选择,编译器将使用具体化程度最高的模板。

  • 可以为指针提供特殊版本来部分具体化现有的模板。
	template<class T*> class Feed{...};
    Feed<char *> fd2

  如果没有部分具体化,上述声明将T转换成char *类型。如果进行了部分具体化,则上述声明将T转换成char

  • 必须显式的提供所需的类型,这与常规的函数模板不同,因为编译器可以根据参数的类型确认要生成那种函数。
  • 可以递归的使用模板
	ArrayTP< Arrarytp<int, 5>, 10> twodee;
   	// same as
    int twodee[10][5];
  • 可以为类型参数提供默认值
	template <class T1, class T2 = int> class Topo {...};
    
    Tope<double> m2; // T1 is double, T2 is int

P584 成员模板

  • 例:
	template< typename T> class beta
    {
    	private:
         	...
        public:
        	template<typename U>
            U blab(U u, T t){...};        
    }
    
    // member definition
    template <typename T>
    	template<typename U>
        	class beta<T>::blab(U u, T t){...};
  • 可以将模板用作参数
	template < template <typename T> class Thing>
    class Crab{...}
    
    Crab<king> legs; // king is a template-class
    
    template <typename T> class king {...} 

P588 模板类和友元

模板类的非模板友元函数

  • 例:
	template <class T>
    class HasFriend
    {
    	public:
        	friend void counts(); 
    }
  • 上述声明将使函数counts()函数成为模板所有实例化的友元。例如,它将是类HasFriend<int>HasFriend<string>的友元。

模板类的约束模板友元函数

  • 第一步:在类定义的前面声明每个模板函数。
	template <typename T> void counts();
  • 第二步:在函数中再次将模板声明为友元。这些语句根据类末班参数的类型声明具体化。
	template <class T>
    class HasFriend
    {
    	public:
        	friend void counts<TT>(); 
    }	

模板类的非约束模板友元函数

  • 例:
	template <class T>
    class HasFriend
    {
    	public:
        	template <typename C, typename D> friend void show(C &, D &); 
    }
    
    void show<HasFriend<int> &, HasFriend<int> &>
    				(HasFriend<int> & c, HasFriend<int> & d);
  • 它是所有HasFriend具体化的友元,所以能够访问所有具体化的item成员,但它只访问了HasFriend<int>对象。

P593 模板别名

  • 可使用typedef为模板具体化指定别名。
	typedef std::array<double, 12> arrd;
    typedef std::array<int, 12> arri;
  • 使用模板提供一系列别名。
	template<typename T>
    		using arrtype = std::array<T, 12>; 
    // template to creat multiple aliases
  • arrtype<T>表示类型std::array<T, 12>

P603 友元类

  • 当两个类需要相互访问对方类中的成员数据或调用成员方法时,需要建立友元类。
  • 例:
	class TV
    {
    	friend class Remote;
        public:
        	...
    };
    class Remote
    {
    	friend class TV;
        public:
         	...
    }
  • 可以声明友元成员函数,让类中的特定方法成员友元。例:
	class TV
    {
    	friend void Remote::set_chan(TV & t, int c);
    }
  • 当一个类用到另一个类中的方法时,需要进行前向声明,例:
	class TV;
    class Remote {...};
    class TV {...};
  • 共同友元
	class TV
    {
    	friend void sync();
        public:
        	...
    };
    class Remote
    {
    	friend void sync;
        public:
         	...
    }
    inline void sync()
    {
    	...
    }

P611 嵌套类

  • 嵌套类声明的位置决定了类的作用域和可见性。

P616 异常

如果yx的负值,下面程序将发生错误:

	20*x*y / (x+y)

为了使程序能够捕捉异常,有以下处理方法。

调用abort()

	double hmean(double a, double b)
    {
    	if(a == -b)
        {
        	std::cout << "untenable arguments to hmean()\n";
            std::abort();
        }
        return 2.0 * a * b / (a + b);
    }
  • abort()函数的典型实现是向标准错误流发送abnormal program termination(程序异常终止),然后终止程序。

返回错误码

	bool hmean(double a, double b, double * ans)
    {
    	if(a == -b)
        {
        	*ans = DBL_MAX;
            return false;
        }
        else
        {
       	 	*ans = 2.0 * a * b / (a + b);
            return true;
        }
    }	
  • 函数返回一个错误码,通过检查错误码判断是否存在异常,而把结果存放在一个结果指针里。
  • 另一种方法是使用一个全局变量,可以在出现问题时将该全局变量设置为特定的值。

异常机制

	double hmean(double a, double b)
    {
    	if(a == -b)
       		throw "bad hmean() arguments: a = -b not allowed"
        return 2.0 * a * b / (a + b);
    }	
    
    //在main中
    try{
    	z = hmean(x, y);
    }
    catch (const chat * s)
    {
    	std::cout << s << std::endl;
        srd::cout << "enter a new pair of numbers:"
        continue;
    }
  • 在出现错误的地方使用throw 发错错误信息。程序将向前找到合适的catch
  • catch(...)表示可捕获所有的异常。
  • 可用throw传递类作为异常类型。
  • 栈解退:假设try块没有直接调用引发异常的函数,而是调用了对引发异常的函数进行调用的函数,则程序流程从引发异常的函数跳到包含try块和处理程序的函数。
  • catch后面经常将引用作为参数,因为基类引用可以执行派生类对象。
	class bad_1{};
    class bad_2 : public bad_1{};
    class bad_3 : public bad_2{};
    ...
    void duper()
    {
    	...
        if(oh_no)
        	throw bad_1();
        if(rats)
        	throw bad_2();
        if(drat)
        	throw bad_3();
    }
    ...
    try{
    	duper();
    }
    catch(bad_3 & be) {}
    catch(bad_2 & be) {}
    catch(bad_1 & be) {}
  • 如果将bad_1 &处理程序放在最前面,它将捕获异常bad_1 &bad_2 &bad_3 &
  • 如果一个异常类继承层次结构,应该这样排列catch块:将捕获位于层次结构最下面的异常类的catch语句放在最前面,将捕获基类异常的catch放在最后面。
  • new分配内存失败后,默认抛出bad_allow异常,所以不需要用throw抛出异常。

类型转换运算符

dynamic_cast运算符

  • 该运算符的用途是,使得能够在类层次结构中进行向上转换。
	pl = dynamic_cast<Low *> ph;
    //当ph指针指向的类是pl指针指向类的派生类时,上式才有效,否则pl为空指针。

const_cast运算符

  • 有时候需要这样一个值,它在大多数时候是常量,而有时又是可以修改的。在这种情况下,可以将这个值声明为const,并在需要修改它的时候,使用const_cast
	const High * pbar = &bar;
    High * pb = const_cast<High *> (pbar); //valid

static_cast运算符

	High bar;
    Low blow;
    High * pb = static_cast<High *> (&blow);
    Low * pl = static_cast<Low *> (&bar);
  • 仅当两个类型可以显式或者隐式转换的时候,上式才有效,当该运算符用于在两个不相关的类中间进行转换的时候,会出错。

P652 reinterpret_cast运算符

P667 智能指针模板类

  • 有三个智能指针模板:auto_ptr(已摒弃)、unique_ptrshare_ptr
  • 常规指针必须在指针失效前 delete,智能指针将在类对象的析构函数中添加 delate,这样在类对象失效前自动删除,不会导致内存泄漏。

智能指针的注意事项

所有智能指针都有一个 explicit函数。

	share_ptr<double> pd;
    double * p_reg = new double;
    pd = p_reg;   // not allowed
    pd = share_ptr<double>(p_reg); //allowed (explicit conversion)

如果两个指针指向同一个 string对象,那么程序可能将删除同一个对象两次。避免这种问题,方法有多种。

  • 定义赋值运算符,这样两个指针将指向不同的对象,其中的一个对象是另一个对象的副本。
  • **建立所有权概念 **,对于特定的对象只能有一个智能指针可拥有它,让赋值操作转让所有权。这是 auto_ptr unique_qtr的策略,但后者的策略更严格。
  • 创建智能更高的指针,跟踪引用特定对象的指针指针数。这称为** 引用计数**。仅当最后一个指针过期时,才调用 delete。这是 share_ptr采用的策略。

unique_ptr为何优于auto_ptr

	auto_ptr<string> p1(new string("auto"));
    auto_ptr<string> p2;
    p2 = p1 ;

上面的程序中,将 p1 的所有权转让给 p2。但是当程序访问 p1 的时候,可能会出现问题。** 但是当两个指针是unique_ptr时,上面程序将在编译阶段报错。**

  • 总之,程序试图将一个 unique_ptr赋给另一个时,如果源 unique_ptr是一个临时右值,编译器允许这样做;如果源unique_ptr 将存在一段时间,编译器将禁止这样做。
  • 使用 new分配内存时,才能使用 auto_ptr share_ptr,使用 new[]分配内存时,不能使用它们,而是用 unique_ptr

选择智能指针

  • 如果程序要使用多个指向同一个对象的指针,应选择 share_ptr,否则应该选择 unique_ptr

P674 标准模板库

 STL提供了一组容器、迭代器、函数对象和算法的模板。

  • 容器是一个与数组类似的单元,可以存储若干个值。
  • 迭代器能够用来遍历容器的对象,和指针类似,是广义指针。
  • 函数对象是类似于函数的对象。

容器操作函数

  • size()函数 ——返回容器中元素数目。
  • **swap()函数 **——交换两个容器的内容。
  • begin()函数——返回一个指向容器中第一个元素的迭代器。
  • **end()函数 **——返回一个表示超过容器尾的迭代器。
  • **push_back()函数 **——将元素添加到容器的末尾。
  • **erase()函数 **——删除容器中给定区间的元素。他接受两个迭代器参数。
  • **insert()函数 **——他接受三个迭代器参数,第一个参数指定了新元素插入的位置,第二个和第三个参数定义了被插入的区间。
  • for_each()函数
	vecter<Review> :: iterator pr;
    for(pr = book.begin(); pr != book.end(); pr++)
    	ShowReview(*pr);
     //same as 
     for_each(book.begin(), book.end(), ShowReview);
     //这样可以避免使用迭代变量
  • **random_shuffle()函数 **——接收两个迭代器参数,随机排列指定区间的所有元素。
  • **sort()函数 **——排序函数
	bool WorseThan(const Review & r1, const Review & r2)
    {
    	if(r1.rating < r2.rating)
        	return true;
        else
        	return false;
    }
    //按照rating升序排序。
    sort(book.begin(), book.end(), WorseThan);
    

什么是迭代器?

它是一个广义指针,通过将指针广义化为迭代器,让STL能够为各种不用的容器类(包括那些简单指针无法处理的类)提供统一的接口。

  • 声明一个迭代器。
	vecter<double> score;
	vecter<double> :: iterator pd = score.begin; 

基于范围的for循环

	double prices[5] = {4.99, 10.99, 6.87, 7.99, 8.49};
    for(double x : prices)
    	cout<< x << std::endl;
  • 在这种for循环中,循环体使用指定的变量依次访问容器中的每个元素。·

P684 泛型编程

  • 为区分++运算符的前缀版本和后缀版本, C++operator++ 作为前缀版本,将operatpr++(int)作为后缀版本,其中的参数永远也不会用到,所以不必指定其名称。

end