C++ Primer Plus 学习笔记

头文件

#include<iostream>  //输入输出函数
using namespace std;
#include<string> //位于命名空间std
#include <vector> //模板类vector
#include<valarray> //支持数组的数值运算
#include <array>
#include <fstream> //文件输入输出函数
#include <new>
#include <exception> //异常类
#include <stdexcept> //异常类 logic_errorruntime_error
#include <typeinfo>  //exception引发的异常
#include <memory>    //包含智能指针
#include <set>  //包含set multiset 关联容器
#include <map> //包含map multimap关联容器
#include <functional> //定义了多个模板类函数对象
#include <algorithm> //算法库
#include <numeric> //算法库,专门用于数值数据
#include <initializer_list> //将STL容器初始化为一系列值
#include <complex> //支持复数数值运算
#include <sstream> //定义了ostringstream和istringstream类,用于内核格式化

C库函数
#include<climits> //C库函数,包含了关于整型限制的信息。具体地说,它定义了表示各种限制的符号名称。
#include<cfloat> //C库函数,包含了关于浮点型限制的信息。
#include<cstring>
#include<ctime> //关于系统时间的一些设定。clock()CLICKS_PER_SEC
#include<climits> 
#include<cctype> //判断输入内容是不是某类型 
#include<cstdlib>	

2.开始学习C++

oop 面向对象

c++对大小写敏感

C语言头文件转换为C++头文件时,去掉后缀,在文件名前加c。eg.math.c——cmath

名称空间

旨在将多个厂商现有代码组合起来。

mic::wanda();
pis::wanda();

c++标准组件被放置在名称空间std中。

std::cout<<"hello";
std::cin<<"hello";

using namespace用于简化程序

using namespace std;
cout<<"hello";
using namespace std::cout;//开放名称空间中指定名称

cout

cout是一个预定义的对象。在打印前,cont将需打印的内容转化为一个字符串。

对象的长处:不用了解对象的内部情况,就可以使用它,只需要知道他的接口。

输出时一个流,即从程序流出的一系列字符。

<<插入运算符

cout<<"hello"; //把字符串插入到输出流中

控制符endl

重启一行

诸如endl等对于cout有特殊含义的特殊符号被称为控制符

cout<<endl;

cin

信息从cin流入变量,从输入流中抽取字符,并将键入的一系列字符转换为接受信息的变量能够接受的形式。

cin 使用空白(空格、制表符和换行符)来确定字符串的结束位置,这意味着 cin 在获取字符数组输入时只读取一个单词 。读取该单词后, cin 将该字符串放到数组中,并自动在结尾添加空字符。

cin>>carrots;

3.处理数据

sizeof 运算符

sizeof(int);
sizeof my_int; //说明不是函数

初始化

int a=3;
int a(3);
int a{3};

进制控制符

dee hex oct,分别用于指示 cout 十进制、十六进制和八进制格式显示整数。

int chest= 42; 
int waist= 42;
int inseam= 42; 

cout << chest << endl; 
cout << hex;  // manipulator for changing number base 
cout << waist << endl; 
cout << oct; // manipulator for changing number base 
cout << inseam << endl;

成员函数 cout.put()

成员函数归类所有,描述了操纵类数据的方法。

例如类 ostream 有一个 put( )成员函数,用来输出字符。 只能通过类的特定对象(例如这里的 cout 对象)来使用成员函数。

要通过对象(如 cout 使用成员函数,必须用句点将对象名和函数名称 put( )连接起来。句点被称为成员运算符。

成员函数 cout.put() 显示一个字符,意思是通过类对象 cout 来使用函数 put( )。

const 限定符

符号名称指出了常量表示的内容。

const type name= value;

在C++中,const 比 #defien 好。首先,它能够明确指定类型。其次,可以使用 C++ 的作用域规则将定义限制在特定的函数或文件中。 第三,可以将 const 用于更复杂的类型,如数组和结构。

ostrearm setf( )

这种调用迫使输出使用定点表示法,以便更好地了解精度。它防止程序把较大的值切换为E表示法并使程序显示到小数点后6位 。参数 ios base: :fixed和ios_base: :floatfield 是通过包含 iostream 来提供的常量。

cout.setf(ios base: :fixed, ios_base: :floatfield);

类型转换

以{ }方式初始化时进行的转换

C++11 将使用大括号的初始化称为列表初始化 ,因为这种初始化常用于给复杂的数据类型提供值列表。

不允许缩窄,在不同 的整型之间转换或将整型转换为浮点型可能被允许。

大括号内不允许是变量,会导致类型不确定。

强制类型转换
(typeName) value // converts value to typeName type C
typeName (value) // converts value typeName type C++

还设置了4个强制类型转换运算符,在这四个运算符中, static_cast<>可用于将值从一种数值类型转换为另一种数值类型。运算符 static_cast<> 比传统强制类型转换更严格。

staic_cast<typeName> (value) // converts value to typeName type
auto声明

让编译器能够根据初始值的类型推断变量的类型

4.复合类型

数组

如果只对数组的一部分进行初始化,则编译器将把其他元素设置为0 。

将使用大括号的初始化(列表初始化)初始化数组时,可省略等号(=);可不在大括号内包含任何东西,这将把所有元素都设置为零;列表初始化禁止缩窄转换。

面向行的输入: getline()

getline( )函数读取整行,它使用通过回车键输入的换行符来确定输入结尾。

cin.getline (name,20);
getline (cin, str); 

读取换行符,并用空字符来替换换行符。

cin.getline中name需要是字符数组类型,不能用string。

getline中name为string。

面向行的输入: get()

cin.get (name, ArSize);
cin.get(); // read newline 
cin.get (dessert, Arsize);

但 get()并不再读取并丢弃换行符,而是将其留在输入队列中。

使用不带任何参数的 cin.get( )调用可读取下一个字符(即使是换行符),因此可以用它来处理换行符

cin.get(name,ArSize).get();
cin.getline(namel,ArSize).getline(name2,ArSize);

另一种使用 get( 的方式是将两个类成员函数拼接起来(合并)。之所以可以这样做,是由于 cin.get (name, ArSize) 返回一个 cin 对象,该对象随后将被用来调用 get()函数。

getline( )使用起来简单一些,但 get( )使得检查错误简单些

string类

隐藏了字符串的数组属性。

类设计让程序能够自动处理 string 的大小。

string strl; 
string str2 = "panher";

不能将一个数组赋给另一个数组,但可以将一个 string 对象赋给另一个 string 对象。

运算符+将两个 string 对象合并起来,还可以使用运算符+=将字符串附加到 string 对象的末尾。

int lenl = strl.size(); 
int len2 = strlen(charrl);

str 是一个对象,而 size() 是一个类方法。方法是一个函数,只能通过其所属类的对象进行调用。

cin.getline (charr, 20); 
getline (cin, str); 

没有使用句点表示法,这表明这个 getline( )不是类方法。它将 cin 作为参数,指出到哪里去查找输入。没有指出字符串长度的参数,因为 string 对象将根据字符串的长度自动调整自己的大小。

结构

C++允许在声明结构变量时省略关键字 struct。

访问类成员函数(如 cin.getline()) 的方式是从访问结构成员变量(vincent.price) 的方式衍生而来的。

可以使用赋值运算符(=)将结构赋给另一个同类型的结构。

C++结构除了成员变量之外,还可以有成员函数。

C++也允许指定占用特定位数的结构成员。字段的类型应为整型或枚举。

共用体(联合体)

(union) 是一种数据格式,它能够存储不同的数据类型,但只能同时存储其中的一种类型。

共用体的长度为其最大成员的长度。

匿名共用体 (anonymous union) 没有名称,其成员将成为位于相同地址处的变量。

枚举

enum 工具提供了另一种创建符号常量的方式。

在不进行强制类型转换的情况下,只能将定义枚举时使用的枚举量赋给这种枚举的变量。

对于枚举,只定义了赋值运算符。具体地说,没有为枚举定义算术运算。

如果 int 值是有效的,则可以通过强制类型转换,将它赋给枚举变量。取值范围的定义如下。首先,要找出上限,需要知道枚举量的最大值。找到大千这个最大值的、最小的2的幂,将它减去 1 得到的便是取值范围的上限。要计算下限,需要知道枚举量的最小值。如果它不小于0 ,则取值范围的下限为 0; 否则,采用与寻找上限方式相同的方式,但加上负号。

如果打算只使用常量,而不创建枚举类型的变量,则可以省略枚举类型的名称。

可以使用赋值运算符来显式地设置枚举量的值。后面没有被初始化的枚举量的值将比其前面的枚举量大 1。可以创建多个值相同的枚举量。

指针

new

指针可以在运行阶段分配未命名的内存以存储值。(C语言中,可以用库函数 malloc( )来分配内存)。

int* pn = new int;
typeName * pointer_name = new typeName;
//为一个数据对象(可以是结构,也可以是基本类型)获得并指定分配内存的基本形式

变量的值都存储在被称为栈 (stack) 的内存区域中,而 new 从被称为堆(heap) 或自由存储区的内存区域分配内存。

delete

使得在使用完内存后,能够将其归还给内存池。这将释放 ps 指向的内存,但不会删除指针 ps 本身。

在编译时给数组分配内存被称为静态联编 (static binding) ,意味着数组是在编译时加入到程序中的。但使用 new 时,如果在运行阶段需要数组,则创建它;如果不需要,则不创建。还可以在程序运行时选择数组的长度。这被称为动态联编( dynamic binding) ,意味着数组是在程序运行时创建的。这种数组叫作动态数组 (dynamic array) 。

new 动态数组
int* psome = new int [10];
delete [] psome;

对于第一个元素,可以使用 psome[0] ;第2个元素,可以使用 psome[1] .

p3 = p3 + 1; // okay for pointers, wrong for array names

数组名和指针之间的根本差别:

不能修改数组名的值。但指针是变量,因此可以修改它的值。(将它减1后,指针将指向原来的值,这样程序便可以给 delete[ ]提供正确的地址)

对数组应用 sizeof 运算符得到的是数组的长度,而对指针应用 sizeof 得到的是指针的长度,即使指针指向的是一个数组。

数组名被解释为其第一个元素的地址,而对数组名应用地址运算符时,得到的是整个数组的地址:

short tell[lO]; // tell an array of 20 bytes ;
cout <<tell<< endl; // displays &tell [O] ;
cout <<&tell << endl; // displays address of whole array;
指针和字符串

cout 对象认为char 的地址是字符串的地址,因此它打印该地址处的字符,然后继续打印后面的字符,直到遇到空字符(\0) 为止。总之,如果给 cout 提供一个字符的地址,则它将从该字符开始打印,直到遇到空字符为止。

字符串字面值是常量,这就是为什么代码在声明中使用关键字 const 的原因,以这种方式使用 const意味着可以用指针来访问字符串,但不能修改它。

如果要显示的是字符串的地址,则必须将这种指针强制转换为另一种指针类型,如 int*。

需要分配内存来存储该字符串,这可以通过声明另一个数组或使用 new 来完成。

ps = new char[strlen(animal) + l]; // get new storage
动态结构
inflatable* ps = new inflatable;

箭头成员运算符(->)用于指针表示结构体成员。

如果 ps 是指向结构的指针,则*ps 就是被指向的值——结构本身。由于*ps 个结构,因此 (*ps). price 是该结构的 price 成员。

自动存储:局部变量,其作用域为包含它的代码块。常存储在栈中,后进先出。

静态存储:静态存储是整个程序执行期间都存在的存储方式 使变量成为静态的方式有两种: 是在函数外面定义它;另一种是在 明变量时使用关键字 static。

动态存储:new和delete 管理了一个内存池(称为自由存储空间或堆)。

数组的替代品
模板类 vector

是一种动态数组,包含头文件 vector,可以将 vi 的初始长度设置为零。时自动调整长度,因此可以将 vi 的初始长度设置为零。 但存储在堆。

vector <typeName> vec(n_elem) //参数 elem 可以是整型常量,也可以是整型变量。
vector<double> vd (n) ;
模板类 array
array<typeName, n_elem> arr; //与创建 vecto 对象不同的是, n_elem 不能是变量。

存储在栈。

使用数组如何禁止非法索引?

a2.at (l) = 2.3 ; // assign 2.3 to a2[1]

于,使用 at()时,将在运行期间捕获非法索引,而程序默认将中断。但运行时间更长。另外,这些类还让您能够降低意外超界错误的概率。例如,它们包含成员函数 begin()和 end( ) ,让您能够确定边界。

5.循环和关系表达式

cout.setf (ios::boolalpha); //函数调用设置了一个标记,该标记命令 cout 显示 true false,

对象 string类的 size( )获得字符串中的字符数

for (in = word.size() - l; i >= O; i - - )

前缀递增、前缀递减和解除引用运算符的优先级相同,以从右到左的方式进行结合。 后缀递增和后缀递减的优先级相同,但比前缀运算符的优先级高,这两个运算符以从左到右的方式进行结合。

比较string类字符串

类函数重载了运算符,因此可以直接比较。

strcmp(word,"mate")!=0; //C
word !="mate"; //C++

延时循环

clock()返回程序开始执行后所用的系统时间。单位不一定是秒,类型也不确定。(ctime)

CLICKS_PER_SEC 该常量等于每秒钟包含的系统时间单位数。

clock_t 返回类型的别名。

typedef 用于定义一系列变量。

基于范围的for循环

int price[5] = {1,2,3,4,5};
for(int x: price)
    cout << x << endl;

若要修改数组的元素,需要使用&,表明x是一个引用变量。

for(int &x: price)
    x = x * 0.8;

结合使用基于范围的for循环和初始化列表。

for(int x: {1,2,3,4,5})
    cout << x << endl;

cin 3种模式

cin 忽略空格和换行符

cin.get(char)逐个字符读取输入的程序需要检查每个字符,包括空格、制表符、换行符。

cin.get(char*,int) 输入字符串,关注\n。

cin.get() 用来获取输入的第一个字符

函数重载:允许创建多个同名函数,条件是参数列表不同。

istream类提供了一个可以将istream对象转换为bool值的方法。当cin出现在需要bool值得地方时,该转换函数被调用。

while(cin){
    
}

cin.clear()

用于重置错误输入标记,同时重置文件尾。

简单文件输入/输出

写入
#include<fstream>
ofstream outFile; //创建一个ofstream对象outFile
outFile.open("carinfo.txt"); //关联对象和TXT文件
outFile << "write" <<endl; //像cout一样使用
outFile.close();

open()再次运行时,将其长度截断到0.

读取
#include<fstream>
ifstream inFile; //创建一个ofstream对象outFile
double value;
inFile.open("carinfo.txt"); //关联对象和TXT文件
inFile >>value ; //像cout一样使用
inFile.close();

检查文件啊是否被成功打开的首先方法是使用方法is_open()。也可以用较老的good()方法来代替。

eof()用来判断是否到达EOF;fail()检查EOF和类型不匹配。good()指出最后一次读取输入的操作是否成功。

8.函数探幽

内联函数

在函数声明前加上关键字inline,通常省略原型,将整个定义放在本该提供原型的地方。

用于提高程序运行速度。如果代码执行时间很短,则内联调用就可以节省非内联调用使用的大部分时间。适用于简单且频繁调用的函数。

不同于C中的宏定义通过文本替换实现,内敛函数是按值传递参数的。

引用

引用变量,已定义的变量的别名。

常用作 函数的形参。函数将使用原始数据,而不是其副本。

创建引用变量

int rats;
int & rodents = rats; //&是类型标识符的一部分,int&指的是指向int的引用。

rat 和 rodents 的值和地址都相同,接近于const指针,只能指向该变量。

int * const pr = &rats; //不能指向别处

将引用作为函数参数

按引用传递,允许被调用函数能够访问调用函数中的变量。

int a = 10;
int b = 20;
swapr(a,b);
void swapr(int &ra,int &rb){
} //类似于向函数传递指针,通过改变ra、rb可以直接改变a和b的值。

临时变量、引用和const

若想不修改参数,又使用引用,应使用引用常量。若代码修改了值,则生成错误信息。

仅当参数为const引用时,如果实参和引用参数不匹配,将生成临时变量,行为类似于按值传递。

int a = 10;
swapr(a);
swapr(5);
swapr(a+5);
void swapr(const int &ra){
} //都合法,但若没有const,则不合法

引用用于结构

传统返回机制将返回值复制到一个临时位置,调用程序将使用这个值。而返回引用直接把返回值复制到调用函数,其效率更高。

应避免返回函数种植时不再存在得内存单元引用。最简单的方式是返回一个作为参数传递给函数的引用。

或者使用new来分配新的存储空间:

const free_throws & clone(free_throws & ft){
    free_throws *pt;
    *pt = ft;
    return *pt;
} //但存在问题,clone隐藏了对new的调用,使得容易忘记使用delete释放。

引用用于类对象

用法与用于结构类似,注意const用法以及作为返回值的引用属性。

对象、继承和引用

继承使得能够将特性从一个类传递给另一个类。

eg. ostream是基类,ofstream是派生类,ofstream可以使用基类的特性。

基类引用可以指向派生类对象,而无需进行强制类型转换。

ofstream fout;
file(fout,objective,eps,LIMIT);
file(cout,objective,eps,LIMIT);
void file(ostream &os, double fo, const double fe[], int n){
    os << "objecyive:" << fo << endl;
}

默认参数

默认参数指的是当函数调用中省略了实参时自动使用的一个值。

设置默认参数需要通过函数原型。

带参数列表的函数,必须从右向左添加默认值。

左值右值

能取地址的是左值,否则是右值。

右值引用

int &&a = 10;

将左值赋值给conat左值引用,将直接复制;但将右值赋值给const左值引用,左值引用指向右值的一个临时副本。

如果没有const,左值引用不能指向右值。

函数重载

函数多态,能够使用多个同名的函数。常用于完成相同的工作,但使用不同的参数列表。

参数列表,也称函数特征标。

编译器检查函数特征标时将类型引用和类型本省视为同一个特征标。

返回值可以不同,但特征标必须不同。

名称修饰

编译器在编译时,会根据函数原型中指定的形参类型对每个函数名进行加密。

函数模板

通用的函数描述,使用泛型来定义函数。通过将类型作为参数传递给模板,可使编译器生成该类型的函数。

template <typename AnyType> //可以用class代替typename
void Swap(AnyType &a, AnyType &b)
{
    AnyType temp;
    temp = a;
    a = b;
    b = temp;
}

模板并不创建任何函数,只是告诉编译器如何定义函数。

重载模板

可以像重载常规函数定义那样重载模板定义。

并非所有的模板参数都必须是模板参数类型。

显式具体化

具体化函数定义,当编译器找到与函数调用匹配的具体化定义时,将使用该定义,而不再寻找模板。

struct job{
}
template <> void Swap<job>(job &j1, job &j2){
}
实例化和具体化

显式实例化

template void Swap<int>(int ,int);

试图在同一个文件中使用同一类型的显式实例和显式具体化将出错。

Add<double>(x,m); //使用函数时强制为double类型实例化
编译器选用的函数版本

**重载解析:**选择函数调用使用哪一个函数定义的策略的过程。

编译器查看为使函数调用参数与可行的候选函数的参数匹配所需要进行的转换。

1.完全匹配,但常规函数优先于模板。

2.提升转换(例如,char和short自动转换为int,float自动转换为double)。

3.标准转换(例如,int转换为char,long转换为double)。

4.用户定义的转换,如类声明中定义的转换。

多个完全匹配的原型,指向非const数据的指针和引用优先与非const指针和引用参数匹配。但这种区别只适用于指针和引用指向的数据。

较具体的模板函数优先。

执行时转换较少的模板优先。

自定义选择
template<class T>
T lesser(T a, T b){        //1
}
int lesser (int a, int b){ //2
}
int m,n;
double x,y;
lesser(m,n);     //2
lesser(x,y);     //1
lesser<>(m,n);   //1 指定使用模板函数
lesser<int>(x,y);//1
关键字decltype
int a;
decltype(a) b;
decltype(fun(a)) b;//与返回值类型相同
decltype((a)) b;//b int& 引用
后置返回类型
auto h(int a,int b)->double;
template<class T1,class T2>
auto gt(T1 x,T2 y)->decltype(x+y){
}

9.内存模型和名称空间

单独编译

头文件中包含的内容:

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

避免同一个文件包含同一个头文件多次:

#ifndef __COORDIN_H_
#define __COORDIN_H_
...
#endif

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

自动存储持续性
静态存储持续性

函数定义外定义的变量和使用关键字static定义的变量

int a = 10; //外部链接性
static b = 2; //内部链接性
{
    static c = 4; //无链接性
}

未被初始化的静态变量所有位都被设为0——零初始化

动态初始化:必须调用函数atan()后才能完成初始化,这要等到该函数被链接且程序执行时。

const double pi = 4.0 * atan(1.0);
线程存储持续性

关键字thread_local定义的变量生命周期与所属线程一样长

动态存储持续性

new分配的内存,直到使用delete释放或程序停止。

静态外部

引用声明使用关键字extern,不进行初始化

extern int a;

作用域解析运算符 :: ,放在变量名前面,表示使用变量的全局版本

静态内部

静态无连接

static限定符用于在代码块中定义的变量,该变量只在该代码块中可用,但它在该代码块不处于活动状态时仍然存在。如果初始了静态局部变量,则程序只在启动时进行一次初始化。

说明符和限定符

volatile

即使代码没有对内存单元进行修改,其值也可能变化。

mutable

即使结构或类变量为canst,其某个成员也可以被修饰。

const

默认情况下全局变量的链接性为外部的,但const全局变量的链接性为内部的。

若希望某个常量的链接性为外部的,则可以使用extern关键字来覆盖默认的内部链接性。

语言链接性

在C++程序中使用C库中预编译的函数,可以用函数原型来指出要使用的约定

extern "C" void spiff(int);
extern void spiff(int);
extern "C++" void spiff(int);

定位new

指定new要使用的位置

char buffer[50];
p2=new (buffer) chaff;
p4=new (buffer+5*sizeof(double)) chaff;
//以buffer为起始地址创建内存,不能用delete释放,因为这本身属于静态内存

名称空间

关键字namespace创建命名空间

namespace jack{
    double pail;
    void fetch();
    struct well{};
}

名称空间可以是全局的,也可以位于另一个名称空间中,但不能位于代码块中。

全局变量被描述为位于全局名称空间中。

名称空间是开放的,可以把名称加入已有的名称空间中。

可以将名称空间进行嵌套。

可以给名称空间创建别名。

namespace mvft = my_very_favorite_things;
namespace MEF = MYTH::ELE::FIRE; //用于简化对嵌套名称的使用。

未命名的名称空间:只能在所属文件中使用,提供了链接性为内部的静态变量的替代品。

10.对象和类

OOP特性:1.抽象;2.封装和数据隐藏;3.多态;4.继承;5.代码的可重用性。

描述对象所需的数据以及描述用户与数据的交互所需的操作。确定如何实现接口和数据存储。

抽象和类

类是一种将抽象转换为用户定义类型的C++工具。将数据表示和操纵数据的方法组合成一个整洁的包。

类规范又两个部分组成:1.类声明;2.类方法定义

通常将接口(类定义)放在头文件中,并将实现(类方法的代码)放在源代码文件中。

class 定义了一个类设计

private 私有部分 通常存储数据项,或私有成员函数来处理不属于公有接口的实现部分。

public 公有部分 类接口的成员函数

要存储的数据以类数据成员的形式出现。

防止程序直接访问数据被称为数据隐藏。

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

**类和结构:**结构体的默认访问类型为public,而类的默认访问类型为private。

**内联方法:**其定义位于类声明中的函数都自动成为内联函数。也可以在类声明之外定义成员函数,使用inline限定符即可。

创建对象:

Stock kate;
kate.show();

每个新对象都有自己的存储空间,但同一个类型的所有对象共享同一组类方法。

构造函数

类构造函数专门用于构造新对象、将值赋给它们的数据成员。构造函数没有返回值,但没有被声明为void类型。

构造函数的参数表示的不是类成员,而是赋予类成员的值。参数名不能与类成员相同。

无法使用对象来调用构造函数,因为在构造函数构造出对象之前,对象是不存在的。

默认构造函数

如果没有提供任何构造函数,则自动提供默认构造函数。

析构函数

对象过期时,程序会自动调用一个特殊的成员函数——析构函数。

析构函数在类名前加~。

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

const成员函数
void Stock::show() const;
void show() const;

尽可能地将其设置为const。

this指针

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

对象数组

如果类包含多个构造函数,可以对不同元素使用不同构造函数。

类作用域

类中定义的名称为整个类,在类外不可知。要调用公有函数对象,必须通过对象。在类声明或成员函数定义中,可以使用未修饰的成员名称。

作用域为类的常量

1.可以用枚举为整型常量提供作用域为类的符号名称。这并不会创建类数据成员。

2.使用关键字static创建常量。该常量和其他静态变量存储在一起,而不是存储在对象内。

enum {Month = 12};
static const int Month = 12;

抽象数据类型

ADT以通用的方式描述数据类型,没有引入语言或实现细节。类特别适合描述ADT,公有成员函数接口提供了ADT描述的服务。

11.使用类

运算符重载

operator op(argument-list);
Time Time::Sum(const Time & t) const;
Time operator+(const Time & t) const;
Time a,b,sum;
sum = a.Sum(const Time & b);
sum = a.operator+(const Time & b); //函数表示法
sum=a+b; //运算符表示法

在运算符表示法中,运算符左侧的对象是调用对象,运算符右侧的对象是作为参数被传递的对象。

有些运算符只能通过成员函数进行重载:= ()[ ] ->

这是因为这些运算符有可能产生调用构造函数,在成员函数内定义时优先使用自定义运算符重载,但是在成员函数外定义可能会引起混乱。

友元

C++控制对类对象私有部分的访问。通常,公有类方法提供唯一的访问途径,但有时候这种限制太严格。这种情况下,C++提供了另一种形式的访问权限:友元。

友元有3种:

友元函数;友元类;友元成员函数。

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

友元函数

创建友元

将原型放在类声明中,并加关键字friend

friend Time operator*(double m,const Time & t);

虽然函数在类声明中声明,但它不是成员函数,因此不能使用成员运算符来调用。

虽然不是成员函数,但与成员函数的访问权限相同。

函数定义,不能使用类成员限定符。不要在定义中使用关键字friend。

Time operator*(double m,const Time & t){
}

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

类的自动转换

当类构造函数接受的参数只有一个时,可以使用自动类型转换进行赋值

Stonewt myCat;
myCat = 19.6;
关键字explicit

但可以关闭这项特性。C++新建关键字explicit用于关闭这项特性。

explicit Stonewt (double lbs);
转换函数

转换函数时用户所定义的强制类型转换。

  • 转换函数必须是类方法
  • 不能指定返回类型,但必须返回转换后的值
  • 不能有参数
double pounds;
oprator double() const;//声明
Stonewt::oprator double() const{
    return pounds;
}                      //定义

double a = pounds;
cout<<(double)pounds; //引用

关键字explicit用于关闭隐式转换。

12.类和动态内存分配

动态内存和类

C++使用new和delete运算符来动态控制内存,在这种情况下,析构函数是必不可少的。

静态类成员

无论创建多少对象,程序都只创建了一个静态类变量副本。(例如,可以用来记录创建的对象数目)

不能在类声明中初始化静态变量成员,因为声明描述了如何分配内存,但不分配内存。

不能在头文件中初始化,防止多次引用同一头文件重复初始化。

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

静态类成员函数

可以将成员函数声明为静态的(函数声明必须包含关键字static,但如果函数定义是独立的,则其中不能包含关键字 static),这样做有两个重要的后果。

首先,不能通过对象调用静态成员函数;实际上,静态成员函数甚至不能使用this指针。如果静态成员函数是在公有部分声明的,则可以使用类名和作用域解析运算符来调用它。

其次,由于静态成员函数不与特定的对象相关联,因此只能使用静态数据成员

**函数调用含有new的类对象时:**应使用对象的引用,而不是按值传递(这会导致析构函数被调用,但按值传递引发的复制构造函数内并没有new)。

StringBad(const StringBad &);//自动生成此构造函数,相当于把一个类对象赋给另一个对象
特殊成员函数

隐式复制构造函数和隐式赋值运算符会导致问题。

复制构造函数

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

Class_name(const Class_name &);

它接受一个指向类对象的常量引用作为参数。

每当程序生成对象副本时,编译器都将使用复制构造函数。即函数按值传递对象或函数返回对象时,编译器生成临时对象时

应尽可能地按引用传递对象,可以节省调用构造函数的时间及存储新对象的空间

解决类设计中重复释放内存问题的方法是进行深度复制,也就是说复制构造函数应当复制字符串并将副本的地址赋予str成员,而不仅仅是复制字符串地址。

赋值运算符

类之间的赋值是通过重载=实现的,而默认的赋值运算符也可能导致问题,可提供赋值运算符定义。

由于目标对象可能引用了以前分配的数据,所以函数应使用delete[ ]来释放这些数据。

函数应避免将对象赋值给自身;否则,给对象重新赋值前,释放内存操作可能删除对象的内容。

函数返回一个调用对象的引用(可进行连续赋值)。

构造函数中使用new

  • new和delete必须兼容
  • 所有构造函数的new初始化都要和唯一的析构函数兼容。
  • 应定义复制构造函数,通过深度复制将一个对象初始化为另一个对象。复制数据,而不仅仅是数据的地址。
  • 可以在一个构造函数中使用new初始化指针,而在另一个构造函数中将指针初始化为空。因为delete可以用于空指针。

有关返回对象

返回指向const对象的引用

const引用常见原因是旨在提高效率

返回都西昂将调用复制构造函数,而返回引用不会

引用指向的对象应在调用函数执行时存在。

返回指向非const对象的引用

重载赋值运算符以及重载与cout一起使用的<<运算符。

前者旨在提高效率,后者必须这么做(因为ostream没有公有的复制构造函数)。

返回对象

被调函数的局部变量不应按引用传递。

被重载的算数运算符属于这一类。

返回const对象

使用指向对象的指针

String * glamour;
String * first = &saying;//将调用相应的类构造函数初始化创建的对象

定位new

  • 使用定位new运算符来为对象分配内存,必须确保其析构函数被调用。
  • delete可与常规new配合使用,但不能与定位new配合使用。
  • 需要显式地为定位new创建地对象调用析构函数。
p1->~JustTesting();
  • 对于使用定位new运算符创建地对象,应以与创建顺序相反地顺序进行删除。因为晚创建的对象可能依赖于早创建的对象。

成员初始化列表

由逗号分隔的初始化列表组成,位于参数列表的右括号之后、函数体左括号之前。

Queue::Queue(int qs) : qsize(qs),front(NULL){}

这种方法不限于初始化常量,但只有构造函数可以使用这种初始化列表语法。

对于const类成员和被声明为引用的类成员,必须使用这种语法。因为它们都只能在被创建时初始化。

数据成员被初始化的顺序与它们出现在类声明中的顺序相同,与初始化器中的排列顺序无关。

13.类继承

类继承,能够从已有的类派生出新的类,而派生类继承了原有类(成为基类)的特征,包括方法。

  • 可以在已有类的基础上添加功能。
  • 可以给类添加数据。
  • 可以修改类方法的行为。

派生一个类

class RatedPlayer : piblic TableTennisPlayer{ 
unsigned int rating;
}

冒号指出RatedPlayer类的基类是TableTennisPlayer类。public指出TableTennisPlayer是一个公有基类,这被称为公有派生。派生类对象包含基类对象。使用公有派生,基类的公有成员将成为派生类的公有成员;基类的私有部分也会成为派生类的一部分,但只能通过基类的公有和保护方法访问

  • 派生类需要自己的构造函数。构造函数必须给新成员和继承的成员提供数据。
  • 派生类可以根据需要添加额外的数据成员和成员函数。
构造函数

派生类不能直接访问基类的私有成员,而必须通过基类方法进行访问。具体来说,派生类构造函数必须使用基类构造函数。基类对象应当在程序进入派生类构造函数之前被创建——成员初始化列表

RatedPlay::RatedPlayer(unsigned int r, const string & fn, const string & ln, bool ht) : TableTennisPlayer(fn, ln, ht){
    rating = r;
}

如果省略成员初始化列表,程序将使用默认的基类构造函数。

也可以对派生类成员使用成员初始化列表语法。

RatedPlay::RatedPlayer(unsigned int r, const string & fn, const string & ln, bool ht) : TableTennisPlayer(fn, ln, ht), rating(r){ }

先执行派生类的析构函数,在自动调用基类的析构函数。

派生类和基类的关系

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

基类指针可以在不进行显示类型转换的情况下指向派生类对象;基类引用可以在不进行显示类型转换的情况下引用派生类对象。(引用和指针类型与赋给的类型匹配,但这一规则对继承来说是例外。这种例外是单向的。)

基类指针或引用只能用于调用基类方法。

引用兼容性属性可以将基类对象初始化为派生类对象

RatedPlay olaf1(1840, "Taras","Boom","false");
TableTennisPlayer olaf2(olaf1); //采用隐式复制构造函数

将 olaf2 初始化为嵌套在 RatedPlay 对象 olaf1 中的 TableTennisPlayer 对象。

将派生类赋给基类对象。

RatedPlay olaf1(1840, "Taras","Boom","false");
TableTennisPlayer winner;
winner = olaf1; //采用隐式重载赋值运算符

将olaf1的基类部分复制给winner。

多态公有继承

有时希望同一个方法在派生类和基类中的行为是不同的。方法的行为应取决于调用该方法的对象。有两种机制可以用于实现多态公有继承:

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

声明函数前使用关键字 virtual 的方法称为虚方法。在函数定义中不用使用关键字。

virtual void Withdraw(double amt);

经常在基类中将派生类会重新定义的方法声明为虚方法。方法在基类中被声明为虚后,在派生类中自动为虚。在派生类中使用关键字virtual指明虚函数也是一个好方法。

基类声明一个虚析构函数是一种惯例,为确保释放派生对象时,按照正确的顺序调用析构函数。

  • 如果虚构函数不是虚的,则将只调用对应于指针类型的析构函数。这意味着只有基类的析构函数被调用。需要确保正确的析构函数序列被调用,也许派生类的析构函数中有一些特殊操作。

如果没有使用关键字virtual,程序将根据引用类型或指针类型选择方法;如果使用了virtual,程序将根据引用或指针指向的对象的类型来选择方法。

**应用:**可以创建指向基类的指针数组,但由于使用的是公有继承模型,可以使用一个数组来表示多种类型的对象。

静态联编和动态联编

将源代码中的函数调用解释为执行特定的函数代码块白称为函数名联编。

在编译过程中进行联编称为静态联编,又称为早期联编。

在程序运行中进行联编称为动态联编,又称为晚期联编。(选择正确的虚方法)

指针和引用类型的兼容性

动态联编与通过指针和引用调用方法相关,从某种程度上说,这是由继承控制的。

将派生类引用或指针转换为基类引用或指针被称为向上强制转换,这使公有继承不需要进行显示类型转换。向上强制转换是可传递的。

将基类指针或引用转换为派生类指针或引用称为向下强制转换。如果不适用显式类型转换,这是不允许的。

虚函数成员和动态联编

不需要动态联编:

  • 类不会用作基类;
  • 派生类不重新定义基类的任何方法。

虚函数的工作原理(P411)

虚函数注意事项
  • 构造函数不能是虚函数;
  • 析构函数应当是虚函数,除非类不用作基类(通常应给基类提供一个虚析构函数,即使它并不需要析构函数);
  • 友元不能是虚函数,因为它不是类成员;
  • 如果派生类位于派生链中,则将使用最新的虚函数版本,例外的情况是基类版本是隐藏的。

重新定义不会生成函数的两个重载版本,而是隐藏了原先的基类版本(无论参数列表是否相同)。

1.如果重新定义继承的方法,应确保与原来的原型完全相同,但如果返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针。这种特性被称为返回类型协变。

2。入伏哦基类声明被重载了,应在派生类中重新定义所有的基类版本。如果只定义一个版本,则其他版本被隐藏,派生类对象无法使用它们。如果不需要修改,新定义只需要调用基类版本。

访问控制protected

关键字protected:对于外界世界来说,保护成员与私有成员相似;对于派生类来说,保护成员的行为与公有成员相似。

抽象基类

抽象基类抽象出类的共性,将这些特性放到一个ABC中。通过纯虚函数提供未实现的函数。纯虚函数的结尾处为=0。

virtual double Area() const =0;

当类声明中包含纯虚函数时,则不能创建该类的对象。包含纯虚函数的类只能作基类。C++允许纯虚函数有定义

继承和动态内存分配

派生类不使用new

派生类的默认析构函数总是要进行一些操作:执行自身的代码后调用基类构造函数。所以不需要显式构造一些默认函数。

派生类使用new

必须为派生类定义显式析构函数、复制构造函数和赋值运算符。

定义赋值运算符时,一般需要使用基类的赋值运算符访问基类私有成员,通过函数表示法,可以使用作用域解析运算符。

baseDMA::operator=(hs);
友元继承

因为友元不是成员函数,所以不能使用作用域解析运算符指出要使用哪个函数。

访问基类私有成员时,需要使用强制类型转换将派生类转换为基类使用友元。

14.C++的代码重用

C++的一个主要目的是促进代码重用。公有继承是实现这种目标的机制之一。还有一种方法是这样一种类成员:本身是另一个类的对象。这种方法称为包含、组合、或层次化。另一种方法是:私有或保护继承,即新的类包含另一个类的对象(has-a)

包含对象成员的类

valarray类

valarray类用于处理数值(或具有类似特性的类),它支持诸如将数组中所有元素的值相加以及在数组中找出最大和最小的值等操作。

valarray是一个模板类,便于处理不同类型的数据。因此声明对象时,必须指定具体的数据类型。

valarray<int> q_value;

使用其构造函数的例子

double gpa[5] ={3.1,3.5,3.8,2.9,3.3};
valarray<double> v1; //创建长度为零的空数组
valarray<int> v2(8); //创建指定长度的空数组
valarray<int> v3(10,8); //长度为8,每个元素初始值为10
valarray<double> v4(gpa,4); //长度为4,填充gpa前四个值

类方法

operator 访问各个元素

size() 返回包含的元素数

sum() 返回总和

max() 最大值

min() 最小值

has-a

用于建立has-a关系的C++技术是组合(包含),即创建一个包含其他类对象的类。

类成员函数可以通过包含类的公有接口来访问和修改包含类对象。

通常描述为类获得了其成员对象的实现,但没有继承接口。

(公有继承既可以继承接口,也可以获得实现)

初始化顺序

项目被初始化的顺序是被声明的顺序,而不是他们在初始化列表中的顺序。

私有继承

在这里插入图片描述

使用私有继承,基类的公有对象和保护成员都将成为派生类的私有成员

基类方法不会成为派生对象公有接口的一部分,但可以在派生类的成员函数中使用它们。

派生类不继承基类的接口——是一种不完全继承。

包含将对象作为一个命名的成员对象添加到类中,而私有继承将对象作为一个未命名的继承对象添加到类中。

子对象用来表示通过继承或包含添加的对象。

因此私有继承提供的特性和包含相同:获得实现,但不获得接口。

关键字 private 用来定义私有继承类。

class Student : private std::string, private std::valarray<double>{}
多重继承

使用多个基类的继承被称作多重继承。

私有继承构造函数使用成员初始化列表语法,它使用类名而不是类成员来标识构造函数。

Student(const char* str,const double* pd,int n): std::string(str),ArrayDb(pd,n){}
访问基类的方法

私有继承使用类名和作用域解析运算符来调用基类的方法。

double Student::Average() const{
    if (ArrayDb::size()>0)
        return ArrayDb::sum()/ArrayDb::size();
    else
        return 0;
}
访问基类对象

如果在私有继承用使用基类对象本身,需要使用强制类型转换。

为避免调用构造函数创建新的对象,可使用强制类型转换创建一个引用。

const string & Student::Name() const{
    return(const string &) *this;
}

访问基类的友元函数

用类名显式地限定函数名不适合于友元函数,因为友元不属于类。可以通过显式地转换为基类来调用正确的函数。

ostream & operator<<(ostream & os,const Student & stu){
	os<<"score for"<<(const string &)stu<<":\n";
}
cout<<plato;

stu指向plato的引用,os指向cout的引用,显式地将stu转换为string对象引用,进而调用operator<<(ostream &, const string& )。

引用stu不会自动转换为string引用。根本原因是,在私有继承中,未进行显式类型转换的派生类引用或指针,无法赋值给基类的引用或指针。

即使这个例子是公有继承,也必须使用显式类型转换:

  • 如果不使用类型转换,代码将与友元函数原型匹配,从而导致递归调用;
  • 由于这个类是多重继承,编译器无法确定应该转换成哪个基类。
包含和私有继承的选择

通常,应使用包含来建立has-a关系;如果新类需要访问原有类的保护成员,或需要重新定义虚函数,应使用私有继承。

包含能够包含多个同类的子对象,而继承只能使用一个这样的对象。

包含不属于继承层次之内,不能访问保护成员。

保护继承

保护继承在列出基类时使用关键字protected。

使用保护继承时,基类的公有成员和保护成员都将成为派生类的保护成员。

和私有继承一样,基类的接口在派生类中也是可用的,但在继承层次外是不可用的。

当派生类派生出另一个类时

使用私有继承时,第三代类将不能使用基类的接口,这是因为基类的公有方法在派生类中将变成私有方法;使用保护继承时,基类的公有方法在第二代中将变成受保护的,因此第三代派生类可以使用他们。

在这里插入图片描述

隐式向上转换意味着无需进行显式类型转换,就可以将基类指针或引用指向派生类对象。

using定义访问权限

让基类的方法在派生类外可用:

  • 定义一个使用该基类方法的派生类方法。
  • 将函数调用包装在另一个函数调用中,即使用一个using声明来指出派生类可以使用特定的基类成员。
class Student : private std::string,private std::valarray<double>{
    public:
    using std::valarray<double>::min;
}

上述声明使得他们就像Student的公有方法一样。

using声明只使用成员名——没有圆括号、函数特征标和返回类型。

*using声明只适用于继承,而不适用于包含。

多重继承

MI描述的是有多个直接基类的类。公有MI表示的也是is-a关系。

必须使用关键字public来限定每一个基类,除非特别指出,否则编译器将认为是私有派生。

二次派生时,从同一基类派生出的派生类共同派生出另一类时,两个派生类中都包含同一基类,这将导致把基类指针设置成派生类对象中的基类地址时,存在二义性。所以应该使用类型转换来指定对象。

但真正的问题是,为什么需要两个基类对象的拷贝?

虚基类

虚基类是的从多个类(他们的基类相同)派生出的对象只继承一个基类对象。

通过再类声明中使用关键字virtual,可以定义虚基类。

class Singer : virtual public Worker{};
class Waiter : virtual public Worker{};
class SingingWaiter : public Singer, public Waiter{};

现在,SingingWaiter对象将只包含Worker对象的一个副本。从本质上讲,继承的Singer和Waiter对象共享一个Worker对象,而不是各自引入自己的Worker对象副本。因为SingingWaiter现在只包含了一个Worker子对象,所以可以使用多态。

如果使用虚基类,信息的自动传递将不起作用。禁止信息通过中间类自动传递给基类。

如果不希望默认构造函数来构造虚基类对象,则需要显式地调用所需的基类构造函数。

SingingWaiter(const Worker & wk, int p = 0,int v = Singer::other) : Worker(wk), Waiter(wk,p),Singer(wk,v){}

注意:对于虚基类,必须这样做;但对于非虚基类,这样做是非法的。

总之,在祖先相同时,使用MI必须引入虚基类,并修改构造函数初始化列表的规则。另外,如果在编写这些类时没有考虑到MI,则还可能需要重新编写它们。

混合使用虚基类和非虚基类

该类将包含一个表示所有的虚途径的基类子对象和分别表示各条非虚途径的多个基类子对象。

虚基类和支配

使用虚基类时,如果某个名称优先于其他所有名称,则使用它时,即使不使用限定符,也不会导致二义性。(派生类中的名称优先于直接或间接祖先类中的相同名称。

主要变化(同时也是使用虚基类的原因)是,从虚基类的一个或多个实例派生而来的类将只继承了一个基类对象。为实现这种特性,必须满足其他要求:

  • 有间接虚基类的派生类包含直接调用间接基类构造函数的构造函数,这对于间接非虚基类来说是非法的;
  • 通过优先规则解决名称二义性。

类模板

模板提供参数化类型,即能够将类型名作为参数传递给接收方来建立类或函数。

定义

模板类以下代码开头:

templete <class Type>
templete <typename Type>
bool Stack<Type>::push(const Type & item){}

尖括号中的内容相当于函数的参数列表。可以把关键字class看作变量的类型名,该变量接受类型作为其值,Type是类型参数,可以看作是该变量的名称。

不能将模板成员函数放在独立的实现文件中。由于模板不是函数,它们不能单独编译。模板必须与特定的模板实例化请求一起使用。最简单的方法是将所有模板信息放在一个头文件中,并在要使用这些模板的文件中包含该头文件。

使用

生成模板类必须请求实例化。

Stack <int> kernels;
Stack <string> colonels;

必须显式地提供所需的类型,这与常规地函数模板是不同的,因为编译器可以根据函数地参数类型来确定要生成哪种函数。

数组模板示例和非类型参数
templete<class T, int n>
ArrayTP<double, 12> eggweights;

这将导致编译器定义名为ArrayTP<double, 12>的类,并创建一个eggweights对象。

  • 表达式参数有一些限制。表达式可以是整形、枚举、引用或指针。
  • 另外模板代码不能修改参数的值,也不能使用参数的地址。所以不能使用诸如n++和&n等表达式。
  • 实例化模板是,用作表达式参数的值必须是常量表达式。

相比于构造函数传参,表达式参数方法的主要缺点是每种数组大小都将生成自己的模板。

ArrayTP<double, 12> eggweights;
ArrayTP<double, 13> dunkers;    //两个类声明
Stack<int> eggs(12);
Stack<int> dunkers(13);        //一个类声明

另一个区别是,构造函数方法更通用,这是因为数组大小作为类成员存储在定义种的。这样可以将一种尺寸的数组赋给另一种尺寸的数组,也可以允许创建数组大小可变的类。

模板多功能性

可将常规类的技术用作模板类。模板类可以用作基类,也可以用作组件类,还可以用作其他模板的类型参数。

template <typename T>
class Array{
    private:
        T entry;
};
template <typename Tp>
class Stack{
    Array<Tp> ar;      //组件类
};
Array< Stack<int> > asi; //表示 Stack<int>类型的数组
递归使用模板
ArrayTP< ArrayTP<INT,5>, 10> twodee;

这使得twodee是一个包含10个元素的数组,其中每个元素都是一个包含5个int元素的数组。等价于

int twodee[10][5];

在模板语法中,维的顺序与二维数组相反。

默认类型模板参数

类模板的类型参数可以提供默认值。

template <class T1, class T2 = int> class Topo{};
模板的具体化
显式实例化

使用关键字template并指出所需类型来声明类时,编译器将生成类声明的显式实例化。声明必须位于模板定义所在的名称空间中。

template class Topo <string, 100>;

这种情况下,虽然没有创建或提及类对象,编译器也将生成类声明(包括定义方法)。

显式具体化

显式具体化是特定类型的定义。

又是需要在为特殊类型实例化时,对模板进行修改,使其行为不同。

template <> class Classname <specialized-type-name>{};
template <> class Topo<const char*>{};
部分具体化

部分限制模板的通用性。

template <class T1, class T2> class Pair {};
template <class T1> class Pair <T1,int> {};
//关键字template后面的<>声明的是没有被具体化的参数类型。
//如果指定所有的类型,则<>为空,即显式具体化

部分具体化特性是的能够设置各种特性。

template <class T1, class T2, class T3> class Trio{};
template <class T1, class T2> class Trio <T1, T2, T2>{};
template <class T1> class Trio <T1, T1*, T1*>{};
成员模板

模板可以用作结构、类或模板类的成员。

按照编译器类型:有些接受模板成员,但不接受类外面的定义。然而,如果编译器接受类外面的定义,定义方法如下:

template <typename T>
	template< typename V>
		class beta<T>::hold{
            private:
            ...
        }
//同样的,在每个类成员函数定义时,也需要这种嵌套格式。
模板参数

模板可以包含类型参数和非类型参数。模板还可以包含本身就是模板的参数。

template <template <template T> class Thing>
    class Crab{
        private:
        Thing<int> s1;
        Thing<double> s2;
    }
//template <template T> class是类型,Thing是参数
int main(){
Crab<Stuck> legs;
legs.push();
}


模板类和友元

模板类声明也可以有友元,分为3类:

  • 非模板友元;
  • 约束模板友元,友元的类型取决于类被实例化时的类型;
  • 非约束模板友元,友元的所有具体化都是类的每个具体化的友元
非模板友元

在模板类中将一个常规函数声明为友元。

假设要为友元函数提供模板类参数,必须指明具体化。

template <class T>
class has{
    friend void report(has<T> &);
}

report()本身并不是模板函数,只是使用一个模板为参数。这意味着每个类的每一个特定的具体化都将有自己的静态成员。

约束模板友元

是在类外面声明的模板的具体化。

template <class T>
class has{
    friend void counts<TT>(); //因为counts函数没有参数,必须指明其具体化
    friend void report<>(has<T> &);//<>内可以省略,因为可以从参数类型推断出模板 friend void report<has<T>>(has<T> &);
}

这种情况下,每种T类型都有自己的友元。

非约束友元函数

通过在类内部声明模板,即每个函数具体化都是每个类具体化的友元。

对于非约束友元,友元模板类型参数与模板类型参数是不同的。

template <class T>
class has{
    private:
    T item;
    public:
    template <template C, template D> friend void show(C&, D&);
}
 template <template C, template D> friend void show(C& c, D& d){
     cout<<c.item;
 }
int main(){
    has<int> h1(10);
    has<double> h2(10.5);
    show(h1,h2);
模板别名
typedef std::array<double,12> arrd;
arrd gallons;
template<typename T>
    using arrtype = std::array<T,12>;

15.友元、异常和其他

友元

类并非只能拥有友元函数,也可以讲类作为友元。在这种情况下,友元类的所有方法都可以访问原始类的私有成员和保护成员。另外,也可以只将特定的成员函数指定为另一个类的友元。

友元类

当is-a和has-a关系都不适用的时候,考虑使用友元类。

friend class Remote;

友元声明可以位于公有、私有或保护部分,这无关紧要。

一般情况下,为了编译器读懂程序,友元类会放在原类之后定义,这是因为友元类中使用原类元素。

友元成员函数

友元类中大多数方法都是依靠原类的公有接口实现,于是,可以选择仅让特定的类成员成为另一个类的友元。

必须小心排列各种声明和定义的顺序。

class TV{
    friend void Remote::set_chan(TV & t, int c);
}

正确的排序方式是:

class TV;
class Romote{ ... }; //只包含函数声明
class TV{ ... };
//对Romote中的函数进行定义
inline bool Remote::volup(TV 7 t){ return t.volup;}
...

内联函数的链接性是内部的,这意味着函数定义必须在使用函数的文件中。将内联函数定义在头文件中,可以通过包含头文件确保定义位置正确。

如果将定义放在实现文件中,必须删除关键字inline,函数的链接性是外部的。

互为友元

可以让类彼此成为对方的友元来实现交互。

class TV{
    friend class Remote;
}
class Remote{
    friend class TV;
}
// TV的定义要放在Remote声明之后
共同的友元

函数需要访问两个类的私有数据时,可以将函数作为两个类的友元。

嵌套类

在另一个类中声明的类称为嵌套类,它通过提供新的类型类作用域来避免名称混乱。

包含类的成员函数可以创建和使用被嵌套类的对象;仅当声明位于公有部分,才能在包含类的外面使用嵌套类,而且必须使用作用域解析运算符。

与包含不同的是,包含意味着将类对象作为另一个类的成员,儿对类进行嵌套不创建类成员,而是定义了一种类型,该类型仅在包含嵌套类声明的类中有效。

访问权限

嵌套类的声明位置决定了嵌套类的作用域或可见性,即它决定了程序的哪些部分可以创建这种类的对象。类可见后,访问控制规则(公有、私有、保护、友元)决定程序对嵌套类成员的访问权限。

嵌套类、结构和枚举的作用域特征

声明位置包含它的类是否可以使用它包含它的类的派生类是否可以使用它在外部是否可以使用它
私有部分
保护部分
公有部分是,通过类限定符来使用

在模板类中也能使用嵌套类。

异常

abort()

位于头文件cstdlib,典型实现是向标准错误流发送消息abnormal program termination(程序异常终止),然后终止程序。它返回一个随显示而异的值,告诉操作系统处理失败。

abort()是否刷新文件缓冲区取决于实现。

也可以使用exit(),该函数刷新文件缓冲区,但不显示消息。

异常机制

异常提供了将控制权从程序的一个部分传递到另一个部分的途径。对异常的处理有3个部分:

  • 引发异常;
  • 使用处理程序捕获异常;
  • 使用try块。

throw,跳转,即命令程序跳到另一条语句。throw关键字表示引发异常,紧随其后的值指出了异常的特征。

catch,捕获异常,异常处理程序位于要处理问题的程序中。catch关键字和异常类型作为标签,指出当异常被引发时,程序应跳到这个位置执行。

try,标识其中特定的异常可能被激活的代码块,它后面跟一个或多个catch块。try后的花括号中,表明需要注意这些代码引发的异常。如果在try块外面调用包含throw的函数,将无法处理异常。

throw类似于执行返回语句,会终止当前程序的执行。但并不将控制权返回上一级函数,而是直到找到try块为止。

如果函数引发了异常,而没有try块或没有匹配的处理程序时,在默认情况下,程序最终将调用abort()。也可以修改这种行为。

对象作为异常类型

异常类型可以是字符串或其他C++类型,通常为类类型。

引发异常的函数传递一个对象,优点是:

  • 可以使用不同的异常类型来区分不同的函数在不同情况下引发的异常。
  • 对象可以携带信息。
栈解退

函数返回仅仅处理该函数放在栈中的对象,而throw语句则处理try块和throw之间整个函数调用序列放在栈中的对象。

如果没有栈解退这种特性,引发异常后,对于中间函数调用放在栈中的自动类对象,其析构函数将不会被调用。

异常特性
  • throw语句将控制权返回到第一个包含能够捕获相应异常的try-catch组合。
  • 引发异常时编译器总是创建一个临时拷贝,即使异常规范和catch块中指定的是引用(引用指向的是拷贝副本)。

既然生成副本,为何要使用引用?

基类引用可以执行派生类对象。假设有一组通过继承关联起来的异常类型,则在异常规范中只需列出一个基类引用,可以和任何派生类对象匹配。

有时可能不知道会发生哪些异常,这种情况下仍能捕获异常,即使不知道异常的类型,方法是使用省略号表示异常类型

catch(...){   }
exception类

C++异常的主要目的是为设计容错程序提供语言级支持,即异常使得在程序设计中包含错误处理功能更容易。

exception头文件定义了exception类,C++可以把它用作其他异常类的基类。

what()虚拟成员函数返回一个字符串,该字符串的特征随实现而异。

由于这是一个虚方法,因此可以在exception派生而来的类中重新定义它。

class bad: public std::exception{
    public:
    const char* what(){return "bad to hmean()";}
}

如果不想以不同方式处理这些派生而来的异常,可以在同一个基类处理程序中捕获它们。

stdexcept异常类

定义了logic_error和runtime_error类,它们都是从公有方法exception中派生而来的。

这些类的构造函数接受一个string对象作为参数,提供what()方法以C-风格字符串方式返回的字符数据。

logic_error 典型的逻辑错误,表明存在可以通过变成修复的问题

  • domain_error 给函数传递的参数不在定义域内
  • invalid_argument 指出给函数传递了一个意料外的值
  • length_error 指出没有足够的空间来执行所需的操作
  • out_of_bounds 指示4索引错误

runtime_error 可能在运行期间发生但难以预计和防范的错误,表明存在无法避免的问题

  • range_error 不在函数允许范围内
  • overflow_error 整型和浮点型都可能发生上溢错误,当计算结果超过某种类型能够表示的最大数量级时。
  • underflow_error 存在浮点类型可以表示的最小非零值,比这个值还小是会导致下溢错误。
bad_alloc异常和new

对于使用new导致的内存分配问题,C++的最新处理方式是让new引发bad_alloc异常。头文件new包含bad_alloc类的声明,它是从exception类公有派生而来的。

此前,当无法分配请求的内存量时,new返回一个空指针。

空指针和new

为处理new的变化,C++提供了一种在失败时返回空指针的new

big * pb;
pb=new(std::nothrow) big[10000];
if(pb==0){
    cout<<"could not allocate memory.";
    exit(EXIT_FAILURE);
}
RTTI

RTTI只用于包含虚函数的类层次结构,因为只有对于这种类层次结构,才应该将派生对象的地址赋给基类指针。

  • 当一个类包含虚函数时,编译器会在对象中添加一个指向虚函数表的指针,这使得对象可以在运行时识别其实际类型。
  • 由于RTTI依赖于这种虚函数表指针来识别类型,所以只有包含虚函数的类层次结构才能正确使用RTTI来进行类型识别和转换。
  • 对于不包含虚函数的类,编译器不会为其添加虚函数表指针,因此RTTI无法在这些类上工作。
dynamic_cast

使用一个指向基类的指针来生成一个指向派生类的指针;否则返回0——空指针。

superb * pm = dynamic_cast<superb *> (pg);

应尽可能使用虚函数,而只在必要时使用RTTI

也可以将dynamic_cast用于引用,但没有与空指针对应的引用值,因此无法使用特殊的引用值来指示失败。当请求不正确时,将引发类型为bad_cast的异常,从exception类派生而来,在头文件typeinfo中定义。

typeid运算符和type_info类

typeid运算符使得能够确定两个对象是否为同种类型。它接受两种参数:

  • 类名
  • 结果为对象的表达式

typeid返回一个对type_info对象的引用,type_info是在头文件typeinfo中定义的一个类。

typeid(magnificent) == typeid(*pg);

typeid包含一个name()成员,返回一个随实现而异的字符串:通常为类的名称。

cout << typeid(*pg).name();
类型转换运算符

dynamic_cast 使得能够在类层次结构中进行向上转换

const_cast 改变值的const和volatile属性

static_cast (1)使得能够在类层次结构中进行向上、向下转换;(2)进行数据类型间的转换(实际上也是一种向上、向下转换。

reinterpret_cast 用于天生危险的类型转换。

16.string类和标准模板库

string类

在这里插入图片描述

getline会丢弃换行符,get会保留换行符。

string风格的getline()将自动调整目标string对象的大小,而c风格不会。

读取c风格字符串的函数时istream类的方法,而string版本是独立的函数。这就是对于c风格字符串输入,cin是调用对象;而对于string对象输入,cin是一个函数参数的原因。

char fname[10];
string lname;
cin>>fname;
cin>>lname;
cin.getline(fname,10);
getline(cin,lname);

string 对象的最大允许长度,由常量string::npos指定,这通常是最大的unsigned int值。

getline(fin,item,':'); //指定分隔符为:,指定分隔符后,'\n'被视为普通字符。

在这里插入图片描述

在这里插入图片描述

方法capacity()返回当前分配给字符串的内存块的大小

reserve()能够请求内存块的最小长度。

如果需要将string对象转换为c风格字符串,可以使用c_srt( )

string fname;
ofstream fouot;
fout.open(fname.c_str());

智能指针模板类

智能指针是行为类似于指针的类对象。包含在头文件memory中

可以用new获得(直接或间接)的地址赋给这种对象。当智能指针过期时,其析构函数将使用delete来释放内存。

因此,如果将new返回的地址赋给这些对象,将无需记住稍后释放这些内存。

所有智能指针都是explicit构造,不允许隐式转换。

在这里插入图片描述

三种智能指针区别体现在指针间互相赋值时:

auto_ptr<double> pd(naw double);
auto_ptr<double> ps(naw double);
ps = pd;

auto_ptr(不推荐使用)此时,pd将控制权移交给ps,所以pd没有指向

unique_ptr (最安全) 此时,会报错。不允许移交控制权后存在没有指向的指针pd

shared_ptr 合法,相当于共享控制权,使用引用计数避免多次delete(见上图)

unique_ptr 讨论

如果unique_ptr 赋值的是一个临时智能指针,这种操作是允许的。

unique_ptr <int> make(int n){
    return unique_ptr <int>(new int(n));
}
int main(){
    vector<unique_ptr <int>> vp(size);
    vp[i] =make(rand()%1000);   //合法
}

如果确实需要赋值操作,标准库函数std::move()可以实现。

unique_ptr <string> ps1,ps2;
string demo;
ps1=demo("dfvz");
ps2=move(ps1);
ps1=demo("xv dgvvg");

为什么它可以区分是否安全呢?

因为它使用了C++11新增的移动构造函数和右值引用。

另外,unique_ptr 有可用于数组的变体。有与new[]匹配的delete[]。 (auto_ptr和shared_ptr没有这一特性)

unique_ptr <int[]>pda(new int(5));

标准模板库

STL(Standard Template Library,标准模板库)是C++标准库的一个重要组成部分,它提供了一组泛型的、可重用的数据结构(如容器)和算法(如排序、搜索等)。STL的设计目的是让数据结构和算法能够独立于数据类型而使用,即模板编程(Template Programming)的思想。

STL主要分为三个部分:

  1. 容器(Containers):用于存储元素的数据结构,如vectorlistdeque(双端队列)、setmap等。容器是STL中最基础的部分,它们为存储和操作数据提供了不同的方式和性能特性。
  2. 迭代器(Iterators):用于访问容器中元素的对象。迭代器提供了一种通用的方法来遍历容器中的元素,而不需要知道容器的具体实现细节。不同的容器类型可能有不同的迭代器类型,但迭代器接口是统一的,这使得算法能够独立于具体的容器类型而实现。
  3. 算法(Algorithms):用于在容器上执行操作的一组函数模板,如排序、搜索、复制等。算法通常通过迭代器来访问容器中的元素,因此它们可以与任何类型的容器一起工作,只要容器提供了合适的迭代器。
模板类vector

vector计算矢量存储了一组可随机访问的值,即可以使用索引来直接访问矢量的第10个元素,而不必首先访问前9个。定义在头文件vector中。

要创建vector模板对象,可使用通常的表示法来指出要使用的类型。vector模板使用动态内存分配,因此可以用初始化参数来指出需要多少矢量。

vector<int> ratings(5);
int n;
vector<double> scores(n);
scores[0] = 98.3;

所有容器都包含的方法:

size() 返回容器中元素数目

swap() 交换两个容器的内容

begin() 返回一个指向容器中第一个元素的迭代器

end() 返回一个表示超过容器尾的迭代器(超尾:他是一种迭代器,指向容器最后一个元素后面的那个元素。这与C-风格字符串最后一个字符后面的空字符类似,只是空字符是一个值,而“超过尾部”是一个指向元素的指针(迭代器))。

vector等某些容器才包含的方法:

push_back() 将元素添加到矢量末尾。它将负责内存管理,增加矢量的长度,使之能够容纳新的成员。无需了解元素的数目,只要能够取得足够的内存。

while(cin>>temp&&temp>=0)
    scores.push_back(temp);

erase()方法删除矢量中给定区间的元素。

scores.erase(scores.begin(),scores.begin()+2);//左闭右开区间

insert()方法在矢量中指定位置插入数据。第一个参数指定了新元素的插入位置,第二和第三个参数定义了被插入区间,该区间通常是另一个容器对象的一部分。

old.insert(old.begin(), new_v.begin()+1, new_v.end()); //将new中除了第一个元素外的所有元素插入到old矢量的第一个元素前面
迭代器

是一个广义指针。事实上,他可以是指针,也可以是一个可对其执行类似指针的操作的对象。

可以将指针广义化为迭代器,让STL能够为各种不同的容器类提供统一的接口。

每个容器类都定义了一个合适的迭代器,该迭代器的类型是一个名为iterator的typedef,其作用域为整个类。

要为vector的double类型规范声明一个迭代器,可以这样做:

vector<double>::iterator pd;
vector<double> scores;
pd = scores.begin();
*pd = 22.3;
++pd;

C++11可以使用自动类型推断

auto pd = scores.begin();
STL非成员函数

通常需要对数组执行很多操作,STL从更广泛的角度定义了非成员函数来执行这些操作。即不是为每个容器定义find(),而是定义了一个适用于所有容器的非成员函数find()。

即使有执行相同任务的非成员函数,STL有时也会定义一个成员函数来执行查找、排序等操作。这是因为类特定算法的效率比通用算法高。

for_each()将被指向的函数应用于容器区间内的各个元素。被指向的函数不能修改容器元素的值。可以用for_each()代替for循环。

接受3个参数,前两个是定义容器中区间的迭代器,最后一个是指向函数的指针(更普遍的,最后一个参数是函数对象)。

for_each(book.begin(), book.end(), ShowReview);
//等价于
vector<Review>::itertor pr;
for(pr=book.begin(); pr!=book.end();pr++)
    ShowReview(*pr);
//避免了显示使用迭代器变量

random_shuffle接受两个指定区间的迭代器参数,并随机排列该区间内的元素。

random_shuffle(book.begin(), book.end());

sort()函数也要求容器支持随机访问。该函数有两个版本,第一个版本接受两个定义区间的迭代器参数,并使用为存储在容器中的类型元素定义的<运算符,对区间中的元素进行操作(按升序排列)。

sort(book.begin(), book.end());

如果容器元素是用户定义的对象,则要使用sort(),必须定义能够处理该类型对象的operator()函数。

另一种格式的sort()接受三个参数,前两个参数接受两个定义区间的迭代器参数,最后一个参数是指向要使用的函数的指针(函数对象),其返回值可转换为bool。

sort(book.begin(), book.end(), WorseThan);
基于范围的for循环

基于范围的for循环是为用于STL而设计的。

for_each(book.begin(), book.end(), ShowReview);
//可替换为
for(auto x : books)ShowReview(x);

不同于for_each,基于范围的for循环可修改容器的内容,诀窍是制定一个引用承诺书

for(auto &x : books)ChangeReview(x);

泛型编程

STL是一种泛型编程。面向对象变成关注的是编程的数据方面,而泛型编程关注的是算法。它们之间的共同点是抽象和创建可重用代码,但他们的理念决然不同。

泛型编程旨在编写独立于数据类型的代码。

迭代器

模板使得算法独立于存储的数据类型,而迭代器使得算法独立于使用的容器类型。

泛型编程旨在使用同一个find函数来处理数组、链表或任何其他容器类型。即函数不仅独立于容器中存储的数据类型,而且独立于容器本身的数据结构。模板提供了存储在容器中的数据类型的通用表示,因此还需要遍历容器中的值的通用表示,迭代器正是这样的通用表示。

为区分++运算符的前缀和后缀版本,将operator++作为前缀版本,将operator++(int)作为后缀版本。

iterator& operator++(){
    pt=pt->p_next;
    return *this;
}
iterator operator++(int){
    iterator tmp = *this;
    pt=pt->p_next;
    return *tmp;
}

1.每个容器类都定义了相应的迭代器类型。不管实现方式如何,都提供所需的操作,如*和++。

2.每个容器类都有一个超尾标记,当迭代器递增岛超越容器的最后一个值后,这个值将赋给迭代器。

3.每个容器类都有begin()和end()方法。

使用auto可以对代码进一步简化:

list<double>::iterator pr;
for(pr = scores.begin(); pr!=scores.end(); pr++)
    cout<<*pr;
//简化为
for(auto pr = scores.begin(); pr!=scores.end(); pr++)
    cout<<*pr;
//最好避免直接使用迭代器,尽可能通过STL函数处理细节。也可以用基于范围的for循环
for(auto x : score) cout<<x;
迭代器类型

输入迭代器

来自容器的信息被视为输入。

  1. 单向遍历:输入迭代器只支持单向遍历容器中的元素。这意味着你可以从一个位置移动到下一个位置,但不能直接从当前位置“回退”到前一个位置。这种单向性限制了迭代器的使用方式,使得它只能按照从头到尾的顺序遍历容器。
  2. 只读访问:通过输入迭代器,你只能读取容器中元素的值,不能修改它们。如果需要修改元素,需要使用支持写操作的迭代器类型(如前向迭代器、双向迭代器或随机访问迭代器)。
  3. 多趟遍历:虽然每次只能遍历容器一次,但你可以使用相同的输入迭代器(或重新开始的迭代器)再次遍历容器。然而,每次遍历必须从头开始,因为你不能直接跳转到容器中的特定位置。也不能保证第二次遍历容器时,顺序不变。
  4. 递增操作:输入迭代器支持递增操作(++),允许你移动到容器中的下一个元素。但是,由于它是单向的,所以不支持递减操作(--)。被递增后,不能保证其先前的值仍然可以被解除引用。

输出迭代器

用于将信息从程序传输给容器。

单通行,只写权限。

正向迭代器

  1. 只使用++运算符来遍历容器,所以它每次沿容器向前移动一个元素。
  2. 它总是按相同顺序遍历一系列值。
  3. 递增后,仍然可以对前面的迭代器值解除引用(如果保存了它),并可以得到相同的值。
  4. 可以读取和修改数据,也可以设置为只读。

双向迭代器

双向通行,具有正向迭代器的所有功能,同时支持两种递减运算符。

可进行交换元素、指针加一、指针减一,并重复这种处理过程。

随机访问迭代器

能够直接跳到容器中的任何一个元素。

具有双向迭代器得到所有特性,同时添加了支持随机访问的操作(如指针增加运算)和用于对元素进行排序的关系运算符。a和b为迭代器值,n为整数,r为随机迭代器变量或引用。

在这里插入图片描述

迭代器层次结构

在这里插入图片描述

概念、改进和模型

概念用来描述STL中的一系列要求。

概念可以具有类似继承的关系。改进来表示这种概念上的继承。

概念的具体实现被称为模型。

一些迭代器

指针

double pt[100];
sort(pt, pt+100);

ostream_iterator

ostream_iterator<int,char> out_iter(cout," "); //cout显示信息,每个数据项以空格为分隔符
*out_iter++=15; //works like cout << 15 << " ";
copy(dice.begin(), dice.end(), ostream_iterator<int,char>(cout," "));

istream_iterator

copy(istream_iterator<int,char)(cin), istream_iterator<int,char>(), dice>begin());
//从输入流读取,直到文件结尾、类型不匹配或出现其他输入故障为止

reverse_iterator

反向输出,对reverse_iterator执行递增操作将导致它被递减。

vector类有一个名为rbegin()和rend()的成员函数,前者返回一个指向超尾的反向迭代器,后者指向一个指向第一个元素的反向迭代器。

copy(dice.rbegin(), dice.rend(), out_iter);

反向拷贝,甚至不必声明反向迭代器。

如果要插入而非覆盖,back_insert_iterator将元素插入到容器尾部,front_insert_iterator将元素插入到容器的前端,insert_iterator将元素插入到insert_iterator构造函数的参数指定的位置前面。这三个插入迭代器都是输出容器概念的模型

这些迭代器将容器类型作为模板参数,将实际的容器标识符作为构造函数参数。

back_insert_iterator<vector<int>> back_iter(dice);
//为名为dice的vector<int>容器创建一个back_insert_iterator

对于insert_iretator声明,还需一个指示插入位置的构造函数参数

insert_iterator<vector<int>> insert_iter(dice,dice.begin());
容器种类

STL具有容器概念和容器类型。

概念是具有名称(如容器、序列容器、关联容器)的通用类别;

容器类型是可用于创建具体容器对象的模板。

11个容器类型分别是deque、list、queue、priority_queue、stack、vector、map、multimap、set、multiset、bitset

C++11新增forward_list、unordered_map、unordered_multimap、unordered_set、unordered_multiset,且不将bitset视为容器,讲起视为一种独立的类别。

容器概念

没有与基本容器概念对应的类型,但概念描述了所有容器类都通用的元素。它是一个概念化的抽象基类——说它概念化,是因为容器类并不真正使用继承机制。容器概念指定了所有STL容器累都必须满足的一系列要求。

容器时存储其他对象的对象。

  • 被存储的对象必须是同一类型的。
  • 对象的类型必须是可复制构造的和可赋值的。基本类型满足这些要求;类定义没有将复制构造函数和赋值运算符声明为私有或保护的也满足。C++11改进了这些概念,添加了术语可复制插入和可移动插入。

在这里插入图片描述

“复杂度”一列描述了执行操作所需的时间。表中三种可能性从快到慢依次是:

  • 编译时间,操作在编译时执行,执行时间为0
  • 固定时间,操作发生在运行阶段,但独立于对象中的元素数目
  • 线性时间,时间与元素数目成正比

时间复杂度概念描述了容器长度对执行时间的影响,而忽略其他因素。

复杂度要求是STL特征,虽然实现细节可以隐藏,但性能规格应该公开,以便程序员能够知道完成特定操作的计算成本。

C++11新增的容器要求

在这里插入图片描述

rv表示类型为X的非常量右值。另外,在表16.5中,要求X::iterator满足正向迭代器的要求,而以前只要求它不是输出迭代器。

序列

在这里插入图片描述

vector 数组的一种类表示:尾部添加和删除元素得时间时固定的,但在头部或者中间插入和删除元素得复杂度为线性时间。

deque 双端队列:尾部和头部都是固定的。

list 双向链表:链表中任意位置进行插入和删除的时间都是固定的。 因此vector强调通过随机访问进行快速访问,list强调元素的快速插入和删除。

  • list也是可反转容器。
  • list不支持数组表示法和随机访问。
  • 从容器中插入或删除元素后,链表迭代器指向元素将不变。
  • 包含链表专用的成员函数。附录G

在这里插入图片描述

注意splice和insert的区别。

forward_list 单链表:每个节点只链接到下一个节点,而没有链接到前一个节点。因此只需要正向迭代器,不需要双向迭代器。是不可反转的容器,更简单紧凑,但功能更少。

queue 适配器类:ostream_iterator就是一个适配器,让输出列能够使用迭代器接口。同样,queue让底层类(默认为deque)展示典型的队列接口。不允许随机访问队列元素,也不允许遍历队列。把使用限制在定义队列的基本操作上。

在这里插入图片描述

priority_queue 适配器类:和queue操作相同。主要区别是,最大的元素被移到队首。内部区别在于,默认的底层类是vector。可以修改用于确定哪个元素放在队首的比较方法。

priority_queue<int> pq1;
priority_queue<int> pq1(greater<int>);

stack 适配器类:给底层类(默认为vector)提供了典型的栈接口。不允许随机访问栈元素,不允许遍历栈。

在这里插入图片描述

array:并非STL容器,因为其长度是固定的。因此array没有调整容器大小的操作,如push_back()和insert(),但定义了operator和at()。可将许多算法用于array,如copy(),for_each()。

关联容器

关联容器是对容器概念的另一个改进。关联容器将值与键关联在一起,并使用键来查找值。

对于容器X,表达式X::value_type通常指出了存储在容器中的值类型。

对于关联容器来说,表达式X::key_type指出了键的类型。

关联容器提供了对元素的快速访问。允许插入新元素,但不能指定元素的插入位置。原因在于关联容器通常有用于确定数据位置的算法,以便快速检索信息。

关联容器通常是使用某种树实现的。节点使得添加或删除数据项比较容易。

STL提供了四种关联容器

set

头文件set定义。其值类型和键相同,键是唯一的(不能存储多个相同的值),意味着集合中不会有多个相同的键。对于set来说,键就是值。可反转、可排序。

set<string> A;
set<string, less<string> > A; //第二个模板参数是可选的,可用于指示用来对键进行排序的比较函数或对象。默认情况下,将使用模板less<>。

set.union() 得出两个集合的并集

set.intersection() 交集

set.difference() 差,即第一个集合的元素减去二者共有的元素。

set.lower_bound() 将键作为参数并返回一个迭代器,指向集合中第一个不小于键参数的成员。

set.upper_bound() 第一个大于键参数的成员。

set_uniun(A.begin(),A.end(),B.begin(), B.end(), ostream_iterator<string, char> out(cout," "));
set_uniun(A.begin(),A.end(),B.begin(), B.end(), insert_iterator<set<string>>(C,C.begin()));

因为排序决定了插入的位置,所以这种类包含值指定要插入的信息,而不指定位置的插入方法。

string s("tennies");
A.insert(s);
B.insert(A.begin(),A.end());

multiset

头文件set定义。类似于set,只是可能有多个值的键相同。

map

头文件map定义。值和键的类型不同,键是唯一的。

multimap

头文件map定义。和map类似,只是一个键可以和多个值相关联。

multimap<int,string> codes;//声明一个键类型为int,存储的值类型为string的multiset类
//第三个模板参数可选,指出对键进行排序的比较函数或对象

为将信息结合在一起,实际的值类型将键类型和数据类型结合为一对。

pair<const keytype,datatype>;
//创建pair,再将它插入
pair<const int, string>item(213,"los");
codes.insert(item);
//创建匿名pair对象并将它插入
codes.insert(pair<const int, string>(213,"los"));
//访问其两个部分
cout<<item.first<<item.second;
无序关联容器

和关联容器一样,无序关联容器也将值和键关联起来,并使用键来查找值。但底层差别在于,关联容器是基于树结构的,而无序关联容器是基于数据结构哈希表的,这旨在提高添加和删除元素的速度以及提高查找算法的效率。

有四种无序关联容器:unordered_set, unordered_multiset, unordered_map, unordered_multimap

函数对象

很多STL算法都是用函数对象,也叫函数符,也叫仿函数。函数符是可以以函数方式和()结合使用的任意对象。这包括函数名、指向函数的指针和重载了()运算符的类对象(即定义了函数operator()()的类)。

class Linear{
    private:
    	double slope;
    	double y0;
    public:
    	Linear(double s1_=1,double y_=0):slope(s1_),y0(y_){}
    	double operator()(double x){return y0+slope*x;}
}
Linear f1;
Linear f2(2.5,10.0);
double y = f1(12.5);
double y2 = f2(0.4);
函数符概念
  • 生成器是不用参数就可以调用的函数符。
  • 一元函数是用一个参数就可以调用的函数符。
  • 二元函数是用两个参数可以调用的函数符。
  • 返回bool值的一元函数叫谓词。
  • 返回bool值的二元函数叫二元谓词。

list模板有一个将谓词作为参数的remove_if()成员。该函数将谓词应用于区间中的每个元素。如果谓词返回true,则删除这些元素。

bool tooBig(int n){return n > 100;}
list<int> score;
score.remove_if(tooBig);
//还可以设计一个tooBig类完成这一功能(p573)
预定义的函数符

头文件functional中定义了多个模板类函数对象。

#include<functional>
plus<double> add;
double y = add(2.2,3.4);

transform(gr8.begin(),gr8.end(),m8.begin(),out,plus<double>());

对于所有内置的算术运算符、关系运算符和逻辑运算符,STL都提供了等价的函数符。

在这里插入图片描述

算法

STL将算法库分为4组

  • 非修改式序列操作;
  • 修改式序列操作;
  • 排序和相关操作;
  • 通用数字运算。

前三组在头文件algorithm中描述,第四组是专门用于数值数据的,在numeric头文件中。

其他库

vector和valarray

valarray类模板是面向数值计算的。重载了所有算数运算符。

valarray确实有一个resize()方法,但不能使用vector的push_back时那样自动调整大小,没有只吃插入、排序、搜索等操作的方法。

valarray对象不是一个指针,因此不能像常规数组一样

sort(vad, vad+10);

但可以使用地址运算符,而valarray又没有定义超尾,所有这并不安全。

C++11提供了接收valarray对象作为参数的模板函数begin()和eng()。

sort(begin(vad), end(vad));
slice

slice类对象可用作数组索引,它表示的不是一个值而是一组值。

slice对象被初始化为三个整数值:起始索引、索引数和跨距。

slice(1, 4, 3)表示选择4个元素,他们的索引分别是1,4,7,10.

valarray var;
var[slice(1,4,3)] = 10; //把第1,4,7,10个元素设置为10

slice能够使用一个一维valarray对象来表示二维数组。

initializer_list

可以使用初始化列表语法将STL初始化为一系列值。包含在头文件initializer_list中。

实现了不确定元素个数的传递。将一系列值看作一个参数传递。

如果类构造函数使用initializer_list作为参数,也类内只能有构造函数可以使用initializer_list。

17.输入、输出和文件

C++程序把输入和输出看作字节流。字节可以构成字符或数值数据得二进制表示。流充当了程序和流源或流目标之间的桥梁。

管理输入包含两步:

  • 将流与输入去向的程序关联起来。
  • 将流与文件连接起来。

输入流需要两个连接,文件端部提供了流的来源,程序端连接将流的流出部分转储到程序中。

通过使用缓冲区可以:

  • 高效地处理输入和输出。
  • 让用户在将输入传输给程序之前返回并更正。

iostream

cin(wcin)

cout(wcout)

cerr(wcerr)标准错误流,无缓冲

clog(wclog)标准错误流,缓冲

重定向

输入重定向 <

输出重定向 >

C>counter <okla >cow //使用程序counter,从okla读取数据进行操作,并输出至cow

cout

ostream最重要的任务之一是将数值类型转换为以文本形式表示的字符流。也就是说,将数据内部藐视转换为由字符字节组成的输出流。

C++用指向字符串存储位置的指针来表示字符串。如果要获得字符串的地址,则必须将其强制转换为其他类型。

put()显示字符

write()显示字符串 不会在遇到空字符时自动停止打印字符,而只是打印指定数目的字符,即使超出了字符串的边界

cout.put('w');
cout.put('w').put('q');
cout.put(65);//display A; 在原型合适的情况下,可以将数值型参数(如int)用于put(),让函数原型自动将参数转换为正确的char值
cout.put(66.3);//display B
cout.write(state,5); //第一个参数为字符串地址,第二个参数为显示字符数

刷新输出缓冲区

  • 将换行符发送到缓冲区后
  • 在输入即将发生时

会刷新缓冲区。

如果实现不能在所希望时刷新输出,可以使用控制符来强行执行刷新:

  • flush 刷新缓冲区
  • endl 刷新缓冲区并插入一个换行符
cout<<"hello"<<flush;

控制符也是函数,可以直接调用flush()来刷新cout缓冲区:

flush(cout);

调整字段宽度

width()函数将长度不同的数字放到宽度相同的字段里。只影响将显示的下一个项目。

int width(); //返回字段宽度的当前设置
int width(int i);//将宽度设置为i个空格,返回以前的字段宽度值,以便恢复

cout.width(12);

填充字符

默认情况下,cout用空格填充字段中未被使用的部分,可以用fill()成员函数来改变填充字符。

cout.fill('*');

新的填充字符一直有效,直到修改它。

设置浮点数的显示精度

默认进度为6,precision()函数使得能够选择其他值

cout.precision(2);

新的精度一直有效,直到修改它。

打印末尾的0和小数点

setf()函数能够控制多种格式化特性。类中定义了多个常量,可用作该函数的参数。

cout.setf(ios_base::showpoint);//导致末尾的0被显示出来
//showpoint是ios_base定义的类级静态常量。类级意味着如果要在成员函数定义的外面使用它,则必须在常量名前面加上作用运算符

cin

单字符输入

get()方法读取下一个输入字符

get(char& ch)将输入字符赋给其参数

get(void)版本将输入字符转换为整型,并将其返回

字符串输入

getline()、get()、ignore()

get()将换行符留在输入流中,而getline()抽取并丢弃输入流中的换行符。

ignore()接受两个参数,一个是数字,指定要读取的最大字符数;另一个是字符,用作输入分界符。

cin.ignore(255,'\n'); //丢弃接下来的255个字符或直到到达第一个换行符

read()读取指定数目的字符,不会在输入后加上空字符,因此不能将输入转化为字符串。最常与write()结合。

cin.read(gross,44);

peek()返回输入中的下一个字符,但不抽取输入流中的字符。也就是说可以查看下一个字符。

gcount()返回最后一个非格式化抽取方法读取的字符数。

putback()将一个字符插入到输入字符串中,被插入的字符将是下一条输入语句中读取的第一个字符。

while(cin.get(ch)){
if(ch!='#')
    cout<<ch;
else{
    cin.putback(ch);
    break;
}
}

文件输入和输出

ofstream fout;
fout.open("jar.txt");//将对象和特定的文件关联起来
//也可以使用另一个构造函数合并两步
ofstream fout("jar.txt");
fout.close(); //关闭这样的连接并不会删除流,而只是断开流到文件的连接

ifstream fin;
fin.open("jar.txt");//将对象和特定的文件关联起来
//也可以使用另一个构造函数合并两步
ifstream fin("jar.txt");
fin.close();
流状态检查和is_open()
if(!fin.is_open()){
} //这种方式更好是因为能检测出其他方式不能检测出的微妙问题
打开多个文件

如果需要同时打开两个文件,则必须为每个文件创建一个流。

如果依次打开文件,可以共用一个流,依次关联。

ifstream fin;
fin.open("jar.txt");
...
fin.close();
fin.clear(); //reset fin
fin.open("yar.txt");
...
fin.close();
命令行处理技术
int main(int argc, char* argv[]){
}
文件模式

文件模式描述的是文件将被如何使用:读、写、追加等。

在这里插入图片描述

ofstream fout(file, ios::out | ios::app);
二进制文件
struct pla{
char name[20];
double popu;
double g;
};
pla p1;
//文本格式保存
ofstream fout("pla.dat", ios_base::out | ios_base::app);
fout<<p1.name<<" "<<p1.popu<<" "<<p1.g<<endl;
//二进制格式保存
ofstream fout("pla.dat", ios_base::out | ios_base::app | ios_base::binary);
fout.write((char*)&p1, sizeof p1);//将结构看作一个整体,保存的信息更为紧凑、精确;更便于键入代码
//需要注意的是,必须将地址强制转换为指向char的指针

要使用文件恢复信息,可通过read()方法。

ifstream fin("pla.dat", ios_base::in | ios_base::binary);
fin.read((char*)&p1, sizeof p1);

这将从文件中赋值sizeof p1个字节到p1结构中。

同样的方法也适用于不使用虚函数的类。这种情况下,只有数据成员被保存,而方法不会被保存。

如果类有虚方法,也也将复制隐藏指针(该指针指向虚函数的指针表)。

由于下一次运行程序时,虚函数表可能在不同的位置,因此将文件中的旧指针信息复制到对象中,将可能造成混乱。

提示:是否可以用string类代替结构体中name的字符串呢?

不能。

因为string对象本身并没有包含字符串,而是包含一个指向其中存储了字符串的内存单元的指针。因此,将结构复制到文件中,复制的将不是字符串数据,而是字符串的地址。当再次运行该程序时,该地址将毫无意义。

随机存取

如果需要同时读写文件,可以设置对象:

fstream finout;//包含 ifstream 和 ofstream

模式设置成:

finout.open(file,ios_base::in | ios_base::out | ios_base::binary); //二进制视情况而定

接下来,需要一种在文件中移动的方式。seekg()和seekp(),前者将输入指针移到指定的文件位置,后者将输出指针移到指定的文件位置。

istream & seekg(streamoff, ios_base::seekdir); //定位到离第二个参数指定的文件位置特定距离的位置
istream & seekg(streampos); //定位到离文件开头特定距离(单位为字节)的位置

内核格式化

1.使用内核格式化,可以轻松地把数字转换为字符串,也可以把字符串转换为数字。

2.stringstream类可以使用iostream类地方法,来管理存储在字符串中的数据。

头文件sstream定义了ostringstream和istringstream类。

ostringstream outstr;
int cap;
cin >> cap;
outstr << cap; //可对放入字符串的信息进行格式化

18.探讨C++新标准

类内成员初始化

类定义内可使用等号或大括号版本的初始化,但不能使用圆括号版本。

右值引用

新增右值引用,使用&&表示。右值引用可关联到右值,即可出现在赋值表达式右边。但不能对其应用地址运算符的值。

int &&t = x+y;

t关联的是x+y的结果,所以后续更改x或y的值,也不会对t造成影响。

右值引用导致右值被存储到特定区域,也就是说,虽然不能对x+y的值取地址,但是可以对t取址。通过将地址和数值关联,运用引用访问。

移动语义和右值引用

为避免大量的数值拷贝和临时变量生成等无用功,可以将原始数据留在原地,只修改记录,这种做法叫做移动语义。

要实现移动语义,需要采取某种方式,让编译器知道何时需要复制,何时不需要。这需要右值引用发挥作用。

可定义两个构造函数。其中一个是常规复制构造函数,它使用const左值引用作为参数,这个引用关联到左值实参。另一个是移动构造函数,它使用右值引用作为参数,该引用关联到右值实参。

复制构造函数可执行深复制,而移动构造函数只调整记录。在将所有权转移给新对象的过程中,移动构造函数可能修改其实参(复制类对象的值),这意味着右值引用参数不应是 const。

在进行移动语义操作时,需要转交指针,并将旧指针指向空,避免多次delete同一空间。这种夺取所有权的方式被叫做窃取

移动语义触发需要两个条件:

  • 识别到右值引用
  • 有移动构造函数

赋值:移动语义也可以用于赋值运算符。

**强制移动:**如果想对左值使用移动构造函数,可以使用强制类型转换std::move()。

类的新功能

特殊的成员函数

如果提供了析构函数、复制构造函数或复制赋值运算符,编译器将不会自动提供移动构造函数和移动赋值运算符。

如果提供了移动构造函数或移动赋值运算符,编译器将不会自动提供复制构造函数和复制赋值运算符。

默认和禁用

默认

假定要使用某个默认的函数,而这个函数由于某种原因不会自动创建(如上)。可使用关键字default显式地声明这些方法的默认版本。

someclass() = default; //使用构造函数默认版本

禁止

delete可以用于禁用任何方法。可以使用delete禁止特定的转换。

void redo(double);
void redo(int) = delete;
委托构造函数

允许在构造函数的定义中使用另一个构造函数,这被称为委托。因为构造函数暂时将创建对象的工作委托给另一个构造函数。

继承构造函数

C++98提供一种让名称空间中函数可用的语法。这让函数fn的所有重载版本都可用。

namespace box{
    int fn(){int};
    int fn(double){};
    int fn(const char*){};
}
using box::fn;

也可以使用这种方法让基类的所有非特殊成员函数对派生类可用。c2中的using对象可使用c1的三个fn()方法,但优先选择C2.

class c1{
        int fn(){int};
    int fn(double){};
    int fn(const char*){};
};
class c2 : public c1{
    using c1::fn;
    double fn(double){};
}

c++11提供一种让派生类能够继承基类构造函数的机制。当派生类中没有与之匹配的构造函数时,将使用基类版本的构造函数。

管理虚方法:override和final

假设基类声明了一个虚方法,在派生类中提供不同版本将覆盖旧版本。但如果特征标不匹配,将隐藏而不是覆盖旧版本。当派生类对象函数使用基类同名函数特征标,则不合法。

可使用虚说明符override指出要覆盖一个函数,编写函数时声明特征标如果和基类不匹配,编译器将视为错误。

virtual void f(char * ch) const override{};

如果想禁止派生类覆盖特定的虚方法,可在参数列表后面加上final。下面的代码禁止派生类重新定义函数f()。

virtual void f(char ch) const final{};

override和final并非关键字,而是具有特殊含义的标识符,意味着编译器根据上下文确定它们是否具有特殊含义。

lambda

一种定义和应用函数的数学系统。这个系统能够使用匿名函数。

[](int x){return x % 3 == 0;}
//这与以下定义很相似
bool f3(int x){return x % 3 == 0;}

差别有两个:

  • 使用 [] 替代了函数名
  • 没有声明返回类型。返回类型相当于使用decltpy根据返回值推断得到的。

可以这么使用lambda

count3 = std::count_if(numbers.begin(),numbers.end(),[](int x){return x % 3 == 0;});

仅当lambda表达式完全由一条返回语句组成时,自动类型推断才管用。否则,需要使用新增的返回类型后置语法。

[](double x)->double{int y = x; return x - y;}

在这里插入图片描述

包装器

C++提供了多个包装器。这些对象用于给其他编程接口提供更一致或更合适的接口。

在这里插入图片描述

模板function是在头文件functional中声明的,它从调用特征标的角度定义了一个对象,可用于包装调用特征标相同的函数指针、函数对象或lambda表达式。

std::function<double(char, int)> fdci;

可以将接受一个char参数和一个int参数,并返回一个double值得任何函数指针、函数对象或lambda表达式赋给它。

这样做避免了同一特性标不同参数的多次实例化。

可变参数模板

可变参数模板可以创建这样的模板函数和模板类,即可接受可变数量的参数。

要创建可变参数模板,需要理解几个要点:

  • 模板参数包
  • 函数参数包
  • 展开参数包
  • 递归

C++11提供了一个用省略号表示的元运算符,能够声明表示模板参数包的标识符,模板参数包基本上是一个类型列表。同样,它还能声明表示函数参数包的标识符,而函数参数包基本上是一个值列表。

template<typename... Args>
void show_list(Args...args){
    
}

Args是一个模板参数包,而args是一个函数参数包。可将这些参数包的名称指定为任何符合C++标识符规则的名称。

Args和T的区别在于,T与一种类型匹配,而Args与任何数量(包括0)的类型匹配。

在使用时,应使用递归。将函数参数包展开,对列表的第一项进行处理,再将余下的内容传递给递归调用,直到列表为空。确保递归终止很重要。

template<typename T, typename... Args> //特殊处理1个和0个元素的情况
void show_list(){};
    
template<typename T, typename... Args>
void show_list(const T& value){       //使用引用提高效率
    cout << value << endl;
};

template<typename T, typename... Args>
void show_list(const T& value, const Args&...args){
    cout << value << ',';
    show_list(args...);//可将省略号放在函数参数包名的右边,将参数包展开
}

数可能修改其实参(复制类对象的值),这意味着右值引用参数不应是 const。

在进行移动语义操作时,需要转交指针,并将旧指针指向空,避免多次delete同一空间。这种夺取所有权的方式被叫做窃取

移动语义触发需要两个条件:

  • 识别到右值引用
  • 有移动构造函数

赋值:移动语义也可以用于赋值运算符。

**强制移动:**如果想对左值使用移动构造函数,可以使用强制类型转换std::move()。

类的新功能

特殊的成员函数

如果提供了析构函数、复制构造函数或复制赋值运算符,编译器将不会自动提供移动构造函数和移动赋值运算符。

如果提供了移动构造函数或移动赋值运算符,编译器将不会自动提供复制构造函数和复制赋值运算符。

默认和禁用

默认

假定要使用某个默认的函数,而这个函数由于某种原因不会自动创建(如上)。可使用关键字default显式地声明这些方法的默认版本。

someclass() = default; //使用构造函数默认版本

禁止

delete可以用于禁用任何方法。可以使用delete禁止特定的转换。

void redo(double);
void redo(int) = delete;
委托构造函数

允许在构造函数的定义中使用另一个构造函数,这被称为委托。因为构造函数暂时将创建对象的工作委托给另一个构造函数。

继承构造函数

C++98提供一种让名称空间中函数可用的语法。这让函数fn的所有重载版本都可用。

namespace box{
    int fn(){int};
    int fn(double){};
    int fn(const char*){};
}
using box::fn;

也可以使用这种方法让基类的所有非特殊成员函数对派生类可用。c2中的using对象可使用c1的三个fn()方法,但优先选择C2.

class c1{
        int fn(){int};
    int fn(double){};
    int fn(const char*){};
};
class c2 : public c1{
    using c1::fn;
    double fn(double){};
}

c++11提供一种让派生类能够继承基类构造函数的机制。当派生类中没有与之匹配的构造函数时,将使用基类版本的构造函数。

管理虚方法:override和final

假设基类声明了一个虚方法,在派生类中提供不同版本将覆盖旧版本。但如果特征标不匹配,将隐藏而不是覆盖旧版本。当派生类对象函数使用基类同名函数特征标,则不合法。

可使用虚说明符override指出要覆盖一个函数,编写函数时声明特征标如果和基类不匹配,编译器将视为错误。

virtual void f(char * ch) const override{};

如果想禁止派生类覆盖特定的虚方法,可在参数列表后面加上final。下面的代码禁止派生类重新定义函数f()。

virtual void f(char ch) const final{};

override和final并非关键字,而是具有特殊含义的标识符,意味着编译器根据上下文确定它们是否具有特殊含义。

lambda

一种定义和应用函数的数学系统。这个系统能够使用匿名函数。

[](int x){return x % 3 == 0;}
//这与以下定义很相似
bool f3(int x){return x % 3 == 0;}

差别有两个:

  • 使用 [] 替代了函数名
  • 没有声明返回类型。返回类型相当于使用decltpy根据返回值推断得到的。

可以这么使用lambda

count3 = std::count_if(numbers.begin(),numbers.end(),[](int x){return x % 3 == 0;});

仅当lambda表达式完全由一条返回语句组成时,自动类型推断才管用。否则,需要使用新增的返回类型后置语法。

[](double x)->double{int y = x; return x - y;}

[外链图片转存中…(img-FlydlLAO-1724483711206)]

包装器

C++提供了多个包装器。这些对象用于给其他编程接口提供更一致或更合适的接口。

[外链图片转存中…(img-VBejOHW1-1724483711206)]

模板function是在头文件functional中声明的,它从调用特征标的角度定义了一个对象,可用于包装调用特征标相同的函数指针、函数对象或lambda表达式。

std::function<double(char, int)> fdci;

可以将接受一个char参数和一个int参数,并返回一个double值得任何函数指针、函数对象或lambda表达式赋给它。

这样做避免了同一特性标不同参数的多次实例化。

可变参数模板

可变参数模板可以创建这样的模板函数和模板类,即可接受可变数量的参数。

要创建可变参数模板,需要理解几个要点:

  • 模板参数包
  • 函数参数包
  • 展开参数包
  • 递归

C++11提供了一个用省略号表示的元运算符,能够声明表示模板参数包的标识符,模板参数包基本上是一个类型列表。同样,它还能声明表示函数参数包的标识符,而函数参数包基本上是一个值列表。

template<typename... Args>
void show_list(Args...args){
    
}

Args是一个模板参数包,而args是一个函数参数包。可将这些参数包的名称指定为任何符合C++标识符规则的名称。

Args和T的区别在于,T与一种类型匹配,而Args与任何数量(包括0)的类型匹配。

在使用时,应使用递归。将函数参数包展开,对列表的第一项进行处理,再将余下的内容传递给递归调用,直到列表为空。确保递归终止很重要。

template<typename T, typename... Args> //特殊处理1个和0个元素的情况
void show_list(){};
    
template<typename T, typename... Args>
void show_list(const T& value){       //使用引用提高效率
    cout << value << endl;
};

template<typename T, typename... Args>
void show_list(const T& value, const Args&...args){
    cout << value << ',';
    show_list(args...);//可将省略号放在函数参数包名的右边,将参数包展开
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值