字节跳动面试

1常用的语言,选择一种开始面试,我选的c++

2 const和define的区别

一:区别


(1)就起作用的阶段而言: #define是在编译的预处理阶段起作用,而const是在 编译、运行的时候起作用。
(2)就起作用的方式而言: #define只是简单的字符串替换,没有类型检查。而const有对应的数据类型,是要进行判断的,可以避免一些低级的错误。 
(3)就存储方式而言:#define只是进行展开,有多少地方使用,就替换多少次,它定义的宏常量在内存中有若干个备份;const定义的只读变量在程序运行过程中只有一份备份。
(4)从代码调试的方便程度而言: const常量可以进行调试的,define是不能进行调试的,因为在预编译阶段就已经替换掉了。

二:const优点


(1)const常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误。
(2)有些集成化的调试工具可以对const常量进行调试,但是不能对宏常量进行调试。
(3)const可节省空间,避免不必要的内存分配,提高效率

3 c++的内存分区

1、栈区(stack)— 由编译器自动分配释放 ,存放函数的参数名,局部变量的名等。其操作方式类似于数据结构中的栈。

2、堆区(heap)— 由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。

3、全局/静态存储区 —全局变量和局部静态变量的存储是放在一块的(在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分)。程序结束后由系统释放。

4、常量存储区 — 常量字符串就是放在这里的,正常情况不允许修改,程序结束后由系统释放 。

5、程序代码区 — 存放函数体的二进制代码。

const变量的内存位于C++的5大内存中的栈区或者静态存储区。在编译的时候,对于不试图通过内存来修改const变量值的,编译器统统将const变量存放在编译器内部产生的临时列表中,也就是所谓的符号表,该符号表与目标文件连接用的符号表是两个完全不同的东西。此临时符号表的作用就是提高效率,是编译器优化形成了,所以大家不必过多纠结const变量内存存放位置了,它的内存就是位于栈区或者静态存储区。

4 堆和栈的区别

1、申请方式不同:堆的话,程序员动态申请,new和malloc()申请的空间;栈,由编译器在需要的时候分配,在不需要的时候自动清除的变量的存储区。里面的变量通常是局部变量、函数参数等。

2、管理方式不同。堆,程序员自己管理,若没有free、delete释放,程序结束后由OS释放。栈,系统管理。

3、空间大小不同。栈是自高址向低地址扩展的结构,是一块连续内存区域,栈顶的地址和栈的最大容量是系统预先规定好的,当申请的空间超过栈的剩余空间时,将提示溢出。因此,用户能从栈获得的空间较小,通常为1M,也有2M的,可修改。

Linux下,可用命令:ulimit -s 查看栈大小 并重新设置大小。

堆是自低地址向高地址扩展的数据结构(它的生长方向与内存的生长方向相同),是不连续的内存区域。因为系统是用链表来存储空闲内存地址的,且链表的遍历方向是由低地址向高地址。由此可见,堆获得的空间较灵活,也较大。堆的大小受限于计算机系统中有效的虚拟内存。一般来讲在32位系统下,堆内存可以达到2.9G的大小。(除去1G的内核空间,几乎占满3G的用户空间)

4、申请后系统的响应:

栈:只要栈的空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。

堆:操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的free语句才能正确的释放本内存空间。另外,找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。

对于堆来讲,频繁的malloc/free势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈就不会存在这个问题。
5.碎片问题:

堆上频繁的new delete会产生内存碎片,栈上是连续的空间则不会有这个问题。

6)分配效率:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函 数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法,在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是 由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。
 

 

5 指针和引用的区别

1.指针和引用的定义和性质区别:

(1)指针:指针是一个变量,只不过这个变量存储的是一个地址,指向内存的一个存储单元;而引用跟原来的变量实质上是同一个东西,只不过是原变量的一个别名而已。如:

int a=1;int *p=&a;

int a=1;int &b=a;

上面定义了一个整形变量和一个指针变量p,该指针变量指向a的存储单元,即p的值是a存储单元的地址。

而下面2句定义了一个整形变量a和这个整形a的引用b,事实上a和b是同一个东西,在内存占有同一个存储单

元。

(2)引用不可以为空,当被创建的时候,必须初始化,而指针可以是空值,可以在任何时候被初始化。

(3)可以有const指针,但是没有const引用;

(4)指针可以有多级,但是引用只能是一级(int **p;合法 而 int &&a是不合法的)

(5)指针的值可以为空,但是引用的值不能为NULL,并且引用在定义的时候必须初始化;

(6)指针的值在初始化后可以改变,即指向其它的存储单元,而引用在进行初始化后就不会再改变了。

(7)”sizeof引用”得到的是所指向的变量(对象)的大小,而”sizeof指针”得到的是指针本身的大小;

(8)指针和引用的自增(++)运算意义不一样;

(9)如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄漏;
 

6 多态

什么是多态
顾名思义就是同一个事物在不同场景下的多种形态。
这里写图片描述

动态多态的条件:
●基类中必须包含虚函数,并且派生类中一定要对基类中的虚函数进行重写。
●通过基类对象的指针或者引用调用虚函数。

重写 :
(a)基类中将被重写的函数必须为虚函数(上面的检测用例已经证实过了)
(b)基类和派生类中虚函数的原型必须保持一致(返回值类型,函数名称以及参数列表),协变和析构函数(基类和派生类的析构函数是不一样的)除外
(c)访问限定符可以不同
那么问题又来了,什么是协变?
协变:基类(或者派生类)的虚函数返回基类(派生类)的指针(引用)
总结一道面试题:那些函数不能定义为虚函数?
经检验下面的几个函数都不能定义为虚函数:
1)友元函数,它不是类的成员函数
2)全局函数
3)静态成员函数,它没有this指针
3)构造函数,拷贝构造函数,以及赋值运算符重载(可以但是一般不建议作为虚函数)
派生类虚表:
1.先将基类的虚表中的内容拷贝一份
2.如果派生类对基类中的虚函数进行重写,使用派生类的虚函数替换相同偏移量位置的基类虚函数
3.如果派生类中新增加自己的虚函数,按照其在派生类中的声明次序,放在上述虚函数之后

7  设计模式,手写单例设计模式

 

8 算法题

给一数,输出这个数的下一个比他大的数,如1234下一个是1243,下一个是1324。

/*
* @Author: lenovouser
* @Date:   2020-07-27 17:45:00
* @Last Modified by:   lenovouser


* @Last Modified time: 2020-07-27 18:02:55


*/

#include <iostream>
#include <string>
#include <cstdlib>
#include <cmath>
#include<ctime>
#include<vector>
#include<queue>
#include <algorithm>
using namespace std;
void myprint(auto a)
{
    for(auto &b:a)
    {
        cout<<b;
    }
}
void myswap(auto a,auto b)
{
    int temp;
    temp=*a;
    *a=*b;
    *b=temp;

}

bool cmp(auto n1)
{
    bool flag=true;
    for (std::vector<int>::iterator i = n1.begin(); i != n1.end()-1; ++i)
    {
        flag=*i>*(i+1);
        if (!flag)
        {
            break;
        }
    }
    return flag;
}

void fun(auto start,auto end)
{
    if (cmp(std::vector<int>(start,end)))//如果降序
    {

        auto p=start;
        for (std::vector<int>::iterator i = end-1; i != start-1; --i)
        {
            if (*(i)>*(start-1))
            {
                p=i;
                break;
            }
        }
        myswap(start-1,p);
        sort(start,end);//升序

    }
    else
    {
        if (start+1!=end)
        {
            fun(start+1,end);
        }

    }
}

int main(int argc, char const *argv[])
{
    int n;
    vector<int> n1;
    cin>>n;

    while(n)
    {
        n1.push_back(n%10);

        n/=10;
    }


    int l=n1.size();
    reverse(n1.begin(),n1.end());

    if (cmp(n1))//如果整体降序
    {
        cout<<"none";
    }
    else
    {

        fun(n1.begin()+1,n1.end());
    }
    myprint(n1);
    return 0;
}

这个思路是如何输入的数已经按照高位到低位是降序,那么肯定是没有更大的。如果不是降序,那么递归找到一个降序的子集,这个降序的子集S肯定无法更大,那么上一位k需要和子集S中比k大的数中最小的交换。然后S变了,对S升序排列。

算法复杂度为O(n^2+nlogn)=O(n^2),n^2是循环比较的次数,nlogn是一次排序。

后来想了想里面明显还能优化。因为比较的次数太多了,而且有重复比较,其实递归也没必要。其实DP的核心就是用空间换时间,有的计算重复了,后面还要用,那么我们就存起来,免得再算。下面的优化和这个思想一样。

/*
* @Author: lenovouser
* @Date:   2020-07-27 17:45:00
* @Last Modified by:   lenovouser



* @Last Modified time: 2020-07-28 14:07:02



*/

#include <iostream>
#include <string>
#include <cstdlib>
#include <cmath>
#include<ctime>
#include<vector>
#include<queue>
#include <algorithm>
using namespace std;
void myprint(auto a)
{
    for(auto &b:a)
    {
        cout<<b;
    }
}
void myswap(auto a,auto b)
{
    int temp;
    temp=*a;
    *a=*b;
    *b=temp;

}



int main(int argc, char const *argv[])
{
    int n;
    vector<int> n1;
    cin>>n;

    while(n)
    {
        n1.push_back(n%10);

        n/=10;
    }


    int l=n1.size();
    reverse(n1.begin(),n1.end());
    bool flag[n1.size()-1];
    for (std::vector<int>::iterator i = n1.end()-1; i != n1.begin(); --i)
    {
        flag[n1.end()-1-i]=*i<=*(i-1);
    }
    // cout<<flag[0];

    if (!flag[0])//如果个位大于十位
    {
        myswap(n1.end()-1,n1.end()-2);;
    }
    else
    {
        for (int i = 1; i <n1.size()-1; ++i)
        {
            if (!flag[i])
            {
                 std::vector<int>::iterator p;
                for (std::vector<int>::iterator j = n1.end()-1; j != n1.end()-i-2; --j)
                {
                    if (*(j)>*(n1.end()-i-2))
                    {
                        p=j;
                        break;
                    }
                }
                myswap(n1.end()-i-2,p);
                sort(n1.end()-i-1,n1.end());//升序
                break;
            }
        }
    }
    myprint(n1);
    return 0;
}

 

这个时间复杂度是O(nlogn)。空间复杂度O(n)。

9 面向对象开发的开闭

https://www.cnblogs.com/vincent0928/p/6568354.html

 

10  char[]和char *的区别

1. char[] p表示p是一个数组指针,相当于const pointer,不允许对该指针进行修改。但该指针所指向的数组内容,是分配在栈上面的,是可以修改的。

2. char * pp表示pp是一个可变指针,允许对其进行修改,即可以指向其他地方,如pp = p也是可以的。对于*pp = "abc";这样的情况,由于编译器优化,一般都会将abc存放在常量区域内,然后pp指针是局部变量,存放在栈中,因此,在函数返回中,允许返回该地址(实际上指向一个常量地址,字符串常量区);而,char[] p是局部变量,当函数结束,存在栈中的数组内容均被销毁,因此返回p地址是不允许的。

 

同时,从上面的例子可以看出,cout确实存在一些规律:

1、对于数字指针如int *p=new int; 那么cout<<p只会输出这个指针的值,而不会输出这个指针指向的内容。
2、对于字符指针入char *p="sdf f";那么cout<<p就会输出指针指向的数据,即sdf f

那么,像&(p+1),由于p+1指向的是一个地址,不是一个指针,无法进行取址操作。

&p[1] = &p + 1,这样取到的实际上是从p+1开始的字符串内容。

char * a=”string1”;是实现了3个操作:
1声明一个char*变量(也就是声明了一个指向char的指针变量)。
2在内存中的文字常量区中开辟了一个空间存储字符串常量”string1”。
3返回这个区域的地址,作为值,赋给这个字符指针变量a
最终的结果:指针变量a指向了这一个字符串常量“string1”
(注意,如果这时候我们再执行:char * c=”string1”;则,c==a,实际上,只会执行上述步骤的1和3,因为这个常量已经在内存中创建)

char b[]=”string2”;则是实现了2个操作:
1声明一个char 的数组,
2为该数组“赋值”,即将”string2”的每一个字符分别赋值给数组的每一个元素,存储在栈上。
最终的结果:“数组的值”(注意不是b的值)等于”string2”,而不是b指向一个字符串常量

PS:
实际上, char * a=”string1”; 的写法是不规范的!
因为a指向了即字符常量,一旦strcpy(a,”string2”)就糟糕了,试图向只读的内存区域写入,程序会崩溃的!尽管VS下的编译器不会警告,但如果你使用了语法严谨的Linux下的C编译器GCC,或者在windows下使用MinGW编译器就会得到警告。

所以,我们还是应当按照”类型相同赋值”的原则来写代码: const char * a=”string1”;
保证意外赋值语句不会通过编译。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值