跳转至

C++ 常识

1 位运算

  • 按位或运算 (|):两数按位其中任意一个为 1 得 1, 2 数都为 0 得 0
  • 按位与运算 (&):两数按位都为 1 得 1,否则得 0
  • 按位异或运算 (^):两数按位不同得 1,相同得 0

2 预处理、编译、汇编、链接

2.1 预处理

主要任务: - 宏展开:将宏定义(#define)替换为实际的代码。 - 文件包含:将 #include 指令指定的文件内容插入到当前文件中。 - 条件编译:根据 #if#ifdef#ifndef#else#elif 和 #endif 指令决定哪些代码需要编译。 - 删除注释:删除源代码中的注释。

2.2 编译

模板处理主要发生在编译阶段,而不是预处理阶段。

主要任务: - 语法分析:检查代码的语法是否正确。 - 语义分析:检查代码的语义是否正确,例如类型检查。 - 代码优化:对代码进行优化,以提高执行效率。 - 生成汇编代码:将优化后的代码转换为汇编语言。

2.3 汇编

主要任务: - 生成机器代码:将汇编指令转换为机器指令。 - 生成符号表:记录代码中的符号(如函数名、变量名)及其地址。

2.4 链接

主要任务: - 符号解析:解析目标文件中的符号引用,确定它们的实际地址。 - 地址重定位:调整目标文件中的地址,使其指向正确的内存位置。 - 生成可执行文件:将所有目标文件和库文件组合成一个可执行文件。

3 5 大内存区

title: 成员变量并不能决定自身的存储空间位置。决定存储位置的对象的创建方式。
- 如果对象是函数内的非静态局部变量,则对象,对象的成员变量保存在栈区。   
- 如果对象是全局变量,则对象,对象的成员变量保存在静态区。   
- 如果对象是函数内的静态局部变量,则对象,对象的成员变量保存在静态区。   
- 如果对象是new出来的,则对象,对象的成员变量保存在堆区。
  1. 栈区(stack) -- 编译器自动分配释放,主要存放函数的参数值,局部变量值等;
  2. 堆区(heap) -- 由程序员分配释放;
  3. 全局区或静态区 -- 存放全局变量和静态变量;程序结束时由系统释放,分为全局初始化区和全局未初始化区;
  4. 字符常量区 -- 常量字符串放于此处,程序结束时由系统释放;
  5. 程序代码区 -- 存放函数体的二进制代码。

4 内存对齐

规则如下,详解: 1. 对于结构体的各个成员,第一个成员的偏移量是 0,排列在后面的成员其当前偏移量必须是当前成员类型的整数倍 2. 结构体内所有数据成员各自内存对齐后,结构体本身还要进行一次内存对齐,保证整个结构体占用内存大小是结构体内最大数据成员的最小整数倍 3. 如果结构体(联合体)中还有结构体(联合体),子结构体也按相同规则处理 4. 先处理子成员结构,再处理基本成员;子成员结构中最大成员基本数据大小作为类对齐大小。 5. 如程序中有 #pragma pack(n) 预编译指令,则所有成员对齐以 n 字节为准 (即偏移量是 n 的整数倍),不再考虑当前类型以及最大结构体内类型。

虚函数表指针也参与内存对齐,内存地址在首位,并且遵守上述规则。

5 大端、小端

大端:低地址存数据高位 小端:低地址存数据低位 判断算法:

bool checkCPU()
{//true 小端,fasle 大端
    union MyUnion
    {
        int i;
        char c;
    }u;
    u.i=1;
    return u.c == 1;
}

或者

    int ix = 0x12345678;
    std::cout << (0x78 == *((char*)&ix) ? "小端" : "大端") << std::endl;
在x86上是小端,网络上是大端

6 逗号运算符

规则如下: 1. 逗号运算符优先级最低 2. 从左往右依次计算 3. 最后一个计算值作为逗号表达式的返回值 示例 1 如下:

int x, y, z;
x = y = 1;
z = x++, y++, ++y;
printf("%d,%d,%d\n", x, y, z); // 2,3,1

解释如下: 4. 由于规则 1,等号表达式 z=x++ 优先执行,x++ 结果是 1,x 变成 2,z 结果是 1; 5. 再计算 y++,最后 ++y,y 结果是 3,整个表达式结果是 ++y 的结果,即也是 3。

示例 2 如下:

int a, b;
a = 1, a + 1, a++;
b = (a = 1, a + 1, a++);
printf("%d,%d\n", a, b);// 2,1

解释如下: 1. 小括号运算符>等号运算符>逗号运算符,先进行 a=1 运算,a+1 结果不影响 a 的值,再计算 a++ 后,a 等于 2,a++ 表达式返回 1,根据规则 3,a++ 结果作为 (a = 1, a + 1, a++) 表达式的结果,即 b=1

7 返回忘记 return 会返回什么

当一个有返回值函数,但并没有 return 返回值时,会将寄存器 rax(eax ax al)中的保存的值返回。

8 虚函数 virtual

虚函数要么定义

virtual void test(){};

要么是纯虚函数

virtual void test()=0;

但不能只声明

virtual void test();//错误的

virtual 函数是动态绑定,而缺省参数值却是静态绑定,下例输出结果 B->1。

#include<iostream>
using namespace std;
class A
{
public:
    virtual void func(int val = 1)
    { std::cout<<"A->"<<val <<std::endl;}
    virtual void test()
    { func();}
};
class B : public A
{
public:
    void func(int val=0)
    {std::cout<<"B->"<<val <<std::endl;}
};
int main(int argc ,char* argv[])
{
    B*p = new B;
    p->test();
    return 0;
}
title: 为什么C++不支持内联成员函数为虚函数?
内联函数就是为了在代码中直接展开,减少函数调用花费的代价,虚函数是为了在继承后对象能够准确的执行自己的动作,这是不可能统一的。(再说了,inline函数在编译时被展开,虚函数在运行时才能动态的绑定函数
title: 为什么C++不支持静态成员函数为虚函数?
这也很简单,静态成员函数对于每个类来说只有一份代码,所有的对象都共享这一份代码,也没有动态邦定的必要性。
title: 为什么C++不支持成员函数模板为虚函数?
因为每个包含虚函数的类具有一个virtual table(vtable),包含该类的所有虚函数的地址,因此vtable的大小是确定的。成员函数模板只有被使用时才会被实例化,将其声明为虚函数会使vtable的大小不确定(函数模板可能用到,可能用不到)。所以,成员函数模板不能为虚函数。
title: 为什么C++不支持友元函数为虚函数?
C++不支持友元函数的继承,对于没有继承特性的函数没有虚函数的说法。

8.1 纯虚函数

  1. 定义了纯虚函数的类是抽象类,派生的具体类需要实现
  2. 纯虚函数是只声明未定义的,形如 virtual ~Base()=0;
  3. 析构函数也可以是纯虚的

8.2 虚函数表

详见 https://cloud.tencent.com/developer/article/1599283

8.2.1 概述

虚函数表就是存放着当前类中所有 虚函数地址 的表。 在实例化一个具有虚函数的类时,这个表也被分配到这个实例对象的内存中,通过虚函数表可以找到所要调用的虚函数的位置。 1. 类有虚函数,类就会有一个指针指向虚函数表。 2. 在多继承子类里,每一个父类含有虚函数,子类会分别生成一个虚函数表指针指向不同的虚函数表。 3. 虚函数表结构上如一个数组,存储虚函数指针。 4. 虚函数表指针大小由 32/64 位程序影响,大小占 4/8 字节。 5. 虚函数表在类内存的首位地址上。 6. 如果子类没有重写虚函数,子类父类虚函数表中指向相同的虚函数地址。

8.2.2 获取虚函数表方式

class Base {
public:
    virtual void a() { cout << "Base a()" << endl; }
    virtual void b() { cout << "Base b()" << endl; }
    virtual void c() { cout << "Base c()" << endl; }
};

class Derive : public Base {
public:
    virtual void b() { cout << "Derive b()" << endl; }
};
Derive* p = new Derive;
long* tmp = (long*)p;             // 先将p强制转换为long类型指针tmp
// 由于tmp是虚函数表指针,那么*tmp就是虚函数表
long* vptr = (long*)(*tmp);//vptr就是虚函数表(数组结构)的首地址
typedef void(*vpFun)();//定义虚函数指针
for (int i = 0; i < 3; i++) {//3个虚函数
    printf("vptr[%d] : %p\n", i, vptr[i]);//数组形式访问表中下一个虚函数地址
    vpFun fun=(vpFun)vptr[i];
    fun();//调用虚函数
}

8.2.3 虚函数表内存布局

  1. 派生类对象在首地址存放虚函数表指针;
  2. 虚函数表类似数组,每一个数组元素是虚函数的指针;
  3. 虚函数表中先按父类虚函数声明顺序排列,如果派生类重写了虚函数,虚函数指针就指向派生类的虚函数地址;
  4. 派生类增加的虚函数,放在虚函数表末尾;
  5. 在多重继承情况下,会有多个虚函数表(前提每一个父类都是有虚函数的,没虚函数不产生);
  6. 在多重继承下按继承顺序(如 class A : public B,public C{}; 则第一个虚函数指针指向 B,第二个指向 C);
  7. 在多重继承下,派生类新增的虚函数,放在第一个虚函数表末尾。

参考 https://www.shuzhiduo.com/A/MAzAp8Ged9/

9 虚继承

虚继承(Virtual Inheritance)是 C++ 中用于解决多重继承中的菱形继承问题(Diamond Problem)的一种机制。虚继承确保派生类只继承一个基类的实例,从而避免了二义性和重复继承的问题。

9.1 菱形继承问题(Diamond Problem)

菱形继承问题是指在多重继承中,如果两个基类都继承自同一个基类,那么派生类会继承两个基类的成员,从而导致二义性和重复继承的问题。

9.1.1 示例:

#include <iostream>

class Base {
public:
    int data;
};

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

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

class FinalDerived : public Derived1, public Derived2 {
public:
    void baz() { std::cout << "FinalDerived::baz()" << std::endl; }
};

int main() {
    FinalDerived fd;
    // fd.data = 42;  // 错误:二义性
    fd.Derived1::data = 42;  // 需要显式指定基类
    fd.Derived2::data = 43;  // 需要显式指定基类
    return 0;
}

在这个例子中,FinalDerived 类从 Derived1Derived2 继承,而 Derived1Derived2 都从 Base 继承。因此,FinalDerived 类会继承两个 Base 类的 data 成员,导致二义性问题。

9.2 虚继承的解决方案

通过使用虚继承,可以确保派生类只继承一个基类的实例,从而避免二义性和重复继承的问题。

9.2.1 示例:

#include <iostream>

class Base {
public:
    int data;
};

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

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

class FinalDerived : public Derived1, public Derived2 {
public:
    void baz() { std::cout << "FinalDerived::baz()" << std::endl; }
};

int main() {
    FinalDerived fd;
    fd.data = 42;  // 不再有二义性
    return 0;
}

在这个例子中,Derived1Derived2 都使用虚继承 Base,因此 FinalDerived 类只会继承一个 Base 类的实例,从而避免了二义性问题。

9.3 虚继承的内存布局

虚继承会导致派生类的内存布局变得更加复杂。虚继承的基类在派生类中只有一个实例,但需要额外的指针来管理虚继承的基类。

9.3.1 示例:

#include <iostream>

class Base {
public:
    int data;
};

class Derived1 : virtual public Base {
public:
    int data1;
};

class Derived2 : virtual public Base {
public:
    int data2;
};

class FinalDerived : public Derived1, public Derived2 {
public:
    int data3;
};

int main() {
    FinalDerived fd;
    std::cout << "Size of FinalDerived: " << sizeof(FinalDerived) << std::endl;
    return 0;
}

在这个例子中,FinalDerived 类的内存布局如下: - Derived1 子对象(包含 data1 和虚基类指针) - Derived2 子对象(包含 data2 和虚基类指针) - Base 子对象(包含 data) - FinalDerived 自己的成员 data3

9.4 虚继承的构造函数

虚继承的基类的构造函数需要通过派生类的构造函数显式调用。虚继承的基类的构造函数不会被自动调用。

9.4.1 示例:

#include <iostream>

class Base {
public:
    Base(int d) : data(d) { std::cout << "Base constructor" << std::endl; }
    int data;
};

class Derived1 : virtual public Base {
public:
    Derived1(int d) : Base(d) { std::cout << "Derived1 constructor" << std::endl; }
};

class Derived2 : virtual public Base {
public:
    Derived2(int d) : Base(d) { std::cout << "Derived2 constructor" << std::endl; }
};

class FinalDerived : public Derived1, public Derived2 {
public:
    FinalDerived(int d) : Base(d), Derived1(d), Derived2(d) {
        std::cout << "FinalDerived constructor" << std::endl;
    }
};

int main() {
    FinalDerived fd(42);
    return 0;
}

10 构造函数与析构函数

10.1 构造析构顺序

知识点: 1. 先调用基类构造,再调用本身;先调用本身析构,再调用基类析构。 2. 如果类含有成员,按成员出现的顺序先调用成员构造。

不建议在构造函数中抛出异常。构造函数抛出异常时,析构函数将不会被执行,需要手动的去释放内存;
析构函数不应该抛出异常。当析构函数中会有一些可能发生异常时,那么就必须要把这种可能发生的异常完全封装在析构函数内部,决不能让它抛出函数之外。因为如果对象抛出异常了,异常处理模块为了维护系统对象数据的一致性,避免资源泄露,有必要调用析构函数释放资源,这时如果析构过程又出现异常,那么谁来保证新对象的资源释放呢?前面的异常还没处理完又来了新的异常,这样可能会陷入无限的递归嵌套中。所以,从析构函数抛出异常,C++运行时系统会处于无法决断的境遇,因此C++语言担保,当处于这一点时,会调用 terminate()来杀死进程。

10.2 构造函数、析构函数注意事项

  1. 构造函数不能是虚函数
  2. 需要继承的类析构函数最好设置为虚的
title: 为什么构造函数不能是虚函数?
1. 虚函数表指针是存在对象中的,构造函数还没调用,类对象还没有,也就无法使用虚函数表,这样是矛盾的。
2. 虚函数的作用是使运行时调用函数,但构造函数构造的类对象是在编译期就确定的,所以构造函数不需要虚的。
title: 虚析构函数作用
可以通过delete父类而调用派生类的析构,从而避免内存泄漏,资源释放不干净。

10.3 构造函数初始化列表

  • 成员初始化顺序按 类中出现顺序 进行,与 初始化列表顺序无关
  • 需要在初始化列表中进行初始化的情况
  • 非 static 或 const 成员
  • 引用成员
  • 成员对象不含默认构造函数

示例 https://www.nowcoder.com/questionTerminal/da5c9884bc824b72a345c8fdfb53b79b

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

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

12 const 函数与同名的非 const 函数是重载函数

类的 const 对象只能调用 const 函数,非 const 对象可以调用 const 函数和非 const 成员函数

12.1 const 对象不能调用非 const 成员函数

int main()
{
    class A
    {
        public:
        A() :i(0){}
        A(int j) :i(j) {}
        virtual ~A() {}
        void fun1() {};
        void fun2() const{};
        int i;
    };
    const A a;
    a.fun1();//error
    a.fun2();//successful
}

13 this

13.1 this 是一个 const 指针

  • 为什么 this 需要是 const 呢?假设 this 不是 const,则 this 可以被赋值如 this=NULL;,显然这是不能出现的

14 private、protected、public

  • 成员关系
  • private、protected 成员在类外都不可访问
  • protected 在派生类中可访问,private 派生类中不可访问
  • 继承关系
  • public 继承,派生类继承的成员可见性与父类一致;
  • protected 继承,将基类的公有成员和保护成员变成自己的保护成员
  • private 继承,将基类的公有成员和保护成员变成自己的私有成员。

15 友元

  1. 友元关系是单向的,即 A 是 B 的友元,但 B 不是 A 的友元。
  2. 友元关系不能被继承,即父类 A 是 B 的友元,但子类 C 不是 B 的友元。

15.1 友元函数

class MyClass {
private:
    int privateData;
public:
    MyClass(int data) : privateData(data) {}
    friend void FriendFunction(MyClass& obj); // 声明友元函数
};

void FriendFunction(MyClass& obj) {
    // 友元函数可以访问私有成员
    std::cout << "FriendFunction: privateData = " << obj.privateData << std::endl;
}

int main() {
    MyClass myObject(42);
    FriendFunction(myObject); // 调用友元函数
    return 0;
}

15.2 友元类

class MyClass {
private:
    int privateData;
public:
    MyClass(int data) : privateData(data) {}
    friend class FriendClass; // 声明友元类
};

class FriendClass {
public:
    void AccessPrivateData(MyClass& obj) {
        // 友元类可以访问私有成员
        std::cout << "FriendClass: privateData = " << obj.privateData << std::endl;
    }
};

int main() {
    MyClass myObject(42);
    FriendClass friendObj;
    friendObj.AccessPrivateData(myObject); // 调用友元类的函数
    return 0;
}

16 override、final、import、module

  • override:用于说明虚函数重写
  • final:用于类表示此类将不再能被继承;用于虚函数表示此虚函数不再被重写。
  • 示例 1
struct Base
{
    virtual void foo();
};

struct A : Base
{
    virtual void foo() final; // A::foo is final
    void bar() final; // Error: non-virtual function cannot be final
};

struct B final : A // struct B is final
{
    void foo(); // Error: foo cannot be overridden as it's final in A
};

struct C : B // Error: B is final
{
};
  • 示例 2
struct A
{
    virtual void foo();
    void bar();
};

struct B : A
{
    void foo() const override; // Error: B::foo does not override A::foo
                               // (signature mismatch)
    void foo() override; // OK: B::foo overrides A::foo
    void bar() override; // Error: A::bar is not virtual
};
override、final、import、module不是保留字(关键字),而是标识符。

17 返回函数的函数

  • 返回函数的函数
int (*F(int))(int , int);
  • 返回函数的函数指针
//F是函数指针,函数含1个int参数,返回值是一个函数指针,这个函数指针含2个int参数,返回值是int
int (*(*F)(int))(int , int);

18 函数调用栈

程序在运行期间,内存中有一块区域,用来实现程序的函数调用机制。这块区域是一块 LIFO 的数据结构区域,我们可以叫函数栈(调用栈)。每个未退出的函数都会在函数栈中拥有一块数据区,我们叫函数的栈帧。函数的调用栈帧中,保存了相应的函数的一些重要信息:函数中使用的局部变量,函数的参数,另外还有一些维护函数栈所需要的数据,比如 EBP 指针,函数的返回地址。 - 入栈顺序 1. 参数 2. 返回地址 3. 局部变量

参考 https://zhuanlan.zhihu.com/p/59479513

19 const 常量的编译器优化

代码如下:

const int a = 1;
const int* p = &a;
auto q = const_cast<int*>(p);
cout << a << endl;
*q = 2;
cout << a << endl;
cout << *q << endl;

volatile const int b = 1;
volatile const int* t = &b;
auto r = const_cast<int*>(t);
cout << b << endl;
*r = 2;
cout << b << endl;
cout << *r << endl;

输出结果如下:

1
1
2
1
2
2

解释: 1. 通过 p 确实间接修改了 a 内存地址上的数据,但由于编译器优化,a 在编译阶段其在上下文中已经被替换,等同于 宏,因此 a 输出依然是 1。 2. 给 const 前加上 volatile,编译器将不会对 a 进行优化,因此 a 的值被改变了。

20 常量折叠

概念:常量折叠说的是,在编译阶段,对该变量进行值替换,同时,该常量拥有自己的内存空间,并非像宏定义一样不分配空间。 如下示例输出结果:a = 10, *p = 20

#include <iostream> 
using namespace std;
int main(void)
{
    const int a = 10;
    int* p = (int*)(&a);
    *p = 20;
    cout << "a = " << a << ", *p = " << *p << endl;
    return 0;
}

21 static

  1. 静态函数和全局静态变量的作用域是当前文件
  2. 静态函数和全局静态变量会覆盖外部同名函数或变量
  3. 静态变量或函数放在头文件会导致包含它的文件都有相同的拷贝(功能一致,但地址不一样)
非静态函数默认是extern的
静态函数和静态变量可以达到避免链接时报重复链接错误。
title: 是否在头文件定义静态函数或静态变量?
在头文件放静态函数或静态变量没有实际意义,如果其他文件包含了这个头文件,这些文件都会有一份这个拷贝,这和inline一样,放入源文件才能真正达到原本的目的。

22 禁止类在栈或堆上分配内存的方法

22.1 禁止栈上分配

原理:栈上构造对象时,编译器会检测类析构是否可访问;故将析构设为 privateprotected; 类中实现专门调用析构的函数

class A
{
protected:
    A(){}
    ~A(){}
public:
    static A* create()
    {
        return new A();
    }
    void destory()
    {
        delete this;
    }
};

22.2 禁止堆上分配

原理:在堆上分配内存会调用类中 operate new,重写 operate new 并将其设为私用。

class A
{
private:
    void* operator new(size_t t){}     // 注意函数的第一个参数和返回值都是固定的
    void operator delete(void* ptr){}  // 重载了new就需要重载delete
public:
    A(){}
    ~A(){}
};

23 数组

  1. 不能作自增、自减等操作,如 ++、--,但能 +n 或 -n
  2. 数组名指代数组首地址
  3. 数组作为参数会自动转换为指针,不管数组元素多少个。
  4. 数组坑 1,看示例
int a[5]={1,2,3,4,5};
int *ptr=(int *)(&a+1);
printf("%d",*(ptr-1));

解释如下:这里 &a+1 并不是数组的首地址 a+1,因为 &a 是指向数组的指针,其类型为 int(* )[5]。而指针加 1 要根据指针类型加上一定的值,不同类型的指针 +1 之后增加的大小不同,a 是长度为 5 的 int 数组指针,所以要加 5 * sizeof(int),所以 ptr 指向的位置是 a+5。但是 ptr 与(&a+1)类型是不一样的,所以 ptr-1 只会减去 sizeof(int*)。

title: 数组是const指针
char a[]等价于char *const a,这也是为什么数组不能自加自减的原因

23.1 数组指针、指针数组

  • 数组指针:一个指针指向数组
  • 指针数组:一个数组存储的是指针
char (*p_Arr)[]={'a',1,2};//数组指针
char *Arr_p[]={"helle","world"};//指针数组
title: 数组指针、指针数组判断技巧
1. 首先 `[]`优先级大于`*`
2. char `*Arr_p[]`中先`Arr_p[]`是一个数组,数组元素是`char*`
3. char `(*p_Arr)[]`中先`*p_Arr`是一个指针,指向`char []`数组

24 野指针、悬空指针

  • 野指针:是指向“垃圾”内存的指针,产生原因如下
  • 指针创建时未初始化,指针会指向一个不合法的内存
  • delete 指针后,未设置指针为 NULL
  • 指针操作超越了变量的作用域范围
  • 悬空指针:指针指向一块已被释放了的内存,如局部变量指针赋值给全局指针,出了局部变量作用域,全局指针指向的内存已被释放。

25 new 和 malloc 的对比

  1. new/delete 是 C++ 操作符 (运算符),malloc/free 是 C/C++ 函数。
  2. 使用 new 操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算,而 malloc 则需要显式地指出所需内存的大小。
  3. new/delete 会调用对象的构造函数/析构函数以完成对象的构造/析构,而 malloc 只负责分配空间。
  4. new 操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故 new 是符合类型安全性的操作符。而 malloc 内存分配成功则是返回 void * ,需要通过强制类型转换将 void* 指针转换成我们需要的类型。
  5. 效率上:malloc 的效率高一点,因为只分配了空间。
  6. operator new /operator delete 可以被重载,而 malloc/free 并不允许重载。

26 内存泄漏检测工具

  1. valgrind
  2. mtrace

27 main 函数执行前操作

  1. 全局对象的构造函数在 main 函数之前调用,析构函数在 main 函数之后调用。
  2. 局部栈对象在定义的时候调用构造函数,出了可见范围的时候调用析构函数。
  3. 堆对象在 new 的时候调用构造函数,delete 的时候调用析构。
  4. 全局静态对象和全局对象一样。
  5. 局部静态对象在定义的时候调用构造,main 函数之后调用析构

28 编译时多态性,运行时多态性

  1. 编译时多态性(静态多态):通过重载函数实现:先期联编 early binding.
  2. 运行时多态性(动态多态):通过虚函数实现:滞后联编 late binding.

29 模板的实现原理

模板是编译器支持实现的。编译器识别具体化模板后生成相应的类或函数,当没有一个模板被实例化时,编译器也不会产生任何类或函数。

30 异常机制

31 IO 缓存区

  • 缓冲区 (buffer),它是内存空间的一部分。 也就是说,在内存空间中预留了一定的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间就叫做缓冲区,显然缓冲区是具有一定大小的。
  • 缓冲区根据其对应的是输入设备还是输出设备,分为输入缓冲区和输出缓冲区。

31.1 缓冲区的刷新条件

  1. 缓冲区满时
  2. 执行 flush 语句
  3. 执行 endl 语句
  4. 关闭文件

31.2 Cache(缓存)和 Buffer(缓冲)区别

  1. cache 是为了弥补高速设备和低速设备的鸿沟而引入的中间层,最终起到 加快访问速度 的作用。而 buffer 的主要目的进行流量整形,把突发的大数量较小规模的 I/O 整理成平稳的小数量较大规模的 I/O,以 减少响应次数(比如从网上下电影,你不能下一点点数据就写一下硬盘,而是积攒一定量的数据以后一整块一起写,不然硬盘都要被你玩坏了)
  2. A buffer is something that has yet to be "written" to disk.A cache is something that has been "read" from the disk and stored for later use.
  3. buffer 是用于存放将要输出到 disk(块设备)的数据,而 cache 是存放从 disk 上读出的数据。二者都是为提高 IO 性能而设计的。 参考 https://www.zhihu.com/question/26190832

32 strcpy 和 memcpy 区别

  1. 复制的内容不同。strcpy 只能复制字符串,而 memcpy 可以复制任意内容,例如字符数组、整型、结构体、类等。
  2. 复制的方法不同。strcpy 不需要指定长度,它遇到被复制字符的串结束符 "\0" 才结束,所以容易溢出。memcpy 则是根据其第 3 个参数决定复制的长度。
  3. 用途不同。通常在复制字符串时用 strcpy,而需要复制其他类型数据时则一般用 memcpy

33 运算符优先级