C++学习笔记——类和对象

news2024/11/25 12:48:45

1.面向对象的三大特性:封装、继承、多态

2.对象有其属性和行为

3.具有相同性质的对象,可被抽象为类

1.封装

1.封装是C++面向对象三大特性之一

2.封装的意义:

(1)将属性(变量)和行为(函数)作为一个整体,表现生活中的事物

(2)将属性和行为加以权限控制(公共public,私有private,保护protected)

1.1.封装的意义

​1.在设计类的时候,属性和行为写在一起,表现事物

语法:

class 类名{ 访问权限: 属性 / 行为 };

可以通过成员方法对成员属性进行赋值:调用成员方法→传入形式参数→赋值成员属性

//学生类
class Student {
public:
    void setName(string name) {
        m_name = name;
    }
    void setID(int id) {
        m_id = id;
    }

    void showStudent() {
        cout << "name:" << m_name << " ID:" << m_id << endl;
    }
public:
    string m_name;
    int m_id;
};

int main() {

    Student stu;
    stu.setName("德玛西亚");
    stu.setID(250);
    stu.showStudent();

    system("pause");

    return 0;
}

2.类在设计时,可以把属性和行为放在不同的权限下,加以控制(作用域)

访问权限有三种:(例如:class的括号内称为类内;main函数中对类成员属性进行修改,称为类外)

(1)public 公共权限:类内可以访问,类外可以访问

(2)protected 保护权限:类内可以访问,类外不可以访问

(3)private 私有权限:类内可以访问,类外不可以访问

(4)protected和private的区别体现在继承中:子类虽然能够继承父类的private和protected的成员变量,但子类可以访问父类的保护内容,而不可以访问父类的私有内容

//三种权限
//公共权限  public     类内可以访问  类外可以访问
//保护权限  protected  类内可以访问  类外不可以访问
//私有权限  private    类内可以访问  类外不可以访问

class Person
{
    //姓名  公共权限
public:
    string m_Name;

    //汽车  保护权限
protected:
    string m_Car;

    //银行卡密码  私有权限
private:
    int m_Password;

public:
    void func()
    {
        m_Name = "张三";
        m_Car = "拖拉机";
        m_Password = 123456;
    }
};

int main() {

    Person p;
    p.m_Name = "李四";
    //p.m_Car = "奔驰";  //保护权限类外访问不到
    //p.m_Password = 123; //私有权限类外访问不到

    system("pause");

    return 0;
}

1.2.struct和class的区别

1.在C++中 struct和class唯一的区别就在于默认的访问权限不同:

(1)struct默认权限为public

(2)class默认权限为private

2..尽管结构体可以包含成员函数,但它们很少这样做。所以,通常情况下结构体声明只会声明成员变量

3..声明结构体的方式和声明类的方式大致相同,其区别如下:

(1)使用关键字 struct 而不是关键字 class

(2)结构体声明通常不包括 public 或 private 的访问修饰符

class C1
{
    int  m_A; //默认是私有权限
};

struct C2
{
    int m_A;  //默认是公共权限
};

int main() {

    C1 c1;
    //c1.m_A = 10; //错误,访问权限是私有

    C2 c2;
    c2.m_A = 10; //正确,访问权限是公共

    system("pause");

    return 0;
}

1.3.成员属性设置为私有

优点1:将所有成员属性设置为私有,可以自己控制读写权限,通过设置和调用对应的成员方法的方式(无权限即不提供相应成员方法)

优点2:对于写权限,我们可以检测数据的有效性

在设置成员属性时,可以在成员方法中通过if判断输入的参数是否符合标准

通过公开的成员方法设置私有的成员属性:因为私有属性默认只有类内可以访问,但可以通过调用函数类外实现访问

class Person {
public:

    //姓名设置可读可写
    void setName(string name) {
        m_Name = name;
    }
    string getName()
    {
        return m_Name;
    }


    //获取年龄 
    int getAge() {
        return m_Age;
    }
    //设置年龄
    void setAge(int age) {
        if (age < 0 || age > 150) {
            cout << "你个老妖精!" << endl;
            return;
        }
        m_Age = age;
    }

    //情人设置为只写
    void setLover(string lover) {
        m_Lover = lover;
    }

private:
    string m_Name; //可读可写  姓名

    int m_Age; //只读  年龄

    string m_Lover; //只写  情人
};


int main() {

    Person p;
    //姓名设置
    p.setName("张三");
    cout << "姓名: " << p.getName() << endl;

    //年龄设置
    p.setAge(50);
    cout << "年龄: " << p.getAge() << endl;

    //情人设置
    p.setLover("苍井");
    //cout << "情人: " << p.m_Lover << endl;  //只写属性,不可以读取

    system("pause");

    return 0;
}

1.4.练习案例

1.对比两个类的成员属性是否相等:

(1)通过全局函数判断:需要传入两个参数(两个类)

(2)通过成员方法判断:以一个类为基准,仅需传入一个参数(另一个类),并且,关于成员属性的使用需要注意传入的类需要加上前缀(例如:p.m_H),而基准的类不需要(m_H)

2.当成员属性为私有时,通过成员方法设置和访问成员属性

3.在类中可以让另一个类作为本类的成员(Circle类中包含Point类)

4.类可以拆分在不同的文件中(以点类为例,若是圆类,因其包含有点类,故需要加上#include "point.h",即点类的头文件)

(1)头文件中创建一个.h文件,且仅保留方法和属性声明(删去成员方法中的具体实现)

#program once //防止头文件重复包含
#include<iostream>
using namespace std;
using namespace std;
class Point {	//点类
public:
	void setXY(int x, int y);
	int getX();
	int getY();
private:
	int m_X, m_Y;	//x和y坐标
};

(2)在源文件中创建一个.cpp文件,且仅保留方法的实现(与头文件相对应),需在方法名前加上作用域(若不加默认是在全局作用域下,则会报错)

#include "point.h"    //头文件
void Point::setXY(int x, int y) {	//设置x和y
	m_X = x;
	m_Y = y;
}
int Point::getX() {	//获取x
	return m_X;
}
int Point::getY() {	//获取y
	return m_Y;
}

(3) 在具体程序使用时,需要加上#incude "circle.h"(因为circle.h中已带有point.h头文件,故可将point.h省略)

点和圆关系:

#include<iostream>
using namespace std;
class Point {	//点类
public:
	void setXY(int x, int y) {	//设置x和y
		m_X = x;
		m_Y = y;
	}
	int getX() {	//获取x
		return m_X;
	}
	int getY() {	//获取y
		return m_Y;
	}
private:
	int m_X, m_Y;	//x和y坐标
};

class Circle {
public:
	void setCenter(Point p) {	//设置圆心
		m_Center.setXY(p.getX(), p.getY());
	}
	Point getCenter() {	//查看圆心
		return m_Center;
	}
	void setR(int r) {	//设置半径
		m_R = r;
	}
	int getR() {	//查看半径
		return m_R;	
	}
	int calculate(Point p) {
		int dis_X = p.getX() - m_Center.getX();
		int dis_Y = p.getY() - m_Center.getY();
		if ((dis_X * dis_X + dis_Y * dis_Y) > m_R) {
			cout << "点在圆外";
			return 0;	//点在圆外
		}
		else if ((dis_X * dis_X + dis_Y * dis_Y) == m_R) {
			cout << "点在圆上";
			return 1;	//点在圆上
		}
		else {
			cout << "点在圆内";
			return 2;	//点在圆内
		}
	}
private:
	Point m_Center;	//圆心坐标
	int m_R;	//半径
};

int main() {
	Point p;
	p.setXY(10, 11);
	Circle c;
	Point t;
	t.setXY(10, 0);
	c.setCenter(t);
	c.calculate(p);
}

2.对象的初始化和清理

2.1.构造函数和析构函数

1.c++利用了构造函数析构函数进行对象的初始化和清理

2.构造函数和析构函数将会被编译器自动调用,如果我们不提供构造和析构,编译器会提供默认的的构造函数和析构函数,并且其是空实现

3.构造函数:主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用

构造函数语法:

类名(){}

(1)构造函数,没有返回值也不写void(与普通函数区别)

(2)函数名称与类名相同

(3)构造函数可以有参数,因此可以发生重载(允许构造函数重载)

(4)程序在申明对象时候会自动调用构造,无须手动调用,而且只会调用一次

4.析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作

析构函数语法: 

~类名(){}

(1) 析构函数,没有返回值也不写void

(2)函数名称与类名相同,在名称前加上符号 ~(与构造函数区别)

(3)析构函数不可以有参数,因此不可以发生重载(与构造函数区别)

(4)程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次

5.(1)在函数test01中Person p属于局部变量,被存储在栈区,在test01执行完后,test01占有的栈区空间将被释放(使用权被回收),此时将会执行p的析构函数(Person类的析构函数)(p占用的内存空间属于test01占用的栈区空间)

(2)而若Person p存在于main函数中,则会先被卡在system("pause"),即只会执行构造函数,而不会执行析构函数,因为程序还在运行,即p还在使用中,未被释放;在按下任意键后,main函数执行完毕,此时才会执行p的析构函数

(原因:析构函数在对象销毁前系统自动调用)

class Person
{
public:
    //构造函数
    Person()
    {
        cout << "Person的构造函数调用" << endl;
    }
    //析构函数
    ~Person()
    {
        cout << "Person的析构函数调用" << endl;
    }

};

void test01()
{
    //p存放在栈中,在test01执行完后释放p的内存空间,即会执行p的析构函数
    Person p;
}

int main() {

    test01();

    system("pause");

    return 0;
}

2.2.构造函数的分类和调用

1.编译器在程序员未提供构造函数时,默认提供的是无参的构造函数(空实现)

2.拷贝构造函数的作用是将传入参数的成员属性赋值给自己(相当于传入一个有属性的类,来初始化我这个类的属性),其中用引用或取地址,是为防止出现副本而增加内存开销,用const是为防止本身被改变

3.为什么拷贝构造函数中要用引用传递不用值传递? 

因为值传递本身也是会创建一个副本, 创造副本的时候又会执行一次拷贝构造函数, 然后一直递归,故不用值传递

4.使用括号法调用构造函数时,调用无参构造函数不能加括号,编译器会认为是返回值为某类的函数的声明

//这样调用无参的构造函数时错误的
//编译器会认为这是函数的声明,即返回值为Person类的函数P
Person P();

//返回值为空的P函数的声明
void P();

//调用无参的构造函数,不加括号
Person P;

5. 使用显示法调用构造函数时,若没有类去接受申明的类,则该类为匿名对象;匿名对象会在调用后立刻销毁(该行代码执行结束后立即销毁),有名的对象会在程序结束的时候销毁,两者生存周期不同

Person p = Person(10);    //有名对象
Person(10);    //匿名对象

6.不要在匿名对象里,以拷贝构造函数的格式创建已声明的对象(若该对象未被申明,则可以)

编译器会创建另一个对象p2,相当于去掉括号,但由于p2已经被申明,故重定义

Person p1;
Person p2 = Person(10);
//Person(P2);
//错误,编译器会认为Person(P2)等价为Person P2,即声明Person类的P2

7.隐式转换法编译器将会自动帮你转换成显示法调用匹配的构造函数

Person p4 = 10; // Person p4 = Person(10); 
Person p5 = p4; // Person p5 = Person(p4); 
//1、构造函数分类
// 按照参数分类分为 有参和无参构造   无参又称为默认构造函数
// 按照类型分类分为 普通构造和拷贝构造

class Person {
public:
    //无参(默认)构造函数
    Person() {
        cout << "无参构造函数!" << endl;
    }
    //有参构造函数
    Person(int a) {
        age = a;
        cout << "有参构造函数!" << endl;
    }
    //拷贝构造函数
    Person(const Person& p) {
        age = p.age;
        cout << "拷贝构造函数!" << endl;
    }
    //析构函数
    ~Person() {
        cout << "析构函数!" << endl;
    }
public:
    int age;
};

//2、构造函数的调用
//调用无参构造函数
void test01() {
    Person p; //调用无参构造函数
}

//调用有参的构造函数
void test02() {

    //2.1  括号法,常用
    Person p1(10);
    //注意1:调用无参构造函数不能加括号,如果加了编译器认为这是一个函数声明
    //Person p2();

    //2.2 显式法
    Person p2 = Person(10); 
    Person p3 = Person(p2);
    //Person(10)单独写就是匿名对象  当前行结束之后,马上析构
    Person(10);

    //2.3 隐式转换法
    Person p4 = 10; // Person p4 = Person(10); 
    Person p5 = p4; // Person p5 = Person(p4); 

    //注意2:不能利用 拷贝构造函数 初始化匿名对象 编译器认为是对象声明
    //Person p5(p4);
}

int main() {

    test01();
    //test02();

    system("pause");

    return 0;
}

2.3.拷贝构造函数调用时机

1.使用一个已经创建完毕的对象来初始化一个新对象

//1. 使用一个已经创建完毕的对象来初始化一个新对象
void test01() {

    //调用有参构造函数声明一个Person类对象man
    Person man(100);

    //调用拷贝构造函数声明一个Person类对象newman(man的副本)
    Person newman(man); //调用拷贝构造函数

    //相当于Person newman2 = Person(man)
    //调用拷贝构造函数声明一个Person类对象newman2(man的副本)
    Person newman2 = man; 

    //Person newman3;
    //newman3 = man; //不是调用拷贝构造函数,赋值操作
}

2.值传递的方式给函数参数传值(作为实参的副本)

值传递相当于Person p1 = p,即拷贝构造函数隐式写法

(值传递的本质是拷贝一个临时的副本,即通过调用拷贝构造函数完成创建副本的操作)

//编译器通过Person类的拷贝构造函数创建了一个副本p1
//即Person p1 = p,doWork函数内部实际上是对p1进行修改
void doWork(Person p1) {
    ...
}

void test02() {
    Person p; //声明Person类的p,调用无参构造函数
    doWork(p); //调用doWork函数,传入参数p
}

3.以值方式返回局部对象

class Person {
public:
    Person() {
        cout << "无参构造函数!" << endl;
        mAge = 0;
    }
    Person(int age) {
        cout << "有参构造函数!" << endl;
        mAge = age;
    }
    Person(const Person& p) {
        cout << "拷贝构造函数!" << endl;
        mAge = p.mAge;
    }
    
    //析构函数在释放内存之前调用
    ~Person() {
        cout << "析构函数!" << endl;
    }
public:
    int mAge;
};

//1. 使用一个已经创建完毕的对象来初始化一个新对象
void test01() {

    Person man(100); //p对象已经创建完毕
    Person newman(man); //调用拷贝构造函数
    Person newman2 = man; //拷贝构造

    //Person newman3;
    //newman3 = man; //不是调用拷贝构造函数,赋值操作
}

//2. 值传递的方式给函数参数传值
//相当于Person p1 = p;
void doWork(Person p1) {}
void test02() {
    Person p; //无参构造函数
    doWork(p);
}

//3. 以值方式返回局部对象
Person doWork2()
{
    Person p1;
    cout << (int *)&p1 << endl;
    return p1;
}

void test03()
{
    Person p = doWork2();
    cout << (int *)&p << endl;
}


int main() {

    //test01();
    //test02();
    test03();

    system("pause");

    return 0;
}

2.4.构造函数的调用规则

1.默认情况下,c++编译器至少给一个类添加3个函数

(1)默认构造函数(无参,函数体为空)(空实现)

(2)默认析构函数(无参,函数体为空)(空实现)

(3)默认拷贝构造函数,对属性进行值拷贝(默认的拷贝构造函数将会对每个成员属性进行复制,浅拷贝)

2.构造函数调用规则如下:

(1)如果用户定义有参构造函数,c++不在提供默认无参构造,但是会提供默认拷贝构造(相当于系统将有参构造和无参构造视为同一类型的,你提供了,系统就不提供了。拷贝构造函数和析构函数又视为另外两种类型)

(2)如果用户定义拷贝构造函数,c++不会再提供其他构造函数(定义类不能只写拷贝构造函数,需写无参/有参构造函数)

2.5.深拷贝和浅拷贝

1.浅拷贝:简单的赋值拷贝操作(等号赋值操作等)(编译器提供的默认拷贝构造函数实现的是浅拷贝,对数据进行逐字节的复制)

//浅拷贝
m_age = p.m_age;
m_Height = p.m_Height;

2.深拷贝:在堆区重新申请空间,进行拷贝操作

(1)为什么开辟到堆区:因为堆区的数据可以手动释放,在函数体外也可以使用(区别于局部变量),实际应用中会用到堆区,用到堆区而且又需要拷贝的时候要防止指针悬挂的问题(指针悬挂,即存在指针指向了一块该进程没有使用权限的内存空间)

(2)new申请了一片堆区的内存空间存储数据,并将这片内存空间的地址返回,需要一个指针去接受(指向)这个地址

3.

int  *Height = new int (160);

//两者等价
int *Height ;
Height = new int(10);

4.输出Height为输出该指针指向的内存空间的地址;输出*Height为输出该指针指向的内存空间所存储的数据(解引用)

①输出指针:输出指针指向的地址

②输出解引用:输出指针指向的地址存放的数据

int a = 10;
//指向存放a地址的指针ptr
int *ptr = &a;

//输出指针ptr,输出的是存放a的地址
cout << ptr << endl;

//输出解引用,输出的是a
cout << *ptr << endl;

5.构造函数使用new在堆区申请一片内存空间存储变量

在函数执行后,析构函数使用delete释放该变量在堆区占用的内存空间

当类中的构造函数使用new后,需要相应的使用delete在析构函数中释放;若没有使用delete释放,则编译器只会释放该类中用于接收new而声明的指针变量(在栈区),而不能释放new的空间(在堆区),就会造成内存泄漏(堆区空间需要手动释放)

6.默认拷贝构造函数(浅拷贝)导致的问题:

①p1在进行有参初始化时,在堆区申请了一个空间,p1的m_height指针就指向这个空间

②p2在进行拷贝初始化时使用的是编译器提供的浅拷贝(默认拷贝构造函数)

浅拷贝是对成员变量的简单赋值(对数据进行逐字节的拷贝),所以p2的m_height指针的值等于p1的height指针,即两个height指针指向堆区的同一个地址,函数test01结束后,p1和p2把同一个空间释放了两次,所以程序崩了

(因栈特性,先进后出,先进行p2的释放后,再进行p1的释放)

解决方法:设置拷贝构造函数,采用深拷贝的方式,在堆区申请一片内存空间存储该数据,并让类中的指针指向这片堆区的内存空间,每个类的堆区空间相互独立,并只释放自己的堆区的空间

⑤其中拷贝构造函数的解释:

//1.解引用,得到传入参数p中m_heigh指向的地址的值
//2.new申请一片内存空间存放该值
//3.让自己的指针m_Height指向这片堆区
m_Height = new int (*p.m_Height);
class Person {
public:
    //无参(默认)构造函数
    Person() {
        cout << "无参构造函数!" << endl;
    }
    //有参构造函数
    Person(int age ,int height) {

        cout << "有参构造函数!" << endl;

        m_age = age;
        m_height = new int(height);

    }
    //拷贝构造函数  
    Person(const Person& p) {
        cout << "拷贝构造函数!" << endl;
        //如果不利用深拷贝在堆区创建新内存,会导致浅拷贝带来的重复释放堆区问题
        m_age = p.m_age;
        m_height = new int(*p.m_height);

    }

    //析构函数
    ~Person() {
        cout << "析构函数!" << endl;
        if (m_height != NULL)
        {
            delete m_height;
        }
    }
public:
    int m_age;
    int* m_height;
};

void test01()
{
    Person p1(18, 180);

    Person p2(p1);

    cout << "p1的年龄: " << p1.m_age << " 身高: " << *p1.m_height << endl;

    cout << "p2的年龄: " << p2.m_age << " 身高: " << *p2.m_height << endl;
}

int main() {

    test01();

    system("pause");

    return 0;
}

2.6.初始化列表

作用:C++提供了初始化列表语法,用来初始化属性(类似构造函数的作用)

构造函数():属性1(值1),属性2(值2)... {}

使用初始化列表的好处:(初始化列表相当于直接声明一个有初始值的类型,省略了赋值操作)

1.类成员中存在常量,如 const int a;   因为常量只能初始化不能赋值

2.类成员中存在引用,同样只能使用初始化不能赋值

3.提高效率 

Person p(1, 2, 3) → 1赋值给a → a赋值给m_A

class Person {
public:

    传统方式初始化
    //Person(int a, int b, int c) {
    //    m_A = a;
    //    m_B = b;
    //    m_C = c;
    //}

    //初始化列表方式初始化
    Person(int a, int b, int c) :m_A(a), m_B(b), m_C(c) {}
    void PrintPerson() {
        cout << "mA:" << m_A << endl;
        cout << "mB:" << m_B << endl;
        cout << "mC:" << m_C << endl;
    }
private:
    int m_A;
    int m_B;
    int m_C;
};

int main() {

    Person p(1, 2, 3);
    p.PrintPerson();


    system("pause");

    return 0;
}

2.7.类对象作为类成员

①Person类中的初始化列表:Phone m_phone = pName (隐式转换法)

等价于 Phone m_phone = Phone(pName)(显示法)

等价于 Phone m_phone(pName)(括号法)

调用Phone类中的有参构造

②当其他类作为本类成员,构造时候先构造其他类对的象,再构造自身

析构顺序则与构造顺序相反(栈先进后出的特性)

class Phone
{
public:
    Phone(string name)
    {
        m_PhoneName = name;
        cout << "Phone构造" << endl;
    }

    ~Phone()
    {
        cout << "Phone析构" << endl;
    }

    string m_PhoneName;

};


class Person
{
public:

    //初始化列表可以告诉编译器调用哪一个构造函数
    Person(string name, string pName) :m_Name(name), m_Phone(pName)
    {
        cout << "Person构造" << endl;
    }

    ~Person()
    {
        cout << "Person析构" << endl;
    }

    void playGame()
    {
        cout << m_Name << " 使用" << m_Phone.m_PhoneName << " 牌手机! " << endl;
    }

    string m_Name;
    Phone m_Phone;

};
void test01()
{
    //当类中成员是其他类对象时,我们称该成员为 对象成员
    //构造的顺序是 :先调用对象成员的构造,再调用本类构造
    //析构顺序与构造相反
    Person p("张三" , "苹果X");
    p.playGame();

}


int main() {

    test01();

    system("pause");

    return 0;
}

2.8.静态成员

静态成员就是在成员变量和成员函数前加上关键字static,称为静态成员

1.静态成员变量

(1)所有对象共享同一份数据(A和B共用一份数据,若A或者B修改,则另一方使用时,使用的就是修改后的数据),即静态变量不属于某个特定的对象,因此有两种访问方式:

 ①通过某个对象访问(也就是之前成员变量的访问方式)

Person p;
cout << p.m_A << endl;

通过类名访问(需要指明哪个作用域下)(Person::表示在Person作用域下)

cout << Person::m_A << endl;

(2)在编译阶段分配内存(程序运行前就在全局区中分配好内存)

(3)类内声明,类外初始化(必须要有初始值):

class Person{
public:
    static int m_A;    //类内申明
}

//A::member表示A类中的成员member
//B::member表示B类中的成员member
//Person::m_A表示Person作用域下的成员m_A
int Person::m_A = 100;    //类外初始化

void test(){
    Person P1;
    Person p2;
    //修改p2的m_A为200,此时,因为Person中的m_A为静态变量
    //即p1和p2共用同一个变量m_A
    //因此,p1的m_A也被修改为200
    p2.m_A = 200;    
}

(4)静态成员变量仍然可以有访问权限:当不为pubilc时,类外可以对其初始化,但是不能访问

class Person
{

public:

    static int m_A; //静态成员变量

    //静态成员变量特点:
    //1 在编译阶段分配内存
    //2 类内声明,类外初始化
    //3 所有对象共享同一份数据

private:
    static int m_B; //静态成员变量也是有访问权限的
};
int Person::m_A = 10;
int Person::m_B = 10;

void test01()
{
    //静态成员变量两种访问方式

    //1、通过对象
    Person p1;
    p1.m_A = 100;
    cout << "p1.m_A = " << p1.m_A << endl;

    Person p2;
    p2.m_A = 200;
    cout << "p1.m_A = " << p1.m_A << endl; //共享同一份数据
    cout << "p2.m_A = " << p2.m_A << endl;

    //2、通过类名
    cout << "m_A = " << Person::m_A << endl;


    //cout << "m_B = " << Person::m_B << endl; //私有权限访问不到
}

int main() {

    test01();

    system("pause");

    return 0;
}

2.静态成员函数

(1)所有对象共享同一个函数

(2)静态成员函数只能访问静态成员变量(无法访问非静态成员变量)

①静态函数不是属于某个特定对象的,即所有该类的所有对象都可以调用静态函数,因此编译器不知道非静态成员变量(m_B)是哪个对象的成员(引发歧义)

②静态static成员函数它只属于类本身,不属于每一个对象实例,独立存在。非静态成员,仅当实例化对象之后才存在。静态成员函数产生在前,非静态成员函数产生在后,静态函数无法访问一个不存在的东西。

class Person
{
public:
    static void func()
    {
        cout << "func调用" << endl;
        m_A = 100;
        //m_B = 100; //错误,不可以访问非静态成员变量
    }
    static int m_A; //静态成员变量
    int m_B; //非静态成员变量
};

(3)静态成员函数不属于某个特定的对象,有两种访问方式:(与静态成员变量相同)

①通过某个对象访问

Person p;
cout << p.func() << endl;

②通过类名访问

cout << Person::func() << endl;

(4)静态成员函数可以设置访问权限 

class Person
{

public:

    //静态成员函数特点:
    //1 程序共享一个函数
    //2 静态成员函数只能访问静态成员变量

    static void func()
    {
        cout << "func调用" << endl;
        m_A = 100;
        //m_B = 100; //错误,不可以访问非静态成员变量
    }

    static int m_A; //静态成员变量
    int m_B; // 
private:

    //静态成员函数也是有访问权限的
    static void func2()
    {
        cout << "func2调用" << endl;
    }
};
int Person::m_A = 10;


void test01()
{
    //静态成员变量两种访问方式

    //1、通过对象
    Person p1;
    p1.func();

    //2、通过类名
    Person::func();


    //Person::func2(); //私有权限访问不到
}

int main() {

    test01();

    system("pause");

    return 0;
}

3.C++对象模型和this指针

3.1.成员变量和成员函数分开存储

1.c++编译器会为每一个空对象也分配一个字节空间,用于区分不同的对象(每个空对象也应该有一个独一无二的内存地址)

class Person{

};
Person p;     //p为空对象,占用空间为1B

2.当定义了一个非静态成员变量时,分配给对象的内存空间所占字节由变量的数据类型决定(需内存对齐)

①结构体或类中的内存分配默认是按照4个字节的倍数进行分配

②内存对齐:在32位操作系统下,我们普遍用的gcc编译器和vs编译器都是默认按照4个大小进行内存对齐的

(1)比如有两个成员,  int a;   char b; 如果按大小相加的话应该为5字节,但由于内存对齐是8个字节

(2)比如有两个成员  char a; char b;   这时的类的大小就为2字节,因为这个时候成员都是char型的,都占一个字节的空间,本身就对齐了,不要内存对齐操作

(3)比如有四个成员,3个char型,一个int型,内存对齐后为8

class Person{
    int m_A;    
};
Person p;     //p占用空间为4B

3.只有非静态成员变量是属于类的对象,占用对象的空间(int m_A)

静态成员变量(static int m_A)、静态成员函数(static void func())和非静态成员函数(void func())不属于类的对象,不占用对象空间(static关键字和函数)

4.所有函数共享一个函数实例

class Person {
public:
    Person() {
        mA = 0;
    }
    //非静态成员变量占对象空间
    int mA;
    //静态成员变量不占对象空间
    static int mB; 
    //函数也不占对象空间,所有函数共享一个函数实例
    void func() {
        cout << "mA:" << this->mA << endl;
    }
    //静态成员函数也不占对象空间
    static void sfunc() {
    }
};

int main() {

    cout << sizeof(Person) << endl;

    system("pause");

    return 0;
}

3.2.this指针的概念

this指针的本质是指针常量:指针的指向的地址是不可以更改的,指向的地址存放的数据可以更改

1.this指针是隐含每一个非静态成员函数内的一种指针(this指针指向被调用的成员函数所属的对象

2.this指针不需要定义,直接使用即可(成员函数内部都有一个this指针)

3.this指针的用途:

(1)当形参和成员变量同名时,可用this指针来区分:(相当于python中的self)(解决形参和成员变量重名时,编译器无法区分的问题)

this修饰的变量,即 this->成员变量(this->age) 指的是本对象的成员变量

没有被this修饰的变量,即 变量(age) 指的是形式参数

//采用this指针
class Person
{
public:

    Person(int age)    //传入形式参数age
    {
        //当形参和成员变量同名时,可用this指针来区分
        //传入Person构造函数中的形式参数age赋值给该类中的age变量
        this->age = age;
    }
    int age;
};

//不适用this指针
class Person{
public:
    //编译器会认为Person构造函数中的形式参数age和赋值操作的左右两个age
    //这三个age都是同一个age,即都是形式参数age
    //故执行完Person构造函数后,Person类中的age并没有被赋值
    Person(int age){
        age = age;
    }
    int age;
};

(2)返回*this,可在类的非静态成员函数中返回对象本身

①构造函数中参数使用引用传递 / 值传递:

引用传递:函数体内部的形参(p)与传入的实参(p1)是同一个对象

值传递:利用默认拷贝构造函数构造一个p1的副本p,即p和p1是不同的对象

②构造函数中返回值使用引用返回 / 值返回:

引用返回(不发生拷贝,返回的是p2本体,始终对p2进行加):每次返回的都是p2的本体,最后p2.age = 40

值返回(发生拷贝,返回的是p2的副本,只对p2进行一次加):只有第一次的this指针指向的是p2的本体,因此,只对p2进行一次加,最后p2.age = 20;第二次是对p2'(通过默认拷贝构造函数浅拷贝得到的p2的副本)进行加操作,第三次是对p2''(通过默认拷贝构造函数浅拷贝得到的p2'的副本)进行操作,且p2''.age = 40,但p2.age = 20

注意这里是按引用传递,所以没有发生拷贝,相当于传了对象本身;然后按引用返回,返回的即对象本身

②this是指向对象p2的指针,即this的值为p2存储的地址,*this则为这个地址的数据,即对象p2

class Person
{
public:

    Person(int age)
    {
        //1、当形参和成员变量同名时,可用this指针来区分
        this->age = age;
    }

    Person& PersonAddPerson(Person p)
    {
        this->age += p.age;
        //返回对象本身
        return *this;
    }

    int age;
};

void test01()
{
    Person p1(10);
    cout << "p1.age = " << p1.age << endl;

    Person p2(10);
    p2.PersonAddPerson(p1).PersonAddPerson(p1).PersonAddPerson(p1);
    cout << "p2.age = " << p2.age << endl;
}

int main() {

    test01();

    system("pause");

    return 0;
}

3.3.空指针访问成员函数

1.空指针可以调用成员函数(可以是cout输出语句等),但该成员函数不能访问成员变量

(Person *p = NULL,即this = NULL,而执行ShowPerson函数时,被要求通过this访问成员变量mAge,出错)

(可以这么理解:你连女朋友都没有,怎么会有女朋友的年龄)

2.解决方法:可以通过在需要访问成员变量的成员函数中添加一个对于this指针指向的if判断,判断是否this是否为空指针,如果为空则return,防止空指针导致程序出错

3.成员函数访问本类成员变量的方式实际上是通过this指针的形式,用于强调这是当前对象的成员变量:this->成员变量

class Person{
public:
    void test() {
        //编译器实现时时通过指针的方式,即this->m_Age = 0;
        m_Age = 0;
    }

    int m_Age;
};
//空指针访问成员函数
class Person {
public:

    void ShowClassName() {
        cout << "我是Person类!" << endl;
    }

    void ShowPerson() {
        if (this == NULL) {    //判断当前对象是否为空指针
            return;
        }
        cout << mAge << endl;    //实际上是this->mAge
    }

public:
    int mAge;
};

void test01()
{
    Person * p = NULL;
    p->ShowClassName(); //空指针,可以调用成员函数
    //空指针,且成员函数中用到了this指针,就不可以了
    //需增加个if判断指针是否为空,为空则返回,可以避免错误
    p->ShowPerson();
}

int main() {

    test01();

    system("pause");

    return 0;
}

3.4.const修饰成员函数

常函数:在成员函数后加const,修饰的是this的指向,让指针指向的地址和值都不能更改

常对象:在申明时加const,使得成员属性不能被更改

2.(1)原来的this是指针常量,Person * const this,其中const修饰的是this,即不允许修改this指针指向的地址,但可以修改this指针指向的地址中存放的数据

因此,this->m_A = 100;修改其指向地址的值是允许的;而this = NULL;即即修改其指向的地址是不允许的

(2)修改之后this指针是常量指针常量,const Person * const this(体现在成员函数中则是const添加在括号后),其中第二个const依然修饰的是this,即不允许修改this指针指向的地址;而第一个const则使得this指针指向的地址存放的数据也不能更改

(this指针本身就是指针常量,现在再用const修饰,就是常量指针常量,指针指向的地址和该地址的数据就不能再修改了

3.在成员属性前面加上mutable关键字,则可以使得该成员属性在常函数/常对象中可以被修改

4.常对象只能调用常函数:如果常对象能够调用普通成员函数,而该普通成员函数中含有修改对象的成员属性的语句,则就可以通过该函数修改成员属性,与常对象不能修改成员属性的特性相悖(即常对象如果允许调用非常函数,则可能修改属性

class Person {
public:
	Person() {
		m_A = 0;
		m_B = 0;
	}

	//this指针的本质是一个指针常量,指针的指向不可修改
	//如果想让指针指向的值也不可以修改,需要声明常函数
	void ShowPerson() const {
		//const Type* const pointer;
		//this = NULL; //不能修改指针的指向 Person* const this;
		//this->mA = 100; //但是this指针指向的对象的数据是可以修改的

		//const修饰成员函数,表示指针指向的内存空间的数据不能修改,除了mutable修饰的变量
		this->m_B = 100;
	}

	void MyFunc() const {
		//m_A = 10000;
        //错误,不允许在常函数中修改成员属性
	}

public:
	int m_A;
	mutable int m_B; //mutable 可修改 可变的
};


//const修饰对象  常对象
void test01() {

	const Person person; //常量对象  
	cout << person.m_A << endl;
	//person.m_A = 100; //常对象不能修改成员变量的值,但是可以访问
	person.m_B = 100; //但是常对象可以修改mutable修饰成员变量

	//常对象访问成员函数
	person.MyFunc(); 

}

int main() {

	test01();

	system("pause");

	return 0;
}

4.友元

在程序里,有些私有属性也想让类外特殊的一些函数或者类进行访问,就需要用到友元的技术

友元的目的就是让一个函数或者类访问另一个类中私有成员

友元的关键字为 ==friend==

4.1.全局函数做友元

全局函数做友元,需要加上friend在类里声明,表示该全局函数可以访问私有变量

class Building
{
    //告诉编译器 goodGay全局函数 是 Building类的好朋友,可以访问类中的私有内容
    friend void goodGay(Building * building);

public:

    Building()
    {
        this->m_SittingRoom = "客厅";
        this->m_BedRoom = "卧室";
    }


public:
    string m_SittingRoom; //客厅

private:
    string m_BedRoom; //卧室
};


void goodGay(Building * building)
{
    cout << "好基友正在访问: " << building->m_SittingRoom << endl;
    cout << "好基友正在访问: " << building->m_BedRoom << endl;
}


void test01()
{
    Building b;
    goodGay(&b);
}

int main(){

    test01();

    system("pause");
    return 0;
}

4.2.类做友元

1.类或结构体的前向声明只能用来定义指针对象或引用(且不能解引用):因为编译到这里时还没有发现定义,因此不知道该类或者结构的内部成员,没有办法具体的构造一个对象,所以会报错

class Building;    //类的声明
Building *p;    //一个类只有声明则可以创建指针
Building *&t = p;    //一个类只有声明可以创建引用
//Building a;是错误的,一个类只有声明无法创建实例化对象

2.类内声明,类外实现函数:类内可以只给出成员函数的实现,类外再给出具体的实现,但是在类外实现的函数需要指明作用域,即类名 :: 函数名 (参数列表)

class Building {
public:
    Building();    //类内构造函数的声明
    void visit();    //类内成员函数的声明
    
    int m_A;    //类内成员属性的声明
}

Building::Building(){    //类外构造函数的实现
    ...
}

void Building::visit(){    //类外成员函数的实现
    ...
}

3.类做友元就是将A类前添加friend关键字,并放在B类中:这样就能使得A类访问B类的私有成员变量

class Building;
class goodGay
{
public:

    goodGay();
    void visit();

private:
    Building *building;
};


class Building
{
    //告诉编译器 goodGay类是Building类的好朋友,可以访问到Building类中私有内容
    friend class goodGay;

public:
    Building();

public:
    string m_SittingRoom; //客厅
private:
    string m_BedRoom;//卧室
};

Building::Building()
{
    this->m_SittingRoom = "客厅";
    this->m_BedRoom = "卧室";
}

goodGay::goodGay()
{
    building = new Building;
}

void goodGay::visit()
{
    cout << "好基友正在访问" << building->m_SittingRoom << endl;
    cout << "好基友正在访问" << building->m_BedRoom << endl;
}

void test01()
{
    goodGay gg;
    gg.visit();

}

int main(){

    test01();

    system("pause");
    return 0;
}

4.3.成员函数做友元

1.A->B:指针指向所指对象的各种东西时用 -> 

A::B:表示A作用域下的B

A.B:表示A的成员B . 

2.(1)因为Goodgay类需要声明Building类变量,所以Building类必须Goodgay类之前声明(前置声明,但前置声明只能是以引用形式或者指针形式声明,而不能是实例化对象)

(2)因为Building的定义中需要将Goodgay类的成员函数声明成友元成员函数,所以Building类必须Goodgay类之后定义,即Building类需要使用到具体的Goodgay类

(3)因为Goodgay中的构造函数需要调用Building的构造函数,所以Goodgay类中构造函数的实现必须在Building类的定义之后

(程序是一行一行顺序执行的,如果顺序颠倒,将会出错)

3.goodGay类的成员函数visit是Building类的友元,因此可以访问Building类的私有成员变量

goodGay类的成员函数visit不是Building类的友元,因此不可以访问Building类的私有成员变量

class Building;
class goodGay
{
public:

    goodGay();
    void visit(); //只让visit函数作为Building的好朋友,可以发访问Building中私有内容
    void visit2(); 

private:
    Building *building;
};


class Building
{
    //告诉编译器  goodGay类中的visit成员函数 是Building好朋友,可以访问私有内容
    friend void goodGay::visit();

public:
    Building();

public:
    string m_SittingRoom; //客厅
private:
    string m_BedRoom;//卧室
};

Building::Building()
{
    this->m_SittingRoom = "客厅";
    this->m_BedRoom = "卧室";
}

goodGay::goodGay()
{
    building = new Building;
}

void goodGay::visit()
{
    cout << "好基友正在访问" << building->m_SittingRoom << endl;
    cout << "好基友正在访问" << building->m_BedRoom << endl;
}

void goodGay::visit2()
{
    cout << "好基友正在访问" << building->m_SittingRoom << endl;
    //cout << "好基友正在访问" << building->m_BedRoom << endl;
}

void test01()
{
    goodGay  gg;
    gg.visit();

}

int main(){

    test01();

    system("pause");
    return 0;
}

5.运算符重载

1.编译器只能实现特定数据类型(int、float等等)的加减乘除等操作,而编译器不能实现自定义类的加减乘除等操作(编译器也不知道该如何正确的实现,将会出错:没有与这些操作数匹配的运算符),需要通过运算符重载的方式实现(或者说规定)对类的加减乘除等操作

2.可以通过成员函数的方式和全局函数的方式实现运算符重载:

(1)成员函数写在类内,可以使用this指针(this指向的是调用该函数的实例对象),只需传入一个变量

(2)全局函数无法使用this指针,需要传入两个变量

(3)可以通过正常的运算方式(+-*/,例:person p3 = p2 + p1)简化调用实现运算符重载的成员函数/全局函数(通过指定函数名的方式,编译器帮忙实现转换),也可以通过传入参数的方式调用

//成员函数方式
Person p3 = p2 + p1;  //相当于 p2.operaor+(p1)

//全局函数方式
Person p4 = p3 + 10; //相当于 operator+(p3,10)

3.运算符重载也允许实现函数重载,且与函数重载规则一致

4.对于内置的数据类型的表达式的的运算符是不可能改变的(int,float等数据类型的运算符不能重新定义)

5.不要滥用运算符重载(加号运算符重载,不能定义为相减)

5.1.加号运算符重载

1.实际上就是把p2传入p1的成员函数然后拷贝相加,再把结果返回

2.需要使用指定的函数名,即operator+

3.使用引用传递是减少内存占用;使用const是因使用引用传递,防止在函数体内部中对引用的值进行修改

class Person {
public:
    Person() {};
    Person(int a, int b)
    {
        this->m_A = a;
        this->m_B = b;
    }
    //成员函数实现 + 号运算符重载
    Person operator+(const Person& p) {
        Person temp;
        temp.m_A = this->m_A + p.m_A;
        temp.m_B = this->m_B + p.m_B;
        return temp;
    }


public:
    int m_A;
    int m_B;
};

//全局函数实现 + 号运算符重载
//Person operator+(const Person& p1, const Person& p2) {
//    Person temp(0, 0);
//    temp.m_A = p1.m_A + p2.m_A;
//    temp.m_B = p1.m_B + p2.m_B;
//    return temp;
//}

//运算符重载 可以发生函数重载 
Person operator+(const Person& p2, int val)  
{
    Person temp;
    temp.m_A = p2.m_A + val;
    temp.m_B = p2.m_B + val;
    return temp;
}

void test() {

    Person p1(10, 10);
    Person p2(20, 20);

    //成员函数方式
    Person p3 = p2 + p1;  //相当于 p2.operaor+(p1)
    cout << "mA:" << p3.m_A << " mB:" << p3.m_B << endl;


    Person p4 = p3 + 10; //相当于 operator+(p3,10)
    cout << "mA:" << p4.m_A << " mB:" << p4.m_B << endl;

}

int main() {

    test();

    system("pause");

    return 0;
}

5.2.左移运算符重载

如果不重载左移运算符,则无法通过cout << 输出自定义类:编译器没有与这些操作数匹配的运算符

1.左移运算符重载只能通过全局函数

(1)cout << X(输出的对象) 这种情况是cout在调用operator<<,而不是X(输出的对象)在调用operator<<

(2)通过比较加号运算符重载的成员函数形式:p.operator+(a) 等价于 p+a,可以证明,如果通过成员函数重在左移运算符,则会出现p.operator<<(cout),即p<<cout,与我们想要实现的方式cout << p相反,故左移运算符不能通过成员函数重载

2.cout的类型是ostream,即ostream cout;且cout全局只能有一个,而值传递的方式会调用默认拷贝构造函数创建一个cout的副本,故需要使用引用传递的方式

3.如果要链式调用,返回值就要作为下一个调用的参数,返回的对象必须为本体;相应的,如果返回值类型为void,则不能实现链式调用,即不允许cout << p << endl;

在之前的加法运算符重载调用链式就可以很好说明这点,把返回值和参数都设为同一种类型

但是这里ostream类型必须是引用类型,所以返回值必须是引用,就可以链式调用

4.在重载左移运算符时,通过引用传递的方式传入参数cout,此时因为是引用传递,可以为cout起别名,不一定在函数体内部的参数名必须为cout

5.重载左移运算符配合友元可以实现输出某些私有/保护权限的自定义数据类型

class Person {
    friend ostream& operator<<(ostream& out, Person& p);

public:

    Person(int a, int b)
    {
        this->m_A = a;
        this->m_B = b;
    }

    //成员函数 实现不了  p << cout 不是我们想要的效果
    //void operator<<(Person& p){
    //}

private:
    int m_A;
    int m_B;
};

//全局函数实现左移重载
//ostream对象只能有一个
ostream& operator<<(ostream& out, Person& p) {
    out << "a:" << p.m_A << " b:" << p.m_B;
    return out;
}

void test() {

    Person p1(10, 20);

    cout << p1 << "hello world" << endl; //链式编程
}

int main() {

    test();

    system("pause");

    return 0;
}

5.3.递增运算符重载

1.前置递增(++a):先实现递增

后置递增(a++):后实现递增

int a = 0;
cout << a++ << endl;    //先输出a = 0,然后再a = a + 1 = 0 + 1 = 1
cout << a << endl;    //输出a = 1
cout << ++a << endl;    //先a = a + 1 = 1 + 1 = 2,再输出a 
cout << a << endl;    //输出a = 2

2.前置运算符是直接递增变量,而后置要先拷贝旧值,然后递增,最后返回旧值

设a = 0

(1)(a++)++是不被允许的:编译器将会提示++需要可修改的左值

①输出a++是可以的:将会输出a = 0,

出错的点在括号外的++:a++的实现过程是编译器首先将会重新声明一个临时变量存储当前a的值,然后进行自增操作,最后返回这个临时变量的值;而这个临时变量返回后,再继续进行括号外的++时,将是对这个临时变量进行操作,这是不被允许的

由此得出我们进行后置递增时,返回值类型应该选择值返回:如果不带引用的话,那么相当于是进行了一次拷贝构造函数,从而使得生成了一个新的MyInteger类型的数据;如果加了引用之后,那么相当于使用的还是temp这个数据,由于局部会释放,会出错

同时,这也导致了这段代码中的左移运算符重载的第二个参数中使用的是值传递而不是引用传递:原因就是因为后置++是值返回若采用引用返回,则函数调用结束后,该函数的局部变量所占用的内存空间被释放,不再拥有对这片内存空间的操作权限,即导致出错

(2)++(++a)是允许的:

①值得注意的是,在默认的前置++中,++(++a)的过程始终是对同一个a进行前置++操作

②如果采用值返回的方式,则调用前置++函数时,函数将会调用拷贝构造函数构造一个匿名对象,并且在执行结束后,返回这个匿名对象,与传入的对象并不是同一个对象;因此实际上只会进行一次前置++操作,但是能够输出两次前置++的结果(因为第二次前置++操作是传入的参数是这个匿名对象)

如果采用引用返回的方式,则与默认的前置++相同,始终都会对同一个对象操作

故前置++需要使用引用返回方式

图为值传递,每次传入对象和函数体内部实际操作的对象并不是一个对象,而是传入对象的拷贝

int a = 0;    
//1.进行++a,即a = 1
//2.输出a = 1
//3.进行++a,即a = 2;
cout << ++(++a) << endl;

3.前置++和后置++需要通过占位参数的方式函数重载

原因:前置++和后置++的函数名相同,但是,不能通过返回值类型的不同区分函数重载

(函数重载条件:同一作用域;函数名相同;参数类型不同或个数不同或顺序不同)

class MyInteger {

    friend ostream& operator<<(ostream& out, MyInteger myint);

public:
    MyInteger() {
        m_Num = 0;
    }
    //前置++
    MyInteger& operator++() {
        //先++
        m_Num++;
        //再返回
        return *this;
    }

    //后置++
    MyInteger operator++(int) {
        //先返回
        MyInteger temp = *this; //记录当前本身的值,然后让本身的值加1,但是返回的是以前的值,达到先返回后++;
        m_Num++;
        return temp;
    }

private:
    int m_Num;
};


ostream& operator<<(ostream& out, MyInteger myint) {
    out << myint.m_Num;
    return out;
}


//前置++ 先++ 再返回
void test01() {
    MyInteger myInt;
    cout << ++myInt << endl;
    cout << myInt << endl;
}

//后置++ 先返回 再++
void test02() {

    MyInteger myInt;
    cout << myInt++ << endl;
    cout << myInt << endl;
}

int main() {

    test01();
    //test02();

    system("pause");

    return 0;
}

5.4.赋值运算符重载

1.c++编译器至少给一个类添加4个函数

(1)默认构造函数(无参,函数体为空)

(2)默认析构函数(无参,函数体为空)

(3)默认拷贝构造函数,对属性进行值拷贝浅拷贝,逐字节的复制

(4)赋值运算符 operator=, 对属性进行值拷贝浅拷贝,逐字节的复制

2.(1)①代码中Person类的成员变量m_Age为int型的指针,因为编译器默认提供的赋值运算符使用的是浅拷贝,即当执行p2 = p1时,实际上执行的是p2.m_Age = p1.m_Age,即将p1的指针指向的地址直接赋值给了p2的指针指针的值存储的是地址,这就导致p2和p1都指向同一个地址,即p1指向的地址(虽然改变了指针的值,但实际上只是改变了指针的指向,且原p2指向的地址的数据仍然在那里,造成内存泄漏)

②p1和p2的指针是在经过赋值后虽然指向的是相同的地址,但是仍然不同的两个指针(体现在两者的地址不同),对其中一个指针的修改并不影响对另外一个(这里的修改指的是修改指针的指向)

(2)①此时析构函数中的delete语句,在p1执行结束时,对该地址进行一次释放,而p2执行结束时,又对该地址又进行一次释放,导致出错(不能对同一地址释放两次)

②析构函数中对成员变量指针m_Age是否为空的判断针对的是m_Age是否为空指针(m_Age是否指向一个地址),而非m_Age指向的地址存放的数据是否有效(是否被释放)

(本质就是深拷贝与浅拷贝的问题)

解决方法:利用深拷贝解决浅拷贝带来的问题即让p2和p1指向的是不同的地址

3.p2 = p1实际上是p2.operator=(p1),即p2作为本类调用自己的成员函数operator=,传入参数为p1

4.使用深拷贝(重载赋值运算符)解决2中浅拷贝引发的问题:

(1)对m_Age执行delete,并且将m_Age置空(如果直接进行赋值,则会导致原来m_Age指向地址的数据仍然占据这个地址,即发生内存泄漏

(内存泄漏是在计算机科学中,由于疏忽或错误造成程序未能释放已经不再使用的内存)

(2)利用new在堆区重新申请一片内存空间,进行深拷贝

注意事项:

(1)这里的重载函数的参数一定要以引用或者指针的形式传入(对传入的对象p进行操作)

(2)不然在传入的时候进行了一次拷贝将赋值右边p的值传入的时候临时变量记录的p的属性m_Age的地址(在调用函数的过程中,传入的对象p作为形参,编译器将会重新利用默认构造函数浅拷贝一个实参p',即函数实际上是对实参p'进行操作,而非p,并且p'与p的成员属性相同)(这个p'是匿名对象,即临时变量)

(3)而出了赋值运算符重载函数会进行一次析构,这时p的属性new出来的空间已经被释放了(p'先进行一次释放,p再进行一次释放,调用了两次析构函数)

(4)最后结束调用虽然你深拷贝了但是程序还是会崩

5.为了能实现连等,即p3 = p2 = p1,赋值运算符需要返回自身,即return *this(链式编程思想)

(赋值运算符是从右往左赋值,故最后的结果为p1、p2和p3的值都为p1)

class Person
{
public:

    Person(int age)
    {
        //将年龄数据开辟到堆区
        m_Age = new int(age);
    }

    //重载赋值运算符 
    Person& operator=(Person &p)
    {
        if (m_Age != NULL)
        {
            delete m_Age;
            m_Age = NULL;
        }
        //编译器提供的代码是浅拷贝
        //m_Age = p.m_Age;

        //提供深拷贝 解决浅拷贝的问题
        m_Age = new int(*p.m_Age);

        //返回自身
        return *this;
    }


    ~Person()
    {
        if (m_Age != NULL)
        {
            delete m_Age;
            m_Age = NULL;
        }
    }

    //年龄的指针
    int *m_Age;

};


void test01()
{
    Person p1(18);

    Person p2(20);

    Person p3(30);

    p3 = p2 = p1; //赋值操作

    cout << "p1的年龄为:" << *p1.m_Age << endl;

    cout << "p2的年龄为:" << *p2.m_Age << endl;

    cout << "p3的年龄为:" << *p3.m_Age << endl;
}

int main() {

    test01();

    //int a = 10;
    //int b = 20;
    //int c = 30;

    //c = b = a;
    //cout << "a = " << a << endl;
    //cout << "b = " << b << endl;
    //cout << "c = " << c << endl;

    system("pause");

    return 0;
}

5.5.关系运算符重载

class Person
{
public:
    Person(string name, int age)
    {
        this->m_Name = name;
        this->m_Age = age;
    };

    bool operator==(Person & p)
    {
        if (this->m_Name == p.m_Name && this->m_Age == p.m_Age)
        {
            return true;
        }
        else
        {
            return false;
        }
    }

    bool operator!=(Person & p)
    {
        if (this->m_Name == p.m_Name && this->m_Age == p.m_Age)
        {
            return false;
        }
        else
        {
            return true;
        }
    }

    string m_Name;
    int m_Age;
};

void test01()
{
    //int a = 0;
    //int b = 0;

    Person a("孙悟空", 18);
    Person b("孙悟空", 18);

    if (a == b)
    {
        cout << "a和b相等" << endl;
    }
    else
    {
        cout << "a和b不相等" << endl;
    }

    if (a != b)
    {
        cout << "a和b不相等" << endl;
    }
    else
    {
        cout << "a和b相等" << endl;
    }
}


int main() {

    test01();

    system("pause");

    return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/169252.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

python学习 --- 字典基础

目录 一、什么是字典&#xff1f; 1、字典示意图-无序说明 2、字典实现原理 二、字典的创建 1、使用花括号 2、使用内置函数dict() 三、字典常用操作 1、字典中元素的获取 2、key的判断&#xff08;存在与否&#xff09; 3、字典元素的删除 4、字典元素的新增 5、获…

计讯物联智慧水务解决方案:用“智水”捍卫生命之泉

项目背景 水是生命的源泉&#xff0c;是城市的灵魂&#xff0c;是农业的命脉&#xff0c;是工业的基石。2022年以来&#xff0c;基于国家政策的引导、科技革新的驱动与供排水需求增长&#xff0c;智慧水务的发展突飞猛进&#xff0c;从信息化到数字化&#xff0c;再到智能化&a…

若依 ruoyi vue el-switch 列表开关状态显示有误 全部关闭的问题

后台使用int类型传状态status的值但是前端列表展示的开关状态是未开启&#xff0c;实际上&#xff0c;后台传的都是开启的状态结果应该是这样确定后台传的status值 在 el-switch 标签中是否使用了正确的值判断&#xff0c;比如 后台用的是字符串、布尔 或者是 数值类型&#xf…

android判断文件是否存在跳转不同activity

android studio版本&#xff1a;2021.2.1Patch 2例程名称&#xff1a;ActivityJump完成日期&#xff1a;2023.1.17一直在完善一个小东西&#xff0c;也是不断的在学习。之前做的那个桌面日历天气&#xff08;老旧安卓手机发挥余热做桌面时钟摆件使用&#xff09;&#xff0c;有…

java 数列排序

试题 基础练习 数列排序提交此题 评测记录 资源限制内存限制&#xff1a;512.0MB C/C时间限制&#xff1a;1.0s Java时间限制&#xff1a;3.0s Python时间限制&#xff1a;5.0s问题描述给定一个长度为n的数列&#xff0c;将这个数列按从小到大的顺序排列。1<n<200输入格式…

【渗透测试】信息搜集总结

前言零零散散的发布过很多文章了&#xff0c;但是也没有完整的总结一下&#xff0c;今天就从信息搜集入手&#xff0c;系统的总结一下。一方面可以巩固自己的基础&#xff0c;作为自己的字典随用随查&#xff0c;另一方面希望对大家的学习和工作起到帮助作用。按照这个过程基本…

求两点间的距离-C语言实现

任务描述 已知两点A(x1,y1),B(x2,y2),求其间的距离。 输入 一行四个浮点数,x1,y1,x2,y2 输出 两点间的距离,要求不要输出多余的零。 本关知识 两点间的距离公式 pow函数 在C语言中,提供了用于求x的y次幂的函数,函数原型为: double pow(double x, double y…

啊啊啊小红书爆款标题技巧被我找到了!

我通过平日里对小红书的了解和积累&#xff0c;再加上这一阵子对小红书爆款笔记的研究&#xff0c;终于在这么多笔记当中发现了小红书爆款笔记标题的撰写规律&#xff01; 我们在撰写小红书的时候恨不得篇篇都是爆文&#xff0c;那么今天就来讲一下小红书那些爆款笔记标题的撰写…

75、DiffRF: Rendering-Guided 3D Radiance Field Diffusion

简介 主页&#xff1a;https://sirwyver.github.io/DiffRF/ 对应用于三维亮度场的概率扩散过程进行去噪。在3D监控和体积渲染的指导下&#xff0c;模型能够无条件地合成高保真3D资产(左)。 蒙面补全的新应用(右)&#xff0c;即从不完整的对象中恢复形状和外观的任务(在右上方…

超实用的百度百科人物词条创建攻略分享,纯干货

自媒体时代&#xff0c;人们越来越有IP意识&#xff0c;打造个人IP就是在为自己创造更多价值。 个人IP的打造是一个提升知名度的过程&#xff0c;怎么才能快速提升&#xff1f;创建一个百度百科词条不失为一个好的选择。 现在用户有问题就会在百度上搜索一下&#xff0c;当用户…

java实现模拟调用接口

本文总结如何用fiddler和postman调试接口&#xff0c;并用java模拟调用接口。fiddler用法当页面点击事件后&#xff0c;在fiddler出现一个请求&#xff0c;单击左侧请求&#xff0c;在右侧的raw的tab标签&#xff0c;出现该请求的详细内容。其实一个请求需要两类参数&#xff0…

Linux常用命令——umask命令

在线Linux命令查询工具(http://www.lzltool.com/LinuxCommand) umask 用来设置限制新建文件权限的掩码 补充说明 umask命令用来设置限制新建文件权限的掩码。当新文件被创建时&#xff0c;其最初的权限由文件创建掩码决定。用户每次注册进入系统时&#xff0c;umask命令都被…

大数据技术架构(组件)——Hive:流程剖析1

1.1、流程剖析大致流程&#xff1a;1、客户端连接到HS2(HiveServer2&#xff0c;目前大多数通过beeline形式连接&#xff0c;Hive Cli模式相对较重&#xff0c;且直接略过授权访问元数据),建立会话2、提交sql&#xff0c;通过Driver进行编译、解析、优化逻辑计划&#xff0c;生…

OCR识别

阿里云和百度云识别&#xff0c;京东智能识别。 图片识别需求 1&#xff0c;拿到一个文件&#xff0c;2&#xff0c;变成InputStream 3&#xff0c;base64编码将流解析下载&#xff0c; 4 调用方法识别 1.身份证 2&#xff0c;行驶证 3&#xff0c;驾驶证 FileUtils file…

Elasticsearch:通过例子快速入门

Elasticsearch 是业界最流行的开源企业搜索引擎&#xff0c;应用广泛。 在我们的手机里的 App 背后的搜索引擎好多都是 Elasticsearch&#xff0c;比如我们熟知的抖音&#xff0c;滴滴&#xff0c;美团&#xff0c;携程&#xff0c;点评&#xff0c;银行 app&#xff0c;保险&a…

ssh反向代理实现内网穿透【亲测可用】

常用内网穿透方式 1、网卡层映射&#xff0c;包括购买公网ip 推荐指数&#xff1a;&#x1f44d;&#x1f3fb;&#x1f44d;&#x1f3fb;&#x1f44d;&#x1f3fb;&#x1f44d;&#x1f3fb;&#x1f44d;&#x1f3fb;。 缺点&#xff1a;主要申请困难。 2、自己搭建内网…

JavaWeb-HTTPTomcatServlet

JavaWeb-HTTP&Tomcat&Servlet 1&#xff0c;Web概述 1.1 Web和JavaWeb的概念 Web是全球广域网&#xff0c;也称为万维网(www)&#xff0c;能够通过浏览器访问的网站。 在我们日常的生活中&#xff0c;经常会使用浏览器去访问百度、京东、传智官网等这些网站&#xf…

韩顺平老师的linux基础课(复习笔记)

今天听了韩老师的课程&#xff0c;深受启发啊&#xff01;&#xff01;&#xff01; 卖油翁的“我亦无他&#xff0c;唯手熟尔”&#xff0c;只是手法熟练罢了&#xff01;&#xff01; 还有老黄牛的坚持&#xff0c;别人把时间都放在努力上&#xff0c;而我把时间放在选择上&a…

微信小程序分类菜单激活状态跟随列表滚动自动切换

这里主要用到微信小程序提供的SelectorQuery获取页面节点信息实现&#xff0c;组件用的是微信小程序的scroll-view 逻辑就是获取右侧盒子的节点信息&#xff0c;获取右侧子分类的节点信息&#xff0c;当子分类滑动到顶部的之后&#xff0c;则切换左侧分类状态&#xff0c;而且当…

【java】冒泡排序/选择排序/希尔排序

文章目录排序分类/排序算法的分类冒泡排序代码1&#xff1a;代码2&#xff08;优化代码3&#xff08;算法优化 --当次排序没有进行交换则退出循环代码4&#xff08;封装为方法代码5&#xff08;检测冒泡排序时间复杂度选择排序代码1代码2&#xff08;优化算法代码3&#xff08;…