0.引子
下面以一个例子来讲解类的简单用途
Gun.h
#pragma once
#include <string>
class Gun{
public:
Gun(std::string type){
this->_bullet_count = 0;
this->_type = type;
}
void addBullet(int bullet_num);
bool shoot();
private:
int _bullet_count;
std::string _type;
};
Gun.cpp
#include "Gun.h"
#include <iostream>
using namespace std;
void Gun::addBullet(int bullet_num){
this->_bullet_count += bullet_num;
}
bool Gun::shoot(){
if(this->_bullet_count <= 0){
cout << "There is no bullet!" << endl;
return false;
}
this->_bullet_count -= 1;
cout << "shoot successfully!" << endl;
return true;
}
1.构造函数
构造函数是一种特殊的成员函数,可以在创建对象的的时候自动调用,构造函数的作用是初始化对象中的数据成员。
构造函数的名称必须与类名相同,其没有返回值类型。
构造函数可以重载(无参构造函数、有参构造函数、拷贝构造函数)。
在 Gun.h 中,Gun(std::string type)
为构造函数。
Gun(std::string type){
this->_bullet_count = 0;
this->_type = type;
}
可以看出,这个构造函数是一个有参构造函数。
因此,我们在 new 的时候便可以直接向这个类里传入参数,例如new Gun("AK47")
关于 new 的用法:((20220604194429-cozdgou ‘new’))
在本文中,这里的new实际上是执行如下3个过程(这三个过程都在一个new operator中完成):
- 调用malloc/heap_alloc分配内存 ;2. 调用构造函数生成类对象;3. 返回相应指针。
所以,我们使用new Gun("AK47")
的时候,实际上是返回了一个指向 Gun 类的指针,于是可以这样做:
Gun *ptr_gun= new Gun("AK47")
同时,初始化这个新对象中的数据成员
关于 this 关键字:
在C++里面,每一个对象都能通过this指针来访问自己的地址,即它指向当前的对象,通过它可以访问当前对象的所有成员,是所有成员函数的隐藏参数(所以只能用在成员函数的内部)。
通过 this 可以访问类的所有成员,包括 private、protected、public 属性。
例如在上述构造函数中的语句中,将成员变量 _bullet_count 初始化为0,_type 初始化为传入对象的变量。
this->_bullet_count = 0; this->_type = type;
以上,便是通过构造函数来初始化对象的内容。
2.构造函数的其它方法
上述举例了括号法中有参构造函数的使用,括号法还包括无参和拷贝构造函数,下面介绍拷贝构造函数的使用。
我们可以在Gun类中加入一个新的构造函数:
// 拷贝构造函数
Gun(const Gun &g){
this->_bullet_count = 0;
this->_type = g.type;
}
这样,在main函数中我们便能复制之前创建的对象给新的对象使用:
int main(){
Gun gun1("AK47");
Gun gun2(gun1);
Gun gun3 = gun1; // 等号法初始化调用拷贝构造函数
Gun gun4; //调用了无参构造函数
gun4 = gun1; //这里是等号赋值!!!和等号法初始化是两个概念
//不调用构造函数,而是执行赋值操作,把gun1的数据赋值给gun4 (默认=浅拷贝,将在后面介绍)
}
注意,上面的 gun1 是一个类指针,指向内存中存放的类对象,所以在构造函数中我们才要取其地址。
那么,在这里说一下类指针和类对象,关于二者的应用我们已经在上文尝试过一遍了。
类的指针: 他是一个内存地址值,他指向内存中存放的类对象(包括一些成员变量所赋的值).
对象: 他是利用类的构造函数在内存中分配一块内存(包括一些成员变量所赋的值).
指针变量是间接访问,但可实现多态( 通过父类指针可调用子类对象 )。
直接声明可直接访问,但不能实现多态。
类的对象:用的是 内存栈, 是个局部的临时变量.
类的指针:用的是 内存堆, 是个永久变量,除非你释放它,所以在 new 之后一定要 delete
在应用时,成员变量通过类的析构函数来释放空间,函数中的临时变量会在函数结束后自动释放,而指针需利用 delete 在相应的地方释放分配的内存块.
C++的精髓之一就是 多态性,只有指针或者引用可以达到多态 ,对象不行
所以在函数调用时,我们通常传类指针。不管对象或结构参数多么庞大,用指针传过去的只有4个字节。如果用对象,参数传递占用的资源就太大了
至此,我们已经大致理解了类指针和类变量的区别,接下来理解下面两个例子:
参考博客:https://blog.csdn.net/qq_43471489/article/details/123018837
void ParaFuncTest1(MyClassA A)
{
A.PrintData();
}
void ParaFuncTest2(MyClassA& A)
{
A.PrintData();
}
void ParaFuncTest3(MyClassA* A)
{
A->PrintData();
}
//第二种调用场景:类定义对象做函数参数
void ClassTest2()
{
MyClassA A1(1, 2);
ParaFuncTest1(A1); //实参A1初始化形参A对象元素的时候,会调用拷贝构造函数
ParaFuncTest2(A1); //不会调用拷贝构造函数,因为引用是变量别名(A1的别名A,因为在同一个地址),引用传递并没有出现新对象,
//只是给现有对象起个别名进行传递
ParaFuncTest3(&A1); //不会调用拷贝构造函数,因为传递的是对象A1的地址,并没有新的对象元素出现
}
3.构造函数初始化列表
如果我们想在一个类中初始化另一个类,则需要掌握构造函数初始化列表的使用方法。
Soldier.h
#include <string>
#include "Gun.h"
class Soldier{
public:
Soldier(std::string name);
~Soldier();
void addBulletToGun(int num);
bool fire();
private:
std::string _name;
Gun _gun;
};
Soldier.cpp
#include "Soldier.h"
Soldier::Soldier(std::string name) : _gun("AK47"){
this->_name = name;
}
void Soldier::addBulletToGun(int num){
this->_gun.addBullet(num);
}
bool Soldier::fire(){
return this->_gun.shoot();
}
Soldier::~Soldier(){}
这样,我们在初始化Soldier对象的时候,会初始化该对象中的成员对象Gun,并为其传入初始值“AK47”
更多初始化列表内容,参考博客https://blog.csdn.net/yili_xie/article/details/4803428
以及https://blog.csdn.net/weicao1990/article/details/81536022
4.析构函数
在上文中可以看到一个函数~Soldier()
,这个便是析构函数。
析构函数是清理对象资源的一类特殊成员函数,析构函数的名称是类名前加~号,它在对象释放前自动调用,无参数(因为没有参数,所以无法重载—函数重载的判断依据是参数类型、参数顺序、参数个数)、无返回值类型且析构函数禁止使用return语句。
了解了析构函数后,可以看学习下面的类函数思想:
5.匿名对象
详细参考:https://blog.csdn.net/wysnkyd/article/details/82709243
以及https://blog.csdn.net/u014583317/article/details/108705360
下面是构造函数的第三种调用场景,先看一遍下述代码,捋清逻辑,从而开始匿名对象的学习。
MyClassA RetuFuncTest1()
{
MyClassA A(1, 2);
return A; //执行 return A;会先产生一个匿名对象,执行拷贝构造函数,此对象作为返回值返回,
//然后释放临时对象A,A的生命周期到此结束(是否执行析构函数视情况而定)
//MyClassA& RetuFuncTest2()
//MyClassA* RetuFuncTest3()
//{
// MyClassA A(1, 2);
// return &A; //A是局部变量,不能返回它的地址
//}
//第三种调用场景:函数返回类型为类定义的元素
void ClassTest3()
{
RetuFuncTest1(); //如果不用变量来接这个函数,那么会在 RetuFuncTest1() 函数的 return A;
//语句处调用拷贝构造函数,并立即执行析构函数
//这是因为,函数返回一个对象元素,而局部变量A的生命周期只在函数体内,
//不能返回出来,所以会在return时创建一个匿名对象,主调函数种若没有
//对象元素来接,那么会立即调用析构函数把匿名对象析构
//RetuFuncTest2();
//RetuFuncTest3();
MyClassA A1 = RetuFuncTest1(); //这里不会再次调用拷贝构造函数,
//因为编译器会把函数RetuFuncTest1()
//返回出来的匿名对象直接转化为A1,因此匿名对象不会被析构,
//在RetuFuncTest1()函数结束时只调用一次析构函数来析构局部变量A
//匿名对象已经分配好了资源,并直接转化为A1,
//所以A1初始化不需要再次调用拷贝构造函数
A1.PrintData();
MyClassA A2; //调用无参构造函数
A2 = RetuFuncTest1(); //这是等号赋值操作!!!此时匿名对象也不会立即析构,而是在执行完这句话,
//对A2赋值完之后,执行析构函数,析构匿名对象
//(区别于匿名对象初始化A1,匿名对象转为A1,不会析构)
A2.PrintData();
} //生命周期结束,析构所有局部变量 A1(匿名对象) A2
在上述代码中,我们看到了一个新的名词——匿名对象,让我们来学习一下。
产生匿名对象的三种情况:
1)以值的方式给函数传参;
MyClassA(1, 2);
—> 生成了一个匿名对象,执行完其构造函数中的代码后,此匿名对象就此消失,执行析构函数。这就是匿名对象的生命周期。
MyClassA A= MyClassA(1, 2);
—>首先生成了一个匿名对象,然后将此匿名对象变为了A对象,其生命周期就变成了A对象的生命周期。
2)类型转换;
3)函数需要返回一个对象时:return A;
注意,在上述代码中,如果在MyClassA A1 = RetuFuncTest1();
的后面调用拷贝构造函数 MyClassA(A1);
会报错,因为当MyClassA(A1)
有变量来接的时候,编译器认为他是一个匿名对象。当没有变量来接的时候,编译器认为你MyClassA(A1)
等价于MyClassA A1
,所以就造成了重定义错误。
6.拷贝构造函数中的深拷贝与浅拷贝
浅拷贝:简单的赋值拷贝操作,在上文中已经用过(2.中的拷贝构造函数,以及5.中的等号赋值操作,都属于浅拷贝)。
深拷贝:在堆区重新申请空间,进行拷贝操作,解决浅拷贝带来的堆区重复释放问题。如果堆区有内存,这时需要在析构代码里将堆区的内存释放掉。
代码来源:https://blog.csdn.net/qq_40618919/article/details/118179069
class Person
{
public:
Person()
{
cout << "Person的默认构造函数调用" << endl;
}
Person(int age , int height)
{
m_Age = age;
m_Height = new int(height);
cout << "Person的有参构造函数调用" << endl;
}
自己实现拷贝构造函数 解决浅拷贝带来的问题
Person(const Person &p)
{
cout << "Person 拷贝构造函数调用" << endl;
m_Age = p.m_Age;
//编译器默认实现就是下面的这行代码,浅拷贝,会造成堆区重复释放问题
//m_Height = p.m_Height;
//深拷贝操作,即在堆区创建一块内存
m_Height = new int(*p.m_Height);
}
~Person()
{
//析构代码,将堆区开辟数据做释放操作
if (m_Height != NULL)
{
delete m_Height;
m_Height = NULL;
}
cout << "Person的析构函数调用" << endl;
}
int m_Age; //年龄
int *m_Height; //身高
};
void test1()
{
Person p1(18 , 160);
cout << "p1的年龄为: " << p1.m_Age << " 身高为: " << *p1.m_Height <<endl;
Person p2(p1);
cout << "p2的年龄为: " << p2.m_Age << " 身高为: " << *p2.m_Height << endl;
}
int main()
{
test1();
system("pause");
return 0;
}
我们又会注意到,什么是堆区重复释放?
参考:https://blog.csdn.net/weixin_46195203/article/details/114386453
首先我们知道:浅拷贝(Person p2(p1);
)之后两个对象(P1,P2)指向同一个堆区地址;当P1执行析构函数时,释放掉了该堆区地址的内容。当P2执行析构函数时,也会释放掉该堆区地址的内容,但此时堆区的内容已经释放完了。就会报错了
当深拷贝时,p2指向的新的堆区地址,这样在p1析构后,p2也能正常析构。
补充new的用法:m_Height = new int(height);
参考:https://blog.csdn.net/h799710/article/details/107794434
int *a = new int(10); //动态创建整型数,无参数是 * a=0,有参数则 * a = 参数
int *p = new int[10]; //创建一个有10个元素的动态整型数组,没有赋值,元素为随机数
int *p = new int[10] (); //创建一个有10个元素的动态整型数组,并都赋值为0
注:当其他类对象作为本类成员,构造时候先构造类对象,再构造自身,析构的顺序与构造相反
7.静态成员
在成员变量和成员函数前加上关键字 static , 称为静态成员。
静态成员变量:
- 所有对象共享同一份数据
- 在编译阶段分配内存
- 类内声明,类外初始化
class Person
{
public:
static int m_A;
private:
static int m_B;
};
int Person::m_A = 100;
int Person::m_B = 200;
注:类外访问不到私有静态成员变量,例如本案例中的 m_B
静态成员函数:
- 所有对象共享同一个函数
- 静态成员函数只能访问静态成员变量
class Person
{
public:
//静态成员函数
static void func()
{
m_A = 100; //静态成员函数可以访问 静态成员变量
//m_B = 200; //静态成员函数 不可以访问 非静态成员变量,无法区分到底是哪个对象的m_B属性
cout << "static void func调用 " << endl;
}
static int m_A; //静态成员变量
int m_B; // 非静态成员变量
//静态成员函数也是有访问权限的
private:
static void func2()
{
cout << "static void func2调用" << endl;
}
};
int Person::m_A =0;
//有两种访问方式
void test01()
{
//1、通过对象访问
Person p;
p.func();
//2、通过类名访问
Person::func();
//Person::func2(); 类外访问不到私有静态成员函数
}
int main()
{
test01();
system("pause");
return 0;
}