Tips for C++ Primer Chapter 2 变量和基本类型
第2章 变量和基本类型
基本内置类型
基本内置类型:算术类型和空类型;算术类型又包括整型(字符和布尔类型也包括在内)和浮点型。
可寻址的最小内存块称为“字节”(byte),存储的基本单元称为“字”(word);一个字通常由几个字节组成。
1字节(byte)=8比特(bit),比特非0即1。
类型unsigned int可以缩写为unsigned。
int与singed int相同(除了布尔型和扩展字符类型(例如宽字符wchat_t)之外,其他的整型类型(例如short、long)也类似),但字符型例外,字符型有char、unsigned char、signed char三种类型,但char与signed char不一定相同;字符的表现形式只有两种:带符号的和无符号的,char实际上会表现为上述两者的其中一种,具体由编译器决定。
在算数表达式中不要使用char或bool。因为char在一些机器上可能是有符号的,而在另外的机器上可能是无符号的,使用char进行运算可能出错;如果硬要使用一个不大的整数,那么明确指定它为unsigned char或signed char。bool取值非0即1,不适宜用于算数表达式。
当赋给无符号类型一个超出它表示范围的值时,结果是初始值对无符号类型表示数值的总数取模后的余数。
例如:unsigned char占8个比特(1个字节),可以表示[0, 255]的共256个数值,当赋予区间外的一个值,假设是-1,则实际结果是(-1) % 256 = 255。
当赋给一个带符号类型一个超出它表示范围的值时,结果是未定义的,此时程序可能继续工作、崩溃或产生垃圾数据。
当一个算数表达式中既有无符号数又有int值时,那个int值会转换成无符号数。把int转换成无符号数的过程相当于把int赋给无符号类型。
切勿混用带符号类型和无符号类型。如果表达式既有带符号类型又有无符号类型,当带符号类型取值为负时会出现异常结果,这是因为带符号数会自动转换成无符号数。
例如:当a=-1,b=1,c=a*b,若a、b都是int,则c为-1;若a是int,b是unsigned int,则c的结果是(-1)%(2^32) * 1 = 4294967295(视当前机器上int所占bit数而定,这里假设int是32bit)。
整型字面值可写作十进制数、八进制数、十六进制数的形式。
0开头:八进制
0x、0X开头:十六进制
浮点数字面值可省略整数部分或小数部分(若该部分为0)。
例如:0.、.001
浮点数字面值默认是一个double。
可以以前缀或后缀指定字面值类型。
前缀
u:char16_t(Unicode16字符)
U:char32_t(Unicode32字符)
L:wchar_t(宽字符)
u8:char(UTF-8、仅用于字符串字面常量)
后缀
对整型字面值:
u、U:unsigned
l、L:long
ll、LL:long long
对浮点型字面值:
f、F:float
l、L:long double
变量
初始化与赋值是不同的概念。
初始化:创建变量时赋予其一个初始值
赋值:把对象的当前值擦除,并以一个新值替代
所以赋值的开销大于初始化。
默认初始化规则
如果内置类型的变量未被显式初始化,它的值由定义的位置决定。
定义在任何函数体外:被初始化为0
定义在函数体内:将不被初始化,其值未定义
注:指针类型并非基本内置类型,但也有上述特性。
如果类的对象没有显式初始化,其值由类确定。(通过类的默认构造函数、类内初始值等)
extern关键字
声明使得名字为程序所知,而定义创建与名字关联的实体。
如果想声明一个变量而非定义它,就在变量名前添加关键字extern,而不要显式地初始化变量。
例如:
extern int i; //声明i而非定义i
int j; //声明并定义j
包含显式初始化的声明即成为定义。
extern double pi = 3.14; //定义;允许在函数体外这么做,但extern关键字的存在就没意义了
不允许在函数体内部初始化一个由extern关键字标记的变量。
复合类型
引用本身并非对象,也没有实际的地址(若尝试用取地址符&取得某个“引用的地址”,实际取得的是所引用的对象的地址),它只是已存在的对象的别名。无法定义引用的引用,也无法定义指向引用的指针。
引用必须初始化,因为无法令引用重新绑定到另一对象。
所有引用的类型都要和与之绑定的对象严格匹配(有两个情况例外,将在后面提到)。
引用必须绑定到对象上,不能绑定到字面值或某个表达式的计算结果。
(可以将引用绑定到const对象上,就像绑定到其它的对象上一样,称之为“对常量的引用”。与普通引用不同的是,对常量的引用不能被用作修改它所绑定的对象。)
指针本身是一个对象,允许对指针赋值和拷贝,指针在其生命周期内可以指向不同对象。
指针无需在定义时赋初值。和基本内置类型相同,定义在所有函数体外的指针若未初始化,将有默认初值0,而块作用域内定义的指针将拥有一个不确定的值。
所有指针的类型都要和所指的对象严格匹配(有两个情况例外,将在后面提到)。
&、*出现在声明语句中,用来组成复合类型;&出现在表达式中,是一个取地址符;*出现在表达式中,是一个解引用符(或乘法运算符)。
nullptr = NULL = 0
void*是一种特殊的指针类型,可用于存放任意对象的地址。但不能直接操作void*指针所指的对象,因为对象的类型决定了能对对象所做的操作,而我们并不知道对象的类型。
指向指针的指针
指针是内存中的对象,像其它对象一样也有自己的地址,因此允许把指针的地址再存放到另一个指针当中。
通过*的个数区分指针的级别。**是指向指针的指针,***是指向指针的指针的指针,等等。
int i = 1024;
int *pi = &i; //pi指向一个int型的数
int **ppi = π //ppi指向一个int型的指针
解引用int型指针会得到一个int型的数,解引用一个指向指针的指针会得到一个指针。
这里有三种方式取得i的值:i、*pi、**ppi。
指向指针的引用
int i = 1;
int *p; // p是一个int型指针
int *&r = p; // r是一个对指针p的引用
r = &i; // 因为r是对指针p的引用,即r是p的别名,因此给r赋值&i就是令p指向i
*r = 0; // 解引用r得到i,也就是p指向的对象,因此i的值将改为0
const限定符
const对象一旦创建后其值就不能再改变,所以const对象必须初始化(初始值允许是任意的表达式,不一定是字面量常量)。
const int i = get_size(); // i将在运行时初始化
const int j = 1; // j将在编译时初始化
const int k; // 错误,k没有初始化
默认状态下,const对象仅在文件内有效;如果想在多个文件共享const对象,必须在变量的定义前添加extern关键字。
当多个文件出现了同名的const对象时,其实等同于在不同文件中分别定义了独立的变量。
如果想要:只在一个文件中定义const对象,而在其它多个文件中声明并使用它,则:对于const变量不管是声明还是定义都添加extern关键字,这样只需定义一次就够了。
【file_1.cc】
int get_size() {...}
extern const int bufSize = get_size(); //定义并初始化了一个常量(extern表示bufSize能被其它文件访问)
【file_1.h】
extern const int bufSize; //只是声明,这个bufSize与file_1.cc中定义的bufSize是同一个(extern表示bufSize并非本文件所有,它的定义在别处出现)
对const的引用
可以将引用绑定到const对象上,就像绑定到其它的对象上一样,称之为“对常量的引用”(“reference to const”,或“对const的引用”,或“常量引用”)。与普通引用不同的是,对常量的引用不能被用作修改它所绑定的对象。
const int ci = 1;
const int &r1 = ci; //正确;引用及其对应的对象都是常量
r1 = 2; //错误;r1是对常量的引用
int &r2 = ci; //错误;试图让一个非常量引用指向一个常量对象(设想:若这条语句合法,即默认“允许通过r2来修改ci的值”,而ci是常量,显然矛盾)
对const的引用与初始化
前面提到引用的类型必须与其所引用的对象一致,但是有两个例外。这里说它的第一种例外:
在初始化常量引用时,允许用任意表达式作为初始值(只要该表达式的结果能转换成引用的类型即可);
尤其,允许为一个常量引用绑定非常量的对象、字面值,或者一般的表达式。
int i = 1;
const int &r1 = i; //允许将const int&绑定到一个普通int对象上
const int &r2 = 1; //正确;r2是一个常量引用
const int &r3 = r1 * 2; //正确;r3是一个常量引用
int &r4 = r1 * 2; //错误;r4是一个普通的非常量引用,不允许绑定到字面值或某个表达式的计算结果
看看当一个常量引用被绑定到另外一种类型(合法)时发生了什么:
double dval = 3.14;
const int &ri = dval; //合法;ri值为3
ri引用了一个int型的数,但dval却是一个double型的浮点数而非整数。因此为了确保让ri绑定一个整数,编译器把上述代码变成了:
const int temp = dval; //由double型浮点数生成一个临时的int型整数常量
const int &ri = temp; //让ri绑定这个临时量(临时量是临时量对象的简称,是内存中的一个未命名对象)
顺便,对前例,探讨一下当ri不是常量(非法)时会发生什么:
如果ri不是常量,意味着允许对ri赋值(或者说通过ri修改其绑定的对象)。上例谈到,ri绑定到了一个临时量,而非dval。
我们既然让ri引用dval,肯定想通过ri改变dval的值,所以不会想要把引用绑定到临时量上,所以,C++认为“普通的非常量指针去绑定另外一种常量类型”的行为是非法的。
对const的引用可能引用一个并非const的对象
对const的引用的机制是:不允许通过引用来修改所引用的对象的值,但所引用的对象的值可通过其它途径修改。
int i = 1;
int &r1 = i; //引用r1绑定非const的对象i
const int &r2 = i; //r2也绑定对象i
r1 = 0; //合法;i的值修改为0
r2 = 0; //非法;r2是一个常量引用,不允许通过r2修改i的值
修改i的值的“其它途径”可以是各种合法的途径,例如直接对i赋值。
指针和const
与引用一样,也可以令指针指向常量或非常量。
类似于常量引用,指向常量的指针(pointer to const)不能用于改变其所指对象的值。
要想存放常量对象的地址,只能使用指向常量的指针。
const double pi = 3.14;
double *ptr = π //非法;ptr是一个普通指针
const double *cptr = π //合法
*cptr = 1.0; //非法
前面提到,指针的类型必须与其所指的对象的类型一致,但是有两个例外。这里先讨论第一种例外:
允许令一个指向常量的指针指向一个非常量对象。
double dval = 3.14; //dval是一个非常量对象
const double *cptr = &dval; //合法;但是不能通过cptr改变dval的值(与指向常量的引用类似,这里的dval仍然可能通过其它途径修改)
const指针(常量指针)
常量指针的意义是指针本身是常量。
注:注意“指向常量的指针”与“常量指针”相区别。前者机制是:不能通过指针改变所指对象的值;后者的机制是:不能改变指针本身的值(也就是存放在指针中的那个地址)。
注:在讨论“对const的引用”(reference to const,对常量的引用)时,说“对const的引用”简称“常量引用”,但严格来说并不存在“常量引用”。因为引用并不是一个对象,所以没办法让引用“恒定不变”。事实上,由于C++不允许改变引用所绑定的对象,所以从这层意义上理解,所有的引用都算是“常量”。所以以“常量引用”代称“对const的引用”未尝不可。
const double pi = 3.14; //pi是一个常量对象
const double *const pip = π //pip是一个指向常量对象的常量指针
从右往左,离pip最近的那个const说明pip本身是一个常量对象,对象的类型由声明符的其余部分决定。声明符中的下一个符号是*,说明pip是一个常量指针,最后const double确定了常量指针指向的是一个double型常量对象。
顶层const(top-level const)与底层const(low-level const)
当仅对指针而言:
顶层const:指针本身是个常量
底层const:指针所指的对象是一个常量
更一般的:
顶层const可以表示任意对象是常量,这一点对任何数据类型都适用,比如算数类型、类、指针等;
底层const则与指针和引用等复合类型的基本类型部分有关。
特殊的是:
指针既可以是顶层const也可以是底层const;用于声明引用的const都是底层const。
int i = 0;
int *const p1 = &i; //不能改变p1的值,顶层const
const int ci = 1; //不能改变ci的值,顶层const(PS:事实上,“const int ci = 1”等价于“int const ci = 1”)
const int *p2 = &ci; //允许改变p2的值,底层const
const int *const p3 = p2; //靠右的是顶层const,靠左的是底层const
const int &r = ci; //用于声明引用的const都是底层const
当执行对象的拷贝操作时,常量的顶层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上
思考一下:为何“非常量可以转换成常量,反之不行”?
看第一条语句(非法:常量不可转为非常量),假设第一条语句合法,也就是说,假设允许以p3初始化p,看会发生什么:
p3包含底层const定义,即p3指向的对象是常量;而p不包含底层const定义,p指向的是一个普通的(非常量)整数,也就是说:“可能通过p修改它所指的对象的值”,但所指的对象是常量,这是矛盾的。
constexpr和常量表达式
常量表达式(const expression):值不会改变,且在编译过程就能得到计算结果的表达式。
const int i = 1; //i是常量表达式
const int j = i + 1; //j是常量表达式
int k = 2; //k不是常量表达式(值可能改变)
const int sz = get_size(); //sz不是常量表达式(在运行过程才能得到具体值)
constexpr变量
C++11可以用constexpr来明确声明常量表达式。
constexpr int ce = 1; //ok
constexpr int sz = size(); //仅当size()函数是一个constexpr函数时才是一条合法语句
字面值类型
字面值类型:包含算术类型、引用、指针、字面值常量类、枚举类型。
字面值类型可以定义成constexpr。
字面值类型定义constexpr的特殊情况
指针和引用定义成constexpr时,它们的初始值有严格限制:
关于指针:
一个constexpr指针的初始值必须是0(nullptr),或者是存储于某个固定地址中的对象;
故constexpr指针不能指向函数体内定义的变量,可以指向函数体外的变量;
函数体内的local static object局部静态对象与函数体外的对象一样有固定地址,故constexpr指针能指向局部静态对象。
在constexpr的声明中如果定义了一个指针,限定符constexpr仅对指针有效,而与指针所指的对象无关;
const int *p = nullptr; //p是一个指向整型常量的指针(p本身可被改变)
constexpr int *q = nullptr; //q是一个指向整数的常量指针(q本身不可被改变)
原因是:constexpr把它所定义的对象置为了顶层const。
与const指针类似,constexpr指针既可以指向常量,也可以指向非常量。(指向非常量时,必须是存储于某个固定地址中的对象)
constexpr int i = 1; //i是整型常量(i必须定义在函数体外,否则编译错误)
constexpr const int *p = &i; //p是常量指针,指向整型常量i
注意这个特殊写法,若以const限定符声明常量指针,const的位置紧邻p的左边:const int *const p = &i;
而这里由于constexpr的特殊性(限定符constexpr仅对指针有效)而有其特殊语法:constexpr const int *p = &i。
关于引用:
constexpr引用只能绑定到局部静态对象。
处理类型
类型别名
方式一:使用关键字typedef。
typedef double wages; //wages是double的同义词
方式二:(C++11)使用别名声明(alias declaration)。
using wages = double;
指针、常量和类型别名
如果某个类型别名指代的是复合类型或常量,那么把它用到声明语句里会产生意想不到的后果。
typedef char *pstring; //类型pstring是类型char*的别名
const pstring cstr = 0; //cstr是指向char的常量指针(指针本身是常量)
const pstring *ps; //ps是一个指针,它所指的对象是一个指向char的常量指针
上述两条语句的基本数据类型都是const pstring,const是对类型pstring的修饰,类型pstring是指向char的指针,因此const pstring就是指向char的常量指针,而非指向常量字符对象的指针。
一个错误的理解方式:
尝试把类型别名替换成它本来的样子,以理解该语句的含义。(这种方式是错误的)
尝试把pstring替换成char*,有:
const char *cstr = 0;
这样看来,*成了声明符的一部分,用以说明cstr是一个指针,const char成了基本数据类型,const是对类型char的修饰,这条语句的含义就是“指向const char的(普通)指针”。这与替换前的实际意义截然不同。
auto类型说明符
使用auto也允许在一条语句中声明多个变量,但是,因为一条语句只能有一个基本数据类型,所以该语句中的所有变量的初始基本数据类型都必须相同。
auto i = 0, *p = &i; //合法;i是int型、p是int型指针
auto sz = 0, pi = 3.14; //非法;sz是int,pi是double,类型不一致
const int ci = i;
auto &n = i, *p2 = &ci; //非法;i的类型是int,而ci的类型是const int
复合类型、常量和auto
编译器推断出来的auto类型有时候和初始值的类型并不完全一样,编译器会适当地改变结果类型使其更符合初始化规则。
例如:
使用引用其实就是使用引用的对象,特别是当引用作为初始值时,真正参与初始化的其实是引用对象的值。此时编译器以引用对象的类型作为auto的类型。
int i = 0, &ri = i;
auto a = ri; //a是一个整数,因为ri是i的别名,而i是一个整数
其次,auto一般会忽略顶层const,保留底层const。(后面会谈到特例)
例如:
当初始值是一个指向常量的指针时:
const int ci = i, &cr = ci; //ci是一个常量,cr是对常量的引用
auto b = ci; //b是一个整数(非const),因为ci的顶层const特性被忽略了
auto c = cr; //c是一个整数(非const),因为cr是ci的别名,ci本身是一个顶层const,而顶层const被忽略了
auto d = &i; //d是一个整型指针,因为一个整数的地址也就是指向整数的指针
auto e = &ci; //e是一个指向整数常量(const)的指针,因为ci是一个常量对象,而对常量对象取地址是一种底层const,且auto会保留底层cons
如果希望推断出的auto类型是一个顶层const,需要明确指出:
const auto f = ci; //ci的推演类型为int(而非const int),但明确指出后,f是const int
前面说到“auto一般会忽略顶层const”,此例即为特例:
当设置一个类型为auto引用时,初始值的顶层const仍然保留。
auto &g = ci; //g绑定到ci,g是一个整型常量引用(reference to const,对const的引用),因为ci的顶层const特性被保留了
decltype类型指示符
decltype:编译器分析表达式并得到它的类型,却不实际计算表达式的值。
decltype(f()) sum = x; //sum的类型被设定为函数f的返回类型(注意:编译器并不实际调用函数f)
decltype处理顶层const和引用的方式与auto不同。如果decltype使用的表达式是一个变量,则decltype返回该变量的类型(包括顶层const和引用在内)。
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的类型是const int&,是一个引用,引用必须初始化
引用从来都作为其所指对象的同义词出现,只有用在decltype处是一个例外。
也就是说:不要因为cj引用ci,就认为“decltype(cj) y = x”等同于“decltype(ci) y = x”。
decltype和引用
如果decltype使用的表达式不是一个变量,则decltype返回表达式结果的对应类型。
int i = 1, *p = &i, &r = i;
decltype(r+0) b; //合法;加法的结果是int,因此b是一个未初始化的int
decltype(*p) c; //非法;c是int&(而非int;下面会解释),必须初始化
解释:因为r是一个引用,所以decltype(r)的结果是引用类型(而不是引用所指对象的类型)。如果想让结果是r所指对象的类型,可以把r作为表达式的一部分,如r+0,显然这个表达式的结果是一个具体值而非一个引用;
如果表达式的内容是解引用操作,则decltype将得到引用类型。解引用指针可以得到指针所指的对象,而且还能给这个对象赋值。因此,decltype(*p)的结果是int&,而非int。
给变量加上一层或多层括号,编译器就会把它当作一个表达式。变量是一个可以作为赋值语句左值的特殊表达式,所以这样的decltype就会得到引用类型。
int i = 1;
decltype(i) d; //合法;d是一个(未初始化的)int
decltype((i)) e; //非法;e是int&,必须初始化
总结:decltype((variable))的结果永远是引用,而decltype(variable)的结果仅当variable本身就是一个引用的时候才是引用。
补充例子:
赋值是会产生引用的一类表达式,引用的类型就是左值的类型。例如:如果i是int,则表达式i=x的类型是int&。
int a = 3, b = 4;
decltype(a) c = a; //c是int,值为3
decltype(a=b) d = a; //d是int&,值为3
d的值为何不是4?
由于“a=b”,故a的值变为4,而d是a的引用,故d的值是4?这是错误的,“a=b”只是用于编译器推断表达式的类型,实际上该表达式并不会真的执行,所以a的值仍旧是它原来的值3。
自定义数据结构
C++11允许为类内数据成员提供一个类内初始值(in-class initializer)。创建对象时,类内初始值将用于初始化数据成员。没有初始值的成员将被默认初始化(前面讨论过各种变量的默认初始化的规则)。
struct Sales_data
{
std::string bookNo; //将会进行默认初始化;将会初始化为空字符串
unsigned sold = 0; //将由类内初始值初始化;将会初始化为0
double revenue; //将会进行默认初始化(结果:其值未定义,可能是乱七八糟的值)
};
头文件保护符
【Sales_data.h】(类通常定义在头文件中,头文件的名字应与类名相同)
#ifndef SALES_DATA_H
#define SALES_DATA_H
//#include <...>
struct Sales_data {
//...
};
#endif
防止头文件被多次include,避免重复定义。
预处理指令#ifdef用于判断给定预处理变量是否已经定义;#ifndef用于判断给定预处理变量是否尚未定义。