声明
在 C++ 程序中,想要使用某个名字(标识符),必须先对其进行声明。我们必须制定他的类型以便编译器知道这个名字对应的是何种实体。
1
2
3
4
5
6
char ch; // 为一个 char 类型的变量分配内存空间并赋初始值 0
auto count = 1; // 为一个 int 类型的变量分配内存空间并赋初始值 1
const char *name = "Njal"; // 为一个指向 char 的指针分配内存空间;为字符串字面值常量 "Njal" 分配内存空间;用字符串字面值常量的地址初始化指针。
struct Date{int d, m, y}; // Date 是一个 Struct,包含 3 个成员。
int day(Date *p) { return p->d; } // day 是一个函数,执行某段代码。
using Point = std::complex<short>; // Point 是类型 std::complex<short> 的别名
大多数声明同时也是定义。我们可以把定义看成是一种特殊的声明,他提供了在程序中使用改实体所需的一切信息。
1
2
3
4
// 以下声明语句并不是定义,也就是说,如果想使用它们,必须现在其他某处进行定义。
double sqrt(double); // 函数声明
extern int error_number; // 变量声明
struct User; // 类型名字声明
C++ 中,每个名字都可以对应多个声明语句,但是只能有一个定义。同一实体的所有声明中,类型必须保持一致。
1
2
3
4
5
6
int count;
int count; // 错误:重定义
extern int error_number;
extern short error_number; // 错误:类型不匹配
extern int err_number;
extern int err_number; // 正确:多次声明
1. 前置修饰符
指定所声明对象的某些非类型属性。
staticvirtualexternconstexpr
2. 声明符
声明符由一个名字1和一些可选的声明运算符组成
| 运算符 | 含义 | |
|---|---|---|
| 前缀 | * | 指针 |
| 前缀 | *const | 常量指针 |
| 前缀 | *volitile | volatile指针 |
| 前缀 | & | 左值引用 |
| 前缀 | && | 右值引用 |
| 前缀 | auto | 函数(使用后置返回类型) |
| 后缀 | [] | 数组 |
| 后缀 | () | 函数 |
| 后缀 | -> | 从函数返回 |
3. 作用域
- block scope(a.k.a. local scope)
- 局部作用域。在由大括号
{}括起来的代码块内声明的名称仅在该代码块及其包含的代码块内可访问,并且只能在声明之后访问。 函数参数名称具有块级作用域,就像它们在包含函数体的代码块内声明一样。 - class scope
- 类作用域。类成员的名称(数据成员和成员函数名称)具有类作用域。具有类作用域的名称可以在类的所有成员函数中访问。 类外的代码只能在这些名称是公有的情况下访问类作用域的名称。 此外,非静态类成员必须通过对象访问——对象名称、对象的引用或指向对象的指针都可以使用——并且需要使用适当的成员选择运算符。 在成员函数内部声明的变量(包括函数的参数)具有块级作用域,而不是类作用域。
- namespace scope
- 名字空间作用域
- file scope(a.k.a. global scope)
- 全局作用域。在所有代码块或类之外声明的任何名称具有文件作用域。这样的名称在声明之后,可以在文件中的任何位置访问。
- statement scope
- 语句作用域
- function scope
- 函数作用域
4. Variable Shadowing
变量隐藏(Variable shadowing)发生在给定作用域内声明的变量与外部作用域中声明的变量具有相同名称时。外部变量被称为“被隐藏”(shadowed)。 这种情况可能会导致混淆,因为后续使用该变量名时,可能不清楚到底指的是哪个变量。
如果你给全局变量或者大函数中的局部变量起类似于
i或者x之类的名字,无异于自找麻烦。
可以通过作用域解析运算符 :: 反问被隐藏了的全局名字。
我们无法访问被隐藏的局部变量。
1
2
3
4
5
6
7
8
int x;
void f()
{
int x = 1; // 隐藏全局变量 x
::x = 2; // 为全局变量x赋值
x = 2; // 为局部变量 x 赋值
}
5. 初始化
1
2
3
4
X a1 {v}; // 不受任何限制,在任何场景都能使用。建议使用这种形式为变量赋初始值,含义清晰,与其他形式相比不太容易出错。
X a2 = {v};
X a3 = v;
X a4(v);
- 列表初始化
- 通过
{}进行初始化的方式,可以防止窄化转换。
- 如果一种整型存不下另一种整型的值,则后者不会被转换成前者。例如:允许
char转换到int但是不允许int到char的类型转换。 - 如果一种浮点型存不下另一种整型的值,则后者不会被转换成前者。例如:允许
float转换到double但是不允许double到float的类型转换。 - 浮点型的值不能转换成整型值。
- 整型值不能转换成浮点型的值。
1
2
3
4
5
6
7
8
9
10
11
12
13
void f(double d, int i)
{
int x2 = d; // 如果 d == 7.9,则 x2 的值变为7
char c2 = i; // 如果 i == 1025,则 c2 的值变为1
int x3{d}; // 错误:可能发生截断
char c3{i}; // 错误:可能发生窄化转换
char c4{24}; // OK:24 能精确的表达一个 char
char c5{264}; // 错误:假定 char 占 8 位,那么 264 不能表示成一个 char
int x4{2.0}; // 错误:不允许 double 到 int 的转换
}
空初始化器列表 {} 指定使用默认值进行初始化
1
2
3
4
5
int x4{}; // x4 被赋值为 0
double d4{}; // d4 被赋值为 0.0
char *p{}; // p 被赋值为 nullptr
vector<int> v4{}; // v4被赋值为一个空向量
string s4{}; // s4 被赋值为 ""
5.1 auto
使用 auto 关键字从初始化器推断变量的类型时,没必要采用列表初始化的方式,应该选择 = 的初始化形式。 而如果初始化器是 {},则推断得到的数据类型肯定不是我们想要的结果。
当声明语句中有
auto关键字时,=是比{}更好的选择。
1
2
auto z1{99}; // 类型是 initializer_list<int>
auto z2 = 99; // 类型是 int
如果没有指定初始化器,则全局变量、名字空间变量、局部 static 变量和 static 成员将会执行相应数据类型的列表
{}初始化。
5.2 decltype
当我们既想要推断得到类型,又不想在此过程中定义一个初始化的变量,此时我们应该使用声明类型修饰符 decltype(expr), 推断所得的结果是 expr 的声明类型。**这种做法在泛型编程中很有效。
1
2
3
4
5
6
7
8
9
10
// 编写一个函数令其执行两个矩阵的加法运算,但是两个矩阵的元素类型可能不同。那么这个结果矩阵的元素类型应该是对应元素求和后的类型
template<class T, class U>
auto operator+(const Matrix<T>& a, const Matrix<U>& b) -> Matrix<decltype(T{}+U{})>
{
Matrix<decltype(T{}+U{})> res;
for(int i = 0; i != a.rows(); ++i)
for(int j = 0; j != a.cols(); ++j)
res(i, j) += a(i, j) + b(i, j);
return res;
}
6. 类型别名
类型别名绝不代表一种新类型,他只是某种已知类型的同义词。别名就是类型的另外一个名字。
- 原来的名字太长、太复杂或者太难看
- 某项程序设计技术要求在同一段上下文中,不同类型有相同的名字
- 在某处提及某种类型仅仅是为了便于后期维护
1
2
3
using Pchar = char*; // 字符串指针
using PF = int(*)(double); // 函数指针,该函数接受一个 double 且返回一个 int
using Vector = std::vector<T, My_allocator<T>>; // 引入一个 template 别名
不允许在类型别名前加修饰符
1
2
3
using Char = char;
using Uchar = unsigned Char; // 错误
using Uchar = unsigned char; // 正确
后缀 _t 通常用于表示类型别名
1
2
3
typedef int int32_t; // 等价于 using int32_t = int;
typedef short int16_t; // 等价于 using int16_t = short;
typedef void(*PtoF)(int); // 等价于 using PtoF = void(*)(int);