目录
2.4.1 模板参数可变化(variadic templates)
1.前言
上一篇写了“C++面向对象程序设计(基础版上)”,里面大致介绍了不带指针的类、带指针的类、模板、命名空间等基础知识。详情链接:C++面向对象程序设计(基础版上)-CSDN博客。
这章是在学习泛型编程、面向对象、以及对象模型的一些基础笔记。如有不准确地方还请指教。
(Tip:里面的代码都是自己手敲过的编译过滴~~)
2.正式开始咯
2.1 转换函数(conversion Function)
顾名思义,转换函数就是将 一个类型的对象 转换成 其他类型。最常见的例子就是将分数转换成小数(整数)再进行计算。(此处以分数和小数相加为例。)转换函数使用operator关键字来实现的。
2.1.1 重载double()函数
我们来一起尝试使用关键字operator(它通常和运算符一起使用,表示运算符函数。在这里就是将操作符double重载来实现转换,当主函数要将分数类型的数据和double类型的数据,就会在分数类中查找是否有double重载函数,如果存在就编译通过。否则报错)来简单写一下分数类中转换函数。
1.首先,分数有分子和分母,所有我们的数据有num(分子)、deo(分母),对于我们的私有数据会提供一个只读的函数,加上构造函数:
#include<iostream>
using namespace std;
class Fraction {
public:
Fraction(int f_num, int f_deo = 1)//构造函数,让整数分母默认为1
:num(f_num), deo(f_deo) {}
int GetNum()const {//读分子
return num;
}
int GetDeo() const {//读分母
return deo;
}
private:
int num;//分子
int deo;//分母
};
2.定义double操作符重载函数:
#include<iostream>
using namespace std;
class Fraction {
public:
Fraction(int f_num, int f_deo = 1)//构造函数,让整数分母默认为1
:num(f_num), deo(f_deo) {}
int GetNum()const {//读分子
return num;
}
int GetDeo() const {//读分母
return deo;
}
//转换函数:opeartor关键字,double为函数名,同时也说明了返回类型,所以在前面没有写返回类型也不会报错
operator double() const {
return (double)num / deo;
}
private:
int num;//分子
int deo;//分母
};
3.写个测试函数验证是否转换成功:
int main() {
Fraction f(3, 2);
double sum = f + 0.5;
cout << sum << endl;
return 0;
}
完整函数如下:
#include<iostream>
using namespace std;
class Fraction {
public:
Fraction(int f_num, int f_deo = 1)//构造函数,让整数分母默认为1
:num(f_num), deo(f_deo) {}
int GetNum()const {//读分子
return num;
}
int GetDeo() const {//读分母
return deo;
}
operator double() {
return (double)num / deo;
}
private:
int num;//分子
int deo;//分母
};
int main() {
Fraction f(3, 2);
double sum = f + 0.5;
cout << sum << endl; //输出结果为2
return 0;
}
2.1.2 重载操作符“+”
不少同学注意到了既然可以重载double ,那我像你上篇笔记的复数加法那样重载运算符“+”也可以实现 分数和小数的相加呀?那么参考上面的步骤我们可以写出一下代码:
#include<iostream>
using namespace std;
class Fraction {
public:
Fraction(int f_num, int f_deo = 1)//构造函数,让整数分母默认为1
:num(f_num), deo(f_deo) {}
int GetNum()const {//读分子
return num;
}
int GetDeo() const {//读分母
return deo;
}
Fraction operator +(const Fraction& fa) {//先通分,再相加
int t_num = fa.num * this->deo + this->num * fa.deo;
int t_deo = fa.deo * this->deo;
return Fraction(t_num, t_deo);//生成临时对象
}
private:
int num;//分子
int deo;//分母
};
int main() {
Fraction f(3, 2);
Fraction sum = f + 4;
cout << sum.GetNum() << endl;//输出分子为11
cout << sum.GetDeo() << endl;//分母为2
return 0;
}
大家是否有疑问为什么在主函数中‘4’是怎么变成分数类型作为参数传入“+”的重载函数的?
宾果!是“4”隐式的调用了Fraction的构造函数 将 ‘4’变成了 Fraction(4,1),所以就可以调用“+”的重载函数。
其实在C++中,隐式的调用了Fraction的构造函数也叫做non-explicit。那对应的也有explicit,往下看吧。
2.1.3 C++中的explicit关键字
explicit 关键字只能用来修饰函数的构造函数,被它修饰之后就只能显式调用,换言之,也就是只有在真正创建对象的时候才可以被调用,像2.1.2中那种调用方式就不可行。
举个例子来更好理解吧:
#include<iostream>
using namespace std;
class Test1{
public:
Test1(int i):im(i){}//没有explicit关键字
private:
int im;
};
class Test2{
public:
explicit Test2(int i):im(i){}//有explicit关键字的
private:
int im;
};
int main(){
Test1 t1 = 8;
Test1 t2(8);
Test2 t3(8);
Test2 t4 = 8;//执行不通过,可以注释这句再试试
}
执行结果报错:(Test2拒绝用隐式,来试试看吧!)
2.2 智能指针
对于指针来说,需要手动的申请和释放内存指针,不然就会造成内存泄漏的问题。但是我们的智能指针就不需要程序员手动干活,用了智能指针就不用考虑对象内存的申请和释放,它会在对象过期时自动调用析构函数。换言之,智能指针是为了防止内存泄漏。
智能指针的头文件引入:
#include<memory>
2.2.1 分类、原理、使用方法、
在C++中,智能指针主要分为三大类:(tip:智能指针也是在std命名空间里面的哦。)
- 独占智能指针: std::unique_ptr;
- 共享智能指针:std::shared_ptr;
- 弱智能指针:std::weak_ptr;
- (auto_ptr:已经被淘汰了,这里不做讨论。)
让我们一起往下学习一下这三类指针!
2.2.1.1 unique_ptr
顾名思义,那就是独一无二的。只能指向单一对象,且这个对象不能被共享和复制。
实现原理:既然要求不能被共享和复制,那么就可以将独占智能指针的 拷贝构造函数 和 重载的“=”函数声明成private或delete,不让外部拷贝或使用“=”。如图:
(截图来源:unique_ptr 类 | Microsoft Learn)
(Tip:当我们定义一个类的成员函数时,如果后面使用"=delete"去修饰,那么就表示这个函数被定义为deleted,也就意味着这个成员函数不能再被调用,否则就会出错。)
同时,它的构造函数使用了explicit修饰,也说明不能隐形构造智能指针。
使用方法:
unique_ptr<Object> smartptr_name(new Object());
从下图中可以看出unique_ptr确实是禁止拷贝的:
我们可以 在右值是临时指针时 进行赋值:(此处可以参考理解)
常用操作:
u.get(); //返回unique_ptr中保存的裸指针,裸指针是什么?通俗讲就是地址,例子下图:
u.reset(); //重置unique_ptr
u.release(); //放弃指针的控制权,返回裸指针,并将unique_ptr自身置为空。
u.swap(); //交换两个unique_ptr所指向的对象
2.2.1.2 shared_ptr
既然是共享,那么必要知道有多少人再用,进而管理共享它的对象。在它里面通过计数来实现的,当对象赋值给其他时,计数器自动加1。
指针的释放是由最后一个共享者来进行的,所以在析构函数中,要先判断计数器是否为0再释放内存。
除了有一个计数器之外,在shared_ptr中,还必须有一个普通的指针 以及 重载操作符“*” 和 “->”来让shared_ptr可以和和指针一样可以使用:
//重载 * 运算符,解参考,解指针引用
T& operator*() const//主函数用:(*p).method();调用方法
{
return *ptr;
}
//重载 -> 运算符,调用对象的公共成员
T* operator->() const主函数用:p->method();调用方法
{
return ptr;
}
使用方法:
shared_ptr<int> p(new int(2));
确实可以赋值和拷贝:
常见函数:
s.get() ; //获取其保存的原生指针,尽量不要使用
s.bool() ; //判断是否拥有指针
s.reset() ; //释放并销毁原生指针。如果参数为一个新指针,将管理这个新指针
s.unique() ; //如果引用计数为 1,则返回 true,否则返回 false
s.use_count(); //返回引用计数的大小
智能指针还是比较复杂的,大家在使用时要多加注意。
2.2.1.3 weak_ptr
weak,弱小的,不控制对象的生命周期,不参与引用计数,只是提供一个管理对象的访问手段。
它的出现是为了协助shared_ptr工作的,尽管智能指针可以自动释放内存,但是当两个对象使用同一个shared_ptr相互指向对方时,形成循环引用(当a指向b,b指向a时,要调用析构函数都得要对方的析构函数先调用,类似于锁机制。),计数器失效,导致释放内存时机不对,内存泄漏。
或者是不能共享计数器情况:
A* a = new A();
shared_ptr<A> s1(a),s2(a);
//两者不能共享计数器。s1和s2都会将计数器计为1,s2并不知道s1已经接管了计数器。
解决以上问题的方法之一就是使用weak_ptr指针。由于weak_ptr指针没有重载操作符“*” 和 “->”,不能单独使用,只能和shared_ptr结合使用。
更多推荐阅读:C++智能指针循环引用问题分析-CSDN博客
以及:C++11智能指针 shared_ptr、weak_ptr和unique_ptr详解 - 知乎 (zhihu.com)
2.2.3 point-like class ——迭代器
通过上述学习,你可能也发现了智能指针实际就是一个类,一个point-like class。在C++STL内部的所有迭代器(iterator)就是这样的,实际上也是一个point-like class。
迭代器配合容器来食用,它是用来检查容器内的元素 并 遍历元素的数据类型。(常见的元素有string、vector等,详细的学习了STL内容我会再写一篇,或者大家自行百度先学习也可以哦。)
既然作为point-like class,那么它也要重载*和->操作符啦。除此之外,它要遍历元素,自然也要重载“++”和“--”操作符。前面的shared_ptr提到具体实现需要一个普通指针,那迭代器也一样,只不过在这里需要的指针是一个双向指针(来形成一个双向链表,在链表中有一个prev节点、next节点、一个date节点。)
2.2.4 Function-like class
functiom-like class,像函数一样的。
像指针一样的类,需要重载“*”和“->”,那咱们的函数必然是参数传递的“()”。我们的这个类实现还需要继承其他父类unary_function和binary_function。
2.3 模板
2.3.1 成员模板
在前面一篇文章中介绍了参数模板和函数模板,其实还有成员模板。
成员模板,就是模板之中又有一个模板,这种模板一般放在构造函数里。
例如有4个类,罗非鱼类继承鱼类,麻雀继承鸟类,先要将子类(罗非和麻雀)组成一对pair,拷贝到父类(🐟和鸟)组成一对pair。具体实现解说如下:
#include<iostream>
using namespace std;
template<class T1,class T2>//这次是鸟类和鱼类,下次可以是人类和车类,使用模板好处大大有
class pair {
public:
pair() :frist_type(T1()), sec_type(T2()) {
cout << "无参构造" << endl;
}//无参构造
pair(const T1& a,const T2& b):frist_type(a), sec_type(b) {}//有参构造
//接下来要写拷贝构造
template<class U1, class U2>
pair(const pair<U1,U2>& p)//调用无参生成对象
: frist_type(p.frist_type), sec_type(p.sec_type) {
cout << "拷贝构造" << endl;
}
private:
T1 frist_type;
T2 sec_type;
};
//写四个类,这里只是将类的的定义大致写出,运行时要补全
class Fish {
public:
Fish() {
cout << "这是一条鱼" << endl;
}
};
class Tilapia :public Fish {
Tilapia() {
cout << "这是一条罗非鱼" << endl;
}
};
class Bird {
};
class Sparrow :public Bird {
};
int main() {
pair<Tilapia, Sparrow> P1;
pair<Fish, Bird> P2(P1);//
//pair<Tilapia, Sparrow> P3(pair<Fish, Bird>(P2));
}
将拷贝构造函数注释掉则会出现以下情况:
2.3.2 模板特化(specialization)
上面提到的模板是模板的泛化,那与之对应的就有特化。顾名思义,就是特定的特征,独特的类型。它会在类名后面指定类型,如下:
template<>
class hash<int> {//在这里绑定类型
};
除了特化,还有一个模板偏特化的概念。偏特化也就是局部特化,“局部”可以从个数和范围上来考虑。
2.4 三个主题
2.4.1 模板参数可变化(variadic templates)
参数可变?当我们我们还不确定参数的个数、类型时,我们就可以使用“...”来实现模板参数可变化。printf()函数就是一个很好的例子。当我们使用printf()函数时,是不是可以一条打印一个数值,或者两个数值,或者一个数值一个字符,这些其实就是模板参数可变化。以下时printf()函数实现:
int printf(const char *fmt, ...) //参数可变化“...”
{
int i;
char buf[256];
va_list arg = (va_list)((char*)(&fmt) + 4);
i = vsprintf(buf, fmt, arg);
write(buf, i);
return i;
}
2.4.2 auto
“auto”关键字可简化类型定义。怎么说呢,举个例子你就清晰了。
#include<vector>
using namespace std;
int main() {
vector<int> vtr;
vector<int>::iterator it;
it = vtr.begin();//定义vtr的迭代器
//VS
vector<int> vtr1;
auto it1= vtr.begin();//定义vtr的迭代器,auto会自动将it定义成迭代器
return 0;
}
在练习时不建议总是这样写,不然用久了自己也不知类型是什么,容易造成混乱。(大佬请忽略这句提示。)
2.4.3 ranged-base
这是一种访问容器的另一种形式。也是直接写一个简单的代码就理解了:
#include<iostream>
#include<vector>
using namespace std;
int main() {
vector<int> vtr = {1,3,3};
for (int i = 0; i < 3; i++) {//传统上
cout << vtr[i] << endl; ;
}
//VS
for (int i : vtr) {//range-based for循环
cout << i << endl;
}
return 0;
}
首先直观上的优点就是有
2.5 关于多态
C++的多态必须满足两个条件:
1.要通过父类的指针或者引用调用虚函数;
2.被调用的函数是虚函数,且必须完成对基类虚函数的重写 。
2.5.1 虚指针和虚表
在前面一篇文章中,也提到了继承的实现主要是通过重写虚函数。若一个类中存在虚函数(无论个数)都必然存在一个虚指针。所以我们在计算一个类的大小除了数据大小还需要加上虚指针的大小:4字节(32位操作系统)。
虚指针是用来指向虚表的位置的,虚表里面就存放着类中各个虚函数的地址(包括子类覆写的虚函数地址)。当我们的父类中存在虚函数时,子类继承时,虚表会先父类的虚函数地址,当子类覆写虚函数,会增加子类虚函数地址。
2.5.2 动态绑定
何谓动态绑定?
在了解动态绑定,我们先来了解一下另一种绑定方式——静态绑定。在我们通常使用的:对象.函数名()的这种call xx方式就是静态绑定,其中xx其实是函数的地址,是固定的,所以称其为静态绑定。既然了解了静态绑定,我们说回动态绑定。
动态绑定其实就是在内存中通过虚指针找到虚表,再根据调用的函数在表中找到对应的入口。在实现这种虚机制有三个关键点:指针、向上转型、调用虚函数。
有2个类:B继承A,vfun()是虚函数,有以下两种情况,请判断是哪种绑定?
B b;
A a = A(b);//向上转型
a.vfun();//调用虚函数,这调用谁的虚函数?A的还是B的??
A* p = new B;//向上转型
P->vfun();//通过指针调用虚函数
很明显只有有第二种情况才是动态绑定。第一种情况仍然是通过对象.调用虚函数。虽然如此,这样还是调用父类的虚函数。
2.5.3 基于上理解多态
如果我们要实现画图的功能,小明想画长方形,小红想画圆形,小绿想画五角星等等,在C语言中实现它"只能"通过if else判断哪个图形。(没有拉踩C语言的意思)
但是在C++中,我们可以先定义一个叫形状的父类,在类中定义一个画图的虚函数,当我们要画哪个图形,就继承形状覆写虚函数就可以实现了,而且这样子可以画超多的图形,要画啥就写一个继承就好了。
其实C++的多态实现是依赖于 虚函数的实现。
多态,其实就是多种形态啦。像本来只有形状的父类,但是通过继承可以有长方形、圆形、五角星等等多种形态。多态的实现方式分为三块:重载、重写、重定义。
- 重载
重载是指在同一个作用域相同下,函数名相同,但是参数类别不一样。比如以下fun()重载:
class A
{
void fun() {};
void fun(int i) {};
int fun(int i, int j) {};
};
- 重写(覆盖、覆写)
重写就是上面提到的,子类继承父类重写虚函数,通过动态绑定实现重写。其中返回值,参数值、名字都必须一致,但是函数体就不一样,用来写子类自己的实现。
class Fish {
public:
Fish() {
cout << "这是一条鱼" << endl;
}
virtual void test(){
cout<<"这个是父类的虚函数"<<endl;
}
};
class Tilapia :public Fish {
public:
Tilapia() {
cout << "这是一条罗非鱼" << endl;
}
void test(){
cout<<"这个是子类的虚函数"<<endl;
}
};
- 重定义
也叫做隐藏,子类重新定义父类中有相同名称的非虚函数 ( 参数可以不同 ) ,指派生类的函数屏蔽了与其同名的基类函数。可以理解成发生在继承中的重载。当继承之后,父类的函数会隐藏。
3.结语
陆陆续续写了好几天,终于把主要的内容写出来了,我这两篇顶多算是入门基本的,是自己学习C++过程中小理解,的要想深度还得继续学习。
现在准备学习标准库STL的知识,下一篇可能是关于标准库的内容,中间应该也会穿插一些其他知识的学习。如果对于你的学习有用的话可以给俺点个赞,有问题也欢迎指出改正。