C++ primer 第二章变量和基本类型
指针和const
- 指向常量的指针:指针指向一个同基本数据类型变量,那么将不可以通过指针修改变量的值,但是仍然能够使指针指向其他常量:
const double pi = 3.14; // pi是个常量,它的值不能改变
double *ptr = π //错误:ptr是一个普通指针
const double *cptr = π //正确:cptr可以指向一个双精度常量
*cptr = 42; //错误:不能给*cptr赋值
指针的类型必须与其所指对象的类型一致,但是有两个例外。第一种例外情况是允许令一个指向常量的指针指向一个非常量对象:
double dval = 3.14; //dval是一个双精度浮点数,它的值可以改变
cptr = &dval; //正确:但是不能通过cptr改变dval的值
- 常量指针:普通指针建议初始化,常量指针必须初始化,一旦初始化完成,它的值(地址)也就不能改变了!指针本身是一个常量,其意义是指针本身存储的地址是一个常量,而地址指向的对象并非常量,因此可以修改,我们通过如下方式定义:
const double *const pip = π//这行代码定义了一个指向常量对象的常量指针,要理解它我们
//采用从右到左的顺序读,*const代表指针内容为常量,const
//double为该指针指向的对象类型。
这类常量指针如果指向的对象类型并非常量就可以通过解引用符‘*’修改对象的值,如:
*curErr=0;
顶层const
- 顶层const(top-level const),表示指针本身是个常量
- 底层const(low-level const) ,表示指针所指的对象是一个常量
当执行对象的拷贝操作时,常量是顶层const还是底层const区别明显。其中,顶层const不受什么影响:
i = ci; //正确:拷贝ci的值,ci是一个顶层const,对此操作无影响
p2 = p3; //正确:p2和p3指向的对象类型相同,p3顶层 const的部分不影响
另一方面,底层const的限制却不能忽视。当执行对象的拷贝操作时,拷入和拷出的对象必须具有相同的底层const资格,或者两个对象的数据类型必须能够转换。一般来说,非常量可以转换成常量,反之则不行:
int *p= p3; //错误:p3包含底层const的定义,而p没有
p2 = p3; //正确:p2和p3都是底层 const
p2 = &i; //正确:int*能转换成const int*
int &r = ci; //错误:普通的int &不能绑定到int常量上
const int &r2 = i; //正确:const int&可以绑定到一个普通int上
constexpr 和常量表达式
常量表达式(const expression)是指值不会改变并且在编译过程就能得到计算结果的表达式。显然,字面值属于常量表达式,用常量表达式初始化的const对象也是常量表达式。后面将会提到,C++语言中有几种情况下是要用到常量表达式的。
一个对象(或表达式)是不是常量表达式由它的数据类型和初始值共同决定。
constexpr变量
C++11新标准规定,允许将变量声明为constexpr类型以便由编译器来验证变量的值是否是一个常量表达式。声明为constexpr的变量一定是一个常量,而且必须用常量表达式初始化。
字面值类型
常量表达式的值需要在编译时就得到计算,因此对声明constexpr时用到的类型必须有所限制。因为这些类型一般比较简单,值也显而易见、容易得到,就把它们称为“字面值类型”(literal type)。
指针和constexpr
必须明确一点,在constexpr声明中如果定义了一个指针,限定符constexpr仅对指针有效,与指针所指的对象无关:
const int *p = nullptr; // p是一个指向整型常量的指针
constexpr int *q = nullptr; // q是一个指向整数的常量指针 dsf
那么constexpr int * 可以说是和int *const 意思接近。
constexpr将它所定义的对象置为顶层const。
处理类型
类型别名(type alias)是一个名字,它是某种类型的同义词。使用类型别名有很多好处,它让复杂的类型名字变得简单明了、易于理解和使用,还有助于程序员清楚地知道使用该类型的真实目的。
typedef double wages;
另一种写法
using sI = Sales_item; //SI是Sales item的同义词
指针、常量和类型别名
如果某个类型别名指代的是复合类型或常量,那么把它用到声明语句里就会产生意想不到的后果。例如下面的声明语句用到了类型pstring,它实际上是类型char*的别名:
typedef char *pstring;
const pstring cstr = 0;// cstr是指向char的常量指针
const pstring *ps;//ps是一个指针,它的对象是指向char的常量指针上述两条声明语句的基本数据类型都是const pstring,和过去一样,const是对给定类型的修饰。pstring 实际上是指向char 的指针,因此,const pstring 就是指向char的常量指针,而非指向常量字符的指针。
auto类型说明符
显然,auto定义的变量必须有初始值:
auto item = vall + val2;// item初始化为val1和val2相加的结果
auto也能在一条语句中声明多个变量。因为一条声明语句只能有一个基本数据类型,所以该语句中所有变量的初始基本数据类型都必须一样:
auto sz = 0,pi = 3.14;//错误:sz和pi的类型不一致
复合类型、常量和 auto
首先,正如我们所熟知的,使用引用其实是使用引用的对象,特别是当引用被用作初始值时,真正参与初始化的其实是引用对象的值。此时编译器以引用对象的类型作为auto的类型:
int i =0,&r = i;
auto a= r; // a是一个整数(r是i的别名,而i是一个整数)
其次,auto一般会忽略掉顶层const(参见2.4.3节,第57页),同时底层const则会保留下来,比如当初始值是一个指向常量的指针时:
如果希望推断出的auto类型是一个顶层const,需要明确指出:
const auto f = ci; //ci的推演类型是int,f是const int
decltype
decltype,它的作用是选择并返回操作数的数据类型。在此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值:
decltype (f())sum = x; // sum的类型就是函数f的返回类型
编译器并不实际调用函数f,而是使用当调用发生时f的返回值类型作为sum 的类型。换句话说,编译器为sum 指定的类型是什么呢?就是假如f被调用的话将会返回的那个类型。
需要指出的是,引用从来都作为其所指对象的同义词出现,只有用在 decltype处是一个例外,如上式:sum的类型绑定的是‘f()’,而一般的绑定应当绑定‘x’的类型
如果表达式的内容是解引用操作,则decltype将得到引用类型。
int i =42,*p = &i, &r = i;
decltype(r + 0) b;//正确:加法的结果是int,因此b是一个(未初始化的)int
因为r是一个引用,因此decltype(r)的结果是引用类型。如果想让结果类型是r所指的类型,可以把r作为表达式的一部分,如r+0,显然这个表达式的结果将是一个具体值而非一个引用。
切记:decltype ((variable))(注意是双层括号)的结果永远是引用,而decltype(variable)结果只有当 variable本身就是一个引用时才是引用。
自定义数据结构
struct Sales_data {/* ...*/ } accum,trans, *salesptr;
struct sales_data {/*...*/ };//与上一条语句等价,但可能更好一些
Sales_data accum,trans, *salesptr;
分号表示声明符(通常为空)的结束。一般来说,最好不要把对象的定义和类的定义放在一起。这么做无异于把两种不同实体的定义混在了一条语句里,一会儿定义类,一会儿又定义变量,显然这是一种不被建议的行为。
C++11新标准规定,可以为数据成员提供一个类内初始值(in-class initializer)。
对类内初始值的限制:或者放在花括号里,或者放在等号右边,记住不能使用圆括号。
编写自己的头文件
使用sales data类的程序就先后两次包含了string.h头文件:一次是直接包含的,另有一次是随着包含sales_data.h被隐式地包含进来的。有必要在书写头文件时做适当处理,使其遇到多次包含的情况也能安全和正常地工作。
预处理器概述
确保头文件多次包含仍能安全工作的常用技术是预处理器(preprocessor),它由C++语言从C语言继承而来。预处理器是在编译之前执行的一段程序,可以部分地改变我们所写的程序。之前已经用到了一项预处理功能#include,当预处理器看到#include标记时就会用指定的头文件的内容代替#include。
C++程序还会用到的一项预处理功能是头文件保护符(header guard),头文件保护符依赖于预处理变量(参见2.3.2节,第48页)。预处理变量有两种状态:已定义和未定义#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也会被拷贝到我们的程序中来。后面如果再一次包含Sales_data.h则#ifndef的检查结果将为假,编译器将忽略#ifndef到#endif之间的部分。
头文件即使(目前还)没有被包含在任何其他头文件中,也应该设置保护符。头文件保护符很简单,程序员只要习惯性地加上就可以了,没必要太在乎你的程序到底需不需要。

浙公网安备 33010602011771号