《C++Primer》第二章 变量和基本类型
写这篇文章的目的
身为C++的零基础初学者,短期内把《C++Primer》啃下来是一个比较笨但是有效的方法,一方面可以掌握比较规范的C++语法(避免被项目中乱七八糟的风格带跑偏),另一方面又可以全面地了解C++语法以及C++11新标准(后续要做的事情就剩下查漏补缺,不断完善自己的知识体系)。
个人感觉从零学习一门新知识比较好的方法是快速了解知识的全貌,然后构建自己的知识地图,后续不断地补充相应的细节。
由于《C++Primer》和大多数的教科书一样废话连篇,因此想要精炼一下每篇文章的内容再打印成pdf,方便温故知新。
全文链接
基本内置类型
1. 内置类型的及其实现
- 字节
byte
:可寻址的最小内存块,大多数机器的字节由8
比特构成 - 通常
float
由32
位来表示,double
由64
位来表示;一般float
和double
分别有7
和16
个有效位
2. 如何选择类型
- 明知数值不可能为负时则选用无符号类型
- 一般用
int
执行整数运算(因为short
太短而long
一般与int
有相同的尺寸),如果你的数值超过了int
的表示范围则选用long long
- 执行浮点数运算时选用
double
,一方面是因为float
精度不够,另一方面是因为双精度浮点数和单精度浮点数的计算代价相差无几
3. 类型转换
- 当我们赋给无符号类型一个超过它表示范围的值时,结果是初始值对无符号类型表示数值总数取模后的余数。比如
8
比特大小的unsigned char
可以表示0~255
,如果我们将-1
赋给它将会得到255
- 当我们赋给带符号类型一个超出它表示范围的值时,结果是未定义的,程序可能会继续工作、崩溃,也可能产生垃圾数据
- 当一个算数表达式中既有无符号类型又有
int
值时,int
型的变量会被转化为无符号类型,结果可能是出乎意料的:
// 切勿混用带符号类型和无符号类型
unsigned u = 10;
int i = -42;
std::cout << i + i << std::endl; // -84
std::cout << i + u << std::endl; // 如果int占32位则输出4294967264
4. 字面值常量literal
- 整型和浮点型字面量:
20
十进制;024
八进制;0x14
十六进制;3.14159E0
浮点型 - 字符和字符串字面量:
'a'
表示一个字符;"a"
字符串字面量包含字母a
和空字符\0
变量
1. 初始化
- 含义:当对象在创建时获得了一个特定的值:则我们说这个对象被初始化了
initialized
- 初始化不等于赋值:初始化指的是创建变量时赋予其一个初始值,而赋值指的是把对象的当前值擦除并用一个新值替代
- 列表初始化:
C++11
新标准的一部分,用花括号来初始化变量,这种方法有一定的优势:当使用列表初始化且初始值存在丢失信息的风险时则编译器将报错 - 默认初始化:如果定义变量时没有指定初值,则变量将被默认初始化。对于内置类型而言,定义于任何函数体之外的变量被初始化为
0
,但是定义在函数体内部的内置变量将不被初始化 - 绝大多数类都支持无须显式初始化而定义对象,这样的类提供了一个合适的默认值,而某些类要求每个对象都显式初始化
2. 变量声明与定义的关系
C++
支持分离式编译机制,该机制允许将程序分割为若干个文件,每个文件可独立编译。
为了支持分离式编译,C++
将定义和声明区分开。其中声明规定了变量的类型和名字,定义除此功能外还会申请存储空间并可能为变量赋一个初始值。如果想声明一个变量而非定义它,就使用关键字extern
并且不要显式地初始化变量:
变量能且仅能被定义一次,但是可以被多次声明。
extern int i; // 声明i而非定义i
extern int i = 1; // 定义i, 这样做抵消了extern的作用
复合类型
1. 引用
C++11
中新增了“右值引用”,而我们这里讲的引用指的是“左值引用”。
- 引用必须初始化
- 引用本身并非对象,它是一个已经存在的对象的别名
- 因为引用本身不是对象,所以不能定义引用的引用
2. 指针
指针只可能是以下四种情况:
- 指向一个对象
- 指向紧邻对象所占空间的下一个位置
- 空指针
- 无效指针
试图拷贝或者以其他方式访问无效指针的值都会引发错误,编译器并不会负责检查此类错误。空指针不指向任何对象,在试图使用一个指针之前最好先判断它是否为空。C++11
中得到空指针最直接的方法就是字面值nullptr
。
建议:初始化所有指针。访问未经初始化的指针相当于去访问一个本不存在的位置上本不存在的对象。如果指针所占空间中恰好有内容,而这些内容又被当做某个地址。我们就很难分清它是否是合法的了。因此建议初始化所有指针,并且尽量等定义了对象之后再定义指向它的指针。如果实在不清楚指针应该指向何处,就将它初始化为
nullptr
,这样程序就能检测并在非法引用时报错。
const限定符
const
对象一旦创建后其值就不能再改变,所以const
对象必须初始化。
1. 多个文件共享const对象
如果想在多个文件之间共享
const
对象,那么必须在变量的定义之前添加extern
关键字。
默认状况下,const
对象仅在文件内有效。const int bufSize = 512;
以编译时初始化的方式定义一个const
对象时,编译器将在编译过程中把用到该变量的地方都替换成对应的值。如果我们希望只在一个文件中定义const
然后在其他多个文件中声明并使用它。解决的方法是对于const
变量无论是声明还是定义都使用extern
关键字,这样就仅需定义一次了。
// file_1.cc 定义并初始化了一个常量,该常量能被其他文件访问
extern const int bufSize = fcn();
// file_1.h 头文件
extern const int bufSize; // 与file_1.cc中定义的常量是同一个
2. 常量引用
与普通引用不同的是,对常量的引用不能被用作修改它所绑定的对象。
const int c1 = 1024;
const int &r1 = c1;
3. 指针和const
- 指向常量的指针不能用于修改其所指对象的值,要想存放常量对象的地址,只能使用指向常量的指针。
- 常量指针:允许将指针本身定为常量,常量指针必须初始化
int errNum = 0;
int *const curErr = &errNum; // curErr会一直指向errNum
const double pi = 3.14159;
const double *const pip = π // pip是一个指向常量对象的常量指针
4. 顶层const
指针本身是一个对象,它又可以指向另一个对象。因此指针本身是不是常量以及指针所指的是不是一个常量就是两个相互独立的问题。顶层
const
表示指针本身是一个常量,底层const
表示指针所指的对象是不是一个常量。
当执行对象的拷贝操作时,拷入和拷出的对象必须具有相同的底层const
资格。
5. constexpr和常量表达式
常量表达式
const expression
是指值不会改变并且在编译过程就能得到计算结果的表达式。
C++11
新标准规定,允许将变量声明为constexpr
类型以便由编译器来验证变量的值是否是一个常量表达式。声明为constexpr
的变量一定是一个常量,且必须用常量表达式初始化。
处理类型
1. 类型别名
类型别名
type alias
是一个名字,它是某种类型的同义词。它让复杂的类型名字变得简单明了、易于理解和使用。
// 传统方法
typedef double wages; // wages是double的同义词
// 新标准
using SI = Sales_item
2. auto类型说明符
C++11
引入了auto
类型说明符,可以让编译器通过初始值来推断变量的类型。需要注意的是,编译器推断出来的auto
类型有时候与初始值的类型并不完全一样,编译器会适当地改变结果类型使其更加符合初始化规则。
- 当引用作为初始值时,真正参与初始化的是引用对象的值
-
atuo
一般会忽略掉顶层const
,底层const
会保留下来,比如当初始值是一个指向常量的指针。如果希望推断出的auto
类型是一个顶层const
时,需要明确指出:
const int ci = i;
const auto f = ci;
自定义数据结构
1. 定义一个不带有任何运算功能的数据结构
struct Sales_data {
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
Sales_data accum, trans, *salesptr;
2. 编写自己的头文件
为了确保各个文件中类的定义一致,类通常被定义在头文件中,而且类所在头文件的名字应与类的名字一样。
- 头文件通常包含哪些只能被定义一次的实体,如类、
const
和constexpr
变量等 -
C++
会使用头文件保护符来防止包含多份相同的头文件。
#ifndef SALES_DATA_H
#define SALES_DATA_H
#include <string>
struct Sales_data {
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
#endif
-
C++
将C
语言中的头文件如name.h
重命名为cname
,即去掉了.h
后缀同时在前面加上字母c
。一般而言C++
程序员应该使用cname
的头文件而非name.h
的形式,标准库中的名字总能在命名空间std
中找到,如果使用name.h
则程序员不得不时刻牢记从属于C
还是C++
。