开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第2天,点击查看活动详情
面向对象编程
一、类(class)
本质上说class是一种将数据(data)和函数(methods、funcitons)组织在一起的数据结构。class中的成员变量默认访问控制为private。
#include <iostream>
class Player {
public:
int x;
int y;
float speed;
void Move(int xa, int ya) {
x += xa * speed;
y += ya * speed;
}
};
int main(int argc, char** argv) {
Player player1;
player1.x = 1;
player1.y = 1;
player1.speed = 3;
player1.Move(2, 2);
std::cout << player1.x << std::endl;
std::cout << player1.y << std::endl;
}
1、类(class)与结构体(struct)区别
类默认成员变量为private,而struct默认为public。一般情况下,struct是数据的结构体,仅仅用于存放pod(plain old data),例如数学中的向量Vector概念就可以用struct编写。简而言之,struct和class其实可以相互替换,但是为了代码可读性及逻辑通顺,不建议这样做,struct就让他处理一些不复杂(逻辑上的)的数据。
struct Vec{
float x;
float y;
void Add(Vec& other){
x+=other.x;
y+=other.y;
}
};
2、static关键字
static可以放在类和结构体中,也可放在之外。
static在类外表示,static修饰的符号在link(链接)阶段是局部的,即只对定义它的编译单元可见(.obj)。
//Static.cpp
int s_Var = 5;
//Main.cpp
int main(){
int s_Var = 10;
// crtl+f7 编译单个文件不会有问题,但是f5编译并链接项目时,由于Static.cpp中已经存在s_Var
//因此编译器会报“找到一个或多个多重定义的符号”错误,但是当我们修改Static.cpp中s_Var为static时
//由于static修饰的符号在link(链接)阶段是局部的,即只对定义它的编译单元(.obj)、声明他的cpp文件可见
//因此不会报错。
//ps(如果我们想在一个cpp中调用另一个cpp中的全局变量可以用extern关键字,该关键词可以让编译器在另外的编译单元(obj)中找定义,相当于变成了该变量的引用)
}
总结:尽量让全局变量和函数static,除非你想在其他cpp文件中要到他。
static在类和结构体中表示,这部分内存是由这个类的所有实例共享的。静态方法里没有该实例的指针,即this。
#include <iostream>
class Singleton {
public:
static Singleton& Get() {
static Singleton instance;
return instance;
}
void Hello() {
std::cout << "hello" << std::endl;
}
};
int main(int argc,char** argv){
Singleton::Get().Hello();
}
//第二种返回Singleton实例的方式
class Singleton {
public:
static Singleton* instance;
static Singleton& Get() {
return *instance;
}
void Hello() {
std::cout << "hello" << std::endl;
}
};
Singleton* Singleton::instance = nullptr;
int main(int argc,char** argv){
Singleton::Get().Hello();
}
cpp中每个类中的非静态方法总是将当前类的实例当作参数传入。因此静态方法无法读取类中的非静态变量。
对于类需要用到但是类实例之间不变的变量可以把他设置为static。
static void Print(Entity e){ // 非静态方法本质上是会传入当前Entity实例
std::cout<<e.x<<endl;
}
3、构造函数(Constructors)
实例化对象时会运行,与类的名称一致。默认情况下cpp中的类会有一个空的构造函数,但是你也可以删除它, Log()=delete;
4、析构函数(Destructors)
对象(实例)被摧毁时调用,当你在堆(heap)上开放内存空间时,你必须手动销毁他,此时析构函数就很有用了。
class Demo(){
public:
int a;
int b;
Example e;
//一定用下面这种方式初始化构造函数,可以节约资源
Demo(int aa,int bb):a(aa),b(bb),e(Example(8)){}//构造函数,这里时cpp的赋值语法,这里Example只会被创建一次
Demo(int aa){ //与上面类似,但不完全等价,在这里Example会被创建实例两次,前一次的实例被Example(8)构造函数覆盖掉
e=Example(8)
a=aa;
}
~Demo(){}//析构函数
};
5、继承
类之间存在经常需要复用的内容时可以用继承。可以先创造一个基类(模板),再用子类继承它。
class Base{
public:
float x;
float y;
void BaseFunc(){std::cout<<"base func"<<std::endl;}
};
class Son : public Base{
public:
char* name;
void SonFunc(){std::cout<<"Son func"<<std::endl;}
};
6、虚拟函数(Virtual Function)
虚拟函数起到动态调度的作用,会编译一个V-table,v-table会保存一个基类中所有虚拟函数的映射关系。如果需要override一个函数,你需要再父类中的那个被复写的函数前加上关键词virtual。
#include<iostream>
#include<string>
class Entity {
public:
virtual std::string GetName() { return "Entity"; }
};
class Player : public Entity {
private:
std::string m_name;
public:
Player(const std::string& name) :m_name(name) {}
std::string GetName() override { return m_name; }
};
void printName(Entity* entity) {
std::cout << entity->GetName() << std::endl;
}
int main(int argc, char** argv) {
Player* p = new Player("Dragon");
Entity* e = new Entity();
printName(e);
printName(p);
}
0x1、虚析构函数
如果用基类指针来引用派生类对象(多态),那么基类的析构函数必须是 virtual 的,否则 C++ 只会调用基类的析构函数,不会调用派生类的析构函数。
如果父类的析构函数不加virtual,那么如果以多态的方式创造子类时可能会造成内存泄漏
父类的析构函数加virtual后cpp就知道可能在层次结构下会有一个子类重写的方法,普通函数加virtual表明该函数可以被覆写(override),析构函数加virtual不是覆写析构函数而是加上一个析构函数,cpp他会调用两个析构函数
class Base{
public:
Base(){
std::cout <<"Base Constructed"<<std::endl;
}
virtual ~Base(){ // 一定要加
std::cout <<"Base Destructed"<<std::endl;
}
}
class Son:public Base{
int* arr;
public:
Son(){
arr = new int[5];
std::cout <<"Son Constructed"<<std::endl;
}
~Son(){
delete[] arr;
std::cout <<"Son Destructed"<<std::endl;
}
}
int main(int argc,char** argv){
Base* test = new Son();
delete test;
//如果~Base不加virtual delete test时就不会调用~Son(),那么arr在堆的内存就不会释放造成内存泄漏
}
不加virtual
加virutal
7、纯虚拟函数(接口)
cpp没有interface关键字,但是可以把纯虚拟函数当接口的抽象方法,继承自纯虚拟函数所在类的子类必须实现纯虚拟函数,才能创造实例。
class Entity {
public:
virtual std::string GetName() = 0; //让virtual 函数体=0即为纯虚拟函数。
};
class A{
public:
std::string GetName()override{} //必须实现该纯虚拟函数
};
8、public、private、protected
private对子类都不可见,且被private修饰的变量和方法在外部不能被调用。
protected对子类可见,但不能在外部被调用。
9、const、mutable关键字
当类方法不应该修改类,那么记住将方法标记为const,这样就保证调用者不能利用方法修改类中的内容
class Entity{
private:
int X;
mutable int var;
public:
int getX() const{ //这里的const,代表getX函数为read-only函数,不能对X有任何改变,即X=2,会报错
var = 2;//这里不会报错,因为var被mutable修饰,使其可以在const函数中被更改。
return X;
}
10、this关键字
代指当前类的对象
11、创建实例方法
分配给堆会比分配给栈使用更多时间,同时堆需要我们手动销毁。只有在class非常大和我们需要手动控制销毁时刻时再用new。
{
Entity e = Entity("dragon"); //栈上创建实例,随着栈被销毁而消失
}
{
// Entity* e = (Entity*)malloc(sizeof(Entity));这样是不会创造实例,只会开辟一片内存空间
Entity* e = new Entity("dragon"); //new堆上创建实例,只有我们delete了才会销毁,注意new返回的是实例所在的地址,即指针
std::cout<<e->getName()<<std::endl;
}
delete e;//需要我们手动销毁
int a = new int[50];
Entity*e = new(a) Entity("dragon"); //将Entity实体分配到先前创造的内存空间中,这里时a的内存地址
delete[] a;
Entity e(new Entity()); // 和Entity e = new Entity;等价,两种写法都可以
二、特殊类
1、Enum(枚举类)
enum 相当于给了integer一个名字,
每个枚举项都与一个底层类型的值相关联。当在 枚举项列表 中提供了初始化器时,各枚举项的值由那些初始化器所定义。若首个枚举项无初始化器,则其关联值为零。对于其他任何定义中无初始化器的枚举项,其关联值为前一枚举项加一。
enum Foo { a, b, c = 10, d, e = 1, f, g = f + c }; //a = 0, b = 1, c = 10, d = 11, e = 1, f = 2, g = 12
enum Test: unsigned char{ //默认是4字节int ,不能是浮点类型
};
//eg.log 类
class Log {
public:
enum Level {
LevelError, LevelWarning, LevelInfo
};
private:
Level m_LogLevel= LevelInfo;
public:
void setLogLevel(Level LogLevel) {
m_LogLevel = LogLevel;
}
void Error(const char* messages) {
if(m_LogLevel>= LevelError){
std::cout << "[Error]" << messages << std::endl;
}
}
void Warn(const char* messages) {
if (m_LogLevel >= LevelWarning) {
std::cout << "[Warn]" << messages << std::endl;
}
}
void Info(const char* messages) {
if (m_LogLevel >= LevelInfo) {
std::cout << "[Info]" << messages << std::endl;
}
}
};
int main(){
Log log;
log.setLogLevel(Log::LevelInfo);
log.Error("error msg");
log.Warn("warn msg");
log.Info("info msg");
}