被误解的C++——类型

longshanks 2007-06-26 05:01:27
类型
任何一种语言都有类型。对类型的不同的态度,造就了语言的个性。我们通常会将语言分为“强类型”和“弱类型”。
通常认为C++是强类型的。但也有反对意见。反对者认为,既然C++拥有隐式类型转换,那么就不该作为强类型语言。我这里不打算趟这潭混水,强类型还是弱类型,没有什么实际意义。
这里,我打算认真地考察一下C++独特的类型系统,来探寻C++在语言中特立独行的根源。我会尽可能不涉及语言的比较,至少不涉及他们的好坏,以免引发新一轮的口水仗。
强类型提供了很好的类型安全,但缺少灵活性。弱类型化后,灵活性提高了,但类型安全无法保障。C++所作的探索,就是寻找一种方式,在强类型的情况下,允许提供灵活,但又安全的类型系统。
让我们先从C++的内置类型说起。
C++的类型分为内置类型和用户定义类型。内置类型主要包括整数类型、浮点类型、引用类型(指针和引用)等等。我们先来分析一下内置类型,以整数类型为例。
我们知道,一个整数类型可以进行初始化、赋值、算术运算、比较、位操作,以及参与逻辑运算:
int a=10; //初始化
int b;
b=a; //赋值
int c=(a+b)*(a-b); //算术运算
if(c==b) … //比较
a=c&b; //位操作
if(c==b || !a) … //逻辑运算
当然,其他的还包括取地址、取引用等类型的基本操作。
这些操作都是语言赋予整数类型的基本操作,我们无需对其进行而外的转换或者处理。但是,当我们把目光转向用户定义类型后,问题就复杂化了。由于C++被定位于系统级开发语言(实际上C++什么开发领域都可以胜任,但最初发明它时是打算用于开发系统软件的),所以时常会需要一些古怪的操作,比如把一个用户定义类型赋值给int类型,这种操作在强类型语言中是不合规矩的。
如果我们不管三七二十一,把用户定义类型按位拷贝给int类型(这是int类型之间赋值操作的语义),那么准保会惹上大麻烦的。但如果在特定情况下,这种操作是需要的(当然不一定是必需的)。那么,我们就应当提供一种方法,允许这种赋值操作在受控的情况下进行。
为此,C++引入了操作符重载(学自Ada),以及一些相关的机制。通过这些机制,使我们(几乎)可以按照内置类型(如整数)的行为设计用户定义的类型。下面我通过一个案例慢慢讲述如何把一个用户类型变成内置类型的模仿者。这个案例来源于前些日子论坛上的口水仗,就是开发variant类型。为了简化问题,我选取了三种具有代表性的类型int,double,char*作为variant包容的目标,并且不考虑性能问题。
首先,我定义了一个枚举,为了使代码能够更加清晰:
enum
{
vt_empty=-1, //空variant
vt_double=0, //double类型
vt_int=1, //int类型
vt_string=2 //字符串类型
};
然后,定义variant的基本结构。我使用了最传统的手法,union。
class variant
{
private:
int var_type; //variant包含的类型标记
union
{
double dbval;
int ival;
char* csval; //由于union不能存放拥有non-trivial构造函数等成员,
// 所以只能用char*,提取数据时另行处理
};
};
现在,我们一步步使variant越来越像一个内置类型。看一下int类型的初始化方式:
int a(0), b=0;
int(0); //创建并初始化一个int临时对象
我们先来考虑用一个variant对象初始化另一个variant对象。实现这个功能,需要通过重载构造函数:
class variant
{
public:
variant(const variant& v) {…}

};
这是一个拷贝构造函数,使得我们可以用一个variant对象初始化另一个variant对象:
variant x(a), y=a; variant(a); //假设a是一个拥有有效值的variant对象
如果我们没有定义任何构造函数,那么编译器会为我们生成一个复制构造函数。但这不是我们要的,因为编译器生成的复制构造函数执行浅拷贝,它只会将一个对象按位赋值给另一个。由于variant需要管理资源引用,必须执行深拷贝,所以必须另行定义一个赋值构造函数。
按C++标准,一旦定义了一个构造函数,那么编译器将不会再生成默认构造函数。所以为了能够如下声明对象:
variant x;
我们必须定义一个默认构造函数:
class variant
{
public:
variant(): var_type(vt_empty) {…}

};
下一步,实现variant对象间的赋值。C++中内置类型的对象间赋值使用=操作符:
int a=100, b;
b=a;
用户定义的类型间的赋值也使用=操作符。所以,只需重载operator=便可实现对象间的赋值:
class variant
{
public:
variant& operator=(const variant& v) {…}

};
variant x, y;
x=y;
int是一种可以计算的数值类型。所以,我们可以对int类型的变量执行算术运算、比较、逻辑运算、位运算等:
int a、b、c、d、e、f、g;
a=b+c;
d=a-b;
e/=c;
c==d;
if(!c) …
f=f<<3;

同样,variant涵盖了几种数值类型,那么要求其能够进行这些运算,也是理所当然的:
variant a、b、c、d、e、f、g;
a=b+c;
d=a-b;
e/=c;
c==d;
if(!c) …
f=f<<3;

为实现这一点,C++提供了大量的操作符重载。在C++中,除了“.” 、“.*”、“? :”、“#”、“##”五个操作符,RTTI操作符,以及xxx_cast外,其余都能重载。操作符可以作为类的成员,也可以作为全局函数。(类型转换操作符和“=”只能作为类的成员)。通常,将操作符重载作为全局函数更灵活,同时也能避免一些问题。
我们先重载操作数都是variant的操作符:
bool operator==(const variant& v1, const variant& v2) {…}
bool operator!=( const variant& v1, const variant& v2) {…}
variant& operator+=( const variant& v1, const variant& v2) {…}
variant operator+( const variant& v1, const variant& v2) {…}

需要注意的是,对与variant而言,他可能代表了多种不同的类型。这些类型间不一定都能进行运算。所以,variant应当在运算前进行类型检查。不匹配时,应抛出运行时错误。
C++允许内置类型按一定的规则相互转换。比如:
int a=100;
double b=a;
a=b; //可以转换,但有warning
为了使variant融入C++的类型体系,我们应当允许variant同所包容的类型间相互转换。C++为我们提供了这类机制。下面我们逐步深入。
我们先处理初始化。非variant类型初始化也是通过重载构造函数:
class variant
{
public:
variant(double val) {…}
variant(int val) {…}
variant(const string& val) {…}

}
这些是所谓的“类型转换构造函数”。它们接受一个其它类型的对象作为参数,在函数体中执行特定的初始化操作。最终达到如下效果:
int a=10;
double b=23;
string c(“abc”);
variant x(a), y=b; variant(c);
接下来,处理不同类型和variant对象赋值的问题。先看向variant对象赋值。同样通过=操作符:
class variant
{
public:
variant& operator=(double v) {…}
variant& operator=(int v) {…}
variant& operator=(const string& v) {…}
variant& operator=(const char*) {…}//该重载为了处理字符串常量

};
这样,便可以如下操作:
int a=10;
double b=23;
string c(“abc”);
variant x,y,z;
x=a;
y=b;
z=c;
然后再看由variant对象向其它类型赋值。实现这种操作需要利用类型转换操作符:
class variant
{
public:
operator double() {…}
operator int() {…}
operator string() {…}

};
使用起来和内置类型赋值或初始化一样:
variant x(10), y(2.5), z(“abc”);
int a=x;
double b=y;
string c;
c=z;
现在,variant已经非常“象”内置类型了。最后只需要让variant同其它类型一起参与运算便大功告成了。我们依然需要依靠操作符重载,不过此处使用全局函数方式的操作符重载:
bool operator==(const variant& v1, int v2){…}
bool operator==(int v1, const variant& v2){…}
bool operator==(const variant& v1, double v2){…}
bool operator==(double v1, const variant& v2){…}
bool operator==(const variant& v1, const string& v2){…}
bool operator==(const string& v1, const variant& v2){…}
bool operator==(const variant& v1, const char* v2){…}
bool operator==(const char* v1, const variant& v2){…}

variant& *=(const variant& v1, double v2){…}
variant& *=(double v2, const variant& v2){…}

我们可以看到,对于每个非variant类型,操作符都成对地重载。通过交换参数的次序,实现不同的操作数类型次序:
10+x; x+10;
至此,variant已经基本完成了。variant可以象内置类型那样使用了。
...全文
1249 58 打赏 收藏 转发到动态 举报
写回复
用AI写文章
58 条回复
切换为时间正序
请发表友善的回复…
发表回复
shezhang999 2008-06-10
  • 打赏
  • 举报
回复
mark
life02 2008-03-24
  • 打赏
  • 举报
回复
非常好的帖子,我定了
收藏
tmhlcwp 2007-07-09
  • 打赏
  • 举报
回复
加剧晕~。~
庄鱼 2007-06-29
  • 打赏
  • 举报
回复
to:Vitin(卫亭)
我不建议楼主作函数重载覆盖的工作,这是有原因的:因为使用C++的人本身要对自己驾驭的数据负责,所以,其会根据需要自由的选择合适的类型,对一些个例化的东西适用相应的替代方案,以达到效率与便捷的统一。
variant这问题实际上本身就存在悖论,在我一个玩Delphi的朋友就很少直接使用该类型做运算,他仅仅拿它作为一个容器来进行数据转换用,用他的话说,用这种变量运算,出了错都不知道错在哪里!举个例子:
var v1,v2,v3 :variant;
v1 := '123';
v2 := '456';
v3 := v1 + v2; ===> v3='579' or v3='123456'!!!
var i1,i2: integer;
var s1,s2:string;
i1 := v1;
i2 := v2;
v3 := i1+i2; ==>v3='579'
s1 := v1;
s2 := v2;
v3 := s1+s2; ===>v3 = '123456'
v3:=s1+v2; ==>v3 = '123456'
v3:=i1+v2; ==>v3 = '579'
用variant变量存在这么多陷阱,有必要将这种陷阱引入到C++里吗?我觉得楼主该完善并不是运算功能,二是完善其容器功能与数据源类型的识别及后续转换功能。也就是说,将类型之间的转化提供便捷与安全的有效手段。
Vitin 2007-06-29
  • 打赏
  • 举报
回复
to FreeFice(庄鱼):
你说的是有道理的。

其实隐式转换或操作符重载等等技术归根到底也只是体现了某种语义的语法形式而已。使用语言的目的是为了实现需要的语义。那么,即使使用其他的语法形式,只要实现了相同的语义,目的就达到了。这里讨论可以用某些技术直接地(隐式地)达到Delphi中variant的相同语义,说的只是一种能力而已,并不是说,在实际使用中必须这样做。这是可行性,不是必然性。
在实际使用中,达到自己期望的语义是最重要的。显式的转换,以及其他的方式,并不比隐式转换或操作符重载低一等的。之所以没有把重点放在这里,是因为LZ希望讨论的是如果使用variant(这样的混合类型),应该怎么做。而不是说variant本身有多好。我们参与讨论,也不是说喜欢使用variant,而是借这个课题将一些技术理解的更深刻、把握的更好。当然,其side effect 是更加了解variant.

至于variant中的语义冲突,我在LZ的下一篇“被误解的C++——优化variant实现 ”的回复中也提到了。当一个类被设计出来时,有些语义可以沿用一些惯例,这比较容易掌握;有些则是它所特有的(如你举的例子应该怎么办)。这里不讨论如果一些特有语义与惯例发生冲突所带来的危害(你举的例子就是一种表现),只说一点:如果存在这些特有语义,那么必须让它的使用者知道这一点,并且帮助使用者学会使用它。因此,variant的设计者会提供相关文档或教程的,当然他们也会保证自恰和行为一致性的。这就是为什么,我强调首先要了解variant的语义,然后才是选择合适的技术手段(以及相应的语法形式)来实现它。

LZ的下一篇“被误解的C++——优化variant实现 ”就是关注类型识别和转换的,当然识别和转换的目的也是为了运算。可以去那边讨论这个主题,呵呵。
snprintf 2007-06-28
  • 打赏
  • 举报
回复
mark
healer_kx 2007-06-28
  • 打赏
  • 举报
回复
以前我是一个泛型的爱好者,看到的都是类型,现在我研究Shellcode呢,看到的都是数字。。。 。。。
Vitin 2007-06-28
  • 打赏
  • 举报
回复
variant& variant::operator=(const char*);
variant& variant::operator=(double);
variant::operator double();

定义上述两个操作符重载和一个隐式转换函数,就可以支持如下的代码:
variant v1;
v1 = "123";
v1 = sqrt(v1);

此外,还可以重载sqrt函数,以支持第二个式子。
例如写一个 variant sqrt(const variant&);
或者为效率起见(避免构造一次variant),可以写两个函数
variant& variant::operator=(double);
double sqrt(const variant&);

theendname 2007-06-28
  • 打赏
  • 举报
回复
先mark
shan_ghost 2007-06-28
  • 打赏
  • 举报
回复
又见芙蓉~~~~~

不告诉你有自动类型转换了吗?

自动类型转换到int时,如果发现当前存储的是字符串,就先检查是否能转换为数字——这个简单问题,肯定不是某些人的S级智力所能理解的。
flysky_zhou 2007-06-28
  • 打赏
  • 举报
回复
mark.............

好好的一个帖子......

却有些人在这里口水........

不懂.................
dazhuaye 2007-06-28
  • 打赏
  • 举报
回复
mark
Vitin 2007-06-28
  • 打赏
  • 举报
回复
上面的一些讨论我觉得都有道理。
是的,在C++中要实现一个variant,技术上是可行的,关键是代价和收益的均衡。
使用隐式转换是危险的(要考虑的东西很多,不能遗漏),使用函数重载是繁复的(要实现的东西多,工作量大),还有一些技术则是高深的(比如LZ的下一篇“被误解的C++——优化variant实现 ”中的模板元编程)。毕竟,每一种技术在获得收益时,都是要付出代价的。
因此,如何做技术上的取舍平衡还是很重要的。如对于某些类型,做隐式转换;对于某些操作,做操作符重载;对于某些应用,提供显式的可调用函数;对于某些方面,使用更前沿的技术。如此等等。

此外,我觉得比技术更重要的是variant的语义。技术只是说C++能够实现怎样的语义。但对一个类来说,它需要怎样的语义才是最关键的。如什么时候应该把variant转换成某种类型,什么时候不应该;variant内部的类型应该以怎样的顺序和优先级做转换;有哪些例外,有哪些限制;如此等等。只有先确定了语义(或者说,根据对variant的需求设计出合适的语义),才能用相应的技术实现它。在这里,我觉得可以参考Boost、MFC等的variant实现,体会它们各自的语义;也可以参考Delphi或其他语言对variant的实现(不知道能不能获得,其他语言我不熟悉。有类库及源代码自然最好;如果是内建类型,就要看编译器的实现;得不到第一手的材料,就要研究汇编码——不过,代价很高地说),看看它们又使用了什么语义,以及在C++中怎样实现同样的语义。
其实任何语言都是一样的,她们都通过一定的语法支持一定的语义集合。从这个角度看,语言的区别只是有的语义集合大,有的语义集合小。如果对语言已经掌握了很好,那么问题的关键就是怎样把需求转化为本语言能够支持的语义了。
yuyunliuhen 2007-06-28
  • 打赏
  • 举报
回复
mark
shan_ghost 2007-06-28
  • 打赏
  • 举报
回复
这种缺德小人,妓俩不外乎:栽赃诬陷、造谣辱骂、名誉威胁、马甲满天。

我就看着你一样一样用出来。
shan_ghost 2007-06-28
  • 打赏
  • 举报
回复
回网友关于“强龙不压地头蛇”的言论:


csdn是谁的地盘?

它是我们真正学习、研究技术的程序员的地盘!

路不平,有人踩。屎当道,有人埋。

本人生平最恨这种缺德小人。既然犯我手里,咱不制住它就没完。
shan_ghost 2007-06-28
  • 打赏
  • 举报
回复
呵呵,芙蓉gay,从现在开始,我不再和你谈任何技术问题。我不能侮辱自己的智商,更不能侮辱网友的智商。

至于你这种SB,我见多了。在我已经彻底驳倒你,逼得你看着自己气大的肚皮,写出下面这首歪诗的情况下,我犯的上拿你当菜吗?

这水平,你还号称“从05年以来在CSDN上和我叫板的无一例外都倒了”?被你熏倒了吧。


轰的一声你倒了,
正如你噗的一声鼓起来,
你拍一拍爆裂的肚皮,
垂死中流露出几许无奈;

轰的一声你倒了,
正如你噗的一声鼓起来,
你喷出几个蝌蚪,
把希望寄托给蛤蟆的下一代。
  • 打赏
  • 举报
回复
散分还需要什么胆量吗???
是你自己心惊肉跳了吧。

http://community.csdn.net/Expert/TopicView3.asp?id=5626551
http://community.csdn.net/Expert/TopicView1.asp?id=5624805

还有4个被删了。

没错,是非自有公论,不过不可能是你自造的“业界公认”!
你那些笑料言论,放到哪里也只能是笑料!
你已经出了大丑,你就等着继续丢人吧。
shan_ghost 2007-06-28
  • 打赏
  • 举报
回复
绝不会像某疯狗,只敢背后咬人。
shan_ghost 2007-06-28
  • 打赏
  • 举报
回复
另外,关于你的任何链接,我都会在csdn一一贴出。
加载更多回复(38)

64,282

社区成员

发帖
与我相关
我的任务
社区描述
C++ 语言相关问题讨论,技术干货分享,前沿动态等
c++ 技术论坛(原bbs)
社区管理员
  • C++ 语言社区
  • encoderlee
  • paschen
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
  1. 请不要发布与C++技术无关的贴子
  2. 请不要发布与技术无关的招聘、广告的帖子
  3. 请尽可能的描述清楚你的问题,如果涉及到代码请尽可能的格式化一下

试试用AI创作助手写篇文章吧