C++:基本知识

1.⚡ 封装、继承、多态

(1)封装

封装(Encapsulation):封装是指将数据和操作数据的方法捆绑在一起的概念。它隐藏了数据的内部细节,只暴露对外的接口。在C++中,可以使用类来实现封装,将数据成员私有化,通过公有的成员函数来操作数据。

(2)继承

继承(Inheritance):继承是指一个类可以派生出另一个类,从而继承父类的属性和方法。子类可以访问父类的公有和保护成员,但不能访问私有成员。在C++中,可以通过派生类来实现继承。

(3)多态

多态(Polymorphism):多态是指同一操作作用于不同的对象上会产生不同的行为。C++支持两种多态:编译时多态(函数重载、运算符重载)和运行时多态(虚函数和纯虚函数)。

  • 运行时多态通过虚函数实现。在基类中声明虚函数,在派生类中重写这些虚函数。通过基类指针或引用调用这些函数时,会根据实际对象类型的不同而调用不同版本的函数。

在C++中,虚函数和纯虚函数都是为了实现多态性而设计的。它们允许在派生类中重写基类的成员函数,从而实现基类指针或引用调用派生类特定的函数。

  • 虚函数是在基类中声明为 virtual 的成员函数。当在派生类中重写(覆盖)这个函数时,可以通过基类指针或引用来调用派生类中的版本。虚函数使得编译器在运行时动态地确定要调用的函数版本。

  • 纯虚函数是在基类中声明为纯虚函数(没有实现体)的虚函数。纯虚函数在基类中没有具体的实现,而是在派生类中重写。一个包含纯虚函数的类称为抽象类,不能直接创建该类的对象,但可以通过派生类来创建对象。派生类必须重写(实现)所有的纯虚函数,否则它也会成为抽象类。

2.⚡ STL常用容器

C++标准库提供了多种容器,每种都有自己的特点和适用场景。以下是一些常用的C++容器

(1)std::vector

  • 动态数组,支持随机访问和在尾部快速插入/删除元素。

  • 适用于需要动态大小且需要随机访问元素的场景。

#include <vector>

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    vec.push_back(6); // 在尾部添加元素
    return 0;
}

(2)std::list

  • 双向链表,支持在任意位置快速插入/删除元素。

  • 适用于频繁的插入/删除操作,但不需要随机访问元素的场景。

#include <list>

int main() {
    std::list<int> myList = {1, 2, 3, 4, 5};
    myList.push_back(6); // 在尾部添加元素
    return 0;
}

(3)std::map / std::unordered_map

  • std::map 是基于红黑树实现的有序键值对容器,键值对按照键的大小有序存储。

  • std::unordered_map 是基于哈希表实现的无序键值对容器,不会按照键的大小排序。

  • 适用于键值对的存储和检索,std::unordered_map 在查找方面通常更快。

#include <map>
#include <unordered_map>

int main() {
    std::map<std::string, int> myMap = {{"apple", 5}, {"banana", 3}};
    std::unordered_map<std::string, int> myHashMap = {{"apple", 5}, {"banana", 3}};
    return 0;
}

(4)std::set / std::unordered_set

  • std::set 是基于红黑树实现的有序集合容器,元素按照大小有序存储。

  • std::unordered_set 是基于哈希表实现的无序集合容器,不会按照元素的大小排序。

  • 适用于需要快速查找元素,std::unordered_set 在查找方面通常更快。

#include <set>
#include <unordered_set>

int main() {
    std::set<int> mySet = {3, 1, 4, 1, 5}; // 自动去重
    std::unordered_set<int> myHashSet = {3, 1, 4, 1, 5}; // 不会自动去重
    return 0;
}

(5)std::deque (Double-ended queue)

  • std::deque 是双端队列容器,允许在队列的两端进行高效地插入和删除操作。

  • 与 std::vector 相比,std::deque 允许在头部和尾部进行 O(1) 复杂度的插入和删除操作。

  • 内部实现通常采用分段连续内存块来实现,因此在大量插入和删除操作时可能更高效。

#include <deque>

int main() {
    std::deque<int> myDeque = {1, 2, 3};
    myDeque.push_front(0); // 在头部添加元素
    myDeque.push_back(4); // 在尾部添加元素
    return 0;
}

(6)std::stack

  • std::stack 是一个后进先出 (LIFO) 的容器适配器,基于其他底层容器实现,如 std::deque, std::list, 或 std::vector。

  • std::stack 仅提供了栈的基本操作,包括 push()、pop()、top() 等。

#include <stack>

int main() {
    std::stack<int> myStack;
    myStack.push(1);
    myStack.push(2);
    int topElement = myStack.top(); // 获取栈顶元素
    myStack.pop(); // 弹出栈顶元素
    return 0;
}

(7)std::queue

  • std::queue 是一个先进先出 (FIFO) 的容器适配器,同样基于其他底层容器实现,如 std::deque 或 std::list。

  • std::queue 也提供了一些基本操作,包括 push()、pop()、front()、back() 等。

#include <queue>

int main() {
    std::queue<int> myQueue;
    myQueue.push(1);
    myQueue.push(2);
    int frontElement = myQueue.front(); // 获取队首元素
    myQueue.pop(); // 弹出队首元素
    return 0;
}

3.⚡ 智能指针

在C++中,智能指针是一种用来管理动态分配的内存的对象。智能指针可以自动地管理内存的生命周期,从而避免内存泄漏和空悬指针等问题。在标准C++库中,有两种主要的智能指针:std::unique_ptr 和 std::shared_ptr。

(1)std::unique_ptr

  • std::unique_ptr 提供了对动态分配的对象的独占所有权。它保证在其生命周期结束时自动释放其所管理的内存。

  • 当 std::unique_ptr 被复制或移动给其他对象时,所有权会从原始对象转移到新的对象,因此每个时刻只能有一个 std::unique_ptr 指向给定的对象。

  • 通过 std::move() 函数可以转移 std::unique_ptr 的所有权。

#include <memory>

int main() {
    std::unique_ptr<int> ptr(new int(42));
    // 使用智能指针
    *ptr = 10;
    // 不需要手动释放内存
    return 0;
} // 在此处 ptr 所管理的内存会被自动释放

(2)std::shared_ptr

  • std::shared_ptr 允许多个指针共享同一个对象。它通过引用计数来管理内存,当最后一个 std::shared_ptr 被销毁时,它所管理的对象会被释放。

  • std::shared_ptr 可以通过 std::make_shared() 函数创建,这是一种更安全的方式。

#include <iostream>
#include <memory>

class MyClass {
public:
    MyClass() {
        std::cout << "MyClass constructor called" << std::endl;
    }
    ~MyClass() {
        std::cout << "MyClass destructor called" << std::endl;
    }
};

int main() {
    std::shared_ptr<MyClass> ptr1(new MyClass); // 引用计数为1
    {
        std::shared_ptr<MyClass> ptr2 = ptr1; // 引用计数加1,现在为2
        {
            std::shared_ptr<MyClass> ptr3 = ptr1; // 引用计数加1,现在为3
        } // ptr3 超出作用域,引用计数减1,现在为2
    } // ptr2 超出作用域,引用计数减1,现在为1
    // ptr1 是最后一个指向资源的 shared_ptr,当其超出作用域时,资源被销毁,引用计数为0
    return 0;
} // 资源被释放

4.⚡ 左、右值引用

在 C++ 中,左值引用和右值引用是两种引用的分类,它们的主要区别在于它们对应的表达式的生命周期和可修改性。理解这两种引用对于理解移动语义和完美转发等高级 C++ 特性至关重要。

(1)左值引用 (Lvalue Reference)

  • 左值引用是最常见的引用类型。它们绑定到具有名称的对象(左值),并且可以持续在程序中使用。左值引用通过使用 & 符号声明。

  • 左值是具有标识符的表达式,即可寻址的、具有名称的实体。通常情况下,变量、函数返回的左值以及具有名称的表达式都是左值。

  • 左值引用通常用于函数参数中,以便对参数进行修改而不复制其内容。

int x = 5; // x 是左值
int& ref = x; // ref 是左值引用

(2)右值引用 (Rvalue Reference)

通过使用右值引用和移动语义,可以避免不必要的深拷贝,并在某些情况下获得更高的性能。右值引用在移动构造函数、移动赋值运算符和完美转发等情况下发挥重要作用。

  • 右值引用是 C++11 引入的新特性。它们绑定到临时对象(右值),它们可能是无名的临时对象或者具有移动语义的对象。

  • 右值引用通过使用 && 符号声明。

  • 右值通常是不具有名称的表达式,如临时对象、返回右值引用的函数、std::move 返回的结果等。

  • 右值引用允许程序员实现移动语义,即转移资源的所有权而不进行深拷贝,从而提高性能。

int&& rvref = 10; // 10 是右值

5.⚡ 复制构造函数和移动构造函数的区别

复制构造函数(Copy Constructor)和移动构造函数(Move Constructor)是 C++ 中用于创建对象副本的两种特殊成员函数。它们之间的主要区别在于它们对对象的资源所有权的处理方式。

(1)复制构造函数(Copy Constructor)

  • 复制构造函数用于创建一个新对象,其内容与另一个已存在的对象完全相同。它通过复制另一个对象的内容来初始化新对象。

  • 复制构造函数的形式是接受一个常量引用作为参数,通常声明为 const ClassName(const ClassName& other)。

  • 复制构造函数在对象之间进行深拷贝,即它复制了另一个对象的所有成员变量的值,而不是简单地复制指针或引用。

class MyClass {
public:
    // Copy constructor
    MyClass(const MyClass& other) {
        // Perform deep copy
    }
};

(2)移动构造函数(Move Constructor)

  • 移动构造函数用于从一个临时对象(右值)转移资源的所有权到一个新对象,而不进行深拷贝。移动构造函数通常用于提高性能,避免了不必要的资源复制。

  • 移动构造函数的形式是接受一个右值引用作为参数,通常声明为 ClassName(ClassName&& other)。

  • 移动构造函数将源对象的资源指针(如堆分配的内存)"偷走"(通常通过将源对象的指针置为空来实现),而不是进行深拷贝。

class MyClass {
public:
    // Move constructor
    MyClass(MyClass&& other) noexcept {
        // Perform move semantics
    }
};

因此,移动构造函数通常用于那些可以修改资源指针以移动资源的类,例如动态分配的内存、文件句柄等。通过使用移动构造函数,可以避免不必要的资源复制,并提高程序的性能。

下面是移动构造函数和移动赋值函数的示例:

#include <iostream>
#include <vector>

class MyObject {
public:
    MyObject() {
        std::cout << "Constructor called" << std::endl;
    }
    
    ~MyObject() {
        std::cout << "Destructor called" << std::endl;
    }
    
    // 移动构造函数
    MyObject(MyObject&& other) noexcept {
        std::cout << "Move constructor called" << std::endl;
        // 这里通常是将资源从 `other` 移动到当前对象
    }
    
    // 移动赋值运算符
    MyObject& operator=(MyObject&& other) noexcept {
        std::cout << "Move assignment operator called" << std::endl;
        if (this != &other) {
            // 这里通常是将资源从 `other` 移动到当前对象
        }
        return *this;
    }
};

// 接受对象所有权的函数
void processObject(MyObject&& obj) {
    // 在这里处理传递过来的对象,它的所有权已经转移到了 `obj`
}

int main() {
    // 创建对象
    MyObject obj;

    // 将对象传递给函数,并传递所有权
    processObject(std::move(obj));

    return 0;
}

6.⚡ std::move

std::move 是 C++ 标准库中的一个函数,用于将其参数转换为右值引用,通常用于实现移动语义。移动语义允许在不进行深拷贝的情况下转移资源的所有权,提高了程序的效率。你可以在以下情况下使用 std::move。

  • 在使用容器时,通过移动元素而不是复制元素来提高性能。

  • 在实现移动构造函数和移动赋值运算符时,可以使用 std::move 来转移资源的所有权。

  • 当你希望在函数中传递对象的所有权时,但又不想进行深拷贝时。

下面是 std::move 的基本用法示例:

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

int main() {
    std::vector<int> source = {1, 2, 3, 4, 5};
    std::vector<int> destination;

    // 使用 std::move 将 source 中的元素移动到 destination 中
    destination = std::move(source);

    // 现在 source 已经为空
    std::cout << "Source size after move: " << source.size() << std::endl;
    // destination 包含了原始 source 的元素
    std::cout << "Destination size after move: " << destination.size() << std::endl;

    return 0;
}

在上面的示例中,std::move 用于将 source 向量的内容移动到 destination 向量中。这样做会将 source 的状态置为空,避免了不必要的深拷贝,从而提高了性能。

posted @ 2024-03-19 23:34  capybara-lsq  阅读(79)  评论(0)    收藏  举报