四、运算符重载
1.友元
1.1 概念
类实现了数据的隐藏和封装,类的数据成员一般定义为私有成员,仅能通过类的公有成员函数才能进行读写。
如果数据成员定义成公共的,则又破坏了封装性。但是在某些情况下,需要频繁的读写数据成员,特别实在对某些成员函数多次调用时,由于参数传递、类型检查和安全性检查都是需要时间开销的,而影响程序的运行效率。
友元有三种实现方式:
- 友元函数
- 友元类
- 友元成员函数
友元函数是一种定义在类外部的普通函数,但是他需要在类内进行声明,为了和该的成员函数加以区分,在声明时前面加一个关键字friend。
友元不是成员函数,但是能够访问当前类中的所有成员(包括私有成员)。
友元在于提高的程序运行效率,但是它破坏了类的封装性和隐藏性,使得非成员函数能够访问类的私有成员,导致程序的维护性变差,因此使用友元要慎重。
1.2 友元函数(熟悉)
友元函数不属于任何一个类,但是可以访问类中所有的成员(包括私有成员)。
需要在类内进行声明。
#include <iostream> // 引入标准输入输出流库,用于控制台输入输出
using namespace std; // 使用标准命名空间,以便直接使用标准库中的对象和函数(如cout、endl)而无需加上std::前缀。
class Test
{
private: // private访问控制符声明以下成员是私有的,即只能在类的内部访问。
int a;
public: // 声明以下成员是公共的,即可以从类外部访问。
Test(int i):a(i){} // 是一个构造函数,用于创建对象时初始化成员变量a,参数i传递的值赋给a。
void show()
{
cout << a << " " << &a << endl;
}
// 友元函数,类内声明
friend void and_test(Test &t); // 可以访问类Test的私有成员。虽然声明在类内部,但定义在类外部。
};
// 友元函数定义
void and_test(Test &t)
{
cout << t.a << endl; // 打印传递对象t的私有成员a的值
cout << ++t.a << " " << &t.a << endl;
}
int main()
{
Test t1(1); // 创建一个名为t1的Test对象,并将a初始化为1
and_test(t1); // 调用友元函数and_test,传递t1对象
t1.show(); // 调用t1的show方法,打印t1的成员a的值及其内存地址
return 0;
}
友元函数的使用需要注意以下几点:
- 友元函数没有this指针。
- 友元函数的“声明”可以放置到类中的任何位置,不受权限修饰符的影响。
- 一个友元函数理论上来说可以访问多个类,只需要在各个类中进行声明。
1.3 友元类(掌握)
当一个类B成为了另一个类Test的朋友时,类Test的所有成员都可以被类B访问,此时类B就是类Test的友元类。
#include <iostream>
using namespace std;
class Test
{
private:
int a;
public:
Test(int i):a(i){}
void show()
{
cout << a << " " << &a << endl;
}
// 友元类,类内声明
friend class B;
};
class B
{
public:
void and_test(Test &t)
{
cout << t.a << endl;
cout << ++t.a << " " << &t.a << endl;
}
void and_test2(Test &t)
{
cout << t.a << endl;
cout << ++t.a << " " << &t.a << endl;
}
};
int main()
{
Test t1(2);
B b;
b.and_test(t1);
b.and_test2(t1);
t1.show();
return 0;
}
友元类的使用需要主要以下几点:
- 友元关系不能被继承。
- 友元关系不具有交换性(比如:类B声明成类Test的友元。类B可以访问类Test中的成员,但是类Test不能访问类B的私有成员,如果需要访问,需要将类Test声明成类B的友元,即互为友元)。
互为友元,需要类内声明,类外实现。
#include <iostream>
using namespace std;
class Cat;
class Test
{
private:
int a;
public:
Test(int i):a(i){}
void test(Cat &c);
friend class Cat;
};
class Cat
{
private:
int b;
public:
Cat(int i):b(i){}
void test1(Test &t);
friend class Test;
};
void Test::test(Cat &c)
{
cout <<c.b<<endl;
}
void Cat::test1(Test &t)
{
cout <<t.a++<<endl;
}
int main()
{
Test t(44);
Cat c(12);
c.test1(t);
return 0;
}
1.4 友元成员函数(熟悉)
使类B中的成员函数成为类Test的友元成员函数,这样类B的该成员函数就可以访问类Test所有的成员了。
#include <iostream>
using namespace std;
// 第四步:声明被访问的类
class Test;
class B
{
public:
// 第二步:声明友元成员函数(类内声明,类外实现)
void and_test(Test &t);
};
class Test
{
private:
int a;
public:
Test(int i):a(i){}
void show()
{
cout << a << " " << &a << endl;
}
// 友元成员函数 第一步:确定友元函数的格式并声明
friend void B::and_test(Test &t);
};
// 第三步:类外定义友元成员函数
void B::and_test(Test &t)
{
cout << t.a << endl;
cout << ++t.a << " " << &t.a << endl;
}
int main()
{
Test t1(2);
B b;
b.and_test(t1);
t1.show();
return 0;
}
2、运算符重载(掌握)
2.1 概念
C++中可以把部分运算符看作是函数,此时运算符也可以重载。
运算符预定义的操作只能针对基本数据类型,但是对于自定义类型,也需要类似的运算操作。此时就可以重新定义这些运算符的功能,使其支持特定类型,完成特定的操作。
运算符重载有两种实现方式:
- 友元函数运算符重载
- 成员函数运算符重载
2.2 友元函数运算符重载
运算符表达式与函数的对应关系如下所示:
#include <iostream>
using namespace std;
class MyInt
{
private:
int a;
public:
MyInt(int a):a(a){}
int get_int()
{
return a;
}
// +运算符重载 友元函数实现
friend MyInt operator +(MyInt &i,MyInt &i2);
};
MyInt operator +(MyInt &i,MyInt &i2)
{
// MyInt i3(0);
// i3.a = i.a + i2.a;
// return i3;
// int → MyInt 隐式调用构造函数
return i.a + i2.a;
}
int main()
{
MyInt a1(1);
MyInt a2(2);
MyInt a3 = a1 + a2;
cout << a3.get_int() << endl;
return 0;
}
自增运算符重载
#include <iostream>
using namespace std;
class MyInt
{
private:
int a;
public:
MyInt(int a):a(a)
{
// cout << "构造函数" << endl;
}
int get_int()
{
return a;
}
// +运算符重载 友元函数实现
friend MyInt operator +(MyInt &i,MyInt &i2);
friend MyInt operator ++(MyInt &i); // 前置++
friend MyInt operator ++(MyInt &i,int); // 后置++
};
MyInt operator +(MyInt &i,MyInt &i2)
{
// MyInt i3(0);
// i3.a = i.a + i2.a;
// return i3;
// int → MyInt 隐式调用构造函数
return i.a + i2.a;
}
MyInt operator ++(MyInt &i)
{
return ++i.a;
}
MyInt operator ++(MyInt &i,int)
{
return i.a++;
}
int main()
{
MyInt a1(1);
MyInt a2(2);
MyInt a3 = a1 + a2;
cout << (++a3).get_int() << endl; // 前置自增 4
cout << (a3++).get_int() << endl; // 4
cout << (a3++).get_int() << endl; // 5
cout << a3.get_int() << endl; // 6
return 0;
}
2.3 成员函数运算符重载
成员函数运算符重载相比于友元函数运算符重载,最大区别在于,友元函数的第一个输入参数,在成员函数运算符中使用this指针代替。因此同样的运算符重载,成员函数比友元函数参数少一个。
#include <iostream>
using namespace std;
class MyInt
{
private:
int a;
public:
MyInt(int a):a(a)
{
// cout << "构造函数" << endl;
}
int get_int()
{
return a;
}
// +运算符重载 友元函数实现
MyInt operator +(MyInt &i2);
MyInt operator ++(); // 前置++
MyInt operator ++(int); // 后置++
};
MyInt MyInt::operator +(MyInt &i2)
{
// int → MyInt 隐式调用构造函数
return this->a + i2.a;
}
MyInt MyInt::operator ++()
{
return ++this->a;
}
MyInt MyInt::operator ++(int)
{
return this->a++;
}
int main()
{
MyInt a1(1);
MyInt a2(2);
MyInt a3 = a1 + a2; // a1.op+(a2)
cout << (++a3).get_int() << endl; // 前置自增 4
cout << (a3++).get_int() << endl; // 4
cout << (a3++).get_int() << endl; // 5
cout << a3.get_int() << endl; // 6
return 0;
}
2.4 特殊运算符重载
2.4.1 赋值运算符重载
除了之前学的无参构造函数、拷贝构造函数与析构函数以外,如果程序员不手写,编译器还会给一个类添加赋值运算符重载。
赋值运算符重载只能使用成员函数运算符重载。
#include <iostream>
using namespace std;
class MyInt
{
private:
int a;
public:
MyInt(int a):a(a)
{
// cout << "构造函数" << endl;
}
int get_int()
{
return a;
}
// +运算符重载 友元函数实现
MyInt operator +(MyInt &i2);
MyInt operator ++(); // 前置++
MyInt operator ++(int); // 后置++
// 编译器会自动添加赋值运算符重载函数
MyInt & operator =(const MyInt &i)
{
cout << "赋值运算符重载函数被调用了" << endl;
this->a = i.a;
return *this;
}
};
MyInt MyInt::operator +(MyInt &i2)
{
// int → MyInt 隐式调用构造函数
return this->a + i2.a;
}
MyInt MyInt::operator ++()
{
return ++this->a;
}
MyInt MyInt::operator ++(int)
{
return this->a++;
}
int main()
{
MyInt a1(1);
MyInt a2(2);
MyInt a3 = a1 + a2;
cout << (++a3).get_int() << endl; // 前置自增 4
cout << (a3++).get_int() << endl; // 4
cout << (a3++).get_int() << endl; // 5
cout << a3.get_int() << endl; // 6
MyInt a4 = a3; // 调用拷贝构造函数
a4 = a2; // 赋值运算符重载
a3 = a2;
cout << a3.get_int() << endl;
return 0;
}
当类中出现指针类型的成员变量时,默认的赋值运算符重载函数类似于默认的浅拷贝构造函数,因此也需要手动辨析解决“浅拷贝”的问题。
【面试题】一个类什么也不写,编译器加了那些代码?
无参构造函数、拷贝构造函数、析构函数、赋值运算符重载函数。
空类的大小是一个字节。
如果不写任何权限,默认权限为私有权限。
2.4.2 类型转换运算符重载
必须使用成员函数运算符重载,且格式比较特殊。
#include <iostream>
using namespace std;
class MyInt
{
private:
int a;
string str = "hello";
public:
MyInt(int a):a(a)
{
// cout << "构造函数" << endl;
}
int get_int()
{
return a;
}
// 编译器会自动添加赋值运算符重载函数
MyInt &operator =(const MyInt &i)
{
cout << "赋值运算符重载函数被调用了" << endl;
this->a = i.a;
return *this;
}
operator int()
{
return a;
}
operator string()
{
return str;
}
};
int main()
{
MyInt int1(2);
int a1 = int1;
cout << a1 << endl;
string str2 = int1;
cout << str2 << endl;
return 0;
}
2.5 注意事项
- 重载的运算符限制在C++语言中已有的运算符范围,不能创建新的运算符。
- 运算符重载本质上也是函数重载,但是不支持函数参数默认值的设定。
- 重载之后的运算符不能改变运算符的优先级和结合性,也不能改变运算符的操作数和语法结构。
- 运算符重载必须基于或包含自定义类型,即不能改变基本类型的运算规则。
- 重载的功能应该与原有功能类似,避免没有目的的滥用运算符重载。
- 一般情况下,双目运算符建议使用友元函数进行重载,单目运算符建议使用成员函数运算符重载。
3.std::string 字符串类(熟悉)
字符串对象是一种特殊类型的容器,专门设计用于操作字符串。
#include <iostream>
#include <string.h>
using namespace std;
int main()
{
string s; // 创建一个空字符串
// 判断是否为空
cout << s.empty() << endl;
// 调用隐式构造函数
string s1 = "hello";
cout << s1 << endl;
// 显式调用构造
string s2("world");
cout << s2 << endl;
// 判断编码 ==、!= 、> <
cout << (s1 == s2) << endl; // 0
cout << (s1 != s2) << endl; // 1
cout << (s1 > s2) << endl; // 0
cout << (s1 < s2) << endl; // 1
// 拷贝构造函数
string s3(s2); // 等同于 string s3 = s2;
cout << s3 << endl; // world
// 参数1:char * 源字符串
// 参数2:保留的字符数
string s4("ABCDEFG",3);
cout << s4 << endl; // ABC
// 参数1:std::string 源字符串
// 参数2:不保留的字符数
string s5(s2,3);
cout << s5 << endl; // ld
// 参数1:字符数量
// 参数2:字符内容 char
string s6(5,'a');
cout << s6 << endl; // aaaaa
// 交换
cout << "原s5=" << s5 <<" " << "原s6=" << s6 << endl;
swap(s5,s6);
cout << "s5=" << s5 <<" " << "s6=" << s6 << endl;
// 字符串连接
string s7 = s5 + s6;
cout << s7 << endl; // aaaaald
// 向后追加字符串
s7.append("jiajia");
cout << s7 << endl; // aaaaaldjiajia
// 向后追加单字符
s7.push_back('s');
cout << s7 << endl; // aaaaaldjiajias
// 插入
// 参数1:插入的位置
// 参数2:插入的内容
s7.insert(1,"234");
cout << s7 << endl; // a234aaaaldjiajias
// 删除字符串
// 参数1:起始位置
// 参数2:删除的字符数量
s7.erase(2,5);
cout << s7 << endl; // a2aldjiajias
// 替换
// 参数1:起始位置
// 参数2:被替换的字符数
// 参数3:替换的新内容
s7.replace(0,3,"*********");
cout << s7 << endl; // *********ldjiajias
// 清空
s7.clear();
cout << s7.length() << endl; // 0
// 直接赋值初始化(隐式调用构造函数)
string s8 = "hahaha";
cout << s8 << endl;
// 重新赋值
s8 = "ABCDEFGH";
cout << s8 << endl; // ABCDEFGH
// C++的string到c的string也就是数组
// 参数1:拷贝的目标
// 参数2:拷贝的字符数量
// 参数3:拷贝的起始位置
char arr[20] = {0};
s8.copy(arr,6,1);
cout << arr << endl; // BCDEFG
// C++ string 到C string用到了c语言的strcpy
// c_str C++的字符串转换成C语言的字符数组
// c_str返回一个const char *
char c[20] ={0};
strcpy(c,s8.c_str());
cout << c << endl; // ABCDEFGH
return 0;
}
五、模板与容器
1.模板
模板可以让类或者函数支持一种通用类型,这种通用类型在实际运行的过程中可以使用任何数据类型,因此程序员可以写出一些与类型无关的代码,这种编程方式也被称为“泛型编程”。
通常有两种形式:
- 函数模板
- 类模板
1.1 函数模板(掌握)
使一个函数支持模板编程,可以使函数支持通用数据类型。
#include <iostream>
using namespace std;
template<typename T> // class在当前学习两种关键字效果一样
T add(T a,T b)
{
return a+b;
}
int main()
{
string a = "hello";
string b = "world";
cout << add(a,b) << endl;
// 如果T类型是自定义类型,则需要重载+运算符
return 0;
}
1.2 类模板(掌握)
使一个类支持模板编程,可以使一个类支持通用数据类型。
#include <iostream>
using namespace std;
template<class T>
class Test
{
private:
T val;
public:
Test(T v):val(v){}
T get_val()const
{
return val;
}
void set_val(const T& val)
{
this->val = val;
}
};
int main()
{
// Test t1(20);
// cout << t1.get_val() << endl;
Test<int> t1(20);
cout << t1.get_val() << endl;
Test<double>t2(23);
cout << t2.get_val() << endl;
Test<string>t3("hello");
cout << t3.get_val() << endl;
return 0;
}
类内声明类外实现
#include <iostream>
using namespace std;
template<class T>
class Test
{
private:
T val;
public:
Test(T v);
T get_val()const;
void set_val(const T& val);
};
template<class T>
Test<T>::Test(T v):val(v)
{
}
template<class T>
T Test<T>::get_val()const
{
return val;
}
template<class T>
void Test<T>::set_val(const T& val)
{
this->val = val;
}
int main()
{
// Test t1(20);
// cout << t1.get_val() << endl;
Test<int> t1(20);
cout << t1.get_val() << endl;
Test<double>t2(23);
cout << t2.get_val() << endl;
Test<string>t3("hello");
cout << t3.get_val() << endl;
return 0;
}
2、容器
2.1 标准模板库STL
标准模板库(Standard Template Library,STL)是惠普实验室开发的一系列软件的统称。虽说它主要出现到了C++中,但是在被引入C++之前该技术就已经存在了很长时间。
STL的代码从广义上讲分为三类:algorithm(算法)、container(容器)和iterator(迭代器),几乎所有的代码都采用了模板类和模板函数的方式,这相比于传统的由函数和类组成的库来说提供了更好的代码重用机会。
2.2 概念
容器是用来存储数据的集合,数据元素可以是任何类型。(因为是使用模板实现)
容器类的使用,都需要引入对应的头文件。
2.3 顺序容器
顺序容器中每个元素均有固定的位置并呈现线性排布,除非使用删除或者是插入的操作改变元素位置。
2.3.1 array数组(熟悉)
array是C++11新增的容器类型,与传统数组相比更加安全、更加易于使用。array数组是定长的,没有办法方便的伸缩。
#include <iostream>
#include <array>
using namespace std;
int main()
{
// 创建一个长度为5的int数组
array<int,5>arr = {1,2,3}; // 后面两位补零
cout << arr[1] << endl;
cout << arr[4] << endl;
arr[3] = 200;
cout << arr.at(3) << endl;
// for 循环遍历
for(int i = 0; i < arr.size(); i++)
{
cout << arr.at(i) << " ";
}
cout << endl;
for(int i:arr)
{
cout << i <<" ";
}
cout << endl;
return 0;
}
2.3.2 vector向量(掌握)
vector内部是由数组实现的,比较适合进行随机存取操作,不擅长删除插入操作。
#include <iostream>
#include <vector>
using namespace std;
int main()
{
// vector<int> v = {1,2,3};
// for(int i:v)
// {
// cout << i << endl;
// }
// 创建一个长度为5的向量(int)
vector<int> vec(5);
cout << vec.size() << endl;
// 增
vec.push_back(222); // 向后追加元素
cout << vec.size() << endl; // 6
// 插入操作
vec.insert(vec.begin()+2,333); // begin()可以返回一个指向第一个元素的迭代器指针,+2是在第三个位置上插入333
// 改
vec[0] = 1;
vec.at(1) = 2;
vec.at(3) = 4;
vec.at(4) = 5;
// 删
// 删除最后一个元素
vec.pop_back();
vec.erase(vec.begin()+1); // 删除第二个元素
vec.erase(vec.end()-2); // 删除倒数第二个元素
// 查
cout << vec[1] << endl;
cout << vec.at(0) << endl;
// for遍历
for(int i = 0; i < vec.size(); i++)
{
cout << vec[i]<< " ";
}
cout << endl;
for(int i:vec)
{
cout <<i <<" ";
}
cout << endl;
// 判断是否为空,0非空,1空
cout << vec.empty() << endl;
// 清空
vec.clear();
cout << vec.empty() << endl;
cout << vec.size() << endl;
// 迭代器遍历
return 0;
}
2.3.3 list列表(掌握)
list内部是由双向循环链表实现的。内存空间不连续,不支持下标。优点:可以高效的插入和删除操作。不适合随机存取。
#include <iostream>
#include <list>
using namespace std;
int main()
{
// 创建一个默认无数值的list
// list<string> lis1;
// 创建一个长度为2的列表,第一个元素为hello第二个元素为world
// list<string> lis2{"hello","world"};
// for(string s:lis2)
// {
// cout << s << endl;
// }
// 创建一个长度为5的列表,每个元素都是“hello”
list<string> lis(5,"hello");
// 增
lis.push_back("world"); // 向后追加单元素
lis.push_front("hahaha"); // 向前追加单元素
lis.insert(++lis.begin(),"222"); // 在第二个位置上插入“222”
// 删
// lis.pop_back(); //删除最后一个元素
lis.pop_front(); // 删除第一个元素
// 保存迭代器指针
list<string>::iterator iter = lis.begin();
advance(iter,1); // 移动迭代器指针
lis.insert(iter,"333"); // 插入333
// iter = lis.end();
// iter--;
// lis.erase(iter);
iter = lis.begin();
advance(iter,1);
lis.erase(iter);
// 第一个元素的引用
// cout << lis.front() << endl;
// 返回最后一个元素的引用
// cout << lis.back() << endl;
// 改
iter = lis.end();
advance(iter,2);
*iter = "200";
cout << "----" << *iter << endl;
// 不能用普通循环遍历,因为不支持下标
for(string s:lis)
{
cout << s << endl;
}
// 也支持迭代器遍历
// 清空
lis.clear();
cout << lis.size() << endl;
return 0;
}
2.3.4 deque队列(熟悉)
deque支持几乎所有vector的API,性能位于vector与list两者之间。最擅长两端存取的顺序容器。
#include <iostream>
#include <deque>
using namespace std;
int main()
{
// deque<int> v = {1,2,3};
// for(int i:v)
// {
// cout << i << endl;
// }
// 创建一个长度为5的向量(int)
deque<int> deq(5);
cout << deq.size() << endl;
// 增
deq.push_back(222); // 向后追加元素
cout << deq.size() << endl; // 6
// 插入操作
deq.insert(deq.begin()+2,333); // begin()可以返回一个指向第一个元素的迭代器指针,+2是在第三个位置上插入333
// 改
deq[0] = 1;
deq.at(1) = 2;
deq.at(3) = 4;
deq.at(4) = 5;是用作限定符
// 删
// 删除最后一个元素
deq.pop_back();
deq.erase(deq.begin()+1); // 删除第二个元素
deq.erase(deq.end()-2); // 删除倒数第二个元素
// 查
cout << deq[1] << endl;
cout << deq.at(0) << endl;
// for遍历
for(int i = 0; i < deq.size(); i++)
{
cout << deq[i]<< " ";
}
cout << endl;
for(int i:deq)
{
cout <<i <<" ";
}
cout << endl;
// 判断是否为空,0非空,1空
cout << deq.empty() << endl;
// 清空
deq.clear();
cout << deq.empty() << endl;
cout << deq.size() << endl;
// 迭代器遍历
return 0;
}
2.4 关联容器(掌握)
关联容器的各个元素之间没有严格的顺序,虽然内部具有排序的特点,但是在使用时没有任何顺序相关的接口。
最常用的关联容器就是map-键值对映射。
对于map而言,键具有唯一性,键通常使用字符串类型,值可能是任何类型。通过键找到对应的值。
#include <iostream>
#include <map>
using namespace std;
int main()
{
// 列表初始化创建C++11支持
map<string,int>ma1 = {{"身高",190},{"体重",250}};
cout << ma1.size() << endl; // 2
map<string,int>ma;
cout << ma.size() << endl;
// 增
ma["身高"] = 180; // 插入元素
cout << ma.size() << endl;
ma.insert(pair<string,int>("体重",70)); // 插入元素
cout << ma.size() << endl;
// 改
ma["身高"] = 175;
// 查
cout << ma["身高"] << endl; // 输出元素
cout << ma["体重"] << endl;
if(ma.find("身高") == ma.end()) // find从头开始查找,如果没有找到返回end
{
cout << "没有查找到身高元素" << endl;
}
else
{
cout << ma["身高"] << endl;
}
// 删
int ret = ma.erase("身高"); // 返回值1删除成功,0失败
cout << ret << endl;
ret = ma.erase("月薪") ;
cout << ret << endl;
cout << ma.size() << endl;
ma.clear();
cout << ma.size() << endl;
return 0;
}
2.5 迭代器遍历
迭代器是一个特殊的指针,主要用于容器的元素读写以及遍历。
如果迭代器不进行修改操作,建议使用只读迭代器,const_iterator。反之使用iterator。
#include <iostream>
#include <map>
#include <list>
#include <deque>
#include <vector>
#include <array>
using namespace std;
int main()
{
string s = "abcdefg";
// 迭代器遍历string
for(string::const_iterator iter = s.begin(); iter != s.end(); iter++)
{
cout << *iter << " ";
}
cout << endl;
cout << "-----------------" << endl;
// 迭代器遍历array
array<int,5> arr= {23,54,1,34,6};
for(array<int,5>::const_iterator iter = arr.begin(); iter != arr.end(); iter++)
{
cout << *iter << " ";
}
cout << endl;
cout << "-----------------" << endl;
// 迭代器遍历vector
vector<string> vec(6,"hello");
for(vector<string>::const_iterator iter = vec.begin(); iter != vec.end(); iter++)
{
cout << *iter << " ";
}
cout << endl;
cout << "-----------------" << endl;
// 迭代器遍历list
list<string>lis(6,"world");
for(list<string>::const_iterator iter = lis.begin();iter != lis.end(); iter++)
{
cout << *iter << " ";
}
cout << endl;
cout << "-----------------" << endl;
// 迭代器遍历deque
deque<string>deq(6,"hahaha");
for(deque<string>::const_iterator iter = deq.begin(); iter != deq.end(); iter++)
{
cout << *iter << " ";
}
cout << endl;
cout << "-----------------" << endl;
// 迭代器遍历map
map<string,int>ma;
ma["年龄"] = 100;
ma["身高"] = 190;
ma["体重"] = 170;
ma["薪资"] = 50000;
for(map<string,int>::const_iterator iter = ma.begin();iter != ma.end(); iter++)
{
// first 是键 second 是值
cout << iter->first << " " << iter->second << endl;
}
cout << endl;
cout << "-----------------" << endl;
return 0;
}
六、面向对象核心
1.继承(重点)
1.1 概念
继承是面向对象的三大特性之一,体现了代码复用的思想。
继承就是在一个已存在的类的基础上,创建一个新的类,并拥有其特性。
- 已存在的类被称为“基类”或者“父类”
- 新建立的类被称为“派生类”或“子类”
#include <iostream>
using namespace std;
// 基类
class Father
{
private:
string name = "孙";
public:
void set_name(string name)
{
this->name = name;
}
string get_name()
{
return name;
}
void work()
{
cout << "我的工作是农民,我负责种地" << endl;
}
};
// 派生类(派生类继承基类Father)
class Son:public Father
{
};
int main()
{
// Father f1;
// cout << f1.get_name() << endl;
// f1.work();
Son son;
cout << son.get_name() << endl;
son.work();
return 0;
}
上面的代码,Son类的功能几乎与Father类重叠,在实际的使用过程中,派生类会做出一些与基类的差异化。
- 修改基类下来基类内容
属性:1、公有属性,直接更改。2、私有属性派生类无法直接进行修改和访问。如果需要使用或者更改基类的私有属性,需要使用基类公有函数进行修改。
函数:函数隐藏。通过派生类实现一个同名同参数的函数,来隐藏基类的函数。
- 新增派生类的内容
#include <iostream>
using namespace std;
// 基类
class Father
{
private:
string name = "孙";
public:
int age = 12;
void set_name(string name)
{
this->name = name;
}
string get_name()
{
return name;
}
void work()
{
cout << "我的工作是农民,我负责种地" << endl;
}
};
// 派生类(派生类继承基类Father)
class Son:public Father
{
public:
void init()
{
age = 10;
set_name("王");
}
void game()
{
cout << "我不光干活,我还打游戏,王者荣耀启动" << endl;
}
void work()
{
cout << "我是一个程序员,我在敲代码" << endl;
}
};
int main()
{
Son son;
son.init();
cout << son.get_name() << endl; // 王
cout << son.age << endl; // 10
son.work(); // 我是一个程序员,我在敲代码
son.game(); // 我不光干活,我还打游戏,王者荣耀启动
son.Father::work(); // 调用基类被隐藏的成员函数
return 0;
}
基类与派生类是相对的,一个类可能存在又是基类又是派生类的情况,取决于那两个类进行比较。
1.2 构造函数(重点)
1.2.1 派生类与基类构造函数的关系
构造函数与析构函数不能被继承。
#include <iostream>
using namespace std;
// 基类
class Father
{
private:
string name = "孙";
public:
// 有参构造函数
Father(string name):name(name){}
void set_name(string name)
{
this->name = name;
}
string get_name()
{
return name;
}
void work()
{
cout << "我的工作是农民,我负责种地" << endl;
}
};
// 派生类(派生类继承基类Father)
class Son:public Father
{
public:
};
int main()
{
// 找不到基类的无参构造函数
// Son son
// Son son("张"); // 找不到派生类有参构造函数
return 0;
}
派生类的任意构造函数,都必须直接或者间接调用基类的任意一个构造函数。
1.2.2 解决方案
1.2.2.1 补充基类的无参构造函数
#include <iostream>
using namespace std;
// 基类
class Father
{
private:
string name = "孙";
public:
Father()
{
}
// 有参构造函数
Father(string name):name(name){}
void set_name(string name)
{
this->name = name;
}
string get_name()
{
return name;
}
void work()
{
cout << "我的工作是农民,我负责种地" << endl;
}
};
// 派生类(派生类继承基类Father)
class Son:public Father
{
public:
};
int main()
{
// Son son;
Son son;
return 0;
}
1.2.2.2 手动在派生中调用基类构造函数
1.2.2.2.1 透传构造
在派生类的构造函数中,调用基类的构造函数,实际上编译器自动添加的派生类的构造函数,调用基类构造函数时,采用的就是这种方式。
#include <iostream>
using namespace std;
// 基类
class Father
{
private:
string name = "孙";
public:
// 有参构造函数
Father(string name):name(name){}
void set_name(string name)
{
this->name = name;
}
string get_name()
{
return name;
}
void work()
{
cout << "我的工作是农民,我负责种地" << endl;
}
};
// 派生类(派生类继承基类Father)
class Son:public Father
{
public:
// 透传构造
Son():Father("张"){}
// 手动添加派生类有参构造函数
Son(string fn):Father(fn){}
};
int main()
{
// Son son;
// cout << son.get_name() << endl;
Son son("王");
cout << son.get_name() << endl;
return 0;
}
1.2.2.2.2 委托构造
一个类的构造函数可以调用这个类的另一个构造函数,但是要避免循环委托。
委托构造的性能低于透传构造,但是代码的“维护性更好”,因为通常一个类中构造函数都会委托给能力最强(参数最多)的构造函数,代码重构时,只需要更改这个能力最强的构造函数即可。
#include <iostream>
using namespace std;
// 基类
class Father
{
private:
string name = "孙";
public:
// 有参构造函数
Father(string name):name(name){}
void set_name(string name)
{
this->name = name;
}
string get_name()
{
return name;
}
void work()
{
cout << "我的工作是农民,我负责种地" << endl;
}
};
// 派生类(派生类继承基类Father)
class Son:public Father
{
public:
// 委托构造
Son():Son("张"){}
// 手动添加派生类有参构造函数
Son(string fn):Father(fn){}
};
int main()
{
// Son son;
// cout << son.get_name() << endl;
Son son;
cout << son.get_name() << endl;
return 0;
}
1.2.2.2.3 继承构造
C++11新增的写法,只需要一句话,就可以自动给派生类添加n(n为基类构造函数的个数)个构造函数。并且每个派生类的构造函数格式都与基类相同,每个派生类的构造函数都通过透传构造调用对应格式的基类构造函数。
#include <iostream>
using namespace std;
// 基类
class Father
{
private:
string name = "孙";
public:
Father():Father("张"){} // 委托构造
// 有参构造函数
Father(string name):name(name){}
void set_name(string name)
{
this->name = name;
}
string get_name()
{
return name;
}
void work()
{
cout << "我的工作是农民,我负责种地" << endl;
}
};
// 派生类(派生类继承基类Father)
class Son:public Father
{
public:
// 只加这一句话,编译器就会自动添加下面两种构造函数
using Father::Father;
// Son():Father(){}
// // 手动添加派生类有参构造函数
// Son(string fn):Father(fn){}
};
int main()
{
// Son son;
// cout << son.get_name() << endl;
Son son("王");
cout << son.get_name() << endl;
return 0;
}
1.3 对象的创建与销毁流程(掌握)
在继承中,构造函数与析构函数的调用顺序。
#include <iostream>
using namespace std;
class Value
{
private:
string str;
public:
Value(string str):str(str)
{
cout << str <<"构造函数" << endl;
}
~Value()
{
cout << str << "析构函数" << endl;
}
};
class Father
{
public:
static Value s_value;
Value val = Value("Father成员变量"); // 相当于 int i = int(10);
Father()
{
cout << "Father构造函数调用了" << endl;
}
~Father()
{
cout << "Father析构函数调用了" << endl;
}
};
Value Father::s_value = Value("静态FatherValue被创建了");
class Son:public Father
{
public:
static Value s_value;
Value val = Value("son成员变量");
Son()
{
cout << "Son 构造函数被调用了" << endl;
}
~Son()
{
cout << "Son 析构函数被调用了" << endl;
}
};
Value Son::s_value = Value("静态SonValue被创建了");
int main()
{
cout << "主函数被调用了" << endl;
{ // 局部代码块
Son s;
cout << "对象执行中" << endl;
}
cout << "主函数结束了" << endl;
return 0;
}
上面的执行结果中,可以得到以下规律:
- 静态的创建早于非静态。
- 变量早于函数执行。
- 在创建的过程中,同类型的内存区域基类先开辟。析构时派生类早于基类。
- 以“对象执行中为轴”,上下对称。
1.4 多重继承(熟悉)
1.4.1 概念
C++支持多重继承,即一个派生类可以有多个基类。派生类对于每个基类的关系仍然可以看作是一个单继承。
#include <iostream>
using namespace std;
class Sofa
{
public:
void sit()
{
cout << "沙发可以坐着" << endl;
}
};
class Bed
{
public:
void lay()
{
cout << "床可以躺着" << endl;
}
};
// 多重继承
class SofaBed:public Sofa,public Bed
{
public:
};
int main()
{
SofaBed sb;
sb.lay();
sb.sit();
return 0;
}
1.4.2 可能出现的问题
1.4.2.1 问题1-重名问题
当多个基类具有重名成员时,编译器在编译的过程中会出现二义性的问题。
#include <iostream>
using namespace std;
class Sofa
{
public:
void sit()
{
cout << "沙发可以坐着" << endl;
}
void clean()
{
cout << "打扫沙发" << endl;
}
};
class Bed
{
public:
void lay()
{
cout << "床可以躺着" << endl;
}
void clean()
{
cout << "打扫床" << endl;
}
};
// 多重继承
class SofaBed:public Sofa,public Bed
{
public:
};
int main()
{
SofaBed sb;
sb.lay();
sb.sit();
sb.Bed::clean();
sb.Sofa::clean();
return 0;
}
1.4.2.2 问题2-菱形继承(熟悉)
当一个派生类有多个基类,且这些基类又有一个共同基类时,就会出现二义性问题,这种现象也被称为菱形(钻石)继承。
有两种解决方式:
- 使用基类的类名::方式调用。
#include <iostream>
using namespace std;
// 家具厂
class Furniture
{
public:
void func()
{
cout << "家具厂里有家具" << endl;
}
};
class Sofa:public Furniture
{
public:
};
class Bed:public Furniture
{
public:
};
// 多重继承
class SofaBed:public Sofa,public Bed
{
public:
};
int main()
{
SofaBed sb;
sb.Sofa::func();
sb.Bed::func();
return 0;
}
2、使用虚继承
当出现虚继承时,Furniture类中会产生一张虚基类表,这个表不占用任何对象的存储空间,属于Furniture类持有,在程序启动时加载进内存,表中记录了Furniture函数的调用地址偏移量。
Bed和Sofa对象会出现一个隐藏的成员变量指针,指向Furniture类中的虚基类表,占用对象4个字节。
虚继承(SofaBed继承Sofa和Bed)时,SofaBed类对象会同时持有两个虚基类表指针成员,在调用时查表解决二义性问题。
#include <iostream>
using namespace std;
// 家具厂
class Furniture
{
public:
void func()
{
cout << "家具厂里有家具" << endl;
}
};
class Sofa:virtual public Furniture
{
public:
};
class Bed:virtual public Furniture
{
public:
};
// 多重继承
class SofaBed:public Sofa,public Bed
{
public:
};
int main()
{
SofaBed sb;
sb.func();
return 0;
}
2、权限(掌握)
2.1 权限修饰符
三种权限一共对应九种场景。要求心中有表,遇到每种场景都能直接反映出是否能访问。
类内 | 派生类中 | 全局 | |
private | √ | × | × |
protected | √ | √ | × |
public | √ | √ | √ |
#include <iostream>
using namespace std;
class Base
{
// 最小权限法则。首先推荐用私有,其次保护、最后公有
protected:
string s = "保护权限";
public:
Base()
{
cout << s << endl;
}
};
class Son:public Base
{
public:
Son()
{
cout << s << endl;
}
};
int main()
{
Son s1;
// cout << s1.s << endl; // 错误s是保护权限,类外无法访问
return 0;
}
2.2 不同权限的继承(掌握)
2.2.1 公有继承
上面的代码中一直使用的就是公有继承,公有继承也是使用最多的一种继承方式。
在公有继承中,派生类可以继承基类的成员,不可以访问基类的私有成员,基类的公有成员与保护成员、私有成员在派生类中权限不变。
#include <iostream>
using namespace std;
class Base
{
private:
string str1 = "私有成员";
protected:
string str2 = "保护成员";
public:
string str3 = "公有成员";
};
class Son:public Base // 公有继承
{
public:
Son()
{
// cout << str1 << endl; // 错误 str1为私有成员
cout << str2 << endl;
cout << str3 << endl;
}
};
int main()
{
Son s1;
// cout << s1.str1 << endl; // 错误私有成员
// cout << s1.str2 << endl; // 错误保护成员
cout << s1.str3 << endl;
return 0;
}
2.2.2 保护继承
在保护继承中,派生类可以继承基类的成员,但是不可以访问基类的私有成员,基类的公有成员在派生类的权限都是保护权限。(只能在基类与派生类中访问,外部无法访问)。
#include <iostream>
using namespace std;
class Base
{
private:
string str1 = "私有成员";
protected:
string str2 = "保护成员";
public:
string str3 = "公有成员";
};
class Son:protected Base // 保护继承
{
public:
Son()
{
// cout << str1 << endl; // 错误 str1为私有成员
cout << str2 << endl;
cout << str3 << endl;
}
};
int main()
{
Son s1;
// cout << s1.str1 << endl; // 错误私有成员
// cout << s1.str2 << endl; // 错误保护成员
// cout << s1.str3 << endl; // 错误保护成员
return 0;
}
2.2.3 私有继承
在私有继承中,派生类可以继承基类的成员,但是不可以访问基类的私有成员,基类的公有成员与保护成员在派生类中都是私有权限。
#include <iostream>
using namespace std;
class Base
{
private:
string str1 = "私有成员";
protected:
string str2 = "保护成员";
public:
string str3 = "公有成员";
};
class Son:private Base // 私有继承
{
public:
Son()
{
// cout << str1 << endl; // 错误 str1为私有成员
cout << str2 << endl;
cout << str3 << endl;
}
};
class Test:public Son
{
public:
Test()
{
// cout << str1 << endl; // 错误
// cout << str2 << endl;
// cout << str3 << endl;
}
};
int main()
{
Son s1;
// cout << s1.str1 << endl; // 错误私有成员
// cout << s1.str2 << endl; // 错误私有成员
// cout << s1.str3 << endl; // 错误私有成员
return 0;
}
3、多态(重点)
3.1 什么是多态
在面向对象编程中,我们通常将多态分为两种:静态多态(也称为编译时多态)和动态多态(称为运行时多态)两种多态性是多态概念的不同表现方式。
静态多态
- 静态多态是指在编译时就能确定要调用的函数,通过函数重载和运算符重载来实现。
动态多态
- 动态多态是指在运行时根据对象的实际类型来确定调用的函数,通过继承和函数覆盖来实现。
静态多态发生在编译时,因为在编译阶段编辑器就可以确定要调用的函数。
动态多态发生在运行时,因为具体调用那个函数是在程序运行时根据对象的实际类型来确定。
注:本文后续说的多态均为动态多态。
3.2 多态的概念
多态可以理解为“一种接口,多种状态”,只需要编写一个函数接口,根据传入的参数类型,执行不同的策略代码。
多态的使用具有三个前提条件:
- 公有继承
- 函数覆盖
- 基类的指针/引用指向派生类的对象
多态的优点:多态会让你的代码更加灵活、更加具有可拓展性、可维护性。它能使代码更具有通用性,减少重复代码的编写。
多态的缺点:多态的缺点包括代码的复杂性、不易读、运行效率低、在运行时会产生一些额外的开销。
3.3 函数覆盖
函数覆盖、函数隐藏、这两个比较相似,但是函数隐藏不支持多态,而函数覆盖是多态的必备条件,函数覆盖比函数隐藏有以下几点区别:
- 函数隐藏是指派生类中存在与基类同名同参的函数,编译器会将基类的同名同参的函数进行隐藏。
- 函数覆盖是基类中定义一个虚函数,派生类编写一个与基类同名同参数的函数将基类中的虚函数进行重写并覆盖。注:覆盖的函数必须是虚函数。
3.4 虚函数的定义
一个函数使用virtual关键字修饰,就是虚函数。虚函数是函数覆盖的前提。在QtCretor中虚函数的函数名称使用斜体字。
#include <iostream>
using namespace std;
class Animal
{
public:
// 虚函数
virtual void eat()
{
cout << "动物爱吃饭" << endl;
}
};
int main()
{
return 0;
}
虚函数具有以下性质:
- 虚函数具有传递性,基类中被覆盖的函数是虚函数,派生类中新覆盖的函数也是虚函数。
#include <iostream>
using namespace std;
class Animal
{
public:
// 虚函数
virtual void eat()
{
cout << "动物爱吃饭" << endl;
}
};
class Dog:public Animal
{
public:
// 覆盖基类中的虚函数,派生类的virtual可写可不写
void eat()
{
cout << "狗爱吃骨头" << endl;
}
};
int main()
{
return 0;
}
- 只有普通成员函数与析构函数可以声明为虚函数。
#include <iostream>
using namespace std;
class Animal
{
public:
// 错误,构造函数不能声明为虚函数
// virtual Animal()
// {
// }
// 错误,静态函数不能为虚函数
// virtual static void testStatic()
// {
// cout << "测试静态成员函数 虚函数" << endl;
// }
// 虚函数
virtual void eat()
{
cout << "动物爱吃饭" << endl;
}
};
class Dog:public Animal
{
public:
// 覆盖基类中的虚函数,派生类的virtual可写可不写
void eat()
{
cout << "狗爱吃骨头" << endl;
}
};
int main()
{
return 0;
}
- 在C++11中,可以在派生类的新覆盖的函数上使用override关键字验证覆盖是否成功。
#include <iostream>
using namespace std;
class Animal
{
public:
// 错误,构造函数不能声明为虚函数
// virtual Animal()
// {
// }
// 错误,静态函数不能为虚函数
// virtual static void testStatic()
// {
// cout << "测试静态成员函数 虚函数" << endl;
// }
// 虚函数
virtual void eat()
{
cout << "动物爱吃饭" << endl;
}
void funcHide()
{
cout << "测试 override关键字函数" << endl;
}
};
class Dog:public Animal
{
public:
// 覆盖基类中的虚函数,派生类的virtual可写可不写
void eat()override
{
cout << "狗爱吃骨头" << endl;
}
// 错误,标记覆盖但是没覆盖
// 注:这个函数隐藏,并不是函数覆盖,因为基类中的函数是一个普通成员函数,不是虚函数
void funcHide()override
{
cout << "测试 派生类override关键字函数" << endl;
}
};
int main()
{
return 0;
}
3.5 多态的实现
我们在开篇的时候提到过,要实现动态多态,需要具有三个前提条件。
- 公有继承(已经实现)
- 函数覆盖(已经实现)
- 基类指针/引用指向派生类对象(还未编写)
【思考】为什么要基类的指针/引用指向派生类对象?
- 实现运行时多态:当使用基类的指针或引用指向派生类对象时,程序在运行时会根据对象的实际类型来调用相应的函数,而不是根据指针或者引用的类型。
- 统一接口:基类的指针可以作为一个通用的接口,用于操作不同类型的派生类对象。这样可以使代码更加灵活,减少重复代码,并且能够更好支持代码的扩展性和维护性。
#include <iostream>
using namespace std;
class Animal
{
public:
// 虚函数
virtual void eat()
{
cout << "动物爱吃饭" << endl;
}
};
class Dog:public Animal
{
public:
// 覆盖基类中的虚函数,派生类的virtual可写可不写
void eat()override
{
cout << "狗爱吃骨头" << endl;
}
};
class Cat:public Animal
{
public:
// 覆盖基类中的虚函数,派生类的virtual可写可不写
void eat()override
{
cout << "猫爱吃鱼" << endl;
}
};
int main()
{
// 基类的指针指向派生类的对象
Animal *a1 = new Dog;
a1->eat();
Animal *a2 = new Cat;
a2->eat();
return 0;
}
提供通用函数接口,参数设计成基类的指针或者引用,这样这个函数就可以访问到此基类所有派生类中的虚函数了。
#include <iostream>
using namespace std;
class Animal
{
public:
// 虚函数
virtual void eat()
{
cout << "动物爱吃饭" << endl;
}
};
class Dog:public Animal
{
public:
// 覆盖基类中的虚函数,派生类的virtual可写可不写
void eat()override
{
cout << "狗爱吃骨头" << endl;
}
};
class Cat:public Animal
{
public:
// 覆盖基类中的虚函数,派生类的virtual可写可不写
void eat()override
{
cout << "猫爱吃鱼" << endl;
}
};
// 提供通用函数,形参为基类指针
void animal_eat1(Animal *a1)
{
a1->eat();
}
// 提供通用函数,形参为基类引用
void animal_eat2(Animal &a1)
{
a1.eat();
}
int main()
{
// 基类的指针指向派生类的对象
// Animal *a1 = new Dog;
// a1->eat();
// Animal *a2 = new Cat;
// a2->eat();
Dog *d1 = new Dog;
Cat *c1 = new Cat;
animal_eat1(d1);
animal_eat1(c1);
Dog d2;
Cat c2;
animal_eat2(d2);
animal_eat2(c2);
return 0;
}
3.6 多态原理
具有虚函数的类会存在一张虚函数表,每个对象内部都有一个隐藏的虚函数表指针成员,指向当前类的虚函数表。
多态的实现流程:
在代码运行时,通过对象的虚函数表指针找到虚函数表,在表中定位到虚函数的调用地址,从而执行对应的虚函数内容。
3.7 虚析构函数
如果不使用虚析构函数,且基类指针或引用指向派生类对象,使用delete销毁对象时,只能触发基类的析构函数,如果在派生类中申请堆内存等资源,则会导致无法释放,出现内存泄漏的问题。
#include <iostream>
using namespace std;
class Animal
{
public:
// 虚函数
// virtual void eat()
// {
// cout << "动物爱吃饭" << endl;
// }
// 虚析构函数
virtual ~Animal()
{
cout << "基类析构函数" << endl;
}
};
class Dog:public Animal
{
public:
// 覆盖基类中的虚函数,派生类的virtual可写可不写
// void eat()override
// {
// cout << "狗爱吃骨头" << endl;
// }
~Dog()
{
cout << "派生类析构函数" << endl;
}
};
int main()
{
// 基类的指针指向派生类的对象
Animal *a1 = new Dog;
// a1->eat();
delete a1;
return 0;
}
解决方案是给基类的析构函数使用virtual修饰为虚析构函数,通过传递性可以把各个派生类的析构函数都变为虚析构函数,因此建议给一个可能为基类的类中的析构函数设置成虚析构函数。
3.8 类型转换(熟悉)
在上一节中除了虚析构函数外,还可以使用类型转换解决内存泄漏问题,以下是传统的类型转换写法:
#include <iostream>
using namespace std;
class Animal
{
public:
~Animal()
{
cout << "基类析构函数" << endl;
}
};
class Dog:public Animal
{
public:
~Dog()
{
cout << "派生类析构函数" << endl;
}
};
int main()
{
Animal *a1 = new Dog;
// 可以把a转换回Dog*类型
Dog *d = (Dog*)a1;
delete d;
return 0;
}
在C++11中不建议使用C风格的类型转换,因此可能会带来一些安全隐患,让程序的错误难以发现。
C++11提供了一组适用于不同场景的强制类型转换函数。
- static_cast(静态转换)
- dynamic_cast(动态转换)
- const_cast(常量转换)
- reinterpret_cast(重解释转换)
3.8.1 static_cast
- 主要用于基本数据类型之间的转换。
#include <iostream>
using namespace std;
int main()
{
int x = 1;
double y = static_cast<double>(x);
cout << y << endl;
return 0;
}
static_cast没有运行时类型检查来保证转换的安全性,需要程序员手动判断转化是否安全。
#include <iostream>
using namespace std;
int main()
{
double x = 3.14;
int y = static_cast<int>(x);
cout << y << endl;
return 0;
}
static_cast也可以用于类层次的转换中,即基类和派生类指针或引用之间的转换。
- static_cast进行上行转换是安全的,即把派生类的指针或引用转为基类的。
- static_cast进行下行转换是不安全的,即把基类的指针或引用转换为派生类的。
static仅仅可以完成上述转换,但是不建议。
#include <iostream>
using namespace std;
class Father
{
public:
string a = "Father";
};
class Son:public Father
{
public:
string b = "Son";
};
int main()
{
// 指针转换
// 上行转换 派生类→基类
// Son *s1 = new Son;
// Father *f1 = static_cast<Father*>(s1);
// cout << f1->a << endl; // Father
// // 下行转换 基类→派生类
// Father *f2 = new Father;
// Son *s2 = static_cast<Son*>(f2);
// cout << s2->a << endl; // Father
// cout << s2->b << endl; // 结果不定
// 引用转换 上行转换:派生类→基类
Son s1;
Father f1 = static_cast<Father>(s1);
Father &f2 = static_cast<Father&>(s1);
cout << f1.a << endl;
cout << f2.a << endl;
cout << &s1 << endl; // 0x61fe84
cout << &f1 << endl; // 0x61fe80
cout << &f2 << endl; // 0x61fe84
// 下行转换 基类→派生类
Father f3;
// Son s2 = static_cast<Son>(f3); // 错误
Son &s3 = static_cast<Son&>(f3);
cout << s3.a << endl; // Father
cout << s3.b << endl; // Father
return 0;
}
3.8.2 dynamic_cast
dynamic_cast主要用于类层次之间的上行与下行转换。
在进行上行转换时,dynamic_cast与static_cast效果相同,但是进行下行转换时,dynamic_cast会比static_cast更加安全。
关于下行转换的类型检查如下:
#include <iostream>
using namespace std;
class Father
{
public:
virtual void func()
{
cout << "Father" << endl;
}
};
class Son:public Father
{
public:
void func()
{
cout << "Son" << endl;
}
};
int main()
{
// 指针且形成多态
Father *f0 = new Son;
Son *s0 = dynamic_cast<Son*>(f0);
cout << f0 << " " << s0 << endl;
f0->func();
s0->func();
// 指针未形成多态
Father *f1 = new Father;
Son* s1 = dynamic_cast<Son*>(f1);
cout << f1 << " " << s1 << endl; // 0xf62750 0
f1->func(); // Father
// s1->func(); // 非法调用
// 引用且形成多态
Son s;
Father &f2 = s;
Son &s2 = dynamic_cast<Son&>(f2);
cout << &s2 << " " << &f2 << " " << &s << endl; // 0x61fe74 0x61fe74 0x61fe74
s2.func(); // Son
f2.func(); // Son
s.func(); // Son
Father f;
// Son& s3 = dynamic_cast<Son&>(f); // 运行终止
cout << &s3 << " " << &f << endl;
s3.func();
f.func();
return 0;
}
3.8.3 const_cast
const_cast可以添加或者移除对象的const限定符。
主要用于改变指针或引用的const效果,以便于在一定的情况下修改原本被声明为常量的对象,应该避免使用const_cast,而是考虑通过良好的接口设计或者其他正常手段避免需要进行此种转换。
#include <iostream>
using namespace std;
class Test
{
public:
string str = "A";
};
int main()
{
const Test* t1 = new Test;
// t1->str = "B"; // 错误
Test *t2 = const_cast<Test*>(t1);
t2->str = "B";
cout << t1 << " " << t2 << endl;
cout << t1->str << " " << t2->str << endl;
return 0;
}
3.8.3 reinterpret_cast
reinterpret_cast 可以把内存里的值重新解释,这种转换风险极高,慎用!!!!!!
#include <iostream>
using namespace std;
class A
{
public:
void print()
{
cout << "A" << endl;
}
};
class B
{
public:
void print()
{
cout << "B" << endl;
}
};
int main()
{
A*a = new A;
B *b = reinterpret_cast<B*>(a);
cout << a << " " << b << endl; // 0x1052740 0x1052740
a->print(); // A
b->print(); // B
return 0;
}
4、抽象类(掌握)
如果基类只表达一些抽象的概念,并不与实际的对象相关联,这时候就可以使用抽象类。
如果一类中有纯虚函数,则这个类是一个抽象类。
如果一类是抽象类,则这个类中一定有纯虚函数。
纯虚函数是虚函数的一种,这种函数只有声明没有定义。
virtual 返回值类型 函数名(参数列表)= 0;
不能直接使用抽象类作为声明类型,因为不存在抽象类类型的对象。(不能实例化对象)
抽象类作为基类时,具有两种情况:
- 派生类继承抽象类,覆盖并实现其所有的纯虚函数,此时派生类可以作为普通类进行使用,即不再是抽象类。
- 派生类继承抽象类,没有把抽象类中的所有纯虚函数覆盖并实现,此时派生类也变为抽象类,等待它的派生类覆盖并实现剩余的纯虚函数。
#include <iostream>
using namespace std;
// 抽象类::形状
class Shape
{
public:
// 纯虚函数
virtual void area() = 0; // 面积
virtual void perimeter() = 0; // 周长
};
// 圆形
class Circle:public Shape
{
public:
// 函数覆盖并实现所有纯虚函数
void area()
{
cout << "圆形计算面积" << endl;
}
void perimeter()
{
cout << "圆形计算周长" << endl;
}
};
// 多边形
class polygon:public Shape
{
public:
void perimeter()
{
cout << "多边形计算周长" << endl;
}
};
// 矩形
class Rectangle:public polygon
{
public:
void area()
{
cout << "矩形计算面积" << endl;
}
};
int main()
{
// Shape s; // 错误抽象类无法实例化对象(形状类)
Circle c; // 圆类
c.area();
c.perimeter();
// polygon p; // 错误 抽象类无法实例化对象
Rectangle r;
r.area(); // 矩形的面积
r.perimeter(); // 多边形的周长
return 0;
}
使用抽象类需要注意以下几点:
- 抽象类的析构函数必须是虚析构。
- 抽象类支持多态,可以存在引用或指针的声明格式。
- 因为抽象类的作用是指定算法框架,因此在一个继承体系中,抽象类的内容相对丰富且重要。
5、纯虚析构(熟悉)
纯虚析构函数的定义:
纯虚析构函数的本质:是析构函数,作用是各个类的回收工作,而且析构函数不能被继承。
必须为纯虚析构函数提供一个函数体。
纯虚析构函数,必须在类外实现。
#include <iostream>
using namespace std;
class Animal
{
public:
// 纯虚析构函数
virtual ~Animal() = 0;
};
// 实现
Animal::~Animal()
{
cout << "基类的析构函数被调用了" << endl;
}
class Dog:public Animal
{
public:
~Dog()
{
cout << "Dog析构函数" << endl;
}
};
int main()
{
Animal *a1 = new Dog;
delete a1;
// Animal a2;
Dog d;
return 0;
}
虚析构函数与纯虚析构函数的区别:
虚析构函数:virtual 关键字修饰,有函数体,不会导致基类为抽象类。
纯析构函数:virtual关键字修饰,结尾=0,函数体需要类外实现,会导致类为抽象类。