c++ primer 第二章

第2章 变量和基本类型

2.1 基本内置类型

2.1.1 算术类型

类型含义最小尺寸备注
char字符8位1B(8b)
wchar_t宽字符16位
char16_tUnicode字符16位
char32_tUnicode字符32位
float单精度浮点数6位有效数字1个字(32b)
double双精度浮点数10位有效数字2个字(64b)
long double扩展精度浮点数10位有效数字4个字(128b)

存储的基本单元:字,32b/64b。

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

char 是否有符号并不确定,因此使用 signed char 或 unsigned char 来指定是否有符号。

整数运算用 int/long long。

浮点数运算用 double。float 和 double 的计算代价相差无几。

2.1.2 类型转换

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

赋给带符号类型超出范围的值时,结果是未定义的。

带符号数与无符号数做运算,会被转化为无符号数。

2.1.3 字面值常量

整型和浮点型字面值

0 开头的整数是 8 进制,0x 开头的整数是十六进制。

十进制字面值的类型是 int, long, long long 中可以放下它的最小尺寸。

八进制和十六进制字面值的类型是 int, long, long long, unsigned int, unsigned long, unsigned long long 中可以放下它的最小尺寸。

浮点型字面值可以用小数或科学计数法表示,科学计数法中的指数部分用 E 或 e 标识,默认 double:3.14, 0., 0e0, .001, 3.14E2。

字符和字符串字面值

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

字符串字面值的类型实际上是字符数组,编译器会向每个字符串结尾添加一个空字符('\0'),因此字符串字面值的实际长度要比它的内容多 1。

如果两个字符串字面值紧邻且仅由空格、缩进和换行符分隔,则它们实际上是一个整体。因此字符串比较长时可以直接分行书写。

1
2
cout << "A "
"B" << endl;

转义序列

换行符:,横向制表符:,报警符:,纵向制表符:退格符:双引号:",

反斜线:\,单引号:'问号:?,回车符:进纸符:述转义序列被当作一个字符使用。

也可以使用泛化的转移序列,形式是  后跟 1~3 个八进制数字或 后跟 1 个或多个十六进制数字。

如果  后面跟着的八进制数字超过 3 个,只有前三个数字与  构成转义序列。

1
2
\115  //字符 M
\x4d //字符 M

指定字面值的类型

可以通过给字面值增加前缀和后缀来改变字面值的默认类型。

字符和字符串字面值

前缀含义类型
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 Fdouble
l or Llong double

12f 是错的,不能给整型字面值加 f 后缀,可以使用 12.f。

用 LL 代替 ll 防止与 1 混淆。

2.2 变量

2.2.1 变量定义

初始化

可以在同一条定义语句中使用先定义的变量去初始化后定义的其他变量。

1
double price = 109.99, discount = price * 0.6;

初始化不是赋值,初始化是创建变量时赋予一个初始值,赋值是把对象的当前值擦除并用一个新值来替代。

列表初始化

四种初始化方式,其中使用花括号的方式叫做列表初始化。

1
2
3
4
int i = 0;
int i = {0};
int i{0};
int i(0);

当用于内置类型的变量时,使用列表初始化且初始值存在信息丢失的风险,编译器会报错。

1
2
long double ld = 3.14; int a{ld}, b = {ld}; //错误,存在信息丢失的风险,转换未执行
int c(ld), d = ld; //正确

默认初始化

定义于任何函数之外的内置类型则被初始化为 0;

定义于函数体内的内置类型的对象如果没有初始化,则其值未定义

类的对象如果没有显式地初始化,则其由类确定。

2.2.2 变量声明和定义的关系

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

定义:在声明的基础上,创建与名字关联的实体,即申请存储空间,还可能会为变量赋一个初始值。

要声明一个变量加 extern,声明变量不能赋值,任何包含了显式初始化的声明即成为定义。

1
2
3
extern int i;     // 声明 i
int i; // 定义 i;
extern int i = 1; // 定义 i,初始化抵消了 extern 的作用。

变量只能被定义一次,但是可以多次声明。

extern 定义的变量必须是全局的,这样才可能在其他文件中使用。

如果要在多个文件中使用同一个变量,变量的定义必须出现在且只能出现在一个文件中,而其他用到该变量的文件必须对其进行声明,却绝对不能重复定义。

C++ 是静态类型语言,在编译阶段检查类型。

2.2.3 标识符

标识符组成:字母、数字、下划线。不能以数字开头。标识符的长度没有限制。

用户自定义的标识符不能连续出现两个下划线,也不能以下划线紧连大写字母开头。定义在函数体外的标识符不能以下划线开头。

变量命名规范:

  1. 变量名一般用小写字母。
  2. 用户自定义的类型一般以大写字母开头。
  3. 包含多个单词的标识符,使用驼峰命名法或使用下划线连接不同单词。
  4. 当你第一次使用变量再定义它。

2.2.4 名字的作用域

花括号以内是作用域。

1
2
3
do {
int ival = read();
} while(ival); // 错误

2.3 复合类型

复合类型就是基于其他类型定义的类型,引用和指针是其中两种。

声明语句 = 基本数据类型 + 声明符(类型修饰符(&/*) + 标识符(变量名))

2.3.1 引用

引用是给对象起的别名,不能与字面值或表达式绑定。

初始化引用时,是将引用和对象绑定在一起。引用无法重定向,只能一直指向初始值。故引用必须初始化。

对引用的所有操作都是对与之绑定的对象的操作。

引用非对象。不能定义对引用的引用,因为引用非对象。

引用只能绑定同类型对象。

2.3.2 指针

指针存放某个对象的地址。

指针必须指向同类型对象。

指针与引用的不同:

  1. 指针可以不初始化,引用不可以。
  2. 指针是一个对象,引用不是。
  3. 指针可以重定向,引用不可以。

从右向左读比较易于弄清:

  • 离变量名最近的符号(&)对变量的类型有最直接的影响,因此 r 是一个引用。
  • 声明符的其余部分用以确定 r 引用的类型是什么,*说明 r 引用的是一个指针。
  • 声明的基本数据类型部分指出 r 引用的是一个int指针。
1
2
int *p;
int *&r = p;

空指针

1
2
3
int *p = nullptr; // 三种定义空指针的方式。最好用第一种
int *p = 0;
int *p = NULL; // NULL 是在头文件 cstdlib 中定义的预处理变量,值为 0。

初始化指针,如果不清楚该指向谁,先初始化为 nullptr。

void* 指针

void* 指针是特殊的指针类型,可以存放任意对象的地址。它的用处比较有限,不能直接操作它所指的对象。

2.4 const限定符

const 对象必须初始化,因为一旦创建就不能再改变值。

默认情况下,const 对象仅在文件内有效。

如果想在多个文件间共享 const 对象,必须在变量的声明和定义前都添加 extern 关键字并在本文件中声明。

2.4.1 const的引用

const 引用不能改变引用的对象。

不能用非常量引用指向一个常量对象。可以用常量引用指向一个非常量对象。

当用常量引用绑定一个非常量对象时,不能通过引用改变引用对象的值,但是可以通过其他方式改变值。常量指针也一样。

1
2
3
4
const int ci = 1;
int i = 1;
int &r1 = ci; // 错误,试图让一个非常量引用指向一个常量对象
const int &r2 = i; // 正确,一个常量引用指向一个非常量对象

引用的类型必须与其所引用对象的类型一致,但是有一个例外就是初始化常量引用时允许用任意表达式作为初始值(包括常量表达式),只要该表达式结果可以转换为引用的类型。

2.4.2 指针和const

指向常量的指针的用法和常量引用相似,但是是不一样的。它既可以指向常量也可以指向非常量,不能改变对象的值。但是非常量对象可以通过其他途径改变值

2.4.3 顶层const

类型顶层 const底层 const
含义对象本身是个常量所指的对象是一个常量
适用范围任何数据类型通用引用和指针
初始化必须不必须

引用实际上只能是底层 const(常量引用),不存在顶层 const 的引用,因为引用非对象。

1
const int &const p2 = p1;// 错误

对于指针和引用而言,顶层 const 在右边,底层 const 在左边。对于其他类型,全都是顶层 const

执行对象的拷贝操作时,顶层 const 没有影响,两个对象要有相同的底层 const 资格。不能将底层 const 拷贝给非常量,反之可以,非常量将会转化为常量。

1
2
const int ci = 1;
int i = ci; // 正确,顶层 const 无影响

2.4.4 constexpr和常量表达式

常量表达式是指值不会改变并且在编译过程就能得到计算结果的表达式。

字面值属于常量表达式,由常量表达式初始化的 const 对象也是常量表达式。

1
2
3
const int a = 32;          // 是常量表达式
const int b = a + 1; // 是常量表达式
const int sz = get_size(); // 不是常量表达式,因为虽然 sz 是常量,但它的具体值等到运行时才知道。

cosntexpr变量

由 constexpr 声明的变量必须用常量表达式初始化。

不能用普通函数初始化 constexpr 变量,但可以使用 constexpr 函数初始化 constexpr 变量。

constexpr int sz = size(); //只有当 size() 是一个 constexpr 函数时这才是一条正确的声明语句。

constexpr 变量是真正的“常量”,而 const 现在一般只用来表示 “只读”

字面值类型

算术类型、引用、指针、字面值常数类、枚举属于字面值类型,自定义类、IO 库,string类则不属于。

cosntexpr 指针的初始值必须是 nullptr 或存储于固定地址的对象。函数体之外的对象和静态变量的地址都是固定不变的。

指针和constexpr

注意区分 constexpr 和 const 。constexpr 都是顶层 const,仅对指针本身有效。

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

2.5 处理类型

2.5.1 类型别名

1
2
typedef double wages; using wages = double;
typedef wages base, *p; // base 是 double 的别名,p 是 double* 的别名。

指针、常量和类型别名

1
2
typedef char* pstring; 
const pstring cstr = 0; // 是一个指向 char 的常量指针。

不能采用直接替换的方式将其理解为 const char* cstr = 0,这是错误的,是一个指向 const char 的指针。

替换前基本数据类型是 char*。

替换后基本数据类型是 const char,*是声明符的一部分。

2.5.2 auto类型说明符

auto 可以在一条语句中声明多个变量,但是多个变量必须是同一个基本数据类型。

复合类型、常量和auto

编译器推断出的 auto 类型有时和初始值并不一样,编译器会进行适当的调整:

  1. auto 根据引用来推断类型时会以引用对象的类型作为 auto 的类型。
  2. auto 一般会忽略掉顶层 const,如果希望 auto 是一个顶层 const,需要明确指出。
  3. 设置一个类型为 auto 的引用/指针时,顶层 const 保留。
  4. auto 会保留底层 const。
1
2
3
4
5
auto e = &ci;       // e是一个指向整数常量的指针(对常量对象取地址是一种底层const)
const auto f = ci; // ci的推演类型是int,f是const int (明确指出auto是顶层const)
auto &g = ci; // g是一个整型常量引用,绑定到ci
auto &h = 42; // 错误:不能为非常量引用绑定字面值
const auto &j = 42; // 正确:可以为常量引用绑定字面值

int 与 int*、int& 是一个基本数据类型,而 const int 与 int 不是一种类型。

用 auto 定义引用时,必须用 & 指明要定义的是引用。

2.5.3 decltype类型指示符

当希望获得表达式的类型但是不要值的时候,可以使用类型说明符 decltype。

如果 decltype 使用的表达式是一个变量,则它返回该变量的类型(包括顶层 const 和引用在内)。

当获得的类型是引用时,必须初始化。

1
2
3
4
const int ci = 0, &cj = ci;
decltype(ci) x = 0; // x 的类型是 const int
decltype(cj) y = x; // y 的类型是 const int&
decltype(cj) z; // z 是一个引用,必须初始化

decltype 和引用

表达式的值类别为左值产生 T& ,右值产生 T。

左值类型:解引用指针给变量加括号,赋值操作

1
2
3
4
int i = 42, &r = i, *p;
decltype(r+0) b; // b 的类型是 int
decltype(*p) c = i; // c 的类型是 int&
decltype((i)) d = i; // d 的类型是 int&

2.6 自定义数据结构

2.6.1 定义Sales_data类型

定义类时可以给数据成员提供类内初始值以进行初始化。没有类内初始值的成员则被默认初始化。

类内初始值可以放在花括号中或等号的右边,不能使用圆括号。

2.6.3 编写自己的头文件

类通常定义在头文件中,类所在头文件的名字应与类的名字一样。

头文件通常定义那些只能被定义一次的实体,比如类、const、constexpr 等。

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

预处理器概述

确保头文件多次包含仍能安全工作的常用技术是预处理器。

预处理变量有两种状态:已定义和未定义。预处理变量的名字全部大写。

整个程序中的预处理变量包括头文件保护符必须唯一,通常基于头文件中类的名字来构建保护符的名字,以确保其唯一性。

c++ 中包含三个头文件保护符:

  1. #define:把一个名字设定为预处理变量
  2. #ifdef:当且仅当变量已定义时为真
  3. #ifndef:当且仅当变量未定义时为真,一旦检查结果为真,则执行后续操作直到遇到 #endif 为止
  4. #endif

预处理变量无视作用域的规则,作用范围是文件内。