C++ 学习8 (10.8-10.9)

197 阅读8分钟

1.1  C++ 接口(纯虚函数) interface  &  pure virtural function

1. 在面向对象编程中,接口是用于描述类所提供的功能集合,而不涉及具体实现细节的抽象类。接口定义了一组纯虚函数(pure virtual function),这些函数在派生类中必须被实现
2. 接口的概念在C++中通常通过抽象基类(Abstract Base Class)来实现。抽象基类是一个包含至少一个纯虚函数的类,它可以被继承,并且作为其他类的公共接口。 一般情况下,将接口类的名称以及命名约定加上 "Interface" 后缀,

1, 纯虚函数(Pure Virtual Function)是在C++中的一种特殊函数,它在基类中声明但没有提供实际的实现。纯虚函数通过在函数声明的末尾使用 "= 0" 进行标记。
2, 纯虚函数的主要目的是定义一个接口,而不涉及具体的实现细节。在基类中声明一个或多个纯虚函数后,派生类必须实现这些函数,以便成为一个完整的类。如果派生类没有实现基类中的所有纯虚函数,那么派生类也会成为一个抽象类,无法直接实例化。
3 , 纯虚函数为多态提供了一个重要的机制。基类的指针或引用可以指向派生类的对象,并且通过调用纯虚函数,可以根据指针或引用所指向的实际对象类型来执行相应的操作。

其他语言有 interface 关键字而不是class.接口只是C++的类。接口中的纯虚函数是所有的实例都要实现的,否则不能进行实例化。

#include <iostream>
#include <string>
class Printable//接口也是一个类,只不过里面有纯虚函数
{
public:
   virtual std::string GetClassName() = 0;
};//纯虚函数,所有的实列都要实现。

class Entity : public Printable
{
public:
   virtual std::string GetName(){ return "Entity" ;} //这里需要特别注意不要忘记了  ;
   std::string GetClassName() override {return "Entity"; }
};

class Player : public Entity //间接继承     : Entity, Printable //继承
{
private:
   std::string m_Name;
public:
   Player(const std::string& name)
      : m_Name(name) {}

   std::string GetName() override { return m_Name; }
   std::string GetClassName() override {return "Player";}
};

class A : public Printable
{
public:
   std::string GetClassName() override { return "A"; }
};

void PrintName(Entity* entity)
{
   std::cout << entity->GetName() << std::endl;
}

void Print(Printable* obj)
{
   std::cout << obj->GetClassName() << std::endl;
}

int main()
{
   Entity* e = new Entity();
   //PrintName(e);
   Player* p = new Player("Cherno");
   Print(e);
   Print(p);
   Print(new A());//会造成内存泄漏

   delete e;  // 释放通过 new 创建的对象
   delete p;// 因为Player类继承自Entity类,
//Entity类中有虚析构函数,所以可以通过Entity类指针释放Player对象
   delete new A();  // 释放通过 new 创建的对象

   std::cin.get();
}

结果:

解析:这段代码定义了一个名为Printable的接口类和它的两个派生类Entity和Player,以及另一个派生类A。 在Printable类中,声明了一个纯虚函数GetClassName(),该函数在派生类中必须被实现。Printable类是一个接口类,无法被直接实例化,只能被用作基类。 Entity类继承自Printable类,并重写了GetClassName()函数,还定义了一个GetName()函数,返回字符串"Entity"。Entity类是一个具体类,可以直接实例化。 Player类继承自Entity类,间接继承了Printable接口,并在其构造函数中接受一个字符串参数作为玩家的名字。Player类也重写了GetClassName()函数和GetName()函数。 A类继承自Printable类,并实现了GetClassName()函数,返回字符串"A"。 PrintName()函数接受一个指向Entity对象的指针,并使用GetName()函数打印出该对象的名字。 Print()函数接受一个指向Printable对象的指针,并使用GetClassName()函数打印出该对象的类名。 在main()函数中,创建了一个Entity对象和一个Player对象,并分别调用Print()函数打印它们的类名。 注意,在最后使用new关键字创建的A对象没有被释放,导致内存泄漏。应该在使用完毕后通过delete关键字释放动态分配的内存,以免造成内存泄漏。 修正代码即为将main()函数中的delete new A();改为delete new A();,确保正确释放内存。

#include <iostream>

class Animal {
public:
   virtual void MakeSound() const = 0;//声明纯虚函数
};
//派生类 Cat
class Cat : public Animal {
public:
   void MakeSound() const override {
      std::cout << "Memo!" << std::endl;//实现纯虚函数
   }
};
//派生类 Dog
class Dog : public Animal {
public:
   void MakeSound() const override
   {
      std::cout << "Woof!" << std::endl;
   }
};

int main()
{
   Animal* cat = new Cat();
   Animal* dog = new Dog();

   cat->MakeSound();
   dog->MakeSound();

   delete cat;
   delete dog;

   return 0;
}

结果:

关于const的作用:用于修饰成员函数纯虚函数中添加const,表示该成员函数在执行的过程中不会修改任何成员变量的值。具体意味着修饰的该函数也是一个常成员函数。                总之,const修饰的纯虚函数的声明中表示这个函数不会修改成员变量,同时也支持被常对象调用。

关于 Animal* cat = new Cat(); 它的作用是用基类指针指向一个派生类的对象,也成为向上转型(upcasting)。

new Cat()在内存中动态创建一个Cat对象。返回该对象在内存中的地址,也就是指向该对象的指针,因此,new Cat()的返回值实际上就是一个指向cat对象的指针。

Animal* cat = new Cat(); 指针变量为cat 类型为  Animal*。所以返回的是指向Cat对象的指针,也就是该对象在内存中的地址。

 为什么需要用指针:为了实现多态性;子类Cat 和 Dog 实例化为父类对象Animal的Dog类的MakeSound()函数。

1.2 C++的可见性(visibility)

可见性:指的是类中的成员变量和成员函数对外部的可访问性。

补充一些单词:  Public - 公有的           Protected - 保护的             Private - 私有的 Access modifier - 访问修饰符              Encapsulation - 封装性                     Member access - 成员访问                  Inheritance - 继承              Friend - 友元             Class scope - 类作用域               Namespace - 命名空间                       Access control - 访问控制              Member visibility - 成员可见性                 Inaccessible - 不可访问的 Scope resolution operator - 作用域解析运算符 (::)

C++常见的可见性的修饰符:                                                                                      Public: 公有成员可以被类的对象、派生类和其他外部代码访问。                      Protected: 保护成员可以被类的对象和派生类访问,但不能被其他外部代码直接访问。 Private: 私有成员只能被类的对象访问,对于派生类和其他外部代码都不可见。

#include <iostream>
#include <string>

class Entity
{
private://private 内部类和友元都可以正常访问
   int a;
   void Print1(){ std::cout << "private" << std::endl; }
protected://protected 内部类和派生类
   int b;
   void Print2( ){ std::cout << "protected" << std::endl; }
public:
   Entity() {}
   int c;
   Entity(int x)
   {
      c = x;
      std::cout << " public " << x << std::endl;
   }
};

class Player : public Entity
{
public:
   Player()
   {
      std::cout << "派生类player, private 派生类不可访问" <<std::endl;
      Print2();
   }
};

int main()
{
   Entity* e = new Entity(3);//private protected 外部类不可访问
   e->c = 2;

   Player* p = new Player();//派生类


   std::cin.get();
   delete p,e;
}

结果:

解析:第一个是外部类只能访问public,第二个是派生类,private不可以访问。

注意:基类中有默认的构造函数的情况下,派生类才可以重写派生类的构造函数。就是如            果基类 Entity 中定义了带参数的构造函数,那么你需要在派生类 Player 中显式地            调用基类的构造函数,或者提供一个无参构造函数来满足派生类的构造需求。

1.3 C++ 数组

在C++中,数组是一种用于存储多个相同类型元素的数据结构。它提供了一种连续存储的方式,可以通过索引访问其中的元素。需要注意的是,数组的索引是从0开始的数组名是一个指向数组第一个元素的整型指针。

#include <iostream>

int main()
{
   int example[5];
   example[0] = 2;
   example[4] = 4;

   int a = example[0];
   std::cout << example << std::endl;
   std::cout << a << std::endl;



   std::cin.get();
}

结果:

关于数组的遍历,数组的存储,与赋值

#include <iostream>

int main()
{
   int example[5];
   int* ptr = example;
   for (int i = 0;i < 5; i++)
      example[i] = 2;

   example[2] = 5;
   *(ptr + 2) = 6;//int型(4个字节)偏移,会增加2*4的偏移
   *(int*)((char*)ptr + 12) = 9;

   int a = example[0];
   std::cout << example << std::endl;
   std::cout << a << std::endl;

   for (int i = 0; i < 5; i++) {
      std::cout << example[i] << " ";
   }

   std::cin.get();
}

结果:

静态数组和动态分配的数组

(静态数组一般是在栈上分配的,动态数组一般是在堆上动态分配的)

#include <iostream>

int main()
{
   int example[5];//静态数组
   for (int i = 0; i < 5;i++)
      example[i] = 2;

   int* another = new int[5];//动态分配的数组
   for (int i = 0; i < 5; i++)
      another[i] = 2;


   for(int i = 0; i < 5; i++)
      std::cout << example[i] << "------" << another[i] << std::endl;

   delete[] another;

   std::cin.get();
}

解析:

使用new关键字动态分配了一个长度为5的整型数组,并将指针赋值给another

注意:

new关键字创建的动态分配的内存必须使用**delete[]**进行释放,而不是单独的delete关键字。因为another是一个指向数组的指针,使用delete[]可以确保释放整个数组所占用的内存空间。

结果:

size 一般用来记字节数,number一般用来记元素个数。

数组的构造类

#include <iostream>

class Entity
{
public:
   int* example = new int[5];

   Entity()
   {
      int a[5];
      int count = sizeof(a) / sizeof(int);
      int maybe = sizeof(example) / sizeof(int);

      for (int i = 0; i < 5; i++)
         example[i] = 2;
      std::cout << count << "      " << maybe << std::endl;
   }


};

int main()
{

   Entity e;
}

整型指针通常是与计算机的地址总线宽度相对应的位数。在 32 位架构下,整型指针通常是 32 位(4 字节),而在 64 位架构下,整型指针通常是 64 位(8 字节)。

大多数现代编译器在64位系统上仍然将int类型定义为4个字节

结果:

数组的间接寻址

C++内存的间接寻址(Indirect Addressing)是通过指针变量来访问或修改内存位置上存储的数据。在C++中,指针是一种特殊类型的变量,它存储了另一个变量的内存地址。 通过指针变量,程序可以灵活地引用和处理不同大小和位置的数据,以及在运行时动态分配和释放内存。需要注意的是,在进行内存间接寻址时,需要确保指针变量指向的对象存在且未被销毁。如果程序企图访问非法或已销毁的内存位置,可能会导致运行时错误或数据损坏。

#include <iostream>

int main() {
    int arr[5] = {1, 2, 3, 4, 5}; // 声明并初始化一个整型数组
    int* ptr = &arr[0]; // 声明一个指向数组首元素的指针

    std::cout << "Array Elements: ";
    for (int i = 0; i < 5; i++) {
        std::cout << arr[i] << " ";
    }
    std::cout << std::endl;

    std::cout << "Pointer Elements: ";
    for (int i = 0; i < 5; i++) {
        std::cout << *(ptr + i) << " "; // 通过指针间接访问数组元素
    }
    std::cout << std::endl;

    return 0;
}

解析:

声明了一个指向arr首元素的指针ptr,使用指针算术运算符+和解引用操作符*来访问数组元素,从而实现了指针的间接寻址。需要注意的是,在进行数组间接寻址时,我们需要确保指针变量指向的地址合法,即指向数组元素的地址。如果指针指向的地址越界或指向非法内存位置,可能会导致程序崩溃或产生未定义行为。

#include <iostream>
#include <array>

class Entity
{
public:
   static const int exampleSize = 5;
   int example[exampleSize];

   std::array<int,5> another;

   Entity()
   {
      for (int i = 0; i < another.size(); i++)
         example[i] = 2;
   }
};

int main()
{
   Entity e;
}

another 数组是通过 std::array 类模板实现的。std::array 是 C++ 标准库中提供的模板类,用于表示固定大小的数组。在 Entity 类的构造函数中,通过循环将 example 数组的所有元素初始化为2,这里使用了 another.size() 来获取 another 数组的大小。 相比于普通的数组,std::array 提供了更多的功能和安全性,例如提供了成员函数 size() 来获取数组大小,还可以通过迭代器等方式进行访问和操作。此外,std::array 还提供了一些其他的便利特性,如范围检查和异常安全等。

需要注意的是,无论是普通的数组 example 还是 std::array 数组 another,它们的分配和释放都是在对象的生命周期内进行的,不需要手动管理内存。当 e 对象被销毁时,与之相关的内存空间会被自动释放。