开发面经——秋招

快手客户端——安卓/ios

进程/线程

进程、线程区别

  • 进程有独立地址空间,创建销毁开销大,进程切换需要保存恢复整个进程的状态,开销大;线程共享进程的地址空间,创建销毁开销小,进程切换只需要保存恢复整少量线程上下文,开销小。
  • 进程同步需要特殊机制,管道,消息队列等;线程共享进程地址空间,直接共享数据
  • 进程崩溃不会直接影响其它进程,线程崩毁可能影响整个进程

进程地址怎样隔离?

        每个进程有独立的虚拟地址空间,通过映射机制将虚拟地址映射到不同的物理地址。

虚拟地址空间作用

  • 内存扩展:每个进程都可以使用比实际内存更大的内存,通过swap实现
  • 内存保护:每个进程有独立的虚拟地址空间,互不干扰,无法访问其它进程的虚拟地址空间,

进程通信

线程独有资源

  • 栈空间,用于函数调用,保存函数调用信息,如局部变量、返回地址
  • 线程局部变量:thread_local
  • 线程切换时,也会保存一些寄存器的状态

进程调度算法

进程的状态

        new(创建) ready running blocked(阻塞) terminated

        new:刚刚创建,还未开始执行

        ready:线程已经初始化完成,准备好资源,等待获取cpu后就可以执行

什么时候阻塞

        等待IO,等待资源,等待信号,等待系统调用完成

多线程开发要注意的问题

  • 注意资源互斥,使用互斥锁
  • 注意死锁
  • 数据同步问题,使用条件变量等
  • 可使用线程池复用线程,减少线程的创建和销毁
  • 确保正确处理异常,避免导致其它线程崩溃
  • 尽量减少锁的使用范围(减少临界区大小),提高性能
  • 控制线程数量,避免大量线程竞争cpu,导致上下文切换消耗过大

死锁(参考场景面经)

死锁什么时候发生

怎样解决死锁

写死锁代码

        unique_lock怎样解锁:ul.unlock();

        线程创建不会

内核态和用户态

        区别

        为什么要有内核态

程序的分段

哪些段

堆栈区别

为什么栈比堆快?

什么是栈溢出?

什么情况栈溢出?

导致什么后果?

动态申请空间方式

野指针

static什么作用,  存放在哪里?  

http://t.csdnimg.cn/13LiM

指针和引用区别

  • 指针是一个变量,存储地址;引用跟原来的变量实质上是同一个东西,是原变量的别名
  • 指针可以有多级,引用只有一级
  • 指针可以为空,引用不能为NULL,且在定义时必须初始化
  • 指针可以改变指向,而引用在初始化之后不可再改变
  • sizeof指针得到的是本指针的大小,sizeof引用得到的是引用所指向变量的大小
  • 当把指针作为参数进行传递时,是拷贝传参,和引用传参不同。拷贝的是指针,形参和实参指向的地址相同,但不是同一个变量

面向对象

封装继承多态的理解

封装

  • 对事物的抽象,隐藏内部细节,外界通过接口来使用类。
  • 可以保护数据,避免被外部修改,增强安全性。
  • 将相关的数据和操作封装,使代码结构清晰,易于理解和维护

继承

  • 代码复用:子类复用父类的属性和方法,
  • 且可以修改和扩展父类的功能
  • 建立类层次结构,使代码结构清晰

多态

  • 可根据对象的实际类型,灵活的调用对应的方法
  • 方便扩展新增子类无需修改现有代码,只需要子类实现响应的虚函数

继承的作用?

数据结构

链表数组区别

哈希表

哈希表底层怎样实现?

哈希冲突解决?

元素哈希冲突,怎样向后寻找位置?

设计模式

1 设计模式的原则?(不会)

单一职责:功能明确、单一(高内聚,低耦合:内部功能单一且集中,不同模块间的依赖尽可能弱)

开闭原则:对扩展开放,对修改关闭(做好封装,隐藏内部实现细节,开发足够多的接口,外部代码就能通过接口去扩展功能不需要入侵到类的内部

里氏替换:子类能够完全替换父类,不会改变父类的行为 

接口隔离:只依赖需要的接口

依赖倒置:父类不依赖于子类,上层的实现不依赖于底层的实现

组合优于继承:组合更加灵活,耦合度低;继承耦合度高

2 工厂模式(3种),作用,什么时候使用,优缺点?

单例模式实现

        c11静态局部变量,好像不太满意

算法:跳台阶

紫龙游戏一面

口述:

反转链表,递归反转链表

栈溢出

原因:

        递归过深,大局部变量

后果:

  • 程序崩溃
  • 覆盖了相邻区域内存的数据,导致数据损坏

解决:

  • 大变量动态内存分配
  • 控制递归深度
  • 增加栈空间

递归优化:

        尾递归优化:有的编译器不支持,c++标准不要求尾递归优化。用户可手动将递归转为迭代。

        尾递归:函数最后一个动作是递归,可以重用当前的堆栈帧,不用重新创建,显著减少了堆栈的使用。

new

vector在哪分配内存:堆

new,malloc区别

new重载

#include <iostream>
#include <new>  // 包含标准异常类

// 重载单个对象的new操作符
void* operator new(size_t size) {
    std::cout << "Custom new called, size: " << size << " bytes\n";
    void* ptr = malloc(size);  // 使用malloc进行内存分配
    if (!ptr) {
        throw std::bad_alloc();  // 如果分配失败,抛出异常
    }
    return ptr;
}

// 重载单个对象的delete操作符
void operator delete(void* ptr) noexcept {
    std::cout << "Custom delete called\n";
    free(ptr);  // 使用free释放内存
}

// 重载对象数组的new操作符
void* operator new[](size_t size) {
    std::cout << "Custom new[] called, size: " << size << " bytes\n";
    void* ptr = malloc(size);
    if (!ptr) {
        throw std::bad_alloc();
    }
    return ptr;
}

// 重载对象数组的delete操作符
void operator delete[](void* ptr) noexcept {
    std::cout << "Custom delete[] called\n";
    free(ptr);
}

class MyClass {
public:
    MyClass() {
        std::cout << "MyClass constructor called\n";
    }
    ~MyClass() {
        std::cout << "MyClass destructor called\n";
    }
};

int main() {
    MyClass* obj = new MyClass();
    delete obj;
    MyClass* arr = new MyClass[5];
    delete[] arr;
    return 0;
}

运行结果: 

Custom new called, size: 1 bytes
MyClass constructor called
MyClass destructor called
Custom delete called
Custom new[] called, size: 13 bytes
MyClass constructor called
MyClass constructor called
MyClass constructor called
MyClass constructor called
MyClass constructor called
MyClass destructor called
MyClass destructor called
MyClass destructor called
MyClass destructor called
MyClass destructor called
Custom delete[] called

  重载:

void* operator new(size_t size){}

new调用malloc分配空间,返回指针,然后编译器自动调用构造函数

分配失败抛出bad_alloc异常

Tcp头部字段  

说了ip地址,不对

第一次握手要设置的标志位:

        SYN,初始序列号

Nat

Network Address Translation,网络地址转换。私有IP地址和公有IP地址之间转换。

帮助解决IPv4地址空间不足的问题。

1 锁用过哪些

2 信号量机制

sem_t

int sem_wait(sem_t *sem);
        - 对信号量加锁(多线程并发访问),调用一次对信号量的值-1;如果sem值为0,就阻塞
 int sem_post(sem_t *sem);
        - 对信号量解锁,调用一次对信号量的值+1

注意:sem_wait()要在上锁之前执行,如果阻塞了,就放弃CPU,让消费者执行。如果在上锁之后执行sem_wait()并阻塞,那么消费者无法获取锁,就形成了死锁

示例:

 
void * producer(void * arg){
    //不断创建新的节点,添加到链表中
    while(1){
        sem_wait(&psem);    //消耗空间
        pthread_mutex_lock(&mutex);
 
        struct Node * newNode = (struct Node *)malloc(sizeof(struct Node));
        newNode->num = rand() % 100;
        newNode->next = head;
        head = newNode;
        printf("add node, num : %d, tid : %ld\n",newNode->num,pthread_self());
        
        pthread_mutex_unlock(&mutex);
        sem_post(&csem);    //生产产品

        usleep(1000);
    }
    return NULL;
}
 

3 用户态的锁

有没有实现过用户态的锁?没有

C++中用户态的锁(获取失败不会导致线程阻塞):

自旋锁

std::mutex,std::shared_mutex:本身不是用户态的锁,但是提供了try_lock()尝试获取锁,如果获取失败,不会阻塞线程,而是执行其它操作。

std::atomic

std::shared_mutex示例

#include <iostream>
#include <shared_mutex>
#include <thread>

class SharedData {
public:
    int data;
    std::shared_mutex mutex;    

    SharedData() : data(0) {}
};

void readData(SharedData& shared) {
    // 获取共享读锁
    std::shared_lock<std::shared_mutex> lock(shared.mutex);
    std::cout << "Reading data: " << shared.data << std::endl;
}

void writeData(SharedData& shared, int newData) {
    // 获取独占写锁
    std::unique_lock<std::shared_mutex> lock(shared.mutex);
    shared.data = newData;
    std::cout << "Writing data: " << shared.data << std::endl;
}

int main() {
    SharedData shared;

    // 创建多个读线程
    std::thread reader1([&shared]() { readData(shared); });
    std::thread reader2([&shared]() { readData(shared); });

    // 创建一个写线程
    std::thread writer([&shared]() { writeData(shared, 42); });

    reader1.join();
    reader2.join();
    writer.join();

    return 0;
}

内核态的锁:

  • pthread_mutex_t
  • sem_t  信号量  sem_wait()上锁
  • rwlock 
  • spinlock_t 内核态自旋锁

4 内核态的锁有什么缺点 

用户态的锁运行在用户空间,通常由高级语言提供,如std::mutex,不需要操作做系统内核的支持,性能开销较小可移植性更好(通常依赖与编程语言的标准库或跨平台的第三方库)。主要用于用户空间线程的同步

内核态的锁运行在内核空间运行,由内核提供支持开销较大可移植性更差(依赖于特定的操作系统或硬件架构)。适用于内核间的同步,如对硬件资源的管理、内核模块间的同步。

动态实现二维数组

1 指针

int** create2DArray(int rows, int cols) {
    int** array = new int*[rows]; // 创建指针数组

    for (int i = 0; i < rows; ++i) {
        array[i] = new int[cols];  // 为每一行分配一个数组
    }

    return array;
}

void delete2DArray(int** array, int rows) {
    for (int i = 0; i < rows; ++i) {
        delete[] array[i]; // 删除每一行的数组
    }
    delete[] array;       // 删除指针数组
}

2 vector

std::vector<std::vector<int>> create2DArray(int rows, int cols) {
    std::vector<std::vector<int>> array(rows, std::vector<int>(cols));
    return array;
}

原子操作

++i是原子操作吗?

不是,包含三个步骤:读取i;将读取的值+1;将结果写回i的存储位置

多线程情况下,++i是不安全的。

怎样实现原子操作

1 使用std::atomic<int> 

std::atomic可以实现无锁编程,开销比std::mutex小,推荐优先选择std::atomic.如果临界区较大,或需要锁定多个资源,使用std::mutex.

#include <atomic>
#include <thread>
#include<iostream>
using namespace std;

std::atomic<int> i(0); // 原子整数
//int i = 0;

void increment() {
    i++; // 原子递增操作
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);
    std::thread t3(increment);
    std::thread t4(increment);

    t1.join();
    t2.join();
    t3.join();
    t4.join();

    cout << i << endl;
}

2 std::mutex上锁


IO多路复用

Select,epoll区别

Linux怎样设置fd非阻塞        

       int flags = fcntl(fd, F_GETFL, 0);
       if (flags == -1) {
           // 处理错误,获取文件状态标志失败
       }
       flags |= O_NONBLOCK;
       if (fcntl(fd, F_SETFL, flags) == -1) {
           // 处理错误,设置文件状态标志失败
       }

非阻塞和阻塞应用场景

        阻塞:顺序执行任务,下面的操作需要上面的结果;用户界面编程,主线程阻塞等待用户输入

        非阻塞:服务端,需要处理大量连接;异步事件的处理;不用顺序执行任务

非阻塞+ ET

非阻塞模式,如果数据未准备好,直接返回,并设置错误码为EAGAINEWOULDBLOCK       

LT模式下,只要缓冲区还有未读取的数据,epoll就会不断返回通知。

ET模式下,只有在新数据到达时 才通知,减少了epoll_wait()系统调用)的调用次数,减少了上下文切换的次数,效率更高。

内存对齐 

规则

为什么要内存对齐

        方便cpu访问,减少cpu的访存次数,cpu一次读取的数据长度是固定的,尽量保证一次性读完一条数据

        可移植性,代码在不同架构上运行,可减少潜在的问题。

可使用#pragma pack控制内存对齐方式

定义声明

定义:分配内存

声明:不分配内存

初始化:赋初始值

memcpy,memmove,strcpy

strcpy():用于拷贝字符串,直到遇到'\0'为止只能用于字符串拷贝

memcpy,memmove可以用于其它类型的拷贝,

memcpy无法处理覆盖旧内存的场景(现在似乎可以处理内存覆盖的场景了),memmove可以。

哈希

map,unordered_map区别,底层

哈希冲突

1.拉链法

如果链表过长,可使用红黑树优化,时间由O(n)变为O(logn)

2 开放寻址法,寻找下一个空闲位置

线性探测:查找下一个连续的空槽

平方探查:探查的步长是x的二次方。如果散列函数为,发生冲突后,依次检查h(k) + pow(1,2),h(k) - pow(1,2),h(k) + pow(2,2),h(k) - pow(2,2)等桶。

双重散列:使用两个不同的哈希函数,第一个哈希函数决定初始位置,第二个哈希函数决定探查步长。

3 再哈希法(双重哈希法)

冲突后,使用第二个哈希函数计算新的位置

最短路路径

 bfs,适合无权图,时间O(V+E)

迪杰斯特拉:适合带权图

Floyd-Warshall算法:适用于带权重的有向图或无向图

A*:适用于带权重的有向图或无向图,是一种启发式搜索算法

强制类型转换

拷贝构造和移动构造区别

Static

Ratti??反射,泛型相关。

智能指针

指针和引用区别

浮点数怎样存储:IEEE754标准  :符号位 + 指数 + 尾数

紫龙二面

广度优先怎么实现

N皇后

带权最短路径,迪杰斯特拉

auto和dynamic_cast:

        auto:编译时实现,不涉及运行时检查

        dynamic_cast:运行时检查

怎么学C++

dynamic_cast失败:指针类型,返回空指针;引用类型,抛出std::bad_cast异常

static_cast失败:编译错误

场景题

static int i =0;
j = i++;
return j;

多线程循环,会不会出现某个值被跳过的情况? 

        会的,解决办法,std::atomic

循环20次,会不会出现大于20 的结果?

        不会

堆栈寄存器区别:

        寄存器速度最快,是cpu内部的小部分高速存储资源。由编译器和cpu管理,程序员无法控制。

程序启动之后,有几个堆和栈:

        一个进程一个堆,每个线程一个栈

i++内部详细实现:

        拷贝当当前值;

        i + 1;

        返回拷贝的值

atomic底层原理:

        依赖cpu提供的原子指令,实现不可分割的操作,原子读,原子写等。

        原子操作保证执行的时候不被中断

互斥锁自旋锁区别

阻塞时间较长,但阻塞概率较小,使用什么锁?

使用带有自旋等待的互斥锁。这种锁被占用时首先自旋等待多次尝试获取锁,如果失败,则阻塞线程。结合了互斥锁和自旋锁的优点,在锁竞争不激烈时减少上下文切换开销(不会立刻切换上下文),在锁持有时间较长时,避免cpu资源浪费(不会长时间等待)。

.NET中,存在这样的混合锁

float相关

内存池

怎样实现

怎样为struct分配空间内存池的空间?重载new

重载new 这一行为的专属名词?

三维矩阵,第四维作用

点成叉乘

实现读写锁

除了ucontext_t,协程还能使用什么实现?

Boost.Context: Boost库提供了一个Context库,可以用来实现协程的上下文切换。它是一个跨平台的解决方案,可以在不支持ucontext.h的系统上使用。

沐瞳一面

epoll ET LT 区别

Epoll怎样监测客户端连接断开?

events[i].events & (EPOLLRDHUP | EPOLLHUP | EPOLLERR

epoll_wait监测到 EPOLLRDHUP / EPOLLHUP / EPOLLERR,说明连接断开

检测到EPOLLIN,但是read()返回0,说明连接断开

Linux

客户端怎样判断服务端的端口是否访问?

telnet [服务端IP] [端口号]如果能够建立连接,则表明端口是开放的。

服务端某个端口无法被客户端访问,有什么可能

  • 服务端防火墙阻止对该端口的访问
  • 如果是云服务器,需要对安全组进行设置
  • 端口可能没有被listen
  • 端口被其它进程占用,且没有使用端口复用
  • 服务端未正常运行

怎样查看某个端口是否在运行?

lsof -i :[端口号] 可以检测某个端口是否被占用  lsof -i:22

读写锁互斥

项目中读写锁怎样实现?  对rwlock进行封装。 c++高标准有读写锁吗

死锁场景  ,只说一个

四次挥手  状态

Mls是什么?2msl作用?只说一个

为什么选择游戏服务端?

100个数据去重 :us

unordered_set,set区别

哈希冲突解决。多次遇到

工厂模式

Mysql删除表

删除表中数据

统计行数

like

开销:

        左模糊匹配,索引会失效,导致全表扫描。面试时,认为右模糊匹配也会导致索引失效。

        右模糊匹配可以使用索引

优化:

        避免使用左模糊匹配

特殊情况:

        表中只有两个列,都是索引列:id(主键),name(二级索引)

        select * from users where name = '%d'

        索引不会失效。会使用二级索引,进行全索引扫描,并且由于二级索引的叶子节点存放的是主键值,所以不用回表。

        如果表中所有字段都是索引,即使联合索引没有遵循最左匹配原则,也是会使用索引的,进行全索引扫描。

全索扫描一般比全表扫描更快,因为 索引的信息比较少,不需要读取所有数据。

explain

  1. id:查询中各部分的标识符,如果是一个子查询,子查询会有一个不同的id。

  2. select_type:查询的类型,例如SIMPLEPRIMARYSUBQUERYDERIVED等。

  3. table查询的表名

  4. partitions:表分区的信息,如果没有分区则为NULL

  5. type:这是最重要的字段之一,表示表的连接类型,例如ALLINDEXrangerefeq_refconst等。type字段的值从最好到最差依次是:systemconsteq_refreffulltextref_or_nullindex_mergeunique_subqueryindex_subqueryrangeindexALL

  6. possible_keys可能使用的索引

  7. key实际使用的索引

  8. key_len使用的索引的长度

  9. ref:与索引一起使用的列或常量。

  10. rows估计需要检查的行数

  11. filtered估计满足WHERE子句条件的行数的百分比

  12. Extra:额外的信息,例如Using indexUsing whereUsing temporaryUsing filesort

type字段:

  1. system:这是一个特殊的const类型,表示表中只有一条记录匹配,通常是因为表是系统表,只有一行数据。

  2. const:表示通过索引一次就能找到结果,通常用于主键或唯一索引的等值查询。因为只有一行数据,所以查询速度非常快。

  3. eq_ref:表示对于前面的表,每一个结果行都只能匹配到后面的表的一行记录。这通常发生在使用JOIN并且连接条件是主键或唯一索引时。

  4. ref:表示使用非唯一索引进行等值比较的查询。返回的行数取决于索引列的值有多少个不同的值。

  5. fulltext:表示使用全文索引进行的查询。

  6. ref_or_null:类似于ref,但是MySQL会额外搜索包含NULL值的行

  7. index_merge:表示查询使用了两个以上的索引,通过索引合并策略来找到结果。

  8. unique_subquery:表示子查询中使用了IN谓词,并且子查询返回的是唯一值

  9. index_subquery:表示子查询中使用了非唯一索引进行查找。

  10. range:表示使用索引的范围查询,如BETWEEN><等操作符。

  11. index:表示全索引扫描,即查询结果集是通过索引树进行查找的,不需要回表查询。

  12. ALLfull_scan:表示全表扫描,即查询结果集是通过扫描整个表来获取的,这是最慢的访问类型。

多乐游戏 参考面经

map[]和insert区别

[]:

  • 运算符
  • key存在,返回对该元素的引用,覆盖旧value
  • key不存在,插入key,value默认初始化。然后赋值为要求的值。

insert:

  • 函数
  • 插入元素
  • 如果key已经存在,不会覆盖原来的value
  • 返回一个pair,first指向插入或找到的元素,second表示是否插入成功

使用迭代器删除所有可以被5整除的数

#include <iostream>
#include <vector>

int main() {
    std::vector<int> nums = { 1, 5, 10, 11, 15, 20, 21 };
    for (auto it = nums.begin(); it != nums.end();) {
        if (*it % 5 == 0) {
            //erase()返回下一个有效的迭代器,必须立刻赋值给it。否则,it此时已经失效,会出现非法访问
            it = nums.erase(it);    
        }
        else {
            ++it;
        }
    }

    for (int num : nums) {
        std::cout << num << " ";
    }
    std::cout << std::endl;
    return 0;
}
#include <iostream>
#include <vector>
#include <algorithm>

int main() {
    // 初始化一个vector
    std::vector<int> vec = { 1, 5, 10, 15, 20, 25, 30, 35, 40, 45 };

    //remove_if把所有不满足条件的全都移动到前面,返回逻辑末尾迭代器,
    //指向所有不满足条件的下一个位置
    auto newEnd = std::remove_if(vec.begin(), vec.end(), [](int x) {
        return x % 5 == 0;
        });
    vec.erase(newEnd, vec.end());

    // 输出结果
    for (int num : vec) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    return 0;
}

 指针

char s[] = "asdf";

sizeof(s) = 5; //包含结束符

但是,当s作为参数传递,sizeof(s) = 8; 是指针的大小

strlen(s) = 4; //不包含结束符

当s作为参数传递,strlen(s) = 4; //不包含结束符

*s++; 先解引用,s再递增

ln 

在 Linux 系统中,ln 命令用于创建链接,这些链接可以是硬链接或软链接(符号链接)。链接允许你为文件或目录创建一个额外的名称,使得你可以从不同的位置访问同一个文件或目录。

tcp如何确认连接断开

系统:

  • 超时重传超过一定次数
  • tcp保活机制,多次探测没有响应

用户:

  • epoll_wait监测到 EPOLLRDHUP / EPOLLHUP / EPOLLERR,说明连接断开
  • 收到EPOLLIN,但read返回-1

事务

  • START TRANSACTION;
  • SAVEPOINT savepoint_name;   设置保存点
  • ROLLBACK TO SAVEPOINT savepoint_name;  回滚到保存点
  • RELEASE SAVEPOINT savepoint_name;释放保存点
  • ROLLBACK;
  • COMMIT;

可中断函数,可重入函数

可中断函数是指在执行过程中可以被外部事件(如信号)中断的函数

可重入函数是指可以在多个线程中安全调用的函数,即使这些线程同时执行也不会引起问题。可重入函数不会依赖于任何全局状态,也不会修改任何全局变量或静态变量。

 SIGPIPE

当一个进程尝试入一个没有读端的管道(pipe)或者 socket 时,系统会向该进程发送 SIGPIPE 信号。

接收到 SIGPIPE 信号的进程会终止,并生成核心转储(core dump),这是一种用于调试的二进制文件,包含了程序终止时的内存映像。

处理 SIGPIPE

  • 忽略信号 signal()sigaction()
  • 自定义信号处理函数
  • 使用 send() 函数的 MSG_NOSIGNAL 标志: 当使用 socket 进行数据发送时,可以在 send() 函数中使用 MSG_NOSIGNAL 标志。这样,当 socket 的读取端被关闭时,send() 调用会返回一个错误,而不是发送 SIGPIPE 信号

想在目录中创建一个文件,需要什么权限

  • 写:用于创建
  • 执行:有执行权限才能进入目录

多乐游戏一面

单例模式怎样实现?

map底层

cpu高速缓存

作用:减少cpu访问主存的次数,提高速度

原理:时间局部(被访问的数据,不久的将来也会被访问)和空间局部(一个数据被访问,附近的数据也可能被访问)

B+树优点,插入结点步骤

协程和线程区别?

协程库怎样测试?配置怎样?

hook怎样实现?

哪里有高并发,阻塞需求?还是说自己想当然的。

怎样将协程库接入别的项目,需要做大的改动吗?

raft项目有什么可以优化的地方? 拓扑变更

怎样理解protobuf协议

怎样根据序列化数据得到原来的消息?反序列化?

进程间通信

不同语言写的程序,怎样交互?

  • 定义好接口,可以通过HTTP,RPC等进行通信交互
  • 共享数据库
  • 共享内存
  • 消息队列

多态原理

空指针调用成员函数

#include<iostream>
using namespace std;

class Stu {
	int a;
public:
	void f1() {
		cout << "f1" << endl;
	}
	void f2() {
		cout << a << endl;
	}
};

int main() {
	Stu* p = nullptr;
	p->f1();		//编译器优化,OK;本身不合法
	p->f2();		//运行失败
}

拷贝传递指针


void getmemory(char * p)
{
//此处的修改会影响到实参吗?不会
//new返回一个地址,赋值给p。但p是拷贝传参,对此处p的修改不会影响实参
//*p = c  ,这样,直接修改p所指向的对象,可以影响到外面
 p = new char[100];
 strcpy(p , "hello world");
 return ;
}
int main(){
 char * p =NULL ;
 getmemory(p) ;
 printf("%s\n", p);
 return 0;
}
//传指针
void getmemory(char ** p)
{
 p = new char[100];
 strcpy(p , "hello world");
 return ;
}
int main(){
 char * p =NULL ;
 getmemory(&p) ;
 printf("%s\n", p);
 return 0;
}
//拷贝包含 结束符
char* strcpy(char* destination, const char* source);

字符串指针相关

#include<iostream>
using namespace std;

void func(const char str_arg[100]) { 
    cout << sizeof(str_arg) << endl;    //返回指针的大小
    cout << strlen(str_arg) << endl;    //返回字符串长度
}

int main(int argc, char* argv[]) {
    char str[] = "Hello";
    char* p = str;

    cout << sizeof(str) << endl;    //6  包含结束符
    cout << strlen(str) << endl;    //5  不包含结束符

    cout << sizeof(p) << endl;      //8

    func("test");                   //8  4

    cout << *p++ << endl;           //H  先解引用,再递增
    
    return 0;
}

算法题 麻将胡牌

麻将中,2个牌面相同的是对子,3个或者4个牌面相同的为刻子,3个相邻的同花色为顺子。现在规定一手牌中如果有一个对子,并且刻子和顺子的总数目为4,则为胡牌牌型。现在给定一个牌型,如何判断是否可以胡牌?

给定长度为14的整型数组,判断是否胡牌2 + 3*4 dd + (abc or aaa) * 4

范围 1~9之间代表数字1~9

形如 111 222 333 456 77 返回True

形如 111 223 345 67 999 返回True

形如 111 222 333 457 99 返回False

记录每个牌的数量,遍历所有排,每个牌有三个可能:组成对子,组成刻字,组成顺子。

分别对每一种进行判断、递归,记录当前的对子和刻字+顺子的数量,条件满足后,返回true 。

#include <iostream>
#include <vector>
#include <map>
#include <algorithm>
#include <climits> // 用于INT_MAX

// 递归函数,用于检查是否可以胡牌
bool is_agari_impl(std::map<int, int>& tehai_cnt, int rest_tiles) {
    // 如果剩余手牌数为0,说明已经胡牌
    if (rest_tiles == 0) {
        return true;
    }

    // 查找当前可以操作的最小牌值
    int min_tile = -1;
    int min_count = INT_MAX;
    for (auto& pair : tehai_cnt) {
        if (pair.second > 0) {
            if (pair.first < min_count) {
                min_tile = pair.first;
                min_count = pair.second;
            }
        }
    }

    // 尝试拆刻子(aaa)
    if (tehai_cnt[min_tile] >= 3) {
        tehai_cnt[min_tile] -= 3; // 减少3张
        if (is_agari_impl(tehai_cnt, rest_tiles - 3)) {
            return true;
        }
        tehai_cnt[min_tile] += 3; // 恢复状态
    }

    // 尝试拆雀头(对子,即dd)
    if (rest_tiles % 3 && tehai_cnt[min_tile] >= 2) {
        tehai_cnt[min_tile] -= 2; // 减少2张
        if (is_agari_impl(tehai_cnt, rest_tiles - 2)) {
            return true;
        }
        tehai_cnt[min_tile] += 2; // 恢复状态
    }

    // 尝试拆顺子(abc)
    if (min_tile < 27 && min_tile % 9 < 7 && tehai_cnt[min_tile + 1] > 0 && tehai_cnt[min_tile + 2] > 0) {
        tehai_cnt[min_tile] -= 1;
        tehai_cnt[min_tile + 1] -= 1;
        tehai_cnt[min_tile + 2] -= 1;
        if (is_agari_impl(tehai_cnt, rest_tiles - 3)) {
            return true;
        }
        tehai_cnt[min_tile + 2] += 1;
        tehai_cnt[min_tile + 1] += 1;
        tehai_cnt[min_tile] += 1;
    }

    // 如果以上步骤都不能胡牌,则返回false
    return false;
}

// 主函数,用于初始化牌组并调用递归函数
bool is_agari(const std::vector<int>& tehai) {
    // 如果牌组长度不是3的倍数加2,则直接返回false
    if (tehai.size() % 3 != 2) {
        return false;
    }

    std::map<int, int> tehai_cnt;
    for (int tile : tehai) {
        tehai_cnt[tile]++; // 统计每种牌的数量
    }

    // 调用递归函数检查是否可以胡牌
    return is_agari_impl(tehai_cnt, tehai.size());
}

int main() {
    std::vector<int> tehai = {1,1,1,2,3,4,2,3,4,5,6,8,9,9};
    // 检查是否可以胡牌,并输出结果
    if (is_agari(tehai)) {
        std::cout << "Agari!" << std::endl;
    } else {
        std::cout << "Not Agari!" << std::endl;
    }
    return 0;
}

多乐二面

for循环,时间复杂度

++i i++

多态

虚表

基类的虚表:

  • 存放基类的虚函数的地址

派生类的虚表:

  • 基类虚表的拷贝
  • 如果派生类重写基类虚函数,则用派生类中虚函数的地址替换拷贝的基类中虚函数的地址
  • 派生类自定义虚函数,按照声明顺序,放在派生类虚表的后面。

重载

函数名相同,参数的类型数量不同

extern C

C++内存分段

栈溢出

Linux中,用户程序栈空间多大:8MB

new和malloc区别

  • new失败抛出bad_alloc异常,可以被try捕获
  • malloc失败返回空指针

构造顺序

内存对齐

class A{
    int x;
    char* b;
    virtual f(){}
};

A类的大小:24B,因为有虚函数,所以对象有虚表指针,占用8B

如果f不是虚函数,函数本身不占用对象存储空间,函数存储在代码段,大小是16B

面试时没考虑到,直接说24B,以为函数是个指针,占用8B

为什么要内存对齐?

怎样控制内存对齐?

1 alignas关键字:指定变量内存对齐的要求

struct alignas(16) Stu{
    int i;
};

2 #pragma pack()

//控制内存对齐大小为4字节
#pragma pack(push, 4)

struct MyStruct {
    char a;
    int b;
};

//取消自定义的内存对齐
#pragma pack(pop)

3 在C++17中,new操作符可以与align_val_t一起使用来分配特定对齐的内存。例如: 

auto p = new (std::align_val_t{16}) int;

4 使用标准库函数 std::aligned_alloc:这个函数可以用来分配满足特定对齐要求的内存 

void* ptr = std::aligned_alloc(16, sizeof(int) * 10);

32位的代码迁移到64位,需要注意什么问题

  • 32位,int,long,指针都是32位,64位,long,指针都是64位
  • 内存对齐规则不同,结构体的大小发生变化
  • 第三方库需要确保有64位的版本,且兼容

内存泄漏,智能指针

内存碎片

  • 外部内存碎片:内存经过分配和释放,会留下一些不能使用的小内存块,导致外部内存碎片,而解决内存碎片的方式就是内存交换。

内存池

迭代器失效

迭代器遍历删除元素。在参考面经中,幸好看了面经

map: []和insert

协程库怎样体现性能大幅提升?

客户端send()发送数据,服务端recv()为空。什么原因?

  • send()后,数据仅仅是被拷贝到内核缓冲区:
    • 内核缓冲区满,发送失败
    • 如果数据过少,可能会等待其他数据一起发送(Nagle算法,小的数据包会被合并发送
  • 网络延迟,数据还未到达

抓过包吗?

安克创新c++ 一面

unity做的游戏,有什么难点?  震惊

协程调度器怎样实现的?

协程库什么架构?有什么不足的地方?

为什么做协程库?难点?收获

kv数据库的架构

高可用考虑了吗?

mysql事务

分布式系统怎样实现高可用?

数据库读写分离。还有呢?

为什么三次握手?四次挥手?

tcp状态有什么?

close_wait作用

close_wait和time_wait区别

我的问题:三次握手,用户调用connect?

四次挥手,用户close还是shutdown?什么区别?

静态库和动态库后缀?区别?

linux: 静态  .a  动态 .so

select,epoll区别。使用场景

怎样学习

渲染流程

死锁?

完美转发

避免多次包含头文件

迭代器失效场景:

unordered_map说的不对

ai辅助写代码

rpc

protobuf用过吗

安克go

为什么做分布式数据库?

基于raft的软件

什么情况会导致不一致的问题?

怎样保证最终一致?

为什么n/2+1?

怎么能使raft的收敛更快?

为什么做协程?

为什么比线程快?

什么情况下,协程没有优势?

怎样实现协程?

则哪有那个调度协程?

进程线程协程的关系

怎样限制进程使用的资源?

        ulimit

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值