记录一些面试遇到的问题

重载和重写的区别

重载是overload,覆盖是override
在这里插入图片描述
重载属于编译时多态,覆盖属于运行时多态

运行时多态和编译时多态

运行时多态指的是在运行的时候才知道要调用哪一个函数,编译时多态是指在编译的时候就知道调用哪一个函数。

运行时多态

可以使得父类指针调用子类函数,当然子类指针也可以调用父类函数

#include <iostream>

class Base {
public:
    virtual void show() { std::cout << "Base class" << std::endl; }
};

class Derived : public Base {
public:
    void show() override { std::cout << "Derived class" << std::endl; }
};

int main() {
    Base* ptr;
    Derived obj;
    ptr = &obj;
    ptr->show();  // 调用 Derived::show(),发生运行时多态
}

by the way,如果代码不慎写为了这样,编译器也不会报错,而是友善提示:
函数 ‘show’ 从类 ‘Base’ 中隐藏了一个非虚拟函数

#include <iostream>

class Base {
public:
    void show() { std::cout << "Base class" << std::endl; }
};

class Derived : public Base {
public:
    void show() { std::cout << "Derived class" << std::endl; }
};

int main() {
    Base* ptr;
    Derived obj;
    ptr = &obj;
    ptr->show();  // 调用 Derived::show(),发生运行时多态
}

因为在C++ 规定,如果子类定义了与基类同名的函数,则基类中的所有同名函数都会被隐藏(即使参数列表不同)。
那如果有一个需求,首先满足Derived类继承了Base,同时有自己的show函数(参数列表和Base不一样,因此单纯的override是不行的),可以在Derived类里添加一句using Base::show;即可。

class Base {
public:
    virtual void show() { std::cout << "Base class" << std::endl; }
};

class Derived : public Base {
public:
    using Base::show;
    void show(int a)  { std::cout << "Derived class" <<a<< std::endl; }
};

int main() {
    Derived* ptr; //注意这里变为了Derived*
    Derived obj;
    ptr = &obj;
    ptr->show();
    ptr->show(1);
}

如果有对多态学术不精,只记得在虚函数的加持下可以使得父类指针访问子类函数,而将上述代码写为了

    Base* ptr; 
    Derived obj;
    ptr = &obj;
    ptr->show();
    ptr->show(1);

代码是不会通过检查的,因为父类指针可以调用子类的函数,但前提是 这个函数必须在父类中声明为 virtual,这样才能实现运行时多态(动态绑定)。
虽然我们在父类里有show()这个函数,但show(int) 不是 Base 类的虚函数,所以 Base* 看不到 Derived 里的 show(int),导致编译错误。

编译时多态

特点:

  1. 同一作用域内,多个函数同名但参数列表不同(参数个数或类型不同)。
  2. 编译时根据函数调用的参数选择具体的函数(编译器做“名字修饰(Name Mangling)”处理)。
  3. 不会引发运行时开销,函数的匹配完全在编译阶段完成。

继承

公有继承
class Base {
public:
    int a;
protected:
    int b;
    
private:
    int c;
};

class Derived : public Base {
    void print(){
        cout<<b;
    }
};

int main() {
    Derived* ptr;
    Derived obj;
    ptr = &obj;
    cout << ptr->a;
}

基类的 public 变成 public,protected 变成 protected,private 仍然是 private。

保护继承

在这里插入图片描述基类的 public 和 protected 变成 protected,private 不可访问。

私有继承

private(私有继承):基类的 public 和 protected 变成 private,private 不可访问。

多继承下的菱形继承

sizeof

虚函数的size

在这里插入图片描述
输出的结果为:
在这里插入图片描述
这是因为虚函数引入了虚表,因此需要额外存储一个虚表指针,64位系统下size = 8B。
如果再加上一个int,则为16(需要做到对齐,因此还补了4B)
在这里插入图片描述
多继承的情况下:
输出结果为16,因为继承了A和B,因为有两个虚指针
在这里插入图片描述

普通函数的size

普通函数size = 1

struct的size

struct也要遵循内存对齐,对齐原则是结构体或类的整体大小必须是其最大对齐数的整数倍(最大成员的对齐值
因此比如

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
因为char[]里有一个’\0’,因此size是6

指针与引用

先来个很经典的题

void GetMemory1(char* p){
    p = (char*)malloc(100);
}
void Test1(void){
    char* str =NULL;
    GetMemory1(str);
    strcpy(str,"hello");
    printf(str);
}

int main() {
    Test1();
}

这样会导致程序直接崩溃,原因是GetMemory1传入的函数是指针。注意,这里进行的是值传递,也就是说,我们传入的是str的复制值,因此在GetMemory1里修改p对外面的str没有一点用处。
那么如何修改呢?只需要将GetMemory的参数改为指针的引用 or 指针的指针即可
指针的引用版

void GetMemory1(char* p){
    p = (char*)malloc(100);
}
void Test1(void){
    char* str =NULL;
    GetMemory1(str);
    strcpy(str,"hello");
    printf(str);
}

int main() {
    Test1();
}

指针的指针版:

void GetMemory1(char** p){
    *p = (char*)malloc(100);
}
void Test1(void){
    char* str =NULL;
    GetMemory1(&str);
    strcpy(str,"hello");
    printf(str);
}

再来个题目

char* GetMemory2(){
    char p[] = "hello";
    return p;
}
void Test2(){
    char* str = NULL;
    str = GetMemory2();
    printf(str);
}

这会导致系统崩溃,因为
char p[] = “hello”; 是一个局部数组,存储在上。
当 GetMemory2() 结束后,p 变量的生命周期结束,它所在的栈内存可能被覆盖或释放。
str = GetMemory2(); 让 str 指向了这块无效的内存。
printf(str); 试图访问这块无效的内存,导致未定义行为(Undefined Behavior),可能程序崩溃。
但如果我们修改为,此时内存被分配到堆上,就不会报错了。注意还需要对应的free

char* p = (char*)malloc(100);

const系列

指针const
  1. int* const p
  2. const int* p
  3. int const *p
    1指的是p是const,不能再被赋值为别的指针了。后两者指的是不可以通过p去修改 *p了
函数const
  1. return值为const
    一般用在返回const指针和const引用上
  2. 函数为const
    成员函数为const代表不可以在其中对成员变量进行修改,如果要修改的话需要在成员变量前加上Mutable
    引入mutable给人的感觉很奇怪,这不就相当于开了后门吗?但事实上,合理使用mutable能够很好地避免向外界暴露隐私。
    保护类的成员变量不在成员函数中被修改,是为了保证模型的逻辑正确,通过用const关键字来避免在函数中错误的修改了类对象的状态。并且在所有使用该成员函数的地方都可以更准确的预测到使用该成员函数的带来的影响。而mutable则是为了能突破const的封锁线,让类的一些次要的或者是辅助性的成员变量随时可以被更改。
constexpr

为什么引入constexpr,这是因为此前const集合了只读常量两个含义,但实际上只读 ≠ 常量,因此存在 只读的变量和编译器常量两种情况。因此引入constexpr,从此const表明只读,而constexpr表示常量。
只读变量和编译器常量区别就在于:
常量是编译器已经分配好内存储存好了的,直接去读取就行,而只读变量和变量一样要新分配内存区域,只是只读变量不可更改.因此使用常量和只读变量时,它们的寻址方式不一样
常量必须在声明时就赋值,而只读变量可以在函数使用时再赋值。

constexpr + 指针

代表指针本身不可以修改,和所指对象无关

constexpr + 函数

要求函数的返回类型和所有形参类型都是字面值类型,函数体里有且只有一条return语句

volatile + const

volatile是为了告诉编译器,这个变量随时可能会发生改变,因此在取用的时候一定要去内存里拿真正的值,而不要做寄存器优化。

const和volatile放在一起的意义在于:
(1)本程序段中不能对a作修改,任何修改都是非法的,或者至少是粗心,编译器应该报错,防止这种粗心;
(2)另一个程序段则完全有可能修改,因此编译器最好不要做太激进的优化。

“const”含义是“请做为常量使用”,而并非“放心吧,那肯定是个常量”。
“volatile”的含义是“请不要做没谱的优化,这个值可能变掉的”,而并非“你可以修改这个值”。
因此,它们本来就不是矛盾的

static

  1. 什么时候声明,什么时候定义
    如果是成员变量,则类内声明,类外定义。因为static 变量是 所有对象共享的,不属于某个具体对象,而是存储在全局数据区。编译器不会自动分配存储空间,所以需要在类外定义来分配内存。
    如果是静态全局变量,直接在定义处初始化,因为 static 变量的作用域仅限于当前文件,它不能在别处声明然后在另一个地方定义。
    具有内部链接性(internal linkage),无法被 extern 访问。
  2. 静态全局变量和非静态全局变量区别
    非静态全局变量:
    直接定义在文件的全局作用域中(文件的最外层)。具有外部链接性(external linkage),可以被其他文件(翻译单元)访问(即可以使用 extern 关键字引用)。
    贯穿程序的整个生命周期。一般定义在cpp实现中,而不是定义在h文件。
    就算是在A中include 了B的头文件,B的cpp文件中有该非静态全局变量,也要使用extern才能链接到。
    静态全局变量:
    使用 static 关键字修饰的全局变量。
    具有内部链接性(internal linkage),仅限于声明它的文件内部访问,不能被其他文件使用,即使 extern 也无法引用它。贯穿程序的整个生命周期(与非静态全局变量相同)。

构造函数&析构函数

能否是虚函数

基类的构造函数不能是虚函数,但是析构函数最好是虚函数。因为多态的前提是有一个完整的对象,而构造的时候其实并未准备完全。在基类的构造过程中,对象仍然是基类类型,即使派生类重写了该构造函数,也无法在基类构造阶段正确调用。

能否在其中调用虚函数

这个问题真的没道理,当时我绞尽脑汁也想不出来这样的场景,后来才发现最好不要这样做。(证明我的直觉就是有道理的!)

  • 构造函数
    如果基类构造函数里有一个func函数,func函数被子类重写,那么当子类对象调用父类构造函数时,是否能够形成多态呢?
    答案是不可以的,因为先调用基类的构造函数,再调用派生类的构造函数。
    对象完全构造后,虚表(vtable)才会指向最终派生类。

  • 析构函数
    答案也是不对的,因为析构的顺序是:
    先调用子类的析构函数
    再调用父类的析构函数
    最后释放内存
    当执行到父类析构函数时,子类对象已经被释放,这个时候再调用函数则还是基类的版本。

https://blog.youkuaiyun.com/tht2009/article/details/6920511

C++11和其他系列的区别

https://blog.youkuaiyun.com/sinat_38293503/article/details/134612152

C++11最大的贡献就是引入内存模型,以前C++98都是单线程,所以不存在什么数据竞争和内存顺序,但是C++11就不这样了。

C++98:
没有标准的内存模型,对多线程的支持依赖平台特定的扩展(如 POSIX 线程或 Windows API)。
编译器优化和硬件指令重排可能导致多线程程序出现未定义行为(如数据竞争)。

C++11:
明确定义了内存模型,规定了多线程环境下变量的可见性和执行顺序。
提供了 std::atomic、内存序(std::memory_order)等机制,使开发者可以控制并发访问的语义。

首先明确数据竞争是因为并发地访问并修改一个变量,因为没有对数据上锁,所以导致数据计算出错。
内存序是为了实现避免编译器瞎优化导致的问题,

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值