程序设计与算法(三)C++面向对象程序设计笔记 第七周 输入输出和模板

news/2024/5/21 0:14:36/文章来源:https://blog.csdn.net/zimuzi2019/article/details/127424401

笔记按照中国大学MOOC上北京大学郭炜老师主讲的程序设计与算法(三)C++面向对象程序设计所作,B站上也有资源。原课程链接如下:

程序设计与算法(三)C++面向对象程序设计

其他各章节链接如下:

程序设计与算法(三)C++面向对象程序设计笔记 第一周 从C到C++

程序设计与算法(三)C++面向对象程序设计笔记 第二周 类和对象基础

程序设计与算法(三)C++面向对象程序设计笔记 第三周 类和对象提高

程序设计与算法(三)C++面向对象程序设计笔记 第四周 运算符重载

程序设计与算法(三)C++面向对象程序设计笔记 第五周 继承

程序设计与算法(三)C++面向对象程序设计笔记 第六周 多态

程序设计与算法(三)C++面向对象程序设计笔记 第七周 输入输出和模板

程序设计与算法(三)C++面向对象程序设计笔记 第八周 标准模板库STL(一)

程序设计与算法(三)C++面向对象程序设计笔记 第九周 标准模板库STL(二)

程序设计与算法(三)C++面向对象程序设计笔记 第十周 C++11新特性和C++高级主题

其他各科笔记汇总

输入输出和模板

输入输出相关的类

与输入输出流操作相关的类

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wZ74MneH-1666238174597)(C++ 面向对象程序设计.assets/image-20221014202402074.png)]

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;elsecout << x / y ;          //输出结果到 test.txtreturn 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(...)) 

判断输入是否结束

 

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BnRTYRsd-1666238174600)(C++ 面向对象程序设计.assets/image-20221015150645141.png)]

回车位于输入流里面,并不会由于执行 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>

控制整数流的基数

指定输出整数时以多少进制的形式输出

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZAlsueZ1-1666238174601)(C++ 面向对象程序设计.assets/image-20221015150731911.png)]

hex、dec、oct 这种流操纵算子长效,一旦设置以后就一直起作用,直到用另外一个流操纵算子去替代原先设置好的流操纵算子

控制浮点数精度

precision, setprecision

 

precision 是 ostream 的成员函数,其调用方式为:

cout.precision(5);

setprecision 是流操作算子,其调用方式为:

cout << setprecision(5); //可以连续输出

setpresion 是连续起作用的,设置一次以后后面输出多个浮点数都按照这个精度

 

它们的功能相同:

指定输出浮点数的有效位数(非定点方式输出时)

指定输出浮点数的小数点后的有效位数(定点方式输出时)

定点方式:小数点必须出现在个位数后面

缺省的情况下是非定点方式

这两种情况在要输出的数位数太多时都采取四舍五入

 

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WDlTtfMG-1666238174604)(C++ 面向对象程序设计.assets/image-20221015150844902.png)]

n 是整数,输出整数不受 setpresion 这种流操纵算子的影响

 

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UgmPEiGf-1666238174609)(C++ 面向对象程序设计.assets/image-20221015150910047.png)]

流操纵算子 setiosflags(ios::fixed) 设置 ios 流里面的一个 fixed 标记

 

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UKsP8nQj-1666238174612)(C++ 面向对象程序设计.assets/image-20221015150926081.png)]

设置域宽的流操纵算子

setw, width 两者功能相同,一个是成员函数,另一个是流操作算子,调用方式不同:

cin >> setw(4); 
cout << setw(4); 
cin.width(5);
cout.width(5);

 

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FmASs3L7-1666238174614)(C++ 面向对象程序设计.assets/image-20221015150958780.png)]

设置输入宽度是5,就会只把‘1234’四个字符读到 string 里面,string 结尾还要放上一个 ‘\0’

cout.width(w++) 指定输出宽度是 w,w 是4,然后把 w 加1。输出宽度只有超过了实际要输出的字符数时才会用空格等字符补齐,少于则不起作用

cin.width(5) 再次指定输入宽度是5

‘5678’四个字符被读到 string 里面,输出 string 时指定输出宽度是5,而 string 里面只有4个字符,输出的结果就会在 ‘5678’ 前面补一个空格

 

 

 

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ECrPbawz-1666238174615)(C++ 面向对象程序设计.assets/image-20221015153625415.png)]

流操纵算子 fixed 表示之后以定点的方式输出浮点数

流操纵算子 scientific 表示一定要用科学计数法输出

 

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UE7UKTMK-1666238174618)(C++ 面向对象程序设计.assets/image-20221015153654022.png)]

流操纵算子 showpos 如果输出非负数就要显示正号

setfill(’*’) 表示宽度不足时用 ‘*’ 号填补

5) 中小数点后保留5位有效数字是在刚才指定的

noshowpos 表示非负数不显示正号

left 表示左对齐,如果宽度不够右边就用填充字符填充,填充字符 ‘*’ 前面已经设置好了一直有效,宽度每次都得设

right 表示右对齐

internal 表示填充字符要放在负号和数值的中间

用户自定义流操纵算子

用户自定义流操纵算子实际上就是一个函数

这个函数的名字随便取,返回值和参数都必须是 ostream & 且参数只有一个,内部可以用 ostream & 比如 output 进行一些输出

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0NWVYPYQ-1666238174619)(C++ 面向对象程序设计.assets/image-20221015153711253.png)]

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 一样的读写

回顾一下输入输出流的结构层次:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iLh2ymE2-1666238174622)(C++ 面向对象程序设计.assets/image-20221015171730494.png)]

创建文件

包含头文件

#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", ios::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.5return 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); //输出MyMaxMax(i, j);     //输出TemplateMaxMax(1.2, 3);  //输出TemplateMax2return 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

 

这些不同模板类的静态成员尽管名字一样,但肯定放在内存的不同位置

同样要把从包含静态成员的类模板实例化的类的静态成员变量拿到外面声明一下,声明的同时可以初始化也可以不初始化

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_404077.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

《CTF攻防世界web题》之我什么都不会(1)

前言 &#x1f340;作者简介&#xff1a;被吉师散养、喜欢前端、学过后端、练过CTF、玩过DOS、不喜欢java的不知名学生。 &#x1f341;个人主页&#xff1a;被吉师散养的职业混子 &#x1fad2;文章目的&#xff1a;记录唯几我能做上的题 &#x1f342;相应专栏&#xff1a;CT…

[附源码]Java计算机毕业设计SSM高校创新学分申报管理系统

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

从初级进阶为高级程序员,需要经历什么?

“怎样才能成为一名高级程序员&#xff1f;” 对每个新人来说&#xff0c;大概都梦想着尽快实现职场进阶&#xff0c;从萌新成长为技术大佬。但媳妇熬成婆&#xff0c;世界上最难熬的就是工作经验…… 程序员具有明确的职业等级制度&#xff0c;依据工作经验和水平划分&#x…

vite+vue3+ts项目搭建之集成qiankun让其成为子应用模板,并能实现主子应用之间跳转切换(新增在线预览地址)

前言 以下操作&#xff0c;是续接之前 第四步 ——即&#xff1a;vitevue3tspiniaelement-plus项目已完成搭建好&#xff0c;可以直接业务开发了 主应用技术栈&#xff1a;vue2webpackjs 集成qiankun(微前端) 1、安装vite-plugin-qiankun npm install vite-plugin-qiankun2、…

Gradle 入门说难也不难,说简单吧也不简单~

在学习过程中最痛苦的事&#xff0c;并不是认为自己坚持不下去学不会&#xff0c;而是对某块知识点的碎片信息学习了很多&#xff0c;仍然无法窥其门径&#xff0c;处于懵懂状态。 就拿Gradle来说&#xff0c;我之前就是这种状态&#xff0c;不管怎么去查阅文档和视频&#xf…

联邦学习(Federated Learning):技术角度的理解

联邦学习&#xff08;Federated Learning&#xff09;&#xff1a;技术角度的理解 学习笔记 B站学习链接&#xff1a;https://www.bilibili.com/video/BV1YK4y1G7jw/?p7&vd_source7def3d3fc89c6921c7aeadf5e4023d35 1.背景与动机 例子 Example >> 问题&#xff1a…

AES加密解密算法设计(C++)

目 录 1&#xff0e; 背景与意义 4 2. 系统设计 5 2.1系统主要目标 5 2.2主要软件需求&#xff08;运行环境&#xff09; 5 2.3功能模块与系统结构 6 3 系统功能程序设计 8 3.1基本要求部分 8 3.1.1 字节替换 8 3.1.2行移位 9 3.1.3列混合 11 3.1.4密钥加 13 3.1.5密钥扩展 14 …

神经网络训练多少次合适,神经网络训练时间多长

1、tensorflow 训练一个神经网络 需要多长时间 基本使用 使用 TensorFlow, 你必须明白 TensorFlow: 使用图 (graph) 来表示计算任务. 在被称之为 会话 (Session) 的上下文 (context) 中执行图. 使用 tensor 表示数据. 通过 变量 (Variable) 维护状态. 使用 feed 和 fetch 可以…

【附源码】计算机毕业设计SSM实验室设备明细管理系统

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

【自监督论文阅读笔记】Simmim: A simple framework formasked image modeling

本文介绍了 SimMIM&#xff0c;这是一个用于 掩码图像建模 的简单框架。本文简化了最近提出的相关方法&#xff0c;无需特殊设计&#xff0c;例如通过离散 VAE 或聚类 进行 block-wise 分块级的掩码 和 tokenization。为了研究 是什么让掩码图像建模任务学习良好的表示&#xf…

nginx----(1)nginx的单机安装

文章目录Nginx卸载开源版Nginx安装默认简单安装Nginx的源码复杂安装防火墙问题nginx启停安装成系统服务Nginx卸载 步骤一&#xff1a;需要将nginx的进程关闭 ./nginx -s stop步骤二:将安装的nginx进行删除 rm -rf /usr/local/nginx步骤三:将安装包之前编译的环境清除掉 mak…

opc client 客户端软件测试工具

一、概述&#xff1a; 本软件一款OPC客户端测试工具&#xff0c;使用本软件连接到OPC server服务端&#xff0c;获取server数据&#xff0c;简单好上手&#xff0c;软件也很小巧&#xff0c;操作便利&#xff0c;也是绿色软件&#xff0c;免安装。 二、软件基础环境保证&#x…

期货交易结算信息(期货结算系统)

期货中的结算价是怎么计算出来的&#xff1f; 最近常有新手投资者问&#xff1a;为什么期货账户收盘时年持仓是盈利的&#xff0c;怎么当天结算单上却是亏损的&#xff1f; 之所以会有这个疑问&#xff0c;是因为投资者没有明白期货收盘价、结算价、成交价三者之间的关系。今天…

dockerfile编写构建镜像

文章目录一、dockerfile1、什么是dockerfile2、原理二、docker镜像的创建1、创建镜像的三种方法①基于已有镜像创建②基于本地模板创建③基于dockerfile创建三、镜像分层的原理1、docker镜像结构的分层2、bootfs内核空间3、rootfs内核空间4、AUFS与overlay/overlay2overlay结构…

【ViT 微调时关于position embedding如何插值(interpolate)的详解】

目录1. 问题描述2. positional embedding如何interpolate3. 输入的sequence length改变了ViT还能正常前向推断&#xff1f;本文适合对Vision Transformer有一定了解&#xff08;知道内部结构和一些实现细节&#xff0c;最好是精读过ViT这篇论文&#xff09;的读者阅读&#xff…

八种可以简单判断否属于过敏体质的表现,符合四条就是了

最早感知春天的不是鸭子&#xff0c;而是过敏的人&#xff0c;最近门诊荨麻疹的人数逐渐增多。近20年来&#xff0c;随着过敏性鼻炎、湿疹、荨麻疹和哮喘的发病率越来越高&#xff0c;过敏体质开始越来越出名。在西医中&#xff0c;容易患上这种过敏性疾病的人被称为过敏体质。…

JVM(九) —— 运行时数据区之堆的详细介绍(四)

JVM&#xff08;九&#xff09;—— 运行时数据区之堆的详细介绍&#xff08;四&#xff09;TLAB堆空间常用的参数设置HandlePromotionFailure逃逸分析和栈上分配代码优化栈上分配同步省略分离对象或标量替换TLAB 堆区是线程共享区域&#xff0c;任何线程都乐意访问到堆区的共…

智能钢琴-第12届蓝桥杯Scratch省赛1真题第4题

[导读]&#xff1a;超平老师计划推出Scratch蓝桥杯真题解析100讲&#xff0c;这是超平老师解读Scratch蓝桥真题系列的第52讲。 第12届蓝桥杯青少年组省赛分两次进行&#xff0c;这是2020年10月19日举行的第一次省赛考试初级组&#xff0c;形式为在线考试。Scratch分为初级组和…

java实验报告3:数组和字符串案例练习

目录 内容 利用随机1~9整数生成3*3的二维数组&#xff0c;使二维数组每行&#xff0c;每列、以及斜线上的数字之和均为15&#xff0c;并输出这个二维数组 按自然数从小到大的标准为顺序&#xff0c;对以下数组求其逆序数&#xff0c;并列出逆序对&#xff1a; 实验小结 一、…

眼科考研院校排名及考研难度分析

眼科考研院校排名及考研难度分析 文章目录眼科考研院校排名及考研难度分析基础信息专业介绍学科范围和课程眼科学专业研究方向考研分析学校排名分数线学校推荐就业方向学习材料参考链接基础信息 专业介绍 眼科学&#xff08;学科代码&#xff1a;100212&#xff09;是临床医学…