Author: MrGood
Link: [https://www.zhihu.com/question/423364880/answer/3367011077]
在C和C++中,static关键字主要有三种用法,它们在两种语言中的作用是相同的。
另外,C++还有一种特殊的用法,那就是静态成员。在类中,可以使用static关键字来声明静态成员变量和静态成员函数。静态成员变量属于类本身,不管创建多少个类的对象,静态成员变量只有一份。静态成员函数也是属于类本身的,它们不能访问类的非静态成员变量,但可以访问静态成员变量。
union它允许在相同的内存位置存储不同的数据类型和长度的数据。你可以把union理解为一个可以存储不同类型数据的容器。
union的主要用途是节省内存,特别是当你有一些变量不会同时使用时。因为union的所有成员都共享同一块内存,所以它的大小等于其最大的成员。例如,如果你有一个union,包含一个int类型和一个double类型,那么这个union的大小就等于double的大小,因为double的大小大于int。
此外,union还常常和结构体一起使用,来实现一种称为"标记联合"的数据结构。在标记联合中,你可以使用一个结构体成员作为"标记",来表示union中哪个成员当前正在被使用。
cpptypedef enum { INT, FLOAT, STRING } Type;
typedef struct {
Type type; // 标记,用来表示union中哪个成员正在被使用
union {
int intValue;
float floatValue;
char \*stringValue;
} value; // union,可以存储int、float或char \*类型的数据
} Variant;
在这个例子中,Variant
类型的变量可以存储一个int、一个float或一个字符串。你可以通过type
成员来确定union中哪个成员正在被使用,然后通过value
成员来访问该数据。
volatile
关键字在C和C++语言中被用来指示编译器,被其修饰的变量可能在外部程序或者硬件,或者其他未知的因素中改变,即使在程序内部看起来没有明显的代码修改它。这样,编译器在优化代码的时候,就不会对这样的变量进行某些假设,并且每次引用这个变量的时候都会直接从它所在的内存读取,而不是使用可能已经保存在寄存器中的备份。
volatile
关键字常用在多线程编程和嵌入式系统编程中。在多线程编程中,一个线程可能修改一个变量,同时另一个线程也在读取或写入这个变量,这时就需要使用volatile
关键字来确保变量的值的正确性。在嵌入式系统编程中,volatile
关键字常用于硬件寄存器的访问,因为硬件寄存器的值可能随时改变,与程序的运行无关。
在汇编层面,函数调用过程大致包括以下步骤:
面向对象编程(Object-Oriented Programming, OOP)有以下几个基本特性:
这四个特性是面向对象编程的基石,通过它们可以更好地组织和管理代码,提高代码的可复用性和可维护性。
可以分为静态多态和动态多态两种。
静态多态:
javapublic class Polymorphism {
// 方法重载(Overload)
public int add(int a, int b) {
return a + b;
}
public int add(int a, int b, int c) {
return a + b + c;
}
public static void main(String[] args) {
Polymorphism polymorphism = new Polymorphism();
System.out.println(polymorphism.add(1, 2)); // 输出3
System.out.println(polymorphism.add(1, 2, 3)); // 输出6
}
}
在这个例子中,add
方法被重载了,它根据参数列表的不同来进行不同的操作,这就是静态多态。
动态多态:
cpp#include<iostream>
using namespace std;
class Base {
public:
virtual void func() { cout << "Base function.\n"; }
};
class Derived : public Base {
public:
void func() override { cout << "Derived function.\n"; }
};
int main() {
Base\* base = new Derived();
base->func(); // 输出:Derived function.
delete base;
return 0;
}
以上代码中,Base
类中的func
函数被声明为虚函数,Derived
类中重写了这个函数。在main
函数中,我们创建了一个Derived
类的对象,但是用一个Base
类的指针来引用它,并调用func
函数,此时调用的是Derived
类的版本,这就实现了动态多态。
cpp#include <mutex>
class Singleton {
private:
static Singleton\* instance;
static std::mutex mtx;
Singleton() {}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
public:
static Singleton\* getInstance() {
if (instance == nullptr) {
mtx.lock();
if (instance == nullptr) {
instance = new Singleton();
}
mtx.unlock();
}
return instance;
}
};
// 初始化静态成员变量
Singleton\* Singleton::instance = nullptr;
std::mutex Singleton::mtx;
int main() {
// 获取Singleton实例
Singleton\* s1 = Singleton::getInstance();
Singleton\* s2 = Singleton::getInstance();
// 检查两个实例是否相同
if (s1 == s2) {
std::cout << "Singleton works, both instances are the same." << std::endl;
} else {
std::cout << "Singleton failed, instances are not the same." << std::endl;
}
return 0;
}
具体见:
如何高效的实现 C++单例模式? - MrGood的回答 - 知乎
https://www.zhihu.com/answer/3363520861
如果一个对象只有一个int型成员变量,那么sizeof该对象通常会返回4。但是,这只是一种常见的情况,实际的结果可能会因编译器的内存对齐规则和填充规则的不同而不同。
一个int型变量占用4个字节,一个char型变量占用1个字节。然而,由于编译器的内存对齐规则,sizeof一个包含一个int型和一个char型成员变量的对象通常会是8个字节。
一个虚函数在对象中并不直接占用存储空间,但是如果一个类中有虚函数,那么编译器会为该类的对象生成一个虚函数表(vtable),并在每个对象中添加一个指向这个虚函数表的指针。因此,如果一个对象只有一个int型成员变量和一个虚函数,那么它的大小应该是int的大小加上一个指针的大小。
对于32位系统,一个指针通常占用4个字节,对于64位系统,一个指针通常占用8个字节。所以,在32位系统中,sizeof这个对象应该是8个字节(4个字节的int加上4个字节的指针),在64位系统中,sizeof这个对象应该是12个字节(4个字节的int加上8个字节的指针)。
智能指针是通过对动态分配的对象进行引用计数来实现的。每当有一个新的智能指针指向一个对象时,这个对象的引用计数就会增加;当智能指针停止指向一个对象时,这个对象的引用计数就会减少。当对象的引用计数减到0时,就表示没有任何智能指针再指向这个对象,于是对象就会被自动删除。
C++标准库中包含了几种智能指针,比如unique_ptr、shared_ptr和weak_ptr等。unique_ptr是一种独占所有权的智能指针,一个对象只能由一个unique_ptr拥有。shared_ptr则允许多个智能指针共享所有权。weak_ptr则是一种不会增加引用计数的智能指针,主要用于解决循环引用的问题。例如,如果两个对象通过智能指针相互引用,那么它们的引用计数永远不会变为0,因此它们永远不会被删除,从而导致内存泄漏。通过使用weak_ptr,可以打破这种循环引用,从而避免内存泄漏。
当std::vector的容量满了,如果还尝试再向其添加元素,vector会自动重新分配更大的内存空间来存储更多的元素。
具体来说,当vector的容量不足以容纳新的元素时,vector会申请一个新的、更大的内存块,新的内存块的大小通常是原来容量的两倍(这个倍数可能因具体实现而不同)。然后,vector会将原来的元素复制(或移动)到新的内存块中,然后释放原来的内存块。
map和unordered_map都是C++标准库中的关联容器,用于存储键值对。但是它们的内部实现和性能特性有所不同。
map:
unordered_map:
右值引用是C++11引入的一个新特性,主要用于支持移动语义(Move Semantics)和完美转发(Perfect Forwarding)。
右值引用可以绑定到一个将要销毁的对象(也就是右值),这样我们就可以安全地获取对象的资源,而不用担心这个对象在之后会被使用。在C++中,我们可以用&&来声明一个右值引用。
例如,我们可以定义一个移动构造函数,它接受一个右值引用参数,并从这个参数中“偷”取资源。这样,我们就可以避免资源的复制,从而提高性能。这就是所谓的移动语义。
move函数就是为了支持这种移动语义。当我们调用std::move时,可以把一个左值转换为右值,从而可以被移动而不是被复制。
例如,假设我们有一个大型的vector对象,我们想把它传递给另一个vector。通常情况下,这会导致整个vector的复制,这可能很慢。但是,如果我们使用std::move,就可以避免这种复制,直接把原vector的内存移交给新vector,这通常会快得多。
总的来说,右值引用和move是为了优化性能,特别是在涉及大型对象或者“昂贵”的操作(如内存分配)时。
构造函数可以抛出异常。在创建对象的过程中,如果遇到任何问题(例如,无法分配所需的内存或无法打开所需的文件),就可以抛出异常。
然而,析构函数不应抛出异常。如果一个析构函数可能抛出异常,这可能导致两个问题:
std::terminate()
,导致程序突然终止。因此,析构函数应设计为不抛出异常,也就是说,应在析构函数的声明后面加上noexcept
关键字:
class MyClass { public: ~MyClass() noexcept { // 释放资源,但不抛出异常 } };
static_cast
:这是最常用的类型转换运算符。它可以用于基本数据类型之间的转换,也可以用于类之间的转换,但是对于类之间的转换,它们之间必须有继承关系。dynamic_cast
:这个运算符用于动态类型转换。它只能用于类之间的转换,而且这些类之间必须有继承关系。不同于static_cast
,dynamic_cast
在运行时检查转换是否有效。如果转换无效,它将返回空指针。const_cast
:这个运算符用于改变表达式的常量属性,比如,可以用它来去掉const
修饰。reinterpret_cast
:这个运算符用于进行低级别的类型转换。它可以将任何类型的指针转换为任何其他类型的指针,也可以将任何整数类型转换为任何指针类型,以及反向转换。reinterpret_cast
运算符很容易滥用,一般在必要的时候才使用。#include
,#define
和条件编译指令。struct TreeNode { int val; TreeNode\* left; TreeNode\* right; TreeNode(int x) : val(x), left(NULL), right(NULL) {} }; void preorder(TreeNode\* root) { if (root != NULL) { cout << root->val << " "; preorder(root->left); preorder(root->right); } } void inorder(TreeNode\* root) { if (root != NULL) { inorder(root->left); cout << root->val << " "; inorder(root->right); } } void postorder(TreeNode\* root) { if (root != NULL) { postorder(root->left); postorder(root->right); cout << root->val << " "; } } void levelorder(TreeNode\* root) { if (root == NULL) return; queue<TreeNode\*> q; q.push(root); while (!q.empty()) { TreeNode\* node = q.front(); q.pop(); cout << node->val << " "; if (node->left != NULL) q.push(node->left); if (node->right != NULL) q.push(node->right); } } int main() { TreeNode\* root = new TreeNode(1); root->left = new TreeNode(2); root->right = new TreeNode(3); cout << "Preorder: "; preorder(root); cout << "\nInorder: "; inorder(root); cout << "\nPostorder: "; postorder(root); cout << "\nLevel order: "; levelorder(root); return 0; }
哈希表是一种数据结构,它使用哈希函数将键(key)转换为数组的索引来访问数据。这种方法使得无论数据的大小如何,访问时间都保持不变。
哈希表的工作原理:
解决哈希冲突的几种方法:
int binarySearch(int arr[], int left, int right, int x) { if (right >= left) { int mid = left + (right - left) / 2; // 如果元素存在于中间 if (arr[mid] == x) return mid; // 如果元素小于 mid,那么它只能存在于左子数组中 if (arr[mid] > x) return binarySearch(arr, left, mid - 1, x); // 否则元素可以只存在于右子数组中 return binarySearch(arr, mid + 1, right, x); } // 元素不存在于数组中 return -1; } int main(void) { int arr[] = {2, 3, 4, 10, 40}; int x = 10; int n = sizeof(arr) / sizeof(arr[0]); int result = binarySearch(arr, 0, n - 1, x); (result == -1) ? cout << "元素不在数组中" : cout << "元素在数组中的索引为 " << result; return 0; }
int climbStairs(int n) { if (n <= 2) { return n; } return climbStairs(n-1) + climbStairs(n-2); } int climbStairs(int n) { if (n <= 2) { return n; } int dp[n+1]; dp[1] = 1; dp[2] = 2; for (int i = 3; i <= n; i++) { dp[i] = dp[i-1] + dp[i-2]; } return dp[n]; }
可以使用哈希表(HashMap)或者字典(Dictionary)。哈希表是一种数据结构,它支持快速插入和查找操作。在这个问题中,我们可以将IP地址作为键,出现的次数作为值存储在哈希表中。
以下是一个简单的实现方式:
#include <unordered\_map> #include <string> class IPAddressChecker { public: bool hasSeen(std::string ip) { if (ipMap.count(ip) > 0) { return true; } else { ipMap[ip] = 1; return false; } } private: std::unordered\_map<std::string, int> ipMap; }; int main() { IPAddressChecker checker; checker.hasSeen("192.168.0.1"); // 返回 false,因为这个IP地址之前没有出现过 checker.hasSeen("192.168.0.1"); // 返回 true,因为这个IP地址已经出现过了 return 0; }
#include <queue> #include <mutex> #include <condition\_variable> template <typename T> class ThreadSafeQueue { public: ThreadSafeQueue() {} void push(T value) { std::lock\_guard<std::mutex> lock(mtx); data.push(value); cv.notify\_one(); } bool try\_pop(T& value) { std::lock\_guard<std::mutex> lock(mtx); if (data.empty()) { return false; } value = data.front(); data.pop(); return true; } void wait\_and\_pop(T& value) { std::unique\_lock<std::mutex> lock(mtx); cv.wait(lock, [this]{ return !data.empty(); }); value = data.front(); data.pop(); } private: std::queue<T> data; std::mutex mtx; std::condition\_variable cv; };
这个队列的实现满足线程安全:多个线程可以同时对这个队列进行入队和出队操作,而不会出现数据竞争的问题。
push方法将一个元素添加到队列中,并通知一个正在等待的线程。
try_pop尝试从队列中移除一个元素,如果队列为空,则返回false。
wait_and_pop方法将阻塞当前线程,直到队列中有元素为止。它会移除队列中的一个元素,并将其返回。
进程和线程都是一个时间段的描述,是对系统进行资源分配和调度的基本单位,是多进程和多线程技术的基础。它们之间的主要差别在于:
一个进程的地址空间通常包含以下几个部分:
转载图,侵权立刻删除
数据段中静态存储区(Static Storage Area)和常量区(Constant Area)是计算机内存中的两个特定区域,而BSS(Block Started by Symbol)则是静态存储区的一部分。
具体来说:
静态存储区是在程序运行期间一直存在的存储区域,用于存储静态变量、全局变量和静态对象。这些变量和对象在程序执行期间都不会销毁,而是在程序生命周期内保持不变。静态存储区的内存空间在程序加载时被分配,并在程序退出时才会释放。
常量区也被称为只读数据区或文字常量区,用于存储常量值和字符串字面量。常量区的数据在程序执行期间也是不变的,因此只能读取,而不能修改。常量区的数据在程序加载时被分配,并在程序退出时才会释放。常量区中的数据通常是只读的,无法进行修改。
BSS是静态存储区中的一部分,用于存储未初始化的全局变量和静态变量。在程序加载时,BSS段被设置为零或空值,不需要为每个变量分配具体的空间。只有在程序运行时实际分配内存并初始化这些变量。
需要注意的是,具体的内存布局和存储区域的划分可能会因编译器、操作系统和平台的不同而有所差异。因此,在某些情况下,BSS可能被合并到静态存储区中。
总结来说,静态存储区是存储静态变量、全局变量和静态对象的区域,其中BSS是静态存储区的一部分,用于存储未初始化的全局变量和静态变量。常量区是存储常量值和字符串字面量的只读区域。
虚拟内存是一种内存管理技术,主要用来使每个进程仿佛拥有连续的、完整的地址空间,从而使得程序能够更加容易地管理内存。它的工作原理是,将程序的地址空间分割成固定大小的块,称为"页面"(page),同时,将物理内存也分割成同样大小的块,称为"页框"(page frame)。
当程序需要使用一部分内存时,操作系统会将相应的"页面"映射到"页框"上。这个映射关系会被保存在一个叫做"页表"的数据结构中。当程序访问自己的地址空间时,硬件会自动查找页表,找到对应的物理内存地址,然后从那里读取或写入数据。
虚拟内存有几个主要的好处:
虚拟地址的翻译过程是通过硬件和操作系统中的内存管理单元(MMU)完成的。以下是一个简化的虚拟地址翻译过程:
如果在页表中找不到对应的条目,就会产生一个页面错误(page fault)。这通常意味着对应的页面不在内存中,而在磁盘上。此时,操作系统需要将页面从磁盘中读取到内存中,然后更新页表,最后重新进行地址翻译。这个过程被称为页面置换(page replacement)。
当一个进程调用fork()
函数时,操作系统会创建一个新的进程。新进程是原进程的一个副本,包括代码、堆、栈、文件描述符、环境变量等等。
fork()
调用完成后,两个进程(父进程和子进程)会各自独立运行。它们有各自独立的地址空间,互不影响。但是,这两个进程会共享同样的代码段。fork()
返回0;在父进程中,fork()
返回子进程的PID。这样,父进程和子进程可以通过检查fork()
的返回值来确定自己的角色。虽然在fork()
后,父进程和子进程有各自独立的地址空间,但是大部分现代操作系统并不会立即复制所有的内存页面,而是使用一种称为**写时复制(copy-on-write)**的优化技术。具体来说,初始时,父进程和子进程共享同样的内存页面,这些页面被标记为只读。当某个进程试图修改一个页面时,操作系统会创建这个页面的一个副本,供这个进程使用。这样,只有在必要时才会复制内存页面,可以节省内存和提高性能。
C++-一文了解进程间通信,从概念到实现 - MrGood的文章 - 知乎
https://zhuanlan.zhihu.com/p/672264623
在内存中创建一个共享区域,多个进程可以访问这个区域。见上文。
原子操作是指一个被中断的操作,即使在并发环境下也不会被其他进程或线程干扰的操作。换句话说,它是一个单独的不可分割的操作,其他操作无法看到它的中间状态,只能看到它的初始状态或最终状态。
在硬件层面,例如在多核和多处理器环境下,CPU通常提供原子指令(如交换、比较和交换等)来实现原子操作。这些原子指令可以在单个CPU周期内完成,从而确保在它执行过程中不会被其他CPU或线程干扰。
在软件层面,可以使用各种同步机制(如互斥锁、信号量、临界区等)来保证操作的原子性。例如,当一个线程在执行某个操作时,可以通过锁机制阻止其他线程访问同一资源,从而确保该操作能够原子地执行。
详情见下文。
C++-一文了解I/O多路复用,从概念到实现 - MrGood的文章 - 知乎
https://zhuanlan.zhihu.com/p/672454948
epoll是Linux特有的IO多路复用机制,其性能优于select和poll。epoll使用一组函数来操作一个内核事件表,避免了每次调用都需要将全部文件描述符拷贝到内核态的问题。当某个文件描述符状态发生变化时,只需要操作事件表即可,效率较高。但是epoll的使用也较为复杂,需要更多的编程技巧。详情见上文。
信号机制的工作原理可以概括为:一个进程(或者操作系统内核)可以给另一个进程发送一个信号,收到信号的进程则会根据信号的类型采取相应的动作。这个动作可以是默认的(例如终止进程、忽略信号等),也可以是进程自定义的(通过捕获信号并指定信号处理函数来实现)。
常见的信号有:
系统调用是操作系统提供给上层应用的接口,应用程序通过系统调用请求操作系统完成某些特殊的工作。这些工作通常包括:创建和控制进程,进行网络通信,访问硬件设备,访问文件系统等。因为这些操作涉及到系统资源的管理和保护,所以必须由操作系统来完成。
执行系统调用的过程通常如下:
写时拷贝(Copy-On-Write,简称COW)是一种计算机程序资源管理技术,它允许同时拥有一个共享对象的多个部分在没有显式复制的情况下进行独立写操作。写时拷贝技术的主要优点是,如果没有写操作,就不会进行不必要的复制操作,从而节省了内存和CPU的使用。
写时拷贝的底层实现原理如下:
四层模型,也被称为TCP/IP模型,由下至上分别为:数据链路层、网络层、传输层和应用层。
其工作原理是基于ICMP(Internet Control Message Protocol,网络控制报文协议)。
当你在计算机上执行ping命令,向目标主机发送一个ICMP Echo请求报文,如果网络连接通畅,目标主机就会回应一个ICMP Echo回应报文。
在Ping命令执行过程中,它会记录并显示这些关于回应的信息:
通过Ping命令我们可以快速地得知两台主机之间的网络连接状况,以及网络的质量。
三次握手过程如下:
四次挥手过程如下:
TCP的第三次握手是可以携带数据的。在第三次握手时,客户端向服务器发送确认报文(ACK),确认自己收到了服务器的同步请求报文。
本文作者:OhtoAi
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!