C++高频知识点(二十二)
文章目录
- 106. 讲讲vector怎么实现的?
- std::vector 的底层实现原理
- std::vector 的常见操作与实现细节
- 107. 使用过线程吗,detach和join有什么区别?
- join 方法
- detach 方法
- join 和 detach 的对比
- 108. 什么是TCP粘包?
- TCP粘包问题示例
- 客户端代码:
- 服务器代码:
- 解决粘包问题的方法:长度前缀协议
- 修改后的客户端代码:
- 修改后的服务器代码:
- 109. 在什么时候需要使用常引用?
- 1. 避免拷贝大对象
- 2. 保持对象的不可变性
- 3. 避免对象切片问题
- 解决方法:
- 4. 传递类对象给函数
- 5. 提高函数的泛用性
- 1. 左值和右值的区别
- 2. 普通引用(T&)的局限性
- 3. 常引用(const T&)相对就比较灵活性
- 110. 说说熵编码的原理
- 熵编码的基本原理
- 1. 霍夫曼编码(Huffman Coding)
- 2. 算术编码(Arithmetic Coding)
- 3. 游程长度编码(Run-Length Encoding, RLE)
106. 讲讲vector怎么实现的?
std::vector 的底层实现原理
std::vector 的常见操作与实现细节
107. 使用过线程吗,detach和join有什么区别?
在 C++ 中,线程(std::thread)用于实现多线程编程,它是 C++11 标准库提供的类。创建一个线程后,你可以使用 detach 或 join 方法来管理线程的执行和生命周期。detach 和 join 是用于线程管理的两个常用方法,它们有不同的用途和行为。
join 方法
#include <iostream>
#include <thread>void threadFunction() {std::cout << "Thread is running..." << std::endl;
}int main() {std::thread t(threadFunction); // 创建线程// 主线程等待子线程完成t.join();std::cout << "Thread finished." << std::endl;return 0;
}
- 注意事项:调用 join 后,线程对象不再关联任何线程。再调用 join 会引发异常。
detach 方法
#include <iostream>
#include <thread>
#include <chrono>void threadFunction() {std::this_thread::sleep_for(std::chrono::seconds(2));std::cout << "Detached thread is running..." << std::endl;
}int main() {std::thread t(threadFunction); // 创建线程// 分离线程,主线程不等待t.detach();std::cout << "Main thread finished." << std::endl;return 0;
}
join 和 detach 的对比
108. 什么是TCP粘包?
TCP粘包问题示例
假设有一个简单的客户端和服务器程序,客户端每次向服务器发送一条消息,而服务器接收消息后回显给客户端。
客户端代码:
// TCP Client - client.cpp
#include <iostream>
#include <cstring>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>int main() {int sock = socket(AF_INET, SOCK_STREAM, 0); // 创建套接字if (sock == -1) {std::cerr << "创建套接字失败!" << std::endl;return -1;}sockaddr_in server_addr;server_addr.sin_family = AF_INET;server_addr.sin_port = htons(8080); // 服务器端口server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 服务器IP地址// 连接服务器if (connect(sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {std::cerr << "连接服务器失败!" << std::endl;return -1;}// 发送多条消息给服务器const char* messages[] = {"Message 1", "Message 2", "Message 3"};for (const char* msg : messages) {send(sock, msg, strlen(msg), 0); // 发送数据std::cout << "发送消息: " << msg << std::endl;}close(sock); // 关闭套接字return 0;
}
服务器代码:
// TCP Server - server.cpp
#include <iostream>
#include <cstring>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>int main() {int server_sock = socket(AF_INET, SOCK_STREAM, 0); // 创建服务器套接字if (server_sock == -1) {std::cerr << "创建服务器套接字失败!" << std::endl;return -1;}sockaddr_in server_addr;server_addr.sin_family = AF_INET;server_addr.sin_port = htons(8080); // 服务器端口server_addr.sin_addr.s_addr = INADDR_ANY; // 接受所有IP地址// 绑定端口if (bind(server_sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {std::cerr << "绑定失败!" << std::endl;return -1;}// listen() 函数使服务器开始监听客户端连接请求。// 第一个参数是服务器套接字 server_sock。// 第二个参数是待处理的最大连接请求数,这里设置为 3,表示最多同时处理 3 个客户端的连接请求。listen(server_sock, 3); // 开始监听std::cout << "等待连接..." << std::endl;int client_sock = accept(server_sock, nullptr, nullptr); // 接受客户端连接if (client_sock < 0) {std::cerr << "接受连接失败!" << std::endl;return -1;}std::cout << "客户端已连接!" << std::endl;// 接收客户端消息char buffer[1024];// recv() 函数用于接收客户端发送的数据。// 第一个参数是客户端套接字 client_sock。// 第二个参数是接收数据的缓冲区 buffer。// 第三个参数是缓冲区大小,这里设置为 sizeof(buffer) - 1,确保有足够的空间存储数据并留出位置放置字符串结束符 \0。// 第四个参数通常设置为 0,表示默认行为(阻塞式接收)。int bytes_received = recv(client_sock, buffer, sizeof(buffer) - 1, 0); // 接收数据if (bytes_received > 0) {buffer[bytes_received] = '\0'; // 添加字符串结束符std::cout << "收到消息: " << buffer << std::endl;}close(client_sock); // 关闭客户端套接字close(server_sock); // 关闭服务器套接字return 0;
}
解决粘包问题的方法:长度前缀协议
我们可以通过在每条消息的开头添加一个固定长度的消息长度字段来解决粘包问题。服务器在接收消息时首先读取长度字段,然后根据长度读取相应数量的字节数据。
修改后的客户端代码:
// TCP Client - client.cpp (with length prefix)
#include <iostream>
#include <cstring>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>void send_message(int sock, const char* msg) {uint32_t len = htonl(strlen(msg)); // 转换为网络字节序send(sock, &len, sizeof(len), 0); // 发送长度字段send(sock, msg, strlen(msg), 0); // 发送实际消息
}int main() {int sock = socket(AF_INET, SOCK_STREAM, 0); // 创建套接字if (sock == -1) {std::cerr << "创建套接字失败!" << std::endl;return -1;}sockaddr_in server_addr;server_addr.sin_family = AF_INET;server_addr.sin_port = htons(8080); // 服务器端口server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 服务器IP地址// 连接服务器if (connect(sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {std::cerr << "连接服务器失败!" << std::endl;return -1;}// 发送多条消息给服务器const char* messages[] = {"Message 1", "Message 2", "Message 3"};for (const char* msg : messages) {send_message(sock, msg); // 发送消息std::cout << "发送消息: " << msg << std::endl;}close(sock); // 关闭套接字return 0;
}
修改后的服务器代码:
// TCP Server - server.cpp (with length prefix)
#include <iostream>
#include <cstring>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>int main() {int server_sock = socket(AF_INET, SOCK_STREAM, 0); // 创建服务器套接字if (server_sock == -1) {std::cerr << "创建服务器套接字失败!" << std::endl;return -1;}sockaddr_in server_addr;server_addr.sin_family = AF_INET;server_addr.sin_port = htons(8080); // 服务器端口server_addr.sin_addr.s_addr = INADDR_ANY; // 接受所有IP地址// 绑定端口if (bind(server_sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {std::cerr << "绑定失败!" << std::endl;return -1;}listen(server_sock, 3); // 开始监听std::cout << "等待连接..." << std::endl;int client_sock = accept(server_sock, nullptr, nullptr); // 接受客户端连接if (client_sock < 0) {std::cerr << "接受连接失败!" << std::endl;return -1;}std::cout << "客户端已连接!" << std::endl;// 接收客户端消息while (true) {uint32_t len;int len_received = recv(client_sock, &len, sizeof(len), 0); // 读取长度字段if (len_received <= 0) break; // 检查连接是否关闭len = ntohl(len); // 转换为主机字节序char* buffer = new char[len + 1];int bytes_received = recv(client_sock, buffer, len, 0); // 读取实际数据if (bytes_received <= 0) break; // 检查连接是否关闭buffer[bytes_received] = '\0'; // 添加字符串结束符std::cout << "收到消息: " << buffer << std::endl;delete[] buffer;}close(client_sock); // 关闭客户端套接字close(server_sock); // 关闭服务器套接字return 0;
}
在修改后的代码中,客户端每次发送消息前都会先发送一个4字节的长度字段,这样服务器端就能根据长度字段正确解析每条消息,避免粘包问题。
109. 在什么时候需要使用常引用?
在C++编程中,使用常引用(const reference)的主要目的是提高程序的效率和安全性。
以下是一些需要使用常引用的情况:
1. 避免拷贝大对象
当函数参数是一个大对象(例如,大型的std::vector或std::string)时,如果传递该对象的副本,会带来较大的开销。通过使用常引用,我们可以避免对象的拷贝操作,减少内存使用和提高程序性能。例如:
void printVector(const std::vector<int>& vec) {for (const int& val : vec) {std::cout << val << " ";}
}
2. 保持对象的不可变性
常引用确保了引用的对象不能被修改。这样可以提高代码的安全性,防止在不应该修改对象的情况下意外修改。例如:
void printName(const std::string& name) {std::cout << "Name: " << name << std::endl;// name不能被修改
}
3. 避免对象切片问题
对象切片是指,当我们将一个派生类对象传递给一个基类类型的参数时,派生类特有的成员变量或方法信息会丢失,只保留基类部分的数据。这种情况会在对象按值传递时发生。
#include <iostream>class Base {
public:virtual void show() const {std::cout << "Base class" << std::endl;}
};class Derived : public Base {
public:void show() const override {std::cout << "Derived class" << std::endl;}
};void process(Base baseObj) { // 按值传递baseObj.show(); // 调用的是 Base::show()
}int main() {Derived d;process(d); // Derived 对象被切片为 Base 对象return 0;
}
在上面的例子中,Derived类继承自Base类。当我们将Derived对象d传递给process函数时,发生了对象切片。Derived对象中的show方法不会被调用,而是调用了Base类中的show方法。
解决方法:
我们可以使用 常引用来避免对象切片,因为常引用不会创建对象的拷贝,因此派生类的完整对象信息得以保留。
void process(const Base& baseObj) { // 使用常引用传递baseObj.show(); // 如果传递的是 Derived 对象,将调用 Derived::show()
}
4. 传递类对象给函数
#include <iostream>class MyClass {
private:int* data;public:// 构造函数MyClass(int value) {data = new int(value); // 动态分配内存}// 默认拷贝构造函数(浅拷贝)// MyClass(const MyClass& other) = default; // 隐式生成// 销毁对象时释放内存~MyClass() {delete data; // 释放内存}void print() const {std::cout << "Value: " << *data << std::endl;}
};// 按值传递,调用拷贝构造函数
void process(MyClass obj) {obj.print();
}int main() {MyClass obj1(10);process(obj1); // 这里调用默认的拷贝构造函数,发生浅拷贝// 当 process 函数结束时,obj 的析构函数被调用,释放了 data 指针指向的内存// 当 main 函数结束时,obj1 的析构函数再次释放 data 指向的内存,会导致未定义行为return 0;
}
具体来说,这个代码会导致 双重删除(double free)的问题,这是因为在拷贝构造函数(默认浅拷贝)中没有正确处理动态分配的内存。
5. 提高函数的泛用性
在C++模板编程中,常引用使得函数可以接受任何类型的参数,而不需要实际拷贝对象,这不仅提高了代码的泛用性,还能提升性能。
#include <iostream>
#include <string>template <typename T>
void printElement(const T& elem) { // 常引用使得函数可以处理各种类型std::cout << elem << std::endl;
}int main() {int num = 10;std::string text = "Hello";printElement(num); // 传递 int 类型printElement(text); // 传递 std::string 类型return 0;
}
1. 左值和右值的区别
- 左值(lvalue):可以取地址的对象,比如变量、数组元素等,通常表示内存中存储的对象。
- 右值(rvalue):通常是临时对象,不能取地址,例如字面常量42、表达式结果a + b,以及匿名的临时对象std::string(“Hello”)。
2. 普通引用(T&)的局限性
普通引用(T&)只能绑定到左值,不能绑定到右值。例如:
template <typename T>
void printElement(T& elem) { // 非常引用std::cout << elem << std::endl;
}int main() {int num = 10;printElement(num); // 可以,num 是左值printElement(20); // 错误!字面值 20 是右值,不能绑定到非常引用 T&return 0;
}
编译器会报错,因为字面值 20 是右值,不能绑定到T& elem这样的非常引用上。
3. 常引用(const T&)相对就比较灵活性
常引用(const T&)既可以绑定到左值,也可以绑定到右值。这意味着函数可以接受任何类型的参数,包括字面量、临时对象等。例如:
template <typename T>
void printElement(const T& elem) { // 常引用std::cout << elem << std::endl;
}int main() {int num = 10;printElement(num); // 可以,num 是左值printElement(20); // 可以,20 是右值return 0;
}
在这个例子中,printElement(const T& elem)能接受变量num(左值)和字面量20(右值)作为参数,因此更加灵活。
110. 说说熵编码的原理
熵编码是一种无损数据压缩方法,主要用于数据压缩和信源编码。它通过对数据的统计特性进行编码,尽量减少信息冗余,提高压缩效率。在视频和图像压缩中,熵编码是一个重要步骤,用于对量化后的数据进行进一步的压缩。熵编码的原理基于信息论中的熵概念。
熵编码的基本原理
1. 霍夫曼编码(Huffman Coding)
哈夫曼编码的步骤
霍夫曼编码是一种变长编码方法,能够有效减少平均编码长度,从而达到压缩的目的。
2. 算术编码(Arithmetic Coding)
算术编码是图像压缩的主要算法之一。 是一种无损数据压缩方法,也是一种熵编码的方法。和其它熵编码方法不同的地方在于,其他的熵编码方法通常是把输入的消息分割为符号,然后对每个符号进行编码,而算术编码是直接把整个输入的消息编码为一个数,一个满足(0.0 ≤ n < 1.0)的小数n。
好,咱们说人话:
算术编码的核心思想是使用一个区间(通常是[0, 1))来表示整个符号序列。随着符号的逐个处理,这个区间会不断缩小,最终得到一个非常小的子区间,这个子区间的起始值或其区间中的任何值可以作为编码结果。
当我们完成最后一个编码后,最终得到的目标区间为:[0.1686, 0.16868)。我们可以在这个区间内任意选一个小数作为最终的编码小数。
算术编码的解码过程:
优点: 算术编码的压缩效率接近于信源的熵,适用于数据量大且符号分布复杂的情况。
缺点: 算术编码的实现相对复杂,并且存在累积误差,需要考虑精度和性能问题。
3. 游程长度编码(Run-Length Encoding, RLE)
之后我会持续更新,如果喜欢我的文章,请记得一键三连哦,点赞关注收藏,你的每一个赞每一份关注每一次收藏都将是我前进路上的无限动力 !!!↖(▔▽▔)↗感谢支持!