C++ 零散知识点
# 章节 1
# 赋值运算符
通常,C++ 中的初始化赋值,我们是使用赋值运算符 =
。但 C++ 实际上还提供了一种更新的方式 {}
。
double d1 = 2.3;
double d2 {2.3};
double d3 = {2.3}; // = 符号可以有, 但没必要
2
3
两者之间的区别在于:
=
符号存在潜在的发生窄化类型转换(丢失数据)的风险{}
赋值会在编译器检测数据类型,不会导致数据丢失
int x1 = 2.1; // x1 = 2
int x2 {2.1}; // 错误,试图执行浮点数向整数的类型转换
int x3 = {2.1};// 错误,试图执行浮点数向整数的类型转换(=是多余的)
2
3
注意:{}
赋值只能在初始化的时候用。
# 常量
C++ 支持两种不变性概念
const
“我承诺不改变这个值”constexpr
“在编译时求值”
const
主要用于说明接口,表示变量在传入函数时不会在函数内部被改变,编译器负责确认并执行 const
的承诺。而 constexpr
主要用于说明常量,作用是允许将数据置入只读内存中以及提升性能。
const
只是承诺这个值不会被程序员修改(readonly),而 constexpr
是真正的常量(在满足条件的情况下会在编译期就被计算出来),甚至可以用来代替宏替换。
int x = 5; // 变量 x,运行时求值
const int y1 = x; // 只读变量,x 并不是一个常量,所以 y1 也并不是一个常量
const int y2 = 5; // 常量
constexpr int z1 = x; // error
constexpr int z2 = y1; // error
constexpr int z3 = y2; // 常量
constexpr double s1 = 1.4 * square(x); // error
constexpr double s2 = 1.4 * square(z3); // accept
2
3
4
5
6
7
8
9
如果某个函数用在常量表达式中,则该函数必须定义成 constexpr
。例如
constexpr int square(double x) {
return x * x;
}
2
3
函数要想定义成 constexpr
, 必须足够简单:函数中只能有一个用于计算某个值的 return
语句。实际上,constexpr
也可以接受非常量实参,但是,此时其结果将不再是一个常量表达式。
当程序上下文不需要常量表达式时,我们可以使用非常量表达式实参来调用 constexpr
函数。我们不必把同一个函数定义两次,一个用于常量表达式,一个用于变量。
有的场合,常量表达式是语言规则所必须的的。比如数组的界、case
标签、某些模板实参和使用 constexpr
申明的常量。
# 范围循环
C++ 针对数组与迭代器提供了范围循环。
for (<type> <variable-name> : <range_expression>) {
// loop_statement
}
2
3
可遍历的对象包括:
- 数组(不包括指针数组)
- STL 容器以及其他定义了返回该类迭代器的
begin()
和end()
方法的类对象。
也就是说在遍历对象的时候,范围循环其实等效于以下代码。
for (auto x = v.begin(); x != v.end(); ++x) {
// code
}
2
3
通过上述示例不难看出:
- 若要通过范围循环改变序列元素的值,必须将元素声明成引用。否则,此元素相当于一个指定序列元素的拷贝。
- 不能通过范围循环增删序列元素。因为序列的迭代器已经被保存,增删会使迭代器失效。
下面验证在范围循环中改变元素的值必须声明成引用。
int data[]{1, 2, 3, 4, 5, 6};
for (auto &x : data) {
std::cout << x;
++x;
}
std::cout << std::endl;
for (auto x : data) {
std::cout << x;
++x;
}
std::cout << std::endl;
for (auto x : data) {
std::cout << x;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
控制台输出结果
123456
234567
234567
2
3
# 用户自定义类型 - 枚举类
C++11 新增了了枚举类这一自定义类型的简单形式,使得我们可以枚举一系列值。枚举类型常用于描述规模较小的整数值集合。通过使用有指代意义的(且易于记忆的)枚举值名字可提高代码可读性,降低出错风险。
enum class TrafficLight {
red,
green,
yellow,
};
enum class Color {
red,
blue,
green
};
2
3
4
5
6
7
8
9
10
11
接下来可以这么定义枚举类:
Color col = Color::red;
TrafficLight light = TrafficLight::red;
2
其中枚举值(如 red
)位于其 enum class
作用域之内。因此我们可以在不同的 enum class
中重复使用这些枚举值而不致引起混淆。
enum
后的 class
指明了两点:
- 枚举是强类型的。
- 它的枚举值位于指定的作用域中。
不同的 enum class
是不同的类型。不能够混用。在上面的例子中,我们不能混用 TrafficLight
和 Color
的值。
Color fo = red; // 错误,哪个 red ?
Color foo = TrafficLight ::red; // 错误,这个 red 不是 Color 的对象
2
也不能隐式混用 Color
和整数值:
int i = Color::red; // 错误,Color::red 不是一个 int
Color z = 2; // 错误,2 不是一个 Color 对象
2
如果不希望显示地限定枚举值名字,并且希望枚举值可以是 int
(无须显式转换),则应该去掉 enum class
中的 class
而得到一个普通的 enum
。
默认情况下,enum class
之定义了赋值,初始化和比较操作。然后,既然枚举类型是一种用户自定义类型,那么我们就可以为它定义别的运算符。比如为 TrafficLight
定义 ++
运算符:
TrafficLight& operator++(TrafficLight& tl) {
switch (tl) {
case TrafficLight::green:
return tl = TrafficLight::yellow;
case TrafficLight::yellow:
return tl = TrafficLight::red;
case TrafficLight::red:
return tl = TrafficLight::green;
default:
return tl = TrafficLight::red;
}
}
TrafficLight light = TrafficLight::red; // light -> TrafficLight::red
++light; // light -> TrafficLight::green
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 名字空间
名字空间主要为了表达两个语义:
- 某些声明是属于一个整体的
- 他们的名字不会与其他名字空间中的名字冲突
namespace mine {
class complex {};
int main() {}
}
int mine::main() {
}
int main() {
return mine::main();
}
2
3
4
5
6
7
8
9
10
11
12
13
上述代码我们将 complex
类与 main
放在了 mine
名字空间内,就可以确保我们的名字不会和名字空间 std 中的标准库中的名字冲突。
// 全局使用名字空间
using namespace std;
// 使用指定名字空间中的函数
mine::main();
2
3
4
5
# 抽象机制
# 构造函数与析构函数
RAII(资源获取即初始化)
# 初始化容器
容器的作用是保存元素,因此我们需要找到一种便利的方式将元素存入容器中。为了做到这一点,一种可能的方式是先用若干元素创建一个 Vector
,然后再依次为这些元素进行赋值。显然这不是最好的方式,下面列举两种更简洁的途径。
- 初始化器列表(initializer-list constructor):使用元素列表进行初始化
- push_back(): 在序列末尾添加一个新元素
# 拷贝和移动容器
page 62 - 等学会了左值引用与右值引用再来
# 函数对象
实质是重载 ()
运算符