C++ 内存模型学习笔记

C++ 内存模型从上(高地址)到下(低地址)可以分为以下几个部分: 栈区:由编译器自动分配释放,存放函数的参数值、局部变量的值等。 堆区:由程序员分配释放,若程序员不释放,程序结束时可能由操作系统回收。 全局/静态区:分为 .data 段(全局初始化区)和 .bss 段(全局未初始化区),.data 段存放 已初始化 了的全局变量和静态变量,.bss 段存放 未初始化 的变量。 常量区:就是 .rodata 段,存放常量。 代码区:存放函数体的代码。

2024年06月29日 · 1 分钟 · Cassius0924

C++ delete ptr 和 ptr = nullptr 的区别

delete ptrdelete ptr 是释放 ptr 所指向的对象资源,而 ptr 依然存在,且依然指向那片内存地址。 ptr = nullptrptr = nullptr 是将 ptr 指向空指针,和其所指向的对象没关系。 试着实现一个 unique_ptrtemplate <typename T> class UniquePtr { private: T *_ptr; public: // 默认构造 UniquePtr() : _ptr(nullptr) { } explicit UniquePtr(T *ptr) : _ptr(ptr) { } ~UniquePtr() { delete _ptr; // 无需置 nullptr,因为析构函数会被调用,_ptr 会被销毁 // 置空无意义 } // 拷贝构造 删除 UniquePtr(const UniquePtr &) = delete; UniquePtr &operator=(const UniquePtr &) = delete; // 移动构造 UniquePtr(UniquePtr &&p) noexcept : _ptr(p._ptr) { // 至于这里为什么不需要 delete _ptr // 是因为这是移动构造函数,是个构造函数!_ptr 本来就没有资源 p._ptr = nullptr; } UniquePtr &operator=(UniquePtr &&p) noexcept { if (p != *this) { delete _ptr; // 第一步,释放当前资源 _ptr = p._ptr; // 第二步,将当前指针指向新的资源 p._ptr = nullptr; // 第三步,将原来的指针置空 } return *this; } T *get() const { // 返回指针 return _ptr; } T *operator->() const { // 返回指针 return _ptr; } T &operator*() const { // 解引用 return *_ptr; } T *release() { // 这里不能 delete _ptr // 因为 release 只是解除 UniquePtr 对资源的所有权,但资源还是存在的 T *tmp = _ptr; _ptr = nullptr; return tmp; } void reset(T *newptr = nullptr) { if (_ptr != newptr) { delete _ptr; // 释放当前资源 _ptr = newptr; // 指向新资源 // 这里不需要置空 newptr // 是否置空 new ptr 由用户决定 } } }; UniquePtr &operator=(UniquePtr &&p) 移动赋值运算符的原理如下图: ...

2024年06月29日 · 1 分钟 · Cassius0924

C++ STL 常用容器和迭代器学习笔记

常用容器 序列容器 vector: 动态数组,随机插入/删除 O(n),随机访问 O(1),尾插 O(1) array: 静态数组,不支持插入/删除,随机访问 O(1) deque: 双端队列,头尾插入/删除 O(1),随机访问 O(1),中间插入/删除 O(n) list: 双向链表,插入/删除 O(1),不支持随机访问 forward_list: 单向链表,插入/删除 O(1),不支持随机访问 关联容器(底层实现为 红黑树 ) set: 有序集合,插入/删除/查找 O(logn) map: 有序映射,插入/删除/查找 O(logn) multiset: 有序多重集合,插入/删除/查找 O(logn) multimap: 有序多重映射,插入/删除/查找 O(logn) 无序容器(底层实现为 哈希表 ) unordered_set: 无序集合,插入/删除/查找 O(1) unordered_map: 无序映射,插入/删除/查找 O(1) unordered_multiset: 无序多重集合,插入/删除/查找 O(1) unordered_multimap: 无序多重映射,插入/删除/查找 O(1) 容器适配器 stack: 栈,后进先出,只能在栈顶插入/删除元素 queue: 队列,先进先出,只能在队尾插入,在队头删除元素 priority_queue: 优先队列,元素按照一定规则排序,每次取出的是最大/最小元素,底层实现为堆 vector#include <iostream> #include <vector> using namespace std; // vector使用示例 int main() { vector<int> vec = {1, 2, 3, 4, 5}; // 尾部插入元素:复杂度为O(1) vec.push_back(6); // 尾部删除元素:复杂度为O(1) vec.pop_back(); // 随机插入和删除元素:复杂度为O(n) vec.insert(vec.begin() + 1, 3); vec.erase(vec.begin() + 1); // vector的大小 cout << vec.size() << endl; // 获取vector的容量 cout << vec.capacity() << endl; // 判断vector是否为空 cout << vec.empty() << endl; // 获取vector的第一个元素和最后一个元素 cout << vec.front() << endl; cout << vec.back() << endl; // 访问指定位置的元素 cout << vec[2] << endl; cout << vec.at(2) << endl; // at函数会检查索引是否越界,更安全 vector<int> vec2 = {7, 8, 9, 10}; vec.swap(vec2); // 交换两个vector的元素 // 清空vector vec.clear(); } vector 常用的成员函数: ...

2024年06月28日 · 7 分钟 · Cassius0924

C++ 对象和指针的区别学习笔记

对象MyClass obj; obj.fun(); obj.count = 10; 对象是类的实例,占据实际的内存空间,可以调用类的成员函数和访问类的成员变量。 对象大小 = 成员变量大小 + 对齐填充 指针MyClass *p = new MyClass; p->fun(); p->count = 10; 指针是一个变量,存储对象的地址,可以通过指针访问对象的成员函数和成员变量。 指针大小 = 4 字节(32 位系统)或 8 字节(64 位系统) 对象和指针的区别 内存管理 对象:内存分配和释放通常是自动的(除非使用动态分配)。 指针:指向的内存需要手动管理,尤其是动态分配的内存。 访问方式: 对象:直接访问成员。 指针:通过解引用访问成员(使用 -> 操作符)。 生命周期: 对象:由作用域决定,局部对象在离开作用域时自动销毁。 指针:生命周期由程序员控制,指针可以指向任何作用域的变量。

2024年06月28日 · 1 分钟 · Cassius0924

C++ 多态学习笔记

C++ 的多态性是面向对象程序设计的三大特性之一(封装、继承、多态),它允许将子类对象赋值给父类对象,从而实现基类指针指向子类对象,实现基类指针调用子类对象的成员函数。 C++ 的多态性主要有两种实现方式:静态多态和动态多态。 静态多态:通过函数重载和模板实现。 动态多态:通过虚函数实现。 静态多态函数重载函数重载是指在同一个作用域内,可以定义 多个名称相同 但 参数列表不同 的函数。注意,不能用 返回值类型 来区分重载函数。 int add(int a, int b) { return a + b; } double add(double a, double b) { return a + b; } double add(double a, double b, double c) { return a + b + c; } 笔记 编译过程 ...

2024年06月27日 · 2 分钟 · Cassius0924

C++ 智能指针学习笔记

智能指针简介智能指针是一种 RAII(Resource Acquisition Is Initialization)技术,用于管理动态分配的内存。智能指针的优点是可以自动释放内存,避免内存泄漏。 C++11 标准引入了三种智能指针:std::unique_ptr、std::shared_ptr 和 std::weak_ptr。 它们都定义在头文件 <memory> 中。 unique_ptrunique_ptr 是一种独占所有权的智能指针,它保证同一时间只有一个指针可以指向对象。 unique_ptr 的创建#include <iostream> #include <memory> int main() { // 使用 new 创建 unique_ptr std::unique_ptr<int> up1(new int(10)); std::cout << *up1 << std::endl; // 使用裸指针创建 unique_ptr int count = 20; std::unique_ptr<int> up2(&count); std::cout << *up2 << std::endl; // 使用 make_unique 创建 unique_ptr auto up2 = std::make_unique<int>(20); std::cout << *up2 << std::endl; return 0; } unique_ptr 的拷贝和赋值unique_ptr 不能拷贝,但可以移动。 #include <iostream> int main() { std::unique_ptr<int> up1(new int(10)); std::unique_ptr<int> up2 = std::move(up1); std::cout << *up2 << std::endl; return 0; } unique_ptr 的释放unique_ptr 会在离开作用域时自动释放内存。 #include <iostream> int main() { std::unique_ptr<int> up1(new int(10)); std::cout << *up1 << std::endl; return 0; } unique_ptr 的自定义删除器unique_ptr 支持自定义删除器,可以用于释放动态分配的内存。 ...

2024年06月26日 · 3 分钟 · Cassius0924

堆排序学习笔记

在学习堆排序之前,我们先来了解一下堆这种数据结构。 堆的概念堆是一种特殊的树形数据结构,它满足以下性质: 堆必须是一个 完全二叉树 。 堆序性:堆中任意节点的值总是不大于或不小于其子节点的值。 根据堆序性,我们可以将堆分为两种类型: 大顶堆:每个节点的值都大于或等于其子节点的值 小顶堆:每个节点的值都小于或等于其子节点的值 所以,如果一个完全二叉树的一个节点即大于其父节点,又大于其子节点,那么这个树就不是一个堆。小于同理。 笔记 完全二叉树的性质 ...

2024年06月26日 · 2 分钟 · Cassius0924

C++ 类的运算符为什么要使用引用返回

代码class AClass { private: int _count; public: AClass() : _count(0) { std::cout << "Default constructor called\n"; } // 赋值运算符,返回引用 AClass &operator=(int cnt) { _count = cnt;k return *this; } // 后置自增运算符,返回引用 int &operator++(int) { ++_count; return *this; } }; 如果不使用引用返回,其实也是可以运行的,只不过会在返回时调用拷贝构造函数,生成临时对象,然后再调用析构函数释放临时对象,这样会多出一次拷贝构造和析构的开销。而使用引用返回,可以直接返回对象的引用,避免了这个开销。 需要注意的是,如果我们返回值类型,我们是不能直接修改返回值的: class AClass { private: int _count; public: // 省略构造函数 // 赋值运算符,返回引用 AClass operator=(int cnt) { _count = cnt; return *this; } // 后置自增运算符,返回引用 int operator++(int) { ++_count; return *this; } void print() const { std::cout << "AClass: count = " << _count << '\n'; } }; int main() { AClass a; a.print(); // 输出 AClass: count = 0 (a++) = 10; a.print(); // 输出 AClass: count = 1 (a++)++; a.print(); // 输出 AClass: count = 2 } 可以看到,如果我们返回值类型,我们是不能直接修改返回值的。虽然 a++ 已经修改了 a 的值,但是 a++ 返回的是一个修改后的 a 对象的拷贝,所以 (a++) = 10; 或者 (a++)++; 修改的是这个拷贝对象,而不是原对象 a。 ...

2024年06月25日 · 1 分钟 · Cassius0924

C++ 类的流插入和流提取运算符为什么要声明为友元函数

友元函数版代码class AClass { private: int _count; std::string _str; std::vector<int> _vec; public: friend std::ostream &operator<<(std::ostream &os, const AClass &a) { os << "AClass: count = " << a._count << ", str = " << a._str << ", vec size = " << a._vec.size(); return os; } }; 为什么要声明为友元函数先理解一下友元函数,它实际上是一个普通函数,不属于类成员,但它又是一个特殊的普通函数,因为它可以访问类的私有成员。因此 operator<< 和 operator>> 声明为友元函数的目的很明显,就是为了能够访问类的私有成员。 实际上,如果它们不声明为友元函数,也是可以实现的,例如下面代码: class AClass { private: int _count; std::string _str; std::vector<int> _vec; public: std::ostream &operator<<(std::ostream &os) { os << "AClass: count = " << _count << ", str = " << _str << ", vec size = " << _vec.size(); return os; } }; 但是这样就需要特殊的方法来调用这个 operator<< 函数,因为它不再是一个普通函数,而是一个类成员函数: int main() { AClass a; a << std::cout; // 错误,不能这样调用 a.operator<<(std::cout); // 正确 return 0; } 这样显然不够直观,不是一个正常人类写的代码:) 所以,为了代码的可读性和可维护性,我们将 operator<< 和 operator>> 声明为友元函数,这样就可以直接使用 << 和 >> 运算符来操作类的对象了。 ...

2024年06月25日 · 1 分钟 · Cassius0924

Windows Socket API 和 Linux Socket API

本文章主要介绍 Windows 下和 Linux 下的 Socket 编程区别,即 Windows Socket API 和 Linux Socket API 的区别。 头文件Windows 环境下的 Socket 编程需要以下头文件: <WinSock2.h> <WS2tcpip.h> 笔记 如果使用 MSVC 编译器,那么还需要使用预处理指令 #pragma comment(lib, "Ws2_32.lib") 来链接 Ws2_32.lib 库。 ...

2024年06月16日 · 2 分钟 · Cassius0924