0%

C++ 变量与基本类型

《C++ Primer(第五版)》第二章笔记

基本内置类型

类型 含义 最小尺寸
bool 布尔类型 未定义
char 字符 8位
wchar_t 宽字符 16位
char16_t Unicode 字符 16位
char32_t Unicode 字符 32位
short 短整型 16位
int 整型 16位
long 长整型 32位
long long 长整型 64位
float 单精度浮点数 6位有效数字
double 双精度浮点数 10位有效数字
long double 扩展精度浮点数 10位有效数字

可寻址最小内存块称为字节(Byte),存储的基本单元称为字(Word)。在一台32位的计算机上,32位即字长。

  • 一个int至少和一个short一样大,一个long至少和一个int一样大,一个long long之上和一个long一样大
  • float 以1个字来表示,double用2个字来表示,long double以3或4个字来表示

如何选择类型:

  1. 当明确知晓数值不可能为负时,选用无符号类型。
  2. 使用int执行整数运算。在实际应用中,short常常显得太小而long一般和int有一样的尺寸。如果你的数值超过了int的表示范围,选用long long。
  3. 在算数表达式中不要使用char或bool,只有在存放字符或布尔值时才使用它们。原因在于char类型在一些机器上是有符号的,而在另一些机器上又是无符号的。如果你需要使用一个不大的整数,那么明确指定它的类型是signed char还是unsigned char。比如一些数字char的操作。
  4. 执行浮点数运算选用double,这是因为floa通常精度不够而且双精度浮点数和单精度浮点数的计算代价相差无几。

类型转换

  • 当我们赋予无符号类型一个超出它表示范围的值时,结果是初始值对无符号类型表示数值总数取模后的余数。
  • 当我们赋予带符号一个超出它表示范围的值时,结果是未定义的
  • 当一个算数表达式中既有无符号数又有int值时,那个int值就会转换成无符号数。

字面值常量(literal)

1
2
3
20 /* 十进制 */
024 /* 八进制 */
0x14 /* 十六进制 */

转义序列:‘\’后跟八进制数字,\x后跟十六进制数字转义字符。如果反斜线\后面跟着的数字超过3个,只有前3个数字构成转义序列u8"\1234"="S4"

指定字面值类型(整型和浮点型):

后缀 最小匹配类型 后缀 类型
u/U unsigned f/F float
l/L long l/L long double
ll/LL long long

变量

对象(object): 一块能存储数据并具有某种类型的内存空间

初始化

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

默认初始化

如果内置类型的变量未被显式初始化,它的值由定义的位置决定。定义域仍和函数体之外的变量被初始化为0,而定义在函数体内部的内置类型变量将不被初始化。一个未被初始化的内置类型变量的值式未定义的,如果试图拷贝或以其他形式访问此类值将引发错误。

未初始化的变量含有一个不确定的值,使用未初始化变量的值是一种错误的编程行为且很难调试。

变量声明和定义的关系

1
2
3
extern int i; // 声明i而非定义i
int j; // 声明并定义j
extern double pi = 3.14; //定义

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

声明和定义的区别看起来也许微不足道,但实际上却非常重要。如果要在多个文件中使用同一个变量,就必须将声明和定义分离。此时,变量的定义必须出现且只能出现在一个文件中,而其他用到该变量的文件必须对其进行声明,却绝对不能重复定义。

标识符

C++的标识符(identifier)由字母、数字和下划线组成,其中必须以字母或下划线开头。

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

作用域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
// 函数内部不宜定义与全局变量名同名的新变量
int reused = 42;
int main()
{
int unique = 0;
std::count << reused << " " << unique << std::endl;
// 输出#1:使用全局变量reused;输出 42 0
int reused = 0;// 新建局部变量reused,覆盖了全局变量reused
std::cout << reused << " " << unqiue << std::endl;
// 输出#2:使用局部变量reused; 输出 0 0
std::cout << ::reused << " " << unique << std::endl;
// 输出#3: 显式地访问全局变量reused; 输出 42 0
return 0;
}

复合类型

引用

  1. 引用并非对象而是为一个就已经存在地对象所起的别名
  2. 不能定义引用的引用
  3. 引用只能绑定在对象上,而不能与字面值某个表达式的计算结果绑定在一起(除非是常量引用)
  4. 必须初始化
  5. 一旦引用完成初始化,该引用将无法重新绑定到另外一个对象上

指针

  1. 指针本身是一个对象,允许对指针赋值和拷贝
  2. 指针无需在定义时赋值
  3. 指针存放某个对象的地址,需要使用取地址符&
  4. 引用不是对象,没有实际地址,所以不能定义指向引用的指针
  5. *p解引用符访问对象,解引用符仅适合于那些确实指向了某个对象的有效指针

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

1
2
3
4
int *p1 = nullptr; //等价于int *p1 = 0;
int *p2 = 0; //直接将p2初始化为字面常量0
// 需要首先#include cstdlib
int *p3 = NULL; // 等价于 int *p3 = 0; 尽量使用 nullptr

建议:初始化所有指针。在大多数编译器环境下,如果使用了未经初始化的指针,则该指针所占内存空间的当前内容将被看作一个地址值。访问该指针,相当于去访问一个本不存在的位置上的本不存在的对象。

void* 指针

void*是一种特殊的指针类型,可用于存放任意对象的地址。然而对于 void*来说,内存空间仅仅是内存空间,没办法访问内存空间中所存的对象。(对void*指针使用解引用符*会报错)

理解复合指针的声明

1
2
3
4
int ival = 1024;
int *pi = &ival; // pi 指向一个int类型的数
int **ppi = &pi; // ppi 指向一个int类型的指针
int *&r = pi; // r 一个对整型指针pi的引用

以上可以改写为:

1
int ival=1024, *pi=&ival, **ppi=&pi, *&r=pi;

面对一条比较复杂的声明语句时,从右向左读,离变量名最近的符号对变量的类型右最直接的影响。

const 限定符

const大法:

  1. const对象一旦创建后其值就不能再改变,所以const对象必须初始化
  2. 如果利用一个对象去初始化另外一个对象,则它们是不是const都无关紧要
  3. 如果想在多个文件之间共享const对象,必须在变量的定义之前添加extern关键字

const引用

常量引用: 对const常量的引用,不能被用作修改它所绑定的对象

常量引用初始化:

1
2
3
4
5
6
7
8
int i=42;
const int &r1 = i; // 正确 不必在意对象是否为常量
const int &r2 = 42; // 正确 允许字面值
int &r2_ = 42; // 错误 非常量引用不允许字面值
const int &r3 = r1 * 2; // 正确 允许表达式
int &r3_ = r1 * 2; // 错误 非常量引用不允许表达式结果
const double &dr1 = i; // 正确 允许数据类型隐性转换
double &dr2 = i; // 错误 非常量引用必须和对象数据类型一致

为啥C++这样设计?

1
2
double dval = 3.14;
const int &ri = dval;

编译器把上述代码变成如下:

1
2
3
double dval = 3.14;
const int temp = dval;
const int &ri = temp;

如果riri不是常量引用,就允许对riri赋值,并且丢弃精度,这样会改变riri所引用对象的值。否则,我们绑定的对象就是一个临时量而非dval。我们既然用riri引用dval,就肯定想通过ri改变dval的值,否则干什么给riri赋值呢?既然大家不会想着把引用绑定到临时量上,C++也就把这种行为归为非法。

常量引用值对引用做限制而对引用的对象是否为常量不做限制。

指针和const

指向常量的指针: 令指针指向常量,存放常量对象的地址。

允许一个指向常量的指针指向一个非常量对象(普通指针不允许)

常量指针: const pointer,必须初始化,而且一旦初始化完成,则它的值(存放在指针中的那个地址)就不能再改变了。把*放在const关键字之前用以说明指针是一个常量。

1
2
const double pi = 3.1415;
const double *const pip = &pi; // pip是一个指向常量对象的常量指针

顶层const

顶层const: (top-level const) 表示指针本身是个常量;表示任意对象都可以是一个常量。

底层const: (low-level const) 表示指针所指的对象是一个常量;表示所指对象是常量像引用和指针。

只有指针既可以是顶层const也可以是底层const

拷贝操作

  • 顶层const不受拷贝操作影响

  • 而底层从上图的拷贝需要拷入和拷出的对象必须具有相同的底层const的资格,或者两个对象的数据类型必须能够转换。非常量可以转换成常量,反之则不行。

1
2
3
4
const int ci = 42;
int i = 42;
int &r = ci; // 错误:普通指针不能绑定到int常量上
const int &r2 = i; // 正确: 常量引用可以绑定到普通int上

常量表达式

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

1
const int sz = get_size(); // sz 不是常量表达式

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

1
2
3
constexpr int mf = 20;
constexpr int limit = mf + 1; // mf+1是常量表达式
constexpr int sz = size(); // 只有当size是一个constexpr函数时才是一条正确的声明语句

如果在constexpr声明中是一个指针,那constexpr只对针有效:

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

处理类型

类型别名

两种方法定义类型别名,一是typedef,二是using。

typedef

定义类别别名,让复杂的类型名字变得简单明了、简单易用(更复杂(雾))。

1
2
typedef double wages; // wages 是 double 的同义词
typedef wages base, *p; // base是double的同义词,p是double*的同义词

using

1
using SI = Sales_item; // SI是Sales_item的同义词

这样声明别名后就可以用别名来定义对象了。

指针、常量和类别别名

typedef就是晦涩难懂的代名词

1
2
3
4
typedef char *pstring;	// 为指向char的指针做别名
const pstring cstr = 0; // cstr是一个指向char的常量指针
const pstring *ps; // ps是一个指向 指向char的常量指针 的指针
const char *cst = 0; // 是一个指向常量char的普通指针

auto和decltype

auto: 类型说明符,用它能让编译器代替我们去分析表达式所属的类型。auto定义的变量必须有初始值,auto在一条语句中声明多个变量必须保证同样的基本数据类型。

decltype: 类型指示符,为了从表达式的类型推断出要定义的变量的类型,但是不想用该表达式的值初始化变量。

  1. decltype处理顶层const和引用的方式与auto不同,auto 会忽略顶层const和直接推演引用所指的类型,decltype会将顶层const和引用保留起来。
1
2
3
4
5
6
const int ci = 0, &cj = ci;
decltype(ci) x1 = 0; // x1的类型是const int
auto x2 = ci; // x2的类型是int
decltype(cj) y1 = x; // y1的类型是const int &
auto y2 = cj; // y2的类型是int
auto y3 = &cj; // y3的类型是const int*,取值算底层const会保留
  1. decltype处理(对指针)解引用操作得到引用类型,而auto得到指针所指对象的类型
1
2
3
int i = 1, *p = &i;		
decltype(*p) pr = i; // pr的类型是int &
auto pa = *p; // pa的类型是int
  1. decltype结果类型与表达式形式密切相关

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

1
2
3
4
int a = 1, b = 0;
decltype((a)) d=a; // d的类型为int &
decltype(a) d2; // d2的类型为未初始化的int
decltype(a=b) d3=b; // d3的类型为int &

自定义数据结构

struct:通篇只讲这个。

  • 类内定义的名字必须唯一,但是可以和类外部定义的名字重复
  • 类体右侧的表示结束的花括号必须写一个分号,这是因为类体后面可以紧跟变量名以实对该类型对象的定义(但是不推荐这样做)
  • 类内允许有类内初始值来初始化类
1
2
3
4
struct Sales_data {/*...*/} accum, trans, *sp;
/* <==> */
struct Sales_Data {/*...*/};
Sales_data accum, trans, *sp;

一般来说最好不要把对象的定义和类的定义放在一起。这样做无异于把两种不同实体的定义混在了一条语句离,一会儿类,一会儿变量,非常不合适。

头文件保护符

1
2
3
4
#ifndef SALES_DATA_H
#define SALES_DATA_H
/* ... */
#endif

SALES_DATA_H为头文件保护符,如此的预处理变量必须唯一,通常的做法是基于头文件的名字来构建保护符的名字。

#defube: 把一个名字设定为预处理变量

#ifdef: 当且仅当变量已定义为真

#ifndef: 当且仅当变量未定义为真

一旦检查结果为真,则执行后续操作直至遇到#endif为止。