C++ Primer 第二章 变量和基本类型
2.1 基本内置类型
C++定义了一组表示整数、浮点数、单个字符和布尔值的算术类型(arithmetic type),此外还定义了Void类型。
算术类型的存储空间大小(指用了表示该类型的二进制位数)依机器而定,C++标准规定了每个算术类型的最小存储空间。实际上,大部分编译器都使用了更大的存储空间。
表2-1 C++:算术类型 |
|||||
类型 |
含义 |
最小存储空间 |
备注 |
||
整型 |
布尔值 |
bool |
布尔型 |
-------- |
|
字符型 |
char |
字符型 |
8位 |
通常是单个机器字节(Byte) |
|
wchar_t |
宽字符型 |
16位 |
用于扩展字符集,如汉字 |
||
整数 |
short |
短整型 |
16位 |
||
int |
整形 |
16位 |
|||
long |
长整形 |
32位 |
|||
浮点型 |
float |
单精度浮点型 |
6位有效数字 |
||
Double |
双精度浮点型 |
10位有效数字 |
|||
Long double |
扩展精度浮点型 |
10位有效数字 |
基本内置类型是C++“自带”的类型,区别于标准库定义的类型。使用时不需要应用标准库就可以使用。
2.2.1 整型
表示整数、字符、布尔值的算术类型合称为整形。
1. 带符号和无符号类型
除bool类型外,其他类型可为带符号的(signed)或无符号的(unsigned),无符号的取值范围不能为负,有符号取值可以有正有负。
整型int、short、long默认为带符号型。若得无符号型必须指定类型为unsigned。
例如:unsigned long,unsigned int (可缩写为unsigned)。
2. 整型的赋值
若经一个超出其取值范围的值赋给一个指定类型的对象时,结果如何?
1) 对于unsigned类型,编译器必须调整越界值使其满足要求。
编译器将该值对unsigned类型的可能取值数码求模,然后取所得值。
2) 对于unsigned类型,负数总是超出其取值范围。
C++中,
2.1.2 浮点型
就是带小数的数,包括float , double , long double他们之间的区别是取值范围和精度,可以根据你的需要选择合适的类型。
建议:使用内置算术类型
1)char , wchar_t 两个类型虽然都是字符型但区别比较大:
在宽度上来说,一个是1byte,一个是2byte(在linux上实际是4byte)在编码上来说 wchar_t表示unicode编码方式,以上不同说明 wchar_t 可以包含更多内容比如中文,日文等等。
2)当执行整型算术运算时,很少使用short类型,因其可能会隐含赋值越界的错误。
3)char虽是整型,但通常用来存储字符而不是运算。
4)使用浮点数时,建议使用double类型,因其基本上不会有错,在float类型中隐式的精度损失是不能忽视的,而双精度计算的代价相对于单精度可以忽略。
2.2 字面值常量
即常量。只有内置类型存在字面值,无类类型的字面值,故也无任何标准库类型的字面值。
1、整形字面值规则
例:
20 //decimal 十进制
024 //octal 八进制 以零(0)开头
0x14 //hexadecimal 十六进制 以0x或0X开头
通过在数值后面加L或l指定常量为long类型。
通过在数值后面加U或u定义unsigned类型。
没有short类型的字面值常量。
2、浮点型字面值规则
默认的浮点字面值常量为double类型。在数值后面加F或f表示单精度。同时加L或l表示扩展精度。
3、布尔字面值和字符字面值
单词true和false是布尔型的字面值:如bool test = false;
可打印的字符字面值常用单引号定义:如’a’ ‘2’ ‘,’
在字符字面值前加L,得到wchar_t类型的宽字符字面值:如L’a’
4、非打印字符的转义序列
换行符 |
\n |
水平制表符 |
\t |
纵向制表符 |
\v |
退格符 |
\b |
回车符 |
\r |
进纸符 |
\f |
报警符 |
\a |
反斜符 |
\\ |
疑问号 |
\? |
单引号 |
\’ |
双引号 |
\” |
可将任何字符表示为通用转义字符:\ooo,此处的ooo表示三个八进制数字。
如 \7 响铃符 \12 换行符 \0 空字符 \062 ‘2’
也可用十六进制转义字符定义:\xddd
5、字符串字面值
用双引号括起来的零个或多个字符表示。如:“name”
宽字符串字面值,如:L”a wide string literal”
6、字符串字面值的连接
std::cout<<”a multi-line ”
“string literal”
“using concatenation”
<<std::endl;
此语句将输出:a multi-line string literal using concatenation
7、多行字面值
在一行的末尾加一反斜线符号可将此行和下一行当作同一行处理。
2.3 变量
变量提供了程序可以操作的有名字的存储区。
2.3.1 什么是变量
先了解两个表达式概念:
左值:左值可以出现在赋值语句的左面或者右面,如:变量
右值:右值只能出现在赋值语句的右面而不能出现在左面,如:常量、字面值常量
2.3.3 定义对象
1、初始化
复制初始化:用符号(=);
直接初始化:把初始化式放在括号中;(语法更灵活且效率更高)。
注意:C++中理解“初始化不是赋值”。初始化:指创建变量并给它赋初始值;赋值:擦出对象的当前值并用新值代替。
2、使用多个初始化式
对于内置类型来说,复制初始化与直接初始化几乎没有区别;
int a ; // 未初始化,栈区和堆区(函数内定义或者类里面定义)都取随机值,在全局区(全局常量,静态变量)都是全零值。
int b=1 ; // 赋值初始化
int c(1),d(b + 2) ; // 直接初始化,“()”中可为常量、变量、表达式
需要特别强调的是未初始化的内置类型虽然也会有值但是其分配规则导致其值不确定性,所以我们定义内置类型时一定要初始化值而不依赖系统的内部非配规则。
对类类型,有些初始化仅能用直接初始化完成。
内置类型不能隐身初始化,类类型一定要可以隐式初始化。
string a; // 隐式初始化,初始化成了“”
string b = "name" ;
string c(b) ;
string d(10,'a') ; // string类特有的初始化方法,等同于10个字符a组成字符串并赋值初始化给变量d
类类型的初始化其实就是构造函数:
隐式初始化实际上就调用类型的默认构造函数来初始化类型对象;
直接初始化是调用类的拷贝构造函数;
复制初始化是调用赋值操作符重载和拷贝构造函数
2.3.4 变量初始化规则
2.3.5 声明和定义
c++是一门奇怪的语言,我们先看如下代码
void main()
{
std::cout << v1 << std::endl;
}
int v1 = 1;
C++是严格的顺序编译非类对象代码的,当main函数编译时要用到的变量v1还没有执行定义,c++就会抛出异常说使用了未定义的变量v1,若把int v1;语句放到函数前面就没问题。
也可以不改变现在代码顺序而使用声明解决这个编译错误,可以这样修改代码:
void main()
{
extern int v1;
std::cout << v1 << std::endl;
}
int v1;
注意红色新增代码就是一个声明。他的意思是说:HI,系统中的某个地方定义了一个叫v1的int变量,所以你可以放心的使用。
声明是告诉编译器一些信息所以不会分配内存空间也不会产生具体的数据。
声明的对象一旦被使用就一定要在某个地方定义它,否则编译器即使暂时不抛出异常,在编译完毕后发现根本没有地方找到相关定义也一样会编译失败。
以上例子只适合变量,同一个文件中常量在使用前一定要先定义,否则即使做了申明也会出错。
声明最大的好处体现在多文件的编程中,比如说我编写了一个头文件并引入到了主文件中,头文件需要使用主文件定义的变量。由于头文件一般是先于主文件编译执行的,所以就会出现未定义的编译错误,如果在头文件中先声明一下这变量再使用就没有问题了。
除了变量C++还有常量(后面会讲到),它和变量有一些不同
2.3.6 名字的作用域
虽然C++支持面向对象,但并不是完全的面向对象,它也支持面向过程。因此有一些代码是不属于任何类的,比如说main函数就不属于任何类。
针对这种情况变量根据其位置可分为两种:全局变量、局部变量。广义上说定义在类内部(包括类所属函数内部)和函数内部的变量叫局部变量,除此之外的变量就是全局变量了。
用来区分名字的不同意义的上下文称为作用域(Scope)。作用域是程序的一段区域,一个名字可以和不同作用域中的不同实体相关联。
C++中,大多数作用域是用花括号来界定的。一般来说,名字从其声明点开始直到其声明所在的作用域结束处都是可见的。
全局作用域;局部作用域;语句作用域。
C++中作用域可嵌套。
2.4 const限定符
const限定符表示定义一个常量,常量一旦定义则不可更改。不可更改的意思是既不能对常量句柄重新赋值,也不能更改常量对象的数据成员。
因为常量定义后就不能被修改,故定义时必须初始化:
conststd::string hi = “hello!”; //正确
constint i , j=0; //错误,i未初始化 constint bufsize=512;
bufsize=5; //错误,常量初始化后其值不可以修改
常量分为两类:
(1)编译时常量:定义后直接初始化的常量;
(2)运行时常量:要初始化的值必须要通过代码运行才可以确定。
constint a(1); // 编译时常量
constint b = getval(); // 运行时常量值来自一个函数的运行结果
const myclass my(1,"tom"); // 自定义类的常量定义都是运行时常量,因为需要运行类的构造函数
常量初始化之后就不可以做任何的更改操作。
Const对象默认为文件的局部变量:此变量只存在于定义的那个文件,不能被其他文件访问。
// head.h
externint a;
// usend a
externconstint b; // 常量声明语法也可写成 const extern int b
// usend b
头文件中用到两个在其他地方定义的变量,故需要申明一下再使用。
//main.cc
#include head.h
int a(1); // 这样定义的变量可以被其他相关文件(head.h)声明并使用
// const int b(2); 对于常量这样写不行是因为这样定义的变量作用范围只在本文件中,头文件虽然做了声明但无法使用这个常量,应该用下面的语法来定义
externconstint b(2); // 这样定义的常量才可以被其他相关文件(head.h)声明并使用
int main()
{
// do...
}
主文件中定义了头文件需要的对象,和变量不同,常量若在其他地方被声明或使用一定要在定义的时候加上关键字extern,因为常量默认作用范围是定义它的那个文件,变量默认是所有相关文件。
2.5 引用
引用就是对某个对象起一个别名,故作用在引用上的所有操作事实上都作用在该引用绑定的对象上。
引用最重要的作用是函数传参。
变量引用
int val = 1;
int &refval = val; // 引用了一个变量 int &refval2; // 错误,引用必须初始化 int&refval3 =10 ; // 错误,10是个字面常量,常量必须要用常量引用
refval = 2; // 等价于 val = 2
Const引用--(常量引用) constint val = 1;
constint &ref1 = val; // 引用了一个一般常量 constint &ref2 = 9; // 引用了一个字面常量 int&ref3 = val; // 错误,常量必须要使用常量引用,ref3是个变量引用 int var = 2; constint &ref2 = var // 常量引用指向了一个变量,这时候
ref2 = 3; // 不允许通过常量引用来做任何更改操作
var = 3; // 但是可以用原始变量来更改内容
总之,常量引用可以引用常量或变量,但是无论如何都不能通过引用来更改数据内容。
变量引用只能引用变量,用引用可以更改内容效果与用原始变量更改内容是一样的。
对于引用还有一点很重要:非常量引用类型必须严格匹配,常量引用可以在内置类型之间相互引用
double a = 123.4;
int &b = a; // 错误,类型不匹配
constint &c = a; // ok
// 这个操作实际等同于
int temp = a;
constint &c = temp;
如果对非const引用b不做类型匹配限制,b实际就会引用临时变量temp,对b的修改无法反应到变量a,引用失去了其意义。const引用c没有这样的问题,因为它本身不允许修改。
2.6 typedef
用来给某个类型指定一个别名,比如
typedef int whl;
whl a(1) ; // 等同于 int a(1)
typedef 数据类型/标示符 别名
使用typedef的原因:
1) 为了隐藏特定类型的实现,强调使用类型的目的;
2) 简化复制的类型定义,使其更易理解;
3) 允许一种类型用于多个目的,同时使得每次使用该类型的目的明确。
2.7 枚举
枚举是一组可选常量值,既然是一组可选值,说明包含多个常量。
枚举定义语法:enum 枚举类型名{枚举成员1,枚举成员2,…,枚举成员n}
enum val{val1 = 2, val2 = 4, val3} // 最后一个内容没有显示给值等价于 val3 = 5
enum whl{whl1,whl2,whl3} //如果不指定值,默认第一个值从0开始,下一个依次+1递增。
枚举成员是常量:每一项都是一个唯一的const类型值,上面的定义有点类似于:
const val1 = 2; const val2 = 4; const val3 = 5;
由于是const的,所以 val2 = 1 或者 val a = 2; 都不允许。
枚举项和int类型值有对应关系,但是二者只能单向转换,枚举可以自动转成int,而int却不能转成枚举
val a = val2 ; // 枚举之间赋值初始化
int b = val2 ; // 枚举转成int并初始化
val a = 2 ; // int 不能转成枚举,无法初始化
2.8 类类型
一般来说C++吧除了内置类型之外的类型都叫类类型,我们习惯上把自定义的类class称为类类型。 类一般采用先定义类并声明类的成员函数,然后在外部定义成员函数的语法形式。
这部分内容不在这里详述会在后续章节中专门说明。
2.9 编写自己的头文件
从上面的课程我们应该大概知道什么是头文件,一般来说头文件中包含声明而不是定义,但是下面两种情况比较特殊
类定义要放在头文件里。这样如果某个文件需要这个类只需要把头文件include进来即可。
运行时常量也可以在头文件定义,表达式可以在包含文件中定义。这样就能实现在在不同的包含文件得到不同的常量值。
// head.h
int getval();
const int p = getval(); // mast1.cc
#include "head.h"
int getval()
{
return 100;
} int main()
{
cout << p << endl; // 输出100
} // mast2.cc
#include "head.h"
int getval()
{
return 200;
} int main()
{
cout << p << endl; // 输出200
}
有时候某个头文件可能会被包含多次(直接包含+间接包含)。比如文件包含了头文件A,也包含了头文件B,头文件B同时也包含了A,则文件重复包含了A。这个时候如果A中有定义语句就会产生“重复定义”的编译错误,这个时候可以用#ifndef 把头文件的内容都放在#ifndef和#endif中吧。不管你的头文件会不会被多个文件引用,你都要加上这个。一般格式是这样的:
#ifndef <标识>
#define <标识>
...... 头文件代码
#endif
<标识>在理论上来说可以是*命名的,但每个头文件的这个“标识”都应该是唯一的。标识的命名规则一般是头文件名全大写,前后加下划线,并把文件名中的“.”也变成下划线,如:stdio.h
#ifndef _STDIO_H_
#define _STDIO_H_
...... 头文件代码
#endif
该预定义标示不单能防止头文件被重复包含编译,而且还可以用在不同头文件定义同名对象时出现异常的处理上。