C++类与对象

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

一、封装

1、语法

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

class Person{
public:
	string name;
	int age;
};

2、三种权限

(1)公共权限public:类内可访问,类外也可以
(2)保护权限protected:类内可访问,类外不可以,子可以访问父的保护内容
(3)私有权限private:类内可访问,类外不可以,子不可以访问父的私有内容

3、struct与class的区别

struct的默认权限为public,class默认权限为private

4、成员属性设置为私有

优点:可以自己控制读写权限,对于写权限可以检测数据的有效性

class Person{
public:
	//写入name
	void setName(string name){
		m_Name=name;
	}
	//获取name
	void getName(){
		return m_Name;
	}
	//检测数据的有效性
	void setAge(int age){
		if(age<0||age>150){
			cout<<"error!"<<endl;
		}
		else{
			m_Age=age;
		}
private:
	string m_Name;//可读可写
	int m_Age;//只写
};
int main(){
	Person p;
	p.setName("Amy");
	p.setAge(10);
	cout<<"name:"<<p.getName()<<endl;
	return 0;
}

5、函数中引用类

建议使用引用传递

bool cmp(Cube &c1,Cube &c2){}

6、类的封装性和信息隐蔽

(1)公用接口与私有实现的分离

当接口与实现分离时,只要类的接口没有改变,对私有实现的修改不会引起程序的其他部分的修改。

(2)类声明和成员函数定义的分离

一个C++程序是由三个部分组成:类声明头文件(.h),类实现文件(.cpp,包含类的定义),类的使用文件(.cpp,即主文件)

  • 类声明头文件

    #include<string>
    using namespace std;
    class Student
    {
    public:
        void display();
    private:
        int num;
        string name;
        char sex;
    }
    
  • 类实现文件

    #include<iostream>
    #include"student.h"
    void Student::diaplay()
    {
        cout<<"num:"<<num<<endl;
    }
    
  • 主文件

    #include<iostream>
    #include"student.h"
    using namespace std;
    int main()
    {
        Student stu;
        stu.diaplay();
        return 0;
    }
    

ps:可以认为类声明头文件时用户使用类库的公用接口

二、对象特性

1、析构函数

用于对对象的清理
(1)语法:~类名(){}
注意:
a.不可以有参数,不可发生重载
b.程序在对象销毁前会自动调用析构,无需手动调用,且只会调用一次

调用析构函数的具体情况:

  • 如果在一个函数中定义了一个对象(假设是自动局部对象),当这个函数被调用结束时,对象应该释放,在对象释放前自动执行析构函数
  • 静态(static)局部对象在函数调用结束时对象并不释放,因此也不调用析构函数,只有main函数结束或调用exit函数结束程序时,才调用static局部对象的析构函数
  • 如果定义了一个全局对象,则在程序的流程离开其作用域时(如main函数结束或调用exit函数时),调用该全局对象的析构函数
  • 如果new运算符动态地建立了一个对象,当用delete运算符释放该对象时,先调用该对象的析构函数
class Person(){
public:
	~Person(){
		cout<<"析构函数"<<endl;
	}
}	

注意:如果直接调用,运行结果不会显示“析构函数”
但如果通过函数调用运行结果会输出“析构函数”
因为当函数执行完后会释放对象,所以会调用析构函数
而直接调用在代码执行完毕后才释放对象

如果用户没有定义析构函数,C++编译系统会自动生成一个析构函数,但它徒有析构函数的名称和形式,实际上什么操作都不执行。

2、构造函数

用于对对象的初始化

(1)语法

类名(){}
注意:
a.可以有参数,可发生重载,可以使用默认参数
b.程序在调用对象时会自动调用构造,无需手动调用,且只会调用一次

c.无返回值,作用只是对对象进行初始化,因此不需要在定义构造函数时声明类型

(2)分类

按有无参数:无参构造和有参构造
按类型:普通构造和拷贝构造

(3)调用方法

a.括号法

class Person{
public:
	Person(){
		cout<<"无参拷贝构造"<<endl;
	}
	Person(int a){
		cout<<"有参拷贝构造"<<endl;
	}
	Person(const Person &p){
		cout<<"拷贝构造"<<endl;
	}
};
int main(){
	Person p1;
	Person p2(10);
	Person p3(p2);
	return 0;
}

b.显示法

Person p1;
Person p2=Person(10);
Person p3=Person(p2);

注:其中Person(10)是一个匿名对象,当前执行结束后系统会立即回收匿名对象。不要利用拷贝构造初始化匿名对象。

c.隐式转换法

Person p1;
Person p2=10;
Person p3=p2;

(4)调用规则

默认情况下,C++编译器至少会给一个类添加3个函数:
a.默认构造(无参空实现)
b.默认析构(无参空实现)
c.默认拷贝构造 对属性进行值拷贝
如果用户有定义有参构造函数,C++不再提供默认无参构造,但会提供默认拷贝构造
如果用户有定义拷贝构造,C++不会再提供其他构造函数

(5)拷贝构造

class Person{
public:
    int myage1,myage2;
    Person(int age){
        myage1=age;
    }
    Person(const Person &p){
        myage2=p.myage1;
    }
};
int main(){
    Person p1(20);
    Person p2(p1);
    cout<<p2.myage2<<endl;
    return 0;
}

拷贝函数的调用时机:
a.使用一个已创建的对象来初始化一个新对象时
b.通过值传递的方式给函数参数传值时

void test1(){
	Person p3;
	test2(p3);//在调用参数时也会调用拷贝构造函数
}

c.通过值传递方式返回局部对象时

class Person{
public:
    int myage1,myage2;
    Person(int age){
        myage1=age;
    }
    Person(const Person &p){
        myage2=p.myage1;
    }
};
Person dowork(){
	Person p3;
	return p3;//此处会调用拷贝构造
}
void test3(){
	Person p4=dowork();
}
int main(){
	test3;
	return 0;
}

深拷贝与浅拷贝:
浅拷贝:简单的拷贝操作
深拷贝:在堆区重新申请空间进行拷贝操作
注:如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的问题

(6)初始化列表

类并不是一个实体,而是一种抽象类型,并不占用存储空间,显然无处容纳数据,不能在类声明中对数据成员初始化

如果一个类中所有成员都是公用的,则可以在定义对象时对数据成员进行初始化。但这一方法对类中有private和protected的成员不适用。

Person p1={0,0,0};

用构造函数初始化列表

class Person{
public:
	Person(int a,int b,int c){
		A=a;
		B=b;
		C=c;
	}
};
void test1(){
	Person p(10,20,30);
}

在构造函数中用参数初始化列表对数据成员初始化

class Person{
public:
	int A,B,C;
	Person(int a,int b,int c):A(a),B(b),C(c){
	}
};
void test1(){
	Person p(10,20,30);
}

也可以用一个类对象初始化另一个类对象

Person p1;
Person p2=p1;

(7)类对象作为类成员

当其他类对象作为本类成员,构造时先构造类对象,再构造自身,析构顺序相反

先构造的后析构,后构造的先析构

class A{};
class B{
public:
	A a;
};

3、静态成员

(1)静态成员变量

class Person{
public:
    static int A;//类内声明
};
int Person::A=100;//类外初始化
int main(){
    Person p;
    cout<<p.A<<endl;//通过对象进行访问
    cout<<Person::A<<endl;//通过类名进行访问
    return 0;
}

特点:
a.所有对象共享同一份数据

    Person p1;
    p1.A=200;//通过p1将上述数据改为200
    Person p2;
    cout<<p2.A<<endl;//通过p2来访问,输出结果为200

b.在编译阶段就分配内存,内存分配在全局区
c.类内声明,类外初始化
d.静态成员变量也有访问权限,访问权限为private时也不可在类外访问

(2)静态成员函数

class Person{
public:
    static int A;
    static void func(){
        cout<<Person::A<<endl;
    }//静态成员函数
};
int Person::A=100;
int main(){
    Person p;
    p.func();//通过对象进行访问
    Person::func();//通过类名进行访问
    return 0;
}

特点:
a.所有成员共享同一个函数

    Person p1;
    p1.func();
    Person p2;
    p2.func();

其中p1,p2访问的是同一个函数
b.只能访问静态成员变量,若用普通变量,则会无法判别应该用哪个对象对应的变量
c.静态成员函数也有访问权限

4、C++对象模型和this指针

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

空对象占用内存空间为1个字节:C++编译器会给每个空对象分配一个字节空间,是为了区分空对象占内存的位置。每个空对象也应该有一个独一无二的内存地址

class Person{
public:
    int m_A;//非静态成员变量占用对象空间
    static int m_B;//静态成员变量不占用对象空间
    void show1(){
        cout<<"非静态成员函数"<<endl;
    }//非静态成员函数不占用对象空间
    static void show2(){
        cout<<"静态成员函数"<<endl;
    }//静态成员变量不占用对象空间
};
int Person::m_B=100;
int main(){
    Person p;
    cout<<"对象占用空间为:"<<sizeof(p)<<endl;//输出结果为4
    return 0;
}

(2)对象指针

1.指向对象的指针

一个对象存储空间的起始地址就是对象的指针。

可以定义一个指针变量,用来存放对象的地址,这就是指向对象的指针变量。

Student *stu;
(*stu).display();
stu->display();
2.指向对象成员的指针
  • 指向对象数据成员的指针

    int *p=&stu.num;
    cout<<*p<<endl;
    
  • 指向成员函数的指针

    指向普通函数的指针:

    *类型名(指针变量名)(参数表列);

    void (*p)();//p是指向void型函数的指针变量
    p=func;//将func函数的入口地址赋给指针变量p,p就指向了func
    (*p)();//调用func函数
    

    指向对象成员函数的指针:

    *类型名(类名::指针变量名)(参数表列);

    Student stu;
    void(Student::*p)();
    p=&Student::display;//成员函数的入口地址写法:&类名::成员函数名
    (stu.*p)();
    
3.指向当前对象的this指针

this指针指向被调用的成员函数所属的对象
this指针是隐含在每一个非静态成员函数内部的一种指针,不需定义,直接使用即可
用途:
a.当形参与成员变量同名时,可用this指针区分

class Person{
public:
    Person(int age){
    	//age=age 无法输出正确结果
        this->age=age;
    }
    int age;
};
int main(){
    Person p(18);
    cout<<"age="<<p.age<<endl;
    return 0;
}

形参与成员变量不同名的情况:

class Person{
public:
    Person(int age){
        m_age=age;//其实也相当于this->m_age=age;
    }
    int m_age;
};
int main(){
    Person p(18);
    cout<<"age="<<p.age<<endl;
    return 0;
}

b.返回对象本身用*this

class Person{
public:
    Person showage(Person &p){
        this->m_age+=p.m_age;
        return *this;//返回对象本身
    }
    int m_age;
};
int main(){
    Person p1;
    p1.m_age=10;
    Person p2;
    p2.m_age=10;
    p2.showage(p1);
    cout<<"p2.m_age="<<p2.m_age<<endl;
    return 0;
}

(3)空指针访问成员函数

C++中空指针是可以调用成员函数的,但是也要注意函数中有没有用到this指针

class Person{
public:
    void showclassname(){
        cout<<"this is Person class"<<endl;
    }
    void showage(){
        cout<<"age="<<m_age<<endl;//m_age相当于this->m_age
    }
    int m_age;
};
int main(){
    Person *p=NULL;
    p->showclassname();//能运行
    p->showage();//报错
    return 0;
}

如果遇到this指针,要加以判断:

class Person{
public:
    void showclassname(){
        cout<<"this is Person class"<<endl;
    }
    void showage(){
        if(this==NULL){
            return;
        }//如果遇到空指针则不运行下方程序
        cout<<"age="<<m_age<<endl;//m_age相当于this->m_age
    }
    int m_age;
};
int main(){
    Person *p=NULL;
    p->showclassname();
    p->showage();
    return 0;
}

(4)const修饰成员函数

a.常函数
成员函数后加const
常函数内不能修改成员属性,但如果成员属性声明时加了mutable关键字,则这个成员属性在常函数中可以修改

class Person{
public:
    //this指针的本质:Person *const this;指向不可修改,但指向的值可修改
    //在成员函数后面加const修饰的是this的指向,让指针指向的值也不能改变
    void show() const{
        this->m_a=100;//报错
        this->m_b=100;//可修改
    }
    int m_a;
    mutable int m_b;
};

b.常对象
常对象不能修改成员属性,但如果成员属性声明时加了mutable关键字,则这个成员属性可以修改

const Person p;
p.m_a=100;//报错
p.m_b=100;//可修改

常对象只能调用常函数,因为普通成员函数可以修改成员属性

5、内置成员函数(inline成员函数)

为了减少时间开销,如果在类中定义的成员函数中不包括循环等控制结构,C++系统会自动地将它们作为内置(inline)函数来处理。

无论是否有inline声明,成员函数的代码都不占用对象的存储空间。inline函数只影响程序执行的效率。

6、对象数组

#include<iostream>
#include<string>
using namespace std;

class Student
{
public:
	Student(int a,string b);
private:
	int num;
	string name;
};

Student::Student(int a,string b)
{
	num=a;
	name=b;
}

int main()
{
	//定义对象数组
	Student stu[3]={
		Student(1,"John"),   //调用构造函数,提供第一个元素的形参
		Student(2,"Amy"),
		Student(3,"Nancy")
	};
	return 0;
}

三、友元

关键字:friend

作用:让一个函数或者类访问另一个类中的私有成员

1、全局函数做友元

class Building{
    //告诉编译器该全局函数是Building类的友元
    friend void goodfriend(Building &b);
public:
    string m_livingroom;
    Building(){
        m_livingroom="客厅";
        m_bedroom="卧室";
    }
private:
    string m_bedroom;
};
void goodfriend(Building &b){
    cout<<"正在参观"<<b.m_livingroom<<endl;
    cout<<"正在参观"<<b.m_bedroom<<endl;
}
int main(){
    Building b1;
    goodfriend(b1);
    return 0;
}

2、类做友元

class Building{
    //告诉编译器该类为Building类的友元
    friend class Goodfriend;
public:
    string m_livingroom;
    Building(){
        m_livingroom="客厅";
        m_bedroom="卧室";
    }
private:
    string m_bedroom;
};

class Goodfriend{
public:
    Goodfriend();
    void visit();
private:
    Building *building;
};
//在类外写成员函数
Goodfriend::Goodfriend(){
    //创建对象
    building=new Building;
}//该函数运行时Building类的构造函数也会运行
void Goodfriend::visit(){
    cout<<"正在参观"<<building->m_livingroom<<endl;
    cout<<"正在参观"<<building->m_bedroom<<endl;
}
int main(){
    Goodfriend g;
    g.visit();
    return 0;
}

3、成员函数做友元

class Building;//由于Goodfriend类中有用到Building类,所以需要先声明
class Goodfriend{
public:
    Goodfriend();
    void visit();
private:
    Building *building;
};
class Building{
    //告诉编译器该成员函数为Building类的友元
    friend void Goodfriend::visit();
public:
    string m_livingroom;
    Building(){
        m_livingroom="客厅";
        m_bedroom="卧室";
    }
private:
    string m_bedroom;
};
//在类外写成员函数
//由于函数中含有Building类的东西,所以需要写在Building类定义之后
Goodfriend::Goodfriend(){
    //创建对象
    building=new Building;
}//该函数运行时Building类的构造函数也会运行
void Goodfriend::visit(){
    cout<<"正在参观"<<building->m_livingroom<<endl;
    cout<<"正在参观"<<building->m_bedroom<<endl;
}
int main(){
    Goodfriend g;
    g.visit();
    return 0;
}

四、C++运算符重载

1、加号运算符重载

class Person{
public:
    //通过成员函数实现
    Person operator+(Person &p){
        Person temp;
        temp.m_a=p.m_a+this->m_a;
        temp.m_b=p.m_b+this->m_b;
        return temp;    
    }
    int m_a,m_b;
};
//通过全局函数实现
Person operator+(Person &p1,Person &p2){
    Person temp;
    temp.m_a=p1.m_a+p2.m_a;
    temp.m_b=p1.m_b+p2.m_b;
    return temp;
}
//重载版本
Person operator+(Person &p,int num){
    Person temp;
    temp.m_a=p.m_a+num;
    temp.m_b=p.m_b+num;
    return temp;
}
int main(){
    Person p1;
    p1.m_a=10;
    p1.m_b=20;
    Person p2;
    p2.m_a=10;
    p2.m_b=20;
    //成员函数本质调用:Person p3=p1.operator+(p2);
    //全局函数本质调用:Person p3=operator+(p1,p2);
    Person p3=p1+p2;
    //运算符重载允许发生函数重载
    Person p4=p1+10;
    cout<<"p3.m_a="<<p3.m_a<<endl;
    cout<<"p3.m_b="<<p3.m_b<<endl;
    cout<<"p4.m_a="<<p4.m_a<<endl;
    cout<<"p4.m_b="<<p4.m_b<<endl;
    return 0;
}

2、左移运算符重载

class Person{
public:
    //用成员函数实现
    //相当于p.operator<<(cout),简化版本为p<<cout,由于cout不在左侧所以不常用
//    ostream& operator<<(ostream &cout){
// 	    cout<<this->m_name1<<" "<<this->m_name2<<endl;
//        return cout;       
//    }
    string m_name1;
    string m_name2;
};
//用全局函数实现
//cout是一个标准输出流的对象,且全局只能有一个,所以用引用的方式
ostream& operator<<(ostream &cout,Person &p){
     cout<<p.m_name1<<" "<<p.m_name2<<endl;
     return cout;
 }
int main(){
    Person p;
    p.m_name1="TOM";
    p.m_name2="AMY";
    cout<<p;
    return 0;
}

3、递增运算符重载

(1)前置递增

class Person{
public:
    //重载前置++运算符
    //返回引用是为了一直对同一个数据进行递增
    Person& operator++(){
        this->m_a++;
        this->m_b++;
        return *this;
    }
    int m_a;
    int m_b;
};
//重载<<运算符
ostream& operator<<(ostream& cout,Person &p){
    cout<<"m_a="<<p.m_a<<" "<<"m_b="<<p.m_b<<endl;
    return cout;
}
int main(){
    Person p;
    p.m_a=1;
    p.m_b=10;
    cout<<++p<<endl;
    return 0;
}

(2)后置递增

class Person{
public:
    //返回值不能用引用,不能返回一个局部对象的引用
    //通常用一个占位参数区分前置和后置递增
    Person operator++(int){
        Person temp=*this;
        m_a++;
        m_b++;
        return temp;
    }
    int m_a;
    int m_b;
};
//重载<<运算符,注意此处p无引用,与++重载函数对应
ostream& operator<<(ostream& cout,Person p){
    cout<<"m_a="<<p.m_a<<" "<<"m_b="<<p.m_b<<endl;
    return cout;
}
int main(){
    Person p;
    p.m_a=1;
    p.m_b=10;
    cout<<p++<<endl;
    cout<<p<<endl;
    return 0;
}

4、赋值运算符重载

C++编译器至少给一个类添加4个函数:

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

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

(3)默认拷贝构造函数,对属性进行值拷贝

(4)赋值运算符operator=,对属性进行值拷贝

如果类中有属性指向堆区,做复制操作时也会出现深浅拷贝的问题

class Person{
public:
    //在堆区开辟一个内存,new返回指针,用指针来接收
    Person(int num){
        m_age=new int(num);
    }
    //析构函数释放内存,堆区开辟的数据需要手动释放
    //由于默认提供浅拷贝,所以两者指向的内存都是同一块,会造成内存重复释放的问题
    ~Person(){
        if(m_age!=NULL){
            delete m_age;
            m_age=NULL;
        }
    }
    //解决方案:提供深拷贝
    Person& operator=(Person &p){
        //浅拷贝
        // m_age=p.m_age;
        //深拷贝
        m_age=new int(*p.m_age);
        //要返回自身,返回引用才能返回自身,返回值只是返回拷贝的副本
        return *this;
    }
    int *m_age;
};
int main(){
    Person p1(18);
    Person p2(20);
    p2=p1;
    cout<<"p1年龄:"<<*p1.m_age<<endl;
    cout<<"p2年龄:"<<*p2.m_age<<endl;
    return 0;
}

5、关系运算符重载

以==重载为例

#include<iostream>
#include<string>
using namespace std;
class Person{
    //友元
    friend bool operator==(Person &p1,Person &p2);
public:
    Person(int age,int id){
        m_age=age;
        m_id=id;
    }
private:
    int m_age;
    int m_id;
};
bool operator==(Person &p1,Person &p2){
    if(p1.m_age==p2.m_age&&p1.m_id==p2.m_id){
        return true;
    }
    else{
        return false;
    }
}
int main(){
    Person p1(18,2022);
    Person p2(18,2024);
    if(p1==p2){
        cout<<"p1与p2相等"<<endl;
    }
    else{
        cout<<"p1与p2不相等"<<endl;
    }
    return 0;
}

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

由于重载后使用的方式非常像函数调用,因此称为仿函数

仿函数没有固定写法,非常灵活

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

int main(){
    Myprint myprint;
    myprint("hello");
    //匿名函数对象,执行完毕后立即被释放
    Myprint()("hi");
    return 0;
}

五、继承

作用:减少重复代码

1、基本语法

class 子类:继承方式 父类{};

子类也可以称为派生类,父类也可以称为基类

派生类中的成员,包含两大部分

一类是从基类继承过来的,一类是自己增加的成员。

从基类继承过过来的表现其共性,而新增的成员体现了其个性。

#include<iostream>
using namespace std;
class Base{
public:
    void title(){
        cout<<"公共标题"<<endl;
    }
    void bottle(){
        cout<<"公共底部"<<endl;
    }

};
class Python:public Base{
public:
    void show(){
        cout<<"python课程"<<endl;
    }
};
class Cpp:public Base{
public:
    void show(){
        cout<<"C++课程"<<endl;
    }
};
class Java:public Base{
public:
    void show(){
        cout<<"Java课程"<<endl;
    }
};
int main(){
    Python p;
    p.title();
    p.show();
    p.bottle();
    cout << "--------------------" << endl;
    Cpp c;
    c.title();
    c.show();
    c.bottle();
    cout << "--------------------" << endl;
    Java j;
    j.title();
    j.show();
    j.bottle();
    return 0;
}

2、继承方式

继承方式有:公有继承、保护继承、私有继承

image-20240728150722464

3、继承中的对象模型

class Base{
public:
    int m_a;
private:
    int m_b;
protected:
    int m_c;
};
class Python:public Base{
public:
    void show(){
        cout<<"python课程"<<endl;
    }
};
class Son:public Base{
public:
    int m_d;
};
int main(){
    Son s;
    cout<<"size="<<sizeof(s)<<endl;
    return 0;
}

运行以上代码,输出结果为size=16

由此可知:父类中私有成员也是被子类继承下去了,只是由编译器给隐藏后访问不到

4、继承中构造与析构的顺序

继承中 先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反

class Base{
public:
    Base(){
        cout<<"Base的构造函数"<<endl;
    }
    ~Base(){
        cout<<"Base的析构函数"<<endl;
    }
};
class Son:public Base{
public:
    Son(){
        cout<<"Son的构造函数"<<endl;
    }
    ~Son(){
        cout<<"Son的析构函数"<<endl;
    }
};
void test(){
    Son s;
}
int main(){
    test();
    return 0;
}

5、继承中同名成员的处理

class Base{
public:
    int m_a=20;
    void func(){
        cout<<"Base - func()调用"<<endl;
    }
    void func(int a){
		cout << "Base - func(int a)调用" << endl;
	}
};
class Son:public Base{
public:
    int m_a=10;
    void func(){
        cout<<"Son中的func"<<endl;
    }
};
void test(){
    Son s;
    cout<<"Son中的m_a="<<s.m_a<<endl;
    cout<<"Base中的m_a="<<s.Base::m_a<<endl;
    s.func();
    s.Base::func();
    s.Base::func(10);//
}
int main(){
    test();
    return 0;
}

总结:

  1. 子类对象可以直接访问到子类中同名成员
  2. 子类对象加作用域可以访问到父类同名成员
  3. 当子类与父类拥有同名的成员函数,子类会隐藏父类中同名成员函数,加作用域可以访问到父类中同名函数

6、继承中同名静态成员处理

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

访问子类同名成员 直接访问即可

访问父类同名成员 需要加作用域

只不过访问方式有两种:通过对象访问和通过类名访问

class Base {
public:
	static void func()
	{
		cout << "Base - static void func()" << endl;
	}
	static void func(int a)
	{
		cout << "Base - static void func(int a)" << endl;
	}

	static int m_A;
};

int Base::m_A = 100;

class Son : public Base {
public:
	static void func()
	{
		cout << "Son - static void func()" << endl;
	}
	static int m_A;
};

int Son::m_A = 200;

//同名成员属性
void test01()
{
	//通过对象访问
	cout << "通过对象访问: " << endl;
	Son s;
	cout << "Son  下 m_A = " << s.m_A << endl;
	cout << "Base 下 m_A = " << s.Base::m_A << endl;

	//通过类名访问
	cout << "通过类名访问: " << endl;
	cout << "Son  下 m_A = " << Son::m_A << endl;
	cout << "Base 下 m_A = " << Son::Base::m_A << endl;
}

//同名成员函数
void test02()
{
	//通过对象访问
	cout << "通过对象访问: " << endl;
	Son s;
	s.func();
	s.Base::func();

	cout << "通过类名访问: " << endl;
	Son::func();
	Son::Base::func();
	//出现同名,子类会隐藏掉父类中所有同名成员函数,需要加作作用域访问
	Son::Base::func(100);
}
int main() {

	//test01();
	test02();

	system("pause");

	return 0;
}

7、多继承语法

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

多继承可能会引发父类中有同名成员出现,需要加作用域区分

C++实际开发中不建议用多继承

class Base1 {
public:
	Base1()
	{
		m_A = 100;
	}
public:
	int m_A;
};

class Base2 {
public:
	Base2()
	{
		m_A = 200;  //m_A会出现不明确
	}
public:
	int m_A;
};

class Son : public Base2, public Base1 
{
public:
	Son()
	{
		m_C = 300;
		m_D = 400;
	}
public:
	int m_C;
	int m_D;
};


//多继承容易产生成员同名的情况
//通过使用类名作用域可以区分调用哪一个基类的成员
void test01()
{
	Son s;
	cout << "sizeof Son = " << sizeof(s) << endl;
	cout << s.Base1::m_A << endl;
	cout << s.Base2::m_A << endl;
}

int main() {

	test01();

	system("pause");

	return 0;
}

8、菱形继承

​ 两个派生类继承同一个基类,又有某个类同时继承者两个派生类。这种继承被称为菱形继承,或者钻石继承
在这里插入图片描述

菱形继承的问题:

  1. 羊继承了动物的数据,驼同样继承了动物的数据,当草泥马使用数据时,就会产生二义性。

  2. 草泥马继承自动物的数据继承了两份,其实我们应该清楚,这份数据我们只需要一份就可以。

class Animal{
public:
    int m_age;
};
//继承前加virtual关键字后,变为虚继承
//此时公共的父类Animal称为虚基类
class Sheep:virtual public Animal{};
class Tuo:virtual public Animal{};
//虚继承后继承到两个指针,这两个指针指向同一个地址
class SheepTuo:public Sheep,public Tuo{};

int main(){
    SheepTuo st;
    st.Sheep::m_age=10;
    st.Tuo::m_age=20;//与st.Sheep::m_age指向同一个数据
    cout<<"size="<<sizeof(st)<<endl;
    cout<<st.Sheep::m_age<<endl;
    cout<<st.Tuo::m_age<<endl;
    cout<<st.m_age<<endl;
    return 0;
}

总结:

  • 菱形继承带来的主要问题是子类继承两份相同的数据,导致资源浪费以及毫无意义
  • 利用虚继承可以解决菱形继承问题

六、多态

1、基本概念

多态是C++面向对象三大特性之一

(1)多态分为两类

  • 静态多态: 函数重载 和 运算符重载属于静态多态,复用函数名
  • 动态多态: 派生类和虚函数实现运行时多态

(2)静态多态和动态多态区别:

  • 静态多态的函数地址早绑定 - 编译阶段确定函数地址
  • 动态多态的函数地址晚绑定 - 运行阶段确定函数地址
class Animal{
public:
    //函数前面加上virtual关键字,变成虚函数,那么编译器在编译的时候就不能确定函数调用了。
    virtual void speak(){
        cout<<"动物在叫"<<endl;
    }
};
class Cat:public Animal{
public:
    void speak(){
        cout<<"喵喵喵"<<endl;
    }
};
class Dog:public Animal{
public:
    void speak(){
        cout<<"汪汪汪"<<endl;
    }
};
void test(Animal &animal){
    animal.speak();
}
int main(){
    Cat cat;
    Dog dog;
    //传入什么对象,那么就调用什么对象的函数
    test(cat);
    test(dog);
    return 0;
}

如果函数地址在编译阶段就能确定,那么静态联编;如果函数地址在运行阶段才能确定,就是动态联编。

(3)多态使用:父类指针或引用指向子类对象

(4)多态满足条件:

  • 有继承关系

  • 子类重写父类中的虚函数

2、原理剖析

class Animal{
public:
    //函数前面加上virtual关键字,变成虚函数,那么编译器在编译的时候就不能确定函数调用了。
    virtual void speak(){
        cout<<"动物在叫"<<endl;
    }
};
class Cat:public Animal{
public:
    void speak(){
        cout<<"喵喵喵"<<endl;
    }
};
class Dog:public Animal{
public:
    void speak(){
        cout<<"汪汪汪"<<endl;
    }
};
void test(Animal &animal){
    animal.speak();
}
int main(){
    Cat cat;
    Dog dog;
    cout<<"sizeof="<<sizeof(Animal)<<endl;//输出结果为一个指针的大小
    return 0;
}

请添加图片描述

当子类重写父类的虚函数,子类中的虚函数表内部会替换成子类的虚函数地址。

3、优点

多态的优点:

  • 代码组织结构清晰
  • 可读性强
  • 利于前期和后期的扩展以及维护
class Calculator{
public:
    int m_a=10,m_b=12;
    virtual int getans(){
        return 0;
    }
};
class Add:public Calculator{
public:
    int getans(){
        return m_a+m_b;
    }
};

class Multiply:public Calculator{
public:
    int getans(){
        return m_a*m_b;
    }
};
class Substract:public Calculator{
public:
    int getans(){
        return m_a-m_b;
    }
};
int main(){
    //多态使用:父类指针或引用指向子类对象
    Calculator *c=new Add;
    cout<<c->m_a<<"+"<<c->m_b<<"="<<c->getans()<<endl;
    delete c;//堆区数据用完需手动销毁
    c=new Substract;
    cout<<c->m_a<<"-"<<c->m_b<<"="<<c->getans()<<endl; 
    delete c;
    c=new Multiply;
    cout<<c->m_a<<"*"<<c->m_b<<"="<<c->getans()<<endl;
    delete c;
    return 0;
}

4、纯虚函数和抽象类

在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容

因此可以将虚函数改为纯虚函数

纯虚函数语法:virtual 返回值类型 函数名 (参数列表)= 0 ;

当类中有了纯虚函数,这个类也称为抽象类。

抽象类特点

  • 无法实例化对象
  • 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
class Base
{
public:
	//纯虚函数
	//类中只要有一个纯虚函数就称为抽象类
	//抽象类无法实例化对象
	//子类必须重写父类中的纯虚函数,否则也属于抽象类
	virtual void func() = 0;
};

class Son :public Base
{
public:
	virtual void func() 
	{
		cout << "func调用" << endl;
	};
};

void test01()
{
	Base * base = NULL;
	//base = new Base; // 错误,抽象类无法实例化对象
	base = new Son;
	base->func();
	delete base;//记得销毁
}

int main() {
	test01();
	system("pause");
	return 0;
}

5、虚析构和纯虚析构

多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码

解决方式:将父类中的析构函数改为虚析构或者纯虚析构

虚析构和纯虚析构共性:

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

虚析构和纯虚析构区别:

  • 如果是纯虚析构,该类属于抽象类,无法实例化对象

虚析构语法:

virtual ~类名(){}

纯虚析构语法:

virtual ~类名() = 0;

类名::~类名(){}

class Animal{
public:
    virtual void speak()=0;
    // virtual ~Animal(){
    //     cout<<"Animal的虚析构函数调用"<<endl;
    // }
    virtual ~Animal()=0;
};
Animal::~Animal(){
    cout<<"Animal的纯虚析构函数的调用"<<endl;
}
class Cat:public Animal{
public:
    Cat(string a){
        m_name=new string(a);
    }
    string *m_name;
    void speak(){
        cout<<*m_name<<":喵喵喵"<<endl;
    } 
    //通过父类指针去释放,会导致子类对象可能清理不干净,造成内存泄漏
    //如果父类中没有虚析构函数或纯析构函数,此时子函数的析构函数不会执行
    ~Cat(){
        if(m_name!=NULL){
            cout<<"Cat的析构函数调用"<<endl;
            delete m_name;
        }
    }   
};
void test(Animal *animal){
    animal->speak();
}
int main(){
    test(new Cat("Tom"));
    return 0;
}

总结:

​ 1. 虚析构或纯虚析构就是用来解决通过父类指针释放子类对象

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

  1. 拥有纯虚析构函数的类也属于抽象类

列表)= 0 ;`

当类中有了纯虚函数,这个类也称为抽象类。

抽象类特点

  • 无法实例化对象
  • 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
class Base
{
public:
	//纯虚函数
	//类中只要有一个纯虚函数就称为抽象类
	//抽象类无法实例化对象
	//子类必须重写父类中的纯虚函数,否则也属于抽象类
	virtual void func() = 0;
};

class Son :public Base
{
public:
	virtual void func() 
	{
		cout << "func调用" << endl;
	};
};

void test01()
{
	Base * base = NULL;
	//base = new Base; // 错误,抽象类无法实例化对象
	base = new Son;
	base->func();
	delete base;//记得销毁
}

int main() {
	test01();
	system("pause");
	return 0;
}

5、虚析构和纯虚析构

多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码

解决方式:将父类中的析构函数改为虚析构或者纯虚析构

虚析构和纯虚析构共性:

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

虚析构和纯虚析构区别:

  • 如果是纯虚析构,该类属于抽象类,无法实例化对象

虚析构语法:

virtual ~类名(){}

纯虚析构语法:

virtual ~类名() = 0;

类名::~类名(){}

class Animal{
public:
    virtual void speak()=0;
    // virtual ~Animal(){
    //     cout<<"Animal的虚析构函数调用"<<endl;
    // }
    virtual ~Animal()=0;
};
Animal::~Animal(){
    cout<<"Animal的纯虚析构函数的调用"<<endl;
}
class Cat:public Animal{
public:
    Cat(string a){
        m_name=new string(a);
    }
    string *m_name;
    void speak(){
        cout<<*m_name<<":喵喵喵"<<endl;
    } 
    //通过父类指针去释放,会导致子类对象可能清理不干净,造成内存泄漏
    //如果父类中没有虚析构函数或纯析构函数,此时子函数的析构函数不会执行
    ~Cat(){
        if(m_name!=NULL){
            cout<<"Cat的析构函数调用"<<endl;
            delete m_name;
        }
    }   
};
void test(Animal *animal){
    animal->speak();
}
int main(){
    test(new Cat("Tom"));
    return 0;
}

总结:

​ 1. 虚析构或纯虚析构就是用来解决通过父类指针释放子类对象

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

  1. 拥有纯虚析构函数的类也属于抽象类
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值