C++面试题(~01)

来源:互联网 发布:武汉纵横网络 编辑:程序博客网 时间:2024/06/05 08:44

冯诺依曼体系结构

冯诺依曼体系结构用于存储程序方式,指令和数据不加区别混合存储在同一存储器中。有如下特点:

  • 一律用二进制数表示数据和指令;
  • 顺序执行程序。执行前,将需要的程序和数据先放入存储器(PC为内存),当执行时把要执行的程序和要处理的数据按顺序从存储器中取出指令并一条一条执行,称作顺序执行程序;
  • 计算机硬件由运算器、控制器、存储器、输入设备和输出设备五大部分组成;

编程测试机器大小端存储

请写一个C函数,若处理器是Big_endian的,则返回0;若是Little_endian的,则返回1

int check_CPU(){    union w{        int a;        char b;    }c;    c.a = 1;    return c.b == 1;}

指针和引用

  • 指针是一个变量,只不过这个变量存储的是一个地址,指向的是内存的一个存储单元。而引用跟原来的变量实质是同一个东西,只不过是原变量的一个别名
  • 指针可以有多级,引用只能是一级;
  • 指针的值可以为空,也可能指向一个不确定的内存空间,但引用的值不能为空,并且引用在定义的时候必须初始化为特定对象**(因此引用更安全);
  • 指针的值在初始化以后可以改变,即指向其他的存储单元,而引用在进行初始化后就不会再改变引用对象了;
  • sizeof引用得到的是所指向的变量(对象)的大小,而sizeof指针得到的是指针本身的大小;
  • 指针和引用的自增(++)运算意义不一样;

volatile

volatile关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器位置的因素更改,比如:操作系统,硬件,或其他线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码不再进行优化,从而可以提供对特殊地址的稳定访问;

当要求使用volatile声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据,而且读取的数据立刻被保存;

volatile指出i是随时可能发生改变的,每次使用它的时候必须从i地址读取,因而编译器生成的汇编代码会重新从i的地址读取数据放在b中,而优化的做法是,由于编译器发现两次从i读数据的代码之间没有对i进行过操作,它会自动把上次读的数据放在b中,而不是从重新从i里面读。这样一来,如果i是一个寄存器变量或者表示一个端口数据就容易出错,所以说volatile可以保证对特殊地址的稳定访问;

说出static 和 const关键字尽可能多的作用

static:

  • 函数体内static变量的作用范围是该函数体,不同于auto变量,该变量的内存只被分配一次,因此其值在下次调用时仍维持上次的值;
  • 在模块内的static全局变量可以被模块内的所用函数调用,但不能被模块外的其他函数访问;
  • 在模块内的static函数只可被这一模块内的其他函数调用,这个函数的使用范围被限制在声明它的模块内;
  • 在类中的static成员变量为整个类所拥有,对类的所有对象只有一份拷贝;
  • 在类中的static成员函数为整个类所拥有,这个函数不接收this指针,因而只能访问类的static成员变量;

const:

  • 欲阻止一个变量被改变,可以使用const关键字,在定义该const 变量时,通常需要对它进行初始化,因为以后没有机会再去修改它了;
  • 对指针来说,可以指定指针本身是const,也可以指定指针所指的数据是const,或二者同时为const;
  • 在一个函数声明中,const可以修饰形参,表明它是一个输入参数,在函数内部不能改变其值;
  • 对于类的成员函数,若指定其为const,则表明其是一个常函数,不能修改类的成员变量;
  • 对于类的成员函数,有时候必须指定其返回值是const类型,以使得其返回值不为“左值”,例如:
    • const classA operator*(const classA& a1,const classA& a2);
      operator* 的返回结果必须是一个const 对象,如果不是,这样的变态代码也不会编译出错:classA a,b,c;
      (a * b) = c; // 对a*b的结果赋值

      操作(a * b) = c显然不符合编程者的初衷,也没有任何意义;

String类

编写类String的构造函数、析构函数和赋值函数,已知类String的原型为:

class String{private:    char* m_data;  //用于保存字符串public:    String(const char* str = NULL);  //普通构造函数    String(const String& other);     //拷贝构造函数    ~String();    String& operator=(const String& other); //赋值函数};String::String(const char* str){    if (str == NULL){        m_data = new char[1]; //对空字符串自动申请存放结束标志'\0'的空        *m_data = '\0';    }    else{        int len = strlen(str);        m_data = new char[len + 1];        strcpy(m_data, str);    }}String::String(const String& other){    int len = strlen(other.m_data);    m_data = new char[len + 1];    strcpy(m_data, other.m_data);}String::~String(){    delete[] m_data;}String& String::operator=(const String& other){    if (this == &other)return *this;    delete[] m_data;    int len = strlen(other.m_data);    m_data = new char[len + 1];    strcpy(m_data, other.m_data);    return *this;}

float零值比较

写出float与“零值”比较的if语句:

const float EPSINON = 0.00001;if((x >= -EPSINON) || (x <= EPSINON))

浮点型变量不精确,所以不可将float变量用“==”或“!=”与数字比较,应该设法转化为“>=”或“<=”形式;

实现memcpy函数

void* memcpy(void* dst, const void* src, size_t len){    if (!dst || !src)return NULL;    void* ret = dst;    if (dst <= src || (char*)dst >= (char*)src + len){        //没有内存重叠,从低地址开始复制        while (len--){            *(char*)dst = *(char*)src;            dst = (char*)dst + 1;            src = (char*)src + 1;        }    }    else{        //存在内存重叠,从高地址开始复制        src = (char*)src + len - 1;        dst = (char*)dst + len - 1;        while (len--){            *(char*)dst = *(char*)src;            dst = (char*)dst - 1;            src = (char*)src - 1;        }    }    return ret;}

洗牌算法

给定N张扑克牌和一个随机函数,设计一个洗牌算法

void shuffle(int cards[], int n){    if (cards == NULL)return;    srand(time(0));    for (int i = 0; i < n - 1; i++){        //保证每次第i位的值不会涉及到第i为以前        int index = i + rand() % (n - i);        swap(cards[i], cards[index]);    }}

大数据中位数

100亿个整数,内存足够,如何找到中位数?内存不足,如何找到中位数?

  • 内存充足的情况下:可以使用类似QuickSort的思想进行,均摊复杂度为O(n),算法思想如下:
    • 随机选取一个元素,将比它小的元素放在左边,比它大的元素放在右边;
    • 如果它恰好在中位数的位置,那么它就是中位数,直接返回;
    • 如果小于它的数字超过一半,那么中位数一定在左半边,递归到左边处理;
    • 否则,中位数一定在右半边,根据左半边的元素个数计算出中位数是右半边的第几大,然后递归到右半边处理;
  • 内存不足的情况:
    • 分桶法:化大为小,把所有数划分到各个小区间,把每个数映射到对应的区间里面,对每个区间中数的个数进行计数,数一遍各个区间,看看中位数落在哪个区间;

智能指针

智能指针是一种资源管理类,通过对原始指针进行封装,在资源管理对象进行析构时对指针指向的内存进行释放,通常使用引用计数方式进行管理:

template<class T>class SmartPointer{private:    T* ptr;    size_t* reference_count;    void releaseCount(){        if (ptr){            (*reference_count)--;            if ((*reference_count) == 0){                delete ptr;                delete reference_count;            }        }    }public:    SmartPointer(T* p = NULL) :ptr(p), reference_count(new size_t){        if (p)*reference_count = 1;        else *reference_count = 0;    }    SmartPointer(const SmartPointer& src){        if (ptr != src.ptr){            ptr = src.ptr;            reference_count = src.reference_count;            (*reference_count)++;        }    }    SmartPointer& operator=(const SmartPointer& src){        if (ptr == src.ptr)return *this;        releaseCount();        ptr = src.ptr;        reference_count = src.reference_count;        (*reference_count)++;        return *this;    }    T* operator*(){        return (*ptr);    }    T* operator->(){        return ptr;    }    ~SmartPointer(){        if ((--(*reference_count)) == 0){            delete ptr;            delete reference_count;        }    }    size_t get_reference(){        return *reference_count;    }};

单例模式

实现单例模式,要求线程安全;

#include<iostream>#include<afxmt.h>using namespace std;class Lock{private:    CCriticalSection m_cs;public:    Lock(CCriticalSection cs) :m_cs(cs){        m_cs.Lock();    }    ~Lock(){        m_cs.Unlock();    }};class Singleton{private:    Singleton();    Singleton(const Singleton&);    Singleton& operator=(const Singleton&);public:    static Singleton *Instantialize();    static Singleton *pInstance;    static CCriticalSection cs;};Singleton* Singleton::pInstance = NULL;Singleton* Singleton::Instantialize(){    if (pInstance == NULL){        //double check        Lock lock(cs);  //用lock实现线程安全,用资源管理类,实现异常安全        //使用资源管理类,在抛出异常的时候,资源管理类对象被析构,析构总是发生无论是因为异常抛出还是语句块结束;        if (pInstance == NULL){            pInstance = new Singleton();        }    }    return pInstance;}

const 和 define有什么区别

  • 编译器处理方式不同;
    • define宏是在预处理阶段展开;
    • const常量是在编译运行阶段使用;
  • 类型和安全检查不同;
    • define宏没有类型,不做任何类型检查,仅仅是展开;
    • const常量有具体的类型,在编译阶段会执行类型检查;
  • 存储方式不同;
    • define宏仅仅是展开,有多少个地方使用,就展开多少次,不会分配内存;
    • const常量会在内存中分配(可以在堆中也可以在栈中);
  • const可以节省空间,避免不必要的内存分配;

  • const定义常量从汇编的角度来看,只是给出了对应的内存地址,而不是像#define一样给出的是立即数,所以,const定义的常量在程序运行过程中只有一份拷贝,而#define定义的常量在内存中有若干份拷贝;

  • 提升了效率,编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有存储与读内存的操作,使得其效率也很高;

define 和 inline的区别

表达式形式的宏定义一例:
   #define ExpressionName(Var1,Var2) (Var1+Var2)*(Var1-Var2)
   
这种宏形式作用与函数类似,但它使用预处理器,没有堆栈,使用上比函数高效。但它只是预处理器上符号表的简单替换,不进行函数有效性检测以及使用C++的成员访问控制;

inline 推出的目的,也正是为了取代这种表达式形式的宏定义,它消除了它的缺点,同时又很好的继承了它的优点,inline代码放入了预编译器符号表中,高效;它是真正的函数, 调用时有严格的参数检测;它也可作为类的成员函数;

直接在class定义中定义各函数成员,系统将它们作为内联函数处理;成员函数是内联函数,意味着:每个对象都有该函数一份独立的拷贝;在类外,如果使用inline定义函数,则系统也会作为内联函数处理;

  • 宏define在预处理阶段完成,inline在编译阶段完成;
  • 类型安全检查
    • inline函数就是函数,要做类型检查;
    • 宏定义不用作类型检查;
  • 替换方式
    • #define字符串替换;
    • inline是指嵌入代码,在编译过程中不单独产生代码,在调用函数的地方不是跳转,而是直接把代码写到那里去,对于短小的函数比较实用,且安全可靠;
  • inline函数是否展开由编译器决定,有时候函数体太大时,编译器可能选择不展开相应的函数;

malloc 和 new 有什么区别

  • malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。

  • 对于非内部数据类型的对象而言,光用maloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。

  • 因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以一个能完成清理与释放内存工作的运算符delete。注意new/delete不是库函数。

  • C++程序经常要调用C函数,而C程序只能用malloc/free管理动态内存。

  • new可以认为是malloc加构造函数的执行。new出来的指针是直接带类型信息的。而malloc返回的都是void指针。

  • new建立的是一个对象,malloc分配的是一块内存;

C++中成员函数能够同时用static和const进行修饰?

C++编译器在实现const的成员函数的时候为了确保该函数不能修改类中参数的值,会在函数中添加一个隐式的const this*,但一个成员为static的时候,该函数时没有this指针的,也就是说此时const的用法和static是冲突的;

从对象模型上来说,类的非static成员函数在编译的时候都会扩展加上一个this参数,const的成员函数被要求不能修改this指针所指向的对象,而static函数编译的时候并不扩充加上this参数,自然无所谓const;

简述C++虚函数作用及底层实现原理

C++中虚函数使用虚函数表和虚函数表指针实现,虚函数表是一个类的虚函数的地址表,用于索引类本身以及父类的虚函数的地址,假如子类的虚函数重写了父类的虚函数,则对应的虚函数表会把对应的虚函数替换为子类的虚函数的地址;虚函数表指针存在于每个对象中(出于效率考虑,会放在对象的开始地址处),它指向对象所在类的虚函数表的地址;在多继承环境中,会存在多个虚函数表指针,分别指向对应不同基类的虚函数表;

子类重新定义了父类的虚函数后,父类指针根据赋给它的不同的子类指针,动态的调用数据子类的该函数,且这样的函数调用无法在编译期间确认,而是在运行期确认,也叫作延迟绑定;

一个对象访问普通函数和虚函数哪个更快?

访问普通函数更快,因为普通函数的地址在编译阶段已经确定,因此在访问时直接调用对应地址的函数,而虚函数在调用时,需要首先在虚函数表中寻找虚函数所在地址,因此相比普通函数速度要慢一些;

什么情况下,析构函数需要是虚函数?

若存在类继承关系并且析构函数中需要析构某些资源时,析构函数需要时虚函数,否则当使用父类指针指向子类对象的时候,在delete时只会调用父类的析构函数,而不能调用子类的析构函数,造成内存泄漏等问题;

内联函数、构造函数、静态成员函数可以是虚函数吗?

都不可以:

  • 内联函数是在编译阶段展开的,而虚函数是运行时动态绑定的,编译时无法展开;
  • 构造函数在进行调用时还不存在父类和子类的概念,父类只会调用父类的构造函数,子类调用子类的,不存在动态绑定的概念,但是构造函数中可以调用虚函数,不过没有动态的效果,只会调用本类的对应函数;
  • 静态成员函数是以类为单位的函数,与具体对象无关,虚函数是与对象动态绑定的,因此是两个不冲突的概念;

析构函数异常

  • C++中析构函数的执行不应该抛出异常;
  • 假如析构函数中抛出了异常,系统将变得非常危险,也许很长时间什么错误也不会发生,但也许系统有时就会莫名其妙的崩溃退出了,而什么迹象也没有;
  • 当一个析构函数中会有一些可能(哪怕是一点点可能)发生异常时,那么就必须把这种可能发生的异常完全封装在析构函数内部,局不能让它抛出函数之外,即在析构函数内部写出完整的throw…catch()块;

重载和重写

方法的重写Overriding和重载Overloading是多态性的不同表现。
方法的重写Overriding是父类和子类之间多态性的一种表现,重载Overloading是一个类中多态性的表现。

如果子类中定义的方法和其父类有相同的名称和参数,则该方法被重写(Overriding)。子类的对象使用这个方法时,将调用子类中的定义,对它而言,父类中的定义如同被“屏蔽”了,而且如果子类的方法名和参数类型和个数都和父类相同,那么子类的返回值类型必须和父类的相同;

如果在一个类中定义了多个同名的方法,它们或有不同的参数个数或有不同的参数类型,则称为方法的重载(Overloading)。Overloading的方法是可以改变返回值的类型。也就是说,重载的返回值类型可以相同也可以不同。

等概率产生随机数

给定一个能够生成0,1两个数的等概率随机数生成器”,如何生成⼀个产生0,1,2,3的等概率随机数生成器?
和上题类似,如何用rand7生成rand9?

将两个0,1随机生成器级联,每次产生两个数,则可能产生的结果有(0,0)、(0,1)、(1,0)、(1,1),分别映射到0,1,2,3即可;
两个rand7可以产生49中可能,扔掉后面的4种,保留45种,并平均分成9份,每次产生一个结果时,假如没落在对应区间中就丢掉,否则根据落在那个区间判断是0~8中的哪个?

同样可以实现互斥,互斥锁和信号量有什么差别?

  • 信号量是一种同步机制,可以当做锁来使用,但也可以当做进程/线程之间通信使用,作为通信使用时不一样有锁的概念;
  • 互斥锁是为了锁住一些资源,是为了对临界区做保护;

编程实现三个线程ABC,并让它们顺次打印ABC

思路:设置三个信号量:S1,S2,S3,由S1 post S2,S2 post S3,S3 post S1,由A线程先开始打印,其他线程必然在等待信号量,所以三个线程一定会按照信号量的顺序来打印;

#include<stdio.h>#include<sys/types.h>#include<semaphore.h>#include<pthread.h>sem_t sem_id1, sem_id2, sem_id3;void* func1(void*);void* func2(void*);void* func3(void*);int main(){    sem_init(&sem_id1, 0, 1);    sem_init(&sem_id2, 0, 0);    sem_init(&sem_id3, 0, 0);    pthread_t pthread_id1, pthread_id2, pthread_id3;    pthread_create(&pthread_id1, NULL, func1, NULL);    pthread_create(&pthread_id2, NULL, func2, NULL);    pthread_create(&pthread_id3, NULL, func3, NULL);    pthread_join(pthread_id1, NULL);    pthread_join(pthread_id1, NULL);    pthread_join(pthread_id1, NULL);    return 0;}void* func1(void *){    sem_wait(sem_id1);    printf("A\n");    sem_post(sem_id2);}void* func2(void *){    sem_wait(sem_id2);    printf("B\n");    sem_post(sem_id3);}void* func3(void *){    sem_wait(sem_id3);    printf("B\n");    sem_post(sem_id1);}

参考
参考

简述Linux进程内存空间分为哪几个段?作用分别是什么?

这里写图片描述

Linux 伙伴系统原理

目的:最大限定的降低内存的碎片化;
原理:

  • 将内存块分为11个连续的页框块(1,2,4,8…512,1024),其中每一个页框块中用链表将内存块对应内存大小的块进行链接;
  • 若需要一块256大小的内存块,则从对应256链表中查找空余的内存块,若有则分配,否则,查找512等;
  • 若在256中未找到空余内存块,在512中查找到空余的内存块吗,则将512 分成两部分,一部分进行分配,另一部分则插入256链表中;
  • 内存的释放过程与分配过程相反,在分配过程中由大块分解而成的小块没有被分配的块将一直等着被分配的内存块释放,从而和其合并,最终相当于没有划分小块;

总结:伙伴系统在分配和释放过程中执行互逆的过程,其将会极大力度的抵消碎片的产生;

简述malloc实现原理

malloc可以分别由伙伴系统或基于链表的实现:

  • 它有一个将可用的内存块连接为一个长长的列表的所谓空闲链表;
  • 调用malloc函数时,它沿连接表寻找一个大到足以满足用户请求所需要的内存块,然后,将该内存块一分为二(一块大小与用户请求的大小相等,一块大小就是剩下的字节),接下来,将分配给用户的那块内存传给用户,并将剩下的那块(如果有的话)返回到链表;
  • 调用free函数时,将用户释放的内存块连接到空闲链上,到最后,空闲链会被切成很多小的内存片段,如果这时用户申请一个大的内存片段,那么空闲链上可能没有满足用户要求的片段,于是,malloc函数请求延时,并开始在空闲链上检查内存片段,对它们进行整理,将相邻的小空闲块合并成较大的内存块;

使用mmap读写文件为什么比普通读写函数要快?

mmap函数把文件的某一块的内容映射在用户空间上,用户可以直接读写这一块内容。普通读写函数会经历一个内核缓冲过程,多出了数据拷贝的时间。

无论是UNIX I/O还是标准I/O库函数,他们都是通过read,write等底层系统调用来实现的,而read,write都会使用内核进行缓冲;

Linux中如何实现Signal?

内核调度并运行进程之前,先检查该进程上是否有未处理的信号,有则发出一个软中断,中断处理函数中执行对应的信号处理函数,因为是通过软中断执行,所以信号处理在自己所有的栈上,不会影响原来的栈;

sizeof运算符

在网上看到的代码:

int i=1;cout<<sizeof(++i)<<endl;    //结果为4   int类型4个字节cout<<i<<endl;              //结果为1   在执行了上面的操作后i仍为1 ++i没有执行

改变一下:

 int i=1;cout<<sizeof(i+=1)<<endl;cout<<i<<endl;

这次结果仍然相同!!

int i=1;int j=2;cout<<sizeof(i=j+1)<<endl;cout<<i<<endl;

结果同上。

int i=1;double j=2;cout<<sizeof(j=i+1)<<endl;

根据C99规范,sizeof是一个编译时刻就起作用的运算符,在其内的任何运算都没有意义。

j = sizeof(++i+++i); 在编译的时候被翻译成j = sizeof((++i+++i的数据类型)) 也就是 j = sizeof(int);

只要sizeof的参数表达式没有错误,其结果为从左边数第一个变量的类型的大小值。

原创粉丝点击