栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

C++ Primer Plus学习(十五)——string类和标准模板库

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

C++ Primer Plus学习(十五)——string类和标准模板库

string类和标准模板库
  • string类
    • 构造函数
    • string类输入
    • 赋值
    • 长度
    • 连接
    • 比较
    • 子串
    • 交换
    • 查找
    • 替换
    • 删除
    • 插入
    • 将string对象作为流处理
    • 用STL算法操作string对象
    • next_permutation函数介绍
    • string实战
  • 智能指针模板类
    • 为什么要使用智能指针
    • 非堆内存释放问题
    • 如何避免两个指针指向同一个对象导致的double free问题
    • auto_ptr
    • unique_ptr
    • shared_ptr
    • weak_ptr
    • 举个例子
    • 选择智能指针
  • 泛型编程和标准模板库
    • 模板
    • 什么是STL?
    • STL内容介绍
      • 容器
      • STL迭代器
      • 算法
      • 仿函数
      • 容器适配器(stack/queue/priority_queue)
    • 常用容器用法介绍
      • vector
      • deque
      • list
      • map/multimap
      • set/multiset


string类

string类是STL中basic_string模板实例化得到的模板类。其定义如下:

typedef basic_string string;
构造函数

string类有多个构造函数,用法示例如下:

string s1();  // s1 = ""
string s2("Hello");  // s2 = "Hello"
string s3(4, 'K');   // s3 = "KKKK"
string s4("12345", 1, 3);  // s4 = "234", 即"12345"的从下标1开始,长度为3的子串
string类输入

对于C-风格字符串,有3种方式:

char info[100];
cin >> info;                 // read a word
cin.getline(info, 100);		 // read a line, discard n
cin.get(info, 100);			 // read a line, leave n in queue

对于string对象,有两种方式:

string stuff;
cin >> stuff;		   // read a word
getline(cin, stuff);   // read a line, discard n

两个版本的getline()都有一个可选参数,用于指定使用哪个字符来确定输入的边界:

cin.getline(info, 100, ':');   // read up to :, discard :
getline(stuff, ':');		   // read up to :, discard :

在功能上,它们之间的主要区别在于,string版本的getline()将自动调整目标string对象的大小,使之刚好能够存储输入的字符。自动调整大小的功能让string版本的getline()不需要指定读取多少个字符的数值参数。

string版本的getline()函数从输入中读取字符,并将其存储到目标string中,直到发生下列三种情况之一:

  1. 到达文件尾,在这种情况下,输入流的eofbit将被设置,这意味着方法fail()和eof()都将返回true;
  2. 遇到分界字符(默认为n),在这种情况下,将把分界字符从输入流中删除,但不存储它;
  3. 读取的字符数达到最大允许值(string::npos和可供分配的内存字节数中较小的一个),在这种情况下,将设置输入流的failbit,这意味着方法fail()将返回true。

关于自动调整大小的功能,可以啰嗦几句。随着字符串的不停输入,从内存角度看,将发生什么呢?不能仅仅将已有的字符串加大,因为相邻的内存可能被占用了。因此,可能需要分配一个新的内存块,并将原来的内容复制到新的内存单元中。如果执行大量这样的操作,效率将非常低,因此很多C++实现分配一个比实际字符串大的内存块,为字符串提供了增大空间。然而,如果字符串不断增大,超过了内存块的大小,程序将分配一个大小为原来两倍的新内存块,以提供足够的增大空间,避免不断地分配新的内存块。方法capacity()返回当前分配给字符串的内存块的大小,而reserve()方法让您能够请求内存块的最小长度。

赋值

可以用char*类型的变量、常量,以及char类型的变量、常量对string对象进行赋值。例如:

string s1;
s1 = "Hello";
s2 = 'K';

string类还有assign成员函数 ,可以用来对string对象赋值。assign成员函数返回对象自身的引用。例如:

string s1("12345"),s2;
s3.assign(s1);  // s3 = s1
s2.assign(s1, 1, 2);  // s2 = "23",即s1的子串(1,2)
s2.assign(4, 'K');  // s2 = "KKKK"
s2.assign("abcde", 2, 3);  // s2 = "cde",即"abcde"的子串(2,3)
长度

length成员函数返回字符串的长度。size()成员函数可以实现同样的功能。

连接

除了可以使用+和+=运算符对string对象执行字符串的连接操作外,string类还有append成员函数,可以用来向字符串后面添加内容。append成员函数返回对象自身的引用。

比较

可以比较字符串,string类对全部6个关系运算符都进行了重载。如果在机器排列序列中,一个对象位于另一个对象的前面,则前者被视为小于后者。如果机器排列序列为ASCII码,则数组将小于大写字符,而大写字符小于小写字符。对于每个关系运算符,都以三种方式被重载,以便能够将string对象与另一个string对象、C-风格字符串进行比较,并能够将C-风格字符串与string对象进行比较。

除了可以用<、<=、==、!=、>=、>运算符比较string对象外,string类还有compare成员函数,可用于比较字符串。compare成员函数有以下返回值:
小于0表示当前字符串小;等于0表示两个字符串相等;大于0表示另一个字符串小。

子串

substr成员函数可以用于求子串(n, m),原型如下:

string substr(int n = 0, int m = string::npos) const;

调用时,如果省略m或m超过了字符串的长度,则求出来的子串就是从下标n开始一直到字符串结束的部分。

交换

swap成员函数可以交换两个string对象的内容。

查找

string类有一些查找子串和字符的成员函数,它们的返回值都是子串或字符在string对象字符串中的位置(即下标)。如果查不到,则返回string::npos。string::npos是在string类中定义的一个静态常量。这些函数如下:

  1. find: 从前往后查找子串或字符出现的位置;
  2. rfind: 从后往前查找子串或字符出现的位置;
  3. find_first_of: 从前往后查找何处出现另一个字符串中包含的字符,例如:
    s1.find_last_of("abc");  // 查找s1中第一次出现"abc"中任一字符的位置
    
  4. find_last_of: 从后往前查找何处出现另一个字符串中包含的字符
  5. find_first_not_of: 从前往后查找何处出现另一个字符串中没有包含的字符
  6. find_last_not_of: 从后往前查找何处出现另一个字符串中没有包含的字符
替换

replace成员函数可以对string对象中的子串进行替换,返回值为对象自身的引用。

删除

erase成员函数可以删除string对象中的子串,返回值为对象自身的引用。

插入

insert成员函数可以在string对象中插入另一个字符串,返回值为对象自身的引用。

将string对象作为流处理

使用流对象istringstream和ostringstream,可以将string对象当作一个流进行输入输出。使用这两个类需要包含头文件sstream。示例程序如下:

#include 
#include 
#include 
using namespace std;
int main()
{
	string src("Avatar 123 5.2 Titanic K");
	istringstream istrStream(stc);  // 建立src到istrStream的联系
	string s1, s2;
	int n; double d; char c;
	istrStream >> s1 >> n >> d >> s2 >> c; // 把src的内容当作输入流进行读取
	ostringstream ostrStream;
	ostrStream << s1 << endl << s2 << endl << n << endl << d << endl << c << endl;
	cout << ostrSteam.str();
	return 0;
}

程序的输出结果是:

Avatar
Titanic
123
5.2
K

第11行,从输入流istrSteam进行读取,过程和从cin读取一样,只不过输入的来源由键盘变成了string对象src。
第12行,将变量的值输出到流ostrStream。输出结果不会出现在屏幕上,而是被保存在ostrStream对象管理的某处。用ostringstream类的str()成员函数能将输出到ostringstream对象中的内容提取出来。

用STL算法操作string对象

string对象也可以看作一个顺序容器,它支持随机访问迭代器,也有begin和end等成员函数。STL中的许多算法也适用于string对象。下面是STL算法操作string:

#include 
#include 
#include 
using namespace std;
int main()
{
	string s("afgcbed");
	string::iterator p = find(s.begin(), s.end(), 'c');
	if (p != s.end())
		cout << p - s.begin() << endl; // 输出3
	sort(s.begin(), s.end());
	cout << s << endl;  // 输出 abcdefg
	next_permutation(s.begin(), s.end());
	cout << s << endl;  // 输出 abcdegf
	return 0;
}
next_permutation函数介绍

注意,这里出现一个next_permutation函数,需要详细介绍一下。next_permutaion(start, end)和prev_permutaion(start, end)两个函数作用是一样的,区别就在于前者求的是当前排列的下一个排列,后一个求的是当前排列的上一个排列。至于这里的“前一个”和“后一个”,我们可以把它理解为序列的字典序的前后,严格来讲,就是对于当前序列pn,他的下一个序列pn+1满足:不存在另外的序列pm,使pn

对于next_permutaion函数,其函数原型为:

#include 
bool next_permutaion(iterator start, iterator end);

当当前序列不存在下一个排列时,函数返回false,否则返回true。
举个例子强化一下理解:

#include   
#include   
using namespace std;  
int main()  
{  
    int num[3]={1,2,3};  
    do  
    {  
        cout< 

输出结果为:

1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1

当我们把while(next_permutation(num,num+3))中的3改为2时,输出就变为了:

1 2 3
2 1 3

由此可以看出,next_permutation(num,num+n)函数是对数组num中的前n个元素进行全排列,同时并改变num数组的值。
另外,需要强调的是,next_permutation(num,num+n)在使用前需要对欲排列数组按升序排序,否则只能找出该序列之后的全排列数。比如,如果数组num初始化为2,3,1,那么输出就变为了:

2 3 1
3 1 2
3 2 1

此外,next_permutation(num,num+n, cmp)可以对结构体num按照自定义的排序方式cmp进行排序。
比如,如果要求的字典序是:

// 'A' < 'a' < 'B' < 'b' < ··· < 'Z' < 'z'
#include
#include
#include
using namespace std;
int cmp(char a,char b) 
{
   if(tolower(a)!=tolower(b))//tolower 是将大写字母转化为小写字母.
       return tolower(a)
    char ch[20];
    int n;
    cin>>n;
    while(n--)
	{
	    scanf("%s",ch);
	    sort(ch,ch+strlen(ch),cmp);
	}
	do
	{
	    printf("%sn",ch);
	}while(next_permutation(ch,ch+strlen(ch),cmp));
 	return 0;
}

char类型的next_permutation()

int main()
{
   char ch[205];
   cin >> ch;
   //该语句对输入的数组进行字典升序排序。如输入9874563102 cout<
   		cout<< ch << endl;
   }while(next_permutation(first, last));
   return 0;
}

//这样就不必事先知道ch的大小了,是把整个ch字符串全都进行排序
//若采用 while(next_permutation(ch,ch+5)); 如果只输入1562,就会产生错误,因为ch中第五个元素指向未知
//若要整个字符串进行排序,参数5指的是数组的长度,不含结束符

string类型的next_permutation()

int main()
{
   string line;
   while(cin>>line&&line!="#")
   {
	   sort(line.begin(),line.end());//全排列
	   cout< 
string实战 

可以参考C++ Primer Plus学习(四)—— string类实践

智能指针模板类 为什么要使用智能指针

智能指针的作用是管理一个指针,因为存在以下这种情况:申请的空间在函数结束时忘记释放,造成内存泄漏。使用智能指针可以很大程度上的避免这个问题,因为智能指针是一个类,当超出了类的实例对象的作用域时,会自动调用对象的析构函数,析构函数会自动释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间。

智能指针主要用于管理在堆上分配的内存,它将普通的指针封装为一个栈对象。当栈对象的生存周期结束后,会在析构函数中释放掉申请的内存,从而防止内存泄漏。C++11中最常用的智能指针类型为shared_ptr,它采用引用计数的方法,记录当前内存资源被多少个智能指针引用。该引用计数的内存在堆上分配。当新增一个时引用计数加1,当过期时引用计数减一。只有引用计数为0时,智能指针才会自动释放引用的内存资源。对shared_ptr进行初始化时不能将一个普通指针直接赋值给智能指针,因为一个是指针,一个是类。可以通过make_shared函数或者通过构造函数传入普通指针。并可以通过get函数获得普通指针。C++里面的四个智能指针:auto_ptr, unique_ptr, shared_ptr, weak_ptr。其中后三个是C++11支持,并且第一个已经被C++11弃用。

非堆内存释放问题

先说一下对所有智能指针都应避免的一点:

string vacation("I love you!");
shared_ptr pvac(&vacation);  // No!

pvac过期时,程序将把delete运算符用于非堆内存,这是错误的!

如何避免两个指针指向同一个对象导致的double free问题
  1. 定义赋值运算符,使之进行深复制。这样两个指针将指向不同的对象,其中的一个对象是另一个对象的副本;
  2. 建立所有权(ownership)概念,对于特定的对象,只能有一个智能指针可拥有它,这样只有拥有对象的智能指针的构造函数会删除该对象。然后,让赋值操作转让所有权。这就是用于auto_ptr和unique_ptr的策略,但unique_ptr的策略更严格;
  3. 创建智能更高的指针,跟踪引用特定对象 智能指针数。这称为引用计数(reference counting)。这是shared_ptr采用的策略。
auto_ptr

(C++98的方案,C++11已经抛弃)采用所有权模式。

auto_ptr p1(new string ("I love you!"));
auto_ptr p2;
p2 = p1;  // auto_ptr不会报错

此时不会报错,但是p2已经剥夺了p1的所有权,当程序运行时访问p1将会报错。所以auto_ptr的缺点是:存在潜在的内存崩溃问题!

unique_ptr

(替换auto_ptr)unique_ptr实现独占式拥有或严格拥有概念,保证同一时间内只有一个智能指针可以指向该对象。它对于避免资源泄露(例如“以new创建对象后因为发生异常而忘记调用delete”)特别有用。

采用所有权模式,还是上面那个例子:

unique_ptr p3(new string ("I love you!"));
unique_ptr p4;
p4 = p3;  // 此时会报错!

编译器认为p4 = p3非法, 避免了p3不再指向有效数据的问题。尝试复制p3时会编译期出错,而auto_ptr能通过编译期,从而在运行期埋下出错的隐患。因此,unique_ptr更安全!

另外unique_ptr还有更聪明的地方:当程序试图将一个unique_ptr赋值给另一个时,如果源unique_ptr是个临时右值,编译器允许这么做;如果源unique_ptr将存在一段时间,编译器将禁止这么做。比如:

unique_ptr pu1(new string("Hello!"));
unique_ptr pu2;
pu2 = pu1;    // #1 不允许
unique_ptr pu3;
pu3 = unique_ptr(new string("You!"));  // #2 允许

其中#1留下悬挂的unique_ptr(pu1),这可能导致危害。而#2不会留下悬挂的unique_ptr,因为它调用unique_ptr的构造函数,该构造函数创建的临时对象在其所有权让给pu3后就会被销毁。这种随情况而异的行为表明,unique_ptr优于允许两种赋值的auto_ptr。

注:如果确实想执行类似与#1的操作,要安全的重用这种指针,可给她赋新值。C++有一个标准库函数std::move(),让你能够将一个unique_ptr赋给另一个。尽管转移所有权后还是有可能出现原有指针调用(调用就崩溃)的情况。但是这个语法能强调你是在转移所有权,让你清晰地知道自己在做什么,从而不乱调用原有指针。

相比于auto_ptr,unique_ptr还有另一个优点。它有一个可用于数组的变体。别忘了,必须将delete和new配对,将delete[]和new[]配对。模板auto_ptr使用delete而不是delete[],因此只能与new一起使用,而不能与new[]一起使用。但unique_ptr有使用new[]和delete[]的版本:

std::unique_ptrpda(new double(5));   // will use delete[]
shared_ptr

shared_ptr实现共享式拥有概念。多个智能指针可以指向相同对象,该对象和其相关资源会在“最后一个引用被销毁”时候释放。从名字share就可以看出了资源可以被多个指针共享,它使用计数机制来表明资源被几个指针共享。可以通过成员函数use_count()来查看资源的所有者个数。除了可以通过new来构造,还可以通过传入auto_ptr, unique_ptr, weak_ptr来构造。当我们调用release()时,当前指针会释放资源所有权,计数减一。当计数等于0时,资源会被释放。

shared_ptr是为了解决auto_ptr在对象所有权上的局限性(auto_ptr是独占的),在使用引用计数的机制上提供了可以共享所有权的智能指针。

成员函数:
use_count返回引用计数的个数;
unique返回是否是独占所有权(use_count为1);
swap交换两个shared_ptr对象(即交换所拥有的对象);
reset放弃内部对象的所有权或拥有对象的变更,会引起原有对象的引用计数的减少;
get返回内部对象(指针),由于已经重载了()方法,因此和直接使用对象是一样的,比如:

shared_ptr sp(new int(1));
sp与sp.get()是等价的。
weak_ptr

shared_ptr虽然已经很好用了,但是有一点shared_ptr智能指针还是有内存泄漏的情况,当两个对象相互使用一个shared_ptr成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄漏。

weak_ptr是一种不控制对象生命周期的智能指针,它指向一个shared_ptr管理的对象。进行该对象的内存管理的是那个强引用的shared_ptr,weak_ptr只是提供了对管理对象的一个访问手段。weak_ptr设计的目的是为了配合shared_ptr而引入的一种智能指针来协助shared_ptr工作,它只可以从一个shared_ptr或另一个weak_ptr对象构造,它的构造和析构不会引起引用计数的增加或减少。weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。

举个例子
class B;
class A
{
public:
	shared_ptr pb_;
	~A()
	{
		cout << "A delete! n";
	}
};

class B
{
public:
	shared_ptr pa_;
	~B()
	{
		cout << "B delete! n";
	}
};

void function()
{
	shared_ptr pb(new B());
	shared_ptr pa(new A());
	cout << pb.use_count() << endl;  //1
	cout << pa.use_count() << endl;  //1
	pb->pa_ = pa;
	pa->pb_ = pb;
	cout << pb.use_count() << endl;  //2
	cout << pa.use_count() << endl;  //2
}

int main()
{
	function();
	return 0;
}

可以看到function()函数中pa, pb之间互相引用,两个资源的引用计数为2,当要跳出函数时,智能指针pa, pb析构时两个资源引用计数会减1,但是两者引用计数还是为1,导致跳出函数时资源没有被释放(A、B的析构函数没有被调用)运行结果没有输出析构函数的内容,造成内存泄漏。如果把其中一个改为weak_ptr就可以了,我们把类A里面的shared_ptr pb_改为weak_ptr pb_。

这样的话,资源B的引用开始就只有1,当pb析构时,B的计数变为0,B得到释放。B释放的同时也会使A的计数减1,同时pa析构时使A的计数减1,那么A的计数为0,A得到释放。

注意:我们不能通过weak_ptr直接访问对象的方法,比如B对象中有一个方法print(),我们不能这样访问:pa->pb_->print(),因为pb_是一个weak_ptr,应该先把它转化为shared_ptr,如:

shared_ptr p = pa->pb_.lock();
p->print();

weak_ptr没有重载*和->,但是可以使用lock获得一个可用的shared_ptr对象,注意,weak_ptr在使用前需要检查合法性。
expired用于检测所管理的对象是否已经释放,如果已经释放,返回true;否则返回false。
lock用于获取所管理的对象的强引用(shared_ptr)。如果expired为true,返回一个空的shared_ptr,否则返回一个shared_ptr,其内部对象指向与weak_ptr相同。
use_count返回与shared_ptr共享的对象的引用计数。
reset将weak_ptr置空。
weak_ptr支持拷贝或赋值,但不会影响对应的shared_ptr内部对象的计数。

选择智能指针

应使用哪种智能指针呢?如果程序要使用多个指向同一个对象的指针,应选择shared_ptr。这样的情况包括:有一个指针数组,并使用一些辅助指针来标识特定的元素,如最大的元素和最小的元素;两个对象都包含指向第三个对象的指针;STL容器包含指针。很多STL算法都支持复制和赋值操作,这些操作可用于shared_ptr,但不能用于unique_ptr(编译器发出警告)和auto_ptr(行为不确定)。如果您的编译器没有提供shared_ptr,可使用Boost库提供的shared_ptr。

如果程序不需要多个指向同一个对象的指针,则可使用unique_ptr。如果函数使用new分配内存,并返回指向该内存的指针,将其返回类型声明为unique_ptr是不错的选择。这样,所有权将转让给接受返回值的unique_ptr,而该智能指针将负责调用delete。可将unique_ptr存储到STL容器中,只要不调用将一个unique_ptr复制或赋给另一个的方法或算法(如sort())。模板shared_ptr包含一个显式构造函数,可用于将右值unique_ptr转换为shared_ptr。shared_ptr将接管原来归unique_ptr所有的对象。

泛型编程和标准模板库

STL提供了一组表示容器、迭代器、函数对象和算法的模板。容器是一个与数组类似的单元,可以存储若干个值。STL容器是同质的,即存储的值的类型相同;算法是完成特定任务(如对数组进行排序或在链表中查找特定值)的处方;迭代器能够用来遍历容器的对象,与能够遍历数组的指针类似,是广义指针;函数对象是类似于函数的对象,可以是类对象或函数指针(包括函数名,因为函数名被用作指针)。STL使得能够构造各种容器(包括数组、队列和链表)和执行各种操作(包括搜索、排序和随机排列)。STL不是面向对象的编程,而是一种不同的编程模式——泛型编程(generic programming)。

模板

泛型程序设计(generic programming)是一种算法在实现时不指定具体要操作的数据的类型的程序设计方法。所谓“泛型”,指的是算法只要实现一遍,就能适用于多种数据类型。泛型程序设计方法的优势在于能够减少重复代码的编写。

泛型程序设计的概念最早出现于1983年的Ada语言,其最成功的应用就是C++的标准模板库(STL)。也可以说,泛型程序设计就是大量编写模板、使用模板的程序设计。泛型程序设计在C++中的重要性和带来的好处不亚于面向对象的特性。

在C++中,模板分为函数模板和类模板两种。熟练的C++程序员,在编写函数时都会考虑能否将其写成函数模板,编写类时都会考虑能否将其写成类模板,以便实现重用。

在C++中,数据的类型也可以通过参数来传递,在函数定义时可以不指明具体的数据类型,当发生函数调用时,编译器可以根据传入的实参自动推断数据类型。这就是类型的参数化。

值(Value)和类型(Type)是数据的两个主要特征,它们在C++中都可以被参数化。

  • 函数模板
    所谓函数模板,实际上是建立一个通用函数,它所用到的数据的类型(包括返回值类型、形参类型、局部变量类型)可以不具体指定,而是用一个虚拟的类型来代替(实际上是用一个标识符来占位),等发生函数调用时再根据传入的实参来逆推出真正的类型。这个通用函数就成为函数模板(Function Template)。

    在函数模板中,数据的值和类型都被参数化了,发生函数调用时编译器会根据传入的实参来推演形参的值和类型。换个角度说,函数模板除了支持值的参数化,还支持类型的参数化。

    一旦定义了函数模板,就可以将类型参数用于函数定义和函数声明了。说的直白一点,原来使用int、float、char等内置类型的地方,都可以用类型参数来代替。

  • 函数模板的例子

    #include 
    using namespace std;
    template void Swap(T *a, T *b)
    {
    	T temp = *a;
    	*a = *b;
    	*b = temp;
    }
    
    int main()
    {
    	//交换int变量的值
    	int n1 = 100, n2 = 200;
    	Swap(&n1, &n2);
    	
    	//交换float变量的值
    	float f1 = 12.5, f2 = 56.93;
    	Swap(&f1, &f2);
    	
    	//交换char变量的值
    	char c1 = 'a', c2 = 'b';
    	Swap(&c1, &c2);
    	
    	//交换bool变量的值
    	bool b1 = false, b2 = true;
    	Swap(&b1, &b2);
    
    	return 0;
    }
    

    template是定义函数模板的关键字,它后面紧跟尖括号<>,尖括号包围的是类型参数(也可以说是虚拟的类型,或者说是类型占位符)。typename是另外一个关键字,用来声明具体的类型参数,这里的类型参数就是T。从整体上看,template被称为模板头。

    模板头中包含的类型参数可以用在函数定义的各个位置,包括返回值、形参列表和函数体;本例我们在形参列表和函数体中使用了类型参数T。 类型参数的命名规则跟其他标识符的命名规则一样,不过使用T、T1、T2、Type等已经成为了一种惯例。定义了函数模板后,就可以像调用普通函数一样来调用它们了。

  • 函数模板总结
    总结一下定义模板函数的语法:

    template返回值类型 函数名(形参列表){
    	// 在函数体中可以使用类型参数
    }
    

    类型参数可以有多个,它们之间以逗号分隔。类型参数列表以<>包围,形式参数列表以()包围。

    typename关键字也可以使用class关键字替代,它们没有任何区别。C++早期对模板的支持并不严谨,没有引入新的关键字,而是用class来指明类型参数,但是class关键字本来已经用在类的定义中了,这样做显得不太友好,所以后来C++又引入了一个新的关键字typename,专门用来定义类型参数。不过至今仍然有很多代码在使用class关键字,包括C++标准库、一些开源程序等。

  • 类模板
    C++除了支持函数模板,还支持类模板(Class Template)。函数模板中定义的类型参数可以用在函数声明和函数定义中,类模板中定义的类型参数可以用在类声明和类实现中。类模板的目的同样是将数据 类型参数化。

    声明类模板的语法为:

    templateclass 类名{
    	//TODO:
    };
    

    类模板和函数模板都是以template开头(当然也可以使用class,目前来讲它们没有任何区别),后跟类型参数;类型参数不能为空,多个类型参数用逗号隔开。一旦声明了类模板,就可以将类型参数用于类的成员函数和成员变量了。换句话说,原来使用int、float、char等内置类型的地方,都可以用类型参数来代替。

    假如我们现在要定义一个类来表示坐标,要求坐标的数据类型可以是整数、小数和字符串,例如:

    x = 10, y = 10;
    x = 12.88, y = 129.65;
    x = "东经180度", y = "北纬210度";
    

    这个时候就可以使用类模板,请看下面的代码:

    template  // 这里不能有分号
    class Point{
    public:
    	Point(T1 x, T2 y):m_x(x), m_y(y){}
    public:
    	T1 getX() const;  // 获取x坐标
    	void setX(T1 x);  // 设置x坐标
    	T2 getY() const;  // 获取y坐标
    	void setY(T2 y);  // 设置y坐标
    private:
    	T1 m_x;  // x坐标
    	T2 m_y;  // y坐标
    };
    

    x坐标和y坐标的数据类型不确定,借助类模板可以将数据类型参数化,这样就不必定义多个类了。注意:模板头和类头是一个整体,可以换行,但是中间b態有分号。上面的代码仅仅是类的声明,我们还需要在类外定义成员函数。在类外定义成员函数时仍然需要带上模板头,格式为:

    template
    返回值类型 类名<类型参数1, 类型参数2, ···>::函数名(形参列表){
    	// TODO:
    }
    

    第一行是模板头,第二行是函数头,它们可以合并到一行,不过了让代码格式更加清晰,一般是将它们分成两行。
    下面就对Point类的成员函数进行定义:

    template  // 模板头
    T1 Point::getX() const  {
    	return m_x;
    }
    
    template
    void Point::setX(T1 x){
    	m_x = x;
    }
    
    template  // 模板头
    T2 Point::getY() const  {
    	return m_y;
    }
    
    template
    void Point::setY(T2 y){
    	m_y = y;
    }
    

    除了template关键字后面要指明类型参数,类名Point后面也要带上类型参数,只是不加typaname关键字了。另外需要注意的是,在类外定义成员函数时,template后面的类型参数要和类声明时的一致。

  • 使用类模板创建对象
    使用类模板创建对象时,需要指明具体的数据类型。

    Point p1(10, 20);
    Point p2(10, 15.5);
    Point p3(12.4, "东经180度");
    

    与函数模板不同的是,类模板在实例化时必须显式地指明数据类型,编译器不能根据给定的数据推演出数据类型。

    除了对象变量,我们也可以使用对象指针的方式来实例化:

    Point *p1 = new Point(10.6, 109.3);
    Point *p = new Point("东经180度", "北纬210度");
    

    需要注意的是,赋值号两边都要指明具体的数据类型,且要保持一致,下面的写法是错误的:

    // 赋值号两边的数据类型不一致
    Point *p = new Point(10.6, 109);
    // 赋值号右边没有指明数据类型
    Point *p = new Point(10.6, 109);
    

以下内容很多,贴一篇总结的比较全面的文章,就不自己码字了,大家可以自行了解:
C++中STL用法超详细总结

什么是STL? STL内容介绍 容器 STL迭代器 算法 仿函数 容器适配器(stack/queue/priority_queue) 常用容器用法介绍 vector deque list map/multimap set/multiset
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号