《C++ primer》chapter 2:变量和基本类型
变量和基本类型
主要介绍基本内置类型,变量以及复合类型。
1.基本内置类型
C++定义了包括算术类型(arithmetic type)和空类型(void type)两类基本数据类型。
算术类型可分为:整形(integer type,包括布尔型和字符)和浮点型。
基本算术数据类型:
类型 | 含义 | 最小尺寸 |
---|---|---|
bool |
布尔类型 | 8bits |
char |
字符 | 8bits |
wchar_t |
宽字符 | 16bits |
char16_t |
Unicode字符 | 16bits |
char32_t |
Unicode字符 | 32bits |
short |
短整型 | 16bits |
int |
整型 | 16bits (在32位机器中是32bits) |
long |
长整型 | 32bits |
long long |
长整型 | 64bits (是在C++11中新定义的) |
float |
单精度浮点数 | 6位有效数字 |
double |
双精度浮点数 | 10位有效数字 |
long double |
扩展精度浮点数 | 10位有效数字 |
带符号类型和无符号类型
除了布尔型和扩展的字符型以外,其他整型可分为带符号的(signed)和无符号的(unsigned)。
类型int, short, long和long long都是带符号的,通过在这些类型前面加unsigned, 就可以得到无符号类型。类型unsigned int可以简写为 unsigned。
字符型与前面的整型不同,分为char,signed char和unsigned char三种。特别要注意,char和signed char是不同的,字符型虽然有这三种,但表现形式只有带符号的和无符号的两种,类型char具体表现哪一种要有具体的编译器决定。
实践中如何选择类型
- 1.当明确知晓数值不可能是负数时,选用无符号类型;
- 2.使用
int
执行整数运算。一般long
的大小和int
一样,而short
常常显得太小。除非超过了int
的范围,选择long long
。 - 3.算术表达式中不要使用
char
或bool
。 - 4.浮点运算选用
double
。
类型转换
- 给无符号类型赋一个超出它表示范围的值,结果是初值对无符号类型能表示的值的总数取模后的余数。
- 给带符号类型赋一个超出它表示范围的值,结果是未定义的。此时,程序可能工作,崩溃,也可能产生垃圾数据。
切勿在表达式里混用带符号和无符号类型!
字面值常量
整型字面值可以写作十进制(20),八进制(024),十六进制(0x14)。默认情况下,十进制字面值是带符号的,八进制和十六进制字面值可能带符号也可能无符号。十进制字面值的类型是int, long和long long中能容纳下当前值的尺寸最小者,八进制和十六进制是int, unsigned int, long, unsigned long, long long, unsigned long long之中的能容纳该数值的尺寸最小者。short 类型没有对应的字面值。
浮点字面值表现为一个小数或科学计数法表示的指数如3.14,2e10, 0., .21, 3.14E0,默认浮点型字面值是double类型。
单引号括起来的一个字符为char型字面值,双引号括起来的零个或多个字符构成字符串型字面值。字符串字面值实际上是有常量字符构成的数组,编译器会在每个字符串结尾添加一个空字符‘\0'。如果两个字符串字面值位置紧邻或仅由空格缩进换行分隔,则它们实际上是一个整体。
转义序列 不可打印或有特殊含义的字符,不能被直接使用的字符,需用转义序列表示,转义序列以反斜线‘\’开始,如‘\n’换行,'\r'回车,‘\”’双引号。也可以使用泛化的转义序列,形式为\x紧跟一个或多个十六进制数(\115表示M),或者\紧跟1个,2个或3个八进制数(\x4d表示M)。
可以通过添加前缀(字符,字符串)或后缀(数值),指定字面值的类型,如L‘a’(wchar_t), u8"hi"(utf-8字符串),40ULL(unsigned long long),1E-3F(float),3.14159L(long double)。
true和false是布尔型字面值的,nullptr是指针字面值。
2. 变量
变量提供了一个具名的,可供程序操作的存储空间。变量都有数据类型,其类型决定了它在内存中的存储方式和大小,以及能参与的运算。变量的定义,由类型说明符开始,紧跟一个或多个变量名组成的列表,变量名由逗号分隔,最后以分号结束。定义时可以为变量赋初值。用于初始化变量的值可以是任意复杂的表达式。
初始化不是赋值,其含义是创建变量之初赋予其一个初始值,而赋值的意思是把对象的当前值擦除,赋予一个新值来替代。
C++11新标准新增了用花括号来初始化变量的方式,这种形式的初始化叫做列表初始化。当用于内置类型时,若列表初始化存在丢失信息风险,编译器会报错。
如果内置类型未被初始化,则它的初始值由其位置决定,定义在函数体外的变量初始化为0,定义在函数体内的变量默认未初始化。每个类各自决定其是否未经初始化就定义对象,以及如何初始化对象的方式。
变量的声明和定义 为支持分离式编译,C++把变量的声明和定义区分开来,声明使得变量为程序所知,定义负责创建与名字相关的实体。变量声明规定了变量的类型和名字,但变量定义还申请存储空间,也可能为变量赋初值。如果想声明一个变量而非定义它,在变量类型前使用extern关键字,不要显示初始化,如extern int i; 如果声明的同时赋初值,则extern关键字失效,声明变成了定义。在函数体内试图初始化一个由extern声明的变量将会引发错误。在多个文件中使用变量,就必须将变量的声明和定义分离,变量的定义只能出现在一个文件中,而其他地方要用到该变量则必须对其进行声明,不能重复定义。
作用域 C++语言的作用域以花括号分隔。
3. 复合类型
复合类型是指基于其他类型定义的类型,C++中有几种复合类型,其中最基本的两种:引用和指针。
3.1 引用
引用类型为变量起了另一个名字,通过将声明符写称&d的形式,来定义引用类型,d是声明的变量名。一般初始化变量时,初始值会被拷贝到新建的对象中,定义引用时,程序把引用和它的初始值绑定到一起,而不是将初始值拷贝给引用,一旦初始化完成,引用将和它的初始对象一直绑定在一起,因为引用无法绑定到另一个对象上,引用必须初始化,不能绑定到一个字面值或表达式的计算结果上。除了特定情况,引用的类型必须与绑定的对象严格匹配。
3.2 指针
指针(pointer)是指向另一种类型的复合类型,它与引用的主要区别在于:
- 指针本身就是一个对象,允许对指针赋值和拷贝,且在其生命周期能先后指向不同的对象。
- 指针无需在定义时赋初值,和其他内置类型一样,在块作用域内定义指针如果没有初始化,也将拥有一个不确定的值。
定义指针类型的方法是将声明符写成*d的形式,指针存放它所指向的对象的地址,要获取该地址需用取址符&。如 int ival = 42, *p = &ival; double dval, *pd = &dval, *pd2 = pd;
除某些特定情况外,指针类型必须与它所指向的对象严格匹配。
如果指针指向了一个对象,可以用解引用符*来访问该对象。对指针解引用会得到所指向的对象,故为解引用赋值,实际上就是给指针所指的对象赋值。
int i = 42;
int j = 11;
int k = 33;
int *pi = &i;
cout << *pi << " " << pi << endl; // 输出42 008BFD88
*pi = 0;
cout << i << " " << pi << " " << *pi << endl; // 输出0 008BFD88 0
i = j;
cout << i << " " << pi << " " << *pi << endl; // 输出11 008BFD88 11
pi = &k;
cout << i << " " << pi << " " << *pi << endl; // 输出11 008BFD70 33
空指针 空指针不指向任何对象,生成空指针的方法:int *p1 = nullptr; int *p2 = 0; int *p3 = NULL(需要先#include cstdlib)。
建议:初始化所有指针。
可以将指针用于bool表达式中,任何非0指针对应的条件表达式都是true,两个类型相同的合法指针可以用==和!=来进行比较,比较结果是bool值,如果它们存放的地址相同,则为相等,反之不等。
void*指针 void *是一种特殊指针类型,可以存放任意对象的地址,不需要了解对象的具体类型。void *指针能做的事情比较有限,拿它跟别的指针比较,作为函数的输入输出,或者赋值给另一个void * 指针。
指向指针的指针和指向引用的指针
指针是内存中的对象,像其他对象一样也有自己的地址,因此允许把指针的地址再存放到另一个指针中。* 表示指针对象,**,表示指向指针的指针。
int ival = 1024;
int *pi = &ival; // pi指向一个int数
int **ppi = π // ppi指向一个int指针
引用本身不是对象,因此不能定义指向引用的指针,但是可以定义指向指针的引用。
int i = 42, *p;
int *&r = p; // r是对指针p的引用
r = &i; // r赋值相&i当于让p指向i
*r = 0; // 解引用r赋值相当于为解引用p赋值,也就是为i赋值0
4. const限定符
有时候我们有这个需求,定义一个“变量”,却不希望它的值被改变,此时,const关键字可以实现这一需求,因为const对象一旦创建,它的值不能被修改所以,它必须在创建时初始化。可以用字面值或表达式的计算结果或者变量,const对象来初始化,const对象也能将初值赋给变量,拷贝一个对象的值不会改变它,拷贝完成,新对象就和原始对象没有什么关系了。
默认状态下,const对象仅在文件内有效,当多个文件中出现了同名的const变量,等同于在各自文件中分别定义了独立的变量。如果希望不同文件之间能共享const变量,即只在一个文件中定义const,而在其他文件中声明并使用它。解决办法是,不论定义还是声明都加上extern关键字:
extern const int bufSize = func(); // file_1.cc定义并初始化一个常量
extern const int bufSize; // file_1.h头文件声明,与file_1.cc中的bufSize是同一个
4.1 const的引用
把引用绑定到const对象上,称为对常量的引用,与普通引用的差别在于,常量的引用不能用于修改它所绑定的对象。
初始化常量引用时,允许用任意表达式作为初始值,只要该表达式的结果能转换成引用类型即可。
int i = 42;
const int ci = 1024;
const int &r1 = ci; // 正确,引用及对象都是常量
r1 = 42; // 错误,r1不能用于改变它所绑定的常量ci
int &r2 = ci; // 错误,非常量引用不能指向常量对象
const int &ra = i; // 允许将常量引用绑定到普通int对象
const int &rb = 42; // 允许将常量引用绑定到字面值
const int &rc = ra * 2; // 允许将常量引用绑定到表达式
int &r4 = ra * 2; // 错误,r4是一个普通引用
double dval = 3.14;
const int &ri = dval;// 等价于 const int temp = dval, &ri = temp;ri绑定了一个临时量
int i = 20;
double pi = 3.14;
const int& rpi = pi;
cout << pi << " " << rpi << endl; // 输出3.14 3
pi = 3.1;
cout << pi << " " << rpi << endl; // 输出3.1 3
int& j = i;
const int &ri = i;
j = 9;
cout << i << " " << j << " " << ri << endl; // 输出 9 9 9
4.2 指针和const
可以定义类似对常量的引用来定义指向常量的指针,指向常量的指针不能用于修改其所指向的对象,要想存放常量对象的地址,只能使用指向常量的指针。允许令指向常量的指针指向非常量对象,这一点与常量的引用相同。
const double pi = 3.14;
const double *cptr = π
double dval = 3.1;
cptr = &dval;
*cptr = 42; // 错误 不能对*cptr赋值
把指针本身定义为常量,则称为定义了常量指针,常量指针必须初始化,且一旦初始化完成,它的值(指针存放的那个地址)就不能改变了。
int errNumb = 0;
int *const curErr = &errNumb; // curErr将一直指向errNumb
const double pi = 3.14;
const double *const pip = π // pip是一个指向常量对象的常量指针
*pip = 2.7 // 错误, *pip无法修改
*curErr = 0 // 正确,curErr所指的对象是int变量,能够被修改
顶层const 指针本身是一个对象,它又指向了一个对象,因此指针本身是否为常量和指针所指的对象是否是常量是两个不同的问题。用顶层const表示指针本身是一个常量对象,用底层const表示指针所指的对象是一个常量。更一般而论,顶层const可以表示任意对象是常量,而底层const则和引用和指针等复合类型有关。
int i = 0;
int *const p1 = &i; // p1顶层const
const int ci = 42; // ci顶层const
const int *p2 = &ci; // p2顶层const
const int *const p3 = p2; // 左边是底层const,右边是顶层const
const int &r = ci; // 用于声明引用的const都是底层const
i = ci; // 正确,拷贝ci的值,ci是顶层const
p2 = p3; // p2顶层const,可以被赋值p3
int *p = p3; // 错误,p3包含底层const,p没有
p2 = &i; // 正确,int*能转换成const int*
int &r = ci; // 错误,普通int&不能绑定到常量上
const int &r2 = i; // 正确,普通const int&能绑定到int&
constexpr和常量表达式 常量表达式是指值不会改变,且编译过程就能得到计算结果的表达式。一个对象是不是常量表达式,由它的数据类型和初始值共同决定。
const int max_files = 20; // 常量表达式
const int limit = max_files + 1; // 常量表达式
int staff_size = 27; // 普通int不是const int,不是常量表达式
const int sz = get_size(); // sz的值要等到运行时才能确定,不是常量表达式
constexpr变量 C++11新标准允许将变量声明为constexpr类型,以便由编译器来验证变量的值是否是一个常量表达式。声明为constexpr的变量一定是一个常量,且必须用常量表达式初始化。
constexpr int mf = 20; // 20是常量表达式
constexpr int limit = mf + 1; // mf + 1是常量表达式
constexpr int sz = size(); // 只有当size是一个constexpr函数时才是一条正确的声明语句
字面值类型 到目前为止,接触过的数据类型中,算术类型和引用都是于字面值类型,自定义的类Sales_item,IO库,string类型则不属于字面值类型,不能定义为constexpr类型。尽管指针和引用都能定义为字面值类型,但它们的初始值都受严格限制,一个constexpr指针的初始值必须是nullptr或0,或是存储于某个固定地址中的对象。
函数体内的变量一般不是存储于固定地址中,因此constexpr指针不能指向这样的变量,相反,定义于函数体外的对象地址固定不变,可以用来初始化constexpr指针。
指针和constexpr constexpr声明中若定义了一个指针,限定符constexpr仅仅对指针有限,与指针所指的对象无关,即constexpr把它所定义的对象置为了顶层const。与其他常量指针类似,const常量,即可以指向常量又可以指向非常量。
const int *p = nullptr; // p是一个指向整型常量的指针
constexpr int *q = nullptr; // q是一个指向整数的常量指针
constexpr int *np = nullprt; // np是一个指向整数的常量指针,值为空
int j = 0;
constexpr int i = 42; // i为整型常量
// i和j必须定义在函数体外
constexpr const int *p = &i; // p是常量指针,指向常量i
constexpr int *p1 = &j; // p1是常量指针,指向整数j
5. 处理类型
类型别名 它是某种类型的同义语,用来让复杂的类型名字变得简洁,易于使用。
两种方法可以定义类型别名:
typedef double wages; // wages是double的同义语
typedef wages base, *p; // base是double的同义语,p是double*的同义语
// C++11新标准
using SI = Sales_item; // SI是Sales_items的同义语
// 类型别名和类型名字等价,只要是类型名字能出现的地方,就能使用类型别名
wages hourly, weekly; // 等价于double hourly,double weekly
SI item; // 等价于 Sales_item item
typedef char *pstring; // pstring是 char* 类型的别名
const pstring cstr = 0; // cstr是指向char的常量指针,这里const限定的是cstr.
// 这个赋值语句绝不能等量代换为const char *cstr = 0.
auto类型说明符 C++11新标准引进了auto类型说明符,用它能让编译器替我们分析表达式所属的类型。auto让编译器通过初始值类推算变量的类型,显然auto定义的变量必须由初始值。
auto也能在一条语句中声明多个变量,因为一条语句只能有一个基本数据类型,所以该语句中所有变量的初始基本数据类型必须一样。
auto item = val1 + val2; // 由val1和val2相加的结果推断出item的类型,item初始化为val1 //和val2相加的结果
auto i = 0, *p = &i; // 正确,i是整数,p是整型指针
auto sz = 0, pi = 3.14; // 错误,sz和pi的类型不一致
int i = 0, &r = i;
auto a = r; // a是一个整数(r是i的别名,而i是一个整数)
// auto一般会忽略顶层const,同时顶层const则会保留下来
const int ci = i, &cr = ci;
auto b = ci; // b是一个整数(ci的顶层const特性被忽略了)
auto c = cr; // c是一个整数(cr是ci的别名,ci本身是一个顶层const)
auto d = &i; // d是一个整型指针
auto e = &ci; // e是一个指向整型常量的指针(对常量对象取地址是一种底层const)
// 如果希望推断出的auto类型是一个顶层const,需要明确指出
const auto f = ci; // ci的推演类型是int,f是const int
auto &g = ci; // g是一个整型常量的引用,绑定到ci
auto &h = 42; // 错误,不能为非常量引用绑定字面值
const auto &j = 42; // 正确,可以为常量引用绑定字面值
auto k = ci, &l = i; // k是整数,l是整型引用
auto &m = ci, *p = &ci; // m是对整型常量的引用,p是指向整型常量的指针
auto &n = i, *p2 = &ci; // 错误,i是整型,而&ci是const int
decltype类型指示符
当遇到这种情况,希望从表达式的类型推断要定义的变量的类型,但是不想用该表达式的值初始化变量,为满足这一需求,C++11引入了第二种类型说明符decltype,它的作用是选择并返回操作数的数据类型。此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值:
decltype(f()) sum = x; // sum的类型就是函数f的返回类型
const int ci = 0, &cj = ci;
decltype(ci) x = 0; // x的类型是const int
decltype(cj) y = x; // y的类型是const int&,y绑定到变量x
decltype(cj) z; // 错误,z是一个引用,必须初始化
// decltype的结果可以是引用类型
int i= 42, *p = &i, &r = i;
decltype(r+0) b; // 正确,加法的结果是int,因此b是一个(未初始化的)int
decltype(*p) c; // 错误,c是int&,必须初始化
// decltype的表达式如果是加上了括号的变量,结果将是引用
decltype((i)) d; // 错误,d是int&,必须初始化
decltype(i) e; // 正确,e是一个(未初始化的)int
6. 自定义数据结构
struct
- 类可以以关键字
struct
开始,紧跟类名和类体。 - 类数据成员:类体定义类的成员。
C++11
:可以为类数据成员提供一个类内初始值(in-class initializer)。
编写自己的头文件
- 头文件通常包含哪些只能被定义一次的实体:类、
const
和constexpr
变量。
预处理器概述:
- 预处理器(preprocessor):确保头文件多次包含仍能安全工作。
- 当预处理器看到
#include
标记时,会用指定的头文件内容代替#include
- 头文件保护符(header guard):头文件保护符依赖于预处理变量的状态:已定义和未定义。