一、友元
类实现了对数据的隐藏和封装,类的成员变量一般定义为私有的,仅能通过公开的接口进行读写。如果不封装成员变量,直接公开,则有破坏了面向对象的特性。
但是在某些情况下,需要频繁读写成员变量,但是由于使用公开的接口中的参数传递、类型检查、权限检查等都需要时间和性能开销,会影响程序的执行效率。此时可以通过友元来读写成员变量。
友元是一种定义在类外部的函数,但是可以在类内进行说明,为了和该类中的成员函数加以区别,在说明前使用关键字friend修饰,这样的类外的函数可以直接访问类内的所有成员。
友元提高了程序运行的效率,但是破坏了封装性,导致程序的可维护性变差,因此使用友元要慎重。
友元分类: 友元函数、 友元类、 友元成员函数
1.2 友元函数
友元函数是一种在类内说明,但是属于类外,又可以访问所有成员的函数,因此友元函数没有this指针,需要在参数中增加被访问的类的对象的引用。
#include <iostream>
using namespace std;
class Test
{
private:
int number;
public:
Test(int number):number(number){}
int get_number() const
{
return number;
}
//友元函数说明
friend void test_friend(Test& t);
};
//友元函数定义
void test_friend(Test& t)
{
cout << ++t.number << endl;
}
int main()
{
Test stu(2);
test_friend(stu);
cout << stu.get_number() << endl;
return 0;
}
需要注意:友元函数的说明可以放在类中的任意位置(任意权限下),因为它不是类中的函数。另外,同一个函数理论上可以成为多个类的友元函数,只需要在各个类分别说明即可。
1.3 友元类
当一类B成为了另一个类A的“朋友”时,类A的所有成员就可以被类B访问,此时类B就是类A的友元类。
#include <iostream> using namespace std; class TestA { private: int number; public: TestA(int number):number(number){} int get_number() const { return number; } //友元类说明 friend class TestB; }; class TestB { private: public: int get_a_number(TestA& a) const{ return a.number; } void set_a_number(TestA& a,int m){ a.number = m; } }; int main() { TestA a(0); TestB b; // 通过B类对象读写A类的属性值 cout<< b.get_a_number(a) <<endl; b.set_a_number(a,1); cout << b.get_a_number(a) << endl; return 0; }
需要注意:友元类的特性:
1.友元类无法被继承。
2.友元关系不具有交换性
3.友元关系不具有传递性
1.4 友元成员函数
如果类B的某一个成员函数可以访问类A的所有成员,这个类B的成员函数就是类A的友元成员函数。
编写友元成员函数时,一定要注意相互之间的依赖关系。
#include <iostream>
using namespace std;
//第三步声明类A
class TestA;
//第二步,因为友元的说明中用到了类B,因此这部分要写在第一步之前
//又因为友元的说明在后面,因此这里只能先进行声明
class TestB
{
private:
public:
void test_friend(TestA& a);
};
//第一步,确定被访问的类和友元的关系
class TestA
{
private:
int number;
public:
TestA(int number):number(number){}
int get_number() const
{
return number;
}
//友元成员函数的说明
friend void TestB::test_friend(TestA& a);
};
//第四步:补充友元成员函数的定义
void TestB::test_friend(TestA &a)
{
cout << ++a.number << endl;
}
int main()
{
TestA a(0);
TestB b;
cout<< a.get_number() <<endl;
b.test_friend(a);
cout << a.get_number() << endl;
return 0;
}
二、运算符重载 operator
2.1 概念
函数可以重载,运算符也可以重载。
c++中的运算符操作对象只能是基本数据类型,但是对于程序员自定义的类型,也需要类似的运算符操作,此时就可以重新定义这些运算符的规则,赋予其新的运算功能,执行特定的操作。
在c++中,可以把大多数运算符看做是一个函数,本质就是函数重载。
并不是所有的运算符都支持重载,可以重载的运算符有:
算术运算符:+ 、- 、* 、/ 、% 、++ 、--
位操作运算符: & 、 | 、~ 、^ 、<< 、>>
逻辑运算符:! 、&& 、||
比较运算符:< 、> 、>= 、<= 、== 、!=
赋值运算符:= 、+= 、-= 、*= 、/= 、%= 、&= 、|= 、^= 、<<= 、>>=
其他运算符:[] 、() 、-> 、, 、new 、delete 、new[] 、delete[]
不能重载的运算符有:
成员运算符“ . ” 、指针运算符“ * ” 、三目运算符“ ? : ” 、sizeof 、 作用域“ :: ”
运算符重载有两种:
1.友元函数运算符重载
2.成员函数运算符重载
2.2 友元函数运算符重载
#include <iostream>
using namespace std;
class Integer
{
private:
int value;
public:
Integer(int value):value(value){}
int get_value()
{
return value;
}
//友元函数运算符重载的说明:双目运算符
friend Integer operator +(Integer& i1, Integer& i2);
//单目运算符
friend Integer operator ++(Integer& i);
friend Integer operator ++(Integer& i,int);
};
//定义
Integer operator +(Integer& i1, Integer& i2)
{
return i1.value + i2.value;
}
//单目运算符前置++
Integer operator ++(Integer& i)
{
return ++i.value;
}
//单目运算符后置++
Integer operator ++(Integer& i,int)
{
return i.value++;
}
int main()
{
Integer i1(1);
cout << (i1++).get_value() << endl; // 1
Integer i2(2);
cout << (++i2).get_value() << endl; // 3
Integer i3 = i1 + i2;
cout << i3.get_value() << endl; // 5
return 0;
}
2.3 成员函数运算符重载
与友元函数运算符重载的区别是,成员函数属于类,因此如果在类内声明类外定义时,类外的要使用类名::修饰函数名称。
另外,成员函数运算符重载的函数参数比友元函数运算符重载的函数参数数量少一个,因为在成员函数运算符重载中,第一个运算数使用this指针表示,列如前置单目运算符在成员函数运算符重载中没有参数、双目运算符在成员函数运算符重载中只有一个参数.......
#include <iostream>
using namespace std;
class Integer
{
private:
int value;
public:
Integer(int value):value(value){}
int get_value()
{
return value;
}
//友元函数运算符重载的说明:双目运算符
Integer operator +(Integer& i);
//单目运算符
Integer operator ++();
Integer operator ++(int);
};
//定义
Integer Integer::operator +(Integer& i)
{
return this->value + i.value;
}
//单目运算符前置++
Integer Integer::operator ++()
{
return ++this->value;
}
//单目运算符后置++
Integer Integer::operator ++(int)
{
return this->value++;
}
int main()
{
Integer i1(1);
cout << (i1++).get_value() << endl; // 1
Integer i2(2);
cout << (++i2).get_value() << endl; // 3
Integer i3 = i1 + i2;
cout << i3.get_value() << endl; // 5
return 0;
}
2.4 赋值运算符重载
编译器会为每个类增加一个默认的赋值运算符重载的成员函数,赋值运算符重载只能通过成员函数运算符重载,不支持友元函数运算符重载。
作用:用一个对象的值改变另一个对象的值
注意事项:它和拷贝构造函数类似,也存在浅拷贝问题。如果都是基本数据类型不需写出来,但。如果有指针类型的数据成员时需要给出赋值运算符重载,避免地址值的复制
#include <iostream>
using namespace std;
class Integer
{
private:
int value;
public:
Integer(int value):value(value){}
int get_value()
{
return value;
}
//赋值运算符重载
Integer& operator =(Integer& i)
{
this->value = i.value;
return *this;
}
};
int main()
{
Integer i1(1);
Integer i2(2);
i2 = i1;
cout << i2.get_value() << endl;
return 0;
}
2.5 类型转换运算符重载
目的:使自定义的对象转换成任意类型。
特点:此函数也只支持使用成员函数重载
#include <iostream>
using namespace std;
class Teacher{
private:
string subject;
public:
Teacher(string subject){
this->subject=subject;
}
void show(){
cout<<subject<<endl;
}
operator string(){
return subject;
}
};
int main()
{
Teacher t1("C#");
t1.show();
string str="C语言";
Teacher t2=str;
t2.show();
string s=t1;
cout<<"s的值:"<<s<<endl;
}
2.6 注意事项
●运算符重载限制在C++已有的运算符范围内,不允许创建新的运算符。
●运算符重载也是函数重载,运算符也是函数。
●重载之后的运算符不能改变优先级和结合性。
●重载之后的运算符不能改变操作数和语法结构。
●运算符重载不能改变该运算符用于基本数据类型的含义,但是可以把基本数据类型与自定义类型一起运算,或者都使用自定义类型。
●运算符重载是针对新类型数据的实际需要对原有运算符的功能进行扩充,因此重载之后的功能应该与原有的功能类似,避免没有目的地使用运算符重载。
●通常建议单目运算符使用成员函数运算符重载,双目运算符使用友元函数运算符重载。
三、字符串类 std::string
类内有许多函数(构造函数、成员函数、运算符重载等),内置了许多字符串处理的相关函数。(部分讲解)
#include <iostream>
#include <string.h>
using namespace std;
int main()
{
string s;//生成一个空字符串对象
//判断内容是否为空
cout << s.empty() << endl;
//隐式调用构造函数
string s1 = "hello";
//显式调用构造函数
string s2 = "world";
string semp = "hello";
//比较的是编码
cout << (s1 == s2) << endl;
cout << (s1 == semp) << endl;
//拷贝构造函数
string s3(s2);
cout << s3 << endl;
//参数1:char* 源字符串
//参数2:保留前几个字符
string s4("ABCDEFG",2);
cout << s4 << endl;
//参数1:string 源字符串
//参数2:不保留前几个字符
string s5(s3,2);
cout << s5 << endl;
//参数1:字符数量
//参数2:字符内容
string s6(5,'Z');
cout << s6 << endl;
//字符串交换
swap(s5,s6);
cout << s5 << " " << s6 << endl;
//字符串连接符 +
string s7 = s5 + s6;
cout << s7 << endl;
//向后追加字符串
s7.append("234H").append("210");//支持链式调用
cout << s7 << endl;
//向后追加单字符
s7.push_back('A');
cout << s7 << endl;
//参数1:插入的位置
//参数2:插入的内容
s7.insert(1,"666");
cout << s7 << endl;
//参数1:删除的起始位置
//参数2:删除的字符数量
s7.erase(4,10);
cout << s7 << endl;
//参数1:替换的起始位置
//参数2:替换的字符数量
//参数3:新替换的内容
s7.replace(0,3,"#######");
cout << s7 << endl;
//清空
s7.clear();
cout << s7.length() << endl;
//C's string 转化为 C++'s string
//直接赋值就可以隐式调用对应的构造函数
string s8 = "jhfahffhioahhf";
cout << s8 << endl;
//C++'s string 转化为 C's string
//s8.c_str();//返回值就是char *,但是不稳定
char c[20];
strcpy(c,s8.c_str());
cout << c << endl;
return 0;
}
四、模板 template
4.1 模板的概念
模板是支持参数化多态的工具,可以让类或函数支持一种通用类型,在类中的属性、成员函数的参数、返回值在实际使用时可以是任何数据类型。
通过模板程序员就可以编写一些与类型无关的代码,模板为不同的数据类型制定了一套通用的算法。
模板通常有两种形式:
1.函数模板
2.类模板
4.2 函数模板
函数模板针对参数类型不同的函数,参数类型不一样,但是功能与函数名称相同。
#include <iostream>
using namespace std;
//模板说明
template <class T>
void Swap(T & t1,T & t2){
T temp;
temp=t1;
t1=t2;
t2=temp;
}
//模板说明
template <class Th>
Th Add_add(Th & t1,Th & t2)
{
return t1 + t2;
}
int main()
{
int a=2,b=3;
Swap(a,b);
cout << a << " " << b << endl;
double d1=2.2,d2=3.3;
Swap(d1,d2);
cout << d1 << " " << d2 << endl;
string a1 = "stu";
string a2 = "jfg";
Swap(a1,a2);
cout << a1 << " " << a2 << endl;
cout << Add_add(a,b) << endl;
cout << Add_add(d1,d2) << endl;
cout << Add_add(a1,a2) << endl;
return 0;
}
4.3 类模板
类模板可以把模板的范围扩展到一个类中。
#include <iostream>
using namespace std;
//模板说明
template <typename T>
class Test
{
private:
T value;
public:
Test(T value):value(value){}
T get_value()
{
return value;
}
};
//模块说明
template <typename M>
class Test_M
{
private:
M value;
public:
//类内声明
Test_M(M value);
M get_value();
};
//类外定义
template <typename M>
Test_M<M>::Test_M(M value)
{
this->value = value;
}
template <typename M>
M Test_M<M>::get_value()
{
return value;
}
int main()
{
Test<int> t1(45);
cout << t1.get_value() << endl;
Test<double> t2(4.5);
cout << t2.get_value() << endl;
Test<string> t3("hello");
cout << t3.get_value() << endl;
Test_M<int> m1(100);
cout << m1.get_value() << endl;
Test_M<double> m2(10.5);
cout << m2.get_value() << endl;
Test_M<string> m3("world");
cout << m3.get_value() << endl;
return 0;
}
五、容器
5.1 容器的概念
泛型编程是一种编程理念,其提出的动机是发明一种语言机制,能够在编程中编写完全一般化的可重复使用的算法,不会因为数据类型的不同而有差异。
美国的惠普实验室针对泛型编程开发了一系列软件工具,被称为标准模板库(STL),后在C++中广泛引入。主要包括:算法、容器、迭代器。
在STL中最核心的内容是容器和迭代器。
容器指的是用来存储数据的集合,由于内部存储的数据使用模板实现,因此可以支持绝大多数数据类型。迭代器是配合容器进行高效遍历的工具,是一种特殊的指针。
容器类自动申请和释放内存,无需new和delete操作。
容器类的使用需要导入头文件(string除外),所有的容器类都属于std名字空间。
5.2 顺序容器
顺序容器是一种各个元素之间具有顺序关系的结构集合。每个元素都有固定的位置。可以通过下标访问。除非使用插入和删除等操作才会改变位置。(array的长度是固定死了)
5.2.1 array数组
array是C++11中新增的容器类型,相比传统数组更加安全和易于使用。
#include <iostream>
#include <array>//引入数组容器头文件
using namespace std;
int main()
{
//创建一个长度为5的int数组
array<int,5> arr = {2,4,6,8,10};
cout << arr[0] << endl; // 2
cout << arr[4] << endl; // 10
cout << arr.at(1) << endl; // 4
arr[1] = 666;
cout << arr.at(1) << endl; // 666
//普通遍历for
for(int i = 0; i < arr.size(); i++)
{
cout << arr.at(i) << " ";
}
cout << endl;
//给所有元素赋值都赋值为5
arr.fill(5);
//for-each遍历
for(int i:arr)
{
cout << i << " ";
}
cout << endl;
//迭代器:下面会说
return 0;
}
5.2.2 vector 向量
vector内部使用数组实现(实质是动态数组),能高效地进行随机存取,但是插入和删除的效率较低。最常用的一种顺序容器(string除外的)。
#include <iostream>
#include <vector>//引入向量容器头文件
using namespace std;
int main()
{
//创建一个长度为0的向量对象
vector<int> vec1;
cout << vec1.empty() << endl;//判断是否为空
//创建一个长度为5的向量对象
vector<int> vec2(5);
cout << vec2[0] << endl; // 0
cout << vec2[4] << endl; // 0
//创建一个长度为5的向量对象,且数据都是666
vector<int> vec3(5,666);
//for 遍历
for(int i = 0; i < vec3.size(); ++i)
{
cout << vec3.at(i) << " ";
}
cout << endl;
//向后追加元素
vec3.push_back(888);
//在1位置插入元素2(位置从0开始)
//参数1:插入的位置
//参数2:插入的元素
vec3.insert(vec3.begin()+1,2);
//vec3.end()指向的是最后一个元素的后一位位置
//在最后一位插入元素222
vec3.insert(vec3.end()-1,222);
//for-each遍历
for(int i:vec3)
{
cout << i << " ";
}
cout << endl;
//删除第一个元素
vec3.erase(vec3.begin());
//删除最后一位元素
vec3.erase(vec3.end()-1);
vec3.pop_back();
//for-each遍历
for(int i:vec3)
{
cout << i << " ";
}
cout << endl;
//清空
vec3.clear();
//迭代器:下面会说
return 0;
}
5.2.3 list列表
list内部使用双向链表实现,能高效地进行插入和删除操作,但是随机存取的效率较低。另外,list不支持使用下标操作元素,需要使用迭代器指针操作元素。
#include <iostream>
#include <list>//引入向量容器头文件
using namespace std;
int main()
{
//创建一个四个hello元素的列表对象
list<string> lis1(4,"hello");
//向后追加元素
lis1.push_back("bye");
//向前追加元素
lis1.push_front("start");
//取出头部元素
cout << lis1.front() << endl;
//取出尾部元素
cout << lis1.back() << endl;
//删除头部元素
lis1.pop_front();
//删除尾部元素
lis1.pop_back();
//在第二个元素插入"2B"
lis1.insert(++lis1.begin(),"2B");
//在倒数第二个位置插入元素"3Y"
lis1.insert(--lis1.end(),"3Y");
//保存迭代器指针
list<string>::iterator iter = lis1.begin();
//修改第一个元素
*iter = "one";
//不支持普通的for循环遍历,但支持for-each遍历
for(string s:lis1)
{
cout << s << " ";
}
cout << endl;
//移动迭代器指针
//参数1:要移动的迭代器指针对象
//参数2:移动的距离,可以正负
advance(iter,3);
//在第四个元素的位置插入"S444"
lis1.insert(iter,"s444");
//删除倒数第一个元素
lis1.erase(--lis1.end());
//不支持普通的for循环遍历,但支持for-each遍历
for(string s:lis1)
{
cout << s << " ";
}
cout << endl;
//排序:字符串类型是按编码排序,数字就是大小排序,都是从小到大。
lis1.sort();
//不支持普通的for循环遍历,但支持for-each遍历
for(string s:lis1)
{
cout << s << " ";
}
cout << endl;
//清空
lis1.clear();
list<string> lis2;
cout << lis2.empty() << endl;
//迭代器:下面会说
return 0;
}
5.2.4 deque队列
deque的性能基本兼顾vector和list,性能均衡。基本支持所有的常用API接口。
5.3 关联容器
关联容器的各元素之间没有严格的物理上的顺序关系,但是在内部仍然具有排序的特点,因此可以使用迭代器进行遍历。 常见的关联容器有:map、muitimap等,以map(键值对映射)为例,进行管理容器的讲解。
map与multimap的区别在于,前者是单重键值对映射(—一映射),后者允许一个键对应多个值,map更为常用。
键必须唯一,通常使用字符串类型;值可以重复,可以是任何类型。
#include <iostream>
#include <map>//引入头文件
using namespace std;
int main()
{
//创建一个内容为空的map对象
map<string,int> map1;
//添加元素
map1["height"] = 175;
map1.insert(pair<string,int>("country",86));
map1["age"] = 25;
map1["salary"] = 5000;
map1["weight"] = 80;
//已经存在这个键值对,因此再次赋值就是修改键对应的值
map1["weight"] = 75;
//迭代器指针
//如果有对应的键,则指向键值对
//如果没有对应的键,则指向"最后一个元素"的后面
map<string,int>::iterator iter = map1.find("salary");
if(iter != map1.end())
{
//取出元素
int value = map1.find("salary")->second;
cout << value << endl;
}
else
{
cout << "没有对应的键值" << endl;
}
//删除元素
if(map1.erase("salary"))
{
cout << "删除成功,还剩下" << map1.size() << "个键值对" << endl;
}
else
{
cout << "删除失败" << endl;
}
//清空
map1.clear();
cout << map1.empty() << endl;
//迭代器:下面会说
return 0;
}
5.4 迭代器
迭代器是一种特殊的指针,通常用于遍历所有的容器类型,对容器遍历的兼容性最好,且遍历效率高,推荐使用迭代器进行遍历。
不同的容器类型使用迭代器遍历的方式基本一致,需要注意的是const_iterator是只读迭代器,iterator是读写迭代器。
#include <iostream>
#include <array>
#include <vector>
#include <list>
#include <deque>
#include <map>//引入头文件
using namespace std;
int main()
{
//string 的只读迭代器
string str = "ABCDEF789%";
for(string::const_iterator iter = str.begin(); iter != str.end(); ++iter)
{
cout << *iter << " ";
}
cout << endl;
//array 的读写迭代器
array<double,20> arr;
arr.fill(3.14);
for(array<double,20>::iterator iter = arr.begin(); iter != arr.end(); ++iter)
{
cout << *iter << " ";
}
cout << endl;
//vector 的只读迭代器
vector<int> vec(4,4);
vector<int>::const_iterator iter_vec = vec.begin();
while(iter_vec != vec.end())
{
cout << *iter_vec << endl;
++iter_vec;
}
list<string> lis;
lis.push_back("hafh");
lis.push_back("hello");
lis.push_back("world");
lis.push_back("hahaha");
//list 的读写迭代器
list<string>::iterator iter_lis = lis.begin();
while(iter_lis != lis.end())
{
cout << (*iter_lis).append("~") << endl;
++iter_lis;
}
deque<int> deq(5,5);
for(deque<int>::iterator iter = deq.begin();
iter != deq.end(); ++iter)
{
cout << *iter << " ";
}
cout << endl;
map<string,string> map1;
map1["name"] = "Tom";
map1["age"] = "7";
map1["friend"] = "Jerry";
//map 的只读迭代器
for(map<string,string>::const_iterator iter = map1.begin();
iter != map1.end(); ++iter)
{
cout << iter->first << " ";
cout << iter->second << endl;
}
return 0;
}