1 基本概念
1.1 主函数
每个 C++ 程序都包含一个或多个函数,其中一个必须命名为main
,称为主函数。操作系统通过调用main
来运行 C++ 程序。
一个函数的定义包含四个部分:返回类型、函数名、一个括号包围的形参列表、函数体。虽然main
函数在某种程度上比较特殊,但其定义与其它函数是一样的。下面是一个非常简单的main
函数,它什么也不干,只是返回给操作系统一个值:
int main()
{
return 0;
}
main
函数的返回类型必须为int
,即整数类型。int
类型是一种内置类型(built-in type),即语言自身定义的类型。类型是程序设计最基本的概念之一,一种类型不仅定义了数据元素的内容,还定义了这类数据上可以进行的运算。程序所处理的数据都保存在变量中,而每个变量都有自己的类型。
函数体是一个以左花括号{
开始,以右花括号}
结束的语句块。这个语句块中唯一的一条语句是return
,它结束函数的执行,并且向调用者返回一个值。当return
语句包括一个值时,此返回值的类型必须与函数的返回类型相容。
大多数 C++ 语句以分号;
表示结束。
在大多数系统中,main
的返回值被用来指示状态。返回值 0 表明成功,非 0 的返回值的含义由系统定义,通常用来指出错误类型。
1.2 头文件
每个使用标准库的程序都必须导入相关的头文件,通过#include
指令导入。当预处理器看到#include
标记时就会用指定的头文件的内容代替#include
。
1.2.1 建议使用 C++ 版本的 C 标准库头文件
C++ 标准库中除了定义 C++ 语言特有的功能外,也兼容了 C 语言的标准库。C 语言的头文件形如name.h
,C++ 则将这些文件命名为cname
。也就是去掉了.h
后缀,而在文件名name
之前添加了字母c
,这里的c
表示这是一个属于 C 语言标准库的头文件。
两者的内容是一样的,只不过从命名规范上来讲更符合 C++ 语言的要求。特别的,在名为cname
的头文件中定义的名字从属于命名空间std
,而定义在名为.h
的头文件中的则不然。
一般来说,C++ 程序应该使用名为cname
的头文件而不使用name.h
的形式,标准库中的名字总能在命名空间std
中找到。如果使用.h
形式的头文件,程序员就不得不时刻牢记哪些是从 C 语言继承过来的,哪些又是 C++ 语言所独有的。
1.2.2 编写自己的头文件
为了确保各个文件中类的定义一致,类通常被定义在头文件中,而且类所在头文件的名字应与类的名字一样。
头文件通常包含那些只能被定义一次的实体,如类、const
和constexpr
变量等。头文件也经常用到其它头文件的功能。
1.2.3 预处理器概述
确保头文件多次包含仍能安全工作的常用技术是预处理器,它由 C++ 从 C 语言继承而来。预处理是在编译之前执行的一段程序,可以部分地改变我们所写的程序。
C++ 程序还会用到的一项预处理功能是头文件保护符,头文件保护符依赖于预处理变量。预处理变量有两种状态:已定义和未定义。#define
指令把一个名字设定为预处理变量,另外两个指令则分别检查某个指定的预处理变量是否已经定义:#ifdef
当且仅当变量已定义时为真,#ifndef
当且仅当变量未定义时为真。一旦检查结果为真,则执行后续操作直至遇到#endif
指令为止。预处理变量无视 C++ 语言中关于作用域的规则。
整个程序中的预处理变量包括头文件保护符必须唯一,通常的做法是基于头文件中类的名字来构建保护符的名字,以确保其唯一性。
1.3 输入输出流
C++ 并未定义任何输入输出(IO)语句,而是通过标准库iostream
来提供 IO 机制(以及很多其它设施)。iostream
库包含两个基础类型istream
和ostream
,分别表示输入流和输出流。一个流就是一个字符序列,是从 IO 设备读出或写入 IO 设备的。术语“流”想要表达的是,随着时间的推移,字符是顺序生成或消耗的。
1.3.1 标准输入输出对象
标准库定义了 4 个 IO 对象:(1)cin
,称为标准输入的istream
类型的对象;(2)cout
,称为标准输出的ostream
类型的对象;(3)cerr
,称为标准错误的ostream
类型的对象,用于输出警告和错误消息;(4)clog
,ostream
类型的对象,用于输出程序运行时的一般性信息。
系统通常将程序所运行的窗口与这些对象关联起来。
1.3.2 向流写入和读取数据
输出运算符<<
接受两个运算对象:左侧的运算对象必须是一个ostream
对象,右侧的运算对象是要打印的值。此运算符将给定的值写到给定的ostream
对象中。输出运算符的计算结果就是其左侧运算对象,因此多个输出运算符的输出请求可以连接起来。
std::endl
是一个被称为操纵符的特殊值,效果是结束当前行,并将与设备关联的缓冲区中的内容刷新到设备中。缓冲刷新操作可以保证目前为止程序所产生的所有输出都真正写入输出流中,而不是仅仅停留在内存中等待写入流。
输入运算符>>
与之类似,接受一个istream
作为其左侧运算对象,接受一个对象作为其右侧运算对象。它从给定的istream
读入数据,并存入给定对象中。并也返回其左侧运算对象作为其计算结果,因此也可以将一系列输入请求连接到一起。
例如,以下示例要求输入两个整数,然后程序会输出两个整数的和:
#include <iostream>
int main() {
int a = 0, b = 0;
std::cout << "Enter two numbers: " << std::endl;
std::cin >> a >> b;
std::cout << "The sum of " << a << " and " << b << " is " << a + b << std::endl;
return 0;
}
假设我们要输入的整数是 1 和 2,则程序运行结果为:
Enter two numbers:
1 2
The sum of 1 and 2 is 3
标准库定义了不同版本的输入输出运算符,因此可以在一行语句中处理这些不同类型的运算对象。
1.4 命名空间
命名空间可以帮助我们避免不经意的名字定义冲突,以及使用库中相同名字导致的冲突。标准库定义的所有名字都在命名空间std
中,例如std::cin
表示从标准输入中读取内容。
通过命名空间使用标准库有一个副作用,就是当使用标准库中的一个名字时,必须显式通过作用域运算符(::
)来说明我们想使用来自命名空间中的名字。
我们可以通过使用using
声明,则无须专门的前缀也能使用所需的名字。其形式为:using namespace::name;
。按照规定,每个using
声明引入命名空间中的一个成员,要用到的标准库中的名字都需要一条using
声明语句。
位于头文件的代码一般来说不应该使用using
声明。这是因为头文件的内容会拷贝到所有引用它的文件中去,如果头文件里有某个using
声明,那么每个使用了该头文件的文件就都有这个声明。对于某些程序来说,由于不经意间包含了一些名字,反而可能产生始料未及的名字冲突。
2 实用工具
2.1 注释
编译器会忽略注释,因此注释对程序的行为或性能不会有任何影响。
C++ 有两种注释:单行注释和界定符对注释。单行注释以双斜线//
开始,以换行符结束,可以包含任何文本。界定符对注释继承自 C 语言,以/*
开始,*/
结束,中间可以包含除*/
以外的内容,包括换行符。界定符对注释不能嵌套。
3 基本内置类型
数据类型是程序的基础:它告诉我们数据的意义以及我们能在数据上执行的操作。
C++ 定义了一套包括算术类型和空类型在内的基本数据类型。其中算术类型包含了字符、整型数、布尔值和浮点数。空类型不对应具体的值,仅用于一些特殊场合,例如最常见的是,当函数不返回任何值时使用空类型作为返回类型。
3.1 算术类型
算术类型分为两类:整型和浮点型。其中整型又包括字符、布尔类型和整型数在内。
算术类型的尺寸(也就是该类型数据所占的比特数)在不同机器上有所差别。下表列出了 C++ 标准规定的尺寸最小值,同时允许编译器赋予这些类型更大的尺寸。
表:C++ 算术类型 | ||
---|---|---|
类型 | 含义 | 最小尺寸 |
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 位有效数字 |
基本的字符类型是char
,一个char
的空间应确保可以存放机器基本字符集中任意字符对应的数字值。也就是说,一个char
的大小和一个机器字节一样。其它字符类型用于扩展字符集。类型wchar_t
用于确保可以存放机器最大扩展字符集中的任意一个字符,类型char16_t
和char32_t
则为 Unicode 字符集服务。
整型用于表示不同尺寸的整数。C++ 语言规定一个int
至少和一个short
一样大,一个long
至少和一个int
一样大,一个long long
至少和一个long
一样大。
浮点型可表示单精度、双精度和扩展精度的值。C++ 标准指定了一个浮点数有效位数的最小值,然而大多数编译器都实现了更高的精度。通常,float
以 1 个字(32 比特)来表示,double
以 2 个字(64 比特)来表示,long double
以 3 或 4 个字(96 或128 比特)来表示。一般来说,类型float
和double
分别有 7 和 16 个有效位;类型long double
则常常被用于有特殊浮点需求的硬件,它的具体实现不同,精度也各不相同。
除去布尔型和扩展的字符型之外,其它整型可以划分为带符号的和无符号的两种。带符号类型可以表示正数、负数或 0,无符号类型则仅能表示大于等于 0 的值。
类型int
、short
、long
、long long
都是带符号的,通过在这些类型名前添加unsigned
就可以得到无符号类型,例如unsigned long
。类型unsigned int
可以缩写为unsigned
。
与其它整型不同,字符型被分为了三种:char
、signed char
、unsigned char
。特别需要注意的是:类型char
和类型signed char
并不一样。尽管字符型有三种,但是字符的表现形式却只有两种:带符号的和无符号的。类型char
实际上会表现为上述两种形式中的一种,具体是哪一种由编译器决定。
无符号类型中所有比特都用来存储值,例如,8 比特的unsigned char
可以表示 0 至 255 区间内的值。
4 类型的处理
4.1 auto
类型说明符
auto
类型说明符能让编译器通过初始值替我们去分析表达式所属的类型。因此,auto
定义的变量必须有初始值。例如:
auto item = val1 + val2; // item 初始化为 val1 和 val2 相加的结果,并推断出 item 的类型
使用auto
也能在一条语句中声明多个变量。因为一条声明语句只能有一个基本数据类型,所以该语句中所有变量的初始基本数据类型都必须一样。例如:
auto i = 0, *p = &i; // 正确:i 是整数,p 是整形指针
auto sz = 0, pi = 3.14; // 错误:sz 和 pi 的类型不一致
编译器推断出来的auto
类型有时和初始值的类型并不完全一样,编译器会适当地改变结果类型使其更符合初始化规则。
首先,使用引用其实是使用引用的对象,特别是当引用被用作初始值时,真正参与初始化的其实是引用对象的值。此时编译器以引用对象的类型作为auto
的类型。例如:
int i = 0, &r = i;
auto a = r; // a 是一个整型
其次,auto
一般会忽略掉顶层const
,同时底层const
则会保留下来。例如,当初始值是一个指向常量的指针时:
const int ci = i, &cr = ci;
auto b = ci; // b 是一个整型(ci 的顶层 const 特性被忽略掉了)
auto c = cr; // c 是一个整型(cr 是 ci 的别名,ci 本身是一个顶层 const)
auto d = &i; // d 是一个整型指针(整型的地址就是指向整型的指针)
auto e = &ci; // e 是一个指向整数常量的指针(对常量对象取地址是一种底层 const)
如果希望推断出的auto
类型是一个顶层const
,则需要使用const
关键字明确指出。例如:
const auto f = ci; // ci 的推演类型是 int,f 是 const int
还可以将引用的类型设为auto
,此时原来的初始化规则仍然适用。例如:
auto &g = ci; // g 是一个整型常量引用,绑定到 ci
auto &h = 42; // 错误:不能为非常量引用绑定字面值
const auto &j = 42; // 正确:可以为引用常量绑定字面值
设置一个类型为auto
的引用时,初始值中的顶层常量属性仍然保留。