2.9 技术解惑
2.9.1 C++标识符的长度
在几十年前,ANSI C标准规定名字不准超过6个字符,现在的C++/C规则不再有此限制。一般来说,长名字能更好地表达含义,所以函数名、变量名、类名长达十几个字符不足为怪。那么名字是否越长越好?不见得! 例如,变量名maxval maxValueUntilOverflow好用,单字符的名字也有用,常见的有i、j、k、m、n、x、y、z等,它们通常可用作函数内的局部变量。
2.9.2 字符和字符串的区别
字符和字符串的差异很小,因为字符串也是由一个个字符组合而成的,两者的主要区别如下。
字符使用单引号标注,而字符串使用双引号标注。
字符串需要使用转义字符'0'来说明结束位置,而字符则不存在这个问题。
字符是一个元素,只能存放单个字符。而字符串则是字符的集合,可以存放多个字符。
相同内容的字符数组和字符串都是字符的集合,但是字符数组比字符串数组少了一个转义字符'0'。
2.9.3 C++字符串和C字符串的转换
C ++提供的由C++字符串得到对应C_string的方法使用的是data()、c_str()和copy()。其中,data()以字符数组的形式返回字符串内容,但并不添加'0',c_str()返回一个以'0'结尾的字符数组,而copy()则把字符串的内容复制或写到既有的c_string或字符数组。C++字符串并不以'0'结尾。笔者建议在程序中尽量使用C++字符串,一般情况下不选用c_string。
2.9.4 C++字符串和字符串结束标志
为了测定字符串的实际长度,C++规定了一个“字符串结束标志”,以字符′0′代表。在上面的数组中,第11个字符为′0′,表明字符串的有效字符为其前面的10个字符。也就是说,遇到字符′0′就表示字符串到此结束,由它前面的字符组成字符串。
对一个字符串常量,系统会自动在所有字符的后面加一个'0'作为结束符。例如,字符串“I am happy”共有10个字符,但是在内存中共占11个字节,最后一个字节'0'是由系统自动加上的。
在程序中往往依靠检测'0'的位置来判定字符串是否结束,而不是根据数组的长度来决定字符串的长度。当然,在定义字符数组时应估计实际字符串的长度,应保证数组长度始终大于字符串的实际长度。如果在一个字符数组中先后存放多个不同长度的字符串,则应使数组长度大于最长的字符串的长度。
2.9.5 C++中的面向对象、C中的面向过程的含义
面向对象指的是把属性和方法封装成类,实例化对象后,要完成某个操作时,直接调用类里面相应的方法。面向过程则不进行封装,要完成什么功能需要详细地把算法写出来。举个例子来说,要完成买东西这个任务,面向对象的实现方法就是,先对手下的人办个培训,教他们怎么去买(相当于定义类的属性和方法),以后要让他们买东西,只要喊“张三(或者李四,相当于实例化对象),你用上次我教你的方法去买个东西。”这样就可以了;而面向过程的方法则不进行培训,每次要去买东西,都找张三过来,再教他怎么去买,但是下次再叫他去买,又要重新教一次。
2.9.6 面向对象和面向过程的区别
C语言是一门面向过程的语言,C++是一门面向对象的语言。究竟面向对象和面向过程有什么区别呢?面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了。面向对象是把构成问题的事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描述某个事物在整个解决问题步骤中的行为。
例如,要开发一个五子棋游戏,使用面向过程的设计思路的步骤如下。
(1)开始游戏,(2)黑子先走,(3)绘制画面,(4)判断输赢,(5)轮到白子,(6)绘制画面,(7)判断输赢,(8)返回步骤(2),(9)输出最后的结果。
把上面每个步骤分别用函数来实现,问题就解决了。而面向对象的设计则是从另外的思路来解决问题的,开发整个五子棋游戏的基本过程如下。
(1)设计黑白双方,这两方的行为是一模一样的。
(2)设计棋盘系统,负责绘制画面。
(3)开发规则系统,负责判定,如犯规、输赢等。
上述3个过程分别代表3个对象,其中第一类对象(玩家对象)负责接收用户输入,并告知第二类对象(棋盘对象)棋子布局的变化,棋盘对象接收到棋子的变化就要负责在屏幕上显示出这种变化,同时利用第三类对象(规则系统)来对棋局进行判定。
由此可以明显地看出,面向对象是以功能来划分问题的,而不是步骤。同样是绘制棋局,这样的行为在面向过程的设计中分散在多个步骤中,很可能出现不同的绘制版本,因为通常设计人员会考虑到实际情况进行各种各样的简化。而面向对象的设计中,绘图只可能在棋盘对象中出现,从而保证了绘图的统一。
功能上的统一保证了面向对象设计的可扩展性,比如要加入“悔棋”这一功能,如果要改动面向过程的设计,那么从输入到判断到显示这一连串的步骤都要改动,甚至步骤之间的顺序都要进行大规模调整。如果是面向对象的话,只用改动棋盘对象就行了,棋盘系统保存了黑白双方的棋谱,简单回溯就可以了,而显示和规则判断则不用顾及,同时整个对象功能的调用顺序都没有变化,改动只是局部的。
再如,要把这个五子棋游戏改为围棋游戏,如果使用的是面向过程设计,那么五子棋的规则就分布在程序的每一个角落,要改动还不如重写。但是如果一开始就使用了面向对象的设计,那么只用改动规则对象就可以了,五子棋和围棋的区别主要就是规则,而下棋的大致步骤从面向对象的角度来看没有任何变化。
当然,要达到改动只是局部的效果需要设计人员有足够的经验,使用对象不能保证程序就是面向对象的,初学者或者蹩脚的程序员很可能以面向对象之虚而行面向过程之实,这样设计出来的所谓面向对象的程序很难有良好的可移植性和可扩展性。
2.9.7 C++中常量的命名
因为常量属于标识符,所以也需要遵循C++标识符的命名规范,也和变量的命名规范类似。另外,C++常量还要遵循如下3点规范。
用#define定义的常量最好大写且以下划线开始,如_PI和_MAX。
如果用#define定义的常量用于代替一个常数,则常量名和其常数符号要对应,如圆周率就用PI表示。
用const声明的常量完全遵循C++变量的命名规范。
2.9.8 在C++程序中如何定义常量
在C++程序中,既可以用const定义常量,也可以用#define定义常量,前者比后者有如下4个优势。
(1)const常量有数据类型,而宏常量没有数据类型。编译器可以对const进行类型安全检查,而对后者只进行字符替换,没有类型安全检查,并且在字符替换中可能会产生意料不到的错误(边际效应)。
(2)有些集成化的调试工具可以对const常量进行调试,但是不能对宏常量进行调试。在C++程序中只使用const常量而不适用宏常量,即const常量完全取代宏常量。
(3)编译器处理方式不同。define宏是在预处理阶段展开;const常量在编译运行阶段使用。
(4)存储方式不同。define宏仅仅是展开,有多少地方使用,就展开多少次,不会分配内存;const常量会在内存中分配(可以是堆中也可以是栈中)。
2.9.9 使用关键字const注意事项
在C++程序中,const是一个很重要的关键字,能够对常量施加一种约束。有约束其实不是件坏事情,无穷的权利意味着无穷的灾难。应用了const之后,就不可以改变变量的数值了,要是一不小心改变了编译器就会报错,你就容易找到错误的地方。不要害怕编译器报错,正如不要害怕朋友指出你的缺点一样,编译器是程序员的朋友,编译时期找到的错误越多,隐藏着的错误就会越少。所以,只要你觉得有不变的地方,就用const修饰,用得越多越好。比如想求圆的周长,需要用到PI,PI不会变的,就加const,const double PI=3.1415926;再如,需要在函数中传引用,只读,不会变的,前面加const;比如函数有个返回值,返回值是个引用,只读,不会变的,前面加const;比如类中有个private数据,外界要以函数方式读取,不会变的,加const;这个时候就是加在函数定义末尾,加在末尾只不过是个语法问题。其实语法问题不用太过注重,语法只不过是末节,记不住了,翻翻书就可以了,接触多了,自然记得,主要是一些概念难以理解。想一下,const加在前面修饰函数返回值,这时候const不放在末尾就没有什么地方放了。
2.9.10 关于全局变量的初始化,C语言和C++是否有区别
在C语言中,只能用常数对全局变量进行初始化,否则编译器会报错。在C++中,如果在一个文件中定义了:
int a = 5;
要在另一个文件中定义下面的b:
int b = a;
前面必须对a进行声明:
`
extern int a;`
否则编译不通过。即使是这样,int b = a;这句话也是分两步进行的:在编译阶段,编译器把b当成未初始化数据而将它初始化为0;在执行阶段,在main被执行前有一个全局对象的构造过程,int b = a;被当成int型对象b的副本初始化构造来执行。
其实在C++中,全局对象、变量的初始化是独立的,如果不是像:
`
int a = 5;`
这样的已初始化数据,那么就是像b这样的未初始化数据。而C++中全局对象、变量的构造函数调用顺序是跟声明有一定关系的,即在同一个文件中先声明的先调用。对于不同文件中的全局对象、变量,它们的构造函数调用顺序是未定义的,取决于具体的编译器。
2.9.11 C/C++变量在内存中的分布
变量在内存地址的分布格式为:
`
堆-栈-代码区-全局静态-常量数据`
同一区域的各变量按声明的顺序在内存中依次由低到高分配空间,只有未赋值的全局变量是个例外。全局变量和静态变量如果不赋值,默认为0。栈中的变量如果不赋值,则是一个随机的数据。编译器会认为全局变量和静态变量是等同的,已初始化的全局变量和静态变量分配在一起,未初始化的全局变量和静态变量分配在另一起。
2.9.12 静态变量的初始化顺序
静态变量进行初始化顺序是基类的静态变量先初始化,然后是它的派生类。直到所有的静态变量都被初始化。这里需要注意全局变量和静态变量的初始化是不分次序的。这也不难理解,其实静态变量和全局变量都被放在公共内存区。可以把静态变量理解为带有“作用域”的全局变量。在一切初始化工作结束后,main函数会被调用,如果某个类的构造函数被执行,那么会先初始化基类的成员变量。要注意的是,成员变量的初始化次序只与定义成员变量的顺序有关,与构造函数中初始化列表的顺序无关。因为成员变量的初始化次序是根据变量在内存中次序有关,而内存中的排列顺序早在编译期就根据变量的定义次序决定了。
本文仅用于学习和交流目的,不代表异步社区观点。非商业转载请注明作译者、出处,并保留本文的原始链接。