二十、C++关键字CONST
1、const是啥
const类似于类和结构体的可见性,它是对开发人员写代码强制特定的规则,它承诺某些东西将是不变的。
const首先作用于左边的东西;如果左边没东西,就做用于右边的东西。
const int MAX_AGE=90;
int* a=new int;
*a=2;
a = (int*)& MAX_AGE;
这里的const意味着MAX_AGE是常量,它的值不会被改变。我们用指针在堆上创建一个整数。因为这个声明没有使用const,我们可以逆向引用a然后设置为2,也可以重新分配实际的指针。为了绕开MAX_AGE的const限制,把它强制转换成int*类型。
#include<iostream>
#include<string>
int main()
{
const int MAX_AGE=90;
int* a=new int;
*a=2;
std::cout<<*a<<std::endl;//输出2
a = (int*)& MAX_AGE;//括号为强制转化为int*类型
std::cout<<*a<<std::endl;//输出90
}
2、常量指针
现在我们开始添加const, const int *意味着不能修改该指针指向的内容。
const int MAX_AGE=90;
const int* a = new int;
*a=2;//错误
std::cout << *a << std::endl;
a = (int*)& MAX_AGE;
我们不能逆向引用这个指针然后改变a的值,a的值实际上内存地址上的东西,但我们可以读取a,逆向引用并打印它。也可以尝试改变a本身,让a指向MAX_AGE。只是不能改变指针指向的内容。
const int* a = new int;
int const* a=new int;//意义相同
3、指针常量
第二种使用const的方法是把它放在*号以后;
int* const a = new int;
它的作用刚好相反,我们可以改变指针指向的内容,但不能把实际的指针本身重新赋值,指向别的东西。
4、CONST CONST
我们也可以写两个,这意味着即不能改变指针指向的内容,也不能改变指针本身让它指向别的地址。
5、CONST在类中
class Entity
{
private:
int m_x, m_y;
public:
int GetX() const//该方法不会修改任何实际的类,也不能修改类成员变量
{
return m_x;
}
};
把const放在方法名GetX的右边,在参数列表之后。这意味该方法不会修改任何实际的类,也不能修改类成员变量。若让m_X=2则会报错。
若m_X是一个指针,想让它保持不变。 写成const int* const GetX() const,这意味我们返回了一个不能被修改的指针,指针的内容也不能被修改,这个方法承诺不修改实际的Entity类。
class Entity
{
private:
int* m_x, m_y;
public:
const int* const GetX() const
{
return m_x;
}
};
若m_X不是一个指针,创建Entity实例e,有一个PrintEntity函数可以访问GetX方法,传入的是Entity变量。
void PrintEntity(Entity e)
{
std::cout << e.GetX() << std::endl;
}
我们希望可以通过常量引用传递这个参数,因为不想再次复制Entity类(需要分配空间)。这意味e是常量,若e是一个指针则可以修改它指向的方向。若e是引用,则不能对e重新进行赋值。
void PrintEntity(const Entity& e)
{
std::cout << e.GetX() << std::endl;
}
如果将GetX方法中的const去掉,就不能调用GetX函数,因为GetX函数不能保证不会对Entity类的内容做出修改。常对象只能调用常函数。
我们没有直接修改Entity,但是调用了一个方法它可以修改Entity,这是不被允许的。所以需要将方法标记为const,这意味当我调用const的Entity时,可以调用任何const函数。
二十一、关键字mutable
在某些情况下,我们将方法标记为const,但又需要修改变量var,可以使用C++的关键词mutable,我们把var变量设为mutable,这意味着它是可以改变的。
另一种用途感觉用不到。
二十二、C++成员初始化列表
我们在编写一个类并向该类添加成员(变量)时,通常需要某种方式对这些成员(变量)进行初始化。
1、在构造函数中
通常有两种方法,第一种是我们可以在构造函数中初始化一个类成员。
#include<iostream>
#include<string>
class Entity
{
private:
std::string m_Name;
public:
Entity()
{
m_Name = "Unknown";
}
Entity(const std::string& name)
{
m_Name = name;
}
const std::string& GetName() const { return m_Name; }
};
int main()
{
Entity e0;
std::cout << e0.GetName() << std::endl;//输出‘Unknow’
Entity e1("Cherno");
std::cout << e1.GetName() << std::endl;//输出‘Cherno’
std::cin.get();
}
2、成员初始化
第二种我们可以通过成员初始化列表来实现,若我们新增一个成员m_Score,只需要加一个逗号然后写上这个新成员。在成员初始化列表中这些变量的顺序要和成员变量声明时的顺序一致。
class Entity
{
private:
std::string m_Name;
int m_Score;
public:
Entity()
:m_Name("Unknown"),m_Score(0)//here
{
}
Entity(const std::string& name)
{
m_Name = name;
}
const std::string& GetName() const { return m_Name; }
};
我们也可以改变赋值的方法,用括号替换等号,然后把它移到列表中。
3、区别
class Entity
{
private:
std::string m_Name;
int x, y, z;
public:
Entity()
: x(0),y(0),z(0)
{
m_Name = "Unknown";
//等价于m_Name =std::string("Unknown");
}
};
如上代码中的m_Name对象会被构造两次,一个是使用默认构造函数,另一个是使用Unknown参数(初始化的构造函数)。结果是第一个直接被扔掉,造成性能的浪费。
我们新增Example类,有一个不带参的构造函数和一个带参数x的构造函数,然后把Example类作为Entity实际的类成员。将m_Example设置为Example类对象,有一个参数8。然后创建一个Entity对象的实例e0,使用这个默认构造函数。运行程序发现创建了两个Entity,一个是无参的,一个是有整型参数的。一个是在成员变量的区域Example m_Example;,一个是在m_Example = Example(8);,创建了一个新的Example实例,然后把它赋值给m_Example。
若我们把它移到初始化列表中,我们可以写成:m_Example(Example(8)),也可以写成:m_Example(8)。运行构造函数发现只创建了一个实例。
综上我们应到使用第二种成员初始化列表,不使用它则会浪费性能。
二十三、创建并初始化C++对象
在C++中创建对象时要选择放在内存的什么位置,是在栈上创建还是在堆上创建。栈对象有一个自动的生存期,它的生存期由它声明的作用域决定的,只要变量超出作用域,栈会弹出,内存就会被释放。但堆是完全不同的,一旦在堆中分配一个对象,实际上已经在堆上创建了一个对象,它会一直待那里直到我们释放它。
1、栈上创建
#include<iostream>
#include<string>
using String = std::string;
class Entity
{
private:
String m_Name;
public:
Entity() :m_Name("Unknown") {}
Entity(const String& name) :m_Name(name) {}
const String& GetName() const { return m_Name; }
};
int main()
{
Entity entity;// 在栈上创建对象
//Entity entity("Cherno"); 指定参数
//Entity entity=Entity("Cherno"); 构造函数
std::cout << entity.GetName() << std::endl;
std::cin.get();
}
如果我们想要指定一个参数,只需要用括号括起来给它取个名字。比如Entity entity(“Cherno”);。写成 Entity entity=Entity(“Cherno”);也行,这就是构造函数。
如果可以使用这种方式创建对象,那就尽量以这种方式创建对象,这是在C++中速度最快的方式,也是可管控的方法去初始化对象。
void Function()
{
int a = 2;
Entity entity = Entity("Cherno");
}
但如果想把它放到这个函数的生存期之外,比如有一个函数Function,在这个函数里创建我们自己的Entity,一旦到达Function函数末尾的’}',这个Entity就会从内存中被销毁。当我们调用Function时就为这个函数创建了一个栈结构,也包含了我们声明的所有局部变量。
2、堆上创建
若想让Cherno在作用域之外依然存在,就不能分配到栈上,我们将不得不求助于堆分配。此外若这个Entity的规模太大,就会没有足够的空间在栈上分配,因为栈通常非常小。但是注意在堆上分配要比栈花费更长的时间,而且在堆上需要手动释放被分配的内存。
若想把之前的代码写成堆分配,首先要改变类型,应该是Entity 而不是Entity,然后通过new关键字*。当我们调用new Entity时会在堆上分配内存,我们调用构造函数,new Entity实际上会返回一个Entity*(它会返回这个entity在堆上被分配的内存地址,这也是需改变类型的原因)。我们还要使用delete手动释放分配的内存。在23行设置断点编译发现e设置正确,名字是Cherno。
再次按下F10,到了cin.get,发现e还是名字Cherno,因为它只会在delete之后被释放。
// 栈中
Entity entity("hbh");
// 堆中
Entity* entity = new Entity("hbh");
二十四、关键字new&delete
new的主要目的是在堆上分配内存,它会返回一个指向那块内存的指针。new后面不管是类还是基本类型或数组,都决定了分配内存的大小(以字节为单位)。
int a=2;
int* b=new int;//存储new创建内存的首地址
int* c=new int[50];//
Entity* e=new Entity();//
Entity* f=new Entity;//
Entity* g=new Entity[50];//
我们也可以选择动态分配内存,即通过new关键字在堆上分配四字节,b存储的是它的内存地址;也可以分配数组,一共需要200字节内存;也可以分配Entity类;可以不需要括号(默认构造函数);也可以分配Entity数组。
new不仅会在堆上分配足够的空间去存储Entity,还会去调用构造函数。查看new的定义可以发现它实际上是一个操作符(operator),就像加减等于那样。这意味着可以重载这个操作符,并改变它的行为。
通常调用new会调用隐藏在里面的C函数malloc函数,它代表内存分配以及它的实际作用,传入一共size(也就是我们要多少字节),然后返回一个void指针,也就是说调用new实际上相当于我们写了malloc(sizeof(Entity)),然后将其转换为Entity*类型。
Entity* e = new Entity();//调用了Entity构造函数
Entity* e =(Entity*)malloc(sizeof(Entity));//仅仅是分配内存,然后给我们一个指向那个内存的地址,没有调用构造函数
delete
在我们使用new关键字时,记住必须要用delete ,查看定义发现delete也是一个操作符,有block内存块和size作为参数。这是一个常规函数,它调用了C函数free,free可以释放malloc申请的内存。
int* b=new int[50];
delete[] b;//使用new[]来分配数组,则需要使用delete[]
二十五、C++隐式转换与关键字explicit
C++允许编译器对代码执行一次隐式转换,在两个类型之间C++允许进行隐式转换,而不需要用cast做强制转换,类型转换是将数据类型转换到另一个类型的过程。
#include<iostream>
#include<string>
class Entity
{
private:
std::string m_Name;
int m_Age;
public:
Entity(const std::string& name)
:m_Name(name), m_Age(-1) {}
Entity(int age)
:m_Name("Unknown"), m_Age(age) {}
};
int main()
{
//Entity a("Cherno");
//Entity b(22);
//Entity a = Entity("Cherno");
//Entity b = Entity(22);
Entity a = "Cherno";
Entity b = 22;
std::cin.get();
}
我们也可以直接将a赋值位Cherno,b直接等于22(Java,C#等其他语言不行)。这被称为隐式转换,或者叫隐式构造函数。它隐式地将22转换成一个Entity并构造出一个Entity,因为有一个Entity的构造函数接受一个整数的参数,另一个构造函数接受的字符串name是”Cherno”作为参数。
#include<iostream>
#include<string>
class Entity
{
private:
std::string m_Name;
int m_Age;
public:
Entity(const std::string& name)
:m_Name(name), m_Age(-1) {}
Entity(int age)
:m_Name("Unknown"), m_Age(age) {}
};
void PrintEntity(const Entity& entity)
{
// Printing
}
int main()
{
PrintEntity(22);
PrintEntity("Cherno"); // 报错
PrintEntity((std::string)"Cherno");
PrintEntity(Entity("Cherno"));
std::cin.get();
}
再创建一个PrintEntity函数用来做打印,参数是entity。我们可以直接调用这个函数,参数为22。因为C++认为22可以转换成一个Entity(隐式转换),可以调用这个构造函数,22是创建Entity的唯一参数。PrintEntity(“Cherno”);则会报错,因为"Cherno"不是std::string而是char数组。所以C++需要2次转换,一个从const char数组到string,一个从string到Entity。但因为只允许做一次隐式转换,所以必须把它包装在一共构造函数中,或者包装在一个Entity对象中(隐式地将这个字符串转换为std:string标准字符串,然后推入Entity构造函数)。
关键字explicit
explicit与隐式转换有关,因为explicit是禁用这个隐式转换的功能,explicit关键字放在构造函数前面表示没有隐式的转换。如果要使用整数构造这个Entity对象,则必须显式调用此构造函数。
把explicit放在int age构造函数前面,则会发现PrintEntity(22); 和Entity b = 22;都失败了。
若把explicit放在第一个构造函数前面,第25行同样会失败。但是24行会成功,因为它实际上调用了构造函数。
二十六、C++运算符及其重载
运算符是一种符号,通常代替一个函数来执行一些事情。重载本质上是给运算符重载赋予新的含义(如添加或者创建参数),允许在程序中定义或更改运算符的行为。
#include<iostream>
#include<string>
struct Vector2
{
float x, y;
Vector2(float x, float y)
:x(x), y(y) {}
Vector2 Add(const Vector2& other) const
{
return Vector2(x + other.x, y + other.y);
}
Vector2 Multiply(const Vector2& other) const
{
return Vector2(x * other.x, y * other.y);
}
};
int main()
{
Vector2 position(4.0f, 4.0f);
Vector2 speed(0.5f, 1.5f);
Vector2 powerup(1.1f, 1.1f);
Vector2 result1 = position.Add(speed.Multiply(powerup));
std::cin.get();
}
在C++中,可以使用运算符重载来定义自己的运算符。我们要重新定义*和+运算符就像写其他函数一样。但我们不用函数名,而是用operator接运算符 +,函数体也可以写成return Add(other);,乘法运算符同理。
#include<iostream>
#include<string>
struct Vector2
{
float x, y;
Vector2(float x, float y)
:x(x), y(y) {}
Vector2 Add(const Vector2& other) const
{
return Vector2(x + other.x, y + other.y);
}
// 重载运算符
Vector2 operator+(const Vector2& other) const
{
//return Vector2(x + other.x, y + other.y);
return Add(other);
}
Vector2 Multiply(const Vector2& other) const
{
return Vector2(x * other.x, y * other.y);
}
// 重载运算符
Vector2 operator*(const Vector2& other) const
{
//return Vector2(x * other.x, y * other.y);
return Multiply(other);
}
};
int main()
{
Vector2 position(4.0f, 4.0f);
Vector2 speed(0.5f, 1.5f);
Vector2 powerup(1.1f, 1.1f);
//Vector2 result1 = position.Add(speed.Multiply(powerup));
Vector2 result2 = position + speed * powerup;
std::cin.get();
}
在使用std::cout我们会用到左移运算符,它接受2个参数:一个是输出流cout,另一个是vector2。
我们需要在vector2类的外面将<<运算符重载,它和vector2没有任何关系,我们是在cout中重载;我们写上std::ostream&输出流,是一个引用;这是我们需要重载符号的最初定义,然后写一个operator加上左移运算符,然后需要一个对现有流的引用,传入const vector2通过引用传递。
#include<iostream>
#include<string>
struct Vector2
{
float x, y;
Vector2(float x, float y)
:x(x), y(y) {}
Vector2 Add(const Vector2& other) const
{
return Vector2(x + other.x, y + other.y);
}
Vector2 Multiply(const Vector2& other) const
{
return Vector2(x * other.x, y * other.y);
}
Vector2 operator+(const Vector2& other) const
{
return Add(other);
}
Vector2 operator*(const Vector2& other) const
{
return Multiply(other);
}
};
// 重载<<
std::ostream& operator<<(std::ostream& stream, const Vector2& other)
{
stream << other.x << "," << other.y;
return stream;
}
int main()
{
Vector2 position(4.0f, 4.0f);
Vector2 speed(0.5f, 1.5f);
Vector2 powerup(1.1f, 1.1f);
Vector2 result2 = position + speed * powerup;
std::cout << result2 << std::endl;
std::cin.get();
}
二十六、关键字this
在C++中有一个关键字this,它可以访问成员函数。this是一个指向当前对象实例的指针,该方法属于这个对象实例。
class Entity
{
public:
int x, y;
Entity(int x, int y)// 成员初始化列表
:x(x), y(y)
{}
Entity(int x, int y)//参数x赋值给它自己
{
x = x;
y = y;
}
};
我们可以使用成员初始化列表,但如果想在方法内部写,这里的参数和类中的变量名一样,如果直接写x=x,那么只是让参数中的x赋值给它自己,也就是什么都不做。而我们真正想做的是引用这个类的x和y,this关键字可以让我们做到这一点。this关键字是指向当前对象的指针。
class Entity
{
public:
int x, y;
Entity (int x, int y)
{
Entity* e = this;
// // Entity* const e = this;
// this=nullptr; 不能
// // Entity*& const e = this;不能
// Entity* const& e = this;可以
e->x = x;
}
Entity (int x, int y)//简单写法
{
this->x=x; //(*this).x=x;
this->y=y;
}
};
这里的this是一个Entity类型的指针。也可以加上const,这时右边的this不允许把它重新赋值为别的什么。所以不能写成this=nullptr;,也不能把this赋值给这里的引用,实际上需要const的。现在想要赋值x,可以直接用e->x,更简单的写法是this->x=x;。
class Entity
{
public:
int x, y;
Entity(int x, int y)
{
this->x = x;
this->y = y;
}
int GetX() const
{
const Entity* e = this;//承诺不修改
//Entity* e = this;错误
}
};
若我们想要写一个返回这些变量之一的函数,在函数后面加上const是非常常见的,因为它不会修改这个类。所以在GetX()函数中就不能将this赋值给一个Entity,而是const Entity。
若我们想在Entity类的内部调用一个类外部的函数 PrintEntity,这个函数将Entity作为参数。我们想传递这个Entity类的当前实例到这个函数,就可以传入this,它会传入我们设置的x和y的当前实例。
void PrintEntity(Entity* e);//声明
class Entity
{
public:
int x, y;
Entity(int x, int y)
{
this->x = x;
this->y = y;
PrintEntity(this);//调用
}
};
void PrintEntity(Entity* e)
{
//print
}
如果我们想把它作为一个常量引用const &,这里要做的就是逆向引用*。通过逆向引用this指针,在非const方法中,我们可以赋值给Entity &。如果在一个const方法中,我们可以将*this赋值给const Entity&,因为这是一个指向当前类的指针。我们还可以调用delete this(不建议)。
class Entity
{
public:
int x, y;
Entity(int x, int y)
{
this->x = x;
this->y = y;
Entity& e = *this;
delete this;//非专业不建议使用
}
int GetX() const
{
const Entity& e = *this;
}
};
关于this指针,有以下几点总结:
1)this指针指向当前对象,可以访问当前对象的所有成员变量。包括private、protected、public。
2)this指针是const指针,一切企图修改该指针的操作,如赋值(改变指向)、增减都是不允许的!
3)this指针只有在成员函数中才有定义。因此,在创建一个对象后,也不能通过对象使用this指针。所以,我们也无法知道一个对象的this指针的位置(只有在成员函数里才有this指针的位置)。当然,在成员函数里,你是可以知道this指针的位置的(可以&this获得),也可以直接使用的。
4)只有创建对象后,this指针才有意义。
5)static静态成员函数不能使用this指针。原因静态成员函数属于类,而不属于某个对象,所以static静态成员函数压根就没有this指针。
6)this在成员函数的开始执行前构造的,在成员函数的执行结束后清除。至于如何清除的,由编译器实现,程序员不关心。this是通过函数参数的首参数来传递的。
#include <iostream>
#include <cstring>
using namespace std;
class AA
{
public:
AA(int a)
{
m_a = a;
}
~AA()
{
}
void PinrtThis()
{
printf("this = 0x%x\n", this);
}
private:
int m_a;
};
int main(void)
{
AA *pa = new AA(10);
pa->PinrtThis();
printf("pa = 0x%x\n", pa);
return 0;
}
#include <iostream>
#include <cstring>
using namespace std;
class AA
{
public:
AA(int a)
{
m_a = a;
}
AA& returnAA()
{
return *this;//返回对象本身
}
void Pinrt()
{
printf("m_a = %d\n", m_a);
}
private:
int m_a;
};
int main(void)
{
AA a(10);
AA b = a.returnAA();
b.Pinrt();
return 0;
}
二十七、C++的对象生存期(栈作用域生存期)
每当我们在C++中进入一个作用域,我们是在push栈帧。它不一定非得是将数据push进一个栈帧。
栈上变量自动销毁,在很多方面都很有用,可以帮助我们自动化代码。比如类的作用域,比如像智能指针unique_ptr,这是一个作用域指针,或者像作用域锁(scoped_lock)。
但最简单的例子可能是作用域指针,它基本上是一个类,它是一个指针的包装器,在构造时用堆分配指针,然后在析构时删除指针,所以我们可以自动化这个new和delete。
#include <iostream>
class Entity
{
private:
public:
Entity()
{
std::cout << "Create!" << std::endl;
}
~Entity()
{
std::cout << "Destroy!" << std::endl;
}
};
class ScopedPtr
{
private:
Entity* m_Ptr;
public:
ScopedPtr(Entity* ptr)
: m_Ptr(ptr)
{
}
~ScopedPtr()
{
delete m_Ptr;
}
};
int main()
{
{
ScopedPtr test = new Entity();
}
std::cin.get();
}
可以看到,ScopedPtr就是我们写的一个最基本的作用域指针,由于其是在栈上分配的,然后作用域结束的时候,ScopedPtr这个类就被析构,析构中我们又调用delete把堆上的指针删除内存。