跳转至

Class and Template

类与封装

我们希望能定义一类对象,它包含了数据结构、常用方法,以便我们进行复用——这就是类

引入类后,我们可以方便地自定义对象,并围绕它的设计、应用展开工作——即面向对象编程的核心思想

类的定义

一个好的类定义习惯,是在头文件内定义类的接口,而在另一个cpp文件中给出具体实现

头文件案例:

class StanfordID {
private:    //private下定义了访问受限的变量和函数(需要通过类内接口访问)
    std::string name;
    std::string sunet;
    int idNumber;
public:
    StanfordID(std::string name, std::string sunet, int idNumber); //定义了类的声明
    std::string getName();  
    std::string getSunet();
    int getID();
}

实现案例:

注意,我们在实现前加上了StanfordID::来表明它是类的函数

//类的声明:
StanfordID::StanfordID(std::string name, std::string sunet, int idNumber) {
    this->name = name;  //直接使用name是可以的,但为了表明这是类内的元素,更推荐使用this指针
    this->sunet = sunet;
    if ( idNumber > 0 ) this->idNumber = idNumber;
}
//C++11引入的新特性——直接使用`: attribute{value}`
StanfordID::StanfordID(std::string name, std::string sunet, int idNumber): 
    name{name}, sunet{sunet}, idNumber{idNumber} {};
//不传参时的默认初始化——自动匹配符合的参数格式的函数
StanfordID::StanfordID() {
    name = "john";
    sunet = "jappleseed";
    idNumber = 1;}

常量验证

当我们声明一个常量类时,编译器需要知道其类函数是否会修改类的内容,否则会拒绝使用。

因此,我们需要在不修改类内容的函数后面加入const,来提示编译器。此时,传入函数内的类指针为const class *

同时,即使使用了const提示,当const成员函数返回了非const的引用时,仍存在值被修改的风险,而编译器无法检测。

因此,当const成员函数返回引用时,我们需要在前面加入const,防止引用被修改。此外,为了保证非常量类的能正常返回可修改的引用,我们还需要重载一个非const成员函数。

返回类型 非常量对象 常量对象
副本或者新构建的值 正常成员函数 const成员函数
对象内部数据的 引用或指针 时 正常成员函数 const成员函数且返回类型为常量
为了保证非常量下的使用,需要重载

对于最后一种情况,具体重载实现如下

int& Vector::findElement(const int& value) {  //返回类型为引用
        ...//具体实现
 }

const int& Vector::findElement(const int& value) const {  //双const标记
        return const_cast<Vector&>(*this).findElement(value);
        //const_cast<Vector&>定义了一种能将Vector&从常量转为非常量的对象
        //const_cast<Vector&>(*this),将对象转为非常量
        //.findElement(value),调用非常量对象下的非常量findElement实现
 }

继承与成员访问控制

继承

在class定义后接上如: public Parent来继承父类的属性

使用virtual关键词来允许子类重写父类的函数

(注意virtual同时是一种继承方法,主要是为了解决菱形继承的问题,含义不同)

class Shape {
public:
    virtual double area() const = 0;
};
class Circle : public Shape {
public:
    // constructor
    Circle(double radius): _radius{radius} {};
    double area() const {
        return 3.14 * _radius * _radius;
    }
private:
    double _radius;
};

成员访问控制

为了管理类内成员对外部、子类的成员访问权限,C++中有三种类内访问控制修饰符,其限制如下:

访问控制修饰符 类内访问 派生类访问 类外访问 用途
public ✔️ ✔️ ✔️ 定义类的接口
private ✔️ ❌ ❌ 封装实现细节
protected ✔️ ✔️ ❌ 支持继承中的扩展

其中,private只能由类本身的函数访问,隐藏了封装细节,并保证数据的安全性

而为了在继承关系中提供一定的灵活性,允许派生类访问基类的某些实现细节,我们也可以使用protected,即子类中可以定义新的函数,对此类成员进行访问。

类的继承与成员属性类似,同样具有public, protected, private三类方式,它会对应地,将原先父类的高访问权限降级到低访问权限——例如,使用protected继承,那么原本父类的public成员会变为protected,而private成员不变。具体的访问控制影响如下:

基类成员访问权限 public 继承 protected 继承 private 继承
public 成员 派生类中为 public 派生类中为 protected 派生类中为 private
protected 成员 派生类中为 protected 派生类中为 protected 派生类中为 private
private 成员 不可访问 不可访问 不可访问

其中,private成员只能通过基类提供的public或protected的调用函数进行访问。

友元函数

为了在其他类内访问类内私有函数,我们可以引入友元函数。在被访问的类内,通过 friend 声明外部函数,可以允许其可访问类的私有(private)和保护(protected)成员,举例如friend void func(const MyClass& obj);

  • 用途:
  • 直接访问类私有数据(如 obj.secret)。
  • 重载操作符(如 <</>>)时简化实现(friend ostream& operator<<(...))。

  • 注意:

  • 友元关系单向且不可继承。
  • 友元函数应当慎重使用!过度使用会破坏封装性,降低代码可维护性。

模板

当我们需要类/函数中允许相似处理不同的类型的对象时,我们可以引入模版(Template)

使用template <typename T>放在类/函数前,说明需要使用模板。随后可以直接使用T以及它的衍生类型,进行类和函数的撰写。

一个有趣的事实是,<typename T>可以很自由的改变,包括使用class(完全等价), bool(只能使用真或假), int(可以使用整数)

模板的机理

事实上,模板的实例化是编译期行为。编译器需要在编译每个翻译单元(.cpp文件)时,根据模板的完整定义,替换生成具体类型的代码——例如T& at(size_t index);会变为int at(size_t index);。

如果模板的实现(函数或类的定义)不在头文件中,其他.cpp文件包含头文件时,编译器无法看到实现,会导致链接错误。因此,模板要求在头文件内存在,或插入实现代码。同时,由于每个T的类型会生成对应的具体类型,编译后的代码存在膨胀的隐患。

基础模版

模板类

使用模板类,我们只需要将所有需要再定义的对象类型变为模板即可

当我们使用一个模板类时,使用classname<T>来初始化对应的模板类型

一个具体的模板类案例如下:

//vector.h
template <typename T>
 class Vector {
 public:
     T& at(size_t i);
 };
#include "Vector.cpp"   //注意!我们需要在vector.h的末尾引用vector.cpp

//vector.cpp
//实现函数也需要使用模版标志
template <typename T>
T& Vector<T>::at(size_t i) {...}    //注意,命名空间(类)也含有<T>

模板函数

与模板类类似,我们也需要前置模板标志,同时,在编译过程中,不同类型的函数会被独立实现

当我们使用一个模板函数时,我们可以省去模板类型的初始化——即让编译器去自行判断函数输入参数类型。

一个经典的STL函数的模板案例如下

template< class InputIt, class T>
InputIt find( InputIt first, InputIt last, const T& value);

Concept(C++20)

当需要对模板参数进行约束时,可以使用 C++20 引入的 concept 机制,明确类型必须满足的接口或特性。

使用 template <typename T> 后通过 requires 或直接定义 concept,规范模板参数的行为,限制类型必须支持的操作或特性

一个关键优势是:编译期检查约束条件,替代复杂的 SFINAE 技巧,提升代码可读性和错误提示。

定义 Concept

通过 requires 表达式或逻辑运算符组合约束条件:

// 定义支持加法操作的类型约束
template <typename T>
concept Addable = requires(T a, T b) {
    { a + b } -> std::convertible_to<T>; // 要求支持 + 且结果可转 T
};
// 组合约束:整型或浮点型
template <typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;
//要求对象存在
template <typename C>
concept Container = requires(C c) {
    c.begin();      // 必须存在 begin()
    c.end();        // 必须存在 end()
    typename C::value_type;  // 必须有内部类型 value_type
};

使用 Concept

template <Numeric T>  // 直接使用 concept 作为模板参数
class Calculator { 
    T add(T a, T b) { return a + b; }
};
//或者
template <typename T> requires Addable<T>   // 通过 requires 子句约束
T sum(T a, T b) { return a + b; }

C++20标准中也引入了许多标准的Concept,可以替换typename,提供更显式,便于维护的类型检查

可变参数模板(Variadic Templates)

有时候我们并不确定输入函数的变量的数量——我们可以通过模板来实现针对不同变量的函数的自动化重载,实现关键如下

  • 定义参数包:template <typename... Args>
  • 展开参数包:通过递归或折叠表达式(C++17+)处理参数。

递归版本(C++11)

在一般实现中,我们需要使用递归的技巧——最后会自动展开定义n个min函数

// 递归终止条件:仅一个参数时返回自身
template <typename T>
T min(T val) {
    return val;
}

// 递归展开:比较当前参数与剩余参数的最小值
template <typename T, typename... Args>
T min(T& first, Args&... args) {
    T rest_min = min(args...); // 展开剩余参数包
    return (first < rest_min) ? first : rest_min;
}
//折叠表达式c++17
template <typename... Args>
auto sum(Args... args) {
    return (args + ...); // 展开为 1 + 2 + 3 + ...
}

注意,每个不同参数组合生成独立实例化代码,且递归使用,会有代码膨胀风险,和递归深度限制的风险

重载与多态

符号重载

可以使用类似函数定义的方式,进行符号重载(绝大部分符合都可以重载,除了指针相关的)

符号重载既可以在类当中,也可以是全局的——这主要取决于交互的对象

例如,当符号是有两侧的对象时,且无序时,一般使用全局的定义

//使用全局实现,调用公开接口
MyClass operator+(const MyClass& obj, int value) {
    MyClass result = obj;
    result.setData(result.getData() + value);
    return result;
}
MyClass operator+(int value, const MyClass& obj) {
    return obj + value;  // 复用上面的实现
}

而当需要在类外定义,且调用私密成员时,可以使用友元函数

class Student {
    friend ostream& operator<<(ostream& os, const Student& s);
private:
    string name;
};

ostream& operator<<(ostream& os, const Student& s) {
    return os << "Student: " << s.name;
}

SMF

Move Semantics

报错管理