目录
- 基本数据类型
- 类型转换
- 隐式转换
- 强制转换
- 标识符
- 转义序列
- 运算符
- 表达式
基本数据类型
每种数据类型都有对应的特定字节数,比如32位系统中int类型就是4个字节,double型就是8个字节。字节就是内存编址的最小单位,C和C++的各种数据类型的变量都至少会占用1字节的内存。具体占多少,可以使用sizeof()来确定。
在C中,基本的数据类型包括:int,long,float,char,double,void,以及它们和signed、unsigned、*、&的组合。到了C++,基本类型类增加了bool类型,同时也增加了两个符号常量TRUE和FALSE。这两个是C++的关键字。
特别要提一下,void类型的含义是“空”类型。这种类型的大小是无法确定的。很显然,也不存在void类型的变量或对象,sizeof()也不能用在void上。因为C或C++不能对大小未知的对象进行直接操作。void类型一般是用来定义函数的返回类型、参数列表(无参)和void指针。
void指针是一个通用指针,可以指向任何类型的对象。
这里要注意一点,要区分void类型指针和NULL指针之间的区别。NULL是可以付给任何类型指针的值。对于NULL来讲,C和C++还有些许不同。在C语言中,NULL的类型为void*,但在C++中,由于允许从0到任何指针类型的隐式转换,NULL就是整数0。
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
void*类型的指针一般在函数参数中传递函数与调用者之间约定好类型的对象地址。
如果指针的值等于NULL,那这个指针是一个合法的指针,但却不是有效的指针。
bool类型的变量只有两个,TRUE和FALSE。按理来说,只需要一个bit就可以表示。但是前面说了,字节是内存编址的最小单位,所以bool类型的变量占内存的大小也是1字节。看上去浪费了7bit。
标准C语言中是没有bool类型的,但某些C的库里做了映射,像这样:
typedef int BOOL;
#define TRUE 1
#define FALSE 0
所以包含了对应的库也就定义了bool类型。
在标准C语言里,int被称为默认类型,前一文章中提到的main()就是int类型。事实上,如果你在写函数的时候不明确的定义形参类型或者返回值类型,那它们就默认为int类型。然而,在C++中却是不支持默认类型的,所以不要搞混。我的建议是,不要使用默认数据类型。要明确的指出函数每个形参的类型和返回值类型。
类型转换
这是C和C++学习过程中绕不过去的知识,或者说是所有编程语言都绕不过去的东西。
从本质上说,C和C++是不会对两个类型不同的操作数进行运算的。如果类型不同,编译器就会试图进行类型转换。不管是隐式的,还是用户要求的强制的。类型转换并不是改变原来操作数的类型和值,而是生成一个新的临时变元。
隐式转换
所谓隐式转换,是编译器在背后自动进行的转换工作,写程序的人不不会感觉到。那么隐式转换是否是安全的,就是各种编译器必须考虑的问题。如果编译器认为类型转换有安全隐患,就会报错提醒。当然,如果写程序的人确实想这么做,就需要显式的进行强制转换。
为什么会有类型转换的安全性呢?。因为不同的数据类型所占的内存字节数是不一样的,那么在转换时就可能会发生内存扩张、内存截断、尾数截断、值的改变以及数据溢出等情况。
因此在编译器隐式转换遵循一个基本的原则,字节数低的类型转换为高的类型基本安全。比如,char可以转换为int,int可以转换为long,long可以转换为float,float可以转换为double。
另外,转换时总是优先转换为能够容纳得下它的最大值的、占用内存最少的类型。比如看下面两个重载函数:
void f(long l);
void f(double d);
那么在调用f(100)时,必然会调用f(long l)而不是f(double d)。
上面说的是基本类型的转换,那么派生类和基类的转换又是什么样的呢?在C++中是可以直接将派生类对象转换为基类对象的。当然,这样转换后会发生内存截断,在内存访问和转换结果方面来说都是安全的。这是因为C++有一条保证:派生类对象必须保证其基类子对象的完整性。所以下面这段代码是安全的。
class Base
{
private:
int m_a;
int m_b;
};
class Derived : Base
{
int m_c;
};
Derived objD1;
Base objB1 = objD1;
Derived *pD1 = &objD1;
Base *pB1 = pD1;
在C语言中,允许任何非void类型指针和void类型指针之间进行直接的相互转换。比如可以先把int类型转换为void类型,在把void类型转换为double类型。这在C中是允许的,也不会报错,但是这么做可能会产生不易察觉的安全问题,比如内存扩张和截断什么的。说实在的,这其实是C语言的一个缺陷。在C++中就不是这样了,C++中只可以把任何类型的指针直接指派给void指针,却不能反过来把void指针直接指派给任何非void指针,除非进行强制转换。
强制转换
使用强制转换就一定会有安全风险,导致一些不希望看到的结果。比如下面这个例子:
double d3 = 1.25e+20;
double d4 = 10.25;
int i2 = (int)d3;
int i3 = (int)d4;
从浮点数转换为整数的过程是去掉浮点数的小数部分。因此i3会得到10,但是i2呢,它会溢出。d3的值整数部分远远超出了int类型所能表示的最大值了。
指针之间的强制转换问题更多,比如int、long、float都是4字节的类型,强制转换虽然不会早证内存扩张,但如果它们之间的指针转换改变了编译器对指针指向的内存单元的解释方式,结果必然是错误的。比如:
double d5 = 1000.25;
int *pInt = (int*)&d5;
int i4 = 100;
double *pDbl = (double*)&i4;
用pInt访问d5不会报错的,但是它的值可绝对不会是1000,它是d5开头四个字节的内容被解释为int类型的值。同样,pDbl访问i4获得的值也不一定是100,因为内存被扩张了。
同样,在基类和派生类之间进行强制转换也有安全隐患。比如:
Base objB2;
Derived *pD2 = (Derived *)&objB2;
pD2能够方位的内存范围要比objB2所占用的内存要大,也就是内存扩张了。所以,这时访问pD2的m_c就有可能产生运行错误。因为pD2指向的对象里根本就没有m_c的空间。
所以在写程序的时候有这么几条还是要注意的。
- 不可以把基类对象直接转换为派生类对象,无论是直接赋值还是强制转换。
- 对于基本类型的强制转换一定要区分值的截断和内存截断是不同的概念;
- 如果坚持要使用强制转换,必须保证内存访问的安全性和转换结果的安全性;
- 如果确信需要类型转换,尽量使用显式的强制类型转换,让看你程序的人知道发生了什么事,同时也可以避免编译器隐式转换可能会带来的你不希望的结果。
还要提醒一下,尽量避免做违反编译器类型安全原则和数据保护原则的事情,比如在无符号和有符号数之间转换,或者把const对象的地址指派给非const对象指针,等等。
标识符
C++和C中的标识符是由字母、数字和下划线组成的字符序列,用来标识一个程序元素,比如变量、函数、宏、类型名等。在标准C语言里,标识符的有效长度不超过31位,超过31的编译器会不认。而在C++里,这个长度是255位。
每个标识符都有几个具体的属性:值、值类型、名字、存储类型、作用域范围、连接类型、生存期等等。比如,函数名就是一个标识符它的值就是函数体在内存中的首地址,编译时就确定,是一个常量;值的类型就是函数指针类型;存储类型默认为extern,除非把函数声明为static;作用域为文件作用域;连接类型默认为外连接,生存期为永久。
另外,请避免使用前导“_”和"__"来定义标识符,因为C语言及其实现使用这个东西定义一些内部名称或预定义的宏,容易让人误会,而如果造成命名冲突那问题就更严重了。
给标识符起名字尽量起一个有意义的名字。如果是变量,最好在变量的名字里能体现出它的值得类型。
长的标识符并不会增加可执行代码的体积,因此不要使用过于简单的名字。但也没必要使用过长的名字,用最短的名字包含最多的信息量。
转义序列
在C和C++中,有些字符是具有特殊含义的,比如%,“”,?,\等等。但有些时候,我们希望把他们当普通字符处理,比如把它们输出到控制台上。这时就要做一些特殊处理了。一般来说有两种方法:一种是直接引用ASCII码,另一种就是使用转义序列了。转义序列是由反斜杠()跟后面一个特定转义字符组成的。我把常见的转义序列做个表,供大家查阅。
转义序列 | ASCII | 说明 |
---|---|---|
\’ | 0x27 | 输出单引号字符本身 |
\” | 0x22 | 输出双引号字符 本身 |
\? | 0x3F | 输出问号字符本身 |
\\ | 0x5C | 输出反斜杠字符本身 |
\a | 0x07 | 触发扬声器发出蜂鸣声 |
\b | 0x08 | 使光标退一格 |
\f | 0x0C | 输出换页 |
\n | 0x0A | 换行 |
\r | 0x0D | 回车符 |
\t | 0x09 | 水平制表符 |
\v | 0x0B | 垂直制表符 |
\0 | 0x00 | 空字符 |
“换行”和“回车”?,这两个不一样吗。额,有点区别,首先,这两个的ASCII码不一样。“换行”符一般用于文件操作,比如把键盘输入的回车操作转换为“换行”字符来保存,而不是回车。“换行”用于输出控制,指示终端输出从新行开始,而“回车”是键盘功能,用于输入控制。因此,请记住:输出“换行”,输入“回车”。不过C语言中有些字符输入函数可以把键盘输入的“回车”字符自动转换为“换行”字符返回。比如getchar()。
另外,由%引导的字符序列,比如%d,%f,%%等,也是转义序列。
运算符
C和C++的运算符大致可以分为三种:算术运算符、关系运算符和逻辑运算符。还有一些其他的,比如函数调用、类型转换、成员选择,以及C++提供的类型识别(typeid),作用域解析(::)、动态内存分配和释放(new\delete)、类成员指针这些也叫运算符。
各种运算符和操作数就会构成表达式,那么计算机执行表达式的运算符时就会遵守优先级规则运行。不同的运算符优先级不同,优先级高的先计算,低的后计算。
几乎所有C语言或者C++语言参考书里都有运算符优先级的排列。其实我觉得,没必要死记硬背,记住几个最关键的先括号内,再括号外,先乘除后加减,先算术后逻辑、先计算后赋值,其他现查也来得及。实在把握不了,用小括号来组织是非常良好的习惯,容易理解且不易产生歧义。
“? :”是C和C++中唯一的一个三元运算符,语法是:
条件表达式?表达式1:表达式2
意思是,如果条件表达式为true,则整个表达式的值就是表达式1的值,表达式2忽略;否则就是表达式2的值,表达式1忽略。有点像if/else的结构。
稍微提一下++和–运算符。它们的前置版本和后置版本在单独使用时,效果时一样的。但是当它们在复杂表达式中时,前置版本和后置版本才具有不同效果。这个以后谈到“运算符重载”的时候,我在详细讨论吧。
再有,一定要注意一些不易分辨的运算符,比如“==”和“=”,“&&”和“&”。有过写程序做条件判断时,把“==”误写成“=”的同学请举手。编译器可不一定会自动指出这类的错误。
表达式
通俗点讲,表达式就是使用运算符和标识符按照语法规则连接起来的算式。任何表达式都是有值的。常见的表达式包括常量表达式、算术表达式、关系表达式、逻辑表达式以及复合表达式。此外还有些不常用的,比如逗号表达式、条件运算符(? :),位运算表达式等等。
注意,不要把数学中的表达式和计算机语言中的表达式混淆。比如:
if(a<b<c)
并不表示
if((a<b)&&(b<c))
而是表示令人费解的
if((a<b)<c)
常量表达式全部由常量(字面常量、符号常量、枚举常量、布尔常量等)和运算符组成。由于常量在运行时不能改变,所以常量表达式没有必要在运行时在计算,而是编译时就求值了。
那么,在编译时求值的程序元素是否需要分配运行时的存储空间呢?这就要看它是什么类型的程序元素了。比如基本类型的字面常量、枚举常量、sizeof()、常量表达式等就不需要分配存储空间,因此也没有存储类型,但是字符串常量、const常量都要分配运行时的存储空间,即有特定的存储类型。
不要使用算术表达式作为单独的语句,一般算术表达式都和赋值运算符(=)一起使用,否则没有保存计算结果却白白消耗时间去计算它实在没必要。
关系表达式,也就是由大于号、小于号这些东西构成的表达式总是返回true或false。
逻辑表达式同样也总是返回true或false。建议,在使用逻辑运算符"&&“时,尽可能把最有可能为false的子表达式放在“&&”的左边;同样,在使用“||”运算符时,尽可能把最有可能为true的子表达式放在左边。这是因为C和C++对逻辑表达式的判断采取“突然死亡法”。如果”&&"左边的计算结果为false,则整个表达式就是false,后边的子表达式没必要计算。同理,“||”左边的表达式计算结果为true,则整个表达式就是true,后面的也不计算了。这种方法可以提高程序的执行效率。
如果把简单表达式,通过算术的、关系的和逻辑的运算组合成一个表达式,就是复合表达式。其实,我不建议使用复合表达式,因为它很有可能存在隐患。想来想去,使用复合表达式的理由恐怕也就下面两条:
- 书写简洁,使用简单表达式要完成相同功能需要更多周折
- 生成个的可执行代码更加高效。
但即使这样,我这里仍建议要防止滥用复合表达式,而且,不要编写太过复杂的复合表达式。比如下面这个表达式:
i = a >= b && c < d && c + f <= g + h;
是不是看得晕头转向的。
再有,不要编写多用途的复合表达式。比如:
d = (a = b + c) + r;
上面这个表达式应该拆分成:
a = b + c;
d = a + r;