CPP基础知识(二)

105 阅读1分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 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");
}