C+八股补充Record

发布于:2024-03-28 ⋅ 阅读:(19) ⋅ 点赞:(0)

一、C++

1.模板编程

C++提供了四种强制类型转换运算符,分别用于不同的转换场景,以增强代码的清晰度和安全性。这四种类型转换分别是:

1. static_cast

static_cast是最常用的类型转换运算符,用于非多态类型的转换。它可以在相关类型之间进行转换,例如整数与浮点数、字符与整数等。同时,它也能用于类的对象之间的转换,但前提是这些类之间存在明确的转换路径(如构造函数或类型转换函数)。

示例代码
int main() {
    double d = 9.78;
    // 将 double 转换为 int
    int n = static_cast<int>(d);
    cout << n << endl;
}

2. dynamic_cast

dynamic_cast用于处理多态类型的转换。它在运行时检查对象的类型信息,确保转换的安全性。如果转换不合法,它会返回空指针(对于指针类型)或抛出异常(对于引用类型)。dynamic_cast主要用于类的向下转换(如基类指针转换为派生类指针),确保转换的合法性。

示例代码
class Base { virtual void dummy() {} };
class Derived : public Base { int a; };

int main() {
    Base *b = new Derived;
    // 尝试将基类指针转换为派生类指针
    Derived *d = dynamic_cast<Derived*>(b);
    if (d != nullptr) cout << "转换成功" << endl;
}

3. const_cast

const_cast用于修改类型的const或volatile属性。它常用于去除对象的const性质,允许我们修改原本被声明为常量的变量。需要注意的是,使用const_cast去除const属性并修改值,如果原对象本身是一个常量,这样做是未定义行为。

示例代码
int main() {
    const int a = 10;
    // 去除变量的 const 属性
    int &b = const_cast<int&>(a);
    b = 20; // 如果a本身不是const,这是合法的
}

4. reinterpret_cast

reinterpret_cast提供了低层次的重新解释类型的能力,它可以将任何指针类型转换为任何其他的指针类型,甚至可以将指针转换为足够大的整数类型。使用reinterpret_cast需要格外小心,因为不正确的使用可能会导致程序崩溃或数据损坏。

示例代码
int main() {
    int *p = new int(10);
    // 将 int* 转换为 char* 类型
    char *ch = reinterpret_cast<char*>(p);
    // 将 char* 类型转回 int* 类型
    int *pi = reinterpret_cast<int*>(ch);
    cout << *pi << endl; // 输出 10
}

这四种类型转换提供了C++中强大的类型转换机制。它们各自有不同的用途和适用场景,选择合适的类型转换能够使代码更加安全、清晰。

2.四种强制类型转换

C++提供了四种强制类型转换运算符,分别用于不同的转换场景,以增强代码的清晰度和安全性。这四种类型转换分别是:

1. static_cast

static_cast是最常用的类型转换运算符,用于非多态类型的转换。它可以在相关类型之间进行转换,例如整数与浮点数、字符与整数等。同时,它也能用于类的对象之间的转换,但前提是这些类之间存在明确的转换路径(如构造函数或类型转换函数)。

示例代码
int main() {
    double d = 9.78;
    // 将 double 转换为 int
    int n = static_cast<int>(d);
    cout << n << endl;
}

2. dynamic_cast

dynamic_cast用于处理多态类型的转换。它在运行时检查对象的类型信息,确保转换的安全性。如果转换不合法,它会返回空指针(对于指针类型)或抛出异常(对于引用类型)。dynamic_cast主要用于类的向下转换(如基类指针转换为派生类指针),确保转换的合法性。

示例代码
class Base { virtual void dummy() {} };
class Derived : public Base { int a; };

int main() {
    Base *b = new Derived;
    // 尝试将基类指针转换为派生类指针
    Derived *d = dynamic_cast<Derived*>(b);
    if (d != nullptr) cout << "转换成功" << endl;
}

3. const_cast

const_cast用于修改类型的const或volatile属性。它常用于去除对象的const性质,允许我们修改原本被声明为常量的变量。需要注意的是,使用const_cast去除const属性并修改值,如果原对象本身是一个常量,这样做是未定义行为。

示例代码
int main() {
    const int a = 10;
    // 去除变量的 const 属性
    int &b = const_cast<int&>(a);
    b = 20; // 如果a本身不是const,这是合法的
}

4. reinterpret_cast

reinterpret_cast提供了低层次的重新解释类型的能力,它可以将任何指针类型转换为任何其他的指针类型,甚至可以将指针转换为足够大的整数类型。使用reinterpret_cast需要格外小心,因为不正确的使用可能会导致程序崩溃或数据损坏。

示例代码
int main() {
    int *p = new int(10);
    // 将 int* 转换为 char* 类型
    char *ch = reinterpret_cast<char*>(p);
    // 将 char* 类型转回 int* 类型
    int *pi = reinterpret_cast<int*>(ch);
    cout << *pi << endl; // 输出 10
}

这四种类型转换提供了C++中强大的类型转换机制。它们各自有不同的用途和适用场景,选择合适的类型转换能够使代码更加安全、清晰。

3.Linux内存进程分布

代码段:包括二进制可执行代码;
数据段:括已初始化的静态常量和全局变量;
BSS 段:包括未初始化的静态变量和全局变量;
堆段:包括动态分配的内存,从低地址开始向上增长,通常由开发人员手动分配和释放。堆上的内存需要手动管理,因此需要小心避免内存泄漏和悬挂指针问题。堆的大小通常比栈大,但分配和释放速度较慢;
文件映射段:包括动态库、共享内存等,它也用于动态内存分配,比如malloc()分配内存时,如果分配内存超过一定大小则会调用mmap() 系统调用在文件映射区域分配内存
栈段:包括局部变量和函数调用的上下文等。每当调用一个函数,系统会为其分配一个栈帧,包含局部变量和函数参数。当函数执行完毕,栈帧被弹出,释放相关内存。栈的分配和释放速度很快,但大小通常受限于系统。

4.C++没有内存回收,C++中分配内存要考虑到什么问题?

内存泄漏: 分配了内存但没有释放,导致程序运行时内存不断累积,最终可能耗尽可用内存。解决方法是在不需要使用内存时及时释放。

重复释放: 对同一块内存多次释放会导致程序崩溃或不可预测的行为。解决方法是避免重复释放同一块内存。

野指针: 指向已释放内存的指针称为野指针,访问野指针会导致程序崩溃或不可预测的行为。解决方法是在释放内存后将指针置为nullptr。

内存越界: 访问超出分配内存范围的内存会导致程序崩溃或不可预测的行为。解决方法是确保访问的内存范围不超出分配的范围。

内存对齐: 某些平台要求内存按照一定的字节对齐方式分配,不正确的内存对齐可能导致性能问题或程序崩溃。解决方法是根据平台要求正确对齐内存。

内存拷贝: 在使用动态内存分配时,需要注意拷贝指针指向的内存而不是指针本身,否则可能导致两个指针指向同一块内存,释放时会出错。

内存分配效率: 频繁的内存分配和释放会影响程序性能,可以通过对象池、内存池等方式提高内存分配效率。

异常安全: 在分配内存时要考虑异常安全,即当内存分配失败时程序能够正常处理,避免资源泄漏。

补:C++11提供了智能指针来进行管理内存,可以一定程度上避免内存泄漏问题。

5.避免内存泄漏的方法有哪些?

手动释放内存: 在动态分配内存后,确保在不再需要使用时及时释放。使用delete或delete[]释放对应的内存。

使用智能指针: 使用智能指针管理动态分配的内存,可以避免忘记释放内存的问题。C++11引入的std::shared_ptr和std::unique_ptr是两种常用的智能指针,它们会在对象不再需要时自动释放内存。

避免循环引用: 如果使用std::shared_ptr管理对象,要注意避免循环引用,即两个对象互相持有对方的shared_ptr,导致对象无法释放。

RAII(资源获取即初始化): 使用RAII管理资源,即在对象构造时分配资源,在对象析构时释放资源。通过RAII,可以确保资源在对象不再需要时被正确释放。

内存检测工具: 使用内存检测工具(如Valgrind、Dr.Memory等)来检测内存泄漏问题,及时发现并修复潜在的内存泄漏。

6.C++11提供的成员初始化列表有什么好处?

成员初始化列表是C++中初始化类成员变量的一种方式,它在类的构造函数声明之后使用冒号(:)和初始化列表来直接初始化成员变量,而不是在构造函数体内赋值。使用成员初始化列表可以提高效率,原因有以下几点:

1.直接初始化而非赋值:对于非内置类型的成员变量,使用成员初始化列表可以直接调用成员的构造函数来初始化,避免了先调用默认构造函数然后再赋值的过程。这样不仅减少了构造和赋值的步骤,还避免了不必要的临时对象的创建和销毁,从而提高效率。

2.对于const和引用成员变量是必须的:const成员变量和引用成员变量一旦被创建后就不能被赋值,只能在初始化列表中进行初始化。

3.初始化顺序明确:成员变量的初始化顺序与它们在类定义中的声明顺序一致,而不是初始化列表中的顺序。这样可以避免因初始化顺序不明确导致的错误。

参考

7.C++11提供的可变参数模板有什么好处?

C++11引入了可变参数模板(Variadic Templates),这是一种模板编程的强大功能,它允许函数或类模板接受可变数量的模板参数。毫无疑问,可变参数模板可以简化代码编写,将代码生成操作在编译时进行,减少了运行开销。其次,它提供了类型安全检查,减少运行时出现错误。C语言也提供了一个参数模板,封装在<stdargs.h>头文件中,但是其没有类型检查。例如,标准库中的std::tuple和std::function就是使用可变参数模板实现的,它们能够以类型安全的方式处理不确定数量的类型。
注:可以通多递归的方式取获取参数列表,而不能根据参数列表的size()然后遍历获取。

二、操作系统

1.进程间的通信方式有哪些?

1.管道(Pipe):管道是一种半双工的通信方式,用于具有亲缘关系的进程之间的通信。在Linux中,管道可以是匿名管道(通过pipe函数创建)或命名管道(通过mkfifo函数创建)。管道有一定的缓冲区,可以在一端写入数据,在另一端读取数据。

2.命名管道(Named Pipe):也称为FIFO(First In First Out),是一种特殊的文件类型,用于不具有亲缘关系的进程之间的通信。命名管道在文件系统中有一个路径名,进程可以通过文件I/O操作来进行通信。

3.消息队列(Message Queue):消息队列是一种消息传递机制,允许进程通过将消息发送到队列中来进行通信。消息队列通常具有特定的格式和优先级,可以实现进程间的异步通信。

4.信号量(Semaphore):信号量是一种用于控制对共享资源访问的机制。进程可以通过对信号量进行操作来实现对共享资源的互斥访问和同步。

5.共享内存(Shared Memory):共享内存是一种高效的进程间通信方式,允许多个进程共享同一块内存区域。通过共享内存,进程可以直接读写共享数据,而无需进行数据拷贝,因此可以实现较高的通信速度。

6.套接字(Socket):套接字是一种网络编程接口,可以用于不同主机上的进程间通信,也可以用于同一主机上的进程间通信。套接字通信可以在网络上进行,也可以在同一台计算机上进行本地通信。

2.线程间的通信方式有哪些?

1.共享内存:多个线程可以访问同一块共享内存区域,通过在共享内存中读写数据来进行通信。共享内存通常需要配合使用互斥锁等同步机制来确保数据的一致性和安全性。

2.互斥锁(Mutex):互斥锁用于保护共享资源,确保在同一时间只有一个线程可以访问共享资源。当一个线程获取了互斥锁后,其他线程需要等待该线程释放锁才能访问共享资源。

3.条件变量(Condition Variable):条件变量用于在多个线程之间进行条件等待和通知。线程可以在条件变量上等待某个条件成立,当条件成立时,另一个线程可以通过条件变量通知等待线程。

4.信号量(Semaphore):信号量是一种用于控制并发访问的同步原语。它可以用来限制同时访问共享资源的线程数量,也可以用于线程间的信号通知。

5.屏障(Barrier):屏障用于在多个线程之间进行同步,使得多个线程在达到某个点之前都会阻塞,直到所有线程都到达后才继续执行。

6.消息队列(Message Queue):消息队列可以用于线程之间的异步通信,一个线程可以向消息队列中发送消息,另一个线程可以从队列中接收消息。

7.线程间的数据传递:线程可以通过共享的全局变量或者函数参数来进行数据传递。但需要注意线程安全性,避免出现数据竞争等问题。

本文含有隐藏内容,请 开通VIP 后查看