moon

虽说是工作,但我想找到乐趣

知识元

实际开发会常用到的一些知识点(不限语言)

也许会忘记的 Linux 指令

系统管理

# 查看当前目录的路径
pwd
# 查看磁盘使用情况
df -h
# 查看文件/目录的大小
du -sh file_name
# 查看系统内存使用情况
free -h
# 查看当前登录的用户
who

权限管理

# 查看文件权限
ls -l file_name
# 修改文件权限
chmod 755 file_name

网络操作

# 查看网络状态
ifconfig
# 测试网络连通性
ping host_name
# 下载文件
wget url

进程管理

# 查看系统进程
ps aux
# 实时查看系统进程
top
# 杀死进程
kill pid
# 强制结束进程
kill -9 pid

压缩和解压缩

# 压缩文件
tar -zcvf archive.tar.gz dir_name
# 解压文件
tar -zxvf archive.tar.gz
消息队列的设计思想

消息队列是工程领域内解决异步通信、系统解耦和流量削峰等问题的通用方案,它背后蕴含着一些经典的设计思想和模型。以下是对消息队列背后设计思想的介绍:

1. 消息队列适合的场景

消息队列主要适用于以下三个典型场景:

  • 异步处理数据:通过将任务暂存到消息队列,生产者和消费者之间可以实现异步处理,从而提升效率。
  • 系统应用解耦:消息队列可以在不同的系统之间起到缓冲和解耦的作用,使得系统之间的耦合性降低。
  • 业务流量削峰:在高峰期,通过将大量请求暂存到消息队列,后续系统可以根据自身能力逐步处理,避免因瞬间高负载而导致系统崩溃。

2. 主流的消息队列产品

目前主流的消息队列产品包括:

  • ActiveMQ:基于Java开发的开源消息代理,支持多种客户端和服务器的通信。
  • RabbitMQ:实现了高级消息队列协议(AMQP),以Erlang编写,支持多种消息传递协议。
  • Kafka:由Apache开发,专为实时数据处理设计,支持分布式、高吞吐量、低延迟。
  • RocketMQ:分布式消息和流媒体平台,强调强一致性和高性能。
  • Pulsar:下一代云原生分布式消息流平台,支持多租户和跨区域数据复制,具有强一致性和高可扩展性。

3. 消息队列的核心设计思想

消息队列的设计可以分为三大部分:生产者、消息队列集群和消费者。

  • 生产者:负责生成并发送消息,通常有多种策略(如按key哈希、轮询、随机)来决定将消息发送到哪个队列。
  • 消息队列集群:核心功能包括存储、过滤和分发消息。消息存储决定了消息队列的性能,过滤消息则允许通过规则对消息进行分类处理,分发消息则是将消息发送给合适的消费者。
  • 消费者:从消息队列获取并处理消息,通常有推送(push)和拉取(pull)两种方式获取数据。

4. 消息队列的数据组织方式

消息队列的存储模型是其核心设计之一,不同的存储模型决定了消息队列的吞吐量、性能和可靠性。例如,Kafka采用日志结构存储,支持高吞吐量,而RocketMQ和Pulsar则强调数据的一致性和高可用性。

5. 消费者消费模型

不同的消费模型对消息的处理顺序和一致性有不同的影响。例如,Kafka允许消费者按顺序消费某个分区内的消息,而Pulsar支持不同消费者组对消息的分配。

总结

消息队列不仅是实现异步通信、系统解耦和流量削峰的有效工具,更是一种体现了丰富设计思想和工程经验的通用解决方案。无论选择哪种具体的消息队列产品,理解其背后的设计思想都是有效应用和优化它们的关键。

无法说常存于大脑肌肉记忆中的(GPT生成)

C 与 C++ 的区别

C 与 C++ 的区别

  1. 语言特性:
  • C: 是一种过程式编程语言,强调函数和程序结构。
  • C++: 是 C 的超集,支持面向对象编程(OOP),包括类、继承、多态等特性。
  1. 面向对象编程:
  • C: 不支持类和对象。
  • C++: 支持类、对象、封装、继承、多态等面向对象的特性。
  1. 模板:
  • C: 不支持模板。
  • C++: 支持模板,可以编写泛型代码。
  1. 标准库:
  • C: 标准库包括 C 标准库(如 stdio.h, stdlib.h)。
  • C++: 标准库包括 C++ 标准库(如 iostream, vector, string),提供更多功能和更高层次的抽象。
  1. 内存管理:
  • C: 主要通过 mallocfree 管理动态内存。
  • C++: 支持 newdelete,并且支持 RAII(资源获取即初始化)模式,自动管理资源。
指针与引用的区别

指针与引用的区别

  1. 定义:
  • 指针: 是一个变量,用于存储另一个变量的地址。可以改变指针所指向的地址。
  • 引用: 是一个别名,用于给一个已存在的变量起一个别名。引用在初始化后不能改变指向的变量。
  1. 语法:
  • 指针:
int a = 10;
int *p = &a; // p 是指向 a 的指针
  • 引用:
int a = 10;
int &r = a; // r 是 a 的引用
  1. 空值:
  • 指针: 可以为 nullptr(或 NULL),表示不指向任何有效地址。
  • 引用: 必须初始化,并且不能为 nullptr
  1. 内存大小:
  • 指针: 通常占用固定大小的内存(如 4 字节或 8 字节),依赖于平台。
  • 引用: 通常没有额外的内存开销,内部实现可能使用指针。
  1. 修改:
  • 指针: 可以改变指向的对象(即,重新赋值)。
  • 引用: 绑定后不能改变指向的对象(引用一旦初始化就不可更改)。
struct 与 class 的区别

structclass 的区别

  1. 默认访问控制:
  • struct: 默认公有访问(public)。
  • class: 默认私有访问(private)。
  1. 继承默认访问控制:
  • struct: 默认公有继承(public)。
  • class: 默认私有继承(private)。
  1. 使用目的:
  • struct: 通常用于数据结构,成员通常是公有的,主要用于简单的聚合数据。
  • class: 通常用于面向对象编程,成员通常是私有的,提供封装和抽象。
  1. 语法:
  • struct:
struct Person {
    std::string name;
    int age;
};
  • class:
class Person {
private:
    std::string name;
    int age;
public:
    void setName(const std::string& n) { name = n; }
    std::string getName() const { return name; }
};

在实际编程中,structclass 可以在很多方面互换使用,主要的选择依据是设计意图和访问控制。

struct 内存对齐问题,sizeof 与 strlen 区别

在 C/C++ 编程中,struct 的内存对齐以及 sizeofstrlen 之间的区别是常见的概念。我们逐一说明。

1. struct 的内存对齐问题

C/C++ 中,struct 的成员会按照某种对齐方式存储,以提高内存访问的效率。内存对齐指的是编译器在分配内存时,会按照数据类型的对齐要求进行填充(即可能在成员之间加入一些未使用的空间),确保每个成员的地址都是适合其类型的地址。

例如,对于 32 位系统,int 类型可能要求以 4 字节对齐,因此编译器会将 int 变量存储在能被 4 整除的地址上。如果 struct 中有多个不同类型的成员,那么它们之间可能会存在“填充字节”。

示例:

#include <iostream>

struct Example {
    char a;
    int b;
    short c;
};

int main() {
    std::cout << "Size of Example struct: " << sizeof(Example) << std::endl;
    return 0;
}

在上述示例中,char a 只占用 1 字节,而 int b 可能需要对齐到 4 字节,因此会有 3 字节的填充。short c 可能需要 2 字节的对齐。因此,这个 struct 的大小并不是简单的所有成员字节数之和,而是经过对齐后的大小。

2. sizeofstrlen 的区别

sizeof

sizeof 是一个编译时操作符,用于获取变量或类型的字节大小。对于数组、结构体等,它返回的是整个对象占用的字节数,包括任何可能的填充字节。

例如:

char arr[] = "Hello";
std::cout << sizeof(arr) << std::endl; // 输出 6,因为包含了末尾的 '\0'

sizeof 计算的是整个对象的大小,因此对于 struct,它会考虑内存对齐产生的填充。

strlen

strlen 是一个函数,用于计算字符串的长度(不包括末尾的 '\0')。它仅适用于 C 风格的字符串,并且计算的是字符数组中字符的实际数量,直到遇到字符串的结束符 '\0' 为止。

例如:

char arr[] = "Hello";
std::cout << strlen(arr) << std::endl; // 输出 5,因为不包括末尾的 '\0'

主要区别

  • sizeof 用于计算任何类型的内存大小,包括数组、结构体、指针等,结果是整个对象的字节数。
  • strlen 仅用于计算 C 风格字符串的长度(字符数),不包括终止符 '\0'

总结

  • sizeof 计算内存大小,适用于任意类型。
  • strlen 计算 C 风格字符串的字符长度,不包括 '\0'
  • struct 的内存对齐可能导致 sizeof 结果大于所有成员字节数的总和。
面向对象的三大特性

面向对象编程(OOP)的三大特性是:

  1. 封装(Encapsulation)

    • 定义:封装是将数据(属性)和操作数据的方法(函数)绑定在一起,并对外界隐藏实现细节的一种机制。通过封装,类的内部实现对外界是不可见的,只能通过公开的接口(方法)与类进行交互。
    • 优点:封装提高了代码的可维护性和可重用性,减少了代码的复杂性,并提供了数据的安全性。
  2. 继承(Inheritance)

    • 定义:继承是指一个类(子类)可以继承另一个类(父类)的属性和方法,从而扩展或修改父类的功能。继承可以帮助我们重用已有的代码,并创建具有层次结构的类。
    • 优点:继承提高了代码的复用性,使得在不改变原有类的情况下,可以在子类中增加新功能或修改现有功能。
  3. 多态(Polymorphism)

    • 定义:多态是指同一操作(方法)可以作用于不同类型的对象,并且根据对象的实际类型,执行不同的行为。多态通过方法重载(overloading)和方法重写(overriding)来实现。
    • 优点:多态提高了代码的灵活性和扩展性,使得可以通过统一的接口处理不同类型的对象,而不需要关心具体的实现细节。

这些特性使得面向对象编程在设计和实现复杂系统时更加高效和灵活。

类的访问权限

在C++中,类的访问权限控制了类的成员(数据成员和成员函数)可以被哪些代码访问。C++中有三种主要的访问权限:privateprotectedpublic。下面是它们的简要说明:

  1. private
  • 定义private成员只能被同一个类的其他成员函数访问。
  • 作用:用于封装内部实现细节,确保类的内部数据不被外部直接修改。
  • 示例
class MyClass {
private:
    int privateData; // 只能被 MyClass 的成员函数访问

public:
    void setPrivateData(int value) { privateData = value; }
    int getPrivateData() const { return privateData; }
};
  1. protected
  • 定义protected成员可以被同一个类和派生类的成员函数访问,但不能被类外部的代码访问。
  • 作用:允许子类访问基类的成员,而外部代码不能直接访问。
  • 示例
class BaseClass {
protected:
    int protectedData; // 可以被 BaseClass 和派生类访问

public:
    void setProtectedData(int value) { protectedData = value; }
    int getProtectedData() const { return protectedData; }
};

class DerivedClass : public BaseClass {
public:
    void useProtectedData() {
        protectedData = 10; // 派生类可以访问 protectedData
    }
};
  1. public
  • 定义public成员可以被任何代码访问,包括类外部的代码。
  • 作用:提供接口供外部代码与类交互。
  • 示例
class MyClass {
public:
    int publicData; // 任何代码都可以访问

    void setPublicData(int value) { publicData = value; }
    int getPublicData() const { return publicData; }
};

通过合理使用这三种访问权限,你可以控制类的封装性和接口的暴露程度,从而实现更好的设计和代码维护。

类的构造析构函数、赋值函数、拷贝函数

在 C++ 中,类的构造函数、析构函数、赋值函数和拷贝函数是管理对象生命周期和状态的重要成员函数。下面是它们的简要说明和用法:

  1. 构造函数(Constructor):
  • 用于创建对象并初始化其成员变量。
  • 构造函数的名称与类名相同,没有返回类型。
  • 可以有多个构造函数(重载),以便以不同的方式初始化对象。
class MyClass {
public:
    MyClass() { /* 默认构造函数 */ }
    MyClass(int x) { /* 带参数的构造函数 */ }
};
  1. 析构函数(Destructor):
  • 用于销毁对象并释放资源。
  • 析构函数的名称是类名的前面加上~,没有返回类型和参数。
  • 每个类只能有一个析构函数。
class MyClass {
public:
    ~MyClass() { /* 析构函数 */ }
};
  1. 赋值函数(Assignment Operator):
  • 用于将一个对象的值赋给另一个已经存在的对象。
  • 赋值函数的签名是ClassName& operator=(const ClassName& other),其中ClassName是类的名称。
  • 返回值是对当前对象的引用(*this),以支持链式赋值。
class MyClass {
public:
    MyClass& operator=(const MyClass& other) {
        if (this != &other) {
            // 复制数据
        }
        return *this;
    }
};
  1. 拷贝构造函数(Copy Constructor):
  • 用于通过另一个同类型的对象创建一个新对象。
  • 拷贝构造函数的签名是ClassName(const ClassName& other),其中ClassName是类的名称。
  • 通常用于实现深拷贝(如果类中有动态分配的内存)。
class MyClass {
public:
    MyClass(const MyClass& other) {
        // 复制数据
    }
};

示例代码

#include <iostream>

class MyClass {
public:
    int* data;

    // 构造函数
    MyClass(int value) {
        data = new int(value);
        std::cout << "Constructor called\n";
    }

    // 拷贝构造函数
    MyClass(const MyClass& other) {
        data = new int(*other.data);
        std::cout << "Copy constructor called\n";
    }

    // 赋值函数
    MyClass& operator=(const MyClass& other) {
        if (this != &other) {
            delete data; // 释放已有的内存
            data = new int(*other.data);
        }
        std::cout << "Assignment operator called\n";
        return *this;
    }

    // 析构函数
    ~MyClass() {
        delete data;
        std::cout << "Destructor called\n";
    }
};

int main() {
    MyClass obj1(10);
    MyClass obj2 = obj1; // 调用拷贝构造函数
    MyClass obj3(20);
    obj3 = obj1; // 调用赋值函数

    return 0;
}

这段代码演示了如何实现和使用构造函数、析构函数、拷贝构造函数和赋值函数。注意,在实际应用中,特别是当类中涉及动态分配内存时,确保正确管理资源是非常重要的。

移动构造函数与拷贝构造函数对比

移动构造函数和拷贝构造函数都是C++中用于对象初始化的特殊成员函数,它们的主要区别在于资源的转移方式和效率。

拷贝构造函数

  • 定义: 拷贝构造函数通过复制现有对象的内容来初始化新对象。
  • 调用时机:
    • 当用现有对象初始化一个新对象时,如:MyClass obj1 = obj2;
    • 将对象作为参数传递给函数时(按值传递)。
    • 函数返回对象时。
  • 实现方式: 典型实现是对成员变量进行逐个复制。
  • 资源管理: 使用深拷贝时,新对象会拷贝一份独立的资源;使用浅拷贝时,新旧对象共享资源。
  • 性能: 拷贝构造函数通常涉及深度复制数据,对于大数据结构(如容器),性能较低。

示例

class MyClass {
public:
    MyClass(const MyClass& other) {
        // 拷贝构造函数
    }
};

移动构造函数

  • 定义: 移动构造函数通过转移现有对象的资源来初始化新对象,而不是进行复制。
  • 调用时机:
    • 用右值(临时对象或通过std::move显式转换的对象)初始化新对象时。
    • 对象在函数返回时,可以通过移动语义避免拷贝。
  • 实现方式: 通过将源对象的资源“搬”到新对象中,源对象则进入一个“空”状态。
  • 资源管理: 移动构造函数避免了拷贝构造的深拷贝操作,从而提升了性能。
  • 性能: 移动构造通常只需要指针或资源的“转移”,性能远高于拷贝构造,尤其在管理动态资源时(如堆上的内存或文件句柄)。

示例

class MyClass {
public:
    MyClass(MyClass&& other) noexcept {
        // 移动构造函数
    }
};

对比总结

  1. 资源管理: 拷贝构造函数进行数据的深拷贝,而移动构造函数通过转移资源所有权避免拷贝。
  2. 性能: 移动构造函数比拷贝构造函数效率更高,尤其是在管理动态内存或其他外部资源时。
  3. 调用时机: 拷贝构造用于左值对象,移动构造用于右值对象(临时对象或通过std::move转换的对象)。

在实际开发中,优先使用移动语义可以大大提升性能,尤其是在处理大数据结构和动态资源时。

内存分区:全局区、堆区、栈区、常量区、代码区

在 C/C++ 编程中,内存分为多个区域来管理程序运行时的数据。这些内存区域主要包括全局区、堆区、栈区、常量区和代码区。以下是各个区域的详细说明:

  1. 全局区(静态区)

    • 存储全局变量和静态变量。这些变量在程序开始时分配内存,并在程序结束后释放。
    • 全局变量和静态变量未初始化的部分会存放在 BSS(Block Started by Symbol)段,已初始化的变量会存放在数据段(Data Segment)。
  2. 堆区

    • 堆区用于动态分配内存,程序员可以在运行时通过调用 malloc()new 等函数分配内存,并通过 free()delete 释放内存。
    • 堆区的内存管理由程序员负责,如果忘记释放内存,可能会导致内存泄漏。
  3. 栈区

    • 栈区用于存储局部变量、函数参数、返回地址等。栈的内存分配由编译器自动管理,函数调用结束时会自动释放局部变量的内存。
    • 栈的大小通常有限,如果递归调用层数过多或局部变量占用内存过大,可能会导致栈溢出。
  4. 常量区

    • 常量区用于存放常量数据,如字符串常量、const 修饰的全局常量等。常量区的内容在程序运行时不会改变。
    • 常量区的数据通常是只读的,尝试修改常量会导致运行时错误。
  5. 代码区

    • 代码区用于存放程序的机器指令,即程序的可执行代码。代码区的内容在程序执行过程中不会改变,通常是只读的。
    • 代码区的大小在程序编译时确定。

内存分区图(通常的分布):

+-------------------+
|   栈区 (Stack)    |  ← 向下增长
+-------------------+
|                   |
|     空闲区         |
|                   |
+-------------------+
|     堆区 (Heap)   |  ← 向上增长
+-------------------+
|     BSS 段        |
|                   |
+-------------------+
|   数据段 (Data)   |  (已初始化的全局变量和静态变量)
+-------------------+
|   代码区 (Text)   |
+-------------------+

每个区域都有其特定的用途和生命周期,合理使用这些区域有助于优化程序的内存管理。

虚函数实现动态多态的原理

在C++中,虚函数用于实现动态多态,其原理是通过虚函数表(vtable)和虚指针(vptr)实现的。

  • 虚函数表(vtable):这是一个函数指针数组,用于存储类中的虚函数的地址。每个类都有自己的虚函数表,当派生类覆盖了基类中的虚函数时,虚函数表中的对应条目会指向派生类的实现。

  • 虚指针(vptr):在每个包含虚函数的对象中,编译器会隐式插入一个指向虚函数表的指针,这个指针称为虚指针(vptr)。当调用虚函数时,程序会通过这个虚指针找到对应的虚函数表,然后根据虚函数表中的函数指针调用正确的函数实现。

动态多态的实现关键在于,当通过基类指针或引用调用虚函数时,程序在运行时会根据实际对象类型(派生类或基类)来决定调用哪个函数,而不是在编译时确定。这种机制使得程序可以在运行时灵活地调用合适的函数,支持多态性。

虚函数与纯虚函数的区别
  • 虚函数(Virtual Function):
    • 虚函数是基类中声明的函数,并通过virtual关键字标识。
    • 虚函数可以有默认的实现,派生类可以选择覆盖该虚函数,但也可以不覆盖,直接使用基类的实现。
    • 当通过基类指针或引用调用虚函数时,程序会在运行时根据实际对象类型调用派生类中的实现。
class Base {
public:
    virtual void show() {
        std::cout << "Base class show function" << std::endl;
    }
};

class Derived : public Base {
public:
    void show() override {
        std::cout << "Derived class show function" << std::endl;
    }
};
  • 纯虚函数(Pure Virtual Function):
    • 纯虚函数是虚函数的一种特殊形式,它在基类中声明但不提供实现,必须在派生类中实现。
    • 在基类中定义纯虚函数时,通常使用= 0来表明它没有实现。
    • 包含纯虚函数的类称为抽象类,这种类不能直接实例化,必须由派生类提供纯虚函数的实现才能实例化对象。
class Base {
public:
    virtual void show() = 0; // 纯虚函数
};

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

区别总结

  • 虚函数可以有实现,而纯虚函数没有实现,必须由派生类实现。
  • 包含纯虚函数的类是抽象类,不能实例化;包含虚函数但没有纯虚函数的类是可以实例化的。
深拷贝与浅拷贝的别

深拷贝和浅拷贝是对象复制的两种不同方式。它们的主要区别在于复制的深度及其对原始对象中引用数据的处理方式。

浅拷贝(Shallow Copy)

浅拷贝创建一个新的对象,但这个对象内部的属性或成员是对原始对象属性或成员的引用。换句话说,浅拷贝只复制对象本身的结构,而不复制结构内部的引用对象。

特点:

  • 复制的是对象的引用,而不是对象本身。
  • 修改浅拷贝对象中的引用属性也会影响原始对象中的引用属性。

示例(以 Python 为例):

import copy

original_list = [1, 2, [3, 4]]
shallow_copied_list = copy.copy(original_list)

# 修改浅拷贝中的子列表
shallow_copied_list[2][0] = 'changed'

print(original_list)         # 输出: [1, 2, ['changed', 4]]
print(shallow_copied_list)   # 输出: [1, 2, ['changed', 4]]

在这个示例中,浅拷贝复制了 original_list 的引用,而子列表 [3, 4] 是被共享的,因此修改子列表会影响到原始列表。

深拷贝(Deep Copy)

深拷贝创建一个新的对象,并且递归地复制原始对象中所有的属性和子属性,直到所有层级都被复制。这意味着深拷贝会创建一个完全独立于原始对象的新对象。

特点:

  • 复制对象及其所有子对象,确保所有层级都被复制。
  • 修改深拷贝对象中的属性不会影响原始对象。

示例(以 Python 为例):

import copy

original_list = [1, 2, [3, 4]]
deep_copied_list = copy.deepcopy(original_list)

# 修改深拷贝中的子列表
deep_copied_list[2][0] = 'changed'

print(original_list)         # 输出: [1, 2, [3, 4]]
print(deep_copied_list)      # 输出: [1, 2, ['changed', 4]]

在这个示例中,深拷贝创建了 original_list 的一个完全独立的副本,包括其中的子列表。修改深拷贝中的子列表不会影响原始列表。

总结

  • 浅拷贝:只复制对象的引用,内部的子对象仍然是共享的。
  • 深拷贝:复制对象及其所有嵌套的子对象,完全独立于原始对象。

选择使用浅拷贝还是深拷贝取决于具体的需求和应用场景。

static, const, extern, volatile
  1. static

    • 在函数内部:用于声明静态局部变量,变量的生命周期会延续到程序结束,而不仅仅是函数调用期间。
    • 在全局变量或函数前:限制其作用域,仅在声明所在的文件中可见(文件内链接)。
    • 在类成员变量/函数前(C++):用于声明静态类成员,它属于类而不是对象,可以在类的所有对象之间共享。
  2. const

    • 常量指针/引用:用于定义常量,表示数据不能被修改。例如 const int a = 10;
    • 函数参数(C++):在函数形参中使用 const,可以防止函数对传入的参数进行修改。
    • 类成员函数(C++):用于标记成员函数不会修改对象的状态,例如 void func() const;
  3. extern

    • 用于声明变量或函数是外部定义的,在其他文件中定义但可以在当前文件中使用。一般在多个文件中共享全局变量或函数时使用。
  4. volatile

    • 告诉编译器该变量可能会被外部事件(如硬件或多线程)修改,防止编译器对它进行优化(如寄存器缓存)。常用于嵌入式编程中与硬件交互的变量。

这些关键字在控制程序的内存、作用域、以及编译时优化行为方面起着重要作用。

C++ 中的类型转换

在 C++ 中,有四种类型转换操作符,每种都有特定的用途:

  1. static_cast
  • 用途:用于执行编译时类型检查的类型转换。
  • 特点:可以进行一些显式的转换,如基本数据类型之间的转换、指针或引用的转换(在继承体系中进行向上或向下转换)。
  • 示例
int i = 10;
double d = static_cast<double>(i); // int 转换为 double

class Base {};
class Derived : public Base {};

Base* base = new Derived();
Derived* derived = static_cast<Derived*>(base); // 向下转换
  1. dynamic_cast
  • 用途:用于运行时类型检查的类型转换,通常用于处理继承关系中的指针或引用转换。
  • 特点:主要用于安全地进行多态类型转换(如向下转换),如果转换失败,返回 nullptr(对于指针)或抛出 std::bad_cast 异常(对于引用)。
  • 示例
    class Base {
        virtual void foo() {}
    };
    class Derived : public Base {};
    
    Base* base = new Derived();
    Derived* derived = dynamic_cast<Derived*>(base); // 向下转换
    
    if (derived) {
        // 转换成功
    } else {
        // 转换失败
    }
    
  1. const_cast
  • 用途:用于修改对象的 constvolatile 限定符。
  • 特点:可以用来添加或移除 constvolatile 属性。通常用于将 const 对象的指针转换为非 const 类型,但应小心使用,避免未定义行为。
  • 示例
const int i = 10;
int* p = const_cast<int*>(&i); // 移除 const 限定符
*p = 20; // 注意:修改 const 对象的值是未定义行为
  1. reinterpret_cast
  • 用途:用于执行低级别的位级转换。
  • 特点:可以将任何指针类型转换为任何其他指针类型,或者将整数转换为指针,但通常应小心使用,因为它可能导致未定义行为。
  • 示例
int i = 10;
char* p = reinterpret_cast<char*>(&i); // 整数指针转换为字符指针

每种转换方式都有其特定的使用场景和安全考虑,选择适当的转换方式可以提高代码的可读性和安全性。

静态与多态:重写、重载、模板

静态与多态是面向对象编程中的两个重要概念,特别是在C++中,涉及到重写、重载和模板的使用。下面分别解释这些概念:

1. 静态(编译时)多态

静态多态在编译时决定函数或方法的行为。它通过函数重载模板实现。

- 重载(Overloading)

重载指的是在同一个作用域内定义多个函数,它们有相同的名称,但参数类型或参数个数不同。编译器会根据传入的参数类型和个数来决定调用哪个函数。重载是在编译时进行解析的,因此属于静态多态。

示例:

class Example {
public:
    void func(int a) {
        std::cout << "Integer version: " << a << std::endl;
    }

    void func(double a) {
        std::cout << "Double version: " << a << std::endl;
    }

    void func(int a, double b) {
        std::cout << "Two parameters: " << a << " and " << b << std::endl;
    }
};

- 模板(Template)

模板允许在编译时生成代码,以适应不同的数据类型。C++中常见的是函数模板类模板。模板机制使代码在静态类型检查阶段生成对应类型的函数或类,因此也是静态多态的一种。

示例:

template <typename T>
T add(T a, T b) {
    return a + b;
}

int main() {
    int result_int = add(1, 2);       // 调用 int 类型的模板实例
    double result_double = add(1.5, 2.5); // 调用 double 类型的模板实例
}

2. 动态(运行时)多态

动态多态则是在运行时根据实际的对象类型来决定调用哪个方法。C++通过虚函数(virtual function)实现动态多态,通常和继承一起使用。

- 重写(Overriding)

重写是指在子类中重新定义父类中的虚函数,以实现不同的行为。通过基类指针或引用调用函数时,实际调用的是子类中重写的函数,基于对象的实际类型决定调用哪个版本的函数,这就是动态多态。

示例:

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

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

int main() {
    Base* base_ptr = new Derived();
    base_ptr->show(); // 输出 "Derived class show",因为调用了重写的函数
}

总结:

  • 静态多态(编译时):通过函数重载模板实现,编译时决定调用哪个函数。
  • 动态多态(运行时):通过虚函数重写实现,运行时根据对象的实际类型决定调用哪个函数。
四种智能指针及底层实现

C++ 中的智能指针主要用于自动管理内存,防止内存泄漏。这里分别介绍 auto_ptrunique_ptrshared_ptrweak_ptr 的功能及其底层实现。

1. auto_ptr (已废弃)

  • 功能: auto_ptr 是 C++98 中最早引入的智能指针,用于自动管理动态内存。在作用域结束时自动释放所指向的对象。

  • 特点:

    • 单一所有权:只有一个 auto_ptr 对象可以拥有某个资源。
    • 转移所有权:当 auto_ptr 进行赋值操作或拷贝构造时,所有权会从一个 auto_ptr 转移到另一个。
    • 已废弃:由于它的赋值和拷贝语义容易导致问题,在 C++11 中被 unique_ptr 取代。
  • 底层实现:

    • 拷贝构造或赋值会导致所有权的转移,原指针会被设为 nullptr
template<typename T>
class auto_ptr {
    T* ptr;
public:
    auto_ptr(T* p = nullptr) : ptr(p) {}
    auto_ptr(auto_ptr& a) : ptr(a.ptr) { a.ptr = nullptr; }
    auto_ptr& operator=(auto_ptr& a) {
        if (this != &a) {
            delete ptr;
            ptr = a.ptr;
            a.ptr = nullptr;
        }
        return *this;
    }
    ~auto_ptr() { delete ptr; }
};

2. unique_ptr

  • 功能: unique_ptr 是 C++11 引入的智能指针,提供了独占所有权,即在任何时间只能有一个 unique_ptr 拥有某个资源。

  • 特点:

    • 独占所有权:不允许拷贝构造或赋值,但可以通过 std::move 进行所有权转移。
    • 轻量高效:相比于 shared_ptrunique_ptr 更轻量,因为它不涉及引用计数。
  • 底层实现:

    • 禁用拷贝构造和拷贝赋值,只允许移动语义。
template<typename T>
class unique_ptr {
    T* ptr;
public:
    unique_ptr(T* p = nullptr) : ptr(p) {}
    unique_ptr(unique_ptr&& u) : ptr(u.ptr) { u.ptr = nullptr; }
    unique_ptr& operator=(unique_ptr&& u) {
        if (this != &u) {
            delete ptr;
            ptr = u.ptr;
            u.ptr = nullptr;
        }
        return *this;
    }
    ~unique_ptr() { delete ptr; }
};

3. shared_ptr

  • 功能: shared_ptr 是一种引用计数智能指针,可以实现多个智能指针共享同一块资源,只有最后一个 shared_ptr 被销毁时资源才会释放。

  • 特点:

    • 共享所有权:多个 shared_ptr 可以共享一个对象,底层会维护一个引用计数来跟踪有多少个 shared_ptr 指向同一个对象。
    • 线程安全:引用计数的增加和减少是线程安全的,但对象本身的访问则不保证线程安全。
  • 底层实现:

    • 使用一个独立的控制块来保存引用计数,当引用计数为 0 时销毁对象。
template<typename T>
class shared_ptr {
    T* ptr;
    std::size_t* ref_count;
public:
    shared_ptr(T* p = nullptr) : ptr(p), ref_count(new std::size_t(1)) {}
    shared_ptr(const shared_ptr& s) : ptr(s.ptr), ref_count(s.ref_count) { ++(*ref_count); }
    shared_ptr& operator=(const shared_ptr& s) {
        if (this != &s) {
            release();
            ptr = s.ptr;
            ref_count = s.ref_count;
            ++(*ref_count);
        }
        return *this;
    }
    ~shared_ptr() { release(); }
private:
    void release() {
        if (--(*ref_count) == 0) {
            delete ptr;
            delete ref_count;
        }
    }
};

4. weak_ptr

  • 功能: weak_ptr 是与 shared_ptr 配合使用的智能指针,它提供了一种不参与引用计数的弱引用方式,不会影响对象的生命周期。

  • 特点:

    • 避免循环引用weak_ptr 主要用于解决 shared_ptr 循环引用问题,例如在双向关联的对象结构中,两个对象分别持有对方的 shared_ptr,会导致内存泄漏,而使用 weak_ptr 可以避免这种情况。
    • 弱引用:通过 weak_ptr.lock() 可以获取一个有效的 shared_ptr,但对象可能已被销毁,因此需要判断。
  • 底层实现:

    • weak_ptr 不直接持有对象,只持有一个指向控制块的弱引用。
template<typename T>
class weak_ptr {
    T* ptr;
    std::size_t* ref_count;
public:
    weak_ptr() : ptr(nullptr), ref_count(nullptr) {}
    weak_ptr(const shared_ptr<T>& s) : ptr(s.ptr), ref_count(s.ref_count) {}
    shared_ptr<T> lock() const {
        if (*ref_count > 0) {
            return shared_ptr<T>(*this);
        } else {
            return shared_ptr<T>();
        }
    }
};

总结

  • auto_ptr 已被废弃,unique_ptr 是独占所有权智能指针,shared_ptr 是共享所有权的引用计数智能指针,weak_ptr 是为解决循环引用问题的弱引用智能指针。
右值引用

右值引用(rvalue reference)是 C++11 引入的一个重要特性,它用于支持移动语义(move semantics)和完美转发(perfect forwarding)。右值引用的主要目的是优化资源管理和性能,尤其是在对象的移动而不是复制时。

基本概念

  • 左值(lvalue):指代内存中的一个具体位置,能够被赋值的对象。例如,变量名、数组元素等。
  • 右值(rvalue):不指代任何持久位置的数据,通常是临时对象或字面量。例如,常量、临时对象、表达式的结果等。

右值引用的定义

右值引用使用 && 符号定义。例如:

int &&x = 10; // x 是一个右值引用

使用场景

  1. 移动语义(Move Semantics)

    移动语义允许通过转移资源所有权来避免不必要的资源复制。通过右值引用,我们可以高效地转移对象的资源,从而提高性能。

class MyClass {
public:
    MyClass() : data(new int[100]) {}
    ~MyClass() { delete[] data; }

    // 移动构造函数
    MyClass(MyClass&& other) noexcept : data(other.data) {
        other.data = nullptr; // 使原对象处于有效状态
    }

    // 移动赋值运算符
    MyClass& operator=(MyClass&& other) noexcept {
        if (this != &other) {
            delete[] data;
            data = other.data;
            other.data = nullptr;
        }
        return *this;
    }

private:
    int* data;
};
  1. 完美转发(Perfect Forwarding)

    完美转发通过 std::forward 和右值引用,能够保留传入参数的值类别(lvalue 或 rvalue),以避免不必要的复制或移动。

template <typename T>
void process(T&& arg) {
    func(std::forward<T>(arg)); // 完美转发
}

示例

#include <iostream>
#include <utility>

class MyClass {
public:
    MyClass() : data(new int[10]) {
        std::cout << "Constructor\n";
    }

    ~MyClass() {
        delete[] data;
        std::cout << "Destructor\n";
    }

    // 移动构造函数
    MyClass(MyClass&& other) noexcept : data(other.data) {
        other.data = nullptr;
        std::cout << "Move Constructor\n";
    }

    // 移动赋值运算符
    MyClass& operator=(MyClass&& other) noexcept {
        if (this != &other) {
            delete[] data;
            data = other.data;
            other.data = nullptr;
            std::cout << "Move Assignment\n";
        }
        return *this;
    }

private:
    int* data;
};

int main() {
    MyClass obj1;
    MyClass obj2 = std::move(obj1); // 调用移动构造函数
    MyClass obj3;
    obj3 = std::move(obj2); // 调用移动赋值运算符

    return 0;
}

在这个示例中,obj1 的资源被移动到 obj2,而 obj2 的资源被移动到 obj3,从而避免了不必要的资源复制。

std::move 函数

std::move 是 C++ 标准库中的一个函数模板,用于实现移动语义。它主要用于将一个对象的资源从一个对象“移动”到另一个对象,而不是拷贝。这样可以避免不必要的资源拷贝,提升程序的性能。

std::move 的定义如下:

template <typename T>
typename std::remove_reference<T>::type&& move(T&& t) noexcept;

用法

std::move 将一个对象转换成右值引用,从而使得该对象可以被移动。举个简单的例子:

#include <iostream>
#include <vector>
#include <utility> // for std::move

int main() {
    std::vector<int> vec1 = {1, 2, 3, 4, 5};
    std::vector<int> vec2 = std::move(vec1); // 将 vec1 的内容移动到 vec2

    std::cout << "vec1 size: " << vec1.size() << std::endl; // vec1 的内容已经被移动,vec1 可能处于有效但未指定的状态
    std::cout << "vec2 size: " << vec2.size() << std::endl; // vec2 现在包含了原来 vec1 的内容

    return 0;
}

在这个例子中,std::movevec1 的内容移动到 vec2。这意味着 vec1 现在可能处于一个有效但未指定的状态,因此我们通常不会对 vec1 做进一步的操作,而是用 vec2 来继续使用原来的数据。

注意事项

  1. std::move 不会进行实际的移动操作,它只是将其参数转换为右值引用。真正的移动操作发生在移动构造函数或移动赋值操作符中。

  2. 移动操作的对象 在移动后处于有效但不确定的状态。它们的具体状态取决于类型和实现,通常来说,移动后的对象需要被重置或清理。

  3. 避免重复使用 移动后的对象,因为它们的状态可能不再可靠。

std::move 是实现高效资源管理和避免不必要拷贝的重要工具,在编写性能敏感的代码时尤其重要。

C++ 迭代器

迭代器是用于遍历容器(如数组、链表、集合等)元素的对象。理解迭代器的原理及其失效问题对于编写高效且正确的代码非常重要。以下是关于迭代器原理和迭代器失效问题的详细介绍:

迭代器原理

  1. 基本概念: 迭代器是一个对象,它通过重载 *(解引用)和 ++(递增)运算符来遍历容器中的元素。可以将迭代器视为一个指针,它“指向”容器中的当前元素。

  2. 类型

    • 输入迭代器:只支持单次读取操作。
    • 输出迭代器:只支持单次写入操作。
    • 前向迭代器:支持多次读取和写入操作,但只能向前移动。
    • 双向迭代器:支持向前和向后移动。
    • 随机访问迭代器:支持在常数时间内随机访问容器中的任意位置。
  3. 使用: 迭代器通常与循环结构(如 for 循环)一起使用,通过不断递增迭代器来访问容器中的所有元素。

std::vector<int> vec = {1, 2, 3, 4, 5};
for (auto it = vec.begin(); it != vec.end(); ++it) {
    std::cout << *it << " ";
}

迭代器失效问题

迭代器失效指的是在容器中对元素进行修改(如插入、删除)后,原有的迭代器可能变得无效。这种情况会导致未定义行为。迭代器失效问题主要取决于容器的类型及其操作:

  1. 向量(std::vector

    • 插入:在中间插入元素时,可能导致原有迭代器失效,因为容器可能会重新分配内存。
    • 删除:删除元素时,迭代器可能会失效,尤其是删除的元素位于迭代器所指向的位置之前。

    解决方法: 使用 std::vector 时,如果需要频繁插入或删除操作,考虑使用其他容器或在操作前保存有效的迭代器。

  2. 链表(std::list

    • 插入:在链表中插入元素不会导致迭代器失效,因为链表的结构不涉及重新分配内存。
    • 删除:删除操作可能会使指向删除元素的迭代器失效,但不会影响其他迭代器。
  3. 集合(std::setstd::map

    • 插入:插入操作不会使已存在的迭代器失效,除了插入操作可能使迭代器指向的元素的顺序发生变化。
    • 删除:删除操作会使指向被删除元素的迭代器失效,但其他迭代器通常不会受到影响。
  4. 解决方案

    • 重新获取迭代器:在进行容器操作后,重新获取迭代器。
    • 避免在迭代时修改容器:尽量避免在遍历容器时修改它,尤其是插入和删除操作。
    • 使用合适的容器:根据需要选择适当的容器,例如,使用 std::list 来处理频繁插入和删除操作。

通过理解迭代器原理和失效问题,可以在编写代码时采取相应的策略来避免潜在的错误,确保程序的稳定性和性能。