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.继承方式
一共有三种继承方式:
- 公共继承
- 保护继承
- 私有继承
父类中私有的成员只是被隐藏了,但还是会继承下去
公共继承
除父类中私有成员外,其他所有成员将会被显式继承,其访问权限保持不变
保护继承
除父类中私有成员外,其他所有成员将会被显式继承,其访问权限变为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.菱形继承(钻石继承)
两个子类继承同一个父类,又有某个类同时继承两个子类,这种继承被称为菱形继承(钻石继承)
问题
- 羊继承动物的数据,驼同样继承了动物的数据,当草泥马使用数据时,会产生二义性
- 菱形继承导致数据有两份,资源浪费
解决方案: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.动态多态的满足条件
- 有继承关系
- 子类重写父类的虚函数
3.动态多态的使用
父类的指针或引用 执行子类的对象
4.多态的原理剖析
vfptr:虚函数(表)指针(virtual function pointer)
vftable:虚函数表(virtual function table)
5.多态的好处
- 组织结构清晰
- 可读性强
- 前期和后期扩展及维护性高
6.纯虚函数和抽象类
在多态中,通常父类中虚函数的实现是毫无意义的,主要是调用子类重写的内容
因此,可以将虚函数改为纯虚函数
语法
virtual 返回值类型 函数名 (参数列表) = 0;
当类中有了纯虚函数,此类也叫抽象类
抽象类特点:
- 无法实例化对象
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
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;
}
}
总结
如果子类中没有堆区数据,可以不写虚析构或纯虚析构
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 基于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)