[C++ Primer Note1] 变量和基本类型
本笔记仅针对有一定编程基础的读者,不赘述通俗语法,而是聚焦于一些容易遗漏的细节和值得一录的作者表述,并仅仅罗列陈述,尽量不夹带私货。同时部分原文所述很可能因为时间的发展以及环境不同而与实际情况略有出入,请尽管指出。
- 十进制字面值的类型是int,long,long long尺寸最小的,八进制和十六进制字面值是能容纳其数值的int,unsigned int,long,unsigned long,long long和unsigned long long中尺寸最小的
- 字面值常量的形式和值决定了它的数据类型
- 浮点型字面值表现为一个小数或科学计数法,默认为double类型
- 字符字面值为char类型,字符串字面值是由常量字符构成的数组
- nullptr是指针字面值(表示空指针,C++11 标准)
- C++中变量的初始化(创建时获得值)和赋值完全不同,初始化是创建变量时赋予一个初始值,而赋值是把当前值擦除,以新值代替。
- C++中的对象往往指的是一块能存储数据并具有某种类型的内存空间,并不一定与类关联在一起。(与Java中表述的对象不同)
- 如果定义时没有指定初值,则变量被默认初始化,默认值由变量类型和定义变量的位置决定。
- 如果是内置类型的变量未显式初始化,它的值由定义的位置决定。定义于任何函数体外被初始化为0,而在函数体内部则不被初始化,值是未定义的(危险!),建议初始化每一个内置类型的变量。
- 每个类各自决定初始化对象的方式,是否允许不经初始化就定义对象也由类自己决定。
- 为了允许把程序拆分成多个逻辑部分来编写,C++语言支持分离式编译(seperate compilation),即将程序分割成若干个文件,每个文件可被独立编译。
- 为了支持分离式编译,C++将声明和定义分开来。声明使得名字为程序所知,一个文件如果想使用别处定义的名字则必须包含对那个名字的声明。而定义负责创建与名字关联的实体(申请存储空间,可能赋一个初始值等)
- 如果想声明一个变量而非定义它,就在变量名前添加extern,而且不要显式初始化它。任何包含了显式初始化的声明即成为定义。
- C++是一种静态类型(statically typed)语言,其含义是在编译阶段检查类型是否支持要执行的运算,前提是编译器必须知道每个实体对象的类型。
- 使用作用域操作符 :: 访问全局变量
- 一条声明语句由一个基本数据类型和紧随其后的一个声明符(declarator)列表组成,一种肤浅的认识是声明符就是变量名,此时变量的类型就是声明的基本类型,但其实还有更复杂的声明符,它基于基本数据类型得到更复杂的类型,并把它指定给变量(比如指针和引用)。
- 在定义引用时,程序把引用和它的初始值绑定(bind)在一起,而不是像初始化一样拷贝,一旦初始化完成,引用将和它的初始值对象一直绑定在一起,所以引用必须初始化。
- 引用并非对象,它只是为一个已经存在的对象所起的一个别名。且引用只能和对象绑定,而不能与某个字面值或者表达式计算结果绑定。
- 指针和引用不同,其本身就是一个对象,允许对其赋值和拷贝。如果指针指向了一个对象,通过解引用符*得出所指的对象。
- 空指针不指向任何对象,在试图使用一个指针之前可以首先检查它是否为空。
- 生成空指针的几种方法:
// C++ 11 标准
int *p1 = nullptr; // 等价于 int *p1 = 0;
int *p2 =0;
// 需要 #include cstdlib
int *p3 = NULL; // 等价于 int *p3 = 0;
nullptr是一种特殊类型的字面值,它可以被转换成任意其他的指针类型。过去的程序还会用到一个名为NULL的预处理变量(preprocessor variable)来给指针赋值,这个变量在cstdlib中定义,它的值就是0。预处理变量不属于命名空间std,所以可以直接使用。当用到一个预处理变量时,预处理器会自动将它替换为实际值,因此用NULL初始化指针和用0初始化指针效果完全一样,新标准下,现在的C++程序最好使用nullptr,避免NULL。
把int变量直接赋给指针是错误的操作,即使int变量的值恰好为0。
- 在大多数编译器环境下,如果使用了未经初始化的指针,则该指针所占空间的当前内容将被看作是一个地址值,访问该指针相当于访问一个不存在位置上的不存在对象,因此建议初始化所有的指针
- 指针可以用在条件表达式中,如果其值为0,条件取false
- void* 是一种特殊的指针类型,可以存放任意对象的地址。不能直接操作void* 指针所指的对象,因为我们并不知道这个对象是什么类型,也就无法确定在其上的操作是否合法。
- 经常会有错误观点认为,类型修饰符(*或&)作用于本次定义的全部变量,但实际上形如 int* 并不是基本类型,int才是,* 只是修饰了紧随其后的那一个变量而已。
- 声明符中修饰符的个数并没有限制,比如**(指向指针的指针),比如*&(指针的引用),一个诀窍在于从右向左阅读一个变量的定义,离变量名最近的符号对变量的类型有最直接的影响。
- const可以使对象一旦创建后其值不能改变,所以const对象必须初始化。
- 当以编译时初始化的方式定义一个const对象时(比如初始化为字面值而不是函数返回值),编译器将在编译过程中用到该变量的地方都替换成对应的值。如果程序包含多个文件,则每个用到了const对象的文件都必须能访问它的初始值才行,同时为了避免对同一变量的重复定义,默认情况下const对象仅在文件内有效,当多个文件出现了同名的const对象时,等同于在不同文件中分别定义了独立的变量。如果想让const对象想其他对象一样工作(一个文件中定义,在其他文件声明并使用),只需要不管声明还是定义都添加extern关键字,这样只需定义一次就可以了。
- const变量只能被同样是const的引用引用,但是const引用也可以引用变量,但不能通过它修改绑定对象的值
- 初始化const引用时允许用任意表达式作为初始值,只要能转换成引用的类型即可。
double dval = 3.14;
const int &ri = dval;
此处ri引用了一个int型的数,但dval却是一个双精度浮点数,为了确保让ri绑定一个整数,编译器把上述代码变成了如下形式:
const int temp = dval; // 由双精度浮点数生成一个临时的整型常量
const int &ri = temp; // 让ri绑定这个临时量
在这种情况下,ri绑定了一个临时量(temporary)对象(编译器用来暂存表达式求值结果而临时创建的一个未命名的对象空间)。
如果ri不是const引用,就允许对ri赋值,但此时绑定的是一个临时量而不是dval,因此C++将这种行为视作非法。
- const引用仅对引用可参与的操作做出了限定,对于引用绑定的对象本身不做限定
- 指向常量的指针(pointer to const)用于存放常量对象的地址,不同通过它去改变所指对象的值,但同样可以指向非常量对象。
- 指针本身是对象,允许把指针本身定为常量,常量指针(const pointer)必须初始化,定义时将 * 放在const关键字之前。
- 所谓指向常量的指针或引用,其实只是它们的“自以为是”,自觉不去修改所指对象的值。
- 顶层const(top-level const):表示任意对象是常量,底层const(low-level const):表示所指的对象是常量。执行拷贝操作时,底层const的限制不可忽视。
- 常量表达式是指值不会改变并且在编译过程就能得到计算结果的表达式(比如字面值和用常量表达式初始化的const对象)。C++新标准规定,允许将变量声明为constexpr类型以便由编译器来验证变量的值是否是一个常量表达式,如果认定一个变量是一个常量表达式,就把它声明成constexpr。
- constexpr把它所定义的对象置为了顶层const(特别注意指针)
- 类型别名是一个名字,它是某种类型的同义词。有两种方法可用于定义类型别名。
typedef double wages; // wages是double的同义词
typedef wages base, *p; // base是double的同义词,p是double*的同义词
其中关键字typedef作为声明语句中的基本数据类型,这里的声明符也可以包含类型修饰,从而由基本类型构造出复合类型来。
新标准规定了一种新的方法,使用别名声明(alias declaration)定义类型别名。
using SI = Sales_item; // SI是Sales_item的同义词
如果某个类型别名指代的是复合类型或常量,那么把它用到声明语句会产生意想不到的后果。
typedef char *pstring;
const pstring cstr=0; //cstr是一个指向char的常量指针
const是对给定类型的修饰,而此处pstring的基本数据类型是指针,如果用char *替换重写语句,数据类型就变成了char,* 成为了声明符的一部分。所以前者声明了指向char的常量指针,后者声明了一个指向const char的指针。
- 编程时常需要把表达式的值赋给变量,这就要求在声明变量时清楚知道表达式的类型,但这有时根本做不到。C++11新标准引入了auto类型说明符,它能让编译器替我们去分析表达式所属的类型。显然,auto定义的变量必须有初始值。其次,auto一般会忽略掉顶层const,同时底层const会保留下来。
- 有时希望从表达式的类型推断出要定义的变量的类型,但是不想用该表达式的值初始化变量。C++11新标准引入第二种类型说明符decltype,它的作用是选择并返回操作数的数据类型。
decltype(f()) sum=x; // sum的类型就是函数f的返回类型
如果decltype使用的表达式是一个变量,则返回该变量的类型(包括顶层const和引用在内)
如果decltype使用的表达式不是一个变量,则decltype返回表达式结果对应的类型。有些表达式将返回一个引用类型,一般这种情况发生时,意味着表达式结果对象能作为一条赋值语句的左值。
如果变量名加上了一对括号,编译器会把它当成一个表达式,从而decltype得到引用类型。
- struct体结束后必须写一个分号,因为其后可以紧跟变量名(声明符)表示对这类型对象的定义。一般来说,最好不要把对象的定义和类的定义放在一起,这么做相当于把不同实体的定义混在了一条语句中。
- 为了确保各个文件中类的定义一致,类通常被定义在头文件中,而且类所在头文件的名字应该与类的名字一样。
- 确保头文件多次被包含仍能安全工作的常用技术是预处理器(preprocessor),预处理器是在编译之前执行的一段程序,可以部分地改变我们所写的程序。之前已经用到了一项预处理器功能#include,当预处理器看到该标记会用指定的头文件内容代替#include。
还有一项常见的预处理功能是头文件保护符(header guard),头文件保护符依赖于预处理变量。#define设定预处理变量,#ifdef和#ifndef则检查预处理变量是否已经定义。
#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,编译器都会忽略类的定义。
整个程序中的预处理变量都必须唯一,通常基于头文件中类的名字构建保护符名字,一般全部大写。