1.malloc、realloc、calloc的区别

  • malloc函数:

    • 用于分配指定大小的内存块,并返回指向该内存块的指针。如果分配失败,则返回 NULL

    • 使用 malloc 分配的内存不会被初始化,其中的数据是随机的。

  • calloc 函数

    • 原型为 void* calloc(size_t n, size_t size);
    • 第一个参数 n 表示要分配多少个元素;第二个参数 size 表示每个元素的大小(以字节为单位)。
    • calloc 会将分配的内存初始化为零。
  • realloc 函数

    • 原型为 void* realloc(void *p, size_t new_size);

    • realloc 函数用来改变已经分配的内存块的大小。它可以扩大或缩小已有的内存块。如果新的内存大小大于原来的大小,那么新增的部分内容未定义(即可能包含任何值)。如果缩小内存块,超出部分会被丢弃。

      注:C++中可以通过使用STL容器来避免使用realloc的情况,可以减少由于手动管理内存而引发错误的可能。

2.类成员初始化方式?构造函数的执行顺序 ?为什么用成员初始化列表会快一些?

类成员初始化方式:

  • 赋值初始化:这是通过在构造函数体内对成员变量进行赋值来实现的。
  • 成员初始化列表:使用冒号(:)后跟随初始化列表的方式,在构造函数体执行前对成员变量进行初始化。

构造函数的执行顺序:(在创建派生类对象时,基类的构造函数会在派生类的构造函数之前执行。)

  1. 首先执行虚拟基类的构造函数(如果有多个虚拟基类,则按照它们被继承的顺序)。
  2. 然后是普通基类的构造函数(如果有多个普通基类,则按照它们在派生类继承列表中的顺序)。
  3. 接着是派生类中对象成员(包含其他类类型的对象作为其成员变量)的构造函数(这些成员对象按照它们在类定义中的声明顺序进行初始化,而不是按照初始化列表中的顺序)。
  4. 最后是派生类自己的构造函数

使用成员初始化列表更快的原因:

  • C++的赋值操作是会产生临时对象的:使用赋值初始化时,首先会调用默认构造函数为成员变量分配空间并创建一个临时对象,然后将这个临时对象赋值给成员变量,这会产生额外的开销。
  • 使用成员初始化列表直接在成员变量创建时就对其进行初始化,避免了创建临时对象的过程。

3.有哪些情况必须用到成员列表初始化?作用是什么?

成员列表初始化的四种必须使用的情况:

  • 初始化引用成员:引用成员必须在创建时初始化,且不能重新赋值。因此,必须在成员初始化列表中进行初始化。

  • 初始化常量成员:常量成员(const)一旦初始化后不能修改,因此必须在成员初始化列表中初始化。

  • 调用基类的构造函数:如果基类没有默认构造函数,或者需要传递参数给基类构造函数,必须在成员初始化列表中调用基类的构造函数。

  • 调用成员类的构造函数:如果类的成员是另一个类的对象,并且该成员类没有默认构造函数,或者需要传递参数,必须在成员初始化列表中调用该成员类的构造函数。

    注:在函数体内进行赋值初始化时,成员就已经存在了

成员初始化列表的作用:

  • 初始化顺序:编译器会按照成员初始化列表中的顺序,在构造函数体内插入初始化操作,且这些操作在任何用户代码之前执行。
  • 初始化顺序的决定:初始化列表的顺序并不决定实际的初始化顺序,实际的初始化顺序由类中成员的声明顺序决定。

4.C++中新增了string,它与C语言中的 char *有什么区别吗?它是如何实现的?

  • string是对char*进行了封装,封装的string包含了char*数组,容量,长度等等属性。
  • string可以进行动态扩展,在每次扩展的时候另外申请一块原空间大小两倍的空间(2^n),然后将原字符串拷贝过去,并加上新增的内容。

5.什么是内存泄露,如何检测与避免

内存泄露:指的是堆内存的泄露,分配的内存块在使用完成后没有正确地释放。

避免内存的泄露的方式:

  • 计数法:使用new或者malloc时,让该数+1,delete或free时,该数-1,程序执行完打印这个计数,如果不为0则表示存在内存泄露

  • 将基类的析构函数声明为虚函数:这可以保证当通过基类指针删除派生类对象时,派生类的析构函数能够被正确调用,从而避免因未正确调用析构函数导致的内存泄漏。

    注:如果基类的析构函数不是virtual,通过基类指针删除派生类对象只会调用基类的析构函数。

  • 正确释放对象数组:对于使用 new[] 分配的对象数组,应使用 delete[] 来释放,而不是 delete,以避免内存泄漏。

  • 成对出现原则:确保每一对 new/deletemalloc/free 成对出现,即每次分配内存后都必须有相应的释放操作

检测工具

  • Linux下可以使用Valgrind工具
  • Windows下可以使用CRT库

6.对象复用的了解,零拷贝的了解

对象复用:在设计模式中通常指的是Flyweight(享元)模式。该模式旨在通过共享尽可能多的数据来最小化内存使用,特别是当系统需要创建大量细粒度的对象时。通过将相似或相同的对象存储在一个“对象池”中,并在需要时重复利用这些对象,可以有效减少内存占用和对象创建的开销。

  • 亨元模式: 由亨元工厂创建新的亨元对象前,先检查“对象池”中是否有相同属性对象,若有可以重复使用这些对象,而不是创建新的对象。

    eg:假设我们正在开发一个文本编辑器,其中每个字符都由一个独立的对象表示。如果我们直接为每个字符创建一个新的对象实例,那么对于长文档来说,这将导致大量的内存消耗。Flyweight模式可以帮助我们通过共享相同字符的属性(如字体、颜色等)来优化这一过程。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    #include <iostream>
    #include <unordered_map>
    #include <string>

    // Flyweight接口
    class Character {
    public:
    virtual void display(char ch) const = 0;
    };

    // 具体Flyweight类
    class ConcreteCharacter : public Character {
    private:
    char font_family_;
    int font_size_;
    std::string color_;

    public:
    ConcreteCharacter(char font_family, int font_size, const std::string& color)
    : font_family_(font_family), font_size_(font_size), color_(color) {}

    void display(char ch) const override {
    std::cout << "Character: " << ch
    << ", Font Family: " << font_family_
    << ", Font Size: " << font_size_
    << ", Color: " << color_ << std::endl;
    }
    };

    // Flyweight工厂
    class CharacterFactory {
    private:
    std::unordered_map<std::string, Character*> characters_;

    public:
    ~CharacterFactory() {
    for (auto& pair : characters_) {
    delete pair.second;
    }
    }

    Character* getCharacter(char font_family, int font_size, const std::string& color) {
    std::string key = std::string(1, font_family) + std::to_string(font_size) + color;
    if (characters_.find(key) == characters_.end()) {
    characters_[key] = new ConcreteCharacter(font_family, font_size, color);
    }
    return characters_[key];
    }
    };

    int main() {
    CharacterFactory factory;

    // 创建一些字符对象,但共享相同的属性
    Character* charA = factory.getCharacter('T', 12, "red");
    Character* charB = factory.getCharacter('T', 12, "red"); // 应该复用charA的实例

    charA->display('A');
    charB->display('B');

    // 即使请求了两次相同的属性组合,实际上只会有一个ConcreteCharacter实例被创建
    if (charA == charB) {
    std::cout << "charA and charB are the same instance." << std::endl;
    } else {
    std::cout << "charA and charB are different instances." << std::endl;
    }

    return 0;
    }

    代码中分别请求了两个具有相同属性的字符对象,但实际上它们引用的是同一个ConcreteCharacter实例,实现了对象复用的目的。

零拷贝:一种优化技术,目的是减少数据从一处存储到另一处传输过程中CPU的参与程度,从而提高效率并降低资源消耗。传统上,数据传输可能涉及多次复制操作,比如从磁盘读取数据到内核空间,再复制到用户空间等。零拷贝技术减少了这种不必要的数据复制次数,降低了CPU的工作量和总线活动。

*注:vector的一个成员函数emplace_back()很好地体现了零拷贝技术。emplace_back() 是在容器内部直接构造对象,而不是先创建一个临时对象再进行拷贝或移动。这意味着它可以在不调用拷贝构造函数或移动构造函数的情况下,在容器预留的位置原地构造对象,这通常更高效。*

7.介绍面向对象的三大特性,并且举例说明

  • 继承: 让某种类型对象获得另一个类型对象的属性和方法。
    • 实现继承:指使用基类的属性和方法而无需额外编码的能力
    • 接口继承:指仅使用属性和方法的名称、但是子类必须提供实现的能力
  • 封装: 把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏
  • 多态: 同一事物表现出不同事物的能力
    • 实现多态有二种方式:覆盖(override),重载(overload)
    • 重载实现编译时多态,虚函数实现运行时多态

8.C++的四种强制转换

C++提供了四种类型的显式类型转换运算符:reinterpret_castconst_caststatic_castdynamic_cast

  • reinterpret_cast:在不相关的类型之间进行低级别的强制转换。它可以将任何指针类型转换为另一个指针类型,或指针到整数类型的转换等。转换后的数和原来的数有相同的比特位。

    1
    char *c = reinterpret_cast<char*>(p); // 将int指针转换为char指针

    注:reinterpret_cast没有进行任何的类型检查,因此很容易发生由于指针类型不匹配而引发的内存错误

  • const_cast:主要用于添加或移除变量的constvolatile属性。它只能改变这些限定符,不能改变对象的实际类型。

    1
    2
    const char *p = "hello";
    char *q = const_cast<char*>(p);

    注:这里将 `const char类型的指针p转换为char类型的指针q`。

  • static_cast:用于类层次结构中的上行转换(派生类到基类)和下行转换(基类到派生类),以及基本数据类型之间的转换。

    • 特点不进行运行时类型检查,因此在下行转换时可能不安全。

      1
      2
      int n = 65;
      char c = static_cast<char>(n);//这里将 int 类型的变量 n 转换为 char 类型的变量 c。
  • dynamic_cast: 主要用于类层次结构中的上行转换和下行转换,特别是用于多态类型的转换。

    • 特点:在运行时进行类型检查,如果转换不安全(如下行转换时基类指针不指向派生类对象),则返回 nullptr

      1
      2
      3
      4
      5
      6
      7
      Base *b = new Son;
      Son *s = dynamic_cast<Son*>(b);//这里将 Base* 类型的指针 b 转换为 Son* 类型的指针 s,并在转换失败时返回 nullptr。
      if (s) {
      // 转换成功
      } else {
      // 转换失败
      }

9.C++函数调用的压栈过程

函数调用的压栈过程:

  • 分配栈空间:为被调用函数分配栈空间,用于存储函数的局部变量、参数和返回地址等信息。
  • 参数压栈:将实参的值复制到形参的栈空间中。C++中,参数是从右到左依次压栈的。
  • 保存返回地址:将当前函数的返回地址(即调用函数的下一条指令地址)压入栈中,以便函数执行完毕后能正确返回到调用点。
  • 保存调用函数的运行状态:将调用函数的寄存器状态、局部变量等信息压入栈中,以便函数调用结束后恢复。
  • 执行被调用函数:跳转到被调用函数的代码并执行。
  • 函数返回:函数执行完毕后,从栈中弹出返回地址、恢复调用函数的运行状态,并释放栈空间。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
using namespace std;

int f(int n) {
cout << n << endl;
return n;
}

void func(int param1, int param2) {
int var1 = param1;
int var2 = param2;
printf("var1=%d,var2=%d", f(var1), f(var2));
}

int main(int argc, char* argv[]) {
func(1, 2);
return 0;
}

输出:

1
2
3
2
1
var1=1,var2=2

==10.说说移动构造函数==

既要实现资源的有效转移,又要避免潜在的双重释放的风险

移动构造函数:用“转移”代替“复制”来提高性能

拷贝构造 vs 移动构造

  • 拷贝构造函数:通常涉及到深拷贝,特别是对于包含动态分配资源的对象。例如,如果一个类含有指针成员变量,那么在拷贝构造时需要为新对象分配新的内存,并将原始对象的数据复制过去。这增加了额外的时间和空间开销。
  • 移动构造函数:则是采用浅层复制的方式,直接将资源的所有权从一个对象转移到另一个对象,而不进行实际的数据复制。这样做的前提是确保源对象不再持有这些资源,以防止重复释放同一块内存导致的错误。
  • 参数类型差异
    • 拷贝构造函数:接受一个左值引用作为参数,意味着它可以接受任何有效的已有对象。
    • 移动构造函数:接受一个右值引用(或称为将亡值引用)作为参数。这意味着它只能用于那些即将销毁或不再使用的对象,比如临时对象或者通过std::move显式转换成右值的对象。

如何实现安全的资源转移?

​ 为了避免因浅层复制导致的双重释放问题,移动构造函数会在获取源对象资源的同时,将源对象中的相应指针置为nullptr(或其他适当值)。

1
2
3
4
5
6
7
8
class MyClass {
public:
MyClass(MyClass&& other) noexcept : data(other.data) {
other.data = nullptr; // 确保other不会在析构时释放data指向的内存
}
private:
int* data;
};

在这个例子中,MyClass 的移动构造函数接收一个右值引用 MyClass&&,并将other对象的数据成员data直接赋值给当前对象的data成员,然后将other.data设置为nullptr。这样做既实现了资源的有效转移,又避免了潜在的双重释放风险。

1
2
MyClass a;
MyClass b = std::move(a); // 调用移动构造函数

注:std::move(a)使得a被视为一个右值,从而触发MyClass的移动构造函数。

11.说一下C++左值引用和右值引用

左值(lvalue):

  • 可以获取地址的表达式。

  • 通常有名字,可以出现在赋值语句的左边。

  • 例如:变量、对象、函数返回的引用等。

    1
    2
    int a = 10;  // a 是左值
    int& b = a; // b 是左值引用

右值(rvalue)

  • 不能获取地址的表达式。

  • 通常是临时的、没有名字的值。

  • 例如:常量、临时对象、函数返回值(非引用)、表达式结果等。

    1
    2
    int c = 5 + 10;  // 5 + 10 是右值
    int&& d = 10; // 10 是右值
    • 纯右值(prvalue):如字面量、临时对象、表达式结果等。
    • 将亡值(xvalue):与右值引用相关的表达式,通常是通过 std::move 或返回右值引用的函数得到的值。

左值引用:用 & 声明。

右值引用:用 && 声明。主要用于实现 移动语义完美转发

右值引用的特点:

  • 延长右值的生命周期:右值引用可以将临时对象(右值)的生命周期延长到与右值引用变量的生命周期一致。

    1
    int&& d = 10;  // 临时对象 10 的生命周期被延长
  • 右值引用可能是左值或右值:右值引用类型的变量本身可能是左值或右值,取决于其初始化方式。

    1
    int&& e = 10;  // e 是右值引用,但 e 本身是左值
  • 自动类型推断中的右值引用:在模板函数中,T&& 可以是左值引用或右值引用,取决于传入的参数类型。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    template<typename T>
    void fun(T&& t) {
    cout << t << endl;
    }

    int main() {
    int a = 10;
    fun(a); // t 是左值引用
    fun(10); // t 是右值引用
    return 0;
    }

移动语义:通过 std::move 将左值强制转换为右值引用,从而避免不必要的拷贝操作。

1
2
std::vector<int> v1 = {1, 2, 3};
std::vector<int> v2 = std::move(v1); // v1 的资源被移动到 v2

完美转发:通过右值引用和 std::forward,将参数按照其原始类型转发给其他函数

1
2
3
4
template<typename T>
void wrapper(T&& arg) {
func(std::forward<T>(arg)); // 完美转发
}

12.C++中将临时变量作为返回值时的处理过程

在C++中,当函数返回临时变量时,虽然该临时变量会在函数退出时被销毁,但由于返回值是通过寄存器而非栈或堆内存进行传递的,因此返回值的正确性和完整性得到了保证。

13.静态类型和动态类型,静态绑定和动态绑定的介绍

静态类型与动态类型

  • 静态类型:对象在声明时采⽤的类型,在编译期既已确定;例如,A* pa的静态类型是A*,无论pa指向的是哪个子类对象。

  • 动态类型:指针或引用实际指向的对象的类型,是在运行期决定的。例如,A* pa = new B();pa的静态类型是A*,但动态类型是B*

静态绑定与动态绑定

  • 静态绑定:绑定的是静态类型,函数或属性的调用依赖于对象的静态类型,发生在编译期。非虚函数通常使用静态绑定。
  • 动态绑定:绑定的是动态类型,函数或属性的调用依赖于对象的动态类型,发生在运行期。虚函数使用动态绑定,从而实现多态性。

eg:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <iostream>
using namespace std;

class A {
public:
/*virtual*/ void func() { std::cout << "A::func()\n"; }
};

class B : public A {
public:
void func() { std::cout << "B::func()\n"; }
};

class C : public A {
public:
void func() { std::cout << "C::func()\n"; }
};

int main() {
C* pc = new C();
B* pb = new B();
A* pa = pc;
pa = pb;

pa->func(); // 如果func不是虚函数,则总是调用A::func()
pc->func(); // 总是调用C::func()

delete pc;
delete pb;
return 0;
}

注1:如果 A 中的 func 不是虚函数,那么 pa->func() 将总是调用 A::func(),因为这是基于 pa 的静态类型(即 `A`)进行的静态绑定。*

注2:如果将 func 声明为虚函数(取消注释 virtual),那么 pa->func() 将根据 pa 所指向的实际对象类型(即动态类型)调用相应的 func 函数,实现动态绑定。

14.引用是否能实现动态绑定,为什么可以实现?

引用指针都可以实现动态绑定,但这种动态绑定仅适用于虚函数

动态绑定的条件

  • 虚函数:只有当一个函数被声明为virtual时,才能通过基类的引用或指针调用派生类中的重写版本。这是因为虚函数支持运行期确定实际调用哪个函数,这被称为动态绑定或多态性。
  • 引用必须初始化:创建引用时必须同时初始化它,这意味着你必须指定引用所绑定的具体对象。一旦初始化完成,引用就不能再指向其他对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>
using namespace std;

class Base {
public:
virtual void fun() {
cout << "base :: fun()" << endl;
}
};

class Son : public Base {
public:
virtual void fun() {
cout << "son :: fun()" << endl;
}

void func() {
cout << "son :: not virtual function" <<endl;
}
};

int main(){
Son s;
Base& b = s; // 基类类型引用绑定到Son对象
s.fun(); // 输出: son::fun()
b.fun(); // 输出: son::fun()
return 0;
}