被误解的C++——C(++)

longshanks 2007-06-18 08:57:15
C(++)
今天所要探讨的,是C++中最大的误解:C++的使用。对C++使用的误解是大量其他误解的根源。为此,我找了个真实的案例,来展示C++的两种不同使用方法。相信诸位看完之后,便会明白这层误解的巨大影响。
我差不多可以算是一个C出身的C++程序员。尽管前后也就用了一年多点的C,然后就转向了C++。但是,在长达9年的时间里,我实际上还是在用C编程,在C++里用C编程。
大约一年多以前,一个偶然的机会,我拜读了D&E(以及其他C++大师的著作)。然后对自己的C++使用进行了反思。此后,我便认真地按照大师们的教诲,编写我的代码。正巧,我过去编写的一个软件需要升级重写。我也正好利用这个机会全面地实践一下真正的C++编程。我节选了其中的一个组件,作为案例,展示不同时代的C++。
这个组件的功能很简单,将一组二进制序列转换成N进制的字符串,以及反过来转换。当时的需求是转换成32进制,这样26个字母加上10个数字,扣掉容易混淆的字符,正好可以有32个。但是为了将来的扩展,我还是把它做成了任意进制和字符集的转换,(只要有足够的字符可用)。
我选择了一个比较通用的算法:把二进制序列看作是一个“超级整数”,用进制数N反复除这个“超级整数”,直到所得的商为0。每次除得到的余数,构成一个序列,便是编码的序列,但在使用前还需要将其反序。然后再用序列的每一个数字在字符表中索引,获得最后的编码字符串。解码的操作是编码的逆操作,只需把这个过程反过来,除法变乘法即可。为了节省篇幅,我只考察编码,毕竟我们的目的还是比较C++不同的用法,而不是研究编码问题。
让我们先完整地看我早先的做法,然后再看后来的做法。
首先,我编写了一个函数,LongDiv(),用来执行“超级整数”的除法:
unsigned char LongDiv(unsigned char* dividend, int n, unsigned char divisor, unsigned char* result, unsigned char *eigenvalue=NULL)
{
if(dividend==NULL || result==NULL || n < 0)
return -1;

long op=0;
unsigned char m=0;
unsigned char e=0;

for(int i=(n-1); i>=0; i--)
{
op+=dividend[i];
result[i]=op/divisor;
m=op%divisor;
op=m;
op<<=(szT1*8);
e|=result[i];
}

if(eigenvalue!=NULL)
*eigenvalue=e;

return m;
}
被除数是一个序列,用指针dividend传递,序列的个数用n传递,divisor是除数,结果放在指针result所指定的内存中,eigenvalue是指向特征值的指针,函数返回余数。
我之所以没有重载操作符/是为了能够在除的同时,获得余数和一个特征值。这个特征值表明了在除的过程中,二进制序列的每个数值的商是否都为0。如果都为0,整个序列的商就为0,编码也就完成了。
然后,我编写了编码的核心函数:
int EncodeAll(int N, unsigned char* data, int ndata, unsigned char* sqs)
{
if(data==NULL || ndata<0)
return -1;

DataT mx=-1;
double m=mx; m++;
double dres=(ndata*::log(m))/::log((double)N);
int nres=dres, ne=log(m)/::log((double)N);
nres+=((dres-nres)==0) ? 0 : 1;

if(sqs==NULL)
return nres;

DataT e=1;
int i=0;

for(; e!=0;)
{
sqs[i]=::LongDiv(data, ndata, N, data, &e);
i++;
}

Reverse(sqs, i, sqs);

return i;
}
参数N表示进制数,data是指向二进制序列的指针,ndata是二进制序列的长度,sqs是指向用于存放结果的数据缓存区的指针。
这个函数的核心算法非常简单,只有6行,从for开始,到Reverse()调用。前面那一大堆恼人的东西,纯粹是一组“安全围栏”。这里需要仔细分析,让我们一段段来。
if(data==NULL || ndata<0)
return -1;
这是传入参数的有效性检验,为了防止无效的引用。但是这种检验并不总是有效,因为传输的参数完全可以是一个非NULL值,但却是一个无效的引用(象Win32中著名的0xcccccccc)。这种检验的有效性依赖于整体编程中的一个习惯,就是将所有的指针,在声明后立刻赋NULL。我通常都这样做,安全第一。
DataT mx=-1;
double m=mx; m++;
double dres=(ndata*::log(m))/::log((double)N);
int nres=dres, ne=log(m)/::log((double)N);
nres+=((dres-nres)==0) ? 0 : 1;
这是我最痛恨的一段代码,目的是为了计算出一个长度为ndata的序列,编码后需要多长的数据缓冲区。(这段代码让我把中学代数的指数部分好好地复习了一遍)。
if(sqs==NULL)
return nres;
如果sqs指针的值为NULL(空指针),那么返回所需的数据缓冲区的长度,不执行具体操作。否则,执行数据编码操作。这个手法是从一些C API库中学来的,为了让算法能够适应变长的数组。过会儿我们会看到这个手法是如何工作的。
下一步,需要将编码后的数字序列转换成字符。所以,我作了一个字符表:
class CCharMap
{
public:
CCharMap(const char *CharSet)
{
if(CharSet==NULL)
{
m_nDtoC=0;
m_aDtoC=NULL;
}
else
{
m_nDtoC=::strlen(CharSet);
m_aDtoC=new char[m_nDtoC+1];
::memcpy(m_aDtoC, CharSet, m_nDtoC);
m_aDtoC[m_nDtoC]='\0';
}
::memset(m_aCtoD, 0, ASSICSETSIZE*sizeof(int));
InitCharMap();
}

~CCharMap()
{
delete m_aDtoC;
}

protected:
char *m_aDtoC;
int m_nDtoC;
int m_aCtoD[ASSICSETSIZE];

protected:
void InitCharMap()
{
for(int i=0; i<m_nDtoC; i++)
{
m_aCtoD[m_aDtoC[i]]=i;
}
}

public:
char Data2Char(int data)
{
if(data>m_nDtoC || data<0 || m_aDtoC==0)
return '\0';

return m_aDtoC[data];
}

int Char2Data(char chr)
{
return m_aCtoD[chr];
}
};
从数值到字符的转换,我利用了一个动态分配的数组m_aDtoC(严格地讲,它不是数组)。而相反的转换,我使用一个真正的C数组m_aCtoD[],它的大小是256(#define ASSICSETSIZE 256)。所以,我的这个字符表只能应付ASSIC字符集,无法扩展。在我当时的需求情况下,这足够了。
最后,我用一个类封装了整个编码器:
class CEncoder : public CCharMap
{
public:
CEncoder(const char* CharSet)
: CCharMap(CharSet)
{}

public:
int Data2Text(unsigned char* data, int size, char* text)
{
if(!IsReady())
return -1;

if(text==NULL)
return ::EncodeAll(data, size, NULL);

unsigned char *pdata=new unsigned char[size];

::memcpy(pdata, data, size*sizeof(unsigned char));

int n=::EncodeAll(pdata, size, text);

delete pdata;

for(int i=0; i<n; i++)
{
text[i]=Data2Char(text[i]);
}

return n;
}
int Text2Data(char* text, int size, unsigned char* data)
{…}
};
Data2Text()成员负责编码,Text2Data()负责解码。Data2Text()的参数data是指向二进制序列的指针,size是二进制序列的大小,text是指向存放编码字符串的内存缓冲区的指针。这个函数做了这么些事情:
if(text==NULL)
return ::EncodeAll(data, size, NULL);
当text为NULL时,调用::EncodeAll()。注意,当这样调用时,EncodeAll()将会返回转换后编码序列的长度,也就是编码字符串的长度。
unsigned char *pdata=new unsigned char[size];

::memcpy(pdata, data, size*sizeof(unsigned char));

int n=::EncodeAll(pdata, size, text);

delete pdata;
创建动态数组,制作二进制序列的副本,因为EncodeAll()算法会修改这个序列。最后,释放数组。
for(int i=0; i<n; i++)
{
text[i]=Data2Char(text[i]);
}
把编码序列中的数值转换成字符。
至此,编码器基本上完成了。当然,在实际的代码中,还需要一些辅助的内容,因为和主题无关,我们就不管它们了。
使用起来会是这样的:
CEncoder e(“023456789ABCDEFHIJKLNPQRSTUVWXYZ”);
unsigned char *data=NULL;
int ndata;
//初始化data序列和ndata
int n=e.Data2Text(data, ndata, NULL);
char* text=char[n];
e.Data2Text(data, ndata, text);
delete text;
在这个早期版本的编码器中,我秉承了一贯的C风格。我们可以看到大量的原生指针、不安全的内存分配,以及烦人的动态内存控制。编码器的实现和使用代码,都是这样。
...全文
1177 39 打赏 收藏 转发到动态 举报
写回复
用AI写文章
39 条回复
切换为时间正序
请发表友善的回复…
发表回复
relishou 2007-09-16
  • 打赏
  • 举报
回复
学而时习之,不亦乐乎?楼主有心。
wpalhm 2007-09-15
  • 打赏
  • 举报
回复
学习,但不知道怎么才能学到真正的c++,而不是c(++)
littlebad_boy 2007-09-15
  • 打赏
  • 举报
回复
mark.
longshanks 2007-09-15
  • 打赏
  • 举报
回复
wpalhm():
学习,但不知道怎么才能学到真正的c++,而不是c(++)
===============================
看书啊。当然要看好书,比如C++ Primmer;Meyers和Sutter的系列书。
wishfly 2007-09-13
  • 打赏
  • 举报
回复
up
bbsdkjdx 2007-09-10
  • 打赏
  • 举报
回复
VC6的母语是MFC而不是C++,是吧?
yuyunliuhen 2007-09-10
  • 打赏
  • 举报
回复
mark
xmoon1983 2007-09-10
  • 打赏
  • 举报
回复
开始学习C++
浮生若梦 2007-09-10
  • 打赏
  • 举报
回复
我始终在C++的门外徘徊,遇到要做一些见面的程序,Delphi,C#之类的很容易就搞定了,而我们平时接触到的东西基本上都需要界面,后面的逻辑也不是很多,所以,唉,用C++的地方也不多,什么系统级啊我们都见不到,也做不了,所以,C++再好,我也没那个命用啊
wanglovec 2007-06-21
  • 打赏
  • 举报
回复
不能说 你原来的不是C++ 设计,现在就是C++设计.

只能说你原来的不是好的设计 ,现在的设计比原来好!
yangb2014# 2007-06-21
  • 打赏
  • 举报
回复
我一直把C和C++当作两种语言,
bbin109 2007-06-21
  • 打赏
  • 举报
回复
厉害厉害。虽然没有看懂
dychenyi 2007-06-21
  • 打赏
  • 举报
回复
不好意思 没看懂
jerry_haotian 2007-06-20
  • 打赏
  • 举报
回复
引用wihi的话

算法没细看,也不想看。大概了解你的意思是用了 vector 这些,有了不定长参数的便利,和其它一些自动内存管理方面的东东。。。。

但看不出来你在 C --> C(++) --> C++ 方面说明了什么,说服了什么

我也是这种感觉
BlueTrees 2007-06-20
  • 打赏
  • 举报
回复
没感觉啊。

不用基础库难道就不是C++了?
longshanks 2007-06-20
  • 打赏
  • 举报
回复
使用vector只是一个表象。我的这个文章写的不太好,太散了。
我想说明的是,当一个程序员使用C(++)编程时,无法从C++中得到好处,更多的是遇到大量C++的不足。我们只有用C++的方式编程,才能趋利避害,充分发挥C++的优势。
或许很多人对我老版的代码中那些“安全围栏”和“动态资源管理”操作没有感觉。但是,我却是深受其害。毕竟这里展示的仅仅是一个小模块。我编写的却是一个完整的软件。管理这些恼人的代码是一件恐怖的经历。特别是代码长度过2万以后。(这个软件没有那么大,另一个却远超过这个数,让我着实痛苦了十几个月)。
所以,我总是在不遗余力地向别人介绍C++更好的用法。

附带说明一下:我在案例中使用vector是为了方便,对于一个真正的编码器而言,vector并非最好的选择。因为,变长的vector在特定情况下无法还原编码。比较好的是采用定长的vector,比如boost的array。
stoneyrh 2007-06-20
  • 打赏
  • 举报
回复
很多人误用是事实,但是要转换过来也并非易事
楼主在使用过程中感受到这一点,说明已经有了可喜的进步
wihi 2007-06-19
  • 打赏
  • 举报
回复
算法没细看,也不想看。大概了解你的意思是用了 vector 这些,有了不定长参数的便利,和其它一些自动内存管理方面的东东。。。。

但看不出来你在 C --> C(++) --> C++ 方面说明了什么,说服了什么
sjjf 2007-06-19
  • 打赏
  • 举报
回复
mark
Minkey 2007-06-19
  • 打赏
  • 举报
回复
学习,呵呵~~
加载更多回复(19)
第1篇 理解程序设计 第1章 基础知识 1.1 什么是编程 1.1.1 计算机如何工作 1.1.2 内存中的程序是哪里来的 1.1.3 可执行文件的制作 1.1.4 C语言的演化 1.2 怎样用C语言编程 1.2.1 学习C语言编程都需要什么 1.2.2 最简单的C语言程序的基本结构 1.2.3 Dev C++ 1.3 printf()函数初步 1.3.1 简单的一般用法 1.3.2 特殊的字符 1.4 C语言的“字母”和“单词” 1.4.1 C语言的字母 1.4 12C语言的“词” 小结 概念与术语 风格与习惯 常见错误 牛角尖 练习与自测 第2章 数据类型 2.1 什么是数据类型 2.1.1 “三个世界”理论 2.1.2 问题世界:“万物皆数” 2.1.3 代码世界:书写规则及含义 2.1.4 机器世界里的“机器数” 2.1.5 输出问题 2.1.6 计算2的1到10次幂 2.1.7 代码质量的改进 2.2 让程序记住计算结果——变量 2.2.1 计算机的记忆功能 2.2.2 在代码中实现“记忆 2.3 int类型——总结与补充 2.3.1 计算机表示负整数的几种方法 2.3.2 计算机码制和C语言的关系 2.3.3 暂时不必关心的一些细节 2.3.4 int类型值的范围 2.3.5 int类型常量在代码中的其他写法 2.3.6 Dev C++中int类型的机器数 2.4 对数据类型的进一步讨论 2.4.1 int数据类型的运算 2.4.2 数学公式与数据类型 2.4.3 数据类型——代码与编译器的约定 2.5 莫名其妙的“整型 2.5.1 unsignedint类型 2.5.2 long、short关键字描述的整数类型 2.5.3 没有常量的char类型 2.5.4 其他 2.6 浮点类型 2.6.1 double类型常量的代码书写规则 2.6.2 浮点类型数据存储模型 2.6.3 浮点类型的一些特性 2.6.4 浮点类型的运算 2.6.5 浮点类型的输出及其他 2.7 数据类型与算法 2.7.1 错误的数据类型 217.2 所谓算法 2.7.3 一个技巧 2.7.4 更高效率的写法 2.8 算法的特性 小结 概念与术语 风格与习惯 常见错误 牛角尖 练习与自测 第3章 运算符、表达式及语句 3.1 C的“动词”及“动词”的“宾语” 3.2 表达式——C语言的“词组 3.2.1 初等表达式 3.2.2 被误解的“() 3.2.3 带运算符的表达式 3.2.4 不像表达式的表达式 3.2.5 表达式:专业与副业 3.2.6 赋值运算符左侧的标识符称为左值 3.2.7 函数调用是表达式不是语句 3.3 谁是谁的谁 3.3.1 流行的谬误:优先级决定运算次序 3.3.2 “左结合性”是运算对象先与左面的运算符相结合吗 3.3.3 运算符、表达式小结 3.4 右值的类型转换 3.4.1 明确写出的显式转换——cast运算 3.4.2 cast运算的规则 3.4.3 赋值中的转换 3.4.4 1+1.0=? 3.4.5 算术转换:早已废弃的规则和依然有效的规则 3.5 语句的概念 3.5.1 关于语句的闲话 3.5.2 空语句有两种 3.5.3 表达式语句 3.5.4 顺序结构 3.5.5 复合语句 3.6 例题 3.6.1 简单的类型转换 3.6.2 最基础的算法——交换变量的值 3.6.3 编程不是列公式 3.7 算法和数据结构初窥 3.8 在程序运行时提供数据 小结 概念与术语 风格与习惯 常见错误 牛角尖 练习与自测 第4章 选择语句 4.1 关系运算 4.1.1 “<”的数学含义及代码含义 4.1.2 4种关系运算符 4.1.3 常见误区及与常识不符的结果 4.2 if语句 4.2.1 语法格式及含义 4.2.2 例题 4.2.3 ()内的表达式 4.2.4 ()后面的语句 4.3 判等运算 4.4 表达复杂的条件 4.5 if else语句 4.6 鸡肋——Bool类型(C99) 4.7 判断三角形种类 4.8 显得很有学问的运算符 4.9 大师如是说goto 4.10 给程序更多选项——Switch语句 4.10.1 switch语句的一种应用形式 4.10.2 switch语句中的break语句 4.11 程序开发的过程 小结 概念与术语 风格与习惯 常见错误 牛角尖 练习与自测 第5章 从循环到穷举 5.1 造句:当就 5.1.1 语法要素 5.1.2 猴子吃桃问题更简洁的写法 …… 第2篇 结构化程序设计与简单的数据结构 第6章 最复杂的去处符——“()” 第7章 作为类型说明符和去处符的“[]” 第8章 结构体、共用体与位运算 第9章 指针 第10章 字符串、字符数组及指向字符的指针 第3篇 复杂的数据结构、算法及其他话题 第11章 复杂的数据类型与算法 第12章 程序的输入与输出 第13章 程序组织与编译预处理 第14章 标准库简介 附录 参考文献
► 数据结构--序言数据结构--序言 在可视化化程序设计的今天,借助于集成开发环境可以很快地生成程序,程序设计不再是计算机专业人员的专利。很多人认为,只要掌握几种开发工具就可以成为编程高手,其实,这是一种误解。要想成为一个专业的开发人员,至少需要以下三个条件: 能够熟练地选择和设计各种数据结构和算法。 至少要能够熟练地掌握一门程序设计语言。 熟知所涉及的相关应用领域的知识。 其中,后两个条件比较容易实现,而第一个条件则需要花相当的时间和精力才能够达到,它是区分一个程序设计人员水平高低的一个重要标志,数据结构贯穿程序设计的始终,缺乏数据结构和算法的深厚功底,很难设计出高水平的具有专业水准的应用程序。曾经有一本经典计算机专业书籍叫做《数据结构+算法=程序》,也说明了数据结构和算法的重要性。 《数据结构》是计算机科学与工程的基础研究之一,掌握该领域的知识对于我们进一步进行高效率的计算机程序开发非常重要。无论在中国还是在美国,《数据结构》一直是大学的计算机专业重要的专业基础课。例如,在著名的美国的加州大学伯克利分校(著名的BSD Unix的发源地,很多Unix操作系统由它派生而来或带有它的痕迹——例如FreeBSD、Sun公司的Solaris、IBM的AIX),就用一个学期开设《数据结构和算法》课程(在这之前,用一个学期开设《C++程序设计》课程)。 现行的中学相关的计算机教程或者是关于怎样使用Windows操作系统及其工具、或者是有关办公软件的使用,或者是打字教程。计算机对他们始终有一种神秘感,也许是理论导向吧,因为不可能每个人将来都成为计算机专业人员。 作为一个中学生,在学完C/C++以后,关键的问题是怎样熟练地应用和巩固。本网站希望能够结合《数据结构》和相关的数、理、化知识来巩固C/C++。其实《数据结构》并不难。可以说,数据结构贯穿于我们的数学课程之中,只是思考问题方法的不同。在大学的《数据结构》教程中,很多生僻的词语、晦涩难懂的语句,连大学生就感到望而生畏。本网站将集合小学和中学的数学、物理、化学教材,深入浅出地讲解这门课程。希望不但能够对学习电脑有所帮助,更希望能够对数理化的学习起到一个促进作用。 在学习《数据结构》之前,要求学生有C/C++基础。可以这样说,C/C++是其他程序设计语言的基础。掌握了C/C++,学习其他语言就会易如反掌。例如,微软的MFC类库基于C++;ATL基于C++中的模板类;Java语言基于C++思想,其编程风格与C++差别很小;C++ Builder又是基于C++;Delphi中的有关对象的概念与C++中的对象几乎完全一致。C++相比其他语言具有与计算机硬件集合紧密、代码效率高,这是Java语言和其他高级语言所无法比拟的。这样,C/C++对于学习计算机系统结构有很大的好处。

64,281

社区成员

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

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