本博客将记录:新经典课程知识点的第16节的笔记!
本小节的知识点分别是类型转换构造函数、运算符、类成员指针。
今天总结的知识分为以下6个点:
(1)类型转换构造函数
(2)类型转换运算符(类型转换函数)
(2.1)显式的类型转换运算符
(2.2)有趣范例:类对象转换为函数指针
(3)类型转换的二义性问题
(4)类成员函数指针
(4.1)指向普通成员函数的指针
(4.2)指向virtual虚函数的指针
(4.3)指向静态static成员函数的指针
(5)类成员变量指针
(5.1)指向普通成员变量的指针
(5.2)指向静态成员变量的指针
(6)总结
(1)类型转换构造函数:
类型转换构造函数的作用:把任何类型的对象转换为一个类类型的对象。
通过之前的学习,我们都知道,一般构造函数都有如下特点:
①以对应的类名作为函数名
②没有返回值
③可重载(比如有带一个参数的构造函数,也有带两个参数的构造函数等等)
④有特殊版本(比如拷贝构造函数,移动构造函数)
那么今天,我们就将介绍一种新版本的构造函数,即:类型转换构造函数
类型转换构造函数的特点:
a)只带有一个参数(该参数其实就是待转换的数据类型)
b)在类型转换函数体的实现中,必须要指定从该待转换类型到该类类型的转换方法!(也即你要在该构造函数中去实现这样的类型转换)
下面请看以下代码:(为了演示方便,省去析构函数等复杂代码)
class TestInt{//保存0-100之间的一个数字的类
public:
int m_i;
TestInt(int x):m_i(x) {
//这也是一个普通的带一个参数的构造函数
//这其实就是一个类型转换构造函数,本构造函数可以将一个int数字转换为TestInt类对象
if (x < 0) m_i = 0;
if (x > 100) m_i = 100;
cout << "TestInt的类型转换构造函数执行了!" << endl;
}
};
TestInt t1 = 22;//✓运行。隐式类型转换:类型转换构造函数帮我们将int数字22转换为TestInt类的对象t1
这一行代码就是do隐式类型转换的,编译器会为我们调用上述的类型转换构造函数,用int型数字22生成一个匿名对象(也即其m_i == 22),然后用该匿名对象初始化对象t1。
TestInt t1 = TestInt(22);//✓运行。
//调用类型转换构造函数,帮我们将int数字22转换为TestInt类的对象t2,但没有进行隐式类型转换
TestInt t2(188);//✓运行。
//调用类型转换构造函数,帮我们将int数字188转换为TestInt类的对象t2,但没有进行隐式类型转换
TestInt t3{ 888 };//✓运行。
//调用类型转换构造函数,帮我们将int数字888转换为TestInt类的对象t3,但没有进行隐式类型转换
这三行都是调用的类型转换构造函数,帮我们将int数字转换为TestInt类的对象,但没有进行隐式的类型转换,为什么呢?我们不妨在该类型转换构造函数前声明explicit关键字,这样该类型转换构造函数就不能do隐式的类型转换了。
explicit TestInt(int x):m_i(x) {
//这也是一个普通的带一个参数的构造函数
//这其实就是一个类型转换构造函数,本构造函数可以将一个int数字转换为类类型TestInt
if (x < 0) m_i = 0;
if (x > 100) m_i = 100;
cout << "TestInt的类型转换构造函数执行了!" << endl;
}
运行结果:
(2)类型转换运算符函数(类型转换函数):
类型转换运算符函数:是一个能将对应的类类型的对象转换为其他数据类型的对象的该类的成员函数。
格式: operator typeName() const;
注意:只有当该类类型的对象与待转换的类型对象之间是存在一定的关系,且你又有此需求时,才在一个类中去写(使用)该特殊的成员函数。否则,都是不建议我们去写(用)它!
(毕竟当你在读他人的代码时,如果有这种特殊的函数,就会造成我们不容易读懂代码!)
类型转换运算符函数的特点:
a)const是可选项。const表示:在类型转换时不应该改变 待改变类型对象的数据。(你也可以不写上const,只是不写const你的代码安全性不高而已)。
b)typeName:是待转换成(过去)的某种数据类型。
c)该函数是一种特殊的成员函数:
特殊点1---没有形参(形参列表为空),因为该类型转换运算符函数都是隐式地进行类型转换的。
特殊点2---不能指定返回值类型,但它却能返回一个你所指定的typeName类型的对象。
请看以下代码:
class TestInt{//保存0-100之间的一个数字
public:
int m_i;
TestInt():m_i(0) {}
TestInt(int x):m_i(x) {
if (x < 0) m_i = 0;
if (x > 100) m_i = 100;
}
operator int() const {
return m_i;//按照TestInt类的成员变量m_i是多少,我就转换为对应多少值的int型数字
}
};
int main(void) {
TestInt t22;
t22 = 6;
int k1 = t22 + 5;
//此时编译器认为一个int型的5 + 另一个类型的对象不太合适
//于是乎就帮我们把t22转换为int型的6(当然,我们这里用m_i返回,因此m_i是多少,那么t22就会转换成多少的int)
//隐式地调用TestInt类的operator int() const 类型转换运算符成员函数
int k2 = t22.operator int() + 5;
//显式地调用TestInt类的operator int() const 类型转换运算符成员函数
//把t22转换为int型的6
cout << "k1 = " << k1 << " k2 = " << k2 << endl;
return 0;
}
运行结果:
当然,你如果把类型转换运算符成员函数写成类似这样:
operator int() const {
return 110;//按照TestInt类的成员变量m_i是多少,我就转换为对应多少值的int型数字
}
那么运行结果始终都保持是110 + x:(这里是110 + 5)
(2.1)显式的类型转换运算符:
在类型转换运算符成员函数 定义前加上explicit关键字。本质就是给函数一个显式explicit声明,作用:让该函数调用传参时不能do隐式类型的转换!
请看以下代码:(把operator int() const函数声明为显式explicit传参)
explicit operator int() const {//防止编译器为我们do隐式类型转换
return 110;//按照TestInt类的成员变量m_i是多少,我就转换为对应多少值的int型数字
}
int main(void) {
TestInt t22;
t22 = 6;
int k1 = t22 + 5;//×出错!operator int() const函数不能给t22对象do隐式转换为int型的6
int k2 = t22.operator int() + 5;//✓成功!
//因为我就是显式地调用t22对象的成员函数operator int() const函数来给我的t22对象do类型转换为int型的6
cout << "k1 = " << k1 << " k2 = " << k2 << endl;
return 0;
}
运行结果:
显然,explicit operator int() const函数只会显式地将TestInt类对象抓换为int型数字,这样t22对象就不能被隐式类型转换为int型数字了。But,我们其实还有一种方法来强迫t22对象转换为int型数字,那就是C++的一种新式转型关键字:
static_cast<T>(expression):是用来do强迫性的隐式类型转换的C++中提供的新式转型关键字
请看以下代码:
int main(void) {
TestInt t22;
t22 = 6;
int k1 = static_cast<int>(t22) + 5;
int k2 = t22.operator int() + 5;
cout << "k1 = " << k1 << " k2 = " << k2 << endl;
return 0;
}
运行结果:
可见,用static_cast<T>(expression)也可do从一个类类型到其他数据类型的隐式类型转换。(但是,前提是存在一个 operator int() const;函数让编译器知道能这样去转型!否则用了static_cast也没无法转型!)
比如,当我注释掉该特殊的成员函数后,编译器就报错了:
//explicit operator int() const {
// return 110;//按照TestInt类的成员变量m_i是多少,我就转换为对应多少值的int型数字
//}
相信看到这里,大家都明白了哈~下面我就继续介绍一个有趣的范例吧~
(2.2)有趣范例:类对象转换为函数指针:
废话不多说,请看以下代码:
class TestInt{//保存0-100之间的一个数字
public:
//这2行的等价的!用其一即可定义一个函数指针类型了!
using tfpoint = void(*)(int);//定义了一个指向无返回值且只有一个int形参的函数的函数指针类型,该函数指针类型名 叫 tfpoint
typedef void (*tfpoint)(int);//定义了一个指向无返回值且只有一个int形参的函数的函数指针类型,该函数指针类型名 叫 tfpoint
int m_i;
TestInt():m_i(0) {}
TestInt(int x):m_i(x) {
//这也是一个普通的带一个参数的构造函数
//这其实就是一个类型转换构造函数,本构造函数可以将一个int数字转换为类类型TestInt
if (x < 0) m_i = 0;
if (x > 100) m_i = 100;
//cout << "TestInt的类型转换构造函数执行了!" << endl;
}
static void myfunc(int val) {//静态成员函数
cout << "val = " << val << endl;
}
operator tfpoint() {//类型转换运算符函数 将 obj ---> tfpoint
return myfunc;//返回函数名,也即让函数地址作为函数指针类型返回即可
//因为我要将该类的对象强制类型转换为一个tfpoint类型的函数指针,所以必须返回一个函数地址
}
explicit operator int() const {//类型转换运算符函数 将 obj ---> int
return 110;//按照TestInt类的成员变量m_i是多少,我就转换为对应多少值的int型数字
}
};
int main(void) {
TestInt t22;
t22 = 6;
//因为该类的对象可以当做是一个函数指针,那么因此我就可以
//用该类对象的名字当做函数名or函数指针名去调用相应的类型转换后该指针所保存的地址处的函数了!
t22(8);
(*t22)(999);
//<===>
t22.operator TestInt::tfpoint() (1888);
(* t22.operator TestInt::tfpoint() )(1998);
return 0;
}
解释①:
t22.operator TestInt::tfpoint(x)或者(* t22.operator TestInt::tfpoint() )(x): t22对象调用了operator tfpoint() (x)函数后变成了一种函数指针了,且该函数指针是指向静态成员函数static void myfunc(int val)的函数首地址的,也即指向该函数的意思!
解释②:
这四行调用代码每一行都分别调用了2个函数:
先调用类型转换运算符函数把obj ---> tfpoint,也即:t22拿到了myfunc()函数的首地址。后调用静态成员函数myfunc()。
运行结果:
补充知识点:
补充①函数指针:指向某个函数的指针。且,用(*函数指针名)()或者函数指针名()调用函数和用函数名()来调用函数是等价的!
比如:
void test() { cout << "My test!" << endl; }
void (*pt)() = &test;
(*pt)();
pt();
test();
//result:
//My test!
//My test!
//My test!
补充②定义函数指针类型:定义一个函数指针类型有2种方法:
1---用using 格式:using 函数指针类型名 = 函数返回值类型 (*) (形参表);
2---用typedef 格式:typedef 函数返回值类型 (*函数指针类型名) (形参表);
如上述代码中的:
//这2行的等价的!用其一即可定义一个函数指针类型了!
using tfpoint = void(*)(int);
//定义了一个指向无返回值且只有一个int形参的函数的函数指针类型,该函数指针类型名 叫 tfpoint
typedef void (*tfpoint)(int);
//定义了一个指向无返回值且只有一个int形参的函数的函数指针类型,该函数指针类型名 叫 tfpoint
(3)类型转换运算符所产生的二义性问题:
所谓的类型转换运算符的二义性问题:也即当你在一个类中定义了2个及以上个类型转换运算符函数时,容易让编译器觉得,你的代码这么干也行,那么干也行,最后导致编译器都不知道怎么干了的结果,从而编译器就只能给我们报错了。
请看以下代码:
class TestInt{//保存0-100之间的一个数字
public:
int m_i;
TestInt():m_i(0) {}
TestInt(int x):m_i(x) {
//这也是一个普通的带一个参数的构造函数
//这其实就是一个类型转换构造函数,本构造函数可以将一个int数字转换为类类型TestInt
if (x < 0) m_i = 0;
if (x > 100) m_i = 100;
//cout << "TestInt的类型转换构造函数执行了!" << endl;
}
operator int() const {
return 110;
}
operator double() const {
return 88.88;
}
};
int main(void) {
TestInt t22;
t22 = 6;
int k = t22 + 6;//error!编译器认为既能把t22对象转成int 110,也能转成double 88.88
//既能这么干,又能那么干,因此干脆就直接不干了!报错吧!
return 0;
}
运行结果:
再看以下例子代码:
class CT1 {
public:
CT1(int ct) {}//类型转换构造函数
};
class CT2 {
public:
CT2(int ct) {}//类型转换构造函数
};
//定义一个函数testfunc的2个重载版本
void testfunc(const CT1& C) {}
void testfunc(const CT2& C) {}
int main(void){
testfunc(111);//×错误!因为编译器根本就不知道是将int的111转换为CT1对象还是CT2对象
//也即出现了二义性的问题!既能转成CT1对象又能转成CT2对象,编译器干脆就直接不干了!
testfunc(CT1(112));
//✓!明确显式地调用void testfunc(const CT1& C){}但是这种手段表明你的代码设计得不好!
testfunc(CT2(212));
//✓!明确显式地调用void testfunc(const CT2& C){}但是这种手段表明你的代码设计得不好!
return 0;
}
so,根据这个二义性问题,我们得出结论:在一个类中,尽量只出现一个类型转换运算符函数。(避免出现二义性问题)
(4)类成员函数指针
类成员函数指针:是指向类的成员函数的指针。
注意2个点:
①成员函数是属于类的,而不是属于类对象的!只要你定义了类并给该类定义了成员函数,那么成员函数的地址就必然是存在了的!(并不是说你只有定义了该类的对象后,才有存在各成员函数的地址)
②指向类的普通成员函数or虚函数的指针必须得通过类的对象or指向类的对象的指针才能够调用对应的类的成员函数!
(But指向类的静态static成员函数的指针就不用通过类的对象or指向类的对象的指针来调用对应的静态成员函数!)
(4.1)指向普通成员函数的指针:
声明格式: “类名::*函数指针变量名”
用“&类名::成员函数名”可获取类的普通成员函数的地址。(这里是一个真正的“内存地址”)
用“(类对象名.*函数指针变量名)(形参表) ” 或“(类对象指针->*函数指针变量名)(形参表)” 可调用所指向的类的普通成员函数。
请看以下代码:
class CT {
public:
CT() {}
void ptfunc(int val) { cout << "ptfunc普通成员函数被调用,且value = " << val << endl; }
virtual void virtualptfunc(int val) { cout << "virtualptfunc虚成员函数被调用,且value = " << val << endl; }
static void staticfunc(int val) { cout << "staticfunc静态成员函数被调用,且value = " << val << endl; }
~CT() {
cout << "调用了类CT的析构函数!" << endl;
}
};
//定义一个指向普通类成员函数的指针ptf
void (CT::*ptf)(int) = &CT::ptfunc;
int main(void) {
CT ct;
CT* pct = new CT;//pct为指向CT类对象的指针
(ct.*ptf)(10);//函数指针ptf通过对象ct调用了类的普通成员函数ptfunc()
(pct->*ptf)(20);//函数指针ptf通过对象指针pct调用了类的普通成员函数ptfunc()
delete pct;
return 0;
}
运行结果:
(4.2)指向virtual虚函数的指针:
声明格式: “类名::*函数指针变量名”
用“&类名::成员函数名”可获取类的虚函数的地址。(这里是一个真正的“内存地址”)
用“(类对象名.*函数指针变量名)(形参表) ” 或“(类对象指针->*函数指针变量名)(形参表)” 可调用所指向的类的虚成员函数。
请看以下代码:
class CT {
public:
CT() {}
void ptfunc(int val) { cout << "ptfunc普通成员函数被调用,且value = " << val << endl; }
virtual void virtualptfunc(int val) { cout << "virtualptfunc虚成员函数被调用,且value = " << val << endl; }
static void staticfunc(int val) { cout << "staticfunc静态成员函数被调用,且value = " << val << endl; }
~CT() {
cout << "调用了类CT的析构函数!" << endl;
}
};
//定义一个指向虚成员函数的指针pvtf
void (CT::* pvtf)(int) = &CT::virtualptfunc;
int main(void) {
CT ct;
CT* pct = new CT;
(ct.*pvtf)(11);//函数指针pvtf通过对象ct调用了类的虚函数virtualptfunc
(pct->*pvtf)(22);//函数指针pvtf通过对象指针pct调用了类的虚函数virtualptfunc
delete pct;
return 0;
}
运行结果:
(4.3)指向静态static成员函数的指针:(使用方式和指向非成员函数的指针相同)
声明格式: “*函数指针变量名”
用“&类名::成员函数名”可获取类的static静态成员函数的地址。(这里是一个真正的“内存地址”)
用“(*函数指针变量名)(形参表) ” 或“函数指针变量名(形参表)” 可调用所指向的类的static静态成员函数。(这与之前的指向普通成员函数的指针or指向虚函数的指针必须通过对象or指向对象的指针来调用对应的成员函数是不同的哈!)
请看以下代码:
class CT {
public:
CT() {}
void ptfunc(int val) { cout << "ptfunc普通成员函数被调用,且value = " << val << endl; }
virtual void virtualptfunc(int val) { cout << "virtualptfunc虚成员函数被调用,且value = " << val << endl; }
static void staticfunc(int val) { cout << "staticfunc静态成员函数被调用,且value = " << val << endl; }
~CT() {
cout << "调用了类CT的析构函数!" << endl;
}
};
//定义一个指向static静态成员函数的指针
void (*stf)(int) = &CT::staticfunc;
int main(void) {
stf(100);//指向静态成员函数的指针stf调用了类的静态成员函数staticfunc
(*stf)(200);//指向静态成员函数的指针stf调用了类的静态成员函数staticfunc
return 0;
}
运行结果:
相信,通过以上我的总结,大家能把指向类的成员函数的指针这个小知识点学好~
(5)类成员变量指针:
类成员变量指针:指向类的成员变量的指针。
(5.1)指向普通成员变量的指针:
声明格式:“变量类型 类名::*成员变量指针名 = &类名::成员变量名;”(这里并不是一个真正的“内存地址”,而只是该成员变量与对应的类对象之间的一个偏移量offset)
用“对象名.*成员变量指针名”或“对象指针->*成员变量指针名”来读写对应的普通成员变量。
请看以下代码:
class CT {
public:
int m_a;//普通成员变量,属于(依附于)类的对象
CT() {}
~CT() {
cout << "调用了类CT的析构函数!" << endl;
}
};
//定义一个指向CT类的成员变量m_a的成员变量指针pm_a
int CT::*pm_a = &CT::m_a;//0x0000000000000008(???)
//注意:指向类的成员变量的指针并不是一个真正意义上的地址值!
//也是该成员变量与该类的对象之间的偏移量offset
//只有当指向成员变量的指针依附在类的对象上时,才会让该成员变量产生真正的内存地址值!
int main(void) {
CT ct;
CT* pct = new CT;
ct.*pm_a = 1234;//通过调用指向成员变量m_a的指针来修改对象ct.m_a的值!
//<===>ct.m_a = 1234;
cout << "ct.m_a = " << ct.m_a << endl;
ct.m_a = 5678;
cout << "ct.m_a = " << ct.m_a << endl;
pct->*pm_a = 78;
cout << "ct.m_a = " << pct->m_a << endl;
pct->*pm_a = 88;
cout << "ct.m_a = " << pct->m_a << endl;
delete pct;
return 0;
}
运行结果:
这说明,指向普通成员变量的指针并不是一个真正的内存地址!而是一个偏移量offset而已!
(5.2)指向静态成员变量的指针:
声明格式:“变量类型 *成员变量指针名 = &类名::static成员变量名;”(这里是一个真正的“内存地址”)
用“*成员变量指针名”来读写对应的普通成员变量。
请看以下代码:
class CT {
public:
int m_a;//普通成员变量,属于(依附于)类的对象
static double m_b;//类内声明一个静态成员变量,属于(依附于)类,属于任何对象所共享的
CT() {}
~CT() {
cout << "调用了类CT的析构函数!" << endl;
}
};
double CT::m_b = 1.88;//类外定义(初始化)了一个静态成员变量m_b
//定义一个指向CT类的静态成员变量m_b的成员变量指针pm_b
double* pm_b = &CT::m_b;
//&pm_b 0x00007ff69848f088 说明这种指向static静态成员变量的指针是有真正的地址值的!
int main(void) {
*pm_b = 138.8;//<===> CT::m_b = 138.8
cout << "CT::m_b = " << CT::m_b << endl;
*pm_b = 998.8;//<===> CT::m_b = 998.8
cout << "CT::m_b = " << CT::m_b << endl;
return 0;
}
运行结果:
这说明,指向static静态成员变量的指针是一个真正的内存地址!
(6)总结:
类型转换构造函数、运算符、类成员指针这三方面的知识点我个人感觉是挺细致点,需要我们慢慢吸收,并运用在coding中才能熟练掌握。当然,对于前两者,wjw老师并不建议我们常用,so尽量少用哈~还有就是成员指针这个内容务必要掌握好,方便后续我们阅读他人的代码时不发懵,自己也能写出这样的代码。
今天我总结的内容的深层次的原理是C++对象模型(高级知识)中的内容,包括什么虚函数表,偏移量offset等知识,这些我目前也还没有深入地了解。但当前阶段只需要我们掌握到看到这些知识点就知道它怎么用,怎么写这样一种状态即可了。言归正传,不论是应对日后的找工作面试,还是提高我们作为Cpp开发者的coding修养时,C++对象模型的知识无疑是非常重要!我本人也会在后续的学习coding中找时间补上这方面的知识,提高自己的cpp素养!
好,那么以上就是这一3.16小节我所回顾的内容的学习笔记,希望你能读懂并且消化完,也希望自己能牢记这些小小的细节知识点,加油吧,我们都在coding的路上~