C++ Primer 学习笔记(2)

第二章 变量和基本类型

2.1 基本内置类型

C++定义了一套包括算术类型空类型在内的基本数据类型。

算术类型分为两类:整型(整数型、字符、布尔值)和浮点型(浮点数)

一个char的大小和一个机器字节一样。

内置类型的机器实现:可寻址的最小内存块称为“字节”,字节由8比特构成;存储的基本单元称为“字”,字则由32或64比特构成,也就是4或8字节。

为了赋予内存中某个地址明确的含义,必须首先知道存储在该地址的数据的类型。类型决定了数据所占的比特数以及该如何解释这些比特的内容。

除去布尔类型和扩展的字符型(wchar_t、char16_t、char32_t)之外,其他整型可以划分为带符号的和无符号的两种。

类型int、short、long、long long都是带符号的,通过在这些类型名前添加unsigned就可以得到无符号类型,类型unsigned int可以缩写为unsigned。

与其他整型不同,字符型被分为了三种:char、signed char和unsigned char。类型char和类型unsigned char并不一样,类型char实际上会表现为带符号的和无符号的中的一种,具体是哪种由编译器决定。

如何选用类型的经验准则:

1、明确知晓数值不可能为负时,选用无符号类型;

2、long一般和int有一样的尺寸,如果你的数值超过了int的表示范围,选用long long;

3、在算数表达式中不要使用char或bool。因为类型char在一些机器上是有符号的,而在另一些机器上又是无符号的,所以如果使用char进行运算特别容易出问题,使用时需要明确指定它的类型是signed char或者unsigned char;

4、执行浮点数运算选用double,因为float通常精度不够而且双精度浮点数和单精度浮点数的计算代价相差无几。

2.1.2 类型转换

当在程序的某处我们使用了一种类型而其实对象应该取另一种类型时,程序会自动进行类型转换。

1、把一个浮点数赋给整数类型时,结果值仅保留浮点数中小数点之前的部分;

2、赋给无符号类型一个超出它表示范围的值时,结果是初始值对无符号类型表示数值总数取模后的余数;

3、赋给带符号类型一个超出它表示范围的值时,结果是未定义的。此时,程序可能继续工作、可能崩溃,也可能生成垃圾数据。

含有无符号类型的表达式:

切勿混用带符号类型和无符号类型,如果表达式里既有带符号类型又有无符号类型,带符号数会自动地转换成无符号数。当从无符号数中减去一个值时,不管这个值是不是无符号数,我们都必须确保结果不能是一个负值,否则结果就是取模后的值。

在循环条件中谨慎使用无符号数,可能会造成死循环的问题。

2.1.3 字面值常量

每个字面值常量都对应一种数据类型,字面值常量的形式和值决定了它的数据类型。我们可以通过后缀代表对应的字面值类型。

整型和浮点型字面值

以0开头的整数代表八进制数,以0x或0X开头的代表十六进制数。

十进制字面值是带符号数,八进制和十六进制字面值既可能是带符号的也可能是无符号的。它们类型对应整数型(ing、long、long long)(带符号的或无符号的)中能容纳下当前数值的尺寸最小的那个。如果一个字面值连与之关联的最大的数据类型都放不下,将产生错误。类型short没有对应的字面值。

浮点型字面值表现为一个小数或以科学计数法表示的指数,其中指数部分用E或e标识。默认的,浮点型字面值是一个double。

字符和字符串字面值

由单括号括起来的一个字符称为char型字面值,双引号括起来的零个或多个字符则构成字符串型字面值。

编译器在每个字符串的结尾处添加一个空字符('\0'),因此,字符串字面值的实际长度要比它的内容多1。

如果两个字符串字面值位置紧邻且仅由空格、缩进和换行符分隔,则它们实际上是一个整体。

转义序列

有两类字符程序员不能直接使用:一类是不可打印的字符(如退格或其他控制字符);另一类是在C++中有特殊含义的字符(单引号、双引号、问号、反斜线)。在这些情况下需要用到转义序列,转义序列均以反斜线作为开始。

泛化的转义序列

1、\x后紧跟1个或多个十六进制数字,\x要用到后面跟着的所有数字

2、\后跟1个、2个或3个八进制数字,八进制数字超过3个,只有前3个数字与\构成转义序列

指定字面值的类型

通过添加前缀和后缀,可以改变整型、浮点型和字符型字面值的默认类型。

字符和字符串字面值

字符和字符串字面值
前缀含义类型
uUnicode 16字符char16_t
UUnicode 32字符char32_t
L宽字符wchar_t
u8UTF-8(仅用于字符串字面常量)char
整型字面值
后缀最小匹配类型
u or Uunsigned
l or Llong
ll or LLlong long
浮点型字面值
后缀类型
f or Ffloat
l or Llong double

布尔字面值和指针字面值

true和false是布尔类型的字面值;nullptr是指针字面值。

2.2 变量

变量提供一个具名的、可供程序操作的存储空间,C++中的每个变量都有其数据类型。

2.2.1 变量定义

初始值

当对象在创建时获得了一个特定的值,我们说这个对象被初始化了。

初始化和赋值是两个完全不同的操作。初始化的含义是创建变量时赋予其一个初始值,而赋值的含义是把对象的当前值擦除,而以一个新值来替代。

列表初始化

int units_sold = 0;

int units_sold = {0};

int units_sold(0);

int units_sold{0};

使用花括号来初始化变量的形式被称为列表初始化。无论是初始化对象还是某些时候为对象赋新值,都可以使用一组由花括号括起来的初始值。

如果我们使用列表初始化且初始值存在丢失信息的风险,则编译器将报错,比如:

long double ld = 3.1415926536;

int a{ld}, b = {ld};

默认初始化

如果定义变量时没有指定初值,则变量被默认初始化,此时变量被赋予了“默认值”。默认值到底是什么由变量类型决定,同时定义变量的位置也会对此有影响。

内置类型:定义于任何函数体之外的变量被初始化为0,定义域函数体内的内置类型变量将不被初始化,一个未被初始化的内置类型变量的值是未定义的。

:每个类各自决定其初始化对象的方式以及是否允许不经初始化就定义对象。如果类允许,它将决定对象的初始值到底是什么。绝大多数类都支持无需显式初始化而定义对象,这样的类提供了一个合适的默认初始值。

使用未初始化变量的值是一种错误的编程行为并且很难调试,建议初始化每一个内置类型的变量。

2.2.2 变量声明的定义的关系

为了支持分离式编译,C++语言将声明和定义区分开来。

声明使得名字为程序所知,一个文件如果想使用别处定义的名字则必须包含对那个名字的声明。

定义负责创建与名字关联的实体。

变量声明规定了变量的类型和名字,定义除此之外还申请存储空间,也可能会为变量赋一个初始值。

如果想声明一个变量而非定义它,就在变量名前添加关键字extern,而且不要显式地初始化变量。

任何包含了显式初始化的声明即成为定义,即使变量由extern标记。如果在函数内部试图初始化一个由extern关键字标记的变量,将引发错误。

变量的定义必须出现在且只能出现在一个文件中,而其他用到该变量的文件必须对其进行声明,却绝对不能重复定义。变量能且只能被定义一次,但是可以被多次声明。

2.2.3 标识符

C++的标识符由字母、数字和下划线组成,其中必须以字母或下划线开头,标识符的长度没有限制,但是对大小写字母敏感。用户自定义的标识符中不能连续出现两个下划线,也不能以下划线紧连大写字母开头,此外,定义在函数体外的标识符不能以下划线开头

2.2.4 名字的作用域

C++语言中大多数作用域都以花括号分隔。

全局作用域(定义在函数体之外的名字具有全局作用域,如函数名)和块作用域。

一般来说在对象第一次被使用的地方附近定义它是一种好的选择。

嵌套作用域

作用域能彼此包含:内层作用域和外层作用域。

作用域中一旦声明了某个名字,它所嵌套的所有作用域中都能访问该名字。同时,允许在内层作用域中重新定义外层作用域已有的名字。

可以使用作用域操作符来覆盖默认的作用域规则,如作用域::变量名。因为全局作用域本身并没有名字,所以当作用域操作符的左侧为空时,向全局作用域发出请求获取作用域操作符右侧名字对应的变量。

2.3 复合类型

复合类型是指基于其他类型定义的类型。

一条声明语句由一个基本数据类型和紧随其后的一个声明符列表组成。每个声明符命名了一个变量并指定该变量为与基本数据类型相关的某种类型。声明符不仅仅是变量名。

2.3.1 引用

定义引用时,程序把引用和它的初始值绑定在一起,而不是将初始值拷贝给引用。一旦初始化完成,引用将和它的初始值一直绑定在一起。因为无法令引用重新绑定到另一个对象,因此引用必须初始化。

引用并非对象,它只是为一个已经存在的对象所起的另外一个名字。因为引用本身不是一个对象,所以不能定义引用的引用。

定义一个引用之后,对其进行的所有操作都是在与之绑定的对象上进行的。

所有引用的类型都要和与之绑定的对象严格匹配,而且,引用只能绑定在对象上,而不能与字面值或某个表达式的计算结果绑定在一起。

2.3.2 指针

指针与引用类似,实现了对其他对象的间接访问,但也有很多不同点:

1、指针本身就是一个对象,允许对指针赋值和拷贝,在指针的生命周期内它可以先后指向几个不同的对象;

2、指针无需在定义时赋初值,和其他的内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值

因为引用不是对象,没有实际地址,所以不能定义指向引用的指针。

指针的类型也要和它指向的对象严格匹配。

如果指针指向了一个对象,则允许使用解引用符(操作符*)来访问该对象,仅适用于那些确实指向了某个对象的有效指针。

空指针

空指针不指向任何对象,在试图使用一个指针之前代码可以首先检查它是否为空。

int *p1 = nullptr;

int *p2 = 0;

int *p3 = NULL;

字面值nullptr可以被转换成任意其他的指针类型。

建议初始化所有的指针,并且在可能的情况下,尽量等定义了对象之后再定义指向它的指针。如果实在不清楚指针应该指向何处,就把它初始化为nullptr或0。

任何非0指针对应的条件值都是true。对于两个类型相同的合法指针,可以进行比较,比较的结果是布尔类型。两个空指针也可以比较,结果是相等。

void*指针

void*是一种特殊的指针类型,可用于存放任意对象的地址。

1、可以用来和别的指针比较;

2、作为函数的输入和输出;

3、赋给另一个void*指针

不能直接操作void*指针所指的对象,因为我们并不知道这个对象到底是什么类型,也就无法确定能在这个对象上做哪些操作。

2.3.3 理解复合类型的声明

变量的定义包括一个基本数据类型和一组声明符,类型修饰符(*或&)是声明符的一部分。在同一条定义语句中,虽然基本数据类型只有一个,但是声明符的形式却可以不同。

推荐把类型修饰符和变量标识符写在一起。

面对一条比较复杂的指针或引用的声明语句时,从右向左阅读有助于弄清楚它的真实含义。比如 int *&r:离变量名最近的符号对变量的类型有最直接的影响,因此r是一个引用。声明符的其余部分用以确定r引用的类型是什么,此例中的符号*说明r引用的是一个指针。最后,声明的基本数据类型部分指出r引用的是一个int指针。

2.4 const限定符

const对象一旦创建后其值就不能再改变,所以const对象必须初始化。一如既往,初始值可以使任意复杂的表达式。const对象的常量特征仅仅在执行改变的操作时才会发挥作用。

利用一个对象去初始化另外一个对象,则它们是不是const都无关紧要:

int i = 42;
const int ci = i;
int j = ci;

编译器在编译过程中会把用到const变量的地方都替换成对应的值。为了执行上述替换,编译器必须知道变量的初始值。如果程序包含多个文件,则每个用了const对象的文件都必须得能访问到它的初始值才行。要做到这一点,就必须在每一个用到变量的文件中都有对它的定义。为了支持这一用法,同时避免对同一变量的重复定义,默认情况下,const对象被设定为仅在文件内有效。当多个文件中出现了同名的const变量时,其实等同于在不同文件中分别定义了独立的变量。

如果想在多个文件之间共享const对象,必须在变量的定义之前添加extern关键字。

2.4.1 const的引用

可以把引用绑定在const对象上,就像绑定到其他对象上一样,我们称之为对常量的引用。与普通引用不同的是,对常量的引用不能被用作修改它所绑定的对象。

不允许让一个非常量引用指向一个常量对象。

常量引用仅对引用可参与的操作做出了限定,对于引用的对象本身是不是一个常量未做限定。

之前提到,引用的类型必须与其所引用对象的类型一致,但是有两个例外。第一种例外情况就是在初始化常量引用时允许用任意表达式作为初始值,只要该表达式的结果能转换成引用的类型即可。尤其,允许为一个常量引用绑定非常量的对象、字面值,甚至是个一般表达式。这种情况下,常量引用绑定了一个临时量对象。如果不是常量引用时,就允许对其赋值,这样就会改变所引用对象的值,而此时绑定的对象是一个临时量,c++把这种行为归为非法。

2.4.2 指针和const

类似于常量引用,指向常量的指针不能用于改变其所指对象的值。要想存放常量对象的地址,只能使用指向常量的指针。

和常量引用一样,指向常量的指针也允许指向一个非常量对象。所谓指向常量的指针仅仅要求不能通过该指针改变对象的值,而没有规定那个对象的值不能通过其他途径改变。

const指针

允许把指针本身定位常量,常量指针必须初始化,而且一旦初始化完成,则它的值(也就是存放在指针中的那个地址)就不能再改变了,不变的是指针本身的值而非指向的那个值。

int errNumb = 0;
int *const curErr = &errNumb; // curErr将一直指向errNumb
const double pi = 3.14159;
const double *const pip = π // pip是一个指向常量对象的常量指针

2.4.3 顶层const

名词顶层const表示指针本身是个常量,而用名词底层const表示指针所指的对象是一个常量。

更一般的,顶层const可以表示任意的对象是常量,这一点对任何数据类型都适用,如算术类型、类、指针等。底层const则与指针和引用等复合类型的基本类型部分有关。比较特殊的是,指针类型既可以是顶层const也可以是底层const。

当执行对象的拷贝操作时,常量是顶层const还是底层const区别明显。其中,顶层const不受什么影响,因为执行拷贝操作并不会改变被拷贝对象的值,因此,拷入和拷出的对象是否是常量都没什么影响。另一方面,底层const的限制却不能忽视。当执行对象的拷贝操作时,拷入和拷出的对象必须具有相同的底层const资格,或者两个对象的数据类型必须能够转换。一般来说,非常量可以转换为常量,反之则不行。

2.4.4 constexpr 和常量表达式

常量表达式是指值不会改变并且在编译过程就能得到计算结果的表达式。显然,字面值属于常量表达式,用常量表达式初始化的const对象也是常量表达式。

一个对象(或表达式)是不是常量表达式由它的数据类型(const对象)和初始值共同决定。

constexpr 变量

C++11新标准规定,允许将变量声明为constexpr类型以便由编译器来验证变量的值是否是一个常量表达式。声明为constexpr的变量一定是一个常量,而且必须由常量表达式初始化。

字面值类型

算数类型、引用和指针都属于字面值类型,自定义类、IO库、string类则不属于字面值类型,也就不能被定义成constexpr。

尽管指针和引用都能定义成constexpr,但它们的初始值却受到严格限制。一个constexpr指针的初始值必须是nullptr或者0,或者是存储于某个固定地址中的对象。

函数体内定义的变量一般来说并非存放在固定地址中,因此constexpr指针不能指向这样的变量。相反的,定义于所有函数体之外的对象其地址固定不变,能用来初始化constexpr指针。另一方面,允许函数定义一类有效范围超出函数本身的变量,这类变量和定义在函数体之外的变量一样也有固定地址。因此,constexpr引用能绑定到这样的变量上,constexpr指针也能指向这样的变量。

指针和constexpr

在constexpr声明中如果定义了一个指针,限定符constexpr仅对指针有效,与指针所指的对象无关:

const int *p = nullptr;     // p是一个指向整型常量的指针
constexpr int *q = nullptr; // q是一个指向整数的常量指针

2.5 处理类型

2.5.1 类型别名

类型别名是一个名字,它是某种类型的同义词。有两种方法可用于定义类型别名:

1、传统的方法是使用关键字typedef:

typedef double wages;
typedef wages base, *p;

关键字typedef作为声明语句中的基本数据类型的一部分出现,含有typedef的声明语句定义的不再是变量而是类型别名。和以前的声明语句一样,这里的声明符也可以包含类型修饰,从而也能由基本数据类型构造出复合类型来。

2、新标准规定了一种新的方法,使用别名声明来定义类型的别名:

using SI = Sales_item;

指针、常量和类型别名

typedef char *pstring;
const pstring cstr = 0;  // cstr是指向char的常量指针
const pstring * ps;      // ps是一个指针,它的对象是指向char的常量指针

声明语句中用到pstring时,其基本数据类型是指针,const是对给定类型的修饰。因此,const pstring就是指向char的常量指针,而非指向常量字符的指针。

2.5.2 auto类型说明符

C++新标准引入了auto类型说明符,用它就能让编译器替我们去分析表达式所属的类型。显然,auto定义的变量必须有初始值。

使用auto也能在一条语句中声明多个变量。因为一条声明语句只能有一个基本数据类型,所以该语句中所有变量的初始基本数据类型都必须一样。

复合类型、常量和auto

编译器推断出来的auto类型有时候和初始值的类型并不完全一样,编译器会适当地改变结果类型使其更符合初始化规则。

1、当引用被当做初始值时,真正参与初始化的其实是引用对象的值,此时编译器以引用对象的类型作为auto的类型;

2、auto一般会忽略掉顶层const,同时底层const则会保留下来。

如果希望推断出的auto类型是一个顶层const,需要明确指出:

const auto f = ci;

还可以将引用的类型设为auto,此时原来的初始化规则依然适用:

auto &g = ci;        // g 是一个整型常量引用,绑定到ci
auto &h = 42;        // 错误:不能为非常量引用绑定字面值
const auto &j = 42; // 正确:可以为常量引用绑定字面值

2.5.3 decltype类型指示符

decltype的作用是选择并返回操作数的数据类型,在此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值。

decltype处理顶层const和引用的方式与auto有些许不同。如果decltype使用的表达式是一个变量,则decltype返回该变量的类型(包括顶层const和引用在内)。

decltype和引用

如果decltype使用的表达式不是一个变量,则decltype返回表达式结果对应的类型。表达式的结果有两种可能:1、具体值而非一个引用;2、作为一条赋值语句的左值,即引用。

表达式返回一个引用类型有两种情况:

1、对指针解引用。解引用指针可以得到指针所指的对象,而且还能给这个对象赋值。

2、decltype和auto的另一处重要区别就是decltype的结果类型与表达式形式密切相关。有一种情况需要特别注意:对于decltype所用的表达式来说,如果变量名加上了一对括号,则得到的类型与不加括号时会有不同。

如果decltype使用的是一个不加括号的变量,则得到的结果就是该变量的类型;如果给变量加上了一层或多层括号,编译器就会把它当成是一个表达式。变量是一种可以作为赋值语句左值的特殊表达式,所以这样的decltype就会得到引用类型。

decltype((variable))的结果永远是引用,而decltype(variable)结果只有当variable本身就是一个引用时才是引用。

2.6 自定义数据结构

可以使用关键字struct和class来定义自己的数据结构。

类内部定义的名字必须唯一,但是可以与类外部定义的名字重复。

C++11新标准规定,可以为数据成员提供一个类内初始值。创建对象时,类内初始值将用于初始化数据成员。没有初始值的成员将被默认初始化。对类内初始值的限制:或者放在花括号里,或者放在等号右边,记住不能使用圆括号

2.6.3 编写自己的头文件

为了确保各个文件中类的定义一致,类通常被定义在头文件中,而且类所在头文件的名字应与类的名字一样。头文件通畅包含那些只能被定义一次的实体,如类、const和constexpr变量。

头文件一旦改变,相关的源文件必须重新编译以获取更新过的声明。

预处理器概述

确保头文件多次包含仍能安全工作的常用技术是预处理器。预处理器是在编译之前执行的一段程序,可以部分地改变我们所写的程序。

预处理功能:1、#include;2、头文件保护符

头文件保护符依赖于预处理变量,预处理变量有两种状态:已定义和未定义。#define指令把一个名字设定为预处理变量,另外两个指令则分别检查某个指定的预处理变量是否已经定义:#indef当且仅当变量已定义时为真,#ifndef当且仅当变量未定义时为真。一旦检查结果为真,则执行后续操作直至遇到#endif指令为止。

使用这些功能就能有效地防止重复包含的发生。

整个程序中的预处理变量包括头文件保护符必须唯一,通常的做法是基于头文件中类的名字来构建保护符的名字,以确保其唯一性。为了避免与程序中的其他实体发生名字冲突,一般把预处理变量的名字全部大写

术语表

字节:内存中可寻址的最小单元,大多数机器的字节占8位。

地址:是一个数字,根据它可以找到内存中的一个字节。

字:在指定机器上进行整数运算的自然单位。一般来说,字的空间足够存放地址。32位机器上的字通常占据4个字节。

全局作用域:位于其他所有作用域之外的作用域。

局部作用域:块作用域的习惯叫法。

字面值:是一个不能改变的值,如数字、字符、字符串等。单引号内的是字符字面值,双引号内的是字符串字面值。

对象:是内存的一块区域,具有某种类型,变量是命名了的对象。

预处理器:在C++编译过程中执行的一段程序。

预处理变量:由预处理器管理的变量。在程序编译之前,预处理器负责将程序中的预处理变量替换成它的真实值。

对常量的引用:是一个引用,不能用来改变它所绑定对象的值。对常量的引用可以绑定常量对象,或者非常量对象,或者表达式的结果。

临时值:编译器在计算表达式结果时创建的无名对象。为某表达式创建了一个临时值,则此临时值将一直存在直到包含有该表达式的最大的表达式计算完成为止。

void类型:是一种有特殊用处的类型,既无操作也无值。不能定义一个void类型的变量。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值