#CS225

基础语法

Template 模板

模板的基本概念

模板本质上是一种参数化的类型或函数。 你可以把类型或函数的一部分(通常是类型)作为参数传递给模板,编译器会根据你提供的参数生成特定的代码。

模板的种类

C++ 中有两种主要的模板:

2.1 函数模板

函数模板允许你创建可以处理多种数据类型的函数。

语法:

1
2
3
4
template <typename TypeParameter1, typename TypeParameter2, ...>
return_type function_name(parameter_list) {
  // 函数体
}

例子:交换两个变量的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
template <typename T>
void swap(T& a, T& b) {
  T temp = a;
  a = b;
  b = temp;
}

int main() {
  int x = 5, y = 10;
  swap(x, y);
  std::cout << "x = " << x << ", y = " << y << std::endl; // 输出 x = 10, y = 5

  double p = 3.14, q = 2.71;
  swap(p, q);
  std::cout << "p = " << p << ", q = " << q << std::endl; // 输出 p = 2.71, q = 3.14

  return 0;
}

2.2 类模板

类模板允许你创建可以处理多种数据类型的类。 这在容器类(例如 std::vectorstd::list)的实现中非常有用。

语法:

1
2
3
4
template <typename TypeParameter1, typename TypeParameter2, ...>
class ClassName {
  // 类定义 (成员变量、成员函数等)
};

例子:一个简单的动态数组类

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
template <typename T>
class DynamicArray {
private:
  T* data;
  int size;
  int capacity;

public:
  DynamicArray(int initialCapacity = 10) : size(0), capacity(initialCapacity) {
    data = new T[capacity];
  }

  ~DynamicArray() {
    delete[] data;
  }

  void push_back(const T& value) {
    if (size == capacity) {
      // 扩容
      capacity *= 2;
      T* newData = new T[capacity];
      for (int i = 0; i < size; ++i) {
        newData[i] = data[i];
      }
      delete[] data;
      data = newData;
    }
    data[size++] = value;
  }

  T& operator[](int index) {
    if (index < 0 || index >= size) {
      throw std::out_of_range("Index out of bounds");
    }
    return data[index];
  }

  int getSize() const {
    return size;
  }
};

int main() {
  DynamicArray<int> intArray;
  intArray.push_back(1);
  intArray.push_back(2);
  intArray.push_back(3);

  std::cout << "Int Array Size: " << intArray.getSize() << std::endl; // 输出 Int Array Size: 3
  std::cout << "Int Array[0]: " << intArray[0] << std::endl;        // 输出 Int Array[0]: 1

  DynamicArray<double> doubleArray;
  doubleArray.push_back(3.14);
  doubleArray.push_back(2.71);

  std::cout << "Double Array Size: " << doubleArray.getSize() << std::endl; // 输出 Double Array Size: 2
  std::cout << "Double Array[1]: " << doubleArray[1] << std::endl;      // 输出 Double Array[1]: 2.71

  return 0;
}

在这个例子中,DynamicArray 是一个类模板。 typename T 表示 T 是一个类型参数,可以用来指定数组中存储的数据类型。 DynamicArray<int> 创建一个存储整数的动态数组,而 DynamicArray<double> 创建一个存储双精度浮点数的动态数组。

模板参数

模板参数可以是:

3.1 非类型模板参数

非类型模板参数允许你在编译时指定一个常量值。

例子:固定大小的数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
template <typename T, int N>
class FixedSizeArray {
private:
  T data[N];
public:
  T& operator[](int index) {
    if (index < 0 || index >= N) {
      throw std::out_of_range("Index out of bounds");
    }
    return data[index];
  }
};

int main() {
  FixedSizeArray<int, 5> intArray;
  intArray[0] = 10;
  std::cout << intArray[0] << std::endl; // 输出 10
  return 0;
}

在这个例子中,N 是一个非类型模板参数,它指定了数组的大小。 FixedSizeArray<int, 5> 创建一个可以存储 5 个整数的固定大小的数组。

3.2 模板参数作为模板参数

模板参数本身也可以是另一个模板,这允许你创建更复杂和灵活的数据结构。

例子:存储一个 std::vectorstd::list

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
33
34
35
36
#include <vector>
#include <list>
#include <iostream>

template <typename T>
void printList(const std::list<T>& lst) {
    for (const auto& element : lst) {
        std::cout << element << " ";
    }
    std::cout << std::endl;
}


int main() {
    // Create a list of vectors of integers
    std::list<std::vector<int>> listOfVectors;

    // Add some vectors to the list
    listOfVectors.push_back({1, 2, 3});
    listOfVectors.push_back({4, 5, 6, 7});
    listOfVectors.push_back({8, 9});

    // Iterate through the list and print each vector
    for (const auto& vec : listOfVectors) {
        std::cout << "Vector elements: ";
        for (int element : vec) {
            std::cout << element << " ";
        }
        std::cout << std::endl;
    }

    std::list<int> myList = {10, 20, 30, 40, 50};
    printList(myList);

    return 0;
}

模板实例化

模板本身不是真正的类或函数。 只有当你使用具体的类型参数来实例化模板时,编译器才会生成实际的代码。 模板实例化可以分为两种:

显式实例化例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
template <typename T>
T min(T a, T b) {
  return (a < b) ? a : b;
}

// 显式实例化 min<double>(double a, double b)
template double min<double>(double a, double b);

int main() {
  // 你仍然可以使用隐式实例化
  std::cout << min(5, 10) << std::endl; // 输出 5 (int)

  // 使用显式实例化
  double p = 3.14, q = 2.71;
  std::cout << min<double>(p, q) << std::endl; // 输出 2.71 (double)

  return 0;
}

模板特化 (Template Specialization)

模板特化允许你为特定的类型参数组合提供不同的模板实现。 这在你需要为某些类型提供优化或特殊处理时非常有用。

5.1 函数模板特化

你可以为特定的类型提供不同的函数模板实现。

例子:为 char* 类型的 max 函数提供特殊处理

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <cstring> // for strcmp

template <typename T>
T max(T a, T b) {
  return (a > b) ? a : b;
}

// 函数模板特化:为 char* 类型提供特殊处理
template<>
const char* max<const char*>(const char* a, const char* b) {
  return (std::strcmp(a, b) > 0) ? a : b;
}

Struct

1. struct(结构体)

2. structclass 的区别

类 ->面向对象

Copy Constructor vs Assignment Operator & Rule of Three

1. Copy Constructor vs Assignment Operator

对比项 拷贝构造函数 赋值运算符
作用 用于创建新对象并复制 用于已有对象的赋值
调用时机 初始化对象时 赋值操作时
默认实现 逐成员拷贝 逐成员赋值
是否释放旧资源 否(对象是新创建的) 是(必须释放旧资源)

拷贝构造函数(Copy Constructor)

1
ClassName(const ClassName& other);

示例

1
2
3
4
5
6
7
8
9
class Example {
public:
    int* data;
    
    // 拷贝构造函数
    Example(const Example& other) {
        data = new int(*other.data);
    }
};

2. 赋值运算符(Assignment Operator)

1
ClassName& operator=(const ClassName& other);
1
2
3
4
5
6
7
8
9
10
11
12
13
class Example {
public:
    int* data;
    
    // 赋值运算符
    Example& operator=(const Example& other) {
        if (this != &other) { // 防止自赋值,常见的代码流程
            delete data; // 释放旧资源!!重要
            data = new int(*other.data);
        }
        return *this;
    }
};

2. Rule of Three(三法则)

三法则(Rule of Three) 指的是如果一个类需要显式定义以下三者之一,则很可能需要显式定义另外两个:

  1. 拷贝构造函数
  2. 赋值运算符
  3. 析构函数

为什么需要三法则?

当类涉及动态内存分配或其他资源管理(如文件句柄、互斥锁),默认的拷贝构造、赋值运算符只会浅拷贝,导致资源重复释放(如 delete 两次)或悬挂指针(dangling pointer)。

示例:违反三法则的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class BadExample {
public:
    int* data;

    BadExample(int value) { data = new int(value); }
    ~BadExample() { delete data; }  // 需要释放资源

    // 但是没有提供拷贝构造和赋值运算符,导致浅拷贝问题
};

void test() {
    BadExample obj1(10);
    BadExample obj2 = obj1;  // 这里调用默认拷贝构造,data 指针被浅拷贝!
    obj2 = obj1;  // 这里调用默认赋值运算符,data 指针被浅拷贝!
}  // obj1 和 obj2 都会调用析构函数,导致 delete 两次,程序崩溃!

正确实现(遵循三法则)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class GoodExample {
public:
    int* data;

    // 构造函数
    GoodExample(int value) { data = new int(value); }

    // 拷贝构造函数
    GoodExample(const GoodExample& other) {
        data = new int(*other.data);
    }

    // 赋值运算符
    GoodExample& operator=(const GoodExample& other) {
        if (this != &other) {
            delete data;  // 释放旧资源
            data = new int(*other.data);
        }
        return *this;
    }

    // 析构函数
    ~GoodExample() { delete data; }
};

函数

参数传递

[!tip] Pass by Value, Pass by Reference, Pass by Pointers

1. 概念对比

特性 值传递 (Pass by Value) 引用传递 (Pass by Reference) 指针传递 (Pass by Pointer)
传递内容 实参的副本 实参的别名 实参的地址
修改实参
空值 不可能 不可能 可能 (空指针)
开销 较大 (复制对象) 较小 (无复制) 较小 (复制地址)
安全性 高 (不修改实参) 低 (可能空指针)
Copy Constructor 调用,构造创建对象的副本 不调用,因为传递的是对象的别名 不调用,因为传递的是对象的地址

2. 详细解释

3. 代码示例

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <iostream>

using namespace std;

// 值传递
void modifyValue(int x) {
    x = 10; // 修改的是 x 的副本,不会影响实参
    cout << "Inside modifyValue: x = " << x << endl;
}

// 引用传递
void modifyReference(int &x) {
    x = 20; // 修改的是 x 的别名,会影响实参
    cout << "Inside modifyReference: x = " << x << endl;
}

// 指针传递
void modifyPointer(int *x) {
    if (x != nullptr) { // 检查指针是否为空
        *x = 30; // 通过解引用指针,修改实参
        cout << "Inside modifyPointer: *x = " << *x << endl;
    } else {
        cout << "Error: Null pointer!" << endl;
    }
}

int main() {
    int a = 1;

    cout << "Before modifyValue: a = " << a << endl;
    modifyValue(a);
    cout << "After modifyValue: a = " << a << endl;

    cout << "Before modifyReference: a = " << a << endl;
    modifyReference(a);
    cout << "After modifyReference: a = " << a << endl;

    cout << "Before modifyPointer: a = " << a << endl;
    modifyPointer(&a);
    cout << "After modifyPointer: a = " << a << endl;

    // 指针传递空指针示例
    int *ptr = nullptr;
    modifyPointer(ptr);

    return 0;
}

[!tip] Const 关键字

1. const 成员函数 (Const Member Functions)

const 成员函数是指在类中声明为 const 的成员函数。它承诺不会修改调用它的对象的状态,即不会修改对象的任何非 mutable 成员变量。

2. 哪些函数可以访问 const 变量 (Const Objects’ Member Variables)

3. 代码示例

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
33
34
35
36
37
38
39
40
41
42
#include <iostream>

class MyClass {
private:
    int value;
    mutable int accessCount; // mutable 成员变量可以在 const 成员函数中修改

public:
    MyClass(int val) : value(val), accessCount(0) {}

    // const 成员函数,用于获取 value 的值
    int getValue() const {
        accessCount++; // 允许修改 mutable 成员变量
        std::cout << "getValue() const called. Access count: " << accessCount << std::endl;
        return value;
    }

    // 非 const 成员函数,用于设置 value 的值
    void setValue(int val) {
        value = val;
        std::cout << "setValue() called." << std::endl;
    }

    void print() const {
        std::cout << "Value: " << value << ", Access Count: " << accessCount << std::endl;
    }
};

int main() {
    MyClass obj(10); // 非 const 对象
    std::cout << "Initial value: " << obj.getValue() << std::endl; // 调用 const 成员函数
    obj.setValue(20); // 调用非 const 成员函数
    std::cout << "New value: " << obj.getValue() << std::endl; // 再次调用 const 成员函数
    obj.print();

    const MyClass constObj(30); // const 对象
    std::cout << "Const object value: " << constObj.getValue() << std::endl; // 调用 const 成员函数
    //constObj.setValue(40); // 错误:const 对象不能调用非 const 成员函数
    constObj.print();

    return 0;
}

返回值

[!tip] Return by Value, Reference, Pointer

1. 返回值传递 (return by value):

2. 返回引用传递 (return by reference):

3. 返回指针传递 (return by pointer):

Reference

There shall be no references to references, no arrays of references, and no pointers to references. References are not objects. They do not occupy memory. — Declaring an array of nothing

Polymorphism 多态性

虚函数

1. Virtual Function(虚函数)

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
33
34
35
36
37
#include <iostream>

class Base {
public:
    virtual void print() {
        std::cout << "Base class print" << std::endl;
    }
};

class Derived : public Base {
public:
    void print() override { // 重写基类的 virtual function
        std::cout << "Derived class print" << std::endl;
    }
};

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

    // 1. 使用基类指针指向基类对象
    basePtr->print(); // 输出 "Base class print"

    // 2. 使用基类指针指向派生类对象
    derivedPtr->print(); // 输出 "Derived class print" (多态)

    // 3. 使用派生类指针指向派生类对象
    derivedPtr2->print(); // 输出 "Derived class print"

    delete basePtr;
    delete derivedPtr;
    delete derivedPtr2;

    return 0;
}

2. Pure Virtual Function(纯虚函数)

3. Why Constructor Cannot Be Virtual Function(为什么构造函数不能是虚函数)

派生类继承 & constructor

调用基类构造函数的方法:

主要有两种方式:隐式调用和显式调用。

缺点: 如果基类 没有无参构造函数,则会发生编译错误。

注意事项:

类继承权限

总结表:

Inheritance Type Base Class public Base Class protected Base Class private Outside the Derived Class
public public protected Inaccessible Accessible
protected protected protected Inaccessible Inaccessible
private private private Inaccessible Inaccessible

重要提示:

1. Public Inheritance (公有继承):

2. Protected Inheritance (保护继承):

3. Private Inheritance (私有继承):

运算符重载

运算符重载允许你为自定义类型重新定义标准运算符。 这使得自定义类型的使用更加直观,像内置类型一样。 本质上,运算符重载就是一种多态性,因为同一个运算符可以对不同类型的数据进行操作。

Syntax
运算符重载本质上就是定义一个名为 operator<operator> 的成员函数或友元函数。 <operator> 代表你要重载的运算符符号。

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
#include <iostream>

class Complex {
public:
  double real;
  double imag;

  Complex(double r = 0, double i = 0) : real(r), imag(i) {}

  Complex operator+(const Complex& other) const {
    return Complex(real + other.real, imag + other.imag);
  }

  Complex operator-(const Complex& other) const {
      return Complex(real - other.real, imag - other.imag);
  }

  void print() const {
    std::cout << real << " + " << imag << "i\n";
  }
};


int main() {
  Complex c1(2, 3);
  Complex c2(4, -1);
  Complex c3 = c1 + c2; // 使用 + 运算符
  Complex c4 = c1 - c2; // 使用 - 运算符
  c3.print(); // Output: 6 + 2i
  c4.print(); // Output: -2 + 4i
  return 0;
}

这里,我们重载了 +- 运算符,使它们能够对 Complex 对象进行加减运算。 operator+operator- 函数的实现定义了运算符的行为。

总结:

C++ 的多态性使得代码更具可重用性、可扩展性和可维护性。通过继承和虚函数,你可以用统一的接口操作不同类型的对象,从而简化代码并降低耦合度。 运算符重载是多态性的一个特例,它使自定义类型更加易于使用,并提高了代码的可读性。 记住,虚函数是实现运行时多态性的关键,而运算符重载则增强了自定义类型的表达能力。 需要谨慎设计,避免重载运算符导致代码难以理解。