1.静态绑定与动态绑定

“绑定”指的是将一个名字(例如变量名或函数名)与一个特定的实体(变量或函数)关联起来的过程。

静态绑定:编译阶段确定函数调用关系,编译器根据变量的声明类型或函数的定义位置来选择调用哪个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Base {
public:
void foo() {
}
};

class Derived : public Base {
public:
void foo() {
}
};
//调用的函数由变量的声明类型所决定
int main() {
Base obj;
obj.foo(); // 静态绑定,调用Base类的foo()函数

Derived obj2;
obj2.foo(); // 静态绑定,调用Derived类的foo()函数
return 0;
}

动态绑定:运行时确定函数的调用关系,根据对象的实际类型调用相应的函数。

注:动态绑定适用于虚函数,通过在基类中声明函数为虚函数,可以在派生类中重写该函数,并在运行时根据对象的实际类型调用相应的函数。

动态绑定的条件:

  • 必须通过指针来调用
  • 该指针是向上转型的
  • 调用的是虚函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Base {
public:
virtual void foo() {
}
};

class Derived : public Base {
public:
void foo() override {
}
};

int main() {
Base* obj = new Derived();
obj->foo(); // 动态绑定,调用Derived类的foo()函数
return 0;
}

注:动态绑定与静态绑定相比具有更高的灵活性,但速度也会较慢

2.两种转型

2.1 向上转型

向上转型:派生类向基类转换的过程,是隐式的,不需要显式的类型转换。

注:在向上转型的过程中没有发生对象的拷贝,而是将派生类对象的地址赋给基类指针,基类指针可以访问基类中定义的成员,但不能访问派生类特有的成员。向上转型体现了指针的多态性,可以用来实现动态绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Base {
public:
void baseFunction() {
}
};

class Derived : public Base {
public:
void derivedFunction() {
}
};

int main() {
Derived derivedObj;

// 向上转型,将Derived对象的地址赋给Base指针
Base* basePtr = &derivedObj;

// 可以通过基类指针调用基类的成员函数
basePtr->baseFunction();

return 0;
}

向上转型比较安全,可以由编译器自动完成,不会有数据的丢失,在编译期间转换,如果转换失败会抛出编译错误,所以可以及时地发现错误。

安全的原因:

  • 基类指针只能访问基类成员,降低了误用的风险
  • 向上转型只是将派生类对象的地址赋给基类指针,而不会改变对象本身的内存结构
  • 在向上转型中,编译器能够静态地检查类型兼容性。如果存在不兼容的类型关系,编译时会发出错误,避免了一些在运行时才能检测到的问题。

2.2 向下转型

向下转型:从基类向派生类转换的过程,是显式的,需要使用类型转换操作符。

静态转型:static_cast

静态转型是在编译时进行的转型,不提供运行时类型检查。

注:如果静态转型过程中出现错误,可能会导致未定义行为(如数据损坏、程序错误等)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Base {
public:
virtual void foo() {
std::cout << "Base class" << std::endl;
}
};

class Derived : public Base {
public:
void bar() {
std::cout << "Derived class" << std::endl;
}
};

int main() {
Base* basePtr = new Derived();

// 使用 static_cast 进行向下转型
Derived* derivedPtr = static_cast<Derived*>(basePtr);

// 使用派生类指针调用派生类成员函数
derivedPtr->bar();

delete basePtr;

return 0;
}

动态转型:dynamic_cast

动态转型是在运行时进行的转型,提供了类型安全检查。

注:动态转型只能用于含有虚函数的类层次结构,即只能用于多态类型之间的转换。多态类型是指至少有一个虚函数的类或结构体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Base {
public:
virtual void foo() {
std::cout << "Base class" << std::endl;
}
};

class Derived : public Base {
public:
void bar() {
std::cout << "Derived class" << std::endl;
}
};

int main() {
Base* basePtr = new Derived();

// 使用 dynamic_cast 进行向下转型
Derived* derivedPtr = dynamic_cast<Derived*>(basePtr);

if (derivedPtr) {
// 转型成功,使用派生类指针调用派生类成员函数
derivedPtr->bar();
} else {
// 转型失败,可能是由于对象不是Derived类型
std::cout << "Dynamic casting failed." << std::endl;
}

delete basePtr;

return 0;
}

注:如果转型不安全,dynamic_cast 返回空指针(或引用),而不是导致未定义的行为。

3.左值引用与右值引用

3.1 左值引用

左值引用:给变量取别名,可以减少一层拷贝

  • 修改引用对象的值:左值引用允许对左值进行引用,可以修改其值。

    1
    2
    3
    int x = 42;
    int& lvalueRef = x;
    lvalueRef = 10; // 修改 x 的值
  • 传递引用参数:使函数直接操作传入的参数,而不是通过复制产生新的对象,避免不必要的对象复制。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    void 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
    9
    int x = 42;
    int& getReference() {
    return x;
    }
    int main() {
    int& ref = getReference();
    ref = 10; // 修改 x 的值
    return 0;
    }

3.2 右值引用

右值:一个表达式,通常是一些临时对象、字面常量、表达式的计算结果等。

1
2
3
4
5
6
7
8
9
10
11
int getResult() {
return 42; // 函数返回一个右值
}

int main() {
int x = 10; // x 是左值
int y = x + 5; // x + 5 是一个右值

int&& rvalueRef = getResult(); // getResult() 返回的是右值
return 0;
}

注:左值可以取地址,右值不能被取地址

左值引用只能引用左值,经过const修饰的左值引用,既可以引用左值,也可以引用右值:

1
const int& constLvalueRef = 42;  // 常量左值引用引用右值

注:右值是不能被修改的值,所以左值引用被const修饰后才能引用右值

右值引用可以引用move以后的左值:move相当于一个强制转换,将左值转换为右值

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include <utility>

int main() {
int x = 42;

// 使用 std::move 将左值 x 转换为右值引用
int&& rvalueRef = std::move(x);

std::cout << "x after std::move: " << x << std::endl; // 输出 x 的值,已经被 std::move 转换过

return 0;
}

右值移动:

  • 移动语义:旨在提高对对象的资源管理效率,允许在对象资源的所有权转移(资源窃取)时,避免昂贵的深拷贝操作,而采用更经济高效的移动操作。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    class 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
    #include <iostream>
    #include <utility>

    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
2
3
4
5
6
7
8
9
// 泛化的类模板
template <typename T>
class MyClass {
public:
// 泛化的成员函数
void process(T data) {
std::cout << "Generic process: " << data << std::endl;
}
};

4.2 全特化

全特化:为某些类型提供更高效的实现

成员函数的全特化

1
2
3
4
5
// 成员函数的特化
template <>
void MyClass<int>::process(int data) {
std::cout << "Specialized process for int: " << data * 2 << std::endl;
}

类模板全特化:

1
2
3
4
5
6
7
8
// 模板的特化
template <>
class MyClass<int> {
public:
void process(int data) {
std::cout << "Specialized implementation for int" << std::endl;
}
};

4.3 偏特化

模板偏特化是指在泛化的模板基础上,对其中的某一部分进行特化。

模板参数数量的偏特化:特化部分参数,还存在一部分参数使用通用的模板定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 泛化的类模板,有两个模板参数
template <typename T, typename U>
class Pair {
public:
T first;
U second;
};

// 模板参数数量的偏特化,对第一个模板参数进行特化
template <typename U>
class Pair<int, U> {
public:
int first;
U second;
};

模板参数范围的偏特化:对模板的参数范围进行缩小

  • const 特化:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 泛化的类模板
template <typename T>
class MyClass {
public:
void process(T data) {
std::cout << "Generic process: " << data << std::endl;
}
};

// const 特化
template <typename T>
class MyClass<const T> {
public:
void process(const T data) {
std::cout << "Specialized process for const type: " << data << std::endl;
}
};
  • 指针特化:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 泛化的类模板
template <typename T>
class MyContainer {
public:
void setValue(T value) {
std::cout << "Generic setValue: " << value << std::endl;
}
};

// 指针特化
template <typename T>
class MyContainer<T*> {
public:
void setValue(T* ptr) {
std::cout << "Specialized setValue for pointers: " << *ptr << std::endl;
}
};
  • 左值引用特化:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 泛化的类模板
template <typename T>
class MyReference {
public:
void printValue(T value) {
std::cout << "Generic printValue: " << value << std::endl;
}
};

// 左值引用特化
template <typename T>
class MyReference<T&> {
public:
void printValue(T& ref) {
std::cout << "Specialized printValue for lvalue references: " << ref << std::endl;
}
};
  • 右值引用特化:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 泛化的类模板
template <typename T>
class MyRValueReference {
public:
void printValue(T value) {
std::cout << "Generic printValue: " << value << std::endl;
}
};

// 右值引用特化
template <typename T>
class MyRValueReference<T&&> {
public:
void printValue(T&& rvalue) {
std::cout << "Specialized printValue for rvalue references: " << rvalue << std::endl;
}
};

注:函数模板是不能偏特化的,只有类模板可以进行偏特化。函数模板可以不显式指定类型。

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
2
3
void func() {
int x = 10; // x 是一个栈对象
}

6.2 堆对象

堆对象: 在堆上分配的对象,通过new关键字动态分配内存,或者通过智能指针等管理。堆是一块较大但相对慢的内存区域,适合在运行时动态分配大量内存。

特点:

  • 动态内存管理:堆上的内存分配由程序员手动管理,必须使用delete来释放(或使用智能指针来自动管理)。
  • 分配速度较慢:由于堆内存需要动态管理和查找空闲区域,分配和释放速度较慢。
  • 生命周期灵活:堆对象的生命周期不受限于作用域,直到显式释放内存时才会销毁。
  • 适合大型数据:堆可以容纳较大的对象,不易出现栈溢出。
1
2
3
4
5
void func() {
int* p = new int(10); // p 是一个指向堆对象的指针
// ... 使用 p
delete p; // 手动释放堆内存
}

6.3 实例

堆对象:

1
2
vector<shape*> shapevector;
shapevector.push_back(new Line(p1, p2));

new关键字会在堆上动态分配内存,并返回一个指向该内存的指针。它的生命周期不受限于当前作用域,只有在明确调用delete时才会释放内存。

而如果是栈对象:

1
2
Line line(p1, p2);
shapevector.push_back(&line);

当作用域结束时,例如函数返回后,line会自动销毁。如果shapevector还保留着指向line的指针,那么这个指针会变成悬空指针(dangling pointer),引发未定义行为。

7.mutable

8.explicit

explicit:用于构造函数和转换运算符,防止隐式类型转换

  • 用于构造函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
#include <string>

class MyClass {
public:
explicit MyClass(int value) { // 显式构造函数
std::cout << "MyClass constructed with value: " << value << std::endl;
}
};

int main() {
MyClass obj1(42); // 正确:显式调用构造函数
// MyClass obj2 = 42; // 错误:隐式调用被禁止
return 0;
}
  • 用于转换运算符:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>

class MyClass {
public:
explicit operator int() const { // 显式转换运算符
return 42;
}
};

int main() {
MyClass obj;
// int x = obj; // 错误:隐式转换被禁止
int y = static_cast<int>(obj); // 正确:显式转换
std::cout << "y = " << y << std::endl;
return 0;
}