跳转至

[C++ Basic] User-Defined Types: Class

导言

C++ Class 知识

1. Class 基础概念

定义与创建

介绍如何定义类,包括类的成员变量和成员函数。

class ClassOne
{
public:
    int m_one;
public:
    ClassOne(double len, double a, double b, double c): m_one(len), X(a), Y(b), Z(c){}
    void PrintSomething(const string& strInput, const int& nInput);
};

void ClassOne::PrintSomething(const string & strInput, const int & nInput)
{
    cout << strInput << nInput << this.m_one << endl;
}

classstruct 的区别

  • 比如class有private等数据控制,struct默认是public。
  • 如果你希望类型主要是一个数据结构,并且数据成员公开,使用struct可能更合适;
  • 如果你希望封装数据并隐藏实现细节,使用class则更符合惯例。
  • 提供简单的例子,例如如何定义类并创建对象。
// 直接使用类名创建对象
ClassOne c1;

// 使用new创建对象
ClassOne *c2 = new ClassOne();

// 删除
delete

使用初始化列表来初始化字段

访问控制

解释 public, private, protected 的作用。 - 示例:类的成员如何通过这些访问控制符来限制访问权限。

三个冒号 : :: 表示作用域解析操作符

在 C++ 中,中间的三个冒号 :: 表示作用域解析操作符(scope resolution operator)。这个操作符用来指定一个特定的类、命名空间或枚举类型中的成员。在你给出的例子中:

class CacheCntlr : ::CacheCntlr

这里使用了作用域解析操作符 :: 来指定全局命名空间中的 CacheCntlr 类。这种情况通常出现在有命名冲突的情况下,即当局部作用域(如当前文件或命名空间内)有一个同名的 CacheCntlr 定义时,而你需要明确指示编译器你要使用的是全局命名空间中的 CacheCntlr 类。

  1. 示例解释

假设你在一个文件或命名空间中定义了一个 CacheCntlr 类,同时在全局命名空间中也有一个同名的类。你可能想要创建一个新的类来扩展全局命名空间中的 CacheCntlr 类。在这种情况下,你就需要用到作用域解析操作符来避免命名冲突:

namespace MyNamespace {
    class CacheCntlr { /* 定义一些内容 */ };

    // 这里定义一个新类,继承自全局命名空间的 CacheCntlr
    class MyCacheCntlr : ::CacheCntlr {
        // 这里的 ::CacheCntlr 引用的是全局命名空间中的 CacheCntlr
    };
}

在这个例子中,MyCacheCntlr 类扩展了全局命名空间中的 CacheCntlr 类,而不是 MyNamespace 中定义的 CacheCntlr 类。

  1. 关于作用域解析操作符

作用域解析操作符不仅限于解决命名冲突,它还经常用于以下情景:

  • 访问被子类隐藏的基类成员。
  • 定义在类外部的函数成员。
  • 访问特定命名空间中的变量或函数。

使用作用域解析操作符可以让代码更清晰、更具表现力,也有助于维护代码的逻辑结构。

class后面的semicolon分号

C++ 的分号事告诉编译器你的实例的对象列表,常见:变量(包括函数变量)函数末尾就不需要写。class由于后面可以写实例,所以需要写分号

2. 构造函数与析构函数

  • 构造函数是类的一种特殊的成员函数,它会在每次创建类的新对象时执行。构造函数的名称与类的名称是完全相同的,并且不会返回任何类型,也不会返回 void。构造函数可用于为某些成员变量设置初始值。

  • 析构函数是类的一种特殊的成员函数,它会在每次删除所创建的对象时执行。析构函数的名称与类的名称是完全相同的,只是在前面加了个波浪号(~)作为前缀,它不会返回任何值,也不能带有任何参数。析构函数有助于在跳出程序(比如关闭文件、释放内存等)前释放资源。

A() = default

在C++中,使用default关键字声明的构造函数和析构函数会生成默认实现。这意味着:

  1. 默认构造函数 (default constructor)
  2. 当没有提供构造函数时,编译器会自动生成一个默认构造函数。它会对类的成员进行默认初始化。例如,内置类型的成员将未定义,类类型的成员将调用其默认构造函数。

  3. 默认析构函数 (default destructor)

  4. 当没有提供析构函数时,编译器会自动生成一个默认析构函数。它会对类的成员进行默认析构,释放资源。例如,对于类类型的成员,将调用其析构函数,处理相应的资源释放。

简单来说,default关键字使得编译器自动生成的构造和析构函数能够完成基本的初始化和清理工作。这样可以减少手动管理资源的负担,特别是在简单类中。

3. 类的成员函数

  • 普通成员函数:如何在类中定义和使用普通的成员函数。
    • 提供类内和类外定义成员函数的例子。
  • 常量成员函数:解释 const 成员函数的意义,即该函数不会修改对象的成员变量。
  • 静态成员与静态函数:解释 static 成员变量和函数,以及它们的使用场景。

函数重载

class内的对象的引用

  • a.b is only used if b is a member of the object实例 (or reference to an object) a. So for a.b, a will always be an actual object (or a reference to an object) of a class.
  • a->b is, originally, a shorthand notation for (*a).b.
  • However, -> is the only of the member access operators that can be overloaded, so if a is an object of a class that overloads operator-> (common such types are smart pointers and iterators), then the meaning is whatever the class designer implemented.
  • To conclude:
  • With a->b, if a is a pointer, b will be a member of the object the pointer a refers to.
  • If, however, a is an object of a class that overloads this operator, then the overloaded operator function operator->() gets invoked.

注意不只对变量还有函数

Trie *tree = new Trie();
tree->insert(word);

//default this is a pointer
this->left;

3.5 继承

  • C++中的继承是一种面向对象编程的重要概念,它允许一个类(称为派生类或子类)从另一个类(称为基类或父类)继承属性和行为。
  • 继承允许在基类的基础上构建更具体和特殊化的派生类,从而实现代码重用、层次化和多态性

C++中的继承通过以下语法来定义:

class DerivedClass : access-specifier BaseClass {
    // 派生类的成员和函数
};

其中,DerivedClass是派生类的名称,BaseClass是基类的名称,access-specifier是访问控制符,可以是public、protected或private。

继承有以下几种类型, 由access-specifier控制:

  • 公有继承(public inheritance):通过public继承,
  • 基类的公有成员在派生类中仍然是公有的,保留其访问权限。
  • 基类的保护成员在派生类中变为保护的,
  • 私有成员在派生类中不可直接访问。
  • 保护继承(protected inheritance):通过protected继承,
  • 基类的公有和保护成员在派生类中都变为保护的,
  • 私有成员在派生类中不可直接访问。
  • 私有继承(private inheritance):通过private继承,
  • 基类的公有和保护成员在派生类中都变为私有的,
  • 私有成员在派生类中不可直接访问。
  • 默认情况下,如果不显式指定继承类型,则使用的是私有继承(private inheritance)
  • 但是如果基类使用struct关键字定义并且未指定访问控制修饰符,则默认继承类型为公有继承。
struct BaseStruct {
    // 基类成员和函数
};

struct DerivedStruct : BaseStruct {
    // 派生类成员和函数
};
  1. 通过继承,派生类可以访问并重用基类的成员变量和成员函数。
  2. 派生类还可以添加自己的成员变量和成员函数,从而扩展基类的功能。
  3. 此外,派生类还可以覆盖(override)基类的成员函数,以实现多态性。
  4. 继承还支持多层次(多级)继承,其中一个派生类可以作为另一个派生类的基类。

继承是面向对象编程中的重要概念,它提供了代码的组织结构和灵活性。通过继承,可以构建更具层次结构和复杂性的类体系,并实现代码重用和多态性。

4. 虚函数与多态

  • 虚函数的定义:讲解什么是虚函数,如何使用 virtual 关键字定义虚函数。
    • 解释多态性的概念,以及基类指针或引用如何调用派生类的函数。
    • 提供一个简单的虚函数示例。
  • 纯虚函数与抽象类:进一步介绍如何定义抽象类及其在设计模式中的应用。
    • 提供纯虚函数和抽象类的实际例子。

虚函数

  • 虚函数(Virtual Function)可以实现 动态多态
  • 在基类中使用 virtual 关键字将函数声明为虚函数,并在派生类中重新定义(override)这些虚函数,派生类中重新定义时也可以使用 virtual 修饰符,但不是必需的。
class Geometry{
public:
    virtual void Draw()const = 0;
};

class Line : public Geometry{
public:
    virtual void Draw()const{    std::cout << "Line Draw()\n";    }
};

多态

内存切片

内存切片(slicing) 发生在使用 多态(polymorphism) 的类时,当一个派生类对象被复制或赋值给一个基类对象时,派生类中的特有部分被“切掉”了,只保留了基类的部分。换句话说,内存切片 是指将一个派生类对象赋值给一个基类对象时,派生类对象的额外成员和行为会丢失,导致无法正确表示派生类的完整信息。

  1. 动态多态:虚函数实现为主,性能会有一定损耗. Ref
  2. 静态多态:模版函数为主
template<typename Geometry>
void DrawGeometry(std::vector<Geometry> vecGeo){
}

5. 拷贝构造与赋值操作

  • 拷贝构造函数:介绍什么是拷贝构造函数,如何定义,以及其用途(例如深拷贝)。
  • 赋值操作符重载:说明如何重载赋值操作符 operator=,以及它与拷贝构造函数的区别。

拷贝和浅拷贝的区别

(举例说明深拷贝的安全性)

  • 当出现类的等号赋值时,会调⽤拷贝函数,在未定义显示拷贝构造函数的情况下, 系统会调⽤默认的拷贝函数-即浅拷贝(两类中的两个指针指向同⼀个地址),它能够完成成员的⼀⼀复制。当数据成员中没有指针时,浅拷贝是可⾏的。
  • 但当数据成员中有指针时,如果采⽤简单的浅拷贝,则两类中的两个指针指向同⼀个地址,当对象快要结束时,会调⽤两次析构函数,⽽导致指野指针的问题。
  • 注意所有new的变量,需要在Destructors里显示delete

6. 动态内存管理与RAII

  • 介绍如何在类中管理动态分配的内存,并介绍 RAII(资源获取即初始化)的概念。
  • 示例:如何使用智能指针(如 std::unique_ptr)来管理资源,以避免内存泄漏。

7. 其他高级特性

友元函数与友元类

在 C++ 中,friend 关键字用来允许某些特定的函数或类访问另一个类的私有(private)或受保护(protected)成员。这是一个重要的特性,因为它可以让其他类或函数绕过常规的访问控制规则,这在某些特定的设计场景中非常有用。

使用 friend 的情景

  • 增强封装:有时,两个或多个类协同工作得很密切,而你又不想为了一个类访问另一个类的内部成员而公开这些成员。
  • 实现操作符重载:例如,重载输入输出操作符时,通常会需要访问类的私有数据。

示例

假设有一个 CacheCntlr 类,我们希望它能够访问另一个类 MemoryManager 的私有成员。我们可以在 MemoryManager 类中声明 CacheCntlr 为友元:

class MemoryManager {
    friend class CacheCntlr;  // CacheCntlr 现在可以访问 MemoryManager 的所有私有和受保护成员

private:
    int size;
    void config() {
        // 私有成员函数
    }
};

class CacheCntlr {
public:
    void accessMemory(MemoryManager& mm) {
        mm.size = 1024;  // 可以直接访问私有成员
        mm.config();     // 可以直接调用私有成员函数
    }
};

在这个例子中,尽管 MemoryManagersize 变量和 config() 方法都是私有的,CacheCntlr 类的方法却能够访问它们,这是因为 CacheCntlr 被声明为 MemoryManager 的友元。

注意事项

  • 破坏封装:过度使用 friend 关键字可能会破坏封装和隐藏的原则,使得代码维护和理解变得更加困难。
  • 设计决策:通常,如果发现需要大量使用 friend 关键字,这可能是需要重新考虑你的设计的信号。

使用 friend 关键字时应谨慎,确保其真正必要且有助于代码的清晰和功能的实现。

运算符重载

简单介绍如何对类进行运算符重载,如 +== 等。

重载运算符和重载函数

C++ 允许在同一作用域中的某个函数和运算符指定多个定义,分别称为函数重载运算符重载

重载声明是指一个与之前已经在该作用域内声明过的函数或方法具有相同名称的声明,但是它们的参数列表和定义(实现)不相同

当您调用一个重载函数或重载运算符时,编译器通过把您所使用的参数类型与定义中的参数类型进行比较,决定选用最合适的定义。选择最合适的重载函数或重载运算符的过程,称为重载决策

override关键字作用

如果派生类在虚函数声明时使用了override描述符,那么该函数必须重载其基类中的同名函数,否则代码将无法通过编译。

8. 总结与最佳实践

  • 总结 C++ 类的设计和使用中的最佳实践,强调使用构造函数、析构函数、拷贝构造函数和虚函数时的注意事项。
  • 提供一些常见的 C++ 编程模式和建议。

单例配置类

参考文献

评论