第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用于判断给定预处理变量是否尚未定义。

 

内容来源于网络如有侵权请私信删除
你还没有登录,请先登录注册
  • 还没有人评论,欢迎说说您的想法!

相关课程