《C++ Primer》第2章 变量和基本类型(下)
2.5 处理类型
随着程序愈加复杂,用的类型也愈加复杂:一是类型名愈加复杂且无法体现其真实含义,二是根本搞不清到底需要的类型是什么。程序员只能回头看上下文寻求帮助。
2.5.1 类型别名
类型别名是一个名字,它是某种类型的同义词。
两种方法定义类型别名:
-
传统方法:使用关键字
typedef
:typedef double wages; //wages是double的同义词 typedef wages base, *p; //base是double的同义词,p是double*的同义词
-
新方法:使用关键字
using
:using SI = Sales_item; //SI是Sales_item的同义词
类型别名与类型的名字等价,只要是类型的名字能出现的地方,就能使用类型别名。
wages hourly, weekly; //等价于double hourly, weekly;
SI item; //等价于Sales_item item;
指针、常量和类型别名
当某个类型别名指代的是复合类型或常量时,不要试图把类型别名替换成它本来的样子去理解,这样是错误的。
typedef char *pstring; //pstring是char*的别名
const pstring cstr = 0;//cstr是指向char的常量指针
const pstring *ps; //ps是一个指针,它的对象是指向char的常量指针
上述两条声明语句的基本数据类型都是const pstring
,和过去一样,const
是对给定类型的修饰。pstring
实际上是指向char
的指针,也就是说pstring
是指针类型,因此,const pstring
就是指向char
的常量指针。
如果把它替换成本来的样子去理解
const char *cstr = 0; //这是一个指向常量char的普通指针。所以这种理解方法是错误的。
2.5.2 auto类型说明符
C++11新标准引入了**auto
类型说明符**,用它可以让编译器替我们去分析表达式所属的类型。
auto
让编译器通过初始值推断变量类型,所以auto
定义的变量必须有初始值。
//由val1+val2的结果可以推断出item的类型
auto item = val1 + val2; //item初始化为val1+val2的结果
使用auto也能在一条语句中声明多个变量,但该语句中的初始基本数据类型必须一致:
auto i = 0, *p = &i; //正确:i是整数,p是整型指针
auto sz = 0, pi = 3.14; //错误:sz和pi的类型不一致
复合类型、常量和auto
编译器推断出来的auto
类型有时候和初始值的类型不完全一致,编译器会适当改变结果类型使其更加符合初始化规则。
-
当引用被用作初始值时,编译器以引用对象的类型作为auto的类型。
int i = 0, &r = i; auto a = r; //a是一个整数(r是i的别名,而i是一个整数)
-
auto一般会忽略掉顶层const,而保留底层const。
const int ci = i, &cr = ci; //ci是一个整型常量,cr是对整型常量ci的引用 auto b = ci; //b是int(因为ci的顶层const被忽略了,只剩int) auto c = cr; //c是int(因为cr是ci的别名,而ci的顶层const被忽略,只剩int) auto d = &i; //d是整型指针(整数的地址就是指向整数的指针) auto e = &ci; //e是指向整数常量的指针(对常量对象取地址是一种底层const)
如果希望推断出的auto类型是一个顶层const,需要明确指出:
const auto f = ci; //ci的推演类型是int,f是const int
-
将引用的类型设为auto时,初始值的顶层const属性仍然保留
auto &g = ci; //g是常量引用 auto &h = 42; //错误:非常量引用不能绑定到字面值 const auto &j = 42; //j是常量引用,常量引用可以绑定到字面值
要在一条语句定义多个变量,切记,符号&和*只从属于某个声明符,而非基本数据类型的一部分,因此初始值必须为同一类型:
auto k = ci, &l = i; //k是int(顶层const被忽略),l是整型引用
auto &m = ci, *p = &ci; //m是对整型常量的引用(ci的顶层const被保留),p是指向整型常量的指针(整数的地址就是指向整数的指针,对常量对象取址是一种底层const)
auto &n = i, *p2 = &ci; //错误:i的类型是int,而&ci的类型是const int
2.5.3 decltype类型指示符
decltype:选择并返回操作数的数据类型。在此过程中,编译器分析表达式并得到其类型,却不实际计算表达式的值。
decltype(f()) sum = x; //sum的类型就是函数f的返回类型
注:编译器并不实际调用函数 f
如果decltype
使用的表达式是一个变量,则decltype
会返回该变量的类型(包括顶层const和引用在内):
const int ci = 0, &cj = ci; //ci是整型常量;cj是整型常量引用,绑定到ci
decltype(ci) x = 0; //x的类型是const int (ci的顶层cont也被保留了)
decltype(cj) y = x; //y的类型是const int&,y绑定到x
decltype(cj) z; //错误:z是引用,必须初始化。
注:引用从来都作为所指对象的同义词出现,只有在decltype处是一个例外。
decltype和引用
如果decltype
使用的表达式不是一个变量,则decltype
返回表达式结果对应的类型。
//decltype的结果可以是引用类型
int i =42, *p = &i, &r = i;
decltype(r+0) b; //正确:加法的结果是int,因此b是一个未初始化的int
decltype(*p) c; //错误:c是int&,必须初始化
因为r是一个引用,如果直接decltype(r)
则返回的类型是引用类型,引用类型必须初始化。而如果是decltype(r+0)
就不一样了,在表达式r+0
中,r
作为i
的别名出现,i+0
的返回值是int
,所以b
是int
,可以不用初始化。
如果表达式的内容是解引用操作,则decltype
将得到引用类型。所以decltype(*p)
返回的是int&
。
decltype
与auto
的另一处重要区别:decltype
的结果类型与表达式形式密切相关。
值得注意的是:如果decltype
使用的是一个不加括号的变量,则结果就是该变量的类型
如果给该变量加上一层或多层括号,则会得到引用类型。
//decltype的表达式如果是加了括号的变量,结果将是引用
decltype((i)) d; //错误:d是int&,必须初始化
decltype(i) e; //正确:e是未初始化的int
切记:decltype((var))的结果永远是引用。
而decltype(var)只有当var本身是一个引用时才是引用。
decltype和auto的区别:
对引用变量的不同
auto将引用变量赋给变量后,变量的类型为引用变量所绑定的变量的类型。而decltype则是为引用类型。即引用在decltype处不作为所绑定对象的同义词出现。
处理顶层const的方式不同
auto一般会忽略掉顶层的const,同时底层的const会被保留下来。而decltype 则会返回该变量的完整类型(包括顶层const和引用在内)。
2.6 自定义数据结构
2.6.1 定义Sales_data类
//初步定义Sales_data类
struct Sales_data {
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
类以关键字struct
开始,紧跟着类名和类体(类体部分可为空),类体由花括号包围形成一个新的作用域。
类内部定义的名字必须唯一,但是可以与类外部定义的名字重复。
类体右侧的表示结束的花括号后必须写一个分号。这是因为类体后面可以紧跟变量名以示对该类型对象的定义,所以分号必不可少。
struct Sales_data {
/*...*/} accum, trans, *salesptr;
//与上一语句等价,但可能更好些
struct Sales_data {
/*...*/};
Sales_data accum, trans, *salesptr;
类数据成员
-
每个对象有自己的一份数据成员拷贝。修改一个对象的数据成员,不会影响其他
Sales_data
的对象。 -
可以为数据成员提供一个类内初始值。创建对象时,类内初始值将用于初始化数据成员。没有初始值的成员将被默认初始化,如
bookNo
将初始化为空字符串。 -
类内初始值或者放在花括号里,或者放在赋值运算符右边。记住不能使用圆括号
不能使用圆括号的原因
因为无法避免这样的情况
class Widget { private: typedef int x; int z(x); };
这样,z的声明成了函数声明。
2.6.3 编写自己的头文件
- 类通常定义在头文件中,且类所在的头文件的名字与类的名字一样
- 头文件通常包含那些只能被定义一次的实体,如类、const和constexpr变量等。
预处理器概述
经常出现头文件多次包含的情况,所以有必要进行适当处理。
预处理器可以用来确保头文件多次包含仍能安全工作。
**预处理器是在编译之前执行的一段程序。**如
#include
头文件保护符依赖于预处理变量。预处理变量有两种状态:已定义和未定义。
#define
指令把一个名字设定为预处理变量。
#ifdef
当且仅当变量已定义时为真。
#ifndef
当且仅当变量未定义时为真。
一旦检查结果为真,则执行后续操作直至遇到#endif
指令为止。
使用这些功能可以有效地防止重复包含的发生。
#ifndef SALES_DATA_H
#define SALES_DATA_H
#include <string>
struct Sales_data {
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
#endif
第一次包含Sales_data.h
时,#ifndef
检查结果为真,预处理器将顺序执行后续操作直至遇到#endif
为止。此时,预处理变量SALES_DATA_H
的值将变为已定义,后边如果再一次包含Sales_data.h
,则#ifndef
检查结果将为假,编译器将忽略#ifndef
到#endif
之间的部分。
- 预处理变量无视C++语言中关于作用域的规则
- 整个程序中的预处理变量包括头文件保护符必须唯一,一般把预处理变量的名字全部大写
- 要习惯性地加上头文件保护符,不管程序需不需要。