C++

编译内存相关

C++ 程序编译过程

编译过程分为四个过程:编译(编译预处理、编译、优化),汇编,链接。

  • 编译预处理:处理以 # 开头的指令;

  • 编译、优化:将源码 .cpp 文件翻译成 .s 汇编代码;

  • 汇编:将汇编代码 .s 翻译成机器指令 .o 文件;

  • 链接:汇编程序生成的目标文件,即 .o 文件,并不会立即执行,因为可能会出现:.cpp 文件中的函数引用了另一个 .cpp 文件中定义的符号或者调用了某个库文件中的函数。那链接的目的就是将这些文件对应的目标文件连接成一个整体,从而生成可执行的程序 .exe 文件。

imagepng

链接分为两种:

  • 静态链接:代码从其所在的静态链接库中拷贝到最终的可执行程序中,在该程序被执行时,这些代码会被装入到该进程的虚拟地址空间中。

  • 动态链接:代码被放到动态链接库或共享对象的某个目标文件中,链接程序只是在最终的可执行程序中记录了共享对象的名字等一些信息。在程序执行时,动态链接库的全部内容会被映射到运行时相应进行的虚拟地址的空间。

二者的优缺点:

  • 静态链接:浪费空间,每个可执行程序都会有目标文件的一个副本,这样如果目标文件进行了更新操作,就需要重新进行编译链接生成可执行程序(更新困难);优点就是执行的时候运行速度快,因为可执行程序具备了程序运行的所有内容。

  • 动态链接:节省内存、更新方便,但是动态链接是在程序运行时,每次执行都需要链接,相比静态链接会有一定的性能损失。

C++ 内存管理

C++ 内存分区:栈、堆、全局/静态存储区、常量存储区、代码区。

  • 栈:存放函数的局部变量、函数参数、返回地址等,由编译器自动分配和释放。

  • 堆:动态申请的内存空间,就是由 malloc 分配的内存块,由程序员控制它的分配和释放,如果程序执行结束还没有释放,操作系统会自动回收。

  • 全局区/静态存储区(.bss 段和 .data 段):存放全局变量和静态变量,程序运行结束操作系统自动释放,在 C 语言中,未初始化的放在 .bss 段中,初始化的放在 .data 段中,C++ 中不再区分了。

  • 常量存储区(.data 段):存放的是常量,不允许修改,程序运行结束自动释放。

  • 代码区(.text 段):存放代码,不允许修改,但可以执行。编译后的二进制文件存放在这里。

说明:从操作系统的本身来讲,以上存储区在内存中的分布是如下形式(从低地址到高地址):.text 段 --> .data 段 --> .bss 段 --> 堆 --> unused --> 栈 --> env

#include <iostream>
using namespace std;

/*
说明:C++ 中不再区分初始化和未初始化的全局变量、静态变量的存储区,如果非要区分下述程序标注在了括号中
*/

int g_var = 0; // g_var 在全局区(.data 段)
char *gp_var;  // gp_var 在全局区(.bss 段)

int main()
{
    int var;                    // var 在栈区
    char *p_var;                // p_var 在栈区
    char arr[] = "abc";         // arr 为数组变量,存储在栈区;"abc"为字符串常量,存储在常量区
    char *p_var1 = "123456";    // p_var1 在栈区;"123456"为字符串常量,存储在常量区
    static int s_var = 0;       // s_var 为静态变量,存在静态存储区(.data 段)
    p_var = (char *)malloc(10); // 分配得来的 10 个字节的区域在堆区
    free(p_var);
    return 0;
}

栈和堆的区别

  • 申请方式:栈是系统自动分配,堆是程序员主动申请。

  • 申请后系统响应:分配栈空间,如果剩余空间大于申请空间则分配成功,否则分配失败栈溢出;申请堆空间,堆在内存中呈现的方式类似于链表(记录空闲地址空间的链表),在链表上寻找第一个大于申请空间的节点分配给程序,将该节点从链表中删除,大多数系统中该块空间的首地址存放的是本次分配空间的大小,便于释放,将该块空间上的剩余空间再次连接在空闲链表上。

  • 栈在内存中是连续的一块空间(向低地址扩展)最大容量是系统预定好的,堆在内存中的空间(向高地址扩展)是不连续的。

  • 申请效率:栈是有系统自动分配,申请效率高,但程序员无法控制;堆是由程序员主动申请,效率低,使用起来方便但是容易产生碎片。

  • 存放的内容:栈中存放的是局部变量,函数的参数;堆中存放的内容由程序员控制。

变量的区别

全局变量、局部变量、静态全局变量、静态局部变量的区别

C++ 变量根据定义的位置的不同的生命周期,具有不同的作用域,作用域可分为 6 种:全局作用域,局部作用域,语句作用域,类作用域,命名空间作用域和文件作用域。

从作用域看:

  • 全局变量:具有全局作用域。全局变量只需在一个源文件中定义,就可以作用于所有的源文件。当然,其他不包含全局变量定义的源文件需要用 extern 关键字再次声明这个全局变量。

  • 静态全局变量:具有文件作用域。它与全局变量的区别在于如果程序包含多个文件的话,它作用于定义它的文件里,不能作用到其它文件里,即被 static 关键字修饰过的变量具有文件作用域。这样即使两个不同的源文件都定义了相同名字的静态全局变量,它们也是不同的变量。

  • 局部变量:具有局部作用域。它是自动对象(auto),在程序运行期间不是一直存在,而是只在函数执行期间存在,函数的一次调用执行结束后,变量被撤销,其所占用的内存也被收回。

  • 静态局部变量:具有局部作用域。它只被初始化一次,自从第一次被初始化直到程序运行结束都一直存在,它和全局变量的区别在于全局变量对所有的函数都是可见的,而静态局部变量只对定义自己的函数体始终可见。

从分配内存空间看:

  • 静态存储区:全局变量,静态局部变量,静态全局变量。

  • 栈:局部变量。

说明:

  • 静态变量和栈变量(存储在栈中的变量)、堆变量(存储在堆中的变量)的区别:静态变量会被放在程序的静态数据存储区(.data 段)中(静态变量会自动初始化),这样可以在下一次调用的时候还可以保持原来的赋值。而栈变量或堆变量不能保证在下一次调用的时候依然保持原来的值。

  • 静态变量和全局变量的区别:静态变量用 static 告知编译器,自己仅仅在变量的作用范围内可见。

全局变量定义在头文件中有什么问题?

如果在头文件中定义全局变量,当该头文件被多个文件 include 时,该头文件中的全局变量就会被定义多次,导致重复定义,因此不能再头文件中定义全局变量。

对象创建限制在堆或栈

如何限制类的对象只能在堆上创建?如何限制对象只能在栈上创建?

说明:C++ 中的类的对象的建立分为两种:静态建立、动态建立。

  • 静态建立:由编译器为对象在栈空间上分配内存,直接调用类的构造函数创建对象。例如:A a;、

  • 动态建立:使用 new 关键字在堆空间上创建对象,底层首先调用 operator new() 函数,在堆空间上寻找合适的内存并分配;然后,调用类的构造函数创建对象。例如:A *p = new A();

限制对象只能建立在堆上:

  • 最直观的思想:避免直接调用类的构造函数,因为对象静态建立时,会调用类的构造函数创建对象。但是直接将类的构造函数设为私有并不可行,因为当构造函数设置为私有后,不能在类的外部调用构造函数来构造对象,只能用 new 来建立对象。但是由于 new 创建对象时,底层也会调用类的构造函数,将构造函数设置为私有后,那就无法在类的外部使用 new 创建对象了。因此,这种方法不可行。

  • 解决方法 1:

    • 将析构函数设置为私有。原因:静态对象建立在栈上,是由编译器分配和释放内存空间,编译器为对象分配内存空间时,会对类的非静态函数进行检查,即编译器会检查析构函数的访问性。当析构函数设为私有时,编译器创建的对象就无法通过访问析构函数来释放对象的内存空间,因此,编译器不会在栈上为对象分配内存。
    class A
    {
    public:
        A() {}
        void destory()
        {
            delete this;
        }
    
    private:
        ~A()
        {
        }
    };
    
  • 该方法存在的问题:

    • 用 new 创建的对象,通常会使用 delete 释放该对象的内存空间,但此时类的外部无法调用析构函数,因此类内必须定义一个 destory() 函数,用来释放 new 创建的对象

    • 无法解决继承问题,因为如果这个类作为基类,析构函数要设置成 virtual,然后在派生类中重写该函数,来实现多态。但此时,析构函数是私有的,派生类中无法访问。

  • 解决方法 2

    • 构造函数设置为 protected,并提供一个 public 的静态函数来完成构造,而不是在类的外部使用 new 构造;将析构函数设置为 protected。原因:类似于单例模式,也保证了在派生类中能够访问析构函数。通过调用 create() 函数在堆上创建对象。
    class A
    {
    protected:
        A() {}
        ~A() {}
    
    public:
        static A *create()
        {
            return new A();
        }
        void destory()
        {
            delete this;
        }
    };
    
限制对象只能建立在栈上:

* 解决方法:将 operator new() 设置为私有。原因:当对象建立在堆上时,是采用 new 的方式进行建立,其底层会调用 operator new() 函数,因此只要对该函数加以限制,就能够防止对象建立在堆上。

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

内存对齐

什么是内存对齐?内存对齐的原则?为什么要进行内存对齐,有什么优点?

内存对齐:编译器将程序中的每个“数据单元”安排在字的整数倍的地址指向的内存之中
内存对齐的原则:

  1. 结构体变量的首地址能够被其最宽基本类型成员大小与对齐基数中的较小者所整除;

  2. 结构体每个成员相对于结构体首地址的偏移量 (offset) 都是该成员大小与对齐基数中的较小者的整数倍,如有需要编译器会在成员之间加上填充字节 (internal padding);

  3. 结构体的总大小为结构体最宽基本类型成员大小与对齐基数中的较小者的整数倍,如有需要编译器会在最末一个成员之后加上填充字节 (trailing padding)。

#include <iostream>

using namespace std;

struct A
{
    short var; // 2 字节
    int var1;  // 8 字节 (内存对齐原则:填充 2 个字节) 2 (short) + 2 (填充) + 4 (int)= 8
    long var2; // 12 字节 8 + 4 (long) = 12
    char var3; // 16 字节 (内存对齐原则:填充 3 个字节)12 + 1 (char) + 3 (填充) = 16
    string s;  // 48 字节 16 + 32 (string) = 48
};

int main()
{
    short var;
    int var1;
    long var2;
    char var3;
    string s;
    A ex1;
    cout << sizeof(var) << endl;  // 2 short
    cout << sizeof(var1) << endl; // 4 int
    cout << sizeof(var2) << endl; // 4 long
    cout << sizeof(var3) << endl; // 1 char
    cout << sizeof(s) << endl;    // 32 string
    cout << sizeof(ex1) << endl;  // 48 struct
    return 0;
}

进行内存对齐的原因:(主要是硬件设备方面的问题)

  1. 某些硬件设备只能存取对齐数据,存取非对齐的数据可能会引发异常;

  2. 某些硬件设备不能保证在存取非对齐数据的时候的操作是原子操作;

  3. 相比于存取对齐的数据,存取非对齐的数据需要花费更多的时间;

  4. 某些处理器虽然支持非对齐数据的访问,但会引发对齐陷阱(alignment trap);

  5. 某些硬件设备只支持简单数据指令非对齐存取,不支持复杂数据指令的非对齐存取。

内存对齐的优点:

  1. 便于在不同的平台之间进行移植,因为有些硬件平台不能够支持任意地址的数据访问,只能在某些地址处取某些特定的数据,否则会抛出异常;

  2. 提高内存的访问效率,因为 CPU 在读取内存时,是一块一块的读取。

类的大小

类大小的计算
说明:类的大小是指类的实例化对象的大小,用 sizeof 对类型名操作时,结果是该类型的对象的大小。
计算原则:

  • 遵循结构体的对齐原则。

  • 与普通成员变量有关,与成员函数和静态成员无关。即普通成员函数,静态成员函数,静态数据成员,静态常量数据成员均对类的大小无影响。因为静态数据成员被类的对象共享,并不属于哪个具体的对象。

  • 虚函数对类的大小有影响,是因为虚函数表指针的影响。

  • 虚继承对类的大小有影响,是因为虚基表指针带来的影响。

  • 空类的大小是一个特殊情况,空类的大小为 1,当用 new 来创建一个空类的对象时,为了保证不同对象的地址不同,空类也占用存储空间。

#include <iostream>

using namespace std;

class A
{
private:
    static int s_var; // 不影响类的大小
    const int c_var;  // 4 字节
    int var;          // 8 字节 4 + 4 (int) = 8
    char var1;        // 12 字节 8 + 1 (char) + 3 (填充) = 12
public:
    A(int temp) : c_var(temp) {} // 不影响类的大小
    ~A() {}                    // 不影响类的大小
};

class B
{
};
int main()
{
    A ex1(4);
    B ex2;
    cout << sizeof(ex1) << endl; // 12 字节
    cout << sizeof(ex2) << endl; // 1 字节
    return 0;
}

实例:
简单情况和空类情况

#include <iostream>

using namespace std;

class A
{
private:
    static int s_var; // 不影响类的大小
    const int c_var;  // 4 节
    int var;          // 8 字节 4 + 4 (int) = 8
    char var1;        // 12 字节 8 + 1 (char) + 3 (填充) = 12
public:
    A(int temp) : c_var(temp) {} // 不影响类的大小
    ~A() {}                    // 不影响类的大小
};

class B
{
};
int main()
{
    A ex1(4);
    B ex2;
    cout << sizeof(ex1) << endl; // 12 字节
    cout << sizeof(ex2) << endl; // 1 字节
    return 0;
}

带有虚函数的情况:(注意:虚函数的个数并不影响所占内存的大小,因为类对象的内存中只保存了指向虚函数表的指针。

#include <iostream>

using namespace std;

class A
{
private:
    static int s_var; // 不影响类的大小
    const int c_var;  // 4 字节
    int var;          // 8 字节 4 + 4 (int) = 8
    char var1;        // 12 字节 8 + 1 (char) + 3 (填充) = 12
public:
    A(int temp) : c_var(temp) {} // 不影响类的大小
    ~A() {}                      // 不影响类的大小
    virtual void f() { cout << "A::f" << endl; }

    virtual void g() { cout << "A::g" << endl; }

    virtual void h() { cout << "A::h" << endl; } // 24 字节 12 + 4 (填充) + 8 (指向虚函数的指针) = 24
};

int main()
{
    A ex1(4);
    A *p;
    cout << sizeof(p) << endl;   // 8 字节 注意:指针所占的空间和指针指向的数据类型无关
    cout << sizeof(ex1) << endl; // 24 字节
    return 0;
}

什么是内存泄露

内存泄漏:由于疏忽或错误导致的程序未能释放已经不再使用的内存。
进一步解释:

  • 并非指内存从物理上消失,而是指程序在运行过程中,由于疏忽或错误而失去了对该内存的控制,从而造成了内存的浪费。

  • 常指堆内存泄漏,因为堆是动态分配的,而且是用户来控制的,如果使用不当,会产生内存泄漏。

  • 使用 malloc、calloc、realloc、new 等分配内存时,使用完后要调用相应的 free 或 delete 释放内存,否则这块内存就会造成内存泄漏。

指针重新赋值

char *p = (char *)malloc(10);
char *p1 = (char *)malloc(10);
p = np;

开始时,指针 pp1 分别指向一块内存空间,但指针 p 被重新赋值,导致 p 初始时指向的那块内存空间无法找到,从而发生了内存泄漏。

怎么防止内存泄漏?内存泄漏检测工具的原理?

防止内存泄漏的方法:

内部封装

将内存的分配和释放封装到类中,在构造的时候申请内存,析构的时候释放内存。

#include <iostream>
#include <cstring>

using namespace std;

class A
{
private:
    char *p;
    unsigned int p_size;

public:
    A(unsigned int n = 1) // 构造函数中分配内存空间
    {
        p = new char[n];
        p_size = n;
    };
    ~A() // 析构函数中释放内存空间
    {
        if (p != NULL)
        {
            delete[] p; // 删除字符数组
            p = NULL;   // 防止出现野指针
        }
    };
    char *GetPointer()
    {
        return p;
    };
};
void fun()
{
    A ex(100);
    char *p = ex.GetPointer();
    strcpy(p, "Test");
    cout << p << endl;
}
int main()
{
    fun();
    return 0;
}

说明:但这样做并不是最佳的做法,在类的对象复制时,程序会出现同一块内存空间释放两次的情况,请看如下程序:

void fun1()
{
    A ex(100);
    A ex1 = ex; 
    char *p = ex.GetPointer();
    strcpy(p, "Test");
    cout << p << endl;
}

简单解释:对于 fun1 这个函数中定义的两个类的对象而言,在离开该函数的作用域时,会两次调用析构函数来释放空间,但是这两个对象指向的是同一块内存空间,所以导致同一块内存空间被释放两次,可以通过增加计数机制来避免这种情况,看如下程序

#include <iostream>
 #include <cstring>

 using namespace std;
 class A
 {
 private:
     char *p;
     unsigned int p_size;
     int *p_count; // 计数变量
 public:
     A(unsigned int n = 1) // 在构造函数中申请内存
     {
         p = new char[n];
         p_size = n;
         p_count = new int;
         *p_count = 1;
         cout << "count is : " << *p_count << endl;
     };
     A(const A &temp)
     {
         p = temp.p;
         p_size = temp.p_size;
         p_count = temp.p_count;
         (*p_count)++; // 复制时,计数变量 +1
         cout << "count is : " << *p_count << endl;
     }
     ~A()
     {
         (*p_count)--; // 析构时,计数变量 -1
         cout << "count is : " << *p_count << endl; 

         if (*p_count == 0) // 只有当计数变量为 0 的时候才会释放该块内存空间
         {
             cout << "buf is deleted" << endl;
             if (p != NULL) 
             {
                 delete[] p; // 删除字符数组
                 p = NULL;   // 防止出现野指针
                 if (p_count != NULL)
                 {
                     delete p_count;
                     p_count = NULL;
                 }
             }
         }
     };
     char *GetPointer()
     {
         return p;
     };
 };
 void fun()
 {
     A ex(100);
     char *p = ex.GetPointer();
     strcpy(p, "Test");
     cout << p << endl;

     A ex1 = ex; // 此时计数变量会 +1
     cout << "ex1.p = " << ex1.GetPointer() << endl;
 }
 int main()
 {
     fun();
     return 0;
 }

程序运行结果:

count is : 1
Test
count is : 2
ex1.p = Test
count is : 1
count is : 0
buf is deleted

解释下:程序运行结果的倒数 2、3 行是调用两次析构函数时进行的操作,在第二次调用析构函数时,进行内存空间的释放,从而会有倒数第 1 行的输出结果。

智能指针

智能指针是 C++ 中已经对内存泄漏封装好了一个工具,可以直接拿来使用,将在下一个问题中对智能指针进行详细的解释。

内存泄漏检测工具的实现原理:

内存检测工具有很多,这里重点介绍下 valgrind 。

imagepng

valgrind 是一套 Linux 下,开放源代码(GPL V2)的仿真调试工具的集合,包括以下工具:

  • Memcheck:内存检查器(valgrind 应用最广泛的工具),能够发现开发中绝大多数内存错误的使用情况,比如:使用未初始化的内存,使用已经释放了的内存,内存访问越界等。

  • Callgrind:检查程序中函数调用过程中出现的问题。

  • Cachegrind:检查程序中缓存使用出现的问题。

  • Helgrind:检查多线程程序中出现的竞争问题。

  • Massif:检查程序中堆栈使用中出现的问题。

  • Extension:可以利用 core 提供的功能,自己编写特定的内存调试工具。

Memcheck 能够检测出内存问题,关键在于其建立了两个全局表:

  • Valid-Value 表:对于进程的整个地址空间中的每一个字节(byte),都有与之对应的 8 个 bits ;对于 CPU 的每个寄存器,也有一个与之对应的 bit 向量。这些 bits 负责记录该字节或者寄存器值是否具有有效的、已初始化的值。

  • Valid-Address 表:对于进程整个地址空间中的每一个字节(byte),还有与之对应的 1 个 bit,负责记录该地址是否能够被读写。

检测原理:

  • 当要读写内存中某个字节时,首先检查这个字节对应的 Valid-Address 表中对应的 bit。如果该 bit 显示该位置是无效位置,Memcheck 则报告读写错误。

  • 内核(core)类似于一个虚拟的 CPU 环境,这样当内存中的某个字节被加载到真实的 CPU 中时,该字节在 Valid-Value 表对应的 bits 也被加载到虚拟的 CPU 环境中。一旦寄存器中的值,被用来产生内存地址,或者该值能够影响程序输出,则 Memcheck 会检查 Valid-Value 表对应的 bits,如果该值尚未初始化,则会报告使用未初始化内存错误。

智能指针有哪几种?智能指针的实现原理?

智能指针是为了解决动态内存分配时带来的内存泄漏以及多次释放同一块内存空间而提出的。C++11 中封装在了 头文件中。

C++11 中智能指针包括以下三种:

  • 共享指针(shared_ptr):资源可以被多个指针共享,使用计数机制表明资源被几个指针共享。通过 use_count() 查看资源的所有者的个数,可以通过 unique_ptr、weak_ptr 来构造,调用 release() 释放资源的所有权,计数减一,当计数减为 0 时,会自动释放内存空间,从而避免了内存泄漏。

  • 独占指针(unique_ptr):独享所有权的智能指针,资源只能被一个指针占有,该指针不能拷贝构造和赋值。但可以进行移动构造和移动赋值构造(调用 move() 函数),即一个 unique_ptr 对象赋值给另一个 unique_ptr 对象,可以通过该方法进行赋值。

  • 弱指针(weak_ptr):指向 share_ptr 指向的对象,能够解决由shared_ptr带来的循环引用问题。

智能指针的实现原理: 计数原理。

#include <iostream>
#include <memory>

template <typename T>
class SmartPtr
{
private : 
    T *_ptr;
    size_t *_count;

public:
    SmartPtr(T *ptr = nullptr) : _ptr(ptr)
    {
        if (_ptr)
        {
            _count = new size_t(1);
        }
        else
        {
            _count = new size_t(0);
        }
    }

    ~SmartPtr()
    {
        (*this->_count)--;
        if (*this->_count == 0)
        {
            delete this->_ptr;
            delete this->_count;
        }
    }

    SmartPtr(const SmartPtr &ptr) // 拷贝构造:计数 +1
    {
        if (this != &ptr)
        {
            this->_ptr = ptr._ptr;
            this->_count = ptr._count;
            (*this->_count)++;
        }
    }

    SmartPtr &operator=(const SmartPtr &ptr) // 赋值运算符重载 
    {
        if (this->_ptr == ptr._ptr)
        {
            return *this;
        }
        if (this->_ptr) // 将当前的 ptr 指向的原来的空间的计数 -1
        {
            (*this->_count)--;
            if (this->_count == 0)
            {
                delete this->_ptr;
                delete this->_count;
            }
        }
        this->_ptr = ptr._ptr;
        this->_count = ptr._count;
        (*this->_count)++; // 此时 ptr 指向了新赋值的空间,该空间的计数 +1
        return *this;
    }

    T &operator*()
    {
        assert(this->_ptr == nullptr);
        return *(this->_ptr);
    }

    T *operator->()
    {
        assert(this->_ptr == nullptr);
        return this->_ptr;
    }

    size_t use_count()
    {
        return *this->count;
    }
};

一个 unique_ptr 怎么赋值给另一个 unique_ptr 对象?

借助 std::move() 可以实现将一个 unique_ptr 对象赋值给另一个 unique_ptr 对象,其目的是实现所有权的转移。

// A 作为一个类 
std::unique_ptr<A> ptr1(new A());
std::unique_ptr<A> ptr2 = std::move(ptr1);

使用智能指针会出现什么问题?怎么解决?

智能指针可能出现的问题:循环引用
在如下例子中定义了两个类 Parent、Child,在两个类中分别定义另一个类的对象的共享指针,由于在程序结束后,两个指针相互指向对方的内存空间,导致内存无法释放。

#include <iostream>
#include <memory>

using namespace std;

class Child;
class Parent;

class Parent {
private:
    shared_ptr<Child> ChildPtr;
public:
    void setChild(shared_ptr<Child> child) {
        this->ChildPtr = child;
    }

    void doSomething() {
        if (this->ChildPtr.use_count()) {

        }
    }

    ~Parent() {
    }
};

class Child {
private:
    shared_ptr<Parent> ParentPtr;
public:
    void setPartent(shared_ptr<Parent> parent) {
        this->ParentPtr = parent;
    }
    void doSomething() {
        if (this->ParentPtr.use_count()) {

        }
    }
    ~Child() {
    }
};

int main() {
    weak_ptr<Parent> wpp;
    weak_ptr<Child> wpc;
    {
        shared_ptr<Parent> p(new Parent);
        shared_ptr<Child> c(new Child);
        p->setChild(c);
        c->setPartent(p);
        wpp = p;
        wpc = c;
        cout << p.use_count() << endl; // 2
        cout << c.use_count() << endl; // 2
    }
    cout << wpp.use_count() << endl;  // 1
    cout << wpc.use_count() << endl;  // 1
    return 0;
}

循环引用的解决方法: weak_ptr

循环引用:该被调用的析构函数没有被调用,从而出现了内存泄漏。

  • weak_ptr 对被 shared_ptr 管理的对象存在 非拥有性(弱)引用,在访问所引用的对象前必须先转化为 shared_ptr;

  • weak_ptr 用来打断 shared_ptr 所管理对象的循环引用问题,若这种环被孤立(没有指向环中的外部共享指针),shared_ptr 引用计数无法抵达 0,内存被泄露;令环中的指针之一为弱指针可以避免该情况;

  • weak_ptr 用来表达临时所有权的概念,当某个对象只有存在时才需要被访问,而且随时可能被他人删除,可以用 weak_ptr 跟踪该对象;需要获得所有权时将其转化为 shared_ptr,此时如果原来的 shared_ptr 被销毁,则该对象的生命期被延长至这个临时的 shared_ptr 同样被销毁。

#include <iostream>
#include <memory>

using namespace std;

class Child;
class Parent;

class Parent {
private:
    //shared_ptr<Child> ChildPtr;
    weak_ptr<Child> ChildPtr;
public:
    void setChild(shared_ptr<Child> child) {
        this->ChildPtr = child;
    }

    void doSomething() {
        //new shared_ptr
        if (this->ChildPtr.lock()) {

        }
    }

    ~Parent() {
    }
};

class Child {
private:
    shared_ptr<Parent> ParentPtr;
public:
    void setPartent(shared_ptr<Parent> parent) {
        this->ParentPtr = parent;
    }
    void doSomething() {
        if (this->ParentPtr.use_count()) {

        }
    }
    ~Child() {
    }
};

int main() {
    weak_ptr<Parent> wpp;
    weak_ptr<Child> wpc;
    {
        shared_ptr<Parent> p(new Parent);
        shared_ptr<Child> c(new Child);
        p->setChild(c);
        c->setPartent(p);
        wpp = p;
        wpc = c;
        cout << p.use_count() << endl; // 2
        cout << c.use_count() << endl; // 1
    }
    cout << wpp.use_count() << endl;  // 0
    cout << wpc.use_count() << endl;  // 0
    return 0;
}

语言对比

面向对象

什么是面向对象?面向对象的三大特性

面向对象:对象是指具体的某一个事物,这些事物的抽象就是类,类中包含数据(成员变量)和动作(成员方法)。

面向对象的三大特性:

  • 封装:将具体的实现过程和数据封装成一个函数,只能通过接口进行访问,降低耦合性。

  • 继承:子类继承父类的特征和行为,子类有父类的非 private 方法或成员变量,子类可以对父类的方法进行重写,增强了类之间的耦合性,但是当父类中的成员变量、成员函数或者类本身被 final 关键字修饰时,修饰的类不能继承,修饰的成员不能重写或修改。

  • 多态:多态就是不同继承类的对象,对同一消息做出不同的响应,基类的指针指向或绑定到派生类的对象,使得基类指针呈现不同的表现方式。

重载、重写、隐藏的区别

概念解释:

重载:是指同一可访问区内被声明几个具有不同参数列(参数的类型、个数、顺序)的同名函数,根据参数列表确定调用哪个函数,重载不关心函数返回类型。

class A
{
public:
    void fun(int tmp);
    void fun(float tmp);        // 重载 参数类型不同(相对于上一个函数)
    void fun(int tmp, float tmp1); // 重载 参数个数不同(相对于上一个函数)
    void fun(float tmp, int tmp1); // 重载 参数顺序不同(相对于上一个函数)
    int fun(int tmp);            // error: 'int A::fun(int)' cannot be overloaded 错误:注意重载不关心函数返回类型
};

隐藏:是指派生类的函数屏蔽了与其同名的基类函数,主要只要同名函数,不管参数列表是否相同,基类函数都会被隐藏。

#include <iostream>
using namespace std;

class Base
{
public:
    void fun(int tmp, float tmp1) { cout << "Base::fun(int tmp, float tmp1)" << endl; }
};

class Derive : public Base
{
public:
    void fun(int tmp) { cout << "Derive::fun(int tmp)" << endl; } // 隐藏基类中的同名函数
};

int main()
{
    Derive ex;
    ex.fun(1);       // Derive::fun(int tmp)
    ex.fun(1, 0.01); // error: candidate expects 1 argument, 2 provided
    return 0;
}

说明:上述代码中 ex.fun(1, 0.01); 出现错误,说明派生类中将基类的同名函数隐藏了。若是想调用基类中的同名函数,可以加上类型名指明 ex.Base::fun(1, 0.01);,这样就可以调用基类中的同名函数。

重写(覆盖):是指派生类中存在重新定义的函数。函数名、参数列表、返回值类型都必须同基类中被重写的函数一致,只有函数体不同。派生类调用时会调用派生类的重写函数,不会调用被重写函数。重写的基类中被重写的函数必须有 virtual 修饰。

#include <iostream>
using namespace std;

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

class Derived : public Base
{
public:
    virtual void fun(int tmp) { cout << "Derived::fun(int tmp) : " << tmp << endl; } // 重写基类中的 fun 函数
};
int main()
{
    Base *p = new Derived();
    p->fun(3); // Derived::fun(int) : 3
    return 0;
}

重写和重载的区别:

  • 范围区别:对于类中函数的重载或者重写而言,重载发生在同一个类的内部,重写发生在不同的类之间(子类和父类之间)。

  • 参数区别:重载的函数需要与原函数有相同的函数名、不同的参数列表,不关注函数的返回值类型;重写的函数的函数名、参数列表和返回值类型都需要和原函数相同,父类中被重写的函数需要有 virtual 修饰。

  • virtual 关键字:重写的函数基类中必须有 virtual关键字的修饰,重载的函数可以有 virtual 关键字的修饰也可以没有。

隐藏和重写,重载的区别:

  • 范围区别:隐藏与重载范围不同,隐藏发生在不同类中。

  • 参数区别:隐藏函数和被隐藏函数参数列表可以相同,也可以不同,但函数名一定相同;当参数不同时,无论基类中的函数是否被 virtual 修饰,基类函数都是被隐藏,而不是重写。

如何理解 C++ 是面向对象编程

  • 面向过程编程:一种以执行程序操作的过程或函数为中心编写软件的方法。程序的数据通常存储在变量中,与这些过程是分开的。所以必须将变量传递给需要使用它们的函数。缺点:随着程序变得越来越复杂,程序数据与运行代码的分离可能会导致问题。例如,程序的规范经常会发生变化,从而需要更改数据的格式或数据结构的设计。当数据结构发生变化时,对数据进行操作的代码也必须更改为接受新的格式。查找需要更改的所有代码会为程序员带来额外的工作,并增加了使代码出现错误的机会。

  • 面向对象编程(Object-Oriented Programming, OOP):以创建和使用对象为中心。一个对象(Object)就是一个软件实体,它将数据和程序在一个单元中组合起来。对象的数据项,也称为其属性,存储在成员变量中。对象执行的过程被称为其成员函数。将对象的数据和过程绑定在一起则被称为封装。

面向对象编程进一步说明:

面向对象编程将数据成员和成员函数封装到一个类中,并声明数据成员和成员函数的访问级别(public、private、protected),以便控制类对象对数据成员和函数的访问,对数据成员起到一定的保护作用。而且在类的对象调用成员函数时,只需知道成员函数的名、参数列表以及返回值类型即可,无需了解其函数的实现原理。当类内部的数据成员或者成员函数发生改变时,不影响类外部的代码。

什么是多态?多态如何实现?

多态:多态就是不同继承类的对象,对同一消息做出不同的响应,基类的指针指向或绑定到派生类的对象,使得基类指针呈现不同的表现方式。在基类的函数前加上 virtual 关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数。
实现方法:多态是通过虚函数实现的,虚函数的地址保存在虚函数表中,虚函数表的地址保存在含有虚函数的类的实例对象的内存空间中。

实现过程:

  1. 在类中用 virtual 关键字声明的函数叫做虚函数;

  2. 存在虚函数的类都有一个虚函数表,当创建一个该类的对象时,该对象有一个指向虚函数表的虚表指针(虚函数表和类对应的,虚表指针是和对象对应);

  3. 当基类指针指向派生类对象,基类指针调用虚函数时,基类指针指向派生类的虚表指针,由于该虚表指针指向派生类虚函数表,通过遍历虚表,寻找相应的虚函数。

#include <iostream>
using namespace std;

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

    virtual void fun1() { cout << "Base::fun1()" << endl; }

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

    virtual void D_fun1() { cout << "Derive::D_fun1()" << endl; }

    virtual void D_fun2() { cout << "Derive::D_fun2()" << endl; }
};
int main()
{
    Base *p = new Derive();
    p->fun(); // Derive::fun() 调用派生类中的虚函数
    return 0;
}

基类的虚函数表如下:

imagepng

派生类的对象虚函数表如下:

imagepng

简单解释:当基类的指针指向派生类的对象时,通过派生类的对象的虚表指针找到虚函数表(派生类的对象虚函数表),进而找到相应的虚函数 Derive::f() 进行调用。

关键字库函数

sizeof 和 strlen 的区别

strlen 是头文件 中的函数,sizeof 是 C++ 中的运算符。

strlen 测量的是字符串的实际长度(其源代码如下),以 \0 结束。而 sizeof 测量的是字符数组的分配大小。
strlen 源代码:

size_t strlen(const char *str) {
    size_t length = 0;
    while (*str++)
        ++length;
    return length;
}

举例

#include <iostream>
#include <cstring>

using namespace std;

int main()
{
    char arr[10] = "hello";
    cout << strlen(arr) << endl; // 5
    cout << sizeof(arr) << endl; // 10
    return 0;
}

若字符数组 arr 作为函数的形参,sizeof(arr) 中 arr 被当作字符指针来处理,strlen(arr) 中 arr 依然是字符数组,从下述程序的运行结果中就可以

#include <iostream>
#include <cstring>

using namespace std;

void size_of(char arr[])
{
    cout << sizeof(arr) << endl; // warning: 'sizeof' on array function parameter 'arr' will return size of 'char*' .
    cout << strlen(arr) << endl; 
}

int main()
{
    char arr[20] = "hello";
    size_of(arr); 
    return 0;
}
/*
输出结果:
8
5
*/

strlen 本身是库函数,因此在程序运行过程中,计算长度;而 sizeof 在编译时,计算长度;

sizeof 的参数可以是类型,也可以是变量;strlen 的参数必须是 char* 类型的变量。

lambda 表达式(匿名函数)的具体应用和使用场景

lambda表达式 - DearLeslie - 博客园

lambda 表达式是一个可调度的代码单元,可以视为一个未命名的内部函数

lambda 函数是一个函数对象,编译器在编译时会生成一个 lambda 对象的类,然后再生成一个该命令未命名的对象

lambda 的形式如下:
[捕获列表] (参数列表) -> 返回类型 { 函数部分 }

[capture list] (parameter list) -> reurn type
{
   function body
}

capture list 捕获列表是 lambda 函数所定义的函数的局部变量列表, 通常为空

一个 lambda 只有在其捕获列表中捕获一个所在函数中的局部变量,才能在函数体中使用该变量。

捕获列表只用于局部非 static 变量。 lambda 可以直接使用局部变量 static 变量 和在它所在函数之外的声明的名字。

捕获列表的变量可以分为 值 或 引用传递。

值传递: lambda 捕获的变量在 lambda 函数 创建 就发生了拷贝而非调用时。

隐式捕获:

编译器可以根据 lambda 中的代码推导使用的变量,为指示编译器推断捕获列表,应该在捕获列表中写一个 & 或 =

  • & 告知编译器采用引用传递方式

  • = 告知编译器采用值传递方式

当混合使用时,捕获列表第一个参数必须是 & 或 = 且显示捕获的变量必须和隐式捕获使用不同的传递方式

pameter list
参数列表和普通函数类似,但是 lambda 不能有 默认参数【lambda 实参和形参数目一定相等】

return type

与普通函数不同的是: lambda 必须使用位尾置返回 来指定返回类型。

如果忽略返回类型,lambda 表达式会根据函数体中的代码推断出返回类型

若函数体只有一个 return 语句, 则返回类型从返回表达式的类型推断而来,否则,若未指定返回类型,返回类型为 void

如果 lambda 的函数体包含任意单一 return 之外的内容, 且未指定返回类型,则返回 void当需要为 lambda 定义返回类型时,必须使用尾置返回类型

function body
与常规函数类似

举例:
lambda 表达式常搭配排序算法使用。

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int main()
{
    vector<int> arr = {3, 4, 76, 12, 54, 90, 34};
    sort(arr.begin(), arr.end(), [](int a, int b) { return a > b; }); // 降序排序
    for (auto a : arr)
    {
        cout << a << " ";
    }
    return 0;
}
/*
运行结果:90 76 54 34 12 4 3
*/

C 和 C++ static 的区别

  • 在 C 语言中,使用 static 可以定义局部静态变量、外部静态变量、静态函数

  • 在 C++ 中,使用 static 可以定义局部静态变量、外部静态变量、静态函数、静态成员变量和静态成员函数。因为 C++ 中有类的概念,静态成员变量、静态成员函数都是与类有关的概念。

static 的作用

作用:
static 定义静态变量,静态函数。

保持变量内容持久:static 作用于局部变量,改变了局部变量的生存周期,使得该变量存在于定义后直到程序运行结束的这段时间。

#include <iostream>
using namespace std;

int fun(){
    static int var = 1; // var 只在第一次进入这个函数的时初始化
    var += 1;
    return var;
}

int main()
{
    for(int i = 0; i < 10; ++i)
        cout << fun() << " "; // 2 3 4 5 6 7 8 9 10 11
    return 0;
}

隐藏:static 作用于全局变量和函数,改变了全局变量和函数的作用域,使得全局变量和函数只能在定义它的文件中使用,在源文件中不具有全局可见性。(注:普通全局变量和函数具有全局可见性,即其他的源文件也可以使用。)

static 作用于类的成员变量和类的成员函数,使得类变量或者类成员函数和类有关,也就是说可以不定义类的对象就可以通过类访问这些静态成员。注意:类的静态成员函数中只能访问静态成员变量或者静态成员函数,不能将静态成员函数定义成虚函数。

#include<iostream>
using namespace std;

class A
{
private:
    int var;
    static int s_var; // 静态成员变量
public:
    void show()
    {
        cout << s_var++ << endl;
    }
    static void s_show()
    {
        cout << s_var << endl;
        // cout << var << endl; // error: invalid use of member 'A::a' in static member function. 静态成员函数不能调用非静态成员变量。无法使用 this.var
        // show();  // error: cannot call member function 'void A::show()' without object. 静态成员函数不能调用非静态成员函数。无法使用 this.show()
    }
};
int A::s_var = 1;  // 静态成员变量在类外进行初始化赋值,默认初始化为 0

int main()
{

    // cout << A::sa << endl;    // error: 'int A::sa' is private within this context
    A ex;
    ex.show();
    A::s_show();
}
  1. 在修饰变量的时候,static 修饰的静态局部变量只执行初始化一次,而且延长了局部变量的生命周期,直到程序运行结束以后才释放。

  2. static 修饰全局变量的时候,这个全局变量只能在本文件中访问,不能在其它文件中访问,即便是 extern 外部声明也不可以。

  3. static 修饰一个函数,则这个函数的只能在本文件中调用,不能被其他文件调用。static 修饰的变量存放在全局数据区的静态变量区,包括全局静态变量和局部静态变量,都在全局数据区分配内存。初始化的时候自动初始化为 0。

  4. 不想被释放的时候,可以使用static修饰。比如修饰函数中存放在栈空间的数组。如果不想让这个数组在函数调用结束释放可以使用 static 修饰。

  5. 考虑到数据安全性(当程序想要使用全局变量的时候应该先考虑使用 static)。

static 在类中使用的注意事项(定义、初始化和使用)

static 静态成员变量:

  1. 静态成员变量是在类内进行声明,在类外进行定义和初始化,在类外进行定义和初始化的时候不要出现 static 关键字和private、public、protected 访问规则。

  2. 静态成员变量相当于类域中的全局变量,被类的所有对象所共享,包括派生类的对象。

  3. 静态成员变量可以作为成员函数的参数,而普通成员变量不可以。

#include <iostream>
using namespace std;

class A
{
public:
    static int s_var;
    int var;
    void fun1(int i = s_var); // 正确,静态成员变量可以作为成员函数的参数
    void fun2(int i = var);   //  error: invalid use of non-static data member 'A::var'
};
int main()
{
    return 0;
}
  1. 静态数据成员的类型可以是所属类的类型,而普通数据成员的类型只能是该类类型的指针或引用。
#include <iostream>
using namespace std;

class A
{
public:
    static A s_var; // 正确,静态数据成员
    A var;          // error: field 'var' has incomplete type 'A'
    A *p;           // 正确,指针
    A &var1;        // 正确,引用
};

int main()
{
    return 0;
}

static 静态成员函数:

  1. 静态成员函数不能调用非静态成员变量或者非静态成员函数,因为静态成员函数没有 this 指针。静态成员函数做为类作用域的全局函数。

  2. 静态成员函数不能声明成虚函数(virtual)、const 函数和 volatile 函数。

为何static成员函数不能为const函数

  • 当声明一个非静态成员函数为const时,对this指针会有影响。对于一个Test类中的const修饰的成员函数,this指针相当于Test const *, 而对于非const成员函数,this指针相当于Test *.

  • 而static成员函数没有this指针,所以使用const来修饰static成员函数没有任何意义。

  • volatile的道理也是如此。

为何static成员函数不能为virtual

  • static成员不属于任何类对象或类实例,所以即使给此函数加上virutal也是没有任何意义的。

  • 静态与非静态成员函数之间有一个主要的区别。那就是静态成员函数没有this指针。

  • 虚函数依靠vptr和vtable来处理。vptr是一个指针,在类的构造函数中创建生成,并且能用this指针来访问它,因为它是类的一个成员,并且vptr指向保存虚函数地址的vtable.

  • 对于静态成员函数,它没有this指针,所以无法访问vptr. 这就是为何static函数不能为virtual.

  • 虚函数的调用关系:this -> vptr -> vtable ->virtual function

static 全局变量和普通全局变量的异同

相同点:

  • 存储方式:普通全局变量和 static 全局变量都是静态存储方式。

不同点:

  • 作用域:普通全局变量的作用域是整个源程序,当一个源程序由多个源文件组成时,普通全局变量在各个源文件中都是有效的;静态全局变量则限制了其作用域,即只在定义该变量的源文件内有效,在同一源程序的其它源文件中不能使用它。由于静态全局变量的作用域限于一个源文件内,只能为该源文件内的函数公用,因此可以避免在其他源文件中引起错误。

  • 初始化:静态全局变量只初始化一次,防止在其他文件中使用。

const 作用及用法

作用:

  • const 修饰成员变量,定义成 const 常量,相较于宏常量,可进行类型检查,节省内存空间,提高了效率。

  • const 修饰函数参数,使得传递过来的函数参数的值不能改变。

  • const 修饰成员函数,使得成员函数不能修改任何类型的成员变量(mutable 修饰的变量除外),也不能调用非 const 成员函数,因为非 const 成员函数可能会修改成员变量。
    在类中的用法:

const 成员变量:

  1. const 成员变量只能在类内声明、定义,在构造函数初始化列表中初始化。

  2. const 成员变量只在某个对象的生存周期内是常量,对于整个类而言却是可变的,因为类可以创建多个对象,不同类的 const 成员变量的值是不同的。因此不能在类的声明中初始化 const 成员变量,类的对象还没有创建,编译器不知道他的值。

const 成员函数:

  1. 不能修改成员变量的值,除非有 mutable 修饰;只能访问成员变量。

  2. 不能调用非常量成员函数,以防修改成员变量的值。

#include <iostream>
using namespace std;

class A
{
public:
    int var;
    A(int tmp) : var(tmp) {}
    void c_fun(int tmp) const // const 成员函数
    {
        var = tmp; // error: assignment of member 'A::var' in read-only object. 在 const 成员函数中,不能修改任何类成员变量。        
        fun(tmp); // error: passing 'const A' as 'this' argument discards qualifiers. const 成员函数不能调用非 const 成员函数,因为非 const 成员函数可能会修改成员变量。
    }

    void fun(int tmp)
    {
        var = tmp;
    }
};

int main()
{
    return 0;
}
const int* C::Func(const int* const p) const {};
  • 第四个const:Func函数是const类型的,函数里面不能对成员变量进行修改,也不能调用非const的成员函数

  • 第三个const:参数p是一个常量指针

  • 第二个const:参数p指向的对象是一个常量对象

  • 第一个const:函数返回值是一个指向常量对象的指针

define 和 const 的区别

区别:

  • 编译阶段:define 是在编译预处理阶段进行替换,const 是在编译阶段确定其值。

  • 安全性:define 定义的宏常量没有数据类型,只是进行简单的替换,不会进行类型安全的检查;const 定义的常量是有类型的,是要进行判断的,可以避免一些低级的错误

  • 内存占用:define 定义的宏常量,在程序中使用多少次就会进行多少次替换,内存中有多个备份,占用的是代码段的空间;const 定义的常量占用静态存储区的空间,程序运行过程中只有一份。

  • 调试:define 定义的宏常量不能调试,因为在预编译阶段就已经进行替换了;const 定义的常量可以进行调试。

const 的优点:

  • 有数据类型,在定义式可进行安全性检查。

  • 可调式。

  • 占用较少的空间。

define 和 typedef 的区别

  • 原理:#define 作为预处理指令,在编译预处理时进行替换操作,不作正确性检查,只有在编译已被展开的源程序时才会发现可能的错误并报错。typedef 是关键字,在编译时处理,有类型检查功能,用来给一个已经存在的类型一个别名,但不能在一个函数定义里面使用 typedef 。

  • 功能:typedef 用来定义类型的别名,方便使用。#define 不仅可以为类型取别名,还可以定义常量、变量、编译开关等。

  • 作用域:#define 没有作用域的限制,只要是之前预定义过的宏,在以后的程序中都可以使用,而 typedef 有自己的作用域。

  • 指针的操作:typedef 和 #define 在处理指针时不完全一样

#include <iostream>
#define INTPTR1 int *
typedef int * INTPTR2;

using namespace std;

int main()
{
    INTPTR1 p1, p2; // p1: int *; p2: int
    INTPTR2 p3, p4; // p3: int *; p4: int *

    int var = 1;
    const INTPTR1 p5 = &var; // 相当于 const int * p5; 常量指针,即不可以通过 p5 去修改 p5 指向的内容,但是 p5 可以指向其他内容。
    const INTPTR2 p6 = &var; // 相当于 int * const p6; 指针常量,不可使 p6 再指向其他内容。

    return 0;
}

用宏实现比较大小,以及两个数中的最小值

#include <iostream>
#define MAX(X, Y) ((X)>(Y)?(X):(Y))
#define MIN(X, Y) ((X)<(Y)?(X):(Y))
using namespace std;

int main ()
{
    int var1 = 10, var2 = 100;
    cout << MAX(var1, var2) << endl;
    cout << MIN(var1, var2) << endl;
    return 0;
}
/*
程序运行结果:
100
10
*/

inline 作用及使用方法

作用:
inline 是一个关键字,可以用于定义内联函数。内联函数,像普通函数一样被调用,但是在调用时并不通过函数调用的机制而是直接在调用点处展开,这样可以大大减少由函数调用带来的开销,从而提高程序的运行效率。

使用方法:

  1. 类内定义成员函数默认是内联函数
    在类内定义成员函数,可以不用在函数头部加 inline 关键字,因为编译器会自动将类内定义的函数(构造函数、析构函数、普通成员函数等)声明为内联函数,代码如下:
#include <iostream>
using namespace std;

class A{
public:
    int var;
    A(int tmp){ 
      var = tmp;
    }    
    void fun(){ 
        cout << var << endl;
    }
};

int main()
{    
    return 0;
}
  1. 类外定义成员函数,若想定义为内联函数,需用关键字声明
    当在类内声明函数,在类外定义函数时,如果想将该函数定义为内联函数,则可以在类内声明时不加 inline 关键字,而在类外定义函数时加上 inline 关键字。
#include <iostream>
using namespace std;

class A{
public:
    int var;
    A(int tmp){ 
      var = tmp;
    }    
    void fun();
};

inline void A::fun(){
    cout << var << endl;
}

int main()
{    
    return 0;
}

另外,可以在声明函数和定义函数的同时加上 inline;也可以只在函数声明时加 inline,而定义函数时不加 inline。只要确保在调用该函数之前把 inline 的信息告知编译器即可。

内联函数的作用:

  1. 消除函数调用的开销。
    在内联函数出现之前,程序员通常用 #define 定义一些“函数”来消除调用这些函数的开销。内联函数设计的目的之一,就是取代 #define 的这项功能(因为使用 #define 定义的那些“函数”,编译器不会检查其参数的正确性等,而使用 inline 定义的函数,和普通函数一样,可以被编译器检查,这样有利于尽早发现错误)。

  2. 去除函数只能定义一次的限制。内联函数可以在头文件中被定义,并被多个 .cpp 文件 include,而不会有重定义错误。这也是设计内联函数的主要目的之一。

inline 函数工作原理

  • 内联函数不是在调用时发生控制转移关系,而是在编译阶段将函数体嵌入到每一个调用该函数的语句块中,编译器会将程序中出现内联函数的调用表达式用内联函数的函数体来替换。

  • 普通函数是将程序执行转移到被调用函数所存放的内存地址,当函数执行完后,返回到执行此函数前的地方。转移操作需要保护现场,被调函数执行完后,再恢复现场,该过程需要较大的资源开销。

宏定义(define)和内联函数(inline)的区别

  1. 内联函数是在编译时展开,而宏在编译预处理时展开;在编译的时候,内联函数直接被嵌入到目标代码中去,而宏只是一个简单的文本替换。

  2. 内联函数是真正的函数,和普通函数调用的方法一样,在调用点处直接展开,避免了函数的参数压栈操作,减少了调用的开销。而宏定义编写较为复杂,常需要增加一些括号来避免歧义。

  3. 宏定义只进行文本替换,不会对参数的类型、语句能否正常编译等进行检查。而内联函数是真正的函数,会对参数的类型、函数体内的语句编写是否正确等进行检查。

#include <iostream>

#define MAX(a, b) ((a) > (b) ? (a) : (b))

using namespace std;

inline int fun_max(int a, int b)
{
    return a > b ? a : b;
}

int main()
{
    int var = 1;
    cout << MAX(var, 5) << endl;     
    cout << fun_max(var, 0) << endl; 
    return 0;
}
/*
程序运行结果:
5
1

*/

new 和 malloc 如何判断是否申请到内存?

malloc :成功申请到内存,返回指向该内存的指针;分配失败,返回 NULL 指针。
new :内存分配成功,返回该对象类型的指针;分配失败,抛出 bad_alloc 异常。

delete 实现原理?delete 和 delete[] 的区别?

delete 的实现原理:

  • 首先执行该对象所属类的析构函数;

  • 进而通过调用 operator delete 的标准库函数来释放所占的内存空间。

delete 和 delete [] 的区别:

  • delete 用来释放单个对象所占的空间,只会调用一次析构函数;

  • delete [] 用来释放数组空间,会对数组中的每个成员都调用一次析构函数。

new 和 malloc 的区别,delete 和 free 的区别

在使用的时候 new、delete 搭配使用,malloc、free 搭配使用。

  • malloc、free 是库函数,而new、delete 是关键字。
    new 申请空间时,无需指定分配空间的大小,编译器会根据类型自行计算;malloc 在申请空间时,需要确定所申请空间的大小。

  • new 申请空间时,返回的类型是对象的指针类型,无需强制类型转换,是类型安全的操作符;malloc 申请空间时,返回的是 void* 类型,需要进行强制类型的转换,转换为对象类型的指针。

  • new 分配失败时,会抛出 bad_alloc 异常,malloc 分配失败时返回空指针。

  • 对于自定义的类型,new 首先调用 operator new() 函数申请空间(底层通过 malloc 实现),然后调用构造函数进行初始化,最后返回自定义类型的指针;delete 首先调用析构函数,然后调用 operator delete() 释放空间(底层通过 free 实现)。malloc、free 无法进行自定义类型的对象的构造和析构。

  • new 操作符从自由存储区上为对象动态分配内存,而 malloc 函数从堆上动态分配内存。(自由存储区不等于堆)

堆(heap)是C语言和操作系统的术语。堆是操作系统所维护的一块特殊内存,它提供了动态分配的功能,当程序运行时调用malloc()时就会从中分配,调用free可把内存交换。

而自由存储区是C++中通过new和delete动态分配和释放对象的抽象概念,通过new来申请的内存区域可称为自由存储区。

基本上,所有的C++编译器默认用堆来实现自由存储区,也即是缺省的全局运算符new和delete也许会按照malloc和free的方式来实现,这时由new运算符分配的对象,说它在堆上也对,说它在自由存储区也对。
记住:

  • 堆是c语言和操作系统的术语,是操作系统维护的一块内存。自由存储是C++中通过new和delete动态分配和释放对象的抽象概念。

  • new所申请的内存区域在C++中称为自由存储区,编译器用malloc和free实现new和delete操作符时,new申请的内存可以说是在堆上。

  • 堆和自由内存区有相同之处,但并不等价。

malloc 的原理?malloc 的底层实现?

malloc 的原理:

  • 当开辟的空间小于 128K 时,调用 brk() 函数,通过移动 _enddata 来实现;

  • 当开辟空间大于 128K 时,调用 mmap() 函数,通过在虚拟地址空间中开辟一块内存空间来实现。

malloc 的底层实现:

  • brk() 函数实现原理:向高地址的方向移动指向数据段的高地址的指针 _enddata。

  • mmap 内存映射原理:

    1. 进程启动映射过程,并在虚拟地址空间中为映射创建虚拟映射区域;

    2. 调用内核空间的系统调用函数 mmap(),实现文件物理地址和进程虚拟地址的一一映射关系;

    3. 进程发起对这片映射空间的访问,引发缺页异常,实现文件内容到物理内存(主存)的拷贝。

内存分配的原理

从操作系统角度来看,进程分配内存有两种方式,分别由两个系统调用完成:brk和mmap(不考虑共享内存)。

  1. brk是将数据段(.data)的最高地址指针_edata往高地址推;

  2. mmap是在进程的虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空闲的虚拟内存。

这两种方式分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系。

在标准C库中,提供了malloc/free函数分配释放内存,这两个函数底层是由brk,mmap,munmap这些系统调用实现的。

C 和 C++ struct 的区别?

  • 在 C 语言中 struct 是用户自定义数据类型;在 C++ 中 struct 是抽象数据类型,支持成员函数的定义。

  • C 语言中 struct 没有访问权限的设置,是一些变量的集合体,不能定义成员函数;C++ 中 struct 可以和类一样,有访问权限,并可以定义成员函数。

  • C 语言中 struct 定义的自定义数据类型,在定义该类型的变量时,需要加上 struct 关键字,例如:struct A var;,定义 A 类型的变量;而 C++ 中,不用加该关键字,例如:A var;

在c++中struct和class的唯一区别是成员的默认访问权限。struct默认是public,而class默认是private。

class 和 struct 的异同

  • struct 和 class 都可以自定义数据类型,也支持继承操作。

  • struct 中默认的访问级别是 public,默认的继承级别也是 public;class 中默认的访问级别是 private,默认的继承级别也是 private。

  • 当 class 继承 struct 或者 struct 继承 class 时,默认的继承级别取决于 class 或 struct 本身, class(private 继承),struct(public 继承),即取决于派生类的默认继承级别。

struct A{};
class B : A{}; // private 继承 
struct C : B{}; // public 继承

举例:

#include<iostream>

using namespace std;

class A{
public:
    void funA(){
        cout << "class A" << endl;
    }
};

struct B: A{ // 由于 B 是 struct,A 的默认继承级别为 public
public:
    void funB(){
        cout << "class B" << endl;
    }
};

class C: B{ // 由于 C 是 class,B 的默认继承级别为 private,所以无法访问基类 B 中的 printB 函数

};

int main(){
    A ex1;
    ex1.funA(); // class A

    B ex2;
    ex2.funA(); // class A
    ex2.funB(); // class B

    C ex3;
    ex3.funB(); // error: 'B' is not an accessible base of 'C'.
    return 0;
}

继承方式:
当一个类派生自基类,该基类可以被继承为 public、protected 或 private 几种类型。这与Java不一样,Java只有继承的概念,默认是public继承的。

  • 三种继承方式不影响子类对父类的访问权限,子类对父类只看父类的访问控制权。如三种继承方式都能访问父类中的public和protected成员。

  • 继承方式是为了控制子类(也称派生类)的调用方(也叫用户)对父类(也称基类)的访问权限。

  • public、protected、private三种继承方式,相当于把父类的public访问权限在子类中变成了对应的权限。 protected继承,把父类中的public成员在本类中变成了protected的访问控制权限;private继承,把父类的public成员和protected成员在本类中变成了private访问控制权。

我们几乎不使用 protected 或 private 继承,通常使用 public 继承。当使用不同类型的继承时,遵循以下几个规则:

  • 公有继承(public):当一个类派生自公有基类时,基类的公有成员也是派生类的公有成员,基类的保护成员也是派生类的保护成员,基类的私有成员不能直接被派生类访问,但是可以通过调用基类的公有和保护成员来访问。

  • 保护继承(protected): 当一个类派生自保护基类时,基类的公有和保护成员将成为派生类的保护成员。

  • 私有继承(private):当一个类派生自私有基类时,基类的公有和保护成员将成为派生类的私有成员。

struct 和 union 的区别

说明:union 是联合体,struct 是结构体。

区别:

联合体和结构体都是由若干个数据类型不同的数据成员组成。使用时,联合体只有一个有效的成员;而结构体所有的成员都有效。

对联合体的不同成员赋值,将会对覆盖其他成员的值,而对于结构体的对不同成员赋值时,相互不影响。

联合体的大小为其内部所有变量的最大值,按照最大类型的倍数进行分配大小;结构体分配内存的大小遵循内存对齐原则。

#include <iostream>
using namespace std;

typedef union
{
    char c[10];
    char cc1; // char 1 字节,按该类型的倍数分配大小
} u11;

typedef union
{
    char c[10];
    int i; // int 4 字节,按该类型的倍数分配大小
} u22;

typedef union
{
    char c[10];
    double d; // double 8 字节,按该类型的倍数分配大小
} u33;

typedef struct s1
{
    char c;   // 1 字节
    double d; // 1(char)+ 7(内存对齐)+ 8(double)= 16 字节
} s11;

typedef struct s2
{
    char c;   // 1 字节
    char cc;  // 1(char)+ 1(char)= 2 字节
    double d; // 2 + 6(内存对齐)+ 8(double)= 16 字节
} s22;

typedef struct s3
{
    char c;   // 1 字节
    double d; // 1(char)+ 7(内存对齐)+ 8(double)= 16 字节
    char cc;  // 16 + 1(char)+ 7(内存对齐)= 24 字节
} s33;

int main()
{
    cout << sizeof(u11) << endl; // 10
    cout << sizeof(u22) << endl; // 12
    cout << sizeof(u33) << endl; // 16
    cout << sizeof(s11) << endl; // 16
    cout << sizeof(s22) << endl; // 16
    cout << sizeof(s33) << endl; // 24

    cout << sizeof(int) << endl;    // 4
    cout << sizeof(double) << endl; // 8
    return 0;
}

volatile 的作用?是否具有原子性,对编译器有什么影响?

volatile提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据。如果没有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象。

什么情况下一定要用 volatile, 能否和 const 一起使用?

使用 volatile 关键字的场景:

  • 当多个线程都会用到某一变量,并且该变量的值有可能发生改变时,需要用 volatile 关键字对该变量进行修饰;

  • 中断服务程序中访问的变量或并行设备的硬件寄存器的变量,最好用 volatile 关键字修饰。

volatile 关键字和 const 关键字可以同时使用,某种类型可以既是 volatile 又是 const ,同时具有二者的属性。

两者同时修饰一个对象的典型情况,是用于驱动中访问外部设备的只读寄存器。

全局变量与静态变量构造析构顺序

全局变量(包括静态全局变量)是最先构造的,早于main函数,当然,析构函数也是执行的最晚,晚于main函数。

静态局部变量是要等到执行该声明定义的表达式后,才开始执行构造的。当然,析构函数也是早于全局变量的。

代码执行到声明定义这个静态局部变量的时候才去构造这个静态局部对象。

#include <iostream>
using namespace std;

class A {
public:
    int ma;
    A (int tmp) : ma(tmp) {
        cout << "A::A(" << tmp << ")" << endl;
    }
    ~A() {
        cout << "A::~A()" < endl;
    }
};

void func() {
    cout << "func() start... " << endl;
    static A a(1);
    cout << "func() end." << endl;
}

A g_a(10);
static A sg_a(20);

int main() {
    cout << "main() start" << endl;
    func();
    cout << "main() end" << endl;

    return 0;
}


//A::A(10)
//A::A(20)
//main() start
//func() start...
//A::A(1)
//func() end.
//main() end
//A::~A()
//A::~A()
//A::~A()

memmove 函数的底层原理

memcpy函数

Memcpy原型:

void *memcpy(void *dest, const void *src, size_t n);
memcpy()函数从src内存中拷贝n个字节到dest内存区域,但是源和目的的内存区域不能重叠。
返回值:memcpy()函数返回指向dest的指针。

memmove函数

memmovey原型:

void *memmove(void *dest, const void *src, size_t n);
memmove() 函数从src内存中拷贝n个字节到dest内存区域,但是源和目的的内存可以重叠。
返回值:memmove函数返回一个指向dest的指针。

void *memmove(void *dst, const void *src, size_t size)
{
    char *psrc;
    char *pdst;

    if (NULL == dst || NULL == src)
    {
        return NULL;
    }

    if ((src < dst) && (char *)src + size > (char *)dst) // 出现地址重叠的情况,自后向前拷贝
    {
        psrc = (char *)src + size - 1;
        pdst = (char *)dst + size - 1;
        while (size--)
        {
            *pdst-- = *psrc--;
        }
    }
    else
    {
        psrc = (char *)src;
        pdst = (char *)dst;
        while (size--)
        {
            *pdst++ = *psrc++;
        }
    }

    return dst;
}

extern C 的作用?

当 C++ 程序 需要调用 C 语言编写的函数,C++ 使用链接指示,即 extern "C" 指出任意非 C++ 函数所用的语言。

// 可能出现在 C++ 头文件<cstring>中的链接指示
extern "C"{
    int strcmp(const char*, const char*);
}

c和c++对同一个函数经过编译后生成的函数名是不同的,由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;

而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名。如果在c++中调用一个使用c语言编写的模块中的某个函数,那么c++是根据c++的名称修饰方式来查找并链接这个函数,那么就会发生链接错误。

sizeof(1==1) 在 C 和 C++ 中分别是什么结果?

#include<stdio.h>

void main(){
    printf("%d\n", sizeof(1==1));
}

/*
运行结果:
4
*/

C语言
sizeof(1 == 1) === sizeof(1)按照整数处理,所以是4字节,这里也有可能是8字节(看操作系统)

#include <iostream>
using namespace std;

int main() {
    cout << sizeof(1==1) << endl;
    return 0;
}

/*
1
*/

C++
因为有bool 类型
sizeof(1 == 1) == sizeof(true) 按照bool类型处理,所以是1个字节

相关类

什么是虚函数?什么是纯虚函数?

虚函数:被 virtual 关键字修饰的成员函数,就是虚函数。

 #include <iostream>
using namespace std;

class A
{
public:
    virtual void v_fun() // 虚函数
    {
        cout << "A::v_fun()" << endl;
    }
};
class B : public A
{
public:
    void v_fun()
    {
        cout << "B::v_fun()" << endl;
    }
};
int main()
{
    A *p = new B();
    p->v_fun(); // B::v_fun()
    return 0;
}

纯虚函数:

  • 纯虚函数在类中声明时,加上 =0;

  • 含有纯虚函数的类称为抽象类(只要含有纯虚函数这个类就是抽象类),类中只有接口,没有具体的实现方法;

  • 继承纯虚函数的派生类,如果没有完全实现基类纯虚函数,依然是抽象类,不能实例化对象。

说明:

  • 抽象类对象不能作为函数的参数,不能创建对象,不能作为函数返回类型;

  • 可以声明抽象类指针,可以声明抽象类的引用;

  • 子类必须继承父类的纯虚函数,并全部实现后,才能创建子类的对象。

C++多态(polymorphism)是通过虚函数来实现的,虚函数允许子类重新定义成员函数,而子类重新定义父类的做法称为覆盖(override),或者称为重写。

最常见的用法就是声明基类的指针,利用该指针指向任意一个子类对象,调用相应的虚函数,动态绑定。

由于编写代码的时候并不能确定被调用的是基类的函数还是哪个派生类的函数,所以被成为“虚”函数。

如果没有使用虚函数的话,即没有利用C++多态性,则利用基类指针调用相应的函数的时候,将总被限制在基类函数本身,而无法调用到子类中被重写过的函数。

虚函数和纯虚函数的区别?

  • 虚函数和纯虚函数可以出现在同一个类中,该类称为抽象基类。(含有纯虚函数的类称为抽象基类)

  • 使用方式不同:虚函数可以直接使用,纯虚函数必须在派生类中实现后才能使用;

  • 定义形式不同:虚函数在定义时在普通函数的基础上加上 virtual 关键字,纯虚函数定义时除了加上virtual 关键字还需要加上 =0;

  • 虚函数必须实现,否则编译器会报错;

  • 对于实现纯虚函数的派生类,该纯虚函数在派生类中被称为虚函数,虚函数和纯虚函数都可以在派生类中重写;

  • 析构函数最好定义为虚函数,特别是对于含有继承关系的类;析构函数可以定义为纯虚函数,此时,其所在的类为抽象基类,不能创建实例化对象。

虚函数的实现机制

实现机制:虚函数通过虚函数表来实现。虚函数的地址保存在虚函数表中,在类的对象所在的内存空间中,保存了指向虚函数表的指针(称为“虚表指针”),通过虚表指针可以找到类对应的虚函数表。虚函数表解决了基类和派生类的继承问题和类中成员函数的覆盖问题,当用基类的指针来操作一个派生类的时候,这张虚函数表就指明了实际应该调用的函数。

虚函数表相关知识点:

  • 虚函数表存放的内容:类的虚函数的地址。

  • 虚函数表建立的时间:编译阶段,即程序的编译过程中会将虚函数的地址放在虚函数表中。

  • 虚表指针保存的位置:虚表指针存放在对象的内存空间中最前面的位置,这是为了保证正确取到虚函数的偏移量。

注:虚函数表和类绑定,虚表指针和对象绑定。即类的不同的对象的虚函数表是一样的,但是每个对象都有自己的虚表指针,来指向类的虚函数表。

#include <iostream>
using namespace std;

class Base
{
public:
    virtual void B_fun1() { cout << "Base::B_fun1()" << endl; }
    virtual void B_fun2() { cout << "Base::B_fun2()" << endl; }
    virtual void B_fun3() { cout << "Base::B_fun3()" << endl; }
};

class Derive : public Base
{
public:
    virtual void D_fun1() { cout << "Derive::D_fun1()" << endl; }
    virtual void D_fun2() { cout << "Derive::D_fun2()" << endl; }
    virtual void D_fun3() { cout << "Derive::D_fun3()" << endl; }
};
int main()
{
    Base *p = new Derive();
    p->B_fun1(); // Base::B_fun1()
    return 0;
}

基类和派生类的继承关系:

imagepng

基类的虚函数表:

imagepng

派生类的虚函数表:

imagepng

主函数中基类的指针 p 指向了派生类的对象,当调用函数 B_fun1() 时,通过派生类的虚函数表找到该函数的地址,从而完成调用。

单继承和多继承的虚函数表结构

编译器处理虚函数表:

  • 编译器将虚函数表的指针放在类的实例对象的内存空间中,该对象调用该类的虚函数时,通过指针找到虚函数表,根据虚函数表中存放的虚函数的地址找到对应的虚函数。

  • 如果派生类没有重新定义基类的虚函数 A,则派生类的虚函数表中保存的是基类的虚函数 A 的地址,也就是说基类和派生类的虚函数 A 的地址是一样的。

  • 如果派生类重写了基类的某个虚函数 B,则派生的虚函数表中保存的是重写后的虚函数 B 的地址,也就是说虚函数 B 有两个版本,分别存放在基类和派生类的虚函数表中。

  • 如果派生类重新定义了新的虚函数 C,派生类的虚函数表保存新的虚函数 C 的地址。

  1. 单继承无虚函数覆盖的情况:
#include <iostream>
using namespace std;

class Base
{
public:
    virtual void B_fun1() { cout << "Base::B_fun1()" << endl; }
    virtual void B_fun2() { cout << "Base::B_fun2()" << endl; }
    virtual void B_fun3() { cout << "Base::B_fun3()" << endl; }
};

class Derive : public Base
{
public:
    virtual void D_fun1() { cout << "Derive::D_fun1()" << endl; }
    virtual void D_fun2() { cout << "Derive::D_fun2()" << endl; }
    virtual void D_fun3() { cout << "Derive::D_fun3()" << endl; }
};
int main()
{
    Base *p = new Derive();
    p->B_fun1(); // Base::B_fun1()
    return 0;
}

基类和派生类的继承关系:

imagepng

基类的虚函数表:

imagepng

派生类的虚函数表:

imagepng

  1. 单继承有虚函数覆盖的情况:
#include <iostream>
using namespace std;

class Base
{
public:
    virtual void fun1() { cout << "Base::fun1()" << endl; }
    virtual void B_fun2() { cout << "Base::B_fun2()" << endl; }
    virtual void B_fun3() { cout << "Base::B_fun3()" << endl; }
};

class Derive : public Base
{
public:
    virtual void fun1() { cout << "Derive::fun1()" << endl; }
    virtual void D_fun2() { cout << "Derive::D_fun2()" << endl; }
    virtual void D_fun3() { cout << "Derive::D_fun3()" << endl; }
};
int main()
{
    Base *p = new Derive();
    p->fun1(); // Derive::fun1()
    return 0;
}

派生类的虚函数表:

imagepng

  1. 多继承无虚函数覆盖的情况:
#include <iostream>
using namespace std;

class Base1
{
public:
    virtual void B1_fun1() { cout << "Base1::B1_fun1()" << endl; }
    virtual void B1_fun2() { cout << "Base1::B1_fun2()" << endl; }
    virtual void B1_fun3() { cout << "Base1::B1_fun3()" << endl; }
};
class Base2
{
public:
    virtual void B2_fun1() { cout << "Base2::B2_fun1()" << endl; }
    virtual void B2_fun2() { cout << "Base2::B2_fun2()" << endl; }
    virtual void B2_fun3() { cout << "Base2::B2_fun3()" << endl; }
};
class Base3
{
public:
    virtual void B3_fun1() { cout << "Base3::B3_fun1()" << endl; }
    virtual void B3_fun2() { cout << "Base3::B3_fun2()" << endl; }
    virtual void B3_fun3() { cout << "Base3::B3_fun3()" << endl; }
};

class Derive : public Base1, public Base2, public Base3
{
public:
    virtual void D_fun1() { cout << "Derive::D_fun1()" << endl; }
    virtual void D_fun2() { cout << "Derive::D_fun2()" << endl; }
    virtual void D_fun3() { cout << "Derive::D_fun3()" << endl; }
};

int main(){
    Base1 *p = new Derive();
    p->B1_fun1(); // Base1::B1_fun1()
    return 0;
}

基类和派生类的关系:

imagepng

派生类的虚函数表:(基类的顺序和声明的顺序一致)

imagepng

  1. 多继承有虚函数覆盖的情况:
#include <iostream>
using namespace std;

class Base1
{
public:
    virtual void fun1() { cout << "Base1::fun1()" << endl; }
    virtual void B1_fun2() { cout << "Base1::B1_fun2()" << endl; }
    virtual void B1_fun3() { cout << "Base1::B1_fun3()" << endl; }
};
class Base2
{
public:
    virtual void fun1() { cout << "Base2::fun1()" << endl; }
    virtual void B2_fun2() { cout << "Base2::B2_fun2()" << endl; }
    virtual void B2_fun3() { cout << "Base2::B2_fun3()" << endl; }
};
class Base3
{
public:
    virtual void fun1() { cout << "Base3::fun1()" << endl; }
    virtual void B3_fun2() { cout << "Base3::B3_fun2()" << endl; }
    virtual void B3_fun3() { cout << "Base3::B3_fun3()" << endl; }
};

class Derive : public Base1, public Base2, public Base3
{
public:
    virtual void fun1() { cout << "Derive::fun1()" << endl; }
    virtual void D_fun2() { cout << "Derive::D_fun2()" << endl; }
    virtual void D_fun3() { cout << "Derive::D_fun3()" << endl; }
};

int main(){
    Base1 *p1 = new Derive();
    Base2 *p2 = new Derive();
    Base3 *p3 = new Derive();
    p1->fun1(); // Derive::fun1()
    p2->fun1(); // Derive::fun1()
    p3->fun1(); // Derive::fun1()
    return 0;
}

基类和派生类的关系:

imagepng

派生类的虚函数表:

imagepng

如何禁止构造函数的使用?

为类的构造函数增加 = delete 修饰符,可以达到虽然声明了构造函数但禁止使用的目的。

#include <iostream>

using namespace std;

class A {
public:
    int var1, var2;
    A(){
        var1 = 10;
        var2 = 20;
    }
    A(int tmp1, int tmp2) = delete;
};

int main()
{
    A ex1;    
    A ex2(12,13); // error: use of deleted function 'A::A(int, int)'
    return 0;
}

什么是类的默认构造函数?

默认构造函数:未提供任何实参,来控制默认初始化过程的构造函数称为默认构造函数。

默认构造函数是可以不用实参进行调用的构造函数,它包括了以下两种情况:

// 没有带明显形参的构造函数。
A(){...}
// 提供了默认实参的构造函数。
A(bool _isTrue= true, int _num=10){ isTrue = _isTrue; num = _num; };
#include <iostream>

using namespace std;

class A
{
public:
    A(){ // 类的默认构造函数
        var = 10;
        c = 'q';
    }
    int var;
    char c;
};

int main()
{
    A ex;
    cout << ex.c << endl << ex.var << endl;
    return 0;
}
/*
运行结果:
q
10
*/

构造函数、析构函数是否需要定义成虚函数?为什么?

构造函数一般不定义为虚函数,原因:

  • 从存储空间的角度考虑:构造函数是在实例化对象的时候进行调用,如果此时将构造函数定义成虚函数,需要通过访问该对象所在的内存空间才能进行虚函数的调用(因为需要通过指向虚函数表的指针调用虚函数表,虽然虚函数表在编译时就有了,但是没有虚函数的指针,虚函数的指针只有在创建了对象才有),但是此时该对象还未创建,便无法进行虚函数的调用。所以构造函数不能定义成虚函数。

  • 从使用的角度考虑:虚函数是基类的指针指向派生类的对象时,通过该指针实现对派生类的虚函数的调用,构造函数是在创建对象时自动调用的。

  • 从实现上考虑:虚函数表是在创建对象之后才有的,因此不能定义成虚函数。

  • 从类型上考虑:在创建对象时需要明确其类型。

析构函数一般定义成虚函数,原因:
析构函数定义成虚函数是为了防止内存泄漏,因为当基类的指针或者引用指向或绑定到派生类的对象时,如果未将基类的析构函数定义成虚函数,会调用基类的析构函数,那么只能将基类的成员所占的空间释放掉,派生类中特有的就会无法释放内存空间导致内存泄漏。

如何避免拷贝?

最直观的想法是:将类的拷贝构造函数和赋值构造函数声明为私有 private,但对于类的成员函数和友元函数依然可以调用,达不到完全禁止类的对象被拷贝的目的,而且程序会出现错误,因为未对函数进行定义。

解决方法:声明一个基类,具体做法如下。

  • 定义一个基类,将其中的拷贝构造函数和赋值构造函数声明为私有 private

  • 派生类以私有 private 的方式继承基类

class Uncopyable
{
public:
    Uncopyable() {}
    ~Uncopyable() {}

private:
    Uncopyable(const Uncopyable &);            // 拷贝构造函数
    Uncopyable &operator=(const Uncopyable &); // 赋值构造函数
};
class A : private Uncopyable // 注意继承方式
{ 
};

简单解释:

能够保证,在派生类 A 的成员函数和友元函数中无法进行拷贝操作,因为无法调用基类 Uncopyable 的拷贝构造函数或赋值构造函数。同样,在类的外部也无法进行拷贝操作。

如何减少构造函数开销?

在构造函数中使用类初始化列表,会减少调用默认的构造函数产生的开销。

class A
{
private:
    int val;
public:
    A()
    {
        cout << "A()" << endl;
    }
    A(int tmp)
    {
        val = tmp;
        cout << "A(int " << val << ")" << endl;
    }
};
class Test1
{
private:
    A ex;

public:
    Test1() : ex(1) // 成员列表初始化方式
    {
    }
};

多重继承时会出现什么状况?如何解决?

多重继承(多继承):是指从多个直接基类中产生派生类。

多重继承容易出现的问题:命名冲突和数据冗余问题。

#include <iostream>
using namespace std;

// 间接基类
class Base1
{
public:
    int var1;
};

// 直接基类
class Base2 : public Base1
{
public:
    int var2;
};

// 直接基类
class Base3 : public Base1
{
public:
    int var3;
};

// 派生类
class Derive : public Base2, public Base3
{
public:
    void set_var1(int tmp) { var1 = tmp; } // error: reference to 'var1' is ambiguous. 命名冲突
    void set_var2(int tmp) { var2 = tmp; }
    void set_var3(int tmp) { var3 = tmp; }
    void set_var4(int tmp) { var4 = tmp; }

private:
    int var4;
};

int main()
{
    Derive d;
    return 0;
}

上述程序的继承关系如下:(菱形继承)

imagepng

上述代码中存的问题:

对于派生类 Derive 上述代码中存在直接继承关系和间接继承关系。

  • 直接继承:Base2 、Base3

  • 间接继承:Base1

对于派生类中继承的的成员变量 var1 ,从继承关系来看,实际上保存了两份,一份是来自基类 Base2,一份来自基类 Base3。因此,出现了命名冲突。

解决方法 1: 声明出现冲突的成员变量来源于哪个类

#include <iostream>
using namespace std;

// 间接基类
class Base1
{
public:
    int var1;
};

// 直接基类
class Base2 : public Base1
{
public:
    int var2;
};

// 直接基类
class Base3 : public Base1
{
public:
    int var3;
};

// 派生类 
class Derive : public Base2, public Base3
{
public:
    void set_var1(int tmp) { Base2::var1 = tmp; } // 这里声明成员变量来源于类 Base2,当然也可以声明来源于类 Base3
    void set_var2(int tmp) { var2 = tmp; }
    void set_var3(int tmp) { var3 = tmp; }
    void set_var4(int tmp) { var4 = tmp; }

private:
    int var4;
};

int main()
{
    Derive d;
    return 0;
}

解决方法 2: 虚继承

使用虚继承的目的:保证存在命名冲突的成员变量在派生类中只保留一份,即使间接基类中的成员在派生类中只保留一份。在菱形继承关系中,间接基类称为虚基类,直接基类和间接基类之间的继承关系称为虚继承。

实现方式:在继承方式前面加上 virtual 关键字。

#include <iostream>
using namespace std;

// 间接基类,即虚基类
class Base1
{
public:
    int var1;
};

// 直接基类 
class Base2 : virtual public Base1 // 虚继承
{
public:
    int var2;
};

// 直接基类 
class Base3 : virtual public Base1 // 虚继承
{
public:
    int var3;
};

// 派生类
class Derive : public Base2, public Base3
{
public:
    void set_var1(int tmp) { var1 = tmp; } 
    void set_var2(int tmp) { var2 = tmp; }
    void set_var3(int tmp) { var3 = tmp; }
    void set_var4(int tmp) { var4 = tmp; }

private:
    int var4;
};

int main()
{
    Derive d;
    return 0;
}

类之间的继承关系:

imagepng

虚继承使派生类除了继承基类成员作为自己的成员之外,内部还会有一份内存来保存哪些是基类的成员。当Derive继承Base2和Base3之后,编译器根据虚继承多出来的内存,查到Base2和Base3拥有共同的基类的成员,就不会从Base2和Base3中继承这些,而是直接从共同的基类中继承成员,也就是说,Derive直接继承base的成员,然后再继承Base2和Base3各自新增的成员。这样,Derive就不会继承两份内存。

空类占多少字节?C++ 编译器会给一个空类自动生成哪些函数?

空类声明时编译器不会生成任何成员函数:

对于空类,声明编译器不会生成任何的成员函数,只会生成 1 个字节的占位符。

#include <iostream>
using namespace std;

class A
{
};

int main()
{
    cout << "sizeof(A):" << sizeof(A) << endl; // sizeof(A):1
    return 0;
}

空类定义时编译器会生成 6 个成员函数:

当空类 A 定义对象时,sizeof(A) 仍是为 1,但编译器会生成 6 个成员函数:缺省的构造函数、拷贝构造函数、析构函数、赋值运算符、两个取址运算符。

#include <iostream>
using namespace std;
/*
class A
{}; 该空类的等价写法如下:
*/
class A
{
public:
    A(){};                                       // 缺省构造函数
    A(const A &tmp){};                           // 拷贝构造函数
    ~A(){};                                      // 析构函数
    A &operator=(const A &tmp){};                // 赋值运算符
    A *operator&() { return this; };             // 取址运算符
    const A *operator&() const { return this; }; // 取址运算符(const 版本)
};

int main()
{
    A *p = new A(); 
    cout << "sizeof(A):" << sizeof(A) << endl; // sizeof(A):1
    delete p;       
    return 0;
}

类的成员函数后面加 const,表明这个函数不会对这个类对象的数据成员(准确地说是非静态数据成员)作任何改变。

在设计类的时候,一个原则就是对于不改变数据成员的成员函数都要在后面加 const,而对于改变数据成员的成员函数不能加 const。所以 const 关键字对成员函数的行为作了更加明确的限定:

  1. 有 const 修饰的成员函数(指 const 放在函数参数表的后面,而不是在函数前面或者参数表内),只能读取数据成员,不能改变数据成员;没有 const 修饰的成员函数,对数据成员则是可读可写的。

  2. 除此之外,在类的成员函数后面加 const 还有什么好处呢?那就是常量(即 const)对象可以调用 const 成员函数,而不能调用非const修饰的函数。

为什么拷贝构造函数必须为引用?

原因:避免拷贝构造函数无限制的递归,最终导致栈溢出。

举例说明:

#include <iostream>
using namespace std;

class A
{
private:
    int val;

public:
    A(int tmp) : val(tmp) // 带参数构造函数
    {
        cout << "A(int tmp)" << endl;
    }

    A(const A &tmp) // 拷贝构造函数
    {
        cout << "A(const A &tmp)" << endl;
        val = tmp.val;
    }

    A &operator=(const A &tmp) // 赋值函数(赋值运算符重载)
    {
        cout << "A &operator=(const A &tmp)" << endl;
        val = tmp.val;
        return *this;
    }

    void fun(A tmp)
    {
    }
};

int main()
{
    A ex1(1);
    A ex2(2);
    A ex3 = ex1;
    ex2 = ex1;
    ex2.fun(ex1);
    return 0;
}
/*
运行结果:
A(int tmp)
A(int tmp)
A(const A &tmp)
A &operator=(const A &tmp)
A(const A &tmp)
*/

  • 说明 1:ex2 = ex1; 和 A ex3 = ex1; 为什么调用的函数不一样?
    对象 ex2 已经实例化了,不需要构造,此时只是将 ex1 赋值给 ex2,只会调用赋值函数;但是 ex3 还没有实例化,因此调用的是拷贝构造函数,构造出 ex3,而不是赋值函数,这里涉及到构造函数的隐式调用。

  • 说明 2:如果拷贝构造函数中形参不是引用类型,A ex3 = ex1;会出现什么问题?
    构造 ex3,实质上是 ex3.A(ex1);,假如拷贝构造函数参数不是引用类型,那么将使得 ex3.A(ex1); 相当于 ex1 作为函数 A(const A tmp)的形参,在参数传递时相当于 A tmp = ex1,因为 tmp 没有被初始化,所以在 A tmp = ex1 中继续调用拷贝构造函数,接下来的是构造 tmp,也就是 tmp.A(ex1) ,必然又会有 ex1 作为函数 A(const A tmp); 的形参,在参数传递时相当于即 A tmp = ex1,那么又会触发拷贝构造函数,就这下永远的递归下去。

  • 说明 3:为什么 ex2.fun(ex1); 会调用拷贝构造函数?
    ex1 作为参数传递给 fun 函数, 即 A tmp = ex1;,这个过程会调用拷贝构造函数进行初始化。

#include <iostream>
using namespace std;

class A
{
private:
    int val;

public:
    A(int tmp) : val(tmp) // 带参数构造函数
    {
        cout << "A(int tmp)" << endl;
    }

    A(const A &tmp) // 拷贝构造函数
    {
        cout << "A(const A &tmp)" << endl;
        val = tmp.val;
    }

    A &operator=(const A &tmp) // 赋值函数(赋值运算符重载)
    {
        cout << "A &operator=(const A &tmp)" << endl;
        val = tmp.val;
        return *this;
    }

    void fun(A tmp)
    {
    }
};

int main()
{
    A ex1(1); //A(int tmp)
    A ex2(2); //A(int tmp)
    cout<<endl;
    //1.新建对象并赋值为已有对象 拷贝初始化 因实现方式而不同!
    A ex3 = ex1; //A(const A &tmp) 拷贝构造函数
    cout<<endl;
    //2.给已有对象赋值
    ex2 = ex1; //A &operator=(const A &tmp) 赋值函数
    cout<<endl;
    //3.调用包含非引用非指针的A类型形参的函数
    ex2.fun(ex1); //A(const A &tmp) 拷贝构造函数
    cout<<endl;
    //4.新建对象并赋值为"类型名(int)"  因实现方式而不同!
    A ex4=A(4); // A(int tmp) 构造函数
    cout<<endl;
    //5.新建对象并用已有对象初始化 直接初始化 
    A ex5(ex1); //A(const A &tmp) 拷贝构造函数
    cout<<endl;
    //6.new一个对象,传参为该类的已有对象
    A *ex6=new A(ex1); //A(const A &tmp) 拷贝构造函数
    cout<<endl;
    //7.new一个对象,传参为int
    A *ex7=new A(7); //A(int tmp) 构造函数
    return 0;
}
/*
运行结果:
A(int tmp)
A(int tmp)

A(const A &tmp)

A &operator=(const A &tmp)

A(const A &tmp)

A(int tmp)

A(const A &tmp)

A(const A &tmp)

A(int tmp)
*/

C++ 类对象的初始化顺序

构造函数调用顺序:

  • 按照派生类继承基类的顺序,即派生列表中声明的顺序,依次调用基类的构造函数;

  • 按照派生类中成员变量的声名顺序,依次调用派生类中成员变量所属类的构造函数;

  • 执行派生类自身的构造函数。

综上可以得出,类对象的初始化顺序:基类构造函数–>派生类成员变量的构造函数–>自身构造函数

注:

  • 基类构造函数的调用顺序与派生类的派生列表中的顺序有关;

  • 成员变量的初始化顺序与声明顺序有关;

  • 析构顺序和构造顺序相反。

#include <iostream>
using namespace std;

class A
{
public:
    A() { cout << "A()" << endl; }
    ~A() { cout << "~A()" << endl; }
};

class B
{
public:
    B() { cout << "B()" << endl; }
    ~B() { cout << "~B()" << endl; }
};

class Test : public A, public B // 派生列表
{
public:
    Test() { cout << "Test()" << endl; }
    ~Test() { cout << "~Test()" << endl; }

private:
    B ex1;
    A ex2;
};

int main()
{
    Test ex;
    return 0;
}
/*
运行结果:
A()
B()
B()
A()
Test()
~Test()
~A()
~B()
~B()
~A()
*/

程序运行结果分析:

  • 首先调用基类 A 和 B 的构造函数,按照派生列表 public A, public B 的顺序构造;

  • 然后调用派生类 Test 的成员变量 ex1 和 ex2 的构造函数,按照派生类中成员变量声明的顺序构造;

  • 最后调用派生类的构造函数;

  • 接下来调用析构函数,和构造函数调用的顺序相反。

对于一个类X进行初始化:

  • 按照继承声明顺序,初始化基类对象

  • 初始化X的成员对象

  • 调用X构造函数

文章中的1)和2)都说的是“调用构造函数”而非“初始化对象”,那么以文章中的例子,若B类中多出一个私有成员A a时会怎么样?如果仅调用构造函数输出是不会有任何变化的,然而实验结果如下,初始化应该是一个“递归”的过程:

#include <iostream>
using namespace std;

class A
{
public:
    A() { cout << "A()" << endl; }
    ~A() { cout << "~A()" << endl; }

};

class B
{
public:
    B() { cout << "B()" << endl; }
    ~B() { cout << "~B()" << endl; }
private:
    A a;
};

class Test : public A, public B // 派生列表
{
public:
    Test() { cout << "Test()" << endl; }
    ~Test() { cout << "~Test()" << endl; }


private:
    B ex1;
    A ex2;
};

int main()
{
    Test ex;
    return 0;
}
A()    // 初始化对象Test,C继承A,B,于是初始化对象A,A没有继承和成员对象,调用A的构造函数
A()    // 初始化对象B,B没有继承,B有成员对象A,初始化一个对象A,A没有成员对象,调用A的构造函数
B()    // 接着上面初始化对象B的过程,调用B的构造函数
A()    // 接着初始化对象Test的过程,初始化其B类的成员对象,因为B有A成员,调用A的构造函数
B()    // 接着上面初始化对象B的过程,调用B的构造函数
A()    // 接着初始化对象Test的过程,初始化其A类的成员对象
Test() // 调用Test构造函数
~Test()
~A()
~B()
~A()
~B()
~A()
~A()

如何禁止一个类被实例化?

方法一:

  • 在类中定义一个纯虚函数,使该类成为抽象基类,因为不能创建抽象基类的实例化对象;
#include <iostream>

using namespace std;


class A {
public:
    int var1, var2;
    A(){
        var1 = 10;
        var2 = 20;
    }
    virtual void fun() = 0; // 纯虚函数
};

int main()
{
    A ex1; // error: cannot declare variable 'ex1' to be of abstract type 'A'
    return 0;
}

方法二:

  • 将类的构造函数声明为私有 private

为什么用成员初始化列表会快一些?

说明:数据类型可分为内置类型和用户自定义类型(类类型),对于用户自定义类型,利用成员初始化列表效率高。

原因:用户自定义类型如果使用类初始化列表,直接调用该成员变量对应的构造函数即完成初始化;

如果在构造函数中初始化,因为 C++ 规定,对象的成员变量的初始化动作发生在进入构造函数本体之前,那么在执行构造函数的函数体之前首先调用默认的构造函数为成员变量设初值,在进入函数体之后,调用该成员变量对应的构造函数。因此,使用列表初始化会减少调用默认的构造函数的过程,效率高。

#include <iostream>
using namespace std;
class A
{
private:
    int val;
public:
    A()
    {
        cout << "A()" << endl;
    }
    A(int tmp)
    {
        val = tmp;
        cout << "A(int " << val << ")" << endl;
    }
};

class Test1
{
private:
    A ex;

public:
    Test1() : ex(1) // 成员列表初始化方式
    {
    }
};

class Test2
{
private:
    A ex;

public:
    Test2() // 函数体中赋值的方式
    {
        ex = A(2);
    }
};
int main()
{
    Test1 ex1;
    cout << endl;
    Test2 ex2;
    return 0;
}
/*
运行结果:
A(int 1)

A()
A(int 2)
*/

说明:
从程序运行结果可以看出,使用成员列表初始化的方式会省去调用默认的构造函数的过程。

实例化一个对象需要哪几个阶段

  1. 分配空间
    创建类对象首先要为该对象分配内存空间。不同的对象,为其分配空间的时机未必相同。全局对象、静态对象、分配在栈区域内的对象,在编译阶段进行内存分配;存储在堆空间的对象,是在运行阶段进行内存分配。

  2. 初始化
    首先明确一点:初始化不同于赋值。初始化发生在赋值之前,初始化随对象的创建而进行,而赋值是在对象创建好后,为其赋上相应的值。这一点可以联想下上一个问题中提到:初始化列表先于构造函数体内的代码执行,初始化列表执行的是数据成员的初始化过程,这个可以从成员对象的构造函数被调用看的出来。

  3. 赋值
    对象初始化完成后,可以对其进行赋值。对于一个类的对象,其成员变量的赋值过程发生在类的构造函数的函数体中。当执行完该函数体,也就意味着类对象的实例化过程完成了。(总结:构造函数实现了对象的初始化和赋值两个过程,对象的初始化是通过初始化列表来完成,而对象的赋值则才是通过构造函数的函数体来实现。)

注:对于拥有虚函数的类的对象,还需要给虚表指针赋值。

没有继承关系的类,分配完内存后,首先给虚表指针赋值,然后再列表初始化以及执行构造函数的函数体,即上述中的初始化和赋值操作。
有继承关系的类,分配内存之后,首先进行基类的构造过程,然后给该派生类的虚表指针赋值,最后再列表初始化以及执行构造函数的函数体,即上述中的初始化和赋值操作。

友元函数的作用及使用场景

作用:友元提供了不同类的成员函数之间、类的成员函数与一般函数之间进行数据共享的机制。通过友元,一个不同函数或另一个类中的成员函数可以访问类中的私有成员和保护成员。

使用场景:

  1. 普通函数定义为友元函数,使普通函数能够访问类的私有成员。
#include <iostream>

using namespace std;

class A
{
    friend ostream &operator<<(ostream &_cout, const A &tmp); // 声明为类的友元函数

public:
    A(int tmp) : var(tmp)
    {
    }

private:
    int var;
};

ostream &operator<<(ostream &_cout, const A &tmp)
{
    _cout << tmp.var;
    return _cout;
}

int main()
{
    A ex(4);
    cout << ex << endl; // 4
    return 0;
}

  1. 友元类:类之间共享数据。
#include <iostream>

using namespace std;

class A
{
    friend class B;

public:
    A() : var(10){}
    A(int tmp) : var(tmp) {}
    void fun()
    {
        cout << "fun():" << var << endl;
    }

private:
    int var;
};

class B
{
public:
    B() {}
    void fun()
    {
        cout << "fun():" << ex.var << endl; // 访问类 A 中的私有成员
    }

private:
    A ex;
};

int main()
{
    B ex;
    ex.fun(); // fun():10
    return 0;
}

静态绑定和动态绑定是怎么实现的?

静态类型和动态类型:

  • 静态类型:变量在声明时的类型,是在编译阶段确定的。静态类型不能更改。

  • 动态类型:目前所指对象的类型,是在运行阶段确定的。动态类型可以更改。

静态绑定和动态绑定:

  • 静态绑定是指程序在 编译阶段 确定对象的类型(静态类型)。

  • 动态绑定是指程序在 运行阶段 确定对象的类型(动态类型)。

静态绑定和动态绑定的区别:

  • 发生的时期不同:如上。

  • 对象的静态类型不能更改,动态类型可以更改。

注:对于类的成员函数,只有虚函数是动态绑定,其他都是静态绑定。

#include <iostream>

using namespace std;

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


int main()
{
	Base *p = new Derive(); // p 的静态类型是 Base*,动态类型是 Derive*
    p->fun(); // fun 是虚函数,运行阶段进行动态绑定
	return 0;
}
/*
运行结果:
Derive::fun()
*/

深拷贝和浅拷贝的区别

如果一个类拥有资源,该类的对象进行复制时,如果资源重新分配,就是深拷贝,否则就是浅拷贝。

  • 深拷贝:该对象和原对象占用不同的内存空间,既拷贝存储在栈空间中的内容,又拷贝存储在堆空间中的内容。

  • 浅拷贝:该对象和原对象占用同一块内存空间,仅拷贝类中位于栈空间中的内容。

当类的成员变量中有指针变量时,最好使用深拷贝。因为当两个对象指向同一块内存空间,如果使用浅拷贝,当其中一个对象的删除后,该块内存空间就会被释放,另外一个对象指向的就是垃圾内存。

浅拷贝实例

#include <iostream>

using namespace std;

class Test
{
private:
	int *p;

public:
	Test(int tmp)
	{
		this->p = new int(tmp);
		cout << "Test(int tmp)" << endl;
	}
	~Test()
	{
		if (p != NULL)
		{
			delete p;
		}
		cout << "~Test()" << endl;
	}
};

int main()
{
	Test ex1(10);	
	Test ex2 = ex1; 
	return 0;
}
/*
运行结果:
Test(int tmp)
~Test()
*/

说明:上述代码中,类对象 ex1、ex2 实际上是指向同一块内存空间,对象析构时,ex2 先将内存释放了一次,之后 析构对象 ex1 时又将这块已经被释放过的内存再释放一次。对同一块内存空间释放了两次,会导致程序崩溃。

深拷贝实例:

#include <iostream>

using namespace std;

class Test
{
private:
	int *p;

public:
	Test(int tmp)
	{
		p = new int(tmp);
		cout << "Test(int tmp)" << endl;
	}
	~Test()
	{
		if (p != NULL)
		{
			delete p;
		}
		cout << "~Test()" << endl;
	}
	Test(const Test &tmp) // 定义拷贝构造函数
	{
		p = new int(*tmp.p);
		cout << "Test(const Test &tmp)" << endl;
	}

};

int main()
{
	Test ex1(10);	
	Test ex2 = ex1; 
	return 0;
}
/*
Test(int tmp)
Test(const Test &tmp)
~Test()
~Test()
*/

编译时多态和运行时多态的区别

编译时多态:在程序编译过程中出现,发生在模板和函数重载中(泛型编程)。
运行时多态:在程序运行过程中出现,发生在继承体系中,是指通过基类的指针或引用访问派生类中的虚函数。

编译时多态和运行时多态的区别:

  • 时期不同:编译时多态发生在程序编译过程中,运行时多态发生在程序的运行过程中

  • 实现方式不同:编译时多态运用泛型编程来实现,运行时多态借助虚函数来实现。

实现一个类成员函数,要求不允许修改类的成员变量?

如果想达到一个类的成员函数不能修改类的成员变量,只需用 const 关键字来修饰该函数即可。
该问题本质是考察 const 关键字修饰成员函数的作用,只不过以实例的方式来考察,面试者应熟练掌握 const 关键字的作用。

#include <iostream>

using namespace std;

class A
{
public:
    int var1, var2;
    A()
    {
        var1 = 10;
        var2 = 20;
    }
    void fun() const // 不能在 const 修饰的成员函数中修改成员变量的值,除非该成员变量用 mutable 修饰
    {
        var1 = 100; // error: assignment of member 'A::var1' in read-only object
    }
};

int main()
{
    A ex1;
    return 0;
}


mutalbe的中文意思是“可变的,易变的”,跟constant(既C++中的const)是反义词。

在C++中,mutable也是为了突破const的限制而设置的。被mutable修饰的变量,将永远处于可变的状态,即使在一个const函数中。

我们知道,被const关键字修饰的函数的一个重要作用就是为了能够保护类中的成员变量。即:该函数可以使用类中的所有成员变量,但是不能修改他们的值。然而,在某些特殊情况下,我们还是需要在const函数中修改类的某些成员变量,因为要修改的成员变量与类本身并无多少关系,即使修改了也不会对类造成多少影响。

当然,你可以说,你可以去掉该函数的const关键字呀!但问题是,我只想修改某个成员变量,其余成员变量仍然希望被const保护。

使用mutable的注意事项:

  1. mutable只能作用于类的非静态和非常量数据成员。

  2. 在一个类中,应尽量或者不用mutable,大量使用mutable表示程序设计存在缺陷。

class A{
int mutable var1,int var2; //var1,var2都可被const成员函数修改
//和mutable int var1,var2;效果一样
}

class B{
int mutable var1; //var1可被const成员函数修改
int var2; //var2不可被const成员函数修改
}

如何让类不能被继承?

解决方法一:借助 final 关键字,用该关键字修饰的类不能被继承。

#include <iostream>

using namespace std;

class Base final
{
};

class Derive: public Base{ // error: cannot derive from 'final' base 'Base' in derived type 'Derive'

};

int main()
{
    Derive ex;
    return 0;
}

解决方法二:借助友元、虚继承和私有构造函数来实现

#include <iostream>
using namespace std;

template <typename T>
class Base{
    friend T;
private:
    Base(){
        cout << "base" << endl;
    }
    ~Base(){}
};

class B:virtual public Base<B>{   //一定注意 必须是虚继承
public:
    B(){
        cout << "B" << endl;
    }
};

class C:public B{
public:
    C(){}     // error: 'Base<T>::Base() [with T = B]' is private within this context
};


int main(){
    B b;  
    return 0;
}

说明:在上述代码中 B 类是不能被继承的类。

具体原因:

  • 虽然 Base 类构造函数和析构函数被声明为私有 private,在 B 类中,由于 B 是 Base 的友元,因此可以访问 Base 类构造函数,从而正常创建 B 类的对象;

  • B 类继承 Base 类采用虚继承的方式,创建 C 类的对象时,C 类的构造函数要负责 Base 类的构造,但是 Base 类的构造函数私有化了,C 类没有权限访问。因此,无法创建 C 类的对象, B 类是不能被继承的类。

注意:在继承体系中,友元关系不能被继承,虽然 C 类继承了 B 类,B 类是 Base 类的友元,但是 C 类和 Base 类没有友元关系。

这里采用虚继承的原因是,直接由最低层次的派生类构造函数初始化虚基类。这是因为在菱形继承中,可能会存在对虚基类的多次初始化问题,为了避免出现该问题,在采用虚继承的时候,直接由最低层次的派生类构造函数直接负责虚基类类的构造。

如果不加virtual的话,在构造函数的顺序中,每个类只负责自己的直接基类的初始化,所以还是可以生成对象的。加上了virtual之后,C直接负责Base类的构造,但是Base类的构造函数和析构函数都是private,C无法访问,所以不能生成对象。

语言特性相关

左值和右值的区别?左值引用和右值引用的区别,如何将左值转换成右值?

右值引用 - DearLeslie - 博客园

std::move 可以将一个左值强制转化为右值,继而可以通过右值引用使用该值,以用于移动语义。

#include <iostream>
using namespace std;

void fun1(int& tmp) 
{ 
  cout << "fun1(int& tmp):" << tmp << endl; 
} 

void fun2(int&& tmp) 
{ 
  cout << "fun2(int&& tmp)" << tmp << endl; 
} 

int main() 
{ 
  int var = 11; 
  fun1(12); // error: cannot bind non-const lvalue reference of type 'int&' to an rvalue of type 'int'
  fun1(var);
  fun2(1); 
}

std::move() 函数的实现原理

std::move() 函数原型:

template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
	return static_cast<typename remove_reference<T>::type &&>(t);
}

说明:引用折叠原理

  • 右值传递给上述函数的形参 T&& 依然是右值,即 T&& && 相当于 T&&。

  • 左值传递给上述函数的形参 T&& 依然是左值,即 T&& & 相当于 T&。

小结:通过引用折叠原理可以知道,move() 函数的形参既可以是左值也可以是右值。

remove_reference 具体实现:

//原始的,最通用的版本
template <typename T> struct remove_reference{
    typedef T type;  //定义 T 的类型别名为 type
};
 
//部分版本特例化,将用于左值引用和右值引用
template <class T> struct remove_reference<T&> //左值引用
{ typedef T type; }
 
template <class T> struct remove_reference<T&&> //右值引用
{ typedef T type; }   
  
//举例如下,下列定义的a、b、c三个变量都是int类型
int i;
remove_refrence<decltype(42)>::type a;             //使用原版本,
remove_refrence<decltype(i)>::type  b;             //左值引用特例版本
remove_refrence<decltype(std::move(i))>::type  b;  //右值引用特例版本 

什么是指针?指针的大小及用法?

指针: 指向另外一种类型的复合类型。

指针的大小: 在 64 位计算机中,指针占 8 个字节空间。

#include<iostream>

using namespace std;

int main(){
    int *p = nullptr;
    cout << sizeof(p) << endl; // 8

    char *p1 = nullptr;
    cout << sizeof(p1) << endl; // 8
    return 0;
}

指针的用法:

  1. 指向普通对象的指针
#include <iostream>

using namespace std;

class A
{
};

int main()
{
    A *p = new A();
    return 0;
}

  1. 指向常量对象的指针:常量指针
#include <iostream>
using namespace std;

int main(void)
{
    const int c_var = 10;
    const int * p = &c_var;
    cout << *p << endl;
    return 0;
}
  1. 指向函数的指针:函数指针
#include <iostream>
using namespace std;

int add(int a, int b){
    return a + b;
}

int main(void)
{
    int (*fun_p)(int, int);
    fun_p = add;
    cout << fun_p(1, 6) << endl;
    return 0;
}

  1. 指向对象成员的指针,包括指向对象成员函数的指针和指向对象成员变量的指针。
    特别注意:定义指向成员函数的指针时,要标明指针所属的类。
#include <iostream>

using namespace std;

class A
{
public:
    int var1, var2; 
    int add(){
        return var1 + var2;
    }
};

int main()
{
    A ex;
    ex.var1 = 3;
    ex.var2 = 4;
    int *p = &ex.var1; // 指向对象成员变量的指针
    cout << *p << endl;

    int (A::*fun_p)();
    fun_p = A::add; // 指向对象成员函数的指针 fun_p
    cout << (ex.*fun_p)() << endl;
    return 0;
}
  1. this 指针:指向类的当前对象的指针常量。
#include <iostream>
#include <cstring>
using namespace std;

class A
{
public:
    void set_name(string tmp)
    {
        this->name = tmp;
    }
    void set_age(int tmp)
    {
        this->age = age;
    }
    void set_sex(int tmp)
    {
        this->sex = tmp;
    }
    void show()
    {
        cout << "Name: " << this->name << endl;
        cout << "Age: " << this->age << endl;
        cout << "Sex: " << this->sex << endl;
    }

private:
    string name;
    int age;
    int sex;
};

int main()
{
    A *p = new A();
    p->set_name("Alice");
    p->set_age(16);
    p->set_sex(1);
    p->show();

    return 0;
}

什么是野指针和悬空指针?

悬空指针:

若指针指向一块内存空间,当这块内存空间被释放后,该指针依然指向这块内存空间,此时,称该指针为“悬空指针”。

举例:

void *p = malloc(size);
free(p); 
// 此时,p 指向的内存空间已释放, p 就是悬空指针。

野指针:

“野指针”是指不确定其指向的指针,未初始化的指针为“野指针”。

void *p; 
// 此时 p 是“野指针”。

C++ 11 nullptr 比 NULL 优势

  • NULL:预处理变量,是一个宏,它的值是 0,定义在头文件 中,即 #define NULL 0。

  • nullptr:C++ 11 中的关键字,是一种特殊类型的字面值,可以被转换成任意其他类型。

nullptr 的优势:

  1. 有类型,类型是 typdef decltype(nullptr) nullptr_t;,使用 nullptr 提高代码的健壮性。

  2. 函数重载:因为 NULL 本质上是 0,在函数调用过程中,若出现函数重载并且传递的实参是 NULL,可能会出现,不知和哪一个函数匹配的情况;但是传递实参 nullptr 就不会出现这种情况。

#include <iostream>
#include <cstring>
using namespace std;

void fun(char const *p)
{
    cout << "fun(char const *p)" << endl;
}

void fun(int tmp)
{
    cout << "fun(int tmp)" << endl;
}

int main()
{
    fun(nullptr); // fun(char const *p)
    /*
    fun(NULL); // error: call of overloaded 'fun(NULL)' is ambiguous
    */
    return 0;
}

指针和引用的区别?

  • 指针所指向的内存空间在程序运行过程中可以改变,而引用所绑定的对象一旦绑定就不能改变。(是否可变)

  • 指针本身在内存中占有内存空间,引用相当于变量的别名,在内存中不占内存空间。(是否占内存)

  • 指针可以为空,但是引用必须绑定对象。(是否可为空)

  • 指针可以有多级,但是引用只能一级。(是否能为多级)

常量指针和指针常量的区别

常量指针:

常量指针本质上是个指针,只不过这个指针指向的对象是常量。
特点:const 的位置在指针声明运算符 * 的左侧。只要 const 位于 * 的左侧,无论它在类型名的左边或右边,都表示指向常量的指针。(可以这样理解,* 左侧表示指针指向的对象,该对象为常量,那么该指针为常量指针。)

const int * p;
int const * p;

注意 1:指针指向的对象不能通过这个指针来修改,也就是说常量指针可以被赋值为变量的地址,之所以叫做常量指针,是限制了通过这个指针修改变量的值。
例如:

#include <iostream>
using namespace std;

int main()
{
    const int c_var = 8;
    const int *p = &c_var; 
    *p = 6;            // error: assignment of read-only location '* p'
    return 0;
}

注意 2:虽然常量指针指向的对象不能变化,可是因为常量指针本身是一个变量,因此,可以被重新赋值。
例如:

#include <iostream>
using namespace std;

int main()
{
    const int c_var1 = 8;
    const int c_var2 = 8;
    const int *p = &c_var1; 
    p = &c_var2;
    return 0;
}

指针常量:

指针常量的本质上是个常量,只不过这个常量的值是一个指针。
特点:const 位于指针声明操作符右侧,表明该对象本身是一个常量,* 左侧表示该指针指向的类型,即以 * 为分界线,其左侧表示指针指向的类型,右侧表示指针本身的性质。

const int var;
int * const c_p = &var; 

注意 1:指针常量的值是指针,这个值因为是常量,所以指针本身不能改变。

#include <iostream>
using namespace std;

int main()
{
    int var, var1;
    int * const c_p = &var;
    c_p = &var1; // error: assignment of read-only variable 'c_p'
    return 0;
}

注意 2:指针的内容可以改变。

#include <iostream>
using namespace std;

int main()
{
    int var = 3;
    int * const c_p = &var;
    *c_p = 12; 
    return 0;
}

函数指针和指针函数的区别

指针函数:
指针函数本质是一个函数,只不过该函数的返回值是一个指针。相对于普通函数而言,只是返回值是指针。

#include <iostream>
using namespace std;

struct Type
{
  int var1;
  int var2;
};

Type  fun(int tmp1, int tmp2){
    Type * t = new Type();
    t->var1 = tmp1;
    t->var2 = tmp2;
    return t;
}

int main()
{
    Type *p = fun(5, 6);
    return 0;
}

函数指针:
函数指针本质是一个指针变量,只不过这个指针指向一个函数。函数指针即指向函数的指针。

#include <iostream>
using namespace std;
int fun1(int tmp1, int tmp2)
{
  return tmp1 * tmp2;
}
int fun2(int tmp1, int tmp2)
{
  return tmp1 / tmp2;
}

int main()
{
  int (*fun)(int x, int y); 
  fun = fun1;
  cout << fun(15, 5) << endl; 
  fun = fun2;
  cout << fun(15, 5) << endl; 
  return 0;
}
/*
运行结果:
75
3
*/

函数指针和指针函数的区别:

本质不同

  • 指针函数本质是一个函数,其返回值为指针。

  • 函数指针本质是一个指针变量,其指向一个函数。

定义形式不同

  • 指针函数:int* fun(int tmp1, int tmp2); ,这里* 表示函数的返回值类型是指针类型。

  • 函数指针:int ( * fun)(int tmp1, int tmp2);,这里* 表示变量本身是指针类型。

强制类型转换有哪几种?

C++四种类型转换运算符

如何判断结构体是否相等?能否用 memcmp 函数判断结构体相等?

需要重载操作符 == 判断两个结构体是否相等,不能用函数 memcmp 来判断两个结构体是否相等,因为 memcmp 函数是逐个字节进行比较的,而结构体存在内存空间中保存时存在字节对齐,字节对齐时补的字节内容是随机的,会产生垃圾值,所以无法比较。

利用运算符重载来实现结构体对象的比较:

#include <iostream>

using namespace std;

struct A
{
    char c;
    int val;
    A(char c_tmp, int tmp) : c(c_tmp), val(tmp) {}

    friend bool operator==(const A &tmp1, const A &tmp2); //  友元运算符重载函数
};

bool operator==(const A &tmp1, const A &tmp2)
{
    return (tmp1.c == tmp2.c && tmp1.val == tmp2.val);
}

int main()
{
    A ex1('a', 90), ex2('b', 80);
    if (ex1 == ex2)
        cout << "ex1 == ex2" << endl;
    else
        cout << "ex1 != ex2" << endl; // 输出
    return 0;
}

在本节代码示例中,操作符为双目操作符,因此只接受两个参数。这时在类内定义或声明操作符时需要特别注意,因为类的成员函数会有隐含的this指针,如果定义为bool operator(const A &tmp1, const A &tmp2),实则有三个参数,在编译时会报错提示参数太多。

针对以上问题解决方法有以下三种:

  1. 如本示例一样,定义为友元函数,则不会有隐含的this指针。

  2. 在类外定义重载操作符,成为全局函数。

  3. 在类内定义重载操作符(非友元函数),但只含一个参数,加上隐含的this指针,共两个参数。

参数传递时,值传递、引用传递、指针传递的区别?

c++值传递,指针传递,引用传递以及指针与引用的区别 - Mr左 - 博客园

值传递:

形参是实参的拷贝,改变形参的值并不会影响外部实参的值。从被调用函数的角度来说,值传递是单向的(实参->形参),参数的值只能传入,

不能传出。当函数内部需要修改参数,并且不希望这个改变影响调用者时,采用值传递。

指针传递:

形参为指向实参地址的指针,当对形参的指向操作时,就相当于对实参本身进行的操作

引用传递:

形参相当于是实参的“别名”,对形参的操作其实就是对实参的操作,在引用传递过程中,被调函数的形式参数虽然也作为局部变量在栈

中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。被调函数对形参的任何操作都被处理成间接寻址,即通过

栈中存放的地址访问主调函数中的实参变量。正因为如此,被调函数对形参做的任何操作都影响了主调函数中的实参变量。

最后,总结一下指针和引用的相同点和不同点:

相同点:

  • 都是地址的概念;指针指向一块内存,它的内容是所指内存的地址;而引用则是某块内存的别名。

不同点:

  • 指针是一个实体,而引用仅是个别名;

  • 引用只能在定义时被初始化一次,之后不可变;指针可变;引用“从一而终”,指针可以“见异思迁”;

  • 引用没有const,指针有const,const的指针不可变;(具体指没有int& const a这种形式,而const int& a是有的,  前者指引用本身即别名不可以改变,这是当然的,所以不需要这种形式,后者指引用所指的值不可以改变)

  • 引用不能为空,指针可以为空;

  • “sizeof 引用”得到的是所指向的变量(对象)的大小,而“sizeof 指针”得到的是指针本身的大小;

  • 指针和引用的自增(++)运算意义不一样;

  • 引用是类型安全的,而指针不是 (引用比指针多了类型检查)

什么是模板?如何实现?

模板:创建类或者函数的蓝图或者公式,分为函数模板和类模板。
实现方式:模板定义以关键字 template 开始,后跟一个模板参数列表。

  • 模板参数列表不能为空;

  • 模板类型参数前必须使用关键字 class 或者 typename,在模板参数列表中这两个关键字含义相同,可互换使用。

template <typename T, typename U, ...>

函数模板:通过定义一个函数模板,可以避免为每一种类型定义一个新函数。

  • 对于函数模板而言,模板类型参数可以用来指定返回类型或函数的参数类型,以及在函数体内用于变量声明或类型转换。

  • 函数模板实例化:当调用一个模板时,编译器用函数实参来推断模板实参,从而使用实参的类型来确定绑定到模板参数的类型。

#include<iostream>

using namespace std;

template <typename T>
T add_fun(const T & tmp1, const T & tmp2){
    return tmp1 + tmp2;
}

int main(){
    int var1, var2;
    cin >> var1 >> var2;
    cout << add_fun(var1, var2);

    double var3, var4;
    cin >> var3 >> var4;
    cout << add_fun(var3, var4);
    return 0;
}

类模板:类似函数模板,类模板以关键字 template 开始,后跟模板参数列表。但是,编译器不能为类模板推断模板参数类型,需要在使用该类模板时,在模板名后面的尖括号中指明类型。

#include <iostream>

using namespace std;

template <typename T>
class Complex
{
public:
    //构造函数
    Complex(T a, T b)
    {
        this->a = a;
        this->b = b;
    }

    //运算符重载
    Complex<T> operator+(Complex &c)
    {
        Complex<T> tmp(this->a + c.a, this->b + c.b);
        cout << tmp.a << " " << tmp.b << endl;
        return tmp;
    }

private:
    T a;
    T b;
};

int main()
{
    Complex<int> a(10, 20);
    Complex<int> b(20, 30);
    Complex<int> c = a + b;

    return 0;
}

函数模板和类模板的区别?

  • 实例化方式不同:函数模板实例化由编译程序在处理函数调用时自动完成,类模板实例化需要在程序中显式指定。

  • 实例化的结果不同:函数模板实例化后是一个函数,类模板实例化后是一个类。

  • 默认参数:类模板在模板参数列表中可以有默认参数。

  • 特化:函数模板只能全特化;而类模板可以全特化,也可以偏特化。

  • 调用方式不同:函数模板可以隐式调用,也可以显式调用;类模板只能显式调用。

#include<iostream>

using namespace std;

template <typename T>
T add_fun(const T & tmp1, const T & tmp2){
    return tmp1 + tmp2;
}

int main(){
    int var1, var2;
    cin >> var1 >> var2;
    cout << add_fun<int>(var1, var2); // 显式调用

    double var3, var4;
    cin >> var3 >> var4;
    cout << add_fun(var3, var4); // 隐式调用
    return 0;
}

C++模板 全特化、偏特化_

/**
 * 作者:lyn
 * 时间:2018.10.30
 * 该程序用来演示函数模板和类模板
 */
#include <iostream>

using namespace std;

//模板函数
template<typename T>
void add(T num1, T num2) {
        cout << num1 << " + " << num2 << " = "<< num1 + num2 << endl;
}

//模板类
template<typename T>
class Test_Class {
public:
        static void multi(T num1, T num2) {
                cout << num1 << " * " << num2 << " = "<< num1 * num2 << endl;
        }
};

int main(){
        //Test 1
        int num1 = 1;
        int num2 = 2;
        add<int>(num1, num2);
        Test_Class<int>::multi(num1, num2);
        //Test 2
        double num3 = 3.1;
        double num4 = 4.2;
        add<double>(num3, num4);
        Test_Class<double>::multi(num3, num4);
        return 0;
}

在这里插入图片描述

函数模板是可以被重载的(类模板不能被重载),也就是说允许存在两个同名的函数模板,还可以对它们进行实例化,使它们具有相同的参数类型。

函数模板重载样例:

/**
 * 作者:lyn
 * 时间:2018.10.30
 * 该程序用来掩饰函数模板的重载
 */
#include <iostream>

using namespace std;

//函数模板
template <typename T>
int fun(T){
        return 1;
}
//函数模板的重载
template <typename T>
int fun(T*){
        return 2;
}

int main(){
        cout << fun<int*>((int*)0) << endl;
        cout << fun<int>((int*)0) << endl;
        return 0;
}

在这里插入图片描述

特化

前面提到函数模板能被重载,而类模板不可以,我们可以通过特化来实现相似的效果,从而可以透明地获得具有更高效率的代码。
特化也分为了两种,全特化和偏特化。

不能将特化和重载混为一谈

全特化和偏特化都没有引入一个全新的模板或者模板实例。它们只是对原来的泛型(或者非特化)模板中已经隐式声明的实例提供另一种定义。
在概念上,这是一个相对比较重要的现象,也是特化区别于重载模板的关键之处。

全特化

全特化是模板的一个唯一特例,指定的模板实参列表必须和相应的模板参数列表一一对应。
我们不能用一个非类型值来替换模类型参数。但是如果模板参数具有缺省模板实参,那么用来题还实参就是可选的。

全特化类样例:

/**
 * 作者:lyn
 * 时间:2018.10.30
 * 该程序用来演示全特化类
 */
#include <iostream>
using namespace std;

template<typename T1, typename T2>
class A{
        public:
                void function(T1 value1, T2 value2){
                        cout<<"value1 = "<<value1<<endl;
                        cout<<"value2 = "<<value2<<endl;
                }
};

template<>
class A<int, double>{ // 类型明确化,为全特化类
        public:
                void function(int value1, double value2){
                        cout<<"intValue = "<<value1<<endl;
                        cout<<"doubleValue = "<<value2<<endl;
                }
};

int main(){
        A<int, double> a;
        a.function(12, 12.3);
        return 0;
}                                                                                                                     

在这里插入图片描述

偏特化

偏特化感觉像是介于普通模板和全特化之间,只存在部分的类型明确化,而非将模板唯一化。 再次划重点 函数模板不能被偏特化

偏特化类样例:

/**
 * 作者:lyn
 * 时间:2018.10.30
 * 该程序用来演示偏特化类
 */
#include <iostream>
using namespace std;

template<typename T1, typename T2>
class A{
        public:
                void function(T1 value1, T2 value2){
                        cout<<"value1 = "<<value1<<endl;
                        cout<<"value2 = "<<value2<<endl;
                }
};

template<typename T>
class A<T, double>{ // 部分类型明确化,为偏特化类
        public:
                void function(T value1, double value2){
                        cout<<"Value = "<<value1<<endl;
                        cout<<"doubleValue = "<<value2<<endl;
                }
};

int main(){
        A<char, double> a;
        a.function('a', 12.3);
        return 0;
}

在这里插入图片描述

偏特化的特殊例子

//这段代码是STL源码中的一部分
template <class Iterator>
struct iterator_traits {
  typedef typename Iterator::iterator_category iterator_category;
  typedef typename Iterator::value_type        value_type;
  typedef typename Iterator::difference_type   difference_type;
  typedef typename Iterator::pointer           pointer;
  typedef typename Iterator::reference         reference;
};
/*
 * 偏特化版本,我们可以看到他的唯一不同之处就是T*,这也是偏特化的一种
 */
template <class T>
struct iterator_traits<T*> {
  typedef random_access_iterator_tag iterator_category;
  typedef T                          value_type;
  typedef ptrdiff_t                  difference_type;
  typedef T*                         pointer;
  typedef T&                         reference;
};
重点总结
  1. 类模板和函数模板都可以被全特化;
  2. 类模板能偏特化,不能被重载;
  3. 函数模板全特化,不能被偏特化。

模板类调用优先级

对主版本模板类、全特化类、偏特化类的调用优先级从高到低进行排序是:
全特化类>偏特化类>主版本模板类
这样的优先级顺序对性能也是最好的。

什么是可变参数模板?

可变参数模板:接受可变数目参数的模板函数或模板类。将可变数目的参数被称为参数包,包括模板参数包和函数参数包。

  • 模板参数包:表示零个或多个模板参数;

  • 函数参数包:表示零个或多个函数参数。

用省略号来指出一个模板参数或函数参数表示一个包,在模板参数列表中,class... 或 typename... 指出接下来的参数表示零个或多个类型的列表;一个类型名后面跟一个省略号表示零个或多个给定类型的非类型参数的列表。当需要知道包中有多少元素时,可以使用 sizeof... 运算符。

template <typename T, typename... Args> // Args 是模板参数包
void foo(const T &t, const Args&... rest); // 可变参数模板,rest 是函数参数包

实例:

#include <iostream>

using namespace std;

template <typename T>
void print_fun(const T &t)
{
    cout << t << endl; // 最后一个元素
}

template <typename T, typename... Args>
void print_fun(const T &t, const Args &...args)
{
    cout << t << " ";
    print_fun(args...);
}

int main()
{
    print_fun("Hello", "world", "!");
    return 0;
}
/*运行结果:
Hello world !

*/

说明:可变参数函数通常是递归的,第一个版本的 print_fun 负责终止递归并打印初始调用中的最后一个实参。第二个版本的 print_fun 是可变参数版本,打印绑定到 t 的实参,并用来调用自身来打印函数参数包中的剩余值。

include " " 和 <> 的区别

include<文件名> 和 #include"文件名" 的区别:

  • 查找文件的位置:include<文件名> 在标准库头文件所在的目录中查找,如果没有,再到当前源文件所在目录下查找;#include"文件名" 在当前源文件所在目录中进行查找,如果没有;再到系统目录中查找。

  • 使用习惯:对于标准库中的头文件常用 include<文件名>,对于自己定义的头文件,常用 #include"文件名"

迭代器的作用?

迭代器:一种抽象的设计概念,在设计模式中有迭代器模式,即提供一种方法,使之能够依序寻访某个容器所含的各个元素,而无需暴露该容器的内部表述方式。

作用:在无需知道容器底层原理的情况下,遍历容器中的元素。

实例:

#include <iostream>
#include <vector>
using namespace std;

int main()
{
    vector<int> arr = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
    vector<int>::iterator iter = arr.begin(); // 定义迭代器
    for (; iter != arr.end(); ++iter)
    {
        cout << *iter << " ";
    }
    return 0;
}
/*
运行结果:
1 2 3 4 5 6 7 8 9 0
*/

泛型编程如何实现?

泛型编程实现的基础:模板。模板是创建类或者函数的蓝图或者说公式,当时用一个 vector 这样的泛型,或者 find 这样的泛型函数时,编译时会转化为特定的类或者函数。

泛型编程涉及到的知识点较广,例如:容器、迭代器、算法等都是泛型编程的实现实例。

  • 容器:涉及到 STL 中的容器,例如:vector、list、map 等,可选其中熟悉底层原理的容器进行展开讲解。

  • 迭代器:在无需知道容器底层原理的情况下,遍历容器中的元素。

  • 模板:可参考本章节中的模板相关问题。

什么是类型萃取?

C++之类型萃取

类型萃取一般用于模板中,当我们定义一个模板函数后,需要知道模板类型形参并加以运用时就可以用类型萃取。
比如我们需要在函数中进行拷贝,通常我们可以用内置函数memcpy或者自己写一个for循环来进行拷贝。比如:

template<typename T>
void Copy(T *dest, const T *src, size_t sz)
{
    if (_TypeTraits<T>::_IsPODType().Get())
    {
        cout << "内置类型用memcpy函数进行拷贝:" << endl;
        memcpy(dest, src, sz * sizeof(T));
    }
    else
    {
        cout << "自定义类型用for循环进行拷贝:" << endl;
        for (size_t i = 0; i < sz; i++)
        {
            *(dest + i) = *(src + i);
        }
    }
}

这是一个通用的拷贝函数,可以拷贝多种类型的数据,比如内置类型的int、double等和自定义结构体或类类型。我们可以看到其中用了两种拷贝方式—memcpy和for循环,为什么这里要用两种拷贝方式呢?

我们知道memcpy进行拷贝时效率很高,但是它是将所拷贝对象中的值全部原样地复制到目标对象中去,那么如果我们在对带指针的自定义对象进行拷贝时就会把源对象指针值拷贝到目标对象内指针内去,这样就造成了多个对象的指针指向同一块内存,这样就成了浅拷贝了。

这里为了避免浅拷贝的发生就特意用效率相对较低的for循环来对自定义类型的对象进行拷贝工作。因此这个拷贝函数里面就给出了两种拷贝方式。

搞清楚了为什么要用两种拷贝方式,那么重点来了—怎么分辨需要拷贝的对象是内置类型还是自定义类型呢?方法有很多,比如我们可以在一个函数里面罗列出各种内置类型,将模板函数的类型形参T满足传入这个函数,当满足罗列出的类型时就用返回一个true否则返回false。这是一种方法,但是这里我们给出另一种方法—类型萃取。

类型萃取解析

先给出代码:

struct _TrueType
{
    bool Get()
    {
        return true;
    }
};
struct _FalseType
{
    bool Get()
    {
        return false;
    }
};
template<typename T>
struct _TypeTraits
{
    typedef _FalseType _IsPODType;
};
template<>
struct _TypeTraits<int>
{
    typedef _TrueType _IsPODType;
};

解析:这里给出了四个结构体(ps:结构体默认访问类型为public比默认类型为private的类方便),前两个结构体一个为_TrueType表示内置类型,一个为_FalseType表示自定义类型,两个结构体内都定义了一个返回类型为bool类型的函数Get(),其中一个返回true一个返回false。

第三个为结构体模板,模板内将结构体类型_FalseType重定义为_IsPODType

(ps:在类或者结构体内对类型重定义后,重定义的类型属于这个类或结构体的成员类型,其作用域在这个类或结构体里面),当我们用这个结构体内的_IsPODType类型定义一个对象来调用函数Get()时就会返回一个false,表示T类型为自定义类型。

第四个结构体是对第三个结构体模板的特化,这里特化成为int类型,当然也可以特化为double或者char类型,这个结构体里面将结构体类型_TrueType重定义为_IsPODType,当传入的参数为int类型时用_IsPODType定义对象就可以调用_TrueType结构体内的Get函数,返回一个true,表示传入了内置类型。

进行测试: 对这段代码作了解释后,来结合上面的Copy函数运行一下,看能不能对内置类型和自定义类型的参数执行不同的拷贝方式。我们先调用Copy函数拷贝内置类型:

int main()
{
    //首先进行内置类型的拷贝 
    int array1[10] = { 0,1,2,3,4,5,6,7,8,9 };
    const size_t size = sizeof(array1) / sizeof(array1[0]);
    int array2[size] = { 1,2,3 };
    Copy(array2, array1, size);
    for (size_t i = 0; i < size; i++)
    {
        cout << array2[i] << " ";
    }
    cout << endl;

    return 0;
}

这里定义了两个int类型的数组,程序执行的操作是将数组array1内的数据拷贝到array2中去并对数组array2进行打印,我们的设计应该是用memcpy函数进行拷贝。来看看执行结果:

这里写图片描述

可以看到执行结果明显符合我们的设计。
那么现在来对自定义类型的对象进行拷贝:

int main()
{
    //进行自定义类型string类对象的拷贝
    string s1[] = { "11111","22222","33333","44444","55555" };
    const size_t size = sizeof(s1) / sizeof(s1[0]);
    string s2[size] = { "abcd" };
    Copy(s2, s1, size);
    for (size_t i = 0; i < size; i++)
    {
        cout << s2[i] << endl;
    }

    return 0;
}

这里定义两个string类对象数组s1和s2,程序进行将s1中的数据拷贝到s2中去的工作,按照我们的设计这里会调用for循环进行拷贝。我们来看结果:

这里写图片描述

可以看到,符合我们的预期结果,调用了for循环。
有时候我们需要对指针类型的对象进行类型萃取,可以用这个结构体模板:

template<typename T>
struct _TypeTraits<T*>
{
    typedef _TrueType _IsPODType;
};

因为所有的指针都用4个字节存储,且不会造成浅拷贝的问题,其实际上属于内置类型。
给出其他内置类型的特化:

struct _TypeTraits<short>
{
    typedef _TrueType _IsPODType;
};
struct _TypeTraits<long>
{
    typedef _TrueType _IsPODType;
};
struct _TypeTraits<long long>
{
    typedef _TrueType _IsPODType;
};
struct _TypeTraits<unsigned>
{
    typedef _TrueType _IsPODType;
};
template<>
struct _TypeTraits<char>
{
    typedef _TrueType _IsPODType;
};
struct _TypeTraits<unsigned char>
{
    typedef _TrueType _IsPODType;
};
template<>
struct _TypeTraits<double>
{
    typedef _TrueType IsPODType;
};
struct _TypeTraits<float>
{
    typedef _TrueType _IsPODType;
};
struct _TypeTraits<bool>
{
    typedef _TrueType _IsPODType;
};

设计模式

了解哪些设计模式?

《大话设计模式》一书中提到 24 种设计模式,这 24 种设计模式没必要面面俱到,但一定要深入了解其中的几种,最好结合自己在实际开发过程中的例子进行深入的了解。

设计模式有 6 大设计原则:

  • 单一职责原则:就一个类而言,应该仅有一个引起它变化的原因。

  • 开放封闭原则:软件实体可以扩展,但是不可修改。即面对需求,对程序的改动可以通过增加代码来完成,但是不能改动现有的代码。

  • 里氏代换原则:一个软件实体如果使用的是一个基类,那么一定适用于其派生类。即在软件中,把基类替换成派生类,程序的行为没有变化。

  • 依赖倒转原则:抽象不应该依赖细节,细节应该依赖抽象。即针对接口编程,不要对实现编程。

  • 迪米特原则:如果两个类不直接通信,那么这两个类就不应当发生直接的相互作用。如果一个类需要调用另一个类的某个方法的话,可以通过第三个类转发这个调用。

  • 接口隔离原则:每个接口中不存在派生类用不到却必须实现的方法,如果不然,就要将接口拆分,使用多个隔离的接口。

设计模式分为三类:

  • 创造型模式:单例模式、工厂模式、建造者模式、原型模式

  • 结构型模式:适配器模式、桥接模式、外观模式、组合模式、装饰模式、享元模式、代理模式

  • 行为型模式:责任链模式、命令模式、解释器模式、迭代器模式、中介者模式、备忘录模式、观察者模式、状态模式、策略模式、模板方法模式、访问者模式

下面介绍常见的几种设计模式:

  • 单例模式:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

  • 工厂模式:包括简单工厂模式、抽象工厂模式、工厂方法模式

    • 简单工厂模式:主要用于创建对象。用一个工厂来根据输入的条件产生不同的类,然后根据不同类的虚函数得到不同的结果。

    • 工厂方法模式:修正了简单工厂模式中不遵守开放封闭原则。把选择判断移到了客户端去实现,如果想添加新功能就不用修改原来的类,直接修改客户端即可。

    • 抽象工厂模式:定义了一个创建一系列相关或相互依赖的接口,而无需指定他们的具体类。

  • 观察者模式:定义了一种一对多的关系,让多个观察对象同时监听一个主题对象,主题对象发生变化时,会通知所有的观察者,使他们能够更新自己。

  • 装饰模式:动态地给一个对象添加一些额外的职责,就增加功能来说,装饰模式比生成派生类更为灵活。

什么是单例模式?如何实现?应用场景?

单例模式:保证类的实例化对象仅有一个,并且提供一个访问他的全局访问点。

应用场景:

  • 表示文件系统的类,一个操作系统一定是只有一个文件系统,因此文件系统的类的实例有且仅有一个。

  • 打印机打印程序的实例,一台计算机可以连接好几台打印机,但是计算机上的打印程序只有一个,就可以通过单例模式来避免两个打印作业同时输出到打印机。

实现方式:

单例模式可以通过全局或者静态变量的形式实现,这样比较简单,但是这样会影响封装性,难以保证别的代码不会对全局变量造成影响。

  • 默认的构造函数、拷贝构造函数、赋值构造函数声明为私有的,这样禁止在类的外部创建该对象;

  • 全局访问点也要定义成 静态类型的成员函数,没有参数,返回该类的指针类型。因为使用实例化对象的时候是通过类直接调用该函数,并不是先创建一个该类的对象,通过对象调用。

不安全的实现方式:

原因:考虑当两个线程同时调用 getInstance 方法,并且同时检测到 instance 是 NULL,两个线程会同时实例化对象,不符合单例模式的要求。

class Singleton{
private:
    static Singleton * instance;
    Singleton(){}
    Singleton(const Singleton& tmp){}
    Singleton& operator=(const Singleton& tmp){}
public:
    static Singleton* getInstance(){
        if(instance == NULL){
            instance = new Singleton();
        }
        return instance;
    }
};
Singleton* Singleton::instance = NULL;

分类:

  • 懒汉模式:直到第一次用到类的实例时才去实例化,上面是懒汉实现。

  • 饿汉模式:类定义的时候就实例化。

线程安全的懒汉模式实现:

方法:加锁
存在的问题:每次判断实例对象是否为空,都要被锁定,如果是多线程的话,就会造成大量线程阻塞。

class Singleton{
private:
    static pthread_mutex_t mutex;
    static Singleton * instance;
    Singleton(){
        pthread_mutex_init(&mutex, NULL); 
    }
    Singleton(const Singleton& tmp){}
    Singleton& operator=(const Singleton& tmp){}
public:
    static Singleton* getInstance(){
        pthread_mutex_lock(&mutex);
        if(instance == NULL){            
            instance = new Singleton();            
        }
        pthread_mutex_unlock(&mutex);
        return instance;
    }
};
Singleton* Singleton::instance = NULL;
pthread_mutex_t Singleton::mutex;

方法:内部静态变量,在全局访问点 getInstance 中定义静态实例。

class Singleton{
private:
    static pthread_mutex_t mutex;
    Singleton(){
        pthread_mutex_init(&mutex, NULL);
    }
    Singleton(const Singleton& temp){}
    Singleton& operator=(const Singleton& temp){}
public:
    static Singleton* getInstance(){ 
        static Singleton instance;
        return &instance;
    }
};
pthread_mutex_t Singleton::mutex; 

饿汉模式的实现:
饿汉模式本身就是线程安全的不用加锁。

class Singleton{
private:
    static Singleton* instance;
    Singleton(const Singleton& temp){}
    Singleton& operator=(const Singleton& temp){}
protected:
	Singleton(){} 
public:
    static Singleton* getInstance(){ 
        return instance;    
    }
};
Singleton* Singleton::instance = new Singleton();

C++ 线程安全的单例模式总结

什么是工厂模式?如何实现?应用场景?

工厂模式

这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。

简单来说,使用了C++多态的特性,将存在继承关系的类,通过一个工厂类创建对应的子类(派生类)对象。在项目复杂的情况下,可以便于子类对象的创建。

工厂模式的实现方式可分别简单工厂模式、工厂方法模式、抽象工厂模式,每个实现方式都存在优和劣。

最近炒鞋炒的非常的火,那么以鞋厂的形式,一一分析针对每个实现方式进行分析。

简单工厂模式

具体的情形

鞋厂可以指定生产耐克、阿迪达斯和李宁牌子的鞋子。哪个鞋炒的火爆,老板就生产哪个,看形势生产。

UML图:

简单工厂模式的结构组成

  1. 工厂类:工厂模式的核心类,会定义一个用于创建指定的具体实例对象的接口。

  2. 抽象产品类:是具体产品类的继承的父类或实现的接口。

  3. 具体产品类:工厂类所创建的对象就是此具体产品实例。

简单工厂模式的特点:

工厂类封装了创建具体产品对象的函数。

简单工厂模式的缺陷:

扩展性非常差,新增产品的时候,需要去修改工厂类。

简单工厂模式的代码:

Shoes为鞋子的抽象类(基类),接口函数为Show(),用于显示鞋子广告。

NiKeShoes、AdidasShoes、LiNingShoes为具体鞋子的类,分别是耐克、阿迪达斯和李宁鞋牌的鞋,它们都继承于Shoes抽象类。

 // 鞋子抽象类
class Shoes
{
public:
    virtual ~Shoes() {}
    virtual void Show() = 0;
};

// 耐克鞋子
class NiKeShoes : public Shoes
{
public:
    void Show()
    {
        std::cout << "我是耐克球鞋,我的广告语:Just do it" << std::endl;
    }
};

// 阿迪达斯鞋子
class AdidasShoes : public Shoes
{
public:
    void Show()
    {
        std::cout << "我是阿迪达斯球鞋,我的广告语:Impossible is nothing" << std::endl;
    }
};

// 李宁鞋子
class LiNingShoes : public Shoes
{
public:
    void Show()
    {
        std::cout << "我是李宁球鞋,我的广告语:Everything is possible" << std::endl;
    }
};

ShoesFactory为工厂类,类里实现根据鞋子类型创建对应鞋子产品对象的CreateShoes(SHOES_TYPE type)函数。

enum SHOES_TYPE
{
    NIKE,
    LINING,
    ADIDAS
};

// 总鞋厂
class ShoesFactory
{
public:
    // 根据鞋子类型创建对应的鞋子对象
    Shoes *CreateShoes(SHOES_TYPE type)
    {
        switch (type)
        {
        case NIKE:
            return new NiKeShoes();
            break;
        case LINING:
            return new LiNingShoes();
            break;
        case ADIDAS:
            return new AdidasShoes();
            break;
        default:
            return NULL;
            break;
        }
    }
};

main函数,先是构造了工厂对象,后创建指定类型的具体鞋子产品对象,创建了具体鞋子产品的对象便可直接打印广告。因为采用的是new的方式创建了对象,用完了要通过delete 释放资源资源哦!

int main()
{
    // 构造工厂对象
    ShoesFactory shoesFactory;

    // 从鞋工厂对象创建阿迪达斯鞋对象
    Shoes *pNikeShoes = shoesFactory.CreateShoes(NIKE);
    if (pNikeShoes != NULL)
    {
        // 耐克球鞋广告喊起
        pNikeShoes->Show();

        // 释放资源
        delete pNikeShoes;
        pNikeShoes = NULL;
    }

    // 从鞋工厂对象创建阿迪达斯鞋对象
    Shoes *pLiNingShoes = shoesFactory.CreateShoes(LINING);
    if (pLiNingShoes != NULL)
    {
        // 李宁球鞋广告喊起
        pLiNingShoes->Show();

        // 释放资源
        delete pLiNingShoes;
        pLiNingShoes = NULL;
    }

    // 从鞋工厂对象创建阿迪达斯鞋对象
    Shoes *pAdidasShoes = shoesFactory.CreateShoes(ADIDAS);
    if (pAdidasShoes != NULL)
    {
        // 阿迪达斯球鞋广告喊起
        pAdidasShoes->Show();

        // 释放资源
        delete pAdidasShoes;
        pAdidasShoes = NULL;
    }

    return 0;
}

输出结果:

[root@lincoding factory]# ./simpleFactory 
我是耐克球鞋,我的广告语:Just do it
我是阿迪达斯球鞋,我的广告语:Impossible is nothing
我是李宁球鞋,我的广告语:Everything is possible

工厂方法模式

具体情形:

现各类鞋子抄的非常火热,于是为了大量生产每种类型的鞋子,则要针对不同品牌的鞋子开设独立的生产线,那么每个生产线就只能生产同类型品牌的鞋。

UML图:

工厂方法模式的结构组成:

  1. 抽象工厂类:工厂方法模式的核心类,提供创建具体产品的接口,由具体工厂类实现。

  2. 具体工厂类:继承于抽象工厂,实现创建对应具体产品对象的方式。

  3. 抽象产品类:它是具体产品继承的父类(基类)。

  4. 具体产品类:具体工厂所创建的对象,就是此类。

工厂方法模式的特点:

  • 工厂方法模式抽象出了工厂类,提供创建具体产品的接口,交由子类去实现。
  • 工厂方法模式的应用并不只是为了封装具体产品对象的创建,而是要把具体产品对象的创建放到具体工厂类实现。

工厂方法模式的缺陷:

  • 每新增一个产品,就需要增加一个对应的产品的具体工厂类。相比简单工厂模式而言,工厂方法模式需要更多的类定义。
  • 一条生产线只能一个产品。

工厂方法模式的代码:

ShoesFactory抽象工厂类,提供了创建具体鞋子产品的纯虚函数。

NiKeProducer、AdidasProducer、LiNingProducer`具体工厂类,继承持续工厂类,实现对应具体鞋子产品对象的创建。

// 总鞋厂
class ShoesFactory
{
public:
    virtual Shoes *CreateShoes() = 0;
    virtual ~ShoesFactory() {}
};

// 耐克生产者/生产链
class NiKeProducer : public ShoesFactory
{
public:
    Shoes *CreateShoes()
    {
        return new NiKeShoes();
    }
};

// 阿迪达斯生产者/生产链
class AdidasProducer : public ShoesFactory
{
public:
    Shoes *CreateShoes()
    {
        return new AdidasShoes();
    }
};

// 李宁生产者/生产链
class LiNingProducer : public ShoesFactory
{
public:
    Shoes *CreateShoes()
    {
        return new LiNingShoes();
    }
};

main函数针对每种类型的鞋子,构造了每种类型的生产线,再由每个生产线生产出对应的鞋子。需注意的是具体工厂对象和具体产品对象,用完了需要通过delete释放资源。

int main()
{
    // ================ 生产耐克流程 ==================== //
    // 鞋厂开设耐克生产线
    ShoesFactory *niKeProducer = new NiKeProducer();
    // 耐克生产线产出球鞋
    Shoes *nikeShoes = niKeProducer->CreateShoes();
    // 耐克球鞋广告喊起
    nikeShoes->Show();
    // 释放资源
    delete nikeShoes;
    delete niKeProducer;

    // ================ 生产阿迪达斯流程 ==================== //
    // 鞋厂开设阿迪达斯生产者
    ShoesFactory *adidasProducer = new AdidasProducer();
    // 阿迪达斯生产线产出球鞋
    Shoes *adidasShoes = adidasProducer->CreateShoes();
    // 阿迪达斯球鞋广喊起
    adidasShoes->Show();
    // 释放资源
    delete adidasShoes;
    delete adidasProducer;

    return 0;
}

输出结果

[root@lincoding factory]# ./methodFactory 
我是耐克球鞋,我的广告语:Just do it
我是阿迪达斯球鞋,我的广告语:Impossible is nothing

抽象工厂模式

具体情形:

鞋厂为了扩大了业务,不仅只生产鞋子,把运动品牌的衣服也一起生产了。

UML图:

抽象工厂模式的结构组成(和工厂方法模式一样):

  1. 抽象工厂类:工厂方法模式的核心类,提供创建具体产品的接口,由具体工厂类实现。

  2. 具体工厂类:继承于抽象工厂,实现创建对应具体产品对象的方式。

  3. 抽象产品类:它是具体产品继承的父类(基类)。

  4. 具体产品类:具体工厂所创建的对象,就是此类。

抽象工厂模式的特点:

提供一个接口,可以创建多个产品族中的产品对象。如创建耐克工厂,则可以创建耐克鞋子产品、衣服产品、裤子产品等。

抽象工厂模式的缺陷:

同工厂方法模式一样,新增产品时,都需要增加一个对应的产品的具体工厂类。

抽象工厂模式的代码:

Clothe和Shoes,分别为衣服和鞋子的抽象产品类。

NiKeClothe和NiKeShoes,分别是耐克衣服和耐克衣服的具体产品类。

// 基类 衣服
class Clothe
{
public:
    virtual void Show() = 0;
    virtual ~Clothe() {}
};

// 耐克衣服
class NiKeClothe : public Clothe
{
public:
    void Show()
    {
        std::cout << "我是耐克衣服,时尚我最在行!" << std::endl;
    }
};

// 基类 鞋子
class Shoes
{
public:
    virtual void Show() = 0;
    virtual ~Shoes() {}
};

// 耐克鞋子
class NiKeShoes : public Shoes
{
public:
    void Show()
    {
        std::cout << "我是耐克球鞋,让你酷起来!" << std::endl;
    }
};

Factory为抽象工厂,提供了创建鞋子CreateShoes()和衣服产品CreateClothe()对象的接口。

NiKeProducer为具体工厂,实现了创建耐克鞋子和耐克衣服的方式。

// 总厂
class Factory
{
public:
    virtual Shoes *CreateShoes() = 0;
    virtual Clothe *CreateClothe() = 0;
    virtual ~Factory() {}
};

// 耐克生产者/生产链
class NiKeProducer : public Factory
{
public:
    Shoes *CreateShoes()
    {
        return new NiKeShoes();
    }

    Clothe *CreateClothe()
    {
        return new NiKeClothe();
    }
};

main函数,构造耐克工厂对象,通过耐克工厂对象再创建耐克产品族的衣服和鞋子对象。同样,对象不再使用时,需要手动释放资源。

int main()
{
    // ================ 生产耐克流程 ==================== //
    // 鞋厂开设耐克生产线
    Factory *niKeProducer = new NiKeProducer();

    // 耐克生产线产出球鞋
    Shoes *nikeShoes = niKeProducer->CreateShoes();
    // 耐克生产线产出衣服
    Clothe *nikeClothe = niKeProducer->CreateClothe();

    // 耐克球鞋广告喊起
    nikeShoes->Show();
    // 耐克衣服广告喊起
    nikeClothe->Show();

    // 释放资源
    delete nikeShoes;
    delete nikeClothe;
    delete niKeProducer;


    return 0;
}

输出结果:

[root@lincoding factory]# ./abstractFactory 
我是耐克球鞋,让你酷起来!
我是耐克衣服,时尚我最在行!

总结

以上三种工厂模式,在新增产品时,都存在一定的缺陷。

  • 简单工厂模式,,需要去修改工厂类,这违背了开闭法则。
  • 工厂方式模式和抽象工厂模式,都需要增加一个对应的产品的具体工厂类,这就会增大了代码的编写量。

那么有什么好的方法,在新增产品时,即不用修改工厂类,也不用新增具体的工厂类?

笔者在实际项目中看到一个封装性非常强的工厂类,在扩展新产品时,不需要修改工厂类,也不需要新增具体的工厂类,详细内容可以跳转至

C++ 深入浅出工厂模式(进阶篇)

什么是观察者模式?如何实现?应用场景?

观察者模式:定义一种一(被观察类)对多(观察类)的关系,让多个观察对象同时监听一个被观察对象,被观察对象状态发生变化时,会通知所有的观察对象,使他们能够更新自己的状态。

观察者模式中存在两种角色:

  • 观察者:内部包含被观察者对象,当被观察者对象的状态发生变化时,更新自己的状态。(接收通知更新状态)

  • 被观察者:内部包含了所有观察者对象,当状态发生变化时通知所有的观察者更新自己的状态。(发送通知)

应用场景:

  • 当一个对象的改变需要同时改变其他对象,且不知道具体有多少对象有待改变时,应该考虑使用观察者模式;

  • 一个抽象模型有两个方面,其中一方面依赖于另一方面,这时可以用观察者模式将这两者封装在独立的对象中使它们各自独立地改变和复用。

实现方式:

#include <iostream>
#include <string>
#include <list>
using namespace std;

class Subject;
//观察者 基类 (内部实例化了被观察者的对象sub)
class Observer
{
protected:
    string name;
    Subject *sub;

public:
    Observer(string name, Subject *sub)
    {
        this->name = name;
        this->sub = sub;
    }
    virtual void update() = 0;
};

class StockObserver : public Observer
{
public:
    StockObserver(string name, Subject *sub) : Observer(name, sub)
    {
    }
    void update();
};

class NBAObserver : public Observer
{
public:
    NBAObserver(string name, Subject *sub) : Observer(name, sub)
    {
    }
    void update();
};
//被观察者 基类 (内部存放了所有的观察者对象,以便状态发生变化时,给观察者发通知)
class Subject
{
protected:
    list<Observer *> observers;

public:
    string action; //被观察者对象的状态
    virtual void attach(Observer *) = 0;
    virtual void detach(Observer *) = 0;
    virtual void notify() = 0;
};

class Secretary : public Subject
{
    void attach(Observer *observer)
    {
        observers.push_back(observer);
    }
    void detach(Observer *observer)
    {
        list<Observer *>::iterator iter = observers.begin();
        while (iter != observers.end())
        {
            if ((*iter) == observer)
            {
                observers.erase(iter);
                return;
            }
            ++iter;
        }
    }
    void notify()
    {
        list<Observer *>::iterator iter = observers.begin();
        while (iter != observers.end())
        {
            (*iter)->update();
            ++iter;
        }
    }
};

void StockObserver::update()
{
    cout << name << " 收到消息:" << sub->action << endl;
    if (sub->action == "梁所长来了!")
    {
        cout << "我马上关闭股票,装做很认真工作的样子!" << endl;
    }
}

void NBAObserver::update()
{
    cout << name << " 收到消息:" << sub->action << endl;
    if (sub->action == "梁所长来了!")
    {
        cout << "我马上关闭NBA,装做很认真工作的样子!" << endl;
    }
}

int main()
{
    Subject *dwq = new Secretary();
    Observer *xs = new NBAObserver("xiaoshuai", dwq);
    Observer *zy = new NBAObserver("zouyue", dwq);
    Observer *lm = new StockObserver("limin", dwq);

    dwq->attach(xs);
    dwq->attach(zy);
    dwq->attach(lm);

    dwq->action = "去吃饭了!";
    dwq->notify();
    cout << endl;
    dwq->action = "梁所长来了!";
    dwq->notify();
    return 0;
}

posted @ 2022-02-24 11:29  DearLeslie  阅读(118)  评论(0编辑  收藏  举报