4、类和对象
约 6026 字大约 20 分钟
2025-06-22
面向对象的三大特性:封装、继承、多态
意义
将属性和行为作为一个整体 将属性和行为加以权限控制
实例化
通过一个类创建一个对象的过程
访问限制
一般一个类的属性放到private中,它的方法放到public中
public公共权限
公开的,任何人都可以访问
friend友元
写在public中,friend的授权是在编译的时候检查的
friend可以声明别人(可以是别的类,别的函数,别的人的某个函数)是class的朋友,这样别人就能访问class的private了
在运算符重载的时候用的比较多某些运算符的重载需要friend做授权
全局函数做友元
friend 函数声明
friend void visit(int a,int b);类做友元
friend 类声明
friend class name;成员函数做友元
friend 类函数声明
friend void Godgay::visit(int a,int b);protected保护权限
只有这个类自己以及它的子孙可以访问
private私有权限
私有的,只有这个类的成员函数才可访问这些成员变量或者成员函数
class与struct
c++中结构体跟class很相近,也能在结构体中定义函数 struct包含于class
在数据极其简单的情况下可以使用struct,但绝大多数情况class更方便
区别:
class默认权限是private struct默认权限是public(struct只有public)
初始化列表
定义函数和参数后加:传参
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;
}
int m_a;
int m_b;
int m_c;
};
Person(1,2,3);初始化顺序是声明的顺序,销毁的顺序是相反的顺序
好处是可以初始化任何类型的数据 这样的初始化会早于构造函数被执行
Student::Student(string s):name(s) {}
Student::Student(string s){name=s;}第一个是初始化 第二个是先初始化再赋值
初始化效率比赋值快
继承
语法
子类:派生类 父类:基类,超类 class 子类:继承方式 父类
class A{}
class B : public A{}父类继承过来的是共性,自己新增的是个性
具有父类的private属性但是无法访问private 父类中所有非静态成员属性都会被子类继承下去 父类中私有成员属性 是被编译器给隐藏了,因此是访问不到,但是确实被继承下去了
继承方式
不管哪种继承子类都无法访问到父类的private属性
//父类
class A{
public:
int a;
protected:
int b;
private:
int c;
};公共继承
父类中的公共权限成员到子类中依然是公共权限,保护权限成员到子类中依然是保护权限
class B:public A{
public:
int a;
protected:
int b;
不可访问:
int c;
};保护继承
父类中公共成员、保护成员到子类中变为保护权限
class C:protected A{
protected:
int a;
int b;
不可访问:
int c;
};私有继承
父类中公共成员、保护成员到子类中都变为私有成员
class D:private A{
private:
int a;
int b;
不可访问:
int c;
};继承的构造和析构
先构造父类再构造子类,先析构子类再析构父类
名字隐藏
当子类和父类中出现了重复的函数(名字相同,参数表一样)就会屏蔽父类中的所有函数,只存在子类自己的函数(只有c++这么做)
子类出现和父类同名静态成员函数,也会隐藏父类中所有同名成员函数,如果想访问父类中被隐藏同名成员,需要加作用域
访问子类同名成员 直接访问即可 访问父类同名成员 需要加作用域 访问静态成员时也可以使用调用静态成员的两种方法
class A{
public:
int a;
static int b;
void func(){cout <<"类A的函数func()调用"<<endl;}
void func(int a){cout <<"类A函数func()的重载函数调用"<<endl;}
void f(){cout <<"类A的函数f()调用"<<endl;}
static void g(){cout <<"类A的静态函数g()调用"<<endl;}
};
int A::b=6;
class B:public A{
public:
int a;
static int b;
void func(){cout <<"类B的函数func()调用"<<endl;}
static void g(){cout <<"类B的静态函数g()调用"<<endl;}
};
int B::b=7;
int main(){
B b;
b.func();//直接调用,调用的是子类的同名成员
b.A::func();//通过子类再声明作用域调用父类同名函数
//b.func(10);//如果子类中出现和父类同名的成员函数,子类同名成员会隐藏掉父类中所有同名函数的成员(包括重载函数)
b.A::func(10);//r如果想访问父类中被隐藏的同名成员函数,需要加作用域
b.f();//不同名函数不会隐藏
//通过对象访问
cout<<b.b<<endl;
cout<<b.A::b<<endl;
//通过类名访问
cout<<B::b<<endl;
//此方法是直接通过A访问了A的属性
cout<<A::b<<endl;
//第一个::代表通过类名方式访问 第二个::代表访问父类作用域下
cout<<B::A::b<<endl;
//访问静态函数
//通过对象访问
b.g();
b.A::g();
//通过类名访问
B::g();
B::A::g();
return 0;
}多继承
C++允许一个类继承多个类
语法
class 子类 :继承方式 父类1, 继承方式 父类2... 多继承可能会引发父类中有同名成员出现,需要加作用域区分
class A{
public:
int a=1;
int b=3;
};
class B{
public:
int a=2;
int b=4;
};
class C:public A,public B{
int c;
int d;
};
int main(){
C c1;
cout<<sizeof(c1)<<endl;
//当父类中出现同名成员,需要加作用域区分
cout <<c1.A::a<<endl;
cout <<c1.B::a<<endl;
return 0;
}菱形继承
两个派生类继承同一个基类 又有某个类同时继承者两个派生类
A(动物)
/ \
/ \
B(羊) C(骆驼)
\ /
\ /
D(羊驼)因关系图呈现为菱形故这种继承被称为菱形继承,或者钻石继承
菱形继承问题: 1.B继承了A的数据,C同样继承了A的数据,当D使用数据时,就会产生二义性. 2.D继承自A的数据继承了两份,其实我们应该清楚,这份数据我们只需要一份就可以。
虚继承与虚函数
虚继承解决菱形继承底层继承的不是数据而是两个指针,两个指针会通过偏移量找到唯一的数据
继承之前加上关键字 virtual 变为虚继承 类中的函数加上virtual后变为虚函数 被虚继承的父类称为虚基类
virtual关键字用在class里面
具有virtual的class正常的所占内存大 virtual的内存开头都有一个隐藏的vbptr指针,指向VTable这张表 vtable里面是它所有virtual函数的地址
virtual base pointer虚基指针
如果将来子类的里面重新写了virtual声明的函数那么子类里面那个函数就和这个函数是有联系的,子类和父类的函数才有联系
析构也是virtual
在父类里用virtual声明的函数在子类里同样的函数可加可不加virtual,不加virtual这个函数也是virtual的,它的后代也是virtual的。只要在它的继承树中有一个是virtual,以后它的子子孙孙都是virtual
virtual作用:通过指针或引用调用这个函数的时候,不能直接写进来调到到那个函数,不能确定这个函数是什么类型,只能运行的时候才确定
虚函数表
多态
优点
代码组织结构清晰 可读性强 利于前期和后期的扩展和维护
提倡开闭原则:对扩展进行开放,对修改进行关闭
分类
编译期多态
函数重载就是编译期多态,函数在调用前已经知道传入的参数是重载函数中的其中一个类型
运行期多态
函数调用前编译器并不知道传入的参数是什么类型 本次学习的多态就是运行期多态
静态多态
函数重载和运算符重载属于静态多态,复用函数名
动态多态
派生类和虚函数实现运行时多态
静态多态的函数地址早绑定,编译阶段确定函数地址 动态多态的函数地址晚绑定,运行阶段确定函数地址
C++为了效率默认静态绑定,其他oop语言都是动态绑定
C++中允许父子的类型转换,不需要做强制类型转换父类的引用或指针 就可以直接指向子类对象
class Animal{
public:
void speak(){
cout<<"Animal is speak"<<endl;
}
};
class Cat:public Animal{
public:
void speak(){
cout <<"Cat is speak"<<endl;
}
};
void dospeak(Animal &animal){
animal.speak();
}
int main(){
Cat cat;
dospeak(cat);//Animal
return 0;
}把子类传入到使用父类的全局函数中调用的是父类的属性,原因是地址早绑定,在编译阶段就确定了函数地址
如果想执行子类的属性就需要让这个函数地址不能提前绑定,需要在运行阶段绑定
在父类的成员函数中加virtual关键字 虚函数
class Animal{
public:
virtual void speak(){
cout<<"Animal is speak"<<endl;
}
};动态多态满足条件
1、有继承关系 2、子类重写父类的虚函数
override覆盖(重写)
重写是函数返回值、函数名、形参列表相同
动态多态的使用
父类的指针或引用指向子类对象
多态原理
加了virtual关键字的类比正常的类大,多出来的内存存的是一个vfptr(virtual function pointer)指针,vfptr指向一个vftable表,存放虚函数表表的内部记录虚函数的地址
子类重写父类的虚函数时子类中的虚函数表内部会替换成子类的虚函数地址 当父类的引用或指针指向子类对象时发生多态
虚基指针和虚函数指针统称虚指针
父类的函数前的函数前加了virtual关键字后子类重写的函数可加可不加virtual 在一个大的继承树中一旦有一个类的函数加了virtual,其后代的该函数都是虚函数
纯虚函数和抽象类
堕胎中,通常父类中的虚函数的实现是无意义的,主要都是调用子类重写的内容
因此可以把虚函数改为纯虚函数
语法:
virtual 返回值类型 函数名(参数列表)=0; =0是纯说明符,不可以是其他的 当类中有了纯虚函数,这个类也称为抽象类
抽象类特点:
无法实例化对象 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
虚析构和纯虚析构
问题
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码
解决方法
将父类中的析构函数改成虚析构或纯虚析构
虚析构和纯虚析构共性
可以解决父类指针释放子类对象 都需要有具体的函数实现
区别
如果是纯虚析构,该类属于抽象类无法实例化对象
纯虚析构既要有声明又要有实现,因为父类有可能一些属性开辟到堆区 类内声明,类外实现
class Animal{
public:
Animal(){
cout<<"Animal 构造函数调用"<<endl;
}
virtual void speak()=0;
~Animal(){
cout<<"Animal 析构函数调用"<<endl;
}
};
class Cat:public Animal{
public:
string *m_name;
Cat(string name){
cout<<"Cat 构造函数调用"<<endl;
m_name=new string (name);
}
virtual void speak(){
cout<<*m_name<<" Cat is speaking"<<endl;
}
~Cat(){
cout<<"Cat 析构函数调用"<<endl;
if (m_name!=NULL){
delete m_name;
m_name=NULL;
}
}
};
void test1(){
Animal*animal=new Cat("tom");
animal->speak();
//父类指针在析构时不会调用子类中析构函数,如果子类中有堆区的对象会导致内存泄漏
delete animal;
animal=NULL;
}
int main(){
test1();
return 0;
}
----------------结果----------------
Animal 构造函数调用
Cat构造函数调用
tom Cat is speaking
Animal 析构函数调用没有Cat的析构函数调用 堆区内存没有释放干净,导致内存泄漏
virtual ~Animal(){
cout<<"Animal 析构函数调用"<<endl;
}
------------结果------------
Animal 构造函数调用
Cat 构造函数调用
tom Cat is speaking
Cat 析构函数调用
Animal 析构函数调用利用虚析构解决问题
纯虚析构解决
class Animal{
public:
Animal(){
cout<<"Animal 构造函数调用"<<endl;
}
virtual void speak()=0;
// virtual ~Animal(){
// cout<<"Animal 析构函数调用"<<endl;
// }
//纯虚析构
virtual ~Animal()=0;
};
Animal::~Animal(){
cout<<"Animal 纯虚析构调用"<<endl;
}向上造型upcast
虽然都是cast但是意义不一样
cast本是转换(类型转换)但这种思想被称为造型
类型转换更改了数据,造型没有更改数据只是把这个数据当成另一个数据用
子类可以当做父类看待,他们数据结构都是一样的,自动忽视子类多出来的数据
把父类当成子类看待是downcast
upcast一定是安全的,downcast可能有风险,有可能有些事父类刚好能做也有可能不能做
class XYPos{...};// x,y point
class Shape
{
public:
Shape();
virtual~Shape();
virtual void render);
void move(const XYPos&);
virtual void resize();
protected:
XYPos center;
};默认参数
C++中函数的形参列表可以有默认值 有默认值的形参后面的形参也必须有默认值 如果函数声明有默认参数,函数实现就不能有默认参数(声明和实现只能有一个有默认参数)
占位参数
形参列表中只写数据类型不写形参名占位参数用来占位,调用函数时必须填补该位置 函数重载能用到 可以有默认值
void func(int a,int){
}
void func(int a,int=10){
}函数重载function overload
在同一作用域下,函数名称相同,函数的形参列表不同(类型,个数,顺序)即可构成函数重载(const、引用也可以作为函数重载的条件) 调用函数的时候传入不同的参数就决定了编译器会选择哪一个函数 函数的返回值不可作为函数重载的条件
void func() {
cout << "func()调用" << endl;
}
void func(int a) {
cout << "func(int a)调用" << endl;
}
int main() {
func();
func(2);
return 0;
}const加在后面可以与非const构成overload关系
void f() { cout << "f()" << endl; }
void f() const { cout . << "f() const" << endl; }参数表不同
函数重载遇到默认参数
会出现二义性(歧义)报错,尽量避免这种情况
void func(int a,int b=10) {
cout << "func()调用" << endl;
}
void func(int a) {
cout << "func(int a)调用" << endl;
}
int main() {
func(2);//error
return 0;
}成员指针
成员指针分为成员函数指针和数据成员指针 数据成员指针和虚函数成员指针并没有真正的指向一个地址,它表示的是在当前类中那个字段的位置。类似于偏移量 成员函数指针则是真正存储了一个地址
const修饰的对象只能调用const修饰的函数 其const修饰的函数是这样的
void func()const;类对象作为类的成员
C++中的成员可以是另一个类的对象,我们称该成员为对象成员 当其他类的对象作为本类的成员的时候会先调用其他类的构造,后调用本类的构造
class A{}
class B{
A a;
}静态成员
在成员变量和成员函数前加关键字static,称为静态成员
静态成员变量
所有对象共享同一份数据
另外一个变量把它改了所有这个属性的变量也都发生更改
在编译阶段分配内存
类内声明,类外初始化(必须要的操作)
class Person{
public:
static int m_a;//类内声明
private:
static int m_b;
}
int Person::m_a=100;//类外初始化
//共享数据
Person p;
cout<<p.m-a<<endl;//100
Person p2;
p2.m_a=200;
cout<<p.m_a<<endl;//200
//通过类名访问
cout<<Person::m_a<<endl;
cout<< Person::m_b<<endl;//error可以直接通过类名进行访问
静态成员变量也是有访问权限的,类外访问不到私有的静态成员变量
静态成员函数
所有对象共享同一个函数
静态成员函数只能访问静态成员变量
无法区分到底是哪个对象的非静态成员变量
也可以通过对象访问和通过类名访问
静态成员函数也有访问权限
class Person{
public:
static void func(){
m_a=100;//静态成员函数可以访问静态成员变量
m_b=200;//error 静态函数不能访问非静态的成员变量
}
static int m_a;
int m_b;
private:
}
int Person::m_a;
//通过对象访问
Person p;
p.func();
//通过类名访问
Person::func;成员变量和成员函数分开存储
空对象占用的内存空间为1 编译器会给每个空对象也分配一个字节空间,是为了区分空对象占内存的位置 每个对象都有独一无二的内存地址
非静态成员变量属于类对象上 静态成员变量不属于类对象上 非静态成员函数不属于类对象上 静态成员函数不属于类对象上
this指针
相当于 classname * const this;
指向本类的非静态成员变量
使用场景 形参和成员变量同名时,可以用this指针区分 在类的非静态成员函数中返回对象本身,可使用return * this
this指针本质上是一个指针常量,指向不可修改,但指向的对象可以修改 常量(的)指针指向可以修改,指向的对象不可修改
空指针访问成员函数
C++中允许空指针调用成员函数,但要注意有没有用到this指针
class Person{
public:
int m_age=18;
void show_name(){
cout<<"name is showed"<<endl;
}
void show_age(){
cout<<"age is"<<m_age<<endl;
}
};
int main(){
Person*p=NULL;
p->show_name(); //ok
p->show_age(); //error
}class中的属性都默认加了this指针,即 this->m_age 使用空指针调用类函数并没有创建对象并没有实体因此没有指向确切的数据,有的ide会报错有的不会输出m_age
const与mutable修饰成员
常(成员)函数
成员函数后加const就是常函数 常函数不可修改成员属性 成员属性声明时加关键字mutable后在常函数中依旧可以修改
在成员函数后面加const修饰的是this的指向让this指向的值不可修改,相当于把原来classname * const this;改为const classname * const this;
class Person{
public:
int m_a;
mutable int m_b;
void show() const
{
this->m_a=100;//error
this->m_b=100;//ok
}
void func(){}
};
int main(){
const Person p;
p.m_a=100;//error
p.m_b=100;//ok
p.func();//error
p.show();//ok
}常(成员)对象
声明对象前加const就是常对象 常对象只能调用常函数 mutable修饰的变量在常对象下也能修改
都是由编译器自动调用的 构造函数和析构函数不需要定义返回值,是跟类名同名的函数,析构函数前面加~ 构造函数和析构函数不能私有 构造和析构都是必须有的实现,如果我们自己不提供,编译器会提供一个空实现的构造和析构
构造函数
使用类构建的时候自动调用该函数 主要用在创建对象时为对象的成员属性赋值 可以有参数,因此可以构成函数重载
分类
按参数分:有参构造、无参构造(默认构造)
Person(){}//无参构造
Person(int a){}//有参构造按类型分:普通构造、拷贝构造
拷贝构造
把传入函数的所有属性拷贝到本函数身上
Person(int a) {
cout << "构造函数调用" << endl;
}
Person(const Person& p) {
age=p.age;
}
Person p1(20);
Person p2(p1);//拷贝构造使用场景
1、使用一个已经创建完毕的对象初始化一个新对象
class Person
{
public:
Person() {
cout << "无参构造构造函数调用" << endl;
}
Person(int age) {
cout << "有参构造函数调用" << endl;
m_age = age;
}
Person(const Person& p) {
cout << "拷贝构造函数调用" << endl;
m_age = p.m_age;
}
~Person() {
cout << "默认析构函数调用" << endl;
}
int m_age;
private:
};
void test1() {
Person p1(20);
Person p2(p1);
cout << p2.m_age << endl;
}2、值传递的方式给函数参数传值
void dowork(Person &p) {
}
void test2() {
Person p;
dowork(p);
}3、以值方式返回局部对象
Person dowork2() {
Person p1;
return p1;
}
void test3() {
Person p = dowork2();
}浅拷贝
简单的赋值拷贝操作 编译器默认提供的拷贝构造函数就是浅拷贝
class Person{
public:
int *m_age;
Person(int age){
m_age=new int(age);
}
~Person(){
if(m_age!=NULL){
delete m_age;
m_age=NULL;
}
}
};
int main(){
Person p1(18);
Person p2(20);
p2=p1;
cout<<*p1.m_age<<endl;
cout<<*p2.m_age<<endl;
return 0;
}会造成堆区的内存重复释放 在拷贝时会把数据不动的拷贝过来(只拷贝了指针,没有拷贝指针指向地址上的数据),当析构函数释放内存时第一个变量的析构函数释放了指针指向的内存,第二个变量的指针也指向同样的内存,当第二个变量的析构函数释放指针指向的内存时该内存已经释放过了
浅拷贝的问题要利用深拷贝解决
深拷贝
在堆区重新申请空间,进行拷贝操作 拷贝指针的同时也把指针指向的数据拷贝了放到一个新的地址
class Person
{
public:
Person() {
cout << "无参构造构造函数调用" << endl;
}
Person(int age,int hight) {
m_age = age;
m_hight = new int(hight);
cout << "有参构造函数调用" << endl;
}
Person(const Person &p) {
m_age = p.m_age;
//浅拷贝
m_hight = p.m_hight;//编译器默认实现的代码
//深拷贝
m_hight = new int(*p.m_hight);
cout << "拷贝构造函数调用" << endl;
}
~Person() {
if (m_hight != NULL) {
delete m_hight;
m_hight = NULL;//防止野指针出现
}
cout << "默认析构函数调用" << endl;
}
int m_age;
int* m_hight;
private:
};
void test1() {
Person p1(18, 170);
cout << "p1的年龄和身高为:" << p1.m_age<<*p1.m_hight<< endl;
Person p2(p1);
cout << "p2的年龄和身高为:" << p2.m_age<<*p2.m_hight<< endl;
}调用方法
括号法
Person p1;
Person p2(10);
Person p3(p2);
Person p12();//error调用默认构造函数时候,不要加(),编译器会认为是一个函数的声明,不会认为在创建对象
显示法
Person p1;
Person p2 = Person(10);
Person p3 = Person(p2);
Person(10); // 匿名对象 特点: 当前行执行结束后,系统会立即回收掉匿名对象
Person(p3);//error不要利用拷贝构造函数 初始化匿名对象 编译器会认为 Person (p3) == Person p3:对像
隐式转换法
Person p4 = 10://相当于写了 Person p4 = Person(10);
Person p5 = p4;调用规则
默认情况下C++编译器至少给一个类添加三个函数 1、默认构造函数 2、默认析构函数 3、默认拷贝函数,对属性值进行值拷贝
如果用户定义有参构造函数,C++不再提供默认无参构造函数,但会提供默认拷贝构造函数 如果用户定义拷贝构造函数,C++不会再提供其他构造函数
析构函数
类销毁时自动调用该函数 主要作用于一些清理工作 没有参数,因此无法构成函数重载 通常将开辟的堆区数据做释放操作
贡献者
版权所有
版权归属:wynnsimon
