笔记按照中国大学MOOC上北京大学郭炜老师主讲的程序设计与算法(三)C++面向对象程序设计所作,B站上也有资源。原课程链接如下:
其他各章节链接如下:
程序设计与算法(三)C++面向对象程序设计笔记 第一周 从C到C++
程序设计与算法(三)C++面向对象程序设计笔记 第二周 类和对象基础
程序设计与算法(三)C++面向对象程序设计笔记 第三周 类和对象提高
程序设计与算法(三)C++面向对象程序设计笔记 第四周 运算符重载
程序设计与算法(三)C++面向对象程序设计笔记 第五周 继承
程序设计与算法(三)C++面向对象程序设计笔记 第六周 多态
程序设计与算法(三)C++面向对象程序设计笔记 第七周 输入输出和模板
程序设计与算法(三)C++面向对象程序设计笔记 第八周 标准模板库STL(一)
程序设计与算法(三)C++面向对象程序设计笔记 第九周 标准模板库STL(二)
程序设计与算法(三)C++面向对象程序设计笔记 第十周 C++11新特性和C++高级主题
输入输出和模板
输入输出相关的类
与输入输出流操作相关的类
istream 是用于输入的流类,cin 就是该类的对象
ostream 是用于输出的流类,cout 就是该类的对象
ifstream 是用于从文件读取数据的类
ofstream 是用于向文件写入数据的类
iostream 是既能用于输入,又能用于输出的类
fstream 是既能从文件读取数据,又能向文件写入数据的类
标准流对象
输入流对象:cin 与标准输入设备相连
输出流对象:cout 与标准输出设备相连,cerr 与标准错误输出设备相连,clog 与标准错误输出设备相连
缺省情况下
cerr << "Hello,world" << endl;
clog << "Hello,world" << endl;
和
cout << “Hello,world” << endl;
一样
cin 对应于标准输入流,用于从键盘读取数据,也可以被重定向为从文件中读取数据
cout 对应于标准输出流,用于向屏幕输出数据,也可以被重定向为向文件写入数据
cerr、clog 对应于标准错误输出流,用于向屏幕输出出错信息
cerr 和 clog 的区别在于 cerr 不使用缓冲区,直接向显示器输出信息;而输出到 clog 中的信息先会被存放在缓冲区,缓冲区满或者刷新时才输出到屏幕
输出重定向
#include <iostream>
using namespace std;
int main() {
int x,y;
cin >> x >> y;
freopen("test.txt","w",stdout);//将标准输出重定向到test.txt文件
if( y == 0 ) //除数为0则在屏幕上输出错误信息
cerr << "error." << endl;
else
cout << x / y ; //输出结果到 test.txt
return 0;
}
cout 代表标准输出设备,在缺省的情况下是屏幕,交给 cout 输出的内容会出现在屏幕上。通过 freopen(“test.txt”, “w”, stdout) 这条语句把标准输出设备 stdout 重定向到 test.txt,这种情况下交给 cout 输出的内容就会出现在 test.txt 文件里而不会出现在屏幕上
cerr 并没有被重定向,程序中间可能要输出一些调试信息,调试信息并不希望出现在文件里面,交给 cerr 输出到屏幕上
输入重定向
#include <iostream >
using namespace std;
int main() {
double f; int n;
freopen(“t.txt”,“r”,stdin); //cin被改为从t.txt中读取数据
cin >> f >> n;
cout << f << "," <<n << endl;
return 0;
}
t.txt:
3.14 123
输出:
3.14,123
stdin 代表标准输入设备,被重定向以后再从 cin 读取数据就不会停下来等待用户从键盘敲入数据而是直接从文件里面读取数据
判断输入流结束
用来输入数据的地方统称为输入流,可以是键盘也可以是文件,可以把标准输入重定向从文件里面读取数据
从文件里面输入,怎么知道文件的数据都已经读完?
可以用如下方法判输入流结束:
int x;
while(cin>>x){
......
}
return 0;
当输入流结束时,cin >> x 的返回值是 false,循环结束
之前讲过右移运算符在 istream 类里重载,返回值是 istream & , 即
istream & operator>>(int a)
{
......
return *this;
}
cin >> x 的返回值应该是 istream &,实际上是 cin &,也就是 cin。 虽然返回值是 cin , 但是在 istream 里有一个强制类型转换运算符的重载,可以把 cin 对象强制转换成 bool 类型的值
如果是从文件输入,比如前面有
freopen(“some.txt”,”r”,stdin);
那么,读到文件尾部,输入流就算结束
如果从键盘输入,则在单独一行输入 Ctrl+Z 代表输入流结束
istream 类的成员函数
istream & getline(char * buf, int bufSize);
从输入流中读取 bufSize-1 个字符到缓冲区 buf , 或读到碰到 ‘\n’ 为止(哪个先到算哪个)
istream & getline(char * buf, int bufSize,char delim);
从输入流中读取 bufSize-1 个字符到缓冲区 buf , 或读到碰到 delim 字符为止(哪个先到算哪个)
两个函数都会自动在 buf 中读入数据的结尾添加 ‘\0’。‘\n’ 或 delim 都不会被读入 buf,但会被从输入流中取走。如果输入流中 ‘\n’ 或 delim 之前的字符个数达到或超过了 bufSize 个,就导致读入出错,其结果就是:虽然本次读入已经完成,但是之后再用 cin 做任何读取操作就都会失败了
可以用
if(!cin.getline(...))
判断输入是否结束
回车位于输入流里面,并不会由于执行 cin >> x 被读出来
bool eof();
判断输入流是否结束
int peek();
返回输入流里的下一个字符,但不从流中去掉
istream & putback(char c);
将字符 ch 放回输入流
istream & ignore(int nCount = 1, int delim = EOF);
从流中删掉最多 nCount 个字符,遇到 EOF 时结束,EOF代表输入流的结束,一般值为-1
流操纵算子
整数流的基数:流操纵算子 dec, oct, hex, setbase
浮点数的精度(precision , setprecision)
设置域宽(setw, width)
用户自定义的流操纵算子
使用流操纵算子需要
#include <iomanip>
控制整数流的基数
指定输出整数时以多少进制的形式输出
hex、dec、oct 这种流操纵算子长效,一旦设置以后就一直起作用,直到用另外一个流操纵算子去替代原先设置好的流操纵算子
控制浮点数精度
precision, setprecision
precision 是 ostream 的成员函数,其调用方式为:
cout.precision(5);
setprecision 是流操作算子,其调用方式为:
cout << setprecision(5); //可以连续输出
setpresion 是连续起作用的,设置一次以后后面输出多个浮点数都按照这个精度
它们的功能相同:
指定输出浮点数的有效位数(非定点方式输出时)
指定输出浮点数的小数点后的有效位数(定点方式输出时)
定点方式:小数点必须出现在个位数后面
缺省的情况下是非定点方式
这两种情况在要输出的数位数太多时都采取四舍五入
n 是整数,输出整数不受 setpresion 这种流操纵算子的影响
流操纵算子 setiosflags(ios::fixed) 设置 ios 流里面的一个 fixed 标记
设置域宽的流操纵算子
setw, width 两者功能相同,一个是成员函数,另一个是流操作算子,调用方式不同:
cin >> setw(4);
cout << setw(4);
cin.width(5);
cout.width(5);
设置输入宽度是5,就会只把‘1234’四个字符读到 string 里面,string 结尾还要放上一个 ‘\0’
cout.width(w++) 指定输出宽度是 w,w 是4,然后把 w 加1。输出宽度只有超过了实际要输出的字符数时才会用空格等字符补齐,少于则不起作用
cin.width(5) 再次指定输入宽度是5
‘5678’四个字符被读到 string 里面,输出 string 时指定输出宽度是5,而 string 里面只有4个字符,输出的结果就会在 ‘5678’ 前面补一个空格
流操纵算子 fixed 表示之后以定点的方式输出浮点数
流操纵算子 scientific 表示一定要用科学计数法输出
流操纵算子 showpos 如果输出非负数就要显示正号
setfill(‘*’) 表示宽度不足时用 ‘*’ 号填补
5) 中小数点后保留5位有效数字是在刚才指定的
noshowpos 表示非负数不显示正号
left 表示左对齐,如果宽度不够右边就用填充字符填充,填充字符 ‘*’ 前面已经设置好了一直有效,宽度每次都得设
right 表示右对齐
internal 表示填充字符要放在负号和数值的中间
用户自定义流操纵算子
用户自定义流操纵算子实际上就是一个函数
这个函数的名字随便取,返回值和参数都必须是 ostream & 且参数只有一个,内部可以用 ostream & 比如 output 进行一些输出
tab 是一个函数,函数可以交给 cout 输出,这个函数就称作流操纵算子
为什么可以把一个函数的名字写在这?
因为 iostream 里对 << 进行了重载,重载为 ostream 类的一个成员函数
ostream & operator<<( ostream & ( * p ) ( ostream & ) ) ;
这个重载函数的参数是一个函数指针,这个函数指针指向的函数返回值为 ostresam &,而且这个函数只有一个参数,也是 ostream &
‘<<’ 运算符在执行的过程中会调用指针 p 所指向的函数,且以 * this 也就是这个 ostream 对象,一般来说就是 cout 作为参数
hex、dec、oct 都是函数
上面 cout << “aa” 表达式的结果仍然是 cout,cout << tab 实际上就调用了在 ostream 里面重载的 “<<” 运算符,“<<”运算符在执行的过程中会以 *this 即 cout 作为参数调用 “<<” 运算符的参数 p 所指向的函数 tab ,tab 函数名跟参数 p 类型匹配
文件读写
在 C++ 里面文件和前面提到的流可以认为是一回事,因为可以将顺序文件看作一个有限字符构成的顺序字符流,然后像对 cin, cout 一样的读写
回顾一下输入输出流的结构层次:
创建文件
包含头文件
#include <fstream>
可以通过定义一个 ofstream 类型的对象,在构造函数里面给出参数创建文件
ofstream outFile(“clients.dat”, ios::out|ios::binary);
- “clients.dat” 要创建的文件的名字
- ios::dat 文件打开方式
- ios::out 输出到文件,删除原有内容
- ios::app 输出到文件,保留原有内容,总是在尾部添加
- ios::binary 以二进制文件格式打开文件
ios::out、ios::binary 是在 ios 类里面预先定义好的一些静态常量,这些标识实际上都是只有某一个比特为1的整数,不同的标识为1的比特不一样,就可以把一些标识用或运算或起来表示同时设上若干个标识
也可以先创建 ofstream 对象,再用它的 open 成员函数打开文件来创建文件
ofstream fout;
fout.open("test.out",ios::out|ios::binary);
判断打开是否成功:
if(!fout){
cout << “File open error!”<<endl;
}
fout 是一个 ofstream 类的对象,“!” 能够作用在它上面说明“!”经过了重载
文件名可以给出绝对路径,也可以给相对路径,比如上一层的某文件夹里面。没有交代路径信息,就是在当前文件夹下找文件
程序刚运行时,这个程序的可执行文件所在的文件夹就是当前文件夹,一个程序的当前文件夹在程序运行期间是可以改变的
文件名的绝对路径和相对路径
绝对路径:包括盘符
“c:\\tmp\\mydir\\some.txt”,C++里面这个斜杠得写两遍
相对路径:
“\\tmp\\mydir\\some.txt”
当前盘符的根目录下的 tmp\\dir\\some.txt。当前盘符有可能是C盘有可能是D盘
“tmp\\mydir\\some.txt”
当前文件夹的 tmp 子文件夹里面的 …
…\\tmp\\mydir\\some.txt
当前文件夹的父文件夹下面的 tmp 子文件夹里面的 …。… 代表当前文件夹的上一层文件夹,可以连用
…\\…\\tmp\\mydir\\some.txt
当前文件夹的父文件夹的父文件夹下面的 tmp 子文件夹里面的 …
文件的读写指针
文件的读写是通过 ifstream 或者 ofstream 的一些成员函数进行的,这些函数本身并没有带位置信息,不会指出在什么位置进行读写,在什么位置进行读写由读写指针指定
对于输入文件,有一个读指针
对于输出文件,有一个写指针
对于输入输出文件,有一个读写指针
标识文件操作的当前位置,该指针在哪里,读写操作就在哪里进行。这里的指针概念和 C 语言里面那种指针有一点不一样,它并不是一个指针类型
ofstream fout("a1.out",ios::app); //以添加方式打开
long location = fout.tellp(); //取得写指针的位置
location = 10; //location可以为负值
fout.seekp(location); //将写指针移动到第10个字节处
fout.seekp(location,ios::beg); //从头数location
fout.seekp(location,ios::cur); //从当前位置数location
fout.seekp(location,ios::end); //从尾部数location
ifstream fin(“a1.in”,ios::ate);
//打开文件,定位文件指针到文件尾
long location = fin.tellg(); //取得读指针的位置
location = 10L;
fin.seekg(location); //将读指针移动到第10个字节处
fin.seekg(location,ios::beg); //从头数location
fin.seekg(location,ios::cur); //从当前位置数location
fin.seekg(location,ios::end); //从尾部数location
C++ 里面并没有标准的库函数直接获得文件的长度,想要获得文件的长度就得打开文件把文件的读指针定位到文件尾,然后调用 ifstream 类的成员函数 tellg() 获取读指针的位置,所谓的位置就是距离文件开头多少字节
显式关闭文件
ifstream fin(“test.dat”,ios::in);
fin.close();
ofstream fout(“test.dat”,ios::out);
fout.close();
由于硬盘的访问速度比内存慢很多,现代操作系统要往文件里面写入数据时首先把数据存在内存里面的一个缓冲区,缓冲区满了再写入硬盘。写入一个文件没有最终关闭这个文件,写入的内容可能有一部分就还在内存里面并没有被写到硬盘上
此外在许多操作系统里面一个应用程序所能打开的文件数目是有上限的,如果不停地打开文件不关闭到了一定程度程序就打不开任何文件
字符文件读写
字符文件通常被称为文本文件,可以在 windows 的记事本里面打开且不是乱码
因为文件流也是流,所以流的成员函数和流操作算子也同样适用于文件流
写一个程序,将文件 in.txt 里面的整数排序后,输出到 out.txt
例如,若 in.txt 的内容为:1 234 9 45 6 879。则执行本程序后,生成的 out.txt 的内容为:1 6 9 45 234 879
#include <iostream>
#include <fstream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
vector<int> v;
ifstream srcFile("in.txt",ios::in);
ofstream destFile("out.txt",ios::out);
int x;
while( srcFile >> x )
v.push_back(x);
sort(v.begin(),v.end());
for( int i = 0;i < v.size();i ++ )
destFile << v[i] << " ";
destFile.close();
srcFile.close();
return 0;
}
因为是纯文本文件就不需要以二进制方式打开
把文件流当作 cin 和 cout 来用,读取一个个整数的方式就跟从标准输入里面用 cin 去不停地读整数是一样的,只要文件里面的数据读完了,srcFile >> x 的值就是 false
本来 cout 就是 ostream 的对象,而 ofstream 又是 ostream 的派生类,所以 ostream 的成员函数在 ofstream 里面肯定都有,如“<<” 是 ostream 的成员函数,自然也是 ofstream 的成员函数,所以可以通过”<<“ 把 v[i] 输出到 destFile
二进制文件读写
二进制读文件:
ifstream 和 fstream 的成员函数:
istream& read (char* s, long n);
将文件读指针指向的地方的 n 个字节内容,读入到内存地址 s,然后将文件读指针向后移动 n 字节。一般不太用返回值(以 ios::in 方式打开文件时,文件读指针开始指向文件开头)
二进制写文件:
ofstream 和 fstream 的成员函数:
istream& write (const char* s, long n);
将内存地址 s 处的 n 个字节内容写入到文件中写指针指向的位置,然后将文件写指针向后移动 n 字节(以 ios::out 方式打开文件时,文件写指针开始指向文件开头。以 ios::app 方式打开文件时,文件写指针开始指向文件尾部)
在文件中写入和读取一个整数
#include <iostream>
#include <fstream>
using namespace std;
int main() {
ofstream fout("some.dat", ios::out | ios::binary);
int x=120;
fout.write( (const char *)(&x), sizeof(int) );
fout.close();
ifstream fin("some.dat",ios::in | ios::binary);
int y;
fin.read( (char * ) & y,sizeof(int) );
fin.close();
cout << y <<endl;
return 0;
}
&x 的类型是 int *,跟 write 要求的第一个参数类型 const char * 不匹配,需要强制转换一下
注意这里是用二进制方式读写,写入的是一个整数,占4B。不是用类似于 cout 那样的办法把 x 写到文本文件里面,那样这时候文件里面就应该是一个字符串“120”,一共3B
从键盘输入几个学生的姓名的成绩,并以二进制文件形式保存
在大多数情况下用二进制方式存文件比用纯文本方式存要节省空间,便于查找
#include <iostream>
#include <fstream>
using namespace std;
struct Student {
char name[20];
int score;
};
int main() {
Student s;
ofstream OutFile("c:\\tmp\\students.dat", ios::out| ios::binary);
while( cin >> s.name >> s.score )
OutFile.write( (char * ) & s, sizeof(s) );
OutFile.close();
return 0;
}
所谓的二进制文件就是里面的那些字符并不见得都是我们可识别的 ASCII 码,用记事本打开里面出现的结果可能就感觉是乱码
上面已经识别不出60、80、40,但能够识别出人的名字,因为人的名字本来也是以字符串的形式写入文件的,但是60、80、40这种写入文件时是以 int 的二进制形式而不是以“60”这种字符串形式写入的
将 students.dat 文件的内容读出并显示
#include <iostream>
#include <fstream>
using namespace std;
struct Student {
char name[20];
int score;
};
int main() {
Student s;
ifstream inFile("students.dat",ios::in | ios::binary);
if(!inFile) {
cout << "error" <<endl;
return 0;
}
while( inFile.read( (char * ) & s, sizeof(s) ) ) {
int readedBytes = inFile.gcount(); //看刚才读了多少字节
cout << s.name << " " << s.score << endl;
}
inFile.close();
return 0;
}
输出:
Tom 60
Jack 80
Jane 40
将 students.dat 文件的 Jane 的名字改成 Mike
#include <iostream>
#include <fstream>
using namespace std;
struct Student {
char name[20];
int score;
};
int main()
{
Student s;
fstream iofile("c:\\tmp\\students.dat", i
os::in | ios::out | ios::binary);
if(!iofile) {
cout << "error" ;
return 0;
}
iofile.seekp(2 * sizeof(s),ios::beg); //定位写指针到第三个记录
iofile.write("Mike",strlen("Mike")+1);
iofile.seekg(0,ios::beg); //定位读指针到开头
while( iofile.read( (char* ) & s, sizeof(s)) )
cout << s.name << " " << s.score << endl;
iofile.close();
return 0;
}
输出:
Tom 60
Jack 80
Mike 40
ifstream 和 ofstream 有的东西在 fstream 里面都有
strlen(“Mike”)+1是为了把结尾的 ‘\0’ 写进去,当然这里由于原来的“Jane”也是4个字母跟“Mike”一样长,加1并不是必需的
文件拷贝程序mycopy 示例
/*用法示例:
在命令行方式使用,能够实现文件的拷贝
mycopy src.dat dest.dat 即将 src.dat 拷贝到 dest.dat 如果 dest.dat 原来就有,则原来的文件会被覆盖 */
#include <iostream>
#include <fstream>
using namespace std;
int main(int argc, char * argv[])
{
if( argc != 3 ) {
cout << "File name missing!" << endl;
return 0;
}
ifstream inFile(argv[1], ios::binary | ios::in); //打开文件用于读
if(!inFile) {
cout << "Source file open error." << endl;
return 0;
}
ofstream outFile(argv[2], ios::binary | ios::out);//打开文件用于写
if(!outFile) {
cout << "New file open error." << endl;
inFile.close(); //打开的文件一定要关闭
return 0;
}
char c;
while( inFile.get(c) ) //每次读取一个字符
outFile.put(c); //每次写入一个字符
outFile.close();
inFile.close();
return 0;
}
get 是 istream 的一个成员函数,从流或者文件里面读取一个字节,参数是 char &。put 是 ostream 的一个成员函数,接收一个 char 类型参数
操作系统读写文件时都会使用内存缓冲区,哪怕读取一个字符实际也会把这个字符所在的硬盘扇区甚至包括相邻的好几个扇区整个读入内存,下次就不用再访问硬盘
二进制文件和文本文件的区别
本质上没有差别,都是由01串构成的。一个能够在记事本里面打开看,一个不能
在不同的操作系统下文本文件的换行字符有差别:
Linux,Unix 下的换行符号:‘\n’(ASCII码:0x0a)
Windows 下的换行符号:‘\r\n’ (ASCII码: 0x0d0a), endl 就是 ‘\n’
Mac OS下的换行符号:‘\r’ (ASCII码:0x0d)
导致 Linux,Mac OS 文本文件在 Windows 记事本中打开时不换行
Unix/Linux/Mac OS 下打开文件,用不用 ios::binary 没区别,实际上都是以 ios::binary 的方式打开
Windows 下打开文件,如果不用 ios::binary ,则:
读取文件时,所有的 ‘\r\n’ 会被当做一个字符 ‘\n’ 处理,即少读了一个字符 ‘\r’
写入文件时,写入单独的 ‘\n’ 时,系统自动在前面加一个 ‘\r’ ,即多写了一个 ‘\r’
注意这里并不是说想要写入一个换行符,而是要写入的数据里面正好有一个字节是 0x0a,而这个 0x0a 前面又没有 0x0d,Windows 操作系统认为不是用二进制方式打开文件就一定是文本方式打开,既然是文本方式打开,‘\n’ 一定会跟 ‘\r’ 一起用组成一个换行符,就会帮着加上
使用模板的程序设计就是泛型程序设计。C++ 里有函数模板和类模板
函数模板
基本概念
能否只写一个 Swap 函数,就能交换各种类型的变量?
用函数模板解决:
template <class 类型参数1, class 类型参数2, ......>
返回值类型 模板名 (形参表)
{
函数体
};
类型参数代表一种类型,是可变的。类型参数1、2 … 名字随便起,至少写一个类型参数
template <class T>
void Swap(T & x,T & y)
{
T tmp = x;
x = y;
y = tmp;
}
int main()
{
int n = 1,m = 2;
Swap(n,m); //编译器自动生成 void Swap(int & ,int &) 函数
double f = 1.2,g = 2.3;
Swap(f,g); //编译器自动生成 void Swap(double & ,double &) 函数
return 0;
}
把类型参数 T 替换为 int 得到 Swap 函数,Swap(n, m) 调用语句就变成了对自动生成的 Swap 函数的调用
编译器会把模板里面的类型参数替换成具体类型,到底替换成什么类型根据调用模板时实参的个数和类型,然后调用模板的语句实际上就变成了调用编译器自动生成的函数
由模板生成函数的过程称为模板的实例化,编译器编译到一条调用模板的语句时就会把模板实例化,由模板生成的函数称为模板函数
函数模板中可以有不止一个类型参数
template <class T1, class T2>
T2 print(T1 arg1, T2 arg2)
{
cout << arg1 << " " << arg2 << endl;
return arg2;
}
求数组最大元素的 MaxElement 函数模板
template <class T>
T MaxElement(T a[], int size) //size是数组元素个数
{
T tmpMax = a[0];
for( int i = 1;i < size;++i )
if( tmpMax < a[i] )
tmpMax = a[i];
return tmpMax;
}
不通过参数实例化函数模板,显式地指明把模板里面的类型参数替换成什么
#include <iostream>
using namespace std;
template <class T>
T Inc(T n)
{
return 1 + n;
}
int main()
{
cout << Inc<double>(4)/2; //输出 2.5
return 0;
}
写模板时不用关心’+‘是否被重载,使用模板时如果没有被正确重载就有可能引发编译错误
Inc<double> 使得编译器从 Inc 模板实例化出一个函数
函数模板的重载
函数模板可以重载,只要它们的形参表或类型参数表不同即可
template <class T1, class T2>
void print(T1 arg1, T2 arg2) {
cout<< arg1 << " "<< arg2 << endl;
}
template <class T>
void print(T arg1, T arg2) {
cout<< arg1 << " "<< arg2 << endl;
}
template <class T,class T2>
void print(T arg1, T arg2) {
cout<< arg1 << " "<< arg2 << endl;
}
函数模板和函数的次序
在有多个函数和函数模板名字相同的情况下,编译器如下处理一条函数调用语句
1.先找参数完全匹配的普通函数(非由模板实例化而得的函数)
2.再找参数完全匹配的模板函数
3.再找实参数经过自动类型转换后能够匹配的普通函数
4.上面的都找不到,则报错
template <class T>
T Max(T a, T b) {
cout << "TemplateMax" <<endl;
return 0;
}
template <class T,class T2>
T Max(T a, T2 b) {
cout << "TemplateMax2" <<endl;
return 0;
}
double Max(double a, double b){
cout << "MyMax" << endl;
return 0;
}
int main() {
int i=4, j=5;
Max(1.2,3.4); //输出MyMax
Max(i, j); //输出TemplateMax
Max(1.2, 3); //输出TemplateMax2
return 0;
}
匹配模板函数时,不进行类型自动转换
template <class T>
T myFunction(T arg1, T arg2)
{
cout << arg1 << " " << arg2 << "\n" ;
return arg1;
}
myFunction(5, 7); //ok:replace T with int
myFunction(5.8, 8.4); //ok:replace T with double
myFunction(5, 8.4); //error,no matching function for call to 'myFunction(int, double)'
函数模板示例:Map
Map 把以 s 为起点,e 为终点的区间里的每一个元素通过 op 做变换,把变换结果拷贝到一个起点为 x 的目标区间。e 这个地方的元素不参与运算
#include <iostream>
using namespace std;
template<class T,class Pred>
void Map(T s, T e, T x, Pred op)
{
for(; s != e; ++s,++x) {
*x = op(*s);
}
}
int Cube(int x) { return x * x * x; }
double Square(double x) { return x * x; }
int a[5] = {1,2,3,4,5}, b[5];
double d[5] = { 1.1,2.1,3.1,4.1,5.1}, c[5];
int main() {
Map(a,a+5,b,Square);
for(int i = 0;i < 5; ++i) cout << b[i] << ",";
cout << endl;
Map(a,a+5,b,Cube);
for(int i = 0;i < 5; ++i) cout << b[i] << ",";
cout << endl;
Map(d,d+5,c,Square);
for(int i = 0;i < 5; ++i) cout << c[i] << ",";
cout << endl;
return 0;
}
输出:
1,4,9,16,25,
1,8,27,64,125,
1.21,4.41,9.61,16.81,26.01,
编译器处理到这条模板调用语句时
Map(a, a+5, b, Square);
就会从 Map 模板实例化出以下函数:
void Map(int * s, int * e, int * x, double ( *op)(double)) {
for(; s != e; ++s,++x) {
*x = op(*s);
}
}
Map 实例化出的函数最后一个参数是 op
Square 跟 op 对应,Square 是一个函数名,函数名和函数指针类型可以匹配,pred 类型参数会被替换成跟函数名匹配的函数指针类型。double ( *op)(double) 是一个函数指针类型,所指向的函数返回值是 double,有一个类型为 double 的参数
在函数体内部把 *s 作为参数调用函数指针 op 所指向的函数 Square,把函数调用的结果赋值给 *x
类模板
基本概念
为了多快好省地定义出一批相似的类,可以定义类模板,然后由类模板生成不同的类
类模板:在定义类的时候,加上一个/多个类型参数。在使用类模板时,指定类型参数应该如何替换成具体类型,编译器据此生成相应的模板类
template <class 类型参数1, class 类型参数2, ......> //类型参数表
class 类模板名
{
成员函数和成员变量
};
template <typename 类型参数1, typename 类型参数2, ......>//类型参数表
class 类模板名
{
成员函数和成员变量
};
写函数模板时类型参数表的 class 也可以写成 typename
类模板里成员函数的写法
template <class 类型参数1, class 类型参数2, ......> //类型参数表
返回值类型 类模板名<类型参数名列表>::成员函数名(参数表)
{
......
}
用类模板定义对象的写法
类模板名 <真实类型参数表> 对象名(构造函数实参表);
这个对象所属的类是由类模板实例化出来的
类模板示例: Pair类模板
template <class T1,class T2>
class Pair
{
public:
T1 key; // 关键字
T2 value; // 值
Pair(T1 k,T2 v):key(k),value(v) { };
bool operator < ( const Pair<T1,T2> & p ) const;
};
template <class T1,class T2>
bool Pair<T1,T2>::operator < ( const Pair<T1,T2> & p) const
// Pair 的成员函数 operator <
{
return key < p.key;
}
int main()
{
Pair<string,int> student("Tom",19);
//实例化出一个类 Pair<string,int>
cout << student.key << " " << student.value;
return 0;
}
输出:
Tom 19
const Pair<T1, T2> & p 是同类的另一个对象的引用。Pair<T1, T2> 代表一种类型
把 operator < 这个成员函数拿到类模板外面写,把类型参数表照抄。写成员函数时前面得加一个类的名字 Pair<T1, T2>
用类模板定义对象
编译器由类模板生成类的过程叫类模板的实例化。由类模板实例化得到的类,叫模板类
同一个类模板的两个模板类是不兼容的
Pair<string, int> * p;
Pair<string, double> a;
p = &a; //wrong
函数模版作为类模板成员
#include <iostream>
using namespace std;
template <class T>
class A
{
public:
template <class T2>
void Func(T2 t) { cout << t; } //成员函数模板
};
int main()
{
A<int> a;
a.Func('K'); //成员函数模板Func被实例化
a.Func("hello"); //成员函数模板Func再次被实例化
return 0;
}
输出:
KHello
a.Func(‘K’) 这条语句会导致编译器去 A 模板里面寻找成员函数 Func,但只能找到一个成员函数模板 Func,编译器就会把这个成员函数模板实例化成 A<int> 类的成员函数,T2 被替换成 char
A<int> 类有两个名叫 Func 的成员函数,一个参数是 char,另一个参数是 const char *
类模板与非类型参数
类模板的 “<类型参数表>” 中可以出现非类型参数:
template <class T, int size>
class CArray{
T array[size];
public:
void Print( )
{
for( int i = 0;i < size; ++i)
cout << array[i] << endl;
}
};
CArray<double,40> a2;
CArray<int,50> a3; //a2和a3属于不同的类
size 是非类型参数,因为真正把这个模板实例化时非类型参数不用具体的类型而用一个数替代
类模板与派生
类模板与继承
- 类模板从类模板派生
- 类模板从模板类派生
- 类模板从普通类派生
- 普通类从模板类派生
类模板从类模板派生
template <class T1,class T2>
class A {
T1 v1; T2 v2;
};
template <class T1,class T2>
class B:public A<T2,T1> {
T1 v3; T2 v4;
};
template <class T>
class C:public B<T,T> {
T v5;
};
int main() {
B<int,double> obj1;
C<int> obj2;
return 0;
}
通过 B<int, double> 类名编译器实例化出两个类:
class B<int,double>: public A<double,int>
{
int v3; double v4;
};
class A<double, int>
{
double v1; int v2;
};
类模板从模板类派生
template <class T1,class T2>
class A {
T1 v1; T2 v2;
};
template <class T>
class B:public A<int,double> {
T v;
};
int main() {
B<char> obj1; //自动生成两个模板类:A<int,double> 和 B<char>
return 0;
}
类模板从普通类派生
class A {
int v1;
};
template <class T>
class B:public A { //所有从B实例化得到的类,都以A为基类
T v;
};
int main() {
B<char> obj1;
return 0;
}
普通类从模板类派生
template <class T>
class A {
T v1;
int n;
};
class B:public A<int> {
double v;
};
int main() {
B obj1;
return 0;
}
类模板与友元
类模板与友元
- 函数、类、类的成员函数作为类模板的友元
- 函数模板作为类模板的友元
- 函数模板作为类的友元
- 类模板作为类模板的友元
函数、类、类的成员函数作为类模板的友元
void Func1() { }
class A { };
class B
{
public:
void Func() { }
};
template <class T>
class Tmpl
{
friend void Func1();
friend class A;
friend void B::Func();
}; //任何从Tmp1实例化来的类,都有以上三个友元
函数模板作为类模板的友元
#include <iostream>
#include <string>
using namespace std;
template <class T1,class T2>
class Pair
{
private:
T1 key; //关键字
T2 value; //值
public:
Pair(T1 k,T2 v):key(k),value(v) { };
bool operator< (const Pair<T1,T2> & p) const;
template <class T3,class T4>
friend ostream & operator<< (ostream & o,
const Pair<T3,T4> & p);
};
template<class T1,class T2>
bool Pair<T1,T2>::operator < ( const Pair<T1,T2> & p ) const
{ //"小"的意思就是关键字小
return key < p.key;
}
template <class T1,class T2>
ostream & operator<< (ostream & o,const Pair<T1,T2> & p)
{
o << "(" << p.key << "," << p.value << ")" ;
return o;
}
int main()
{
Pair<string,int> student("Tom",29);
Pair<int,double> obj(12,3.14);
cout << student << " " << obj;
return 0;
}
输出:
(Tom,29) (12,3.14)
写出 cout << student << " " << obj 这条语句时编译器就从 operator << 函数模板实例化出两个重载”<<“运算符函数,这两个函数分别是 Pair<string, int> 和 Pair<int, double> 的友元
任意从函数模板
template <class T1,class T2>
ostream & operator<< ( ostream & o, const Pair<T1,T2> & p )
生成的函数,都是任意 Pair 模板类的友元
函数模板作为类的友元
#include <iostream>
using namespace std;
class A
{
int v;
public:
A(int n):v(n) { }
template <class T>
friend void Print(const T & p);
};
template <class T>
void Print(const T & p)
{
cout << p.v;
}
int main() {
A a(4);
Print(a);
return 0;
}
输出:
4
Print(a) 编译器会从 Print 模板实例化出一个 Print 函数,T 被 A 替换。Print 函数是 class A 的友元
所有从
template <class T>
void Print(const T & p)
生成的函数,都成为 A 的友元
但是自己写的函数
void Print(int a) { }
不会成为 A 的友元
类模板作为类模板的友元
#include <iostream>
using namespace std;
template <class T>
class B {
T v;
public:
B(T n):v(n) { }
template <class T2>
friend class A;
};
template <class T>
class A {
public:
void Func( ) {
B<int> o(10);
cout << o.v << endl;
}
};
int main()
{
A<double> a;
a.Func ();
return 0;
}
输出:
10
A<double> 类,成了 B<int> 类的友元。任何从 A 模版实例化出来的类,都是任何 B 实例化出来的类的友元
类模板与静态成员变量
类模板中可以定义静态成员,那么从该类模板实例化得到的所有类,都包含同样的静态成员
#include <iostream>
using namespace std;
template <class T>
class A
{
private:
static int count;
public:
A() { count ++; }
~A() { count -- ; };
A( A & ) { count ++ ; }
static void PrintCount() { cout << count << endl; }
};
template<> int A<int>::count = 0;
template<> int A<double>::count = 0;
int main()
{
A<int> ia;
A<double> da;
ia.PrintCount();
da.PrintCount();
return 0;
}
输出:
1
1
这些不同模板类的静态成员尽管名字一样,但肯定放在内存的不同位置
同样要把从包含静态成员的类模板实例化的类的静态成员变量拿到外面声明一下,声明的同时可以初始化也可以不初始化