C++ 第四章 类和对象

一、深拷贝与浅拷贝

浅拷贝:简单的赋值操作,会导致指针指向同一内存地址
如果利用编译器提供的拷贝构造函数,会做浅拷贝操作
浅拷贝带来的问题是:堆区内存重复释放,引发崩溃

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

public:
	int age;
	string name;
	int *height;
	person(string name, int age,int height)
	{
		this->age = age;
		this->name = name;
		this->height = new int(height);
	}
	person(const person& p) //深拷贝
	{
		cout << "Person 拷贝构造函数调用" << endl;
		age = p.age;
		//height = p.height;编译器默认浅拷贝时进行的操作
		height = new int (*p.height);
	}
	~person()//析构函数:将堆区开辟的数据进行释放
	{
		if (height != NULL) delete(height);
		cout << "析构" << endl;
	}
};

总结:如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的问题

二、初始化列表

传统赋值初始化相当于:先声明类,再进行属性的赋值操作
初始化列表相当于:直接声明一个有初始值的类型,在构造函数语句前,省略了赋值操作
在大型项目中,class类中成员变量极多的情况下,初始化列表效率更高

	person(string name, int age,int h) :age(age), name(name), height(new int(h))
	{
	//注意:指针成员变量初始化时需用new 类型名(变量)来进行
	}

三、类对象作为类成员

构造顺序:构造时先构造类的成员对象,再构造类自身
析构顺序:析构时先析构自身,再析构类的成员对象

class Phone
{
public:
	string p_Brand;

	Phone()
	{
		cout << "Phone created!" << endl;
	}
	Phone(string brand)
	{
		this->p_Brand = brand;
		cout << "Phone created!" << endl;
	}
	~Phone()
	{
		cout << "Phone deleted!" << endl;
	}
};
class person
{
public:
	int age;
	string name;
	int *height;
	Phone phone;
}
/*
Phone created!
Person created!
18 yxc 180 iphone
person deleted!
Phone deleted!
*/

四、静态成员

1.静态成员函数

两种访问方式:

person::func();//1.通过类名访问
person p1;
p1.func();//2.通过对象访问

特点:
1.程序共享一个函数
2.静态成员函数只能访问静态成员变量
3.静态成员函数也有访问权限

2.静态成员变量

特点:
1.所有对象共享同一份数据,在内存中只有一份
2.在编译阶段分配内存
3.类内声明,类外初始化

class Animal
{
public:
	static const int head = 1;
};

五、C++对象类型和this指针

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

在C++中,类内的成员变量和成员函数分开存储,只有非静态成员变量才属于类的对象上
空变量占用内存空间为1字节,因为编译器会给每个空对象分配一个字节空间,以区分空对象占用内存的位置
一个含int成员变量的对象占用内存空间为4字节

class Person
{
	int m_A; //非静态成员变量 属于类的对象
	static int m_B; //静态成员变量 不属于类的对象
	void func() //非静态成员函数 不属于类的对象
	{

	}
	static void func() //静态成员函数 属于类的对象
	{

	}
};

2.this指针概念

this指针指向被调用的成员函数所属的对象
this指针是隐含每一个非静态成员函数内的一种指针,无需定义,直接使用

class Person
{
public:
	int age;
	Person(int age)
	{
		this->age = age;
	}
	Person& AddAge(const Person p)
	{
		this->age += p.age;
		return *this;
	}
};
int main()
{
	Person p1(10);
	Person p2(20);
	p1.AddAge(p2).AddAge(p2).AddAge(p2);
	cout << p1.age << endl;
	return 0;
}

3.空指针访问成员函数

C++中空指针也是可以调用成员函数的,但也要注意有没有用到this指针
如果用到this指针,需要加以判断代码的健壮性

class Person
{
	void ShowAge()
	{
		if(this)
		{
			cout<<this->age<<endl;
		}
	}
}

4.const修饰成员函数

常函数

成员函数后加const后叫常函数
常函数不可修改成员属性
但是成员属性声明时加关键字mutable后,在常函数、常对象中仍可修改

常对象

声明对象前加const称为常对象
常对象只能调用常函数

this指针的本质是指针常量,指针的指向是不可修改的

class Person
{
public:
	int age;
	mutable int height; //特殊变量,在常函数、常对象中也可修改
	void ShowAge() const //常函数
	{
		if (this)
		{
			cout << this->age << endl;
			this->height = 185;
		}
	}
	void ShowHeight()
	{
		if (this)
		{
			cout << this->height << endl;
		}
	}
};
int main()
{
	Person p1(10);
	const Person p2(10);
	p1.ShowAge();
	p1.ShowHeight();
	//p2.ShowHeight(); //错误:常对象只能调用常函数
	return 0;
}

五、友元

在程序里,有些私有属性,也想让类外特殊的一些函数或者类进行访问,友元的目的就是让一个函数或者类 访问另一个类中似有成员
友元的关键字:friend
友元的三种实现方法:

  • 全局函数做友元
  • 类做友元
  • 成员函数做友元

1.全局函数做友元

class House
{
	friend void GF(House* house);//告诉编译器:全局函数GF是类House的好朋友,可访问private内容
public:
	string Living_room;
private:
	string Bed_room;
public:
	House()
	{
		Living_room = "客厅";
		Bed_room = "卧室";
	}
};
void GF(House* house)
{
	cout << "GF is visiting " << house->Living_room << endl;
	cout << "GF is visiting " << house->Bed_room << endl;

}

2.类做友元

class House
{
	friend class GF; //类GF是House的好朋友,可以访问private内容
			//无权限修饰,不是House的成员
public:
	House(); //类内声明函数,类外实现
public:
	string Living_room;
private:
	string Bed_room;
};
class GF
{
public:
	string name;
	House *house;
public:
	GF(string name);//类内声明函数,类外实现
	void visit(House* house);//类内声明函数,类外实现
};
House::House() //类外实现,注意明确命名空间
{
	Living_room = "客厅";
	Bed_room = "卧室";
}
void GF::visit(House* house)//注意声明命名空间的位置
{
	cout << name << " is visiting " << house->Living_room << endl;
	cout << name << " is visiting " << house->Bed_room << endl;

}
GF::GF(string name)//类外实现
{
	house = new House();
	this->name = name;
}

3.成员函数做友元

class House
{
	friend void GF::visit();
public:
	House(); //类内声明函数,类外实现
public:
	string Living_room;
private:
	string Bed_room;
};

六、运算符重载

概念:对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型

1.加号运算符重载

通过成员函数重载+号

class Person
{
public:
	int age;
	Person operator+(const Person& p)
	{
		Person temp(0);
		temp.age = this->age + p.age;
		return temp;
	}
	Person()
	{

	}
	Person(int age)
	{
		this->age = age;
	}
};

本质

Person p3 = p1.operator+(p2);

通过全局函数重载+号

Person operator+ (const Person& p1, const Person& p2)
{
	Person temp;
	temp.age = p1.age + p2.age;
	return temp;
}

本质

Person p3 = operator+(p1,p2);

2.左移运算符<<重载

只能利用全局函数重载左移运算符
因为利用成员运算符 左移运算符 p.operator<<(cout) 简化版本:p<<cout 无法实现p在左侧

ostream &operator<<(ostream& cout, const Person& p) //ostream是静态的,内存中只有一份
//本质:operator<<(cout,p) 简化:cout<<p p为引用类型,可以防止有开辟在堆区属性的对象崩溃
{
	cout << "姓名:" << p.name << " 年龄:" << p.age << endl;
	return cout;
}

若需要输出类的私有属性,可以将重载<<的函数做类的友元

3.递增运算符++重载

前置递增

	MyInteger& operator++()
	{
		this->val ++; //先加法运算
		return *this;//再返回结果
	}

注意:返回引用

后置递增

	MyInteger operator++(int) //int代表占位参数,可以用于区分前置和后置递增
	{
		MyInteger t = this->val;//保存原先结果
		this->val++;//加法运算
		return t;//返回原先结果,返回类型不是引用,因为要返回后置原先结果
	}

理解:后置递增较为耗时,因为内部发生值传递与拷贝操作

4.赋值运算符=重载

背景知识

C++编译器至少给一个类添加4个函数
1.默认构造函数
2.默认析构函数
3.默认拷贝函数
4.赋值运算符operator=,对属性进行值拷贝(若类存在到堆区的属性,则涉及到深浅拷贝问题)

默认=运算符存在的问题:浅拷贝

若对象存在开辟在堆区的属性,用默认=运算符赋值后,在析构时会导致堆区内容重复释放,程序崩溃

解决方案:利用深拷贝

class Person
{
public:
	int* age;
	Person(int age)
	{
		this->age = new int(age);
	}
	~Person()
	{
		if (age != NULL)
		{
			delete age;
		}
	}
	Person& operator=(const Person& p) //重载赋值运算符= 深拷贝
	{
		if (this->age != NULL) //判断在堆区是否有内存,很重要:防止内存泄漏
		{
			delete this->age;
			this->age = NULL;
		}
		this->age = new int(*p.age); //在堆区开辟新空间,拷贝值
		return *this;//返回引用类型,链式编程思想
	}
};
ostream& operator<<(ostream& cout, Person& p)
{
	cout << *p.age << endl;
	return cout;
}

int main()
{
	Person p1(18),p2(19),p3(20);
	p2 = p1 = p3;
	cout << p1<<p2<<p3;
	return 0;
}

5.关系运算符(<,==,>)重载

算法题中常用

	bool operator<(const Person& p)const
	{
		return age < p.age;
	}

6.函数调用运算符()重载

函数调用运算符()也可以重载
由于重载后使用的方式非常像函数的调用,因此称为仿函数
仿函数没有固定写法,非常灵活

class Myprint
{
public:
	void operator()(string str)
	{
		cout << str << endl;
	}
	Myprint()
	{

	}
};
int main()
{
	Myprint()("12345"); //匿名对象
	Myprint p1;
	p1("54321");
	return 0;
}

7.数组下标访问运算符[]重载

	T& operator[](int index)
	{
		if (index <= 0 || index >= size)
		{
			cout << "Error" << endl;
			exit(-1);
		}
		return this->data[index];
	}

七、继承(OOP三大特征之一)

有些类与类之间存在特殊的关系,下级别的成员除了拥有上一级的共性,还有自己的特性
此时需要考虑利用继承的技术,减少重复代码

1.基本语法

class 子类:继承方式 父类
{

};

2.概念

子类也称派生类,父类也称为基类
派生类中的成员包含从基类继承而来的,以及自己特有的成员
从基类继承而来的表现其共性,特有的成员表现其个性

3.继承方式

一共有三种继承方式:

  • 公共继承
  • 保护继承
  • 私有继承
    image
    父类中私有的成员只是被隐藏了,但还是会继承下去

公共继承

除父类中私有成员外,其他所有成员将会被显式继承,其访问权限保持不变

保护继承

除父类中私有成员外,其他所有成员将会被显式继承,其访问权限变为protected

私有继承

除父类中私有成员外,其他所有成员将会被显式继承,其访问权限变为private

4.继承中构造和析构的顺序

先构造父类,再构造子类
析构顺序一般与构造顺序相反

class Sub : public Base {

    private:
        int z;

    public:
        Sub(int x, int y, int z):Base(x,y){ //构造子类时,对父类构造函数写法
           this->z = z;
        }

        int getZ() {
            return z;
        }

        int calculate() {
            return Base::getX() * Base::getY() * this->getZ();
        }

};

5.继承同名成员处理方式

当子类中出现与父类同名的属性或函数时,
访问子类同名成员,直接访问即可
访问父类同名成员,需要加作用域
当子类与父类拥有同名的成员函数,子类会隐藏父类中同名成员函数,加作用域可以访问到父类中同名函数

6.继承同名静态成员处理方式

静态成员和非静态成员出现同名,处理方式一致

  • 访问子类同名成员,直接访问即可
  • 访问父类同名成员,需要加作用域

通过对象访问

	cout<<s.m_A<<endl;
	cout<<s.Base::m_A<<endl;

通过类名访问

	cout<<Son::m_A<<endl;
	cout<<Son::Base::m_A<<endl;

7.多继承语法

C++允许一个类继承多个类
实际开发中不建议使用,当父类中出现同名成员,需要加作用域区分

语法

class 子类:继承方式1 父类1,继承方式2 父类2…
{

};

8.菱形继承(钻石继承)

两个子类继承同一个父类,又有某个类同时继承两个子类,这种继承被称为菱形继承(钻石继承)

image

问题

  1. 羊继承动物的数据,驼同样继承了动物的数据,当草泥马使用数据时,会产生二义性
  2. 菱形继承导致数据有两份,资源浪费

解决方案:virtual

利用虚继承virtual

class Animal
{
public:
	int age;
};
class Sheep :virtual public Animal
{

};
class Tuo : virtual public Animal
{

};
class SheepTuo :public Sheep, public Tuo
{
	
};
int main()
{
	SheepTuo st;
	st.Sheep::age = 20;
	st.Tuo::age = 15;
	st.age = 21;
	cout << st.age << endl;
	return 0;
}

八、多态(OOP三大特性之一)

1.分类

静态多态:函数重载和运算符重载属于静态多态,复用函数名
动态多态:派生类和虚函数实现运行时多态
区别
静态多态的函数地址早绑定——编译阶段确定函数地址
动态多态的函数地址晚绑定——运行阶段确定函数地址

class Animal
{
public:
	int age;
	void speak()
	{
		cout << "动物在说话" << endl;
	}
};
class Cat:public Animal
{
	void speak()
	{
		cout << "喵~" << endl;
	}
};
//静态多态:地址早绑定,在编译阶段确定函数地址
void DoSpeak(Animal &a)
{
	a.speak();
}
int main()
{
	Cat c1;
	DoSpeak(c1);//动物在说话
	return 0;
}

若想实现子类调用函数,那么函数地址不能提前绑定,需要在运行阶段进行绑定,地址晚绑定

class Animal
{
public:
	int age;
	virtual void speak() //改为虚继承即可
	{
		cout << "动物在说话" << endl;
	}
};

重写:函数返回值 函数名 形参列表 完全相同

2.动态多态的满足条件

  1. 有继承关系
  2. 子类重写父类的虚函数

3.动态多态的使用

父类的指针或引用 执行子类的对象

4.多态的原理剖析

vfptr:虚函数(表)指针(virtual function pointer)
vftable:虚函数表(virtual function table)
image

5.多态的好处

  1. 组织结构清晰
  2. 可读性强
  3. 前期和后期扩展及维护性高

6.纯虚函数和抽象类

在多态中,通常父类中虚函数的实现是毫无意义的,主要是调用子类重写的内容
因此,可以将虚函数改为纯虚函数

语法

virtual 返回值类型 函数名 (参数列表) = 0;

当类中有了纯虚函数,此类也叫抽象类

抽象类特点:

  1. 无法实例化对象
  2. 子类必须重写抽象类中的纯虚函数,否则也属于抽象类

7.虚析构和纯虚析构

多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码
解决方法:将父类中的析构函数改为虚析构和纯虚析构

共性

都需要具体的函数实现
可以解决父类指针释放子类对象

不同

若是纯虚析构,则该类属于抽象类,无法实例化对象

语法

class Base
{
public:
	virtual Base()
	{
	
	}
	virtual ~Base() = 0//纯虚析构
	virtual ~Base() //虚析构
	{
		
	}
}
Base::~Base()//纯虚析构需要有声明,也需要实现
{
	
}

实例分析

class Animal
{
public:
	string* name;
	Animal(string name)
	{
		cout << "Animal created!" <<endl;
		this->name = new string(name);
	}
	virtual ~Animal() //虚析构
	{
		cout << "Animal deleted!" << endl;
		if (name != NULL)
		{
			delete(name);
			name = NULL;
		}
	}
};
class Cat :public Animal
{
public:
	Cat(string name):Animal(name)
	{
		cout << "Cat created!" << endl;
		this->name = new string(name);
	}
	~Cat()
	{
		cout << "Cat deleted!" << endl;
		if (name != NULL)
		{
			delete(name);
			name = NULL;
		}
	}
};
void doSpeak(Animal* a)
{
	cout << *a->name << " is speaking!" << endl;
	delete(a);
}
int main()
{
	doSpeak(new Cat("Tom"));
	return 0;
}
class Animal
{
public:
	string* name;
	Animal(string name)
	{
		cout << "Animal created!" <<endl;
		this->name = new string(name);
	}
	virtual ~Animal() = 0;
};
Animal::~Animal() //纯虚析构
{
	cout << "Animal deleted!" << endl;
	if (name != NULL)
	{
		delete(name);
		name = NULL;
	}
}

总结

如果子类中没有堆区数据,可以不写虚析构或纯虚析构

posted @   安河桥北i  阅读(53)  评论(0编辑  收藏  举报
编辑推荐:
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
点击右上角即可分享
微信分享提示