侧边栏壁纸
博主头像
胜星的博客博主等级

行动起来,活在当下

  • 累计撰写 23 篇文章
  • 累计创建 38 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

C++语言的基础知识

胜星
2021-03-10 / 0 评论 / 0 点赞 / 310 阅读 / 28010 字

C++基本语法

C++兼容C的部分

C++中动态分配内存的方式

  • new 相对于 malloc 的区别
    • new返回对应类型的指针,不需要对返回值进行类型转换
      malloc返回 void* ,在C中需要显式转换为所需的指针类型
      **C
      与C语义不同的地方:**
      C++中任何类型的指针都可以隐式转换为 void* 但不能由其隐式转换而来
      C语言中任何类型的指针都可以和 void* 互相隐式转换
    • new会调用目标类型的构造函数,new[]会调用目标类型的无参构造函数
      malloc只分配内存空间,calloc总是初始化为0
    • new会根据目标类型自动决定所需大小,malloc需要手动调用sizeof获取大小
  • delete 相对于 free 的区别
    • delete 会调用目标类型的析构函数,free 只是释放内存
  • new / delete 相对于 malloc / free 的区别
    • 前者是关键字与运算符,后者是函数

C++中,int等内建类型默认无参函数并不会初始化值,因此new int[] 中的值是随机值,但是 new int[]() 则会将值初始化为0

在Visual Studio编译器中,没有使用匹配的 new/deletenew[]/delete[] 不会产生异常或报错,也不会造成内存泄露,但是某些情况下底层会出现问题?

C++枚举

enum关键字用于定义枚举

// 枚举值从零开始,每次递增一,中间如果有额外值的话,后面则从该值继续加一
// 枚举类型匹配:将枚举作为形参时,必须传入该枚举中定义的某值以明确语义
// 枚举必须继承自内建的定点数类型
enum E_Direction { None, Up, Down, Left, Right };
// 声明一个类型为E_Direction名为direction的枚举变量,并将其值初始化为None
E_Direction direction = None;
// 枚举最常见的使用方式是在switch-case中
switch (direction)
{
    case Up:
        Tank.MoveUP();
        break;
    case Down:
        Tank.MoveDown();
        break;
    case Left:
        Tank.MoveLeft();
        break;
    case Right:
        Tank.MoveRight();
        break;
    case None:
    default:
        break;
}

C++内联

C++内联函数使用 inline 关键字来声明,使用方式与普通函数相同,区别在于,内联函数会将其函数体的部分展开在调用的位置,因此不会造成调用函数带来的性能损耗问题,同时,内联函数以调用函数的形式使用的,因此,其参数表达式会在执行函数体部分代码前计算出最终值然后进入函数体,由此避免了有参宏直接替换带来的运算符优先级问题

#define max(lhs,rhs) lhs>rhs?lhs:rhs
inline int max(int lhs, int rhs) { return lhs > rhs ? lhs : rhs; }

int i0 = 10, int i1 = 20;
// 调用有参宏
int r = max(++i0, ++i1);
// 编译时替换为
int r = ++i0 > ++i1 ? ++i0 : ++i1;
// 因此会产生额外的自增运算

// 调用内联函数
int r = max(++i0, ++i1);
// 编译时改变为
int lhs = ++i0;
int rhs = ++i1;
int r = lhs > rhs ? lhs : rhs;

不过,编译器在编译时会自作主张,较大的函数即使声明了内联也不会编译为内联函数,较小的函数即使没有声明内联,依旧可能被当成内联函数调用,因此,在源文件中定义的内联函数意义不大

头文件中定义的内联函数,若是没有声明为内联,且多个源文件引用了该头文件,则该函数会被重复定义导致报错,原因是,在进行预处理的时候,会将#include 处替换为该头文件中的所有内容——这样做主要是出于要使用一个函数或类,必须先声明该函数或类——然后,编译器会将所有源文件中的高级语言代码转换为机器语言代码,若是头文件中定义了一个函数,则该函数会在所有包含该头文件的源文件中生成对应的二进制代码,接着,链接器会将所有调用该函数的地方替换至包含该函数定义的源文件中位置,不过,此时有多个源文件定义了该函数,链接器并不知道要调用哪一个源文件中的该函数,所以会产生一个重定义的链接器错误。但是如果声明为内联函数的话,该函数会直接展开到源文件调用其的地方,所以不会产生函数定义,因此也不会造成链接器重定义错误

C++新增的特性

引用类型

引用的实质就是封装后的指针

// 一个可以修改的变量
int normalInt = 0;
// 一个不可修改的变量
const int constInt = 0;
// 一个指向常量int类型的指针
const int *pConstInt0 = &constInt;
// 常量指针同样可以指向变量
pConstInt0 = &normalInt;
// 另一种声明指向常量int类型指针的方式
int const *pConstInt1 = pConstInt0;
// 常量指针指向的地址可以改变
pConstInt1 = &normalInt;
// 常量指针指向的地址中的值不可改变,下面语句取消注释的话会产生编译错误
//*pConstInt1 = 5;

// 一个指向int类型的常量指针(在*后面加上const表示常量指针)
int *const constPointerInt = &normalInt;
// 此句会产生编译错误吗?为什么其中的值是可以修改的呢?
// 因为它指向的是int类型而非是指向常量int类型
*constPointerInt = 5;
// 这一句有哪些错误?
// 1. 指针所指向的地址不能修改,因为是一个常量指针
// 2. 指针所指向是普通int类型,而constInt却是常量int类型,因此产生类型不匹配的错误
constPointerInt = &constInt;

// 一个指向常量int类型的常量指针
const int *const constPointerConstInt = &constInt;
// 只可以用来获取该常量的值
*constPointerInt = *constPointerConstInt;

// 以下为对下面文字的应用
int &rInt = normalInt;
int *const cpi = &normalInt;
// rInt == *cpi;
const int &crInt = constInt;
const int *const cpci = &constInt;
// crInt == *cpci;

引用类型,本质上是一个 常量指针 ,而 const引用类型 ,本质上是 指向常量类型的常量指针

引用类型的应用:

  • 作为函数参数

    // 作为函数参数使用引用类型的方式为
    void Pow2(int &target) { target *= target; }
    void Pow2WithNormal(int target) { target *= target; }
    int main()
    {
        int target = 2;
    
        // 调用以值传递的方式声明形参的函数后,target的值并不会改变
        Pow2WithNormal(target);
        // target == 2
    
        // 调用以引用传递的方式声明形参的函数后,target的值将会改变
        Pow2(target);
        // target == 4
    }
    
  • 作为类成员变量

    class Object
    {
        int &m_i;
    public:
        Object(int &i) : m_i(i) { }
        void Pow2() { m_i *= m_i; }
    }
    
    Object* CreateObject()
    {
        int number = 0;
        return new Object(number);
    }
    
    int main()
    {
        int target = 2;
        // 以引用的方式构造对象并将引用赋值给成员变量
        Object object(target);
        // 其后,在类中修改该引用类型成员变量,其引用的变量也会发生改变
        object.Pow2();
        // target == 4
    
        // 需要注意的是,若是target先于Object销毁,在target销毁后,再使用
        // m_i成员变量,等同于悬空指针,例如:
        Object *pObject = CreateObject();
        // 此时,Pow2中修改m_i的值将会产生预期外的后果
        pObject->Pow2();
    
        delete pObject;
    }
    

函数默认参数

struct Tank
{
    int health, attack;
}
Tank GlobalTank;
void ResetTank(Tank &tank = GlobalTank, int health = 6, int attack = 2)
{
    tank.health = health;
    tank.attack = attack;
}

int main()
{
    Tank tank;
    // 以下调用均为合法调用
    ResetTank();
    ResetTank(tank);
    ResetTank(tank, 2);
    ResetTank(tank, 5, 3);
    return 0;
}

当函数中某些参数存在通用的默认值时,可以使用默认参数在形参列表中给其赋值

因为默认参数被实参替代是从左到右的,所以设置的默认参数必须是从右到左的

引用类型默认参数,可以初始化为全局变量

类型转换关键字

常量类型转换 const_cast

只能用于将常量类型指针转为非常量类型指针

int i = 0;
const int *pci = &i;
int *pi = const_cast<int*>(pci);

静态类型转换 static_cast

只能用于隐式类型转换,但是更安全(PS:依旧是鸡肋)

int i = 0;
double d = static_cast<double>(i);

动态类型转换 dynamic_cast

主要用于将基类指针转换为子类指针,如果转换失败会返回 nullptr

class Base { }; class Derived : public Base { };
Base *pBase = new Derived;
// 对象实际类型匹配,因此成功转换并赋值
Derived *pDerived = dynamic_cast<Derived*>(pBase);
// 对象实际类型不匹配,因此pInt被初始化为nullptr
int *pInt = dynamic_cast<int*>(pBase);

重解释(强制)类型转换 reinterpret_cast

是将目标内的内存重解释为想要转换的类型

// 0xFFFFFFFF 作为有符号整型是-1的补码
int i = -1;
// 0xFFFFFFFF 作为无符号整型是4294967295(2^32-1)
unsigned int ui = reinterpret_cast<unsigned int>(i);

命名空间和作用域

命名空间的声明与使用

作用域覆盖问题

  • 局部变量的名称与类名重复
  • 类名与全局变量的名称重复

字符与字符串

面向对象

C++中实现面向对象的方式是类或结构体

在C++中,结构体与类唯一的区别在于结构体默认的访问权限为public,而类为private

类包括成员变量和成员函数

类的大小

  • 没有成员变量的时候,大小为1,否则取决于成员变量总计的大小
  • 有成员变量的时候,默认对齐到4字节,小于4字节的成员变量,同样会占用4字节的空间
  • 若是有虚函数的话,会产生一个虚函数表指针
  • 若是虚继承自某个类的话,会产生一个虚基类表指针(VS才有)
  • 虚函数表指针和虚基类表指针的大小为一个普通指针的大小,32位平台下是4字节,64位平台下是8字节

类的成员

可以包括除了命名空间外的所有顶级类型,不过比较特殊的有:

  • 成员变量
  • 成员函数

其特殊之处在于,必须通过一个类的对象(即实例)来引用它们。

但是,如果将变量或函数声明为静态的,则只是在类作用域下的全局变量或函数,区别在于,它们是通过 类名::名称 的方式来引用的,不需要定义对象也可以获取或调用它们,因此静态成员变量并不占用类对象的大小

类的声明与定义

#include <iostream>
#include <iomanip>
#include <string>
#include <deque>
#include <algorithm>

using namespace std;

// 类的声明形式:class 类名
class Object
{
// 私有的保护限定符
private:
    // 私有类成员变量
    int m_intField;
    // 私有类成员函数
    void PrivateMethod() { }
// 公有的保护限定符
public:
    // 无参构造函数
    Object()
    {
        m_intField = 0;
    }
    // 有参构造函数
    Object(int intField)
    {
        m_intField = intField;
    }
    // 公有类成员函数,且用const修饰了this指针
    int getIntField() const { return m_intField; }
    // 命名空间外的任何顶级类型都可以在类中以任何级别的保护限定符声明
};

int main()
{
    // 使用无参构造函数 定义一个类对象(实例)
    Object object;
    // 使用有参构造函数 定义一个类对象
    Object objectWithParam1(1);
    Object objectWithParam2 = 2;
    // 使用指针引用一个栈上的类对象
    Object *objectPointer = &object;
    // 使用指针引用一个堆上的类对象
    Object *pObject = new Object;
    // 使用指针引用一组堆上的类对象
    Object *objectArray = new Object[5];

    cout
        << "object.getIntField() = " << object.getIntField() << endl
        << "objectWithParam1.getIntField() = " << objectWithParam1.getIntField() << endl
        << "objectWithParam2.getIntField() = " << objectWithParam2.getIntField() << endl << endl;

    cout
        << "&object = " << &object << endl
        << "objectPointer = " << objectPointer << endl
        << "objectPointer->getIntField() = " << objectPointer->getIntField() << endl << endl;

    cin.get();

    return 0;
}

类的基本概念

构造函数

  • 无参构造函数

  • 拷贝构造函数

    • 拷贝构造函数调用于:

      • 以一个现有的类对象创建一个新的类对象时
      • 在实参以值传递的方式调用函数时

      因为拷贝构造函数在以值传递的方式调用函数时会被隐式调用,因此,拷贝构造函数只能用引用传递的方式声明形参,否则会无限调用自身

      class Object
      {
          public:
          // 错误的,以值传递的方式声明形参
          Object(Object object) { }
          // 正确的,以引用传递的方式声明形参
          Object(Object &object) { }
          Object(const Object &object) { }
      }
      
    • 默认的拷贝构造函数会将所有变量的值拷贝至新的对象中

      • 因此,如果某个成员变量是指针的话,可能导致两个对象引用同一块内存空间,在其中一个对象销毁的时候,会释放这块内存空间,导致另一个对象的指针变为悬空指针

      • 由此,出现了浅拷贝与深拷贝的概念

        若是对象中没有动态分配内存的成员变量,则深拷贝等同于浅拷贝
        若是对象中具有使用 new/delete 管理的成员变量,此时深拷贝为在拷贝构造函数中,需要对该成员变量重新分配内存空间并拷贝其中的值,而浅拷贝则只是使它们指向同一块内存空间,因此会出现前面所述的问题,此时就应该用深拷贝

      class Object
      {
      private:
          static const int SIZE = 5;
          int *m_dynamicInt;
      public:
          Object()
          {
              m_dynamicInt = new int[SIZE];
          }
          Object(const Object &object)
          {
              // 浅拷贝
              m_dynamicInt = object.m_dynamicInt;
              // 深拷贝
              m_dynamicInt = new int[SIZE];
              for (int i = 0; i < SIZE; ++i)
                  m_dynamicInt[i] = object.m_dynamicInt[i];
          }
          ~Object()
          {
              delete[] m_dynamicInt;
          }
       }
      
  • 有参构造函数

    • 在某些奇葩的地方会把只有一个参数的有参构造函数成为转换构造函数

    • 在构造函数前修饰 explicit 可以避免将参数隐式转换为当前类型

      class Object
      {
      private:
          int m_i;
      public:
          // void Foo(Object object) { }
          // 使用这种方式声明有参构造函数时,可以用以下方式定义对象
          // Object object = 1;
          // Foo(1); // 隐式调用构造函数
          Object(int i) : m_i(i) { }
          // 如果加了关键字的话
          explicit Object(int i) : m_i(i) { }
          // Object object(1);
          // Foo(Object(1)); // 如果没有显示调用Object构造函数,会直接报类型不匹配的错误
      }
      
  • 构造顺序与初始化列表

    • 构造顺序规则有两条
      基类构造顺序先于当前类构造顺序
      成员变量构造先于构造函数函数体

    • 析构顺序与构造顺序相反

    • 初始化列表语法

      构造函数:
      类名(参数列表) : 初始化列表 { }
      
      初始化列表具体的形式是,在冒号后面写上成员变量名+括号,括号内写初始化的参数,如果没有初始化的值,则表示调用该成员变量的无参构造函数,如果有多个在初始化列表中初始化的成员变量,成员变量之间用逗号隔开
      

      必须在初始化列表初始化的成员变量有:

      • 引用类型成员变量

        // 在类中定义
        int &m_i;
        
      • 常量类型成员变量

        // 在类中定义
        const int m_i;
        
      • 没有无参构造函数的成员变量

      • 或者没有无参构造函数的基类

      成员变量在C++11中引入了默认初始值
      即在类中定义的时候使用 = 或者 { params } 的语法来设定一个初始值,其意义在于若是构造函数的初始化列表没有该成员变量,则会使用初始值来构造该成员变量

      若是也没有初始值的话,则会调用默认的无参构造函数

析构函数

this指针与非静态成员函数

运算符重载

语法:

#include <iostream>

class MyString
{
    // 声明类成员运算符重载函数
    MyString& operator+=(const MyString &ms);
    // 声明友元运算符重载函数
    friend std::ostream operator<<(std::ostream &os, const MyString &ms);
}
// 声明全局运算符重载函数
double operator%(double lhs, double rhs);
// 定义友元运算符重载函数
std::ostream operator<<(std::ostream &os, const MyString &ms)
{
    // ToDo: Codes...
    return os;
}
可以重载的运算符
  • 算术运算符 + - * / % ++ -- 单目的+-(推荐友元重载)
  • 位运算符 << >> & | ~ ^
  • 关系运算符 == != < <= > >=(推荐友元重载)
  • 逻辑运算符 && || !
  • newdelete 运算符
  • 逗号运算符 , (PS:没用过
  • 流运算符必须友元重载
  • 只能作为成员函数重载的有
  • 赋值运算符 = += -= *= /= %= <<= >>= &= |= ^=
  • 地址运算符 * -> &
  • 函数运算符 ()
  • 数组运算符 []
不能重载的运算符
  • 作用域运算符 ::
  • 三目运算符
  • 成员访问运算符 .
  • 成员指针访问运算符 .* (PS:没用过
  • 长度运算符 sizeof
运算符重载操作数

运算符重载不能更改原运算符的操作数

单目运算符有一个操作数

双目运算符有两个操作数

三目运算符有三个操作数(但不能重载

有隐含参数的函数,会将this指针作为第一个操作数

没隐含参数的函数,至少有一个操作数

前置自增自减运算符将当前类型引用作为第一个操作数,后置需要增加一个用作标识没有实际意义的int类型参数

class MyInt
{
    int m_i;
public:
    MyInt() : m_i(0) { }
    MyInt(int i) : m_i(i) { }
    // Prefix 前置
    int operator++() { return m_i; }
    // Suffix 后置
    int operator++(int) { int i = m_i; ++m_i; return i; }
    
    // 自增和自减都可以使用成员函数或友元函数来定义
    friend int operator--(MyInt &mi);
    friend int operator--(MyInt &mi, int);
}
int operator--(MyInt &mi)
{
    return --mi.m_i;
}
int operator--(MyInt &mi, int)
{
    int i = mi.m_i;
    --mi.m_i;
    return i;
}

封装

在类中,基本所有内容都可以使用三种保护级别之一来限定,这三种保护级别是:

  • public 公有的,任何地方都可以直接访问
  • protected 保护的,只有类内或派生类中可以访问
  • private 私有的,只有类内中可以访问

但是,在C++中,有一种特殊的方式,可以破坏类的封装性,那就是友元

友元的声明方式,是在希望被外部访问到私有及保护成员的类中,使用 friend 关键字修饰该类名或函数名

友元可能出现以下几种:

  • 友元函数

    • 友元全局函数
    • 友元成员函数 (其他类的成员函数)
  • 友元类

    class FriendClass
    {
    
    }
    class MasterClass
    {
        // FriendClass 中所有函数都可以访问到MasterClass中的私有及保护成员
        friend FriendClass;
    }
    

在以上数种友元的形式中,比较容易弄混的是友元函数不是成员函数这个概念,其最主要的区别在于,友元函数不需要通过类名作用域修饰符来调用,友元函数没有this指针

继承

继承的语法及方式

C++中类和结构体只有默认的访问权限有区别,因此完全可以互相继承

class Base { private m_pri;protected m_pro;public m_pub}
class Derived : Base { }

继承的语法,就是在派生类声明的名称后加上冒号以及要继承的基类名,继承后,派生类拥有基类的所有成员变量及成员函数

继承的访问级别修饰符作用

继承同样可以使用封装的三种访问级别修饰符,默认使用的是 private 修饰符

其修饰作用,是将基类的公有及保护成员以继承的访问级别在派生类中体现

  • 无论以什么形式继承,派生类无法访问基类的私有成员且可以访问基类的公有与保护成员
  • 以公有的形式继承,外界可以访问基类的公有成员,派生类的派生类可以访问保护成员
  • 以保护的形式继承,外界无法访问基类的公有成员,派生类的派生类可以访问
  • 以私有的形式继承,只有派生类可以访问基类的公有与保护成员

需要注意的是,同时继承多个类时,访问权限修饰符只对当前类有效,因此 :

class PublicBase { } class PrivateBase { }
// PublicBase会被修饰为以public访问级别继承,而PrivateBase会以默认的私有访问级别继承
class Derived : public PublicBase, PrivateBase { }

需要补充的是:结构体默认以什么方式继承

多重继承与虚继承

多重继承的语法如同上面所述,后面的基类与前面的基类名之间用 , 隔开

虚继承的语法就是在访问级别修饰符前面或者后面加上 virtual 关键字,例如

class Derived : virtual public PublicBase, private virtual PrivateBase { }

虚继承可以只修饰部分基类,被修饰的基类才会被虚继承,虚继承是用来解决类似下面这种情况的问题:

菱形继承

一个类继承自两个基类,而这两个基类右同时继承自一个类,就是菱形继承

class JediKnight
{
public:
    int LightSaber = 0x11111111;
    int TheForce = 0x22222222;
};

class AnakinSkywalker : virtual public JediKnight
{
public:
    int LightMaster = 0x33333333;
    int LightSide = 0x44444444;
};

class DarthVader : virtual public JediKnight
{
public:
    int DarkMaster = 0x55555555;
    int DarkSide = 0x66666666;
};

class LukeSkywalker : public AnakinSkywalker, public DarthVader
{
public:
    int Mother = 0x77777777;
    int Friend = 0x88888888;
};

若是 AnakinVader 没有虚继承自 Jedi,那么 Luke 体内将会有两份 Jedi

继承后的内存模型

派生类虚继承自基类后,基类的数据排布在派生类的数据后面,若是派生类被其他类虚继承,则派生类数据会再次排到基类数据后面

存在虚继承的类会在前面生成虚基类表指针,但若是同时继承自另一个已经有虚基类表指针的类时,将会合并在第一个虚基类表指针

多态

函数重载与名称粉碎

函数重载两同三不同

  • 名称相同
  • 作用域相同
  • 参数类型不同
  • 参数个数不同
  • 参数顺序不同

函数重载将会在底层使用名称粉碎的机制实现,即函数在底层中的名称会根据参数类型和返回值类型增加一系列的标志,这个机制将导致未支持函数重载的语言(例如C)无法调用名称粉碎后的函数,因此,可以使用 extern "C" 的语法来关闭某些函数的名称粉碎效果

// 关闭了名称粉碎
extern "C" void Foo();
// 未关闭名称粉碎
void Foo(int);

C++中返回值不同不能构成函数重载

函数与变量重定义

主要指派生类隐藏基类成员,具体形式是,派生类成员变量与基类成员变量名称相同,则优先使用派生类成员变量,派生类成员函数与基类成员函数名称相同参数相同或不同,则隐藏所有该名称基类成员函数

虚函数

虚函数定义语法

虚函数其实很简单,就是在需要声明为虚函数的成员函数前加上 virtual 关键字,然后在子类定义同名同参的函数时,可以重写基类的该函数,重写意味着,将来用基类指针或引用来指向子类对象的时候,调用的函数时重写后的函数

只有成员函数才能被声明为虚函数,因此不能声明为虚函数的包括:

  • 构造函数
  • 全局函数
    • 静态函数 (类作用域下的全局函数
    • 友元函数 (具有类私有及保护成员访问权限的全局函数
    • 全局函数

虚析构函数

虚析构函数最主要的作用在于,使用基类指针释放子类的对象时,如果没有声明为虚析构函数,则不会调用子类的析构函数,因此可能造成子类中动态分配的内存没有释放,由此造成内存泄漏

虚函数调用方式

虚函数在底层是通过一个叫做虚函数表vfptr(Virtual Function Pointer) 的机制实现的

虚函数表包括了一系列该对象成员函数的位置,编译时,生成虚函数表的方式遵循以下原则

  1. 为了调用方便,同名的虚函数在父类与子类中对应的下标是一样的
  2. 当子类重写了父类中的虚函数,虚函数表中存储的就是子类的虚函数
  3. 当子类没有重写父类的虚函数,虚函数表中存储的就是父类的虚函数
  4. 当子类自己增加了新的虚函数,添加的函数就会存储在表的最后

在调用虚函数时,会先获取虚函数表,然后根据虚函数的索引调用实际应调用的函数,此方式被称作动态联编,而其他所有多态都是在编译时决定实际调用的代码,因此称作静态联编

class Animal
{
    int tag = 0x11111111;
public:
    //~Animal() { cout << "Animal Deleted" << endl; }
    Animal() { }
    virtual ~Animal() { cout << "Animal Deleted" << endl; }
    virtual void Voice() = NULL;
};

void Animal::Voice()
{
    cout << "Animal Voice:!!!!!!" << endl;
}

class Cat : public Animal
{
public:
    virtual ~Cat() { cout << "Cat Deleted" << endl; }
    virtual void Voice() override
    {
        cout << "Cat Voice:Miao~~~" << endl;
    }
};

class Dog : public Animal
{
public:
    virtual ~Dog() { cout << "Dog Deleted" << endl; }
    virtual void Voice() override
    {
        cout << "Dog Voice:Wang~~~" << endl;
    }
};

class Pig : public Animal
{
public:
    virtual ~Pig() { cout << "Pig Deleted" << endl; }
    //// 使用override后编译器会自动检测是否成功重新
    //virtual void Voioe() override
    //{
    //    cout << "Dog Voice:Hong~~~" << endl;
    //}
    virtual void Voice() override
    {
        cout << "Dog Voice:Hong~~~" << endl;
    }
};

class BaseEmptyClass
{
    int m_number;
public:
    BaseEmptyClass(int number = 0) : m_number(number) { }
    virtual ~BaseEmptyClass() { }
    virtual void Method1() { cout << "Empty Method1 called!" << endl; }
    virtual void Method2() { cout << "Empty Method2 called!" << endl; }
    virtual void Method3() { cout << "My number is " << m_number << endl; }
};

class Derived1 : public BaseEmptyClass
{
public:
};

class Derived2 : public BaseEmptyClass
{
public:
    virtual void Method1() override { }
};

class Derived3 : public BaseEmptyClass
{
public:
    virtual void Method1() override { }
    virtual void Method2() override { }
};

class Derived4 : public BaseEmptyClass
{
public:
    virtual void Method1() override { }
    virtual void Method2() override { }
    virtual void Method3() { }
};

int main()
{
    Animal *pAnimals[] = { new Cat, new Dog, new Pig };
    for (int i = 0; i < _countof(pAnimals); ++i)
    {
        pAnimals[i]->Voice();
        delete pAnimals[i];
        pAnimals[i] = nullptr;
    }
    void *ptr = operator new (sizeof(Animal));
    Animal *pAnimal = (Animal*)ptr;
    pAnimal->Animal::Animal();
    pAnimal->Voice();
    using Action = void(*)();

    // 当一个类中存在虚函数时,就会产生一个虚函数表(vfptr, Virtual Function Pointer)
    // 虚函数表指针指向一个虚函数表,虚函数表中存放了该类中所有虚函数的调用位置
    // 一个类的所有实例共用同一张虚函数表,一个类只有一个对应的虚函数表

    //vfptr : 
    // - jmp BaseEmptyClass 
    BaseEmptyClass ec;
    int of0 = 0xCCCCCCCC;
    // 当子类没有重新父类的虚函数,虚函数表存储的就是父类的虚函数
    Derived1 d1;
    int of1 = 0xCCCCCCCC;
    // 当子类重写了父类中的虚函数,子类的虚函数表中存储的就是子类自身的虚函数表
    Derived2 d2;
    int of2 = 0xCCCCCCCC;
    Derived3 d3;
    int of3 = 0xCCCCCCCC;
    // 当子类自己添加了虚函数,添加的函数就会存储在表的最后
    Derived4 d4;
    int of4 = 0xCCCCCCCC;
    // 为了调用方便,同名的虚函数在虚函数表中的下标是一样的

    BaseEmptyClass bec, *pBec = &bec;
    // 从对象地址找到虚函数表
    //int vfptr = *(int*)&bec;
    //int* vftable = (int*)vfptr;
    //typedef void(*Action)();
    //Action action = (Action)vftable[1];
    //action();

    int vfptr = *(int*)&bec;
    int* vftable = (int*)vfptr;
    typedef void(*Action)();
    Action action = (Action)vftable[3];
    // 如果虚函数访问了任何成员,都需要传入对象的地址
    __asm lea ecx, [bec];
    action();

    return 0;
}

虚函数对效率的损耗

虚函数因为会先索引虚函数表,因此是调用最慢的函数,而调用最快的函数是内联函数,全局函数静态函数成员函数调用效率相同

纯虚函数与抽象类

声明纯虚函数的格式为:

class Object
{
public:
    virtual void Foo() = NULL;
}

只有虚函数能被声明为纯虚函数

当一个类中定义了纯虚函数存在由基类继承且没有重写的纯虚函数时,该类为抽象类

抽象类的特点是不能被实例化,因此纯虚函数在强制要求派生类实现某些函数的时候使用

模板

模版使用语法

模版有两种形式:模版类,模版函数

在类或函数的前面加上 template<需要参数化的类型> 可以定义一个模板

#include <iostream>
#include <iomanip>
#include <string>
#include <map>
#include <vector>
#include <list>
#include <set>
#include <deque>
#include <algorithm>

using namespace std;

// 1. 编写函数模板通常需要先编写一个非模板的函数,再进行修改
template <typename T>
inline const T& Max(const T &lhs, const T &rhs)
{
    return lhs > rhs ? lhs : rhs;
}
template <typename T>
inline const T& MaxIf(const T &lhs, const T &rhs)
{
    if (lhs > rhs) return lhs; return rhs;
}

template <typename STL, typename T>
inline auto Find(const STL &stl, const T &t)
{
    return find(stl.begin(), stl.end(), t);
}

template <typename T, typename TValue>
inline auto Find(const map<T, TValue> &stl, const T &t)
{
    return stl.find(t);
}

int main()
{
    int i = Max(1, 2);
    int iIf = MaxIf(1, 2);

    cout << i << endl;
    cout << iIf << endl;

    cin.get();

    return 0;
}

模版特化

省略部分 template <> 后面尖括号里的内容,然后进行定义

template <class T>
void sort(T *array, int size)
{
    // 1. 传入的第一个参数是数组
    // 2. 传入数组对应的大小

    for (int i = 0; i < size - 1; ++i)
    {
        for (int j = 0; j < size - i - 1; ++j)
        {
            if (array[j] > array[j + 1])
            {
                T temp = array[j];
                array[j] = array[j + 1];
                array[j + 1] = temp;
            }
            // show(array, size, true);
        }
        // show(array, size);
    }
}

// 特化字符串
template <>
void sort<const char*>(const char* *array, int size)
{
    // 1. 传入的第一个参数是数组
    // 2. 传入数组对应的大小

    for (int i = 0; i < size - 1; ++i)
    {
        for (int j = 0; j < size - i - 1; ++j)
        {
            // 字符串比对, 当大于或小于输出都是非0(TRUE)【>0】
            if (strcmp(array[j], array[j + 1]) > 0)
            {
                const char* temp = array[j];
                array[j] = array[j + 1];
                array[j + 1] = temp;
            }
            // show(array, size, true);
        }
        // show(array, size);
    }
}

int main()
{
    int array[10] = { 7, 5, 9, 6, 8, 3, 4, 2, 1, 0 };
    sort(array, 10);

    const char* str_array[3] = {
        "123",
        "567",
        "456"
    };
    sort(str_array, 3);

    return 0;
}

重载: 相同作用域下,函数名相同,参数的顺序、个数、类型不同(不能重载仅有返回值相同的函数)
重定义: 继承关系中, 函数名相同,对参数列表和返回值没有要求
重写: 继承关系中, 函数名相同,在有虚函数的时候,函数原型必须完全相同

0
C++

评论区