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 对象被销毁时,与之相关的内存空间会被自动释放。