文章目录
C++模板篇
本章主要的内容:
- 友元函数、友元类
- 静态数据成员、静态成员函数
- 运算符重载
- 模板函数+模板类==》标准模板类
- 标准模板库中向量Vector、链表list、映射map的使用
**注意:**关于友元,友元只是封装的补充,会破坏封装性,有定向暴露性(在实在没有办法的情况下使用友元)
1.友元函数-全局友元函数和友元成员函数
1.全局友元函数的定义和调用:
class Time {
// Time类的全局友元函数
friend void printTime(Time &t);
public:
Time(int hour, int min, int sec) {
m_iHour = hour;
m_iMinute = min;
m_iSecond = sec;
}
private:
m_iHour = hour;
m_iMinute = min;
m_iSecond = sec;
};
// Time类的全局友元函数
void printTime(Time &t){
cout << t.m_iHour << ":"
<< t.m_iMinute << ":"
<< t.m_iSecond << endl;
}
int main() {
Time t(6,34,25);
printTime(t);// 调用全局友元函数
return 0;
}
/*输出:
6:34:25
*/
2.友元成员函数的定义和调用:
- Match类-Match.h:
//#include "Time.h"
// 先声明有Time,代表之后会进行实现,但是不能使用include,会导致循环包含
class Time;
class Match {
public:
void printTime(Time &t);
};
2.Match类-Match.cpp:
#include <iostream>
#include "Match.h"
#include "Time.h"// [!]不明白此处为何可以include,不会造成Match和Time的循环包含吗?
using namespace std;
void Match::printTime(Time &t) {
cout << t.m_iHour << ":"
<< t.m_iMinute << ":"
<< t.m_iSecond << endl;
}
3.Time类-Time.h:
#include <iostream>
#include "Match.h"
using namespace std;
class Time {
// 在这里声明友元函数,在此并不受本类的访问限定符限定,
// 故写在public或private下并没有区别
friend void Match::printTime(Time &t);
public:
Time(int hour,int min,int sec);
private:
int m_iHour;
int m_iMinute;
int m_iSecond;
};
4.Time类-Time.cpp:
#include "Time.h"
Time::Time(int hour, int min, int sec) {
m_iHour = hour;
m_iMinute = min;
m_iSecond = sec;
}
5.main.cpp:
#include <iostream>
#include "Time.c"
int main() {
Time t(6,34,25);
Match m;
m.printTime(t);// 调用友元函数
return 0;
}
/*输出:
6:34:25
*/
2.友元类
友元类的写法:
由于编译器不同,友元类有两种写法:1、friend class 类名
; 2、friend 类名
。
友元类的声明与使用举例:
#include <iostream>
using namespace std;
// 事先加入Watch声明,代表当前没有实现,但是之后会进行实现
class Watch;
/**
* 定义Time类
* 数据成员:m_iHour, m_iMinute,m_iSecond
* 成员函数:构造函数
* 友元类:Watch
*/
class Time{
// 声明Time类是Match类的友元类,即声明Match有个友元类是Time
// 所以Match就可以访问Time中的成员了
friend class Watch;
public:
Time(int hour, int min, int sec){
m_iHour = hour;
m_iMinute = min;
m_iSecond = sec;
}
public:
int m_iHour;
int m_iMinute;
int m_iSecond;
};
/**
* 定义Watch类
* 数据成员:m_tTime
* 成员函数:构造函数
* display用于显示时间
*/
class Watch{
public:
Watch(Time &timeObj) : m_tTime(timeObj){
}
void display(){
cout << m_tTime.m_iHour << endl;
cout << m_tTime.m_iMinute << endl;
cout << m_tTime.m_iSecond << endl;
}
public:
Time m_tTime;
};
int main(){
Time t(6, 30, 20);
Watch w(t);
w.display();
return 0;
}
3.友元的总结
注意:
- 友元关系不可传递
- 友元关系得单向性(A是B的友元,不代表B是A的友元)
- 友元声明的形式及数量不受限制
- 友元只是封装的补充,会破坏封装性,有定向暴露性(在实在没有办法的情况下使用友元)
- 友元的声明不受访问限定符影响,可以声明在类中的任何位置。(即用
friend
修饰的声明,仅限被friend
修饰的这一句话不受)
对于友元关系的记法:
- 友元函数:friend在哪个类出现,则此函数就是这个类的朋友,这个函数就可以对这个类的所有数据成员进行访问了。(通过传入的对象的引用来进行访问)
- 友元类:friend在A类出现,则被
friend
修饰的B类就是这个类(A类)的朋友,所以B类就可以对A类中的所有数据成员进行访问了(直接通过B类进行调用即可,无需使用引用)。即在A类中出现friend修饰的B,那么B就是当前类的朋友,可以访问当前类。
4.静态变量与静态函数
静态成员依托于类,而不是对象,就算类没有实例化对象,静态成员也会存在于内存中。
假如有个类A,其中有静态变量cout,在实例化a,b,c,d之前,cout这个静态变量就已经存在于内存中 了。
静态变量:
声明:static int 变量名;
初始化(方法1):static int 变量名 = xxxx;
(在类中声明+初始化)
初始化(方法2):int 类名::变量名 = xxxx;
(在类外初始化)
访问(方法1):类名::变量名
访问(方法2):对象名.变量名
或指针名->变量名
静态函数:
初始化(方法1):static int 函数名(){xxxx;}
(在类中初始化)
访问(方法1):类名::函数名()
访问(方法2):对象名.函数名()
或指针名->函数名()
int 类名::变量名 = xxxx
注意事项:
- 静态数据成员必须单独初始化。(在对象实例化之前就有了,所以不能写到类的构造函数中去)
- 静态成员函数不能调用非静态成员函数和费静态数据成员。
- 静态数据成员只有一份,且不依赖对象而存在。
对于上面注意事项的第2点做一下原理补充:
在类中的普通成员函数中调用普通成员变量,实际上为了辨认出当前使用的变量属于哪个对象,都会自动添加this指针;如果是静态变量,因为不依托于对象,而是依托于类,所以不会自动添加this指针,如下所示:
void FunctionName(){
variable1 = "01"; // 普通成员变量
variable2 = "02"; // 静态成员变量
}
// 实际上会变成下面的这种形式
void FunctionName(ClassName *this){
this->variable1 = "01"; // 会自动添加this指针
variable2 = "02"; // 不会有this指针
}
在静态成员函数中,因为这个函数不依托于对象,所以也不会有代表当前对象的this指针,所以如果使用普通成员变量,会不知道到底是哪个对象的成员变量,所以如果这样调用会报错,如下所示:
static int FunctionName(){
// variable1是普通成员变量,variable2是静态成员变量
variable1 = "01"; // 会报错!
variable2 = "02"; //
}
但是普通的成员函数中使用静态的数据成员,则是完全没有问题。
静态成员函数和const常量修饰符:
禁止在静态函数后面加const,因为const的本质是实际是对隐藏的this指针加const,因为静态函数根本不存在this指针,所以会报错。
小总结:
-
定义静态成员函数和静态数据成员都需要static关键字。
-
公有静态成员函数可以被类直接调用。
-
静态成员函数
既可以访问非静态数据成员和静态数据成员,也可以调用非静态成员函数和静态成员函数。静态成员函数只能访问静态数据成员和调用静态成员函数。 -
静态数据成员不能在构造函数初始化,必须单独初始化。
5.一元运算符的重载
**运算符重载:**给原有运算符赋予新的功能。
**运算符重载的本质:**函数重载。
关键字:operator
-(负号)的重载,有两种重载的方法:
- 成员函数重载
- 友元函数重载
1.负号的重载-成员函数重载:
// 声明
class Coordinate{
public:
Coordinate(int x,int y);
// 成员函数实现负号重载,
// 返回类型是Coordinate&,关键字是operator,
// 函数名是-,无参数
Coordinate& operator-();
private:
int m_iX;
int m_iY;
}
// 实现
Coordinate& Coordinate::operator-(){
// 两种写法都可以,this指针可加可不加
this->m_iX = -(this->m_iX);
this->m_iY = -(this->m_iY);
//m_iX = -m_iX;
//m_iY = -m_iY;
// 【为什么是*this?】
// 这里我们的返回值是Coordinate对象的引用,这个引用该指向的是一个对象
// 而this指针指向这个对象,所以我们要对this指针取值,从而获取到对象
// 也就是使用“*this”
return *this;
}
// 调用
int main(){
Coordinate coor1(3,5);
-coor1; // 相当于coor1.operator-();
return 0;
}
2.负号的重载-友元函数重载:
// 声明
class Coordinate{
// 利用友元函数实现负号重载,
// 友元函数也需要一个像成员函数自带的this一样的参数,
// 故这里表现为Coordinate对象类型的参数,或引用
// 即“Coordinate c”或“Coordinate &c”
friend Coordinate& operator-(Coordinate& coor);
public:
Coordinate(int x,int y);
private:
int m_iX;
int m_iY;
};
// 实现
// 注意:友元函数不属于Coordinate类,所以不能写成Coordinate::XXXX
Coordinate& operator-(){
coor.m_iX = -coor.m_iX;
coor.m_iY = -coor.m_iY;
return *this;
}
// 调用
int main(){
Coordinate coor1(3,5);
-coor1; // 相当于coor1.operator-();coor1这个对象会通过形参被传入
return 0;
}
++符号(前置)的重载:
前置++:先++,再使用
// 声明
class Coordinate{
public:
Coordinate(int x,int y);
Coordinate& operator++(); // 前置++,返回当前对象的引用
private:
int m_iX;
int m_iY;
};
// 定义
Coordinate& Coordinate::operator++(){
m_iX++;
m_iY++;
return *this;
}
// 调用
int main(){
Coordinate coor1(3,5);
++coor1; // coor1.operator++();
return 0;
}
++符号(后置)的重载:
后置++:先使用,后++
// 声明
class Coordinate{
public:
Coordinate(int x,int y);
// 成员函数实现++(后置)重载,后面的int是一个标志,
// int标志代表后置重载,没有为什么
// 返回值也不再返回对象引用,而是返回一个对象
Coordinate operator++(int);
private:
int m_iX;
int m_iY;
};
// 实现
// 注意:这里的int代表后置,是个标志,不需要进行传值,之后也不会使用
Coordinate operator++(int){
// 将当前对象保存在临时对象中,这里会调用拷贝构造函数,
// 作为返回,因为++后置是先使用,再++
Coordinate old(*this);
// 将当前对象中的属性进行++,如果之后再次调用此对象,就是++之后的值
m_iX++;
m_iY++;
// 返回++之前的值
return old;
}
// 调用
int main(){
Coordinate coor1(3,5);
coor1++; // coor1.operator++(0);系统会默认传入一个值到标志中,一般是0
// 这样验证看得更清楚,会输出++之前的m_iX值
cout << (coor1++).m_iX << ",";
cout << (coor1++).m_iY << endl;
return 0;
}
6.二元运算符的重载
+(加号)的重载,也有两种重载的方法:
- 成员函数重载
- 友元函数重载
对于此例来说,采用友元函数重载能说明得更加清楚。
1.加号的重载-成员函数重载:
// 声明
class Coordinate{
public:
Coordinate(int x,int y);
// 返回对象作为结果,参数是一个对象引用常量,
// const是设计上的规范,我们在加的过程中并不希望修改加数本身的值
Coordinate operator+(const Coordinate &coor);
private:
int m_iX;
int m_iY;
}
// 实现
Coordinate operator+(const Coordinate &coor){
// 临时对象,用来保存相加的结果
Coordinate temp;
temp.m_iX = this->m_iX + coor.m_iX;
temp.m_iY = this->m_iY + coor.m_iY;
// 返回结果(临时对象)
return temp;
}
// 调用
int main(){
Coordinate coor1(3,5);
Coordinate coor2(4,7);
Coordinate coor3(0,0);
coor3 = coor1 + coor2; // coor1.operator+(coor2);coor2前面还有一个隐藏参数,就是this指针,指向当前对象
return 0;
}
2.加号的重载-友元函数重载:
// 声明
class Coordinate{
// 加const是设计上的规范,我们在加的过程中并不希望修改加数本身的值
friend Coordinate operator+(const Coordinate &c1,const Coordinate &c2);
public:
Coordinate(int x,int y);
private:
int m_iX;
int m_iY;
}
// 实现
Coordinate operator+(const Coordinate &c1,const Coordinate &c2){
// 临时对象,用来保存相加的结果
Coordinate temp;
temp.m_iX = c1.m_iX + c2.m_iX;
temp.m_iY = c1.m_iY + c2.m_iY;
return temp;
}
// 调用(和利用成员函数重载的调用一样,但过程略有不同)
int main(){
Coordinate coor1(3,5);
Coordinate coor2(4,7);
Coordinate coor3(0,0);
coor3 = coor1 + coor2; // operator+(coor1,coor2);
return 0;
}
<<负号的重载:
这个用法也是规定的,和其他的略有不同。
**问题:**可以使用成员函数进行重载吗?不行,应该使用友元函数进行重载。
因为重载函数的第一个参数也必须是ostream&,所以不能用成员函数进行重载,因为成员函数的第一个参数是this指针(隐藏的)。
// 声明
class Coordinate{
// 利用友元函数进行重载
// 返回值必须是ostream&
// 第一个参数也必须是ostream&
// 之后是要输出的对象
friend ostream& operator<<(ostream &out,const Coordinate &coor);
public:
Coordinate(int x,int y);
private:
int m_iX;
int m_iY;
}
// 实现
ostream& operator<<(ostream& out,const Coordinate& coor){
// 将原来用cout的地方换成out
out << coor.m_iX << "," << coor.m_iY;
// 返回out
return out;
}
// 使用
int main(){
Coordinate coor(3,5);
cout << coor; // operator<<(cout,coor);
return 0;
}
[ ]索引运算符的重载:
**问题:**可以使用友元函数进行重载吗?不行,应该使用成员函数进行重载。
作为索引运算符来说,他的第一个参数必须是this指针,因为只有第一个参数是this指针,才能够传入索引,才能使这个索引所表达的是当前这个对象当中的成员。
但是友元重载,第一个参数可以是this指针,也可以是其他的值。所以不能使用友元函数进行重载。
// 声明
class Coordinate{
public:
Coordinate(int x,int y);
int operator[](int index);
private:
int m_iX;
int m_iY;
}
// 实现
int Coordinate::operator[](int index){
if(0 == index){
return m_iX;
}
if(1 == index){
return m_iY;
}
// 还可以加其余索引的处理...
}
// 使用
int main(){
Coordinate coor(3,5);
cout << coor[0]; // coor.operator[](0);
cout << coor[1]; // coor.operator[](1);
return 0;
}
小总结:
- 运算符重载可以使运算符具有新的功能。
- 运算符重载使用关键字operator
- 所有的运算符
都可以既使用友元函数重载和成员函数重载。有些运算符必须使用成员函数重载,有些则必须使用友元函数重载。 - 运算符重载需要区分前置++重载和后置++重载。
6.5.补充:运算符重载和友元函数的问题
头文件:
`class` `Coordinate {` ` ``// 利用友元函数进行+号重载`` ``friend` `Coordinate operator+(``const` `Coordinate c1,``const` `Coordinate c2);` `public``:`` ``// 构造函数,带初始化参数`` ``Coordinate(``int` `x,``int` `y);` ` ``int` `getX();`` ``int` `getY();` `private``:`` ``// 私有属性`` ``int` `m_iX;`` ``int` `m_iY;``};`
cpp文件:
`#include "Coordinate.h"` `Coordinate operator+(Coordinate c1,Coordinate c2){` ` ``Coordinate temp(0,0);` ` ``temp.m_iX = c1.getX() + c2.getX();`` ``temp.m_iY = c1.getY() + c2.getY();` ` ``return` `temp;``}`
为何这里可以直接使用 temp.m_iX ?
答案:
对象直接访问自己的私有成员这种用法绝对是错的,不要这么使用。可以借助友元类/友元函数或类自己的成员函数进行访问。
我们在Coordinate类中声明了友元函数operator+(),友元函数在访问权限上和类内函数比较相似,可以对这个类的所有数据成员进行访问。
// 验证友元函数中创建的对象是否可以访问自己的私有成员。
// 结论:可以
#include <iostream>
using namespace std;
class hehe{
// hehe类的友元函数
friend void heheFriend(hehe &h);
public:
int getxx(){ return xx; }
int getyy(){ return yy; }
private:
int xx;
int yy;
};
void heheFriend(hehe &h){
// 友元函数可以访问类的所有成员,包括私有
h.xx = 1;
h.yy = 2;
// 【友元函数中创建的对象h2可以访问自己的私有成员】
hehe h2;
h2.xx = h.xx;
cout << "h2.xx = " << h2.xx << endl;
}
int main() {
hehe h1;
// 无法直接访问私有成员
//h1.xx; // 错误提示:'xx' is a private member of 'hehe'
//h1.yy; // 错误提示:'yy' is a private member of 'hehe'
// 调用友元函数,进行函数成员赋值
heheFriend(h1);
cout << "h1.xx = " << h1.getxx() << endl;
return 0;
}
/*
* 输出结果:
* h2.xx = 1
* h1.xx = 1
*/
7.函数模板
函数模板和类模板关键字:
template
, typename
和 class
,其中 typename
和 class
起到的作用是相同的,并且可以混用。
关键字的使用1:
// 函数模板
// 通过template声明函数的模板
// 通过class来声明一个参数,这个参数就能表明这是一种类型
template <class T>
T max(T a, T b){
return (a>b)?a:b;
}
// 模板函数(模板的使用),通过函数模板生产出来的函数
int ival = max(100,99); // 自动生成int数据类型
char cval = max<char>('A','B'); // 用<char>指定数据类型
关键字的使用2:
// 函数模板
template <class T>
void swap(T &a,T &b){
T tmp = a; a = b; b = tmp;
}
// 模板函数
int x = 20,y = 30;
swap<int>(x,y); // 用<int>指定数据类型
变量作为模板参数:
// 函数模板
template <int size>
void display(){
cout << size << endl;
}
// 模板函数
display<10>();
多参数函数模板:
// 函数模板
// 后面的typename也不能省略
template <typename T,typename C>
void display(T a,C b){
cout << a << " " << b << endl;
}
// 模板函数
int a = 1024;string str = "hello world!";
display<int, string>(a,str);
函数模板定义中的混用:
// typename和class混用
template <typename T,class >
// 和变量混用
template <typename T,int size>
void display(T a){
for(int i = 0;i < size;i++){
cout << a << endl;
}
}
display<int,5>(15);
小总结:
- 函数模板的参数个数可以是一个也可以是多个。(模板的参数是指<>中的内容)
- 函数模板的参数个数
可以为零个。不可以为零个参数,参数个数如果为0个,则没有必要使用函数模板。 - 使用函数模板时,需要指定模板参数,此时的函数称为模板函数。
- 当需要定义多个功能相同,数据类型不同的函数时,可以使用函数模板来定义。
8.类模板
类模板的.h和.cpp文件:
在C++中,模板代码不能分离编译(不能写成.h+.cpp这种形式,只能把所有的代码都写在.h文件中使用),不然部分编译器无法通过编译。在使用的时候,只需要把.h文件include
""进当前程序中。
类模板的定义与使用1:
template<class T>
class MyArray{
public:
// 1.类内定义
void display();
private:
T *m_pArr;
}
// 2.类外定义
// 类外定义类名后面需要添加<>
// 每定义一个成员函数,都要在这个成员函数前面添加template<......>
template<class T>
void MyArray<T>::display(){
......
}
// 使用
int main(){
MyArray<int> arr;
arr.display();
return 0;
}
类模板的定义与使用2:
写的时候需要特别注意各种修饰符、限定符、返回值类型,很容易写错。
#include <iostream>
using namespace std;
/**
* 定义一个矩形类模板Rect
* 成员函数:calcArea()、calePerimeter()
* 数据成员:m_length、m_height
*/
template<typename T>
class Rect
{
public:
Rect(T length,T height);
T calcArea();
T calePerimeter();
public:
T m_length;
T m_height;
};
/**
* 类属性赋值
*/
template<typename T>
Rect<T>::Rect(T length,T height)
{
m_length = length;
m_height = height;
}
/**
* 面积方法实现
*/
template<typename T>
T Rect<T>::calcArea()
{
return m_length * m_height;
}
/**
* 周长方法实现
*/
template<typename T>
T Rect<T>::calePerimeter()
{
return ( m_length + m_height) * 2;
}
// 使用
int main(void)
{
Rect<int> rect(3, 6);
cout << rect.calcArea() << endl; // 输出:18
cout << rect.calePerimeter() << endl; // 输出:18
return 0;
}
小总结:
- 定义一个类模板就相当于定义了一系列功能相同类型不同的类。
- 定义类模板需要使用关键字template。
- 定义类模板的参数可以使用typename和class,
但不能混用可以混用。 - 模板参数既可以是类型,也可以是变量。
9.标准模板库STL(Standard Template Lib)
vector向量
**本质:**对数组的封装
**特点:**读取快速,读取能在常数时间内完成,数据插入慢,如果插入数据,后面数据的位置都要移动。
初始化:
[外链图片转存失败(img-8yBYgdSb-1563352671639)(./images/muke_C++/014.png)]
常用操作:
[外链图片转存失败(img-Wy4kKhuO-1563352671640)(./images/muke_C++/015.png)]
向量的遍历:
除了在for、while中使用下标进行遍历,还可以使用迭代器。
// for遍历
for(int i = 0;i < vec.size();i++){
cout << vec[i] << endl;
}
迭代器(iterator)及其使用方法:
int main(){
vector vec;
vec.push_back("hello");
// 定义、初始化迭代器iterator
vector<string>::iterator citer = vec.begin();
// 使用迭代器遍历向量
for( ;citer != vec.end();citer++){
cout << *citer << endl;
}
return 0;
}
链表(list)
**特点:**数据插入速度快,查询较慢。
操作和vector向量类似,也可以使用迭代器,但不能用下标循环遍历。
映射(map)
键值对,有点类似数组。
// 创建一个映射
map<int,string> m;
// 创建映射关系
pair<int,string> p1(10,"shanghai");
pair<int,string> p2(20,"beijing");
// 将创建的映射关系插入映射中
m.insert(p1);
m.insert(p2);
// 调用映射
cout << m[10] << endl;
cout << m[20] << endl;
也可以使用字符串作为索引:
map<string,string> m;
pair<string,string> p1("SH","shanghai");
pair<string,string> p2("BJ","beijing");
m.insert(p1);
m.insert(p2);
cout << m["SH"] << endl;
cout << m["BJ"] << endl;
映射的遍历:
**注意:**打印是按索引(key)的顺序打印的,如“A”,先于“B”打印。
// 使用迭代器进行遍历
map<int,string>::iterator itor = m.begin();
for(;itor != m.end();itor++){
cout << itor->first << endl; // 输出key
cout << itor->second << endl; // 输出value
cout << endl;
}
小总结:
- vector是对数组的封装,所以一旦对象被实例化,
其大小就不能改变了大小可以根据元素数量改变。 - list的特点是数据插入速度快。
- map需要与pair一起使用,用来存储多个key-value对。
- 不同厂商的标准模板库的实现细节可以不同,基本用法及原理相同。
练习:
// 使用vector存储数字3,6,8,4,并遍历。
// 使用map存储S-Shang Hai B-Bei Jing G-Guang Zhou,并遍历
#include <vector>
#include <map>
#include <string>
#include <iostream>
using namespace std;
int main(void)
{
// 使用vector存储数字:3、4、8、4
vector<int> vec;
vec.push_back(3);
vec.push_back(4);
vec.push_back(8);
vec.push_back(4);
//循环打印数字
vector<int>::iterator citer = vec.begin();
for( ; citer != vec.end(); citer++){
cout << *citer << endl;
}
// 使用map来存储字符串键值对
map<string, string> m;
pair<string,string> p1("S","Shang Hai");
pair<string,string> p2("B","Bei Jing");
pair<string,string> p3("G","Guang Zhou");
m.insert(p1);
m.insert(p2);
m.insert(p3);
// 打印map中数据
map<string,string>::iterator citer2 = m.begin();
for( ; citer2 != m.end(); citer2++){
cout << citer2->first << endl; // key
cout << citer2->second << endl; // value
}
return 0;
}
/* 输出:
3
4
8
4
B
Bei Jing
G
Guang Zhou
S
Shang Hai
*/
**注意:**打印是按索引(key)的顺序打印的,如“A”,先于“B”打印。
本篇为视频教程笔记,视频如下: