Cherno C++ 学习(二)

学习来源https://www.youtube.com/user/TheChernoProject/playlists

C++的static关键字

static分可以修饰两种变量或成员函数,一种是在类(或结构体)外,另一种在类(或结构体)内。

在类外的static
对于在类(或结构体)外的函数或变量,我们将其称为全局函数全局变量,对于普通的全局变量,整个项目内的cpp文件都可以对其进行访问,Static关键字可以修改全局变量的作用域与类里的私有成员变量类似,加了static的变量如同该cpp文件的私有全局变量

如果对于一个全局变量,我们只想让他成为该CPP内的全局变量,而不想让整个项目都能访问它的值,那么我们可以使用static关键字:

static int a =5; // 只在当前文件范围内使用

顺便提一句,因为cpp定义的全局变量所有文件里都能访问,所以如果同时有两个CPP有相同的定义,是会报错的:

//A.cpp
int global_a;

//B.cpp
int global_a;

会报错:
在这里插入图片描述
这个时候可以用extern关键字来解决,表示其中一个全局变量是从外面引进来的:

//A.cpp
int global_a;

//B.cpp
extern int global_a;   //正确

值得一提的是,对于static变量,即使用extern关键词去声明,也是不能获取的,如下的代码示例,如果我们光这样声明,不在B.cpp里去调用global_a的值,是不会报错的。

//A.cpp
extern int global_a;
void main()
{

}

//B.cpp
static int global_a; 

比如我们把B.cpp再加一个函数:

```css
//A.cpp
extern int global_a;
void main()
{

}

//B.cpp
static int global_a; 
void Func()
{
	int v = global_a;
}

会发现运行后报错了,因为Link阶段检查到了改变量是无法获取的:
在这里插入图片描述

在类内的static
在类内的static变量或函数,表示该变量或该函数是整个类共有的,相当于类里的单例,即使没有创建任何对象,这些变量和成员函数也随着类存在。

举个例子

class A
{
public:
	int a;
	static int b;//声明b
}
int A::b;//定义b
int main()
{
	cout << A::a; //错误
	cout << A::b; //正确
	A instance;
	cout << instance.a; //正确
	cout << instance.b; //正确,这种语法是允许的,实际上相当于cout << A::a
}

值得注意的是,类的static函数无法直接使用非静态成员变量和成员函数,因为他们是属于类的成员的,举个例子

class A
{
public:
	int a;
	static int b;//声明b
	static void Log()
	{
		cout << b << endl;//正确
		cout << a << endl;//错误,编译不过
	}
}
int A::b;//定义b

如果要访问非静态成员或函数,需要传入类的对象:

static void Log(A& instance)
{
	cout << b << endl;//正确
	cout << instance.a << endl;//正确
}

局部作用域内的static变量
static关键字能够修改局部作用域内的成员的lifetime(生命期),使其跟程序的生命期一样长
举个例子:

//A.cpp
int a = 0;//全局变量
void plusA()
{
	a++;
	cout << a <<endl;
}

//B.cpp
void plusA()
{
	static int a = 0;//只会在第一次进入此函数的时候执行
	a++;
	cout << a <<endl;
}

上述两段代码,如果同时执行n次,最后的结果都是一样的,唯一不同的就是,我们能在A.cpp的任何地方读取a的值,因为a是全局变量,但是在B.cpp里面,我们只能在plusA中读取a的值,也就是说a变量的作用域没变,还是仅限于plusA函数,但是其生命期现在跟整个程序几乎一样长。也就是说,全局成员加上static就变成了该文件的私有成员,class内的成员加上static就变成了全类的共有成员,作用域(比如函数体内,{} 大括号里的范围)内的成员加上static就变成了永不消失的成员

有的人会说这玩意儿没卵用,但是这里可以举个例子,通过local static member使代码更简洁:

//对于单例模式,如果不用local static member
//也是一种通用的写法
class Singleton
{
private:
	static Singleton instance;
	Singleton(){} //构造函数私有化
public:
	static Singleton& Get(){return instance;}
	Singleton (const Singleton&) = delete;//禁止通过"="进行Singleton的复制
}
Singleton Singleton::instance = null;//初始化单例

如果通过local static member可以简化代码

class Singleton
{
private:
	Singleton(){} //构造函数私有化
public:
	static Singleton& Get()
	{
		static Singleton instance;//只会在第一次使用Get时进行创建
		return instance;
	}
	Singleton (const Singleton&) = delete;//禁止通过"="进行Singleton的复制
}

C++的构造函数

1. C++的默认构造函数相当于没有参数的函数体为空的函数,而且只有一个,同样如此的还有默认析构函数和默认复制构造函数,别记错了

ClassName()
{
}

2. 禁止C++自动为类生成默认构造函数的方法有两个:

  • 构造函数私有化
Class Student
{
private:
	Student(){}
};
  • 直接声明禁止构造函数
Class Student
{
public:
	Student() = delete;
};

3. sizeof派生类时,除了派生类本身的变量大小,还要加上从基类继承过来的变量的大小。

4. C++类的protected访问级别介于private和public之间,不可以用类成员加.后缀的形式访问,但是公有继承的派生类可以直接使用protected成员

5. C++中类的构造函数应该被声明为explict,否则会产生隐式转换,如下所示:

class Student
{
public:
	Student(int a){num = a;}
private:
	int num;
}
int main()
{
	//如果构造函数不加explict,那么我们可以这么创建一个对象
	Student s = 5;//这样也能创建对象,如果写错代码,很容易出现这样的错误
}

所以应该在构造函数前面加上关键字explicit

Smart Pointers

智能指针本质上只是一个包含了原始指针的Wrapper,功能就一个:

自动释放指针,无需使用delete,甚至也不需要使用new
Smart pointer is a wrapper class over a pointer with operator like * and -> overloaded. The objects of smart pointer class look like pointer, but can do many things that a normal pointer can’t like automatic destruction (yes, we don’t have to explicitly use delete), reference counting and more.

代码本质上大概是这个样子:

using namespace std; 
  
class SmartPtr 
{ 
   int *ptr;  // Actual pointer 
public: 
   // Constructor: Refer https://www.geeksforgeeks.org/g-fact-93/ 
   // for use of explicit keyword  
   explicit SmartPtr(int *p = NULL) { ptr = p; }  
  
   // Destructor 
   ~SmartPtr() { delete(ptr); }   
  
   // Overloading dereferencing operator 
   int &operator *() {  return *ptr; } 
}; 
  
int main() 
{ 
    SmartPtr ptr(new int()); 
    *ptr = 20; 
    cout << *ptr; 
  
    // We don't need to call delete ptr: when the object  
    // ptr goes out of scope, destructor for it is automatically 
    // called and destructor does delete ptr. 
  
    return 0; 
}

智能指针会在退出局部范围时释放,比如:

void main()
{
	{
		smartPoint p = ....//创建智能指针
	}//会在这里释放
}

C++ 11提供了以下几种Smart Pointers, 使用智能指针需要#include<memory>

std::unique_ptr
a smart pointer that owns a dynamically allocated resource;(不可复制, 一个资源只可以使用一个pointer)

如下所示, unique_ptr的复制构造函数和 = 运算符重载都被禁用了, 所以unique_ptr不能当作函数的参数进行传参
在这里插入图片描述
具体有两种写法:

std::unique_ptr<ClassName>ptrName(new ClassName());
std::unique_ptr<ClassName>ptrName = std::make_unique<ClassName>();//这种更好

std::shared_ptr
a smart pointer that owns a shared dynamically allocated resource. Several std::shared_ptrs may own the same resource and an internal counter keeps track of them;
shared_ptr允许同一个对象出现多个指针指向它, shared_ptr会记录这个指针的数量, 称为refference count, 每复制一个指向该对象的指针, 引用计数会加1, 每一个指针退出其scope函数范围进行释放时,会减1, 当引用计数为0时,会释放所有的指针, delete掉该对象
举个例子:

void main()
{
	{
		//Entity是一个类名, 该类的析构函数执行时会打印消息
		std::shared_ptr<Entity>ptr1;
		{
			std::shared_ptr<Entity>ptr2 = std::make_shared<Entity>();//引用计数加一
			ptr1 = ptr2;//引用计数加一
		}//引用计数减一
	}//引用计数减一, 执行析构函数, 打印消息
}

std::weak_ptr
like a std::shared_ptr, but it doesn’t increment the counter.
与shared_ptr类似, 但是不会改变引用计数, 还是上面的例子:

void main()
{
	{
		//Entity是一个类名, 该类的析构函数执行时会打印消息
		std::weak_ptr<Entity>ptr1;
		{
			std::shared_ptr<Entity>()ptr2 = std::make_shared<Entity>();//引用计数加一
			ptr1 = ptr2;//引用计数不变
		}//引用计数减一, 执行析构函数, 打印消息
	}
}

Templates

C++的template关键字, 可以方便我们写代码:
如下例子是一个log系统, 无论输入什么类型的变量, 都可以打印出来:

template<typename T>
void log(T v)
{
	std::cout << v << std::endl;
}

void main()
{
	log(5);
	log<int>(5);//其实log(5)本质上是这么写
	log(5.6f);
	log("Hello");
}

可以看出来, T是代表了所有的类型, 那么上述功能具体是怎么实现的呢?

template功能是由编译器实现的:
举个例子, 编译器在编译阶段实际干了以下工作:
编译到语句log(5) -> 前往log函数 -> 获取参数的类型为int ->根据模板定义, 复制出一个函数出来, 来替换log(5)这条语句:

void main()
{
		std::cout << 5 << std::endl;
}

也就是说, template这个指令, 是告诉编译器, 按照输入的参数的类型, 给我创建出相应的函数, 再进行执行的这个过程, 如果不调用模板函数, 那么编译器会直接无视这个函数, 如下所示:

template<typename T>
void log(T v)
{
	std::cout << v < std::endl;//可以看到这里有语法错误
}

void main()
{
	return 0;//编译器编译成功
}

拓展函数的参数类型, 是template关键字的用法, 实际上该用法远不止上面的那么简单, 再举一个例子, 如果我们要写下面的一个类:

class C
{
	int array[num];//这里会报错, 因为num不是常数
}

上面的写法会不通过, 因为必须是一个常量, 通过template, 我们可以这么写:

template<int N>
class C
{
private:
	int array[N];//这里能编译
public:
	int getSize() const { return N };
}
void main()
{
	C<5>obj;//<>里的5就是传入模板类的N的值
}

为什么这个能编译呢, 这就涉及到了上面讲的原理了, 实际上编译器会创建一个类, 编译的是以下文件:

class C
{
private:
	int array[5];
public:
	int getSize() const { return 5 };
}
void main()
{
	C c;
}

我们可以再举一个拓展的例子:

template<typename T, int N>
class C
{
private:
	T array[N];//这里能编译
public:
	int getSize() const { return N };
}
void main()
{
	//下面这一句就很像我们用到的STL容器了, 实际上他们也底层都是用模板封装的
	C<int, 5>obj1;
	C<string, 6>obj2;
}

关于template的一点提醒
很多游戏公司明令禁止使用template, 因为程序臃肿之后,template拓展得很复杂, 代码可能会很难看懂.
不过template用于制作log系统、material系统这些东西,可能还是挺方便的

posted @ 2020-02-28 23:08  弹吉他的小刘鸭  阅读(149)  评论(0编辑  收藏  举报