- 编程语言的基本语法特征:内置类型、变量、表达式、语句、控制结构、函数
- 编程语言对其基本语法特征的补充:允许自定义数据类型、提供库函数
- C++是一种静态数据类型语言:编译时进行类型检查
第2章 变量和基本类型
(1) 类型转换:给unsigned或signed类型赋一个超出其表示范围的值
- 赋给无符号类型一个超出它表示范围的值,结果是初始值对无符号类型表示数值总数取模后的余数。
- 赋给带符号类型一个超出它表示范围的值,结果是未定义的(undefined)。
// 假设char占8比特,则表示范围是0~255,可以表示的数值总数为256
unsigned char c1 = -1; // c1值为-1%256 = 255
signed char c2 = 256; // c2值为未定义的
(2) 类型转换:含有unsigned类型的表达式
- 一个算术表达式中同时含有int类型和无符号类型,int类型将转换为无符号类型:
(负数转换为无符号类型:该负数加上无符号类型的模)
unsigned u = 10;
int i = -42; // 假设int占32位
cout << u + i; //结果为 10 + 2^32 + (-42) = 10 + 4294967296 - 42 = 4294967264
// 32位int含有1个符号位,表示的最大数为2^31-1,而32位unsigned int表示的最大数为2^32-1
- 从无符号数中减去一个数,结果总为非负值:
unsigned u1 = 42, u2 = 10;
cout << u1 - u2 << endl; // 32
cout << u2 - u1 << endl; // (10 - 42) % 2^32 = 4294967264
- 无符号数作为循环变量递减,可能导致死循环:
for(unsigned u = 10; u >= 0; --u) // u不会小于0,循环条件一直成立
cout << u;
(3) 字面值
- 编译器在每个字符串的结尾添加一个空字符
'\0'
,字符串的实际长度比它的内容多1。 - 如果2个字符串字面值位置紧邻,仅由空格、缩进、换行符分隔,则它们实际上是一个整体:
cout<<"Primer "
"C++ "
"5th edition"<
- 通过给字面值添加前缀、后缀,可以指定其类型,如
42ULL
类型为unsigned long long
。
(4) 变量的初始化 ≠ 赋值
- 初始化:创建变量时,赋予其一个初始值。
- 赋值:把对象的当前值擦除,以一个新值来替代。
(5) 列表初始化
- 用花括号初始化变量
- 4种初始化变量的形式:
int a = 0;
int a = {0};
int a{0};
int a(0);
(6) 默认初始化
- 定义变量时没有指定初值
- 若内置类型的变量未被显式初始化:
- 定义于任何函数之外的变量被初始化为0;
- 定义于函数体内部的变量将不被初始化,其值为未定义的(undefined),以拷贝等方式访问它将产生错误。
- 每个类各自决定其初始化对象的方式。
- 定义于函数体内的内置类型变量没有显式初始化,其值未定义;类的对象没有显式初始化,其值由类决定。
(7) 变量的声明 ≠ 定义
- C++支持分离式编译:将程序分割成若干个文件,每个文件可被独立编译。
- 一条声明语句:一个基本数据类型 + 一个声明符列表。
- 声明(declaration):使得名字为程序所知。一个文件若想使用其它文件定义的名字,则必须包含该名字的声明。
- 定义(definition):创建与名字关联的实体。
- 声明和定义都包含变量类型和名称;定义还申请存储空间,可能为变量赋一个初始值。
- 声明而非定义:添加关键字
extern
,并且不能显式初始化变量。 - 若既有关键字
extern
,又显式初始化变量,则还是定义,不是声明。
extern int i; //声明而非定义
int j; //声明并定义
extern int k = 1; //定义
- 变量只能被定义一次,但可以多次声明。
- 在多个文件中使用同一个变量:只能在某一个文件中定义一次(不可重复定义),在其它文件中声明。
(8) 名字的作用域
- 名字的有效区域始于名字的声明语句,以声明语句所在的作用域末端为结束。
- 全局作用域 / 块作用域
- 嵌套的作用域:允许在内层作用域中重新定义外层作用域已有的名字,在内层作用域中默认使用内层作用域的变量。
- 可以用作用域操作符
::
显式访问全局变量:::num
,此时作用域操作符左侧为空,向全局作用域发出请求获取名字为num
的变量。
(9) 引用(reference)
- 引用为对象起了另外一个名字。
- 定义引用类型:将声明符写成
&d
的形式,d
是声明的变量名。
int i = 1;
int &ref1 = i; // ref1与i绑定
- 定义引用时,程序把引用和它的初始值绑定,而不是将初始值拷贝给引用。
- 无法重新令引用绑定到另外一个对象,引用必须初始化。
- 引用并非对象,而是为对象起的别名。
- 对引用进行的所有操作,实际上是在与之绑定的对象上进行的。
- 以引用作为初始值,实际上是以与引用绑定的对象作为初始值。
int &ref2 = ref1; // ref2与i绑定
ref2 = 2; // 改变i的值
int j = ref2; // j被初始化为i的值
- 因为引用本身不是一个对象,所以不能定义引用的引用。
- 在一条语句中定义多个引用,每个引用标识符都必须以
&
开头:
int &a = i, &b = i; // a,b都是引用
int &c = i, d = i; // c是引用,d是int
- 引用的类型要与绑定的对象的类型匹配(有2种例外情况)。
- 引用只能绑定在对象上,不能与字面值或某个表达式的计算结果绑定。
int &ref = 10; // 错误,10不是对象
double i = 1;
int &ref = i; // 错误,类型不匹配
(10) 指针(pointer)
- 指针相较于引用的不同之处:
- 指针本身就是一个对象,允许对指针赋值和拷贝,在指针的声明周期内可以先后指向几个不同的对象。
- 指针无须在定义时赋初值。
- 定义指针:将声明符写成
*d
的形式,其中d
为变量名。指针的类型实际上是它指向的对象的类型。 - 在一条语句中定义多个指针变量,每个变量前都必须有符号
*
,如:int *p1, *p2;
- 指针存放某个变量的地址;可以用取地址符
&
获取变量的地址:
int val = 42;
int *p = &val; // p存放val的地址,p是指向val的指针
- 引用不是对象,没有实际的地址,所以不能定义指向引用的指针。
- 指针的类型应与它指向的对象严格匹配(有两种例外情况)。
- 指针的值有四种可能的情况:
- 指向一个对象
- 指向紧邻对象所在空间的下一个位置(访问该指针的对象的后果无法预计)
- 空指针(访问该指针的对象的后果无法预计)
- 无效指针 (访问无效指针的值将引发错误;编译器不负责检查此类错误)
- 如果指针
p
指向一个对象,则可以使用解引用符*
来访问该对象:*p
:
*p = 10;
cout << *p;
- 解引用符只适用于确实指向某一对象的有效指针。
- 空指针不指向任何一个对象。试图使用指针前,可以在代码中判断其是否非空。
- 获得空指针的3种方法:
int *p = nullptr; // 比较推荐的方法
int *p = 0; // 字面值0,不可以是恰好为0的int型变量
int *p = NULL; // NULL值为0
- 直接把
int
型变量赋值给指针是错误的,即使int
型变量的值恰好为0
也不行:
int zero = 0;
p = zero; // 错误,不能将int变量直接赋值给指针p
- 对于一条赋值语句,它改变的到底是指针的值还是指针所指对象的值?
-
p = &val
左侧是指针p
,因而改变的是指针本身的值,现在P
指向val
。 -
*p = 0
左侧是指针指向的对象*p
,因而改变的是该对象的值。
-
-
void*
指针可以存放任意对象的地址。- 不能直接操作
void*
所指的对象。可以拿它与别的对象比较、作为函数的输入或输出、赋给另外一个void*
指针。
- 不能直接操作
(11) 指针和引用的区别
- 指针是变量,占用内存空间,存储另外一个变量的地址,占4字节或8字节;引用本身不是变量,而是另外一个变量的别名,与该变量绑定在一起,不占用内存空间。
- 指针可以为空,无须在定义时赋初值,允许对指针赋值和拷贝,指针可以先后指向几个不同的变量;引用必须在定义时初始化,且无法重新绑定到其它变量。
- 指针可以是多级的;不能定义引用的引用。
(12) 复合类型的声明
- 在同一条定义语句中,基本数据类型只有一个,声明符的形式可以不同(采用不同的类型修饰符)。因此,一条定义语句可以定义出不同类型的变量:
int i = 1024, *p = &i, &r = i;
-
int* p1, p2;
中,*
仅仅修饰p1
,对p2
不产生任何影响。 - 多级指针:
**
表示指向指针的指针:
int i = 1024;
int *pi = &i;
int **ppi = π
cout << *pi; // 1024
cout << *ppi; // 指针ppi存放的内容,即指针pi的地址
cout << **ppi; // 1024
- 对指针的引用:
int i = 42;
int *p;
int *&r = p; // r是对指针p的引用
r = &i; // 作用等同于p = &i,令指针p指向了i
*r = 0; // 作用等同于*p = 0,令i的值变为0
- 对于
int *&r = p;
,对其声明符从右向左分析,离r
最近的符号&
对r
的类型有最直接的影响,故r
是一个引用。左边的*
说明r
是一个对指针的引用。而基本数据类型int
说明,r
引用的是一个int
指针。
(13) const限定符
在定义对象时,用
const
加以修饰,使其成为一个常量。此后,任何对它的赋值将会引发错误。const
对象一旦创建后其值就不能改变,因此它必须被初始化。const int bufSize = 512;
用一个对象初始化另一个对象,则它们是不是
const
都无关紧要。-
默认情况下,
const
对象被设定为仅在文件内有效,多个文件中出现同名的const
对象,其实等同于在不同文件中分别定义了独立的变量。如果希望只在一个文件中定义const
对象、在多个文件中声明,则对于该对象,不管是定义还是声明,都添加关键字extern
:extern const int bufSize = fcn(); // 一个文件中的定义 extern const int bufSize; // 其它文件中的声明
-
对常量的引用:
- 不能用作修改与其绑定的对象(即该常量)。
- 引用和其对应的对象都应当是引用。不能让非常量引用指向一个常量对象。
const int i = 1024; const int &r1 = i; // 正确 int &r2 = i; // 错误
- (引用类型与其对象类型一致的2种例外之1)初始化常量引用时,允许使用任意表达式作初始值,只要该表达式的结果能转换成引用的类型即可。允许为一个常量引用绑定非常量的对象、字面值、一般表达式。
int i = 42; const int &r1 = i; const int &r2 = 42; const int &r3 = r1 * 2;
- 当一个常量引用被绑定到另外一种类型的对象上时,编译器实际上让该引用绑定了一个与该引用同类型的临时量。
// 原本的代码: double i = 3.14; const int &r = i; // 编译器改变后的代码: const int temp = i; const int &r = temp;
- 对
const
的引用可能引用一个非常量的对象。虽然不可以通过该常量引用修改该对象,但是可以通过其它方式修改该对象。
-
指向常量的指针:
- 不能用于改变其所指对象的值。
- 指向常量对象的指针,必须是定义时有
const
修饰的指针(所谓“指向常量的指针”),而非普通指针;所谓“指向常量的指针”,不一定要指向一个常量对象。 - 指向常量的指针或引用,是其自作多情,它们觉得自己指向了常量,因而自觉地不去改变所指对象的值。
-
const指针:
- 常量指针:把指针定义为常量。
- 常量指针必须初始化。一旦初始化完成,其值(存放在指针中的那个地址)就不能再改变了。
- 把
*
放在const
关键字之前:int *const p = &i;
指向int
对象的常量指针。这里,可以通过该指针修改i
的值。 -
const int *const p = &j;
指向int
型常量对象的常量指针。 - 顶层const && 底层const:
- 顶层const:指针本身是一个常量;或者广义上地,任意类型的一个对象是一个常量。
- 底层const:指针指向的对象是一个常量。
- 对于
const int *const p = &j;
,靠右的是顶层const,靠左的是底层const。 - 进行对象拷贝时,顶层const没有什么影响;而拷入和拷出的对象应具有相同的底层const资格,或两个对象的数据类型能够相互转换。(非常量可以转换成常量,反之则不行)
C++11允许将变量声明为
constexpr
类型,编译器将验证变量的值是否是一个常量表达式。该变量是一个常量,且由常量表达式初始化。
(14) 类型别名
- 某种类型的同义词
- 方法一:typedef
typedef 类型原名 类型别名;
- 可以采用类型修饰符:
typedef int *p;
则p
是int *
的别名。 - 可以在一个语句中定义多个别名:
typedef int a, *p;
其中a
是int
的别名,p
是int *
的别名。
- 方法二:别名声明(新标准)
-
using SI = Sales_item;
则SI
是Sales_item
的别名。
-
- 指针、常量和类型别名:
typedef char *pstring; const pstring cstr = 0; // cstr是指向char的常量指针,而非指向常量字符的指针(易混淆) const pstring *ps; // ps是二级指针,指向一个指向char的常量指针
(15) auto类型说明符(C++11)
- 编译器通过
auto
类型变量的初始值来推算其类型。(auto类型变量必须有初始值。) - auto可以在一条语句中声明多个变量。不过其初始基本数据类型都必须一样。
- 当引用被用作初始值时,编译器以引用对象的类型作为auto的类型。
- auto一般会忽略掉顶层const,而底层const一般会保留下来。
- 如果希望推断出一个顶层const,需要明确指出。
(16) decltype类型指示符(C++11)
-
decltype
:选择并返回操作数的数据类型。(注:这个词源自“Declared Type”。) - 编译器分析表达式并得到它的类型,不实际计算表达式的值。
- 例如:
decltype(f()) sum = x;
- 如果decltype使用的表达式是一个变量,则decltype返回该变量的类型(包括顶层const和引用)。
- 如果decltype使用的是一个不加括号的变量,得到的结果就是该变量的类型;若给变量加上了一层或多层括号,编译器会把它当做一个表达式,所以这样的decltype就会得到引用类型。
-
decltype((var))
的类型永远是引用;decltype(var)
只有在var本身是引用类型时才是引用类型。
(17) 头文件保护符
- 预处理变量有两种状态:已定义和未定义。
-
#define
指令把一个名字设定为预处理变量。 -
#ifdef
当且仅当变量已定义时为真;#ifndef
当且仅当变量未定义时为真。 - 上述检查结果一旦为真,则执行后续操作直到遇到
#endif
指令为止。