第二章 变量和基本类型
变量
-
std::cin>>int input_value
报错输入运算符的工作是将输入流istream中的数据赋值给>>右边的变量,而对变量赋值并不是初始化工作。也就是说该变量未经初始化。
-
何为对象
通常情况下,对象是指一块能存储数据并具有某种类型的内存空间。
变量声明和定义的关系
- C++语言支持分离式编译(
separate compilation
), 其实就是让不同文件相互之间联系起来,文件之间共享代码。而我们的西加加为了支持分离式编译,C++语言将声明和定义区分开勒。**声明( declaration )使得名字为程序所知,一个文件如果向使用别处定义的名字则必须包含对那个名字的声明。而定义(definition)**负责创建与名字关联的实体 - 变量声明规定了变量的类型和名字,在这一点定义与之相同。但是除此以外,定义还申请存储空间,也可能会为变量赋一个初始值。
- 如果想要声明一个变量而非定义它,就在变量名前添加关键字
extern
, 而且不要显示地初始化变量 - 任何包含了显示初始化地声明即成为定义
变量能且只能被定义一次,但是可以被多次声明
extern int i; // 声明 i 而非定义
int i; // 声明并定义
extern double pi = 3.1416 // 定义
作用域
#include <iostream>
int reused = 42;
int main(){
int unique = 0; // unique 拥有块作用域
// 输出: 使用全局变量 reused; 输出 42 0
std::cout << reused << " " << unique << std::endl;
int reused = 0; // 新建局部变量 reused, 覆盖全局变量 reused
// 输出:使用局部变量 reused; 输出 0 0
std::cout << reused << " " << unique << std::endl;
// 输出:显示地访问全局变量 reused; 输出 42 0
std::cout << ::reused << " " << unique << std::endl;
return 0;
}
复合类型
引用
引用(reference) 为对象起了另一个名字,引用类型引用(refers to)另一个类型。通过将生命夫协程 &d
的行是来定义引用类型,其中 d
是声明的变量名。
int ival = 1024;
int &refVal = ival; // refVal 指向 ival ( 是 ival 的另一个名字 )
int &refVal2; // 报错: 引用必须被初始化
引用即别名:引用并非对象,相反,它只是为一个已经存在的对象所起的别名,也是一种绑定(bind)的关系。对其进行的所有操作都是在与之绑定的对象上进行的
引用只能在定义时被初始化一次
int i = 10;
int j = 11;
int & a = i;
cout << "a = " << a << endl;
cout << "i = " << i << endl;
a = j;//注意,这里不是被使用j的别名,是i的值被赋值成j.
cout << "a = " << a << endl;
cout << "i = " << i << endl;
指针
指针和引用的不同点:
- 指针本身就是一个对象,允许对指针赋值和拷贝,而且在指针的声明周期内,它可以先后指向几个不同给的对象
- 指针无须再定义时赋初值。
int i = 42;
int &r = i; // &紧随类型名出现,因此是声明的一部分,r是一个引用
int *p; // *紧随类型名出现,因此是声明的一部分,p是一个指针
p = &i; // &出现在表达式中,是一个取地址符
*p = i; // *出现在表达式中,是个解引用
int &r2 = *p; // &是声明的一部分,*是一个解引用符
建议初始化所有指针
在大多数编译器环境下,如果使用了未经初始化的指针,则该指针所占内存空间的当前内容将被看作一个地址值。访问该指针,相当于去访问一个本不存在的位置上的本不存在的对象。糟糕的是,如果指针所占内存空间恰好由内容,而这些内容又被当作了某个地址,我们很难分清它到底是合法的还是非法的了。
void* 指针 是一种特殊的指针类型,可用于存放任意对象的地址。从 void*
的视角来看,内存空间仅仅也就是内存空间,没办法访问内存空间所存的对象。
指针和引用的区别
- 指针有自己的一块空间,而引用指示一个别名
- 使用 sizeof 查看一个指针的大小是 4,而引用则是被引用对象的大小
- 指针可以被初始化 NULL, 而引用必须被初始化且必须是一个已有对象的引用
- 作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引用的修改都会改变所指向的对象
- 可以有 const 指针,但是没有 const 引用
- 指针在使用中可以指向其它对象,但是引用只能是一个对象的引用,不能被改变
- 指针可以有多级指针(**p), 而引用只有一级
- 指针和引用使用 ++ 运算符的意义不一样
- 如果返回动态内存分配的对象或内存,必须使用指针,引用可能引起内存泄漏
指向指针的引用
引用本身不是一个对象,因此不能定义指向引用的指针。但指针是对象,所以存在对指针的引用
int i = 42;
int* p; // p 是一个 int 型指针
int*& r = p; // r 是一个对指针 p 的引用
r = &i; // r 引用了一个指针,因此对 r 赋值 &i 就是令 p 指向 i
*r = 0; // 解引用 r 得到 i, 也就是 p 指向的对象,将 i 的值改为 0
要理解 r
的类型到底是什么,最简单的办法是从右向左阅读 r
的定义。离变量名最近的符号(此例中是 &r
的 &
) 对变量的类型有最直接的影响,因此 r
是一个引用。声明符的其余部分用以确定 r
引用的类型是什么,此例中的符号 *
说明 r
引用的是一个指针。最后,声明的基本数据类型部分指出 r
引用的是一个 int
指针
这一部分一下子让我对 常量指针和指针常量怎么分辨清晰了
const int* p = &i
--> p 的指向可以更改,p
指向的值不可以更改。因为*
离变量名p
最近,所以说明p
是一个指针。接着声明的基本数据类型被cosnt
修饰const int
,说明p
指向的是一个int
常量。所以指向的值是不能改变的,但是p
本身可以改变
int* const p = &i
–>p
指向不可以更改,但是p
指向的值可以更改。因为const
离变量名p
最近,说明这个变量p
是常量,不能更改,接着符号*
说明变量p
是一个指针,那么指针是常量,就说明指针的方向不能更改。最后,声明的基本数据类型部分指出常量指针p
是一个int
指针
const 限定符
const
对象一旦创建后其值就不能再改变,所以 const
对象必须初始化。
默认状态下,const 对象仅在文件内有效
当以编译时初始化的方式定义一个 const
对象时,就如对 bufsize
的定义一样
const int bufSize = 512; // 输入缓冲区的大小
编译器将在编译过程中把用到该变量的地方都替换成对应的值。也就是说,编译器会找到代码中所有用到 bufSize
的地方,然后用 512 替换。
为了执行上述替换,编译器必须知道变量的初始值。如果程序包含多个文件,则每个用了 const
对象的文件都必须得能访问到它得初始值才行。要做到这一点,就必须在每一个用到变量的文件中都有对它的定义。为了支持这一用法,同时避免对同一变量的重复定义,默认情况下,const
对象被设定为尽在文件内有效。当多个文件中出现了同名的 const
变量时,其实等同于在不同文件中分别定义了独立的变量
某些时候有这样一种 const
变量,它的初始值不是一个常量表达式,但又确实有必要在文件间共享。这种情况下,我们不希望编译器为每个文件分别生成独立的变量。相反,我们想让这类 const
对象像其他(非常量)对象一样工作,也就是说,只在一个文件中定义 const
,而在其他多个文件中声明并使用它
解决的办法就是, 对 const
变量不管是声明还是定义都添加 extern
关键字,这样只需定义一次就可以了
// file_1.cc 定义并初始化了一个常量,该常量能被其他文件访问
extern const int bufSize = fcn();
// file_1.h 头文件
extern const int bufSize; // 与 file_1.cc 中定义的 bufSize 是同一个
如上述程序所示,file_1.cc
定义并初始化了一个 bufSize
。
file_1.h
头文件中的声明也由 extern
做了限定,其作用是指明 bufSize
并非本文件独有,它的定义在别处
constexpr 和 常量表达式
常量表达式(const expression) 是指指不会改变并且在编译过程就能得到计算结果的表达式。显然,字面值属于常量表达式,用常量表达式初始化的 const
对象也是常量表达式。
一个对象( 或表达式 )是不是常量表达式由它的数据类型和初始值共同决定
const int max_files = 20; // max_files 是常量表达式
const int limit = max_files + 1; // limit 是常量表达式
int staff_size = 27; // staff_size 不是常量表达式
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
在 constexpr
声明种如果定义了一个指针,限定符 constexpr
仅仅对指针有效,与指针所指对象无关
const int* p = nullptr; // p 是一个指向 整型常量 的指针
constexpr int* q = nullptr; // q 是一个指向 整数 的常量指针
p 和 q 的类型相差甚远,p 是一个指向常量的指针,而 q 是一个常量指针,其中的关键在于 constexpr
把它所定义的对象置为了顶层 const
类型别名
有两种方法可用于定义类型别名。传统方法是使用关键字 typedef
typedef double wages; // wages 是 double 的同义词
typedef wages base, *p; // base 是 double 的同义词,p 是 double* 的同义词
新标准规定了一种新的方法,使用 别名声明( alias declaration ) 来定义类型的别名:
using SI = Sales_item; // SI 是 Sales_item 的同义词
decltype 类型指示符
有时会希望从表达式的类型推断出要定义的变量的类型,但是不想用该表达式的值初始化变量。为了满足这一要求, C++11 新标准引入了第二种类型说明符 decltype
,它的作用是选择并返回操作数的数据类型。在此过程中,编译器分析表达式并得到它的类型,但不实际计算表达式的值:
decltype(f()) sum = x; // sum 的类型就是函数 f 的返回类型
编译器并不实际调用函数 f
, 而是使用当调用发生时 f
的返回值类型作为 sum
的类型。
auto 和 decltype 的差别
// auto
int i = 0, &r = i;
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 )
//decltype
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 和 引用
decltype((variable))
的结果永远是引用
decltype(variable)
的结果只有当variable
本身就是一个引用时才是引用
预处理器概括
确保头文件多次包含仍能安全工作的常用计数时预处理器( preprocessor ), 它由 C++ 语言从 C 语言继承而来。预处理器是在编译之前执行的一段程序,可以部分地改变我们所写地程序。比如最常用地 #include
, 当预处理器看到 #include
标记时就会用指定地头文件地内容代替 #include
C++ 程序还会用到的一项处理功能是 头文件保护符( header guard ), 头文件保护符依赖于预处理变量。预处理变量由两种状态:已定义和未定义。
#define
指令把一个名字设定为预处理变量,另外两个指令则分别检查某个指定的预处理变量是否已经定义:#ifdef
当且仅当变量已定义时为真, #ifndef
当且仅当变量未定义时为真。一旦检查结果为真,则执行后续操作直到遇到 #ndef
指令为止
使用这些功能就能有效地防止重复包含地发生:
#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
之间的部分
头文件即使没有被包含在任何其他头文件中,也应该设置保护符。头文件保护符很简单,程序员只要习惯性地加上就可以,没必要在意程序到底需不需要