cpp 面试题

i++ & ++i 有什么区别?

  • i++返回的是i的值,而++i返回的是i+1的值。也就是++i是一个确定的值,是一个可修改的左值。

  • 这里有很多的经典笔试题:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    int main()
    {

    int i = 1;
    printf("%d,%d\n", ++i, ++i); //3,3
    printf("%d,%d\n", ++i, i++); //5,3
    printf("%d,%d\n", i++, i++); //6,5
    printf("%d,%d\n", i++, ++i); //8,9
    system("pause");
    return 0;
    }
  • 首先是函数的入栈顺序从右向左入栈的,计算顺序也是从右往左计算的,不过都是计算完以后在进行的压栈操作:

  • 对于第5行代码,首先执行++i,返回值是i,这时i的值是2,再次执行++i,返回值是i,得到i=3,将i压入栈中,此时i为3,也就是压入3,3;

  • 对于第6行代码,首先执行i++,返回值是原来的i,也就是3,再执行++i,返回值是i,依次将3,5压入栈中得到输出结果

  • 对于第7行代码,首先执行i++,返回值是5,再执行i++返回值是6,依次将5,6压入栈中得到输出结果

  • 对于第8行代码,首先执行++i,返回i,此时i为8,再执行i++,返回值是8,此时i为9,依次将i,8也就是9,8压入栈中,得到输出结果。


为什么不推荐在析构函数中抛出异常

  • 如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如资源泄漏的问题。

  • 通常异常发生时,c++的机制会调用已经构造对象的析构函数来释放资源,此时若析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序崩溃的问题。

  • 当在某一个析构函数中会有一些可能(哪怕是一点点可能)发生异常时,那么就必须要把这种可能发生的异常完全封装在析构函数内部,决不能让它抛出函数之外

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    virtual ~ MyTest_Base ()
    {
    cout << "开始准备销毁一个MyTest_Base类型的对象"<< endl;

    // 一点小的改动。把异常完全封装在析构函数内部
    try
    {
    // 注意:在析构函数中抛出了异常
    throw std::exception("在析构函数中故意抛出一个异常,测试!");
    }
    catch(…) {}

    }

指针和引用有什么区别?

  • 指针:指针是一个变量,只不过这个变量存储的是一个地址,指向内存的一个存储单元;而引用跟原来的变量实质上是同一个东西,只不过是原变量的一个别名而已
  • 可以有const指针,但是没有const引用;
  • 指针的值可以为空,但是引用的值不能为NULL,并且引用在定义的时候必须初始化;
  • “sizeof引用”得到的是所指向的变量(对象)的大小,而”sizeof指针”得到的是指针本身的大小;
  • 引用和指针一样,占用固定大小的内存(sizeof(A&)并不是一个引用真是占用的内存大小)
    • 函数形参定义为 & 的时候,压栈的时候,A& 也是8个byte,和指针的占用是一样的,引用也具有高效性

非常量的引用只可以指向左值

1
2
int& a = 10; // 错误
int& a = int(10); // 错误
  • 例如函数 void func(std::string& str),就不可以使用 func("test) 来调用
  • 可以修改成 void func(const std::string& str)

空的 class sizeof 之后的大小是多少?

  • 大小为1
  • 类的实例化,所谓类的实例化就是在内存中分配一块地址,每个实例在内存中都有独一无二的地址。同样空类也会被实例化(别拿豆包不当干粮,空类也是类啊),所以编译器会给空类隐含的添加一个字节,这样空类实例化之后就有了独一无二的地址了。所以空类的sizeof为1。

class 中 const 变量如何赋值?

  • 在类中声明变量为const类型,但是不可以初始化
  • const常量的初始化必须在构造函数初始化列表中初始化,而不可以在构造函数函数体内初始化
    1
    2
    3
    4
    5
    6
    7
    class A
    {
    public:
    A(int size) : SIZE(size) {};
    private:
    const int SIZE;
    };

map 遍历时删除某个元素

  • 错误方法:

    1
    2
    3
    4
    5
    for (auto iter = map_to_del.begin(); iter != map_to_del.end(); ++iter) {
    cout << "key: " << iter->first << " value: " << *iter->second << endl;
    delete iter->second;
    map_to_del.erase(iter);
    }
  • 正确方法:

    1
    2
    3
    4
    5
    for (auto iter = map_to_del.begin(); iter != map_to_del.end();) {
    cout << "key: " << iter->first << " value: " << *iter->second << endl;
    delete iter->second;
    map_to_del.erase(iter++);
    }
  • 运行结果与第一种方式相同,这种删除方式也是STL源码一书中推荐的方式,分析 m.erase(it++)语句,map中在删除iter的时候,先将iter做缓存,然后执行iter++使之指向下一个结点,再进入erase函数体中执行删除操作,删除时使用的iter就是缓存下来的iter(也就是当前iter(做了加操作之后的iter)所指向结点的上一个结点)。

  • 根据以上分析,可以看出(m.erase(it++) )和(m.erase(it); iter++; )这个执行序列是不相同的。

    • 前者在erase执行前进行了加操作,在it被删除(失效)前进行了加操作,是安全的;
    • 后者是在erase执行后才进行加操作,而此时iter已经被删除(当前的迭代器已经失效了),对一个已经失效的迭代器进行加操作,行为是不可预期的,这种写法势必会导致 map操作的失败并引起进程的异常。

map / vector 等容器for遍历的时候

1
2
3
for (const auto &iter : vec) {
std::cout << iter.GetA() << std::endl;
}
  • 一定要 referfence,const 根据实际情况来决定加不加
  • 因为取容器中的数据的时候,容器返回的是存的对象的 reference, 如果接受的时候不用 reference 则会调用拷贝构造函数

封装,继承,多态?


函数重载

  • 返回值类型不会作为函数重载的一个标准
  • 下面的 void GetA() 函数就不和 int GetA() 构成重载关系,会编译报错
  • 只有 形参 & const 可以作为函数重载的判断标准
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    #include <iostream>

    class A {
    public:
    int GetA() {
    std::cout << "non-const" << std::endl;
    return 12;
    }
    // void GetA(){}

    int GetA() const {
    std::cout << "const" << std::endl;
    return 12;
    }
    };

    int main() {
    A a1;
    const A a2;

    a1.GetA(); // non-const
    a2.GetA(); // const
    }
  • a1 不是 const 变量,默认会调用非 const 的方法,而 a2 是 const 变量,调用 const 方法
  • 如果只有 const 方法的话,在 a1 调用的时候会默认转化成 const 变量

const & static

const 详解

  • const 修饰类成员函数的时候,该函数不可以修改类成员变量,并且调用其他类成员函数也必须是const函数
  • const 修饰类成员变量的初始化只可以在构造函数的初始化列表中进行。
  • const 修饰普通变量时候,其含义永远看他左边的内容,如果左边没有类型,则看右边的类型
    • const A** var : 指向(const A)类型的二重指针
    • A const ** var : 同上
    • A* const * var : 指向(A* const)类型的常量指针,指针本身不可变
    • A** const var : 指向(A** const)类型的常量指针,指针本身不可变

static 详解

  • static 修饰全局变量的时候,非 lazy 初始化
  • static 修饰局部变量的时候,例如函数中的一个 static 变量,它是 lazy 初始化的
  • static 修饰class 成员函数/变量的时候,与具体的实例无关
    • 甚至可以将 nullptr 转化成对象再调用static方法,也不会出错,因为不会用到该对象

拷贝构造

  • 先看下面两段代码,有啥区别
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class A {};

    int main() {
    A a;
    A a1;
    a1 = a;

    ////////
    A a;
    A a2(a); // A a2(a);
    }
  • 后面的代码段会调用拷贝构造函数,前面则是先构造,再拷贝
  • 区别在于:如果 class A 中存在一些 const变量 的时候,在构造的时候const变量就已经定了,后续无法改变了

c++ 中的四种 cast


智能指针


new和malloc的区别

  1. new/delete是C++关键字,需要编译器支持。malloc/free是库函数,需要头文件支持。
  2. 使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。而malloc则需要显式地指出所需内存的尺寸。
  3. new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型。
  4. new内存分配失败时,会抛出bac_alloc异常。malloc分配内存失败时返回NULL。
  5. new会先调用operator new函数,申请足够的内存(通常底层使用malloc实现)。然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete先调用析构函数,然后调用operator delete函数释放内存(通常底层使用free实现)。
    • malloc/free是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作。

线程之间的数据共享

  • 管道(pipe)
  • 有名管道(namedpipe)
  • 信号量(semaphore)
  • 消息队列(messagequeue)
  • 信号(sinal)
  • 共享内存(shared memory)
  • 套接字(socket)