C++零碎知识
1.静态绑定与动态绑定
“绑定”指的是将一个名字(例如变量名或函数名)与一个特定的实体(变量或函数)关联起来的过程。
静态绑定:在编译阶段确定函数调用关系,编译器根据变量的声明类型或函数的定义位置来选择调用哪个函数
1 | class Base { |
动态绑定:在运行时确定函数的调用关系,根据对象的实际类型调用相应的函数。
注:动态绑定适用于虚函数,通过在基类中声明函数为虚函数,可以在派生类中重写该函数,并在运行时根据对象的实际类型调用相应的函数。
动态绑定的条件:
- 必须通过指针来调用
- 该指针是向上转型的
- 调用的是虚函数
1 | class Base { |
注:动态绑定与静态绑定相比具有更高的灵活性,但速度也会较慢
2.两种转型
2.1 向上转型
向上转型:派生类向基类转换的过程,是隐式的,不需要显式的类型转换。
注:在向上转型的过程中没有发生对象的拷贝,而是将派生类对象的地址赋给基类指针,基类指针可以访问基类中定义的成员,但不能访问派生类特有的成员。向上转型体现了指针的多态性,可以用来实现动态绑定。
1 | class Base { |
向上转型比较安全,可以由编译器自动完成,不会有数据的丢失,在编译期间转换,如果转换失败会抛出编译错误,所以可以及时地发现错误。
安全的原因:
- 基类指针只能访问基类成员,降低了误用的风险
- 向上转型只是将派生类对象的地址赋给基类指针,而不会改变对象本身的内存结构
- 在向上转型中,编译器能够静态地检查类型兼容性。如果存在不兼容的类型关系,编译时会发出错误,避免了一些在运行时才能检测到的问题。
2.2 向下转型
向下转型:从基类向派生类转换的过程,是显式的,需要使用类型转换操作符。
静态转型:static_cast
静态转型是在编译时进行的转型,不提供运行时类型检查。
注:如果静态转型过程中出现错误,可能会导致未定义行为(如数据损坏、程序错误等)
1 | class Base { |
动态转型:dynamic_cast
动态转型是在运行时进行的转型,提供了类型安全检查。
注:动态转型只能用于含有虚函数的类层次结构,即只能用于多态类型之间的转换。多态类型是指至少有一个虚函数的类或结构体。
1 | class Base { |
注:如果转型不安全,dynamic_cast
返回空指针(或引用),而不是导致未定义的行为。
3.左值引用与右值引用
3.1 左值引用
左值引用:给变量取别名,可以减少一层拷贝
修改引用对象的值:左值引用允许对左值进行引用,可以修改其值。
1
2
3int x = 42;
int& lvalueRef = x;
lvalueRef = 10; // 修改 x 的值传递引用参数:使函数直接操作传入的参数,而不是通过复制产生新的对象,避免不必要的对象复制。
1
2
3
4
5
6
7
8
9
10void modifyValue(int& value) {
value *= 2;
}
int main() {
int x = 5;
modifyValue(x); // 传递 x 的引用,函数可以修改 x 的值
// 现在 x 的值为 10
return 0;
}函数返回引用:可以返回左值引用,避免创建临时对象。
1
2
3
4
5
6
7
8
9int x = 42;
int& getReference() {
return x;
}
int main() {
int& ref = getReference();
ref = 10; // 修改 x 的值
return 0;
}
3.2 右值引用
右值:一个表达式,通常是一些临时对象、字面常量、表达式的计算结果等。
1 | int getResult() { |
注:左值可以取地址,右值不能被取地址
左值引用只能引用左值,经过const修饰的左值引用,既可以引用左值,也可以引用右值:
1 | const int& constLvalueRef = 42; // 常量左值引用引用右值 |
注:右值是不能被修改的值,所以左值引用被const修饰后才能引用右值
右值引用可以引用move以后的左值:move相当于一个强制转换,将左值转换为右值
1 |
|
右值移动:
移动语义:旨在提高对对象的资源管理效率,允许在对象资源的所有权转移(资源窃取)时,避免昂贵的深拷贝操作,而采用更经济高效的移动操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22class MyString {
public:
// 移动构造函数
MyString(MyString&& other) : data(other.data), size(other.size) {
other.data = nullptr;
other.size = 0;
}
private:
char* data;
size_t size;
};
int main() {
MyString source = "Hello";
// 移动构造函数被调用,资源的所有权从 source 转移到 destination
MyString destination = std::move(source);
// 此时 source 不再拥有资源
return 0;
}完美转发:实现一种通用的、保留原参数特性的参数传递机制(即接收左值作为参数,也可以接收右值作为参数)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class MyClass {
public:
template <typename T>
MyClass(T&& arg) : data(std::forward<T>(arg)) {
// 构造函数中的完美转发
}
private:
int data;
};
int main() {
int x = 42;
// 通过完美转发调用构造函数
MyClass obj1(x); // 左值
MyClass obj2(10); // 右值
return 0;
}注:
std::forward
的作用是保留原始参数的左值或右值性质,以及 const 修饰符,实现一种通用的参数传递机制,其位于头文件<utility>
中,并定义在命名空间std
中
4.模板(泛化、全特化、偏特化)
4.1 模板泛化
模板泛化是不关心具体的类型,而是提供了通用的实现。
1 | // 泛化的类模板 |
4.2 全特化
全特化:为某些类型提供更高效的实现
成员函数的全特化
1 | // 成员函数的特化 |
类模板全特化:
1 | // 模板的特化 |
4.3 偏特化
模板偏特化是指在泛化的模板基础上,对其中的某一部分进行特化。
模板参数数量的偏特化:特化部分参数,还存在一部分参数使用通用的模板定义。
1 | // 泛化的类模板,有两个模板参数 |
模板参数范围的偏特化:对模板的参数范围进行缩小
- const 特化:
1 | // 泛化的类模板 |
- 指针特化:
1 | // 泛化的类模板 |
- 左值引用特化:
1 | // 泛化的类模板 |
- 右值引用特化:
1 | // 泛化的类模板 |
注:函数模板是不能偏特化的,只有类模板可以进行偏特化。函数模板可以不显式指定类型。
5.vector<shape>
和vector<shape*>
的多态性
std::vector<shape>
:
不支持多态性。如果有一个shape
的派生类(例如circle
),并且你试图将circle
对象存储在std::vector<shape>
中,会发生对象切片(object slicing),即只存储shape
部分的数据。
std::vector<shape*>
:
支持多态性。可以将指向shape
派生类对象的指针存储在vector
中,并通过基类指针调用虚函数,实现多态行为。
注1:对象切片(object slicing):派生类对象被赋值给基类对象,只会复制基类部分的成员变量,而派生类特有的成员变量会被丢弃。
注2:通过基类的指针或引用来指向派生类对象,可以避免对象切片。
6.堆对象 栈对象
6.1 栈对象
栈对象:在栈上分配内存的对象,通过直接声明变量的方式创建。
特点:
- 自动内存管理:栈上的内存由编译器自动管理,超出作用域后自动释放,不需要程序员手动释放。
- 分配速度快:栈内存分配是线性分配,速度非常快,因此栈对象创建和销毁的效率高。
- 生命周期受限于作用域:栈对象的生命周期受到作用域的限制,一旦超出作用域,栈内存即被释放。
- 大小受限:由于栈的大小有限,过大的对象在栈上分配可能会导致栈溢出,尤其是递归调用或大数组时。
eg:
1 | void func() { |
6.2 堆对象
堆对象: 在堆上分配的对象,通过new
关键字动态分配内存,或者通过智能指针等管理。堆是一块较大但相对慢的内存区域,适合在运行时动态分配大量内存。
特点:
- 动态内存管理:堆上的内存分配由程序员手动管理,必须使用
delete
来释放(或使用智能指针来自动管理)。 - 分配速度较慢:由于堆内存需要动态管理和查找空闲区域,分配和释放速度较慢。
- 生命周期灵活:堆对象的生命周期不受限于作用域,直到显式释放内存时才会销毁。
- 适合大型数据:堆可以容纳较大的对象,不易出现栈溢出。
1 | void func() { |
6.3 实例
堆对象:
1 | vector<shape*> shapevector; |
new
关键字会在堆上动态分配内存,并返回一个指向该内存的指针。它的生命周期不受限于当前作用域,只有在明确调用delete
时才会释放内存。
而如果是栈对象:
1 | Line line(p1, p2); |
当作用域结束时,例如函数返回后,line
会自动销毁。如果shapevector
还保留着指向line
的指针,那么这个指针会变成悬空指针(dangling pointer),引发未定义行为。
7.mutable
8.explicit
explicit:用于构造函数和转换运算符,防止隐式类型转换
- 用于构造函数:
1 |
|
- 用于转换运算符:
1 |
|