一、C++11的新特性都有哪些?
1.1 自动类型推断 (auto)
auto
关键字允许编译器自动推断变量的类型,从而简化代码的书写。
auto num = 5; // int
auto pi = 3.14; // double
auto str = "Hello"; // const char*
1.2 范围 for 循环
范围 for 循环可以简化容器(如数组和 STL 容器)的遍历。
std::vector<int> vec = {1, 2, 3, 4, 5};
for (auto& i : vec) {
std::cout << i << " "; // 输出 1 2 3 4 5
}
1.3 Lambda 表达式和函数闭包
Lambda 表达式提供了一种简洁的方式来定义匿名函数,并允许捕获外部变量。
int x = 10;
auto lambda = [x](int y) { return x + y; };
std::cout << lambda(5); // 输出 15
1.4 右值引用和移动语义
右值引用允许对临时对象进行高效的资源管理,移动语义使得资源的转移比复制更高效。
#include <utility>
class MyClass {
public:
MyClass() { /* 资源分配 */ }
MyClass(MyClass&& other) { /* 资源转移 */ }
};
MyClass createMyClass() {
return MyClass(); // 返回一个右值
}
MyClass obj = createMyClass(); // 通过移动构造
1.5 可变参数模板
可变参数模板允许函数接受任意数量的参数。
template<typename... Args>
void printAll(Args... args) {
(std::cout << ... << args) << std::endl; // C++17 折叠表达式
}
printAll(1, 2, 3.5, "Hello"); // 输出: 1 2 3.5 Hello
1.6 初始化列表
初始化列表提供了一种更方便的方式来初始化容器和自定义类型。
std::vector<int> vec = {1, 2, 3, 4}; // 列表初始化
std::array<int, 3> arr = {1, 2, 3}; // std::array 初始化
1.7 强类型枚举
强类型枚举(enum class
)提供了更强的类型安全性和作用域。
enum class Color { Red, Green, Blue };
Color c = Color::Red;
// c = 1; // 错误,不能隐式转换
1.8 智能指针如 std::unique_ptr
和 std::shared_ptr
智能指针帮助管理动态分配的内存,避免内存泄漏。
#include <memory>
std::unique_ptr<int> p1 = std::make_unique<int>(5); // 独占型智能指针
std::shared_ptr<int> p2 = std::make_shared<int>(10); // 共享型智能指针
1.9 空指针关键字 (nullptr)
nullptr
是一个类型安全的空指针常量,取代了 NULL
。
int* p = nullptr; // p 是一个空指针
1.10 线程库支持
C++11 引入了 <thread>
头文件,使得多线程编程变得更加简单。
#include <thread>
void threadFunction() {
std::cout << "Hello from thread!" << std::endl;
}
std::thread t(threadFunction);
t.join(); // 等待线程结束
1.11 新容器如 std::array
和 std::unordered_map
C++11 引入了一些新的标准容器。
std::array
是一个固定大小的数组,可以在编译时确定大小。std::unordered_map
是一个基于哈希表的关联容器,提供常量时间复杂度的插入和查找。
#include <array>
#include <unordered_map>
std::array<int, 5> arr = {1, 2, 3, 4, 5};
std::unordered_map<std::string, int> umap = {{"one", 1}, {"two", 2}};
这些特性极大地增强了 C++ 的功能和可用性,使得编写现代 C++ 代码更加简洁、易于维护和高效。
二、了解什么设计模式?单例了解吗?
单例模式是一种设计模式,它确保一个类只有一个实例,并提供一个全局访问点来获取这个实
例。
在C++中实现单例模式通常需要以下步骤:
私有化构造函数和析构函数:这样可以防止外部代码直接创建或销毁该类的实例。
提供一个私有的静态指针变量:该指针用于存储唯一实例的地址。
提供一个公共的静态方法:这个方法用于返回唯一实例,并确保实例在第一次调用时被创建。
#include <iostream>
#include <mutex>
class Singleton {
private:
// 私有构造函数
Singleton() {
std::cout << "Singleton instance created!" << std::endl;
}
// 私有析构函数
~Singleton() {
std::cout << "Singleton instance destroyed!" << std::endl;
}
// 私有静态指针,指向唯一实例
static Singleton* instance;
static std::mutex mutex_; // 线程安全的互斥锁
public:
// 禁止拷贝构造和赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
// 公共静态方法获取唯一实例
static Singleton* getInstance() {
if (instance == nullptr) {
std::lock_guard<std::mutex> lock(mutex_); // 确保线程安全
if (instance == nullptr) {
instance = new Singleton();
}
}
return instance;
}
};
// 初始化静态指针
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mutex_;
int main() {
Singleton* singleton1 = Singleton::getInstance();
Singleton* singleton2 = Singleton::getInstance();
// 验证两者是同一个实例
if (singleton1 == singleton2) {
std::cout << "Both are the same instance." << std::endl;
}
return 0;
}
三、TCP和UDP的区别?
3.1 连接
-
TCP(传输控制协议):
- TCP 是面向连接的协议,这意味着在实际数据传输之前,必须先建立一个连接。这是通过三次握手(Three-Way Handshake)完成的,确保双方都准备好进行通信。
- 三次握手的过程如下:
- 客户端发送一个 SYN(同步)包到服务器。
- 服务器回应一个 SYN-ACK(同步-确认)包。
- 客户端再发送一个 ACK(确认)包来确认连接建立。
-
UDP(用户数据报协议):
- UDP 是无连接的协议,发送方和接收方之间不需要建立连接。数据可以立即发送,适合需要快速传输且对连接不敏感的应用。
3.2 可靠性
-
TCP:
- TCP 提供可靠的数据传输,确保数据的完整性和按顺序到达。它使用序列号来跟踪每个字节的发送和接收,采用重传机制来处理丢失的数据包,并通过校验和来检测数据错误。
-
UDP:
- UDP 不提供可靠性保证,数据包可能会丢失、重复或乱序到达。由于没有重传机制和流控制,UDP 适用于对实时性要求高的应用,比如视频会议、在线游戏等。
3.3 速度
-
TCP:
- 因为 TCP 需要进行连接建立、数据确认、错误检测和重传等操作,所以相对较慢。它适合需要高可靠性的数据传输,像文件传输、电子邮件等。
-
UDP:
- UDP 传输速度较快,因为它没有连接建立过程和确认机制,适用于速度要求高且可以容忍数据丢失的场景,例如 DNS 查询、实时视频和音频传输。
3.4 数据流
-
TCP:
- TCP 是面向字节流的协议,它将数据视为一个连续的字节流。应用程序发送的数据会被分割成多个数据包,接收方再将这些数据包重新组装成完整的数据流。
-
UDP:
- UDP 是面向消息的协议,每个数据包(称为数据报)都是独立的。数据报的大小有限,UDP 不会将多个数据报组合成一个流,需要应用程序自己处理消息的边界。
四、左值和右值引用?
左值引用是对可寻址的、可重复使用的对象(左值)的引用。它使用传统的单个&符号。
右值引用是对临时对象(右值)的引用,使用双&&符号。右值引用允许实现移动语义和完美转发,它可以将资源从一个(临时的)对象转移到另一个对象,提高效率,避免不必要的复制。
五、能说说static关键字吗?
5.1 static关键字有几个用途:
1. 当在类的数据成员前使用 static
时,表示该成员属于类本身,而不是任何特定的实例。所有实例共享这一成员。
class MyClass {
public:
static int count; // 静态数据成员
MyClass() {
count++; // 每创建一个实例,count 增加
}
};
// 静态成员定义和初始化
int MyClass::count = 0;
int main() {
MyClass obj1;
MyClass obj2;
std::cout << MyClass::count; // 输出 2,表明创建了两个实例
return 0;
}
2.当在类的成员函数前使用 static
时,可以在不创建类实例的情况下直接调用该函数。这通常用于那些不依赖于类的实例的功能。
class MyClass {
public:
static void display() {
std::cout << "Hello from static function!" << std::endl;
}
};
int main() {
MyClass::display(); // 直接调用静态成员函数
return 0;
}
3.在函数内部使用static定义局部变量,使得该变量的值在函数调用间持久存在,而不是每次调
用时都重新创建。
#include <iostream>
void counter() {
static int count = 0; // 静态局部变量
count++;
std::cout << "Count: " << count << std::endl; // 每次调用显示当前计数
}
int main() {
counter(); // 输出 Count: 1
counter(); // 输出 Count: 2
counter(); // 输出 Count: 3
return 0;
}
4.在文件、函数或区域前使用static,可以限制该变量或函数的链接可见性,令其只在定义它的
文件或作用域内部可见。
// 文件 A.cpp
static void helper() {
std::cout << "This is a static function." << std::endl;
}
void publicFunction() {
helper(); // 可以调用
}
// 文件 B.cpp
// void anotherFunction() {
// helper(); // 错误,无法访问 A.cpp 中的 static 函数
// }
2. static
关键字在 C++ 中具有多种用途:
用于定义类的静态数据成员和静态成员函数。
用于保持局部变量在函数调用之间的状态。
用于限制变量和函数的可见性,确保它们仅在定义它们的文件或作用域内可见。
六、进程间通信的方式有哪些?线程间的数据交互?
6.1 进程间通信方式包括:
1.管道(Pipes)
2.命名管道(FIFO)
3.信号(Signals】
4.消息队列(Message Queues)
5.共享内存(Shared Memory)
6.信号量(Semaphores)
7.套接字(Sockets)】
8.内存映射文件(Memory-mapped files)
6.2 线程间的数据交互可以通过:
1.锁(如互斥锁Mutex)
2.条件变量(Condition Variables)
3.信号量(Semaphores)
4.线程局部存储(Thread-local Storage,TLS)
5.全局变量(通过使用同步机制以避免并发访问问题)
七、用过什么容器,map和unordered_map了解吗?vector的底层原理是什么?vector的扩容机制了解多少?map的底层原理能说说吗?
7.1 std::map
- 底层实现:
std::map
是基于红黑树实现的,这是一种自平衡的二叉搜索树。 - 特性:
- 元素根据键值自动排序。
- 插入、删除和查找操作的时间复杂度为 O(log n)。
7.2 std::unordered_map
- 底层实现:
std::unordered_map
采用哈希表作为底层实现。 - 特性:
- 元素不会自动排序,存储顺序取决于哈希值。
- 平均情况下,插入、删除和查找操作的时间复杂度为 O(1),但在最坏情况下(如哈希冲突严重时)可能会达到 O(n)。
7.3 std::vector
1. 底层原理
std::vector
是基于动态数组实现的,允许随机访问。- 它在连续的内存空间中存储元素,这使得访问速度非常快(O(1) 时间复杂度)。
2. 扩容机制
- 当向
vector
添加元素超过当前容量时,会创建一个更大的动态数组(通常是当前容量的两倍),并将现有元素复制到新数组中,然后释放旧数组的内存。这种扩容策略有助于减少频繁内存分配的开销,从而提高性能。
7.4 std::map
的底层原理
std::map
是基于红黑树实现的,自平衡的特性帮助确保在最坏情况下也能保持 O(log n) 的时间复杂度。- 红黑树的特点包括:
- 每个节点都有一个颜色(红色或黑色),并遵循特定规则以确保树的高度保持在对数级别。
- 通过键值对自动排序,保证了数据结构的稳定性和高效操作。
八、std::move??
std::move
是 C++11 中引入的一个标准库函数,其主要作用是将其参数转换为右值引用。这样做可以让程序利用移动语义,以高效地转移资源而不进行不必要的复制。
九、C++转型操作符?
9.1 static_cast
- 用途:用于基本数据类型转换和安全的向上转型。
- 特点:
- 可以转换内置类型(如
int
到float
)。 - 支持向上转型,即将派生类的对象或指针转换为基类。
- 不能进行不安全的向下转型,如果需要向下转型,则需要使用
dynamic_cast
。
- 可以转换内置类型(如
9.2 dynamic_cast
- 用途:用于类层次结构中的安全向下转型及运行时类型检查。
- 特点:
- 需要 RTTI(运行时类型信息)支持。
- 当进行向下转型时,可以确保转换的安全性。如果转换不成功,
dynamic_cast
会返回nullptr
(对于指针类型)或抛出异常(对于引用类型)。 - 常用于多态类,确保在运行时检查对象的真实类型。
9.3 const_cast
- 用途:用于移除对象的
const
或volatile
属性。 - 特点:
- 允许对常量对象进行修改(需谨慎使用,因为这可能导致未定义行为)。
- 主要用于在需要非常量引用或指针的情况下,如与某些 API 交互时。
9.4 reinterpret_cast
- 用途:用于低级别的类型转换,重新解释底层位模式。
- 特点:
- 可以将一个指针转换为任何其他类型的指针。
- 这种转换是非常不安全的,因为它不进行任何类型检查,可能导致未定义行为。
- 通常用于特定的低级编程场景,如系统编程和硬件交互。
十、智能指针了解吗?weak_ptr?
智能指针是 C++11 引入的一个重要概念,主要用于管理动态分配的内存,避免手动管理内存带来的问题。std::weak_ptr
是智能指针中的一种,专门用于解决循环引用的问题
10.1 std::weak_ptr
详细说明
-
定义:
std::weak_ptr
是一种智能指针,用于观察由std::shared_ptr
管理的对象。它并不拥有该对象,因此不会增加对象的引用计数。 -
目的:
主要用于解决循环引用问题。例如,当两个对象相互持有shared_ptr
,如果使用shared_ptr
,这会导致引用计数永远不为零,从而造成内存泄漏。使用weak_ptr
可以避免这种情况,因为它不会影响引用计数。 -
使用方式:
当需要访问weak_ptr
指向的对象时,可以调用lock()
方法,该方法会尝试返回一个shared_ptr
(1)如果原始的 shared_ptr
仍然存在,lock()
将返回一个有效的 shared_ptr
,并增加引用计数。
(2)如果原始的 shared_ptr
已经被销毁,lock()
将返回一个空的 shared_ptr
。
10.2 示例代码:
#include <iostream>
#include <memory>
class Node {
public:
Node(int value) : value(value) {
std::cout << "Node " << value << " created.\n";
}
~Node() {
std::cout << "Node " << value << " destroyed.\n";
}
int value;
std::shared_ptr<Node> next;
};
int main() {
std::shared_ptr<Node> node1 = std::make_shared<Node>(1);
std::shared_ptr<Node> node2 = std::make_shared<Node>(2);
// 产生循环引用
node1->next = node2;
node2->next = node1;
// 使用 weak_ptr 解决循环引用
std::weak_ptr<Node> weakNode1 = node1;
std::weak_ptr<Node> weakNode2 = node2;
// 检查 weak_ptr 是否有效
if (auto shared1 = weakNode1.lock()) {
std::cout << "Weak pointer is valid, value: " << shared1->value << "\n";
}
if (auto shared2 = weakNode2.lock()) {
std::cout << "Weak pointer is valid, value: " << shared2->value << "\n";
}
// 让 node1 和 node2 超出作用域
node1.reset();
node2.reset(); // 这时 node1 和 node2 被销毁
// 检查 weak_ptr 是否有效
if (auto shared1 = weakNode1.lock()) {
std::cout << "Weak pointer is valid, value: " << shared1->value << "\n";
} else {
std::cout << "Weak pointer is no longer valid.\n";
}
return 0;
}