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系统这些东西,可能还是挺方便的