第 2 章:变量和基本类型
-
把一个值赋值给一个无符号 类型的变量的时候,如果该值超过了该变量能够表示的范围,那么结果就是该变量获得了该值经过对表示范围大小求模运算后落在表示范围内的值。比如 1 个字节的
unsigned char
能够表示 0 到 255 区间内的值,表示范围大小为 256,如果你给它赋一个 -3 (显然不落在 [0,255]),那么该变量拿到的值是 -3 % 256 = -3 + 256 = 253。269 的话就是 269 % 256 = 269 - 256 = 13。 -
相对地,把一个值赋值给一个有符号 类型的变量的时候,如果该值超过了该变量能够表示的范围,赋值结果是未定义的,该过程可以正确编译,但是运行了之后会发生什么事情是不可预知的,对于不同的编译器可能有不同的结果,可能是崩溃,可能是不正确的数据,还有可能是其他的事情,所以不要这么做。
-
有无符号数参与运算的时候,结果一定是落在无符号数的表示范围值内的。快速口算的时候可以先全当作有符号,之后把计算结果模到无符号表示范围就行。但是实际上计算机不是这么计算的。
-
例如
int + unsigned int
的时候,会把int
转为unsigned int
。-42 + 10
,其中-42
是int
类型,10
是unsigned int
类型,那么在加法运算前,-42
会转为unsigned int
的值,也就是 4294967254,然后再加10
,变成 4294967264,落在了表示范围内,所以结果就是这个。如果再多加一点,例如给-42
加的不是10
而是50
,运算结果是 4294967304 超过了 2^32-1,那么对加法结果依然会求一次模,得到8
。 -
定义于函数体内的内置类型对象如果没有初始化,则其值未定义;每个类自行决定起初始化对象的方式,自己决定是否允许不经初始化就定义对象。
-
声明 只规定变量的类型和名字,但是定义 还申请内存空间,还可能做出初始化。因此要区分这两者。
-
C++ 支持分离式编译,因此一个文件中的代码可能需要使用另一个文件中定义的变量。这种情况下,每个要使用该变量的文件中都要有对该变量的声明,但是该变量的定义 只能在一个文件中出现。声明 一个变量的时候使用
extern
关键字,如extern int i;
,这个时候不要初始化i
,否则extern
关键字失效,该语句变成了定义 (想要文件间共享const
常量的除外,下面会有一条这个解释)。 -
在函数体内部,如果试图初始化一个由
extern
关键字标记的变量,将引发错误。 -
声明空指针的时候,要显式使用
// T 代表任意类型 T *p = nullptr;
或
T *p = 0;
但是不可以把一个值等于
0
的int
变量直接赋给指针,比如这样子:int i = 0; T *p = i; // Cannot initialize a variable of type 'int *' with an lvalue of type 'int'
-
两个合法指针进行
==
和!=
比较的时候,是比较两个指针所存的地址。当两者的地址都为空时,==
返回true
;当两者都指向同一个对象时,==
返回true
;当两个指针都指向同一个对象的下一地址时,==
也返回true
。还有一种比较特殊的情况,当一个指针指向某对象,另一个指针指向另外一个对象的下一地址,但是因为两个对象恰好在内存中是相邻 的,所以也会出现 == 返回true
的情况,一定不要忘记这种情形, -
void*
指针比较特殊,可以存放任意类型对象的地址,但也因此失去了被解引用的能力:我们不能确定这个被指向的对象是什么类型,自然就不知道解引用后能做什么事情。因此void*
指针能做的事情比较有限:去和别的指针做比较 (也就是比较指针所记录的地址);作为函数的输入或输出 (这点用的较多);把它的值赋给另外一个void*
指针。 -
int *&r = &i;
这句话代表r
指向i
,但是具体而言r
是i
的指针的引用。 -
常规的定义
const
常量的方法 (编译时初始化) 是这样子的:const int num = 214;
在这种定义方式下,编译器将在编译时 在这个文件中找到在这句代码后所有用到
num
变量的地方,然后把num
替换成214
(就像宏定义那样)。 因此如果想要让不同文件共享这个const
常量,那么在每个文件中,都要有const int num = 214;
这句定义 (不然就没法替换啦)。然而这样子的话,违反了前面说过的每个变量只能被定义一次 这个限定。因此在默认状态下,
const
常量被设置成仅在所处的文件内部有效,而文件外部就无法读取。如果想在每个文件里都能用到同样的const
常量,那么要在每个文件内部均定义同名的对象,但这个时候其实他们是不同的独立的对象。或者,可以在该const
常量声明和定义的时候均添加extern
关键字。然后在不同文件内用extern
描述该常量,即可做到只定义一次而能够多文件间共享。 -
指向常量的引用仅对该引用可参与的操作进行限定,指不能通过对被引用对象的引用来修改被引用对象的值。举个例子:
const int a = 3; const int &b = a;
第二句的
const
的含义是不能通过修改b
来修改a
的值。由于在确定了引用绑定关系之后,引用不能易主,所以b
是不能再引用别的对象的,同时b
还不能修改a
的值,所以b
在整个生命周期能做的事情只有读取a
的值。 -
对某一个常量的引用实质上是对这个常量起的别名,因此一般来说 它们类型相同 (也是有例外的),因此都要用
const
来限定。企图用int
来引用一个const int
会发生编译时错误,因为编译器发现这个引用本身是int
类型,那么编译器会认为写代码的人在之后可能会对这个int
类型的引用作出修改,而这是被禁止的,所以编译器要求对const
对象的引用也要用const
限定。但是另一种情况下,一个const int
对象是可以引用一个int
对象的,因为const
限定的是引用关系本身,你不能通过这个引用来改变被引用的变量的值,但是被引用的变量可以使用其他方式进行改变 (例如通过直接调用这个变量的标识符显式的改变这个变量)。在这种情况下,你要确保被引用的非常量 (也就是上面栗子中的那个int
) 可以转换成引用的类型 (即const int
)。举个栗子:int i = 24; double pi = 3.14;
在这时
const int &r1 = i;
是可以的,因为
24
是一个字面值常量,const int &r2 = i - 3;
也是可以的,因为
24 - 3
得到的也是一个字面值常量,const int &r3 = pi;
还是可以的,这个时候编译器会先把
pi
变成一个const int temp = 3
然后用
r3
去引用这个temp
,这个时候r3
绑定了一个临时量。反正你不可以通过r3
来改变temp
的值,所以temp
就一直在那里,一直都是3
,r3
的值也一直都是3
。但是这样子是不行的:const int &r4 = "4"; // Reference to type 'const int' could not bind // to an lvalue of type 'const char[2]
因为一个
string
不能转换成一个const int
。 -
和指向常量的引用一样,指向常量的指针也只对通过指针对被指对象进行的操作 进行限制。因此,被引用的对象可以在其他地方通过显式调用标识符来修改。但是指向常量的指针本身不一定是常量,例如
const int a = 3; const int *p = &a; //Implicit conversion from 'double' to 'int' changes value from 3.14 to 3
-
中的
p
目前指向的是a
,接下来可以通过const int b = 4; p = &b;
来改变
p
指向的对象。 -
指针是一个对象,所以也可以限定一个指针为一个常量,这个时候指针本身是不能改变的,也就是说指针所储存的地址是不能改变的,也就是说这个指针一辈子只能指向那一个对象了。这个时候
const
的位置有所改变,从const int *p
变成int *const p
,const
离p
最近,因此首先p
是个常量,然后他还是个指针。此时被指向的那个对象不仅可以是常量,同时也可以为变量。 -
指针本身是一个常量,并不意味着不可以借助指针来修改被指向的变量,只要被指向的对象是个变量,那就可以通过它的指针来修改它的值,不管这个指针本身是不是常量。
所谓顶层
const
和底层const
,顶层代表对象本身是const
,底层代表对象所引用、指向的对象是const
。所有的引用都是底层const
。见下面的例子:int i = 0; int *const pi = &i; // pi 是顶层,因为 pi 不能改变 const int ci = 0; // ci 是顶层,因为 ci 不能改变 const int *p0 = &i; // p0 是底层,因为 p0 可以改变 const int *p1 = &ci; // p1 是底层,因为 p1 可以改变 const int *const p2 = &ci; // int 右的 const 是顶层,int 左 的 const 是底层 const int &r0 = i; // r0 是底层,用于声明引用的 const 都是底层 const int &r1 = ci; // r1 是底层,用于声明引用的 const 都是底层
顶层还是底层请看注释。当执行对象的拷贝动作时,底层的存在感高于顶层。对于指针的拷贝,顶层不能接受一切拷贝,底层可以接受一切拷贝,变量可以接受顶层但是不可以接受底层。对于引用,不能把
int &
绑定到const int
上,但是可以把const int &
绑定到int
上。 -
举一个例子:
const int r1 = 30; const int r2 = r1 - 3;
这么写的确没问题,但是还有一种更加推荐的 C++11 提出的写法,是
constexpr int r1 = 30; constexpr int r2 = r1 - 3;
这么写的主要原因是像
30
、r1
、r1 - 3
这些值不会改变,并且在编译时就能得到计算结果的简单表达式 被称作常量表达式,在实际应用中,几乎不可能分辨一个初始值是不是常量表达式,因此 C++11 规定允许将变量声明为constexpr
类型以便由编译器来验证变量的值是不是一个常量表达式。如果你在写代码时认定一个变量是一个常量表达式,那么你就直接用constexpr
限定它。你也可以定义一种constexpr
函数,这种函数应该足够简单以使得编译器在编译时就可以计算其结果。这样你就可以用constexpr
函数去初始化constexpr
变量。 -
因为你需要让编译器在编译时就可以计算出结果,所以声明为
constexpr
的时候所用到的类型要足够简单,我们把这些简单的、显而易见的、容易得到的类型成为字面值类型,其中包含算数类型 (内置类型中的bool
、char
、short
、int
、long
、long long
、float
、double
、long double
)、引用和指针等类型。自定义的类、IO 库、string 类则不属于字面值类型,也就不能被定义成constexpr
。事实上,constexpr
函数的内容仅可以有一条return
语句,而且return
的类型一定是字面值类型,形参的类型也都得是字面值类型。 -
constexpr
把它所定义的对象置为了顶层const
,所以const int *p = nullptr;
和
constexpr int* p = nullptr;
有着天壤之别,前者是指
p
是一个指向整形常量的指针,后者是指p
是一个指向整形的常量指针。 -
在使用
typedef
或者using
语句的时候要注意,如果你新定义的类型别名指代的是符合类型或者常量,那么在一个声明语句中使用新类型别名的时候,如果加上const
修饰符,那么语义会发生变化。例如:typedef char *pstring; const pstring cstr = 0; const pstring *ps;
当理解第二句的
const
的作用的时候,pstring
千万不能直接替换成char *
。这个时候要把pstring
当成普通的int
来理解,这个const
修饰pstring
使得cstr
是一个常量。因此第二句的含义是:声明一个常量,他是个指针,他指向一个char
类型的对象,并把它定义为空指针。因此第三句话的含义也显而易见了:声明一个指针,它自己并不是常量 (因为const
在星号的左边),但是它指向了一个常量,他指向的常量的具体类型是pstring
,也就是一个指向一个char
对象的常量指针。第三句话有点绕,简单说就是ps
是一个指向指向char
的常量指针 的变量指针。 -
如果在
auto
语句中连着声明定义多个变量,那么这些变量要是同一个类型。 -
在
auto
语句中,如果用一个引用来初始化一个变量,那么这个变量的类型和被引用的对象的一样:int i = 0, &r = i; auto a = r;
这里的
a
的类型是int
。 -
auto
一般会忽略掉顶层const
,但是会保留底层const
。如果你希望auto
可以推断出顶层const
,那么你要明确写出const
限定符:const int ci = i, &cr = ci;// ci 是顶层,cr 是底层 auto b = ci; // b 不是常量,是一个 int auto c = cr; // c 不是常量,是一个 int auto d = &i; // d 不是常量,是一个 int * auto d = &i; // d 不是常量,是一个 const int * const auto f = ci; // f 是一个 const int const auto g = &ci; // g 是一个 const int *const //由于引用本身就是底层 const,所以会被保留下来 auto &h = ci; // h 是一个 const int & auto &j = 29; // 编译错误:不能给非常量引用绑定字面值 const auto &k = 29; // 编译通过:写出了 const 才能绑定字面值 decltype() 可以分析表达式的类型并且返回它。例如: ```C++ decltype(f()) = x; // x 的类型与函数 f() 的返回类型相同 const int ci = 9, &ri = ci; int i = 1, *pi = &i; decltype(ci) x = 0; // x 是 const int decltype(ri) s = x; // s 是 const int &,只传标识符,那么就返回引用类型 decltype(ri+0) t = s; // s 是 const int,表达式会消除掉引用 decltype(i) y = x; // y 是 int decltype(i+3) z = x; // z 是 int decltype((i)) w = x; // w 是 int &,当变量外面套着一层括号的时候,将被视作表达式,返回引用 decltype(pi) p = &i; // p 是 int * decltype(*pi) r = i; // r 是 int &,解引用操作返回引用类型
总结一下:对于引用,直接传入
decltype
会返回引用类型,普通变量外面套一层括号传进去也会返回引用类型,引用放在表达式里面传入decltype
会返回被引用对象的类型。 -
decltype
可以保留顶层 const 和引用类型,但是auto
不可以。 -
预处理器是在编译之前执行的一段程序,可以部分的改变程序内容,包括例如
NULL
、assert()
、#include
、#ifdef
、#ifndef
、#pragma
这些东西。当预处理器看到#include
这个标记的时候,就会把指定的头文件的内容代替这句#include
语句。 而像ifdef
、endif
这些则叫做头文件保护符,define
用于定义预处理变量,ifdef
、ifndef
用于检查哪个预处理变量是否已经定义,一旦结果为真,则执行后续操作直到遇到#endif
位置。预处理变量包括头文件保护符无视 C++ 中的作用域的规则,并且在整个工程文件中必须唯一。习惯性的把所有头文件用头文件保护符包含住是值得推荐的。